summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am72
-rw-r--r--src/Makefile.in844
-rw-r--r--src/anvil/Makefile.am48
-rw-r--r--src/anvil/Makefile.in867
-rw-r--r--src/anvil/anvil-connection.c226
-rw-r--r--src/anvil/anvil-connection.h10
-rw-r--r--src/anvil/anvil-settings.c50
-rw-r--r--src/anvil/common.h10
-rw-r--r--src/anvil/connect-limit.c192
-rw-r--r--src/anvil/connect-limit.h16
-rw-r--r--src/anvil/main.c90
-rw-r--r--src/anvil/penalty.c273
-rw-r--r--src/anvil/penalty.h22
-rw-r--r--src/anvil/test-penalty.c64
-rw-r--r--src/auth/Makefile.am301
-rw-r--r--src/auth/Makefile.in1928
-rw-r--r--src/auth/auth-cache.c481
-rw-r--r--src/auth/auth-cache.h53
-rw-r--r--src/auth/auth-client-connection.c456
-rw-r--r--src/auth/auth-client-connection.h37
-rw-r--r--src/auth/auth-common.h17
-rw-r--r--src/auth/auth-fields.c226
-rw-r--r--src/auth/auth-fields.h48
-rw-r--r--src/auth/auth-master-connection.c855
-rw-r--r--src/auth/auth-master-connection.h44
-rw-r--r--src/auth/auth-penalty.c176
-rw-r--r--src/auth/auth-penalty.h28
-rw-r--r--src/auth/auth-policy.c620
-rw-r--r--src/auth/auth-policy.h11
-rw-r--r--src/auth/auth-request-fields.c525
-rw-r--r--src/auth/auth-request-handler-private.h27
-rw-r--r--src/auth/auth-request-handler.c991
-rw-r--r--src/auth/auth-request-handler.h69
-rw-r--r--src/auth/auth-request-stats.c77
-rw-r--r--src/auth/auth-request-stats.h15
-rw-r--r--src/auth/auth-request-var-expand.c303
-rw-r--r--src/auth/auth-request-var-expand.h44
-rw-r--r--src/auth/auth-request.c2595
-rw-r--r--src/auth/auth-request.h400
-rw-r--r--src/auth/auth-settings.c553
-rw-r--r--src/auth/auth-settings.h110
-rw-r--r--src/auth/auth-stats.c116
-rw-r--r--src/auth/auth-stats.h16
-rw-r--r--src/auth/auth-token.c177
-rw-r--r--src/auth/auth-token.h11
-rw-r--r--src/auth/auth-worker-client.c972
-rw-r--r--src/auth/auth-worker-client.h27
-rw-r--r--src/auth/auth-worker-server.c519
-rw-r--r--src/auth/auth-worker-server.h18
-rw-r--r--src/auth/auth.c450
-rw-r--r--src/auth/auth.h91
-rw-r--r--src/auth/checkpassword-reply.c110
-rw-r--r--src/auth/crypt-blowfish.c900
-rw-r--r--src/auth/crypt-blowfish.h29
-rw-r--r--src/auth/db-checkpassword.c563
-rw-r--r--src/auth/db-checkpassword.h29
-rw-r--r--src/auth/db-dict-cache-key.c64
-rw-r--r--src/auth/db-dict.c654
-rw-r--r--src/auth/db-dict.h82
-rw-r--r--src/auth/db-ldap.c2035
-rw-r--r--src/auth/db-ldap.h221
-rw-r--r--src/auth/db-lua.c814
-rw-r--r--src/auth/db-lua.h33
-rw-r--r--src/auth/db-oauth2.c915
-rw-r--r--src/auth/db-oauth2.h48
-rw-r--r--src/auth/db-passwd-file.c493
-rw-r--r--src/auth/db-passwd-file.h58
-rw-r--r--src/auth/db-sql.c178
-rw-r--r--src/auth/db-sql.h42
-rw-r--r--src/auth/main.c398
-rw-r--r--src/auth/mech-anonymous.c48
-rw-r--r--src/auth/mech-apop.c173
-rw-r--r--src/auth/mech-cram-md5.c193
-rw-r--r--src/auth/mech-digest-md5-private.h38
-rw-r--r--src/auth/mech-digest-md5.c592
-rw-r--r--src/auth/mech-dovecot-token.c92
-rw-r--r--src/auth/mech-external.c64
-rw-r--r--src/auth/mech-gssapi.c797
-rw-r--r--src/auth/mech-login.c76
-rw-r--r--src/auth/mech-oauth2.c339
-rw-r--r--src/auth/mech-otp-common.c71
-rw-r--r--src/auth/mech-otp-common.h23
-rw-r--r--src/auth/mech-otp.c240
-rw-r--r--src/auth/mech-plain-common.c22
-rw-r--r--src/auth/mech-plain-common.h7
-rw-r--r--src/auth/mech-plain.c88
-rw-r--r--src/auth/mech-scram.c550
-rw-r--r--src/auth/mech-scram.h10
-rw-r--r--src/auth/mech-winbind.c364
-rw-r--r--src/auth/mech.c245
-rw-r--r--src/auth/mech.h77
-rw-r--r--src/auth/mycrypt.c26
-rw-r--r--src/auth/mycrypt.h8
-rw-r--r--src/auth/passdb-blocking.c173
-rw-r--r--src/auth/passdb-blocking.h11
-rw-r--r--src/auth/passdb-bsdauth.c97
-rw-r--r--src/auth/passdb-cache.c214
-rw-r--r--src/auth/passdb-cache.h21
-rw-r--r--src/auth/passdb-checkpassword.c153
-rw-r--r--src/auth/passdb-dict.c184
-rw-r--r--src/auth/passdb-imap.c241
-rw-r--r--src/auth/passdb-ldap.c517
-rw-r--r--src/auth/passdb-lua.c193
-rw-r--r--src/auth/passdb-oauth2.c90
-rw-r--r--src/auth/passdb-pam.c401
-rw-r--r--src/auth/passdb-passwd-file.c210
-rw-r--r--src/auth/passdb-passwd.c127
-rw-r--r--src/auth/passdb-shadow.c125
-rw-r--r--src/auth/passdb-sql.c310
-rw-r--r--src/auth/passdb-static.c113
-rw-r--r--src/auth/passdb-template.c102
-rw-r--r--src/auth/passdb-template.h16
-rw-r--r--src/auth/passdb.c351
-rw-r--r--src/auth/passdb.h123
-rw-r--r--src/auth/password-scheme-crypt.c196
-rw-r--r--src/auth/password-scheme-md5crypt.c147
-rw-r--r--src/auth/password-scheme-otp.c40
-rw-r--r--src/auth/password-scheme-pbkdf2.c82
-rw-r--r--src/auth/password-scheme-scram.c224
-rw-r--r--src/auth/password-scheme-sodium.c92
-rw-r--r--src/auth/password-scheme.c822
-rw-r--r--src/auth/password-scheme.h147
-rw-r--r--src/auth/test-auth-cache.c82
-rw-r--r--src/auth/test-auth-request-fields.c147
-rw-r--r--src/auth/test-auth-request-var-expand.c259
-rw-r--r--src/auth/test-auth.h25
-rw-r--r--src/auth/test-db-dict.c43
-rw-r--r--src/auth/test-libpassword.c137
-rw-r--r--src/auth/test-lua.c182
-rw-r--r--src/auth/test-main.c39
-rw-r--r--src/auth/test-mech.c412
-rw-r--r--src/auth/test-mock.c109
-rw-r--r--src/auth/test-username-filter.c50
-rw-r--r--src/auth/userdb-blocking.c144
-rw-r--r--src/auth/userdb-blocking.h12
-rw-r--r--src/auth/userdb-checkpassword.c92
-rw-r--r--src/auth/userdb-dict.c205
-rw-r--r--src/auth/userdb-ldap.c343
-rw-r--r--src/auth/userdb-lua.c139
-rw-r--r--src/auth/userdb-passwd-file.c247
-rw-r--r--src/auth/userdb-passwd.c253
-rw-r--r--src/auth/userdb-prefetch.c59
-rw-r--r--src/auth/userdb-sql.c319
-rw-r--r--src/auth/userdb-static.c144
-rw-r--r--src/auth/userdb-template.c124
-rw-r--r--src/auth/userdb-template.h15
-rw-r--r--src/auth/userdb.c256
-rw-r--r--src/auth/userdb.h92
-rw-r--r--src/config/Makefile.am98
-rw-r--r--src/config/Makefile.in1079
-rw-r--r--src/config/all-settings.c6112
-rw-r--r--src/config/all-settings.h8
-rw-r--r--src/config/config-connection.c267
-rw-r--r--src/config/config-connection.h9
-rw-r--r--src/config/config-filter.c401
-rw-r--r--src/config/config-filter.h58
-rw-r--r--src/config/config-parser-private.h75
-rw-r--r--src/config/config-parser.c1212
-rw-r--r--src/config/config-parser.h33
-rw-r--r--src/config/config-request.c524
-rw-r--r--src/config/config-request.h61
-rw-r--r--src/config/config-settings.c46
-rw-r--r--src/config/doveconf.c1072
-rw-r--r--src/config/main.c53
-rw-r--r--src/config/old-set-parser.c824
-rw-r--r--src/config/old-set-parser.h16
-rwxr-xr-xsrc/config/settings-get.pl162
-rw-r--r--src/config/sysinfo-get.c127
-rw-r--r--src/config/sysinfo-get.h6
-rw-r--r--src/config/test-config-parser.c170
-rw-r--r--src/dict/Makefile.am43
-rw-r--r--src/dict/Makefile.in847
-rw-r--r--src/dict/dict-commands.c797
-rw-r--r--src/dict/dict-commands.h21
-rw-r--r--src/dict/dict-connection.c272
-rw-r--r--src/dict/dict-connection.h48
-rw-r--r--src/dict/dict-init-cache.c164
-rw-r--r--src/dict/dict-init-cache.h12
-rw-r--r--src/dict/dict-settings.c117
-rw-r--r--src/dict/dict-settings.h15
-rw-r--r--src/dict/main.c179
-rw-r--r--src/dict/main.h6
-rw-r--r--src/director/Makefile.am70
-rw-r--r--src/director/Makefile.in928
-rw-r--r--src/director/auth-connection.c141
-rw-r--r--src/director/auth-connection.h24
-rw-r--r--src/director/director-connection.c2712
-rw-r--r--src/director/director-connection.h47
-rw-r--r--src/director/director-host.c190
-rw-r--r--src/director/director-host.h81
-rw-r--r--src/director/director-request.c354
-rw-r--r--src/director/director-request.h16
-rw-r--r--src/director/director-settings.c125
-rw-r--r--src/director/director-settings.h25
-rw-r--r--src/director/director-test.c605
-rw-r--r--src/director/director.c1589
-rw-r--r--src/director/director.h274
-rw-r--r--src/director/doveadm-connection.c1196
-rw-r--r--src/director/doveadm-connection.h13
-rw-r--r--src/director/login-connection.c346
-rw-r--r--src/director/login-connection.h20
-rw-r--r--src/director/mail-host.c560
-rw-r--r--src/director/mail-host.h90
-rw-r--r--src/director/main.c366
-rw-r--r--src/director/notify-connection.c109
-rw-r--r--src/director/notify-connection.h9
-rw-r--r--src/director/test-user-directory.c109
-rw-r--r--src/director/user-directory.c349
-rw-r--r--src/director/user-directory.h79
-rw-r--r--src/dns/Makefile.am17
-rw-r--r--src/dns/Makefile.in803
-rw-r--r--src/dns/dns-client-settings.c48
-rw-r--r--src/dns/dns-client.c161
-rw-r--r--src/doveadm/Makefile.am201
-rw-r--r--src/doveadm/Makefile.in1515
-rw-r--r--src/doveadm/client-connection-http.c1227
-rw-r--r--src/doveadm/client-connection-private.h22
-rw-r--r--src/doveadm/client-connection-tcp.c558
-rw-r--r--src/doveadm/client-connection.c127
-rw-r--r--src/doveadm/client-connection.h26
-rw-r--r--src/doveadm/doveadm-auth-server.c517
-rw-r--r--src/doveadm/doveadm-auth.c787
-rw-r--r--src/doveadm/doveadm-cmd.c469
-rw-r--r--src/doveadm/doveadm-cmd.h155
-rw-r--r--src/doveadm/doveadm-dict.c329
-rw-r--r--src/doveadm/doveadm-director.c1084
-rw-r--r--src/doveadm/doveadm-dsync.c1567
-rw-r--r--src/doveadm/doveadm-dsync.h10
-rw-r--r--src/doveadm/doveadm-dump-dbox.c231
-rw-r--r--src/doveadm/doveadm-dump-dcrypt-file.c93
-rw-r--r--src/doveadm/doveadm-dump-dcrypt-key.c215
-rw-r--r--src/doveadm/doveadm-dump-index.c833
-rw-r--r--src/doveadm/doveadm-dump-log.c568
-rw-r--r--src/doveadm/doveadm-dump-mailboxlog.c114
-rw-r--r--src/doveadm/doveadm-dump-thread.c139
-rw-r--r--src/doveadm/doveadm-dump.c162
-rw-r--r--src/doveadm/doveadm-dump.h27
-rw-r--r--src/doveadm/doveadm-fs.c608
-rw-r--r--src/doveadm/doveadm-instance.c155
-rw-r--r--src/doveadm/doveadm-kick.c235
-rw-r--r--src/doveadm/doveadm-log.c406
-rw-r--r--src/doveadm/doveadm-mail-altmove.c162
-rw-r--r--src/doveadm/doveadm-mail-batch.c186
-rw-r--r--src/doveadm/doveadm-mail-copymove.c224
-rw-r--r--src/doveadm/doveadm-mail-deduplicate.c146
-rw-r--r--src/doveadm/doveadm-mail-expunge.c286
-rw-r--r--src/doveadm/doveadm-mail-fetch.c683
-rw-r--r--src/doveadm/doveadm-mail-flags.c178
-rw-r--r--src/doveadm/doveadm-mail-import.c276
-rw-r--r--src/doveadm/doveadm-mail-index.c300
-rw-r--r--src/doveadm/doveadm-mail-iter.c176
-rw-r--r--src/doveadm/doveadm-mail-iter.h34
-rw-r--r--src/doveadm/doveadm-mail-mailbox-cache.c440
-rw-r--r--src/doveadm/doveadm-mail-mailbox-metadata.c425
-rw-r--r--src/doveadm/doveadm-mail-mailbox-status.c275
-rw-r--r--src/doveadm/doveadm-mail-mailbox.c857
-rw-r--r--src/doveadm/doveadm-mail-rebuild.c101
-rw-r--r--src/doveadm/doveadm-mail-save.c142
-rw-r--r--src/doveadm/doveadm-mail-search.c103
-rw-r--r--src/doveadm/doveadm-mail-server.c404
-rw-r--r--src/doveadm/doveadm-mail.c998
-rw-r--r--src/doveadm/doveadm-mail.h206
-rw-r--r--src/doveadm/doveadm-mailbox-list-iter.c195
-rw-r--r--src/doveadm/doveadm-mailbox-list-iter.h25
-rw-r--r--src/doveadm/doveadm-master.c297
-rw-r--r--src/doveadm/doveadm-mutf7.c56
-rw-r--r--src/doveadm/doveadm-oldstats.c604
-rw-r--r--src/doveadm/doveadm-penalty.c126
-rw-r--r--src/doveadm/doveadm-print-flow.c111
-rw-r--r--src/doveadm/doveadm-print-formatted.c92
-rw-r--r--src/doveadm/doveadm-print-json.c161
-rw-r--r--src/doveadm/doveadm-print-pager.c113
-rw-r--r--src/doveadm/doveadm-print-private.h31
-rw-r--r--src/doveadm/doveadm-print-server.c119
-rw-r--r--src/doveadm/doveadm-print-tab.c76
-rw-r--r--src/doveadm/doveadm-print-table.c268
-rw-r--r--src/doveadm/doveadm-print.c211
-rw-r--r--src/doveadm/doveadm-print.h48
-rw-r--r--src/doveadm/doveadm-proxy.c214
-rw-r--r--src/doveadm/doveadm-pw.c135
-rw-r--r--src/doveadm/doveadm-replicator.c381
-rw-r--r--src/doveadm/doveadm-server.h34
-rw-r--r--src/doveadm/doveadm-settings.c320
-rw-r--r--src/doveadm/doveadm-settings.h66
-rw-r--r--src/doveadm/doveadm-sis.c330
-rw-r--r--src/doveadm/doveadm-stats.c343
-rw-r--r--src/doveadm/doveadm-util.c221
-rw-r--r--src/doveadm/doveadm-util.h31
-rw-r--r--src/doveadm/doveadm-who.c364
-rw-r--r--src/doveadm/doveadm-who.h37
-rw-r--r--src/doveadm/doveadm-zlib.c297
-rw-r--r--src/doveadm/doveadm.c384
-rw-r--r--src/doveadm/doveadm.h34
-rw-r--r--src/doveadm/dsync/Makefile.am76
-rw-r--r--src/doveadm/dsync/Makefile.in1019
-rw-r--r--src/doveadm/dsync/dsync-brain-mailbox-tree-sync.c229
-rw-r--r--src/doveadm/dsync/dsync-brain-mailbox-tree.c614
-rw-r--r--src/doveadm/dsync/dsync-brain-mailbox.c926
-rw-r--r--src/doveadm/dsync/dsync-brain-mails.c438
-rw-r--r--src/doveadm/dsync/dsync-brain-private.h164
-rw-r--r--src/doveadm/dsync/dsync-brain.c902
-rw-r--r--src/doveadm/dsync/dsync-brain.h136
-rw-r--r--src/doveadm/dsync/dsync-deserializer.c193
-rw-r--r--src/doveadm/dsync/dsync-deserializer.h27
-rw-r--r--src/doveadm/dsync/dsync-ibc-pipe.c599
-rw-r--r--src/doveadm/dsync/dsync-ibc-private.h96
-rw-r--r--src/doveadm/dsync/dsync-ibc-stream.c2138
-rw-r--r--src/doveadm/dsync/dsync-ibc.c239
-rw-r--r--src/doveadm/dsync/dsync-ibc.h178
-rw-r--r--src/doveadm/dsync/dsync-mail.c156
-rw-r--r--src/doveadm/dsync/dsync-mail.h108
-rw-r--r--src/doveadm/dsync/dsync-mailbox-export.c961
-rw-r--r--src/doveadm/dsync/dsync-mailbox-export.h37
-rw-r--r--src/doveadm/dsync/dsync-mailbox-import.c3018
-rw-r--r--src/doveadm/dsync/dsync-mailbox-import.h64
-rw-r--r--src/doveadm/dsync/dsync-mailbox-state.c127
-rw-r--r--src/doveadm/dsync/dsync-mailbox-state.h24
-rw-r--r--src/doveadm/dsync/dsync-mailbox-tree-fill.c463
-rw-r--r--src/doveadm/dsync/dsync-mailbox-tree-private.h38
-rw-r--r--src/doveadm/dsync/dsync-mailbox-tree-sync.c1473
-rw-r--r--src/doveadm/dsync/dsync-mailbox-tree.c571
-rw-r--r--src/doveadm/dsync/dsync-mailbox-tree.h211
-rw-r--r--src/doveadm/dsync/dsync-mailbox.c61
-rw-r--r--src/doveadm/dsync/dsync-mailbox.h44
-rw-r--r--src/doveadm/dsync/dsync-serializer.c117
-rw-r--r--src/doveadm/dsync/dsync-serializer.h18
-rw-r--r--src/doveadm/dsync/dsync-transaction-log-scan.c608
-rw-r--r--src/doveadm/dsync/dsync-transaction-log-scan.h32
-rw-r--r--src/doveadm/dsync/test-dsync-mailbox-tree-sync.c781
-rw-r--r--src/doveadm/main.c129
-rw-r--r--src/doveadm/server-connection.c691
-rw-r--r--src/doveadm/server-connection.h35
-rw-r--r--src/doveadm/test-doveadm-util.c48
-rw-r--r--src/imap-hibernate/Makefile.am27
-rw-r--r--src/imap-hibernate/Makefile.in828
-rw-r--r--src/imap-hibernate/imap-client.c810
-rw-r--r--src/imap-hibernate/imap-client.h40
-rw-r--r--src/imap-hibernate/imap-hibernate-client.c304
-rw-r--r--src/imap-hibernate/imap-hibernate-client.h9
-rw-r--r--src/imap-hibernate/imap-hibernate-settings.c47
-rw-r--r--src/imap-hibernate/imap-master-connection.c140
-rw-r--r--src/imap-hibernate/imap-master-connection.h24
-rw-r--r--src/imap-hibernate/main.c57
-rw-r--r--src/imap-login/Makefile.am41
-rw-r--r--src/imap-login/Makefile.in897
-rw-r--r--src/imap-login/client-authenticate.c213
-rw-r--r--src/imap-login/client-authenticate.h16
-rw-r--r--src/imap-login/imap-login-client.c571
-rw-r--r--src/imap-login/imap-login-client.h97
-rw-r--r--src/imap-login/imap-login-cmd-id.c281
-rw-r--r--src/imap-login/imap-login-commands.c70
-rw-r--r--src/imap-login/imap-login-commands.h25
-rw-r--r--src/imap-login/imap-login-settings.c95
-rw-r--r--src/imap-login/imap-login-settings.h14
-rw-r--r--src/imap-login/imap-proxy.c530
-rw-r--r--src/imap-login/imap-proxy.h12
-rw-r--r--src/imap-urlauth/Makefile.am85
-rw-r--r--src/imap-urlauth/Makefile.in1014
-rw-r--r--src/imap-urlauth/imap-urlauth-client.c380
-rw-r--r--src/imap-urlauth/imap-urlauth-client.h49
-rw-r--r--src/imap-urlauth/imap-urlauth-common.h13
-rw-r--r--src/imap-urlauth/imap-urlauth-login-settings.c75
-rw-r--r--src/imap-urlauth/imap-urlauth-login-settings.h6
-rw-r--r--src/imap-urlauth/imap-urlauth-login.c199
-rw-r--r--src/imap-urlauth/imap-urlauth-settings.c94
-rw-r--r--src/imap-urlauth/imap-urlauth-settings.h24
-rw-r--r--src/imap-urlauth/imap-urlauth-worker-settings.c88
-rw-r--r--src/imap-urlauth/imap-urlauth-worker-settings.h18
-rw-r--r--src/imap-urlauth/imap-urlauth-worker.c1033
-rw-r--r--src/imap-urlauth/imap-urlauth.c290
-rw-r--r--src/imap/Makefile.am130
-rw-r--r--src/imap/Makefile.in1213
-rw-r--r--src/imap/cmd-append.c957
-rw-r--r--src/imap/cmd-cancelupdate.c45
-rw-r--r--src/imap/cmd-capability.c14
-rw-r--r--src/imap/cmd-check.c14
-rw-r--r--src/imap/cmd-close.c36
-rw-r--r--src/imap/cmd-copy.c407
-rw-r--r--src/imap/cmd-create.c48
-rw-r--r--src/imap/cmd-delete.c59
-rw-r--r--src/imap/cmd-enable.c35
-rw-r--r--src/imap/cmd-examine.c9
-rw-r--r--src/imap/cmd-expunge.c68
-rw-r--r--src/imap/cmd-fetch.c391
-rw-r--r--src/imap/cmd-genurlauth.c54
-rw-r--r--src/imap/cmd-getmetadata.c559
-rw-r--r--src/imap/cmd-id.c27
-rw-r--r--src/imap/cmd-idle.c308
-rw-r--r--src/imap/cmd-list.c484
-rw-r--r--src/imap/cmd-logout.c24
-rw-r--r--src/imap/cmd-lsub.c9
-rw-r--r--src/imap/cmd-namespace.c99
-rw-r--r--src/imap/cmd-noop.c15
-rw-r--r--src/imap/cmd-notify.c597
-rw-r--r--src/imap/cmd-rename.c51
-rw-r--r--src/imap/cmd-resetkey.c97
-rw-r--r--src/imap/cmd-search.c49
-rw-r--r--src/imap/cmd-select.c426
-rw-r--r--src/imap/cmd-setmetadata.c378
-rw-r--r--src/imap/cmd-sort.c142
-rw-r--r--src/imap/cmd-status.c54
-rw-r--r--src/imap/cmd-store.c261
-rw-r--r--src/imap/cmd-subscribe.c87
-rw-r--r--src/imap/cmd-thread.c294
-rw-r--r--src/imap/cmd-unselect.c18
-rw-r--r--src/imap/cmd-unsubscribe.c9
-rw-r--r--src/imap/cmd-urlfetch.c410
-rw-r--r--src/imap/cmd-x-cancel.c28
-rw-r--r--src/imap/cmd-x-state.c68
-rw-r--r--src/imap/imap-client-hibernate.c294
-rw-r--r--src/imap/imap-client.c1681
-rw-r--r--src/imap/imap-client.h362
-rw-r--r--src/imap/imap-commands-util.c402
-rw-r--r--src/imap/imap-commands-util.h81
-rw-r--r--src/imap/imap-commands.c247
-rw-r--r--src/imap/imap-commands.h137
-rw-r--r--src/imap/imap-common.h40
-rw-r--r--src/imap/imap-expunge.c111
-rw-r--r--src/imap/imap-expunge.h10
-rw-r--r--src/imap/imap-feature.c46
-rw-r--r--src/imap/imap-feature.h24
-rw-r--r--src/imap/imap-fetch-body.c722
-rw-r--r--src/imap/imap-fetch.c1040
-rw-r--r--src/imap/imap-fetch.h166
-rw-r--r--src/imap/imap-list.c35
-rw-r--r--src/imap/imap-list.h7
-rw-r--r--src/imap/imap-master-client.c448
-rw-r--r--src/imap/imap-master-client.h10
-rw-r--r--src/imap/imap-notify.c523
-rw-r--r--src/imap/imap-notify.h75
-rw-r--r--src/imap/imap-search-args.c353
-rw-r--r--src/imap/imap-search-args.h47
-rw-r--r--src/imap/imap-search.c612
-rw-r--r--src/imap/imap-search.h61
-rw-r--r--src/imap/imap-settings.c197
-rw-r--r--src/imap/imap-settings.h49
-rw-r--r--src/imap/imap-state.c897
-rw-r--r--src/imap/imap-state.h30
-rw-r--r--src/imap/imap-status.c172
-rw-r--r--src/imap/imap-status.h51
-rw-r--r--src/imap/imap-sync-private.h47
-rw-r--r--src/imap/imap-sync.c841
-rw-r--r--src/imap/imap-sync.h25
-rw-r--r--src/imap/mail-storage-callbacks.c45
-rw-r--r--src/imap/main.c591
-rw-r--r--src/imap/test-imap-client-hibernate.c292
-rw-r--r--src/indexer/Makefile.am47
-rw-r--r--src/indexer/Makefile.in869
-rw-r--r--src/indexer/indexer-client.c223
-rw-r--r--src/indexer/indexer-client.h13
-rw-r--r--src/indexer/indexer-queue.c275
-rw-r--r--src/indexer/indexer-queue.h72
-rw-r--r--src/indexer/indexer-settings.c50
-rw-r--r--src/indexer/indexer-worker-settings.c46
-rw-r--r--src/indexer/indexer-worker.c82
-rw-r--r--src/indexer/indexer.c137
-rw-r--r--src/indexer/indexer.h12
-rw-r--r--src/indexer/master-connection.c365
-rw-r--r--src/indexer/master-connection.h8
-rw-r--r--src/indexer/worker-connection.c196
-rw-r--r--src/indexer/worker-connection.h36
-rw-r--r--src/indexer/worker-pool.c98
-rw-r--r--src/indexer/worker-pool.h23
-rw-r--r--src/ipc/Makefile.am25
-rw-r--r--src/ipc/Makefile.in825
-rw-r--r--src/ipc/client.c157
-rw-r--r--src/ipc/client.h9
-rw-r--r--src/ipc/ipc-connection.c253
-rw-r--r--src/ipc/ipc-connection.h46
-rw-r--r--src/ipc/ipc-group.c166
-rw-r--r--src/ipc/ipc-group.h45
-rw-r--r--src/ipc/ipc-settings.c50
-rw-r--r--src/ipc/main.c73
-rw-r--r--src/lda/Makefile.am38
-rw-r--r--src/lda/Makefile.in822
-rw-r--r--src/lda/main.c596
-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.c366
-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.c1566
-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.c1508
-rw-r--r--src/lib-dict/dict-client.h48
-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.h127
-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.c769
-rw-r--r--src/lib-dict/dict.h208
-rw-r--r--src/lib-dict/test-dict-client.c117
-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.c1412
-rw-r--r--src/lib-fs/fs-api.h399
-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.in1141
-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.c1183
-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.c809
-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.c2475
-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.c2557
-rw-r--r--src/lib-imap-client/imapc-connection.h64
-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.c972
-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.c734
-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.c724
-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.c1088
-rw-r--r--src/lib-index/mail-index-sync.c1068
-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.c254
-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.c522
-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.h121
-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.c598
-rw-r--r--src/lib-master/master-login.h51
-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.c279
-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.c1557
-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.c399
-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.c525
-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.c377
-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.c2588
-rw-r--r--src/lib-sql/driver-mysql.c844
-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.h255
-rw-r--r--src/lib-sql/sql-api.c846
-rw-r--r--src/lib-sql/sql-api.h251
-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.c465
-rw-r--r--src/lib-storage/index/dbox-common/dbox-storage.h89
-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.c1492
-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.c493
-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.c529
-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.c535
-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.c1013
-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.c1015
-rw-r--r--src/lib-storage/index/imapc/imapc-save.c829
-rw-r--r--src/lib-storage/index/imapc/imapc-search.c332
-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.c1353
-rw-r--r--src/lib-storage/index/imapc/imapc-storage.h274
-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.c2625
-rw-r--r--src/lib-storage/index/index-mail.h293
-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.c1923
-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.c1296
-rw-r--r--src/lib-storage/index/index-storage.h194
-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.c230
-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.c795
-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.c615
-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.c970
-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.c1234
-rw-r--r--src/lib-storage/list/mailbox-list-index.h250
-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.c108
-rw-r--r--src/lib-storage/mail-search-args-imap.c329
-rw-r--r--src/lib-storage/mail-search-args-simplify.c921
-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.c644
-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.c818
-rw-r--r--src/lib-storage/mail-search.h278
-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.c1811
-rw-r--r--src/lib-storage/mail-storage-service.h190
-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.h1030
-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.c865
-rw-r--r--src/lib-storage/mail-user.h262
-rw-r--r--src/lib-storage/mail.c713
-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.c587
-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.c416
-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.am22
-rw-r--r--src/lib-test/Makefile.in836
-rw-r--r--src/lib-test/fuzzer.c87
-rw-r--r--src/lib-test/fuzzer.h37
-rw-r--r--src/lib-test/ostream-final-trickle.c148
-rw-r--r--src/lib-test/ostream-final-trickle.h9
-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.in3711
-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.c962
-rw-r--r--src/lib/connection.h260
-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.c2297
-rw-r--r--src/lib/event-filter-lexer.l141
-rw-r--r--src/lib/event-filter-parser.c1852
-rw-r--r--src/lib/event-filter-parser.h96
-rw-r--r--src/lib/event-filter-parser.y195
-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.c224
-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.c1776
-rw-r--r--src/lib/lib-event.h440
-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.c194
-rw-r--r--src/lib/process-title.h19
-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.c128
-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
-rw-r--r--src/lmtp/Makefile.am55
-rw-r--r--src/lmtp/Makefile.in916
-rw-r--r--src/lmtp/lmtp-client.c439
-rw-r--r--src/lmtp/lmtp-client.h124
-rw-r--r--src/lmtp/lmtp-commands.c340
-rw-r--r--src/lmtp/lmtp-commands.h45
-rw-r--r--src/lmtp/lmtp-common.h35
-rw-r--r--src/lmtp/lmtp-local.c766
-rw-r--r--src/lmtp/lmtp-local.h34
-rw-r--r--src/lmtp/lmtp-proxy.c860
-rw-r--r--src/lmtp/lmtp-proxy.h29
-rw-r--r--src/lmtp/lmtp-recipient.c57
-rw-r--r--src/lmtp/lmtp-recipient.h48
-rw-r--r--src/lmtp/lmtp-settings.c205
-rw-r--r--src/lmtp/lmtp-settings.h53
-rw-r--r--src/lmtp/main.c173
-rw-r--r--src/log/Makefile.am26
-rw-r--r--src/log/Makefile.in825
-rw-r--r--src/log/doveadm-connection.c86
-rw-r--r--src/log/doveadm-connection.h6
-rw-r--r--src/log/log-connection.c533
-rw-r--r--src/log/log-connection.h15
-rw-r--r--src/log/log-error-buffer.c122
-rw-r--r--src/log/log-error-buffer.h24
-rw-r--r--src/log/log-settings.c49
-rw-r--r--src/log/main.c97
-rw-r--r--src/login-common/Makefile.am40
-rw-r--r--src/login-common/Makefile.in908
-rw-r--r--src/login-common/access-lookup.c118
-rw-r--r--src/login-common/access-lookup.h11
-rw-r--r--src/login-common/client-common-auth.c953
-rw-r--r--src/login-common/client-common.c1212
-rw-r--r--src/login-common/client-common.h373
-rw-r--r--src/login-common/login-common.h79
-rw-r--r--src/login-common/login-proxy-state.c165
-rw-r--r--src/login-common/login-proxy-state.h40
-rw-r--r--src/login-common/login-proxy.c1173
-rw-r--r--src/login-common/login-proxy.h120
-rw-r--r--src/login-common/login-settings.c227
-rw-r--r--src/login-common/login-settings.h50
-rw-r--r--src/login-common/main.c571
-rw-r--r--src/login-common/sasl-server.c604
-rw-r--r--src/login-common/sasl-server.h41
-rw-r--r--src/master/Makefile.am99
-rw-r--r--src/master/Makefile.in971
-rw-r--r--src/master/capabilities-posix.c35
-rw-r--r--src/master/capabilities.h14
-rw-r--r--src/master/common.h27
-rw-r--r--src/master/dup2-array.c78
-rw-r--r--src/master/dup2-array.h13
-rw-r--r--src/master/main.c941
-rw-r--r--src/master/master-client.c191
-rw-r--r--src/master/master-client.h9
-rw-r--r--src/master/master-settings.c806
-rw-r--r--src/master/master-settings.h35
-rw-r--r--src/master/service-anvil.c202
-rw-r--r--src/master/service-anvil.h36
-rw-r--r--src/master/service-listen.c492
-rw-r--r--src/master/service-listen.h18
-rw-r--r--src/master/service-log.c169
-rw-r--r--src/master/service-log.h13
-rw-r--r--src/master/service-monitor.c845
-rw-r--r--src/master/service-monitor.h18
-rw-r--r--src/master/service-process-notify.c101
-rw-r--r--src/master/service-process-notify.h15
-rw-r--r--src/master/service-process.c676
-rw-r--r--src/master/service-process.h56
-rw-r--r--src/master/service.c769
-rw-r--r--src/master/service.h220
-rw-r--r--src/master/test-auth-client.c1287
-rw-r--r--src/master/test-auth-master.c1390
-rw-r--r--src/master/test-master-login-auth.c994
-rw-r--r--src/old-stats/Makefile.am48
-rw-r--r--src/old-stats/Makefile.in882
-rw-r--r--src/old-stats/client-export.c657
-rw-r--r--src/old-stats/client-export.h9
-rw-r--r--src/old-stats/client-reset.c21
-rw-r--r--src/old-stats/client-reset.h9
-rw-r--r--src/old-stats/client.c193
-rw-r--r--src/old-stats/client.h35
-rw-r--r--src/old-stats/fifo-input-connection.c108
-rw-r--r--src/old-stats/fifo-input-connection.h9
-rw-r--r--src/old-stats/global-memory.c46
-rw-r--r--src/old-stats/global-memory.h9
-rw-r--r--src/old-stats/mail-command.c247
-rw-r--r--src/old-stats/mail-command.h18
-rw-r--r--src/old-stats/mail-domain.c133
-rw-r--r--src/old-stats/mail-domain.h22
-rw-r--r--src/old-stats/mail-ip.c129
-rw-r--r--src/old-stats/mail-ip.h19
-rw-r--r--src/old-stats/mail-session.c343
-rw-r--r--src/old-stats/mail-session.h28
-rw-r--r--src/old-stats/mail-stats.c86
-rw-r--r--src/old-stats/mail-stats.h123
-rw-r--r--src/old-stats/mail-user.c177
-rw-r--r--src/old-stats/mail-user.h23
-rw-r--r--src/old-stats/main.c95
-rw-r--r--src/old-stats/stats-carbon.c125
-rw-r--r--src/old-stats/stats-carbon.h13
-rw-r--r--src/old-stats/stats-settings.c104
-rw-r--r--src/old-stats/stats-settings.h22
-rw-r--r--src/plugins/Makefile.am53
-rw-r--r--src/plugins/Makefile.in810
-rw-r--r--src/plugins/acl/Makefile.am78
-rw-r--r--src/plugins/acl/Makefile.in1035
-rw-r--r--src/plugins/acl/acl-api-private.h135
-rw-r--r--src/plugins/acl/acl-api.c847
-rw-r--r--src/plugins/acl/acl-api.h167
-rw-r--r--src/plugins/acl/acl-attributes.c233
-rw-r--r--src/plugins/acl/acl-backend-vfile-acllist.c424
-rw-r--r--src/plugins/acl/acl-backend-vfile-update.c260
-rw-r--r--src/plugins/acl/acl-backend-vfile.c659
-rw-r--r--src/plugins/acl/acl-backend-vfile.h88
-rw-r--r--src/plugins/acl/acl-backend.c194
-rw-r--r--src/plugins/acl/acl-cache.c395
-rw-r--r--src/plugins/acl/acl-cache.h57
-rw-r--r--src/plugins/acl/acl-global-file.c246
-rw-r--r--src/plugins/acl/acl-global-file.h23
-rw-r--r--src/plugins/acl/acl-lookup-dict.c373
-rw-r--r--src/plugins/acl/acl-lookup-dict.h17
-rw-r--r--src/plugins/acl/acl-mailbox-list.c633
-rw-r--r--src/plugins/acl/acl-mailbox.c714
-rw-r--r--src/plugins/acl/acl-plugin.c27
-rw-r--r--src/plugins/acl/acl-plugin.h73
-rw-r--r--src/plugins/acl/acl-shared-storage.c103
-rw-r--r--src/plugins/acl/acl-shared-storage.h6
-rw-r--r--src/plugins/acl/acl-storage.c62
-rw-r--r--src/plugins/acl/acl-storage.h50
-rw-r--r--src/plugins/acl/doveadm-acl.c629
-rw-r--r--src/plugins/acl/test-acl.c66
-rw-r--r--src/plugins/apparmor/Makefile.am14
-rw-r--r--src/plugins/apparmor/Makefile.in816
-rw-r--r--src/plugins/apparmor/apparmor-plugin.c111
-rw-r--r--src/plugins/charset-alias/Makefile.am19
-rw-r--r--src/plugins/charset-alias/Makefile.in822
-rw-r--r--src/plugins/charset-alias/charset-alias-plugin.c198
-rw-r--r--src/plugins/charset-alias/charset-alias-plugin.h7
-rw-r--r--src/plugins/fs-compress/Makefile.am14
-rw-r--r--src/plugins/fs-compress/Makefile.in811
-rw-r--r--src/plugins/fs-compress/fs-compress.c285
-rw-r--r--src/plugins/fts-lucene/Makefile.am61
-rw-r--r--src/plugins/fts-lucene/Makefile.in990
-rw-r--r--src/plugins/fts-lucene/Snowball.cc151
-rw-r--r--src/plugins/fts-lucene/SnowballAnalyzer.h51
-rw-r--r--src/plugins/fts-lucene/SnowballFilter.h42
-rw-r--r--src/plugins/fts-lucene/doveadm-fts-lucene.c70
-rw-r--r--src/plugins/fts-lucene/fts-backend-lucene.c605
-rw-r--r--src/plugins/fts-lucene/fts-lucene-plugin.c146
-rw-r--r--src/plugins/fts-lucene/fts-lucene-plugin.h36
-rw-r--r--src/plugins/fts-lucene/lucene-wrapper.cc1639
-rw-r--r--src/plugins/fts-lucene/lucene-wrapper.h67
-rw-r--r--src/plugins/fts-lucene/textcat.conf25
-rw-r--r--src/plugins/fts-solr/Makefile.am64
-rw-r--r--src/plugins/fts-solr/Makefile.in965
-rw-r--r--src/plugins/fts-solr/fts-backend-solr-old.c879
-rw-r--r--src/plugins/fts-solr/fts-backend-solr.c984
-rw-r--r--src/plugins/fts-solr/fts-solr-plugin.c131
-rw-r--r--src/plugins/fts-solr/fts-solr-plugin.h35
-rw-r--r--src/plugins/fts-solr/solr-connection.c327
-rw-r--r--src/plugins/fts-solr/solr-connection.h26
-rw-r--r--src/plugins/fts-solr/solr-response.c372
-rw-r--r--src/plugins/fts-solr/solr-response.h23
-rw-r--r--src/plugins/fts-solr/test-solr-response.c295
-rw-r--r--src/plugins/fts-squat/Makefile.am47
-rw-r--r--src/plugins/fts-squat/Makefile.in883
-rw-r--r--src/plugins/fts-squat/fts-backend-squat.c497
-rw-r--r--src/plugins/fts-squat/fts-squat-plugin.c18
-rw-r--r--src/plugins/fts-squat/fts-squat-plugin.h14
-rw-r--r--src/plugins/fts-squat/squat-test.c197
-rw-r--r--src/plugins/fts-squat/squat-trie-private.h192
-rw-r--r--src/plugins/fts-squat/squat-trie.c2096
-rw-r--r--src/plugins/fts-squat/squat-trie.h54
-rw-r--r--src/plugins/fts-squat/squat-uidlist.c1624
-rw-r--r--src/plugins/fts-squat/squat-uidlist.h71
-rw-r--r--src/plugins/fts/Makefile.am74
-rw-r--r--src/plugins/fts/Makefile.in1140
-rwxr-xr-xsrc/plugins/fts/decode2text.sh105
-rw-r--r--src/plugins/fts/doveadm-dump-fts-expunge-log.c116
-rw-r--r--src/plugins/fts/doveadm-fts.c472
-rw-r--r--src/plugins/fts/doveadm-fts.h11
-rw-r--r--src/plugins/fts/fts-api-private.h139
-rw-r--r--src/plugins/fts/fts-api.c554
-rw-r--r--src/plugins/fts/fts-api.h173
-rw-r--r--src/plugins/fts/fts-build-mail.c719
-rw-r--r--src/plugins/fts/fts-build-mail.h9
-rw-r--r--src/plugins/fts/fts-expunge-log.c617
-rw-r--r--src/plugins/fts/fts-expunge-log.h58
-rw-r--r--src/plugins/fts/fts-indexer.c300
-rw-r--r--src/plugins/fts/fts-indexer.h22
-rw-r--r--src/plugins/fts/fts-parser-html.c64
-rw-r--r--src/plugins/fts/fts-parser-script.c277
-rw-r--r--src/plugins/fts/fts-parser-tika.c278
-rw-r--r--src/plugins/fts/fts-parser.c127
-rw-r--r--src/plugins/fts/fts-parser.h48
-rw-r--r--src/plugins/fts/fts-plugin.c33
-rw-r--r--src/plugins/fts/fts-plugin.h7
-rw-r--r--src/plugins/fts/fts-search-args.c426
-rw-r--r--src/plugins/fts/fts-search-args.h7
-rw-r--r--src/plugins/fts/fts-search-serialize.c99
-rw-r--r--src/plugins/fts/fts-search-serialize.h16
-rw-r--r--src/plugins/fts/fts-search.c385
-rw-r--r--src/plugins/fts/fts-storage.c981
-rw-r--r--src/plugins/fts/fts-storage.h70
-rw-r--r--src/plugins/fts/fts-user.c412
-rw-r--r--src/plugins/fts/fts-user.h25
-rw-r--r--src/plugins/fts/xml2text.c44
-rw-r--r--src/plugins/imap-acl/Makefile.am31
-rw-r--r--src/plugins/imap-acl/Makefile.in831
-rw-r--r--src/plugins/imap-acl/imap-acl-plugin.c1128
-rw-r--r--src/plugins/imap-acl/imap-acl-plugin.h12
-rw-r--r--src/plugins/imap-old-stats/Makefile.am28
-rw-r--r--src/plugins/imap-old-stats/Makefile.in827
-rw-r--r--src/plugins/imap-old-stats/imap-stats-plugin.c128
-rw-r--r--src/plugins/imap-old-stats/imap-stats-plugin.h12
-rw-r--r--src/plugins/imap-quota/Makefile.am27
-rw-r--r--src/plugins/imap-quota/Makefile.in827
-rw-r--r--src/plugins/imap-quota/imap-quota-plugin.c266
-rw-r--r--src/plugins/imap-quota/imap-quota-plugin.h12
-rw-r--r--src/plugins/imap-zlib/Makefile.am25
-rw-r--r--src/plugins/imap-zlib/Makefile.in827
-rw-r--r--src/plugins/imap-zlib/imap-zlib-plugin.c184
-rw-r--r--src/plugins/imap-zlib/imap-zlib-plugin.h12
-rw-r--r--src/plugins/last-login/Makefile.am19
-rw-r--r--src/plugins/last-login/Makefile.in822
-rw-r--r--src/plugins/last-login/last-login-plugin.c157
-rw-r--r--src/plugins/last-login/last-login-plugin.h7
-rw-r--r--src/plugins/lazy-expunge/Makefile.am21
-rw-r--r--src/plugins/lazy-expunge/Makefile.in824
-rw-r--r--src/plugins/lazy-expunge/lazy-expunge-plugin.c654
-rw-r--r--src/plugins/lazy-expunge/lazy-expunge-plugin.h7
-rw-r--r--src/plugins/listescape/Makefile.am18
-rw-r--r--src/plugins/listescape/Makefile.in821
-rw-r--r--src/plugins/listescape/listescape-plugin.c36
-rw-r--r--src/plugins/listescape/listescape-plugin.h7
-rw-r--r--src/plugins/mail-crypt/Makefile.am116
-rw-r--r--src/plugins/mail-crypt/Makefile.in1200
-rw-r--r--src/plugins/mail-crypt/doveadm-mail-crypt.c1048
-rw-r--r--src/plugins/mail-crypt/fs-crypt-common.c368
-rw-r--r--src/plugins/mail-crypt/fs-crypt-settings.c35
-rw-r--r--src/plugins/mail-crypt/fs-crypt-settings.h11
-rw-r--r--src/plugins/mail-crypt/fs-crypt.c65
-rw-r--r--src/plugins/mail-crypt/fs-mail-crypt.c72
-rw-r--r--src/plugins/mail-crypt/mail-crypt-acl-plugin.c431
-rw-r--r--src/plugins/mail-crypt/mail-crypt-common.h30
-rw-r--r--src/plugins/mail-crypt/mail-crypt-global-key.c172
-rw-r--r--src/plugins/mail-crypt/mail-crypt-global-key.h38
-rw-r--r--src/plugins/mail-crypt/mail-crypt-key.c1242
-rw-r--r--src/plugins/mail-crypt/mail-crypt-key.h119
-rw-r--r--src/plugins/mail-crypt/mail-crypt-plugin.c501
-rw-r--r--src/plugins/mail-crypt/mail-crypt-plugin.h32
-rw-r--r--src/plugins/mail-crypt/mail-crypt-pluginenv.c106
-rw-r--r--src/plugins/mail-crypt/mail-crypt-userenv.c66
-rw-r--r--src/plugins/mail-crypt/test-mail-global-key.c130
-rw-r--r--src/plugins/mail-crypt/test-mail-key.c424
-rw-r--r--src/plugins/mail-log/Makefile.am24
-rw-r--r--src/plugins/mail-log/Makefile.in826
-rw-r--r--src/plugins/mail-log/mail-log-plugin.c546
-rw-r--r--src/plugins/mail-log/mail-log-plugin.h9
-rw-r--r--src/plugins/mail-lua/Makefile.am32
-rw-r--r--src/plugins/mail-lua/Makefile.in873
-rw-r--r--src/plugins/mail-lua/mail-lua-plugin.c167
-rw-r--r--src/plugins/mail-lua/mail-lua-plugin.h14
-rw-r--r--src/plugins/mailbox-alias/Makefile.am18
-rw-r--r--src/plugins/mailbox-alias/Makefile.in821
-rw-r--r--src/plugins/mailbox-alias/mailbox-alias-plugin.c356
-rw-r--r--src/plugins/mailbox-alias/mailbox-alias-plugin.h7
-rw-r--r--src/plugins/notify-status/Makefile.am17
-rw-r--r--src/plugins/notify-status/Makefile.in817
-rw-r--r--src/plugins/notify-status/notify-status-plugin.c363
-rw-r--r--src/plugins/notify/Makefile.am25
-rw-r--r--src/plugins/notify/Makefile.in852
-rw-r--r--src/plugins/notify/notify-plugin-private.h29
-rw-r--r--src/plugins/notify/notify-plugin.c265
-rw-r--r--src/plugins/notify/notify-plugin.h43
-rw-r--r--src/plugins/notify/notify-storage.c269
-rw-r--r--src/plugins/old-stats/Makefile.am33
-rw-r--r--src/plugins/old-stats/Makefile.in898
-rw-r--r--src/plugins/old-stats/mail-stats-connection.c75
-rw-r--r--src/plugins/old-stats/mail-stats-connection.h19
-rw-r--r--src/plugins/old-stats/mail-stats-fill.c156
-rw-r--r--src/plugins/old-stats/mail-stats.c168
-rw-r--r--src/plugins/old-stats/mail-stats.h41
-rw-r--r--src/plugins/old-stats/stats-plugin.c481
-rw-r--r--src/plugins/old-stats/stats-plugin.h60
-rw-r--r--src/plugins/pop3-migration/Makefile.am41
-rw-r--r--src/plugins/pop3-migration/Makefile.in872
-rw-r--r--src/plugins/pop3-migration/pop3-migration-plugin.c1058
-rw-r--r--src/plugins/pop3-migration/pop3-migration-plugin.h13
-rw-r--r--src/plugins/pop3-migration/test-pop3-migration-plugin.c63
-rw-r--r--src/plugins/push-notification/Makefile.am83
-rw-r--r--src/plugins/push-notification/Makefile.in1037
-rw-r--r--src/plugins/push-notification/push-notification-driver-dlog.c114
-rw-r--r--src/plugins/push-notification/push-notification-driver-lua.c663
-rw-r--r--src/plugins/push-notification/push-notification-driver-ox.c470
-rw-r--r--src/plugins/push-notification/push-notification-drivers.c181
-rw-r--r--src/plugins/push-notification/push-notification-drivers.h123
-rw-r--r--src/plugins/push-notification/push-notification-event-flagsclear.c170
-rw-r--r--src/plugins/push-notification/push-notification-event-flagsclear.h22
-rw-r--r--src/plugins/push-notification/push-notification-event-flagsset.c167
-rw-r--r--src/plugins/push-notification/push-notification-event-flagsset.h22
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxcreate.c56
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxcreate.h12
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxdelete.c46
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxdelete.h12
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxrename.c50
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxrename.h11
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxsubscribe.c48
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxsubscribe.h12
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxunsubscribe.c48
-rw-r--r--src/plugins/push-notification/push-notification-event-mailboxunsubscribe.h12
-rw-r--r--src/plugins/push-notification/push-notification-event-message-common.c117
-rw-r--r--src/plugins/push-notification/push-notification-event-message-common.h40
-rw-r--r--src/plugins/push-notification/push-notification-event-messageappend.c99
-rw-r--r--src/plugins/push-notification/push-notification-event-messageappend.h30
-rw-r--r--src/plugins/push-notification/push-notification-event-messageexpunge.c53
-rw-r--r--src/plugins/push-notification/push-notification-event-messageexpunge.h12
-rw-r--r--src/plugins/push-notification/push-notification-event-messagenew.c97
-rw-r--r--src/plugins/push-notification/push-notification-event-messagenew.h36
-rw-r--r--src/plugins/push-notification/push-notification-event-messageread.c58
-rw-r--r--src/plugins/push-notification/push-notification-event-messageread.h11
-rw-r--r--src/plugins/push-notification/push-notification-event-messagetrash.c58
-rw-r--r--src/plugins/push-notification/push-notification-event-messagetrash.h12
-rw-r--r--src/plugins/push-notification/push-notification-events-rfc5423.c59
-rw-r--r--src/plugins/push-notification/push-notification-events-rfc5423.h10
-rw-r--r--src/plugins/push-notification/push-notification-events.c100
-rw-r--r--src/plugins/push-notification/push-notification-events.h124
-rw-r--r--src/plugins/push-notification/push-notification-plugin.c390
-rw-r--r--src/plugins/push-notification/push-notification-plugin.h18
-rw-r--r--src/plugins/push-notification/push-notification-triggers.c215
-rw-r--r--src/plugins/push-notification/push-notification-triggers.h64
-rw-r--r--src/plugins/push-notification/push-notification-txn-mbox.c90
-rw-r--r--src/plugins/push-notification/push-notification-txn-mbox.h29
-rw-r--r--src/plugins/push-notification/push-notification-txn-msg.c139
-rw-r--r--src/plugins/push-notification/push-notification-txn-msg.h39
-rw-r--r--src/plugins/quota-clone/Makefile.am19
-rw-r--r--src/plugins/quota-clone/Makefile.in822
-rw-r--r--src/plugins/quota-clone/quota-clone-plugin.c308
-rw-r--r--src/plugins/quota-clone/quota-clone-plugin.h7
-rw-r--r--src/plugins/quota/Makefile.am148
-rw-r--r--src/plugins/quota/Makefile.in1183
-rw-r--r--src/plugins/quota/doveadm-quota.c165
-rw-r--r--src/plugins/quota/quota-count.c400
-rw-r--r--src/plugins/quota/quota-dict.c269
-rw-r--r--src/plugins/quota/quota-dirsize.c232
-rw-r--r--src/plugins/quota/quota-fs.c970
-rw-r--r--src/plugins/quota/quota-fs.h51
-rw-r--r--src/plugins/quota/quota-imapc.c494
-rw-r--r--src/plugins/quota/quota-maildir.c953
-rw-r--r--src/plugins/quota/quota-plugin.c31
-rw-r--r--src/plugins/quota/quota-plugin.h36
-rw-r--r--src/plugins/quota/quota-private.h230
-rw-r--r--src/plugins/quota/quota-status-settings.c37
-rw-r--r--src/plugins/quota/quota-status-settings.h10
-rw-r--r--src/plugins/quota/quota-status.c360
-rw-r--r--src/plugins/quota/quota-storage.c780
-rw-r--r--src/plugins/quota/quota-util.c465
-rw-r--r--src/plugins/quota/quota.c1543
-rw-r--r--src/plugins/quota/quota.h147
-rw-r--r--src/plugins/quota/rquota-pragmas.h4
-rw-r--r--src/plugins/quota/rquota.x139
-rw-r--r--src/plugins/quota/test-quota-util.c96
-rw-r--r--src/plugins/replication/Makefile.am25
-rw-r--r--src/plugins/replication/Makefile.in827
-rw-r--r--src/plugins/replication/replication-plugin.c404
-rw-r--r--src/plugins/replication/replication-plugin.h9
-rw-r--r--src/plugins/trash/Makefile.am23
-rw-r--r--src/plugins/trash/Makefile.in824
-rw-r--r--src/plugins/trash/trash-plugin.c392
-rw-r--r--src/plugins/trash/trash-plugin.h9
-rw-r--r--src/plugins/var-expand-crypt/Makefile.am39
-rw-r--r--src/plugins/var-expand-crypt/Makefile.in938
-rw-r--r--src/plugins/var-expand-crypt/test-var-expand-crypt.c102
-rw-r--r--src/plugins/var-expand-crypt/var-expand-crypt-plugin.c335
-rw-r--r--src/plugins/virtual/Makefile.am29
-rw-r--r--src/plugins/virtual/Makefile.in859
-rw-r--r--src/plugins/virtual/virtual-config.c567
-rw-r--r--src/plugins/virtual/virtual-mail.c583
-rw-r--r--src/plugins/virtual/virtual-plugin.c25
-rw-r--r--src/plugins/virtual/virtual-plugin.h7
-rw-r--r--src/plugins/virtual/virtual-save.c153
-rw-r--r--src/plugins/virtual/virtual-search.c206
-rw-r--r--src/plugins/virtual/virtual-storage.c953
-rw-r--r--src/plugins/virtual/virtual-storage.h251
-rw-r--r--src/plugins/virtual/virtual-sync.c1973
-rw-r--r--src/plugins/virtual/virtual-transaction.c87
-rw-r--r--src/plugins/virtual/virtual-transaction.h24
-rw-r--r--src/plugins/welcome/Makefile.am14
-rw-r--r--src/plugins/welcome/Makefile.in814
-rw-r--r--src/plugins/welcome/welcome-plugin.c135
-rw-r--r--src/plugins/zlib/Makefile.am24
-rw-r--r--src/plugins/zlib/Makefile.in827
-rw-r--r--src/plugins/zlib/zlib-plugin.c388
-rw-r--r--src/plugins/zlib/zlib-plugin.h7
-rw-r--r--src/pop3-login/Makefile.am34
-rw-r--r--src/pop3-login/Makefile.in830
-rw-r--r--src/pop3-login/client-authenticate.c247
-rw-r--r--src/pop3-login/client-authenticate.h15
-rw-r--r--src/pop3-login/client.c395
-rw-r--r--src/pop3-login/client.h40
-rw-r--r--src/pop3-login/pop3-login-settings.c75
-rw-r--r--src/pop3-login/pop3-login-settings.h6
-rw-r--r--src/pop3-login/pop3-proxy.c320
-rw-r--r--src/pop3-login/pop3-proxy.h12
-rw-r--r--src/pop3/Makefile.am39
-rw-r--r--src/pop3/Makefile.in887
-rw-r--r--src/pop3/main.c433
-rw-r--r--src/pop3/pop3-capability.h14
-rw-r--r--src/pop3/pop3-client.c869
-rw-r--r--src/pop3/pop3-client.h146
-rw-r--r--src/pop3/pop3-commands.c956
-rw-r--r--src/pop3/pop3-commands.h13
-rw-r--r--src/pop3/pop3-common.h27
-rw-r--r--src/pop3/pop3-settings.c184
-rw-r--r--src/pop3/pop3-settings.h40
-rw-r--r--src/replication/Makefile.am4
-rw-r--r--src/replication/Makefile.in773
-rw-r--r--src/replication/aggregator/Makefile.am29
-rw-r--r--src/replication/aggregator/Makefile.in828
-rw-r--r--src/replication/aggregator/aggregator-settings.c85
-rw-r--r--src/replication/aggregator/aggregator-settings.h12
-rw-r--r--src/replication/aggregator/aggregator.c74
-rw-r--r--src/replication/aggregator/notify-connection.c154
-rw-r--r--src/replication/aggregator/notify-connection.h9
-rw-r--r--src/replication/aggregator/replicator-connection.c326
-rw-r--r--src/replication/aggregator/replicator-connection.h25
-rw-r--r--src/replication/replication-common.h48
-rw-r--r--src/replication/replicator/Makefile.am60
-rw-r--r--src/replication/replicator/Makefile.in897
-rw-r--r--src/replication/replicator/doveadm-connection.c354
-rw-r--r--src/replication/replicator/doveadm-connection.h11
-rw-r--r--src/replication/replicator/dsync-client.c274
-rw-r--r--src/replication/replicator/dsync-client.h37
-rw-r--r--src/replication/replicator/notify-connection.c206
-rw-r--r--src/replication/replicator/notify-connection.h13
-rw-r--r--src/replication/replicator/replicator-brain.c202
-rw-r--r--src/replication/replicator/replicator-brain.h20
-rw-r--r--src/replication/replicator/replicator-queue-auth.c37
-rw-r--r--src/replication/replicator/replicator-queue.c527
-rw-r--r--src/replication/replicator/replicator-queue.h104
-rw-r--r--src/replication/replicator/replicator-settings.c86
-rw-r--r--src/replication/replicator/replicator-settings.h16
-rw-r--r--src/replication/replicator/replicator.c117
-rw-r--r--src/replication/replicator/test-replicator-queue.c260
-rw-r--r--src/stats/Makefile.am102
-rw-r--r--src/stats/Makefile.in1024
-rw-r--r--src/stats/client-http.c233
-rw-r--r--src/stats/client-http.h28
-rw-r--r--src/stats/client-reader.c253
-rw-r--r--src/stats/client-reader.h11
-rw-r--r--src/stats/client-writer.c373
-rw-r--r--src/stats/client-writer.h13
-rw-r--r--src/stats/event-exporter-fmt-json.c249
-rw-r--r--src/stats/event-exporter-fmt-none.c12
-rw-r--r--src/stats/event-exporter-fmt-tab-text.c212
-rw-r--r--src/stats/event-exporter-fmt.c78
-rw-r--r--src/stats/event-exporter-transport-drop.c9
-rw-r--r--src/stats/event-exporter-transport-http-post.c76
-rw-r--r--src/stats/event-exporter-transport-log.c12
-rw-r--r--src/stats/event-exporter.h31
-rw-r--r--src/stats/main.c117
-rw-r--r--src/stats/stats-common.h10
-rw-r--r--src/stats/stats-event-category.c32
-rw-r--r--src/stats/stats-event-category.h12
-rw-r--r--src/stats/stats-metrics.c765
-rw-r--r--src/stats/stats-metrics.h134
-rw-r--r--src/stats/stats-service-openmetrics.c826
-rw-r--r--src/stats/stats-service-private.h8
-rw-r--r--src/stats/stats-service.c15
-rw-r--r--src/stats/stats-service.h7
-rw-r--r--src/stats/stats-settings.c538
-rw-r--r--src/stats/stats-settings.h125
-rw-r--r--src/stats/test-client-reader.c235
-rw-r--r--src/stats/test-client-writer.c152
-rw-r--r--src/stats/test-stats-common.c99
-rw-r--r--src/stats/test-stats-common.h36
-rw-r--r--src/stats/test-stats-metrics.c449
-rw-r--r--src/submission-login/Makefile.am32
-rw-r--r--src/submission-login/Makefile.in830
-rw-r--r--src/submission-login/client-authenticate.c360
-rw-r--r--src/submission-login/client-authenticate.h21
-rw-r--r--src/submission-login/client.c329
-rw-r--r--src/submission-login/client.h38
-rw-r--r--src/submission-login/submission-login-settings.c166
-rw-r--r--src/submission-login/submission-login-settings.h24
-rw-r--r--src/submission-login/submission-proxy.c709
-rw-r--r--src/submission-login/submission-proxy.h12
-rw-r--r--src/submission/Makefile.am55
-rw-r--r--src/submission/Makefile.in918
-rw-r--r--src/submission/main.c436
-rw-r--r--src/submission/submission-backend-relay.c1260
-rw-r--r--src/submission/submission-backend-relay.h64
-rw-r--r--src/submission/submission-backend.c448
-rw-r--r--src/submission/submission-backend.h170
-rw-r--r--src/submission/submission-client.c555
-rw-r--r--src/submission/submission-client.h161
-rw-r--r--src/submission/submission-commands.c615
-rw-r--r--src/submission/submission-commands.h98
-rw-r--r--src/submission/submission-common.h46
-rw-r--r--src/submission/submission-recipient.c55
-rw-r--r--src/submission/submission-recipient.h33
-rw-r--r--src/submission/submission-settings.c225
-rw-r--r--src/submission/submission-settings.h60
-rw-r--r--src/util/Makefile.am90
-rw-r--r--src/util/Makefile.in1049
-rwxr-xr-xsrc/util/dovecot-sysreport216
-rw-r--r--src/util/gdbhelper.c71
-rw-r--r--src/util/health-check-settings.c31
-rwxr-xr-xsrc/util/health-check.sh32
-rw-r--r--src/util/maildirlock.c106
-rw-r--r--src/util/rawlog.c428
-rw-r--r--src/util/script-login.c249
-rw-r--r--src/util/script.c321
-rw-r--r--src/util/tcpwrap-settings.c35
-rw-r--r--src/util/tcpwrap.c131
-rw-r--r--src/util/test-fs.c449
2425 files changed, 773251 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..aa0af22
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,72 @@
+if HAVE_LDAP
+LIB_LDAP=lib-ldap
+endif
+if HAVE_LUA
+LIB_LUA=lib-lua
+endif
+
+LIBDOVECOT_SUBDIRS = \
+ lib-test \
+ lib \
+ lib-settings \
+ lib-auth \
+ lib-dns \
+ lib-master \
+ lib-charset \
+ lib-ssl-iostream \
+ lib-dcrypt \
+ lib-dict \
+ lib-sasl \
+ lib-old-stats \
+ lib-http \
+ lib-fs \
+ lib-mail \
+ lib-program-client \
+ lib-smtp \
+ lib-imap \
+ lib-imap-storage \
+ lib-oauth2
+
+SUBDIRS = \
+ $(LIBDOVECOT_SUBDIRS) \
+ lib-dict-extra \
+ lib-dovecot \
+ $(LIB_LDAP) \
+ $(LIB_LUA) \
+ lib-fts \
+ lib-imap-client \
+ lib-imap-urlauth \
+ lib-compression \
+ lib-index \
+ lib-storage \
+ lib-sql \
+ lib-otp \
+ lib-lda \
+ lib-dict-backend \
+ anvil \
+ auth \
+ dict \
+ dns \
+ indexer \
+ ipc \
+ master \
+ login-common \
+ imap-hibernate \
+ imap-login \
+ imap \
+ imap-urlauth \
+ pop3-login \
+ pop3 \
+ submission-login \
+ submission \
+ lda \
+ lmtp \
+ log \
+ config \
+ director \
+ replication \
+ util \
+ doveadm \
+ stats \
+ old-stats \
+ plugins
diff --git a/src/Makefile.in b/src/Makefile.in
new file mode 100644
index 0000000..78edff8
--- /dev/null
+++ b/src/Makefile.in
@@ -0,0 +1,844 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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
+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_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 =
+SOURCES =
+DIST_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
+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 = lib-test lib lib-settings lib-auth lib-dns lib-master \
+ lib-charset lib-ssl-iostream lib-dcrypt lib-dict lib-sasl \
+ lib-old-stats lib-http lib-fs lib-mail lib-program-client \
+ lib-smtp lib-imap lib-imap-storage lib-oauth2 lib-dict-extra \
+ lib-dovecot lib-ldap lib-lua lib-fts lib-imap-client \
+ lib-imap-urlauth lib-compression lib-index lib-storage lib-sql \
+ lib-otp lib-lda lib-dict-backend anvil auth dict dns indexer \
+ ipc master login-common imap-hibernate imap-login imap \
+ imap-urlauth pop3-login pop3 submission-login submission lda \
+ lmtp log config director replication util doveadm stats \
+ old-stats plugins
+am__DIST_COMMON = $(srcdir)/Makefile.in
+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@
+@HAVE_LDAP_TRUE@LIB_LDAP = lib-ldap
+@HAVE_LUA_TRUE@LIB_LUA = lib-lua
+LIBDOVECOT_SUBDIRS = \
+ lib-test \
+ lib \
+ lib-settings \
+ lib-auth \
+ lib-dns \
+ lib-master \
+ lib-charset \
+ lib-ssl-iostream \
+ lib-dcrypt \
+ lib-dict \
+ lib-sasl \
+ lib-old-stats \
+ lib-http \
+ lib-fs \
+ lib-mail \
+ lib-program-client \
+ lib-smtp \
+ lib-imap \
+ lib-imap-storage \
+ lib-oauth2
+
+SUBDIRS = \
+ $(LIBDOVECOT_SUBDIRS) \
+ lib-dict-extra \
+ lib-dovecot \
+ $(LIB_LDAP) \
+ $(LIB_LUA) \
+ lib-fts \
+ lib-imap-client \
+ lib-imap-urlauth \
+ lib-compression \
+ lib-index \
+ lib-storage \
+ lib-sql \
+ lib-otp \
+ lib-lda \
+ lib-dict-backend \
+ anvil \
+ auth \
+ dict \
+ dns \
+ indexer \
+ ipc \
+ master \
+ login-common \
+ imap-hibernate \
+ imap-login \
+ imap \
+ imap-urlauth \
+ pop3-login \
+ pop3 \
+ submission-login \
+ submission \
+ lda \
+ lmtp \
+ log \
+ config \
+ director \
+ replication \
+ util \
+ doveadm \
+ stats \
+ old-stats \
+ plugins
+
+all: all-recursive
+
+.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/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+# 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
+installdirs: installdirs-recursive
+installdirs-am:
+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 mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f Makefile
+distclean-am: clean-am distclean-generic distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+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 Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \
+ check-am clean clean-generic clean-libtool cscopelist-am ctags \
+ ctags-am distclean 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 \
+ installdirs-am maintainer-clean maintainer-clean-generic \
+ mostlyclean 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/anvil/Makefile.am b/src/anvil/Makefile.am
new file mode 100644
index 0000000..1af526f
--- /dev/null
+++ b/src/anvil/Makefile.am
@@ -0,0 +1,48 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = anvil
+
+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 \
+ $(BINARY_CFLAGS)
+
+anvil_LDADD = \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS)
+
+anvil_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+anvil_SOURCES = \
+ main.c \
+ anvil-connection.c \
+ anvil-settings.c \
+ connect-limit.c \
+ penalty.c
+
+noinst_HEADERS = \
+ anvil-connection.h \
+ common.h \
+ connect-limit.h \
+ penalty.h
+
+test_programs = \
+ test-penalty
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_penalty_SOURCES = test-penalty.c
+test_penalty_LDADD = penalty.o $(test_libs)
+test_penalty_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/anvil/Makefile.in b/src/anvil/Makefile.in
new file mode 100644
index 0000000..c919037
--- /dev/null
+++ b/src/anvil/Makefile.in
@@ -0,0 +1,867 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = anvil$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/anvil
+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-penalty$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am_anvil_OBJECTS = main.$(OBJEXT) anvil-connection.$(OBJEXT) \
+ anvil-settings.$(OBJEXT) connect-limit.$(OBJEXT) \
+ penalty.$(OBJEXT)
+anvil_OBJECTS = $(am_anvil_OBJECTS)
+am__DEPENDENCIES_1 =
+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_penalty_OBJECTS = test-penalty.$(OBJEXT)
+test_penalty_OBJECTS = $(am_test_penalty_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-connection.Po \
+ ./$(DEPDIR)/anvil-settings.Po ./$(DEPDIR)/connect-limit.Po \
+ ./$(DEPDIR)/main.Po ./$(DEPDIR)/penalty.Po \
+ ./$(DEPDIR)/test-penalty.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 = $(anvil_SOURCES) $(test_penalty_SOURCES)
+DIST_SOURCES = $(anvil_SOURCES) $(test_penalty_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+anvil_LDADD = \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS)
+
+anvil_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+anvil_SOURCES = \
+ main.c \
+ anvil-connection.c \
+ anvil-settings.c \
+ connect-limit.c \
+ penalty.c
+
+noinst_HEADERS = \
+ anvil-connection.h \
+ common.h \
+ connect-limit.h \
+ penalty.h
+
+test_programs = \
+ test-penalty
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_penalty_SOURCES = test-penalty.c
+test_penalty_LDADD = penalty.o $(test_libs)
+test_penalty_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(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/anvil/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/anvil/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+anvil$(EXEEXT): $(anvil_OBJECTS) $(anvil_DEPENDENCIES) $(EXTRA_anvil_DEPENDENCIES)
+ @rm -f anvil$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(anvil_OBJECTS) $(anvil_LDADD) $(LIBS)
+
+test-penalty$(EXEEXT): $(test_penalty_OBJECTS) $(test_penalty_DEPENDENCIES) $(EXTRA_test_penalty_DEPENDENCIES)
+ @rm -f test-penalty$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_penalty_OBJECTS) $(test_penalty_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/anvil-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/anvil-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/connect-limit.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/penalty.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-penalty.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/anvil-connection.Po
+ -rm -f ./$(DEPDIR)/anvil-settings.Po
+ -rm -f ./$(DEPDIR)/connect-limit.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/penalty.Po
+ -rm -f ./$(DEPDIR)/test-penalty.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-pkglibexecPROGRAMS
+
+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-connection.Po
+ -rm -f ./$(DEPDIR)/anvil-settings.Po
+ -rm -f ./$(DEPDIR)/connect-limit.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/penalty.Po
+ -rm -f ./$(DEPDIR)/test-penalty.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-pkglibexecPROGRAMS
+
+.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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/anvil/anvil-connection.c b/src/anvil/anvil-connection.c
new file mode 100644
index 0000000..20e859b
--- /dev/null
+++ b/src/anvil/anvil-connection.c
@@ -0,0 +1,226 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "master-interface.h"
+#include "connect-limit.h"
+#include "penalty.h"
+#include "anvil-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE 1024
+
+#define ANVIL_CLIENT_PROTOCOL_MAJOR_VERSION 1
+#define ANVIL_CLIENT_PROTOCOL_MINOR_VERSION 0
+
+struct anvil_connection {
+ struct anvil_connection *prev, *next;
+
+ int fd;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+
+ bool version_received:1;
+ bool handshaked:1;
+ bool master:1;
+ bool fifo:1;
+};
+
+static struct anvil_connection *anvil_connections = NULL;
+
+static const char *const *
+anvil_connection_next_line(struct anvil_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_next_line(conn->input);
+ return line == NULL ? NULL : t_strsplit_tabescaped(line);
+}
+
+static int
+anvil_connection_request(struct anvil_connection *conn,
+ const char *const *args, const char **error_r)
+{
+ const char *cmd = args[0];
+ unsigned int value, checksum;
+ time_t stamp;
+ pid_t pid;
+
+ args++;
+ if (strcmp(cmd, "CONNECT") == 0) {
+ if (args[0] == NULL || args[1] == NULL) {
+ *error_r = "CONNECT: Not enough parameters";
+ return -1;
+ }
+ if (str_to_pid(args[0], &pid) < 0) {
+ *error_r = "CONNECT: Invalid pid";
+ return -1;
+ }
+ connect_limit_connect(connect_limit, pid, args[1]);
+ } else if (strcmp(cmd, "DISCONNECT") == 0) {
+ if (args[0] == NULL || args[1] == NULL) {
+ *error_r = "DISCONNECT: Not enough parameters";
+ return -1;
+ }
+ if (str_to_pid(args[0], &pid) < 0) {
+ *error_r = "DISCONNECT: Invalid pid";
+ return -1;
+ }
+ connect_limit_disconnect(connect_limit, pid, args[1]);
+ } else if (strcmp(cmd, "CONNECT-DUMP") == 0) {
+ connect_limit_dump(connect_limit, conn->output);
+ } else if (strcmp(cmd, "KILL") == 0) {
+ if (args[0] == NULL) {
+ *error_r = "KILL: Not enough parameters";
+ return -1;
+ }
+ if (!conn->master) {
+ *error_r = "KILL sent by a non-master connection";
+ return -1;
+ }
+ if (str_to_pid(args[0], &pid) < 0) {
+ *error_r = "KILL: Invalid pid";
+ return -1;
+ }
+ connect_limit_disconnect_pid(connect_limit, pid);
+ } else if (strcmp(cmd, "LOOKUP") == 0) {
+ if (args[0] == NULL) {
+ *error_r = "LOOKUP: Not enough parameters";
+ return -1;
+ }
+ if (conn->output == NULL) {
+ *error_r = "LOOKUP on a FIFO, can't send reply";
+ return -1;
+ }
+ value = connect_limit_lookup(connect_limit, args[0]);
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("%u\n", value));
+ } else if (strcmp(cmd, "PENALTY-GET") == 0) {
+ if (args[0] == NULL) {
+ *error_r = "PENALTY-GET: Not enough parameters";
+ return -1;
+ }
+ value = penalty_get(penalty, args[0], &stamp);
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("%u %s\n", value, dec2str(stamp)));
+ } else if (strcmp(cmd, "PENALTY-INC") == 0) {
+ if (args[0] == NULL || args[1] == NULL || args[2] == NULL) {
+ *error_r = "PENALTY-INC: Not enough parameters";
+ return -1;
+ }
+ if (str_to_uint(args[1], &checksum) < 0 ||
+ str_to_uint(args[2], &value) < 0 ||
+ value > PENALTY_MAX_VALUE ||
+ (value == 0 && checksum != 0)) {
+ *error_r = "PENALTY-INC: Invalid parameters";
+ return -1;
+ }
+ penalty_inc(penalty, args[0], checksum, value);
+ } else if (strcmp(cmd, "PENALTY-SET-EXPIRE-SECS") == 0) {
+ if (args[0] == NULL || str_to_uint(args[0], &value) < 0) {
+ *error_r = "PENALTY-SET-EXPIRE-SECS: "
+ "Invalid parameters";
+ return -1;
+ }
+ penalty_set_expire_secs(penalty, value);
+ } else if (strcmp(cmd, "PENALTY-DUMP") == 0) {
+ penalty_dump(penalty, conn->output);
+ } else {
+ *error_r = t_strconcat("Unknown command: ", cmd, NULL);
+ return -1;
+ }
+ return 0;
+}
+
+static void anvil_connection_input(struct anvil_connection *conn)
+{
+ const char *line, *const *args, *error;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ i_error("BUG: Anvil client connection sent too much data");
+ anvil_connection_destroy(conn);
+ return;
+ case -1:
+ anvil_connection_destroy(conn);
+ return;
+ }
+
+ if (!conn->version_received) {
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "anvil",
+ ANVIL_CLIENT_PROTOCOL_MAJOR_VERSION)) {
+ if (anvil_restarted && (conn->master || conn->fifo)) {
+ /* old pending data. ignore input until we get
+ the handshake. */
+ anvil_connection_input(conn);
+ return;
+ }
+ i_error("Anvil client not compatible with this server "
+ "(mixed old and new binaries?) %s", line);
+ anvil_connection_destroy(conn);
+ return;
+ }
+ conn->version_received = TRUE;
+ }
+
+ while ((args = anvil_connection_next_line(conn)) != NULL) {
+ if (args[0] != NULL) {
+ if (anvil_connection_request(conn, args, &error) < 0) {
+ i_error("Anvil client input error: %s", error);
+ anvil_connection_destroy(conn);
+ break;
+ }
+ }
+ }
+}
+
+struct anvil_connection *
+anvil_connection_create(int fd, bool master, bool fifo)
+{
+ struct anvil_connection *conn;
+
+ conn = i_new(struct anvil_connection, 1);
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ if (!fifo) {
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ }
+ conn->io = io_add(fd, IO_READ, anvil_connection_input, conn);
+ conn->master = master;
+ conn->fifo = fifo;
+ DLLIST_PREPEND(&anvil_connections, conn);
+ return conn;
+}
+
+void anvil_connection_destroy(struct anvil_connection *conn)
+{
+ bool fifo = conn->fifo;
+
+ DLLIST_REMOVE(&anvil_connections, conn);
+
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(anvil conn) failed: %m");
+ i_free(conn);
+
+ if (!fifo)
+ master_service_client_connection_destroyed(master_service);
+}
+
+void anvil_connections_destroy_all(void)
+{
+ while (anvil_connections != NULL)
+ anvil_connection_destroy(anvil_connections);
+}
diff --git a/src/anvil/anvil-connection.h b/src/anvil/anvil-connection.h
new file mode 100644
index 0000000..f94fbab
--- /dev/null
+++ b/src/anvil/anvil-connection.h
@@ -0,0 +1,10 @@
+#ifndef ANVIL_CONNECTION_H
+#define ANVIL_CONNECTION_H
+
+struct anvil_connection *
+anvil_connection_create(int fd, bool master, bool fifo);
+void anvil_connection_destroy(struct anvil_connection *conn);
+
+void anvil_connections_destroy_all(void);
+
+#endif
diff --git a/src/anvil/anvil-settings.c b/src/anvil/anvil-settings.c
new file mode 100644
index 0000000..a94823e
--- /dev/null
+++ b/src/anvil/anvil-settings.c
@@ -0,0 +1,50 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings anvil_unix_listeners_array[] = {
+ { "anvil", 0600, "", "" },
+ { "anvil-auth-penalty", 0600, "", "" }
+};
+static struct file_listener_settings *anvil_unix_listeners[] = {
+ &anvil_unix_listeners_array[0],
+ &anvil_unix_listeners_array[1]
+};
+static buffer_t anvil_unix_listeners_buf = {
+ { { anvil_unix_listeners, sizeof(anvil_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings anvil_service_settings = {
+ .name = "anvil",
+ .protocol = "",
+ .type = "anvil",
+ .executable = "anvil",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 1,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &anvil_unix_listeners_buf,
+ sizeof(anvil_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
diff --git a/src/anvil/common.h b/src/anvil/common.h
new file mode 100644
index 0000000..f9a44bd
--- /dev/null
+++ b/src/anvil/common.h
@@ -0,0 +1,10 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+#include "lib.h"
+
+extern struct connect_limit *connect_limit;
+extern struct penalty *penalty;
+extern bool anvil_restarted;
+
+#endif
diff --git a/src/anvil/connect-limit.c b/src/anvil/connect-limit.c
new file mode 100644
index 0000000..0d27368
--- /dev/null
+++ b/src/anvil/connect-limit.c
@@ -0,0 +1,192 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "connect-limit.h"
+
+struct ident_pid {
+ /* ident string points to ident_hash keys */
+ const char *ident;
+ pid_t pid;
+ unsigned int refcount;
+};
+
+struct connect_limit {
+ /* ident => unsigned int refcount */
+ HASH_TABLE(char *, void *) ident_hash;
+ /* struct ident_pid => struct ident_pid */
+ HASH_TABLE(struct ident_pid *, struct ident_pid *) ident_pid_hash;
+};
+
+static void
+connect_limit_ident_hash_unref(struct connect_limit *limit, const char *ident);
+
+static unsigned int ident_pid_hash(const struct ident_pid *i)
+{
+ return str_hash(i->ident) ^ i->pid;
+}
+
+static int ident_pid_cmp(const struct ident_pid *i1, const struct ident_pid *i2)
+{
+ if (i1->pid < i2->pid)
+ return -1;
+ else if (i1->pid > i2->pid)
+ return 1;
+ else
+ return strcmp(i1->ident, i2->ident);
+}
+
+struct connect_limit *connect_limit_init(void)
+{
+ struct connect_limit *limit;
+
+ limit = i_new(struct connect_limit, 1);
+ hash_table_create(&limit->ident_hash, default_pool, 0, str_hash, strcmp);
+ hash_table_create(&limit->ident_pid_hash, default_pool, 0,
+ ident_pid_hash, ident_pid_cmp);
+ return limit;
+}
+
+void connect_limit_deinit(struct connect_limit **_limit)
+{
+ struct connect_limit *limit = *_limit;
+ struct hash_iterate_context *iter;
+ struct ident_pid *i, *value;
+
+ iter = hash_table_iterate_init(limit->ident_pid_hash);
+ while (hash_table_iterate(iter, limit->ident_pid_hash, &i, &value)) {
+ hash_table_remove(limit->ident_pid_hash, i);
+ for (; i->refcount > 0; i->refcount--)
+ connect_limit_ident_hash_unref(limit, i->ident);
+ i_free(i);
+ }
+ hash_table_iterate_deinit(&iter);
+
+ *_limit = NULL;
+ hash_table_destroy(&limit->ident_hash);
+ hash_table_destroy(&limit->ident_pid_hash);
+ i_free(limit);
+}
+
+unsigned int connect_limit_lookup(struct connect_limit *limit,
+ const char *ident)
+{
+ void *value;
+
+ value = hash_table_lookup(limit->ident_hash, ident);
+ return POINTER_CAST_TO(value, unsigned int);
+}
+
+void connect_limit_connect(struct connect_limit *limit, pid_t pid,
+ const char *ident)
+{
+ struct ident_pid *i, lookup_i;
+ char *key;
+ void *value;
+
+ if (!hash_table_lookup_full(limit->ident_hash, ident,
+ &key, &value)) {
+ key = i_strdup(ident);
+ value = POINTER_CAST(1);
+ hash_table_insert(limit->ident_hash, key, value);
+ } else {
+ value = POINTER_CAST(POINTER_CAST_TO(value, unsigned int) + 1);
+ hash_table_update(limit->ident_hash, key, value);
+ }
+
+ lookup_i.ident = ident;
+ lookup_i.pid = pid;
+ i = hash_table_lookup(limit->ident_pid_hash, &lookup_i);
+ if (i == NULL) {
+ i = i_new(struct ident_pid, 1);
+ i->ident = key;
+ i->pid = pid;
+ i->refcount = 1;
+ hash_table_insert(limit->ident_pid_hash, i, i);
+ } else {
+ i->refcount++;
+ }
+}
+
+static void
+connect_limit_ident_hash_unref(struct connect_limit *limit, const char *ident)
+{
+ char *key;
+ void *value;
+ unsigned int new_refcount;
+
+ if (!hash_table_lookup_full(limit->ident_hash, ident, &key, &value))
+ i_panic("connect limit hash tables are inconsistent");
+
+ new_refcount = POINTER_CAST_TO(value, unsigned int) - 1;
+ if (new_refcount > 0) {
+ value = POINTER_CAST(new_refcount);
+ hash_table_update(limit->ident_hash, key, value);
+ } else {
+ hash_table_remove(limit->ident_hash, key);
+ i_free(key);
+ }
+}
+
+void connect_limit_disconnect(struct connect_limit *limit, pid_t pid,
+ const char *ident)
+{
+ struct ident_pid *i, lookup_i;
+
+ lookup_i.ident = ident;
+ lookup_i.pid = pid;
+
+ i = hash_table_lookup(limit->ident_pid_hash, &lookup_i);
+ if (i == NULL) {
+ i_error("connect limit: disconnection for unknown "
+ "pid %s + ident %s", dec2str(pid), ident);
+ return;
+ }
+
+ if (--i->refcount == 0) {
+ hash_table_remove(limit->ident_pid_hash, i);
+ i_free(i);
+ }
+
+ connect_limit_ident_hash_unref(limit, ident);
+}
+
+void connect_limit_disconnect_pid(struct connect_limit *limit, pid_t pid)
+{
+ struct hash_iterate_context *iter;
+ struct ident_pid *i, *value;
+
+ /* this should happen rarely (or never), so this slow implementation
+ should be fine. */
+ iter = hash_table_iterate_init(limit->ident_pid_hash);
+ while (hash_table_iterate(iter, limit->ident_pid_hash, &i, &value)) {
+ if (i->pid == pid) {
+ hash_table_remove(limit->ident_pid_hash, i);
+ for (; i->refcount > 0; i->refcount--)
+ connect_limit_ident_hash_unref(limit, i->ident);
+ i_free(i);
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+void connect_limit_dump(struct connect_limit *limit, struct ostream *output)
+{
+ struct hash_iterate_context *iter;
+ struct ident_pid *i, *value;
+ string_t *str = t_str_new(256);
+
+ iter = hash_table_iterate_init(limit->ident_pid_hash);
+ while (hash_table_iterate(iter, limit->ident_pid_hash, &i, &value)) {
+ str_truncate(str, 0);
+ str_append_tabescaped(str, i->ident);
+ str_printfa(str, "\t%ld\t%u\n", (long)i->pid, i->refcount);
+ if (o_stream_send(output, str_data(str), str_len(str)) < 0)
+ break;
+ }
+ hash_table_iterate_deinit(&iter);
+ o_stream_nsend(output, "\n", 1);
+}
diff --git a/src/anvil/connect-limit.h b/src/anvil/connect-limit.h
new file mode 100644
index 0000000..2d3c611
--- /dev/null
+++ b/src/anvil/connect-limit.h
@@ -0,0 +1,16 @@
+#ifndef CONNECT_LIMIT_H
+#define CONNECT_LIMIT_H
+
+struct connect_limit *connect_limit_init(void);
+void connect_limit_deinit(struct connect_limit **limit);
+
+unsigned int connect_limit_lookup(struct connect_limit *limit,
+ const char *ident);
+void connect_limit_connect(struct connect_limit *limit, pid_t pid,
+ const char *ident);
+void connect_limit_disconnect(struct connect_limit *limit, pid_t pid,
+ const char *ident);
+void connect_limit_disconnect_pid(struct connect_limit *limit, pid_t pid);
+void connect_limit_dump(struct connect_limit *limit, struct ostream *output);
+
+#endif
diff --git a/src/anvil/main.c b/src/anvil/main.c
new file mode 100644
index 0000000..7e4050b
--- /dev/null
+++ b/src/anvil/main.c
@@ -0,0 +1,90 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "env-util.h"
+#include "fdpass.h"
+#include "ioloop.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-interface.h"
+#include "connect-limit.h"
+#include "penalty.h"
+#include "anvil-connection.h"
+
+#include <unistd.h>
+
+struct connect_limit *connect_limit;
+struct penalty *penalty;
+bool anvil_restarted;
+static struct io *log_fdpass_io;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ bool master = conn->listen_fd == MASTER_LISTEN_FD_FIRST;
+
+ master_service_client_connection_accept(conn);
+ (void)anvil_connection_create(conn->fd, master, conn->fifo);
+}
+
+static void ATTR_NULL(1)
+log_fdpass_input(void *context ATTR_UNUSED)
+{
+ int fd;
+ char c;
+ ssize_t ret;
+
+ /* master wants us to replace the log fd */
+ ret = fd_read(MASTER_ANVIL_LOG_FDPASS_FD, &c, 1, &fd);
+ if (ret < 0)
+ i_error("fd_read(log fd) failed: %m");
+ else if (ret == 0) {
+ /* master died. lib-master should notice it soon. */
+ io_remove(&log_fdpass_io);
+ } else {
+ if (dup2(fd, STDERR_FILENO) < 0)
+ i_fatal("dup2(fd_read log fd, stderr) failed: %m");
+ if (close(fd) < 0)
+ i_error("close(fd_read log fd) failed: %m");
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+ const char *error;
+
+ master_service = master_service_init("anvil", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ if (master_service_settings_read_simple(master_service,
+ NULL, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+ anvil_restarted = getenv("ANVIL_RESTARTED") != NULL;
+
+ /* delay dying until all of our clients are gone */
+ master_service_set_die_with_master(master_service, FALSE);
+
+ connect_limit = connect_limit_init();
+ penalty = penalty_init();
+ log_fdpass_io = io_add(MASTER_ANVIL_LOG_FDPASS_FD, IO_READ,
+ log_fdpass_input, NULL);
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ io_remove(&log_fdpass_io);
+ penalty_deinit(&penalty);
+ connect_limit_deinit(&connect_limit);
+ anvil_connections_destroy_all();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/anvil/penalty.c b/src/anvil/penalty.c
new file mode 100644
index 0000000..2ab6da1
--- /dev/null
+++ b/src/anvil/penalty.c
@@ -0,0 +1,273 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+/* The idea behind checksums is that the same username+password doesn't
+ increase the penalty, because it's most likely a user with a misconfigured
+ account. */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "llist.h"
+#include "ostream.h"
+#include "penalty.h"
+
+#include <time.h>
+
+#define PENALTY_DEFAULT_EXPIRE_SECS (60*60)
+#define PENALTY_CHECKSUM_SAVE_COUNT
+#define CHECKSUM_VALUE_COUNT 2
+#define CHECKSUM_VALUE_PTR_COUNT 10
+
+#define LAST_UPDATE_BITS 15
+
+struct penalty_rec {
+ /* ordered by last_update */
+ struct penalty_rec *prev, *next;
+
+ char *ident;
+ unsigned int last_penalty;
+
+ unsigned int penalty:16;
+ unsigned int last_update:LAST_UPDATE_BITS; /* last_penalty + n */
+ bool checksum_is_pointer:1;
+ /* we use value up to two different checksums.
+ after that switch to pointer. */
+ union {
+ unsigned int value[CHECKSUM_VALUE_COUNT];
+ unsigned int *value_ptr;
+ } checksum;
+};
+
+struct penalty {
+ /* ident => penalty_rec */
+ HASH_TABLE(char *, struct penalty_rec *) hash;
+ struct penalty_rec *oldest, *newest;
+
+ unsigned int expire_secs;
+ struct timeout *to;
+};
+
+struct penalty *penalty_init(void)
+{
+ struct penalty *penalty;
+
+ penalty = i_new(struct penalty, 1);
+ hash_table_create(&penalty->hash, default_pool, 0, str_hash, strcmp);
+ penalty->expire_secs = PENALTY_DEFAULT_EXPIRE_SECS;
+ return penalty;
+}
+
+static void penalty_rec_free(struct penalty *penalty, struct penalty_rec *rec)
+{
+ DLLIST2_REMOVE(&penalty->oldest, &penalty->newest, rec);
+ if (rec->checksum_is_pointer)
+ i_free(rec->checksum.value_ptr);
+ i_free(rec->ident);
+ i_free(rec);
+}
+
+void penalty_deinit(struct penalty **_penalty)
+{
+ struct penalty *penalty = *_penalty;
+
+ *_penalty = NULL;
+
+ while (penalty->oldest != NULL)
+ penalty_rec_free(penalty, penalty->oldest);
+ hash_table_destroy(&penalty->hash);
+
+ timeout_remove(&penalty->to);
+ i_free(penalty);
+}
+
+void penalty_set_expire_secs(struct penalty *penalty, unsigned int expire_secs)
+{
+ penalty->expire_secs = expire_secs;
+}
+
+static bool
+penalty_bump_checksum(struct penalty_rec *rec, unsigned int checksum)
+{
+ unsigned int *checksums;
+ unsigned int i, count;
+
+ if (!rec->checksum_is_pointer) {
+ checksums = rec->checksum.value;
+ count = CHECKSUM_VALUE_COUNT;
+ } else {
+ checksums = rec->checksum.value_ptr;
+ count = CHECKSUM_VALUE_PTR_COUNT;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (checksums[i] == checksum) {
+ if (i > 0) {
+ memmove(checksums + 1, checksums,
+ sizeof(checksums[0]) * i);
+ checksums[0] = checksum;
+ }
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void penalty_add_checksum(struct penalty_rec *rec, unsigned int checksum)
+{
+ unsigned int *checksums;
+
+ i_assert(checksum != 0);
+
+ if (!rec->checksum_is_pointer) {
+ if (rec->checksum.value[CHECKSUM_VALUE_COUNT-1] == 0) {
+ memcpy(rec->checksum.value + 1, rec->checksum.value,
+ sizeof(rec->checksum.value[0]) *
+ (CHECKSUM_VALUE_COUNT-1));
+ rec->checksum.value[0] = checksum;
+ return;
+ }
+
+ /* switch to using a pointer */
+ checksums = i_new(unsigned int, CHECKSUM_VALUE_PTR_COUNT);
+ memcpy(checksums, rec->checksum.value,
+ sizeof(checksums[0]) * CHECKSUM_VALUE_COUNT);
+ rec->checksum.value_ptr = checksums;
+ rec->checksum_is_pointer = TRUE;
+ }
+
+ memmove(rec->checksum.value_ptr + 1, rec->checksum.value_ptr,
+ sizeof(rec->checksum.value_ptr[0]) *
+ (CHECKSUM_VALUE_PTR_COUNT-1));
+ rec->checksum.value_ptr[0] = checksum;
+}
+
+unsigned int penalty_get(struct penalty *penalty, const char *ident,
+ time_t *last_penalty_r)
+{
+ struct penalty_rec *rec;
+
+ rec = hash_table_lookup(penalty->hash, ident);
+ if (rec == NULL) {
+ *last_penalty_r = 0;
+ return 0;
+ }
+
+ *last_penalty_r = rec->last_penalty;
+ return rec->penalty;
+}
+
+static void penalty_timeout(struct penalty *penalty)
+{
+ struct penalty_rec *rec;
+ time_t rec_last_update, expire_time;
+ unsigned int diff;
+
+ timeout_remove(&penalty->to);
+
+ expire_time = ioloop_time - penalty->expire_secs;
+ while (penalty->oldest != NULL) {
+ rec = penalty->oldest;
+
+ rec_last_update = rec->last_penalty + rec->last_update;
+ if (rec_last_update > expire_time) {
+ diff = rec_last_update - expire_time;
+ penalty->to = timeout_add(diff * 1000,
+ penalty_timeout, penalty);
+ break;
+ }
+ hash_table_remove(penalty->hash, rec->ident);
+ penalty_rec_free(penalty, rec);
+ }
+}
+
+void penalty_inc(struct penalty *penalty, const char *ident,
+ unsigned int checksum, unsigned int value)
+{
+ struct penalty_rec *rec;
+ time_t diff;
+
+ i_assert(value > 0 || checksum == 0);
+ i_assert(value <= INT_MAX);
+
+ rec = hash_table_lookup(penalty->hash, ident);
+ if (rec == NULL) {
+ rec = i_new(struct penalty_rec, 1);
+ rec->ident = i_strdup(ident);
+ hash_table_insert(penalty->hash, rec->ident, rec);
+ } else {
+ DLLIST2_REMOVE(&penalty->oldest, &penalty->newest, rec);
+ }
+
+ if (checksum == 0) {
+ rec->penalty = value;
+ rec->last_penalty = ioloop_time;
+ } else {
+ if (penalty_bump_checksum(rec, checksum))
+ rec->penalty = value - 1;
+ else {
+ penalty_add_checksum(rec, checksum);
+ rec->penalty = value;
+ rec->last_penalty = ioloop_time;
+ }
+ }
+
+ diff = ioloop_time - rec->last_penalty;
+ if (diff >= (1 << LAST_UPDATE_BITS)) {
+ rec->last_update = (1 << LAST_UPDATE_BITS) - 1;
+ rec->last_penalty = ioloop_time - rec->last_update;
+ } else {
+ rec->last_update = diff;
+ }
+
+ DLLIST2_APPEND(&penalty->oldest, &penalty->newest, rec);
+
+ if (penalty->to == NULL) {
+ penalty->to = timeout_add(penalty->expire_secs * 1000,
+ penalty_timeout, penalty);
+ }
+}
+
+bool penalty_has_checksum(struct penalty *penalty, const char *ident,
+ unsigned int checksum)
+{
+ struct penalty_rec *rec;
+ const unsigned int *checksums;
+ unsigned int i, count;
+
+ rec = hash_table_lookup(penalty->hash, ident);
+ if (rec == NULL)
+ return FALSE;
+
+ if (!rec->checksum_is_pointer) {
+ checksums = rec->checksum.value;
+ count = CHECKSUM_VALUE_COUNT;
+ } else {
+ checksums = rec->checksum.value_ptr;
+ count = CHECKSUM_VALUE_PTR_COUNT;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (checksums[i] == checksum)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void penalty_dump(struct penalty *penalty, struct ostream *output)
+{
+ const struct penalty_rec *rec;
+ string_t *str = t_str_new(256);
+
+ for (rec = penalty->oldest; rec != NULL; rec = rec->next) {
+ str_truncate(str, 0);
+ str_append_tabescaped(str, rec->ident);
+ str_printfa(str, "\t%u\t%u\t%u\n",
+ rec->penalty, rec->last_penalty,
+ rec->last_penalty + rec->last_update);
+ if (o_stream_send(output, str_data(str), str_len(str)) < 0)
+ break;
+ }
+ o_stream_nsend(output, "\n", 1);
+}
diff --git a/src/anvil/penalty.h b/src/anvil/penalty.h
new file mode 100644
index 0000000..23a182c
--- /dev/null
+++ b/src/anvil/penalty.h
@@ -0,0 +1,22 @@
+#ifndef PENALTY_H
+#define PENALTY_H
+
+#define PENALTY_MAX_VALUE ((1 << 16)-1)
+
+struct penalty *penalty_init(void);
+void penalty_deinit(struct penalty **penalty);
+
+void penalty_set_expire_secs(struct penalty *penalty, unsigned int expire_secs);
+
+unsigned int penalty_get(struct penalty *penalty, const char *ident,
+ time_t *last_penalty_r);
+/* if checksum is non-zero and it already exists for ident, the value
+ is set to "value-1", otherwise it's set to "value". */
+void penalty_inc(struct penalty *penalty, const char *ident,
+ unsigned int checksum, unsigned int value);
+
+bool penalty_has_checksum(struct penalty *penalty, const char *ident,
+ unsigned int checksum);
+void penalty_dump(struct penalty *penalty, struct ostream *output);
+
+#endif
diff --git a/src/anvil/test-penalty.c b/src/anvil/test-penalty.c
new file mode 100644
index 0000000..438bf9e
--- /dev/null
+++ b/src/anvil/test-penalty.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "penalty.h"
+#include "test-common.h"
+
+static void test_penalty_checksum(void)
+{
+ struct penalty *penalty;
+ struct ioloop *ioloop;
+ time_t t;
+ unsigned int i, j;
+
+ test_begin("penalty");
+
+ ioloop = io_loop_create();
+ penalty = penalty_init();
+
+ test_assert(penalty_get(penalty, "foo", &t) == 0);
+ for (i = 1; i <= 10; i++) {
+ ioloop_time = 12345678 + i;
+ penalty_inc(penalty, "foo", i, 5+i);
+
+ for (j = I_MIN(1, i-1); j <= i; j++) {
+ test_assert(penalty_get(penalty, "foo", &t) == 5+i);
+ test_assert(t == (time_t)(12345678 + i));
+ test_assert(penalty_has_checksum(penalty, "foo", i));
+ }
+ test_assert(penalty_get(penalty, "foo", &t) == 5+i);
+ test_assert(t == (time_t)(12345678 + i));
+ test_assert(!penalty_has_checksum(penalty, "foo", j));
+ }
+ test_assert(penalty_get(penalty, "foo2", &t) == 0);
+
+ /* overflows checksum array */
+ ioloop_time = 12345678 + i;
+ penalty_inc(penalty, "foo", i, 5 + i);
+ penalty_inc(penalty, "foo", i, 5 + i);
+ penalty_inc(penalty, "foo", 0, 5 + i);
+
+ test_assert(penalty_get(penalty, "foo", &t) == 5+i);
+ test_assert(t == (time_t)(12345678 + i));
+ test_assert(!penalty_has_checksum(penalty, "foo", 1));
+
+ for (j = 2; j <= i; j++) {
+ test_assert(penalty_get(penalty, "foo", &t) == 5+i);
+ test_assert(t == (time_t)(12345678 + i));
+ test_assert(penalty_has_checksum(penalty, "foo", i));
+ }
+
+ penalty_deinit(&penalty);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_penalty_checksum,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/auth/Makefile.am b/src/auth/Makefile.am
new file mode 100644
index 0000000..9e6200b
--- /dev/null
+++ b/src/auth/Makefile.am
@@ -0,0 +1,301 @@
+noinst_LTLIBRARIES = libpassword.la libauth.la
+auth_moduledir = $(moduledir)/auth
+
+# automake seems to force making this unconditional..
+NOPLUGIN_LDFLAGS =
+
+if GSSAPI_PLUGIN
+GSSAPI_LIB = libmech_gssapi.la
+endif
+
+if LDAP_PLUGIN
+LDAP_LIB = libauthdb_ldap.la
+endif
+
+LUA_LIB =
+AUTH_LUA_LIBS =
+AUTH_LUA_LDADD =
+
+if HAVE_LUA
+if AUTH_LUA_PLUGIN
+LUA_LIB += libauthdb_lua.la
+else
+AUTH_LUA_LIBS += $(LIBDOVECOT_LUA)
+AUTH_LUA_LDADD += $(LUA_LIBS)
+endif
+endif
+
+auth_module_LTLIBRARIES = \
+ $(GSSAPI_LIB) \
+ $(LDAP_LIB) \
+ $(LUA_LIB) \
+ libauthdb_imap.la
+
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = auth checkpassword-reply
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-sql \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-old-stats \
+ -I$(top_srcdir)/src/lib-otp \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-oauth2 \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-lua \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -DAUTH_MODULE_DIR=\""$(auth_moduledir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DSYSCONFDIR=\""$(sysconfdir)/dovecot"\" \
+ $(LUA_CFLAGS) \
+ $(AUTH_CFLAGS)
+
+auth_LDFLAGS = -export-dynamic
+
+libpassword_la_SOURCES = \
+ crypt-blowfish.c \
+ mycrypt.c \
+ password-scheme.c \
+ password-scheme-crypt.c \
+ password-scheme-md5crypt.c \
+ password-scheme-scram.c \
+ password-scheme-otp.c \
+ password-scheme-pbkdf2.c \
+ password-scheme-sodium.c
+libpassword_la_CFLAGS = $(AM_CPPFLAGS) $(LIBSODIUM_CFLAGS)
+
+auth_libs = \
+ libauth.la \
+ libstats_auth.la \
+ libpassword.la \
+ ../lib-otp/libotp.la \
+ $(AUTH_LUA_LIBS) \
+ $(LIBDOVECOT_SQL)
+
+auth_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+auth_LDADD = $(auth_libs) $(LIBDOVECOT) $(AUTH_LIBS) $(BINARY_LDFLAGS) $(AUTH_LUA_LDADD)
+auth_DEPENDENCIES = $(auth_libs) $(LIBDOVECOT_DEPS)
+auth_SOURCES = main.c
+
+ldap_sources = db-ldap.c passdb-ldap.c userdb-ldap.c
+lua_sources = db-lua.c passdb-lua.c userdb-lua.c
+
+libauth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libauth_la_SOURCES = \
+ auth.c \
+ auth-cache.c \
+ auth-client-connection.c \
+ auth-master-connection.c \
+ auth-policy.c \
+ mech-otp-common.c \
+ mech-plain-common.c \
+ auth-penalty.c \
+ auth-request.c \
+ auth-request-fields.c \
+ auth-request-handler.c \
+ auth-request-stats.c \
+ auth-request-var-expand.c \
+ auth-settings.c \
+ auth-fields.c \
+ auth-token.c \
+ auth-worker-client.c \
+ auth-worker-server.c \
+ db-checkpassword.c \
+ db-dict.c \
+ db-dict-cache-key.c \
+ db-oauth2.c \
+ db-sql.c \
+ db-passwd-file.c \
+ mech.c \
+ mech-anonymous.c \
+ mech-plain.c \
+ mech-login.c \
+ mech-cram-md5.c \
+ mech-digest-md5.c \
+ mech-external.c \
+ mech-gssapi.c \
+ mech-otp.c \
+ mech-scram.c \
+ mech-apop.c \
+ mech-winbind.c \
+ mech-dovecot-token.c \
+ mech-oauth2.c \
+ passdb.c \
+ passdb-blocking.c \
+ passdb-bsdauth.c \
+ passdb-cache.c \
+ passdb-checkpassword.c \
+ passdb-dict.c \
+ passdb-oauth2.c \
+ passdb-passwd.c \
+ passdb-passwd-file.c \
+ passdb-pam.c \
+ passdb-shadow.c \
+ passdb-sql.c \
+ passdb-static.c \
+ passdb-template.c \
+ userdb.c \
+ userdb-blocking.c \
+ userdb-checkpassword.c \
+ userdb-dict.c \
+ userdb-passwd.c \
+ userdb-passwd-file.c \
+ userdb-prefetch.c \
+ userdb-static.c \
+ userdb-sql.c \
+ userdb-template.c \
+ $(ldap_sources) \
+ $(lua_sources)
+
+headers = \
+ auth.h \
+ auth-cache.h \
+ auth-client-connection.h \
+ auth-common.h \
+ auth-master-connection.h \
+ mech-otp-common.h \
+ mech-plain-common.h \
+ mech-digest-md5-private.h \
+ mech-scram.h \
+ auth-penalty.h \
+ auth-policy.h \
+ auth-request.h \
+ auth-request-handler.h \
+ auth-request-handler-private.h \
+ auth-request-stats.h \
+ auth-request-var-expand.h \
+ auth-settings.h \
+ auth-stats.h \
+ auth-fields.h \
+ auth-token.h \
+ auth-worker-client.h \
+ auth-worker-server.h \
+ db-dict.h \
+ db-ldap.h \
+ db-sql.h \
+ db-passwd-file.h \
+ db-checkpassword.h \
+ db-oauth2.h \
+ mech.h \
+ mycrypt.h \
+ passdb.h \
+ passdb-blocking.h \
+ passdb-cache.h \
+ passdb-template.h \
+ password-scheme.h \
+ userdb.h \
+ userdb-blocking.h \
+ userdb-template.h
+
+if GSSAPI_PLUGIN
+libmech_gssapi_la_LDFLAGS = -module -avoid-version
+libmech_gssapi_la_LIBADD = $(KRB5_LIBS)
+libmech_gssapi_la_CPPFLAGS = $(AM_CPPFLAGS) $(KRB5_CFLAGS) -DPLUGIN_BUILD
+libmech_gssapi_la_SOURCES = mech-gssapi.c
+endif
+
+if LDAP_PLUGIN
+libauthdb_ldap_la_LDFLAGS = -module -avoid-version
+libauthdb_ldap_la_LIBADD = $(LDAP_LIBS)
+libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+libauthdb_ldap_la_SOURCES = $(ldap_sources)
+endif
+
+if AUTH_LUA_PLUGIN
+libauthdb_lua_la_LDFLAGS = -module -avoid-version
+libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA)
+libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+libauthdb_lua_la_SOURCES = $(lua_sources)
+endif
+
+libauthdb_imap_la_LDFLAGS = -module -avoid-version
+libauthdb_imap_la_LIBADD = \
+ ../lib-imap-client/libimap_client.la \
+ $(LIBDOVECOT)
+libauthdb_imap_la_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client
+libauthdb_imap_la_SOURCES = passdb-imap.c
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+checkpassword_reply_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+checkpassword_reply_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+checkpassword_reply_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+checkpassword_reply_sources = \
+ checkpassword-reply.c
+
+stats_moduledir = $(moduledir)/old-stats
+stats_module_LTLIBRARIES = libstats_auth.la
+
+libstats_auth_la_LDFLAGS = -module -avoid-version
+libstats_auth_la_LIBADD = $(LIBDOVECOT)
+libstats_auth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libstats_auth_la_SOURCES = auth-stats.c
+
+test_programs = \
+ test-libpassword \
+ test-auth-cache \
+ test-auth \
+ test-mech
+
+noinst_PROGRAMS = $(test_programs)
+
+noinst_HEADERS = test-auth.h crypt-blowfish.h db-lua.h
+
+test_libs = \
+ ../lib-dovecot/libdovecot.la
+
+test_libpassword_SOURCES = test-libpassword.c
+test_libpassword_LDADD = \
+ libpassword.la \
+ ../lib-otp/libotp.la \
+ $(CRYPT_LIBS) \
+ $(LIBDOVECOT_SQL) \
+ $(LIBSODIUM_LIBS) \
+ $(test_libs) \
+ $(BINARY_LDFLAGS)
+
+test_libpassword_DEPENDENCIES = libpassword.la
+test_libpassword_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+
+test_auth_cache_SOURCES = auth-cache.c test-auth-cache.c
+test_auth_cache_LDADD = $(test_libs)
+test_auth_cache_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+# this is needed to force auth-cache.c recompilation
+test_auth_cache_CPPFLAGS = $(AM_CPPFLAGS)
+
+test_auth_SOURCES = \
+ test-auth-request-var-expand.c \
+ test-auth-request-fields.c \
+ test-username-filter.c \
+ test-db-dict.c \
+ test-lua.c \
+ test-mock.c \
+ test-main.c
+
+test_auth_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
+test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+
+test_mech_SOURCES = \
+ test-mock.c \
+ test-mech.c
+
+test_mech_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
+test_mech_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/auth/Makefile.in b/src/auth/Makefile.in
new file mode 100644
index 0000000..c2af057
--- /dev/null
+++ b/src/auth/Makefile.in
@@ -0,0 +1,1928 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@am__append_1 = libauthdb_lua.la
+@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__append_2 = $(LIBDOVECOT_LUA)
+@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__append_3 = $(LUA_LIBS)
+pkglibexec_PROGRAMS = auth$(EXEEXT) checkpassword-reply$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/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 $(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-libpassword$(EXEEXT) test-auth-cache$(EXEEXT) \
+ test-auth$(EXEEXT) test-mech$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(auth_moduledir)" "$(DESTDIR)$(stats_moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_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; }; \
+ }
+LTLIBRARIES = $(auth_module_LTLIBRARIES) $(noinst_LTLIBRARIES) \
+ $(stats_module_LTLIBRARIES)
+libauth_la_LIBADD =
+am__objects_1 = db-ldap.lo passdb-ldap.lo userdb-ldap.lo
+am__objects_2 = db-lua.lo passdb-lua.lo userdb-lua.lo
+am_libauth_la_OBJECTS = auth.lo auth-cache.lo \
+ auth-client-connection.lo auth-master-connection.lo \
+ auth-policy.lo mech-otp-common.lo mech-plain-common.lo \
+ auth-penalty.lo auth-request.lo auth-request-fields.lo \
+ auth-request-handler.lo auth-request-stats.lo \
+ auth-request-var-expand.lo auth-settings.lo auth-fields.lo \
+ auth-token.lo auth-worker-client.lo auth-worker-server.lo \
+ db-checkpassword.lo db-dict.lo db-dict-cache-key.lo \
+ db-oauth2.lo db-sql.lo db-passwd-file.lo mech.lo \
+ mech-anonymous.lo mech-plain.lo mech-login.lo mech-cram-md5.lo \
+ mech-digest-md5.lo mech-external.lo mech-gssapi.lo mech-otp.lo \
+ mech-scram.lo mech-apop.lo mech-winbind.lo \
+ mech-dovecot-token.lo mech-oauth2.lo passdb.lo \
+ passdb-blocking.lo passdb-bsdauth.lo passdb-cache.lo \
+ passdb-checkpassword.lo passdb-dict.lo passdb-oauth2.lo \
+ passdb-passwd.lo passdb-passwd-file.lo passdb-pam.lo \
+ passdb-shadow.lo passdb-sql.lo passdb-static.lo \
+ passdb-template.lo userdb.lo userdb-blocking.lo \
+ userdb-checkpassword.lo userdb-dict.lo userdb-passwd.lo \
+ userdb-passwd-file.lo userdb-prefetch.lo userdb-static.lo \
+ userdb-sql.lo userdb-template.lo $(am__objects_1) \
+ $(am__objects_2)
+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__DEPENDENCIES_1 =
+libauthdb_imap_la_DEPENDENCIES = ../lib-imap-client/libimap_client.la \
+ $(am__DEPENDENCIES_1)
+am_libauthdb_imap_la_OBJECTS = libauthdb_imap_la-passdb-imap.lo
+libauthdb_imap_la_OBJECTS = $(am_libauthdb_imap_la_OBJECTS)
+libauthdb_imap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libauthdb_imap_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_DEPENDENCIES = \
+@LDAP_PLUGIN_TRUE@ $(am__DEPENDENCIES_1)
+am__libauthdb_ldap_la_SOURCES_DIST = db-ldap.c passdb-ldap.c \
+ userdb-ldap.c
+am__objects_3 = libauthdb_ldap_la-db-ldap.lo \
+ libauthdb_ldap_la-passdb-ldap.lo \
+ libauthdb_ldap_la-userdb-ldap.lo
+@LDAP_PLUGIN_TRUE@am_libauthdb_ldap_la_OBJECTS = $(am__objects_3)
+libauthdb_ldap_la_OBJECTS = $(am_libauthdb_ldap_la_OBJECTS)
+libauthdb_ldap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libauthdb_ldap_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+@LDAP_PLUGIN_TRUE@am_libauthdb_ldap_la_rpath = -rpath \
+@LDAP_PLUGIN_TRUE@ $(auth_moduledir)
+@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_DEPENDENCIES = \
+@AUTH_LUA_PLUGIN_TRUE@ $(am__DEPENDENCIES_1)
+am__libauthdb_lua_la_SOURCES_DIST = db-lua.c passdb-lua.c userdb-lua.c
+am__objects_4 = libauthdb_lua_la-db-lua.lo \
+ libauthdb_lua_la-passdb-lua.lo libauthdb_lua_la-userdb-lua.lo
+@AUTH_LUA_PLUGIN_TRUE@am_libauthdb_lua_la_OBJECTS = $(am__objects_4)
+libauthdb_lua_la_OBJECTS = $(am_libauthdb_lua_la_OBJECTS)
+libauthdb_lua_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libauthdb_lua_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@am_libauthdb_lua_la_rpath = \
+@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@ -rpath $(auth_moduledir)
+@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_DEPENDENCIES = \
+@GSSAPI_PLUGIN_TRUE@ $(am__DEPENDENCIES_1)
+am__libmech_gssapi_la_SOURCES_DIST = mech-gssapi.c
+@GSSAPI_PLUGIN_TRUE@am_libmech_gssapi_la_OBJECTS = \
+@GSSAPI_PLUGIN_TRUE@ libmech_gssapi_la-mech-gssapi.lo
+libmech_gssapi_la_OBJECTS = $(am_libmech_gssapi_la_OBJECTS)
+libmech_gssapi_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libmech_gssapi_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+@GSSAPI_PLUGIN_TRUE@am_libmech_gssapi_la_rpath = -rpath \
+@GSSAPI_PLUGIN_TRUE@ $(auth_moduledir)
+libpassword_la_LIBADD =
+am_libpassword_la_OBJECTS = libpassword_la-crypt-blowfish.lo \
+ libpassword_la-mycrypt.lo libpassword_la-password-scheme.lo \
+ libpassword_la-password-scheme-crypt.lo \
+ libpassword_la-password-scheme-md5crypt.lo \
+ libpassword_la-password-scheme-scram.lo \
+ libpassword_la-password-scheme-otp.lo \
+ libpassword_la-password-scheme-pbkdf2.lo \
+ libpassword_la-password-scheme-sodium.lo
+libpassword_la_OBJECTS = $(am_libpassword_la_OBJECTS)
+libpassword_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(libpassword_la_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o \
+ $@
+am_libstats_auth_la_OBJECTS = auth-stats.lo
+libstats_auth_la_OBJECTS = $(am_libstats_auth_la_OBJECTS)
+libstats_auth_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libstats_auth_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_auth_OBJECTS = auth-main.$(OBJEXT)
+auth_OBJECTS = $(am_auth_OBJECTS)
+@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__DEPENDENCIES_2 = \
+@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@ $(am__DEPENDENCIES_1)
+am__DEPENDENCIES_3 = $(am__DEPENDENCIES_2)
+am__DEPENDENCIES_4 = libauth.la libstats_auth.la libpassword.la \
+ ../lib-otp/libotp.la $(am__DEPENDENCIES_3) \
+ $(am__DEPENDENCIES_1)
+auth_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(auth_LDFLAGS) $(LDFLAGS) -o $@
+checkpassword_reply_SOURCES = checkpassword-reply.c
+checkpassword_reply_OBJECTS = \
+ checkpassword_reply-checkpassword-reply.$(OBJEXT)
+am_test_auth_OBJECTS = test-auth-request-var-expand.$(OBJEXT) \
+ test-auth-request-fields.$(OBJEXT) \
+ test-username-filter.$(OBJEXT) test-db-dict.$(OBJEXT) \
+ test-lua.$(OBJEXT) test-mock.$(OBJEXT) test-main.$(OBJEXT)
+test_auth_OBJECTS = $(am_test_auth_OBJECTS)
+am_test_auth_cache_OBJECTS = test_auth_cache-auth-cache.$(OBJEXT) \
+ test_auth_cache-test-auth-cache.$(OBJEXT)
+test_auth_cache_OBJECTS = $(am_test_auth_cache_OBJECTS)
+am_test_libpassword_OBJECTS = \
+ test_libpassword-test-libpassword.$(OBJEXT)
+test_libpassword_OBJECTS = $(am_test_libpassword_OBJECTS)
+am_test_mech_OBJECTS = test-mock.$(OBJEXT) test-mech.$(OBJEXT)
+test_mech_OBJECTS = $(am_test_mech_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)/auth-cache.Plo \
+ ./$(DEPDIR)/auth-client-connection.Plo \
+ ./$(DEPDIR)/auth-fields.Plo ./$(DEPDIR)/auth-main.Po \
+ ./$(DEPDIR)/auth-master-connection.Plo \
+ ./$(DEPDIR)/auth-penalty.Plo ./$(DEPDIR)/auth-policy.Plo \
+ ./$(DEPDIR)/auth-request-fields.Plo \
+ ./$(DEPDIR)/auth-request-handler.Plo \
+ ./$(DEPDIR)/auth-request-stats.Plo \
+ ./$(DEPDIR)/auth-request-var-expand.Plo \
+ ./$(DEPDIR)/auth-request.Plo ./$(DEPDIR)/auth-settings.Plo \
+ ./$(DEPDIR)/auth-stats.Plo ./$(DEPDIR)/auth-token.Plo \
+ ./$(DEPDIR)/auth-worker-client.Plo \
+ ./$(DEPDIR)/auth-worker-server.Plo ./$(DEPDIR)/auth.Plo \
+ ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po \
+ ./$(DEPDIR)/db-checkpassword.Plo \
+ ./$(DEPDIR)/db-dict-cache-key.Plo ./$(DEPDIR)/db-dict.Plo \
+ ./$(DEPDIR)/db-ldap.Plo ./$(DEPDIR)/db-lua.Plo \
+ ./$(DEPDIR)/db-oauth2.Plo ./$(DEPDIR)/db-passwd-file.Plo \
+ ./$(DEPDIR)/db-sql.Plo \
+ ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo \
+ ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo \
+ ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo \
+ ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo \
+ ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo \
+ ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo \
+ ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo \
+ ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo \
+ ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo \
+ ./$(DEPDIR)/libpassword_la-mycrypt.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo \
+ ./$(DEPDIR)/libpassword_la-password-scheme.Plo \
+ ./$(DEPDIR)/mech-anonymous.Plo ./$(DEPDIR)/mech-apop.Plo \
+ ./$(DEPDIR)/mech-cram-md5.Plo ./$(DEPDIR)/mech-digest-md5.Plo \
+ ./$(DEPDIR)/mech-dovecot-token.Plo \
+ ./$(DEPDIR)/mech-external.Plo ./$(DEPDIR)/mech-gssapi.Plo \
+ ./$(DEPDIR)/mech-login.Plo ./$(DEPDIR)/mech-oauth2.Plo \
+ ./$(DEPDIR)/mech-otp-common.Plo ./$(DEPDIR)/mech-otp.Plo \
+ ./$(DEPDIR)/mech-plain-common.Plo ./$(DEPDIR)/mech-plain.Plo \
+ ./$(DEPDIR)/mech-scram.Plo ./$(DEPDIR)/mech-winbind.Plo \
+ ./$(DEPDIR)/mech.Plo ./$(DEPDIR)/passdb-blocking.Plo \
+ ./$(DEPDIR)/passdb-bsdauth.Plo ./$(DEPDIR)/passdb-cache.Plo \
+ ./$(DEPDIR)/passdb-checkpassword.Plo \
+ ./$(DEPDIR)/passdb-dict.Plo ./$(DEPDIR)/passdb-ldap.Plo \
+ ./$(DEPDIR)/passdb-lua.Plo ./$(DEPDIR)/passdb-oauth2.Plo \
+ ./$(DEPDIR)/passdb-pam.Plo ./$(DEPDIR)/passdb-passwd-file.Plo \
+ ./$(DEPDIR)/passdb-passwd.Plo ./$(DEPDIR)/passdb-shadow.Plo \
+ ./$(DEPDIR)/passdb-sql.Plo ./$(DEPDIR)/passdb-static.Plo \
+ ./$(DEPDIR)/passdb-template.Plo ./$(DEPDIR)/passdb.Plo \
+ ./$(DEPDIR)/test-auth-request-fields.Po \
+ ./$(DEPDIR)/test-auth-request-var-expand.Po \
+ ./$(DEPDIR)/test-db-dict.Po ./$(DEPDIR)/test-lua.Po \
+ ./$(DEPDIR)/test-main.Po ./$(DEPDIR)/test-mech.Po \
+ ./$(DEPDIR)/test-mock.Po ./$(DEPDIR)/test-username-filter.Po \
+ ./$(DEPDIR)/test_auth_cache-auth-cache.Po \
+ ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po \
+ ./$(DEPDIR)/test_libpassword-test-libpassword.Po \
+ ./$(DEPDIR)/userdb-blocking.Plo \
+ ./$(DEPDIR)/userdb-checkpassword.Plo \
+ ./$(DEPDIR)/userdb-dict.Plo ./$(DEPDIR)/userdb-ldap.Plo \
+ ./$(DEPDIR)/userdb-lua.Plo ./$(DEPDIR)/userdb-passwd-file.Plo \
+ ./$(DEPDIR)/userdb-passwd.Plo ./$(DEPDIR)/userdb-prefetch.Plo \
+ ./$(DEPDIR)/userdb-sql.Plo ./$(DEPDIR)/userdb-static.Plo \
+ ./$(DEPDIR)/userdb-template.Plo ./$(DEPDIR)/userdb.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 = $(libauth_la_SOURCES) $(libauthdb_imap_la_SOURCES) \
+ $(libauthdb_ldap_la_SOURCES) $(libauthdb_lua_la_SOURCES) \
+ $(libmech_gssapi_la_SOURCES) $(libpassword_la_SOURCES) \
+ $(libstats_auth_la_SOURCES) $(auth_SOURCES) \
+ checkpassword-reply.c $(test_auth_SOURCES) \
+ $(test_auth_cache_SOURCES) $(test_libpassword_SOURCES) \
+ $(test_mech_SOURCES)
+DIST_SOURCES = $(libauth_la_SOURCES) $(libauthdb_imap_la_SOURCES) \
+ $(am__libauthdb_ldap_la_SOURCES_DIST) \
+ $(am__libauthdb_lua_la_SOURCES_DIST) \
+ $(am__libmech_gssapi_la_SOURCES_DIST) \
+ $(libpassword_la_SOURCES) $(libstats_auth_la_SOURCES) \
+ $(auth_SOURCES) checkpassword-reply.c $(test_auth_SOURCES) \
+ $(test_auth_cache_SOURCES) $(test_libpassword_SOURCES) \
+ $(test_mech_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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 = libpassword.la libauth.la
+auth_moduledir = $(moduledir)/auth
+@GSSAPI_PLUGIN_TRUE@GSSAPI_LIB = libmech_gssapi.la
+@LDAP_PLUGIN_TRUE@LDAP_LIB = libauthdb_ldap.la
+LUA_LIB = $(am__append_1)
+AUTH_LUA_LIBS = $(am__append_2)
+AUTH_LUA_LDADD = $(am__append_3)
+auth_module_LTLIBRARIES = \
+ $(GSSAPI_LIB) \
+ $(LDAP_LIB) \
+ $(LUA_LIB) \
+ libauthdb_imap.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-sql \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-old-stats \
+ -I$(top_srcdir)/src/lib-otp \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-oauth2 \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-lua \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -DAUTH_MODULE_DIR=\""$(auth_moduledir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DSYSCONFDIR=\""$(sysconfdir)/dovecot"\" \
+ $(LUA_CFLAGS) \
+ $(AUTH_CFLAGS)
+
+auth_LDFLAGS = -export-dynamic
+libpassword_la_SOURCES = \
+ crypt-blowfish.c \
+ mycrypt.c \
+ password-scheme.c \
+ password-scheme-crypt.c \
+ password-scheme-md5crypt.c \
+ password-scheme-scram.c \
+ password-scheme-otp.c \
+ password-scheme-pbkdf2.c \
+ password-scheme-sodium.c
+
+libpassword_la_CFLAGS = $(AM_CPPFLAGS) $(LIBSODIUM_CFLAGS)
+auth_libs = \
+ libauth.la \
+ libstats_auth.la \
+ libpassword.la \
+ ../lib-otp/libotp.la \
+ $(AUTH_LUA_LIBS) \
+ $(LIBDOVECOT_SQL)
+
+auth_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+auth_LDADD = $(auth_libs) $(LIBDOVECOT) $(AUTH_LIBS) $(BINARY_LDFLAGS) $(AUTH_LUA_LDADD)
+auth_DEPENDENCIES = $(auth_libs) $(LIBDOVECOT_DEPS)
+auth_SOURCES = main.c
+ldap_sources = db-ldap.c passdb-ldap.c userdb-ldap.c
+lua_sources = db-lua.c passdb-lua.c userdb-lua.c
+libauth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libauth_la_SOURCES = \
+ auth.c \
+ auth-cache.c \
+ auth-client-connection.c \
+ auth-master-connection.c \
+ auth-policy.c \
+ mech-otp-common.c \
+ mech-plain-common.c \
+ auth-penalty.c \
+ auth-request.c \
+ auth-request-fields.c \
+ auth-request-handler.c \
+ auth-request-stats.c \
+ auth-request-var-expand.c \
+ auth-settings.c \
+ auth-fields.c \
+ auth-token.c \
+ auth-worker-client.c \
+ auth-worker-server.c \
+ db-checkpassword.c \
+ db-dict.c \
+ db-dict-cache-key.c \
+ db-oauth2.c \
+ db-sql.c \
+ db-passwd-file.c \
+ mech.c \
+ mech-anonymous.c \
+ mech-plain.c \
+ mech-login.c \
+ mech-cram-md5.c \
+ mech-digest-md5.c \
+ mech-external.c \
+ mech-gssapi.c \
+ mech-otp.c \
+ mech-scram.c \
+ mech-apop.c \
+ mech-winbind.c \
+ mech-dovecot-token.c \
+ mech-oauth2.c \
+ passdb.c \
+ passdb-blocking.c \
+ passdb-bsdauth.c \
+ passdb-cache.c \
+ passdb-checkpassword.c \
+ passdb-dict.c \
+ passdb-oauth2.c \
+ passdb-passwd.c \
+ passdb-passwd-file.c \
+ passdb-pam.c \
+ passdb-shadow.c \
+ passdb-sql.c \
+ passdb-static.c \
+ passdb-template.c \
+ userdb.c \
+ userdb-blocking.c \
+ userdb-checkpassword.c \
+ userdb-dict.c \
+ userdb-passwd.c \
+ userdb-passwd-file.c \
+ userdb-prefetch.c \
+ userdb-static.c \
+ userdb-sql.c \
+ userdb-template.c \
+ $(ldap_sources) \
+ $(lua_sources)
+
+headers = \
+ auth.h \
+ auth-cache.h \
+ auth-client-connection.h \
+ auth-common.h \
+ auth-master-connection.h \
+ mech-otp-common.h \
+ mech-plain-common.h \
+ mech-digest-md5-private.h \
+ mech-scram.h \
+ auth-penalty.h \
+ auth-policy.h \
+ auth-request.h \
+ auth-request-handler.h \
+ auth-request-handler-private.h \
+ auth-request-stats.h \
+ auth-request-var-expand.h \
+ auth-settings.h \
+ auth-stats.h \
+ auth-fields.h \
+ auth-token.h \
+ auth-worker-client.h \
+ auth-worker-server.h \
+ db-dict.h \
+ db-ldap.h \
+ db-sql.h \
+ db-passwd-file.h \
+ db-checkpassword.h \
+ db-oauth2.h \
+ mech.h \
+ mycrypt.h \
+ passdb.h \
+ passdb-blocking.h \
+ passdb-cache.h \
+ passdb-template.h \
+ password-scheme.h \
+ userdb.h \
+ userdb-blocking.h \
+ userdb-template.h
+
+@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_LDFLAGS = -module -avoid-version
+@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_LIBADD = $(KRB5_LIBS)
+@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_CPPFLAGS = $(AM_CPPFLAGS) $(KRB5_CFLAGS) -DPLUGIN_BUILD
+@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_SOURCES = mech-gssapi.c
+@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_LDFLAGS = -module -avoid-version
+@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_LIBADD = $(LDAP_LIBS)
+@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_SOURCES = $(ldap_sources)
+@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_LDFLAGS = -module -avoid-version
+@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA)
+@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_SOURCES = $(lua_sources)
+libauthdb_imap_la_LDFLAGS = -module -avoid-version
+libauthdb_imap_la_LIBADD = \
+ ../lib-imap-client/libimap_client.la \
+ $(LIBDOVECOT)
+
+libauthdb_imap_la_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client
+
+libauthdb_imap_la_SOURCES = passdb-imap.c
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+checkpassword_reply_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+checkpassword_reply_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+checkpassword_reply_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+checkpassword_reply_sources = \
+ checkpassword-reply.c
+
+stats_moduledir = $(moduledir)/old-stats
+stats_module_LTLIBRARIES = libstats_auth.la
+libstats_auth_la_LDFLAGS = -module -avoid-version
+libstats_auth_la_LIBADD = $(LIBDOVECOT)
+libstats_auth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libstats_auth_la_SOURCES = auth-stats.c
+test_programs = \
+ test-libpassword \
+ test-auth-cache \
+ test-auth \
+ test-mech
+
+noinst_HEADERS = test-auth.h crypt-blowfish.h db-lua.h
+test_libs = \
+ ../lib-dovecot/libdovecot.la
+
+test_libpassword_SOURCES = test-libpassword.c
+test_libpassword_LDADD = \
+ libpassword.la \
+ ../lib-otp/libotp.la \
+ $(CRYPT_LIBS) \
+ $(LIBDOVECOT_SQL) \
+ $(LIBSODIUM_LIBS) \
+ $(test_libs) \
+ $(BINARY_LDFLAGS)
+
+test_libpassword_DEPENDENCIES = libpassword.la
+test_libpassword_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+test_auth_cache_SOURCES = auth-cache.c test-auth-cache.c
+test_auth_cache_LDADD = $(test_libs)
+test_auth_cache_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+# this is needed to force auth-cache.c recompilation
+test_auth_cache_CPPFLAGS = $(AM_CPPFLAGS)
+test_auth_SOURCES = \
+ test-auth-request-var-expand.c \
+ test-auth-request-fields.c \
+ test-username-filter.c \
+ test-db-dict.c \
+ test-lua.c \
+ test-mock.c \
+ test-main.c
+
+test_auth_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
+test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+test_mech_SOURCES = \
+ test-mock.c \
+ test-mech.c
+
+test_mech_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
+test_mech_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(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/auth/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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-auth_moduleLTLIBRARIES: $(auth_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_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)$(auth_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(auth_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(auth_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(auth_moduledir)"; \
+ }
+
+uninstall-auth_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(auth_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(auth_moduledir)/$$f"; \
+ done
+
+clean-auth_moduleLTLIBRARIES:
+ -test -z "$(auth_module_LTLIBRARIES)" || rm -f $(auth_module_LTLIBRARIES)
+ @list='$(auth_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}; \
+ }
+
+install-stats_moduleLTLIBRARIES: $(stats_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(stats_module_LTLIBRARIES)'; test -n "$(stats_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)$(stats_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(stats_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(stats_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(stats_moduledir)"; \
+ }
+
+uninstall-stats_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(stats_module_LTLIBRARIES)'; test -n "$(stats_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(stats_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(stats_moduledir)/$$f"; \
+ done
+
+clean-stats_moduleLTLIBRARIES:
+ -test -z "$(stats_module_LTLIBRARIES)" || rm -f $(stats_module_LTLIBRARIES)
+ @list='$(stats_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}; \
+ }
+
+libauth.la: $(libauth_la_OBJECTS) $(libauth_la_DEPENDENCIES) $(EXTRA_libauth_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libauth_la_OBJECTS) $(libauth_la_LIBADD) $(LIBS)
+
+libauthdb_imap.la: $(libauthdb_imap_la_OBJECTS) $(libauthdb_imap_la_DEPENDENCIES) $(EXTRA_libauthdb_imap_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libauthdb_imap_la_LINK) -rpath $(auth_moduledir) $(libauthdb_imap_la_OBJECTS) $(libauthdb_imap_la_LIBADD) $(LIBS)
+
+libauthdb_ldap.la: $(libauthdb_ldap_la_OBJECTS) $(libauthdb_ldap_la_DEPENDENCIES) $(EXTRA_libauthdb_ldap_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libauthdb_ldap_la_LINK) $(am_libauthdb_ldap_la_rpath) $(libauthdb_ldap_la_OBJECTS) $(libauthdb_ldap_la_LIBADD) $(LIBS)
+
+libauthdb_lua.la: $(libauthdb_lua_la_OBJECTS) $(libauthdb_lua_la_DEPENDENCIES) $(EXTRA_libauthdb_lua_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libauthdb_lua_la_LINK) $(am_libauthdb_lua_la_rpath) $(libauthdb_lua_la_OBJECTS) $(libauthdb_lua_la_LIBADD) $(LIBS)
+
+libmech_gssapi.la: $(libmech_gssapi_la_OBJECTS) $(libmech_gssapi_la_DEPENDENCIES) $(EXTRA_libmech_gssapi_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libmech_gssapi_la_LINK) $(am_libmech_gssapi_la_rpath) $(libmech_gssapi_la_OBJECTS) $(libmech_gssapi_la_LIBADD) $(LIBS)
+
+libpassword.la: $(libpassword_la_OBJECTS) $(libpassword_la_DEPENDENCIES) $(EXTRA_libpassword_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libpassword_la_LINK) $(libpassword_la_OBJECTS) $(libpassword_la_LIBADD) $(LIBS)
+
+libstats_auth.la: $(libstats_auth_la_OBJECTS) $(libstats_auth_la_DEPENDENCIES) $(EXTRA_libstats_auth_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libstats_auth_la_LINK) -rpath $(stats_moduledir) $(libstats_auth_la_OBJECTS) $(libstats_auth_la_LIBADD) $(LIBS)
+
+auth$(EXEEXT): $(auth_OBJECTS) $(auth_DEPENDENCIES) $(EXTRA_auth_DEPENDENCIES)
+ @rm -f auth$(EXEEXT)
+ $(AM_V_CCLD)$(auth_LINK) $(auth_OBJECTS) $(auth_LDADD) $(LIBS)
+
+checkpassword-reply$(EXEEXT): $(checkpassword_reply_OBJECTS) $(checkpassword_reply_DEPENDENCIES) $(EXTRA_checkpassword_reply_DEPENDENCIES)
+ @rm -f checkpassword-reply$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(checkpassword_reply_OBJECTS) $(checkpassword_reply_LDADD) $(LIBS)
+
+test-auth$(EXEEXT): $(test_auth_OBJECTS) $(test_auth_DEPENDENCIES) $(EXTRA_test_auth_DEPENDENCIES)
+ @rm -f test-auth$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_auth_OBJECTS) $(test_auth_LDADD) $(LIBS)
+
+test-auth-cache$(EXEEXT): $(test_auth_cache_OBJECTS) $(test_auth_cache_DEPENDENCIES) $(EXTRA_test_auth_cache_DEPENDENCIES)
+ @rm -f test-auth-cache$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_auth_cache_OBJECTS) $(test_auth_cache_LDADD) $(LIBS)
+
+test-libpassword$(EXEEXT): $(test_libpassword_OBJECTS) $(test_libpassword_DEPENDENCIES) $(EXTRA_test_libpassword_DEPENDENCIES)
+ @rm -f test-libpassword$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_libpassword_OBJECTS) $(test_libpassword_LDADD) $(LIBS)
+
+test-mech$(EXEEXT): $(test_mech_OBJECTS) $(test_mech_DEPENDENCIES) $(EXTRA_test_mech_DEPENDENCIES)
+ @rm -f test-mech$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mech_OBJECTS) $(test_mech_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-client-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-fields.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-master-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-penalty.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-policy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-fields.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-handler.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-stats.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-var-expand.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-stats.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-token.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-worker-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-worker-server.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-checkpassword.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-dict-cache-key.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-oauth2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-passwd-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-sql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-mycrypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-anonymous.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-apop.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-cram-md5.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-digest-md5.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-dovecot-token.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-gssapi.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-oauth2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-otp-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-otp.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain-common.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)/mech-scram.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-winbind.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-blocking.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-bsdauth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-checkpassword.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-oauth2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-pam.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-passwd-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-passwd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-shadow.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-sql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-static.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-template.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-request-fields.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-request-var-expand.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-db-dict.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-lua.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mech.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mock.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-username-filter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_auth_cache-auth-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_auth_cache-test-auth-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_libpassword-test-libpassword.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-blocking.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-checkpassword.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-passwd-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-passwd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-prefetch.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-sql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-static.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-template.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb.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 $@ $<
+
+libauthdb_imap_la-passdb-imap.lo: passdb-imap.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_imap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_imap_la-passdb-imap.lo -MD -MP -MF $(DEPDIR)/libauthdb_imap_la-passdb-imap.Tpo -c -o libauthdb_imap_la-passdb-imap.lo `test -f 'passdb-imap.c' || echo '$(srcdir)/'`passdb-imap.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_imap_la-passdb-imap.Tpo $(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-imap.c' object='libauthdb_imap_la-passdb-imap.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) $(libauthdb_imap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_imap_la-passdb-imap.lo `test -f 'passdb-imap.c' || echo '$(srcdir)/'`passdb-imap.c
+
+libauthdb_ldap_la-db-ldap.lo: db-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-db-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-db-ldap.Tpo -c -o libauthdb_ldap_la-db-ldap.lo `test -f 'db-ldap.c' || echo '$(srcdir)/'`db-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-db-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='db-ldap.c' object='libauthdb_ldap_la-db-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) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-db-ldap.lo `test -f 'db-ldap.c' || echo '$(srcdir)/'`db-ldap.c
+
+libauthdb_ldap_la-passdb-ldap.lo: passdb-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-passdb-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Tpo -c -o libauthdb_ldap_la-passdb-ldap.lo `test -f 'passdb-ldap.c' || echo '$(srcdir)/'`passdb-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-ldap.c' object='libauthdb_ldap_la-passdb-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) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-passdb-ldap.lo `test -f 'passdb-ldap.c' || echo '$(srcdir)/'`passdb-ldap.c
+
+libauthdb_ldap_la-userdb-ldap.lo: userdb-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-userdb-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Tpo -c -o libauthdb_ldap_la-userdb-ldap.lo `test -f 'userdb-ldap.c' || echo '$(srcdir)/'`userdb-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='userdb-ldap.c' object='libauthdb_ldap_la-userdb-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) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-userdb-ldap.lo `test -f 'userdb-ldap.c' || echo '$(srcdir)/'`userdb-ldap.c
+
+libauthdb_lua_la-db-lua.lo: db-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-db-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-db-lua.Tpo -c -o libauthdb_lua_la-db-lua.lo `test -f 'db-lua.c' || echo '$(srcdir)/'`db-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-db-lua.Tpo $(DEPDIR)/libauthdb_lua_la-db-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='db-lua.c' object='libauthdb_lua_la-db-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) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-db-lua.lo `test -f 'db-lua.c' || echo '$(srcdir)/'`db-lua.c
+
+libauthdb_lua_la-passdb-lua.lo: passdb-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-passdb-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-passdb-lua.Tpo -c -o libauthdb_lua_la-passdb-lua.lo `test -f 'passdb-lua.c' || echo '$(srcdir)/'`passdb-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-passdb-lua.Tpo $(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-lua.c' object='libauthdb_lua_la-passdb-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) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-passdb-lua.lo `test -f 'passdb-lua.c' || echo '$(srcdir)/'`passdb-lua.c
+
+libauthdb_lua_la-userdb-lua.lo: userdb-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-userdb-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-userdb-lua.Tpo -c -o libauthdb_lua_la-userdb-lua.lo `test -f 'userdb-lua.c' || echo '$(srcdir)/'`userdb-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-userdb-lua.Tpo $(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='userdb-lua.c' object='libauthdb_lua_la-userdb-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) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-userdb-lua.lo `test -f 'userdb-lua.c' || echo '$(srcdir)/'`userdb-lua.c
+
+libmech_gssapi_la-mech-gssapi.lo: mech-gssapi.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libmech_gssapi_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libmech_gssapi_la-mech-gssapi.lo -MD -MP -MF $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Tpo -c -o libmech_gssapi_la-mech-gssapi.lo `test -f 'mech-gssapi.c' || echo '$(srcdir)/'`mech-gssapi.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Tpo $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mech-gssapi.c' object='libmech_gssapi_la-mech-gssapi.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) $(libmech_gssapi_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libmech_gssapi_la-mech-gssapi.lo `test -f 'mech-gssapi.c' || echo '$(srcdir)/'`mech-gssapi.c
+
+libpassword_la-crypt-blowfish.lo: crypt-blowfish.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-crypt-blowfish.lo -MD -MP -MF $(DEPDIR)/libpassword_la-crypt-blowfish.Tpo -c -o libpassword_la-crypt-blowfish.lo `test -f 'crypt-blowfish.c' || echo '$(srcdir)/'`crypt-blowfish.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-crypt-blowfish.Tpo $(DEPDIR)/libpassword_la-crypt-blowfish.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='crypt-blowfish.c' object='libpassword_la-crypt-blowfish.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-crypt-blowfish.lo `test -f 'crypt-blowfish.c' || echo '$(srcdir)/'`crypt-blowfish.c
+
+libpassword_la-mycrypt.lo: mycrypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-mycrypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-mycrypt.Tpo -c -o libpassword_la-mycrypt.lo `test -f 'mycrypt.c' || echo '$(srcdir)/'`mycrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-mycrypt.Tpo $(DEPDIR)/libpassword_la-mycrypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mycrypt.c' object='libpassword_la-mycrypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-mycrypt.lo `test -f 'mycrypt.c' || echo '$(srcdir)/'`mycrypt.c
+
+libpassword_la-password-scheme.lo: password-scheme.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme.Tpo -c -o libpassword_la-password-scheme.lo `test -f 'password-scheme.c' || echo '$(srcdir)/'`password-scheme.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme.Tpo $(DEPDIR)/libpassword_la-password-scheme.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme.c' object='libpassword_la-password-scheme.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme.lo `test -f 'password-scheme.c' || echo '$(srcdir)/'`password-scheme.c
+
+libpassword_la-password-scheme-crypt.lo: password-scheme-crypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-crypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-crypt.Tpo -c -o libpassword_la-password-scheme-crypt.lo `test -f 'password-scheme-crypt.c' || echo '$(srcdir)/'`password-scheme-crypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-crypt.Tpo $(DEPDIR)/libpassword_la-password-scheme-crypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-crypt.c' object='libpassword_la-password-scheme-crypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-crypt.lo `test -f 'password-scheme-crypt.c' || echo '$(srcdir)/'`password-scheme-crypt.c
+
+libpassword_la-password-scheme-md5crypt.lo: password-scheme-md5crypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-md5crypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Tpo -c -o libpassword_la-password-scheme-md5crypt.lo `test -f 'password-scheme-md5crypt.c' || echo '$(srcdir)/'`password-scheme-md5crypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Tpo $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-md5crypt.c' object='libpassword_la-password-scheme-md5crypt.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-md5crypt.lo `test -f 'password-scheme-md5crypt.c' || echo '$(srcdir)/'`password-scheme-md5crypt.c
+
+libpassword_la-password-scheme-scram.lo: password-scheme-scram.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-scram.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-scram.Tpo -c -o libpassword_la-password-scheme-scram.lo `test -f 'password-scheme-scram.c' || echo '$(srcdir)/'`password-scheme-scram.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-scram.Tpo $(DEPDIR)/libpassword_la-password-scheme-scram.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-scram.c' object='libpassword_la-password-scheme-scram.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-scram.lo `test -f 'password-scheme-scram.c' || echo '$(srcdir)/'`password-scheme-scram.c
+
+libpassword_la-password-scheme-otp.lo: password-scheme-otp.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-otp.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-otp.Tpo -c -o libpassword_la-password-scheme-otp.lo `test -f 'password-scheme-otp.c' || echo '$(srcdir)/'`password-scheme-otp.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-otp.Tpo $(DEPDIR)/libpassword_la-password-scheme-otp.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-otp.c' object='libpassword_la-password-scheme-otp.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-otp.lo `test -f 'password-scheme-otp.c' || echo '$(srcdir)/'`password-scheme-otp.c
+
+libpassword_la-password-scheme-pbkdf2.lo: password-scheme-pbkdf2.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-pbkdf2.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Tpo -c -o libpassword_la-password-scheme-pbkdf2.lo `test -f 'password-scheme-pbkdf2.c' || echo '$(srcdir)/'`password-scheme-pbkdf2.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Tpo $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-pbkdf2.c' object='libpassword_la-password-scheme-pbkdf2.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-pbkdf2.lo `test -f 'password-scheme-pbkdf2.c' || echo '$(srcdir)/'`password-scheme-pbkdf2.c
+
+libpassword_la-password-scheme-sodium.lo: password-scheme-sodium.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) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-sodium.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-sodium.Tpo -c -o libpassword_la-password-scheme-sodium.lo `test -f 'password-scheme-sodium.c' || echo '$(srcdir)/'`password-scheme-sodium.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-sodium.Tpo $(DEPDIR)/libpassword_la-password-scheme-sodium.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-sodium.c' object='libpassword_la-password-scheme-sodium.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) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-sodium.lo `test -f 'password-scheme-sodium.c' || echo '$(srcdir)/'`password-scheme-sodium.c
+
+auth-main.o: main.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT auth-main.o -MD -MP -MF $(DEPDIR)/auth-main.Tpo -c -o auth-main.o `test -f 'main.c' || echo '$(srcdir)/'`main.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/auth-main.Tpo $(DEPDIR)/auth-main.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='main.c' object='auth-main.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) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o auth-main.o `test -f 'main.c' || echo '$(srcdir)/'`main.c
+
+auth-main.obj: main.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT auth-main.obj -MD -MP -MF $(DEPDIR)/auth-main.Tpo -c -o auth-main.obj `if test -f 'main.c'; then $(CYGPATH_W) 'main.c'; else $(CYGPATH_W) '$(srcdir)/main.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/auth-main.Tpo $(DEPDIR)/auth-main.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='main.c' object='auth-main.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) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o auth-main.obj `if test -f 'main.c'; then $(CYGPATH_W) 'main.c'; else $(CYGPATH_W) '$(srcdir)/main.c'; fi`
+
+checkpassword_reply-checkpassword-reply.o: checkpassword-reply.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT checkpassword_reply-checkpassword-reply.o -MD -MP -MF $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo -c -o checkpassword_reply-checkpassword-reply.o `test -f 'checkpassword-reply.c' || echo '$(srcdir)/'`checkpassword-reply.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo $(DEPDIR)/checkpassword_reply-checkpassword-reply.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='checkpassword-reply.c' object='checkpassword_reply-checkpassword-reply.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) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o checkpassword_reply-checkpassword-reply.o `test -f 'checkpassword-reply.c' || echo '$(srcdir)/'`checkpassword-reply.c
+
+checkpassword_reply-checkpassword-reply.obj: checkpassword-reply.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT checkpassword_reply-checkpassword-reply.obj -MD -MP -MF $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo -c -o checkpassword_reply-checkpassword-reply.obj `if test -f 'checkpassword-reply.c'; then $(CYGPATH_W) 'checkpassword-reply.c'; else $(CYGPATH_W) '$(srcdir)/checkpassword-reply.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo $(DEPDIR)/checkpassword_reply-checkpassword-reply.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='checkpassword-reply.c' object='checkpassword_reply-checkpassword-reply.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) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o checkpassword_reply-checkpassword-reply.obj `if test -f 'checkpassword-reply.c'; then $(CYGPATH_W) 'checkpassword-reply.c'; else $(CYGPATH_W) '$(srcdir)/checkpassword-reply.c'; fi`
+
+test_auth_cache-auth-cache.o: auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-auth-cache.o -MD -MP -MF $(DEPDIR)/test_auth_cache-auth-cache.Tpo -c -o test_auth_cache-auth-cache.o `test -f 'auth-cache.c' || echo '$(srcdir)/'`auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-auth-cache.Tpo $(DEPDIR)/test_auth_cache-auth-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='auth-cache.c' object='test_auth_cache-auth-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_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-auth-cache.o `test -f 'auth-cache.c' || echo '$(srcdir)/'`auth-cache.c
+
+test_auth_cache-auth-cache.obj: auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-auth-cache.obj -MD -MP -MF $(DEPDIR)/test_auth_cache-auth-cache.Tpo -c -o test_auth_cache-auth-cache.obj `if test -f 'auth-cache.c'; then $(CYGPATH_W) 'auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/auth-cache.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-auth-cache.Tpo $(DEPDIR)/test_auth_cache-auth-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='auth-cache.c' object='test_auth_cache-auth-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_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-auth-cache.obj `if test -f 'auth-cache.c'; then $(CYGPATH_W) 'auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/auth-cache.c'; fi`
+
+test_auth_cache-test-auth-cache.o: test-auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-test-auth-cache.o -MD -MP -MF $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo -c -o test_auth_cache-test-auth-cache.o `test -f 'test-auth-cache.c' || echo '$(srcdir)/'`test-auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo $(DEPDIR)/test_auth_cache-test-auth-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-auth-cache.c' object='test_auth_cache-test-auth-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_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-test-auth-cache.o `test -f 'test-auth-cache.c' || echo '$(srcdir)/'`test-auth-cache.c
+
+test_auth_cache-test-auth-cache.obj: test-auth-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-test-auth-cache.obj -MD -MP -MF $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo -c -o test_auth_cache-test-auth-cache.obj `if test -f 'test-auth-cache.c'; then $(CYGPATH_W) 'test-auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-auth-cache.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo $(DEPDIR)/test_auth_cache-test-auth-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-auth-cache.c' object='test_auth_cache-test-auth-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_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-test-auth-cache.obj `if test -f 'test-auth-cache.c'; then $(CYGPATH_W) 'test-auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-auth-cache.c'; fi`
+
+test_libpassword-test-libpassword.o: test-libpassword.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_libpassword-test-libpassword.o -MD -MP -MF $(DEPDIR)/test_libpassword-test-libpassword.Tpo -c -o test_libpassword-test-libpassword.o `test -f 'test-libpassword.c' || echo '$(srcdir)/'`test-libpassword.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_libpassword-test-libpassword.Tpo $(DEPDIR)/test_libpassword-test-libpassword.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-libpassword.c' object='test_libpassword-test-libpassword.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_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_libpassword-test-libpassword.o `test -f 'test-libpassword.c' || echo '$(srcdir)/'`test-libpassword.c
+
+test_libpassword-test-libpassword.obj: test-libpassword.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_libpassword-test-libpassword.obj -MD -MP -MF $(DEPDIR)/test_libpassword-test-libpassword.Tpo -c -o test_libpassword-test-libpassword.obj `if test -f 'test-libpassword.c'; then $(CYGPATH_W) 'test-libpassword.c'; else $(CYGPATH_W) '$(srcdir)/test-libpassword.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_libpassword-test-libpassword.Tpo $(DEPDIR)/test_libpassword-test-libpassword.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-libpassword.c' object='test_libpassword-test-libpassword.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_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_libpassword-test-libpassword.obj `if test -f 'test-libpassword.c'; then $(CYGPATH_W) 'test-libpassword.c'; else $(CYGPATH_W) '$(srcdir)/test-libpassword.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)$(pkglibexecdir)" "$(DESTDIR)$(auth_moduledir)" "$(DESTDIR)$(stats_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-auth_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS clean-stats_moduleLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/auth-cache.Plo
+ -rm -f ./$(DEPDIR)/auth-client-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-fields.Plo
+ -rm -f ./$(DEPDIR)/auth-main.Po
+ -rm -f ./$(DEPDIR)/auth-master-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-penalty.Plo
+ -rm -f ./$(DEPDIR)/auth-policy.Plo
+ -rm -f ./$(DEPDIR)/auth-request-fields.Plo
+ -rm -f ./$(DEPDIR)/auth-request-handler.Plo
+ -rm -f ./$(DEPDIR)/auth-request-stats.Plo
+ -rm -f ./$(DEPDIR)/auth-request-var-expand.Plo
+ -rm -f ./$(DEPDIR)/auth-request.Plo
+ -rm -f ./$(DEPDIR)/auth-settings.Plo
+ -rm -f ./$(DEPDIR)/auth-stats.Plo
+ -rm -f ./$(DEPDIR)/auth-token.Plo
+ -rm -f ./$(DEPDIR)/auth-worker-client.Plo
+ -rm -f ./$(DEPDIR)/auth-worker-server.Plo
+ -rm -f ./$(DEPDIR)/auth.Plo
+ -rm -f ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po
+ -rm -f ./$(DEPDIR)/db-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/db-dict-cache-key.Plo
+ -rm -f ./$(DEPDIR)/db-dict.Plo
+ -rm -f ./$(DEPDIR)/db-ldap.Plo
+ -rm -f ./$(DEPDIR)/db-lua.Plo
+ -rm -f ./$(DEPDIR)/db-oauth2.Plo
+ -rm -f ./$(DEPDIR)/db-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/db-sql.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo
+ -rm -f ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-mycrypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme.Plo
+ -rm -f ./$(DEPDIR)/mech-anonymous.Plo
+ -rm -f ./$(DEPDIR)/mech-apop.Plo
+ -rm -f ./$(DEPDIR)/mech-cram-md5.Plo
+ -rm -f ./$(DEPDIR)/mech-digest-md5.Plo
+ -rm -f ./$(DEPDIR)/mech-dovecot-token.Plo
+ -rm -f ./$(DEPDIR)/mech-external.Plo
+ -rm -f ./$(DEPDIR)/mech-gssapi.Plo
+ -rm -f ./$(DEPDIR)/mech-login.Plo
+ -rm -f ./$(DEPDIR)/mech-oauth2.Plo
+ -rm -f ./$(DEPDIR)/mech-otp-common.Plo
+ -rm -f ./$(DEPDIR)/mech-otp.Plo
+ -rm -f ./$(DEPDIR)/mech-plain-common.Plo
+ -rm -f ./$(DEPDIR)/mech-plain.Plo
+ -rm -f ./$(DEPDIR)/mech-scram.Plo
+ -rm -f ./$(DEPDIR)/mech-winbind.Plo
+ -rm -f ./$(DEPDIR)/mech.Plo
+ -rm -f ./$(DEPDIR)/passdb-blocking.Plo
+ -rm -f ./$(DEPDIR)/passdb-bsdauth.Plo
+ -rm -f ./$(DEPDIR)/passdb-cache.Plo
+ -rm -f ./$(DEPDIR)/passdb-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/passdb-dict.Plo
+ -rm -f ./$(DEPDIR)/passdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/passdb-lua.Plo
+ -rm -f ./$(DEPDIR)/passdb-oauth2.Plo
+ -rm -f ./$(DEPDIR)/passdb-pam.Plo
+ -rm -f ./$(DEPDIR)/passdb-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/passdb-passwd.Plo
+ -rm -f ./$(DEPDIR)/passdb-shadow.Plo
+ -rm -f ./$(DEPDIR)/passdb-sql.Plo
+ -rm -f ./$(DEPDIR)/passdb-static.Plo
+ -rm -f ./$(DEPDIR)/passdb-template.Plo
+ -rm -f ./$(DEPDIR)/passdb.Plo
+ -rm -f ./$(DEPDIR)/test-auth-request-fields.Po
+ -rm -f ./$(DEPDIR)/test-auth-request-var-expand.Po
+ -rm -f ./$(DEPDIR)/test-db-dict.Po
+ -rm -f ./$(DEPDIR)/test-lua.Po
+ -rm -f ./$(DEPDIR)/test-main.Po
+ -rm -f ./$(DEPDIR)/test-mech.Po
+ -rm -f ./$(DEPDIR)/test-mock.Po
+ -rm -f ./$(DEPDIR)/test-username-filter.Po
+ -rm -f ./$(DEPDIR)/test_auth_cache-auth-cache.Po
+ -rm -f ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po
+ -rm -f ./$(DEPDIR)/test_libpassword-test-libpassword.Po
+ -rm -f ./$(DEPDIR)/userdb-blocking.Plo
+ -rm -f ./$(DEPDIR)/userdb-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/userdb-dict.Plo
+ -rm -f ./$(DEPDIR)/userdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/userdb-lua.Plo
+ -rm -f ./$(DEPDIR)/userdb-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/userdb-passwd.Plo
+ -rm -f ./$(DEPDIR)/userdb-prefetch.Plo
+ -rm -f ./$(DEPDIR)/userdb-sql.Plo
+ -rm -f ./$(DEPDIR)/userdb-static.Plo
+ -rm -f ./$(DEPDIR)/userdb-template.Plo
+ -rm -f ./$(DEPDIR)/userdb.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-auth_moduleLTLIBRARIES \
+ install-pkginc_libHEADERS install-stats_moduleLTLIBRARIES
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS
+
+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-cache.Plo
+ -rm -f ./$(DEPDIR)/auth-client-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-fields.Plo
+ -rm -f ./$(DEPDIR)/auth-main.Po
+ -rm -f ./$(DEPDIR)/auth-master-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-penalty.Plo
+ -rm -f ./$(DEPDIR)/auth-policy.Plo
+ -rm -f ./$(DEPDIR)/auth-request-fields.Plo
+ -rm -f ./$(DEPDIR)/auth-request-handler.Plo
+ -rm -f ./$(DEPDIR)/auth-request-stats.Plo
+ -rm -f ./$(DEPDIR)/auth-request-var-expand.Plo
+ -rm -f ./$(DEPDIR)/auth-request.Plo
+ -rm -f ./$(DEPDIR)/auth-settings.Plo
+ -rm -f ./$(DEPDIR)/auth-stats.Plo
+ -rm -f ./$(DEPDIR)/auth-token.Plo
+ -rm -f ./$(DEPDIR)/auth-worker-client.Plo
+ -rm -f ./$(DEPDIR)/auth-worker-server.Plo
+ -rm -f ./$(DEPDIR)/auth.Plo
+ -rm -f ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po
+ -rm -f ./$(DEPDIR)/db-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/db-dict-cache-key.Plo
+ -rm -f ./$(DEPDIR)/db-dict.Plo
+ -rm -f ./$(DEPDIR)/db-ldap.Plo
+ -rm -f ./$(DEPDIR)/db-lua.Plo
+ -rm -f ./$(DEPDIR)/db-oauth2.Plo
+ -rm -f ./$(DEPDIR)/db-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/db-sql.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo
+ -rm -f ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo
+ -rm -f ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-mycrypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo
+ -rm -f ./$(DEPDIR)/libpassword_la-password-scheme.Plo
+ -rm -f ./$(DEPDIR)/mech-anonymous.Plo
+ -rm -f ./$(DEPDIR)/mech-apop.Plo
+ -rm -f ./$(DEPDIR)/mech-cram-md5.Plo
+ -rm -f ./$(DEPDIR)/mech-digest-md5.Plo
+ -rm -f ./$(DEPDIR)/mech-dovecot-token.Plo
+ -rm -f ./$(DEPDIR)/mech-external.Plo
+ -rm -f ./$(DEPDIR)/mech-gssapi.Plo
+ -rm -f ./$(DEPDIR)/mech-login.Plo
+ -rm -f ./$(DEPDIR)/mech-oauth2.Plo
+ -rm -f ./$(DEPDIR)/mech-otp-common.Plo
+ -rm -f ./$(DEPDIR)/mech-otp.Plo
+ -rm -f ./$(DEPDIR)/mech-plain-common.Plo
+ -rm -f ./$(DEPDIR)/mech-plain.Plo
+ -rm -f ./$(DEPDIR)/mech-scram.Plo
+ -rm -f ./$(DEPDIR)/mech-winbind.Plo
+ -rm -f ./$(DEPDIR)/mech.Plo
+ -rm -f ./$(DEPDIR)/passdb-blocking.Plo
+ -rm -f ./$(DEPDIR)/passdb-bsdauth.Plo
+ -rm -f ./$(DEPDIR)/passdb-cache.Plo
+ -rm -f ./$(DEPDIR)/passdb-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/passdb-dict.Plo
+ -rm -f ./$(DEPDIR)/passdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/passdb-lua.Plo
+ -rm -f ./$(DEPDIR)/passdb-oauth2.Plo
+ -rm -f ./$(DEPDIR)/passdb-pam.Plo
+ -rm -f ./$(DEPDIR)/passdb-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/passdb-passwd.Plo
+ -rm -f ./$(DEPDIR)/passdb-shadow.Plo
+ -rm -f ./$(DEPDIR)/passdb-sql.Plo
+ -rm -f ./$(DEPDIR)/passdb-static.Plo
+ -rm -f ./$(DEPDIR)/passdb-template.Plo
+ -rm -f ./$(DEPDIR)/passdb.Plo
+ -rm -f ./$(DEPDIR)/test-auth-request-fields.Po
+ -rm -f ./$(DEPDIR)/test-auth-request-var-expand.Po
+ -rm -f ./$(DEPDIR)/test-db-dict.Po
+ -rm -f ./$(DEPDIR)/test-lua.Po
+ -rm -f ./$(DEPDIR)/test-main.Po
+ -rm -f ./$(DEPDIR)/test-mech.Po
+ -rm -f ./$(DEPDIR)/test-mock.Po
+ -rm -f ./$(DEPDIR)/test-username-filter.Po
+ -rm -f ./$(DEPDIR)/test_auth_cache-auth-cache.Po
+ -rm -f ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po
+ -rm -f ./$(DEPDIR)/test_libpassword-test-libpassword.Po
+ -rm -f ./$(DEPDIR)/userdb-blocking.Plo
+ -rm -f ./$(DEPDIR)/userdb-checkpassword.Plo
+ -rm -f ./$(DEPDIR)/userdb-dict.Plo
+ -rm -f ./$(DEPDIR)/userdb-ldap.Plo
+ -rm -f ./$(DEPDIR)/userdb-lua.Plo
+ -rm -f ./$(DEPDIR)/userdb-passwd-file.Plo
+ -rm -f ./$(DEPDIR)/userdb-passwd.Plo
+ -rm -f ./$(DEPDIR)/userdb-prefetch.Plo
+ -rm -f ./$(DEPDIR)/userdb-sql.Plo
+ -rm -f ./$(DEPDIR)/userdb-static.Plo
+ -rm -f ./$(DEPDIR)/userdb-template.Plo
+ -rm -f ./$(DEPDIR)/userdb.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-auth_moduleLTLIBRARIES \
+ uninstall-pkginc_libHEADERS uninstall-pkglibexecPROGRAMS \
+ uninstall-stats_moduleLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-auth_moduleLTLIBRARIES clean-generic \
+ clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS clean-stats_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-auth_moduleLTLIBRARIES 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-pkglibexecPROGRAMS \
+ install-ps install-ps-am install-stats_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-auth_moduleLTLIBRARIES uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS uninstall-stats_moduleLTLIBRARIES
+
+.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/auth/auth-cache.c b/src/auth/auth-cache.c
new file mode 100644
index 0000000..e8aa105
--- /dev/null
+++ b/src/auth/auth-cache.c
@@ -0,0 +1,481 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "lib-signals.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "auth-request.h"
+#include "auth-cache.h"
+
+#include <time.h>
+
+struct auth_cache {
+ HASH_TABLE(char *, struct auth_cache_node *) hash;
+ struct auth_cache_node *head, *tail;
+
+ size_t max_size, size_left;
+ unsigned int ttl_secs, neg_ttl_secs;
+
+ unsigned int hit_count, miss_count;
+ unsigned int pos_entries, neg_entries;
+ unsigned long long pos_size, neg_size;
+};
+
+static bool
+auth_request_var_expand_tab_find(const char *key, unsigned int size,
+ unsigned int *idx_r)
+{
+ const struct var_expand_table *tab = auth_request_var_expand_static_tab;
+ unsigned int i;
+
+ for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) {
+ if (size == 1) {
+ if (key[0] == tab[i].key) {
+ *idx_r = i;
+ return TRUE;
+ }
+ } else if (tab[i].long_key != NULL) {
+ if (strncmp(key, tab[i].long_key, size) == 0 &&
+ tab[i].long_key[size] == '\0') {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static void
+auth_cache_key_add_var(string_t *str, const char *data, unsigned int len)
+{
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append_c(str, '%');
+ if (len == 1)
+ str_append_c(str, data[0]);
+ else {
+ str_append_c(str, '{');
+ str_append_data(str, data, len);
+ str_append_c(str, '}');
+ }
+}
+
+static void auth_cache_key_add_tab_idx(string_t *str, unsigned int i)
+{
+ const struct var_expand_table *tab =
+ &auth_request_var_expand_static_tab[i];
+
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append_c(str, '%');
+ if (tab->key != '\0')
+ str_append_c(str, tab->key);
+ else {
+ str_append_c(str, '{');
+ str_append(str, tab->long_key);
+ str_append_c(str, '}');
+ }
+}
+
+char *auth_cache_parse_key(pool_t pool, const char *query)
+{
+ string_t *str;
+ bool key_seen[AUTH_REQUEST_VAR_TAB_COUNT];
+ const char *extra_vars;
+ unsigned int i, idx, size, tab_idx;
+
+ memset(key_seen, 0, sizeof(key_seen));
+
+ str = t_str_new(32);
+ for (; *query != '\0'; ) {
+ if (*query != '%') {
+ query++;
+ continue;
+ }
+
+ var_get_key_range(++query, &idx, &size);
+ if (size == 0) {
+ /* broken %variable ending too early */
+ break;
+ }
+ query += idx;
+
+ if (!auth_request_var_expand_tab_find(query, size, &tab_idx)) {
+ /* just add the key. it would be nice to prevent
+ duplicates here as well, but that's just too
+ much trouble and probably very rare. */
+ auth_cache_key_add_var(str, query, size);
+ } else {
+ i_assert(tab_idx < N_ELEMENTS(key_seen));
+ key_seen[tab_idx] = TRUE;
+ }
+ query += size;
+ }
+
+ if (key_seen[AUTH_REQUEST_VAR_TAB_USERNAME_IDX] &&
+ key_seen[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX]) {
+ /* %n and %d both used -> replace with %u */
+ key_seen[AUTH_REQUEST_VAR_TAB_USER_IDX] = TRUE;
+ key_seen[AUTH_REQUEST_VAR_TAB_USERNAME_IDX] = FALSE;
+ key_seen[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX] = FALSE;
+ }
+
+ /* we rely on these being at the beginning */
+ i_assert(AUTH_REQUEST_VAR_TAB_USER_IDX == 0);
+ i_assert(AUTH_REQUEST_VAR_TAB_USERNAME_IDX == 1);
+ i_assert(AUTH_REQUEST_VAR_TAB_DOMAIN_IDX == 2);
+
+ extra_vars = t_strdup(str_c(str));
+ str_truncate(str, 0);
+ for (i = 0; i < N_ELEMENTS(key_seen); i++) {
+ if (key_seen[i])
+ auth_cache_key_add_tab_idx(str, i);
+ }
+
+ if (*extra_vars != '\0') {
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append(str, extra_vars);
+ }
+
+ return p_strdup(pool, str_c(str));
+}
+
+static void
+auth_cache_node_unlink(struct auth_cache *cache, struct auth_cache_node *node)
+{
+ if (node->prev != NULL)
+ node->prev->next = node->next;
+ else {
+ /* unlinking tail */
+ cache->tail = node->next;
+ }
+
+ if (node->next != NULL)
+ node->next->prev = node->prev;
+ else {
+ /* unlinking head */
+ cache->head = node->prev;
+ }
+}
+
+static void
+auth_cache_node_link_head(struct auth_cache *cache,
+ struct auth_cache_node *node)
+{
+ node->prev = cache->head;
+ node->next = NULL;
+
+ cache->head = node;
+ if (node->prev != NULL)
+ node->prev->next = node;
+ else
+ cache->tail = node;
+}
+
+static void
+auth_cache_node_destroy(struct auth_cache *cache, struct auth_cache_node *node)
+{
+ char *key = node->data;
+
+ auth_cache_node_unlink(cache, node);
+
+ cache->size_left += node->alloc_size;
+ hash_table_remove(cache->hash, key);
+ i_free(node);
+}
+
+static void sig_auth_cache_clear(const siginfo_t *si ATTR_UNUSED, void *context)
+{
+ struct auth_cache *cache = context;
+
+ i_info("SIGHUP received, %u cache entries flushed",
+ auth_cache_clear(cache));
+}
+
+static void sig_auth_cache_stats(const siginfo_t *si ATTR_UNUSED, void *context)
+{
+ struct auth_cache *cache = context;
+ unsigned int total_count;
+ size_t cache_used;
+
+ total_count = cache->hit_count + cache->miss_count;
+ i_info("Authentication cache hits %u/%u (%u%%)",
+ cache->hit_count, total_count,
+ total_count == 0 ? 100 : (cache->hit_count * 100 / total_count));
+
+ i_info("Authentication cache inserts: "
+ "positive: %u entries %llu bytes, "
+ "negative: %u entries %llu bytes",
+ cache->pos_entries, cache->pos_size,
+ cache->neg_entries, cache->neg_size);
+
+ cache_used = cache->max_size - cache->size_left;
+ i_info("Authentication cache current size: "
+ "%zu bytes used of %zu bytes (%u%%)",
+ cache_used, cache->max_size,
+ (unsigned int)(cache_used * 100ULL / cache->max_size));
+
+ /* reset counters */
+ cache->hit_count = cache->miss_count = 0;
+ cache->pos_entries = cache->neg_entries = 0;
+ cache->pos_size = cache->neg_size = 0;
+}
+
+struct auth_cache *auth_cache_new(size_t max_size, unsigned int ttl_secs,
+ unsigned int neg_ttl_secs
+)
+{
+ struct auth_cache *cache;
+
+ cache = i_new(struct auth_cache, 1);
+ hash_table_create(&cache->hash, default_pool, 0, str_hash, strcmp);
+ cache->max_size = max_size;
+ cache->size_left = max_size;
+ cache->ttl_secs = ttl_secs;
+ cache->neg_ttl_secs = neg_ttl_secs;
+
+ lib_signals_set_handler(SIGHUP, LIBSIG_FLAGS_SAFE,
+ sig_auth_cache_clear, cache);
+ lib_signals_set_handler(SIGUSR2, LIBSIG_FLAGS_SAFE,
+ sig_auth_cache_stats, cache);
+ return cache;
+}
+
+void auth_cache_free(struct auth_cache **_cache)
+{
+ struct auth_cache *cache = *_cache;
+
+ *_cache = NULL;
+ lib_signals_unset_handler(SIGHUP, sig_auth_cache_clear, cache);
+ lib_signals_unset_handler(SIGUSR2, sig_auth_cache_stats, cache);
+
+ auth_cache_clear(cache);
+ hash_table_destroy(&cache->hash);
+ i_free(cache);
+}
+
+unsigned int auth_cache_clear(struct auth_cache *cache)
+{
+ unsigned int ret = hash_table_count(cache->hash);
+
+ while (cache->tail != NULL)
+ auth_cache_node_destroy(cache, cache->tail);
+ hash_table_clear(cache->hash, FALSE);
+ return ret;
+}
+
+static bool auth_cache_node_is_user(struct auth_cache_node *node,
+ const char *username)
+{
+ const char *data = node->data;
+ size_t username_len;
+
+ /* The cache nodes begin with "P"/"U", passdb/userdb ID, optional
+ "+" master user, "\t" and then usually followed by the username.
+ It's too much trouble to keep track of all the cache keys, so we'll
+ just match it as if it was the username. If e.g. '%n' is used in the
+ cache key instead of '%u', it means that cache entries can be
+ removed only when @domain isn't in the username parameter. */
+ if (*data != 'P' && *data != 'U')
+ return FALSE;
+ data++;
+
+ while (*data >= '0' && *data <= '9')
+ data++;
+ if (*data == '+') {
+ /* skip over +master_user */
+ while (*data != '\t' && *data != '\0')
+ data++;
+ }
+ if (*data != '\t')
+ return FALSE;
+ data++;
+
+ username_len = strlen(username);
+ return str_begins(data, username) &&
+ (data[username_len] == '\t' || data[username_len] == '\0');
+}
+
+static bool auth_cache_node_is_one_of_users(struct auth_cache_node *node,
+ const char *const *usernames)
+{
+ unsigned int i;
+
+ for (i = 0; usernames[i] != NULL; i++) {
+ if (auth_cache_node_is_user(node, usernames[i]))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+unsigned int auth_cache_clear_users(struct auth_cache *cache,
+ const char *const *usernames)
+{
+ struct auth_cache_node *node, *next;
+ unsigned int ret = 0;
+
+ for (node = cache->tail; node != NULL; node = next) {
+ next = node->next;
+ if (auth_cache_node_is_one_of_users(node, usernames)) {
+ auth_cache_node_destroy(cache, node);
+ ret++;
+ }
+ }
+ return ret;
+}
+
+static const char *
+auth_cache_escape(const char *string,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ /* cache key %variables are separated by tabs, make sure that there
+ are no tabs in the string */
+ return str_tabescape(string);
+}
+
+static const char *
+auth_request_expand_cache_key(const struct auth_request *request,
+ const char *key, const char *username)
+{
+ static bool error_logged = FALSE;
+ const char *error;
+
+ /* Uniquely identify the request's passdb/userdb with the P/U prefix
+ and by "%!", which expands to the passdb/userdb ID number. */
+ key = t_strconcat(request->userdb_lookup ? "U" : "P", "%!",
+ request->fields.master_user == NULL ? "" : "+%{master_user}",
+ "\t", key, NULL);
+
+ /* It's fine to have unknown %variables in the cache key.
+ For example db-ldap can have pass_attrs containing
+ %{ldap:fields} which are used for output, not as part of
+ the input needed for cache_key. Those could in theory be
+ filtered out early in the cache_key, but that gets more
+ problematic when it needs to support also filtering out
+ e.g. %{sha256:ldap:fields}. */
+ string_t *value = t_str_new(128);
+ unsigned int count = 0;
+ const struct var_expand_table *table =
+ auth_request_get_var_expand_table_full(request,
+ username, auth_cache_escape, &count);
+ if (auth_request_var_expand_with_table(value, key, request, table,
+ auth_cache_escape, &error) < 0 &&
+ !error_logged) {
+ error_logged = TRUE;
+ e_error(authdb_event(request),
+ "Failed to expand auth cache key %s: %s", key, error);
+ }
+ return str_c(value);
+}
+
+const char *
+auth_cache_lookup(struct auth_cache *cache, const struct auth_request *request,
+ const char *key, struct auth_cache_node **node_r,
+ bool *expired_r, bool *neg_expired_r)
+{
+ struct auth_cache_node *node;
+ const char *value;
+ unsigned int ttl_secs;
+ time_t now;
+
+ *expired_r = FALSE;
+ *neg_expired_r = FALSE;
+
+ key = auth_request_expand_cache_key(request, key, request->fields.translated_username);
+ node = hash_table_lookup(cache->hash, key);
+ if (node == NULL) {
+ cache->miss_count++;
+ return NULL;
+ }
+
+ value = node->data + strlen(node->data) + 1;
+ ttl_secs = *value == '\0' ? cache->neg_ttl_secs : cache->ttl_secs;
+
+ now = time(NULL);
+ if (node->created < now - (time_t)ttl_secs) {
+ /* TTL expired */
+ cache->miss_count++;
+ *expired_r = TRUE;
+ } else {
+ /* move to head */
+ if (node != cache->head) {
+ auth_cache_node_unlink(cache, node);
+ auth_cache_node_link_head(cache, node);
+ }
+ cache->hit_count++;
+ }
+ if (node->created < now - (time_t)cache->neg_ttl_secs)
+ *neg_expired_r = TRUE;
+
+ if (node_r != NULL)
+ *node_r = node;
+
+ return value;
+}
+
+void auth_cache_insert(struct auth_cache *cache, struct auth_request *request,
+ const char *key, const char *value, bool last_success)
+{
+ struct auth_cache_node *node;
+ size_t data_size, alloc_size, key_len, value_len = strlen(value);
+ char *hash_key;
+
+ if (*value == '\0' && cache->neg_ttl_secs == 0) {
+ /* we're not caching negative entries */
+ return;
+ }
+
+ key = auth_request_expand_cache_key(request, key, request->fields.translated_username);
+ key_len = strlen(key);
+
+ data_size = key_len + 1 + value_len + 1;
+ alloc_size = sizeof(struct auth_cache_node) + data_size;
+
+ /* make sure we have enough space */
+ while (cache->size_left < alloc_size && cache->tail != NULL)
+ auth_cache_node_destroy(cache, cache->tail);
+
+ node = hash_table_lookup(cache->hash, key);
+ if (node != NULL) {
+ /* key is already in cache (probably expired), remove it */
+ auth_cache_node_destroy(cache, node);
+ }
+
+ /* @UNSAFE */
+ node = i_malloc(alloc_size);
+ node->created = time(NULL);
+ node->alloc_size = alloc_size;
+ node->last_success = last_success;
+ memcpy(node->data, key, key_len);
+ memcpy(node->data + key_len + 1, value, value_len);
+
+ auth_cache_node_link_head(cache, node);
+
+ cache->size_left -= alloc_size;
+ hash_key = node->data;
+ hash_table_insert(cache->hash, hash_key, node);
+
+ if (*value != '\0') {
+ cache->pos_entries++;
+ cache->pos_size += alloc_size;
+ } else {
+ cache->neg_entries++;
+ cache->neg_size += alloc_size;
+ }
+}
+
+void auth_cache_remove(struct auth_cache *cache,
+ const struct auth_request *request, const char *key)
+{
+ struct auth_cache_node *node;
+
+ key = auth_request_expand_cache_key(request, key, request->fields.user);
+ node = hash_table_lookup(cache->hash, key);
+ if (node == NULL)
+ return;
+
+ auth_cache_node_destroy(cache, node);
+}
diff --git a/src/auth/auth-cache.h b/src/auth/auth-cache.h
new file mode 100644
index 0000000..ab02fe5
--- /dev/null
+++ b/src/auth/auth-cache.h
@@ -0,0 +1,53 @@
+#ifndef AUTH_CACHE_H
+#define AUTH_CACHE_H
+
+struct auth_cache_node {
+ struct auth_cache_node *prev, *next;
+
+ time_t created;
+ /* Total number of bytes used by this node */
+ uint32_t alloc_size:31;
+ /* TRUE if the user gave the correct password the last time. */
+ bool last_success:1;
+
+ char data[]; /* key \0 value \0 */
+};
+
+struct auth_cache;
+struct auth_request;
+
+/* Parses all %x variables from query and compresses them into tab-separated
+ list, so it can be used as a cache key. */
+char *auth_cache_parse_key(pool_t pool, const char *query);
+
+/* Create a new cache. max_size specifies the maximum amount of memory in
+ bytes to use for cache (it's not fully exact). ttl_secs specifies time to
+ live for cache record, requests older than that are not used.
+ neg_ttl_secs specifies the TTL for negative entries. */
+struct auth_cache *auth_cache_new(size_t max_size, unsigned int ttl_secs,
+ unsigned int neg_ttl_secs);
+void auth_cache_free(struct auth_cache **cache);
+
+/* Clear the cache. Returns how many entries were removed. */
+unsigned int ATTR_NOWARN_UNUSED_RESULT
+auth_cache_clear(struct auth_cache *cache);
+unsigned int auth_cache_clear_users(struct auth_cache *cache,
+ const char *const *usernames);
+
+/* Look key from cache. key should be the same string as returned by
+ auth_cache_parse_key(). Returned node can't be used after any other
+ auth_cache_*() calls. */
+const char *
+auth_cache_lookup(struct auth_cache *cache, const struct auth_request *request,
+ const char *key, struct auth_cache_node **node_r,
+ bool *expired_r, bool *neg_expired_r);
+/* Insert key => value into cache. "" value means negative cache entry. */
+void auth_cache_insert(struct auth_cache *cache, struct auth_request *request,
+ const char *key, const char *value, bool last_success);
+
+/* Remove key from cache */
+void auth_cache_remove(struct auth_cache *cache,
+ const struct auth_request *request,
+ const char *key);
+
+#endif
diff --git a/src/auth/auth-client-connection.c b/src/auth/auth-client-connection.c
new file mode 100644
index 0000000..961480e
--- /dev/null
+++ b/src/auth/auth-client-connection.c
@@ -0,0 +1,456 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "llist.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "randgen.h"
+#include "safe-memset.h"
+#include "master-service.h"
+#include "mech.h"
+#include "auth-fields.h"
+#include "auth-request-handler.h"
+#include "auth-client-interface.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+
+
+#define OUTBUF_THROTTLE_SIZE (1024*50)
+
+#define AUTH_DEBUG_SENSITIVE_SUFFIX \
+ " (previous base64 data may contain sensitive data)"
+
+static void auth_client_disconnected(struct auth_client_connection **_conn);
+static void auth_client_connection_unref(struct auth_client_connection **_conn);
+static void auth_client_input(struct auth_client_connection *conn);
+
+static struct auth_client_connection *auth_client_connections;
+
+static const char *reply_line_hide_pass(const char *line)
+{
+ string_t *newline;
+ const char *p, *p2;
+
+ if (strstr(line, "pass") == NULL)
+ return line;
+
+ newline = t_str_new(strlen(line));
+
+ const char *const *fields = t_strsplit(line, "\t");
+
+ while(*fields != NULL) {
+ p = strstr(*fields, "pass");
+ p2 = strchr(*fields, '=');
+ if (p == NULL || p2 == NULL || p2 < p) {
+ str_append(newline, *fields);
+ } else {
+ /* include = */
+ str_append_data(newline, *fields, (p2 - *fields)+1);
+ str_append(newline, PASSWORD_HIDDEN_STR);
+ }
+ str_append_c(newline, '\t');
+ fields++;
+ }
+
+ return str_c(newline);
+}
+
+static void auth_client_send(struct auth_client_connection *conn,
+ const char *cmd)
+{
+ struct const_iovec iov[2];
+
+ iov[0].iov_base = cmd;
+ iov[0].iov_len = strlen(cmd);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ o_stream_nsendv(conn->output, iov, 2);
+
+ if (o_stream_get_buffer_used_size(conn->output) >=
+ OUTBUF_THROTTLE_SIZE) {
+ /* stop reading new requests until client has read the pending
+ replies. */
+ io_remove(&conn->io);
+ }
+
+ e_debug(conn->event, "client passdb out: %s",
+ conn->auth->set->debug_passwords ?
+ cmd : reply_line_hide_pass(cmd));
+}
+
+static void auth_callback(const char *reply,
+ struct auth_client_connection *conn)
+{
+ if (reply == NULL) {
+ /* handler destroyed */
+ auth_client_connection_unref(&conn);
+ } else {
+ auth_client_send(conn, reply);
+ }
+}
+
+static bool
+auth_client_input_cpid(struct auth_client_connection *conn, const char *args)
+{
+ struct auth_client_connection *old;
+ unsigned int pid;
+
+ i_assert(conn->pid == 0);
+
+ if (str_to_uint(args, &pid) < 0 || pid == 0) {
+ e_error(conn->event, "BUG: Authentication client said it's PID 0");
+ return FALSE;
+ }
+
+ if (conn->login_requests)
+ old = auth_client_connection_lookup(pid);
+ else {
+ /* the client is only authenticating, not logging in.
+ the PID isn't necessary, and since we allow authentication
+ via TCP sockets the PIDs may conflict, so ignore them. */
+ old = NULL;
+ pid = 0;
+ }
+
+ if (old != NULL) {
+ /* already exists. it's possible that it just reconnected,
+ see if the old connection is still there. */
+ i_assert(old != conn);
+ if (i_stream_read(old->input) == -1) {
+ auth_client_disconnected(&old);
+ old = NULL;
+ }
+ }
+
+ if (old != NULL) {
+ e_error(conn->event, "BUG: Authentication client gave a PID "
+ "%u of existing connection", pid);
+ return FALSE;
+ }
+
+ /* handshake complete, we can now actually start serving requests */
+ conn->refcount++;
+ conn->request_handler =
+ auth_request_handler_create(conn->token_auth, auth_callback, conn,
+ !conn->login_requests ? NULL :
+ auth_master_request_callback);
+ auth_request_handler_set(conn->request_handler, conn->connect_uid, pid);
+
+ conn->pid = pid;
+ e_debug(conn->event, "auth client connected (pid=%u)", conn->pid);
+ return TRUE;
+}
+
+static int auth_client_output(struct auth_client_connection *conn)
+{
+ if (o_stream_flush(conn->output) < 0) {
+ auth_client_disconnected(&conn);
+ return 1;
+ }
+
+ if (o_stream_get_buffer_used_size(conn->output) <=
+ OUTBUF_THROTTLE_SIZE/3 && conn->io == NULL) {
+ /* allow input again */
+ conn->io = io_add(conn->fd, IO_READ, auth_client_input, conn);
+ }
+ return 1;
+}
+
+static const char *
+auth_line_hide_pass(struct auth_client_connection *conn, const char *line)
+{
+ const char *p, *p2;
+
+ p = strstr(line, "\tresp=");
+ if (p == NULL)
+ return line;
+ p += 6;
+
+ if (conn->auth->set->debug_passwords)
+ return t_strconcat(line, AUTH_DEBUG_SENSITIVE_SUFFIX, NULL);
+
+ p2 = strchr(p, '\t');
+ return t_strconcat(t_strdup_until(line, p), PASSWORD_HIDDEN_STR,
+ p2, NULL);
+}
+
+static const char *
+cont_line_hide_pass(struct auth_client_connection *conn, const char *line)
+{
+ const char *p;
+
+ if (conn->auth->set->debug_passwords)
+ return t_strconcat(line, AUTH_DEBUG_SENSITIVE_SUFFIX, NULL);
+
+ p = strchr(line, '\t');
+ if (p == NULL)
+ return line;
+
+ return t_strconcat(t_strdup_until(line, p), PASSWORD_HIDDEN_STR, NULL);
+}
+
+static bool
+auth_client_cancel(struct auth_client_connection *conn, const char *line)
+{
+ unsigned int client_id;
+
+ if (str_to_uint(line, &client_id) < 0) {
+ e_error(conn->event, "BUG: Authentication client sent broken CANCEL");
+ return FALSE;
+ }
+
+ auth_request_handler_cancel_request(conn->request_handler, client_id);
+ return TRUE;
+}
+
+static bool
+auth_client_handle_line(struct auth_client_connection *conn, const char *line)
+{
+ if (str_begins(line, "AUTH\t")) {
+ if (conn->auth->set->debug) {
+ e_debug(conn->event, "client in: %s",
+ auth_line_hide_pass(conn, line));
+ }
+ return auth_request_handler_auth_begin(conn->request_handler,
+ line + 5);
+ }
+ if (str_begins(line, "CONT\t")) {
+ if (conn->auth->set->debug) {
+ e_debug(conn->event, "client in: %s",
+ cont_line_hide_pass(conn, line));
+ }
+ return auth_request_handler_auth_continue(conn->request_handler,
+ line + 5);
+ }
+ if (str_begins(line, "CANCEL\t")) {
+ if (conn->auth->set->debug)
+ e_debug(conn->event, "client in: %s", line);
+ return auth_client_cancel(conn, line + 7);
+ }
+
+ e_error(conn->event, "BUG: Authentication client sent unknown command: %s",
+ str_sanitize(line, 80));
+ return FALSE;
+}
+
+static void auth_client_input(struct auth_client_connection *conn)
+{
+ char *line;
+ bool ret;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ auth_client_disconnected(&conn);
+ return;
+ case -2:
+ /* buffer full */
+ e_error(conn->event, "BUG: Auth client %u sent us more than %d bytes",
+ conn->pid, (int)AUTH_CLIENT_MAX_LINE_LENGTH);
+ auth_client_connection_destroy(&conn);
+ return;
+ }
+
+ while (conn->request_handler == NULL) {
+ /* still handshaking */
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return;
+
+ if (!conn->version_received) {
+ unsigned int vmajor, vminor;
+ const char *p;
+
+ /* split the version line */
+ if (!str_begins(line, "VERSION\t") ||
+ str_parse_uint(line + 8, &vmajor, &p) < 0 ||
+ *(p++) != '\t' || str_to_uint(p, &vminor) < 0) {
+ e_error(conn->event, "Authentication client "
+ "sent invalid VERSION line: %s", line);
+ auth_client_connection_destroy(&conn);
+ return;
+ }
+ /* make sure the major version matches */
+ if (vmajor != AUTH_MASTER_PROTOCOL_MAJOR_VERSION) {
+ e_error(conn->event, "Authentication client "
+ "not compatible with this server "
+ "(mixed old and new binaries?)");
+ auth_client_connection_destroy(&conn);
+ return;
+ }
+ conn->version_minor = vminor;
+ conn->version_received = TRUE;
+ continue;
+ }
+
+ if (str_begins(line, "CPID\t")) {
+ if (!auth_client_input_cpid(conn, line + 5)) {
+ auth_client_connection_destroy(&conn);
+ return;
+ }
+ } else {
+ e_error(conn->event, "BUG: Authentication client sent "
+ "unknown handshake command: %s",
+ str_sanitize(line, 80));
+ auth_client_connection_destroy(&conn);
+ return;
+ }
+ }
+
+ conn->refcount++;
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ ret = auth_client_handle_line(conn, line);
+ safe_memset(line, 0, strlen(line));
+ } T_END;
+
+ if (!ret) {
+ struct auth_client_connection *tmp_conn = conn;
+ auth_client_connection_destroy(&tmp_conn);
+ break;
+ }
+ }
+ auth_client_connection_unref(&conn);
+}
+
+void auth_client_connection_create(struct auth *auth, int fd,
+ bool login_requests, bool token_auth)
+{
+ static unsigned int connect_uid_counter = 0;
+ struct auth_client_connection *conn;
+ const char *mechanisms;
+ string_t *str;
+
+ conn = i_new(struct auth_client_connection, 1);
+ conn->auth = auth;
+ conn->refcount = 1;
+ conn->connect_uid = ++connect_uid_counter;
+ conn->login_requests = login_requests;
+ conn->token_auth = token_auth;
+ conn->event = event_create(auth_event);
+ event_set_forced_debug(conn->event, auth->set->debug);
+ random_fill(conn->cookie, sizeof(conn->cookie));
+
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, AUTH_CLIENT_MAX_LINE_LENGTH);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ o_stream_set_flush_callback(conn->output, auth_client_output, conn);
+ conn->io = io_add(fd, IO_READ, auth_client_input, conn);
+
+ DLLIST_PREPEND(&auth_client_connections, conn);
+
+ if (token_auth) {
+ mechanisms = t_strconcat("MECH\t",
+ mech_dovecot_token.mech_name, "\n", NULL);
+ } else {
+ mechanisms = str_c(auth->reg->handshake);
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "VERSION\t%u\t%u\n%sSPID\t%s\nCUID\t%u\nCOOKIE\t",
+ AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
+ AUTH_CLIENT_PROTOCOL_MINOR_VERSION,
+ mechanisms, my_pid, conn->connect_uid);
+ binary_to_hex_append(str, conn->cookie, sizeof(conn->cookie));
+ str_append(str, "\nDONE\n");
+
+ if (o_stream_send(conn->output, str_data(str), str_len(str)) < 0)
+ auth_client_disconnected(&conn);
+}
+
+void auth_client_connection_destroy(struct auth_client_connection **_conn)
+{
+ struct auth_client_connection *conn = *_conn;
+
+ *_conn = NULL;
+ if (conn->fd == -1)
+ return;
+
+ DLLIST_REMOVE(&auth_client_connections, conn);
+
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+
+ io_remove(&conn->io);
+
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+
+ if (conn->request_handler != NULL) {
+ auth_request_handler_abort_requests(conn->request_handler);
+ auth_request_handler_destroy(&conn->request_handler);
+ }
+
+ master_service_client_connection_destroyed(master_service);
+ auth_client_connection_unref(&conn);
+}
+
+static void auth_client_disconnected(struct auth_client_connection **_conn)
+{
+ struct auth_client_connection *conn = *_conn;
+ unsigned int request_count;
+ int err;
+
+ *_conn = NULL;
+
+ if (conn->input->stream_errno != 0)
+ err = conn->input->stream_errno;
+ else if (conn->output->stream_errno != 0)
+ err = conn->output->stream_errno;
+ else
+ err = 0;
+
+ request_count = conn->request_handler == NULL ? 0 :
+ auth_request_handler_get_request_count(conn->request_handler);
+ if (request_count > 0) {
+ e_error(conn->event, "auth client %u disconnected with %u "
+ "pending requests: %s", conn->pid, request_count,
+ err == 0 ? "EOF" : strerror(err));
+ }
+ auth_client_connection_destroy(&conn);
+}
+
+static void auth_client_connection_unref(struct auth_client_connection **_conn)
+{
+ struct auth_client_connection *conn = *_conn;
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ event_unref(&conn->event);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ i_free(conn);
+}
+
+struct auth_client_connection *
+auth_client_connection_lookup(unsigned int pid)
+{
+ struct auth_client_connection *conn;
+
+ for (conn = auth_client_connections; conn != NULL; conn = conn->next) {
+ if (conn->pid == pid)
+ return conn;
+ }
+ return NULL;
+}
+
+void auth_client_connections_destroy_all(void)
+{
+ struct auth_client_connection *conn;
+
+ while (auth_client_connections != NULL) {
+ conn = auth_client_connections;
+ auth_client_connection_destroy(&conn);
+ }
+}
diff --git a/src/auth/auth-client-connection.h b/src/auth/auth-client-connection.h
new file mode 100644
index 0000000..7b10f76
--- /dev/null
+++ b/src/auth/auth-client-connection.h
@@ -0,0 +1,37 @@
+#ifndef AUTH_CLIENT_CONNECTION_H
+#define AUTH_CLIENT_CONNECTION_H
+
+#include "master-auth.h"
+
+struct auth_client_connection {
+ struct auth_client_connection *prev, *next;
+ struct auth *auth;
+ struct event *event;
+ int refcount;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ unsigned int version_minor;
+ unsigned int pid;
+ unsigned int connect_uid;
+ uint8_t cookie[MASTER_AUTH_COOKIE_SIZE];
+ struct auth_request_handler *request_handler;
+
+ bool login_requests:1;
+ bool version_received:1;
+ bool token_auth:1;
+};
+
+void auth_client_connection_create(struct auth *auth, int fd,
+ bool login_requests, bool token_auth);
+void auth_client_connection_destroy(struct auth_client_connection **conn);
+
+struct auth_client_connection *
+auth_client_connection_lookup(unsigned int pid);
+
+void auth_client_connections_destroy_all(void);
+
+#endif
diff --git a/src/auth/auth-common.h b/src/auth/auth-common.h
new file mode 100644
index 0000000..5ebe8c4
--- /dev/null
+++ b/src/auth/auth-common.h
@@ -0,0 +1,17 @@
+#ifndef AUTH_COMMON_H
+#define AUTH_COMMON_H
+
+#include "lib.h"
+#include "auth.h"
+
+extern bool worker, worker_restart_request;
+extern time_t process_start_time;
+extern struct auth_penalty *auth_penalty;
+extern struct event_category event_category_auth;
+extern struct event *auth_event;
+
+void auth_refresh_proctitle(void);
+void auth_worker_refresh_proctitle(const char *state);
+void auth_module_load(const char *names);
+
+#endif
diff --git a/src/auth/auth-fields.c b/src/auth/auth-fields.c
new file mode 100644
index 0000000..0771390
--- /dev/null
+++ b/src/auth/auth-fields.c
@@ -0,0 +1,226 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "auth-request.h"
+#include "auth-fields.h"
+
+struct auth_fields {
+ pool_t pool;
+ ARRAY_TYPE(auth_field) fields, snapshot_fields;
+ unsigned int snapshot_idx;
+ bool snapshotted;
+};
+
+struct auth_fields *auth_fields_init(pool_t pool)
+{
+ struct auth_fields *fields;
+
+ fields = p_new(pool, struct auth_fields, 1);
+ fields->pool = pool;
+ return fields;
+}
+
+static void auth_fields_snapshot_preserve(struct auth_fields *fields)
+{
+ if (!fields->snapshotted || array_is_created(&fields->snapshot_fields))
+ return;
+
+ p_array_init(&fields->snapshot_fields, fields->pool,
+ array_count(&fields->fields));
+ array_append_array(&fields->snapshot_fields, &fields->fields);
+}
+
+static bool
+auth_fields_find_idx(struct auth_fields *fields, const char *key,
+ unsigned int *idx_r)
+{
+ const struct auth_field *f;
+ unsigned int i, count;
+
+ if (!array_is_created(&fields->fields))
+ return FALSE;
+
+ f = array_get(&fields->fields, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(f[i].key, key) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void auth_fields_add(struct auth_fields *fields,
+ const char *key, const char *value,
+ enum auth_field_flags flags)
+{
+ struct auth_field *field;
+ unsigned int idx;
+
+ i_assert(*key != '\0');
+ i_assert(strchr(key, '\t') == NULL &&
+ strchr(key, '\n') == NULL);
+
+ if (!auth_fields_find_idx(fields, key, &idx)) {
+ if (!array_is_created(&fields->fields))
+ p_array_init(&fields->fields, fields->pool, 16);
+
+ field = array_append_space(&fields->fields);
+ field->key = p_strdup(fields->pool, key);
+ } else {
+ auth_fields_snapshot_preserve(fields);
+ field = array_idx_modifiable(&fields->fields, idx);
+ }
+ field->value = p_strdup_empty(fields->pool, value);
+ field->flags = flags | AUTH_FIELD_FLAG_CHANGED;
+}
+
+void auth_fields_remove(struct auth_fields *fields, const char *key)
+{
+ unsigned int idx;
+
+ if (auth_fields_find_idx(fields, key, &idx)) {
+ auth_fields_snapshot_preserve(fields);
+ array_delete(&fields->fields, idx, 1);
+ }
+}
+
+const char *auth_fields_find(struct auth_fields *fields, const char *key)
+{
+ const struct auth_field *field;
+ unsigned int idx;
+
+ if (!auth_fields_find_idx(fields, key, &idx))
+ return NULL;
+
+ field = array_idx(&fields->fields, idx);
+ return field->value == NULL ? "" : field->value;
+}
+
+bool auth_fields_exists(struct auth_fields *fields, const char *key)
+{
+ return auth_fields_find(fields, key) != NULL;
+}
+
+void auth_fields_reset(struct auth_fields *fields)
+{
+ if (array_is_created(&fields->fields)) {
+ auth_fields_snapshot_preserve(fields);
+ array_clear(&fields->fields);
+ }
+}
+
+void auth_fields_import_prefixed(struct auth_fields *fields, const char *prefix,
+ const char *str, enum auth_field_flags flags)
+{
+ T_BEGIN {
+ const char *const *arg = t_strsplit_tabescaped(str);
+ const char *key, *value;
+
+ for (; *arg != NULL; arg++) {
+ value = strchr(*arg, '=');
+ if (value == NULL) {
+ key = *arg;
+ value = NULL;
+ } else {
+ key = t_strdup_until(*arg, value++);
+ if (*prefix != '\0')
+ key = t_strconcat(prefix, key, NULL);
+ }
+ auth_fields_add(fields, key, value, flags);
+ }
+ } T_END;
+}
+
+void auth_fields_import(struct auth_fields *fields, const char *str,
+ enum auth_field_flags flags)
+{
+ auth_fields_import_prefixed(fields, "", str, flags);
+}
+
+const ARRAY_TYPE(auth_field) *auth_fields_export(struct auth_fields *fields)
+{
+ if (!array_is_created(&fields->fields))
+ p_array_init(&fields->fields, fields->pool, 1);
+ return &fields->fields;
+}
+
+void auth_fields_append(struct auth_fields *fields, string_t *dest,
+ enum auth_field_flags flags_mask,
+ enum auth_field_flags flags_result)
+{
+ const struct auth_field *f;
+ unsigned int i, count;
+ bool first = TRUE;
+
+ if (!array_is_created(&fields->fields))
+ return;
+
+ f = array_get(&fields->fields, &count);
+ for (i = 0; i < count; i++) {
+ if ((f[i].flags & flags_mask) != flags_result)
+ continue;
+
+ if (first)
+ first = FALSE;
+ else
+ str_append_c(dest, '\t');
+ str_append(dest, f[i].key);
+ if (f[i].value != NULL) {
+ str_append_c(dest, '=');
+ str_append_tabescaped(dest, f[i].value);
+ }
+ }
+}
+
+bool auth_fields_is_empty(struct auth_fields *fields)
+{
+ return fields == NULL || !array_is_created(&fields->fields) ||
+ array_count(&fields->fields) == 0;
+}
+
+void auth_fields_booleanize(struct auth_fields *fields, const char *key)
+{
+ struct auth_field *field;
+ unsigned int idx;
+
+ if (auth_fields_find_idx(fields, key, &idx)) {
+ field = array_idx_modifiable(&fields->fields, idx);
+ field->value = NULL;
+ }
+}
+
+void auth_fields_snapshot(struct auth_fields *fields)
+{
+ struct auth_field *field;
+
+ fields->snapshotted = TRUE;
+ if (!array_is_created(&fields->fields))
+ return;
+
+ if (!array_is_created(&fields->snapshot_fields)) {
+ /* try to avoid creating this array */
+ fields->snapshot_idx = array_count(&fields->fields);
+ } else {
+ array_clear(&fields->snapshot_fields);
+ array_append_array(&fields->snapshot_fields, &fields->fields);
+ }
+ array_foreach_modifiable(&fields->fields, field)
+ field->flags &= ENUM_NEGATE(AUTH_FIELD_FLAG_CHANGED);
+}
+
+void auth_fields_rollback(struct auth_fields *fields)
+{
+ if (array_is_created(&fields->snapshot_fields)) {
+ array_clear(&fields->fields);
+ array_append_array(&fields->fields, &fields->snapshot_fields);
+ } else if (array_is_created(&fields->fields)) {
+ array_delete(&fields->fields, fields->snapshot_idx,
+ array_count(&fields->fields) -
+ fields->snapshot_idx);
+ }
+}
diff --git a/src/auth/auth-fields.h b/src/auth/auth-fields.h
new file mode 100644
index 0000000..25c91c7
--- /dev/null
+++ b/src/auth/auth-fields.h
@@ -0,0 +1,48 @@
+#ifndef AUTH_FIELDS_H
+#define AUTH_FIELDS_H
+
+struct auth_request;
+
+enum auth_field_flags {
+ /* This field is internal to auth process and won't be sent to client */
+ AUTH_FIELD_FLAG_HIDDEN = 0x01,
+ /* Changed since last snapshot. Set/cleared automatically. */
+ AUTH_FIELD_FLAG_CHANGED = 0x02
+};
+
+struct auth_field {
+ const char *key, *value;
+ enum auth_field_flags flags;
+};
+ARRAY_DEFINE_TYPE(auth_field, struct auth_field);
+
+struct auth_fields *auth_fields_init(pool_t pool);
+void auth_fields_add(struct auth_fields *fields,
+ const char *key, const char *value,
+ enum auth_field_flags flags) ATTR_NULL(3);
+void auth_fields_reset(struct auth_fields *fields);
+void auth_fields_remove(struct auth_fields *fields, const char *key);
+
+const char *auth_fields_find(struct auth_fields *fields, const char *key);
+bool auth_fields_exists(struct auth_fields *fields, const char *key);
+
+void auth_fields_import(struct auth_fields *fields, const char *str,
+ enum auth_field_flags flags);
+void auth_fields_import_prefixed(struct auth_fields *fields, const char *prefix,
+ const char *str, enum auth_field_flags flags);
+const ARRAY_TYPE(auth_field) *auth_fields_export(struct auth_fields *fields);
+/* Append fields where (flag & flags_mask) == flags_result. */
+void auth_fields_append(struct auth_fields *fields, string_t *dest,
+ enum auth_field_flags flags_mask,
+ enum auth_field_flags flags_result);
+bool auth_fields_is_empty(struct auth_fields *fields);
+/* If the field exists, clear its value (so the exported string will be "key"
+ instead of e.g. "key=y"). */
+void auth_fields_booleanize(struct auth_fields *fields, const char *key);
+
+/* Remember the current fields. */
+void auth_fields_snapshot(struct auth_fields *fields);
+/* Rollback to previous snapshot, or clear the fields if there isn't any. */
+void auth_fields_rollback(struct auth_fields *fields);
+
+#endif
diff --git a/src/auth/auth-master-connection.c b/src/auth/auth-master-connection.c
new file mode 100644
index 0000000..3f439b8
--- /dev/null
+++ b/src/auth/auth-master-connection.c
@@ -0,0 +1,855 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "buffer.h"
+#include "hash.h"
+#include "llist.h"
+#include "str.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "time-util.h"
+#include "hostpid.h"
+#include "hex-binary.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ipwd.h"
+#include "master-service.h"
+#include "userdb.h"
+#include "userdb-blocking.h"
+#include "master-interface.h"
+#include "passdb-cache.h"
+#include "auth-request-handler.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE 1024
+#define MAX_OUTBUF_SIZE (1024*50)
+
+struct master_userdb_request {
+ struct auth_master_connection *conn;
+ struct auth_request *auth_request;
+};
+
+struct master_list_iter_ctx {
+ struct auth_master_connection *conn;
+ struct userdb_iterate_context *iter;
+ struct auth_request *auth_request;
+ bool failed;
+};
+
+static void master_input(struct auth_master_connection *conn);
+
+static struct auth_master_connection *auth_master_connections;
+
+static const char *
+auth_master_reply_hide_passwords(struct auth_master_connection *conn,
+ const char *str)
+{
+ char **args, *p, *p2;
+ unsigned int i;
+
+ if (conn->auth->set->debug_passwords)
+ return str;
+
+ /* hide all parameters that have "pass" in their key */
+ args = p_strsplit(pool_datastack_create(), str, "\t");
+ for (i = 0; args[i] != NULL; i++) {
+ p = strstr(args[i], "pass");
+ p2 = strchr(args[i], '=');
+ if (p != NULL && p < p2) {
+ *p2 = '\0';
+ args[i] = p_strconcat(pool_datastack_create(),
+ args[i], "=<hidden>", NULL);
+ }
+ }
+ return t_strarray_join((void *)args, "\t");
+}
+
+void auth_master_request_callback(const char *reply, struct auth_master_connection *conn)
+{
+ struct const_iovec iov[2];
+
+ e_debug(auth_event, "master userdb out: %s",
+ auth_master_reply_hide_passwords(conn, reply));
+
+ iov[0].iov_base = reply;
+ iov[0].iov_len = strlen(reply);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+
+ o_stream_nsendv(conn->output, iov, 2);
+}
+
+static const char *
+auth_master_event_log_callback(struct auth_master_connection *conn,
+ enum log_type log_type ATTR_UNUSED,
+ const char *message)
+{
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "auth-master client: %s (created %d msecs ago", message,
+ timeval_diff_msecs(&ioloop_timeval, &conn->create_time));
+ if (conn->handshake_time.tv_sec != 0) {
+ str_printfa(str, ", handshake %d msecs ago",
+ timeval_diff_msecs(&ioloop_timeval, &conn->create_time));
+ }
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static bool
+master_input_request(struct auth_master_connection *conn, const char *args)
+{
+ struct auth_client_connection *client_conn;
+ const char *const *list, *const *params;
+ unsigned int id, client_pid, client_id;
+ uint8_t cookie[MASTER_AUTH_COOKIE_SIZE];
+ buffer_t buf;
+
+ /* <id> <client-pid> <client-id> <cookie> [<parameters>] */
+ list = t_strsplit_tabescaped(args);
+ if (str_array_length(list) < 4 ||
+ str_to_uint(list[0], &id) < 0 ||
+ str_to_uint(list[1], &client_pid) < 0 ||
+ str_to_uint(list[2], &client_id) < 0) {
+ e_error(conn->event, "BUG: Master sent broken REQUEST");
+ return FALSE;
+ }
+
+ buffer_create_from_data(&buf, cookie, sizeof(cookie));
+ if (hex_to_binary(list[3], &buf) < 0) {
+ e_error(conn->event, "BUG: Master sent broken REQUEST cookie");
+ return FALSE;
+ }
+ params = list + 4;
+
+ client_conn = auth_client_connection_lookup(client_pid);
+ if (client_conn == NULL) {
+ e_error(conn->event,
+ "Master requested auth for nonexistent client %u",
+ client_pid);
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("FAIL\t%u\n", id));
+ } else if (!mem_equals_timing_safe(client_conn->cookie, cookie, sizeof(cookie))) {
+ e_error(conn->event,
+ "Master requested auth for client %u with invalid cookie",
+ client_pid);
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("FAIL\t%u\n", id));
+ } else if (!auth_request_handler_master_request(
+ client_conn->request_handler, conn, id, client_id, params)) {
+ e_error(conn->event,
+ "Master requested auth for non-login client %u",
+ client_pid);
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("FAIL\t%u\n", id));
+ }
+ return TRUE;
+}
+
+static bool
+master_input_cache_flush(struct auth_master_connection *conn, const char *args)
+{
+ const char *const *list;
+ unsigned int count;
+
+ /* <id> [<user> [<user> [..]] */
+ list = t_strsplit_tabescaped(args);
+ if (list[0] == NULL) {
+ e_error(conn->event, "BUG: doveadm sent broken CACHE-FLUSH");
+ return FALSE;
+ }
+
+ if (passdb_cache == NULL) {
+ /* cache disabled */
+ count = 0;
+ } else if (list[1] == NULL) {
+ /* flush the whole cache */
+ count = auth_cache_clear(passdb_cache);
+ } else {
+ count = auth_cache_clear_users(passdb_cache, list+1);
+ }
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("OK\t%s\t%u\n", list[0], count));
+ return TRUE;
+}
+
+static int
+master_input_auth_request(struct auth_master_connection *conn, const char *args,
+ const char *cmd, struct auth_request **request_r,
+ const char **error_r)
+{
+ struct auth_request *auth_request;
+ const char *const *list, *name, *arg, *username;
+ unsigned int id;
+
+ /* <id> <userid> [<parameters>] */
+ list = t_strsplit_tabescaped(args);
+ if (list[0] == NULL || list[1] == NULL ||
+ str_to_uint(list[0], &id) < 0) {
+ e_error(conn->event, "BUG: Master sent broken %s", cmd);
+ return -1;
+ }
+
+ auth_request = auth_request_new_dummy(auth_event);
+ auth_request->id = id;
+ auth_request->master = conn;
+ auth_master_connection_ref(conn);
+ username = list[1];
+
+ for (list += 2; *list != NULL; list++) {
+ arg = strchr(*list, '=');
+ if (arg == NULL) {
+ name = *list;
+ arg = "";
+ } else {
+ name = t_strdup_until(*list, arg);
+ arg++;
+ }
+
+ (void)auth_request_import_info(auth_request, name, arg);
+ }
+
+ if (auth_request->fields.service == NULL) {
+ e_error(conn->event,
+ "BUG: Master sent %s request without service", cmd);
+ auth_request_unref(&auth_request);
+ auth_master_connection_unref(&conn);
+ return -1;
+ }
+
+ auth_request_init(auth_request);
+
+ if (!auth_request_set_username(auth_request, username, error_r)) {
+ *request_r = auth_request;
+ return 0;
+ }
+ *request_r = auth_request;
+ return 1;
+}
+
+static int
+user_verify_restricted_uid(struct auth_request *auth_request)
+{
+ struct auth_master_connection *conn = auth_request->master;
+ struct auth_fields *reply = auth_request->fields.userdb_reply;
+ const char *value, *reason;
+ uid_t uid;
+
+ if (conn->userdb_restricted_uid == 0)
+ return 0;
+
+ value = auth_fields_find(reply, "uid");
+ if (value == NULL)
+ reason = "userdb reply doesn't contain uid";
+ else if (str_to_uid(value, &uid) < 0)
+ reason = "userdb reply contains invalid uid";
+ else if (uid != conn->userdb_restricted_uid) {
+ reason = t_strdup_printf(
+ "userdb uid (%s) doesn't match peer uid (%s)",
+ dec2str(uid), dec2str(conn->userdb_restricted_uid));
+ } else {
+ return 0;
+ }
+
+ auth_request_log_error(auth_request, "userdb",
+ "client doesn't have lookup permissions for this user: %s "
+ "(to bypass this check, set: service auth { unix_listener %s { mode=0777 } })",
+ reason, conn->path);
+ return -1;
+}
+
+static void
+user_callback(enum userdb_result result,
+ struct auth_request *auth_request)
+{
+ struct auth_master_connection *conn = auth_request->master;
+ string_t *str;
+ const char *value;
+
+ if (auth_request->userdb_lookup_tempfailed)
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+
+ if (result == USERDB_RESULT_OK) {
+ if (user_verify_restricted_uid(auth_request) < 0)
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ str = t_str_new(128);
+ switch (result) {
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ str_printfa(str, "FAIL\t%u", auth_request->id);
+ if (auth_request->userdb_lookup_tempfailed) {
+ value = auth_fields_find(auth_request->fields.userdb_reply,
+ "reason");
+ if (value != NULL)
+ str_printfa(str, "\treason=%s", value);
+ }
+ break;
+ case USERDB_RESULT_USER_UNKNOWN:
+ str_printfa(str, "NOTFOUND\t%u", auth_request->id);
+ break;
+ case USERDB_RESULT_OK:
+ str_printfa(str, "USER\t%u\t", auth_request->id);
+ str_append_tabescaped(str, auth_request->fields.user);
+ str_append_c(str, '\t');
+ auth_fields_append(auth_request->fields.userdb_reply, str,
+ AUTH_FIELD_FLAG_HIDDEN, 0);
+ break;
+ }
+
+ e_debug(auth_event, "userdb out: %s",
+ auth_master_reply_hide_passwords(conn, str_c(str)));
+
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+
+ auth_request_unref(&auth_request);
+ auth_master_connection_unref(&conn);
+}
+
+static bool
+master_input_user(struct auth_master_connection *conn, const char *args)
+{
+ struct auth_request *auth_request;
+ const char *error;
+ int ret;
+
+ ret = master_input_auth_request(conn, args, "USER",
+ &auth_request, &error);
+ if (ret <= 0) {
+ if (ret < 0)
+ return FALSE;
+ auth_request_log_info(auth_request, "userdb", "%s", error);
+ user_callback(USERDB_RESULT_USER_UNKNOWN, auth_request);
+ } else {
+ auth_request_set_state(auth_request, AUTH_REQUEST_STATE_USERDB);
+ auth_request_lookup_user(auth_request, user_callback);
+ }
+ return TRUE;
+}
+
+static void pass_callback_finish(struct auth_request *auth_request,
+ enum passdb_result result)
+{
+ struct auth_master_connection *conn = auth_request->master;
+ string_t *str;
+
+ str = t_str_new(128);
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ if (auth_request->failed || !auth_request->passdb_success) {
+ str_printfa(str, "FAIL\t%u", auth_request->id);
+ break;
+ }
+ str_printfa(str, "PASS\t%u\tuser=", auth_request->id);
+ str_append_tabescaped(str, auth_request->fields.user);
+ if (!auth_fields_is_empty(auth_request->fields.extra_fields)) {
+ str_append_c(str, '\t');
+ auth_fields_append(auth_request->fields.extra_fields,
+ str, AUTH_FIELD_FLAG_HIDDEN, 0);
+ }
+ break;
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_USER_DISABLED:
+ case PASSDB_RESULT_PASS_EXPIRED:
+ str_printfa(str, "NOTFOUND\t%u", auth_request->id);
+ break;
+ case PASSDB_RESULT_NEXT:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ str_printfa(str, "FAIL\t%u", auth_request->id);
+ break;
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ str_printfa(str, "FAIL\t%u\treason=Configured passdbs don't support credentials lookups",
+ auth_request->id);
+ break;
+ }
+
+ e_debug(auth_event, "passdb out: %s", str_c(str));
+
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+
+ auth_request_unref(&auth_request);
+ auth_master_connection_unref(&conn);
+}
+
+static void
+auth_master_pass_proxy_finish(bool success, struct auth_request *auth_request)
+{
+ pass_callback_finish(auth_request, success ? PASSDB_RESULT_OK :
+ PASSDB_RESULT_INTERNAL_FAILURE);
+}
+
+static void
+pass_callback(enum passdb_result result,
+ const unsigned char *credentials ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ struct auth_request *auth_request)
+{
+ int ret;
+
+ if (result != PASSDB_RESULT_OK)
+ auth_request_proxy_finish_failure(auth_request);
+ else {
+ ret = auth_request_proxy_finish(auth_request,
+ auth_master_pass_proxy_finish);
+ if (ret == 0)
+ return;
+ if (ret < 0)
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ pass_callback_finish(auth_request, result);
+}
+
+static const char *auth_restricted_reason(struct auth_master_connection *conn)
+{
+ struct passwd pw;
+ const char *namestr;
+
+ if (i_getpwuid(conn->userdb_restricted_uid, &pw) <= 0)
+ namestr = "";
+ else
+ namestr = t_strdup_printf("(%s)", pw.pw_name);
+ return t_strdup_printf("%s mode=0666, but not owned by UID %lu%s",
+ conn->path,
+ (unsigned long)conn->userdb_restricted_uid,
+ namestr);
+}
+
+static bool
+master_input_pass(struct auth_master_connection *conn, const char *args)
+{
+ struct auth_request *auth_request;
+ const char *error;
+ int ret;
+
+ ret = master_input_auth_request(conn, args, "PASS",
+ &auth_request, &error);
+ if (ret <= 0) {
+ if (ret < 0)
+ return FALSE;
+ auth_request_log_info(auth_request, "passdb", "%s", error);
+ pass_callback(PASSDB_RESULT_USER_UNKNOWN,
+ uchar_empty_ptr, 0, auth_request);
+ } else if (conn->userdb_restricted_uid != 0) {
+ /* no permissions to do this lookup */
+ auth_request_log_error(auth_request, "passdb",
+ "Auth client doesn't have permissions to do "
+ "a PASS lookup: %s", auth_restricted_reason(conn));
+ pass_callback(PASSDB_RESULT_INTERNAL_FAILURE,
+ uchar_empty_ptr, 0, auth_request);
+ } else {
+ auth_request_set_state(auth_request,
+ AUTH_REQUEST_STATE_MECH_CONTINUE);
+ auth_request_lookup_credentials(auth_request, "",
+ pass_callback);
+ }
+ return TRUE;
+}
+
+static void master_input_list_finish(struct master_list_iter_ctx *ctx)
+{
+ i_assert(ctx->conn->iter_ctx == ctx);
+
+ ctx->conn->iter_ctx = NULL;
+ ctx->conn->io = io_add(ctx->conn->fd, IO_READ, master_input, ctx->conn);
+
+ if (ctx->iter != NULL)
+ (void)userdb_blocking_iter_deinit(&ctx->iter);
+ o_stream_uncork(ctx->conn->output);
+ o_stream_unset_flush_callback(ctx->conn->output);
+ auth_request_unref(&ctx->auth_request);
+ auth_master_connection_unref(&ctx->conn);
+ i_free(ctx);
+}
+
+static int master_output_list(struct master_list_iter_ctx *ctx)
+{
+ int ret;
+
+ if ((ret = o_stream_flush(ctx->conn->output)) < 0) {
+ master_input_list_finish(ctx);
+ return 1;
+ }
+ if (ret > 0) {
+ o_stream_cork(ctx->conn->output);
+ userdb_blocking_iter_next(ctx->iter);
+ }
+ return 1;
+}
+
+static void master_input_list_callback(const char *user, void *context)
+{
+ struct master_list_iter_ctx *ctx = context;
+ struct auth_userdb *userdb = ctx->auth_request->userdb;
+ int ret;
+
+ if (user == NULL) {
+ if (userdb_blocking_iter_deinit(&ctx->iter) < 0)
+ ctx->failed = TRUE;
+
+ do {
+ userdb = userdb->next;
+ } while (userdb != NULL &&
+ userdb->userdb->iface->iterate_init == NULL);
+ if (userdb == NULL) {
+ /* iteration is finished */
+ const char *str;
+
+ str = t_strdup_printf("DONE\t%u\t%s\n",
+ ctx->auth_request->id,
+ ctx->failed ? "fail" : "");
+ o_stream_nsend_str(ctx->conn->output, str);
+ master_input_list_finish(ctx);
+ return;
+ }
+
+ /* continue iterating next userdb */
+ ctx->auth_request->userdb = userdb;
+ ctx->iter = userdb_blocking_iter_init(ctx->auth_request,
+ master_input_list_callback, ctx);
+ return;
+ }
+
+ T_BEGIN {
+ const char *str;
+
+ str = t_strdup_printf("LIST\t%u\t%s\n", ctx->auth_request->id,
+ str_tabescape(user));
+ ret = o_stream_send_str(ctx->conn->output, str);
+ } T_END;
+ if (o_stream_get_buffer_used_size(ctx->conn->output) >= MAX_OUTBUF_SIZE)
+ ret = o_stream_flush(ctx->conn->output);
+ if (ret < 0) {
+ /* disconnected, don't bother finishing */
+ master_input_list_finish(ctx);
+ return;
+ }
+ if (o_stream_get_buffer_used_size(ctx->conn->output) < MAX_OUTBUF_SIZE)
+ userdb_blocking_iter_next(ctx->iter);
+ else
+ o_stream_uncork(ctx->conn->output);
+}
+
+static bool
+master_input_list(struct auth_master_connection *conn, const char *args)
+{
+ struct auth_userdb *userdb = conn->auth->userdbs;
+ struct auth_request *auth_request;
+ struct master_list_iter_ctx *ctx;
+ const char *str, *name, *arg, *const *list;
+ unsigned int id;
+
+ /* <id> [<parameters>] */
+ list = t_strsplit_tabescaped(args);
+ if (list[0] == NULL || str_to_uint(list[0], &id) < 0) {
+ e_error(conn->event, "BUG: Master sent broken LIST");
+ return FALSE;
+ }
+ list++;
+
+ if (conn->iter_ctx != NULL) {
+ e_error(conn->event,
+ "Auth client is already iterating users");
+ str = t_strdup_printf("DONE\t%u\tfail\n", id);
+ o_stream_nsend_str(conn->output, str);
+ return TRUE;
+ }
+
+ if (conn->userdb_restricted_uid != 0) {
+ e_error(conn->event,
+ "Auth client doesn't have permissions to list users: %s",
+ auth_restricted_reason(conn));
+ str = t_strdup_printf("DONE\t%u\tfail\n", id);
+ o_stream_nsend_str(conn->output, str);
+ return TRUE;
+ }
+
+ while (userdb != NULL && userdb->userdb->iface->iterate_init == NULL)
+ userdb = userdb->next;
+ if (userdb == NULL) {
+ e_error(conn->event,
+ "Trying to iterate users, but userdbs don't support it");
+ str = t_strdup_printf("DONE\t%u\tfail\n", id);
+ o_stream_nsend_str(conn->output, str);
+ return TRUE;
+ }
+
+ auth_request = auth_request_new_dummy(auth_event);
+ auth_request->id = id;
+ auth_request->master = conn;
+ auth_master_connection_ref(conn);
+
+ for (; *list != NULL; list++) {
+ arg = strchr(*list, '=');
+ if (arg == NULL) {
+ name = *list;
+ arg = "";
+ } else {
+ name = t_strdup_until(*list, arg);
+ arg++;
+ }
+
+ if (!auth_request_import_info(auth_request, name, arg) &&
+ strcmp(name, "user") == 0) {
+ /* username mask */
+ auth_request_set_username_forced(auth_request, arg);
+ }
+ }
+
+ /* rest of the code doesn't like NULL user or service */
+ if (auth_request->fields.user == NULL)
+ auth_request_set_username_forced(auth_request, "");
+ if (auth_request->fields.service == NULL) {
+ if (!auth_request_import(auth_request, "service", ""))
+ i_unreached();
+ i_assert(auth_request->fields.service != NULL);
+ }
+
+ ctx = i_new(struct master_list_iter_ctx, 1);
+ ctx->conn = conn;
+ ctx->auth_request = auth_request;
+ ctx->auth_request->userdb = userdb;
+
+ io_remove(&conn->io);
+ o_stream_cork(conn->output);
+ o_stream_set_flush_callback(conn->output, master_output_list, ctx);
+ ctx->iter = userdb_blocking_iter_init(auth_request,
+ master_input_list_callback, ctx);
+ conn->iter_ctx = ctx;
+ return TRUE;
+}
+
+static bool
+auth_master_input_line(struct auth_master_connection *conn, const char *line)
+{
+ e_debug(auth_event, "master in: %s", line);
+
+ if (str_begins(line, "USER\t"))
+ return master_input_user(conn, line + 5);
+ if (str_begins(line, "LIST\t"))
+ return master_input_list(conn, line + 5);
+ if (str_begins(line, "PASS\t"))
+ return master_input_pass(conn, line + 5);
+
+ if (!conn->userdb_only) {
+ i_assert(conn->userdb_restricted_uid == 0);
+ if (str_begins(line, "REQUEST\t"))
+ return master_input_request(conn, line + 8);
+ if (str_begins(line, "CACHE-FLUSH\t"))
+ return master_input_cache_flush(conn, line + 12);
+ if (str_begins(line, "CPID\t")) {
+ e_error(conn->event,
+ "Authentication client trying to connect to "
+ "master socket");
+ return FALSE;
+ }
+ }
+
+ e_error(conn->event, "BUG: Unknown command in %s socket: %s",
+ conn->userdb_only ? "userdb" : "master",
+ str_sanitize(line, 80));
+ return FALSE;
+}
+
+static void master_input(struct auth_master_connection *conn)
+{
+ char *line;
+ bool ret;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ auth_master_connection_destroy(&conn);
+ return;
+ case -2:
+ /* buffer full */
+ e_error(conn->event, "BUG: Master sent us more than %d bytes",
+ (int)MAX_INBUF_SIZE);
+ auth_master_connection_destroy(&conn);
+ return;
+ }
+
+ if (!conn->version_received) {
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return;
+
+ /* make sure the major version matches */
+ if (!str_begins(line, "VERSION\t") ||
+ !str_uint_equals(t_strcut(line + 8, '\t'),
+ AUTH_MASTER_PROTOCOL_MAJOR_VERSION)) {
+ e_error(conn->event,
+ "Master not compatible with this server "
+ "(mixed old and new binaries?)");
+ auth_master_connection_destroy(&conn);
+ return;
+ }
+ conn->version_received = TRUE;
+ conn->handshake_time = ioloop_timeval;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ ret = auth_master_input_line(conn, line);
+ } T_END;
+ if (!ret) {
+ auth_master_connection_destroy(&conn);
+ return;
+ }
+ }
+}
+
+static int master_output(struct auth_master_connection *conn)
+{
+ if (o_stream_flush(conn->output) < 0) {
+ /* transmit error, probably master died */
+ auth_master_connection_destroy(&conn);
+ return 1;
+ }
+
+ if (conn->io == NULL &&
+ o_stream_get_buffer_used_size(conn->output) <= MAX_OUTBUF_SIZE/2) {
+ /* allow input again */
+ conn->io = io_add(conn->fd, IO_READ, master_input, conn);
+ }
+ return 1;
+}
+
+static int
+auth_master_connection_set_permissions(struct auth_master_connection *conn,
+ const struct stat *st)
+{
+ struct net_unix_cred cred;
+
+ if (st == NULL)
+ return 0;
+
+ /* figure out what permissions we want to give to this client */
+ if ((st->st_mode & 0777) != 0666) {
+ /* permissions were already restricted by the socket
+ permissions. also +x bit indicates that we shouldn't do
+ any permission checks. */
+ return 0;
+ }
+
+ if (net_getunixcred(conn->fd, &cred) < 0) {
+ e_error(conn->event,
+ "userdb connection: Failed to get peer's credentials");
+ return -1;
+ }
+
+ if (cred.uid == st->st_uid || cred.gid == st->st_gid) {
+ /* full permissions */
+ return 0;
+ } else {
+ /* restrict permissions: return only lookups whose returned
+ uid matches the peer's uid */
+ conn->userdb_restricted_uid = cred.uid;
+ return 0;
+ }
+}
+
+struct auth_master_connection *
+auth_master_connection_create(struct auth *auth, int fd,
+ const char *path, const struct stat *socket_st,
+ bool userdb_only)
+{
+ struct auth_master_connection *conn;
+ const char *line;
+
+ i_assert(path != NULL);
+
+ conn = i_new(struct auth_master_connection, 1);
+ conn->refcount = 1;
+ conn->fd = fd;
+ conn->create_time = ioloop_timeval;
+ conn->path = i_strdup(path);
+ conn->auth = auth;
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ o_stream_set_flush_callback(conn->output, master_output, conn);
+ conn->io = io_add(fd, IO_READ, master_input, conn);
+ conn->userdb_only = userdb_only;
+ conn->event = event_create(auth_event);
+ event_set_log_message_callback(conn->event, auth_master_event_log_callback, conn);
+
+ line = t_strdup_printf("VERSION\t%u\t%u\nSPID\t%s\n",
+ AUTH_MASTER_PROTOCOL_MAJOR_VERSION,
+ AUTH_MASTER_PROTOCOL_MINOR_VERSION,
+ my_pid);
+ o_stream_nsend_str(conn->output, line);
+ DLLIST_PREPEND(&auth_master_connections, conn);
+
+ if (auth_master_connection_set_permissions(conn, socket_st) < 0) {
+ auth_master_connection_destroy(&conn);
+ return NULL;
+ }
+ return conn;
+}
+
+void auth_master_connection_destroy(struct auth_master_connection **_conn)
+{
+ struct auth_master_connection *conn = *_conn;
+
+ *_conn = NULL;
+ if (conn->destroyed)
+ return;
+ conn->destroyed = TRUE;
+
+ DLLIST_REMOVE(&auth_master_connections, conn);
+
+ if (conn->iter_ctx != NULL)
+ master_input_list_finish(conn->iter_ctx);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ io_remove(&conn->io);
+ i_close_fd_path(&conn->fd, conn->path);
+
+ master_service_client_connection_destroyed(master_service);
+ auth_master_connection_unref(&conn);
+}
+
+void auth_master_connection_ref(struct auth_master_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+
+ conn->refcount++;
+}
+
+void auth_master_connection_unref(struct auth_master_connection **_conn)
+{
+ struct auth_master_connection *conn = *_conn;
+
+ *_conn = NULL;
+ i_assert(conn->refcount > 0);
+
+ if (--conn->refcount > 0)
+ return;
+
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+
+ event_unref(&conn->event);
+ i_free(conn->path);
+ i_free(conn);
+}
+
+void auth_master_connections_destroy_all(void)
+{
+ struct auth_master_connection *conn;
+
+ while (auth_master_connections != NULL) {
+ conn = auth_master_connections;
+ auth_master_connection_destroy(&conn);
+ }
+}
diff --git a/src/auth/auth-master-connection.h b/src/auth/auth-master-connection.h
new file mode 100644
index 0000000..c2a5dd5
--- /dev/null
+++ b/src/auth/auth-master-connection.h
@@ -0,0 +1,44 @@
+#ifndef AUTH_MASTER_CONNECTION_H
+#define AUTH_MASTER_CONNECTION_H
+
+struct stat;
+struct auth_stream_reply;
+
+struct auth_master_connection {
+ struct auth_master_connection *prev, *next;
+ struct auth *auth;
+ struct event *event;
+ int refcount;
+
+ struct timeval create_time, handshake_time;
+
+ int fd;
+ char *path;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+
+ struct master_list_iter_ctx *iter_ctx;
+ /* If non-zero, allow only USER lookups whose returned uid matches
+ this uid. Don't allow LIST/PASS lookups. */
+ uid_t userdb_restricted_uid;
+
+ bool version_received:1;
+ bool destroyed:1;
+ bool userdb_only:1;
+};
+
+struct auth_master_connection *
+auth_master_connection_create(struct auth *auth, int fd,
+ const char *path, const struct stat *socket_st,
+ bool userdb_only) ATTR_NULL(4);
+void auth_master_connection_destroy(struct auth_master_connection **conn);
+
+void auth_master_connection_ref(struct auth_master_connection *conn);
+void auth_master_connection_unref(struct auth_master_connection **conn);
+
+void auth_master_request_callback(const char *reply, struct auth_master_connection *conn);
+
+void auth_master_connections_destroy_all(void);
+
+#endif
diff --git a/src/auth/auth-penalty.c b/src/auth/auth-penalty.c
new file mode 100644
index 0000000..3816902
--- /dev/null
+++ b/src/auth/auth-penalty.c
@@ -0,0 +1,176 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "crc32.h"
+#include "master-service.h"
+#include "anvil-client.h"
+#include "auth-request.h"
+#include "auth-penalty.h"
+
+#include <stdio.h>
+
+/* We don't want IPv6 hosts being able to flood our penalty
+ tracking with tons of different IPs. */
+#define PENALTY_IPV6_MASK_BITS 48
+
+struct auth_penalty_request {
+ struct auth_request *auth_request;
+ struct anvil_client *client;
+ auth_penalty_callback_t *callback;
+};
+
+struct auth_penalty {
+ struct anvil_client *client;
+
+ bool disabled:1;
+};
+
+struct auth_penalty *auth_penalty_init(const char *path)
+{
+ struct auth_penalty *penalty;
+
+ penalty = i_new(struct auth_penalty, 1);
+ penalty->client = anvil_client_init(path, NULL,
+ ANVIL_CLIENT_FLAG_HIDE_ENOENT);
+ if (anvil_client_connect(penalty->client, TRUE) < 0)
+ penalty->disabled = TRUE;
+ else {
+ anvil_client_cmd(penalty->client, t_strdup_printf(
+ "PENALTY-SET-EXPIRE-SECS\t%u", AUTH_PENALTY_TIMEOUT));
+ }
+ return penalty;
+}
+
+void auth_penalty_deinit(struct auth_penalty **_penalty)
+{
+ struct auth_penalty *penalty = *_penalty;
+
+ *_penalty = NULL;
+ anvil_client_deinit(&penalty->client);
+ i_free(penalty);
+}
+
+unsigned int auth_penalty_to_secs(unsigned int penalty)
+{
+ unsigned int i, secs = AUTH_PENALTY_INIT_SECS;
+
+ for (i = 0; i < penalty; i++)
+ secs *= 2;
+ return secs < AUTH_PENALTY_MAX_SECS ? secs : AUTH_PENALTY_MAX_SECS;
+}
+
+static void auth_penalty_anvil_callback(const char *reply, void *context)
+{
+ struct auth_penalty_request *request = context;
+ unsigned int penalty = 0;
+ unsigned long last_penalty = 0;
+ unsigned int secs, drop_penalty;
+
+ if (reply == NULL) {
+ /* internal failure. */
+ if (!anvil_client_is_connected(request->client)) {
+ /* we probably didn't have permissions to reconnect
+ back to anvil. need to restart ourself. */
+ master_service_stop(master_service);
+ }
+ } else if (sscanf(reply, "%u %lu", &penalty, &last_penalty) != 2) {
+ e_error(request->auth_request->event,
+ "Invalid PENALTY-GET reply: %s", reply);
+ } else {
+ if ((time_t)last_penalty > ioloop_time) {
+ /* time moved backwards? */
+ last_penalty = ioloop_time;
+ }
+
+ /* update penalty. */
+ drop_penalty = AUTH_PENALTY_MAX_PENALTY;
+ while (penalty > 0) {
+ secs = auth_penalty_to_secs(drop_penalty);
+ if (ioloop_time - last_penalty < secs)
+ break;
+ drop_penalty--;
+ penalty--;
+ }
+ }
+
+ request->callback(penalty, request->auth_request);
+ auth_request_unref(&request->auth_request);
+ i_free(request);
+}
+
+static const char *
+auth_penalty_get_ident(struct auth_request *auth_request)
+{
+ struct ip_addr ip;
+
+ ip = auth_request->fields.remote_ip;
+ if (IPADDR_IS_V6(&ip)) {
+ memset(ip.u.ip6.s6_addr + PENALTY_IPV6_MASK_BITS/CHAR_BIT, 0,
+ sizeof(ip.u.ip6.s6_addr) -
+ PENALTY_IPV6_MASK_BITS/CHAR_BIT);
+ }
+ return net_ip2addr(&ip);
+}
+
+void auth_penalty_lookup(struct auth_penalty *penalty,
+ struct auth_request *auth_request,
+ auth_penalty_callback_t *callback)
+{
+ struct auth_penalty_request *request;
+ const char *ident;
+
+ ident = auth_penalty_get_ident(auth_request);
+ if (penalty->disabled || ident == NULL ||
+ auth_request->fields.no_penalty) {
+ callback(0, auth_request);
+ return;
+ }
+
+ request = i_new(struct auth_penalty_request, 1);
+ request->auth_request = auth_request;
+ request->client = penalty->client;
+ request->callback = callback;
+ auth_request_ref(auth_request);
+
+ T_BEGIN {
+ anvil_client_query(penalty->client,
+ t_strdup_printf("PENALTY-GET\t%s", ident),
+ auth_penalty_anvil_callback, request);
+ } T_END;
+}
+
+static unsigned int
+get_userpass_checksum(struct auth_request *auth_request)
+{
+ return auth_request->mech_password == NULL ? 0 :
+ crc32_str_more(crc32_str(auth_request->mech_password),
+ auth_request->fields.user);
+}
+
+void auth_penalty_update(struct auth_penalty *penalty,
+ struct auth_request *auth_request, unsigned int value)
+{
+ const char *ident;
+
+ ident = auth_penalty_get_ident(auth_request);
+ if (penalty->disabled || ident == NULL ||
+ auth_request->fields.no_penalty)
+ return;
+
+ if (value > AUTH_PENALTY_MAX_PENALTY) {
+ /* even if the actual value doesn't change, the last_change
+ timestamp does. */
+ value = AUTH_PENALTY_MAX_PENALTY;
+ }
+ T_BEGIN {
+ const char *cmd;
+ unsigned int checksum;
+
+ checksum = value == 0 ? 0 : get_userpass_checksum(auth_request);
+ cmd = t_strdup_printf("PENALTY-INC\t%s\t%u\t%u",
+ ident, checksum, value);
+ anvil_client_cmd(penalty->client, cmd);
+ } T_END;
+}
diff --git a/src/auth/auth-penalty.h b/src/auth/auth-penalty.h
new file mode 100644
index 0000000..96783e4
--- /dev/null
+++ b/src/auth/auth-penalty.h
@@ -0,0 +1,28 @@
+#ifndef AUTH_PENALTY_H
+#define AUTH_PENALTY_H
+
+struct auth_request;
+
+#define AUTH_PENALTY_INIT_SECS 2
+#define AUTH_PENALTY_MAX_SECS 15
+/* timeout specifies how long it takes for penalty to be irrelevant. */
+#define AUTH_PENALTY_TIMEOUT \
+ (AUTH_PENALTY_INIT_SECS + 4 + 8 + AUTH_PENALTY_MAX_SECS)
+#define AUTH_PENALTY_MAX_PENALTY 4
+
+/* If lookup failed, penalty and last_update are both zero */
+typedef void auth_penalty_callback_t(unsigned int penalty,
+ struct auth_request *request);
+
+struct auth_penalty *auth_penalty_init(const char *path);
+void auth_penalty_deinit(struct auth_penalty **penalty);
+
+unsigned int auth_penalty_to_secs(unsigned int penalty);
+
+void auth_penalty_lookup(struct auth_penalty *penalty,
+ struct auth_request *auth_request,
+ auth_penalty_callback_t *callback);
+void auth_penalty_update(struct auth_penalty *penalty,
+ struct auth_request *auth_request, unsigned int value);
+
+#endif
diff --git a/src/auth/auth-policy.c b/src/auth/auth-policy.c
new file mode 100644
index 0000000..951f85e
--- /dev/null
+++ b/src/auth/auth-policy.c
@@ -0,0 +1,620 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "istream.h"
+#include "ioloop.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "hash-method.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "json-parser.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "auth-request.h"
+#include "auth-penalty.h"
+#include "auth-settings.h"
+#include "auth-policy.h"
+#include "auth-common.h"
+#include "iostream-ssl.h"
+
+#define AUTH_POLICY_DNS_SOCKET_PATH "dns-client"
+
+static struct http_client_settings http_client_set = {
+ .dns_client_socket_path = AUTH_POLICY_DNS_SOCKET_PATH,
+ .max_connect_attempts = 1,
+ .max_idle_time_msecs = 10000,
+ .max_parallel_connections = 100,
+ .debug = 0,
+ .user_agent = "dovecot/auth-policy-client"
+};
+
+static char *auth_policy_json_template;
+
+static struct http_client *http_client;
+
+struct policy_lookup_ctx {
+ pool_t pool;
+ string_t *json;
+ struct auth_request *request;
+ struct http_client_request *http_request;
+ struct json_parser *parser;
+ const struct auth_settings *set;
+ const char *url;
+ bool expect_result;
+ int result;
+ const char *message;
+ auth_policy_callback_t callback;
+ void *callback_context;
+
+ struct istream *payload;
+ struct io *io;
+ struct event *event;
+
+ enum {
+ POLICY_RESULT = 0,
+ POLICY_RESULT_VALUE_STATUS,
+ POLICY_RESULT_VALUE_MESSAGE
+ } parse_state;
+
+ bool parse_error;
+};
+
+struct policy_template_keyvalue {
+ const char *key;
+ const char *value;
+};
+
+static
+int auth_policy_attribute_comparator(const struct policy_template_keyvalue *a,
+ const struct policy_template_keyvalue *b)
+{
+ return strcmp(a->key, b->key);
+}
+
+static
+int auth_policy_strptrcmp(const char *a0, const char *a1,
+ const char *b0, const char *b1)
+{
+ i_assert(a0 <= a1 && b0 <= b1);
+ return memcmp(a0, b0, I_MIN((a1-a0),(b1-b0)));
+}
+
+static
+void auth_policy_open_key(const char *key, string_t *template)
+{
+ const char *ptr;
+ while((ptr = strchr(key, '/')) != NULL) {
+ str_append_c(template,'"');
+ json_append_escaped(template, t_strndup(key, (ptr-key)));
+ str_append_c(template,'"');
+ str_append_c(template,':');
+ str_append_c(template,'{');
+ key = ptr+1;
+ }
+}
+
+static
+void auth_policy_close_key(const char *key, string_t *template)
+{
+ while((key = strchr(key, '/')) != NULL) { str_append_c(template,'}'); key++; }
+}
+
+static
+void auth_policy_open_and_close_to_key(const char *fromkey, const char *tokey, string_t *template)
+{
+ const char *fptr,*tptr,*fdash,*tdash;
+
+ fptr = strrchr(fromkey, '/');
+ tptr = strrchr(tokey, '/');
+
+ if (fptr == NULL && tptr == NULL) return; /* nothing to do */
+
+ if (fptr == NULL && tptr != NULL) {
+ auth_policy_open_key(tokey, template);
+ return;
+ }
+
+ if (fptr != NULL && tptr == NULL) {
+ str_truncate(template, str_len(template)-1);
+
+ auth_policy_close_key(fromkey, template);
+ str_append_c(template, ',');
+ return;
+ }
+
+ if (auth_policy_strptrcmp(fromkey, fptr, tokey, tptr) == 0) {
+ /* nothing to do, again */
+ return;
+ }
+
+ fptr = fromkey;
+ tptr = tokey;
+
+ while (fptr != NULL && tptr != NULL) {
+ fdash = strchr(fptr, '/');
+ tdash = strchr(tptr, '/');
+
+ if (fdash == NULL) {
+ auth_policy_open_key(tptr, template);
+ break;
+ }
+ if (tdash == NULL) {
+ str_truncate(template, str_len(template)-1);
+ auth_policy_close_key(fptr, template);
+ str_append_c(template, ',');
+ break;
+ }
+ if (auth_policy_strptrcmp(fptr, fdash, tptr, tdash) != 0) {
+ str_truncate(template, str_len(template)-1);
+ auth_policy_close_key(fptr, template);
+ str_append_c(template, ',');
+ auth_policy_open_key(tptr, template);
+ break;
+ }
+ fptr = fdash+1;
+ tptr = tdash+1;
+ }
+}
+
+void auth_policy_init(void)
+{
+ const struct master_service_ssl_settings *master_ssl_set =
+ master_service_ssl_settings_get(master_service);
+ struct ssl_iostream_settings ssl_set;
+ i_zero(&ssl_set);
+
+ http_client_set.request_absolute_timeout_msecs = global_auth_settings->policy_server_timeout_msecs;
+ if (global_auth_settings->debug)
+ http_client_set.debug = 1;
+
+ master_service_ssl_client_settings_to_iostream_set(master_ssl_set,
+ pool_datastack_create(), &ssl_set);
+ http_client_set.ssl = &ssl_set;
+ http_client_set.event_parent = auth_event;
+ http_client = http_client_init(&http_client_set);
+
+ /* prepare template */
+
+ ARRAY(struct policy_template_keyvalue) attribute_pairs;
+ const struct policy_template_keyvalue *kvptr;
+ string_t *template = t_str_new(64);
+ const char **ptr;
+ const char *key = NULL;
+ const char **list = t_strsplit_spaces(global_auth_settings->policy_request_attributes, "= ");
+
+ t_array_init(&attribute_pairs, 8);
+ for(ptr = list; *ptr != NULL; ptr++) {
+ struct policy_template_keyvalue pair;
+ if (key == NULL) {
+ key = *ptr;
+ } else {
+ pair.key = key;
+ pair.value = *ptr;
+ key = NULL;
+ array_push_back(&attribute_pairs, &pair);
+ }
+ }
+ if (key != NULL) {
+ i_fatal("auth_policy_request_attributes contains invalid value");
+ }
+
+ /* then we sort it */
+ array_sort(&attribute_pairs, auth_policy_attribute_comparator);
+
+ /* and build a template string */
+ const char *prevkey = "";
+
+ array_foreach(&attribute_pairs, kvptr) {
+ const char *kptr = strchr(kvptr->key, '/');
+ auth_policy_open_and_close_to_key(prevkey, kvptr->key, template);
+ str_append_c(template,'"');
+ json_append_escaped(template, (kptr != NULL?kptr+1:kvptr->key));
+ str_append_c(template,'"');
+ str_append_c(template,':');
+ str_append_c(template,'"');
+ str_append(template,kvptr->value);
+ str_append_c(template,'"');
+ str_append_c(template,',');
+ prevkey = kvptr->key;
+ }
+
+ auth_policy_open_and_close_to_key(prevkey, "", template);
+ str_truncate(template, str_len(template)-1);
+ auth_policy_json_template = i_strdup(str_c(template));
+
+ if (global_auth_settings->policy_log_only)
+ i_warning("auth-policy: Currently in log-only mode. Ignoring "
+ "tarpit and disconnect instructions from policy server");
+}
+
+void auth_policy_deinit(void)
+{
+ if (http_client != NULL)
+ http_client_deinit(&http_client);
+ i_free(auth_policy_json_template);
+}
+
+static
+void auth_policy_log_result(struct policy_lookup_ctx *context)
+{
+ const char *action;
+ struct event_passthrough *e = event_create_passthrough(context->event)->
+ set_name("auth_policy_request_finished");
+ if (!context->expect_result) {
+ e_debug(e->event(), "Policy report action finished");
+ return;
+ }
+ int result = context->result;
+ e->add_int("policy_response", context->result);
+ if (result < 0)
+ action = "drop connection";
+ else if (context->result == 0)
+ action = "continue";
+ else
+ action = t_strdup_printf("tarpit %d second(s)", context->result);
+ if (context->request->set->policy_log_only && result != 0)
+ e_info(e->event(), "Policy check action '%s' ignored",
+ action);
+ else if (result != 0)
+ e_info(e->event(), "Policy check action is %s",
+ action);
+ else
+ e_debug(e->event(), "Policy check action is %s",
+ action);
+}
+
+static
+void auth_policy_finish(struct policy_lookup_ctx *context)
+{
+ if (context->parser != NULL) {
+ const char *error ATTR_UNUSED;
+ (void)json_parser_deinit(&context->parser, &error);
+ }
+ http_client_request_abort(&context->http_request);
+ if (context->request != NULL)
+ auth_request_unref(&context->request);
+ event_unref(&context->event);
+ pool_unref(&context->pool);
+}
+
+static
+void auth_policy_callback(struct policy_lookup_ctx *context)
+{
+ if (context->callback != NULL)
+ context->callback(context->result, context->callback_context);
+ if (context->event != NULL)
+ auth_policy_log_result(context);
+}
+
+static
+void auth_policy_parse_response(struct policy_lookup_ctx *context)
+{
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ while((ret = json_parse_next(context->parser, &type, &value)) == 1) {
+ if (context->parse_state == POLICY_RESULT) {
+ if (type != JSON_TYPE_OBJECT_KEY)
+ continue;
+ else if (strcmp(value, "status") == 0)
+ context->parse_state = POLICY_RESULT_VALUE_STATUS;
+ else if (strcmp(value, "msg") == 0)
+ context->parse_state = POLICY_RESULT_VALUE_MESSAGE;
+ else
+ continue;
+ } else if (context->parse_state == POLICY_RESULT_VALUE_STATUS) {
+ if (type != JSON_TYPE_NUMBER || str_to_int(value, &context->result) != 0)
+ break;
+ context->parse_state = POLICY_RESULT;
+ } else if (context->parse_state == POLICY_RESULT_VALUE_MESSAGE) {
+ if (type != JSON_TYPE_STRING)
+ break;
+ if (*value != '\0')
+ context->message = p_strdup(context->pool, value);
+ context->parse_state = POLICY_RESULT;
+ } else {
+ break;
+ }
+ }
+
+ if (ret == 0 && !context->payload->eof)
+ return;
+
+ context->parse_error = TRUE;
+
+ io_remove(&context->io);
+
+ if (context->payload->stream_errno != 0) {
+ e_error(context->event,
+ "Error reading policy server result: %s",
+ i_stream_get_error(context->payload));
+ } else if (ret == 0 && context->payload->eof) {
+ e_error(context->event,
+ "Policy server result was too short");
+ } else if (ret == 1) {
+ e_error(context->event,
+ "Policy server response was malformed");
+ } else {
+ const char *error = "unknown";
+ if (json_parser_deinit(&context->parser, &error) != 0)
+ e_error(context->event,
+ "Policy server response JSON parse error: %s", error);
+ else if (context->parse_state == POLICY_RESULT)
+ context->parse_error = FALSE;
+ }
+
+ if (context->parse_error) {
+ context->result = (context->set->policy_reject_on_fail ? -1 : 0);
+ }
+
+ context->request->policy_refusal = FALSE;
+
+ if (context->result < 0) {
+ if (context->message != NULL) {
+ /* set message here */
+ e_debug(context->event,
+ "Policy response %d with message: %s",
+ context->result, context->message);
+ auth_request_set_field(context->request, "reason", context->message, NULL);
+ }
+ context->request->policy_refusal = TRUE;
+ } else {
+ e_debug(context->event,
+ "Policy response %d", context->result);
+ }
+
+ if (context->request->policy_refusal == TRUE && context->set->verbose == TRUE) {
+ e_info(context->event, "Authentication failure due to policy server refusal%s%s",
+ (context->message!=NULL?": ":""),
+ (context->message!=NULL?context->message:""));
+ }
+
+ auth_policy_callback(context);
+ i_stream_unref(&context->payload);
+}
+
+static
+void auth_policy_process_response(const struct http_response *response,
+ void *ctx)
+{
+ struct policy_lookup_ctx *context = ctx;
+
+ context->payload = response->payload;
+
+ if ((response->status / 10) != 20) {
+ e_error(context->event,
+ "Policy server HTTP error: %s",
+ http_response_get_message(response));
+ auth_policy_callback(context);
+ return;
+ }
+
+ if (response->payload == NULL) {
+ if (context->expect_result)
+ e_error(context->event,
+ "Policy server result was empty");
+ auth_policy_callback(context);
+ return;
+ }
+
+ if (context->expect_result) {
+ i_stream_ref(response->payload);
+ context->io = io_add_istream(response->payload, auth_policy_parse_response, context);
+ context->parser = json_parser_init(response->payload);
+ auth_policy_parse_response(ctx);
+ } else {
+ auth_policy_callback(context);
+ }
+}
+
+static
+void auth_policy_send_request(struct policy_lookup_ctx *context)
+{
+ const char *error;
+ struct http_url *url;
+
+ auth_request_ref(context->request);
+ if (http_url_parse(context->url, NULL, HTTP_URL_ALLOW_USERINFO_PART,
+ context->pool, &url, &error) != 0) {
+ e_error(context->event,
+ "Could not parse url %s: %s", context->url, error);
+ auth_policy_callback(context);
+ auth_policy_finish(context);
+ return;
+ }
+ context->http_request = http_client_request_url(http_client,
+ "POST", url, auth_policy_process_response, (void*)context);
+ http_client_request_set_destroy_callback(context->http_request, auth_policy_finish, context);
+ http_client_request_add_header(context->http_request, "Content-Type", "application/json");
+ if (*context->set->policy_server_api_header != 0) {
+ const char *ptr;
+ if ((ptr = strstr(context->set->policy_server_api_header, ":")) != NULL) {
+ const char *header = t_strcut(context->set->policy_server_api_header, ':');
+ http_client_request_add_header(context->http_request, header, ptr + 1);
+ } else {
+ http_client_request_add_header(context->http_request,
+ "X-API-Key", context->set->policy_server_api_header);
+ }
+ }
+ if (url->user != NULL) {
+ /* allow empty password */
+ http_client_request_set_auth_simple(context->http_request, url->user,
+ (url->password != NULL ? url->password : ""));
+ }
+ struct istream *is = i_stream_create_from_buffer(context->json);
+ http_client_request_set_payload(context->http_request, is, FALSE);
+ i_stream_unref(&is);
+ http_client_request_submit(context->http_request);
+}
+
+static
+const char *auth_policy_escape_function(const char *string,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ string_t *tmp = t_str_new(64);
+ json_append_escaped(tmp, string);
+ return str_c(tmp);
+}
+
+static
+const struct var_expand_table *policy_get_var_expand_table(struct auth_request *auth_request,
+ const char *hashed_password, const char *requested_username)
+{
+ struct var_expand_table *table;
+ unsigned int count = 2;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, auth_policy_escape_function, &count);
+ table[0].key = '\0';
+ table[0].long_key = "hashed_password";
+ table[0].value = hashed_password;
+ table[1].key = '\0';
+ table[1].long_key = "requested_username";
+ table[1].value = requested_username;
+ if (table[0].value != NULL)
+ table[0].value = auth_policy_escape_function(table[0].value, auth_request);
+ if (table[1].value != NULL)
+ table[1].value = auth_policy_escape_function(table[1].value, auth_request);
+
+ return table;
+}
+
+static
+void auth_policy_create_json(struct policy_lookup_ctx *context,
+ const char *password, bool include_success)
+{
+ const struct var_expand_table *var_table;
+ context->json = str_new(context->pool, 64);
+ unsigned char *ptr;
+ const char *requested_username;
+ const struct hash_method *digest = hash_method_lookup(context->set->policy_hash_mech);
+
+ i_assert(digest != NULL);
+
+ void *ctx = t_malloc_no0(digest->context_size);
+ buffer_t *buffer = t_buffer_create(64);
+
+ digest->init(ctx);
+ digest->loop(ctx,
+ context->set->policy_hash_nonce,
+ strlen(context->set->policy_hash_nonce));
+ if (context->request->fields.requested_login_user != NULL)
+ requested_username = context->request->fields.requested_login_user;
+ else if (context->request->fields.user != NULL)
+ requested_username = context->request->fields.user;
+ else
+ requested_username = "";
+ /* use +1 to make sure \0 gets included */
+ digest->loop(ctx, requested_username, strlen(requested_username)+1);
+ if (password != NULL)
+ digest->loop(ctx, password, strlen(password));
+ ptr = buffer_get_modifiable_data(buffer, NULL);
+ digest->result(ctx, ptr);
+ buffer_set_used_size(buffer, digest->digest_size);
+ if (context->set->policy_hash_truncate > 0) {
+ buffer_truncate_rshift_bits(buffer, context->set->policy_hash_truncate);
+ }
+ const char *hashed_password = binary_to_hex(buffer->data, buffer->used);
+ str_append_c(context->json, '{');
+ var_table = policy_get_var_expand_table(context->request, hashed_password, requested_username);
+ const char *error;
+ if (auth_request_var_expand_with_table(context->json, auth_policy_json_template,
+ context->request, var_table,
+ auth_policy_escape_function, &error) <= 0) {
+ e_error(context->event,
+ "Failed to expand auth policy template: %s", error);
+ }
+ if (include_success) {
+ str_append(context->json, ",\"success\":");
+ if (!context->request->failed &&
+ context->request->fields.successful &&
+ !context->request->internal_failure)
+ str_append(context->json, "true");
+ else
+ str_append(context->json, "false");
+ str_append(context->json, ",\"policy_reject\":");
+ str_append(context->json, context->request->policy_refusal ? "true" : "false");
+ }
+ str_append(context->json, ",\"tls\":");
+ if (context->request->fields.secured == AUTH_REQUEST_SECURED_TLS)
+ str_append(context->json, "true");
+ else
+ str_append(context->json, "false");
+ str_append_c(context->json, '}');
+ e_debug(context->event,
+ "Policy server request JSON: %s", str_c(context->json));
+}
+
+static
+void auth_policy_url(struct policy_lookup_ctx *context, const char *command)
+{
+ size_t len = strlen(context->set->policy_server_url);
+ if (context->set->policy_server_url[len-1] == '&')
+ context->url = p_strdup_printf(context->pool, "%scommand=%s",
+ context->set->policy_server_url, command);
+ else
+ context->url = p_strdup_printf(context->pool, "%s?command=%s",
+ context->set->policy_server_url, command);
+}
+
+static const char *auth_policy_get_prefix(struct auth_request *request)
+{
+ string_t *str = t_str_new(256);
+ auth_request_get_log_prefix(str, request, "policy");
+ return str_c(str);
+}
+
+void auth_policy_check(struct auth_request *request, const char *password,
+ auth_policy_callback_t cb, void *context)
+{
+ if (request->master != NULL || *(request->set->policy_server_url) == '\0') {
+ cb(0, context);
+ return;
+ }
+ pool_t pool = pool_alloconly_create("auth policy", 512);
+ struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1);
+ ctx->pool = pool;
+ ctx->request = request;
+ ctx->expect_result = TRUE;
+ ctx->callback = cb;
+ ctx->callback_context = context;
+ ctx->set = request->set;
+ ctx->event = event_create(request->event);
+ event_add_str(ctx->event, "mode", "allow");
+ event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request));
+ auth_policy_url(ctx, "allow");
+ ctx->result = (ctx->set->policy_reject_on_fail ? -1 : 0);
+ e_debug(ctx->event, "Policy request %s", ctx->url);
+ T_BEGIN {
+ auth_policy_create_json(ctx, password, FALSE);
+ } T_END;
+ auth_policy_send_request(ctx);
+}
+
+void auth_policy_report(struct auth_request *request)
+{
+ if (request->master != NULL)
+ return;
+
+ if (*(request->set->policy_server_url) == '\0')
+ return;
+ pool_t pool = pool_alloconly_create("auth policy", 512);
+ struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1);
+ ctx->pool = pool;
+ ctx->request = request;
+ ctx->expect_result = FALSE;
+ ctx->set = request->set;
+ ctx->event = event_create(request->event);
+ event_add_str(ctx->event, "mode", "report");
+ event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request));
+ auth_policy_url(ctx, "report");
+ e_debug(ctx->event, "Policy request %s", ctx->url);
+ T_BEGIN {
+ auth_policy_create_json(ctx, request->mech_password, TRUE);
+ } T_END;
+ auth_policy_send_request(ctx);
+}
diff --git a/src/auth/auth-policy.h b/src/auth/auth-policy.h
new file mode 100644
index 0000000..1c81945
--- /dev/null
+++ b/src/auth/auth-policy.h
@@ -0,0 +1,11 @@
+#ifndef AUTH_POLICY_H
+#define AUTH_POLICY_H
+
+typedef void (*auth_policy_callback_t)(int, void *);
+
+void auth_policy_check(struct auth_request *request, const char *password, auth_policy_callback_t cb, void *context);
+void auth_policy_report(struct auth_request *request);
+void auth_policy_init(void);
+void auth_policy_deinit(void);
+
+#endif
diff --git a/src/auth/auth-request-fields.c b/src/auth/auth-request-fields.c
new file mode 100644
index 0000000..590e671
--- /dev/null
+++ b/src/auth/auth-request-fields.c
@@ -0,0 +1,525 @@
+/* Copyright (c) 2002-2020 Dovecot authors, see the included COPYING file */
+
+#define AUTH_REQUEST_FIELDS_CONST
+
+#include "auth-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "auth-request.h"
+#include "userdb-template.h"
+
+void auth_request_fields_init(struct auth_request *request)
+{
+ request->fields.extra_fields = auth_fields_init(request->pool);
+ if (request->mech != NULL) {
+ request->fields.mech_name = request->mech->mech_name;
+ event_add_str(request->event, "mechanism",
+ request->mech->mech_name);
+ }
+ /* Default to "insecure" until it's changed later */
+ event_add_str(request->event, "transport", "insecure");
+}
+
+static void
+auth_str_add_keyvalue(string_t *dest, const char *key, const char *value)
+{
+ str_append_c(dest, '\t');
+ str_append(dest, key);
+ if (value != NULL) {
+ str_append_c(dest, '=');
+ str_append_tabescaped(dest, value);
+ }
+}
+
+static void
+auth_request_export_fields(string_t *dest, struct auth_fields *auth_fields,
+ const char *prefix)
+{
+ const ARRAY_TYPE(auth_field) *fields = auth_fields_export(auth_fields);
+ const struct auth_field *field;
+
+ array_foreach(fields, field) {
+ str_printfa(dest, "\t%s%s", prefix, field->key);
+ if (field->value != NULL) {
+ str_append_c(dest, '=');
+ str_append_tabescaped(dest, field->value);
+ }
+ }
+}
+
+void auth_request_export(struct auth_request *request, string_t *dest)
+{
+ const struct auth_request_fields *fields = &request->fields;
+
+ str_append(dest, "user=");
+ str_append_tabescaped(dest, fields->user);
+
+ auth_str_add_keyvalue(dest, "service", fields->service);
+
+ if (fields->master_user != NULL)
+ auth_str_add_keyvalue(dest, "master-user", fields->master_user);
+ auth_str_add_keyvalue(dest, "original-username",
+ fields->original_username);
+ if (fields->requested_login_user != NULL) {
+ auth_str_add_keyvalue(dest, "requested-login-user",
+ fields->requested_login_user);
+ }
+
+ if (fields->local_ip.family != 0) {
+ auth_str_add_keyvalue(dest, "lip",
+ net_ip2addr(&fields->local_ip));
+ }
+ if (fields->remote_ip.family != 0) {
+ auth_str_add_keyvalue(dest, "rip",
+ net_ip2addr(&fields->remote_ip));
+ }
+ if (fields->local_port != 0)
+ str_printfa(dest, "\tlport=%u", fields->local_port);
+ if (fields->remote_port != 0)
+ str_printfa(dest, "\trport=%u", fields->remote_port);
+ if (fields->real_local_ip.family != 0) {
+ auth_str_add_keyvalue(dest, "real_lip",
+ net_ip2addr(&fields->real_local_ip));
+ }
+ if (fields->real_remote_ip.family != 0) {
+ auth_str_add_keyvalue(dest, "real_rip",
+ net_ip2addr(&fields->real_remote_ip));
+ }
+ if (fields->real_local_port != 0)
+ str_printfa(dest, "\treal_lport=%u", fields->real_local_port);
+ if (fields->real_remote_port != 0)
+ str_printfa(dest, "\treal_rport=%u", fields->real_remote_port);
+ if (fields->local_name != 0) {
+ str_append(dest, "\tlocal_name=");
+ str_append_tabescaped(dest, fields->local_name);
+ }
+ if (fields->session_id != NULL) {
+ str_append(dest, "\tsession=");
+ str_append_tabescaped(dest, fields->session_id);
+ }
+ if (event_want_debug(request->event))
+ str_append(dest, "\tdebug");
+ switch (fields->secured) {
+ case AUTH_REQUEST_SECURED_NONE: break;
+ case AUTH_REQUEST_SECURED: str_append(dest, "\tsecured"); break;
+ case AUTH_REQUEST_SECURED_TLS: str_append(dest, "\tsecured=tls"); break;
+ default: break;
+ }
+ if (fields->skip_password_check)
+ str_append(dest, "\tskip-password-check");
+ if (fields->delayed_credentials != NULL)
+ str_append(dest, "\tdelayed-credentials");
+ if (fields->valid_client_cert)
+ str_append(dest, "\tvalid-client-cert");
+ if (fields->no_penalty)
+ str_append(dest, "\tno-penalty");
+ if (fields->successful)
+ str_append(dest, "\tsuccessful");
+ if (fields->mech_name != NULL)
+ auth_str_add_keyvalue(dest, "mech", fields->mech_name);
+ if (fields->client_id != NULL)
+ auth_str_add_keyvalue(dest, "client_id", fields->client_id);
+ /* export passdb extra fields */
+ auth_request_export_fields(dest, fields->extra_fields, "passdb_");
+ /* export any userdb fields */
+ if (fields->userdb_reply != NULL)
+ auth_request_export_fields(dest, fields->userdb_reply, "userdb_");
+}
+
+bool auth_request_import_info(struct auth_request *request,
+ const char *key, const char *value)
+{
+ struct auth_request_fields *fields = &request->fields;
+ struct event *event = request->event;
+
+ i_assert(value != NULL);
+
+ /* authentication and user lookups may set these */
+ if (strcmp(key, "service") == 0) {
+ fields->service = p_strdup(request->pool, value);
+ event_add_str(event, "service", value);
+ } else if (strcmp(key, "lip") == 0) {
+ if (net_addr2ip(value, &fields->local_ip) < 0)
+ return TRUE;
+ event_add_str(event, "local_ip", value);
+ if (fields->real_local_ip.family == 0)
+ auth_request_import_info(request, "real_lip", value);
+ } else if (strcmp(key, "rip") == 0) {
+ if (net_addr2ip(value, &fields->remote_ip) < 0)
+ return TRUE;
+ event_add_str(event, "remote_ip", value);
+ if (fields->real_remote_ip.family == 0)
+ auth_request_import_info(request, "real_rip", value);
+ } else if (strcmp(key, "lport") == 0) {
+ if (net_str2port(value, &fields->local_port) < 0)
+ return TRUE;
+ event_add_int(event, "local_port", fields->local_port);
+ if (fields->real_local_port == 0)
+ auth_request_import_info(request, "real_lport", value);
+ } else if (strcmp(key, "rport") == 0) {
+ if (net_str2port(value, &fields->remote_port) < 0)
+ return TRUE;
+ event_add_int(event, "remote_port", fields->remote_port);
+ if (fields->real_remote_port == 0)
+ auth_request_import_info(request, "real_rport", value);
+ } else if (strcmp(key, "real_lip") == 0) {
+ if (net_addr2ip(value, &fields->real_local_ip) == 0)
+ event_add_str(event, "real_local_ip", value);
+ } else if (strcmp(key, "real_rip") == 0) {
+ if (net_addr2ip(value, &fields->real_remote_ip) == 0)
+ event_add_str(event, "real_remote_ip", value);
+ } else if (strcmp(key, "real_lport") == 0) {
+ if (net_str2port(value, &fields->real_local_port) == 0)
+ event_add_int(event, "real_local_port",
+ fields->real_local_port);
+ } else if (strcmp(key, "real_rport") == 0) {
+ if (net_str2port(value, &fields->real_remote_port) == 0)
+ event_add_int(event, "real_remote_port",
+ fields->real_remote_port);
+ } else if (strcmp(key, "local_name") == 0) {
+ fields->local_name = p_strdup(request->pool, value);
+ event_add_str(event, "local_name", value);
+ } else if (strcmp(key, "session") == 0) {
+ fields->session_id = p_strdup(request->pool, value);
+ event_add_str(event, "session", value);
+ } else if (strcmp(key, "debug") == 0)
+ event_set_forced_debug(request->event, TRUE);
+ else if (strcmp(key, "client_id") == 0) {
+ fields->client_id = p_strdup(request->pool, value);
+ event_add_str(event, "client_id", value);
+ } else if (strcmp(key, "forward_fields") == 0) {
+ auth_fields_import_prefixed(fields->extra_fields,
+ "forward_", value, 0);
+ /* make sure the forward_ fields aren't deleted by
+ auth_fields_rollback() if the first passdb lookup fails. */
+ auth_fields_snapshot(fields->extra_fields);
+ } else
+ return FALSE;
+ /* NOTE: keep in sync with auth_request_export() */
+ return TRUE;
+}
+
+bool auth_request_import_auth(struct auth_request *request,
+ const char *key, const char *value)
+{
+ struct auth_request_fields *fields = &request->fields;
+
+ i_assert(value != NULL);
+
+ if (auth_request_import_info(request, key, value))
+ return TRUE;
+
+ /* auth client may set these */
+ if (strcmp(key, "secured") == 0) {
+ if (strcmp(value, "tls") == 0) {
+ fields->secured = AUTH_REQUEST_SECURED_TLS;
+ event_add_str(request->event, "transport", "TLS");
+ } else {
+ fields->secured = AUTH_REQUEST_SECURED;
+ event_add_str(request->event, "transport", "trusted");
+ }
+ }
+ else if (strcmp(key, "final-resp-ok") == 0)
+ fields->final_resp_ok = TRUE;
+ else if (strcmp(key, "no-penalty") == 0)
+ fields->no_penalty = TRUE;
+ else if (strcmp(key, "valid-client-cert") == 0)
+ fields->valid_client_cert = TRUE;
+ else if (strcmp(key, "cert_username") == 0) {
+ if (request->set->ssl_username_from_cert && *value != '\0') {
+ /* get username from SSL certificate. it overrides
+ the username given by the auth mechanism. */
+ auth_request_set_username_forced(request, value);
+ fields->cert_username = TRUE;
+ }
+ } else {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool auth_request_import(struct auth_request *request,
+ const char *key, const char *value)
+{
+ struct auth_request_fields *fields = &request->fields;
+
+ i_assert(value != NULL);
+
+ if (auth_request_import_auth(request, key, value))
+ return TRUE;
+
+ /* for communication between auth master and worker processes */
+ if (strcmp(key, "user") == 0)
+ auth_request_set_username_forced(request, value);
+ else if (strcmp(key, "master-user") == 0) {
+ fields->master_user = p_strdup(request->pool, value);
+ event_add_str(request->event, "master_user", value);
+ } else if (strcmp(key, "original-username") == 0) {
+ fields->original_username = p_strdup(request->pool, value);
+ event_add_str(request->event, "original_user", value);
+ } else if (strcmp(key, "requested-login-user") == 0)
+ auth_request_set_login_username_forced(request, value);
+ else if (strcmp(key, "successful") == 0)
+ auth_request_set_auth_successful(request);
+ else if (strcmp(key, "skip-password-check") == 0)
+ auth_request_set_password_verified(request);
+ else if (strcmp(key, "delayed-credentials") == 0) {
+ /* just make passdb_handle_credentials() work identically in
+ auth-worker as it does in auth-master. the worker shouldn't
+ care about the actual contents of the credentials. */
+ fields->delayed_credentials = &uchar_nul;
+ fields->delayed_credentials_size = 1;
+ } else if (strcmp(key, "mech") == 0) {
+ fields->mech_name = p_strdup(request->pool, value);
+ event_add_str(request->event, "mechanism", value);
+ } else if (str_begins(key, "passdb_"))
+ auth_fields_add(fields->extra_fields, key+7, value, 0);
+ else if (str_begins(key, "userdb_")) {
+ if (fields->userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, FALSE);
+ auth_fields_add(fields->userdb_reply, key+7, value, 0);
+ } else
+ return FALSE;
+
+ return TRUE;
+}
+
+static int
+auth_request_fix_username(struct auth_request *request, const char **username,
+ const char **error_r)
+{
+ const struct auth_settings *set = request->set;
+ unsigned char *p;
+ char *user;
+
+ if (*set->default_realm != '\0' &&
+ strchr(*username, '@') == NULL) {
+ user = p_strconcat(unsafe_data_stack_pool, *username, "@",
+ set->default_realm, NULL);
+ } else {
+ user = t_strdup_noconst(*username);
+ }
+
+ for (p = (unsigned char *)user; *p != '\0'; p++) {
+ if (set->username_translation_map[*p & 0xff] != 0)
+ *p = set->username_translation_map[*p & 0xff];
+ if (set->username_chars_map[*p & 0xff] == 0) {
+ *error_r = t_strdup_printf(
+ "Username character disallowed by auth_username_chars: "
+ "0x%02x (username: %s)", *p,
+ str_sanitize(*username, 128));
+ return -1;
+ }
+ }
+
+ if (*set->username_format != '\0') {
+ /* username format given, put it through variable expansion.
+ we'll have to temporarily replace request->user to get
+ %u to be the wanted username */
+ const char *error;
+ string_t *dest;
+
+ dest = t_str_new(256);
+ unsigned int count = 0;
+ const struct var_expand_table *table =
+ auth_request_get_var_expand_table_full(request,
+ user, NULL, &count);
+ if (auth_request_var_expand_with_table(dest,
+ set->username_format, request,
+ table, NULL, &error) <= 0) {
+ *error_r = t_strdup_printf(
+ "Failed to expand username_format=%s: %s",
+ set->username_format, error);
+ }
+ user = str_c_modifiable(dest);
+ }
+
+ if (user[0] == '\0') {
+ /* Some PAM plugins go nuts with empty usernames */
+ *error_r = "Empty username";
+ return -1;
+ }
+ *username = user;
+ return 0;
+}
+
+bool auth_request_set_username(struct auth_request *request,
+ const char *username, const char **error_r)
+{
+ const struct auth_settings *set = request->set;
+ const char *p, *login_username = NULL;
+
+ if (*set->master_user_separator != '\0' && !request->userdb_lookup) {
+ /* check if the username contains a master user */
+ p = strchr(username, *set->master_user_separator);
+ if (p != NULL) {
+ /* it does, set it. */
+ login_username = t_strdup_until(username, p);
+
+ /* username is the master user */
+ username = p + 1;
+ }
+ }
+
+ if (request->fields.original_username == NULL) {
+ /* the username may change later, but we need to use this
+ username when verifying at least DIGEST-MD5 password. */
+ request->fields.original_username =
+ p_strdup(request->pool, username);
+ event_add_str(request->event, "original_user",
+ request->fields.original_username);
+ }
+ if (request->fields.cert_username) {
+ /* cert_username overrides the username given by
+ authentication mechanism. but still do checks and
+ translations to it. */
+ username = request->fields.user;
+ }
+
+ if (auth_request_fix_username(request, &username, error_r) < 0) {
+ request->fields.user = NULL;
+ event_field_clear(request->event, "user");
+ return FALSE;
+ }
+ auth_request_set_username_forced(request, username);
+ if (request->fields.translated_username == NULL) {
+ /* similar to original_username, but after translations */
+ request->fields.translated_username = request->fields.user;
+ event_add_str(request->event, "translated_user",
+ request->fields.translated_username);
+ }
+ request->user_changed_by_lookup = TRUE;
+
+ if (login_username != NULL) {
+ if (!auth_request_set_login_username(request,
+ login_username,
+ error_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void auth_request_set_username_forced(struct auth_request *request,
+ const char *username)
+{
+ i_assert(username != NULL);
+
+ request->fields.user = p_strdup(request->pool, username);
+ event_add_str(request->event, "user", request->fields.user);
+}
+
+void auth_request_set_login_username_forced(struct auth_request *request,
+ const char *username)
+{
+ i_assert(username != NULL);
+
+ request->fields.requested_login_user =
+ p_strdup(request->pool, username);
+ event_add_str(request->event, "login_user",
+ request->fields.requested_login_user);
+}
+
+bool auth_request_set_login_username(struct auth_request *request,
+ const char *username,
+ const char **error_r)
+{
+ struct auth_passdb *master_passdb;
+
+ if (username[0] == '\0') {
+ *error_r = "Master user login attempted to use empty login username";
+ return FALSE;
+ }
+
+ if (strcmp(username, request->fields.user) == 0) {
+ /* The usernames are the same, we don't really wish to log
+ in as someone else */
+ return TRUE;
+ }
+
+ /* lookup request->user from masterdb first */
+ master_passdb = auth_request_get_auth(request)->masterdbs;
+ if (master_passdb == NULL) {
+ *error_r = "Master user login attempted without master passdbs";
+ return FALSE;
+ }
+ request->passdb = master_passdb;
+
+ if (auth_request_fix_username(request, &username, error_r) < 0) {
+ request->fields.requested_login_user = NULL;
+ event_field_clear(request->event, "login_user");
+ return FALSE;
+ }
+ auth_request_set_login_username_forced(request, username);
+
+ e_debug(request->event,
+ "%sMaster user lookup for login: %s",
+ auth_request_get_log_prefix_db(request),
+ request->fields.requested_login_user);
+ return TRUE;
+}
+
+void auth_request_master_user_login_finish(struct auth_request *request)
+{
+ if (request->failed)
+ return;
+
+ /* master login successful. update user and master_user variables. */
+ e_info(authdb_event(request),
+ "Master user logging in as %s",
+ request->fields.requested_login_user);
+
+ request->fields.master_user = request->fields.user;
+ event_add_str(request->event, "master_user",
+ request->fields.master_user);
+
+ auth_request_set_username_forced(request,
+ request->fields.requested_login_user);
+ request->fields.translated_username = request->fields.requested_login_user;
+ event_add_str(request->event, "translated_user",
+ request->fields.translated_username);
+ request->fields.requested_login_user = NULL;
+ event_field_clear(request->event, "login_user");
+}
+
+void auth_request_set_realm(struct auth_request *request, const char *realm)
+{
+ i_assert(realm != NULL);
+
+ request->fields.realm = p_strdup(request->pool, realm);
+ event_add_str(request->event, "realm", request->fields.realm);
+}
+
+void auth_request_set_auth_successful(struct auth_request *request)
+{
+ request->fields.successful = TRUE;
+}
+
+void auth_request_set_password_verified(struct auth_request *request)
+{
+ request->fields.skip_password_check = TRUE;
+}
+
+void auth_request_init_userdb_reply(struct auth_request *request,
+ bool add_default_fields)
+{
+ const char *error;
+
+ request->fields.userdb_reply = auth_fields_init(request->pool);
+ if (add_default_fields) {
+ if (userdb_template_export(request->userdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ }
+ }
+}
+
+void auth_request_set_delayed_credentials(struct auth_request *request,
+ const unsigned char *credentials,
+ size_t size)
+{
+ request->fields.delayed_credentials =
+ p_memdup(request->pool, credentials, size);
+ request->fields.delayed_credentials_size = size;
+}
diff --git a/src/auth/auth-request-handler-private.h b/src/auth/auth-request-handler-private.h
new file mode 100644
index 0000000..4d733df
--- /dev/null
+++ b/src/auth/auth-request-handler-private.h
@@ -0,0 +1,27 @@
+#ifndef AUTH_REQUEST_HANDLER_PRIVATE_H
+#define AUTH_REQUEST_HANDLER_PRIVATE_H
+
+struct auth_request;
+struct auth_client_connection;
+
+struct auth_request_handler {
+ int refcount;
+ pool_t pool;
+ HASH_TABLE(void *, struct auth_request *) requests;
+
+ unsigned int connect_uid, client_pid;
+
+ auth_client_request_callback_t *callback;
+ struct auth_client_connection *conn;
+
+ auth_master_request_callback_t *master_callback;
+ auth_request_handler_reply_callback_t *reply_callback;
+ auth_request_handler_reply_continue_callback_t *reply_continue_callback;
+ verify_plain_continue_callback_t *verify_plain_continue_callback;
+
+ bool destroyed:1;
+ bool token_auth:1;
+};
+
+
+#endif
diff --git a/src/auth/auth-request-handler.c b/src/auth/auth-request-handler.c
new file mode 100644
index 0000000..d592924
--- /dev/null
+++ b/src/auth/auth-request-handler.c
@@ -0,0 +1,991 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "array.h"
+#include "aqueue.h"
+#include "base64.h"
+#include "hash.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "master-interface.h"
+#include "auth-penalty.h"
+#include "auth-request.h"
+#include "auth-token.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+#include "auth-request-handler.h"
+#include "auth-request-handler-private.h"
+#include "auth-policy.h"
+
+#define AUTH_FAILURE_DELAY_CHECK_MSECS 500
+static ARRAY(struct auth_request *) auth_failures_arr;
+static struct aqueue *auth_failures;
+static struct timeout *to_auth_failures;
+
+static void auth_failure_timeout(void *context) ATTR_NULL(1);
+
+
+static void
+auth_request_handler_default_reply_callback(struct auth_request *request,
+ enum auth_client_result result,
+ const void *auth_reply,
+ size_t reply_size);
+
+static void
+auth_request_handler_default_reply_continue(struct auth_request *request,
+ const void *reply,
+ size_t reply_size);
+
+struct auth_request_handler *
+auth_request_handler_create(bool token_auth, auth_client_request_callback_t *callback,
+ struct auth_client_connection *conn,
+ auth_master_request_callback_t *master_callback)
+{
+ struct auth_request_handler *handler;
+ pool_t pool;
+
+ pool = pool_alloconly_create("auth request handler", 4096);
+
+ handler = p_new(pool, struct auth_request_handler, 1);
+ handler->refcount = 1;
+ handler->pool = pool;
+ hash_table_create_direct(&handler->requests, pool, 0);
+ handler->callback = callback;
+ handler->conn = conn;
+ handler->master_callback = master_callback;
+ handler->token_auth = token_auth;
+ handler->reply_callback =
+ auth_request_handler_default_reply_callback;
+ handler->reply_continue_callback =
+ auth_request_handler_default_reply_continue;
+ handler->verify_plain_continue_callback =
+ auth_request_default_verify_plain_continue;
+ return handler;
+}
+
+unsigned int
+auth_request_handler_get_request_count(struct auth_request_handler *handler)
+{
+ return hash_table_count(handler->requests);
+}
+
+void auth_request_handler_abort_requests(struct auth_request_handler *handler)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct auth_request *auth_request;
+
+ iter = hash_table_iterate_init(handler->requests);
+ while (hash_table_iterate(iter, handler->requests, &key, &auth_request)) {
+ switch (auth_request->state) {
+ case AUTH_REQUEST_STATE_NEW:
+ case AUTH_REQUEST_STATE_MECH_CONTINUE:
+ case AUTH_REQUEST_STATE_FINISHED:
+ auth_request->removed_from_handler = TRUE;
+ auth_request_unref(&auth_request);
+ hash_table_remove(handler->requests, key);
+ break;
+ case AUTH_REQUEST_STATE_PASSDB:
+ case AUTH_REQUEST_STATE_USERDB:
+ /* can't abort a pending passdb/userdb lookup */
+ break;
+ case AUTH_REQUEST_STATE_MAX:
+ i_unreached();
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+void auth_request_handler_unref(struct auth_request_handler **_handler)
+{
+ struct auth_request_handler *handler = *_handler;
+
+ *_handler = NULL;
+
+ i_assert(handler->refcount > 0);
+ if (--handler->refcount > 0)
+ return;
+
+ i_assert(hash_table_count(handler->requests) == 0);
+
+ /* notify parent that we're done with all requests */
+ handler->callback(NULL, handler->conn);
+
+ hash_table_destroy(&handler->requests);
+ pool_unref(&handler->pool);
+}
+
+void auth_request_handler_destroy(struct auth_request_handler **_handler)
+{
+ struct auth_request_handler *handler = *_handler;
+
+ *_handler = NULL;
+
+ i_assert(!handler->destroyed);
+
+ handler->destroyed = TRUE;
+ auth_request_handler_unref(&handler);
+}
+
+void auth_request_handler_set(struct auth_request_handler *handler,
+ unsigned int connect_uid,
+ unsigned int client_pid)
+{
+ handler->connect_uid = connect_uid;
+ handler->client_pid = client_pid;
+}
+
+static void auth_request_handler_remove(struct auth_request_handler *handler,
+ struct auth_request *request)
+{
+ i_assert(request->handler == handler);
+
+ if (request->removed_from_handler) {
+ /* already removed it */
+ return;
+ }
+ request->removed_from_handler = TRUE;
+
+ /* if db lookup is stuck, this call doesn't actually free the auth
+ request, so make sure we don't get back here. */
+ timeout_remove(&request->to_abort);
+
+ hash_table_remove(handler->requests, POINTER_CAST(request->id));
+ auth_request_unref(&request);
+}
+
+static void
+auth_str_add_keyvalue(string_t *dest, const char *key, const char *value)
+{
+ str_append_c(dest, '\t');
+ str_append(dest, key);
+ str_append_c(dest, '=');
+ str_append_tabescaped(dest, value);
+}
+
+static void
+auth_str_append_extra_fields(struct auth_request *request, string_t *dest)
+{
+ const struct auth_request_fields *fields = &request->fields;
+
+ if (!auth_fields_is_empty(fields->extra_fields)) {
+ str_append_c(dest, '\t');
+ auth_fields_append(fields->extra_fields, dest,
+ AUTH_FIELD_FLAG_HIDDEN, 0);
+ }
+
+ if (fields->original_username != NULL &&
+ null_strcmp(fields->original_username, fields->user) != 0 &&
+ !auth_fields_exists(fields->extra_fields, "original_user")) {
+ auth_str_add_keyvalue(dest, "original_user",
+ fields->original_username);
+ }
+ if (fields->master_user != NULL &&
+ !auth_fields_exists(fields->extra_fields, "auth_user"))
+ auth_str_add_keyvalue(dest, "auth_user", fields->master_user);
+ if (*request->set->anonymous_username != '\0' &&
+ null_strcmp(fields->user, request->set->anonymous_username) == 0) {
+ /* this is an anonymous login, either via ANONYMOUS
+ SASL mechanism or simply logging in as the anonymous
+ user via another mechanism */
+ str_append(dest, "\tanonymous");
+ }
+ if (!request->auth_only &&
+ auth_fields_exists(fields->extra_fields, "proxy")) {
+ /* we're proxying */
+ if (!auth_fields_exists(fields->extra_fields, "pass") &&
+ request->mech_password != NULL) {
+ /* send back the password that was sent by user
+ (not the password in passdb). */
+ auth_str_add_keyvalue(dest, "pass",
+ request->mech_password);
+ }
+ if (fields->master_user != NULL &&
+ !auth_fields_exists(fields->extra_fields, "master") &&
+ *fields->master_user != '\0') {
+ /* the master username needs to be forwarded */
+ auth_str_add_keyvalue(dest, "master",
+ fields->master_user);
+ }
+ }
+}
+
+static void
+auth_request_handle_failure(struct auth_request *request, const char *reply)
+{
+ struct auth_request_handler *handler = request->handler;
+
+ /* handle failure here */
+ auth_request_log_finished(request);
+
+ if (request->in_delayed_failure_queue) {
+ /* we came here from flush_failures() */
+ handler->callback(reply, handler->conn);
+ return;
+ }
+
+ /* remove the request from requests-list */
+ auth_request_ref(request);
+ auth_request_handler_remove(handler, request);
+
+ if (request->set->policy_report_after_auth)
+ auth_policy_report(request);
+
+ if (auth_fields_exists(request->fields.extra_fields, "nodelay")) {
+ /* passdb specifically requested not to delay the reply. */
+ handler->callback(reply, handler->conn);
+ auth_request_unref(&request);
+ return;
+ }
+
+ /* failure. don't announce it immediately to avoid
+ a) timing attacks, b) flooding */
+ request->in_delayed_failure_queue = TRUE;
+ handler->refcount++;
+
+ if (auth_penalty != NULL) {
+ auth_penalty_update(auth_penalty, request,
+ request->last_penalty + 1);
+ }
+
+ auth_request_refresh_last_access(request);
+ aqueue_append(auth_failures, &request);
+ if (to_auth_failures == NULL) {
+ to_auth_failures =
+ timeout_add_short(AUTH_FAILURE_DELAY_CHECK_MSECS,
+ auth_failure_timeout, NULL);
+ }
+}
+
+static void
+auth_request_handler_reply_success_finish(struct auth_request *request)
+{
+ struct auth_request_handler *handler = request->handler;
+ string_t *str = t_str_new(128);
+
+ auth_request_log_finished(request);
+
+ if (request->last_penalty != 0 && auth_penalty != NULL) {
+ /* reset penalty */
+ auth_penalty_update(auth_penalty, request, 0);
+ }
+
+ /* sanitize these fields, since the login code currently assumes they
+ are exactly in this format. */
+ auth_fields_booleanize(request->fields.extra_fields, "nologin");
+ auth_fields_booleanize(request->fields.extra_fields, "proxy");
+
+ str_printfa(str, "OK\t%u\tuser=", request->id);
+ str_append_tabescaped(str, request->fields.user);
+ auth_str_append_extra_fields(request, str);
+
+ if (request->set->policy_report_after_auth)
+ auth_policy_report(request);
+
+ if (handler->master_callback == NULL ||
+ auth_fields_exists(request->fields.extra_fields, "nologin") ||
+ auth_fields_exists(request->fields.extra_fields, "proxy")) {
+ /* this request doesn't have to wait for master
+ process to pick it up. delete it */
+ auth_request_handler_remove(handler, request);
+ }
+
+ handler->callback(str_c(str), handler->conn);
+}
+
+static void
+auth_request_handler_reply_failure_finish(struct auth_request *request)
+{
+ const char *code = NULL;
+ string_t *str = t_str_new(128);
+
+ auth_fields_remove(request->fields.extra_fields, "nologin");
+
+ str_printfa(str, "FAIL\t%u", request->id);
+ if (request->fields.user != NULL)
+ auth_str_add_keyvalue(str, "user", request->fields.user);
+ else if (request->fields.original_username != NULL) {
+ auth_str_add_keyvalue(str, "user",
+ request->fields.original_username);
+ }
+
+ if (request->internal_failure) {
+ code = AUTH_CLIENT_FAIL_CODE_TEMPFAIL;
+ } else if (request->fields.master_user != NULL) {
+ /* authentication succeeded, but we can't log in
+ as the wanted user */
+ code = AUTH_CLIENT_FAIL_CODE_AUTHZFAILED;
+ } else {
+ switch (request->passdb_result) {
+ case PASSDB_RESULT_NEXT:
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ case PASSDB_RESULT_OK:
+ break;
+ case PASSDB_RESULT_USER_DISABLED:
+ code = AUTH_CLIENT_FAIL_CODE_USER_DISABLED;
+ break;
+ case PASSDB_RESULT_PASS_EXPIRED:
+ code = AUTH_CLIENT_FAIL_CODE_PASS_EXPIRED;
+ break;
+ }
+ }
+
+ if (auth_fields_exists(request->fields.extra_fields, "nodelay")) {
+ /* this is normally a hidden field, need to add it explicitly */
+ str_append(str, "\tnodelay");
+ }
+
+ if (code != NULL) {
+ str_append(str, "\tcode=");
+ str_append(str, code);
+ }
+ auth_str_append_extra_fields(request, str);
+
+ auth_request_handle_failure(request, str_c(str));
+}
+
+static void
+auth_request_handler_proxy_callback(bool success, struct auth_request *request)
+{
+ struct auth_request_handler *handler = request->handler;
+
+ if (success)
+ auth_request_handler_reply_success_finish(request);
+ else
+ auth_request_handler_reply_failure_finish(request);
+ auth_request_handler_unref(&handler);
+}
+
+void auth_request_handler_reply(struct auth_request *request,
+ enum auth_client_result result,
+ const void *auth_reply, size_t reply_size)
+{
+ struct auth_request_handler *handler = request->handler;
+
+ request->handler_pending_reply = FALSE;
+ handler->reply_callback(request, result, auth_reply, reply_size);
+}
+
+static void
+auth_request_handler_default_reply_callback(struct auth_request *request,
+ enum auth_client_result result,
+ const void *auth_reply,
+ size_t reply_size)
+{
+ struct auth_request_handler *handler = request->handler;
+ string_t *str;
+ int ret;
+
+ if (handler->destroyed) {
+ /* the client connection was already closed. we can't do
+ anything but abort this request */
+ request->internal_failure = TRUE;
+ result = AUTH_CLIENT_RESULT_FAILURE;
+ /* make sure this request is set to finished state
+ (it's not with result=continue) */
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+ }
+
+ switch (result) {
+ case AUTH_CLIENT_RESULT_CONTINUE:
+ str = t_str_new(16 + MAX_BASE64_ENCODED_SIZE(reply_size));
+ str_printfa(str, "CONT\t%u\t", request->id);
+ base64_encode(auth_reply, reply_size, str);
+
+ request->accept_cont_input = TRUE;
+ handler->callback(str_c(str), handler->conn);
+ break;
+ case AUTH_CLIENT_RESULT_SUCCESS:
+ if (reply_size > 0) {
+ str = t_str_new(MAX_BASE64_ENCODED_SIZE(reply_size));
+ base64_encode(auth_reply, reply_size, str);
+ auth_fields_add(request->fields.extra_fields, "resp",
+ str_c(str), 0);
+ }
+ ret = auth_request_proxy_finish(request,
+ auth_request_handler_proxy_callback);
+ if (ret < 0)
+ auth_request_handler_reply_failure_finish(request);
+ else if (ret > 0)
+ auth_request_handler_reply_success_finish(request);
+ else
+ return;
+ break;
+ case AUTH_CLIENT_RESULT_FAILURE:
+ auth_request_proxy_finish_failure(request);
+ if (reply_size > 0) {
+ str = t_str_new(MAX_BASE64_ENCODED_SIZE(reply_size));
+ base64_encode(auth_reply, reply_size, str);
+ auth_fields_add(request->fields.extra_fields, "resp",
+ str_c(str), 0);
+ }
+ auth_request_handler_reply_failure_finish(request);
+ break;
+ }
+ /* NOTE: request may be destroyed now */
+
+ auth_request_handler_unref(&handler);
+}
+
+void auth_request_handler_reply_continue(struct auth_request *request,
+ const void *reply, size_t reply_size)
+{
+ request->handler->reply_continue_callback(request, reply, reply_size);
+}
+
+static void
+auth_request_handler_default_reply_continue(struct auth_request *request,
+ const void *reply,
+ size_t reply_size)
+{
+ auth_request_handler_reply(request, AUTH_CLIENT_RESULT_CONTINUE,
+ reply, reply_size);
+}
+
+void auth_request_handler_abort(struct auth_request *request)
+{
+ i_assert(request->handler_pending_reply);
+
+ /* request destroyed while waiting for auth_request_penalty_finish()
+ to be called. */
+ auth_request_handler_unref(&request->handler);
+}
+
+static void
+auth_request_handler_auth_fail_code(struct auth_request_handler *handler,
+ struct auth_request *request,
+ const char *fail_code, const char *reason)
+{
+ string_t *str = t_str_new(128);
+
+ e_info(request->mech_event, "%s", reason);
+
+ str_printfa(str, "FAIL\t%u", request->id);
+ if (*fail_code != '\0') {
+ str_append(str, "\tcode=");
+ str_append(str, fail_code);
+ }
+ str_append(str, "\treason=");
+ str_append_tabescaped(str, reason);
+
+ handler->callback(str_c(str), handler->conn);
+ auth_request_handler_remove(handler, request);
+}
+
+static void auth_request_handler_auth_fail
+(struct auth_request_handler *handler, struct auth_request *request,
+ const char *reason)
+{
+ auth_request_handler_auth_fail_code(handler, request, "", reason);
+}
+
+static void auth_request_timeout(struct auth_request *request)
+{
+ unsigned int secs = (unsigned int)(time(NULL) - request->last_access);
+
+ if (request->state != AUTH_REQUEST_STATE_MECH_CONTINUE) {
+ /* client's fault */
+ e_error(request->mech_event,
+ "Request %u.%u timed out after %u secs, state=%d",
+ request->handler->client_pid, request->id,
+ secs, request->state);
+ } else if (request->set->verbose) {
+ e_info(request->mech_event,
+ "Request timed out waiting for client to continue authentication "
+ "(%u secs)", secs);
+ }
+ auth_request_handler_remove(request->handler, request);
+}
+
+static void auth_request_penalty_finish(struct auth_request *request)
+{
+ timeout_remove(&request->to_penalty);
+ auth_request_initial(request);
+}
+
+static void
+auth_penalty_callback(unsigned int penalty, struct auth_request *request)
+{
+ unsigned int secs;
+
+ request->last_penalty = penalty;
+
+ if (penalty == 0)
+ auth_request_initial(request);
+ else {
+ secs = auth_penalty_to_secs(penalty);
+ request->to_penalty = timeout_add(secs * 1000,
+ auth_request_penalty_finish,
+ request);
+ }
+}
+
+bool auth_request_handler_auth_begin(struct auth_request_handler *handler,
+ const char *args)
+{
+ const struct mech_module *mech;
+ struct auth_request *request;
+ const char *const *list, *name, *arg, *initial_resp;
+ void *initial_resp_data;
+ unsigned int id;
+ buffer_t *buf;
+
+ i_assert(!handler->destroyed);
+
+ /* <id> <mechanism> [...] */
+ list = t_strsplit_tabescaped(args);
+ if (list[0] == NULL || list[1] == NULL ||
+ str_to_uint(list[0], &id) < 0 || id == 0) {
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u "
+ "sent broken AUTH request", handler->client_pid);
+ return FALSE;
+ }
+
+ if (handler->token_auth) {
+ mech = &mech_dovecot_token;
+ if (strcmp(list[1], mech->mech_name) != 0) {
+ /* unsupported mechanism */
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u requested invalid "
+ "authentication mechanism %s (DOVECOT-TOKEN required)",
+ handler->client_pid, str_sanitize(list[1], MAX_MECH_NAME_LEN));
+ return FALSE;
+ }
+ } else {
+ struct auth *auth_default = auth_default_service();
+ mech = mech_register_find(auth_default->reg, list[1]);
+ if (mech == NULL) {
+ /* unsupported mechanism */
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u requested unsupported "
+ "authentication mechanism %s", handler->client_pid,
+ str_sanitize(list[1], MAX_MECH_NAME_LEN));
+ return FALSE;
+ }
+ }
+
+ request = auth_request_new(mech, handler->conn->event);
+ request->handler = handler;
+ request->connect_uid = handler->connect_uid;
+ request->client_pid = handler->client_pid;
+ request->id = id;
+ request->auth_only = handler->master_callback == NULL;
+
+ /* parse optional parameters */
+ initial_resp = NULL;
+ for (list += 2; *list != NULL; list++) {
+ arg = strchr(*list, '=');
+ if (arg == NULL) {
+ name = *list;
+ arg = "";
+ } else {
+ name = t_strdup_until(*list, arg);
+ arg++;
+ }
+
+ if (auth_request_import_auth(request, name, arg))
+ ;
+ else if (strcmp(name, "resp") == 0) {
+ initial_resp = arg;
+ /* this must be the last parameter */
+ list++;
+ break;
+ }
+ }
+
+ if (*list != NULL) {
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u "
+ "sent AUTH parameters after 'resp'",
+ handler->client_pid);
+ auth_request_unref(&request);
+ return FALSE;
+ }
+
+ if (request->fields.service == NULL) {
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u "
+ "didn't specify service in request",
+ handler->client_pid);
+ auth_request_unref(&request);
+ return FALSE;
+ }
+ if (hash_table_lookup(handler->requests, POINTER_CAST(id)) != NULL) {
+ e_error(handler->conn->event,
+ "BUG: Authentication client %u "
+ "sent a duplicate ID %u", handler->client_pid, id);
+ auth_request_unref(&request);
+ return FALSE;
+ }
+ auth_request_init(request);
+
+ request->to_abort = timeout_add(MASTER_AUTH_SERVER_TIMEOUT_SECS * 1000,
+ auth_request_timeout, request);
+ hash_table_insert(handler->requests, POINTER_CAST(id), request);
+
+ if (request->set->ssl_require_client_cert &&
+ !request->fields.valid_client_cert) {
+ /* we fail without valid certificate */
+ auth_request_handler_auth_fail(handler, request,
+ "Client didn't present valid SSL certificate");
+ return TRUE;
+ }
+
+ if (request->set->ssl_require_client_cert &&
+ request->set->ssl_username_from_cert &&
+ !request->fields.cert_username) {
+ auth_request_handler_auth_fail(handler, request,
+ "SSL certificate didn't contain username");
+ return TRUE;
+ }
+
+ /* Handle initial respose */
+ if (initial_resp == NULL) {
+ /* No initial response */
+ request->initial_response = NULL;
+ request->initial_response_len = 0;
+ } else if (handler->conn->version_minor < 2 && *initial_resp == '\0') {
+ /* Some authentication clients like Exim send and empty initial
+ response field when it is in fact absent in the
+ authentication command. This was allowed for older versions
+ of the Dovecot authentication protocol. */
+ request->initial_response = NULL;
+ request->initial_response_len = 0;
+ } else if (*initial_resp == '\0' || strcmp(initial_resp, "=") == 0 ) {
+ /* Empty initial response - Protocols that use SASL often
+ use '=' to indicate an empty initial response; i.e., to
+ distinguish it from an absent initial response. However, that
+ should not be conveyed to the SASL layer (it is not even
+ valid Base64); only the empty string should be passed on.
+ Still, we recognize it here anyway, because we used to make
+ the same mistake. */
+ request->initial_response = uchar_empty_ptr;
+ request->initial_response_len = 0;
+ } else {
+ size_t len = strlen(initial_resp);
+
+ /* Initial response encoded in Bas64 */
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(len));
+ if (base64_decode(initial_resp, len, NULL, buf) < 0) {
+ auth_request_handler_auth_fail_code(handler, request,
+ AUTH_CLIENT_FAIL_CODE_INVALID_BASE64,
+ "Invalid base64 data in initial response");
+ return TRUE;
+ }
+ initial_resp_data =
+ p_malloc(request->pool, I_MAX(buf->used, 1));
+ memcpy(initial_resp_data, buf->data, buf->used);
+ request->initial_response = initial_resp_data;
+ request->initial_response_len = buf->used;
+ }
+
+ /* handler is referenced until auth_request_handler_reply()
+ is called. */
+ handler->refcount++;
+ request->handler_pending_reply = TRUE;
+
+ /* before we start authenticating, see if we need to wait first */
+ auth_penalty_lookup(auth_penalty, request, auth_penalty_callback);
+ return TRUE;
+}
+
+bool auth_request_handler_auth_continue(struct auth_request_handler *handler,
+ const char *args)
+{
+ struct auth_request *request;
+ const char *data;
+ size_t data_len;
+ buffer_t *buf;
+ unsigned int id;
+
+ data = strchr(args, '\t');
+ if (data == NULL || str_to_uint(t_strdup_until(args, data), &id) < 0) {
+ e_error(handler->conn->event,
+ "BUG: Authentication client sent broken CONT request");
+ return FALSE;
+ }
+ data++;
+
+ request = hash_table_lookup(handler->requests, POINTER_CAST(id));
+ if (request == NULL) {
+ const char *reply = t_strdup_printf(
+ "FAIL\t%u\treason=Authentication request timed out", id);
+ handler->callback(reply, handler->conn);
+ return TRUE;
+ }
+
+ /* accept input only once after mechanism has sent a CONT reply */
+ if (!request->accept_cont_input) {
+ auth_request_handler_auth_fail(handler, request,
+ "Unexpected continuation");
+ return TRUE;
+ }
+ request->accept_cont_input = FALSE;
+
+ data_len = strlen(data);
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(data_len));
+ if (base64_decode(data, data_len, NULL, buf) < 0) {
+ auth_request_handler_auth_fail_code(handler, request,
+ AUTH_CLIENT_FAIL_CODE_INVALID_BASE64,
+ "Invalid base64 data in continued response");
+ return TRUE;
+ }
+
+ /* handler is referenced until auth_request_handler_reply()
+ is called. */
+ handler->refcount++;
+ auth_request_continue(request, buf->data, buf->used);
+ return TRUE;
+}
+
+static void auth_str_append_userdb_extra_fields(struct auth_request *request,
+ string_t *dest)
+{
+ str_append_c(dest, '\t');
+ auth_fields_append(request->fields.userdb_reply, dest,
+ AUTH_FIELD_FLAG_HIDDEN, 0);
+
+ if (request->fields.master_user != NULL &&
+ !auth_fields_exists(request->fields.userdb_reply, "master_user")) {
+ auth_str_add_keyvalue(dest, "master_user",
+ request->fields.master_user);
+ }
+ auth_str_add_keyvalue(dest, "auth_mech", request->mech->mech_name);
+ if (*request->set->anonymous_username != '\0' &&
+ strcmp(request->fields.user, request->set->anonymous_username) == 0) {
+ /* this is an anonymous login, either via ANONYMOUS
+ SASL mechanism or simply logging in as the anonymous
+ user via another mechanism */
+ str_append(dest, "\tanonymous");
+ }
+ /* generate auth_token when master service provided session_pid */
+ if (request->request_auth_token &&
+ request->session_pid != (pid_t)-1) {
+ const char *auth_token =
+ auth_token_get(request->fields.service,
+ dec2str(request->session_pid),
+ request->fields.user,
+ request->fields.session_id);
+ auth_str_add_keyvalue(dest, "auth_token", auth_token);
+ }
+ if (request->fields.master_user != NULL) {
+ auth_str_add_keyvalue(dest, "auth_user",
+ request->fields.master_user);
+ } else if (request->fields.original_username != NULL &&
+ strcmp(request->fields.original_username,
+ request->fields.user) != 0) {
+ auth_str_add_keyvalue(dest, "auth_user",
+ request->fields.original_username);
+ }
+}
+
+static void userdb_callback(enum userdb_result result,
+ struct auth_request *request)
+{
+ struct auth_request_handler *handler = request->handler;
+ string_t *str;
+ const char *value;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_USERDB);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+
+ if (request->userdb_lookup_tempfailed)
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+
+ str = t_str_new(128);
+ switch (result) {
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ str_printfa(str, "FAIL\t%u", request->id);
+ if (request->userdb_lookup_tempfailed) {
+ value = auth_fields_find(request->fields.userdb_reply,
+ "reason");
+ if (value != NULL)
+ auth_str_add_keyvalue(str, "reason", value);
+ }
+ break;
+ case USERDB_RESULT_USER_UNKNOWN:
+ str_printfa(str, "NOTFOUND\t%u", request->id);
+ break;
+ case USERDB_RESULT_OK:
+ str_printfa(str, "USER\t%u\t", request->id);
+ str_append_tabescaped(str, request->fields.user);
+ auth_str_append_userdb_extra_fields(request, str);
+ break;
+ }
+ handler->master_callback(str_c(str), request->master);
+
+ auth_master_connection_unref(&request->master);
+ auth_request_unref(&request);
+ auth_request_handler_unref(&handler);
+}
+
+static bool
+auth_master_request_failed(struct auth_request_handler *handler,
+ struct auth_master_connection *master,
+ unsigned int id)
+{
+ if (handler->master_callback == NULL)
+ return FALSE;
+ handler->master_callback(t_strdup_printf("FAIL\t%u", id), master);
+ return TRUE;
+}
+
+bool auth_request_handler_master_request(struct auth_request_handler *handler,
+ struct auth_master_connection *master,
+ unsigned int id, unsigned int client_id,
+ const char *const *params)
+{
+ struct auth_request *request;
+ struct net_unix_cred cred;
+
+ request = hash_table_lookup(handler->requests, POINTER_CAST(client_id));
+ if (request == NULL) {
+ e_error(master->event, "Master request %u.%u not found",
+ handler->client_pid, client_id);
+ return auth_master_request_failed(handler, master, id);
+ }
+
+ auth_request_ref(request);
+ auth_request_handler_remove(handler, request);
+
+ for (; *params != NULL; params++) {
+ const char *name, *param = strchr(*params, '=');
+
+ if (param == NULL) {
+ name = *params;
+ param = "";
+ } else {
+ name = t_strdup_until(*params, param);
+ param++;
+ }
+
+ (void)auth_request_import_master(request, name, param);
+ }
+
+ /* verify session pid if specified and possible */
+ if (request->session_pid != (pid_t)-1 &&
+ net_getunixcred(master->fd, &cred) == 0 &&
+ cred.pid != (pid_t)-1 && request->session_pid != cred.pid) {
+ e_error(master->event,
+ "Session pid %ld provided by master for request %u.%u "
+ "did not match peer credentials (pid=%ld, uid=%ld)",
+ (long)request->session_pid,
+ handler->client_pid, client_id,
+ (long)cred.pid, (long)cred.uid);
+ return auth_master_request_failed(handler, master, id);
+ }
+
+ if (request->state != AUTH_REQUEST_STATE_FINISHED ||
+ !request->fields.successful) {
+ e_error(master->event,
+ "Master requested unfinished authentication request "
+ "%u.%u", handler->client_pid, client_id);
+ handler->master_callback(t_strdup_printf("FAIL\t%u", id),
+ master);
+ auth_request_unref(&request);
+ } else {
+ /* the request isn't being referenced anywhere anymore,
+ so we can do a bit of kludging.. replace the request's
+ old client_id with master's id. */
+ auth_request_set_state(request, AUTH_REQUEST_STATE_USERDB);
+ request->id = id;
+ request->master = master;
+
+ /* master and handler are referenced until userdb_callback i
+ s called. */
+ auth_master_connection_ref(master);
+ handler->refcount++;
+ auth_request_lookup_user(request, userdb_callback);
+ }
+ return TRUE;
+}
+
+void auth_request_handler_cancel_request(struct auth_request_handler *handler,
+ unsigned int client_id)
+{
+ struct auth_request *request;
+
+ request = hash_table_lookup(handler->requests, POINTER_CAST(client_id));
+ if (request != NULL)
+ auth_request_handler_remove(handler, request);
+}
+
+void auth_request_handler_flush_failures(bool flush_all)
+{
+ struct auth_request **auth_requests, *auth_request;
+ unsigned int i, j, count;
+ time_t diff;
+
+ count = aqueue_count(auth_failures);
+ if (count == 0) {
+ timeout_remove(&to_auth_failures);
+ return;
+ }
+
+ auth_requests = array_front_modifiable(&auth_failures_arr);
+ /* count the number of requests that we need to flush */
+ for (i = 0; i < count; i++) {
+ auth_request = auth_requests[aqueue_idx(auth_failures, i)];
+
+ /* FIXME: assumes that failure_delay is always the same. */
+ diff = ioloop_time - auth_request->last_access;
+ if (diff < (time_t)auth_request->set->failure_delay &&
+ !flush_all)
+ break;
+ }
+
+ /* shuffle these requests to try to prevent any kind of timing attacks
+ where attacker performs multiple requests in parallel and attempts
+ to figure out results based on the order of replies. */
+ count = i;
+ for (i = 0; i < count; i++) {
+ j = random() % (count - i) + i;
+ auth_request = auth_requests[aqueue_idx(auth_failures, i)];
+
+ /* swap i & j */
+ auth_requests[aqueue_idx(auth_failures, i)] =
+ auth_requests[aqueue_idx(auth_failures, j)];
+ auth_requests[aqueue_idx(auth_failures, j)] = auth_request;
+ }
+
+ /* flush the requests */
+ for (i = 0; i < count; i++) {
+ auth_request = auth_requests[aqueue_idx(auth_failures, 0)];
+ aqueue_delete_tail(auth_failures);
+
+ i_assert(auth_request != NULL);
+ i_assert(auth_request->state == AUTH_REQUEST_STATE_FINISHED);
+ auth_request_handler_reply(auth_request,
+ AUTH_CLIENT_RESULT_FAILURE,
+ uchar_empty_ptr, 0);
+ auth_request_unref(&auth_request);
+ }
+}
+
+static void auth_failure_timeout(void *context ATTR_UNUSED)
+{
+ auth_request_handler_flush_failures(FALSE);
+}
+
+void auth_request_handler_init(void)
+{
+ i_array_init(&auth_failures_arr, 128);
+ auth_failures = aqueue_init(&auth_failures_arr.arr);
+}
+
+void auth_request_handler_deinit(void)
+{
+ auth_request_handler_flush_failures(TRUE);
+ array_free(&auth_failures_arr);
+ aqueue_deinit(&auth_failures);
+
+ timeout_remove(&to_auth_failures);
+}
diff --git a/src/auth/auth-request-handler.h b/src/auth/auth-request-handler.h
new file mode 100644
index 0000000..ceba935
--- /dev/null
+++ b/src/auth/auth-request-handler.h
@@ -0,0 +1,69 @@
+#ifndef AUTH_REQUEST_HANDLER_H
+#define AUTH_REQUEST_HANDLER_H
+
+struct auth_request;
+struct auth_client_connection;
+struct auth_master_connection;
+struct auth_stream_reply;
+
+enum auth_client_result {
+ AUTH_CLIENT_RESULT_CONTINUE = 1,
+ AUTH_CLIENT_RESULT_SUCCESS,
+ AUTH_CLIENT_RESULT_FAILURE
+};
+
+typedef void
+auth_client_request_callback_t(const char *reply, struct auth_client_connection *conn);
+typedef void
+auth_master_request_callback_t(const char *reply, struct auth_master_connection *conn);
+
+typedef void
+auth_request_handler_reply_callback_t(struct auth_request *request,
+ enum auth_client_result result,
+ const void *auth_reply,
+ size_t reply_size);
+typedef void
+auth_request_handler_reply_continue_callback_t(struct auth_request *request,
+ const void *reply,
+ size_t reply_size);
+
+
+struct auth_request_handler *
+auth_request_handler_create(bool token_auth, auth_client_request_callback_t *callback,
+ struct auth_client_connection *conn,
+ auth_master_request_callback_t *master_callback);
+
+void auth_request_handler_destroy(struct auth_request_handler **handler);
+void auth_request_handler_unref(struct auth_request_handler **handler);
+void auth_request_handler_abort_requests(struct auth_request_handler *handler);
+
+void auth_request_handler_set(struct auth_request_handler *handler,
+ unsigned int connect_uid,
+ unsigned int client_pid);
+
+bool auth_request_handler_auth_begin(struct auth_request_handler *handler,
+ const char *args);
+bool auth_request_handler_auth_continue(struct auth_request_handler *handler,
+ const char *args);
+void auth_request_handler_reply(struct auth_request *request,
+ enum auth_client_result result,
+ const void *reply, size_t reply_size);
+void auth_request_handler_reply_continue(struct auth_request *request,
+ const void *reply, size_t reply_size);
+void auth_request_handler_abort(struct auth_request *request);
+
+unsigned int
+auth_request_handler_get_request_count(struct auth_request_handler *handler);
+bool auth_request_handler_master_request(struct auth_request_handler *handler,
+ struct auth_master_connection *master,
+ unsigned int id, unsigned int client_id,
+ const char *const *params);
+void auth_request_handler_cancel_request(struct auth_request_handler *handler,
+ unsigned int client_id);
+
+void auth_request_handler_flush_failures(bool flush_all);
+
+void auth_request_handler_init(void);
+void auth_request_handler_deinit(void);
+
+#endif
diff --git a/src/auth/auth-request-stats.c b/src/auth/auth-request-stats.c
new file mode 100644
index 0000000..7af4e9a
--- /dev/null
+++ b/src/auth/auth-request-stats.c
@@ -0,0 +1,77 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "buffer.h"
+#include "base64.h"
+#include "stats.h"
+#include "stats-connection.h"
+#include "auth-stats.h"
+#include "auth-request.h"
+#include "auth-request-stats.h"
+
+#define USER_STATS_SOCKET_NAME "old-stats-user"
+
+static struct stats_connection *auth_stats_conn = NULL;
+static struct stats_item *auth_stats_item;
+
+struct auth_stats *auth_request_stats_get(struct auth_request *request)
+{
+ if (request->stats == NULL)
+ request->stats = stats_alloc(request->pool);
+ return stats_fill_ptr(request->stats, auth_stats_item);
+}
+
+void auth_request_stats_add_tempfail(struct auth_request *request)
+{
+ struct auth_stats *stats = auth_request_stats_get(request);
+
+ stats->auth_db_tempfail_count++;
+}
+
+void auth_request_stats_send(struct auth_request *request)
+{
+ string_t *str;
+ buffer_t *buf;
+
+ /* we'll send stats only when the request is finished. this reduces
+ memory usage and is a bit simpler. auth requests are typically
+ pretty short lived anyway. */
+ i_assert(!request->stats_sent);
+ request->stats_sent = TRUE;
+
+ if (request->stats == NULL) {
+ /* nothing happened in this request - don't send it */
+ return;
+ }
+ if (!request->set->stats)
+ return;
+
+ buf = t_buffer_create(128);
+ stats_export(buf, request->stats);
+
+ str = t_str_new(256);
+ str_append(str, "ADD-USER\t");
+ if (request->fields.user != NULL)
+ str_append_tabescaped(str, request->fields.user);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, request->fields.service);
+ str_append_c(str, '\t');
+ base64_encode(buf->data, buf->used, str);
+
+ str_append_c(str, '\n');
+ stats_connection_send(auth_stats_conn, str);
+}
+
+void auth_request_stats_init(void)
+{
+ auth_stats_conn = stats_connection_create(USER_STATS_SOCKET_NAME);
+ auth_stats_item = stats_register(&auth_stats_vfuncs);
+}
+
+void auth_request_stats_deinit(void)
+{
+ stats_connection_unref(&auth_stats_conn);
+ stats_unregister(&auth_stats_item);
+}
diff --git a/src/auth/auth-request-stats.h b/src/auth/auth-request-stats.h
new file mode 100644
index 0000000..5095c2e
--- /dev/null
+++ b/src/auth/auth-request-stats.h
@@ -0,0 +1,15 @@
+#ifndef AUTH_REQUEST_STATS_H
+#define AUTH_REQUEST_STATS_H
+
+#include "auth-stats.h"
+
+struct auth_request;
+
+struct auth_stats *auth_request_stats_get(struct auth_request *request);
+void auth_request_stats_add_tempfail(struct auth_request *request);
+void auth_request_stats_send(struct auth_request *request);
+
+void auth_request_stats_init(void);
+void auth_request_stats_deinit(void);
+
+#endif
diff --git a/src/auth/auth-request-var-expand.c b/src/auth/auth-request-var-expand.c
new file mode 100644
index 0000000..04d774e
--- /dev/null
+++ b/src/auth/auth-request-var-expand.c
@@ -0,0 +1,303 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "auth-request.h"
+
+struct auth_request_var_expand_ctx {
+ const struct auth_request *auth_request;
+ auth_request_escape_func_t *escape_func;
+};
+
+const struct var_expand_table
+auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT+1] = {
+ { 'u', NULL, "user" },
+ { 'n', NULL, "username" },
+ { 'd', NULL, "domain" },
+ { 's', NULL, "service" },
+ { 'h', NULL, "home" },
+ { 'l', NULL, "lip" },
+ { 'r', NULL, "rip" },
+ { 'p', NULL, "pid" },
+ { 'w', NULL, "password" },
+ { '!', NULL, NULL },
+ { 'm', NULL, "mech" },
+ { 'c', NULL, "secured" },
+ { 'a', NULL, "lport" },
+ { 'b', NULL, "rport" },
+ { 'k', NULL, "cert" },
+ { '\0', NULL, "login_user" },
+ { '\0', NULL, "login_username" },
+ { '\0', NULL, "login_domain" },
+ { '\0', NULL, "session" },
+ { '\0', NULL, "real_lip" },
+ { '\0', NULL, "real_rip" },
+ { '\0', NULL, "real_lport" },
+ { '\0', NULL, "real_rport" },
+ { '\0', NULL, "domain_first" },
+ { '\0', NULL, "domain_last" },
+ { '\0', NULL, "master_user" },
+ { '\0', NULL, "session_pid" },
+ { '\0', NULL, "orig_user" },
+ { '\0', NULL, "orig_username" },
+ { '\0', NULL, "orig_domain" },
+ { '\0', NULL, "auth_user" },
+ { '\0', NULL, "auth_username" },
+ { '\0', NULL, "auth_domain" },
+ { '\0', NULL, "local_name" },
+ { '\0', NULL, "client_id" },
+
+ /* aliases: */
+ { '\0', NULL, "local_ip" },
+ { '\0', NULL, "remote_ip" },
+ { '\0', NULL, "local_port" },
+ { '\0', NULL, "remote_port" },
+ { '\0', NULL, "real_local_ip" },
+ { '\0', NULL, "real_remote_ip" },
+ { '\0', NULL, "real_local_port" },
+ { '\0', NULL, "real_remote_port" },
+ { '\0', NULL, "mechanism" },
+ { '\0', NULL, "original_user" },
+ { '\0', NULL, "original_username" },
+ { '\0', NULL, "original_domain" },
+
+ /* be sure to update AUTH_REQUEST_VAR_TAB_COUNT */
+ { '\0', NULL, NULL }
+};
+
+static const char *
+escape_none(const char *string,
+ const struct auth_request *request ATTR_UNUSED)
+{
+ return string;
+}
+
+const char *
+auth_request_str_escape(const char *string,
+ const struct auth_request *request ATTR_UNUSED)
+{
+ return str_escape(string);
+}
+
+struct var_expand_table *
+auth_request_get_var_expand_table_full(const struct auth_request *auth_request,
+ const char *username,
+ auth_request_escape_func_t *escape_func,
+ unsigned int *count)
+{
+ const struct auth_request_fields *fields = &auth_request->fields;
+ const unsigned int auth_count =
+ N_ELEMENTS(auth_request_var_expand_static_tab);
+ struct var_expand_table *tab, *ret_tab;
+ const char *orig_user, *auth_user;
+
+ if (escape_func == NULL)
+ escape_func = escape_none;
+
+ /* keep the extra fields at the beginning. the last static_tab field
+ contains the ending NULL-fields. */
+ tab = ret_tab = t_new(struct var_expand_table,
+ MALLOC_ADD(*count, auth_count));
+ tab += *count;
+ *count += auth_count;
+
+ memcpy(tab, auth_request_var_expand_static_tab,
+ auth_count * sizeof(*tab));
+
+ if (username == NULL)
+ username = "";
+ tab[0].value = escape_func(username, auth_request);
+ tab[1].value = escape_func(t_strcut(username, '@'),
+ auth_request);
+ tab[2].value = i_strchr_to_next(username, '@');
+ if (tab[2].value != NULL)
+ tab[2].value = escape_func(tab[2].value, auth_request);
+ tab[3].value = escape_func(fields->service, auth_request);
+ /* tab[4] = we have no home dir */
+ if (fields->local_ip.family != 0)
+ tab[5].value = tab[35].value =
+ net_ip2addr(&fields->local_ip);
+ if (fields->remote_ip.family != 0)
+ tab[6].value = tab[36].value =
+ net_ip2addr(&fields->remote_ip);
+ tab[7].value = dec2str(auth_request->client_pid);
+ if (auth_request->mech_password != NULL) {
+ tab[8].value = escape_func(auth_request->mech_password,
+ auth_request);
+ }
+ if (auth_request->userdb_lookup) {
+ tab[9].value = auth_request->userdb == NULL ? "" :
+ dec2str(auth_request->userdb->userdb->id);
+ } else {
+ tab[9].value = auth_request->passdb == NULL ? "" :
+ dec2str(auth_request->passdb->passdb->id);
+ }
+ tab[10].value = tab[43].value = fields->mech_name == NULL ? "" :
+ escape_func(fields->mech_name, auth_request);
+ switch (fields->secured) {
+ case AUTH_REQUEST_SECURED_NONE: tab[11].value = ""; break;
+ case AUTH_REQUEST_SECURED: tab[11].value = "secured"; break;
+ case AUTH_REQUEST_SECURED_TLS: tab[11].value = "TLS"; break;
+ default: tab[11].value = ""; break;
+ };
+ tab[12].value = tab[37].value = dec2str(fields->local_port);
+ tab[13].value = tab[38].value = dec2str(fields->remote_port);
+ tab[14].value = fields->valid_client_cert ? "valid" : "";
+
+ if (fields->requested_login_user != NULL) {
+ const char *login_user = fields->requested_login_user;
+
+ tab[15].value = escape_func(login_user, auth_request);
+ tab[16].value = escape_func(t_strcut(login_user, '@'),
+ auth_request);
+ tab[17].value = i_strchr_to_next(login_user, '@');
+ if (tab[17].value != NULL) {
+ tab[17].value = escape_func(tab[17].value,
+ auth_request);
+ }
+ }
+ tab[18].value = fields->session_id == NULL ? NULL :
+ escape_func(fields->session_id, auth_request);
+ if (fields->real_local_ip.family != 0)
+ tab[19].value = tab[39].value =
+ net_ip2addr(&fields->real_local_ip);
+ if (fields->real_remote_ip.family != 0)
+ tab[20].value = tab[40].value =
+ net_ip2addr(&fields->real_remote_ip);
+ tab[21].value = tab[41].value = dec2str(fields->real_local_port);
+ tab[22].value = tab[42].value = dec2str(fields->real_remote_port);
+ tab[23].value = i_strchr_to_next(username, '@');
+ if (tab[23].value != NULL) {
+ tab[23].value = escape_func(t_strcut(tab[23].value, '@'),
+ auth_request);
+ }
+ tab[24].value = strrchr(username, '@');
+ if (tab[24].value != NULL)
+ tab[24].value = escape_func(tab[24].value+1, auth_request);
+ tab[25].value = fields->master_user == NULL ? NULL :
+ escape_func(fields->master_user, auth_request);
+ tab[26].value = auth_request->session_pid == (pid_t)-1 ? NULL :
+ dec2str(auth_request->session_pid);
+
+ orig_user = fields->original_username != NULL ?
+ fields->original_username : username;
+ tab[27].value = tab[44].value = escape_func(orig_user, auth_request);
+ tab[28].value = tab[45].value = escape_func(t_strcut(orig_user, '@'), auth_request);
+ tab[29].value = tab[46].value = i_strchr_to_next(orig_user, '@');
+ if (tab[29].value != NULL)
+ tab[29].value = tab[46].value =
+ escape_func(tab[29].value, auth_request);
+
+ if (fields->master_user != NULL)
+ auth_user = fields->master_user;
+ else
+ auth_user = orig_user;
+ tab[30].value = escape_func(auth_user, auth_request);
+ tab[31].value = escape_func(t_strcut(auth_user, '@'), auth_request);
+ tab[32].value = i_strchr_to_next(auth_user, '@');
+ if (tab[32].value != NULL)
+ tab[32].value = escape_func(tab[32].value, auth_request);
+ if (fields->local_name != NULL)
+ tab[33].value = escape_func(fields->local_name, auth_request);
+ if (fields->client_id != NULL)
+ tab[34].value = escape_func(fields->client_id, auth_request);
+ return ret_tab;
+}
+
+const struct var_expand_table *
+auth_request_get_var_expand_table(const struct auth_request *auth_request,
+ auth_request_escape_func_t *escape_func)
+{
+ unsigned int count = 0;
+
+ return auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, escape_func, &count);
+}
+
+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;
+ }
+}
+
+static int
+auth_request_var_expand_func_passdb(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct auth_request_var_expand_ctx *ctx = context;
+ const char *field_name = t_strcut(data, ':');
+ const char *value;
+
+ value = auth_fields_find(ctx->auth_request->fields.extra_fields, field_name);
+ *value_r = ctx->escape_func(value != NULL ? value : field_get_default(data),
+ ctx->auth_request);
+ return 1;
+}
+
+static int
+auth_request_var_expand_func_userdb(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct auth_request_var_expand_ctx *ctx = context;
+ const char *field_name = t_strcut(data, ':');
+ const char *value;
+
+ value = ctx->auth_request->fields.userdb_reply == NULL ? NULL :
+ auth_fields_find(ctx->auth_request->fields.userdb_reply, field_name);
+ *value_r = ctx->escape_func(value != NULL ? value : field_get_default(data),
+ ctx->auth_request);
+ return 1;
+}
+
+const struct var_expand_func_table auth_request_var_funcs_table[] = {
+ { "passdb", auth_request_var_expand_func_passdb },
+ { "userdb", auth_request_var_expand_func_userdb },
+ { NULL, NULL }
+};
+
+int auth_request_var_expand(string_t *dest, const char *str,
+ const struct auth_request *auth_request,
+ auth_request_escape_func_t *escape_func,
+ const char **error_r)
+{
+ return auth_request_var_expand_with_table(dest, str, auth_request,
+ auth_request_get_var_expand_table(auth_request, escape_func),
+ escape_func, error_r);
+}
+
+int auth_request_var_expand_with_table(string_t *dest, const char *str,
+ const struct auth_request *auth_request,
+ const struct var_expand_table *table,
+ auth_request_escape_func_t *escape_func,
+ const char **error_r)
+{
+ struct auth_request_var_expand_ctx ctx;
+
+ i_zero(&ctx);
+ ctx.auth_request = auth_request;
+ ctx.escape_func = escape_func == NULL ? escape_none : escape_func;
+ return var_expand_with_funcs(dest, str, table,
+ auth_request_var_funcs_table, &ctx, error_r);
+}
+
+int t_auth_request_var_expand(const char *str,
+ const struct auth_request *auth_request ATTR_UNUSED,
+ auth_request_escape_func_t *escape_func ATTR_UNUSED,
+ const char **value_r, const char **error_r)
+{
+ string_t *dest = t_str_new(128);
+ int ret = auth_request_var_expand(dest, str, auth_request,
+ escape_func, error_r);
+ *value_r = str_c(dest);
+ return ret;
+}
diff --git a/src/auth/auth-request-var-expand.h b/src/auth/auth-request-var-expand.h
new file mode 100644
index 0000000..57e18a8
--- /dev/null
+++ b/src/auth/auth-request-var-expand.h
@@ -0,0 +1,44 @@
+#ifndef AUTH_REQUEST_VAR_EXPAND_H
+#define AUTH_REQUEST_VAR_EXPAND_H
+
+typedef const char *
+auth_request_escape_func_t(const char *string,
+ const struct auth_request *auth_request);
+
+#define AUTH_REQUEST_VAR_TAB_USER_IDX 0
+#define AUTH_REQUEST_VAR_TAB_USERNAME_IDX 1
+#define AUTH_REQUEST_VAR_TAB_DOMAIN_IDX 2
+#define AUTH_REQUEST_VAR_TAB_COUNT 47
+extern const struct var_expand_table
+auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT+1];
+
+extern const struct var_expand_func_table auth_request_var_funcs_table[];
+
+const struct var_expand_table *
+auth_request_get_var_expand_table(const struct auth_request *auth_request,
+ auth_request_escape_func_t *escape_func)
+ ATTR_NULL(2);
+struct var_expand_table *
+auth_request_get_var_expand_table_full(const struct auth_request *auth_request,
+ const char *username,
+ auth_request_escape_func_t *escape_func,
+ unsigned int *count) ATTR_NULL(3);
+
+int auth_request_var_expand(string_t *dest, const char *str,
+ const struct auth_request *auth_request,
+ auth_request_escape_func_t *escape_func,
+ const char **error_r);
+int auth_request_var_expand_with_table(string_t *dest, const char *str,
+ const struct auth_request *auth_request,
+ const struct var_expand_table *table,
+ auth_request_escape_func_t *escape_func,
+ const char **error_r);
+int t_auth_request_var_expand(const char *str,
+ const struct auth_request *auth_request,
+ auth_request_escape_func_t *escape_func,
+ const char **value_r, const char **error_r);
+
+const char *auth_request_str_escape(const char *string,
+ const struct auth_request *request);
+
+#endif
diff --git a/src/auth/auth-request.c b/src/auth/auth-request.c
new file mode 100644
index 0000000..635ee20
--- /dev/null
+++ b/src/auth/auth-request.c
@@ -0,0 +1,2595 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "hash.h"
+#include "sha1.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "array.h"
+#include "safe-memset.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "dns-lookup.h"
+#include "auth-cache.h"
+#include "auth-request.h"
+#include "auth-request-handler.h"
+#include "auth-request-handler-private.h"
+#include "auth-request-stats.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+#include "auth-policy.h"
+#include "passdb.h"
+#include "passdb-blocking.h"
+#include "passdb-cache.h"
+#include "passdb-template.h"
+#include "userdb-blocking.h"
+#include "userdb-template.h"
+#include "password-scheme.h"
+#include "wildcard-match.h"
+
+#include <sys/stat.h>
+
+#define AUTH_SUBSYS_PROXY "proxy"
+#define AUTH_DNS_SOCKET_PATH "dns-client"
+#define AUTH_DNS_DEFAULT_TIMEOUT_MSECS (1000*10)
+#define AUTH_DNS_WARN_MSECS 500
+#define AUTH_REQUEST_MAX_DELAY_SECS (60*5)
+#define CACHED_PASSWORD_SCHEME "SHA1"
+
+struct auth_request_proxy_dns_lookup_ctx {
+ struct auth_request *request;
+ auth_request_proxy_cb_t *callback;
+ struct dns_lookup *dns_lookup;
+};
+
+struct auth_policy_check_ctx {
+ enum {
+ AUTH_POLICY_CHECK_TYPE_PLAIN,
+ AUTH_POLICY_CHECK_TYPE_LOOKUP,
+ AUTH_POLICY_CHECK_TYPE_SUCCESS,
+ } type;
+ struct auth_request *request;
+
+ buffer_t *success_data;
+
+ verify_plain_callback_t *callback_plain;
+ lookup_credentials_callback_t *callback_lookup;
+};
+
+const char auth_default_subsystems[2];
+
+unsigned int auth_request_state_count[AUTH_REQUEST_STATE_MAX];
+
+static void get_log_identifier(string_t *str, struct auth_request *auth_request);
+static void
+auth_request_userdb_import(struct auth_request *request, const char *args);
+
+static
+void auth_request_lookup_credentials_policy_continue(struct auth_request *request,
+ lookup_credentials_callback_t *callback);
+static
+void auth_request_policy_check_callback(int result, void *context);
+
+static const char *get_log_prefix_mech(struct auth_request *auth_request)
+{
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_MECH);
+ return str_c(str);
+}
+
+const char *auth_request_get_log_prefix_db(struct auth_request *auth_request)
+{
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_DB);
+ return str_c(str);
+}
+
+static struct event *get_request_event(struct auth_request *request,
+ const char *subsystem)
+{
+ if (subsystem == AUTH_SUBSYS_DB)
+ return authdb_event(request);
+ else if (subsystem == AUTH_SUBSYS_MECH)
+ return request->mech_event;
+ else
+ return request->event;
+}
+
+static void auth_request_post_alloc_init(struct auth_request *request, struct event *parent_event)
+{
+ enum log_type level;
+ request->state = AUTH_REQUEST_STATE_NEW;
+ auth_request_state_count[AUTH_REQUEST_STATE_NEW]++;
+ request->refcount = 1;
+ request->last_access = ioloop_time;
+ request->session_pid = (pid_t)-1;
+ request->set = global_auth_settings;
+ request->event = event_create(parent_event);
+ request->mech_event = event_create(request->event);
+ auth_request_fields_init(request);
+
+ level = request->set->verbose ? LOG_TYPE_INFO : LOG_TYPE_WARNING;
+ event_set_min_log_level(request->event, level);
+ event_set_min_log_level(request->mech_event, level);
+
+ p_array_init(&request->authdb_event, request->pool, 2);
+ event_set_log_prefix_callback(request->mech_event, FALSE, get_log_prefix_mech,
+ request);
+ event_set_forced_debug(request->event, request->set->debug);
+ event_add_category(request->event, &event_category_auth);
+}
+
+struct auth_request *
+auth_request_new(const struct mech_module *mech, struct event *parent_event)
+{
+ struct auth_request *request;
+
+ request = mech->auth_new();
+ request->mech = mech;
+ auth_request_post_alloc_init(request, parent_event);
+
+ return request;
+}
+
+struct auth_request *auth_request_new_dummy(struct event *parent_event)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"auth_request", 1024);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+
+ auth_request_post_alloc_init(request, parent_event);
+ return request;
+}
+
+void auth_request_set_state(struct auth_request *request,
+ enum auth_request_state state)
+{
+ if (request->state == state)
+ return;
+
+ i_assert(request->to_penalty == NULL);
+
+ i_assert(auth_request_state_count[request->state] > 0);
+ auth_request_state_count[request->state]--;
+ auth_request_state_count[state]++;
+
+ request->state = state;
+ auth_refresh_proctitle();
+}
+
+void auth_request_init(struct auth_request *request)
+{
+ struct auth *auth;
+
+ auth = auth_request_get_auth(request);
+ request->set = auth->set;
+ request->passdb = auth->passdbs;
+ request->userdb = auth->userdbs;
+}
+
+struct auth *auth_request_get_auth(struct auth_request *request)
+{
+ return auth_find_service(request->fields.service);
+}
+
+void auth_request_success(struct auth_request *request,
+ const void *data, size_t data_size)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (!request->set->policy_check_after_auth) {
+ struct auth_policy_check_ctx *ctx =
+ p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->success_data = buffer_create_dynamic(request->pool, 1);
+ ctx->request = request;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS;
+ auth_request_policy_check_callback(0, ctx);
+ return;
+ }
+
+ /* perform second policy lookup here */
+ struct auth_policy_check_ctx *ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->success_data = buffer_create_dynamic(request->pool, data_size);
+ buffer_append(ctx->success_data, data, data_size);
+ ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS;
+ auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx);
+}
+
+struct event_passthrough *
+auth_request_finished_event(struct auth_request *request, struct event *event)
+{
+ struct event_passthrough *e = event_create_passthrough(event);
+
+ if (request->failed) {
+ if (request->internal_failure) {
+ e->add_str("error", "internal failure");
+ } else {
+ e->add_str("error", "authentication failed");
+ }
+ } else if (request->fields.successful) {
+ e->add_str("success", "yes");
+ }
+ if (request->userdb_lookup) {
+ return e;
+ }
+ if (request->policy_penalty > 0)
+ e->add_int("policy_penalty", request->policy_penalty);
+ if (request->policy_refusal) {
+ e->add_str("policy_result", "refused");
+ } else if (request->policy_processed && request->policy_penalty > 0) {
+ e->add_str("policy_result", "delayed");
+ e->add_int("policy_penalty", request->policy_penalty);
+ } else if (request->policy_processed) {
+ e->add_str("policy_result", "ok");
+ }
+ return e;
+}
+
+void auth_request_log_finished(struct auth_request *request)
+{
+ if (request->event_finished_sent)
+ return;
+ request->event_finished_sent = TRUE;
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, request, "auth");
+ struct event_passthrough *e =
+ auth_request_finished_event(request, request->event)->
+ set_name("auth_request_finished");
+ e_debug(e->event(), "%sAuth request finished", str_c(str));
+}
+
+static
+void auth_request_success_continue(struct auth_policy_check_ctx *ctx)
+{
+ struct auth_request *request = ctx->request;
+ struct auth_stats *stats;
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ timeout_remove(&request->to_penalty);
+
+ if (request->failed || !request->passdb_success) {
+ /* password was valid, but some other check failed. */
+ auth_request_fail(request);
+ return;
+ }
+ auth_request_set_auth_successful(request);
+
+ /* log before delay */
+ auth_request_log_finished(request);
+
+ if (request->delay_until > ioloop_time) {
+ unsigned int delay_secs = request->delay_until - ioloop_time;
+ request->to_penalty = timeout_add(delay_secs * 1000,
+ auth_request_success_continue, ctx);
+ return;
+ }
+
+ if (ctx->success_data->used > 0 && !request->fields.final_resp_ok) {
+ /* we'll need one more SASL round, since client doesn't support
+ the final SASL response */
+ auth_request_handler_reply_continue(request,
+ ctx->success_data->data, ctx->success_data->used);
+ return;
+ }
+
+ if (request->set->stats) {
+ stats = auth_request_stats_get(request);
+ stats->auth_success_count++;
+ if (request->fields.master_user != NULL)
+ stats->auth_master_success_count++;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+ auth_request_refresh_last_access(request);
+ auth_request_handler_reply(request, AUTH_CLIENT_RESULT_SUCCESS,
+ ctx->success_data->data, ctx->success_data->used);
+}
+
+void auth_request_fail_with_reply(struct auth_request *request,
+ const void *final_data, size_t final_data_size)
+{
+ struct auth_stats *stats;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->set->stats) {
+ stats = auth_request_stats_get(request);
+ stats->auth_failure_count++;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+ auth_request_refresh_last_access(request);
+ auth_request_log_finished(request);
+ auth_request_handler_reply(request, AUTH_CLIENT_RESULT_FAILURE,
+ final_data, final_data_size);
+}
+
+void auth_request_fail(struct auth_request *request)
+{
+ auth_request_fail_with_reply(request, "", 0);
+}
+
+void auth_request_internal_failure(struct auth_request *request)
+{
+ request->internal_failure = TRUE;
+ auth_request_fail(request);
+}
+
+void auth_request_ref(struct auth_request *request)
+{
+ request->refcount++;
+}
+
+void auth_request_unref(struct auth_request **_request)
+{
+ struct auth_request *request = *_request;
+
+ *_request = NULL;
+ i_assert(request->refcount > 0);
+ if (--request->refcount > 0)
+ return;
+
+ i_assert(array_count(&request->authdb_event) == 0);
+
+ if (request->handler_pending_reply)
+ auth_request_handler_abort(request);
+
+ event_unref(&request->mech_event);
+ event_unref(&request->event);
+ auth_request_stats_send(request);
+ auth_request_state_count[request->state]--;
+ auth_refresh_proctitle();
+
+ if (request->mech_password != NULL) {
+ safe_memset(request->mech_password, 0,
+ strlen(request->mech_password));
+ }
+
+ if (request->dns_lookup_ctx != NULL)
+ dns_lookup_abort(&request->dns_lookup_ctx->dns_lookup);
+ timeout_remove(&request->to_abort);
+ timeout_remove(&request->to_penalty);
+
+ if (request->mech != NULL)
+ request->mech->auth_free(request);
+ else
+ pool_unref(&request->pool);
+}
+
+bool auth_request_import_master(struct auth_request *request,
+ const char *key, const char *value)
+{
+ pid_t pid;
+
+ i_assert(value != NULL);
+
+ /* master request lookups may set these */
+ if (strcmp(key, "session_pid") == 0) {
+ if (str_to_pid(value, &pid) == 0)
+ request->session_pid = pid;
+ } else if (strcmp(key, "request_auth_token") == 0)
+ request->request_auth_token = TRUE;
+ else
+ return FALSE;
+ return TRUE;
+}
+
+static bool auth_request_fail_on_nuls(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ if ((request->mech->flags & MECH_SEC_ALLOW_NULS) != 0)
+ return FALSE;
+ if (memchr(data, '\0', data_size) != NULL) {
+ e_debug(request->mech_event, "Unexpected NUL in auth data");
+ auth_request_fail(request);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void auth_request_initial(struct auth_request *request)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_NEW);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (auth_request_fail_on_nuls(request, request->initial_response,
+ request->initial_response_len))
+ return;
+
+ request->mech->auth_initial(request, request->initial_response,
+ request->initial_response_len);
+}
+
+void auth_request_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->fields.successful) {
+ auth_request_success(request, "", 0);
+ return;
+ }
+
+ if (auth_request_fail_on_nuls(request, data, data_size))
+ return;
+
+ auth_request_refresh_last_access(request);
+ request->mech->auth_continue(request, data, data_size);
+}
+
+static void auth_request_save_cache(struct auth_request *request,
+ enum passdb_result result)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *encoded_password;
+ string_t *str;
+ struct password_generate_params gen_params = {
+ .user = request->fields.user,
+ .rounds = 0
+ };
+
+ switch (result) {
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ case PASSDB_RESULT_OK:
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ /* can be cached */
+ break;
+ case PASSDB_RESULT_NEXT:
+ case PASSDB_RESULT_USER_DISABLED:
+ case PASSDB_RESULT_PASS_EXPIRED:
+ /* FIXME: we can't cache this now, or cache lookup would
+ return success. */
+ return;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ i_unreached();
+ }
+
+ if (passdb_cache == NULL || passdb->cache_key == NULL)
+ return;
+
+ if (result < 0) {
+ /* lookup failed. */
+ if (result == PASSDB_RESULT_USER_UNKNOWN) {
+ auth_cache_insert(passdb_cache, request,
+ passdb->cache_key, "", FALSE);
+ }
+ return;
+ }
+
+ if (request->passdb_password == NULL &&
+ !auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ /* passdb didn't provide the correct password */
+ if (result != PASSDB_RESULT_OK ||
+ request->mech_password == NULL)
+ return;
+
+ /* we can still cache valid password lookups though.
+ strdup() it so that mech_password doesn't get
+ cleared too early. */
+ if (!password_generate_encoded(request->mech_password,
+ &gen_params,
+ CACHED_PASSWORD_SCHEME,
+ &encoded_password))
+ i_unreached();
+ request->passdb_password =
+ p_strconcat(request->pool, "{"CACHED_PASSWORD_SCHEME"}",
+ encoded_password, NULL);
+ }
+
+ /* save all except the currently given password in cache */
+ str = t_str_new(256);
+ if (request->passdb_password != NULL) {
+ if (*request->passdb_password != '{') {
+ /* cached passwords must have a known scheme */
+ str_append_c(str, '{');
+ str_append(str, passdb->passdb->default_pass_scheme);
+ str_append_c(str, '}');
+ }
+ str_append_tabescaped(str, request->passdb_password);
+ }
+
+ if (!auth_fields_is_empty(request->fields.extra_fields)) {
+ str_append_c(str, '\t');
+ /* add only those extra fields to cache that were set by this
+ passdb lookup. the CHANGED flag does this, because we
+ snapshotted the extra_fields before the current passdb
+ lookup. */
+ auth_fields_append(request->fields.extra_fields, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ }
+ auth_cache_insert(passdb_cache, request, passdb->cache_key, str_c(str),
+ result == PASSDB_RESULT_OK);
+}
+
+static bool
+auth_request_mechanism_accepted(const char *const *mechs,
+ const struct mech_module *mech)
+{
+ /* no filter specified, anything goes */
+ if (mechs == NULL) return TRUE;
+ /* request has no mechanism, see if none is accepted */
+ if (mech == NULL)
+ return str_array_icase_find(mechs, "none");
+ /* check if request mechanism is accepted */
+ return str_array_icase_find(mechs, mech->mech_name);
+}
+
+/**
+
+Check if username is included in the filter. Logic is that if the username
+is not excluded by anything, and is included by something, it will be accepted.
+By default, all usernames are included, unless there is a inclusion item, when
+username will be excluded if there is no inclusion for it.
+
+Exclusions are denoted with a ! in front of the pattern.
+*/
+bool auth_request_username_accepted(const char *const *filter, const char *username)
+{
+ bool have_includes = FALSE;
+ bool matched_inc = FALSE;
+
+ for(;*filter != NULL; filter++) {
+ /* if filter has ! it means the pattern will be refused */
+ bool exclude = (**filter == '!');
+ if (!exclude)
+ have_includes = TRUE;
+ if (wildcard_match(username, (*filter)+(exclude?1:0))) {
+ if (exclude) {
+ return FALSE;
+ } else {
+ matched_inc = TRUE;
+ }
+ }
+ }
+
+ return matched_inc || !have_includes;
+}
+
+static bool
+auth_request_want_skip_passdb(struct auth_request *request,
+ struct auth_passdb *passdb)
+{
+ /* if mechanism is not supported, skip */
+ const char *const *mechs = passdb->passdb->mechanisms;
+ const char *const *username_filter = passdb->passdb->username_filter;
+ const char *username;
+
+ username = request->fields.user;
+
+ if (!auth_request_mechanism_accepted(mechs, request->mech)) {
+ auth_request_log_debug(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH
+ : "none",
+ "skipping passdb: mechanism filtered");
+ return TRUE;
+ }
+
+ if (passdb->passdb->username_filter != NULL &&
+ !auth_request_username_accepted(username_filter, username)) {
+ auth_request_log_debug(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH
+ : "none",
+ "skipping passdb: username filtered");
+ return TRUE;
+ }
+
+ /* skip_password_check basically specifies if authentication is
+ finished */
+ bool authenticated = request->fields.skip_password_check;
+
+ switch (passdb->skip) {
+ case AUTH_PASSDB_SKIP_NEVER:
+ return FALSE;
+ case AUTH_PASSDB_SKIP_AUTHENTICATED:
+ return authenticated;
+ case AUTH_PASSDB_SKIP_UNAUTHENTICATED:
+ return !authenticated;
+ }
+ i_unreached();
+}
+
+static bool
+auth_request_want_skip_userdb(struct auth_request *request,
+ struct auth_userdb *userdb)
+{
+ switch (userdb->skip) {
+ case AUTH_USERDB_SKIP_NEVER:
+ return FALSE;
+ case AUTH_USERDB_SKIP_FOUND:
+ return request->userdb_success;
+ case AUTH_USERDB_SKIP_NOTFOUND:
+ return !request->userdb_success;
+ }
+ i_unreached();
+}
+
+static const char *
+auth_request_cache_result_to_str(enum auth_request_cache_result result)
+{
+ switch(result) {
+ case AUTH_REQUEST_CACHE_NONE:
+ return "none";
+ case AUTH_REQUEST_CACHE_HIT:
+ return "hit";
+ case AUTH_REQUEST_CACHE_MISS:
+ return "miss";
+ default:
+ i_unreached();
+ }
+}
+
+void auth_request_passdb_lookup_begin(struct auth_request *request)
+{
+ struct event *event;
+ const char *name;
+
+ i_assert(request->passdb != NULL);
+ i_assert(!request->userdb_lookup);
+
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+
+ name = (request->passdb->set->name[0] != '\0' ?
+ request->passdb->set->name :
+ request->passdb->passdb->iface.name);
+
+ event = event_create(request->event);
+ event_add_str(event, "passdb_id", dec2str(request->passdb->passdb->id));
+ event_add_str(event, "passdb_name", name);
+ event_add_str(event, "passdb", request->passdb->passdb->iface.name);
+ event_set_log_prefix_callback(event, FALSE,
+ auth_request_get_log_prefix_db, request);
+
+ /* check if we should enable verbose logging here */
+ if (*request->passdb->set->auth_verbose == 'y')
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+ else if (*request->passdb->set->auth_verbose == 'n')
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+
+ e_debug(event_create_passthrough(event)->
+ set_name("auth_passdb_request_started")->
+ event(),
+ "Performing passdb lookup");
+ array_push_back(&request->authdb_event, &event);
+}
+
+void auth_request_passdb_lookup_end(struct auth_request *request,
+ enum passdb_result result)
+{
+ i_assert(array_count(&request->authdb_event) > 0);
+ struct event *event = authdb_event(request);
+ struct event_passthrough *e =
+ event_create_passthrough(event)->
+ set_name("auth_passdb_request_finished")->
+ add_str("result", passdb_result_to_string(result));
+ if (request->passdb_cache_result != AUTH_REQUEST_CACHE_NONE &&
+ request->set->cache_ttl != 0 && request->set->cache_size != 0)
+ e->add_str("cache", auth_request_cache_result_to_str(request->passdb_cache_result));
+ e_debug(e->event(), "Finished passdb lookup");
+ event_unref(&event);
+ array_pop_back(&request->authdb_event);
+}
+
+void auth_request_userdb_lookup_begin(struct auth_request *request)
+{
+ struct event *event;
+ const char *name;
+
+ i_assert(request->userdb != NULL);
+ i_assert(request->userdb_lookup);
+
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+
+ name = (request->userdb->set->name[0] != '\0' ?
+ request->userdb->set->name :
+ request->userdb->userdb->iface->name);
+
+ event = event_create(request->event);
+ event_add_str(event, "userdb_id", dec2str(request->userdb->userdb->id));
+ event_add_str(event, "userdb_name", name);
+ event_add_str(event, "userdb", request->userdb->userdb->iface->name);
+ event_set_log_prefix_callback(event, FALSE,
+ auth_request_get_log_prefix_db, request);
+
+ /* check if we should enable verbose logging here*/
+ if (*request->userdb->set->auth_verbose == 'y')
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+ else if (*request->userdb->set->auth_verbose == 'n')
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+
+ e_debug(event_create_passthrough(event)->
+ set_name("auth_userdb_request_started")->
+ event(),
+ "Performing userdb lookup");
+ array_push_back(&request->authdb_event, &event);
+}
+
+void auth_request_userdb_lookup_end(struct auth_request *request,
+ enum userdb_result result)
+{
+ i_assert(array_count(&request->authdb_event) > 0);
+ struct event *event = authdb_event(request);
+ struct event_passthrough *e =
+ event_create_passthrough(event)->
+ set_name("auth_userdb_request_finished")->
+ add_str("result", userdb_result_to_string(result));
+ if (request->userdb_cache_result != AUTH_REQUEST_CACHE_NONE &&
+ request->set->cache_ttl != 0 && request->set->cache_size != 0)
+ e->add_str("cache", auth_request_cache_result_to_str(request->userdb_cache_result));
+ e_debug(e->event(), "Finished userdb lookup");
+ event_unref(&event);
+ array_pop_back(&request->authdb_event);
+}
+
+static bool
+auth_request_handle_passdb_callback(enum passdb_result *result,
+ struct auth_request *request)
+{
+ struct auth_passdb *next_passdb;
+ enum auth_db_rule result_rule;
+ bool passdb_continue = FALSE;
+
+ if (request->passdb_password != NULL) {
+ safe_memset(request->passdb_password, 0,
+ strlen(request->passdb_password));
+ }
+
+ auth_request_passdb_lookup_end(request, *result);
+
+ if (request->passdb->set->deny &&
+ *result != PASSDB_RESULT_USER_UNKNOWN) {
+ /* deny passdb. we can get through this step only if the
+ lookup returned that user doesn't exist in it. internal
+ errors are fatal here. */
+ if (*result != PASSDB_RESULT_INTERNAL_FAILURE) {
+ e_info(authdb_event(request),
+ "User found from deny passdb");
+ *result = PASSDB_RESULT_USER_DISABLED;
+ }
+ return TRUE;
+ }
+ if (request->failed) {
+ /* The passdb didn't fail, but something inside it failed
+ (e.g. allow_nets mismatch). Make sure we'll fail this
+ lookup, but reset the failure so the next passdb can
+ succeed. */
+ if (*result == PASSDB_RESULT_OK)
+ *result = PASSDB_RESULT_USER_UNKNOWN;
+ request->failed = FALSE;
+ }
+
+ /* users that exist but can't log in are special. we don't try to match
+ any of the success/failure rules to them. they'll always fail. */
+ switch (*result) {
+ case PASSDB_RESULT_USER_DISABLED:
+ return TRUE;
+ case PASSDB_RESULT_PASS_EXPIRED:
+ auth_request_set_field(request, "reason",
+ "Password expired", NULL);
+ return TRUE;
+
+ case PASSDB_RESULT_OK:
+ result_rule = request->passdb->result_success;
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ result_rule = request->passdb->result_internalfail;
+ break;
+ case PASSDB_RESULT_NEXT:
+ e_debug(authdb_event(request),
+ "Not performing authentication (noauthenticate set)");
+ result_rule = AUTH_DB_RULE_CONTINUE;
+ break;
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ default:
+ result_rule = request->passdb->result_failure;
+ break;
+ }
+
+ switch (result_rule) {
+ case AUTH_DB_RULE_RETURN:
+ break;
+ case AUTH_DB_RULE_RETURN_OK:
+ request->passdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_RETURN_FAIL:
+ request->passdb_success = FALSE;
+ break;
+ case AUTH_DB_RULE_CONTINUE:
+ passdb_continue = TRUE;
+ if (*result == PASSDB_RESULT_OK) {
+ /* password was successfully verified. don't bother
+ checking it again. */
+ auth_request_set_password_verified(request);
+ }
+ break;
+ case AUTH_DB_RULE_CONTINUE_OK:
+ passdb_continue = TRUE;
+ request->passdb_success = TRUE;
+ /* password was successfully verified. don't bother
+ checking it again. */
+ auth_request_set_password_verified(request);
+ break;
+ case AUTH_DB_RULE_CONTINUE_FAIL:
+ passdb_continue = TRUE;
+ request->passdb_success = FALSE;
+ break;
+ }
+ /* nopassword check is specific to a single passdb and shouldn't leak
+ to the next one. we already added it to cache. */
+ auth_fields_remove(request->fields.extra_fields, "nopassword");
+ auth_fields_remove(request->fields.extra_fields, "noauthenticate");
+
+ if (request->fields.requested_login_user != NULL &&
+ *result == PASSDB_RESULT_OK) {
+ auth_request_master_user_login_finish(request);
+ /* if the passdb lookup continues, it continues with non-master
+ passdbs for the requested_login_user. */
+ next_passdb = auth_request_get_auth(request)->passdbs;
+ } else {
+ next_passdb = request->passdb->next;
+ }
+
+ while (next_passdb != NULL &&
+ auth_request_want_skip_passdb(request, next_passdb))
+ next_passdb = next_passdb->next;
+
+ if (*result == PASSDB_RESULT_OK || *result == PASSDB_RESULT_NEXT) {
+ /* this passdb lookup succeeded, preserve its extra fields */
+ auth_fields_snapshot(request->fields.extra_fields);
+ request->snapshot_have_userdb_prefetch_set =
+ request->userdb_prefetch_set;
+ if (request->fields.userdb_reply != NULL)
+ auth_fields_snapshot(request->fields.userdb_reply);
+ } else {
+ /* this passdb lookup failed, remove any extra fields it set */
+ auth_fields_rollback(request->fields.extra_fields);
+ if (request->fields.userdb_reply != NULL) {
+ auth_fields_rollback(request->fields.userdb_reply);
+ request->userdb_prefetch_set =
+ request->snapshot_have_userdb_prefetch_set;
+ }
+ }
+
+ if (passdb_continue && next_passdb != NULL) {
+ /* try next passdb. */
+ request->passdb = next_passdb;
+ request->passdb_password = NULL;
+
+ if (*result == PASSDB_RESULT_USER_UNKNOWN) {
+ /* remember that we did at least one successful
+ passdb lookup */
+ request->passdbs_seen_user_unknown = TRUE;
+ } else if (*result == PASSDB_RESULT_INTERNAL_FAILURE) {
+ /* remember that we have had an internal failure. at
+ the end return internal failure if we couldn't
+ successfully login. */
+ request->passdbs_seen_internal_failure = TRUE;
+ }
+ return FALSE;
+ } else if (*result == PASSDB_RESULT_NEXT) {
+ /* admin forgot to put proper passdb last */
+ e_error(request->event,
+ "%sLast passdb had noauthenticate field, cannot authenticate user",
+ auth_request_get_log_prefix_db(request));
+ *result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else if (request->passdb_success) {
+ /* either this or a previous passdb lookup succeeded. */
+ *result = PASSDB_RESULT_OK;
+ } else if (request->passdbs_seen_internal_failure) {
+ /* last passdb lookup returned internal failure. it may have
+ had the correct password, so return internal failure
+ instead of plain failure. */
+ *result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ return TRUE;
+}
+
+void
+auth_request_verify_plain_callback_finish(enum passdb_result result,
+ struct auth_request *request)
+{
+ const char *error;
+
+ if (passdb_template_export(request->passdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand override_fields: %s", error);
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ if (!auth_request_handle_passdb_callback(&result, request)) {
+ /* try next passdb */
+ auth_request_verify_plain(request, request->mech_password,
+ request->private_callback.verify_plain);
+ } else {
+ auth_request_ref(request);
+ request->passdb_result = result;
+ request->private_callback.verify_plain(request->passdb_result, request);
+ auth_request_unref(&request);
+ }
+}
+
+void auth_request_verify_plain_callback(enum passdb_result result,
+ struct auth_request *request)
+{
+ struct auth_passdb *passdb = request->passdb;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_PASSDB);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (result == PASSDB_RESULT_OK &&
+ auth_fields_exists(request->fields.extra_fields, "noauthenticate"))
+ result = PASSDB_RESULT_NEXT;
+
+ if (result != PASSDB_RESULT_INTERNAL_FAILURE)
+ auth_request_save_cache(request, result);
+ else {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = passdb->cache_key;
+
+ auth_request_stats_add_tempfail(request);
+ if (passdb_cache_verify_plain(request, cache_key,
+ request->mech_password,
+ &result, TRUE)) {
+ e_info(authdb_event(request),
+ "Falling back to expired data from cache");
+ return;
+ }
+ }
+
+ auth_request_verify_plain_callback_finish(result, request);
+}
+
+static bool password_has_illegal_chars(const char *password)
+{
+ for (; *password != '\0'; password++) {
+ switch (*password) {
+ case '\001':
+ case '\t':
+ case '\r':
+ case '\n':
+ /* these characters have a special meaning in internal
+ protocols, make sure the password doesn't
+ accidentally get there unescaped. */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool auth_request_is_disabled_master_user(struct auth_request *request)
+{
+ if (request->fields.requested_login_user == NULL ||
+ request->passdb != NULL)
+ return FALSE;
+
+ /* no masterdbs, master logins not supported */
+ e_info(request->mech_event,
+ "Attempted master login with no master passdbs "
+ "(trying to log in as user: %s)",
+ request->fields.requested_login_user);
+ return TRUE;
+}
+
+static
+void auth_request_policy_penalty_finish(void *context)
+{
+ struct auth_policy_check_ctx *ctx = context;
+
+ timeout_remove(&ctx->request->to_penalty);
+
+ i_assert(ctx->request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ switch(ctx->type) {
+ case AUTH_POLICY_CHECK_TYPE_PLAIN:
+ ctx->request->handler->verify_plain_continue_callback(ctx->request, ctx->callback_plain);
+ return;
+ case AUTH_POLICY_CHECK_TYPE_LOOKUP:
+ auth_request_lookup_credentials_policy_continue(ctx->request, ctx->callback_lookup);
+ return;
+ case AUTH_POLICY_CHECK_TYPE_SUCCESS:
+ auth_request_success_continue(ctx);
+ return;
+ default:
+ i_unreached();
+ }
+}
+
+static
+void auth_request_policy_check_callback(int result, void *context)
+{
+ struct auth_policy_check_ctx *ctx = context;
+
+ ctx->request->policy_processed = TRUE;
+ /* It's possible that multiple policy lookups return a penalty.
+ Sum them all up to the event. */
+ ctx->request->policy_penalty += result < 0 ? 0 : result;
+
+ if (ctx->request->set->policy_log_only && result != 0) {
+ auth_request_policy_penalty_finish(context);
+ return;
+ }
+ if (result < 0) {
+ /* fail it right here and now */
+ auth_request_fail(ctx->request);
+ } else if (ctx->type != AUTH_POLICY_CHECK_TYPE_SUCCESS && result > 0 &&
+ !ctx->request->fields.no_penalty) {
+ ctx->request->to_penalty = timeout_add(result * 1000,
+ auth_request_policy_penalty_finish,
+ context);
+ } else {
+ auth_request_policy_penalty_finish(context);
+ }
+}
+
+void auth_request_verify_plain(struct auth_request *request,
+ const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct auth_policy_check_ctx *ctx;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->mech_password == NULL)
+ request->mech_password = p_strdup(request->pool, password);
+ else
+ i_assert(request->mech_password == password);
+ request->user_changed_by_lookup = FALSE;
+
+ if (request->policy_processed || !request->set->policy_check_before_auth) {
+ request->handler->verify_plain_continue_callback(request,
+ callback);
+ } else {
+ ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->callback_plain = callback;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_PLAIN;
+ auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx);
+ }
+}
+
+void auth_request_default_verify_plain_continue(struct auth_request *request,
+ verify_plain_callback_t *callback)
+{
+ struct auth_passdb *passdb;
+ enum passdb_result result;
+ const char *cache_key, *error;
+ const char *password = request->mech_password;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (auth_request_is_disabled_master_user(request)) {
+ callback(PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ if (password_has_illegal_chars(password)) {
+ e_info(authdb_event(request),
+ "Attempted login with password having illegal chars");
+ callback(PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ passdb = request->passdb;
+
+ while (passdb != NULL && auth_request_want_skip_passdb(request, passdb))
+ passdb = passdb->next;
+
+ request->passdb = passdb;
+
+ if (passdb == NULL) {
+ auth_request_log_error(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH : "none",
+ "All password databases were skipped");
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+
+ auth_request_passdb_lookup_begin(request);
+ request->private_callback.verify_plain = callback;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (passdb_cache_verify_plain(request, cache_key, password,
+ &result, FALSE)) {
+ return;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB);
+ /* In case this request had already done a credentials lookup (is it
+ even possible?), make sure wanted_credentials_scheme is cleared
+ so passdbs don't think we're doing a credentials lookup. */
+ request->wanted_credentials_scheme = NULL;
+
+ if (passdb->passdb->iface.verify_plain == NULL) {
+ /* we're deinitializing and just want to get rid of this
+ request */
+ auth_request_verify_plain_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE, request);
+ } else if (passdb->passdb->blocking) {
+ passdb_blocking_verify_plain(request);
+ } else if (passdb_template_export(passdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_verify_plain_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE, request);
+ } else {
+ passdb->passdb->iface.verify_plain(request, password,
+ auth_request_verify_plain_callback);
+ }
+}
+
+static void
+auth_request_lookup_credentials_finish(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request)
+{
+ const char *error;
+
+ if (passdb_template_export(request->passdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand override_fields: %s", error);
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ if (!auth_request_handle_passdb_callback(&result, request)) {
+ /* try next passdb */
+ if (request->fields.skip_password_check &&
+ request->fields.delayed_credentials == NULL && size > 0) {
+ /* passdb continue* rule after a successful lookup.
+ remember these credentials and use them later on. */
+ auth_request_set_delayed_credentials(request,
+ credentials, size);
+ }
+ auth_request_lookup_credentials(request,
+ request->wanted_credentials_scheme,
+ request->private_callback.lookup_credentials);
+ } else {
+ if (request->fields.delayed_credentials != NULL && size == 0) {
+ /* we did multiple passdb lookups, but the last one
+ didn't provide any credentials (e.g. just wanted to
+ add some extra fields). so use the first passdb's
+ credentials instead. */
+ credentials = request->fields.delayed_credentials;
+ size = request->fields.delayed_credentials_size;
+ }
+ if (request->set->debug_passwords &&
+ result == PASSDB_RESULT_OK) {
+ e_debug(authdb_event(request),
+ "Credentials: %s",
+ binary_to_hex(credentials, size));
+ }
+ if (result == PASSDB_RESULT_SCHEME_NOT_AVAILABLE &&
+ request->passdbs_seen_user_unknown) {
+ /* one of the passdbs accepted the scheme,
+ but the user was unknown there */
+ result = PASSDB_RESULT_USER_UNKNOWN;
+ }
+ request->passdb_result = result;
+ request->private_callback.
+ lookup_credentials(result, credentials, size, request);
+ }
+}
+
+void auth_request_lookup_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *cache_cred, *cache_scheme;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_PASSDB);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (result == PASSDB_RESULT_OK &&
+ auth_fields_exists(request->fields.extra_fields, "noauthenticate"))
+ result = PASSDB_RESULT_NEXT;
+
+ if (result != PASSDB_RESULT_INTERNAL_FAILURE)
+ auth_request_save_cache(request, result);
+ else {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = passdb->cache_key;
+
+ auth_request_stats_add_tempfail(request);
+ if (passdb_cache_lookup_credentials(request, cache_key,
+ &cache_cred, &cache_scheme,
+ &result, TRUE)) {
+ e_info(authdb_event(request),
+ "Falling back to expired data from cache");
+ passdb_handle_credentials(
+ result, cache_cred, cache_scheme,
+ auth_request_lookup_credentials_finish,
+ request);
+ return;
+ }
+ }
+
+ auth_request_lookup_credentials_finish(result, credentials, size,
+ request);
+}
+
+void auth_request_lookup_credentials(struct auth_request *request,
+ const char *scheme,
+ lookup_credentials_callback_t *callback)
+{
+ struct auth_policy_check_ctx *ctx;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->wanted_credentials_scheme == NULL)
+ request->wanted_credentials_scheme =
+ p_strdup(request->pool, scheme);
+ request->user_changed_by_lookup = FALSE;
+
+ if (request->policy_processed || !request->set->policy_check_before_auth)
+ auth_request_lookup_credentials_policy_continue(request, callback);
+ else {
+ ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->callback_lookup = callback;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_LOOKUP;
+ auth_policy_check(request, ctx->request->mech_password, auth_request_policy_check_callback, ctx);
+ }
+}
+
+static
+void auth_request_lookup_credentials_policy_continue(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct auth_passdb *passdb;
+ const char *cache_key, *cache_cred, *cache_scheme, *error;
+ enum passdb_result result;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+ if (auth_request_is_disabled_master_user(request)) {
+ callback(PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request);
+ return;
+ }
+ passdb = request->passdb;
+ while (passdb != NULL && auth_request_want_skip_passdb(request, passdb))
+ passdb = passdb->next;
+ request->passdb = passdb;
+
+ if (passdb == NULL) {
+ auth_request_log_error(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH : "none",
+ "All password databases were skipped");
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request);
+ return;
+ }
+
+ auth_request_passdb_lookup_begin(request);
+ request->private_callback.lookup_credentials = callback;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (cache_key != NULL) {
+ if (passdb_cache_lookup_credentials(request, cache_key,
+ &cache_cred, &cache_scheme,
+ &result, FALSE)) {
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ passdb_handle_credentials(
+ result, cache_cred, cache_scheme,
+ auth_request_lookup_credentials_finish,
+ request);
+ return;
+ } else {
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ }
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB);
+
+ if (passdb->passdb->iface.lookup_credentials == NULL) {
+ /* this passdb doesn't support credentials */
+ e_debug(authdb_event(request),
+ "passdb doesn't support credential lookups");
+ auth_request_lookup_credentials_callback(
+ PASSDB_RESULT_SCHEME_NOT_AVAILABLE,
+ uchar_empty_ptr, 0, request);
+ } else if (passdb->passdb->blocking) {
+ passdb_blocking_lookup_credentials(request);
+ } else if (passdb_template_export(passdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_lookup_credentials_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE,
+ uchar_empty_ptr, 0, request);
+ } else {
+ passdb->passdb->iface.lookup_credentials(request,
+ auth_request_lookup_credentials_callback);
+ }
+}
+
+void auth_request_set_credentials(struct auth_request *request,
+ const char *scheme, const char *data,
+ set_credentials_callback_t *callback)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *cache_key, *new_credentials;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (cache_key != NULL)
+ auth_cache_remove(passdb_cache, request, cache_key);
+
+ request->private_callback.set_credentials = callback;
+
+ new_credentials = t_strdup_printf("{%s}%s", scheme, data);
+ if (passdb->passdb->blocking)
+ passdb_blocking_set_credentials(request, new_credentials);
+ else if (passdb->passdb->iface.set_credentials != NULL) {
+ passdb->passdb->iface.set_credentials(request, new_credentials,
+ callback);
+ } else {
+ /* this passdb doesn't support credentials update */
+ callback(FALSE, request);
+ }
+}
+
+static void auth_request_userdb_save_cache(struct auth_request *request,
+ enum userdb_result result)
+{
+ struct auth_userdb *userdb = request->userdb;
+ string_t *str;
+ const char *cache_value;
+
+ if (passdb_cache == NULL || userdb->cache_key == NULL)
+ return;
+
+ if (result == USERDB_RESULT_USER_UNKNOWN)
+ cache_value = "";
+ else {
+ str = t_str_new(128);
+ auth_fields_append(request->fields.userdb_reply, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ if (request->user_changed_by_lookup) {
+ /* username was changed by passdb or userdb */
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append(str, "user=");
+ str_append_tabescaped(str, request->fields.user);
+ }
+ if (str_len(str) == 0) {
+ /* no userdb fields. but we can't save an empty string,
+ since that means "user unknown". */
+ str_append(str, AUTH_REQUEST_USER_KEY_IGNORE);
+ }
+ cache_value = str_c(str);
+ }
+ /* last_success has no meaning with userdb */
+ auth_cache_insert(passdb_cache, request, userdb->cache_key,
+ cache_value, FALSE);
+}
+
+static bool auth_request_lookup_user_cache(struct auth_request *request,
+ const char *key,
+ enum userdb_result *result_r,
+ bool use_expired)
+{
+ struct auth_stats *stats = auth_request_stats_get(request);
+ const char *value;
+ struct auth_cache_node *node;
+ bool expired, neg_expired;
+
+ value = auth_cache_lookup(passdb_cache, request, key, &node,
+ &expired, &neg_expired);
+ if (value == NULL || (expired && !use_expired)) {
+ stats->auth_cache_miss_count++;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ e_debug(request->event,
+ value == NULL ? "%suserdb cache miss" :
+ "%suserdb cache expired",
+ auth_request_get_log_prefix_db(request));
+ return FALSE;
+ }
+ stats->auth_cache_hit_count++;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ e_debug(request->event,
+ "%suserdb cache hit: %s",
+ auth_request_get_log_prefix_db(request), value);
+
+ if (*value == '\0') {
+ /* negative cache entry */
+ *result_r = USERDB_RESULT_USER_UNKNOWN;
+ auth_request_init_userdb_reply(request, FALSE);
+ return TRUE;
+ }
+
+ /* We want to preserve any userdb fields set by the earlier passdb
+ lookup, so initialize userdb_reply only if it doesn't exist.
+ Don't add userdb's default_fields, because the entire userdb part of
+ the result comes from the cache. */
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, FALSE);
+ auth_request_userdb_import(request, value);
+ *result_r = USERDB_RESULT_OK;
+ return TRUE;
+}
+
+void auth_request_userdb_callback(enum userdb_result result,
+ struct auth_request *request)
+{
+ struct auth_userdb *userdb = request->userdb;
+ struct auth_userdb *next_userdb;
+ enum auth_db_rule result_rule;
+ const char *error;
+ bool userdb_continue = FALSE;
+
+ switch (result) {
+ case USERDB_RESULT_OK:
+ result_rule = userdb->result_success;
+ break;
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ auth_request_stats_add_tempfail(request);
+ result_rule = userdb->result_internalfail;
+ break;
+ case USERDB_RESULT_USER_UNKNOWN:
+ default:
+ result_rule = userdb->result_failure;
+ break;
+ }
+
+ switch (result_rule) {
+ case AUTH_DB_RULE_RETURN:
+ break;
+ case AUTH_DB_RULE_RETURN_OK:
+ request->userdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_RETURN_FAIL:
+ request->userdb_success = FALSE;
+ break;
+ case AUTH_DB_RULE_CONTINUE:
+ userdb_continue = TRUE;
+ break;
+ case AUTH_DB_RULE_CONTINUE_OK:
+ userdb_continue = TRUE;
+ request->userdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_CONTINUE_FAIL:
+ userdb_continue = TRUE;
+ request->userdb_success = FALSE;
+ break;
+ }
+
+ auth_request_userdb_lookup_end(request, result);
+
+ next_userdb = userdb->next;
+ while (next_userdb != NULL &&
+ auth_request_want_skip_userdb(request, next_userdb))
+ next_userdb = next_userdb->next;
+
+ if (userdb_continue && next_userdb != NULL) {
+ /* try next userdb. */
+ if (result == USERDB_RESULT_INTERNAL_FAILURE)
+ request->userdbs_seen_internal_failure = TRUE;
+
+ if (result == USERDB_RESULT_OK) {
+ /* this userdb lookup succeeded, preserve its extra
+ fields */
+ if (userdb_template_export(userdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(request->event,
+ "%sFailed to expand override_fields: %s",
+ auth_request_get_log_prefix_db(request), error);
+ request->private_callback.userdb(
+ USERDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+ auth_fields_snapshot(request->fields.userdb_reply);
+ } else {
+ /* this userdb lookup failed, remove any extra fields
+ it set */
+ auth_fields_rollback(request->fields.userdb_reply);
+ }
+ request->user_changed_by_lookup = FALSE;
+
+ request->userdb = next_userdb;
+ auth_request_lookup_user(request,
+ request->private_callback.userdb);
+ return;
+ }
+
+ if (request->userdb_success) {
+ if (userdb_template_export(userdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(request->event,
+ "%sFailed to expand override_fields: %s",
+ auth_request_get_log_prefix_db(request), error);
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else {
+ result = USERDB_RESULT_OK;
+ }
+ } else if (request->userdbs_seen_internal_failure ||
+ result == USERDB_RESULT_INTERNAL_FAILURE) {
+ /* one of the userdb lookups failed. the user might have been
+ in there, so this is an internal failure */
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else if (request->client_pid != 0) {
+ /* this was an actual login attempt, the user should
+ have been found. */
+ if (auth_request_get_auth(request)->userdbs->next == NULL) {
+ e_error(request->event,
+ "%suser not found from userdb",
+ auth_request_get_log_prefix_db(request));
+ } else {
+ e_error(request->mech_event,
+ "user not found from any userdbs");
+ }
+ result = USERDB_RESULT_USER_UNKNOWN;
+ } else {
+ result = USERDB_RESULT_USER_UNKNOWN;
+ }
+
+ if (request->userdb_lookup_tempfailed) {
+ /* no caching */
+ } else if (result != USERDB_RESULT_INTERNAL_FAILURE) {
+ if (request->userdb_cache_result != AUTH_REQUEST_CACHE_HIT)
+ auth_request_userdb_save_cache(request, result);
+ } else if (passdb_cache != NULL && userdb->cache_key != NULL) {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = userdb->cache_key;
+
+ if (auth_request_lookup_user_cache(request, cache_key,
+ &result, TRUE)) {
+ e_info(request->event,
+ "%sFalling back to expired data from cache",
+ auth_request_get_log_prefix_db(request));
+ }
+ }
+
+ request->private_callback.userdb(result, request);
+}
+
+void auth_request_lookup_user(struct auth_request *request,
+ userdb_callback_t *callback)
+{
+ struct auth_userdb *userdb = request->userdb;
+ const char *cache_key, *error;
+
+ request->private_callback.userdb = callback;
+ request->user_changed_by_lookup = FALSE;
+ request->userdb_lookup = TRUE;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, TRUE);
+ else {
+ /* we still want to set default_fields. these override any
+ existing fields set by previous userdbs (because if that is
+ unwanted, ":protected" can be used). */
+ if (userdb_template_export(userdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_userdb_callback(
+ USERDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+ }
+
+ auth_request_userdb_lookup_begin(request);
+
+ /* (for now) auth_cache is shared between passdb and userdb */
+ cache_key = passdb_cache == NULL ? NULL : userdb->cache_key;
+ if (cache_key != NULL) {
+ enum userdb_result result;
+
+ if (auth_request_lookup_user_cache(request, cache_key,
+ &result, FALSE)) {
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ auth_request_userdb_callback(result, request);
+ return;
+ } else {
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ }
+ }
+
+ if (userdb->userdb->iface->lookup == NULL) {
+ /* we are deinitializing */
+ auth_request_userdb_callback(USERDB_RESULT_INTERNAL_FAILURE,
+ request);
+ } else if (userdb->userdb->blocking)
+ userdb_blocking_lookup(request);
+ else
+ userdb->userdb->iface->lookup(request, auth_request_userdb_callback);
+}
+
+static void
+auth_request_validate_networks(struct auth_request *request,
+ const char *name, const char *networks,
+ const struct ip_addr *remote_ip)
+{
+ const char *const *net;
+ struct ip_addr net_ip;
+ unsigned int bits;
+ bool found = FALSE;
+
+ for (net = t_strsplit_spaces(networks, ", "); *net != NULL; net++) {
+ e_debug(authdb_event(request),
+ "%s: Matching for network %s", name, *net);
+
+ if (strcmp(*net, "local") == 0) {
+ if (remote_ip->family == 0) {
+ found = TRUE;
+ break;
+ }
+ } else if (net_parse_range(*net, &net_ip, &bits) < 0) {
+ e_info(authdb_event(request),
+ "%s: Invalid network '%s'", name, *net);
+ } else if (remote_ip->family != 0 &&
+ net_is_in_network(remote_ip, &net_ip, bits)) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (found)
+ ;
+ else if (remote_ip->family == 0) {
+ e_info(authdb_event(request),
+ "%s check failed: Remote IP not known and 'local' missing", name);
+ } else {
+ e_info(authdb_event(request),
+ "%s check failed: IP %s not in allowed networks",
+ name, net_ip2addr(remote_ip));
+ }
+ if (!found)
+ request->failed = TRUE;
+}
+
+static void
+auth_request_set_password(struct auth_request *request, const char *value,
+ const char *default_scheme, bool noscheme)
+{
+ if (request->passdb_password != NULL) {
+ e_error(authdb_event(request),
+ "Multiple password values not supported");
+ return;
+ }
+
+ /* if the password starts with '{' it most likely contains
+ also '}'. check it anyway to make sure, because we
+ assert-crash later if it doesn't exist. this could happen
+ if plaintext passwords are used. */
+ if (*value == '{' && !noscheme && strchr(value, '}') != NULL)
+ request->passdb_password = p_strdup(request->pool, value);
+ else {
+ i_assert(default_scheme != NULL);
+ request->passdb_password =
+ p_strdup_printf(request->pool, "{%s}%s",
+ default_scheme, value);
+ }
+}
+
+static const char *
+get_updated_username(const char *old_username,
+ const char *name, const char *value)
+{
+ const char *p;
+
+ if (strcmp(name, "user") == 0) {
+ /* replace the whole username */
+ return value;
+ }
+
+ p = strchr(old_username, '@');
+ if (strcmp(name, "username") == 0) {
+ if (strchr(value, '@') != NULL)
+ return value;
+
+ /* preserve the current @domain */
+ return t_strconcat(value, p, NULL);
+ }
+
+ if (strcmp(name, "domain") == 0) {
+ if (p == NULL) {
+ /* add the domain */
+ return t_strconcat(old_username, "@", value, NULL);
+ } else {
+ /* replace the existing domain */
+ p = t_strdup_until(old_username, p + 1);
+ return t_strconcat(p, value, NULL);
+ }
+ }
+ return NULL;
+}
+
+static bool
+auth_request_try_update_username(struct auth_request *request,
+ const char *name, const char *value)
+{
+ const char *new_value;
+
+ new_value = get_updated_username(request->fields.user, name, value);
+ if (new_value == NULL)
+ return FALSE;
+ if (new_value[0] == '\0') {
+ e_error(authdb_event(request),
+ "username attempted to be changed to empty");
+ request->failed = TRUE;
+ return TRUE;
+ }
+
+ if (strcmp(request->fields.user, new_value) != 0) {
+ e_debug(authdb_event(request),
+ "username changed %s -> %s",
+ request->fields.user, new_value);
+ auth_request_set_username_forced(request, new_value);
+ request->user_changed_by_lookup = TRUE;
+ }
+ return TRUE;
+}
+
+static void
+auth_request_passdb_import(struct auth_request *request, const char *args,
+ const char *key_prefix, const char *default_scheme)
+{
+ const char *const *arg, *field;
+
+ for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) {
+ field = t_strconcat(key_prefix, *arg, NULL);
+ auth_request_set_field_keyvalue(request, field, default_scheme);
+ }
+}
+
+void auth_request_set_field(struct auth_request *request,
+ const char *name, const char *value,
+ const char *default_scheme)
+{
+ size_t name_len = strlen(name);
+
+ i_assert(*name != '\0');
+ i_assert(value != NULL);
+
+ i_assert(request->passdb != NULL);
+
+ if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) {
+ /* set this field only if it hasn't been set before */
+ name = t_strndup(name, name_len-10);
+ if (auth_fields_exists(request->fields.extra_fields, name))
+ return;
+ } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) {
+ /* remove this field entirely */
+ name = t_strndup(name, name_len-7);
+ auth_fields_remove(request->fields.extra_fields, name);
+ return;
+ }
+
+ if (strcmp(name, "password") == 0) {
+ auth_request_set_password(request, value,
+ default_scheme, FALSE);
+ return;
+ }
+ if (strcmp(name, "password_noscheme") == 0) {
+ auth_request_set_password(request, value, default_scheme, TRUE);
+ return;
+ }
+
+ if (auth_request_try_update_username(request, name, value)) {
+ /* don't change the original value so it gets saved correctly
+ to cache. */
+ } else if (strcmp(name, "login_user") == 0) {
+ auth_request_set_login_username_forced(request, value);
+ } else if (strcmp(name, "allow_nets") == 0) {
+ auth_request_validate_networks(request, name, value,
+ &request->fields.remote_ip);
+ } else if (strcmp(name, "fail") == 0) {
+ request->failed = TRUE;
+ } else if (strcmp(name, "delay_until") == 0) {
+ time_t timestamp;
+ unsigned int extra_secs = 0;
+ const char *p;
+
+ p = strchr(value, '+');
+ if (p != NULL) {
+ value = t_strdup_until(value, p++);
+ if (str_to_uint(p, &extra_secs) < 0) {
+ e_error(authdb_event(request),
+ "Invalid delay_until randomness number '%s'", p);
+ request->failed = TRUE;
+ } else {
+ extra_secs = i_rand_limit(extra_secs);
+ }
+ }
+ if (str_to_time(value, &timestamp) < 0) {
+ e_error(authdb_event(request),
+ "Invalid delay_until timestamp '%s'", value);
+ request->failed = TRUE;
+ } else if (timestamp <= ioloop_time) {
+ /* no more delays */
+ } else if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS) {
+ e_error(authdb_event(request),
+ "delay_until timestamp %s is too much in the future, failing", value);
+ request->failed = TRUE;
+ } else {
+ /* add randomness, but not too much of it */
+ timestamp += extra_secs;
+ if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS)
+ timestamp = ioloop_time + AUTH_REQUEST_MAX_DELAY_SECS;
+ request->delay_until = timestamp;
+ }
+ } else if (strcmp(name, "allow_real_nets") == 0) {
+ auth_request_validate_networks(request, name, value,
+ &request->fields.real_remote_ip);
+ } else if (str_begins(name, "userdb_")) {
+ /* for prefetch userdb */
+ request->userdb_prefetch_set = TRUE;
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, TRUE);
+ if (strcmp(name, "userdb_userdb_import") == 0) {
+ /* we can't put the whole userdb_userdb_import
+ value to extra_cache_fields or it doesn't work
+ properly. so handle this explicitly. */
+ auth_request_passdb_import(request, value,
+ "userdb_", default_scheme);
+ return;
+ }
+ auth_request_set_userdb_field(request, name + 7, value);
+ } else if (strcmp(name, "noauthenticate") == 0) {
+ /* add "nopassword" also so that passdbs won't try to verify
+ the password. */
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ auth_fields_add(request->fields.extra_fields, "nopassword", NULL, 0);
+ } else if (strcmp(name, "nopassword") == 0) {
+ /* NULL password - anything goes */
+ const char *password = request->passdb_password;
+
+ if (password != NULL &&
+ !auth_fields_exists(request->fields.extra_fields, "noauthenticate")) {
+ (void)password_get_scheme(&password);
+ if (*password != '\0') {
+ e_error(authdb_event(request),
+ "nopassword set but password is "
+ "non-empty");
+ return;
+ }
+ }
+ request->passdb_password = NULL;
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ return;
+ } else if (strcmp(name, "passdb_import") == 0) {
+ auth_request_passdb_import(request, value, "", default_scheme);
+ return;
+ } else {
+ /* these fields are returned to client */
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ return;
+ }
+
+ /* add the field unconditionally to extra_fields. this is required if
+ a) auth cache is used, b) if we're a worker and we'll need to send
+ this to the main auth process that can store it in the cache,
+ c) for easily checking :protected fields' existence. */
+ auth_fields_add(request->fields.extra_fields, name, value,
+ AUTH_FIELD_FLAG_HIDDEN);
+}
+
+void auth_request_set_null_field(struct auth_request *request, const char *name)
+{
+ if (str_begins(name, "userdb_")) {
+ /* make sure userdb prefetch is used even if all the fields
+ were returned as NULL. */
+ request->userdb_prefetch_set = TRUE;
+ }
+}
+
+void auth_request_set_field_keyvalue(struct auth_request *request,
+ const char *field,
+ const char *default_scheme)
+{
+ const char *key, *value;
+
+ value = strchr(field, '=');
+ if (value == NULL) {
+ key = field;
+ value = "";
+ } else {
+ key = t_strdup_until(field, value);
+ value++;
+ }
+ auth_request_set_field(request, key, value, default_scheme);
+}
+
+void auth_request_set_fields(struct auth_request *request,
+ const char *const *fields,
+ const char *default_scheme)
+{
+ for (; *fields != NULL; fields++) {
+ if (**fields == '\0')
+ continue;
+ auth_request_set_field_keyvalue(request, *fields, default_scheme);
+ }
+}
+
+static void auth_request_set_uidgid_file(struct auth_request *request,
+ const char *path_template)
+{
+ string_t *path;
+ struct stat st;
+ const char *error;
+
+ path = t_str_new(256);
+ if (auth_request_var_expand(path, path_template, request,
+ NULL, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand uidgid_file=%s: %s", path_template, error);
+ request->userdb_lookup_tempfailed = TRUE;
+ } else if (stat(str_c(path), &st) < 0) {
+ e_error(authdb_event(request),
+ "stat(%s) failed: %m", str_c(path));
+ request->userdb_lookup_tempfailed = TRUE;
+ } else {
+ auth_fields_add(request->fields.userdb_reply,
+ "uid", dec2str(st.st_uid), 0);
+ auth_fields_add(request->fields.userdb_reply,
+ "gid", dec2str(st.st_gid), 0);
+ }
+}
+
+static void
+auth_request_userdb_import(struct auth_request *request, const char *args)
+{
+ const char *key, *value, *const *arg;
+
+ for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) {
+ value = strchr(*arg, '=');
+ if (value == NULL) {
+ key = *arg;
+ value = "";
+ } else {
+ key = t_strdup_until(*arg, value);
+ value++;
+ }
+ auth_request_set_userdb_field(request, key, value);
+ }
+}
+
+void auth_request_set_userdb_field(struct auth_request *request,
+ const char *name, const char *value)
+{
+ size_t name_len = strlen(name);
+ uid_t uid;
+ gid_t gid;
+
+ i_assert(value != NULL);
+
+ if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) {
+ /* set this field only if it hasn't been set before */
+ name = t_strndup(name, name_len-10);
+ if (auth_fields_exists(request->fields.userdb_reply, name))
+ return;
+ } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) {
+ /* remove this field entirely */
+ name = t_strndup(name, name_len-7);
+ auth_fields_remove(request->fields.userdb_reply, name);
+ return;
+ }
+
+ if (strcmp(name, "uid") == 0) {
+ uid = userdb_parse_uid(request, value);
+ if (uid == (uid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+ value = dec2str(uid);
+ } else if (strcmp(name, "gid") == 0) {
+ gid = userdb_parse_gid(request, value);
+ if (gid == (gid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+ value = dec2str(gid);
+ } else if (strcmp(name, "tempfail") == 0) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ } else if (auth_request_try_update_username(request, name, value)) {
+ return;
+ } else if (strcmp(name, "uidgid_file") == 0) {
+ auth_request_set_uidgid_file(request, value);
+ return;
+ } else if (strcmp(name, "userdb_import") == 0) {
+ auth_request_userdb_import(request, value);
+ return;
+ } else if (strcmp(name, "system_user") == 0) {
+ /* FIXME: the system_user is for backwards compatibility */
+ static bool warned = FALSE;
+ if (!warned) {
+ e_warning(authdb_event(request),
+ "Replace system_user with system_groups_user");
+ warned = TRUE;
+ }
+ name = "system_groups_user";
+ } else if (strcmp(name, AUTH_REQUEST_USER_KEY_IGNORE) == 0) {
+ return;
+ }
+
+ auth_fields_add(request->fields.userdb_reply, name, value, 0);
+}
+
+void auth_request_set_userdb_field_values(struct auth_request *request,
+ const char *name,
+ const char *const *values)
+{
+ if (*values == NULL)
+ return;
+
+ if (strcmp(name, "gid") == 0) {
+ /* convert gids to comma separated list */
+ string_t *value;
+ gid_t gid;
+
+ value = t_str_new(128);
+ for (; *values != NULL; values++) {
+ gid = userdb_parse_gid(request, *values);
+ if (gid == (gid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+
+ if (str_len(value) > 0)
+ str_append_c(value, ',');
+ str_append(value, dec2str(gid));
+ }
+ auth_fields_add(request->fields.userdb_reply, name, str_c(value), 0);
+ } else {
+ /* add only one */
+ if (values[1] != NULL) {
+ e_warning(authdb_event(request),
+ "Multiple values found for '%s', "
+ "using value '%s'", name, *values);
+ }
+ auth_request_set_userdb_field(request, name, *values);
+ }
+}
+
+static bool auth_request_proxy_is_self(struct auth_request *request)
+{
+ const char *port = NULL;
+
+ /* check if the port is the same */
+ port = auth_fields_find(request->fields.extra_fields, "port");
+ if (port != NULL && !str_uint_equals(port, request->fields.local_port))
+ return FALSE;
+ /* don't check destuser. in some systems destuser is intentionally
+ changed to proxied connections, but that shouldn't affect the
+ proxying decision.
+
+ it's unlikely any systems would actually want to proxy a connection
+ to itself only to change the username, since it can already be done
+ without proxying by changing the "user" field. */
+ return TRUE;
+}
+
+static bool
+auth_request_proxy_ip_is_self(struct auth_request *request,
+ const struct ip_addr *ip)
+{
+ unsigned int i;
+
+ if (net_ip_compare(ip, &request->fields.real_local_ip))
+ return TRUE;
+
+ for (i = 0; request->set->proxy_self_ips[i].family != 0; i++) {
+ if (net_ip_compare(ip, &request->set->proxy_self_ips[i]))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+auth_request_proxy_finish_ip(struct auth_request *request,
+ bool proxy_host_is_self)
+{
+ const struct auth_request_fields *fields = &request->fields;
+
+ if (!auth_fields_exists(fields->extra_fields, "proxy_maybe")) {
+ /* proxying */
+ } else if (!proxy_host_is_self ||
+ !auth_request_proxy_is_self(request)) {
+ /* proxy destination isn't ourself - proxy */
+ auth_fields_remove(fields->extra_fields, "proxy_maybe");
+ auth_fields_add(fields->extra_fields, "proxy", NULL, 0);
+ } else {
+ /* proxying to ourself - log in without proxying by dropping
+ all the proxying fields. */
+ bool proxy_always = auth_fields_exists(fields->extra_fields,
+ "proxy_always");
+
+ auth_request_proxy_finish_failure(request);
+ if (proxy_always) {
+ /* setup where "self" refers to the local director
+ cluster, while "non-self" refers to remote clusters.
+
+ we've matched self here, so add proxy field and
+ let director fill the host. */
+ auth_fields_add(request->fields.extra_fields,
+ "proxy", NULL, 0);
+ }
+ }
+}
+
+static void
+auth_request_proxy_dns_callback(const struct dns_lookup_result *result,
+ struct auth_request_proxy_dns_lookup_ctx *ctx)
+{
+ struct auth_request *request = ctx->request;
+ const char *host;
+ unsigned int i;
+ bool proxy_host_is_self;
+
+ request->dns_lookup_ctx = NULL;
+ ctx->dns_lookup = NULL;
+
+ host = auth_fields_find(request->fields.extra_fields, "host");
+ i_assert(host != NULL);
+
+ if (result->ret != 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "DNS lookup for %s failed: %s", host, result->error);
+ request->internal_failure = TRUE;
+ auth_request_proxy_finish_failure(request);
+ } else {
+ if (result->msecs > AUTH_DNS_WARN_MSECS) {
+ auth_request_log_warning(request, AUTH_SUBSYS_PROXY,
+ "DNS lookup for %s took %u.%03u s",
+ host, result->msecs/1000, result->msecs % 1000);
+ }
+ auth_fields_add(request->fields.extra_fields, "hostip",
+ net_ip2addr(&result->ips[0]), 0);
+ proxy_host_is_self = FALSE;
+ for (i = 0; i < result->ips_count; i++) {
+ if (auth_request_proxy_ip_is_self(request,
+ &result->ips[i])) {
+ proxy_host_is_self = TRUE;
+ break;
+ }
+ }
+ auth_request_proxy_finish_ip(request, proxy_host_is_self);
+ }
+ if (ctx->callback != NULL)
+ ctx->callback(result->ret == 0, request);
+ auth_request_unref(&request);
+}
+
+static int auth_request_proxy_host_lookup(struct auth_request *request,
+ const char *host,
+ auth_request_proxy_cb_t *callback)
+{
+ struct auth_request_proxy_dns_lookup_ctx *ctx;
+ struct dns_lookup_settings dns_set;
+ const char *value;
+ unsigned int secs;
+
+ /* need to do dns lookup for the host */
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path = AUTH_DNS_SOCKET_PATH;
+ dns_set.timeout_msecs = AUTH_DNS_DEFAULT_TIMEOUT_MSECS;
+ dns_set.event_parent = request->event;
+ value = auth_fields_find(request->fields.extra_fields, "proxy_timeout");
+ if (value != NULL) {
+ if (str_to_uint(value, &secs) < 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "Invalid proxy_timeout value: %s", value);
+ } else {
+ dns_set.timeout_msecs = secs*1000;
+ }
+ }
+
+ ctx = p_new(request->pool, struct auth_request_proxy_dns_lookup_ctx, 1);
+ ctx->request = request;
+ auth_request_ref(request);
+ request->dns_lookup_ctx = ctx;
+
+ if (dns_lookup(host, &dns_set, auth_request_proxy_dns_callback, ctx,
+ &ctx->dns_lookup) < 0) {
+ /* failed early */
+ return -1;
+ }
+ ctx->callback = callback;
+ return 0;
+}
+
+int auth_request_proxy_finish(struct auth_request *request,
+ auth_request_proxy_cb_t *callback)
+{
+ const char *host, *hostip;
+ struct ip_addr ip;
+ bool proxy_host_is_self;
+
+ if (request->auth_only)
+ return 1;
+ if (!auth_fields_exists(request->fields.extra_fields, "proxy") &&
+ !auth_fields_exists(request->fields.extra_fields, "proxy_maybe"))
+ return 1;
+
+ host = auth_fields_find(request->fields.extra_fields, "host");
+ if (host == NULL) {
+ /* director can set the host. give it access to lip and lport
+ so it can also perform proxy_maybe internally */
+ proxy_host_is_self = FALSE;
+ if (request->fields.local_ip.family != 0) {
+ auth_fields_add(request->fields.extra_fields, "lip",
+ net_ip2addr(&request->fields.local_ip), 0);
+ }
+ if (request->fields.local_port != 0) {
+ auth_fields_add(request->fields.extra_fields, "lport",
+ dec2str(request->fields.local_port), 0);
+ }
+ } else if (net_addr2ip(host, &ip) == 0) {
+ proxy_host_is_self =
+ auth_request_proxy_ip_is_self(request, &ip);
+ } else {
+ hostip = auth_fields_find(request->fields.extra_fields, "hostip");
+ if (hostip != NULL && net_addr2ip(hostip, &ip) < 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "Invalid hostip in passdb: %s", hostip);
+ return -1;
+ }
+ if (hostip == NULL) {
+ /* asynchronous host lookup */
+ return auth_request_proxy_host_lookup(request, host, callback);
+ }
+ proxy_host_is_self =
+ auth_request_proxy_ip_is_self(request, &ip);
+ }
+
+ auth_request_proxy_finish_ip(request, proxy_host_is_self);
+ return 1;
+}
+
+void auth_request_proxy_finish_failure(struct auth_request *request)
+{
+ /* drop all proxying fields */
+ auth_fields_remove(request->fields.extra_fields, "proxy");
+ auth_fields_remove(request->fields.extra_fields, "proxy_maybe");
+ auth_fields_remove(request->fields.extra_fields, "proxy_always");
+ auth_fields_remove(request->fields.extra_fields, "host");
+ auth_fields_remove(request->fields.extra_fields, "port");
+ auth_fields_remove(request->fields.extra_fields, "destuser");
+}
+
+static void log_password_failure(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme,
+ const struct password_generate_params *params,
+ const char *subsystem)
+{
+ struct event *event = get_request_event(request, subsystem);
+ static bool scheme_ok = FALSE;
+ string_t *str = t_str_new(256);
+ const char *working_scheme;
+
+ str_printfa(str, "%s(%s) != '%s'", scheme,
+ plain_password, crypted_password);
+
+ if (!scheme_ok) {
+ /* perhaps the scheme is wrong - see if we can find
+ a working one */
+ working_scheme = password_scheme_detect(plain_password,
+ crypted_password, params);
+ if (working_scheme != NULL) {
+ str_printfa(str, ", try %s scheme instead",
+ working_scheme);
+ }
+ }
+
+ e_debug(event, "%s", str_c(str));
+}
+
+static void
+auth_request_append_password(struct auth_request *request, string_t *str)
+{
+ const char *p, *log_type = request->set->verbose_passwords;
+ unsigned int max_len = 1024;
+
+ if (request->mech_password == NULL)
+ return;
+
+ p = strchr(log_type, ':');
+ if (p != NULL) {
+ if (str_to_uint(p+1, &max_len) < 0)
+ i_unreached();
+ log_type = t_strdup_until(log_type, p);
+ }
+
+ if (strcmp(log_type, "plain") == 0) {
+ str_printfa(str, "(given password: %s)",
+ t_strndup(request->mech_password, max_len));
+ } else if (strcmp(log_type, "sha1") == 0) {
+ unsigned char sha1[SHA1_RESULTLEN];
+
+ sha1_get_digest(request->mech_password,
+ strlen(request->mech_password), sha1);
+ str_printfa(str, "(SHA1 of given password: %s)",
+ t_strndup(binary_to_hex(sha1, sizeof(sha1)),
+ max_len));
+ } else {
+ i_unreached();
+ }
+}
+
+void auth_request_log_password_mismatch(struct auth_request *request,
+ const char *subsystem)
+{
+ auth_request_log_login_failure(request, subsystem, AUTH_LOG_MSG_PASSWORD_MISMATCH);
+}
+
+void auth_request_log_unknown_user(struct auth_request *request,
+ const char *subsystem)
+{
+ auth_request_log_login_failure(request, subsystem, "unknown user");
+}
+
+void auth_request_log_login_failure(struct auth_request *request,
+ const char *subsystem,
+ const char *message)
+{
+ struct event *event = get_request_event(request, subsystem);
+ string_t *str;
+
+ if (strcmp(request->set->verbose_passwords, "no") == 0) {
+ e_info(event, "%s", message);
+ return;
+ }
+
+ /* make sure this gets logged */
+ enum log_type orig_level = event_get_min_log_level(event);
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+
+ str = t_str_new(128);
+ str_append(str, message);
+ str_append(str, " ");
+
+ auth_request_append_password(request, str);
+
+ if (request->userdb_lookup) {
+ if (request->userdb->next != NULL)
+ str_append(str, " - trying the next userdb");
+ } else {
+ if (request->passdb->next != NULL)
+ str_append(str, " - trying the next passdb");
+ }
+ e_info(event, "%s", str_c(str));
+ event_set_min_log_level(event, orig_level);
+}
+
+enum passdb_result
+auth_request_password_verify(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem)
+{
+ return auth_request_password_verify_log(request, plain_password,
+ crypted_password, scheme, subsystem, TRUE);
+}
+
+enum passdb_result
+auth_request_password_verify_log(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem,
+ bool log_password_mismatch)
+{
+ enum passdb_result result;
+ const unsigned char *raw_password;
+ size_t raw_password_size;
+ const char *error;
+ int ret;
+ struct password_generate_params gen_params = {
+ .user = request->fields.original_username,
+ .rounds = 0
+ };
+
+ if (request->fields.skip_password_check) {
+ /* passdb continue* rule after a successful authentication */
+ return PASSDB_RESULT_OK;
+ }
+
+ if (request->passdb->set->deny) {
+ /* this is a deny database, we don't care about the password */
+ return PASSDB_RESULT_PASSWORD_MISMATCH;
+ }
+
+ if (auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ auth_request_log_debug(request, subsystem,
+ "Allowing any password");
+ return PASSDB_RESULT_OK;
+ }
+
+ ret = password_decode(crypted_password, scheme,
+ &raw_password, &raw_password_size, &error);
+ if (ret <= 0) {
+ if (ret < 0) {
+ auth_request_log_error(request, subsystem,
+ "Password data is not valid for scheme %s: %s",
+ scheme, error);
+ } else {
+ auth_request_log_error(request, subsystem,
+ "Unknown scheme %s", scheme);
+ return PASSDB_RESULT_SCHEME_NOT_AVAILABLE;
+ }
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ /* Use original_username since it may be important for some
+ password schemes (eg. digest-md5). Otherwise the username is used
+ only for logging purposes. */
+ ret = password_verify(plain_password, &gen_params,
+ scheme, raw_password, raw_password_size, &error);
+ if (ret < 0) {
+ const char *password_str = request->set->debug_passwords ?
+ t_strdup_printf(" '%s'", crypted_password) : "";
+ auth_request_log_error(request, subsystem,
+ "Invalid password%s in passdb: %s",
+ password_str, error);
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else if (ret == 0) {
+ if (log_password_mismatch)
+ auth_request_log_password_mismatch(request, subsystem);
+ result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ } else {
+ result = PASSDB_RESULT_OK;
+ }
+ if (ret <= 0 && request->set->debug_passwords) T_BEGIN {
+ log_password_failure(request, plain_password,
+ crypted_password, scheme,
+ &gen_params,
+ subsystem);
+ } T_END;
+ return result;
+}
+
+enum passdb_result auth_request_password_missing(struct auth_request *request)
+{
+ if (request->fields.skip_password_check) {
+ /* This passdb wasn't used for authentication */
+ return PASSDB_RESULT_OK;
+ }
+ e_info(authdb_event(request),
+ "No password returned (and no nopassword)");
+ return PASSDB_RESULT_PASSWORD_MISMATCH;
+}
+
+void auth_request_get_log_prefix(string_t *str, struct auth_request *auth_request,
+ const char *subsystem)
+{
+ const char *name;
+
+ if (subsystem == AUTH_SUBSYS_DB) {
+ if (!auth_request->userdb_lookup) {
+ i_assert(auth_request->passdb != NULL);
+ name = auth_request->passdb->set->name[0] != '\0' ?
+ auth_request->passdb->set->name :
+ auth_request->passdb->passdb->iface.name;
+ } else {
+ i_assert(auth_request->userdb != NULL);
+ name = auth_request->userdb->set->name[0] != '\0' ?
+ auth_request->userdb->set->name :
+ auth_request->userdb->userdb->iface->name;
+ }
+ } else if (subsystem == AUTH_SUBSYS_MECH) {
+ i_assert(auth_request->mech != NULL);
+ name = t_str_lcase(auth_request->mech->mech_name);
+ } else {
+ name = subsystem;
+ }
+ str_append(str, name);
+ str_append_c(str, '(');
+ get_log_identifier(str, auth_request);
+ str_append(str, "): ");
+}
+
+#define MAX_LOG_USERNAME_LEN 64
+static void get_log_identifier(string_t *str, struct auth_request *auth_request)
+{
+ const char *ip;
+
+ if (auth_request->fields.user == NULL)
+ str_append(str, "?");
+ else
+ str_sanitize_append(str, auth_request->fields.user,
+ MAX_LOG_USERNAME_LEN);
+
+ ip = net_ip2addr(&auth_request->fields.remote_ip);
+ if (ip[0] != '\0') {
+ str_append_c(str, ',');
+ str_append(str, ip);
+ }
+ if (auth_request->fields.requested_login_user != NULL)
+ str_append(str, ",master");
+ if (auth_request->fields.session_id != NULL)
+ str_printfa(str, ",<%s>", auth_request->fields.session_id);
+}
+
+void auth_request_log_debug(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_debug(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_info(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_info(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_warning(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_warning(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_error(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_error(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_refresh_last_access(struct auth_request *request)
+{
+ request->last_access = ioloop_time;
+ if (request->to_abort != NULL)
+ timeout_reset(request->to_abort);
+}
diff --git a/src/auth/auth-request.h b/src/auth/auth-request.h
new file mode 100644
index 0000000..22f33a4
--- /dev/null
+++ b/src/auth/auth-request.h
@@ -0,0 +1,400 @@
+#ifndef AUTH_REQUEST_H
+#define AUTH_REQUEST_H
+
+#ifndef AUTH_REQUEST_FIELDS_CONST
+# define AUTH_REQUEST_FIELDS_CONST const
+#endif
+
+#include "array.h"
+#include "net.h"
+#include "var-expand.h"
+#include "mech.h"
+#include "userdb.h"
+#include "passdb.h"
+#include "auth-request-var-expand.h"
+#include "password-scheme.h"
+
+#define AUTH_REQUEST_USER_KEY_IGNORE " "
+
+struct auth_client_connection;
+
+enum auth_request_state {
+ AUTH_REQUEST_STATE_NEW,
+ AUTH_REQUEST_STATE_PASSDB,
+ AUTH_REQUEST_STATE_MECH_CONTINUE,
+ AUTH_REQUEST_STATE_FINISHED,
+ AUTH_REQUEST_STATE_USERDB,
+
+ AUTH_REQUEST_STATE_MAX
+};
+
+enum auth_request_secured {
+ AUTH_REQUEST_SECURED_NONE,
+ AUTH_REQUEST_SECURED,
+ AUTH_REQUEST_SECURED_TLS,
+};
+
+enum auth_request_cache_result {
+ AUTH_REQUEST_CACHE_NONE,
+ AUTH_REQUEST_CACHE_MISS,
+ AUTH_REQUEST_CACHE_HIT,
+};
+
+/* All auth request fields are exported to auth-worker process. */
+struct auth_request_fields {
+ /* user contains the user who is being authenticated.
+ When master user is logging in as someone else, it gets more
+ complicated. Initially user is set to master's username and the
+ requested_login_user is set to destination username. After masterdb
+ has validated user as a valid master user, master_user is set to
+ user and user is set to requested_login_user. */
+ char *user, *requested_login_user, *master_user;
+ /* original_username contains the username exactly as given by the
+ client. this is needed at least with DIGEST-MD5 for password
+ verification. however with master logins the master username has
+ been dropped from it. */
+ const char *original_username;
+ /* the username after doing all internal translations, but before
+ being changed by a db lookup */
+ const char *translated_username;
+ /* realm for the request, may be specified by some auth mechanisms */
+ const char *realm;
+
+ const char *service, *mech_name, *session_id, *local_name, *client_id;
+ 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;
+
+ /* extra_fields are returned in authentication reply. Fields prefixed
+ with "userdb_" are automatically placed to userdb_reply instead. */
+ struct auth_fields *extra_fields;
+ /* the whole userdb result reply */
+ struct auth_fields *userdb_reply;
+
+ /* Credentials from the first successful passdb lookup. These are used
+ as the final credentials, unless overridden by later passdb
+ lookups. Note that the requests in auth-worker processes see these
+ only as 1 byte sized \0 strings. */
+ const unsigned char *delayed_credentials;
+ size_t delayed_credentials_size;
+
+ enum auth_request_secured secured;
+
+ /* Authentication was successfully finished, including policy checks
+ and such. There may still be some final delay or final SASL
+ response. */
+ bool successful:1;
+ /* Password was verified successfully by a passdb. The following
+ passdbs shouldn't attempt to verify the password again. Note that
+ this differs from passdb_success, which may be set to FALSE due to
+ the result_* rules. */
+ bool skip_password_check:1;
+
+ /* flags received from auth client: */
+ bool final_resp_ok:1;
+ bool no_penalty:1;
+ bool valid_client_cert:1;
+ bool cert_username:1;
+};
+
+struct auth_request {
+ int refcount;
+
+ pool_t pool;
+
+ struct event *event;
+ struct event *mech_event;
+ ARRAY(struct event *) authdb_event;
+
+ enum auth_request_state state;
+ char *mech_password; /* set if verify_plain() is called */
+ char *passdb_password; /* set after password lookup if successful */
+ struct auth_request_proxy_dns_lookup_ctx *dns_lookup_ctx;
+ /* The final result of passdb lookup (delayed due to asynchronous
+ proxy DNS lookups) */
+ enum passdb_result passdb_result;
+
+ const struct mech_module *mech;
+ const struct auth_settings *set;
+ struct auth_passdb *passdb;
+ struct auth_userdb *userdb;
+
+ struct stats *stats;
+
+ /* passdb lookups have a handler, userdb lookups don't */
+ struct auth_request_handler *handler;
+ struct auth_master_connection *master;
+
+ /* FIXME: Remove this once mech-oauth2 correctly does the processing */
+ const char *openid_config_url;
+
+ unsigned int connect_uid;
+ unsigned int client_pid;
+ unsigned int id;
+ time_t last_access;
+ time_t delay_until;
+ pid_t session_pid;
+
+ /* These are const for most of the code, so they don't try to modify
+ the fields directly. Only auth-request-fields.c and unit tests have
+ the fields writable. This way it's more difficult to make them
+ out-of-sync with events. */
+ AUTH_REQUEST_FIELDS_CONST struct auth_request_fields fields;
+
+ struct timeout *to_abort, *to_penalty;
+ unsigned int policy_penalty;
+ unsigned int last_penalty;
+ size_t initial_response_len;
+ const unsigned char *initial_response;
+
+ union {
+ verify_plain_callback_t *verify_plain;
+ lookup_credentials_callback_t *lookup_credentials;
+ set_credentials_callback_t *set_credentials;
+ userdb_callback_t *userdb;
+ } private_callback;
+ /* Used by passdb's credentials lookup to determine which scheme is
+ wanted by the caller. For example CRAM-MD5 SASL mechanism wants
+ CRAM-MD5 scheme for passwords.
+
+ When doing a PASS lookup (without authenticating), this is set to ""
+ to imply that caller accepts any kind of credentials. After the
+ credentials lookup is finished, this is set to the scheme that was
+ actually received.
+
+ Otherwise, this is kept as NULL. */
+ const char *wanted_credentials_scheme;
+
+ void *context;
+
+ enum auth_request_cache_result passdb_cache_result;
+ enum auth_request_cache_result userdb_cache_result;
+
+ /* this is a lookup on auth socket (not login socket).
+ skip any proxying stuff if enabled. */
+ bool auth_only:1;
+ /* we're doing a userdb lookup now (we may have done passdb lookup
+ earlier) */
+ bool userdb_lookup:1;
+ /* DIGEST-MD5 kludge */
+ bool domain_is_realm:1;
+
+ bool request_auth_token:1;
+
+ /* success/failure states: */
+ bool failed:1; /* overrides any other success */
+ bool internal_failure:1;
+ bool passdbs_seen_user_unknown:1;
+ bool passdbs_seen_internal_failure:1;
+ bool userdbs_seen_internal_failure:1;
+
+ /* current state: */
+ bool handler_pending_reply:1;
+ bool accept_cont_input:1;
+ bool prefer_plain_credentials:1;
+ bool in_delayed_failure_queue:1;
+ bool removed_from_handler:1;
+ bool snapshot_have_userdb_prefetch_set:1;
+ /* username was changed by this passdb/userdb lookup. Used by
+ auth-workers to determine whether to send back a changed username. */
+ bool user_changed_by_lookup:1;
+ /* each passdb lookup can update the current success-status using the
+ result_* rules. the authentication succeeds only if this is TRUE
+ at the end. mechanisms that don't require passdb, but do a passdb
+ lookup anyway (e.g. GSSAPI) need to set this to TRUE by default. */
+ bool passdb_success:1;
+ /* userdb equivalent of passdb_success */
+ bool userdb_success:1;
+ /* the last userdb lookup failed either due to "tempfail" extra field
+ or because one of the returned uid/gid fields couldn't be translated
+ to a number */
+ bool userdb_lookup_tempfailed:1;
+ /* userdb_* fields have been set by the passdb lookup, userdb prefetch
+ will work. */
+ bool userdb_prefetch_set:1;
+ bool stats_sent:1;
+ bool policy_refusal:1;
+ bool policy_processed:1;
+
+ bool event_finished_sent:1;
+
+ /* ... mechanism specific data ... */
+};
+
+typedef void auth_request_proxy_cb_t(bool success, struct auth_request *);
+
+extern unsigned int auth_request_state_count[AUTH_REQUEST_STATE_MAX];
+
+extern const char auth_default_subsystems[2];
+#define AUTH_SUBSYS_DB &auth_default_subsystems[0]
+#define AUTH_SUBSYS_MECH &auth_default_subsystems[1]
+
+struct auth_request *
+auth_request_new(const struct mech_module *mech, struct event *parent_event);
+struct auth_request *auth_request_new_dummy(struct event *parent_event);
+void auth_request_init(struct auth_request *request);
+struct auth *auth_request_get_auth(struct auth_request *request);
+
+void auth_request_set_state(struct auth_request *request,
+ enum auth_request_state state);
+
+void auth_request_ref(struct auth_request *request);
+void auth_request_unref(struct auth_request **request);
+
+void auth_request_success(struct auth_request *request,
+ const void *data, size_t data_size);
+void auth_request_fail_with_reply(struct auth_request *request,
+ const void *final_data, size_t final_data_size);
+void auth_request_fail(struct auth_request *request);
+void auth_request_internal_failure(struct auth_request *request);
+
+void auth_request_export(struct auth_request *request, string_t *dest);
+bool auth_request_import(struct auth_request *request,
+ const char *key, const char *value);
+bool auth_request_import_info(struct auth_request *request,
+ const char *key, const char *value);
+bool auth_request_import_auth(struct auth_request *request,
+ const char *key, const char *value);
+bool auth_request_import_master(struct auth_request *request,
+ const char *key, const char *value);
+
+void auth_request_initial(struct auth_request *request);
+void auth_request_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size);
+
+void auth_request_verify_plain(struct auth_request *request,
+ const char *password,
+ verify_plain_callback_t *callback);
+void auth_request_lookup_credentials(struct auth_request *request,
+ const char *scheme,
+ lookup_credentials_callback_t *callback);
+void auth_request_lookup_user(struct auth_request *request,
+ userdb_callback_t *callback);
+
+bool auth_request_set_username(struct auth_request *request,
+ const char *username, const char **error_r);
+/* Change the username without any translations or checks. */
+void auth_request_set_username_forced(struct auth_request *request,
+ const char *username);
+bool auth_request_set_login_username(struct auth_request *request,
+ const char *username,
+ const char **error_r);
+/* Change the login username without any translations or checks. */
+void auth_request_set_login_username_forced(struct auth_request *request,
+ const char *username);
+void auth_request_set_realm(struct auth_request *request, const char *realm);
+/* Request was fully successfully authenticated, including policy checks etc. */
+void auth_request_set_auth_successful(struct auth_request *request);
+/* Password was successfully verified by a passdb. */
+void auth_request_set_password_verified(struct auth_request *request);
+/* Save credentials from a successful passdb lookup. */
+void auth_request_set_delayed_credentials(struct auth_request *request,
+ const unsigned char *credentials,
+ size_t size);
+
+void auth_request_set_field(struct auth_request *request,
+ const char *name, const char *value,
+ const char *default_scheme) ATTR_NULL(4);
+void auth_request_set_null_field(struct auth_request *request, const char *name);
+void auth_request_set_field_keyvalue(struct auth_request *request,
+ const char *field,
+ const char *default_scheme) ATTR_NULL(3);
+void auth_request_set_fields(struct auth_request *request,
+ const char *const *fields,
+ const char *default_scheme) ATTR_NULL(3);
+
+void auth_request_init_userdb_reply(struct auth_request *request,
+ bool add_default_fields);
+void auth_request_set_userdb_field(struct auth_request *request,
+ const char *name, const char *value);
+void auth_request_set_userdb_field_values(struct auth_request *request,
+ const char *name,
+ const char *const *values);
+/* returns -1 = failed, 0 = callback is called later, 1 = finished */
+int auth_request_proxy_finish(struct auth_request *request,
+ auth_request_proxy_cb_t *callback);
+void auth_request_proxy_finish_failure(struct auth_request *request);
+
+void auth_request_log_password_mismatch(struct auth_request *request,
+ const char *subsystem);
+enum passdb_result
+auth_request_password_verify(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem)
+ ATTR_WARN_UNUSED_RESULT;
+enum passdb_result
+auth_request_password_verify_log(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem,
+ bool log_password_mismatch)
+ ATTR_WARN_UNUSED_RESULT;
+enum passdb_result auth_request_password_missing(struct auth_request *request);
+
+void auth_request_get_log_prefix(string_t *str, struct auth_request *auth_request,
+ const char *subsystem);
+
+void auth_request_log_debug(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...) ATTR_FORMAT(3, 4);
+void auth_request_log_info(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...) ATTR_FORMAT(3, 4);
+void auth_request_log_warning(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...) ATTR_FORMAT(3, 4);
+void auth_request_log_error(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...) ATTR_FORMAT(3, 4);
+void auth_request_log_unknown_user(struct auth_request *auth_request,
+ const char *subsystem);
+
+void auth_request_log_login_failure(struct auth_request *request,
+ const char *subsystem,
+ const char *message);
+void
+auth_request_verify_plain_callback_finish(enum passdb_result result,
+ struct auth_request *request);
+void auth_request_verify_plain_callback(enum passdb_result result,
+ struct auth_request *request);
+void auth_request_lookup_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request);
+void auth_request_set_credentials(struct auth_request *request,
+ const char *scheme, const char *data,
+ set_credentials_callback_t *callback);
+void auth_request_userdb_callback(enum userdb_result result,
+ struct auth_request *request);
+void auth_request_default_verify_plain_continue(struct auth_request *request,
+ verify_plain_callback_t *callback);
+
+void auth_request_refresh_last_access(struct auth_request *request);
+void auth_str_append(string_t *dest, const char *key, const char *value);
+bool auth_request_username_accepted(const char *const *filter, const char *username);
+struct event_passthrough *
+auth_request_finished_event(struct auth_request *request, struct event *event);
+void auth_request_log_finished(struct auth_request *request);
+void auth_request_master_user_login_finish(struct auth_request *request);
+const char *auth_request_get_log_prefix_db(struct auth_request *auth_request);
+void auth_request_fields_init(struct auth_request *request);
+
+void auth_request_passdb_lookup_begin(struct auth_request *request);
+void auth_request_passdb_lookup_end(struct auth_request *request,
+ enum passdb_result result);
+void auth_request_userdb_lookup_begin(struct auth_request *request);
+void auth_request_userdb_lookup_end(struct auth_request *request,
+ enum userdb_result result);
+
+/* Fetches the current authdb event, this is done because
+ some lookups can recurse into new lookups, requiring new event,
+ which will be returned here. */
+static inline struct event *authdb_event(const struct auth_request *request)
+{
+ if (array_count(&request->authdb_event) == 0)
+ return request->event;
+ struct event **e = array_back_modifiable(&request->authdb_event);
+ return *e;
+}
+
+#endif
diff --git a/src/auth/auth-settings.c b/src/auth/auth-settings.c
new file mode 100644
index 0000000..4268b9a
--- /dev/null
+++ b/src/auth/auth-settings.c
@@ -0,0 +1,553 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash-method.h"
+#include "settings-parser.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "service-settings.h"
+#include "auth-settings.h"
+
+#include <stddef.h>
+
+static bool auth_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool auth_passdb_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool auth_userdb_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings auth_unix_listeners_array[] = {
+ { "login/login", 0666, "", "" },
+ { "token-login/tokenlogin", 0666, "", "" },
+ { "auth-login", 0600, "$default_internal_user", "" },
+ { "auth-client", 0600, "$default_internal_user", "" },
+ { "auth-userdb", 0666, "$default_internal_user", "" },
+ { "auth-master", 0600, "", "" }
+};
+static struct file_listener_settings *auth_unix_listeners[] = {
+ &auth_unix_listeners_array[0],
+ &auth_unix_listeners_array[1],
+ &auth_unix_listeners_array[2],
+ &auth_unix_listeners_array[3],
+ &auth_unix_listeners_array[4],
+ &auth_unix_listeners_array[5]
+};
+static buffer_t auth_unix_listeners_buf = {
+ { { auth_unix_listeners, sizeof(auth_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings auth_service_settings = {
+ .name = "auth",
+ .protocol = "",
+ .type = "",
+ .executable = "auth",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &auth_unix_listeners_buf,
+ sizeof(auth_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+
+/* <settings checks> */
+static struct file_listener_settings auth_worker_unix_listeners_array[] = {
+ { "auth-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *auth_worker_unix_listeners[] = {
+ &auth_worker_unix_listeners_array[0]
+};
+static buffer_t auth_worker_unix_listeners_buf = {
+ { { auth_worker_unix_listeners, sizeof(auth_worker_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings auth_worker_service_settings = {
+ .name = "auth-worker",
+ .protocol = "",
+ .type = "worker",
+ .executable = "auth -w",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &auth_worker_unix_listeners_buf,
+ sizeof(auth_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_passdb_settings)
+
+static const struct setting_define auth_passdb_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, driver),
+ DEF(STR, args),
+ DEF(STR, default_fields),
+ DEF(STR, override_fields),
+ DEF(STR, mechanisms),
+ DEF(STR, username_filter),
+
+ DEF(ENUM, skip),
+ DEF(ENUM, result_success),
+ DEF(ENUM, result_failure),
+ DEF(ENUM, result_internalfail),
+
+ DEF(BOOL, deny),
+ DEF(BOOL, pass),
+ DEF(BOOL, master),
+ DEF(ENUM, auth_verbose),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct auth_passdb_settings auth_passdb_default_settings = {
+ .name = "",
+ .driver = "",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+ .mechanisms = "",
+ .username_filter = "",
+
+ .skip = "never:authenticated:unauthenticated",
+ .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail",
+ .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+ .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+
+ .deny = FALSE,
+ .pass = FALSE,
+ .master = FALSE,
+ .auth_verbose = "default:yes:no"
+};
+
+const struct setting_parser_info auth_passdb_setting_parser_info = {
+ .defines = auth_passdb_setting_defines,
+ .defaults = &auth_passdb_default_settings,
+
+ .type_offset = offsetof(struct auth_passdb_settings, name),
+ .struct_size = sizeof(struct auth_passdb_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &auth_setting_parser_info,
+
+ .check_func = auth_passdb_settings_check
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_userdb_settings)
+
+static const struct setting_define auth_userdb_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, driver),
+ DEF(STR, args),
+ DEF(STR, default_fields),
+ DEF(STR, override_fields),
+
+ DEF(ENUM, skip),
+ DEF(ENUM, result_success),
+ DEF(ENUM, result_failure),
+ DEF(ENUM, result_internalfail),
+
+ DEF(ENUM, auth_verbose),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct auth_userdb_settings auth_userdb_default_settings = {
+ /* NOTE: when adding fields, update also auth.c:userdb_dummy_set */
+ .name = "",
+ .driver = "",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+
+ .skip = "never:found:notfound",
+ .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail",
+ .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+ .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+
+ .auth_verbose = "default:yes:no"
+};
+
+const struct setting_parser_info auth_userdb_setting_parser_info = {
+ .defines = auth_userdb_setting_defines,
+ .defaults = &auth_userdb_default_settings,
+
+ .type_offset = offsetof(struct auth_userdb_settings, name),
+ .struct_size = sizeof(struct auth_userdb_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &auth_setting_parser_info,
+
+ .check_func = auth_userdb_settings_check
+};
+
+/* we're kind of kludging here to avoid "auth_" prefix in the struct fields */
+#undef DEF
+#undef DEF_NOPREFIX
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type("auth_"#name, name, struct auth_settings)
+#define DEF_NOPREFIX(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct auth_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define auth_setting_defines[] = {
+ DEF(STR, mechanisms),
+ DEF(STR, realms),
+ DEF(STR, default_realm),
+ DEF(SIZE, cache_size),
+ DEF(TIME, cache_ttl),
+ DEF(TIME, cache_negative_ttl),
+ DEF(BOOL, cache_verify_password_with_worker),
+ DEF(STR, username_chars),
+ DEF(STR, username_translation),
+ DEF(STR, username_format),
+ DEF(STR, master_user_separator),
+ DEF(STR, anonymous_username),
+ DEF(STR, krb5_keytab),
+ DEF(STR, gssapi_hostname),
+ DEF(STR, winbind_helper_path),
+ DEF(STR, proxy_self),
+ DEF(TIME, failure_delay),
+
+ DEF(STR, policy_server_url),
+ DEF(STR, policy_server_api_header),
+ DEF(UINT, policy_server_timeout_msecs),
+ DEF(STR, policy_hash_mech),
+ DEF(STR, policy_hash_nonce),
+ DEF(STR, policy_request_attributes),
+ DEF(BOOL, policy_reject_on_fail),
+ DEF(BOOL, policy_check_before_auth),
+ DEF(BOOL, policy_check_after_auth),
+ DEF(BOOL, policy_report_after_auth),
+ DEF(BOOL, policy_log_only),
+ DEF(UINT, policy_hash_truncate),
+
+ DEF(BOOL, stats),
+ DEF(BOOL, verbose),
+ DEF(BOOL, debug),
+ DEF(BOOL, debug_passwords),
+ DEF(STR, verbose_passwords),
+ DEF(BOOL, ssl_require_client_cert),
+ DEF(BOOL, ssl_username_from_cert),
+ DEF(BOOL, use_winbind),
+
+ DEF(UINT, worker_max_count),
+
+ DEFLIST(passdbs, "passdb", &auth_passdb_setting_parser_info),
+ DEFLIST(userdbs, "userdb", &auth_userdb_setting_parser_info),
+
+ DEF_NOPREFIX(STR, base_dir),
+ DEF_NOPREFIX(BOOL, verbose_proctitle),
+ DEF_NOPREFIX(UINT, first_valid_uid),
+ DEF_NOPREFIX(UINT, last_valid_uid),
+ DEF_NOPREFIX(UINT, first_valid_gid),
+ DEF_NOPREFIX(UINT, last_valid_gid),
+
+ DEF_NOPREFIX(STR, ssl_client_ca_dir),
+ DEF_NOPREFIX(STR, ssl_client_ca_file),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct auth_settings auth_default_settings = {
+ .mechanisms = "plain",
+ .realms = "",
+ .default_realm = "",
+ .cache_size = 0,
+ .cache_ttl = 60*60,
+ .cache_negative_ttl = 60*60,
+ .cache_verify_password_with_worker = FALSE,
+ .username_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@",
+ .username_translation = "",
+ .username_format = "%Lu",
+ .master_user_separator = "",
+ .anonymous_username = "anonymous",
+ .krb5_keytab = "",
+ .gssapi_hostname = "",
+ .winbind_helper_path = "/usr/bin/ntlm_auth",
+ .proxy_self = "",
+ .failure_delay = 2,
+
+ .policy_server_url = "",
+ .policy_server_api_header = "",
+ .policy_server_timeout_msecs = 2000,
+ .policy_hash_mech = "sha256",
+ .policy_hash_nonce = "",
+ .policy_request_attributes = "login=%{requested_username} pwhash=%{hashed_password} remote=%{rip} device_id=%{client_id} protocol=%s session_id=%{session}",
+ .policy_reject_on_fail = FALSE,
+ .policy_check_before_auth = TRUE,
+ .policy_check_after_auth = TRUE,
+ .policy_report_after_auth = TRUE,
+ .policy_log_only = FALSE,
+ .policy_hash_truncate = 12,
+
+ .stats = FALSE,
+ .verbose = FALSE,
+ .debug = FALSE,
+ .debug_passwords = FALSE,
+ .verbose_passwords = "no",
+ .ssl_require_client_cert = FALSE,
+ .ssl_username_from_cert = FALSE,
+ .ssl_client_ca_dir = "",
+ .ssl_client_ca_file = "",
+
+ .use_winbind = FALSE,
+
+ .worker_max_count = 30,
+
+ .passdbs = ARRAY_INIT,
+ .userdbs = ARRAY_INIT,
+
+ .base_dir = PKG_RUNDIR,
+ .verbose_proctitle = FALSE,
+ .first_valid_uid = 500,
+ .last_valid_uid = 0,
+ .first_valid_gid = 1,
+ .last_valid_gid = 0,
+};
+
+const struct setting_parser_info auth_setting_parser_info = {
+ .module_name = "auth",
+ .defines = auth_setting_defines,
+ .defaults = &auth_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct auth_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = auth_settings_check
+};
+
+/* <settings checks> */
+static bool
+auth_settings_set_self_ips(struct auth_settings *set, pool_t pool,
+ const char **error_r)
+{
+ const char *const *tmp;
+ ARRAY(struct ip_addr) ips_array;
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int ret;
+
+ if (*set->proxy_self == '\0') {
+ set->proxy_self_ips = p_new(pool, struct ip_addr, 1);
+ return TRUE;
+ }
+
+ p_array_init(&ips_array, pool, 4);
+ tmp = t_strsplit_spaces(set->proxy_self, " ");
+ for (; *tmp != NULL; tmp++) {
+ ret = net_gethostbyname(*tmp, &ips, &ips_count);
+ if (ret != 0) {
+ *error_r = t_strdup_printf("auth_proxy_self_ips: "
+ "gethostbyname(%s) failed: %s",
+ *tmp, net_gethosterror(ret));
+ }
+ array_append(&ips_array, ips, ips_count);
+ }
+ array_append_zero(&ips_array);
+ set->proxy_self_ips = array_front(&ips_array);
+ return TRUE;
+}
+
+static bool
+auth_verify_verbose_password(struct auth_settings *set,
+ const char **error_r)
+{
+ const char *p, *value = set->verbose_passwords;
+ unsigned int num;
+
+ p = strchr(value, ':');
+ if (p != NULL) {
+ if (str_to_uint(p+1, &num) < 0 || num == 0) {
+ *error_r = t_strdup_printf("auth_verbose_passwords: "
+ "Invalid truncation number: '%s'", p+1);
+ return FALSE;
+ }
+ value = t_strdup_until(value, p);
+ }
+ if (strcmp(value, "no") == 0)
+ return TRUE;
+ else if (strcmp(value, "plain") == 0)
+ return TRUE;
+ else if (strcmp(value, "sha1") == 0)
+ return TRUE;
+ else if (strcmp(value, "yes") == 0) {
+ /* just use it as alias for "plain" */
+ set->verbose_passwords = "plain";
+ return TRUE;
+ } else {
+ *error_r = "auth_verbose_passwords: Invalid value";
+ return FALSE;
+ }
+}
+
+static bool auth_settings_check(void *_set, pool_t pool,
+ const char **error_r)
+{
+ struct auth_settings *set = _set;
+ const char *p;
+
+ if (set->debug_passwords)
+ set->debug = TRUE;
+ if (set->debug)
+ set->verbose = TRUE;
+
+ if (set->worker_max_count == 0) {
+ *error_r = "auth_worker_max_count must be above zero";
+ return FALSE;
+ }
+
+ if (set->cache_size > 0 && set->cache_size < 1024) {
+ /* probably a configuration error.
+ older versions used megabyte numbers */
+ *error_r = t_strdup_printf("auth_cache_size value is too small "
+ "(%"PRIuUOFF_T" bytes)",
+ set->cache_size);
+ return FALSE;
+ }
+
+ if (!auth_verify_verbose_password(set, error_r))
+ return FALSE;
+
+ if (*set->username_chars == '\0') {
+ /* all chars are allowed */
+ memset(set->username_chars_map, 1,
+ sizeof(set->username_chars_map));
+ } else {
+ for (p = set->username_chars; *p != '\0'; p++)
+ set->username_chars_map[(int)(uint8_t)*p] = 1;
+ }
+
+ if (*set->username_translation != '\0') {
+ p = set->username_translation;
+ for (; *p != '\0' && p[1] != '\0'; p += 2)
+ set->username_translation_map[(int)(uint8_t)*p] = p[1];
+ }
+ set->realms_arr =
+ (const char *const *)p_strsplit_spaces(pool, set->realms, " ");
+
+ if (*set->policy_server_url != '\0') {
+ if (*set->policy_hash_nonce == '\0') {
+
+ *error_r = "auth_policy_hash_nonce must be set when policy server is used";
+ return FALSE;
+ }
+ const struct hash_method *digest = hash_method_lookup(set->policy_hash_mech);
+ if (digest == NULL) {
+ *error_r = "invalid auth_policy_hash_mech given";
+ return FALSE;
+ }
+ if (set->policy_hash_truncate > 0 && set->policy_hash_truncate >= digest->digest_size*8) {
+ *error_r = t_strdup_printf("policy_hash_truncate is not smaller than digest size (%u >= %u)",
+ set->policy_hash_truncate,
+ digest->digest_size*8);
+ return FALSE;
+ }
+ }
+
+ if (!auth_settings_set_self_ips(set, pool, error_r))
+ return FALSE;
+ return TRUE;
+}
+
+static bool
+auth_passdb_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct auth_passdb_settings *set = _set;
+
+ if (set->driver == NULL || *set->driver == '\0') {
+ *error_r = "passdb is missing driver";
+ return FALSE;
+ }
+ if (set->pass && strcmp(set->result_success, "return-ok") != 0) {
+ *error_r = "Obsolete pass=yes setting mixed with non-default result_success";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+auth_userdb_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct auth_userdb_settings *set = _set;
+
+ if (set->driver == NULL || *set->driver == '\0') {
+ *error_r = "userdb is missing driver";
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+
+struct auth_settings *global_auth_settings;
+
+struct auth_settings *
+auth_settings_read(const char *service, pool_t pool,
+ struct master_service_settings_output *output_r)
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &auth_setting_parser_info,
+ NULL
+ };
+ struct master_service_settings_input input;
+ struct setting_parser_context *set_parser;
+ const char *error;
+ void **sets;
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.module = "auth";
+ input.service = service;
+ if (master_service_settings_read(master_service, &input,
+ output_r, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ pool_ref(pool);
+ set_parser = settings_parser_dup(master_service->set_parser, pool);
+ if (!settings_parser_check(set_parser, pool, &error))
+ i_unreached();
+
+ sets = master_service_settings_parser_get_others(master_service,
+ set_parser);
+ settings_parser_deinit(&set_parser);
+ return sets[0];
+}
diff --git a/src/auth/auth-settings.h b/src/auth/auth-settings.h
new file mode 100644
index 0000000..10ac379
--- /dev/null
+++ b/src/auth/auth-settings.h
@@ -0,0 +1,110 @@
+#ifndef AUTH_SETTINGS_H
+#define AUTH_SETTINGS_H
+
+struct master_service;
+struct master_service_settings_output;
+
+struct auth_passdb_settings {
+ const char *name;
+ const char *driver;
+ const char *args;
+ const char *default_fields;
+ const char *override_fields;
+ const char *mechanisms;
+ const char *username_filter;
+
+ const char *skip;
+ const char *result_success;
+ const char *result_failure;
+ const char *result_internalfail;
+ bool deny;
+ bool pass; /* deprecated, use result_success=continue instead */
+ bool master;
+ const char *auth_verbose;
+};
+
+struct auth_userdb_settings {
+ const char *name;
+ const char *driver;
+ const char *args;
+ const char *default_fields;
+ const char *override_fields;
+
+ const char *skip;
+ const char *result_success;
+ const char *result_failure;
+ const char *result_internalfail;
+ const char *auth_verbose;
+};
+
+struct auth_settings {
+ const char *mechanisms;
+ const char *realms;
+ const char *default_realm;
+ uoff_t cache_size;
+ unsigned int cache_ttl;
+ unsigned int cache_negative_ttl;
+ bool cache_verify_password_with_worker;
+ const char *username_chars;
+ const char *username_translation;
+ const char *username_format;
+ const char *master_user_separator;
+ const char *anonymous_username;
+ const char *krb5_keytab;
+ const char *gssapi_hostname;
+ const char *winbind_helper_path;
+ const char *proxy_self;
+ unsigned int failure_delay;
+
+ const char *policy_server_url;
+ const char *policy_server_api_header;
+ unsigned int policy_server_timeout_msecs;
+ const char *policy_hash_mech;
+ const char *policy_hash_nonce;
+ const char *policy_request_attributes;
+ bool policy_reject_on_fail;
+ bool policy_check_before_auth;
+ bool policy_check_after_auth;
+ bool policy_report_after_auth;
+ bool policy_log_only;
+ unsigned int policy_hash_truncate;
+
+ bool stats;
+ bool verbose, debug, debug_passwords;
+ const char *verbose_passwords;
+ bool ssl_require_client_cert;
+ bool ssl_username_from_cert;
+ bool use_winbind;
+
+ unsigned int worker_max_count;
+
+ /* settings that don't have auth_ prefix: */
+ ARRAY(struct auth_passdb_settings *) passdbs;
+ ARRAY(struct auth_userdb_settings *) userdbs;
+
+ const char *base_dir;
+ const char *ssl_client_ca_dir;
+ const char *ssl_client_ca_file;
+
+ bool verbose_proctitle;
+ unsigned int first_valid_uid;
+ unsigned int last_valid_uid;
+ unsigned int first_valid_gid;
+ unsigned int last_valid_gid;
+
+ /* generated: */
+ char username_chars_map[256];
+ char username_translation_map[256];
+ const char *const *realms_arr;
+ const struct ip_addr *proxy_self_ips;
+};
+
+extern const struct setting_parser_info auth_setting_parser_info;
+extern struct auth_settings *global_auth_settings;
+
+struct auth_settings *
+auth_settings_read(const char *service, pool_t pool,
+ struct master_service_settings_output *output_r)
+ ATTR_NULL(1);
+
+#endif
diff --git a/src/auth/auth-stats.c b/src/auth/auth-stats.c
new file mode 100644
index 0000000..71264a4
--- /dev/null
+++ b/src/auth/auth-stats.c
@@ -0,0 +1,116 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "stats.h"
+#include "stats-parser.h"
+#include "auth-stats.h"
+
+static struct stats_parser_field auth_stats_fields[] = {
+#define E(parsename, name, type) { parsename, offsetof(struct auth_stats, name), sizeof(((struct auth_stats *)0)->name), type }
+#define EN(parsename, name) E(parsename, name, STATS_PARSER_TYPE_UINT)
+ EN("auth_successes", auth_success_count),
+ EN("auth_master_successes", auth_master_success_count),
+ EN("auth_failures", auth_failure_count),
+ EN("auth_db_tempfails", auth_db_tempfail_count),
+
+ EN("auth_cache_hits", auth_cache_hit_count),
+ EN("auth_cache_misses", auth_cache_miss_count)
+};
+
+static size_t auth_stats_alloc_size(void)
+{
+ return sizeof(struct auth_stats);
+}
+
+static unsigned int auth_stats_field_count(void)
+{
+ return N_ELEMENTS(auth_stats_fields);
+}
+
+static const char *auth_stats_field_name(unsigned int n)
+{
+ i_assert(n < N_ELEMENTS(auth_stats_fields));
+
+ return auth_stats_fields[n].name;
+}
+
+static void
+auth_stats_field_value(string_t *str, const struct stats *stats,
+ unsigned int n)
+{
+ i_assert(n < N_ELEMENTS(auth_stats_fields));
+
+ stats_parser_value(str, &auth_stats_fields[n], stats);
+}
+
+static bool
+auth_stats_diff(const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r)
+{
+ return stats_parser_diff(auth_stats_fields, N_ELEMENTS(auth_stats_fields),
+ stats1, stats2, diff_stats_r, error_r);
+}
+
+static void auth_stats_add(struct stats *dest, const struct stats *src)
+{
+ stats_parser_add(auth_stats_fields, N_ELEMENTS(auth_stats_fields),
+ dest, src);
+}
+
+static bool
+auth_stats_have_changed(const struct stats *_prev, const struct stats *_cur)
+{
+ return memcmp(_prev, _cur, sizeof(struct auth_stats)) != 0;
+}
+
+static void auth_stats_export(buffer_t *buf, const struct stats *_stats)
+{
+ const struct auth_stats *stats = (const struct auth_stats *)_stats;
+
+ buffer_append(buf, stats, sizeof(*stats));
+}
+
+static bool
+auth_stats_import(const unsigned char *data, size_t size, size_t *pos_r,
+ struct stats *_stats, const char **error_r)
+{
+ struct auth_stats *stats = (struct auth_stats *)_stats;
+
+ if (size < sizeof(*stats)) {
+ *error_r = "auth_stats too small";
+ return FALSE;
+ }
+ memcpy(stats, data, sizeof(*stats));
+ *pos_r = sizeof(*stats);
+ return TRUE;
+}
+
+const struct stats_vfuncs auth_stats_vfuncs = {
+ "auth",
+ auth_stats_alloc_size,
+ auth_stats_field_count,
+ auth_stats_field_name,
+ auth_stats_field_value,
+ auth_stats_diff,
+ auth_stats_add,
+ auth_stats_have_changed,
+ auth_stats_export,
+ auth_stats_import
+};
+
+/* for the stats_auth plugin: */
+void stats_auth_init(void);
+void stats_auth_deinit(void);
+
+static struct stats_item *auth_stats_item;
+
+void stats_auth_init(void)
+{
+ auth_stats_item = stats_register(&auth_stats_vfuncs);
+}
+
+void stats_auth_deinit(void)
+{
+ stats_unregister(&auth_stats_item);
+}
diff --git a/src/auth/auth-stats.h b/src/auth/auth-stats.h
new file mode 100644
index 0000000..a3431bd
--- /dev/null
+++ b/src/auth/auth-stats.h
@@ -0,0 +1,16 @@
+#ifndef AUTH_STATS_H
+#define AUTH_STATS_H
+
+struct auth_stats {
+ uint32_t auth_success_count;
+ uint32_t auth_master_success_count;
+ uint32_t auth_failure_count;
+ uint32_t auth_db_tempfail_count;
+
+ uint32_t auth_cache_hit_count;
+ uint32_t auth_cache_miss_count;
+};
+
+extern const struct stats_vfuncs auth_stats_vfuncs;
+
+#endif
diff --git a/src/auth/auth-token.c b/src/auth/auth-token.c
new file mode 100644
index 0000000..74a1020
--- /dev/null
+++ b/src/auth/auth-token.c
@@ -0,0 +1,177 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+/* Auth process maintains a random secret. Once a user authenticates the
+ response to the REQUEST command from a master service is augmented with an
+ auth_token value. This token is the SHA1 hash of the secret, the service
+ name and the username of the user that just logged in. Using this token the
+ service (e.g. imap) can login to another service (e.g. imap-urlauth) to
+ gain access to resources that require additional privileges (e.g. another
+ user's e-mail).
+*/
+
+#include "auth-common.h"
+#include "hex-binary.h"
+#include "hmac.h"
+#include "sha1.h"
+#include "randgen.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "safe-memset.h"
+#include "auth-settings.h"
+#include "auth-token.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define AUTH_TOKEN_SECRET_LEN 32
+
+#define AUTH_TOKEN_SECRET_FNAME "auth-token-secret.dat"
+
+static unsigned char auth_token_secret[AUTH_TOKEN_SECRET_LEN];
+
+static int
+auth_token_read_secret(const char *path,
+ unsigned char secret_r[AUTH_TOKEN_SECRET_LEN])
+{
+ struct stat st, lst;
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno != ENOENT)
+ i_error("open(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ /* check secret len and file type */
+ if (st.st_size != AUTH_TOKEN_SECRET_LEN || !S_ISREG(st.st_mode)) {
+ i_error("Corrupted token secret file: %s", path);
+ i_close_fd(&fd);
+ i_unlink(path);
+ return -1;
+ }
+
+ /* verify that we're not dealing with a symbolic link */
+ if (lstat(path, &lst) < 0) {
+ i_error("lstat(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ /* check security parameters for compromise */
+ if ((st.st_mode & 07777) != 0600 ||
+ st.st_uid != geteuid() || st.st_nlink > 1 ||
+ !S_ISREG(lst.st_mode) || st.st_ino != lst.st_ino ||
+ !CMP_DEV_T(st.st_dev, lst.st_dev)) {
+ i_error("Compromised token secret file: %s", path);
+ i_close_fd(&fd);
+ i_unlink(path);
+ return -1;
+ }
+
+ /* FIXME: fail here to generate new secret if stored one is too old */
+
+ ret = read_full(fd, secret_r, AUTH_TOKEN_SECRET_LEN);
+ if (ret < 0)
+ i_error("read(%s) failed: %m", path);
+ else if (ret == 0) {
+ i_error("Token secret file unexpectedly shrank: %s", path);
+ ret = -1;
+ }
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", path);
+
+ e_debug(auth_event, "Read auth token secret from %s", path);
+ return ret;
+}
+
+static int
+auth_token_write_secret(const char *path,
+ const unsigned char secret[AUTH_TOKEN_SECRET_LEN])
+{
+ const char *temp_path;
+ mode_t old_mask;
+ int fd, ret;
+
+ temp_path = t_strconcat(path, ".tmp", NULL);
+
+ old_mask = umask(0);
+ fd = open(temp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ umask(old_mask);
+
+ if (fd == -1) {
+ i_error("open(%s) failed: %m", temp_path);
+ return -1;
+ }
+
+ ret = write_full(fd, secret, AUTH_TOKEN_SECRET_LEN);
+ if (ret < 0)
+ i_error("write(%s) failed: %m", temp_path);
+ if (close(fd) < 0) {
+ i_error("close(%s) failed: %m", temp_path);
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ i_unlink(temp_path);
+ return -1;
+ }
+
+ if (rename(temp_path, path) < 0) {
+ i_error("rename(%s, %s) failed: %m", temp_path, path);
+ i_unlink(temp_path);
+ return -1;
+ }
+
+ e_debug(auth_event, "Wrote new auth token secret to %s", path);
+ return 0;
+}
+
+void auth_token_init(void)
+{
+ const char *secret_path =
+ t_strconcat(global_auth_settings->base_dir, "/",
+ AUTH_TOKEN_SECRET_FNAME, NULL);
+
+ if (auth_token_read_secret(secret_path, auth_token_secret) < 0) {
+ random_fill(auth_token_secret, sizeof(auth_token_secret));
+
+ if (auth_token_write_secret(secret_path, auth_token_secret) < 0) {
+ i_error("Failed to write auth token secret file; "
+ "returned tokens will be invalid once auth restarts");
+ }
+ }
+}
+
+void auth_token_deinit(void)
+{
+ /* not very useful, but we do it anyway */
+ safe_memset(auth_token_secret, 0, sizeof(auth_token_secret));
+}
+
+const char *auth_token_get(const char *service, const char *session_pid,
+ const char *username, const char *session_id)
+{
+ struct hmac_context ctx;
+ unsigned char result[SHA1_RESULTLEN];
+
+ hmac_init(&ctx, (const unsigned char*)username, strlen(username),
+ &hash_method_sha1);
+ hmac_update(&ctx, session_pid, strlen(session_pid));
+ if (session_id != NULL && *session_id != '\0')
+ hmac_update(&ctx, session_id, strlen(session_id));
+ hmac_update(&ctx, service, strlen(service));
+ hmac_update(&ctx, auth_token_secret, sizeof(auth_token_secret));
+ hmac_final(&ctx, result);
+
+ return binary_to_hex(result, sizeof(result));
+}
diff --git a/src/auth/auth-token.h b/src/auth/auth-token.h
new file mode 100644
index 0000000..3e427c0
--- /dev/null
+++ b/src/auth/auth-token.h
@@ -0,0 +1,11 @@
+#ifndef AUTH_TOKEN_H
+#define AUTH_TOKEN_H
+
+void auth_token_init(void);
+void auth_token_deinit(void);
+
+const char *auth_token_get(const char *service, const char *session_pid,
+ const char *username, const char *session_id);
+
+#endif
+
diff --git a/src/auth/auth-worker-client.c b/src/auth/auth-worker-client.c
new file mode 100644
index 0000000..5405144
--- /dev/null
+++ b/src/auth/auth-worker-client.c
@@ -0,0 +1,972 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "base64.h"
+#include "connection.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "strescape.h"
+#include "process-title.h"
+#include "master-service.h"
+#include "auth-request.h"
+#include "auth-worker-client.h"
+
+
+#define AUTH_WORKER_WARN_DISCONNECTED_LONG_CMD_SECS 30
+#define OUTBUF_THROTTLE_SIZE (1024*10)
+
+#define CLIENT_STATE_HANDSHAKE "handshaking"
+#define CLIENT_STATE_ITER "iterating users"
+#define CLIENT_STATE_IDLE "idling"
+#define CLIENT_STATE_STOP "waiting for shutdown"
+
+static unsigned int auth_worker_max_service_count = 0;
+static unsigned int auth_worker_service_count = 0;
+
+struct auth_worker_client {
+ struct connection conn;
+ int refcount;
+
+ struct auth *auth;
+ struct event *event;
+ time_t cmd_start;
+
+ bool error_sent:1;
+ bool destroyed:1;
+};
+
+struct auth_worker_command {
+ struct auth_worker_client *client;
+ struct event *event;
+};
+
+struct auth_worker_list_context {
+ struct auth_worker_command *cmd;
+ struct auth_worker_client *client;
+ struct auth_request *auth_request;
+ struct userdb_iterate_context *iter;
+ bool sending, sent, done;
+};
+
+static struct connection_list *clients = NULL;
+static bool auth_worker_client_error = FALSE;
+
+static int auth_worker_output(struct auth_worker_client *client);
+static void auth_worker_client_destroy(struct connection *conn);
+static void auth_worker_client_unref(struct auth_worker_client **_client);
+
+void auth_worker_set_max_service_count(unsigned int count)
+{
+ auth_worker_max_service_count = count;
+}
+
+static struct auth_worker_client *auth_worker_get_client(void)
+{
+ if (!auth_worker_has_client())
+ return NULL;
+ struct auth_worker_client *client =
+ container_of(clients->connections, struct auth_worker_client, conn);
+ return client;
+}
+
+void auth_worker_refresh_proctitle(const char *state)
+{
+ if (!global_auth_settings->verbose_proctitle || !worker)
+ return;
+
+ if (auth_worker_client_error)
+ state = "error";
+ else if (!auth_worker_has_client())
+ state = "waiting for connection";
+ process_title_set(t_strdup_printf("worker: %s", state));
+}
+
+static void
+auth_worker_client_check_throttle(struct auth_worker_client *client)
+{
+ if (o_stream_get_buffer_used_size(client->conn.output) >=
+ OUTBUF_THROTTLE_SIZE) {
+ /* stop reading new requests until client has read the pending
+ replies. */
+ connection_input_halt(&client->conn);
+ }
+}
+
+static void
+auth_worker_request_finished_full(struct auth_worker_command *cmd,
+ const char *error, bool log_as_error)
+{
+ event_set_name(cmd->event, "auth_worker_request_finished");
+ if (error != NULL) {
+ event_add_str(cmd->event, "error", error);
+ if (log_as_error)
+ e_error(cmd->event, "Finished: %s", error);
+ else
+ e_debug(cmd->event, "Finished: %s", error);
+ } else {
+ e_debug(cmd->event, "Finished");
+ }
+ auth_worker_client_check_throttle(cmd->client);
+ auth_worker_client_unref(&cmd->client);
+ event_unref(&cmd->event);
+ i_free(cmd);
+
+ auth_worker_refresh_proctitle(CLIENT_STATE_IDLE);
+}
+
+static void auth_worker_request_finished(struct auth_worker_command *cmd,
+ const char *error)
+{
+ auth_worker_request_finished_full(cmd, error, FALSE);
+}
+
+static void auth_worker_request_finished_bug(struct auth_worker_command *cmd,
+ const char *error)
+{
+ auth_worker_request_finished_full(cmd, error, TRUE);
+}
+
+bool auth_worker_auth_request_new(struct auth_worker_command *cmd, unsigned int id,
+ const char *const *args, struct auth_request **request_r)
+{
+ struct auth_request *auth_request;
+ const char *key, *value;
+
+ auth_request = auth_request_new_dummy(cmd->event);
+
+ cmd->client->refcount++;
+ auth_request->context = cmd;
+ auth_request->id = id;
+
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL)
+ (void)auth_request_import(auth_request, *args, "");
+ else {
+ key = t_strdup_until(*args, value++);
+ (void)auth_request_import(auth_request, key, value);
+ }
+ }
+ if (auth_request->fields.user == NULL ||
+ auth_request->fields.service == NULL) {
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+
+ /* reset changed-fields, so we'll export only the ones that were
+ changed by this lookup. */
+ auth_fields_snapshot(auth_request->fields.extra_fields);
+ if (auth_request->fields.userdb_reply != NULL)
+ auth_fields_snapshot(auth_request->fields.userdb_reply);
+
+ auth_request_init(auth_request);
+ *request_r = auth_request;
+
+ return TRUE;
+}
+
+static void auth_worker_send_reply(struct auth_worker_client *client,
+ struct auth_request *request,
+ string_t *str)
+{
+ time_t cmd_duration = time(NULL) - client->cmd_start;
+ const char *p;
+
+ if (worker_restart_request)
+ o_stream_nsend_str(client->conn.output, "RESTART\n");
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ if (o_stream_flush(client->conn.output) < 0 && request != NULL &&
+ cmd_duration > AUTH_WORKER_WARN_DISCONNECTED_LONG_CMD_SECS) {
+ p = i_strchr_to_next(str_c(str), '\t');
+ p = p == NULL ? "BUG" : t_strcut(p, '\t');
+
+ e_warning(client->conn.event, "Auth master disconnected us while handling "
+ "request for %s for %ld secs (result=%s)",
+ request->fields.user, (long)cmd_duration, p);
+ }
+}
+
+static void
+reply_append_extra_fields(string_t *str, struct auth_request *request)
+{
+ if (!auth_fields_is_empty(request->fields.extra_fields)) {
+ str_append_c(str, '\t');
+ /* export only the fields changed by this lookup, so the
+ changed-flag gets preserved correctly on the master side as
+ well. */
+ auth_fields_append(request->fields.extra_fields, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ }
+ if (request->fields.userdb_reply != NULL &&
+ auth_fields_is_empty(request->fields.userdb_reply)) {
+ /* all userdb_* fields had NULL values. we'll still
+ need to tell this to the master */
+ str_append(str, "\tuserdb_"AUTH_REQUEST_USER_KEY_IGNORE);
+ }
+}
+
+static void verify_plain_callback(enum passdb_result result,
+ struct auth_request *request)
+{
+ struct auth_worker_command *cmd = request->context;
+ struct auth_worker_client *client = cmd->client;
+ const char *error = NULL;
+ string_t *str;
+
+ if (request->failed && result == PASSDB_RESULT_OK)
+ result = PASSDB_RESULT_PASSWORD_MISMATCH;
+
+ str = t_str_new(128);
+ str_printfa(str, "%u\t", request->id);
+
+ if (result == PASSDB_RESULT_OK)
+ if (auth_fields_exists(request->fields.extra_fields, "noauthenticate"))
+ str_append(str, "NEXT");
+ else
+ str_append(str, "OK");
+ else {
+ str_printfa(str, "FAIL\t%d", result);
+ error = passdb_result_to_string(result);
+ }
+ if (result != PASSDB_RESULT_INTERNAL_FAILURE) {
+ str_append_c(str, '\t');
+ if (request->user_changed_by_lookup)
+ str_append_tabescaped(str, request->fields.user);
+ str_append_c(str, '\t');
+ if (request->passdb_password != NULL)
+ str_append_tabescaped(str, request->passdb_password);
+ reply_append_extra_fields(str, request);
+ }
+ str_append_c(str, '\n');
+ auth_worker_send_reply(client, request, str);
+
+ auth_request_passdb_lookup_end(request, result);
+ auth_worker_request_finished(cmd, error);
+ auth_request_unref(&request);
+}
+
+static bool
+auth_worker_handle_passv(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ /* verify plaintext password */
+ struct auth_request *auth_request;
+ struct auth_passdb *passdb;
+ const char *password;
+ unsigned int passdb_id;
+
+ /* <passdb id> <password> [<args>] */
+ if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) {
+ *error_r = "BUG: Auth worker server sent us invalid PASSV";
+ return FALSE;
+ }
+ password = args[1];
+
+ if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) {
+ *error_r = "BUG: Auth worker server sent us invalid PASSV";
+ return FALSE;
+ }
+ auth_request->mech_password =
+ p_strdup(auth_request->pool, password);
+
+ passdb = auth_request->passdb;
+ while (passdb != NULL && passdb->passdb->id != passdb_id)
+ passdb = passdb->next;
+
+ if (passdb == NULL) {
+ /* could be a masterdb */
+ passdb = auth_request_get_auth(auth_request)->masterdbs;
+ while (passdb != NULL && passdb->passdb->id != passdb_id)
+ passdb = passdb->next;
+
+ if (passdb == NULL) {
+ *error_r = "BUG: PASSV had invalid passdb ID";
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+ }
+
+ auth_request->passdb = passdb;
+ auth_request_passdb_lookup_begin(auth_request);
+ passdb->passdb->iface.
+ verify_plain(auth_request, password, verify_plain_callback);
+ return TRUE;
+}
+
+static bool
+auth_worker_handle_passw(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ struct auth_worker_client *client = cmd->client;
+ struct auth_request *request;
+ string_t *str;
+ const char *password;
+ const char *crypted, *scheme, *error;
+ unsigned int passdb_id;
+ enum passdb_result ret;
+
+ if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL ||
+ args[2] == NULL) {
+ *error_r = "BUG: Auth worker server sent us invalid PASSW";
+ return FALSE;
+ }
+ password = args[1];
+ crypted = args[2];
+ scheme = password_get_scheme(&crypted);
+ if (scheme == NULL) {
+ *error_r = "BUG: Auth worker server sent us invalid PASSW (scheme is NULL)";
+ return FALSE;
+ }
+
+ if (!auth_worker_auth_request_new(cmd, id, args + 3, &request)) {
+ *error_r = "BUG: PASSW had missing parameters";
+ return FALSE;
+ }
+ request->mech_password =
+ p_strdup(request->pool, password);
+
+ ret = auth_request_password_verify(request, password,
+ crypted, scheme, "cache");
+ str = t_str_new(128);
+ str_printfa(str, "%u\t", request->id);
+
+ if (ret == PASSDB_RESULT_OK) {
+ str_printfa(str, "OK\t\t");
+ error = NULL;
+ } else {
+ str_printfa(str, "FAIL\t%d", ret);
+ error = passdb_result_to_string(ret);
+ }
+
+ str_append_c(str, '\n');
+ auth_worker_send_reply(client, request, str);
+
+ auth_worker_request_finished(cmd, error);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+static void
+lookup_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *request)
+{
+ struct auth_worker_command *cmd = request->context;
+ struct auth_worker_client *client = cmd->client;
+ string_t *str;
+
+ if (request->failed && result == PASSDB_RESULT_OK)
+ result = PASSDB_RESULT_PASSWORD_MISMATCH;
+
+ str = t_str_new(128);
+ str_printfa(str, "%u\t", request->id);
+
+ if (result != PASSDB_RESULT_OK && result != PASSDB_RESULT_NEXT)
+ str_printfa(str, "FAIL\t%d", result);
+ else {
+ if (result == PASSDB_RESULT_NEXT)
+ str_append(str, "NEXT\t");
+ else
+ str_append(str, "OK\t");
+ if (request->user_changed_by_lookup)
+ str_append_tabescaped(str, request->fields.user);
+ str_append_c(str, '\t');
+ if (request->wanted_credentials_scheme[0] != '\0') {
+ str_printfa(str, "{%s.b64}", request->wanted_credentials_scheme);
+ base64_encode(credentials, size, str);
+ } else {
+ i_assert(size == 0);
+ }
+ reply_append_extra_fields(str, request);
+ }
+ str_append_c(str, '\n');
+ auth_worker_send_reply(client, request, str);
+
+ auth_request_passdb_lookup_end(request, result);
+ auth_request_unref(&request);
+ auth_worker_request_finished(cmd, NULL);
+}
+
+static bool
+auth_worker_handle_passl(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ /* lookup credentials */
+ struct auth_request *auth_request;
+ const char *scheme;
+ unsigned int passdb_id;
+
+ /* <passdb id> <scheme> [<args>] */
+ if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) {
+ *error_r = "BUG: Auth worker server sent us invalid PASSL";
+ return FALSE;
+ }
+ scheme = args[1];
+
+ if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) {
+ *error_r = "BUG: PASSL had missing parameters";
+ return FALSE;
+ }
+ auth_request->wanted_credentials_scheme =
+ p_strdup(auth_request->pool, scheme);
+
+ while (auth_request->passdb->passdb->id != passdb_id) {
+ auth_request->passdb = auth_request->passdb->next;
+ if (auth_request->passdb == NULL) {
+ *error_r = "BUG: PASSL had invalid passdb ID";
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+ }
+
+ if (auth_request->passdb->passdb->iface.lookup_credentials == NULL) {
+ *error_r = "BUG: PASSL lookup not supported by given passdb";
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+
+ auth_request->prefer_plain_credentials = TRUE;
+ auth_request_passdb_lookup_begin(auth_request);
+ auth_request->passdb->passdb->iface.
+ lookup_credentials(auth_request, lookup_credentials_callback);
+ return TRUE;
+}
+
+static void
+set_credentials_callback(bool success, struct auth_request *request)
+{
+ struct auth_worker_command *cmd = request->context;
+ struct auth_worker_client *client = cmd->client;
+
+ string_t *str;
+
+ str = t_str_new(64);
+ str_printfa(str, "%u\t%s\n", request->id, success ? "OK" : "FAIL");
+ auth_worker_send_reply(client, request, str);
+
+ auth_worker_request_finished(cmd, success ? NULL :
+ "Failed to set credentials");
+ auth_request_unref(&request);
+}
+
+static bool
+auth_worker_handle_setcred(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ struct auth_request *auth_request;
+ unsigned int passdb_id;
+ const char *creds;
+
+ /* <passdb id> <credentials> [<args>] */
+ if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) {
+ *error_r = "BUG: Auth worker server sent us invalid SETCRED";
+ return FALSE;
+ }
+ creds = args[1];
+
+ if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) {
+ *error_r = "BUG: SETCRED had missing parameters";
+ return FALSE;
+ }
+
+ while (auth_request->passdb->passdb->id != passdb_id) {
+ auth_request->passdb = auth_request->passdb->next;
+ if (auth_request->passdb == NULL) {
+ *error_r = "BUG: SETCRED had invalid passdb ID";
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+ }
+
+ auth_request->passdb->passdb->iface.
+ set_credentials(auth_request, creds, set_credentials_callback);
+ return TRUE;
+}
+
+static void
+lookup_user_callback(enum userdb_result result,
+ struct auth_request *auth_request)
+{
+ struct auth_worker_command *cmd = auth_request->context;
+ struct auth_worker_client *client = cmd->client;
+ const char *error;
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "%u\t", auth_request->id);
+ switch (result) {
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ str_append(str, "FAIL\t");
+ break;
+ case USERDB_RESULT_USER_UNKNOWN:
+ str_append(str, "NOTFOUND\t");
+ break;
+ case USERDB_RESULT_OK:
+ str_append(str, "OK\t");
+ if (auth_request->user_changed_by_lookup)
+ str_append_tabescaped(str, auth_request->fields.user);
+ str_append_c(str, '\t');
+ /* export only the fields changed by this lookup */
+ auth_fields_append(auth_request->fields.userdb_reply, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ if (auth_request->userdb_lookup_tempfailed)
+ str_append(str, "\ttempfail");
+ break;
+ }
+ str_append_c(str, '\n');
+
+ auth_worker_send_reply(client, auth_request, str);
+
+ auth_request_userdb_lookup_end(auth_request, result);
+ error = result == USERDB_RESULT_OK ? NULL :
+ userdb_result_to_string(result);
+ auth_worker_request_finished(cmd, error);
+ auth_request_unref(&auth_request);
+}
+
+static struct auth_userdb *
+auth_userdb_find_by_id(struct auth_userdb *userdbs, unsigned int id)
+{
+ struct auth_userdb *db;
+
+ for (db = userdbs; db != NULL; db = db->next) {
+ if (db->userdb->id == id)
+ return db;
+ }
+ return NULL;
+}
+
+static bool
+auth_worker_handle_user(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ /* lookup user */
+ struct auth_request *auth_request;
+ unsigned int userdb_id;
+
+ /* <userdb id> [<args>] */
+ if (str_to_uint(args[0], &userdb_id) < 0) {
+ *error_r = "BUG: Auth worker server sent us invalid USER";
+ return FALSE;
+ }
+
+ if (!auth_worker_auth_request_new(cmd, id, args + 1, &auth_request)) {
+ *error_r = "BUG: USER had missing parameters";
+ return FALSE;
+ }
+
+ auth_request->userdb_lookup = TRUE;
+ auth_request->userdb =
+ auth_userdb_find_by_id(auth_request->userdb, userdb_id);
+ if (auth_request->userdb == NULL) {
+ *error_r = "BUG: USER had invalid userdb ID";
+ auth_request_unref(&auth_request);
+ return FALSE;
+ }
+
+ if (auth_request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(auth_request, TRUE);
+ auth_request_userdb_lookup_begin(auth_request);
+ auth_request->userdb->userdb->iface->
+ lookup(auth_request, lookup_user_callback);
+ return TRUE;
+}
+
+static void
+auth_worker_client_idle_kill(struct connection *conn ATTR_UNUSED)
+{
+ auth_worker_client_send_shutdown();
+}
+
+static void list_iter_deinit(struct auth_worker_list_context *ctx)
+{
+ struct auth_worker_command *cmd = ctx->cmd;
+ struct auth_worker_client *client = ctx->client;
+ const char *error = NULL;
+ string_t *str;
+
+ i_assert(client->conn.io == NULL);
+
+ str = t_str_new(32);
+ if (ctx->auth_request->userdb->userdb->iface->
+ iterate_deinit(ctx->iter) < 0) {
+ error = "Iteration failed";
+ str_printfa(str, "%u\tFAIL\n", ctx->auth_request->id);
+ } else
+ str_printfa(str, "%u\tOK\n", ctx->auth_request->id);
+ auth_worker_send_reply(client, NULL, str);
+
+ connection_input_resume(&client->conn);
+ o_stream_set_flush_callback(client->conn.output, auth_worker_output,
+ client);
+ auth_request_userdb_lookup_end(ctx->auth_request, USERDB_RESULT_OK);
+ auth_worker_request_finished(cmd, error);
+ auth_request_unref(&ctx->auth_request);
+ i_free(ctx);
+}
+
+static void list_iter_callback(const char *user, void *context)
+{
+ struct auth_worker_list_context *ctx = context;
+ string_t *str;
+
+ if (user == NULL) {
+ if (ctx->sending)
+ ctx->done = TRUE;
+ else
+ list_iter_deinit(ctx);
+ return;
+ }
+
+ if (!ctx->sending)
+ o_stream_cork(ctx->client->conn.output);
+ T_BEGIN {
+ str = t_str_new(128);
+ str_printfa(str, "%u\t*\t%s\n", ctx->auth_request->id, user);
+ o_stream_nsend(ctx->client->conn.output, str_data(str), str_len(str));
+ } T_END;
+
+ if (ctx->sending) {
+ /* avoid recursively looping to this same function */
+ ctx->sent = TRUE;
+ return;
+ }
+
+ do {
+ ctx->sending = TRUE;
+ ctx->sent = FALSE;
+ T_BEGIN {
+ ctx->auth_request->userdb->userdb->iface->
+ iterate_next(ctx->iter);
+ } T_END;
+ if (o_stream_get_buffer_used_size(ctx->client->conn.output) > OUTBUF_THROTTLE_SIZE) {
+ if (o_stream_flush(ctx->client->conn.output) < 0) {
+ ctx->done = TRUE;
+ break;
+ }
+ }
+ } while (ctx->sent &&
+ o_stream_get_buffer_used_size(ctx->client->conn.output) <= OUTBUF_THROTTLE_SIZE);
+ o_stream_uncork(ctx->client->conn.output);
+ ctx->sending = FALSE;
+ if (ctx->done)
+ list_iter_deinit(ctx);
+ else
+ o_stream_set_flush_pending(ctx->client->conn.output, TRUE);
+}
+
+static int auth_worker_list_output(struct auth_worker_list_context *ctx)
+{
+ int ret;
+
+ if ((ret = o_stream_flush(ctx->client->conn.output)) < 0) {
+ list_iter_deinit(ctx);
+ return 1;
+ }
+ if (ret > 0) T_BEGIN {
+ ctx->auth_request->userdb->userdb->iface->
+ iterate_next(ctx->iter);
+ } T_END;
+ return 1;
+}
+
+static bool
+auth_worker_handle_list(struct auth_worker_command *cmd,
+ unsigned int id, const char *const *args,
+ const char **error_r)
+{
+ struct auth_worker_client *client = cmd->client;
+ struct auth_worker_list_context *ctx;
+ struct auth_userdb *userdb;
+ unsigned int userdb_id;
+
+ if (str_to_uint(args[0], &userdb_id) < 0) {
+ *error_r = "BUG: Auth worker server sent us invalid LIST";
+ return FALSE;
+ }
+
+ userdb = auth_userdb_find_by_id(client->auth->userdbs, userdb_id);
+ if (userdb == NULL) {
+ *error_r = "BUG: LIST had invalid userdb ID";
+ return FALSE;
+ }
+
+ ctx = i_new(struct auth_worker_list_context, 1);
+ ctx->cmd = cmd;
+ ctx->client = client;
+ if (!auth_worker_auth_request_new(cmd, id, args + 1, &ctx->auth_request)) {
+ *error_r = "BUG: LIST had missing parameters";
+ i_free(ctx);
+ return FALSE;
+ }
+ ctx->auth_request->userdb = userdb;
+
+ connection_input_halt(&ctx->client->conn);
+
+ o_stream_set_flush_callback(ctx->client->conn.output,
+ auth_worker_list_output, ctx);
+ ctx->auth_request->userdb_lookup = TRUE;
+ auth_request_userdb_lookup_begin(ctx->auth_request);
+ ctx->iter = ctx->auth_request->userdb->userdb->iface->
+ iterate_init(ctx->auth_request, list_iter_callback, ctx);
+ ctx->auth_request->userdb->userdb->iface->iterate_next(ctx->iter);
+ return TRUE;
+}
+
+static bool auth_worker_verify_db_hash(const char *passdb_hash, const char *userdb_hash)
+{
+ string_t *str = t_str_new(MD5_RESULTLEN*2);
+ unsigned char passdb_md5[MD5_RESULTLEN];
+ unsigned char userdb_md5[MD5_RESULTLEN];
+
+ passdbs_generate_md5(passdb_md5);
+ userdbs_generate_md5(userdb_md5);
+
+ binary_to_hex_append(str, passdb_md5, sizeof(passdb_md5));
+ if (strcmp(str_c(str), passdb_hash) != 0)
+ return FALSE;
+ str_truncate(str, 0);
+ binary_to_hex_append(str, userdb_md5, sizeof(userdb_md5));
+ return strcmp(str_c(str), userdb_hash) == 0;
+};
+
+static int auth_worker_client_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 (str_array_length(args) < 3 ||
+ strcmp(args[0], "DBHASH") != 0) {
+ e_error(conn->event, "BUG: Invalid input: %s",
+ t_strarray_join(args, "\t"));
+ return -1;
+ }
+
+ if (!auth_worker_verify_db_hash(args[1], args[2])) {
+ e_error(conn->event,
+ "Auth worker sees different passdbs/userdbs "
+ "than auth server. Maybe config just changed "
+ "and this goes away automatically?");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+auth_worker_client_input_args(struct connection *conn, const char *const *args)
+{
+ unsigned int id;
+ bool ret = FALSE;
+ const char *error = NULL;
+ struct auth_worker_command *cmd;
+ struct auth_worker_client *client =
+ container_of(conn, struct auth_worker_client, conn);
+
+ if (str_array_length(args) < 3 ||
+ str_to_uint(args[0], &id) < 0) {
+ e_error(conn->event, "BUG: Invalid input: %s",
+ t_strarray_join(args, "\t"));
+ return -1;
+ }
+
+ io_loop_time_refresh();
+
+ cmd = i_new(struct auth_worker_command, 1);
+ cmd->client = client;
+ cmd->event = event_create(client->conn.event);
+ event_add_category(cmd->event, &event_category_auth);
+ event_add_str(cmd->event, "command", args[1]);
+ event_add_int(cmd->event, "command_id", id);
+ event_set_append_log_prefix(cmd->event, t_strdup_printf("auth-worker<%u>: ", id));
+ client->cmd_start = ioloop_time;
+ client->refcount++;
+ e_debug(cmd->event, "Handling %s request", args[1]);
+
+ /* Check if we have reached service_count */
+ if (auth_worker_max_service_count > 0) {
+ auth_worker_service_count++;
+ if (auth_worker_service_count >= auth_worker_max_service_count)
+ worker_restart_request = TRUE;
+ }
+
+ auth_worker_refresh_proctitle(args[1]);
+ if (strcmp(args[1], "PASSV") == 0)
+ ret = auth_worker_handle_passv(cmd, id, args + 2, &error);
+ else if (strcmp(args[1], "PASSL") == 0)
+ ret = auth_worker_handle_passl(cmd, id, args + 2, &error);
+ else if (strcmp(args[1], "PASSW") == 0)
+ ret = auth_worker_handle_passw(cmd, id, args + 2, &error);
+ else if (strcmp(args[1], "SETCRED") == 0)
+ ret = auth_worker_handle_setcred(cmd, id, args + 2, &error);
+ else if (strcmp(args[1], "USER") == 0)
+ ret = auth_worker_handle_user(cmd, id, args + 2, &error);
+ else if (strcmp(args[1], "LIST") == 0)
+ ret = auth_worker_handle_list(cmd, id, args + 2, &error);
+ else {
+ error = t_strdup_printf("BUG: Auth-worker received unknown command: %s",
+ args[1]);
+ }
+
+ i_assert(ret || error != NULL);
+
+ if (!ret) {
+ auth_worker_request_finished_bug(cmd, error);
+ return -1;
+ }
+ auth_worker_client_unref(&client);
+ return 1;
+}
+
+static int auth_worker_output(struct auth_worker_client *client)
+{
+ if (o_stream_flush(client->conn.output) < 0) {
+ auth_worker_client_destroy(&client->conn);
+ return 1;
+ }
+
+ if (o_stream_get_buffer_used_size(client->conn.output) <=
+ OUTBUF_THROTTLE_SIZE/3 && client->conn.io == NULL) {
+ /* allow input again */
+ connection_input_resume(&client->conn);
+ }
+ return 1;
+}
+
+static void auth_worker_client_unref(struct auth_worker_client **_client)
+{
+ struct auth_worker_client *client = *_client;
+ if (client == NULL)
+ return;
+ if (--client->refcount > 0)
+ return;
+
+ /* the connection should've been destroyed before getting here */
+ i_assert(client->destroyed);
+ connection_deinit(&client->conn);
+ i_free(client);
+}
+
+static void auth_worker_client_destroy(struct connection *conn)
+{
+ struct auth_worker_client *client =
+ container_of(conn, struct auth_worker_client, conn);
+
+ i_assert(!client->destroyed);
+ client->destroyed = TRUE;
+ connection_input_halt(conn);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ net_disconnect(conn->fd_in);
+ conn->fd_out = conn->fd_in = -1;
+ auth_worker_client_unref(&client);
+ master_service_client_connection_destroyed(master_service);
+}
+
+static const struct connection_vfuncs auth_worker_client_v =
+{
+ .input_args = auth_worker_client_input_args,
+ .handshake_args = auth_worker_client_handshake_args,
+ .destroy = auth_worker_client_destroy,
+ .idle_timeout = auth_worker_client_idle_kill,
+};
+
+static const struct connection_settings auth_worker_client_set =
+{
+ .service_name_in = "auth-worker",
+ .service_name_out = "auth-worker",
+ .major_version = AUTH_WORKER_PROTOCOL_MAJOR_VERSION,
+ .minor_version = AUTH_WORKER_PROTOCOL_MINOR_VERSION,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX, /* we use throttling */
+};
+
+struct auth_worker_client *
+auth_worker_client_create(struct auth *auth,
+ const struct master_service_connection *master_conn)
+{
+ struct auth_worker_client *client;
+
+ if (clients == NULL)
+ clients = connection_list_init(&auth_worker_client_set, &auth_worker_client_v);
+
+ client = i_new(struct auth_worker_client, 1);
+ client->refcount = 1;
+ client->auth = auth;
+ client->conn.event_parent = auth_event;
+ client->conn.input_idle_timeout_secs = master_service_get_idle_kill_secs(master_service);
+ connection_init_server(clients, &client->conn, master_conn->name,
+ master_conn->fd, master_conn->fd);
+
+ auth_worker_refresh_proctitle(CLIENT_STATE_HANDSHAKE);
+
+ if (auth_worker_client_error)
+ auth_worker_client_send_error();
+ return client;
+}
+
+void auth_worker_client_send_error(void)
+{
+ struct auth_worker_client *auth_worker_client =
+ auth_worker_get_client();
+ auth_worker_client_error = TRUE;
+ if (auth_worker_client != NULL &&
+ !auth_worker_client->error_sent) {
+ o_stream_nsend_str(auth_worker_client->conn.output, "ERROR\n");
+ auth_worker_client->error_sent = TRUE;
+ }
+ auth_worker_refresh_proctitle("");
+}
+
+void auth_worker_client_send_success(void)
+{
+ struct auth_worker_client *auth_worker_client =
+ auth_worker_get_client();
+ auth_worker_client_error = FALSE;
+ if (auth_worker_client == NULL)
+ return;
+ if (auth_worker_client->error_sent) {
+ o_stream_nsend_str(auth_worker_client->conn.output,
+ "SUCCESS\n");
+ auth_worker_client->error_sent = FALSE;
+ }
+ if (auth_worker_client->conn.io != NULL)
+ auth_worker_refresh_proctitle(CLIENT_STATE_IDLE);
+}
+
+void auth_worker_client_send_shutdown(void)
+{
+ struct auth_worker_client *auth_worker_client =
+ auth_worker_get_client();
+ if (auth_worker_client != NULL)
+ o_stream_nsend_str(auth_worker_client->conn.output,
+ "SHUTDOWN\n");
+ auth_worker_refresh_proctitle(CLIENT_STATE_STOP);
+}
+
+void auth_worker_connections_destroy_all(void)
+{
+ if (clients == NULL)
+ return;
+ while (clients->connections != NULL)
+ connection_deinit(clients->connections);
+ connection_list_deinit(&clients);
+}
+
+bool auth_worker_has_client(void)
+{
+ return clients != NULL && clients->connections_count > 0;
+}
diff --git a/src/auth/auth-worker-client.h b/src/auth/auth-worker-client.h
new file mode 100644
index 0000000..e41ee83
--- /dev/null
+++ b/src/auth/auth-worker-client.h
@@ -0,0 +1,27 @@
+#ifndef AUTH_WORKER_CLIENT_H
+#define AUTH_WORKER_CLIENT_H
+
+#define AUTH_WORKER_PROTOCOL_MAJOR_VERSION 1
+#define AUTH_WORKER_PROTOCOL_MINOR_VERSION 0
+#define AUTH_WORKER_MAX_LINE_LENGTH 8192
+
+struct master_service_connection;
+struct auth_worker_command;
+
+struct auth_worker_client *
+auth_worker_client_create(struct auth *auth,
+ const struct master_service_connection *master_conn);
+bool auth_worker_auth_request_new(struct auth_worker_command *cmd, unsigned int id,
+ const char *const *args, struct auth_request **request_r);
+
+bool auth_worker_has_client(void);
+void auth_worker_client_send_error(void);
+void auth_worker_client_send_success(void);
+void auth_worker_client_send_shutdown(void);
+
+void auth_worker_connections_destroy_all(void);
+
+/* Stop master service after this many requests. 0 is unlimited. */
+void auth_worker_set_max_service_count(unsigned int count);
+
+#endif
diff --git a/src/auth/auth-worker-server.c b/src/auth/auth-worker-server.c
new file mode 100644
index 0000000..41b2b2b
--- /dev/null
+++ b/src/auth/auth-worker-server.c
@@ -0,0 +1,519 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "array.h"
+#include "aqueue.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "eacces-error.h"
+#include "auth-request.h"
+#include "auth-worker-client.h"
+#include "auth-worker-server.h"
+
+#include <unistd.h>
+
+/* Initial lookup timeout */
+#define AUTH_WORKER_LOOKUP_TIMEOUT_SECS 60
+/* Timeout for multi-line replies, e.g. listing users. This should be a much
+ higher value, because e.g. doveadm could be doing some long-running commands
+ for the users. And because of buffering this timeout is for handling
+ multiple users, not just one. */
+#define AUTH_WORKER_RESUME_TIMEOUT_SECS (30*60)
+#define AUTH_WORKER_MAX_IDLE_SECS (60*5)
+#define AUTH_WORKER_ABORT_SECS 60
+#define AUTH_WORKER_DELAY_WARN_SECS 3
+#define AUTH_WORKER_DELAY_WARN_MIN_INTERVAL_SECS 300
+
+struct auth_worker_request {
+ unsigned int id;
+ time_t created;
+ const char *username;
+ const char *data;
+ auth_worker_callback_t *callback;
+ void *context;
+};
+
+struct auth_worker_connection {
+ int fd;
+
+ struct event *event;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to;
+
+ struct auth_worker_request *request;
+ unsigned int id_counter;
+
+ bool received_error:1;
+ bool restart:1;
+ bool shutdown:1;
+ bool timeout_pending_resume:1;
+ bool resuming:1;
+};
+
+static ARRAY(struct auth_worker_connection *) connections = ARRAY_INIT;
+static unsigned int idle_count = 0, auth_workers_with_errors = 0;
+static ARRAY(struct auth_worker_request *) worker_request_array;
+static struct aqueue *worker_request_queue;
+static time_t auth_worker_last_warn;
+static unsigned int auth_workers_throttle_count;
+
+static const char *worker_socket_path;
+
+static void worker_input(struct auth_worker_connection *conn);
+static void auth_worker_destroy(struct auth_worker_connection **conn,
+ const char *reason, bool restart) ATTR_NULL(2);
+
+static void auth_worker_idle_timeout(struct auth_worker_connection *conn)
+{
+ i_assert(conn->request == NULL);
+
+ if (idle_count > 1)
+ auth_worker_destroy(&conn, NULL, FALSE);
+ else
+ timeout_reset(conn->to);
+}
+
+static void auth_worker_call_timeout(struct auth_worker_connection *conn)
+{
+ i_assert(conn->request != NULL);
+
+ auth_worker_destroy(&conn, "Lookup timed out", TRUE);
+}
+
+static bool auth_worker_request_send(struct auth_worker_connection *conn,
+ struct auth_worker_request *request)
+{
+ struct const_iovec iov[3];
+ unsigned int age_secs = ioloop_time - request->created;
+
+ i_assert(conn->to != NULL);
+
+ if (age_secs >= AUTH_WORKER_ABORT_SECS) {
+ e_error(conn->event,
+ "Aborting auth request that was queued for %d secs, "
+ "%d left in queue",
+ age_secs, aqueue_count(worker_request_queue));
+ request->callback(conn, t_strdup_printf(
+ "FAIL\t%d", PASSDB_RESULT_INTERNAL_FAILURE),
+ request->context);
+ return FALSE;
+ }
+ if (age_secs >= AUTH_WORKER_DELAY_WARN_SECS &&
+ ioloop_time - auth_worker_last_warn >
+ AUTH_WORKER_DELAY_WARN_MIN_INTERVAL_SECS) {
+ auth_worker_last_warn = ioloop_time;
+ e_error(conn->event, "Auth request was queued for %d "
+ "seconds, %d left in queue "
+ "(see auth_worker_max_count)",
+ age_secs, aqueue_count(worker_request_queue));
+ }
+
+ request->id = ++conn->id_counter;
+
+ iov[0].iov_base = t_strdup_printf("%d\t", request->id);
+ iov[0].iov_len = strlen(iov[0].iov_base);
+ iov[1].iov_base = request->data;
+ iov[1].iov_len = strlen(request->data);
+ iov[2].iov_base = "\n";
+ iov[2].iov_len = 1;
+
+ o_stream_nsendv(conn->output, iov, 3);
+
+ i_assert(conn->request == NULL);
+ conn->request = request;
+
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(AUTH_WORKER_LOOKUP_TIMEOUT_SECS * 1000,
+ auth_worker_call_timeout, conn);
+ idle_count--;
+ return TRUE;
+}
+
+static void auth_worker_request_send_next(struct auth_worker_connection *conn)
+{
+ struct auth_worker_request *request;
+
+ do {
+ if (aqueue_count(worker_request_queue) == 0)
+ return;
+
+ request = array_idx_elem(&worker_request_array,
+ aqueue_idx(worker_request_queue, 0));
+ aqueue_delete_tail(worker_request_queue);
+ } while (!auth_worker_request_send(conn, request));
+}
+
+static void auth_worker_send_handshake(struct auth_worker_connection *conn)
+{
+ string_t *str;
+ unsigned char passdb_md5[MD5_RESULTLEN];
+ unsigned char userdb_md5[MD5_RESULTLEN];
+
+ str = t_str_new(128);
+ str_printfa(str, "VERSION\tauth-worker\t%u\t%u\n",
+ AUTH_WORKER_PROTOCOL_MAJOR_VERSION,
+ AUTH_WORKER_PROTOCOL_MINOR_VERSION);
+
+ passdbs_generate_md5(passdb_md5);
+ userdbs_generate_md5(userdb_md5);
+ str_append(str, "DBHASH\t");
+ binary_to_hex_append(str, passdb_md5, sizeof(passdb_md5));
+ str_append_c(str, '\t');
+ binary_to_hex_append(str, userdb_md5, sizeof(userdb_md5));
+ str_append_c(str, '\n');
+
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+}
+
+static struct auth_worker_connection *auth_worker_create(void)
+{
+ struct auth_worker_connection *conn;
+ struct event *event;
+ int fd;
+
+ if (array_count(&connections) >= auth_workers_throttle_count)
+ return NULL;
+
+ event = event_create(auth_event);
+ event_set_append_log_prefix(event, "auth-worker: ");
+
+ fd = net_connect_unix_with_retries(worker_socket_path, 5000);
+ if (fd == -1) {
+ if (errno == EACCES) {
+ e_error(event, "%s",
+ eacces_error_get("net_connect_unix",
+ worker_socket_path));
+ } else {
+ e_error(event, "net_connect_unix(%s) failed: %m",
+ worker_socket_path);
+ }
+ event_unref(&event);
+ return NULL;
+ }
+
+ conn = i_new(struct auth_worker_connection, 1);
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, AUTH_WORKER_MAX_LINE_LENGTH);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ conn->io = io_add(fd, IO_READ, worker_input, conn);
+ conn->to = timeout_add(AUTH_WORKER_MAX_IDLE_SECS * 1000,
+ auth_worker_idle_timeout, conn);
+ conn->event = event;
+ auth_worker_send_handshake(conn);
+
+ idle_count++;
+ array_push_back(&connections, &conn);
+ return conn;
+}
+
+static void auth_worker_destroy(struct auth_worker_connection **_conn,
+ const char *reason, bool restart)
+{
+ struct auth_worker_connection *conn = *_conn;
+ struct auth_worker_connection *const *conns;
+ unsigned int idx;
+
+ *_conn = NULL;
+
+ if (conn->received_error) {
+ i_assert(auth_workers_with_errors > 0);
+ i_assert(auth_workers_with_errors <= array_count(&connections));
+ auth_workers_with_errors--;
+ }
+
+ array_foreach(&connections, conns) {
+ if (*conns == conn) {
+ idx = array_foreach_idx(&connections, conns);
+ array_delete(&connections, idx, 1);
+ break;
+ }
+ }
+
+ if (conn->request == NULL)
+ idle_count--;
+
+ if (conn->request != NULL) {
+ e_error(conn->event, "Aborted %s request for %s: %s",
+ t_strcut(conn->request->data, '\t'),
+ conn->request->username, reason);
+ conn->request->callback(conn, t_strdup_printf(
+ "FAIL\t%d", PASSDB_RESULT_INTERNAL_FAILURE),
+ conn->request->context);
+ }
+
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ timeout_remove(&conn->to);
+
+ if (close(conn->fd) < 0)
+ e_error(conn->event, "close() failed: %m");
+ event_unref(&conn->event);
+ i_free(conn);
+
+ if (idle_count == 0 && restart) {
+ conn = auth_worker_create();
+ if (conn != NULL)
+ auth_worker_request_send_next(conn);
+ }
+}
+
+static struct auth_worker_connection *auth_worker_find_free(void)
+{
+ struct auth_worker_connection *conn;
+
+ if (idle_count == 0)
+ return NULL;
+
+ array_foreach_elem(&connections, conn) {
+ if (conn->request == NULL)
+ return conn;
+ }
+ i_unreached();
+ return NULL;
+}
+
+static bool auth_worker_request_handle(struct auth_worker_connection *conn,
+ struct auth_worker_request *request,
+ const char *line)
+{
+ if (str_begins(line, "*\t")) {
+ /* multi-line reply, not finished yet */
+ if (conn->resuming)
+ timeout_reset(conn->to);
+ else {
+ conn->resuming = TRUE;
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(AUTH_WORKER_RESUME_TIMEOUT_SECS * 1000,
+ auth_worker_call_timeout, conn);
+ }
+ } else {
+ conn->resuming = FALSE;
+ conn->request = NULL;
+ conn->timeout_pending_resume = FALSE;
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(AUTH_WORKER_MAX_IDLE_SECS * 1000,
+ auth_worker_idle_timeout, conn);
+ idle_count++;
+ }
+
+ if (!request->callback(conn, line, request->context) && conn->io != NULL) {
+ conn->timeout_pending_resume = FALSE;
+ timeout_remove(&conn->to);
+ io_remove(&conn->io);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool auth_worker_error(struct auth_worker_connection *conn)
+{
+ if (conn->received_error)
+ return TRUE;
+ conn->received_error = TRUE;
+ auth_workers_with_errors++;
+ i_assert(auth_workers_with_errors <= array_count(&connections));
+
+ if (auth_workers_with_errors == 1) {
+ /* this is the only failing auth worker connection.
+ don't create new ones until this one sends SUCCESS. */
+ auth_workers_throttle_count = array_count(&connections);
+ return TRUE;
+ }
+
+ /* too many auth workers, reduce them */
+ i_assert(array_count(&connections) > 1);
+ if (auth_workers_throttle_count >= array_count(&connections))
+ auth_workers_throttle_count = array_count(&connections)-1;
+ else if (auth_workers_throttle_count > 1)
+ auth_workers_throttle_count--;
+ auth_worker_destroy(&conn, "Internal auth worker failure", FALSE);
+ return FALSE;
+}
+
+static void auth_worker_success(struct auth_worker_connection *conn)
+{
+ unsigned int max_count = global_auth_settings->worker_max_count;
+
+ if (!conn->received_error)
+ return;
+
+ i_assert(auth_workers_with_errors > 0);
+ i_assert(auth_workers_with_errors <= array_count(&connections));
+ auth_workers_with_errors--;
+
+ if (auth_workers_with_errors == 0) {
+ /* all workers are succeeding now, set the limit back to
+ original. */
+ auth_workers_throttle_count = max_count;
+ } else if (auth_workers_throttle_count < max_count)
+ auth_workers_throttle_count++;
+ conn->received_error = FALSE;
+}
+
+static void worker_input(struct auth_worker_connection *conn)
+{
+ const char *line, *id_str;
+ unsigned int id;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ auth_worker_destroy(&conn, "Worker process died unexpectedly",
+ TRUE);
+ return;
+ case -2:
+ /* buffer full */
+ e_error(conn->event,
+ "BUG: Auth worker sent us more than %d bytes",
+ (int)AUTH_WORKER_MAX_LINE_LENGTH);
+ auth_worker_destroy(&conn, "Worker is buggy", TRUE);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ if (strcmp(line, "RESTART") == 0) {
+ conn->restart = TRUE;
+ continue;
+ }
+ if (strcmp(line, "SHUTDOWN") == 0) {
+ conn->shutdown = TRUE;
+ continue;
+ }
+ if (strcmp(line, "ERROR") == 0) {
+ if (!auth_worker_error(conn))
+ return;
+ continue;
+ }
+ if (strcmp(line, "SUCCESS") == 0) {
+ auth_worker_success(conn);
+ continue;
+ }
+ id_str = line;
+ line = strchr(line, '\t');
+ if (line == NULL ||
+ str_to_uint(t_strdup_until(id_str, line), &id) < 0)
+ continue;
+
+ if (conn->request != NULL && id == conn->request->id) {
+ if (!auth_worker_request_handle(conn, conn->request,
+ line + 1))
+ break;
+ } else {
+ if (conn->request != NULL) {
+ e_error(conn->event,
+ "BUG: Worker sent reply with id %u, "
+ "expected %u", id, conn->request->id);
+ } else {
+ e_error(conn->event,
+ "BUG: Worker sent reply with id %u, "
+ "none was expected", id);
+ }
+ auth_worker_destroy(&conn, "Worker is buggy", TRUE);
+ return;
+ }
+ }
+
+ if (conn->request != NULL) {
+ /* there's still a pending request */
+ } else if (conn->restart)
+ auth_worker_destroy(&conn, "Max requests limit", TRUE);
+ else if (conn->shutdown)
+ auth_worker_destroy(&conn, "Idle kill", FALSE);
+ else
+ auth_worker_request_send_next(conn);
+}
+
+static void worker_input_resume(struct auth_worker_connection *conn)
+{
+ conn->timeout_pending_resume = FALSE;
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(AUTH_WORKER_RESUME_TIMEOUT_SECS * 1000,
+ auth_worker_call_timeout, conn);
+ worker_input(conn);
+}
+
+void auth_worker_call(pool_t pool, const char *username, const char *data,
+ auth_worker_callback_t *callback, void *context)
+{
+ struct auth_worker_connection *conn;
+ struct auth_worker_request *request;
+
+ request = p_new(pool, struct auth_worker_request, 1);
+ request->created = ioloop_time;
+ request->username = p_strdup(pool, username);
+ request->data = p_strdup(pool, data);
+ request->callback = callback;
+ request->context = context;
+
+ if (aqueue_count(worker_request_queue) > 0) {
+ /* requests are already being queued, no chance of
+ finding/creating a worker */
+ conn = NULL;
+ } else {
+ conn = auth_worker_find_free();
+ if (conn == NULL) {
+ /* no free connections, create a new one */
+ conn = auth_worker_create();
+ }
+ }
+ if (conn != NULL) {
+ if (!auth_worker_request_send(conn, request))
+ i_unreached();
+ } else {
+ /* reached the limit, queue the request */
+ aqueue_append(worker_request_queue, &request);
+ }
+}
+
+void auth_worker_server_resume_input(struct auth_worker_connection *conn)
+{
+ if (conn->request == NULL) {
+ /* request was just finished, don't try to resume it */
+ return;
+ }
+
+ if (conn->io == NULL)
+ conn->io = io_add(conn->fd, IO_READ, worker_input, conn);
+ if (!conn->timeout_pending_resume) {
+ conn->timeout_pending_resume = TRUE;
+ timeout_remove(&conn->to);
+ conn->to = timeout_add_short(0, worker_input_resume, conn);
+ }
+}
+
+void auth_worker_server_init(void)
+{
+ worker_socket_path = "auth-worker";
+ auth_workers_throttle_count = global_auth_settings->worker_max_count;
+ i_assert(auth_workers_throttle_count > 0);
+
+ i_array_init(&worker_request_array, 128);
+ worker_request_queue = aqueue_init(&worker_request_array.arr);
+
+ i_array_init(&connections, 16);
+}
+
+void auth_worker_server_deinit(void)
+{
+ struct auth_worker_connection **connp, *conn;
+
+ while (array_count(&connections) > 0) {
+ connp = array_front_modifiable(&connections);
+ conn = *connp;
+ auth_worker_destroy(&conn, "Shutting down", FALSE);
+ }
+ array_free(&connections);
+
+ aqueue_deinit(&worker_request_queue);
+ array_free(&worker_request_array);
+}
diff --git a/src/auth/auth-worker-server.h b/src/auth/auth-worker-server.h
new file mode 100644
index 0000000..d2a3063
--- /dev/null
+++ b/src/auth/auth-worker-server.h
@@ -0,0 +1,18 @@
+#ifndef AUTH_WORKER_SERVER_H
+#define AUTH_WORKER_SERVER_H
+
+struct auth_request;
+struct auth_stream_reply;
+struct auth_worker_connection;
+
+typedef bool auth_worker_callback_t(struct auth_worker_connection *conn,
+ const char *reply, void *context);
+
+void auth_worker_call(pool_t pool, const char *username, const char *data,
+ auth_worker_callback_t *callback, void *context);
+void auth_worker_server_resume_input(struct auth_worker_connection *conn);
+
+void auth_worker_server_init(void);
+void auth_worker_server_deinit(void);
+
+#endif
diff --git a/src/auth/auth.c b/src/auth/auth.c
new file mode 100644
index 0000000..845c43c
--- /dev/null
+++ b/src/auth/auth.c
@@ -0,0 +1,450 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "settings-parser.h"
+#include "master-service-settings.h"
+#include "mech.h"
+#include "userdb.h"
+#include "passdb.h"
+#include "passdb-template.h"
+#include "userdb-template.h"
+#include "auth.h"
+
+struct event *auth_event;
+struct event_category event_category_auth = {
+ .name = "auth",
+};
+
+static const struct auth_userdb_settings userdb_dummy_set = {
+ .name = "",
+ .driver = "static",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+
+ .skip = "never",
+ .result_success = "return-ok",
+ .result_failure = "continue",
+ .result_internalfail = "continue",
+
+ .auth_verbose = "default",
+};
+
+ARRAY_TYPE(auth) auths;
+
+static enum auth_passdb_skip auth_passdb_skip_parse(const char *str)
+{
+ if (strcmp(str, "never") == 0)
+ return AUTH_PASSDB_SKIP_NEVER;
+ if (strcmp(str, "authenticated") == 0)
+ return AUTH_PASSDB_SKIP_AUTHENTICATED;
+ if (strcmp(str, "unauthenticated") == 0)
+ return AUTH_PASSDB_SKIP_UNAUTHENTICATED;
+ i_unreached();
+}
+
+static enum auth_userdb_skip auth_userdb_skip_parse(const char *str)
+{
+ if (strcmp(str, "never") == 0)
+ return AUTH_USERDB_SKIP_NEVER;
+ if (strcmp(str, "found") == 0)
+ return AUTH_USERDB_SKIP_FOUND;
+ if (strcmp(str, "notfound") == 0)
+ return AUTH_USERDB_SKIP_NOTFOUND;
+ i_unreached();
+}
+
+static enum auth_db_rule auth_db_rule_parse(const char *str)
+{
+ if (strcmp(str, "return") == 0)
+ return AUTH_DB_RULE_RETURN;
+ if (strcmp(str, "return-ok") == 0)
+ return AUTH_DB_RULE_RETURN_OK;
+ if (strcmp(str, "return-fail") == 0)
+ return AUTH_DB_RULE_RETURN_FAIL;
+ if (strcmp(str, "continue") == 0)
+ return AUTH_DB_RULE_CONTINUE;
+ if (strcmp(str, "continue-ok") == 0)
+ return AUTH_DB_RULE_CONTINUE_OK;
+ if (strcmp(str, "continue-fail") == 0)
+ return AUTH_DB_RULE_CONTINUE_FAIL;
+ i_unreached();
+}
+
+static void
+auth_passdb_preinit(struct auth *auth, const struct auth_passdb_settings *set,
+ struct auth_passdb **passdbs)
+{
+ struct auth_passdb *auth_passdb, **dest;
+
+ auth_passdb = p_new(auth->pool, struct auth_passdb, 1);
+ auth_passdb->set = set;
+ auth_passdb->skip = auth_passdb_skip_parse(set->skip);
+ auth_passdb->result_success =
+ auth_db_rule_parse(set->result_success);
+ auth_passdb->result_failure =
+ auth_db_rule_parse(set->result_failure);
+ auth_passdb->result_internalfail =
+ auth_db_rule_parse(set->result_internalfail);
+
+ auth_passdb->default_fields_tmpl =
+ passdb_template_build(auth->pool, set->default_fields);
+ auth_passdb->override_fields_tmpl =
+ passdb_template_build(auth->pool, set->override_fields);
+
+ /* for backwards compatibility: */
+ if (set->pass)
+ auth_passdb->result_success = AUTH_DB_RULE_CONTINUE;
+
+ for (dest = passdbs; *dest != NULL; dest = &(*dest)->next) ;
+ *dest = auth_passdb;
+
+ auth_passdb->passdb = passdb_preinit(auth->pool, set);
+ /* make sure any %variables in default_fields exist in cache_key */
+ if (auth_passdb->passdb->default_cache_key != NULL) {
+ auth_passdb->cache_key =
+ p_strconcat(auth->pool, auth_passdb->passdb->default_cache_key,
+ set->default_fields, NULL);
+ }
+ else {
+ auth_passdb->cache_key = NULL;
+ }
+}
+
+static void
+auth_userdb_preinit(struct auth *auth, const struct auth_userdb_settings *set)
+{
+ struct auth_userdb *auth_userdb, **dest;
+
+ auth_userdb = p_new(auth->pool, struct auth_userdb, 1);
+ auth_userdb->set = set;
+ auth_userdb->skip = auth_userdb_skip_parse(set->skip);
+ auth_userdb->result_success =
+ auth_db_rule_parse(set->result_success);
+ auth_userdb->result_failure =
+ auth_db_rule_parse(set->result_failure);
+ auth_userdb->result_internalfail =
+ auth_db_rule_parse(set->result_internalfail);
+
+ auth_userdb->default_fields_tmpl =
+ userdb_template_build(auth->pool, set->driver,
+ set->default_fields);
+ auth_userdb->override_fields_tmpl =
+ userdb_template_build(auth->pool, set->driver,
+ set->override_fields);
+
+ for (dest = &auth->userdbs; *dest != NULL; dest = &(*dest)->next) ;
+ *dest = auth_userdb;
+
+ auth_userdb->userdb = userdb_preinit(auth->pool, set);
+ /* make sure any %variables in default_fields exist in cache_key */
+ if (auth_userdb->userdb->default_cache_key != NULL) {
+ auth_userdb->cache_key =
+ p_strconcat(auth->pool, auth_userdb->userdb->default_cache_key,
+ set->default_fields, NULL);
+ }
+ else {
+ auth_userdb->cache_key = NULL;
+ }
+}
+
+static bool auth_passdb_list_have_verify_plain(const struct auth *auth)
+{
+ const struct auth_passdb *passdb;
+
+ for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) {
+ if (passdb->passdb->iface.verify_plain != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool auth_passdb_list_have_lookup_credentials(const struct auth *auth)
+{
+ const struct auth_passdb *passdb;
+
+ for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) {
+ if (passdb->passdb->iface.lookup_credentials != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool auth_passdb_list_have_set_credentials(const struct auth *auth)
+{
+ const struct auth_passdb *passdb;
+
+ for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next) {
+ if (passdb->passdb->iface.set_credentials != NULL)
+ return TRUE;
+ }
+ for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) {
+ if (passdb->passdb->iface.set_credentials != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+auth_mech_verify_passdb(const struct auth *auth, const struct mech_module_list *list)
+{
+ switch (list->module.passdb_need) {
+ case MECH_PASSDB_NEED_NOTHING:
+ break;
+ case MECH_PASSDB_NEED_VERIFY_PLAIN:
+ if (!auth_passdb_list_have_verify_plain(auth))
+ return FALSE;
+ break;
+ case MECH_PASSDB_NEED_VERIFY_RESPONSE:
+ case MECH_PASSDB_NEED_LOOKUP_CREDENTIALS:
+ if (!auth_passdb_list_have_lookup_credentials(auth))
+ return FALSE;
+ break;
+ case MECH_PASSDB_NEED_SET_CREDENTIALS:
+ if (!auth_passdb_list_have_lookup_credentials(auth))
+ return FALSE;
+ if (!auth_passdb_list_have_set_credentials(auth))
+ return FALSE;
+ break;
+ }
+ return TRUE;
+}
+
+static void auth_mech_list_verify_passdb(const struct auth *auth)
+{
+ const struct mech_module_list *list;
+
+ for (list = auth->reg->modules; list != NULL; list = list->next) {
+ if (!auth_mech_verify_passdb(auth, list))
+ break;
+ }
+
+ if (list != NULL) {
+ if (auth->passdbs == NULL) {
+ i_fatal("No passdbs specified in configuration file. "
+ "%s mechanism needs one",
+ list->module.mech_name);
+ }
+ i_fatal("%s mechanism can't be supported with given passdbs",
+ list->module.mech_name);
+ }
+}
+
+static struct auth * ATTR_NULL(2)
+auth_preinit(const struct auth_settings *set, const char *service, pool_t pool,
+ const struct mechanisms_register *reg)
+{
+ struct auth_passdb_settings *const *passdbs;
+ struct auth_userdb_settings *const *userdbs;
+ struct auth *auth;
+ unsigned int i, count, db_count, passdb_count, last_passdb = 0;
+
+ auth = p_new(pool, struct auth, 1);
+ auth->pool = pool;
+ auth->service = p_strdup(pool, service);
+ auth->set = set;
+ auth->reg = reg;
+
+ if (array_is_created(&set->passdbs))
+ passdbs = array_get(&set->passdbs, &db_count);
+ else {
+ passdbs = NULL;
+ db_count = 0;
+ }
+
+ /* initialize passdbs first and count them */
+ for (passdb_count = 0, i = 0; i < db_count; i++) {
+ if (passdbs[i]->master)
+ continue;
+
+ /* passdb { skip=unauthenticated } as the first passdb doesn't
+ make sense, since user is never authenticated at that point.
+ skip over them silently. */
+ if (auth->passdbs == NULL &&
+ auth_passdb_skip_parse(passdbs[i]->skip) == AUTH_PASSDB_SKIP_UNAUTHENTICATED)
+ continue;
+
+ auth_passdb_preinit(auth, passdbs[i], &auth->passdbs);
+ passdb_count++;
+ last_passdb = i;
+ }
+ if (passdb_count != 0 && passdbs[last_passdb]->pass)
+ i_fatal("Last passdb can't have pass=yes");
+
+ for (i = 0; i < db_count; i++) {
+ if (!passdbs[i]->master)
+ continue;
+
+ /* skip skip=unauthenticated, as explained above */
+ if (auth->masterdbs == NULL &&
+ auth_passdb_skip_parse(passdbs[i]->skip) == AUTH_PASSDB_SKIP_UNAUTHENTICATED)
+ continue;
+
+ if (passdbs[i]->deny)
+ i_fatal("Master passdb can't have deny=yes");
+ if (passdbs[i]->pass && passdb_count == 0) {
+ i_fatal("Master passdb can't have pass=yes "
+ "if there are no passdbs");
+ }
+ auth_passdb_preinit(auth, passdbs[i], &auth->masterdbs);
+ }
+
+ if (array_is_created(&set->userdbs)) {
+ userdbs = array_get(&set->userdbs, &count);
+ for (i = 0; i < count; i++)
+ auth_userdb_preinit(auth, userdbs[i]);
+ }
+
+ if (auth->userdbs == NULL) {
+ /* use a dummy userdb static. */
+ auth_userdb_preinit(auth, &userdb_dummy_set);
+ }
+ return auth;
+}
+
+static void auth_passdb_init(struct auth_passdb *passdb)
+{
+ passdb_init(passdb->passdb);
+
+ i_assert(passdb->passdb->default_pass_scheme != NULL ||
+ passdb->cache_key == NULL);
+}
+
+static void auth_init(struct auth *auth)
+{
+ struct auth_passdb *passdb;
+ struct auth_userdb *userdb;
+
+ for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next)
+ auth_passdb_init(passdb);
+ for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next)
+ auth_passdb_init(passdb);
+ for (userdb = auth->userdbs; userdb != NULL; userdb = userdb->next)
+ userdb_init(userdb->userdb);
+}
+
+static void auth_deinit(struct auth *auth)
+{
+ struct auth_passdb *passdb;
+ struct auth_userdb *userdb;
+
+ for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next)
+ passdb_deinit(passdb->passdb);
+ for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next)
+ passdb_deinit(passdb->passdb);
+ for (userdb = auth->userdbs; userdb != NULL; userdb = userdb->next)
+ userdb_deinit(userdb->userdb);
+}
+
+struct auth *auth_find_service(const char *name)
+{
+ struct auth *const *a;
+ unsigned int i, count;
+
+ a = array_get(&auths, &count);
+ if (name != NULL) {
+ for (i = 1; i < count; i++) {
+ if (strcmp(a[i]->service, name) == 0)
+ return a[i];
+ }
+ /* not found. maybe we can instead find a !service */
+ for (i = 1; i < count; i++) {
+ if (a[i]->service[0] == '!' &&
+ strcmp(a[i]->service + 1, name) != 0)
+ return a[i];
+ }
+ }
+ return a[0];
+}
+
+struct auth *auth_default_service(void)
+{
+ struct auth *const *a;
+ unsigned int count;
+
+ a = array_get(&auths, &count);
+ return a[0];
+}
+
+void auths_preinit(const struct auth_settings *set, pool_t pool,
+ const struct mechanisms_register *reg,
+ const char *const *services)
+{
+ struct master_service_settings_output set_output;
+ const struct auth_settings *service_set;
+ struct auth *auth;
+ unsigned int i;
+ const char *not_service = NULL;
+ bool check_default = TRUE;
+
+ auth_event = event_create(NULL);
+ event_set_forced_debug(auth_event, set->debug);
+ event_add_category(auth_event, &event_category_auth);
+ i_array_init(&auths, 8);
+
+ auth = auth_preinit(set, NULL, pool, reg);
+ array_push_back(&auths, &auth);
+
+ for (i = 0; services[i] != NULL; i++) {
+ if (services[i][0] == '!') {
+ if (not_service != NULL) {
+ i_fatal("Can't have multiple protocol "
+ "!services (seen %s and %s)",
+ not_service, services[i]);
+ }
+ not_service = services[i];
+ }
+ service_set = auth_settings_read(services[i], pool,
+ &set_output);
+ auth = auth_preinit(service_set, services[i], pool, reg);
+ array_push_back(&auths, &auth);
+ }
+
+ if (not_service != NULL && str_array_find(services, not_service+1))
+ check_default = FALSE;
+
+ array_foreach_elem(&auths, auth) {
+ if (auth->service != NULL || check_default)
+ auth_mech_list_verify_passdb(auth);
+ }
+}
+
+void auths_init(void)
+{
+ struct auth *auth;
+
+ /* sanity checks */
+ i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_USER_IDX].key == 'u');
+ i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_USERNAME_IDX].key == 'n');
+ i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX].key == 'd');
+ i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT].key == '\0' &&
+ auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT].long_key == NULL);
+ i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT-1].key != '\0' ||
+ auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT-1].long_key != NULL);
+
+ array_foreach_elem(&auths, auth)
+ auth_init(auth);
+}
+
+void auths_deinit(void)
+{
+ struct auth *auth;
+
+ array_foreach_elem(&auths, auth)
+ auth_deinit(auth);
+ event_unref(&auth_event);
+}
+
+void auths_free(void)
+{
+ struct auth **auth;
+ unsigned int i, count;
+
+ /* deinit in reverse order, because modules have been allocated by
+ the first auth pool that used them */
+ auth = array_get_modifiable(&auths, &count);
+ for (i = count; i > 0; i--)
+ pool_unref(&auth[i-1]->pool);
+ array_free(&auths);
+}
diff --git a/src/auth/auth.h b/src/auth/auth.h
new file mode 100644
index 0000000..3ca5a9b
--- /dev/null
+++ b/src/auth/auth.h
@@ -0,0 +1,91 @@
+#ifndef AUTH_H
+#define AUTH_H
+
+#include "auth-settings.h"
+
+#define PASSWORD_HIDDEN_STR "<hidden>"
+
+ARRAY_DEFINE_TYPE(auth, struct auth *);
+extern ARRAY_TYPE(auth) auths;
+
+enum auth_passdb_skip {
+ AUTH_PASSDB_SKIP_NEVER,
+ AUTH_PASSDB_SKIP_AUTHENTICATED,
+ AUTH_PASSDB_SKIP_UNAUTHENTICATED
+};
+
+enum auth_userdb_skip {
+ AUTH_USERDB_SKIP_NEVER,
+ AUTH_USERDB_SKIP_FOUND,
+ AUTH_USERDB_SKIP_NOTFOUND
+};
+
+enum auth_db_rule {
+ AUTH_DB_RULE_RETURN,
+ AUTH_DB_RULE_RETURN_OK,
+ AUTH_DB_RULE_RETURN_FAIL,
+ AUTH_DB_RULE_CONTINUE,
+ AUTH_DB_RULE_CONTINUE_OK,
+ AUTH_DB_RULE_CONTINUE_FAIL
+};
+
+struct auth_passdb {
+ struct auth_passdb *next;
+
+ const struct auth_passdb_settings *set;
+ struct passdb_module *passdb;
+
+ /* The caching key for this passdb, or NULL if caching isn't wanted. */
+ const char *cache_key;
+
+ struct passdb_template *default_fields_tmpl;
+ struct passdb_template *override_fields_tmpl;
+
+ enum auth_passdb_skip skip;
+ enum auth_db_rule result_success;
+ enum auth_db_rule result_failure;
+ enum auth_db_rule result_internalfail;
+};
+
+struct auth_userdb {
+ struct auth_userdb *next;
+
+ const struct auth_userdb_settings *set;
+ struct userdb_module *userdb;
+
+ /* The caching key for this userdb, or NULL if caching isn't wanted. */
+ const char *cache_key;
+
+ struct userdb_template *default_fields_tmpl;
+ struct userdb_template *override_fields_tmpl;
+
+ enum auth_userdb_skip skip;
+ enum auth_db_rule result_success;
+ enum auth_db_rule result_failure;
+ enum auth_db_rule result_internalfail;
+};
+
+struct auth {
+ pool_t pool;
+ const char *service;
+ const struct auth_settings *set;
+
+ const struct mechanisms_register *reg;
+ struct auth_passdb *masterdbs;
+ struct auth_passdb *passdbs;
+ struct auth_userdb *userdbs;
+};
+
+extern struct auth_penalty *auth_penalty;
+
+struct auth *auth_find_service(const char *name);
+struct auth *auth_default_service(void);
+
+void auths_preinit(const struct auth_settings *set, pool_t pool,
+ const struct mechanisms_register *reg,
+ const char *const *services);
+void auths_init(void);
+void auths_deinit(void);
+void auths_free(void);
+
+#endif
diff --git a/src/auth/checkpassword-reply.c b/src/auth/checkpassword-reply.c
new file mode 100644
index 0000000..71f231a
--- /dev/null
+++ b/src/auth/checkpassword-reply.c
@@ -0,0 +1,110 @@
+/* simple checkpassword wrapper to send userdb data back to dovecot-auth */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "write-full.h"
+
+#include <unistd.h>
+
+int main(void)
+{
+ string_t *str;
+ const char *user, *home, *authorized, *orig_uid_env;
+ const char *extra_env, *key, *value, *const *tmp;
+ bool uid_found = FALSE, gid_found = FALSE;
+ uid_t orig_uid;
+
+ lib_init();
+ str = t_str_new(1024);
+
+ orig_uid_env = getenv("ORIG_UID");
+ if (orig_uid_env == NULL || str_to_uid(orig_uid_env, &orig_uid) < 0)
+ orig_uid = (uid_t)-1;
+
+ /* ORIG_UID should have the auth process's UID that forked us.
+ if the checkpassword changed the UID, this could be a security hole
+ because the UID's other processes can ptrace this process and write
+ any kind of a reply to fd 4. so we can run only if:
+
+ a) INSECURE_SETUID environment is set.
+ b) process isn't ptraceable (this binary is setuid/setgid)
+ c) checkpassword didn't actually change the UID (but used
+ userdb_uid instead)
+ */
+ if (getenv("INSECURE_SETUID") == NULL &&
+ (orig_uid == (uid_t)-1 || orig_uid != getuid()) &&
+ getuid() == geteuid() && getgid() == getegid()) {
+ if (orig_uid_env == NULL) {
+ i_error("checkpassword: ORIG_UID environment was dropped by checkpassword. "
+ "Can't verify if we're safe to run. See "
+ "http://wiki2.dovecot.org/AuthDatabase/CheckPassword#Security");
+ } else {
+ i_error("checkpassword: The checkpassword couldn't be run securely. See "
+ "http://wiki2.dovecot.org/AuthDatabase/CheckPassword#Security");
+ }
+ return 111;
+ }
+
+ user = getenv("USER");
+ if (user != NULL) {
+ if (strchr(user, '\t') != NULL) {
+ i_error("checkpassword: USER contains TAB");
+ return 1;
+ }
+ str_printfa(str, "user=");
+ str_append_tabescaped(str, user);
+ str_append_c(str, '\t');
+ }
+
+ home = getenv("HOME");
+ if (home != NULL) {
+ if (strchr(home, '\t') != NULL) {
+ i_error("checkpassword: HOME contains TAB");
+ return 1;
+ }
+ str_printfa(str, "userdb_home=");
+ str_append_tabescaped(str, home);
+ str_append_c(str, '\t');
+ }
+
+ extra_env = getenv("EXTRA");
+ if (extra_env != NULL) {
+ for (tmp = t_strsplit(extra_env, " "); *tmp != NULL; tmp++) {
+ value = getenv(*tmp);
+ if (value != NULL) {
+ key = t_str_lcase(*tmp);
+ if (strcmp(key, "userdb_uid") == 0)
+ uid_found = TRUE;
+ else if (strcmp(key, "userdb_gid") == 0)
+ gid_found = TRUE;
+ str_append_tabescaped(str, key);
+ str_append_c(str, '=');
+ str_append_tabescaped(str, value);
+ str_append_c(str, '\t');
+ }
+ }
+ }
+ if (!uid_found)
+ str_printfa(str, "userdb_uid=%s\t", dec2str(getuid()));
+ if (!gid_found)
+ str_printfa(str, "userdb_gid=%s\t", dec2str(getgid()));
+
+ i_assert(str_len(str) > 0);
+
+ if (write_full(4, str_data(str), str_len(str)) < 0) {
+ i_error("checkpassword: write_full() failed: %m");
+ lib_exit(111);
+ }
+ authorized = getenv("AUTHORIZED");
+ if (authorized == NULL) {
+ /* authentication */
+ return 0;
+ } else if (strcmp(authorized, "2") == 0) {
+ /* successful passdb/userdb lookup */
+ return 2;
+ } else {
+ i_error("checkpassword: Script doesn't support passdb/userdb lookup");
+ return 111;
+ }
+}
diff --git a/src/auth/crypt-blowfish.c b/src/auth/crypt-blowfish.c
new file mode 100644
index 0000000..460f8cf
--- /dev/null
+++ b/src/auth/crypt-blowfish.c
@@ -0,0 +1,900 @@
+/*
+ * The crypt_blowfish homepage is:
+ *
+ * http://www.openwall.com/crypt/
+ *
+ * This code comes from John the Ripper password cracker, with reentrant
+ * and crypt(3) interfaces added, but optimizations specific to password
+ * cracking removed.
+ *
+ * Written by Solar Designer <solar at openwall.com> in 1998-2014.
+ * No copyright is claimed, and the software is hereby placed in the public
+ * domain. In case this attempt to disclaim copyright and place the software
+ * in the public domain is deemed null and void, then the software is
+ * Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
+ * general public under the following terms:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted.
+ *
+ * There's ABSOLUTELY NO WARRANTY, express or implied.
+ *
+ * It is my intent that you should be able to use this on your system,
+ * as part of a software package, or anywhere else to improve security,
+ * ensure compatibility, or for any other purpose. I would appreciate
+ * it if you give credit where it is due and keep your modifications in
+ * the public domain as well, but I don't require that in order to let
+ * you place this code and any modifications you make under a license
+ * of your choice.
+ *
+ * This implementation is fully compatible with OpenBSD's bcrypt.c for prefix
+ * "$2b$", originally by Niels Provos <provos at citi.umich.edu>, and it uses
+ * some of his ideas. The password hashing algorithm was designed by David
+ * Mazieres <dm at lcs.mit.edu>. For information on the level of
+ * compatibility for bcrypt hash prefixes other than "$2b$", please refer to
+ * the comments in BF_set_key() below and to the included crypt(3) man page.
+ *
+ * There's a paper on the algorithm that explains its design decisions:
+ *
+ * http://www.usenix.org/events/usenix99/provos.html
+ *
+ * Some of the tricks in BF_ROUND might be inspired by Eric Young's
+ * Blowfish library (I can't be sure if I would think of something if I
+ * hadn't seen his code).
+ *
+ * 2017-10-10 - Adapted for dovecot code by Aki Tuomi <aki.tuomi@dovecot.fi>
+ */
+
+#include "lib.h"
+
+#include <errno.h>
+#ifndef __set_errno
+#define __set_errno(val) errno = (val)
+#endif
+
+/* Just to make sure the prototypes match the actual definitions */
+#include "crypt-blowfish.h"
+
+#ifdef __i386__
+#define BF_SCALE 1
+#elif defined(__x86_64__) || defined(__alpha__) || defined(__hppa__)
+#define BF_SCALE 1
+#else
+#define BF_SCALE 0
+#endif
+
+typedef unsigned int BF_word;
+typedef signed int BF_word_signed;
+
+/* Number of Blowfish rounds, this is also hardcoded into a few places */
+#define BF_N 16
+
+typedef BF_word BF_key[BF_N + 2];
+
+typedef struct {
+ BF_word S[4][0x100];
+ BF_key P;
+} BF_ctx;
+
+/*
+ * Magic IV for 64 Blowfish encryptions that we do at the end.
+ * The string is "OrpheanBeholderScryDoubt" on big-endian.
+ */
+static BF_word BF_magic_w[6] = {
+ 0x4F727068, 0x65616E42, 0x65686F6C,
+ 0x64657253, 0x63727944, 0x6F756274
+};
+
+/*
+ * P-box and S-box tables initialized with digits of Pi.
+ */
+static BF_ctx BF_init_state = {
+ {
+ {
+ 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
+ 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
+ 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
+ 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
+ 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
+ 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
+ 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
+ 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
+ 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
+ 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
+ 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
+ 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
+ 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
+ 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
+ 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
+ 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
+ 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
+ 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
+ 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
+ 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
+ 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
+ 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
+ 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
+ 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
+ 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
+ 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
+ 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
+ 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
+ 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
+ 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
+ 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
+ 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
+ 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
+ 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
+ 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
+ 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
+ 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
+ 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
+ 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
+ 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
+ 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
+ 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
+ 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
+ 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
+ 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
+ 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
+ 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
+ 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
+ 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
+ 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
+ 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
+ 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
+ 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
+ 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
+ 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
+ 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
+ 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
+ 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
+ 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
+ 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
+ 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
+ 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
+ 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
+ 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a
+ }, {
+ 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
+ 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
+ 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
+ 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
+ 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
+ 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
+ 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
+ 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
+ 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
+ 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
+ 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
+ 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
+ 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
+ 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
+ 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
+ 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
+ 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
+ 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
+ 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
+ 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
+ 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
+ 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
+ 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
+ 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
+ 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
+ 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
+ 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
+ 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
+ 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
+ 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
+ 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
+ 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
+ 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
+ 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
+ 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
+ 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
+ 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
+ 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
+ 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
+ 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
+ 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
+ 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
+ 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
+ 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
+ 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
+ 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
+ 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
+ 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
+ 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
+ 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
+ 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
+ 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
+ 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
+ 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
+ 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
+ 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
+ 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
+ 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
+ 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
+ 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
+ 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
+ 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
+ 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
+ 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7
+ }, {
+ 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
+ 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
+ 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
+ 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
+ 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
+ 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
+ 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
+ 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
+ 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
+ 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
+ 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
+ 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
+ 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
+ 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
+ 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
+ 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
+ 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
+ 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
+ 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
+ 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
+ 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
+ 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
+ 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
+ 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
+ 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
+ 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
+ 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
+ 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
+ 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
+ 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
+ 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
+ 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
+ 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
+ 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
+ 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
+ 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
+ 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
+ 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
+ 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
+ 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
+ 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
+ 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
+ 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
+ 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
+ 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
+ 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
+ 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
+ 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
+ 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
+ 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
+ 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
+ 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
+ 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
+ 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
+ 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
+ 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
+ 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
+ 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
+ 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
+ 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
+ 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
+ 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
+ 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
+ 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0
+ }, {
+ 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
+ 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
+ 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
+ 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
+ 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
+ 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
+ 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
+ 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
+ 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
+ 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
+ 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
+ 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
+ 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
+ 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
+ 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
+ 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
+ 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
+ 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
+ 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
+ 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
+ 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
+ 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
+ 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
+ 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
+ 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
+ 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
+ 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
+ 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
+ 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
+ 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
+ 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
+ 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
+ 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
+ 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
+ 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
+ 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
+ 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
+ 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
+ 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
+ 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
+ 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
+ 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
+ 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
+ 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
+ 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
+ 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
+ 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
+ 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
+ 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
+ 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
+ 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
+ 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
+ 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
+ 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
+ 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
+ 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
+ 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
+ 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
+ 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
+ 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
+ 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
+ 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
+ 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
+ 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
+ }
+ }, {
+ 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
+ 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
+ 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
+ 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
+ 0x9216d5d9, 0x8979fb1b
+ }
+};
+
+static unsigned char BF_itoa64[64 + 1] =
+ "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+static unsigned char BF_atoi64[0x60] = {
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 1,
+ 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 64, 64, 64, 64, 64,
+ 64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 64, 64, 64, 64, 64,
+ 64, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 64, 64, 64, 64, 64
+};
+
+#define BF_safe_atoi64(dst, src) \
+{ \
+ tmp = (unsigned char)(src); \
+ if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \
+ tmp = BF_atoi64[tmp]; \
+ if (tmp > 63) return -1; \
+ (dst) = tmp; \
+}
+
+static int BF_decode(BF_word *dst, const char *src, size_t size)
+{
+ unsigned char *dptr = (unsigned char *)dst;
+ unsigned char *end = dptr + size;
+ const unsigned char *sptr = (const unsigned char *)src;
+ unsigned int tmp, c1, c2, c3, c4;
+
+ do {
+ BF_safe_atoi64(c1, *sptr++);
+ BF_safe_atoi64(c2, *sptr++);
+ *dptr++ = (c1 << 2) | ((c2 & 0x30) >> 4);
+ if (dptr >= end) break;
+
+ BF_safe_atoi64(c3, *sptr++);
+ *dptr++ = ((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2);
+ if (dptr >= end) break;
+
+ BF_safe_atoi64(c4, *sptr++);
+ *dptr++ = ((c3 & 0x03) << 6) | c4;
+ } while (dptr < end);
+
+ return 0;
+}
+
+static void BF_encode(char *dst, const BF_word *src, size_t size)
+{
+ const unsigned char *sptr = (const unsigned char *)src;
+ const unsigned char *end = sptr + size;
+ unsigned char *dptr = (unsigned char *)dst;
+ unsigned int c1, c2;
+
+ do {
+ c1 = *sptr++;
+ *dptr++ = BF_itoa64[c1 >> 2];
+ c1 = (c1 & 0x03) << 4;
+ if (sptr >= end) {
+ *dptr++ = BF_itoa64[c1];
+ break;
+ }
+
+ c2 = *sptr++;
+ c1 |= c2 >> 4;
+ *dptr++ = BF_itoa64[c1];
+ c1 = (c2 & 0x0f) << 2;
+ if (sptr >= end) {
+ *dptr++ = BF_itoa64[c1];
+ break;
+ }
+
+ c2 = *sptr++;
+ c1 |= c2 >> 6;
+ *dptr++ = BF_itoa64[c1];
+ *dptr++ = BF_itoa64[c2 & 0x3f];
+ } while (sptr < end);
+}
+
+static void ATTR_UNSIGNED_WRAPS
+BF_swap(BF_word *x, int count)
+{
+ static int endianness_check = 1;
+ char *is_little_endian = (char *)&endianness_check;
+ BF_word tmp;
+
+ if (*is_little_endian != 0)
+ do {
+ tmp = *x;
+ tmp = (tmp << 16) | (tmp >> 16);
+ *x++ = ((tmp & 0x00FF00FF) << 8) | ((tmp >> 8) & 0x00FF00FF);
+ } while (--count > 0);
+}
+
+#if BF_SCALE
+/* Architectures which can shift addresses left by 2 bits with no extra cost */
+#define BF_ROUND(L, R, N) \
+ tmp1 = L & 0xFF; \
+ tmp2 = L >> 8; \
+ tmp2 &= 0xFF; \
+ tmp3 = L >> 16; \
+ tmp3 &= 0xFF; \
+ tmp4 = L >> 24; \
+ tmp1 = data.ctx.S[3][tmp1]; \
+ tmp2 = data.ctx.S[2][tmp2]; \
+ tmp3 = data.ctx.S[1][tmp3]; \
+ tmp3 += data.ctx.S[0][tmp4]; \
+ tmp3 ^= tmp2; \
+ R ^= data.ctx.P[N + 1]; \
+ tmp3 += tmp1; \
+ R ^= tmp3;
+#else
+/* Architectures with no complicated addressing modes supported */
+#define BF_INDEX(S, i) \
+ (*((BF_word *)(((unsigned char *)S) + (i))))
+#define BF_ROUND(L, R, N) \
+ tmp1 = L & 0xFF; \
+ tmp1 <<= 2; \
+ tmp2 = L >> 6; \
+ tmp2 &= 0x3FC; \
+ tmp3 = L >> 14; \
+ tmp3 &= 0x3FC; \
+ tmp4 = L >> 22; \
+ tmp4 &= 0x3FC; \
+ tmp1 = BF_INDEX(data.ctx.S[3], tmp1); \
+ tmp2 = BF_INDEX(data.ctx.S[2], tmp2); \
+ tmp3 = BF_INDEX(data.ctx.S[1], tmp3); \
+ tmp3 += BF_INDEX(data.ctx.S[0], tmp4); \
+ tmp3 ^= tmp2; \
+ R ^= data.ctx.P[N + 1]; \
+ tmp3 += tmp1; \
+ R ^= tmp3;
+#endif
+
+/*
+ * Encrypt one block, BF_N is hardcoded here.
+ */
+#define BF_ENCRYPT \
+ L ^= data.ctx.P[0]; \
+ BF_ROUND(L, R, 0); \
+ BF_ROUND(R, L, 1); \
+ BF_ROUND(L, R, 2); \
+ BF_ROUND(R, L, 3); \
+ BF_ROUND(L, R, 4); \
+ BF_ROUND(R, L, 5); \
+ BF_ROUND(L, R, 6); \
+ BF_ROUND(R, L, 7); \
+ BF_ROUND(L, R, 8); \
+ BF_ROUND(R, L, 9); \
+ BF_ROUND(L, R, 10); \
+ BF_ROUND(R, L, 11); \
+ BF_ROUND(L, R, 12); \
+ BF_ROUND(R, L, 13); \
+ BF_ROUND(L, R, 14); \
+ BF_ROUND(R, L, 15); \
+ tmp4 = R; \
+ R = L; \
+ L = tmp4 ^ data.ctx.P[BF_N + 1];
+
+#define BF_body() \
+ L = R = 0; \
+ ptr = data.ctx.P; \
+ do { \
+ ptr += 2; \
+ BF_ENCRYPT; \
+ *(ptr - 2) = L; \
+ *(ptr - 1) = R; \
+ } while (ptr < &data.ctx.P[BF_N + 2]); \
+\
+ ptr = data.ctx.S[0]; \
+ do { \
+ ptr += 2; \
+ BF_ENCRYPT; \
+ *(ptr - 2) = L; \
+ *(ptr - 1) = R; \
+ } while (ptr < &data.ctx.S[3][0xFF]);
+
+static void ATTR_UNSIGNED_WRAPS
+BF_set_key(const char *key, BF_key expanded, BF_key initial,
+ unsigned char flags)
+{
+ const char *ptr = key;
+ unsigned int bug, i, j;
+ BF_word safety, sign, diff, tmp[2];
+
+/*
+ * There was a sign extension bug in older revisions of this function. While
+ * we would have liked to simply fix the bug and move on, we have to provide
+ * a backwards compatibility feature (essentially the bug) for some systems and
+ * a safety measure for some others. The latter is needed because for certain
+ * multiple inputs to the buggy algorithm there exist easily found inputs to
+ * the correct algorithm that produce the same hash. Thus, we optionally
+ * deviate from the correct algorithm just enough to avoid such collisions.
+ * While the bug itself affected the majority of passwords containing
+ * characters with the 8th bit set (although only a percentage of those in a
+ * collision-producing way), the anti-collision safety measure affects
+ * only a subset of passwords containing the '\xff' character (not even all of
+ * those passwords, just some of them). This character is not found in valid
+ * UTF-8 sequences and is rarely used in popular 8-bit character encodings.
+ * Thus, the safety measure is unlikely to cause much annoyance, and is a
+ * reasonable tradeoff to use when authenticating against existing hashes that
+ * are not reliably known to have been computed with the correct algorithm.
+ *
+ * We use an approach that tries to minimize side-channel leaks of password
+ * information - that is, we mostly use fixed-cost bitwise operations instead
+ * of branches or table lookups. (One conditional branch based on password
+ * length remains. It is not part of the bug aftermath, though, and is
+ * difficult and possibly unreasonable to avoid given the use of C strings by
+ * the caller, which results in similar timing leaks anyway.)
+ *
+ * For actual implementation, we set an array index in the variable "bug"
+ * (0 means no bug, 1 means sign extension bug emulation) and a flag in the
+ * variable "safety" (bit 16 is set when the safety measure is requested).
+ * Valid combinations of settings are:
+ *
+ * Prefix "$2a$": bug = 0, safety = 0x10000
+ * Prefix "$2b$": bug = 0, safety = 0
+ * Prefix "$2x$": bug = 1, safety = 0
+ * Prefix "$2y$": bug = 0, safety = 0
+ */
+ bug = (unsigned int)flags & 1;
+ safety = ((BF_word)flags & 2) << 15;
+
+ sign = diff = 0;
+
+ for (i = 0; i < BF_N + 2; i++) {
+ tmp[0] = tmp[1] = 0;
+ for (j = 0; j < 4; j++) {
+ tmp[0] <<= 8;
+ tmp[0] |= (unsigned char)*ptr; /* correct */
+ tmp[1] <<= 8;
+ tmp[1] |= (BF_word)(signed char)*ptr; /* bug */
+/*
+ * Sign extension in the first char has no effect - nothing to overwrite yet,
+ * and those extra 24 bits will be fully shifted out of the 32-bit word. For
+ * chars 2, 3, 4 in each four-char block, we set bit 7 of "sign" if sign
+ * extension in tmp[1] occurs. Once this flag is set, it remains set.
+ */
+ if (j != 0)
+ sign |= tmp[1] & 0x80;
+ if (*ptr == '\0')
+ ptr = key;
+ else
+ ptr++;
+ }
+ diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */
+
+ expanded[i] = tmp[bug];
+ initial[i] = BF_init_state.P[i] ^ tmp[bug];
+ }
+
+/*
+ * At this point, "diff" is zero iff the correct and buggy algorithms produced
+ * exactly the same result. If so and if "sign" is non-zero, which indicates
+ * that there was a non-benign sign extension, this means that we have a
+ * collision between the correctly computed hash for this password and a set of
+ * passwords that could be supplied to the buggy algorithm. Our safety measure
+ * is meant to protect from such many-buggy to one-correct collisions, by
+ * deviating from the correct algorithm in such cases. Let's check for this.
+ */
+ diff |= diff >> 16; /* still zero iff exact match */
+ diff &= 0xffff; /* ditto */
+ diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */
+ sign <<= 9; /* move the non-benign sign extension flag to bit 16 */
+ sign &= ~diff & safety; /* action needed? */
+
+/*
+ * If we have determined that we need to deviate from the correct algorithm,
+ * flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but
+ * let's stick to it now. It came out of the approach we used above, and it's
+ * not any worse than any other choice we could make.)
+ *
+ * It is crucial that we don't do the same to the expanded key used in the main
+ * Eksblowfish loop. By doing it to only one of these two, we deviate from a
+ * state that could be directly specified by a password to the buggy algorithm
+ * (and to the fully correct one as well, but that's a side-effect).
+ */
+ initial[0] ^= sign;
+}
+
+static const unsigned char flags_by_subtype[26] =
+ {2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0};
+
+static char * ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+BF_crypt(const char *key, const char *setting,
+ char *output, size_t size,
+ BF_word min)
+{
+ struct {
+ BF_ctx ctx;
+ BF_key expanded_key;
+ union {
+ BF_word salt[4];
+ BF_word output[6];
+ } binary;
+ } data;
+ BF_word L, R;
+ BF_word tmp1, tmp2, tmp3, tmp4;
+ BF_word *ptr;
+ BF_word count;
+ int i;
+
+ if (size < 7 + 22 + 31 + 1) {
+ __set_errno(ERANGE);
+ return NULL;
+ }
+
+ if (setting[0] != '$' ||
+ setting[1] != '2' ||
+ setting[2] < 'a' || setting[2] > 'z' ||
+ flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a'] == 0 ||
+ setting[3] != '$' ||
+ setting[4] < '0' || setting[4] > '3' ||
+ setting[5] < '0' || setting[5] > '9' ||
+ (setting[4] == '3' && setting[5] > '1') ||
+ setting[6] != '$') {
+ __set_errno(EINVAL);
+ return NULL;
+ }
+
+ count = (BF_word)1 << ((setting[4] - '0') * 10 + (setting[5] - '0'));
+ if (count < min || BF_decode(data.binary.salt, &setting[7], 16) != 0) {
+ __set_errno(EINVAL);
+ return NULL;
+ }
+ BF_swap(data.binary.salt, 4);
+
+ BF_set_key(key, data.expanded_key, data.ctx.P,
+ flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a']);
+
+ memcpy(data.ctx.S, BF_init_state.S, sizeof(data.ctx.S));
+
+ L = R = 0;
+ for (i = 0; i < BF_N + 2; i += 2) {
+ L ^= data.binary.salt[i & 2];
+ R ^= data.binary.salt[(i & 2) + 1];
+ BF_ENCRYPT;
+ data.ctx.P[i] = L;
+ data.ctx.P[i + 1] = R;
+ }
+
+ ptr = data.ctx.S[0];
+ do {
+ ptr += 4;
+ L ^= data.binary.salt[(BF_N + 2) & 3];
+ R ^= data.binary.salt[(BF_N + 3) & 3];
+ BF_ENCRYPT;
+ *(ptr - 4) = L;
+ *(ptr - 3) = R;
+
+ L ^= data.binary.salt[(BF_N + 4) & 3];
+ R ^= data.binary.salt[(BF_N + 5) & 3];
+ BF_ENCRYPT;
+ *(ptr - 2) = L;
+ *(ptr - 1) = R;
+ } while (ptr < &data.ctx.S[3][0xFF]);
+
+ do {
+ bool done;
+
+ for (i = 0; i < BF_N + 2; i += 2) {
+ data.ctx.P[i] ^= data.expanded_key[i];
+ data.ctx.P[i + 1] ^= data.expanded_key[i + 1];
+ }
+
+ done = FALSE;
+ do {
+ BF_body();
+ if (done)
+ break;
+ done = TRUE;
+
+ tmp1 = data.binary.salt[0];
+ tmp2 = data.binary.salt[1];
+ tmp3 = data.binary.salt[2];
+ tmp4 = data.binary.salt[3];
+ for (i = 0; i < BF_N; i += 4) {
+ data.ctx.P[i] ^= tmp1;
+ data.ctx.P[i + 1] ^= tmp2;
+ data.ctx.P[i + 2] ^= tmp3;
+ data.ctx.P[i + 3] ^= tmp4;
+ }
+ data.ctx.P[16] ^= tmp1;
+ data.ctx.P[17] ^= tmp2;
+ } while (TRUE);
+ } while (--count > 0);
+
+ for (i = 0; i < 6; i += 2) {
+ L = BF_magic_w[i];
+ R = BF_magic_w[i + 1];
+
+ count = 64;
+ do {
+ BF_ENCRYPT;
+ } while (--count > 0);
+
+ data.binary.output[i] = L;
+ data.binary.output[i + 1] = R;
+ }
+
+ memcpy(output, setting, 7 + 22 - 1);
+ output[7 + 22 - 1] = BF_itoa64[(int)
+ BF_atoi64[(int)setting[7 + 22 - 1] - 0x20] & 0x30];
+
+/* This has to be bug-compatible with the original implementation, so
+ * only encode 23 of the 24 bytes. :-) */
+ BF_swap(data.binary.output, 6);
+ BF_encode(&output[7 + 22], data.binary.output, 23);
+ output[7 + 22 + 31] = '\0';
+
+ return output;
+}
+
+int crypt_output_magic(const char *setting, char *output, size_t size)
+{
+ if (size < 3)
+ return -1;
+
+ output[0] = '*';
+ output[1] = '0';
+ output[2] = '\0';
+
+ if (setting[0] == '*' && setting[1] == '0')
+ output[1] = '1';
+
+ return 0;
+}
+
+/*
+ * Please preserve the runtime self-test. It serves two purposes at once:
+ *
+ * 1. We really can't afford the risk of producing incompatible hashes e.g.
+ * when there's something like gcc bug 26587 again, whereas an application or
+ * library integrating this code might not also integrate our external tests or
+ * it might not run them after every build. Even if it does, the miscompile
+ * might only occur on the production build, but not on a testing build (such
+ * as because of different optimization settings). It is painful to recover
+ * from incorrectly-computed hashes - merely fixing whatever broke is not
+ * enough. Thus, a proactive measure like this self-test is needed.
+ *
+ * 2. We don't want to leave sensitive data from our actual password hash
+ * computation on the stack or in registers. Previous revisions of the code
+ * would do explicit cleanups, but simply running the self-test after hash
+ * computation is more reliable.
+ *
+ * The performance cost of this quick self-test is around 0.6% at the "$2a$08"
+ * setting.
+ */
+char *crypt_blowfish_rn(const char *key, const char *setting,
+ char *output, size_t size)
+{
+ const char *test_key = "8b \xd0\xc1\xd2\xcf\xcc\xd8";
+ const char *test_setting = "$2a$00$abcdefghijklmnopqrstuu";
+ static const char * const test_hashes[2] =
+ {"i1D709vfamulimlGcq0qq3UvuUasvEa\0\x55", /* 'a', 'b', 'y' */
+ "VUrPmXD6q/nVSSp7pNDhCR9071IfIRe\0\x55"}; /* 'x' */
+ const char *test_hash = test_hashes[0];
+ char *retval;
+ const char *p;
+ int save_errno;
+ bool ok;
+ struct {
+ char s[7 + 22 + 1];
+ char o[7 + 22 + 31 + 1 + 1 + 1];
+ } buf;
+
+/* Hash the supplied password */
+ crypt_output_magic(setting, output, size);
+ retval = BF_crypt(key, setting, output, size, 16);
+ save_errno = errno;
+
+/*
+ * Do a quick self-test. It is important that we make both calls to BF_crypt()
+ * from the same scope such that they likely use the same stack locations,
+ * which makes the second call overwrite the first call's sensitive data on the
+ * stack and makes it more likely that any alignment related issues would be
+ * detected by the self-test.
+ */
+ memcpy(buf.s, test_setting, sizeof(buf.s));
+ if (retval != NULL) {
+ unsigned int flags = flags_by_subtype[
+ (unsigned int)(unsigned char)setting[2] - 'a'];
+ test_hash = test_hashes[flags & 1];
+ buf.s[2] = setting[2];
+ }
+ memset(buf.o, 0x55, sizeof(buf.o));
+ buf.o[sizeof(buf.o) - 1] = 0;
+ p = BF_crypt(test_key, buf.s, buf.o, sizeof(buf.o) - (1 + 1), 1);
+
+ ok = (p == buf.o &&
+ memcmp(p, buf.s, 7 + 22) == 0 &&
+ memcmp(p + (7 + 22), test_hash, 31 + 1 + 1 + 1) == 0);
+
+ {
+ const char *k = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345";
+ BF_key ae, ai, ye, yi;
+ BF_set_key(k, ae, ai, 2); /* $2a$ */
+ BF_set_key(k, ye, yi, 4); /* $2y$ */
+ ai[0] ^= 0x10000; /* undo the safety (for comparison) */
+ ok = ok && ai[0] == 0xdb9c59bc && ye[17] == 0x33343500 &&
+ memcmp(ae, ye, sizeof(ae)) == 0 &&
+ memcmp(ai, yi, sizeof(ai)) == 0;
+ }
+
+ __set_errno(save_errno);
+ if (ok)
+ return retval;
+
+ i_unreached();
+}
+
+char *crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count,
+ const char *input, size_t size, char *output, size_t output_size)
+{
+ if (size < 16 || output_size < 7 + 22 + 1 ||
+ (count > 0 && (count < 4 || count > 31)) ||
+ prefix[0] != '$' || prefix[1] != '2' ||
+ (prefix[2] != 'a' && prefix[2] != 'b' && prefix[2] != 'y')) {
+ if (output_size > 0) output[0] = '\0';
+ __set_errno((output_size < 7 + 22 + 1) ? ERANGE : EINVAL);
+ return NULL;
+ }
+
+ if (count == 0) count = 5;
+
+ output[0] = '$';
+ output[1] = '2';
+ output[2] = prefix[2];
+ output[3] = '$';
+ output[4] = '0' + count / 10;
+ output[5] = '0' + count % 10;
+ output[6] = '$';
+
+ BF_encode(&output[7], (const BF_word *)input, 16);
+ output[7 + 22] = '\0';
+
+ return output;
+}
diff --git a/src/auth/crypt-blowfish.h b/src/auth/crypt-blowfish.h
new file mode 100644
index 0000000..e7123b3
--- /dev/null
+++ b/src/auth/crypt-blowfish.h
@@ -0,0 +1,29 @@
+/*
+ * Written by Solar Designer <solar at openwall.com> in 2000-2011.
+ * No copyright is claimed, and the software is hereby placed in the public
+ * domain. In case this attempt to disclaim copyright and place the software
+ * in the public domain is deemed null and void, then the software is
+ * Copyright (c) 2000-2011 Solar Designer and it is hereby released to the
+ * general public under the following terms:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted.
+ *
+ * There's ABSOLUTELY NO WARRANTY, express or implied.
+ *
+ * See crypt_blowfish.c for more information.
+ *
+ * 2017-10-10 - Adapted for dovecot code by Aki Tuomi <aki.tuomi@dovecot.fi>
+ */
+
+#ifndef CRYPT_BLOWFISH_H
+#define CRYPT_BLOWFISH_H
+
+extern int crypt_output_magic(const char *setting, char *output, size_t size);
+extern char *crypt_blowfish_rn(const char *key, const char *setting,
+ char *output, size_t size);
+extern char *crypt_gensalt_blowfish_rn(const char *prefix,
+ unsigned long count,
+ const char *input, size_t size, char *output, size_t output_size);
+
+#endif
diff --git a/src/auth/db-checkpassword.c b/src/auth/db-checkpassword.c
new file mode 100644
index 0000000..7d52eef
--- /dev/null
+++ b/src/auth/db-checkpassword.c
@@ -0,0 +1,563 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#if defined(PASSDB_CHECKPASSWORD) || defined(USERDB_CHECKPASSWORD)
+
+#include "lib-signals.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "execv-const.h"
+#include "env-util.h"
+#include "safe-memset.h"
+#include "strescape.h"
+#include "child-wait.h"
+#include "db-checkpassword.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+
+#define CHECKPASSWORD_MAX_REQUEST_LEN 512
+
+struct chkpw_auth_request {
+ struct db_checkpassword *db;
+ struct auth_request *request;
+ char *auth_password;
+
+ db_checkpassword_callback_t *callback;
+ void (*request_callback)();
+
+ pid_t pid;
+ int fd_out, fd_in;
+ struct io *io_out, *io_in;
+
+ string_t *input_buf;
+ size_t output_pos, output_len;
+
+ int exit_status;
+ bool exited:1;
+};
+
+struct db_checkpassword {
+ char *checkpassword_path, *checkpassword_reply_path;
+
+ HASH_TABLE(void *, struct chkpw_auth_request *) clients;
+ struct child_wait *child_wait;
+};
+
+static void
+env_put_extra_fields(const ARRAY_TYPE(auth_field) *extra_fields)
+{
+ const struct auth_field *field;
+ const char *key, *value;
+
+ array_foreach(extra_fields, field) {
+ key = t_str_ucase(field->key);
+ value = field->value != NULL ? field->value : "1";
+ env_put(key, value);
+ }
+}
+
+static void checkpassword_request_close(struct chkpw_auth_request *request)
+{
+ io_remove(&request->io_in);
+ io_remove(&request->io_out);
+
+ i_close_fd(&request->fd_in);
+ i_close_fd(&request->fd_out);
+}
+
+static void checkpassword_request_free(struct chkpw_auth_request **_request)
+{
+ struct chkpw_auth_request *request = *_request;
+
+ *_request = NULL;
+
+ if (!request->exited) {
+ hash_table_remove(request->db->clients,
+ POINTER_CAST(request->pid));
+ child_wait_remove_pid(request->db->child_wait, request->pid);
+ }
+ checkpassword_request_close(request);
+
+ if (request->auth_password != NULL) {
+ safe_memset(request->auth_password, 0,
+ strlen(request->auth_password));
+ i_free(request->auth_password);
+ }
+ auth_request_unref(&request->request);
+ str_free(&request->input_buf);
+ i_free(request);
+}
+
+static void checkpassword_finish(struct chkpw_auth_request **_request,
+ enum db_checkpassword_status status)
+{
+ struct chkpw_auth_request *request = *_request;
+ const char *const *extra_fields;
+
+ *_request = NULL;
+
+ extra_fields = t_strsplit_tabescaped(str_c(request->input_buf));
+ request->callback(request->request, status, extra_fields,
+ request->request_callback);
+ checkpassword_request_free(&request);
+}
+
+static void checkpassword_internal_failure(struct chkpw_auth_request **request)
+{
+ checkpassword_finish(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE);
+}
+
+static void
+checkpassword_request_finish_auth(struct chkpw_auth_request *request)
+{
+ switch (request->exit_status) {
+ /* standard checkpassword exit codes: */
+ case 1:
+ e_info(authdb_event(request->request),
+ "Login failed (status=%d)",
+ request->exit_status);
+ checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE);
+ break;
+ case 0:
+ if (request->input_buf->used == 0) {
+ e_error(authdb_event(request->request),
+ "Received no input");
+ checkpassword_internal_failure(&request);
+ break;
+ }
+ checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK);
+ break;
+ case 2:
+ /* checkpassword is called with wrong parameters? unlikely */
+ e_error(authdb_event(request->request),
+ "Child %s exited with status 2 (tried to use "
+ "userdb-only checkpassword program for passdb?)",
+ dec2str(request->pid));
+ checkpassword_internal_failure(&request);
+ break;
+ case 111:
+ /* temporary problem, treat as internal error */
+ default:
+ /* whatever error.. */
+ e_error(authdb_event(request->request),
+ "Child %s exited with status %d",
+ dec2str(request->pid), request->exit_status);
+ checkpassword_internal_failure(&request);
+ break;
+ }
+}
+
+static void
+checkpassword_request_finish_lookup(struct chkpw_auth_request *request)
+{
+ switch (request->exit_status) {
+ case 3:
+ /* User does not exist. */
+ e_info(authdb_event(request->request),
+ "User unknown");
+ checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE);
+ break;
+ case 2:
+ /* This is intentionally not 0. checkpassword-reply exits with
+ 2 on success when AUTHORIZED is set. */
+ if (request->input_buf->used == 0) {
+ e_error(authdb_event(request->request),
+ "Received no input");
+ checkpassword_internal_failure(&request);
+ break;
+ }
+ checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK);
+ break;
+ default:
+ /* whatever error... */
+ e_error(authdb_event(request->request),
+ "Child %s exited with status %d",
+ dec2str(request->pid), request->exit_status);
+ checkpassword_internal_failure(&request);
+ break;
+ }
+}
+
+static void
+checkpassword_request_half_finish(struct chkpw_auth_request *request)
+{
+ /* the process must have exited, and the input fd must have closed */
+ if (!request->exited || request->fd_in != -1)
+ return;
+
+ if (request->auth_password != NULL)
+ checkpassword_request_finish_auth(request);
+ else
+ checkpassword_request_finish_lookup(request);
+}
+
+static void env_put_auth_vars(struct auth_request *request)
+{
+ const struct var_expand_table *tab;
+ unsigned int i;
+
+ tab = auth_request_get_var_expand_table(request, NULL);
+ for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) {
+ /* avoid keeping passwords in environment .. just in case
+ an attacker might find it from there. environment is no
+ longer world-readable in modern OSes, but maybe the attacker
+ could be running with the same UID. of course then the
+ attacker could usually ptrace() the process, except that is
+ disabled on some secured systems. so, although I find it
+ highly unlikely anyone could actually attack Dovecot this
+ way in a real system, be safe just in case. besides, lets
+ try to keep at least minimally compatible with the
+ checkpassword API. */
+ if (tab[i].long_key != NULL && tab[i].value != NULL &&
+ strcasecmp(tab[i].long_key, "password") != 0) {
+ env_put(t_strdup_printf("AUTH_%s",
+ t_str_ucase(tab[i].long_key)),
+ tab[i].value);
+ }
+ }
+}
+
+static void checkpassword_setup_env(struct auth_request *request)
+{
+ const struct auth_request_fields *fields = &request->fields;
+
+ /* Besides passing the standard username and password in a
+ pipe, also pass some other possibly interesting information
+ via environment. Use UCSPI names for local/remote IPs. */
+ env_put("PROTO", "TCP"); /* UCSPI */
+ env_put("ORIG_UID", dec2str(getuid()));
+ env_put("SERVICE", fields->service);
+ if (fields->local_ip.family != 0) {
+ env_put("TCPLOCALIP", net_ip2addr(&fields->local_ip));
+ /* FIXME: for backwards compatibility only,
+ remove some day */
+ env_put("LOCAL_IP", net_ip2addr(&fields->local_ip));
+ }
+ if (fields->remote_ip.family != 0) {
+ env_put("TCPREMOTEIP", net_ip2addr(&fields->remote_ip));
+ /* FIXME: for backwards compatibility only,
+ remove some day */
+ env_put("REMOTE_IP", net_ip2addr(&fields->remote_ip));
+ }
+ if (fields->local_port != 0)
+ env_put("TCPLOCALPORT", dec2str(fields->local_port));
+ if (fields->remote_port != 0)
+ env_put("TCPREMOTEPORT", dec2str(fields->remote_port));
+ if (fields->master_user != NULL)
+ env_put("MASTER_USER", fields->master_user);
+ if (!auth_fields_is_empty(fields->extra_fields)) {
+ const ARRAY_TYPE(auth_field) *extra_fields =
+ auth_fields_export(fields->extra_fields);
+
+ /* extra fields could come from master db */
+ env_put_extra_fields(extra_fields);
+ }
+ env_put_auth_vars(request);
+}
+
+static const char *
+checkpassword_get_cmd(struct auth_request *request, const char *args,
+ const char *checkpassword_reply_path)
+{
+ string_t *str;
+ const char *error;
+
+ str = t_str_new(256);
+ if (auth_request_var_expand(str, args, request, NULL, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand checkpassword_path=%s: %s",
+ args, error);
+ }
+
+ return t_strconcat(str_c(str), " ", checkpassword_reply_path, NULL);
+}
+
+static void checkpassword_child_input(struct chkpw_auth_request *request)
+{
+ unsigned char buf[1024];
+ ssize_t ret;
+
+ ret = read(request->fd_in, buf, sizeof(buf));
+ if (ret > 0) {
+ str_append_data(request->input_buf, buf, ret);
+ return;
+ }
+
+ if (ret < 0) {
+ e_error(authdb_event(request->request),
+ "read() failed: %m");
+ checkpassword_internal_failure(&request);
+ } else if (memchr(str_data(request->input_buf), '\0',
+ str_len(request->input_buf)) != NULL) {
+ e_error(authdb_event(request->request),
+ "NUL characters in checkpassword reply");
+ checkpassword_internal_failure(&request);
+ } else if (strchr(str_c(request->input_buf), '\n') != NULL) {
+ e_error(authdb_event(request->request),
+ "LF characters in checkpassword reply");
+ checkpassword_internal_failure(&request);
+ } else {
+ e_debug(authdb_event(request->request),
+ "Received input: %s", str_c(request->input_buf));
+ checkpassword_request_close(request);
+ checkpassword_request_half_finish(request);
+ }
+}
+
+static void checkpassword_child_output(struct chkpw_auth_request *request)
+{
+ /* Send: username \0 password \0 timestamp \0.
+ Must be 512 bytes or less. The "timestamp" parameter is actually
+ useful only for APOP authentication. We don't support it, so
+ keep it empty */
+ struct auth_request *auth_request = request->request;
+ buffer_t *buf;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ buf = t_buffer_create(CHECKPASSWORD_MAX_REQUEST_LEN);
+ buffer_append(buf, auth_request->fields.user,
+ strlen(auth_request->fields.user)+1);
+ if (request->auth_password != NULL) {
+ buffer_append(buf, request->auth_password,
+ strlen(request->auth_password)+1);
+ } else {
+ buffer_append_c(buf, '\0');
+ }
+ buffer_append_c(buf, '\0');
+ data = buffer_get_data(buf, &size);
+
+ i_assert(size == request->output_len);
+ /* already checked this */
+ i_assert(size <= CHECKPASSWORD_MAX_REQUEST_LEN);
+
+ ret = write(request->fd_out, data + request->output_pos,
+ size - request->output_pos);
+ if (ret <= 0) {
+ if (ret < 0) {
+ e_error(authdb_event(request->request),
+ "write() failed: %m");
+ } else {
+ e_error(authdb_event(request->request),
+ "write() returned 0");
+ }
+ checkpassword_internal_failure(&request);
+ return;
+ }
+
+ request->output_pos += ret;
+ if (request->output_pos < size)
+ return;
+
+ /* finished sending the data */
+ io_remove(&request->io_out);
+
+ if (close(request->fd_out) < 0)
+ e_error(authdb_event(request->request), "close() failed: %m");
+ request->fd_out = -1;
+}
+
+static void ATTR_NORETURN
+checkpassword_exec(struct db_checkpassword *db, struct auth_request *request,
+ int fd_in, int fd_out, bool authenticate)
+{
+ const char *cmd, *const *args;
+
+ /* fd 3 is used to send the username+password for the script
+ fd 4 is used to communicate with checkpassword-reply */
+ if (dup2(fd_out, 3) < 0 || dup2(fd_in, 4) < 0) {
+ e_error(authdb_event(request),
+ "dup2() failed: %m");
+ lib_exit(111);
+ }
+
+ if (!authenticate) {
+ /* We want to retrieve passdb/userdb data and don't do
+ authorization, so we need to signalize the
+ checkpassword program that the password shall be
+ ignored by setting AUTHORIZED. This needs a
+ special checkpassword program which knows how to
+ handle this. */
+ env_put("AUTHORIZED", "1");
+ if (request->wanted_credentials_scheme != NULL) {
+ /* passdb credentials lookup */
+ env_put("CREDENTIALS_LOOKUP", "1");
+ env_put("SCHEME", request->wanted_credentials_scheme);
+ }
+ }
+ checkpassword_setup_env(request);
+ cmd = checkpassword_get_cmd(request, db->checkpassword_path,
+ db->checkpassword_reply_path);
+ e_debug(authdb_event(request), "execute: %s", cmd);
+
+ /* very simple argument splitting. */
+ args = t_strsplit(cmd, " ");
+ execv_const(args[0], args);
+}
+
+static void sigchld_handler(const struct child_wait_status *status,
+ struct db_checkpassword *db)
+{
+ struct chkpw_auth_request *request =
+ hash_table_lookup(db->clients, POINTER_CAST(status->pid));
+
+ i_assert(request != NULL);
+
+ hash_table_remove(db->clients, POINTER_CAST(status->pid));
+ request->exited = TRUE;
+
+ if (WIFSIGNALED(status->status)) {
+ e_error(authdb_event(request->request),
+ "Child %s died with signal %d",
+ dec2str(status->pid), WTERMSIG(status->status));
+ checkpassword_internal_failure(&request);
+ } else if (WIFEXITED(status->status)) {
+ request->exit_status = WEXITSTATUS(status->status);
+
+ e_debug(authdb_event(request->request),
+ "exit_status=%d", request->exit_status);
+ checkpassword_request_half_finish(request);
+ } else {
+ /* shouldn't happen */
+ e_debug(authdb_event(request->request),
+ "Child %s exited with status=%d",
+ dec2str(status->pid), status->status);
+ checkpassword_internal_failure(&request);
+ }
+}
+
+void db_checkpassword_call(struct db_checkpassword *db,
+ struct auth_request *request,
+ const char *auth_password,
+ db_checkpassword_callback_t *callback,
+ void (*request_callback)())
+{
+ struct chkpw_auth_request *chkpw_auth_request;
+ size_t output_len;
+ int fd_in[2], fd_out[2];
+ pid_t pid;
+
+ /* <username> \0 <password> \0 timestamp \0 */
+ output_len = strlen(request->fields.user) + 3;
+ if (auth_password != NULL)
+ output_len += strlen(auth_password);
+ if (output_len > CHECKPASSWORD_MAX_REQUEST_LEN) {
+ e_info(authdb_event(request),
+ "Username+password combination too long (%zu bytes)",
+ output_len);
+ callback(request, DB_CHECKPASSWORD_STATUS_FAILURE,
+ NULL, request_callback);
+ return;
+ }
+
+ fd_in[0] = -1;
+ if (pipe(fd_in) < 0 || pipe(fd_out) < 0) {
+ e_error(authdb_event(request),
+ "pipe() failed: %m");
+ if (fd_in[0] != -1) {
+ i_close_fd(&fd_in[0]);
+ i_close_fd(&fd_in[1]);
+ }
+ callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE,
+ NULL, request_callback);
+ return;
+ }
+
+ pid = fork();
+ if (pid == -1) {
+ e_error(authdb_event(request),
+ "fork() failed: %m");
+ i_close_fd(&fd_in[0]);
+ i_close_fd(&fd_in[1]);
+ i_close_fd(&fd_out[0]);
+ i_close_fd(&fd_out[1]);
+ callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE,
+ NULL, request_callback);
+ return;
+ }
+
+ if (pid == 0) {
+ /* child */
+ i_close_fd(&fd_in[0]);
+ i_close_fd(&fd_out[1]);
+ checkpassword_exec(db, request, fd_in[1], fd_out[0],
+ auth_password != NULL);
+ /* not reached */
+ }
+
+ if (close(fd_in[1]) < 0) {
+ e_error(authdb_event(request),
+ "close(fd_in[1]) failed: %m");
+ }
+ if (close(fd_out[0]) < 0) {
+ e_error(authdb_event(request),
+ "close(fd_out[0]) failed: %m");
+ }
+
+ auth_request_ref(request);
+ chkpw_auth_request = i_new(struct chkpw_auth_request, 1);
+ chkpw_auth_request->db = db;
+ chkpw_auth_request->pid = pid;
+ chkpw_auth_request->fd_in = fd_in[0];
+ chkpw_auth_request->fd_out = fd_out[1];
+ chkpw_auth_request->auth_password = i_strdup(auth_password);
+ chkpw_auth_request->request = request;
+ chkpw_auth_request->output_len = output_len;
+ chkpw_auth_request->input_buf = str_new(default_pool, 256);
+ chkpw_auth_request->callback = callback;
+ chkpw_auth_request->request_callback = request_callback;
+
+ chkpw_auth_request->io_in =
+ io_add(fd_in[0], IO_READ, checkpassword_child_input,
+ chkpw_auth_request);
+ chkpw_auth_request->io_out =
+ io_add(fd_out[1], IO_WRITE, checkpassword_child_output,
+ chkpw_auth_request);
+
+ hash_table_insert(db->clients, POINTER_CAST(pid), chkpw_auth_request);
+ child_wait_add_pid(db->child_wait, pid);
+}
+
+struct db_checkpassword *
+db_checkpassword_init(const char *checkpassword_path,
+ const char *checkpassword_reply_path)
+{
+ struct db_checkpassword *db;
+
+ db = i_new(struct db_checkpassword, 1);
+ db->checkpassword_path = i_strdup(checkpassword_path);
+ db->checkpassword_reply_path = i_strdup(checkpassword_reply_path);
+ hash_table_create_direct(&db->clients, default_pool, 0);
+ db->child_wait =
+ child_wait_new_with_pid((pid_t)-1, sigchld_handler, db);
+ return db;
+}
+
+void db_checkpassword_deinit(struct db_checkpassword **_db)
+{
+ struct db_checkpassword *db = *_db;
+ struct hash_iterate_context *iter;
+ void *key;
+ struct chkpw_auth_request *request;
+
+ *_db = NULL;
+
+ iter = hash_table_iterate_init(db->clients);
+ while (hash_table_iterate(iter, db->clients, &key, &request))
+ checkpassword_internal_failure(&request);
+ hash_table_iterate_deinit(&iter);
+
+ child_wait_free(&db->child_wait);
+ hash_table_destroy(&db->clients);
+ i_free(db->checkpassword_reply_path);
+ i_free(db->checkpassword_path);
+ i_free(db);
+}
+
+#endif
diff --git a/src/auth/db-checkpassword.h b/src/auth/db-checkpassword.h
new file mode 100644
index 0000000..64eedd8
--- /dev/null
+++ b/src/auth/db-checkpassword.h
@@ -0,0 +1,29 @@
+#ifndef CHECKPASSWORD_COMMON_H
+#define CHECKPASSWORD_COMMON_H
+
+#include "auth-request.h"
+
+enum db_checkpassword_status {
+ DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE = -1,
+ /* auth unsuccessful / user not found */
+ DB_CHECKPASSWORD_STATUS_FAILURE = 0,
+ DB_CHECKPASSWORD_STATUS_OK = 1
+};
+
+typedef void db_checkpassword_callback_t(struct auth_request *request,
+ enum db_checkpassword_status status,
+ const char *const *extra_fields,
+ void (*request_callback)());
+
+struct db_checkpassword *
+db_checkpassword_init(const char *checkpassword_path,
+ const char *checkpassword_reply_path);
+void db_checkpassword_deinit(struct db_checkpassword **db);
+
+void db_checkpassword_call(struct db_checkpassword *db,
+ struct auth_request *request,
+ const char *auth_password,
+ db_checkpassword_callback_t *callback,
+ void (*request_callback)()) ATTR_NULL(3);
+
+#endif
diff --git a/src/auth/db-dict-cache-key.c b/src/auth/db-dict-cache-key.c
new file mode 100644
index 0000000..a220d8d
--- /dev/null
+++ b/src/auth/db-dict-cache-key.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "db-dict.h"
+
+const struct db_dict_key *
+db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name)
+{
+ const struct db_dict_key *key;
+
+ array_foreach(keys, key) {
+ if (strcmp(key->name, name) == 0)
+ return key;
+ }
+ return NULL;
+}
+
+
+const char *
+db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys,
+ const ARRAY_TYPE(db_dict_field) *fields,
+ const ARRAY_TYPE(db_dict_key_p) *objects)
+{
+ const struct db_dict_field *field;
+ const struct db_dict_key *key;
+ const char *p, *name;
+ unsigned int idx, size;
+ string_t *str = t_str_new(128);
+
+ array_foreach(fields, field) {
+ for (p = field->value; *p != '\0'; ) {
+ if (*p != '%') {
+ p++;
+ continue;
+ }
+
+ var_get_key_range(++p, &idx, &size);
+ if (size == 0) {
+ /* broken %variable ending too early */
+ break;
+ }
+ p += idx;
+ if (size > 5 && memcmp(p, "dict:", 5) == 0) {
+ name = t_strcut(t_strndup(p+5, size-5), ':');
+ key = db_dict_set_key_find(keys, name);
+ if (key != NULL)
+ str_printfa(str, "\t%s", key->key);
+ } else if (size == 1) {
+ str_printfa(str, "\t%%%c", p[0]);
+ } else {
+ str_append(str, "\t%{");
+ str_append_data(str, p, size);
+ str_append_c(str, '}');
+ }
+ p += size;
+ }
+ }
+ array_foreach_elem(objects, key)
+ str_printfa(str, "\t%s", key->key);
+ return str_c(str);
+}
diff --git a/src/auth/db-dict.c b/src/auth/db-dict.c
new file mode 100644
index 0000000..96b1a63
--- /dev/null
+++ b/src/auth/db-dict.c
@@ -0,0 +1,654 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#include "array.h"
+#include "istream.h"
+#include "str.h"
+#include "json-parser.h"
+#include "settings.h"
+#include "dict.h"
+#include "auth-request.h"
+#include "auth-worker-client.h"
+#include "db-dict.h"
+
+#include <stddef.h>
+
+enum dict_settings_section {
+ DICT_SETTINGS_SECTION_ROOT = 0,
+ DICT_SETTINGS_SECTION_KEY,
+ DICT_SETTINGS_SECTION_PASSDB,
+ DICT_SETTINGS_SECTION_USERDB
+};
+
+struct dict_settings_parser_ctx {
+ struct dict_connection *conn;
+ enum dict_settings_section section;
+ struct db_dict_key *cur_key;
+};
+
+struct db_dict_iter_key {
+ const struct db_dict_key *key;
+ bool used;
+ const char *value;
+};
+
+struct db_dict_value_iter {
+ pool_t pool;
+ struct auth_request *auth_request;
+ struct dict_connection *conn;
+ const struct var_expand_table *var_expand_table;
+ ARRAY(struct db_dict_iter_key) keys;
+
+ const ARRAY_TYPE(db_dict_field) *fields;
+ const ARRAY_TYPE(db_dict_key_p) *objects;
+ unsigned int field_idx;
+ unsigned int object_idx;
+
+ struct json_parser *json_parser;
+ string_t *tmpstr;
+ const char *error;
+};
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, db_dict_settings)
+static struct setting_def setting_defs[] = {
+ DEF_STR(uri),
+ DEF_STR(default_pass_scheme),
+ DEF_STR(iterate_prefix),
+ DEF_BOOL(iterate_disable),
+
+ DEF_STR(passdb_objects),
+ DEF_STR(userdb_objects),
+ { 0, NULL, 0 }
+};
+
+static struct db_dict_settings default_dict_settings = {
+ .uri = NULL,
+ .default_pass_scheme = "MD5",
+ .iterate_prefix = "",
+ .iterate_disable = FALSE,
+ .passdb_objects = "",
+ .userdb_objects = ""
+};
+
+#undef DEF_STR
+#define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_key)
+static struct setting_def key_setting_defs[] = {
+ DEF_STR(name),
+ DEF_STR(key),
+ DEF_STR(format),
+ DEF_STR(default_value),
+
+ { 0, NULL, 0 }
+};
+
+static struct db_dict_key default_key_settings = {
+ .name = NULL,
+ .key = "",
+ .format = "value",
+ .default_value = NULL
+};
+
+static struct dict_connection *connections = NULL;
+
+static struct dict_connection *dict_conn_find(const char *config_path)
+{
+ struct dict_connection *conn;
+
+ for (conn = connections; conn != NULL; conn = conn->next) {
+ if (strcmp(conn->config_path, config_path) == 0)
+ return conn;
+ }
+
+ return NULL;
+}
+
+static bool
+parse_obsolete_setting(const char *key, const char *value,
+ struct dict_settings_parser_ctx *ctx,
+ const char **error_r)
+{
+ const struct db_dict_key *dbkey;
+
+ if (strcmp(key, "password_key") == 0) {
+ /* key passdb { key=<value> format=json }
+ passdb_objects = passdb */
+ ctx->cur_key = array_append_space(&ctx->conn->set.keys);
+ *ctx->cur_key = default_key_settings;
+ ctx->cur_key->name = "passdb";
+ ctx->cur_key->format = "json";
+ ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON;
+ ctx->cur_key->key = p_strdup(ctx->conn->pool, value);
+
+ dbkey = ctx->cur_key;
+ array_push_back(&ctx->conn->set.parsed_passdb_objects, &dbkey);
+ return TRUE;
+ }
+ if (strcmp(key, "user_key") == 0) {
+ /* key userdb { key=<value> format=json }
+ userdb_objects = userdb */
+ ctx->cur_key = array_append_space(&ctx->conn->set.keys);
+ *ctx->cur_key = default_key_settings;
+ ctx->cur_key->name = "userdb";
+ ctx->cur_key->format = "json";
+ ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON;
+ ctx->cur_key->key = p_strdup(ctx->conn->pool, value);
+
+ dbkey = ctx->cur_key;
+ array_push_back(&ctx->conn->set.parsed_userdb_objects, &dbkey);
+ return TRUE;
+ }
+ if (strcmp(key, "value_format") == 0) {
+ if (strcmp(value, "json") == 0)
+ return TRUE;
+ *error_r = "Deprecated value_format must be 'json'";
+ return FALSE;
+ }
+ return FALSE;
+}
+
+static const char *parse_setting(const char *key, const char *value,
+ struct dict_settings_parser_ctx *ctx)
+{
+ struct db_dict_field *field;
+ const char *error = NULL;
+
+ switch (ctx->section) {
+ case DICT_SETTINGS_SECTION_ROOT:
+ if (parse_obsolete_setting(key, value, ctx, &error))
+ return NULL;
+ if (error != NULL)
+ return error;
+ return parse_setting_from_defs(ctx->conn->pool, setting_defs,
+ &ctx->conn->set, key, value);
+ case DICT_SETTINGS_SECTION_KEY:
+ return parse_setting_from_defs(ctx->conn->pool, key_setting_defs,
+ ctx->cur_key, key, value);
+ case DICT_SETTINGS_SECTION_PASSDB:
+ field = array_append_space(&ctx->conn->set.passdb_fields);
+ field->name = p_strdup(ctx->conn->pool, key);
+ field->value = p_strdup(ctx->conn->pool, value);
+ return NULL;
+ case DICT_SETTINGS_SECTION_USERDB:
+ field = array_append_space(&ctx->conn->set.userdb_fields);
+ field->name = p_strdup(ctx->conn->pool, key);
+ field->value = p_strdup(ctx->conn->pool, value);
+ return NULL;
+ }
+ i_unreached();
+}
+
+static bool parse_section(const char *type, const char *name,
+ struct dict_settings_parser_ctx *ctx,
+ const char **errormsg)
+{
+ if (type == NULL) {
+ ctx->section = DICT_SETTINGS_SECTION_ROOT;
+ if (ctx->cur_key != NULL) {
+ if (strcmp(ctx->cur_key->format, "value") == 0) {
+ ctx->cur_key->parsed_format =
+ DB_DICT_VALUE_FORMAT_VALUE;
+ } else if (strcmp(ctx->cur_key->format, "json") == 0) {
+ ctx->cur_key->parsed_format =
+ DB_DICT_VALUE_FORMAT_JSON;
+ } else {
+ *errormsg = t_strconcat("Unknown key format: ",
+ ctx->cur_key->format, NULL);
+ return FALSE;
+ }
+ }
+ ctx->cur_key = NULL;
+ return TRUE;
+ }
+ if (ctx->section != DICT_SETTINGS_SECTION_ROOT) {
+ *errormsg = "Nested sections not supported";
+ return FALSE;
+ }
+ if (strcmp(type, "key") == 0) {
+ if (name == NULL) {
+ *errormsg = "Key section is missing name";
+ return FALSE;
+ }
+ if (strchr(name, '.') != NULL) {
+ *errormsg = "Key section names must not contain '.'";
+ return FALSE;
+ }
+ ctx->section = DICT_SETTINGS_SECTION_KEY;
+ ctx->cur_key = array_append_space(&ctx->conn->set.keys);
+ *ctx->cur_key = default_key_settings;
+ ctx->cur_key->name = p_strdup(ctx->conn->pool, name);
+ return TRUE;
+ }
+ if (strcmp(type, "passdb_fields") == 0) {
+ ctx->section = DICT_SETTINGS_SECTION_PASSDB;
+ return TRUE;
+ }
+ if (strcmp(type, "userdb_fields") == 0) {
+ ctx->section = DICT_SETTINGS_SECTION_USERDB;
+ return TRUE;
+ }
+ *errormsg = "Unknown section";
+ return FALSE;
+}
+
+static void
+db_dict_settings_parse(struct db_dict_settings *set)
+{
+ const struct db_dict_key *key;
+ const char *const *tmp;
+
+ tmp = t_strsplit_spaces(set->passdb_objects, " ");
+ for (; *tmp != NULL; tmp++) {
+ key = db_dict_set_key_find(&set->keys, *tmp);
+ if (key == NULL) {
+ i_fatal("dict: passdb_objects refers to key %s, "
+ "which doesn't exist", *tmp);
+ }
+ if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) {
+ i_fatal("dict: passdb_objects refers to key %s, "
+ "but it's in value-only format", *tmp);
+ }
+ array_push_back(&set->parsed_passdb_objects, &key);
+ }
+
+ tmp = t_strsplit_spaces(set->userdb_objects, " ");
+ for (; *tmp != NULL; tmp++) {
+ key = db_dict_set_key_find(&set->keys, *tmp);
+ if (key == NULL) {
+ i_fatal("dict: userdb_objects refers to key %s, "
+ "which doesn't exist", *tmp);
+ }
+ if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) {
+ i_fatal("dict: userdb_objects refers to key %s, "
+ "but it's in value-only format", *tmp);
+ }
+ array_push_back(&set->parsed_userdb_objects, &key);
+ }
+}
+
+struct dict_connection *db_dict_init(const char *config_path)
+{
+ struct dict_settings dict_set;
+ struct dict_settings_parser_ctx ctx;
+ struct dict_connection *conn;
+ const char *error;
+ pool_t pool;
+
+ conn = dict_conn_find(config_path);
+ if (conn != NULL) {
+ conn->refcount++;
+ return conn;
+ }
+
+ if (*config_path == '\0')
+ i_fatal("dict: Configuration file path not given");
+
+ pool = pool_alloconly_create("dict_connection", 1024);
+ conn = p_new(pool, struct dict_connection, 1);
+ conn->pool = pool;
+
+ conn->refcount = 1;
+
+ conn->config_path = p_strdup(pool, config_path);
+ conn->set = default_dict_settings;
+ p_array_init(&conn->set.keys, pool, 8);
+ p_array_init(&conn->set.passdb_fields, pool, 8);
+ p_array_init(&conn->set.userdb_fields, pool, 8);
+ p_array_init(&conn->set.parsed_passdb_objects, pool, 2);
+ p_array_init(&conn->set.parsed_userdb_objects, pool, 2);
+
+ i_zero(&ctx);
+ ctx.conn = conn;
+ if (!settings_read(config_path, NULL, parse_setting,
+ parse_section, &ctx, &error))
+ i_fatal("dict %s: %s", config_path, error);
+ db_dict_settings_parse(&conn->set);
+
+ if (conn->set.uri == NULL)
+ i_fatal("dict %s: Empty uri setting", config_path);
+
+ i_zero(&dict_set);
+ dict_set.base_dir = global_auth_settings->base_dir;
+ dict_set.event_parent = auth_event;
+ if (dict_init(conn->set.uri, &dict_set, &conn->dict, &error) < 0)
+ i_fatal("dict %s: Failed to init dict: %s", config_path, error);
+
+ conn->next = connections;
+ connections = conn;
+ return conn;
+}
+
+void db_dict_unref(struct dict_connection **_conn)
+{
+ struct dict_connection *conn = *_conn;
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ dict_deinit(&conn->dict);
+ pool_unref(&conn->pool);
+}
+
+static struct db_dict_iter_key *
+db_dict_iter_find_key(struct db_dict_value_iter *iter, const char *name)
+{
+ struct db_dict_iter_key *key;
+
+ array_foreach_modifiable(&iter->keys, key) {
+ if (strcmp(key->key->name, name) == 0)
+ return key;
+ }
+ return NULL;
+}
+
+static void db_dict_iter_find_used_keys(struct db_dict_value_iter *iter)
+{
+ const struct db_dict_field *field;
+ struct db_dict_iter_key *key;
+ const char *p, *name;
+ unsigned int idx, size;
+
+ array_foreach(iter->fields, field) {
+ for (p = field->value; *p != '\0'; ) {
+ if (*p != '%') {
+ p++;
+ continue;
+ }
+
+ var_get_key_range(++p, &idx, &size);
+ if (size == 0) {
+ /* broken %variable ending too early */
+ break;
+ }
+ p += idx;
+ if (size > 5 && memcmp(p, "dict:", 5) == 0) {
+ name = t_strcut(t_strndup(p+5, size-5), ':');
+ key = db_dict_iter_find_key(iter, name);
+ if (key != NULL)
+ key->used = TRUE;
+ }
+ p += size;
+ }
+ }
+}
+
+static void db_dict_iter_find_used_objects(struct db_dict_value_iter *iter)
+{
+ const struct db_dict_key *dict_key;
+ struct db_dict_iter_key *key;
+
+ array_foreach_elem(iter->objects, dict_key) {
+ key = db_dict_iter_find_key(iter, dict_key->name);
+ i_assert(key != NULL); /* checked at init */
+ i_assert(key->key->parsed_format != DB_DICT_VALUE_FORMAT_VALUE);
+ key->used = TRUE;
+ }
+}
+
+static int
+db_dict_iter_key_cmp(const struct db_dict_iter_key *k1,
+ const struct db_dict_iter_key *k2)
+{
+ return null_strcmp(k1->key->default_value, k2->key->default_value);
+}
+
+static int db_dict_iter_lookup_key_values(struct db_dict_value_iter *iter)
+{
+ struct db_dict_iter_key *key;
+ string_t *path;
+ const char *error;
+ int ret;
+
+ /* sort the keys so that we'll first lookup the keys without
+ default value. if their lookup fails, the user doesn't exist. */
+ array_sort(&iter->keys, db_dict_iter_key_cmp);
+
+ path = t_str_new(128);
+ str_append(path, DICT_PATH_SHARED);
+
+ struct dict_op_settings set = {
+ .username = iter->auth_request->fields.user,
+ };
+
+ array_foreach_modifiable(&iter->keys, key) {
+ if (!key->used)
+ continue;
+
+ str_truncate(path, strlen(DICT_PATH_SHARED));
+ str_append(path, key->key->key);
+ ret = dict_lookup(iter->conn->dict, &set, iter->pool,
+ str_c(path), &key->value, &error);
+ if (ret > 0) {
+ e_debug(authdb_event(iter->auth_request),
+ "Lookup: %s = %s", str_c(path),
+ key->value);
+ } else if (ret < 0) {
+ e_error(authdb_event(iter->auth_request),
+ "Failed to lookup key %s: %s", str_c(path), error);
+ return -1;
+ } else if (key->key->default_value != NULL) {
+ e_debug(authdb_event(iter->auth_request),
+ "Lookup: %s not found, using default value %s",
+ str_c(path), key->key->default_value);
+ key->value = key->key->default_value;
+ } else {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+int db_dict_value_iter_init(struct dict_connection *conn,
+ struct auth_request *auth_request,
+ const ARRAY_TYPE(db_dict_field) *fields,
+ const ARRAY_TYPE(db_dict_key_p) *objects,
+ struct db_dict_value_iter **iter_r)
+{
+ struct db_dict_value_iter *iter;
+ struct db_dict_iter_key *iterkey;
+ const struct db_dict_key *key;
+ pool_t pool;
+ int ret;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"auth dict lookup", 1024);
+ iter = p_new(pool, struct db_dict_value_iter, 1);
+ iter->pool = pool;
+ iter->conn = conn;
+ iter->fields = fields;
+ iter->objects = objects;
+ iter->tmpstr = str_new(pool, 128);
+ iter->auth_request = auth_request;
+ iter->var_expand_table = auth_request_get_var_expand_table(auth_request, NULL);
+
+ /* figure out what keys we need to lookup, and lookup them */
+ p_array_init(&iter->keys, pool, array_count(&conn->set.keys));
+ array_foreach(&conn->set.keys, key) {
+ iterkey = array_append_space(&iter->keys);
+ struct db_dict_key *new_key = p_new(iter->pool, struct db_dict_key, 1);
+ memcpy(new_key, key, sizeof(struct db_dict_key));
+ string_t *expanded_key = str_new(iter->pool, strlen(key->key));
+ const char *error;
+ if (auth_request_var_expand_with_table(expanded_key, key->key, auth_request,
+ iter->var_expand_table,
+ NULL, &error) <= 0) {
+ e_error(authdb_event(iter->auth_request),
+ "Failed to expand key %s: %s", key->key, error);
+ pool_unref(&pool);
+ return -1;
+ }
+ new_key->key = str_c(expanded_key);
+ iterkey->key = new_key;
+ }
+ T_BEGIN {
+ db_dict_iter_find_used_keys(iter);
+ db_dict_iter_find_used_objects(iter);
+ ret = db_dict_iter_lookup_key_values(iter);
+ } T_END;
+ if (ret <= 0) {
+ pool_unref(&pool);
+ return ret;
+ }
+ *iter_r = iter;
+ return 1;
+}
+
+static bool
+db_dict_value_iter_json_next(struct db_dict_value_iter *iter,
+ string_t *tmpstr,
+ const char **key_r, const char **value_r)
+{
+ enum json_type type;
+ const char *value;
+
+ if (json_parse_next(iter->json_parser, &type, &value) < 0)
+ return FALSE;
+ if (type != JSON_TYPE_OBJECT_KEY) {
+ iter->error = "Object expected";
+ return FALSE;
+ }
+ if (*value == '\0') {
+ iter->error = "Empty object key";
+ return FALSE;
+ }
+ str_truncate(tmpstr, 0);
+ str_append(tmpstr, value);
+
+ if (json_parse_next(iter->json_parser, &type, &value) < 0) {
+ iter->error = "Missing value";
+ return FALSE;
+ }
+ if (type == JSON_TYPE_OBJECT) {
+ iter->error = "Nested objects not supported";
+ return FALSE;
+ }
+ *key_r = str_c(tmpstr);
+ *value_r = value;
+ return TRUE;
+}
+
+static void
+db_dict_value_iter_json_init(struct db_dict_value_iter *iter, const char *data)
+{
+ struct istream *input;
+
+ i_assert(iter->json_parser == NULL);
+
+ input = i_stream_create_from_data(data, strlen(data));
+ iter->json_parser = json_parser_init(input);
+ i_stream_unref(&input);
+}
+
+static bool
+db_dict_value_iter_object_next(struct db_dict_value_iter *iter,
+ const char **key_r, const char **value_r)
+{
+ const struct db_dict_key *dict_key;
+ struct db_dict_iter_key *key;
+
+ if (iter->json_parser != NULL)
+ return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r);
+ if (iter->object_idx == array_count(iter->objects))
+ return FALSE;
+
+ dict_key = array_idx_elem(iter->objects, iter->object_idx);
+ key = db_dict_iter_find_key(iter, dict_key->name);
+ i_assert(key != NULL); /* checked at init */
+
+ switch (key->key->parsed_format) {
+ case DB_DICT_VALUE_FORMAT_VALUE:
+ i_unreached();
+ case DB_DICT_VALUE_FORMAT_JSON:
+ db_dict_value_iter_json_init(iter, key->value);
+ return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r);
+ }
+ i_unreached();
+}
+
+static int
+db_dict_field_find(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct db_dict_value_iter *iter = context;
+ struct db_dict_iter_key *key;
+ const char *name, *value, *dotname = strchr(data, '.');
+ string_t *tmpstr;
+
+ *value_r = NULL;
+
+ if (dotname != NULL)
+ data = t_strdup_until(data, dotname++);
+ key = db_dict_iter_find_key(iter, data);
+ if (key == NULL)
+ return 1;
+
+ switch (key->key->parsed_format) {
+ case DB_DICT_VALUE_FORMAT_VALUE:
+ *value_r = dotname != NULL ? NULL :
+ (key->value == NULL ? "" : key->value);
+ return 1;
+ case DB_DICT_VALUE_FORMAT_JSON:
+ if (dotname == NULL)
+ return 1;
+ db_dict_value_iter_json_init(iter, key->value);
+ *value_r = "";
+ tmpstr = t_str_new(64);
+ while (db_dict_value_iter_json_next(iter, tmpstr, &name, &value)) {
+ if (strcmp(name, dotname) == 0) {
+ *value_r = t_strdup(value);
+ break;
+ }
+ }
+ (void)json_parser_deinit(&iter->json_parser, &iter->error);
+ return 1;
+ }
+ i_unreached();
+}
+
+bool db_dict_value_iter_next(struct db_dict_value_iter *iter,
+ const char **key_r, const char **value_r)
+{
+ static struct var_expand_func_table var_funcs_table[] = {
+ { "dict", db_dict_field_find },
+ { NULL, NULL }
+ };
+ const struct db_dict_field *field;
+ const char *error;
+
+ if (iter->field_idx == array_count(iter->fields))
+ return db_dict_value_iter_object_next(iter, key_r, value_r);
+ field = array_idx(iter->fields, iter->field_idx++);
+
+ str_truncate(iter->tmpstr, 0);
+ if (var_expand_with_funcs(iter->tmpstr, field->value,
+ iter->var_expand_table, var_funcs_table,
+ iter, &error) <= 0) {
+ iter->error = p_strdup_printf(iter->pool,
+ "Failed to expand %s=%s: %s",
+ field->name, field->value, error);
+ return FALSE;
+ }
+ *key_r = field->name;
+ *value_r = str_c(iter->tmpstr);
+ return TRUE;
+}
+
+int db_dict_value_iter_deinit(struct db_dict_value_iter **_iter,
+ const char **error_r)
+{
+ struct db_dict_value_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ *error_r = iter->error;
+ if (iter->json_parser != NULL) {
+ if (json_parser_deinit(&iter->json_parser, &iter->error) < 0 &&
+ *error_r == NULL)
+ *error_r = iter->error;
+ }
+
+ pool_unref(&iter->pool);
+ return *error_r != NULL ? -1 : 0;
+}
diff --git a/src/auth/db-dict.h b/src/auth/db-dict.h
new file mode 100644
index 0000000..f4feb0a
--- /dev/null
+++ b/src/auth/db-dict.h
@@ -0,0 +1,82 @@
+#ifndef DB_DICT_H
+#define DB_DICT_H
+
+#include "sql-api.h"
+
+struct auth_request;
+struct db_dict_value_iter;
+
+enum db_dict_value_format {
+ DB_DICT_VALUE_FORMAT_VALUE = 0,
+ DB_DICT_VALUE_FORMAT_JSON
+};
+
+struct db_dict_key {
+ const char *name;
+ const char *key;
+ const char *format;
+ const char *default_value;
+
+ enum db_dict_value_format parsed_format;
+};
+ARRAY_DEFINE_TYPE(db_dict_key, struct db_dict_key);
+ARRAY_DEFINE_TYPE(db_dict_key_p, const struct db_dict_key *);
+
+struct db_dict_field {
+ const char *name;
+ const char *value;
+};
+ARRAY_DEFINE_TYPE(db_dict_field, struct db_dict_field);
+
+struct db_dict_settings {
+ const char *uri;
+ const char *default_pass_scheme;
+ const char *iterate_prefix;
+ bool iterate_disable;
+
+ ARRAY_TYPE(db_dict_key) keys;
+
+ const char *passdb_objects;
+ const char *userdb_objects;
+ ARRAY_TYPE(db_dict_field) passdb_fields;
+ ARRAY_TYPE(db_dict_field) userdb_fields;
+
+ ARRAY_TYPE(db_dict_key_p) parsed_passdb_objects;
+ ARRAY_TYPE(db_dict_key_p) parsed_userdb_objects;
+};
+
+struct dict_connection {
+ struct dict_connection *next;
+
+ pool_t pool;
+ int refcount;
+
+ char *config_path;
+ struct db_dict_settings set;
+ struct dict *dict;
+};
+
+struct dict_connection *db_dict_init(const char *config_path);
+void db_dict_unref(struct dict_connection **conn);
+
+/* Returns 1 if ok, 0 if a key without default_value wasn't returned
+ ("user doesn't exist"), -1 if internal error */
+int db_dict_value_iter_init(struct dict_connection *conn,
+ struct auth_request *auth_request,
+ const ARRAY_TYPE(db_dict_field) *fields,
+ const ARRAY_TYPE(db_dict_key_p) *objects,
+ struct db_dict_value_iter **iter_r);
+bool db_dict_value_iter_next(struct db_dict_value_iter *iter,
+ const char **key_r, const char **value_r);
+int db_dict_value_iter_deinit(struct db_dict_value_iter **iter,
+ const char **error_r);
+
+const char *db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys,
+ const ARRAY_TYPE(db_dict_field) *fields,
+ const ARRAY_TYPE(db_dict_key_p) *objects);
+
+/* private: */
+const struct db_dict_key *
+db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name);
+
+#endif
diff --git a/src/auth/db-ldap.c b/src/auth/db-ldap.c
new file mode 100644
index 0000000..8083108
--- /dev/null
+++ b/src/auth/db-ldap.c
@@ -0,0 +1,2035 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#if defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)
+
+#include "net.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hash.h"
+#include "aqueue.h"
+#include "str.h"
+#include "time-util.h"
+#include "env-util.h"
+#include "var-expand.h"
+#include "settings.h"
+#include "userdb.h"
+#include "db-ldap.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+#define HAVE_LDAP_SASL
+#ifdef HAVE_SASL_SASL_H
+# include <sasl/sasl.h>
+#elif defined (HAVE_SASL_H)
+# include <sasl.h>
+#else
+# undef HAVE_LDAP_SASL
+#endif
+#ifdef LDAP_OPT_X_TLS
+# define OPENLDAP_TLS_OPTIONS
+#endif
+#if !defined(SASL_VERSION_MAJOR) || SASL_VERSION_MAJOR < 2
+# undef HAVE_LDAP_SASL
+#endif
+
+#ifndef LDAP_SASL_QUIET
+# define LDAP_SASL_QUIET 0 /* Doesn't exist in Solaris LDAP */
+#endif
+
+/* Older versions may require calling ldap_result() twice */
+#if LDAP_VENDOR_VERSION <= 20112
+# define OPENLDAP_ASYNC_WORKAROUND
+#endif
+
+/* Solaris LDAP library doesn't have LDAP_OPT_SUCCESS */
+#ifndef LDAP_OPT_SUCCESS
+# define LDAP_OPT_SUCCESS LDAP_SUCCESS
+#endif
+
+#define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3
+
+static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= ";
+
+struct db_ldap_result {
+ int refcount;
+ LDAPMessage *msg;
+};
+
+struct db_ldap_value {
+ const char **values;
+ bool used;
+};
+
+struct db_ldap_result_iterate_context {
+ pool_t pool;
+
+ struct ldap_request *ldap_request;
+ const ARRAY_TYPE(ldap_field) *attr_map;
+ unsigned int attr_idx;
+
+ /* attribute name => value */
+ HASH_TABLE(char *, struct db_ldap_value *) ldap_attrs;
+
+ const char *val_1_arr[2];
+ string_t *var, *debug;
+
+ bool skip_null_values;
+ bool iter_dn_values;
+ LDAPMessage *ldap_msg;
+ LDAP *ld;
+};
+
+struct db_ldap_sasl_bind_context {
+ const char *authcid;
+ const char *passwd;
+ const char *realm;
+ const char *authzid;
+};
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, ldap_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, ldap_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, ldap_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(hosts),
+ DEF_STR(uris),
+ DEF_STR(dn),
+ DEF_STR(dnpass),
+ DEF_BOOL(auth_bind),
+ DEF_STR(auth_bind_userdn),
+ DEF_BOOL(tls),
+ DEF_BOOL(sasl_bind),
+ DEF_STR(sasl_mech),
+ DEF_STR(sasl_realm),
+ DEF_STR(sasl_authz_id),
+ DEF_STR(tls_ca_cert_file),
+ DEF_STR(tls_ca_cert_dir),
+ DEF_STR(tls_cert_file),
+ DEF_STR(tls_key_file),
+ DEF_STR(tls_cipher_suite),
+ DEF_STR(tls_require_cert),
+ DEF_STR(deref),
+ DEF_STR(scope),
+ DEF_STR(base),
+ DEF_INT(ldap_version),
+ DEF_STR(debug_level),
+ DEF_STR(ldaprc_path),
+ DEF_STR(user_attrs),
+ DEF_STR(user_filter),
+ DEF_STR(pass_attrs),
+ DEF_STR(pass_filter),
+ DEF_STR(iterate_attrs),
+ DEF_STR(iterate_filter),
+ DEF_STR(default_pass_scheme),
+ DEF_BOOL(userdb_warning_disable),
+ DEF_BOOL(blocking),
+
+ { 0, NULL, 0 }
+};
+
+static struct ldap_settings default_ldap_settings = {
+ .hosts = NULL,
+ .uris = NULL,
+ .dn = NULL,
+ .dnpass = NULL,
+ .auth_bind = FALSE,
+ .auth_bind_userdn = NULL,
+ .tls = FALSE,
+ .sasl_bind = FALSE,
+ .sasl_mech = NULL,
+ .sasl_realm = NULL,
+ .sasl_authz_id = NULL,
+ .tls_ca_cert_file = NULL,
+ .tls_ca_cert_dir = NULL,
+ .tls_cert_file = NULL,
+ .tls_key_file = NULL,
+ .tls_cipher_suite = NULL,
+ .tls_require_cert = NULL,
+ .deref = "never",
+ .scope = "subtree",
+ .base = NULL,
+ .ldap_version = 3,
+ .debug_level = "0",
+ .ldaprc_path = "",
+ .user_attrs = "homeDirectory=home,uidNumber=uid,gidNumber=gid",
+ .user_filter = "(&(objectClass=posixAccount)(uid=%u))",
+ .pass_attrs = "uid=user,userPassword=password",
+ .pass_filter = "(&(objectClass=posixAccount)(uid=%u))",
+ .iterate_attrs = "uid=user",
+ .iterate_filter = "(objectClass=posixAccount)",
+ .default_pass_scheme = "crypt",
+ .userdb_warning_disable = FALSE,
+ .blocking = FALSE
+};
+
+static struct ldap_connection *ldap_connections = NULL;
+
+static int db_ldap_bind(struct ldap_connection *conn);
+static void db_ldap_conn_close(struct ldap_connection *conn);
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init_full(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values,
+ bool iter_dn_values);
+static bool db_ldap_abort_requests(struct ldap_connection *conn,
+ unsigned int max_count,
+ unsigned int timeout_secs,
+ bool error, const char *reason);
+static void db_ldap_request_free(struct ldap_request *request);
+
+static int deref2str(const char *str, int *ref_r)
+{
+ if (strcasecmp(str, "never") == 0)
+ *ref_r = LDAP_DEREF_NEVER;
+ else if (strcasecmp(str, "searching") == 0)
+ *ref_r = LDAP_DEREF_SEARCHING;
+ else if (strcasecmp(str, "finding") == 0)
+ *ref_r = LDAP_DEREF_FINDING;
+ else if (strcasecmp(str, "always") == 0)
+ *ref_r = LDAP_DEREF_ALWAYS;
+ else
+ return -1;
+ return 0;
+}
+
+static int scope2str(const char *str, int *scope_r)
+{
+ if (strcasecmp(str, "base") == 0)
+ *scope_r = LDAP_SCOPE_BASE;
+ else if (strcasecmp(str, "onelevel") == 0)
+ *scope_r = LDAP_SCOPE_ONELEVEL;
+ else if (strcasecmp(str, "subtree") == 0)
+ *scope_r = LDAP_SCOPE_SUBTREE;
+ else
+ return -1;
+ return 0;
+}
+
+#ifdef OPENLDAP_TLS_OPTIONS
+static int tls_require_cert2str(const char *str, int *value_r)
+{
+ if (strcasecmp(str, "never") == 0)
+ *value_r = LDAP_OPT_X_TLS_NEVER;
+ else if (strcasecmp(str, "hard") == 0)
+ *value_r = LDAP_OPT_X_TLS_HARD;
+ else if (strcasecmp(str, "demand") == 0)
+ *value_r = LDAP_OPT_X_TLS_DEMAND;
+ else if (strcasecmp(str, "allow") == 0)
+ *value_r = LDAP_OPT_X_TLS_ALLOW;
+ else if (strcasecmp(str, "try") == 0)
+ *value_r = LDAP_OPT_X_TLS_TRY;
+ else
+ return -1;
+ return 0;
+}
+#endif
+
+static int ldap_get_errno(struct ldap_connection *conn)
+{
+ int ret, err;
+
+ ret = ldap_get_option(conn->ld, LDAP_OPT_ERROR_NUMBER, (void *) &err);
+ if (ret != LDAP_SUCCESS) {
+ e_error(conn->event, "Can't get error number: %s",
+ ldap_err2string(ret));
+ return LDAP_UNAVAILABLE;
+ }
+
+ return err;
+}
+
+const char *ldap_get_error(struct ldap_connection *conn)
+{
+ const char *ret;
+ char *str = NULL;
+
+ ret = ldap_err2string(ldap_get_errno(conn));
+
+ ldap_get_option(conn->ld, LDAP_OPT_ERROR_STRING, (void *)&str);
+ if (str != NULL) {
+ ret = t_strconcat(ret, ", ", str, NULL);
+ ldap_memfree(str);
+ }
+ ldap_set_option(conn->ld, LDAP_OPT_ERROR_STRING, NULL);
+ return ret;
+}
+
+static void ldap_conn_reconnect(struct ldap_connection *conn)
+{
+ db_ldap_conn_close(conn);
+ if (db_ldap_connect(conn) < 0)
+ db_ldap_conn_close(conn);
+}
+
+static int ldap_handle_error(struct ldap_connection *conn)
+{
+ int err = ldap_get_errno(conn);
+
+ switch (err) {
+ case LDAP_SUCCESS:
+ i_unreached();
+ 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:
+ /* invalid input */
+ return -1;
+ case LDAP_SERVER_DOWN:
+ case LDAP_TIMEOUT:
+ case LDAP_UNAVAILABLE:
+ case LDAP_BUSY:
+#ifdef LDAP_CONNECT_ERROR
+ case LDAP_CONNECT_ERROR:
+#endif
+ case LDAP_LOCAL_ERROR:
+ case LDAP_INVALID_CREDENTIALS:
+ case LDAP_OPERATIONS_ERROR:
+ default:
+ /* connection problems */
+ ldap_conn_reconnect(conn);
+ return 0;
+ }
+}
+
+static int db_ldap_request_bind(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ struct ldap_request_bind *brequest =
+ (struct ldap_request_bind *)request;
+
+ i_assert(request->type == LDAP_REQUEST_TYPE_BIND);
+ i_assert(request->msgid == -1);
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_AUTH ||
+ conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT);
+ i_assert(conn->pending_count == 0);
+
+ request->msgid = ldap_bind(conn->ld, brequest->dn,
+ request->auth_request->mech_password,
+ LDAP_AUTH_SIMPLE);
+ if (request->msgid == -1) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_bind(%s) failed: %s",
+ brequest->dn, ldap_get_error(conn));
+ if (ldap_handle_error(conn) < 0) {
+ /* broken request, remove it */
+ return 0;
+ }
+ return -1;
+ }
+ conn->conn_state = LDAP_CONN_STATE_BINDING;
+ return 1;
+}
+
+static int db_ldap_request_search(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT);
+ i_assert(request->msgid == -1);
+
+ request->msgid =
+ ldap_search(conn->ld, *srequest->base == '\0' ? NULL :
+ srequest->base, conn->set.ldap_scope,
+ srequest->filter, srequest->attributes, 0);
+ if (request->msgid == -1) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(%s) parsing failed: %s",
+ srequest->filter, ldap_get_error(conn));
+ if (ldap_handle_error(conn) < 0) {
+ /* broken request, remove it */
+ return 0;
+ }
+ return -1;
+ }
+ return 1;
+}
+
+static bool db_ldap_request_queue_next(struct ldap_connection *conn)
+{
+ struct ldap_request *request;
+ int ret = -1;
+
+ /* connecting may call db_ldap_connect_finish(), which gets us back
+ here. so do the connection before checking the request queue. */
+ if (db_ldap_connect(conn) < 0)
+ return FALSE;
+
+ if (conn->pending_count == aqueue_count(conn->request_queue)) {
+ /* no non-pending requests */
+ return FALSE;
+ }
+ if (conn->pending_count > DB_LDAP_MAX_PENDING_REQUESTS) {
+ /* wait until server has replied to some requests */
+ return FALSE;
+ }
+
+ request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue,
+ conn->pending_count));
+
+ if (conn->pending_count > 0 &&
+ request->type == LDAP_REQUEST_TYPE_BIND) {
+ /* we can't do binds until all existing requests are finished */
+ return FALSE;
+ }
+
+ switch (conn->conn_state) {
+ case LDAP_CONN_STATE_DISCONNECTED:
+ case LDAP_CONN_STATE_BINDING:
+ /* wait until we're in bound state */
+ return FALSE;
+ case LDAP_CONN_STATE_BOUND_AUTH:
+ if (request->type == LDAP_REQUEST_TYPE_BIND)
+ break;
+
+ /* bind to default dn first */
+ i_assert(conn->pending_count == 0);
+ (void)db_ldap_bind(conn);
+ return FALSE;
+ case LDAP_CONN_STATE_BOUND_DEFAULT:
+ /* we can do anything in this state */
+ break;
+ }
+
+ if (request->send_count >= DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT) {
+ /* Enough many times retried. Server just keeps disconnecting
+ whenever attempting to send the request. */
+ ret = 0;
+ } else {
+ /* clear away any partial results saved before reconnecting */
+ db_ldap_request_free(request);
+
+ switch (request->type) {
+ case LDAP_REQUEST_TYPE_BIND:
+ ret = db_ldap_request_bind(conn, request);
+ break;
+ case LDAP_REQUEST_TYPE_SEARCH:
+ ret = db_ldap_request_search(conn, request);
+ break;
+ }
+ }
+
+ if (ret > 0) {
+ /* success */
+ i_assert(request->msgid != -1);
+ request->send_count++;
+ conn->pending_count++;
+ return TRUE;
+ } else if (ret < 0) {
+ /* disconnected */
+ return FALSE;
+ } else {
+ /* broken request, remove from queue */
+ aqueue_delete_tail(conn->request_queue);
+ request->callback(conn, request, NULL);
+ return TRUE;
+ }
+}
+
+static void
+db_ldap_check_hanging(struct ldap_connection *conn)
+{
+ struct ldap_request *first_request;
+ unsigned int count;
+ time_t secs_diff;
+
+ count = aqueue_count(conn->request_queue);
+ if (count == 0)
+ return;
+
+ first_request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, 0));
+ secs_diff = ioloop_time - first_request->create_time;
+ if (secs_diff > DB_LDAP_REQUEST_LOST_TIMEOUT_SECS) {
+ db_ldap_abort_requests(conn, UINT_MAX, 0, TRUE,
+ "LDAP connection appears to be hanging");
+ ldap_conn_reconnect(conn);
+ }
+}
+
+void db_ldap_request(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ i_assert(request->auth_request != NULL);
+
+ request->msgid = -1;
+ request->create_time = ioloop_time;
+
+ db_ldap_check_hanging(conn);
+
+ aqueue_append(conn->request_queue, &request);
+ (void)db_ldap_request_queue_next(conn);
+}
+
+static int db_ldap_connect_finish(struct ldap_connection *conn, int ret)
+{
+ if (ret == LDAP_SERVER_DOWN) {
+ e_error(conn->event, "Can't connect to server: %s",
+ conn->set.uris != NULL ?
+ conn->set.uris : conn->set.hosts);
+ return -1;
+ }
+ if (ret != LDAP_SUCCESS) {
+ e_error(conn->event, "binding failed (dn %s): %s",
+ conn->set.dn == NULL ? "(none)" : conn->set.dn,
+ ldap_get_error(conn));
+ return -1;
+ }
+
+ timeout_remove(&conn->to);
+ conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT;
+ while (db_ldap_request_queue_next(conn))
+ ;
+ return 0;
+}
+
+static void db_ldap_default_bind_finished(struct ldap_connection *conn,
+ struct db_ldap_result *res)
+{
+ int ret;
+
+ i_assert(conn->pending_count == 0);
+ conn->default_bind_msgid = -1;
+
+ ret = ldap_result2error(conn->ld, res->msg, FALSE);
+ if (db_ldap_connect_finish(conn, ret) < 0) {
+ /* lost connection, close it */
+ db_ldap_conn_close(conn);
+ }
+}
+
+static bool db_ldap_abort_requests(struct ldap_connection *conn,
+ unsigned int max_count,
+ unsigned int timeout_secs,
+ bool error, const char *reason)
+{
+ struct ldap_request *request;
+ time_t diff;
+ bool aborts = FALSE;
+
+ while (aqueue_count(conn->request_queue) > 0 && max_count > 0) {
+ request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, 0));
+
+ diff = ioloop_time - request->create_time;
+ if (diff < (time_t)timeout_secs)
+ break;
+
+ /* timed out, abort */
+ aqueue_delete_tail(conn->request_queue);
+
+ if (request->msgid != -1) {
+ i_assert(conn->pending_count > 0);
+ conn->pending_count--;
+ }
+ if (error) {
+ e_error(authdb_event(request->auth_request),
+ "%s", reason);
+ } else {
+ e_info(authdb_event(request->auth_request),
+ "%s", reason);
+ }
+ request->callback(conn, request, NULL);
+ max_count--;
+ aborts = TRUE;
+ }
+ return aborts;
+}
+
+static struct ldap_request *
+db_ldap_find_request(struct ldap_connection *conn, int msgid,
+ unsigned int *idx_r)
+{
+ struct ldap_request *const *requests, *request = NULL;
+ unsigned int i, count;
+
+ count = aqueue_count(conn->request_queue);
+ if (count == 0)
+ return NULL;
+
+ requests = array_front(&conn->request_array);
+ for (i = 0; i < count; i++) {
+ request = requests[aqueue_idx(conn->request_queue, i)];
+ if (request->msgid == msgid) {
+ *idx_r = i;
+ return request;
+ }
+ if (request->msgid == -1)
+ break;
+ }
+ return NULL;
+}
+
+static int db_ldap_fields_get_dn(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct auth_request *auth_request = request->request.auth_request;
+ struct ldap_request_named_result *named_res;
+ struct db_ldap_result_iterate_context *ldap_iter;
+ const char *name, *const *values;
+
+ ldap_iter = db_ldap_result_iterate_init_full(conn, request, res->msg,
+ TRUE, TRUE);
+ while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) {
+ if (values[1] != NULL) {
+ e_warning(authdb_event(auth_request),
+ "Multiple values found for '%s', "
+ "using value '%s'", name, values[0]);
+ }
+ array_foreach_modifiable(&request->named_results, named_res) {
+ if (strcmp(named_res->field->name, name) != 0)
+ continue;
+ /* In future we could also support LDAP URLs here */
+ named_res->dn = p_strdup(auth_request->pool,
+ values[0]);
+ }
+ }
+ db_ldap_result_iterate_deinit(&ldap_iter);
+ return 0;
+}
+
+struct ldap_field_find_subquery_context {
+ ARRAY_TYPE(string) attr_names;
+ const char *name;
+};
+
+static int
+db_ldap_field_subquery_find(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct ldap_field_find_subquery_context *ctx = context;
+ char *ldap_attr;
+ const char *p;
+
+ if (*data != '\0') {
+ data = t_strcut(data, ':');
+ p = strchr(data, '@');
+ if (p != NULL && strcmp(p+1, ctx->name) == 0) {
+ ldap_attr = p_strdup_until(unsafe_data_stack_pool,
+ data, p);
+ array_push_back(&ctx->attr_names, &ldap_attr);
+ }
+ }
+ *value_r = NULL;
+ return 1;
+}
+
+static int
+ldap_request_send_subquery(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct ldap_request_named_result *named_res)
+{
+ const struct ldap_field *field;
+ const char *p, *error;
+ char *name;
+ struct auth_request *auth_request = request->request.auth_request;
+ struct ldap_field_find_subquery_context ctx;
+ const struct var_expand_table *table =
+ auth_request_get_var_expand_table(auth_request, NULL);
+ const struct var_expand_func_table *ptr;
+ struct var_expand_func_table *ftable;
+ string_t *tmp_str = t_str_new(64);
+ ARRAY(struct var_expand_func_table) var_funcs_table;
+ t_array_init(&var_funcs_table, 8);
+
+ for(ptr = auth_request_var_funcs_table; ptr->key != NULL; ptr++) {
+ array_push_back(&var_funcs_table, ptr);
+ }
+ ftable = array_append_space(&var_funcs_table);
+ ftable->key = "ldap";
+ ftable->func = db_ldap_field_subquery_find;
+ ftable = array_append_space(&var_funcs_table);
+ ftable->key = "ldap_ptr";
+ ftable->func = db_ldap_field_subquery_find;
+ array_append_zero(&var_funcs_table);
+
+ i_zero(&ctx);
+ t_array_init(&ctx.attr_names, 8);
+ ctx.name = named_res->field->name;
+
+ /* get the attributes names into array (ldapAttr@name -> ldapAttr) */
+ array_foreach(request->attr_map, field) {
+ if (field->ldap_attr_name[0] == '\0') {
+ str_truncate(tmp_str, 0);
+ if (var_expand_with_funcs(tmp_str, field->value, table,
+ array_front(&var_funcs_table), &ctx, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand subquery %s: %s",
+ field->value, error);
+ return -1;
+ }
+ } else {
+ p = strchr(field->ldap_attr_name, '@');
+ if (p != NULL &&
+ strcmp(p+1, named_res->field->name) == 0) {
+ name = p_strdup_until(unsafe_data_stack_pool,
+ field->ldap_attr_name, p);
+ array_push_back(&ctx.attr_names, &name);
+ }
+ }
+ }
+ array_append_zero(&ctx.attr_names);
+
+ request->request.msgid =
+ ldap_search(conn->ld, named_res->dn, LDAP_SCOPE_BASE,
+ NULL, array_front_modifiable(&ctx.attr_names), 0);
+ if (request->request.msgid == -1) {
+ e_error(authdb_event(auth_request),
+ "ldap_search(dn=%s) failed: %s",
+ named_res->dn, ldap_get_error(conn));
+ return -1;
+ }
+ return 0;
+}
+
+static int db_ldap_search_save_result(struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_named_result *named_res;
+
+ if (!array_is_created(&request->named_results)) {
+ if (request->result != NULL)
+ return -1;
+ request->result = res;
+ } else {
+ named_res = array_idx_modifiable(&request->named_results,
+ request->name_idx);
+ if (named_res->result != NULL)
+ return -1;
+ named_res->result = res;
+ }
+ res->refcount++;
+ return 0;
+}
+
+static int db_ldap_search_next_subsearch(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_named_result *named_res;
+ const struct ldap_field *field;
+
+ if (request->result != NULL)
+ res = request->result;
+
+ if (!array_is_created(&request->named_results)) {
+ /* see if we need to do more LDAP queries */
+ p_array_init(&request->named_results,
+ request->request.auth_request->pool, 2);
+ array_foreach(request->attr_map, field) {
+ if (!field->value_is_dn)
+ continue;
+ named_res = array_append_space(&request->named_results);
+ named_res->field = field;
+ }
+ if (db_ldap_fields_get_dn(conn, request, res) < 0)
+ return -1;
+ } else {
+ request->name_idx++;
+ }
+ while (request->name_idx < array_count(&request->named_results)) {
+ /* send the next LDAP query */
+ named_res = array_idx_modifiable(&request->named_results,
+ request->name_idx);
+ if (named_res->dn != NULL) {
+ if (ldap_request_send_subquery(conn, request,
+ named_res) < 0)
+ return -1;
+ return 1;
+ }
+ /* dn field wasn't returned, skip this */
+ request->name_idx++;
+ }
+ return 0;
+}
+
+static bool
+db_ldap_handle_request_result(struct ldap_connection *conn,
+ struct ldap_request *request, unsigned int idx,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_search *srequest = NULL;
+ const struct ldap_request_named_result *named_res;
+ int ret;
+ bool final_result;
+
+ i_assert(conn->pending_count > 0);
+
+ if (request->type == LDAP_REQUEST_TYPE_BIND) {
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING);
+ i_assert(conn->pending_count == 1);
+ conn->conn_state = LDAP_CONN_STATE_BOUND_AUTH;
+ } else {
+ srequest = (struct ldap_request_search *)request;
+ switch (ldap_msgtype(res->msg)) {
+ case LDAP_RES_SEARCH_ENTRY:
+ case LDAP_RES_SEARCH_RESULT:
+ break;
+ case LDAP_RES_SEARCH_REFERENCE:
+ /* we're going to ignore this */
+ return FALSE;
+ default:
+ e_error(conn->event, "Reply with unexpected type %d",
+ ldap_msgtype(res->msg));
+ return TRUE;
+ }
+ }
+ if (ldap_msgtype(res->msg) == LDAP_RES_SEARCH_ENTRY) {
+ ret = LDAP_SUCCESS;
+ final_result = FALSE;
+ } else {
+ final_result = TRUE;
+ ret = ldap_result2error(conn->ld, res->msg, 0);
+ }
+ /* LDAP_NO_SUCH_OBJECT is returned for nonexistent base */
+ if (ret != LDAP_SUCCESS && ret != LDAP_NO_SUCH_OBJECT &&
+ request->type == LDAP_REQUEST_TYPE_SEARCH) {
+ /* handle search failures here */
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+
+ if (!array_is_created(&srequest->named_results)) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(base=%s filter=%s) failed: %s",
+ srequest->base, srequest->filter,
+ ldap_err2string(ret));
+ } else {
+ named_res = array_idx(&srequest->named_results,
+ srequest->name_idx);
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(base=%s) failed: %s",
+ named_res->dn, ldap_err2string(ret));
+ }
+ res = NULL;
+ }
+ if (ret == LDAP_SUCCESS && srequest != NULL && !srequest->multi_entry) {
+ /* expand any @results */
+ if (!final_result) {
+ if (db_ldap_search_save_result(srequest, res) < 0) {
+ e_error(authdb_event(request->auth_request),
+ "LDAP search returned multiple entries");
+ res = NULL;
+ } else {
+ /* wait for finish */
+ return FALSE;
+ }
+ } else {
+ ret = db_ldap_search_next_subsearch(conn, srequest, res);
+ if (ret > 0) {
+ /* more LDAP queries left */
+ return FALSE;
+ }
+ if (ret < 0)
+ res = NULL;
+ }
+ }
+ if (res == NULL && !final_result) {
+ /* wait for the final reply */
+ request->failed = TRUE;
+ return TRUE;
+ }
+ if (request->failed)
+ res = NULL;
+ if (final_result) {
+ conn->pending_count--;
+ aqueue_delete(conn->request_queue, idx);
+ }
+
+ T_BEGIN {
+ if (res != NULL && srequest != NULL && srequest->result != NULL)
+ request->callback(conn, request, srequest->result->msg);
+
+ request->callback(conn, request, res == NULL ? NULL : res->msg);
+ } T_END;
+
+ if (idx > 0) {
+ /* see if there are timed out requests */
+ if (db_ldap_abort_requests(conn, idx,
+ DB_LDAP_REQUEST_LOST_TIMEOUT_SECS,
+ TRUE, "Request lost"))
+ ldap_conn_reconnect(conn);
+ }
+ return TRUE;
+}
+
+static void db_ldap_result_unref(struct db_ldap_result **_res)
+{
+ struct db_ldap_result *res = *_res;
+
+ *_res = NULL;
+ i_assert(res->refcount > 0);
+ if (--res->refcount == 0) {
+ ldap_msgfree(res->msg);
+ i_free(res);
+ }
+}
+
+static void
+db_ldap_request_free(struct ldap_request *request)
+{
+ if (request->type == LDAP_REQUEST_TYPE_SEARCH) {
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+ struct ldap_request_named_result *named_res;
+
+ if (srequest->result != NULL)
+ db_ldap_result_unref(&srequest->result);
+
+ if (array_is_created(&srequest->named_results)) {
+ array_foreach_modifiable(&srequest->named_results, named_res) {
+ if (named_res->result != NULL)
+ db_ldap_result_unref(&named_res->result);
+ }
+ array_free(&srequest->named_results);
+ srequest->name_idx = 0;
+ }
+ }
+}
+
+static void
+db_ldap_handle_result(struct ldap_connection *conn, struct db_ldap_result *res)
+{
+ struct auth_request *auth_request;
+ struct ldap_request *request;
+ unsigned int idx;
+ int msgid;
+
+ msgid = ldap_msgid(res->msg);
+ if (msgid == conn->default_bind_msgid) {
+ db_ldap_default_bind_finished(conn, res);
+ return;
+ }
+
+ request = db_ldap_find_request(conn, msgid, &idx);
+ if (request == NULL) {
+ e_error(conn->event, "Reply with unknown msgid %d", msgid);
+ ldap_conn_reconnect(conn);
+ return;
+ }
+ /* request is allocated from auth_request's pool */
+ auth_request = request->auth_request;
+ auth_request_ref(auth_request);
+ if (db_ldap_handle_request_result(conn, request, idx, res))
+ db_ldap_request_free(request);
+ auth_request_unref(&auth_request);
+}
+
+static void ldap_input(struct ldap_connection *conn)
+{
+ struct timeval timeout;
+ struct db_ldap_result *res;
+ LDAPMessage *msg;
+ time_t prev_reply_diff;
+ int ret;
+
+ do {
+ if (conn->ld == NULL)
+ return;
+
+ i_zero(&timeout);
+ ret = ldap_result(conn->ld, LDAP_RES_ANY, 0, &timeout, &msg);
+#ifdef OPENLDAP_ASYNC_WORKAROUND
+ if (ret == 0) {
+ /* try again, there may be another in buffer */
+ ret = ldap_result(conn->ld, LDAP_RES_ANY, 0,
+ &timeout, &msg);
+ }
+#endif
+ if (ret <= 0)
+ break;
+
+ res = i_new(struct db_ldap_result, 1);
+ res->refcount = 1;
+ res->msg = msg;
+ db_ldap_handle_result(conn, res);
+ db_ldap_result_unref(&res);
+ } while (conn->io != NULL);
+
+ prev_reply_diff = ioloop_time - conn->last_reply_stamp;
+ conn->last_reply_stamp = ioloop_time;
+
+ if (ret > 0) {
+ /* input disabled, continue once it's enabled */
+ i_assert(conn->io == NULL);
+ } else if (ret == 0) {
+ /* send more requests */
+ while (db_ldap_request_queue_next(conn))
+ ;
+ } else if (ldap_get_errno(conn) != LDAP_SERVER_DOWN) {
+ e_error(conn->event, "ldap_result() failed: %s", ldap_get_error(conn));
+ ldap_conn_reconnect(conn);
+ } else if (aqueue_count(conn->request_queue) > 0 ||
+ prev_reply_diff < DB_LDAP_IDLE_RECONNECT_SECS) {
+ e_error(conn->event, "Connection lost to LDAP server, reconnecting");
+ ldap_conn_reconnect(conn);
+ } else {
+ /* server probably disconnected an idle connection. don't
+ reconnect until the next request comes. */
+ db_ldap_conn_close(conn);
+ }
+}
+
+#ifdef HAVE_LDAP_SASL
+static int
+sasl_interact(LDAP *ld ATTR_UNUSED, unsigned flags ATTR_UNUSED,
+ void *defaults, void *interact)
+{
+ struct db_ldap_sasl_bind_context *context = defaults;
+ sasl_interact_t *in;
+ const char *str;
+
+ for (in = interact; in->id != SASL_CB_LIST_END; in++) {
+ switch (in->id) {
+ case SASL_CB_GETREALM:
+ str = context->realm;
+ break;
+ case SASL_CB_AUTHNAME:
+ str = context->authcid;
+ break;
+ case SASL_CB_USER:
+ str = context->authzid;
+ break;
+ case SASL_CB_PASS:
+ str = context->passwd;
+ break;
+ default:
+ str = NULL;
+ break;
+ }
+ if (str != NULL) {
+ in->len = strlen(str);
+ in->result = str;
+ }
+
+ }
+ return LDAP_SUCCESS;
+}
+#endif
+
+static void ldap_connection_timeout(struct ldap_connection *conn)
+{
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING);
+
+ e_error(conn->event, "Initial binding to LDAP server timed out");
+ db_ldap_conn_close(conn);
+}
+
+#ifdef HAVE_LDAP_SASL
+static int db_ldap_bind_sasl(struct ldap_connection *conn)
+{
+ struct db_ldap_sasl_bind_context context;
+ int ret;
+
+ i_zero(&context);
+ context.authcid = conn->set.dn;
+ context.passwd = conn->set.dnpass;
+ context.realm = conn->set.sasl_realm;
+ context.authzid = conn->set.sasl_authz_id;
+
+ /* There doesn't seem to be a way to do SASL binding
+ asynchronously.. */
+ ret = ldap_sasl_interactive_bind_s(conn->ld, NULL,
+ conn->set.sasl_mech,
+ NULL, NULL, LDAP_SASL_QUIET,
+ sasl_interact, &context);
+ if (db_ldap_connect_finish(conn, ret) < 0)
+ return -1;
+
+ conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT;
+
+ return 0;
+}
+#else
+static int db_ldap_bind_sasl(struct ldap_connection *conn ATTR_UNUSED)
+{
+ i_unreached(); /* already checked at init */
+
+ return -1;
+}
+#endif
+
+static int db_ldap_bind_simple(struct ldap_connection *conn)
+{
+ int msgid;
+
+ i_assert(conn->conn_state != LDAP_CONN_STATE_BINDING);
+ i_assert(conn->default_bind_msgid == -1);
+ i_assert(conn->pending_count == 0);
+
+ msgid = ldap_bind(conn->ld, conn->set.dn, conn->set.dnpass,
+ LDAP_AUTH_SIMPLE);
+ if (msgid == -1) {
+ i_assert(ldap_get_errno(conn) != LDAP_SUCCESS);
+ if (db_ldap_connect_finish(conn, ldap_get_errno(conn)) < 0) {
+ /* lost connection, close it */
+ db_ldap_conn_close(conn);
+ }
+ return -1;
+ }
+
+ conn->conn_state = LDAP_CONN_STATE_BINDING;
+ conn->default_bind_msgid = msgid;
+
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(DB_LDAP_REQUEST_LOST_TIMEOUT_SECS*1000,
+ ldap_connection_timeout, conn);
+ return 0;
+}
+
+static int db_ldap_bind(struct ldap_connection *conn)
+{
+ if (conn->set.sasl_bind) {
+ if (db_ldap_bind_sasl(conn) < 0)
+ return -1;
+ } else {
+ if (db_ldap_bind_simple(conn) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+static void db_ldap_get_fd(struct ldap_connection *conn)
+{
+ int ret;
+
+ /* get the connection's fd */
+ ret = ldap_get_option(conn->ld, LDAP_OPT_DESC, (void *)&conn->fd);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't get connection fd: %s",
+ conn->config_path, ldap_err2string(ret));
+ }
+ if (conn->fd <= STDERR_FILENO) {
+ /* Solaris LDAP library seems to be broken */
+ i_fatal("LDAP %s: Buggy LDAP library returned wrong fd: %d",
+ conn->config_path, conn->fd);
+ }
+ i_assert(conn->fd != -1);
+ net_set_nonblock(conn->fd, TRUE);
+}
+
+static void ATTR_NULL(1)
+db_ldap_set_opt(struct ldap_connection *conn, LDAP *ld, int opt,
+ const void *value, const char *optname, const char *value_str)
+{
+ int ret;
+
+ ret = ldap_set_option(ld, opt, value);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't set option %s to %s: %s",
+ conn->config_path, optname, value_str, ldap_err2string(ret));
+ }
+}
+
+static void ATTR_NULL(1)
+db_ldap_set_opt_str(struct ldap_connection *conn, LDAP *ld, int opt,
+ const char *value, const char *optname)
+{
+ if (value != NULL)
+ db_ldap_set_opt(conn, ld, opt, value, optname, value);
+}
+
+static void db_ldap_set_tls_options(struct ldap_connection *conn)
+{
+#ifdef OPENLDAP_TLS_OPTIONS
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTFILE,
+ conn->set.tls_ca_cert_file, "tls_ca_cert_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTDIR,
+ conn->set.tls_ca_cert_dir, "tls_ca_cert_dir");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CERTFILE,
+ conn->set.tls_cert_file, "tls_cert_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_KEYFILE,
+ conn->set.tls_key_file, "tls_key_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CIPHER_SUITE,
+ conn->set.tls_cipher_suite, "tls_cipher_suite");
+ if (conn->set.tls_require_cert != NULL) {
+ db_ldap_set_opt(conn, NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, &conn->set.ldap_tls_require_cert_parsed,
+ "tls_require_cert", conn->set.tls_require_cert);
+ }
+#else
+ if (conn->set.tls_ca_cert_file != NULL ||
+ conn->set.tls_ca_cert_dir != NULL ||
+ conn->set.tls_cert_file != NULL ||
+ conn->set.tls_key_file != NULL ||
+ conn->set.tls_cipher_suite != NULL) {
+ i_fatal("LDAP %s: tls_* settings aren't supported by your LDAP library - they must not be set",
+ conn->config_path);
+ }
+#endif
+}
+
+static void db_ldap_set_options(struct ldap_connection *conn)
+{
+ unsigned int ldap_version;
+ int value;
+
+#ifdef LDAP_OPT_NETWORK_TIMEOUT
+ struct timeval tv;
+ int ret;
+
+ tv.tv_sec = DB_LDAP_CONNECT_TIMEOUT_SECS; tv.tv_usec = 0;
+ ret = ldap_set_option(conn->ld, LDAP_OPT_NETWORK_TIMEOUT, &tv);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't set network-timeout: %s",
+ conn->config_path, ldap_err2string(ret));
+ }
+#endif
+
+ db_ldap_set_opt(conn, conn->ld, LDAP_OPT_DEREF, &conn->set.ldap_deref,
+ "deref", conn->set.deref);
+#ifdef LDAP_OPT_DEBUG_LEVEL
+ if (str_to_int(conn->set.debug_level, &value) >= 0 && value != 0) {
+ db_ldap_set_opt(conn, NULL, LDAP_OPT_DEBUG_LEVEL, &value,
+ "debug_level", conn->set.debug_level);
+ event_set_forced_debug(conn->event, TRUE);
+ }
+#endif
+
+ ldap_version = conn->set.ldap_version;
+ db_ldap_set_opt(conn, conn->ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version,
+ "protocol_version", dec2str(ldap_version));
+ db_ldap_set_tls_options(conn);
+}
+
+static void db_ldap_init_ld(struct ldap_connection *conn)
+{
+ int ret;
+
+ if (conn->set.uris != NULL) {
+#ifdef LDAP_HAVE_INITIALIZE
+ ret = ldap_initialize(&conn->ld, conn->set.uris);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: ldap_initialize() failed with uris %s: %s",
+ conn->config_path, conn->set.uris,
+ ldap_err2string(ret));
+ }
+#else
+ i_unreached(); /* already checked at init */
+#endif
+ } else {
+ conn->ld = ldap_init(conn->set.hosts, LDAP_PORT);
+ if (conn->ld == NULL) {
+ i_fatal("LDAP %s: ldap_init() failed with hosts: %s",
+ conn->config_path, conn->set.hosts);
+ }
+ }
+ db_ldap_set_options(conn);
+}
+
+int db_ldap_connect(struct ldap_connection *conn)
+{
+ struct timeval start, end;
+ int ret;
+
+ if (conn->conn_state != LDAP_CONN_STATE_DISCONNECTED)
+ return 0;
+
+ i_gettimeofday(&start);
+ i_assert(conn->pending_count == 0);
+
+ if (conn->delayed_connect) {
+ conn->delayed_connect = FALSE;
+ timeout_remove(&conn->to);
+ }
+ if (conn->ld == NULL)
+ db_ldap_init_ld(conn);
+
+ if (conn->set.tls) {
+#ifdef LDAP_HAVE_START_TLS_S
+ ret = ldap_start_tls_s(conn->ld, NULL, NULL);
+ if (ret != LDAP_SUCCESS) {
+ if (ret == LDAP_OPERATIONS_ERROR &&
+ conn->set.uris != NULL &&
+ str_begins(conn->set.uris, "ldaps:")) {
+ i_fatal("LDAP %s: Don't use both tls=yes "
+ "and ldaps URI", conn->config_path);
+ }
+ e_error(conn->event, "ldap_start_tls_s() failed: %s",
+ ldap_err2string(ret));
+ return -1;
+ }
+#else
+ i_unreached(); /* already checked at init */
+#endif
+ }
+
+ if (db_ldap_bind(conn) < 0)
+ return -1;
+
+ i_gettimeofday(&end);
+ int msecs = timeval_diff_msecs(&end, &start);
+ e_debug(conn->event, "LDAP initialization took %d msecs", msecs);
+
+ db_ldap_get_fd(conn);
+ conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
+ return 0;
+}
+
+static void db_ldap_connect_callback(struct ldap_connection *conn)
+{
+ i_assert(conn->conn_state == LDAP_CONN_STATE_DISCONNECTED);
+ (void)db_ldap_connect(conn);
+}
+
+void db_ldap_connect_delayed(struct ldap_connection *conn)
+{
+ if (conn->delayed_connect)
+ return;
+ conn->delayed_connect = TRUE;
+
+ i_assert(conn->to == NULL);
+ conn->to = timeout_add_short(0, db_ldap_connect_callback, conn);
+}
+
+void db_ldap_enable_input(struct ldap_connection *conn, bool enable)
+{
+ if (!enable) {
+ io_remove(&conn->io);
+ } else {
+ if (conn->io == NULL && conn->fd != -1) {
+ conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
+ ldap_input(conn);
+ }
+ }
+}
+
+static void db_ldap_disconnect_timeout(struct ldap_connection *conn)
+{
+ db_ldap_abort_requests(conn, UINT_MAX,
+ DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS, FALSE,
+ "Aborting (timeout), we're not connected to LDAP server");
+
+ if (aqueue_count(conn->request_queue) == 0) {
+ /* no requests left, remove this timeout handler */
+ timeout_remove(&conn->to);
+ }
+}
+
+static void db_ldap_conn_close(struct ldap_connection *conn)
+{
+ struct ldap_request *const *requests, *request;
+ unsigned int i;
+
+ conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
+ conn->delayed_connect = FALSE;
+ conn->default_bind_msgid = -1;
+
+ timeout_remove(&conn->to);
+
+ if (conn->pending_count != 0) {
+ requests = array_front(&conn->request_array);
+ for (i = 0; i < conn->pending_count; i++) {
+ request = requests[aqueue_idx(conn->request_queue, i)];
+
+ i_assert(request->msgid != -1);
+ request->msgid = -1;
+ }
+ conn->pending_count = 0;
+ }
+
+ if (conn->ld != NULL) {
+ ldap_unbind(conn->ld);
+ conn->ld = NULL;
+ }
+ conn->fd = -1;
+
+ /* the fd may have already been closed before ldap_unbind(),
+ so we'll have to use io_remove_closed(). */
+ io_remove_closed(&conn->io);
+
+ if (aqueue_count(conn->request_queue) > 0) {
+ conn->to = timeout_add(DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS *
+ 1000/2, db_ldap_disconnect_timeout, conn);
+ }
+}
+
+struct ldap_field_find_context {
+ ARRAY_TYPE(string) attr_names;
+ pool_t pool;
+};
+
+static int
+db_ldap_field_find(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct ldap_field_find_context *ctx = context;
+ char *ldap_attr;
+
+ if (*data != '\0') {
+ ldap_attr = p_strdup(ctx->pool, t_strcut(data, ':'));
+ if (strchr(ldap_attr, '@') == NULL)
+ array_push_back(&ctx->attr_names, &ldap_attr);
+ }
+ *value_r = NULL;
+ return 1;
+}
+
+void db_ldap_set_attrs(struct ldap_connection *conn, const char *attrlist,
+ char ***attr_names_r, ARRAY_TYPE(ldap_field) *attr_map,
+ const char *skip_attr)
+{
+ static struct var_expand_func_table var_funcs_table[] = {
+ { "ldap", db_ldap_field_find },
+ { "ldap_ptr", db_ldap_field_find },
+ { NULL, NULL }
+ };
+ struct ldap_field_find_context ctx;
+ struct ldap_field *field;
+ string_t *tmp_str;
+ const char *const *attr, *attr_data, *p, *error;
+ char *ldap_attr, *name, *templ;
+ unsigned int i;
+
+ if (*attrlist == '\0')
+ return;
+
+ attr = t_strsplit_spaces(attrlist, ",");
+
+ tmp_str = t_str_new(128);
+ ctx.pool = conn->pool;
+ p_array_init(&ctx.attr_names, conn->pool, 16);
+ for (i = 0; attr[i] != NULL; i++) {
+ /* allow spaces here so "foo=1, bar=2" works */
+ attr_data = attr[i];
+ while (*attr_data == ' ') attr_data++;
+
+ p = strchr(attr_data, '=');
+ if (p == NULL)
+ ldap_attr = name = p_strdup(conn->pool, attr_data);
+ else if (attr_data[0] == '@') {
+ ldap_attr = "";
+ name = p_strdup(conn->pool, attr_data);
+ } else {
+ ldap_attr = p_strdup_until(conn->pool, attr_data, p);
+ name = p_strdup(conn->pool, p + 1);
+ }
+
+ templ = strchr(name, '=');
+ if (templ == NULL) {
+ if (*ldap_attr == '\0') {
+ /* =foo static value */
+ templ = "";
+ }
+ } else {
+ *templ++ = '\0';
+ str_truncate(tmp_str, 0);
+ if (var_expand_with_funcs(tmp_str, templ, NULL,
+ var_funcs_table, &ctx, &error) <= 0) {
+ /* This var_expand_with_funcs call fills the
+ * ldap_field_find_context in ctx, but the
+ * resulting string_t is not used, and the
+ * return value or error_r is not checked since
+ * it gives errors for non-ldap variable
+ * expansions. */
+ }
+ if (strchr(templ, '%') == NULL) {
+ /* backwards compatibility:
+ attr=name=prefix means same as
+ attr=name=prefix%$ when %vars are missing */
+ templ = p_strconcat(conn->pool, templ,
+ "%$", NULL);
+ }
+ }
+
+ if (*name == '\0')
+ e_error(conn->event, "Invalid attrs entry: %s", attr_data);
+ else if (skip_attr == NULL || strcmp(skip_attr, name) != 0) {
+ field = array_append_space(attr_map);
+ if (name[0] == '@') {
+ /* @name=ldapField */
+ name++;
+ field->value_is_dn = TRUE;
+ } else if (name[0] == '!' && name == ldap_attr) {
+ /* !ldapAttr */
+ name = "";
+ i_assert(ldap_attr[0] == '!');
+ ldap_attr++;
+ field->skip = TRUE;
+ }
+ field->name = name;
+ field->value = templ;
+ field->ldap_attr_name = ldap_attr;
+ if (*ldap_attr != '\0' &&
+ strchr(ldap_attr, '@') == NULL) {
+ /* root request's attribute */
+ array_push_back(&ctx.attr_names, &ldap_attr);
+ }
+ }
+ }
+ array_append_zero(&ctx.attr_names);
+ *attr_names_r = array_front_modifiable(&ctx.attr_names);
+}
+
+static const struct var_expand_table *
+db_ldap_value_get_var_expand_table(struct auth_request *auth_request,
+ const char *ldap_value)
+{
+ struct var_expand_table *table;
+ unsigned int count = 1;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, NULL, &count);
+ table[0].key = '$';
+ table[0].value = ldap_value;
+ return table;
+}
+
+#define IS_LDAP_ESCAPED_CHAR(c) \
+ ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
+
+const char *ldap_escape(const char *str,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ 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_field_hide_password(struct db_ldap_result_iterate_context *ctx,
+ const char *attr)
+{
+ const struct ldap_field *field;
+
+ if (ctx->ldap_request->auth_request->set->debug_passwords)
+ return FALSE;
+
+ array_foreach(ctx->attr_map, field) {
+ if (strcmp(field->ldap_attr_name, attr) == 0) {
+ if (strcmp(field->name, "password") == 0 ||
+ strcmp(field->name, "password_noscheme") == 0)
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+get_ldap_fields(struct db_ldap_result_iterate_context *ctx,
+ struct ldap_connection *conn, LDAPMessage *entry,
+ const char *suffix)
+{
+ struct db_ldap_value *ldap_value;
+ char *attr, **vals;
+ unsigned int i, count;
+ BerElement *ber;
+
+ attr = ldap_first_attribute(conn->ld, entry, &ber);
+ while (attr != NULL) {
+ vals = ldap_get_values(conn->ld, entry, attr);
+
+ ldap_value = p_new(ctx->pool, struct db_ldap_value, 1);
+ if (vals == NULL) {
+ ldap_value->values = p_new(ctx->pool, const char *, 1);
+ count = 0;
+ } else {
+ for (count = 0; vals[count] != NULL; count++) ;
+ }
+
+ ldap_value->values = p_new(ctx->pool, const char *, count + 1);
+ for (i = 0; i < count; i++)
+ ldap_value->values[i] = p_strdup(ctx->pool, vals[i]);
+
+ if (ctx->debug != NULL) {
+ str_printfa(ctx->debug, " %s%s=", attr, suffix);
+ if (count == 0)
+ str_append(ctx->debug, "<no values>");
+ else if (ldap_field_hide_password(ctx, attr))
+ str_append(ctx->debug, PASSWORD_HIDDEN_STR);
+ else {
+ str_append(ctx->debug, ldap_value->values[0]);
+ for (i = 1; i < count; i++) {
+ str_printfa(ctx->debug, ",%s",
+ ldap_value->values[0]);
+ }
+ }
+ }
+ hash_table_insert(ctx->ldap_attrs,
+ p_strconcat(ctx->pool, attr, suffix, NULL),
+ ldap_value);
+
+ ldap_value_free(vals);
+ ldap_memfree(attr);
+ attr = ldap_next_attribute(conn->ld, entry, ber);
+ }
+ ber_free(ber, 0);
+}
+
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init_full(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values,
+ bool iter_dn_values)
+{
+ struct db_ldap_result_iterate_context *ctx;
+ const struct ldap_request_named_result *named_res;
+ const char *suffix;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"ldap result iter", 1024);
+ ctx = p_new(pool, struct db_ldap_result_iterate_context, 1);
+ ctx->pool = pool;
+ ctx->ldap_request = &ldap_request->request;
+ ctx->attr_map = ldap_request->attr_map;
+ ctx->skip_null_values = skip_null_values;
+ ctx->iter_dn_values = iter_dn_values;
+ hash_table_create(&ctx->ldap_attrs, pool, 0, strcase_hash, strcasecmp);
+ ctx->var = str_new(ctx->pool, 256);
+ if (event_want_debug(ctx->ldap_request->auth_request->event))
+ ctx->debug = t_str_new(256);
+ ctx->ldap_msg = res;
+ ctx->ld = conn->ld;
+
+ get_ldap_fields(ctx, conn, res, "");
+ if (array_is_created(&ldap_request->named_results)) {
+ array_foreach(&ldap_request->named_results, named_res) {
+ suffix = t_strdup_printf("@%s", named_res->field->name);
+ if (named_res->result != NULL) {
+ get_ldap_fields(ctx, conn,
+ named_res->result->msg, suffix);
+ }
+ }
+ }
+ return ctx;
+}
+
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values)
+{
+ return db_ldap_result_iterate_init_full(conn, ldap_request, res,
+ skip_null_values, FALSE);
+}
+
+static const char *db_ldap_field_get_default(const char *data)
+{
+ const char *p;
+
+ p = i_strchr_to_next(data, ':');
+ if (p == NULL)
+ return "";
+ else {
+ /* default value given */
+ return p;
+ }
+}
+
+static int
+db_ldap_field_expand(const char *data, void *context,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ struct db_ldap_value *ldap_value;
+ const char *field_name = t_strcut(data, ':');
+
+ ldap_value = hash_table_lookup(ctx->ldap_attrs, field_name);
+ if (ldap_value == NULL) {
+ /* requested ldap attribute wasn't returned at all */
+ if (ctx->debug != NULL)
+ str_printfa(ctx->debug, "; %s missing", field_name);
+ *value_r = db_ldap_field_get_default(data);
+ return 1;
+ }
+ ldap_value->used = TRUE;
+
+ if (ldap_value->values[0] == NULL) {
+ /* no value for ldap attribute */
+ *value_r = db_ldap_field_get_default(data);
+ return 1;
+ }
+ if (ldap_value->values[1] != NULL) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Multiple values found for '%s', using value '%s'",
+ field_name, ldap_value->values[0]);
+ }
+ *value_r = ldap_value->values[0];
+ return 1;
+}
+
+static int
+db_ldap_field_ptr_expand(const char *data, void *context,
+ const char **value_r, const char **error_r)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ const char *field_name, *suffix;
+
+ suffix = strchr(t_strcut(data, ':'), '@');
+ if (db_ldap_field_expand(data, ctx, &field_name, error_r) <= 0)
+ i_unreached();
+ if (field_name[0] == '\0') {
+ *value_r = "";
+ return 1;
+ }
+ field_name = t_strconcat(field_name, suffix, NULL);
+ return db_ldap_field_expand(field_name, ctx, value_r, error_r);
+}
+
+static int
+db_ldap_field_dn_expand(const char *data ATTR_UNUSED, void *context ATTR_UNUSED,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ char *dn = ldap_get_dn(ctx->ld, ctx->ldap_msg);
+ *value_r = t_strdup(dn);
+ ldap_memfree(dn);
+ return 1;
+}
+
+static struct var_expand_func_table ldap_var_funcs_table[] = {
+ { "ldap", db_ldap_field_expand },
+ { "ldap_ptr", db_ldap_field_ptr_expand },
+ { "ldap_dn", db_ldap_field_dn_expand },
+ { NULL, NULL }
+};
+
+static const char *const *
+db_ldap_result_return_value(struct db_ldap_result_iterate_context *ctx,
+ const struct ldap_field *field,
+ struct db_ldap_value *ldap_value)
+{
+ const struct var_expand_table *var_table;
+ const char *const *values, *error;
+
+ if (ldap_value != NULL)
+ values = ldap_value->values;
+ else {
+ /* LDAP attribute doesn't exist */
+ ctx->val_1_arr[0] = NULL;
+ values = ctx->val_1_arr;
+ }
+
+ if (field->value == NULL) {
+ /* use the LDAP attribute's value */
+ } else {
+ /* template */
+ if (values[0] == NULL && *field->ldap_attr_name != '\0') {
+ /* ldapAttr=key=template%$, but ldapAttr doesn't
+ exist. */
+ return values;
+ }
+ if (values[0] != NULL && values[1] != NULL) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Multiple values found for '%s', "
+ "using value '%s'",
+ field->name, values[0]);
+ }
+
+ /* do this lookup separately for each expansion, because:
+ 1) the values are allocated from data stack
+ 2) if "user" field is updated, we want %u/%n/%d updated
+ (and less importantly the same for other variables) */
+ var_table = db_ldap_value_get_var_expand_table(
+ ctx->ldap_request->auth_request, values[0]);
+ if (var_expand_with_funcs(ctx->var, field->value, var_table,
+ ldap_var_funcs_table, ctx, &error) <= 0) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Failed to expand template %s: %s",
+ field->value, error);
+ }
+ ctx->val_1_arr[0] = str_c(ctx->var);
+ values = ctx->val_1_arr;
+ }
+ return values;
+}
+
+bool db_ldap_result_iterate_next(struct db_ldap_result_iterate_context *ctx,
+ const char **name_r,
+ const char *const **values_r)
+{
+ const struct var_expand_table *tab;
+ const struct ldap_field *field;
+ struct db_ldap_value *ldap_value;
+ unsigned int pos;
+ const char *error;
+
+ do {
+ if (ctx->attr_idx == array_count(ctx->attr_map))
+ return FALSE;
+ field = array_idx(ctx->attr_map, ctx->attr_idx++);
+ } while (field->value_is_dn != ctx->iter_dn_values ||
+ field->skip);
+
+ ldap_value = *field->ldap_attr_name == '\0' ? NULL :
+ hash_table_lookup(ctx->ldap_attrs, field->ldap_attr_name);
+ if (ldap_value != NULL)
+ ldap_value->used = TRUE;
+ else if (ctx->debug != NULL && *field->ldap_attr_name != '\0')
+ str_printfa(ctx->debug, "; %s missing", field->ldap_attr_name);
+
+ str_truncate(ctx->var, 0);
+ *values_r = db_ldap_result_return_value(ctx, field, ldap_value);
+
+ if (strchr(field->name, '%') == NULL)
+ *name_r = field->name;
+ else {
+ /* expand %variables also for LDAP name fields. we'll use the
+ same ctx->var, which may already contain the value. */
+ str_append_c(ctx->var, '\0');
+ pos = str_len(ctx->var);
+
+ tab = auth_request_get_var_expand_table(
+ ctx->ldap_request->auth_request, NULL);
+ if (var_expand_with_funcs(ctx->var, field->name, tab,
+ ldap_var_funcs_table, ctx, &error) <= 0) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Failed to expand %s: %s", field->name, error);
+ }
+ *name_r = str_c(ctx->var) + pos;
+ }
+
+ if (ctx->skip_null_values && (*values_r)[0] == NULL) {
+ /* no values. don't confuse the caller with this reply. */
+ return db_ldap_result_iterate_next(ctx, name_r, values_r);
+ }
+ return TRUE;
+}
+
+static void
+db_ldap_result_finish_debug(struct db_ldap_result_iterate_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ char *name;
+ struct db_ldap_value *value;
+ unsigned int unused_count = 0;
+ size_t orig_len;
+
+ if (ctx->ldap_request->result_logged)
+ return;
+
+ orig_len = str_len(ctx->debug);
+ if (orig_len == 0) {
+ e_debug(authdb_event(ctx->ldap_request->auth_request),
+ "no fields returned by the server");
+ return;
+ }
+
+ str_append(ctx->debug, "; ");
+
+ iter = hash_table_iterate_init(ctx->ldap_attrs);
+ while (hash_table_iterate(iter, ctx->ldap_attrs, &name, &value)) {
+ if (!value->used) {
+ str_printfa(ctx->debug, "%s,", name);
+ unused_count++;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (unused_count == 0)
+ str_truncate(ctx->debug, orig_len);
+ else {
+ str_truncate(ctx->debug, str_len(ctx->debug)-1);
+ str_append(ctx->debug, " unused");
+ }
+ e_debug(authdb_event(ctx->ldap_request->auth_request),
+ "result: %s", str_c(ctx->debug) + 1);
+
+ ctx->ldap_request->result_logged = TRUE;
+}
+
+void db_ldap_result_iterate_deinit(struct db_ldap_result_iterate_context **_ctx)
+{
+ struct db_ldap_result_iterate_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->debug != NULL)
+ db_ldap_result_finish_debug(ctx);
+ hash_table_destroy(&ctx->ldap_attrs);
+ pool_unref(&ctx->pool);
+}
+
+static const char *parse_setting(const char *key, const char *value,
+ struct ldap_connection *conn)
+{
+ return parse_setting_from_defs(conn->pool, setting_defs,
+ &conn->set, key, value);
+}
+
+static struct ldap_connection *ldap_conn_find(const char *config_path)
+{
+ struct ldap_connection *conn;
+
+ for (conn = ldap_connections; conn != NULL; conn = conn->next) {
+ if (strcmp(conn->config_path, config_path) == 0)
+ return conn;
+ }
+
+ return NULL;
+}
+
+struct ldap_connection *db_ldap_init(const char *config_path, bool userdb)
+{
+ struct ldap_connection *conn;
+ const char *str, *error;
+ pool_t pool;
+
+ /* see if it already exists */
+ conn = ldap_conn_find(config_path);
+ if (conn != NULL) {
+ if (userdb)
+ conn->userdb_used = TRUE;
+ conn->refcount++;
+ return conn;
+ }
+
+ if (*config_path == '\0')
+ i_fatal("LDAP: Configuration file path not given");
+
+ pool = pool_alloconly_create("ldap_connection", 1024);
+ conn = p_new(pool, struct ldap_connection, 1);
+ conn->pool = pool;
+ conn->refcount = 1;
+
+ conn->userdb_used = userdb;
+ conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
+ conn->default_bind_msgid = -1;
+ conn->fd = -1;
+ conn->config_path = p_strdup(pool, config_path);
+ conn->set = default_ldap_settings;
+ if (!settings_read_nosection(config_path, parse_setting, conn, &error))
+ i_fatal("ldap %s: %s", config_path, error);
+
+ if (conn->set.base == NULL)
+ i_fatal("LDAP %s: No base given", config_path);
+
+ if (conn->set.uris == NULL && conn->set.hosts == NULL)
+ i_fatal("LDAP %s: No uris or hosts set", config_path);
+#ifndef LDAP_HAVE_INITIALIZE
+ if (conn->set.uris != NULL) {
+ i_fatal("LDAP %s: uris set, but Dovecot compiled without support for LDAP uris "
+ "(ldap_initialize() not supported by LDAP library)", config_path);
+ }
+#endif
+#ifndef LDAP_HAVE_START_TLS_S
+ if (conn->set.tls)
+ i_fatal("LDAP %s: tls=yes, but your LDAP library doesn't support TLS", config_path);
+#endif
+#ifndef HAVE_LDAP_SASL
+ if (conn->set.sasl_bind)
+ i_fatal("LDAP %s: sasl_bind=yes but no SASL support compiled in", conn->config_path);
+#endif
+ if (conn->set.ldap_version < 3) {
+ if (conn->set.sasl_bind)
+ i_fatal("LDAP %s: sasl_bind=yes requires ldap_version=3", config_path);
+ if (conn->set.tls)
+ i_fatal("LDAP %s: tls=yes requires ldap_version=3", config_path);
+ }
+#ifdef OPENLDAP_TLS_OPTIONS
+ if (conn->set.tls_require_cert != NULL) {
+ if (tls_require_cert2str(conn->set.tls_require_cert,
+ &conn->set.ldap_tls_require_cert_parsed) < 0)
+ i_fatal("LDAP %s: Unknown tls_require_cert value '%s'",
+ config_path, conn->set.tls_require_cert);
+ }
+#endif
+
+ if (*conn->set.ldaprc_path != '\0') {
+ str = getenv("LDAPRC");
+ if (str != NULL && strcmp(str, conn->set.ldaprc_path) != 0) {
+ i_fatal("LDAP %s: Multiple different ldaprc_path "
+ "settings not allowed (%s and %s)",
+ config_path, str, conn->set.ldaprc_path);
+ }
+ env_put("LDAPRC", conn->set.ldaprc_path);
+ }
+
+ if (deref2str(conn->set.deref, &conn->set.ldap_deref) < 0)
+ i_fatal("LDAP %s: Unknown deref option '%s'", config_path, conn->set.deref);
+ if (scope2str(conn->set.scope, &conn->set.ldap_scope) < 0)
+ i_fatal("LDAP %s: Unknown scope option '%s'", config_path, conn->set.scope);
+
+ conn->event = event_create(auth_event);
+ event_set_append_log_prefix(conn->event, t_strdup_printf(
+ "ldap(%s): ", conn->config_path));
+
+ i_array_init(&conn->request_array, 512);
+ conn->request_queue = aqueue_init(&conn->request_array.arr);
+
+ conn->next = ldap_connections;
+ ldap_connections = conn;
+
+ db_ldap_init_ld(conn);
+ return conn;
+}
+
+void db_ldap_unref(struct ldap_connection **_conn)
+{
+ struct ldap_connection *conn = *_conn;
+ struct ldap_connection **p;
+
+ *_conn = NULL;
+ i_assert(conn->refcount >= 0);
+ if (--conn->refcount > 0)
+ return;
+
+ for (p = &ldap_connections; *p != NULL; p = &(*p)->next) {
+ if (*p == conn) {
+ *p = conn->next;
+ break;
+ }
+ }
+
+ db_ldap_abort_requests(conn, UINT_MAX, 0, FALSE, "Shutting down");
+ i_assert(conn->pending_count == 0);
+ db_ldap_conn_close(conn);
+ i_assert(conn->to == NULL);
+
+ array_free(&conn->request_array);
+ aqueue_deinit(&conn->request_queue);
+
+ event_unref(&conn->event);
+ pool_unref(&conn->pool);
+}
+
+#ifndef BUILTIN_LDAP
+/* Building a plugin */
+extern struct passdb_module_interface passdb_ldap_plugin;
+extern struct userdb_module_interface userdb_ldap_plugin;
+
+void authdb_ldap_init(void);
+void authdb_ldap_deinit(void);
+
+void authdb_ldap_init(void)
+{
+ passdb_register_module(&passdb_ldap_plugin);
+ userdb_register_module(&userdb_ldap_plugin);
+
+}
+void authdb_ldap_deinit(void)
+{
+ passdb_unregister_module(&passdb_ldap_plugin);
+ userdb_unregister_module(&userdb_ldap_plugin);
+}
+#endif
+
+#endif
diff --git a/src/auth/db-ldap.h b/src/auth/db-ldap.h
new file mode 100644
index 0000000..e919e79
--- /dev/null
+++ b/src/auth/db-ldap.h
@@ -0,0 +1,221 @@
+#ifndef DB_LDAP_H
+#define DB_LDAP_H
+
+/* Functions like ldap_bind() have been deprecated in OpenLDAP 2.3
+ This define enables them until the code here can be refactored */
+#define LDAP_DEPRECATED 1
+
+/* Maximum number of pending requests before delaying new requests. */
+#define DB_LDAP_MAX_PENDING_REQUESTS 8
+/* connect() timeout to LDAP */
+#define DB_LDAP_CONNECT_TIMEOUT_SECS 5
+/* If LDAP connection is down, fail requests after waiting for this long. */
+#define DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS 4
+/* If request is still in queue after this many seconds and other requests
+ have been replied, assume the request was lost and abort it. */
+#define DB_LDAP_REQUEST_LOST_TIMEOUT_SECS 60
+/* If server disconnects us, don't reconnect if no requests have been sent
+ for this many seconds. */
+#define DB_LDAP_IDLE_RECONNECT_SECS 60
+
+#include <ldap.h>
+
+struct auth_request;
+struct ldap_connection;
+struct ldap_request;
+
+typedef void db_search_callback_t(struct ldap_connection *conn,
+ struct ldap_request *request,
+ LDAPMessage *res);
+
+struct ldap_settings {
+ const char *hosts;
+ const char *uris;
+ const char *dn;
+ const char *dnpass;
+ bool auth_bind;
+ const char *auth_bind_userdn;
+
+ bool tls;
+ bool sasl_bind;
+ const char *sasl_mech;
+ const char *sasl_realm;
+ const char *sasl_authz_id;
+
+ const char *tls_ca_cert_file;
+ const char *tls_ca_cert_dir;
+ const char *tls_cert_file;
+ const char *tls_key_file;
+ const char *tls_cipher_suite;
+ const char *tls_require_cert;
+
+ const char *deref;
+ const char *scope;
+ const char *base;
+ unsigned int ldap_version;
+
+ const char *ldaprc_path;
+ const char *debug_level;
+
+ const char *user_attrs;
+ const char *user_filter;
+ const char *pass_attrs;
+ const char *pass_filter;
+ const char *iterate_attrs;
+ const char *iterate_filter;
+
+ const char *default_pass_scheme;
+ bool userdb_warning_disable; /* deprecated for now at least */
+ bool blocking;
+
+ /* ... */
+ int ldap_deref, ldap_scope, ldap_tls_require_cert_parsed;
+ uid_t uid;
+ gid_t gid;
+};
+
+enum ldap_request_type {
+ LDAP_REQUEST_TYPE_SEARCH,
+ LDAP_REQUEST_TYPE_BIND
+};
+
+struct ldap_field {
+ /* Dovecot field name. */
+ const char *name;
+ /* Field value template with %vars. NULL = same as LDAP value. */
+ const char *value;
+ /* LDAP attribute name, or "" if this is a static field. */
+ const char *ldap_attr_name;
+
+ /* LDAP value contains a DN, which is looked up and used for @name
+ attributes. */
+ bool value_is_dn;
+ /* This attribute is used internally only via %{ldap_ptr},
+ it shouldn't be returned in iteration. */
+ bool skip;
+};
+ARRAY_DEFINE_TYPE(ldap_field, struct ldap_field);
+
+struct ldap_request {
+ enum ldap_request_type type;
+
+ /* msgid for sent requests, -1 if not sent */
+ int msgid;
+ /* timestamp when request was created */
+ time_t create_time;
+
+ /* Number of times this request has been sent to LDAP server. This
+ increases when LDAP gets disconnected and reconnect send the request
+ again. */
+ unsigned int send_count;
+
+ bool failed:1;
+ /* This is to prevent double logging the result */
+ bool result_logged:1;
+
+ db_search_callback_t *callback;
+ struct auth_request *auth_request;
+};
+
+struct ldap_request_named_result {
+ const struct ldap_field *field;
+ const char *dn;
+ struct db_ldap_result *result;
+};
+
+struct ldap_request_search {
+ struct ldap_request request;
+
+ const char *base;
+ const char *filter;
+ char **attributes; /* points to pass_attr_names / user_attr_names */
+ const ARRAY_TYPE(ldap_field) *attr_map;
+
+ struct db_ldap_result *result;
+ ARRAY(struct ldap_request_named_result) named_results;
+ unsigned int name_idx;
+
+ bool multi_entry;
+};
+
+struct ldap_request_bind {
+ struct ldap_request request;
+
+ const char *dn;
+};
+
+enum ldap_connection_state {
+ /* Not connected */
+ LDAP_CONN_STATE_DISCONNECTED,
+ /* Binding - either to default dn or doing auth bind */
+ LDAP_CONN_STATE_BINDING,
+ /* Bound to auth dn */
+ LDAP_CONN_STATE_BOUND_AUTH,
+ /* Bound to default dn */
+ LDAP_CONN_STATE_BOUND_DEFAULT
+};
+
+struct ldap_connection {
+ struct ldap_connection *next;
+
+ pool_t pool;
+ int refcount;
+ struct event *event;
+
+ char *config_path;
+ struct ldap_settings set;
+
+ LDAP *ld;
+ enum ldap_connection_state conn_state;
+ int default_bind_msgid;
+
+ int fd;
+ struct io *io;
+ struct timeout *to;
+
+ /* Request queue contains sent requests at tail (msgid != -1) and
+ queued requests at head (msgid == -1). */
+ struct aqueue *request_queue;
+ ARRAY(struct ldap_request *) request_array;
+ /* Number of messages in queue with msgid != -1 */
+ unsigned int pending_count;
+
+ /* Timestamp when we last received a reply */
+ time_t last_reply_stamp;
+
+ char **pass_attr_names, **user_attr_names, **iterate_attr_names;
+ ARRAY_TYPE(ldap_field) pass_attr_map, user_attr_map, iterate_attr_map;
+ bool userdb_used;
+ bool delayed_connect;
+};
+
+/* Send/queue request */
+void db_ldap_request(struct ldap_connection *conn,
+ struct ldap_request *request);
+
+void db_ldap_set_attrs(struct ldap_connection *conn, const char *attrlist,
+ char ***attr_names_r, ARRAY_TYPE(ldap_field) *attr_map,
+ const char *skip_attr) ATTR_NULL(5);
+
+struct ldap_connection *db_ldap_init(const char *config_path, bool userdb);
+void db_ldap_unref(struct ldap_connection **conn);
+
+int db_ldap_connect(struct ldap_connection *conn);
+void db_ldap_connect_delayed(struct ldap_connection *conn);
+
+void db_ldap_enable_input(struct ldap_connection *conn, bool enable);
+
+const char *ldap_escape(const char *str,
+ const struct auth_request *auth_request);
+const char *ldap_get_error(struct ldap_connection *conn);
+
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values);
+bool db_ldap_result_iterate_next(struct db_ldap_result_iterate_context *ctx,
+ const char **name_r,
+ const char *const **values_r);
+void db_ldap_result_iterate_deinit(struct db_ldap_result_iterate_context **ctx);
+
+#endif
diff --git a/src/auth/db-lua.c b/src/auth/db-lua.c
new file mode 100644
index 0000000..e31d5d9
--- /dev/null
+++ b/src/auth/db-lua.c
@@ -0,0 +1,814 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD)
+
+#include "llist.h"
+#include "istream.h"
+#include "array.h"
+#include "sha1.h"
+#include "hex-binary.h"
+#include "strescape.h"
+#include "auth.h"
+#include "passdb.h"
+#include "userdb.h"
+#include "auth-request.h"
+#include "userdb-template.h"
+#include "passdb-template.h"
+#include "password-scheme.h"
+#include "auth-request-var-expand.h"
+
+#define AUTH_LUA_PASSDB_LOOKUP "auth_passdb_lookup"
+#define AUTH_LUA_USERDB_LOOKUP "auth_userdb_lookup"
+#define AUTH_LUA_USERDB_ITERATE "auth_userdb_iterate"
+
+#define AUTH_LUA_DOVECOT_AUTH "dovecot_auth"
+#define AUTH_LUA_AUTH_REQUEST "auth_request*"
+
+#include "db-lua.h"
+#include "dlua-script-private.h"
+
+struct auth_lua_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ pool_t pool;
+ unsigned int idx;
+ ARRAY_TYPE(const_string) users;
+};
+
+static struct auth_request *
+auth_lua_check_auth_request(lua_State *L, int arg);
+
+static int
+auth_request_lua_do_var_expand(struct auth_request *req, const char *tpl,
+ const char **value_r, const char **error_r)
+{
+ const char *error;
+ if (t_auth_request_var_expand(tpl, req, NULL, value_r, &error) < 0) {
+ *error_r = t_strdup_printf("var_expand(%s) failed: %s",
+ tpl, error);
+ return -1;
+ }
+ return 0;
+}
+
+static int auth_request_lua_var_expand(lua_State *L)
+{
+ struct auth_request *req = auth_lua_check_auth_request(L, 1);
+ const char *tpl = luaL_checkstring(L, 2);
+ const char *value, *error;
+
+ if (auth_request_lua_do_var_expand(req, tpl, &value, &error) < 0) {
+ return luaL_error(L, "%s", error);
+ } else {
+ lua_pushstring(L, value);
+ }
+ return 1;
+}
+
+static const char *const *
+auth_request_template_build(struct auth_request *req, const char *str,
+ unsigned int *count_r)
+{
+ if (req->userdb_lookup) {
+ struct userdb_template *tpl =
+ userdb_template_build(pool_datastack_create(), "lua", str);
+ if (userdb_template_is_empty(tpl))
+ return NULL;
+ return userdb_template_get_args(tpl, count_r);
+ } else {
+ struct passdb_template *tpl =
+ passdb_template_build(pool_datastack_create(), str);
+ if (passdb_template_is_empty(tpl))
+ return NULL;
+ return passdb_template_get_args(tpl, count_r);
+ }
+}
+
+static int auth_request_lua_response_from_template(lua_State *L)
+{
+ struct auth_request *req = auth_lua_check_auth_request(L, 1);
+ const char *tplstr = luaL_checkstring(L, 2);
+ const char *error,*expanded;
+ unsigned int count,i;
+
+ const char *const *fields = auth_request_template_build(req, tplstr, &count);
+
+ /* push new table to stack */
+ lua_newtable(L);
+
+ if (fields == NULL)
+ return 1;
+
+ i_assert((count % 2) == 0);
+
+ for(i = 0; i < count; i+=2) {
+ const char *key = fields[i];
+ const char *value = fields[i+1];
+
+ if (value == NULL) {
+ lua_pushnil(L);
+ } else if (auth_request_lua_do_var_expand(req, value, &expanded, &error) < 0) {
+ return luaL_error(L, "%s", error);
+ } else {
+ lua_pushstring(L, expanded);
+ }
+ lua_setfield(L, -2, key);
+ }
+
+ /* stack should be left with table */
+ return 1;
+}
+
+static int auth_request_lua_log_debug(lua_State *L)
+{
+ if (global_auth_settings->debug) {
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *msg = luaL_checkstring(L, 2);
+ e_debug(authdb_event(request), "db-lua: %s", msg);
+ }
+ return 0;
+}
+
+static int auth_request_lua_log_info(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *msg = luaL_checkstring(L, 2);
+ e_info(authdb_event(request), "db-lua: %s", msg);
+ return 0;
+}
+
+static int auth_request_lua_log_warning(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *msg = luaL_checkstring(L, 2);
+ e_warning(authdb_event(request), "db-lua: %s", msg);
+ return 0;
+}
+
+static int auth_request_lua_log_error(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *msg = luaL_checkstring(L, 2);
+ e_error(authdb_event(request), "db-lua: %s", msg);
+ return 0;
+}
+
+static int auth_request_lua_passdb(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ lua_pop(L, 1);
+
+ if (request->fields.extra_fields == NULL) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushstring(L, auth_fields_find(request->fields.extra_fields, key));
+ return 1;
+}
+
+static int auth_request_lua_userdb(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ lua_pop(L, 1);
+
+ if (request->fields.userdb_reply == NULL) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushstring(L, auth_fields_find(request->fields.userdb_reply, key));
+ return 1;
+}
+
+static int auth_request_lua_password_verify(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ const char *crypted_password = lua_tostring(L, 2);
+ const char *scheme;
+ const char *plain_password = lua_tostring(L, 3);
+ const char *error = NULL;
+ const unsigned char *raw_password = NULL;
+ size_t raw_password_size;
+ int ret;
+ struct password_generate_params gen_params = {
+ .user = request->fields.original_username,
+ .rounds = 0
+ };
+ scheme = password_get_scheme(&crypted_password);
+ if (scheme == NULL)
+ scheme = "PLAIN";
+ ret = password_decode(crypted_password, scheme,
+ &raw_password, &raw_password_size, &error);
+ if (ret <= 0) {
+ if (ret < 0) {
+ error = t_strdup_printf("Password data is not valid for scheme %s: %s",
+ scheme, error);
+ } else {
+ error = t_strdup_printf("Unknown scheme %s", scheme);
+ }
+ } else {
+ /* Use original_username since it may be important for some
+ password schemes (eg. digest-md5).
+ */
+ ret = password_verify(plain_password, &gen_params,
+ scheme, raw_password, raw_password_size, &error);
+ }
+
+ lua_pushnumber(L, ret);
+ lua_pushstring(L, error);
+
+ return 2;
+}
+
+static int auth_request_lua_event(lua_State *L)
+{
+ struct auth_request *request = auth_lua_check_auth_request(L, 1);
+ struct event *event = event_create(authdb_event(request));
+
+ dlua_push_event(L, event);
+ event_unref(&event);
+ return 1;
+}
+
+/* put all methods here */
+static const luaL_Reg auth_request_methods[] ={
+ { "var_expand", auth_request_lua_var_expand },
+ { "response_from_template", auth_request_lua_response_from_template },
+ { "log_debug", auth_request_lua_log_debug },
+ { "log_info", auth_request_lua_log_info },
+ { "log_warning", auth_request_lua_log_warning },
+ { "log_error", auth_request_lua_log_error },
+ { "password_verify", auth_request_lua_password_verify },
+ { "event", auth_request_lua_event },
+ { NULL, NULL }
+};
+
+static int auth_request_lua_index(lua_State *L)
+{
+ struct auth_request *req = auth_lua_check_auth_request(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ lua_pop(L, 1);
+
+ const struct var_expand_table *table =
+ auth_request_get_var_expand_table(req, NULL);
+
+ /* check if it's variable */
+ for(unsigned int i = 0; i < AUTH_REQUEST_VAR_TAB_COUNT; i++) {
+ if (null_strcmp(table[i].long_key, key) == 0) {
+ lua_pushstring(L, table[i].value);
+ return 1;
+ }
+ }
+
+ /* check if it's function, then */
+ const luaL_Reg *ptr = auth_request_methods;
+ while(ptr->name != NULL) {
+ if (null_strcmp(key, ptr->name) == 0) {
+ lua_pushcfunction(L, ptr->func);
+ return 1;
+ }
+ ptr++;
+ }
+
+ lua_pushstring(L, key);
+ lua_rawget(L, 1);
+
+ return 1;
+}
+
+static void auth_lua_push_auth_request(lua_State *L, struct auth_request *req)
+{
+ luaL_checkstack(L, 4, "out of memory");
+ /* create a table for holding few things */
+ lua_createtable(L, 0, 3);
+ luaL_setmetatable(L, AUTH_LUA_AUTH_REQUEST);
+
+ lua_pushlightuserdata(L, req);
+ lua_setfield(L, -2, "item");
+
+ lua_newtable(L);
+ lua_pushlightuserdata(L, req);
+ lua_setfield(L, -2, "item");
+ luaL_setmetatable(L, "passdb_"AUTH_LUA_AUTH_REQUEST);
+ lua_setfield(L, -2, "passdb");
+
+ lua_newtable(L);
+ lua_pushlightuserdata(L, req);
+ lua_setfield(L, -2, "item");
+ luaL_setmetatable(L, "userdb_"AUTH_LUA_AUTH_REQUEST);
+ lua_setfield(L, -2, "userdb");
+
+ lua_pushboolean(L, req->fields.skip_password_check);
+ lua_setfield(L, -2, "skip_password_check");
+
+#undef LUA_TABLE_SET_BOOL
+#define LUA_TABLE_SET_BOOL(field) \
+ lua_pushboolean(L, req->field); \
+ lua_setfield(L, -2, #field);
+
+ LUA_TABLE_SET_BOOL(passdbs_seen_user_unknown);
+ LUA_TABLE_SET_BOOL(passdbs_seen_internal_failure);
+ LUA_TABLE_SET_BOOL(userdbs_seen_internal_failure);
+}
+
+static struct auth_request *
+auth_lua_check_auth_request(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, "auth_request",
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushstring(L, "item");
+ lua_rawget(L, arg);
+ void *bp = (void*)lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return (struct auth_request*)bp;
+}
+
+static void auth_lua_auth_request_register(lua_State *L)
+{
+ luaL_newmetatable(L, AUTH_LUA_AUTH_REQUEST);
+ lua_pushcfunction(L, auth_request_lua_index);
+ lua_setfield(L, -2, "__index");
+ lua_pop(L, 1);
+
+ /* register passdb */
+ luaL_newmetatable(L, "passdb_"AUTH_LUA_AUTH_REQUEST);
+ lua_pushcfunction(L, auth_request_lua_passdb);
+ lua_setfield(L, -2, "__index");
+ lua_pop(L, 1);
+
+ /* register userdb */
+ luaL_newmetatable(L, "userdb_"AUTH_LUA_AUTH_REQUEST);
+ lua_pushcfunction(L, auth_request_lua_userdb);
+ lua_setfield(L, -2, "__index");
+ lua_pop(L, 1);
+}
+
+static struct dlua_table_values auth_lua_dovecot_auth_values[] = {
+ DLUA_TABLE_ENUM(PASSDB_RESULT_INTERNAL_FAILURE),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_SCHEME_NOT_AVAILABLE),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_USER_UNKNOWN),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_USER_DISABLED),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_PASS_EXPIRED),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_NEXT),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_PASSWORD_MISMATCH),
+ DLUA_TABLE_ENUM(PASSDB_RESULT_OK),
+
+ DLUA_TABLE_ENUM(USERDB_RESULT_INTERNAL_FAILURE),
+ DLUA_TABLE_ENUM(USERDB_RESULT_USER_UNKNOWN),
+ DLUA_TABLE_ENUM(USERDB_RESULT_OK),
+
+ DLUA_TABLE_END
+};
+static luaL_Reg auth_lua_dovecot_auth_methods[] = {
+ { NULL, NULL }
+};
+
+static void auth_lua_dovecot_auth_register(lua_State *L)
+{
+ dlua_get_dovecot(L);
+ /* Create new table for holding values */
+ lua_newtable(L);
+
+ /* register constants */
+ dlua_set_members(L, auth_lua_dovecot_auth_values, -1);
+
+ /* push new metatable to stack */
+ luaL_newmetatable(L, AUTH_LUA_DOVECOT_AUTH);
+ /* this will register functions to the metatable itself */
+ luaL_setfuncs(L, auth_lua_dovecot_auth_methods, 0);
+ /* point __index to self */
+ lua_pushvalue(L, -1);
+ lua_setfield(L, -1, "__index");
+ /* set table's metatable, pops stack */
+ lua_setmetatable(L, -2);
+
+ /* put this as "dovecot.auth" */
+ lua_setfield(L, -2, "auth");
+
+ /* pop dovecot */
+ lua_pop(L, 1);
+}
+
+int auth_lua_script_init(struct dlua_script *script, const char **error_r)
+{
+ dlua_dovecot_register(script);
+ auth_lua_dovecot_auth_register(script->L);
+ auth_lua_auth_request_register(script->L);
+ return dlua_script_init(script, error_r);
+}
+
+static int auth_lua_call_lookup(lua_State *L, const char *fn,
+ struct auth_request *req, const char **error_r)
+{
+ int err = 0;
+
+ e_debug(authdb_event(req), "Calling %s", fn);
+
+ /* call with auth request as parameter */
+ auth_lua_push_auth_request(L, req);
+ if (dlua_pcall(L, fn, 1, 2, error_r) < 0)
+ return -1;
+
+ if (!lua_isnumber(L, -2)) {
+ *error_r = t_strdup_printf("db-lua: %s(req) invalid return value "
+ "(expected number got %s)",
+ fn, luaL_typename(L, -2));
+ err = -1;
+ } else if (!lua_isstring(L, -1) && !lua_istable(L, -1)) {
+ *error_r = t_strdup_printf("db-lua: %s(req) invalid return value "
+ "(expected string or table, got %s)",
+ fn, luaL_typename(L, -1));
+ err = -1;
+ }
+
+ if (err != 0) {
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ return 0;
+}
+
+static void
+auth_lua_export_fields(struct auth_request *req,
+ const char *str,
+ const char **scheme_r, const char **password_r)
+{
+ const char *const *fields = t_strsplit_spaces(str, " ");
+ while(*fields != NULL) {
+ const char *value = strchr(*fields, '=');
+ const char *key;
+
+ if (value == NULL) {
+ key = *fields;
+ value = "";
+ } else {
+ key = t_strdup_until(*fields, value++);
+ }
+
+ if (password_r != NULL && strcmp(key, "password") == 0) {
+ *scheme_r = password_get_scheme(&value);
+ *password_r = value;
+ } else if (req->userdb_lookup) {
+ auth_request_set_userdb_field(req, key, value);
+ } else {
+ auth_request_set_field(req, key, value, STATIC_PASS_SCHEME);
+ }
+ fields++;
+ }
+}
+
+static void auth_lua_export_table(lua_State *L, struct auth_request *req,
+ const char **scheme_r, const char **password_r)
+{
+ lua_pushvalue(L, -1);
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ const char *key = t_strdup(lua_tostring(L, -2));
+ if (*key == '\0') {
+ e_warning(authdb_event(req),
+ "db-lua: Field key cannot be empty - ignoring");
+ lua_pop(L, 1);
+ continue;
+ }
+ if (strpbrk(key, "\t\n\r") != NULL) {
+ e_warning(authdb_event(req),
+ "db-lua: Field key cannot contain <CR>, <LF> or <TAB> - ignoring");
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const char *value;
+ int type = lua_type(L, -1);
+ switch(type) {
+ case LUA_TNUMBER:
+ value = dec2str(lua_tointeger(L, -1));
+ break;
+ case LUA_TBOOLEAN:
+ value = lua_toboolean(L, -1) ? "yes" : "no";
+ break;
+ case LUA_TSTRING:
+ value = t_strdup(lua_tostring(L, -1));
+ break;
+ case LUA_TNIL:
+ value = "";
+ break;
+ default:
+ e_warning(authdb_event(req),
+ "db-lua: '%s' has invalid value type %s - ignoring",
+ key, lua_typename(L, -1));
+ value = NULL;
+ }
+
+ if (value == NULL) {
+ /* do not add */
+ } else if (password_r != NULL && strcmp(key, "password") == 0) {
+ *scheme_r = password_get_scheme(&value);
+ *password_r = value;
+ } else if (req->userdb_lookup) {
+ auth_request_set_userdb_field(req, key, value);
+ } else {
+ auth_request_set_field(req, key, value, STATIC_PASS_SCHEME);
+ }
+ lua_pop(L, 1);
+ }
+
+ /* stack has
+ key
+ table
+ passdb_result
+ */
+ lua_pop(L, 3);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+}
+
+static enum userdb_result
+auth_lua_export_userdb_table(lua_State *L, struct auth_request *req,
+ const char **error_r)
+{
+ enum userdb_result ret = lua_tointeger(L, -2);
+
+ if (ret != USERDB_RESULT_OK) {
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ *error_r = "userdb failed";
+ return ret;
+ }
+
+ auth_lua_export_table(L, req, NULL, NULL);
+ return USERDB_RESULT_OK;
+}
+
+static enum passdb_result
+auth_lua_export_passdb_table(lua_State *L, struct auth_request *req,
+ const char **scheme_r, const char **password_r,
+ const char **error_r)
+{
+ enum passdb_result ret = lua_tointeger(L, -2);
+
+ if (ret != PASSDB_RESULT_OK) {
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ *error_r = "passb failed";
+ return ret;
+ }
+
+ auth_lua_export_table(L, req, scheme_r, password_r);
+ return PASSDB_RESULT_OK;
+}
+
+static enum passdb_result
+auth_lua_call_lookup_finish(lua_State *L, struct auth_request *req,
+ const char **scheme_r, const char **password_r,
+ const char **error_r)
+{
+ if (lua_istable(L, -1)) {
+ return auth_lua_export_passdb_table(L, req, scheme_r,
+ password_r, error_r);
+ }
+
+ enum passdb_result ret = lua_tointeger(L, -2);
+ const char *str = t_strdup(lua_tostring(L, -1));
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ /* stack should be empty now */
+ i_assert(lua_gettop(L) == 0);
+
+ if (ret != PASSDB_RESULT_OK && ret != PASSDB_RESULT_NEXT) {
+ *error_r = str;
+ } else {
+ auth_lua_export_fields(req, str, scheme_r, password_r);
+ }
+
+ if (scheme_r != NULL && *scheme_r == NULL)
+ *scheme_r = "PLAIN";
+
+ return ret;
+}
+
+enum passdb_result
+auth_lua_call_password_verify(struct dlua_script *script,
+ struct auth_request *req, const char *password, const char **error_r)
+{
+ lua_State *L = script->L;
+ int err = 0;
+
+ e_debug(authdb_event(req), "Calling %s", AUTH_LUA_PASSWORD_VERIFY);
+
+ /* call with auth request, password as parameters */
+ auth_lua_push_auth_request(L, req);
+ lua_pushstring(L, password);
+
+ if (dlua_pcall(L, AUTH_LUA_PASSWORD_VERIFY, 2, 2, error_r) < 0)
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+
+ if (!lua_isnumber(L, -2)) {
+ *error_r = t_strdup_printf("db-lua: %s invalid return value "
+ "(expected number got %s)",
+ AUTH_LUA_PASSWORD_VERIFY,
+ luaL_typename(L, -2));
+ err = -1;
+ } else if (!lua_isstring(L, -1) && !lua_istable(L, -1)) {
+ *error_r = t_strdup_printf("db-lua: %s invalid return value "
+ "(expected string or table, got %s)",
+ AUTH_LUA_PASSWORD_VERIFY,
+ luaL_typename(L, -1));
+ err = -1;
+ }
+
+ if (err != 0) {
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+
+ return auth_lua_call_lookup_finish(L, req, NULL, NULL, error_r);
+}
+
+
+enum passdb_result
+auth_lua_call_passdb_lookup(struct dlua_script *script,
+ struct auth_request *req, const char **scheme_r,
+ const char **password_r, const char **error_r)
+{
+ lua_State *L = script->L;
+
+ *scheme_r = *password_r = NULL;
+ if (auth_lua_call_lookup(L, AUTH_LUA_PASSDB_LOOKUP, req, error_r) < 0) {
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ return auth_lua_call_lookup_finish(L, req, scheme_r, password_r, error_r);
+}
+
+
+enum userdb_result
+auth_lua_call_userdb_lookup(struct dlua_script *script,
+ struct auth_request *req, const char **error_r)
+{
+ lua_State *L = script->L;
+
+ if (auth_lua_call_lookup(L, AUTH_LUA_USERDB_LOOKUP, req, error_r) < 0) {
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return USERDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ if (lua_istable(L, -1))
+ return auth_lua_export_userdb_table(L, req, error_r);
+
+ enum userdb_result ret = lua_tointeger(L, -2);
+ const char *str = t_strdup(lua_tostring(L, -1));
+ lua_pop(L, 2);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+
+ if (ret != USERDB_RESULT_OK) {
+ *error_r = str;
+ return ret;
+ }
+ auth_lua_export_fields(req, str, NULL, NULL);
+
+ return USERDB_RESULT_OK;
+}
+
+struct userdb_iterate_context *
+auth_lua_call_userdb_iterate_init(struct dlua_script *script, struct auth_request *req,
+ userdb_iter_callback_t *callback, void *context)
+{
+ lua_State *L = script->L;
+
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING"lua userdb iterate", 128);
+ struct auth_lua_userdb_iterate_context *actx =
+ p_new(pool, struct auth_lua_userdb_iterate_context, 1);
+
+ actx->pool = pool;
+ actx->ctx.auth_request = req;
+ actx->ctx.callback = callback;
+ actx->ctx.context = context;
+
+ if (!dlua_script_has_function(script, AUTH_LUA_USERDB_ITERATE)) {
+ actx->ctx.failed = TRUE;
+ return &actx->ctx;
+ }
+
+ e_debug(authdb_event(req), "Calling %s", AUTH_LUA_USERDB_ITERATE);
+
+ const char *error;
+ if (dlua_pcall(L, AUTH_LUA_USERDB_ITERATE, 0, 1, &error) < 0) {
+ e_error(authdb_event(req),
+ "db-lua: " AUTH_LUA_USERDB_ITERATE " failed: %s",
+ error);
+ actx->ctx.failed = TRUE;
+ return &actx->ctx;
+ }
+
+ if (!lua_istable(L, -1)) {
+ e_error(authdb_event(req),
+ "db-lua: Cannot iterate, return value is not table");
+ actx->ctx.failed = TRUE;
+ lua_pop(L, 1);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return &actx->ctx;
+ }
+
+ p_array_init(&actx->users, pool, 8);
+
+ /* stack is now
+ table */
+
+ /* see lua_next documentation */
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ /* stack is now
+ value
+ key
+ table */
+ if (!lua_isstring(L, -1)) {
+ e_error(authdb_event(req),
+ "db-lua: Value is not string");
+ actx->ctx.failed = TRUE;
+ lua_pop(L, 3);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+ return &actx->ctx;
+ }
+ const char *str = p_strdup(pool, lua_tostring(L, -1));
+ array_push_back(&actx->users, &str);
+ lua_pop(L, 1);
+ /* stack is now
+ key
+ table */
+ }
+
+ /* stack is now
+ table
+ */
+
+ lua_pop(L, 1);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ i_assert(lua_gettop(L) == 0);
+
+ return &actx->ctx;
+}
+
+void auth_lua_userdb_iterate_next(struct userdb_iterate_context *ctx)
+{
+ struct auth_lua_userdb_iterate_context *actx =
+ container_of(ctx, struct auth_lua_userdb_iterate_context, ctx);
+
+ if (ctx->failed || actx->idx >= array_count(&actx->users)) {
+ ctx->callback(NULL, ctx->context);
+ return;
+ }
+
+ const char *user = array_idx_elem(&actx->users, actx->idx++);
+ ctx->callback(user, ctx->context);
+}
+
+int auth_lua_userdb_iterate_deinit(struct userdb_iterate_context *ctx)
+{
+ struct auth_lua_userdb_iterate_context *actx =
+ container_of(ctx, struct auth_lua_userdb_iterate_context, ctx);
+
+ int ret = ctx->failed ? -1 : 0;
+ pool_unref(&actx->pool);
+ return ret;
+}
+
+#ifndef BUILTIN_LUA
+/* Building a plugin */
+extern struct passdb_module_interface passdb_lua_plugin;
+extern struct userdb_module_interface userdb_lua_plugin;
+
+void authdb_lua_init(void);
+void authdb_lua_deinit(void);
+
+void authdb_lua_init(void)
+{
+ passdb_register_module(&passdb_lua_plugin);
+ userdb_register_module(&userdb_lua_plugin);
+
+}
+void authdb_lua_deinit(void)
+{
+ passdb_unregister_module(&passdb_lua_plugin);
+ userdb_unregister_module(&userdb_lua_plugin);
+}
+#endif
+
+#endif
diff --git a/src/auth/db-lua.h b/src/auth/db-lua.h
new file mode 100644
index 0000000..ebb697a
--- /dev/null
+++ b/src/auth/db-lua.h
@@ -0,0 +1,33 @@
+#ifndef DB_LUA_H
+#define DB_LUA_H 1
+
+#include "dlua-script.h"
+
+#define DB_LUA_CACHE_KEY "%u"
+
+#define AUTH_LUA_PASSWORD_VERIFY "auth_password_verify"
+
+struct dlua_script;
+
+int auth_lua_script_init(struct dlua_script *script, const char **error_r);
+
+int auth_lua_call_password_verify(struct dlua_script *script,
+ struct auth_request *req, const char *password,
+ const char **error_r);
+
+enum passdb_result
+auth_lua_call_passdb_lookup(struct dlua_script *script,
+ struct auth_request *req, const char **scheme_r,
+ const char **password_r, const char **error_r);
+
+enum userdb_result
+auth_lua_call_userdb_lookup(struct dlua_script *script,
+ struct auth_request *req, const char **error_r);
+
+struct userdb_iterate_context *
+auth_lua_call_userdb_iterate_init(struct dlua_script *script, struct auth_request *req,
+ userdb_iter_callback_t *callback, void *context);
+void auth_lua_userdb_iterate_next(struct userdb_iterate_context *ctx);
+int auth_lua_userdb_iterate_deinit(struct userdb_iterate_context *ctx);
+
+#endif
diff --git a/src/auth/db-oauth2.c b/src/auth/db-oauth2.c
new file mode 100644
index 0000000..b36a4ce
--- /dev/null
+++ b/src/auth/db-oauth2.c
@@ -0,0 +1,915 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "env-util.h"
+#include "var-expand.h"
+#include "settings.h"
+#include "oauth2.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "iostream-ssl.h"
+#include "auth-request.h"
+#include "auth-settings.h"
+#include "passdb.h"
+#include "passdb-template.h"
+#include "llist.h"
+#include "db-oauth2.h"
+#include "dcrypt.h"
+#include "dict.h"
+
+#include <stddef.h>
+
+struct passdb_oauth2_settings {
+ /* tokeninfo endpoint, format https://endpoint/somewhere?token= */
+ const char *tokeninfo_url;
+ /* password grant endpoint, format https://endpoint/somewhere */
+ const char *grant_url;
+ /* introspection endpoint, format https://endpoint/somewhere */
+ const char *introspection_url;
+ /* expected scope, optional */
+ const char *scope;
+ /* mode of introspection, one of get, get-auth, post
+ - get: append token to url
+ - get-auth: send token with header Authorization: Bearer token
+ - post: send token=<token> as POST request
+ */
+ const char *introspection_mode;
+ /* normalization var-expand template for username, defaults to %Lu */
+ const char *username_format;
+ /* name of username attribute to lookup, mandatory */
+ const char *username_attribute;
+ /* name of account is active attribute, optional */
+ const char *active_attribute;
+ /* expected active value for active attribute, optional */
+ const char *active_value;
+ /* client identificator for oauth2 server */
+ const char *client_id;
+ /* not really used, but have to present by oauth2 specs */
+ const char *client_secret;
+ /* template to expand into passdb */
+ const char *pass_attrs;
+ /* template to expand into key path, turns on local validation support */
+ const char *local_validation_key_dict;
+ /* valid token issuers */
+ const char *issuers;
+ /* The URL for a document following the OpenID Provider Configuration
+ Information schema, see
+
+ https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2
+ */
+ const char *openid_configuration_url;
+
+ /* TLS options */
+ const char *tls_ca_cert_file;
+ const char *tls_ca_cert_dir;
+ const char *tls_cert_file;
+ const char *tls_key_file;
+ const char *tls_cipher_suite;
+
+ /* HTTP rawlog directory */
+ const char *rawlog_dir;
+
+ /* HTTP client options */
+ unsigned int timeout_msecs;
+ unsigned int max_idle_time_msecs;
+ unsigned int max_parallel_connections;
+ unsigned int max_pipelined_requests;
+ bool tls_allow_invalid_cert;
+
+ bool debug;
+ /* Should introspection be done even if not necessary */
+ bool force_introspection;
+ /* Should we send service and local/remote endpoints as X-Dovecot-Auth headers */
+ bool send_auth_headers;
+ bool use_grant_password;
+};
+
+struct db_oauth2 {
+ struct db_oauth2 *prev,*next;
+
+ pool_t pool;
+
+ const char *config_path;
+ struct passdb_oauth2_settings set;
+ struct http_client *client;
+ struct passdb_template *tmpl;
+ struct oauth2_settings oauth2_set;
+
+ struct db_oauth2_request *head;
+
+ unsigned int refcount;
+};
+
+static struct db_oauth2 *db_oauth2_head = NULL;
+
+#undef DEF_STR
+#undef DEF_BOOL
+#undef DEF_INT
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, passdb_oauth2_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, passdb_oauth2_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, passdb_oauth2_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(tokeninfo_url),
+ DEF_STR(grant_url),
+ DEF_STR(introspection_url),
+ DEF_STR(scope),
+ DEF_BOOL(force_introspection),
+ DEF_STR(introspection_mode),
+ DEF_STR(username_format),
+ DEF_STR(username_attribute),
+ DEF_STR(pass_attrs),
+ DEF_STR(local_validation_key_dict),
+ DEF_STR(active_attribute),
+ DEF_STR(active_value),
+ DEF_STR(client_id),
+ DEF_STR(client_secret),
+ DEF_STR(issuers),
+ DEF_STR(openid_configuration_url),
+ DEF_INT(timeout_msecs),
+ DEF_INT(max_idle_time_msecs),
+ DEF_INT(max_parallel_connections),
+ DEF_INT(max_pipelined_requests),
+ DEF_BOOL(send_auth_headers),
+ DEF_BOOL(use_grant_password),
+
+ DEF_STR(tls_ca_cert_file),
+ DEF_STR(tls_ca_cert_dir),
+ DEF_STR(tls_cert_file),
+ DEF_STR(tls_key_file),
+ DEF_STR(tls_cipher_suite),
+ DEF_BOOL(tls_allow_invalid_cert),
+
+ DEF_STR(rawlog_dir),
+
+ DEF_BOOL(debug),
+
+ { 0, NULL, 0 }
+};
+
+static struct passdb_oauth2_settings default_oauth2_settings = {
+ .tokeninfo_url = "",
+ .grant_url = "",
+ .introspection_url = "",
+ .scope = "",
+ .force_introspection = FALSE,
+ .introspection_mode = "",
+ .username_format = "%Lu",
+ .username_attribute = "email",
+ .active_attribute = "",
+ .active_value = "",
+ .client_id = "",
+ .client_secret = "",
+ .issuers = "",
+ .openid_configuration_url = "",
+ .pass_attrs = "",
+ .local_validation_key_dict = "",
+ .rawlog_dir = "",
+ .timeout_msecs = 0,
+ .max_idle_time_msecs = 60000,
+ .max_parallel_connections = 10,
+ .max_pipelined_requests = 1,
+ .tls_ca_cert_file = NULL,
+ .tls_ca_cert_dir = NULL,
+ .tls_cert_file = NULL,
+ .tls_key_file = NULL,
+ .tls_cipher_suite = "HIGH:!SSLv2",
+ .tls_allow_invalid_cert = FALSE,
+ .send_auth_headers = FALSE,
+ .use_grant_password = FALSE,
+ .debug = FALSE,
+};
+
+static const char *parse_setting(const char *key, const char *value,
+ struct db_oauth2 *db)
+{
+ return parse_setting_from_defs(db->pool, setting_defs,
+ &db->set, key, value);
+}
+
+struct db_oauth2 *db_oauth2_init(const char *config_path)
+{
+ struct db_oauth2 *db;
+ const char *error;
+ struct ssl_iostream_settings ssl_set;
+ struct http_client_settings http_set;
+
+ for(db = db_oauth2_head; db != NULL; db = db->next) {
+ if (strcmp(db->config_path, config_path) == 0) {
+ db->refcount++;
+ return db;
+ }
+ }
+
+ pool_t pool = pool_alloconly_create("db_oauth2", 128);
+ db = p_new(pool, struct db_oauth2, 1);
+ db->pool = pool;
+ db->refcount = 1;
+ db->config_path = p_strdup(pool, config_path);
+ db->set = default_oauth2_settings;
+
+ if (!settings_read_nosection(config_path, parse_setting, db, &error))
+ i_fatal("oauth2 %s: %s", config_path, error);
+
+ db->tmpl = passdb_template_build(pool, db->set.pass_attrs);
+
+ i_zero(&ssl_set);
+ i_zero(&http_set);
+
+ ssl_set.cipher_list = db->set.tls_cipher_suite;
+ ssl_set.ca_file = db->set.tls_ca_cert_file;
+ ssl_set.ca_dir = db->set.tls_ca_cert_dir;
+ if (db->set.tls_cert_file != NULL && *db->set.tls_cert_file != '\0') {
+ ssl_set.cert.cert = db->set.tls_cert_file;
+ ssl_set.cert.key = db->set.tls_key_file;
+ }
+ ssl_set.prefer_server_ciphers = TRUE;
+ ssl_set.allow_invalid_cert = db->set.tls_allow_invalid_cert;
+ ssl_set.verbose = db->set.debug;
+ ssl_set.verbose_invalid_cert = db->set.debug;
+ http_set.ssl = &ssl_set;
+
+ http_set.dns_client_socket_path = "dns-client";
+ http_set.user_agent = "dovecot-oauth2-passdb/" DOVECOT_VERSION;
+
+ if (*db->set.local_validation_key_dict == '\0' &&
+ *db->set.tokeninfo_url == '\0' &&
+ (*db->set.grant_url == '\0' || *db->set.client_id == '\0') &&
+ *db->set.introspection_url == '\0')
+ i_fatal("oauth2: Password grant, tokeninfo, introspection URL or "
+ "validation key dictionary must be given");
+
+ if (*db->set.rawlog_dir != '\0')
+ http_set.rawlog_dir = db->set.rawlog_dir;
+
+ http_set.max_idle_time_msecs = db->set.max_idle_time_msecs;
+ http_set.max_parallel_connections = db->set.max_parallel_connections;
+ http_set.max_pipelined_requests = db->set.max_pipelined_requests;
+ http_set.no_auto_redirect = FALSE;
+ http_set.no_auto_retry = TRUE;
+ http_set.debug = db->set.debug;
+ http_set.event_parent = auth_event;
+
+ db->client = http_client_init(&http_set);
+
+ i_zero(&db->oauth2_set);
+ db->oauth2_set.client = db->client;
+ db->oauth2_set.tokeninfo_url = db->set.tokeninfo_url,
+ db->oauth2_set.grant_url = db->set.grant_url,
+ db->oauth2_set.introspection_url = db->set.introspection_url;
+ db->oauth2_set.client_id = db->set.client_id;
+ db->oauth2_set.client_secret = db->set.client_secret;
+ db->oauth2_set.timeout_msecs = db->set.timeout_msecs;
+ db->oauth2_set.send_auth_headers = db->set.send_auth_headers;
+ db->oauth2_set.use_grant_password = db->set.use_grant_password;
+ db->oauth2_set.scope = db->set.scope;
+
+ if (*db->set.active_attribute == '\0' &&
+ *db->set.active_value != '\0')
+ i_fatal("oauth2: Cannot have empty active_attribute is active_value is set");
+
+ if (*db->set.introspection_mode == '\0' ||
+ strcmp(db->set.introspection_mode, "auth") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET_AUTH;
+ } else if (strcmp(db->set.introspection_mode, "get") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET;
+ } else if (strcmp(db->set.introspection_mode, "post") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_POST;
+ } else if (strcmp(db->set.introspection_mode, "local") == 0) {
+ if (*db->set.local_validation_key_dict == '\0')
+ i_fatal("oauth2: local_validation_key_dict is required "
+ "for local introspection.");
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_LOCAL;
+ } else {
+ i_fatal("oauth2: Invalid value '%s' for introspection mode, must be on auth, get, post or local",
+ db->set.introspection_mode);
+ }
+
+ if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL) {
+ struct dict_settings dict_set = {
+ .base_dir = global_auth_settings->base_dir,
+ .event_parent = auth_event,
+ };
+ if (dict_init(db->set.local_validation_key_dict, &dict_set,
+ &db->oauth2_set.key_dict, &error) < 0)
+ i_fatal("Cannot initialize key dict: %s", error);
+ /* failure to initialize dcrypt is not fatal - we can still
+ validate HMAC based keys */
+ (void)dcrypt_initialize(NULL, NULL, NULL);
+ /* initialize key cache */
+ db->oauth2_set.key_cache = oauth2_validation_key_cache_init();
+ }
+
+ if (*db->set.issuers != '\0')
+ db->oauth2_set.issuers = (const char *const *)
+ p_strsplit_spaces(pool, db->set.issuers, " ");
+
+ if (*db->set.openid_configuration_url != '\0') {
+ struct http_url *parsed_url ATTR_UNUSED;
+ if (http_url_parse(db->set.openid_configuration_url, NULL, 0,
+ pool_datastack_create(), &parsed_url,
+ &error) < 0) {
+ i_fatal("Invalid openid_configuration_url: %s",
+ error);
+ }
+ }
+
+ DLLIST_PREPEND(&db_oauth2_head, db);
+
+ return db;
+}
+
+void db_oauth2_ref(struct db_oauth2 *db)
+{
+ i_assert(db->refcount > 0);
+ db->refcount++;
+}
+
+void db_oauth2_unref(struct db_oauth2 **_db)
+{
+ struct db_oauth2 *ptr, *db = *_db;
+ i_assert(db->refcount > 0);
+
+ if (--db->refcount > 0) return;
+
+ for(ptr = db_oauth2_head; ptr != NULL; ptr = ptr->next) {
+ if (ptr == db) {
+ DLLIST_REMOVE(&db_oauth2_head, ptr);
+ break;
+ }
+ }
+
+ i_assert(ptr != NULL && ptr == db);
+
+ /* make sure all requests are aborted */
+ while (db->head != NULL)
+ oauth2_request_abort(&db->head->req);
+
+ http_client_deinit(&db->client);
+ if (db->oauth2_set.key_dict != NULL)
+ dict_deinit(&db->oauth2_set.key_dict);
+ oauth2_validation_key_cache_deinit(&db->oauth2_set.key_cache);
+ pool_unref(&db->pool);
+}
+
+static void
+db_oauth2_add_openid_config_url(struct db_oauth2_request *req)
+{
+ /* FIXME: HORRIBLE HACK - REMOVE ME!!!
+ It is because the mech has not been implemented properly
+ that we need to pass the config url in this strange way.
+
+ This **must** be moved to mech-oauth2 once the validation
+ result et al is handled there.
+ */
+ req->auth_request->openid_config_url =
+ p_strdup_empty(req->auth_request->pool,
+ req->db->set.openid_configuration_url);
+}
+
+const char *db_oauth2_get_openid_configuration_url(const struct db_oauth2 *db)
+{
+ return db->set.openid_configuration_url;
+}
+
+static bool
+db_oauth2_have_all_fields(struct db_oauth2_request *req)
+{
+ unsigned int n,i;
+ unsigned int size,idx;
+ const char *const *args = passdb_template_get_args(req->db->tmpl, &n);
+
+ if (req->fields == NULL)
+ return FALSE;
+
+ for(i=1;i<n;i+=2) {
+ const char *ptr = args[i];
+ while(ptr != NULL) {
+ ptr = strchr(ptr, '%');
+ if (ptr != NULL) {
+ const char *field;
+ ptr++;
+ var_get_key_range(ptr, &idx, &size);
+ ptr = ptr+idx;
+ field = t_strndup(ptr,size);
+ if (str_begins(field, "oauth2:") &&
+ !auth_fields_exists(req->fields, field+7))
+ return FALSE;
+ ptr = ptr+size;
+ }
+ }
+ }
+
+ if (!auth_fields_exists(req->fields, req->db->set.username_attribute))
+ return FALSE;
+ if (*req->db->set.active_attribute != '\0' && !auth_fields_exists(req->fields, req->db->set.active_attribute))
+ return FALSE;
+
+ return TRUE;
+}
+
+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;
+ }
+}
+
+static int db_oauth2_var_expand_func_oauth2(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct db_oauth2_request *ctx = context;
+ const char *field_name = t_strcut(data, ':');
+ const char *value = NULL;
+
+ if (ctx->fields != NULL)
+ value = auth_fields_find(ctx->fields, field_name);
+ *value_r = value != NULL ? value : field_get_default(data);
+
+ return 1;
+}
+
+static const char *escape_none(const char *value, const struct auth_request *req ATTR_UNUSED)
+{
+ return value;
+}
+
+static const struct var_expand_table *
+db_oauth2_value_get_var_expand_table(struct auth_request *auth_request,
+ const char *oauth2_value)
+{
+ struct var_expand_table *table;
+ unsigned int count = 1;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, NULL, &count);
+ table[0].key = '$';
+ table[0].value = oauth2_value;
+ return table;
+}
+
+static bool
+db_oauth2_template_export(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ /* var=$ expands into var=${oauth2:var} */
+ const struct var_expand_func_table funcs_table[] = {
+ { "oauth2", db_oauth2_var_expand_func_oauth2 },
+ { NULL, NULL }
+ };
+ string_t *dest;
+ const char *const *args, *value, *error;
+ struct passdb_template *tmpl = req->db->tmpl;
+ unsigned int i, count;
+
+ if (passdb_template_is_empty(tmpl))
+ return TRUE;
+
+ dest = t_str_new(256);
+ args = passdb_template_get_args(tmpl, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (args[i+1] == NULL)
+ value = "";
+ else {
+ str_truncate(dest, 0);
+ const struct var_expand_table *
+ table = db_oauth2_value_get_var_expand_table(req->auth_request,
+ auth_fields_find(req->fields, args[i]));
+ if (var_expand_with_funcs(dest, args[i+1], table, funcs_table,
+ req, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "var_expand(%s) failed: %s",
+ args[i+1], error);
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ return FALSE;
+ }
+ value = str_c(dest);
+ }
+
+ auth_request_set_field(req->auth_request, args[i], value,
+ STATIC_PASS_SCHEME);
+ }
+ return TRUE;
+}
+
+static void db_oauth2_fields_merge(struct db_oauth2_request *req,
+ ARRAY_TYPE(oauth2_field) *fields)
+{
+ const struct oauth2_field *field;
+
+ if (req->fields == NULL)
+ req->fields = auth_fields_init(req->pool);
+
+ array_foreach(fields, field) {
+ e_debug(authdb_event(req->auth_request),
+ "Processing field %s",
+ field->name);
+ auth_fields_add(req->fields, field->name, field->value, 0);
+ }
+}
+
+static const char *
+db_oauth2_field_find(const ARRAY_TYPE(oauth2_field) *fields, const char *name)
+{
+ const struct oauth2_field *f;
+
+ array_foreach(fields, f) {
+ if (strcmp(f->name, name) == 0)
+ return f->value;
+ }
+ return NULL;
+}
+
+static void db_oauth2_callback(struct db_oauth2_request *req,
+ enum passdb_result result,
+ const char *error_prefix, const char *error)
+{
+ db_oauth2_lookup_callback_t *callback = req->callback;
+ req->callback = NULL;
+
+ i_assert(result == PASSDB_RESULT_OK || error != NULL);
+
+ if (result != PASSDB_RESULT_OK)
+ db_oauth2_add_openid_config_url(req);
+
+ /* Successful lookups were logged by the caller. Failed lookups will be
+ logged either with e_error() or e_info() by the callback. */
+ if (callback != NULL) {
+ DLLIST_REMOVE(&req->db->head, req);
+ if (result != PASSDB_RESULT_OK)
+ error = t_strconcat(error_prefix, error, NULL);
+ callback(req, result, error, req->context);
+ }
+}
+
+static bool
+db_oauth2_validate_username(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ const char *error;
+ struct var_expand_table table[] = {
+ { 'u', NULL, "user" },
+ { 'n', NULL, "username" },
+ { 'd', NULL, "domain" },
+ { '\0', NULL, NULL }
+ };
+ const char *username_value =
+ auth_fields_find(req->fields, req->db->set.username_attribute);
+
+ if (username_value == NULL) {
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ *error_r = "No username returned";
+ return FALSE;
+ }
+
+ table[0].value = username_value;
+ table[1].value = t_strcut(username_value, '@');
+ table[2].value = i_strchr_to_next(username_value, '@');
+
+ string_t *username_req = t_str_new(32);
+ string_t *username_val = t_str_new(strlen(username_value));
+
+ if (auth_request_var_expand(username_req, req->db->set.username_format, req->auth_request, escape_none, &error) < 0 ||
+ var_expand(username_val, req->db->set.username_format, table, &error) < 0) {
+ *error_r = t_strdup_printf("var_expand(%s) failed: %s",
+ req->db->set.username_format, error);
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ return FALSE;
+ } else if (!str_equals(username_req, username_val)) {
+ *error_r = t_strdup_printf("Username '%s' did not match '%s'",
+ str_c(username_req), str_c(username_val));
+ *result_r = PASSDB_RESULT_USER_UNKNOWN;
+ return FALSE;
+ } else {
+ req->username = p_strdup(req->pool, str_c(username_val));
+ return TRUE;
+ }
+}
+
+static bool
+db_oauth2_user_is_enabled(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ if (*req->db->set.active_attribute == '\0' ) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute is not configured; skipping the check");
+ return TRUE;
+ }
+
+ const char *active_value =
+ auth_fields_find(req->fields, req->db->set.active_attribute);
+
+ if (active_value == NULL) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute \"%s\" is not present in the oauth2 server's response",
+ req->db->set.active_attribute);
+ *error_r = "Missing active_attribute from token";
+ *result_r = PASSDB_RESULT_PASSWORD_MISMATCH;
+ return FALSE;
+ }
+
+ if (*req->db->set.active_value == '\0') {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute \"%s\" present; skipping the check on value",
+ req->db->set.active_attribute);
+ return TRUE;
+ }
+
+ if (strcmp(req->db->set.active_value, active_value) != 0) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute check failed: expected %s=\"%s\" but got \"%s\"",
+ req->db->set.active_attribute,
+ req->db->set.active_value,
+ active_value);
+ *error_r = "Provided token is not valid";
+ *result_r = PASSDB_RESULT_PASSWORD_MISMATCH;
+ return FALSE;
+ }
+
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute check succeeded");
+ return TRUE;
+}
+
+static bool
+db_oauth2_token_in_scope(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ if (*req->db->set.scope != '\0') {
+ bool found = FALSE;
+ const char *value = auth_fields_find(req->fields, "scope");
+ if (value == NULL)
+ value = auth_fields_find(req->fields, "aud");
+ e_debug(authdb_event(req->auth_request),
+ "Token scope(s): %s",
+ value);
+ if (value != NULL) {
+ const char **wanted_scopes =
+ t_strsplit_spaces(req->db->set.scope, " ");
+ const char **scopes = t_strsplit_spaces(value, " ");
+ for (; !found && *wanted_scopes != NULL; wanted_scopes++)
+ found = str_array_find(scopes, *wanted_scopes);
+ }
+ if (!found) {
+ *error_r = t_strdup_printf("Token is not valid for scope '%s'",
+ req->db->set.scope);
+ *result_r = PASSDB_RESULT_USER_DISABLED;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void db_oauth2_process_fields(struct db_oauth2_request *req,
+ enum passdb_result *result_r,
+ const char **error_r)
+{
+ *error_r = NULL;
+
+ if (db_oauth2_user_is_enabled(req, result_r, error_r) &&
+ db_oauth2_validate_username(req, result_r, error_r) &&
+ db_oauth2_token_in_scope(req, result_r, error_r) &&
+ db_oauth2_template_export(req, result_r, error_r)) {
+ *result_r = PASSDB_RESULT_OK;
+ } else {
+ i_assert(*result_r != PASSDB_RESULT_OK && *error_r != NULL);
+ }
+}
+
+static void
+db_oauth2_introspect_continue(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ enum passdb_result passdb_result;
+ const char *error;
+
+ req->req = NULL;
+
+ if (result->error != NULL) {
+ /* fail here */
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ error = result->error;
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Introspection succeeded");
+ db_oauth2_fields_merge(req, result->fields);
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ }
+ db_oauth2_callback(req, passdb_result, "Introspection failed: ", error);
+}
+
+static void db_oauth2_lookup_introspect(struct db_oauth2_request *req)
+{
+ struct oauth2_request_input input;
+ i_zero(&input);
+
+ e_debug(authdb_event(req->auth_request),
+ "Making introspection request to %s",
+ req->db->set.introspection_url);
+ input.token = req->token;
+ input.local_ip = req->auth_request->fields.local_ip;
+ input.local_port = req->auth_request->fields.local_port;
+ input.remote_ip = req->auth_request->fields.remote_ip;
+ input.remote_port = req->auth_request->fields.remote_port;
+ input.real_local_ip = req->auth_request->fields.real_local_ip;
+ input.real_local_port = req->auth_request->fields.real_local_port;
+ input.real_remote_ip = req->auth_request->fields.real_remote_ip;
+ input.real_remote_port = req->auth_request->fields.real_remote_port;
+ input.service = req->auth_request->fields.service;
+
+ req->req = oauth2_introspection_start(&req->db->oauth2_set, &input,
+ db_oauth2_introspect_continue, req);
+}
+
+static void db_oauth2_local_validation(struct db_oauth2_request *req,
+ const char *token)
+{
+ bool is_jwt ATTR_UNUSED;
+ const char *error = NULL;
+ enum passdb_result passdb_result;
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+ if (oauth2_try_parse_jwt(&req->db->oauth2_set, token,
+ &fields, &is_jwt, &error) < 0) {
+ passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ } else {
+ db_oauth2_fields_merge(req, &fields);
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ }
+ if (passdb_result == PASSDB_RESULT_OK) {
+ e_debug(authdb_event(req->auth_request),
+ "Local validation succeeded");
+ }
+ db_oauth2_callback(req, passdb_result,
+ "Local validation failed: ", error);
+}
+
+static void
+db_oauth2_lookup_continue_valid(struct db_oauth2_request *req,
+ ARRAY_TYPE(oauth2_field) *fields,
+ const char *error_prefix)
+{
+ enum passdb_result passdb_result;
+ const char *error;
+
+ db_oauth2_fields_merge(req, fields);
+ if (db_oauth2_have_all_fields(req) &&
+ !req->db->set.force_introspection) {
+ /* pass */
+ } else if (req->db->oauth2_set.introspection_mode ==
+ INTROSPECTION_MODE_LOCAL) {
+ e_debug(authdb_event(req->auth_request),
+ "Attempting to locally validate token");
+ db_oauth2_local_validation(req, req->token);
+ return;
+ } else if (!db_oauth2_user_is_enabled(req, &passdb_result, &error)) {
+ db_oauth2_callback(req, passdb_result,
+ "Token is not valid: ", error);
+ return;
+ } else if (*req->db->set.introspection_url != '\0') {
+ db_oauth2_lookup_introspect(req);
+ return;
+ }
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ db_oauth2_callback(req, passdb_result, error_prefix, error);
+}
+
+static void
+db_oauth2_lookup_continue(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ i_assert(req->token != NULL);
+ req->req = NULL;
+
+ if (result->error != NULL) {
+ db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE,
+ "Token validation failed: ", result->error);
+ } else if (!result->valid) {
+ db_oauth2_callback(req, PASSDB_RESULT_PASSWORD_MISMATCH,
+ "Token validation failed: ",
+ "Invalid token");
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Token validation succeeded");
+ db_oauth2_lookup_continue_valid(req, result->fields,
+ "Token validation failed: ");
+ }
+}
+
+static void
+db_oauth2_lookup_passwd_grant(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ enum passdb_result passdb_result;
+ const char *token, *error;
+
+ i_assert(req->token == NULL);
+ req->req = NULL;
+
+ if (result->valid) {
+ e_debug(authdb_event(req->auth_request),
+ "Password grant succeeded");
+ token = db_oauth2_field_find(result->fields, "access_token");
+ if (token == NULL) {
+ db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE,
+ "Password grant failed: ",
+ "OAuth2 token missing from reply");
+ } else {
+ req->token = p_strdup(req->pool, token);
+ db_oauth2_lookup_continue_valid(req, result->fields,
+ "Password grant failed: ");
+ }
+ return;
+ }
+
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ if (result->error != NULL)
+ error = result->error;
+ else {
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ error = db_oauth2_field_find(result->fields, "error");
+ if (error == NULL)
+ error = "OAuth2 server returned failure without error field";
+ else if (strcmp("invalid_grant", error) == 0)
+ passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ }
+ db_oauth2_callback(req, passdb_result,
+ "Password grant failed: ", error);
+}
+
+#undef db_oauth2_lookup
+void db_oauth2_lookup(struct db_oauth2 *db, struct db_oauth2_request *req,
+ const char *token, struct auth_request *request,
+ db_oauth2_lookup_callback_t *callback, void *context)
+{
+ struct oauth2_request_input input;
+ i_zero(&input);
+
+ req->db = db;
+ req->token = p_strdup(req->pool, token);
+ req->callback = callback;
+ req->context = context;
+ req->auth_request = request;
+
+ input.token = token;
+ input.local_ip = req->auth_request->fields.local_ip;
+ input.local_port = req->auth_request->fields.local_port;
+ input.remote_ip = req->auth_request->fields.remote_ip;
+ input.remote_port = req->auth_request->fields.remote_port;
+ input.real_local_ip = req->auth_request->fields.real_local_ip;
+ input.real_local_port = req->auth_request->fields.real_local_port;
+ input.real_remote_ip = req->auth_request->fields.real_remote_ip;
+ input.real_remote_port = req->auth_request->fields.real_remote_port;
+ input.service = req->auth_request->fields.service;
+
+ if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL &&
+ !db_oauth2_uses_password_grant(db)) {
+ /* try to validate token locally */
+ e_debug(authdb_event(req->auth_request),
+ "Attempting to locally validate token");
+ db_oauth2_local_validation(req, request->mech_password);
+ return;
+
+ }
+ if (db->oauth2_set.use_grant_password) {
+ e_debug(authdb_event(req->auth_request),
+ "Making grant url request to %s",
+ db->set.grant_url);
+ /* There is no valid token until grant looks it up. */
+ req->token = NULL;
+ req->req = oauth2_passwd_grant_start(&db->oauth2_set, &input,
+ request->fields.user, request->mech_password,
+ db_oauth2_lookup_passwd_grant, req);
+ } else if (*db->oauth2_set.tokeninfo_url == '\0') {
+ e_debug(authdb_event(req->auth_request),
+ "Making introspection request to %s",
+ db->set.introspection_url);
+ req->req = oauth2_introspection_start(&req->db->oauth2_set, &input,
+ db_oauth2_introspect_continue, req);
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Making token validation lookup to %s",
+ db->oauth2_set.tokeninfo_url);
+ req->req = oauth2_token_validation_start(&db->oauth2_set, &input,
+ db_oauth2_lookup_continue, req);
+ }
+ i_assert(req->req != NULL);
+ DLLIST_PREPEND(&db->head, req);
+}
+
+bool db_oauth2_uses_password_grant(const struct db_oauth2 *db)
+{
+ return db->set.use_grant_password;
+}
diff --git a/src/auth/db-oauth2.h b/src/auth/db-oauth2.h
new file mode 100644
index 0000000..c2d2d37
--- /dev/null
+++ b/src/auth/db-oauth2.h
@@ -0,0 +1,48 @@
+#ifndef DB_OAUTH2_H
+#define DB_OAUTH2_H 1
+
+struct db_oauth2;
+struct oauth2_request;
+struct db_oauth2_request;
+
+typedef void db_oauth2_lookup_callback_t(struct db_oauth2_request *request,
+ enum passdb_result result,
+ const char *error,
+ void *context);
+struct db_oauth2_request {
+ pool_t pool;
+ struct db_oauth2_request *prev,*next;
+
+ struct db_oauth2 *db;
+ struct oauth2_request *req;
+
+ /* username to match */
+ const char *username;
+ /* token to use */
+ const char *token;
+
+ struct auth_request *auth_request;
+ struct auth_fields *fields;
+
+ db_oauth2_lookup_callback_t *callback;
+ void *context;
+ verify_plain_callback_t *verify_callback;
+};
+
+
+struct db_oauth2 *db_oauth2_init(const char *config_path);
+
+void db_oauth2_ref(struct db_oauth2 *);
+void db_oauth2_unref(struct db_oauth2 **);
+
+bool db_oauth2_uses_password_grant(const struct db_oauth2 *db);
+
+const char *db_oauth2_get_openid_configuration_url(const struct db_oauth2 *db);
+
+void db_oauth2_lookup(struct db_oauth2 *db, struct db_oauth2_request *req, const char *token, struct auth_request *request, db_oauth2_lookup_callback_t *callback, void *context);
+#define db_oauth2_lookup(db, req, token, request, callback, context) \
+ db_oauth2_lookup(db, req, token - \
+ CALLBACK_TYPECHECK(callback, void(*)(struct db_oauth2_request *, enum passdb_result, const char*, typeof(context))), \
+ request, (db_oauth2_lookup_callback_t*)callback, (void*)context)
+
+#endif
diff --git a/src/auth/db-passwd-file.c b/src/auth/db-passwd-file.c
new file mode 100644
index 0000000..e26c146
--- /dev/null
+++ b/src/auth/db-passwd-file.c
@@ -0,0 +1,493 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#if defined (USERDB_PASSWD_FILE) || defined(PASSDB_PASSWD_FILE)
+
+#include "userdb.h"
+#include "db-passwd-file.h"
+
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "hash.h"
+#include "str.h"
+#include "eacces-error.h"
+#include "ioloop.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#include <sys/stat.h>
+
+#define PARSE_TIME_STARTUP_WARN_SECS 60
+#define PARSE_TIME_RELOAD_WARN_SECS 10
+
+static struct db_passwd_file *passwd_files;
+
+static void ATTR_NULL(3)
+passwd_file_add(struct passwd_file *pw, const char *username,
+ const char *pass, const char *const *args)
+{
+ /* args = uid, gid, user info, home dir, shell, extra_fields */
+ struct passwd_user *pu;
+ const char *extra_fields = NULL;
+ char *user;
+ size_t len;
+
+ if (hash_table_lookup(pw->users, username) != NULL) {
+ e_error(pw->event, "User %s exists more than once", username);
+ return;
+ }
+
+ pu = p_new(pw->pool, struct passwd_user, 1);
+ user = p_strdup(pw->pool, username);
+
+ len = pass == NULL ? 0 : strlen(pass);
+ if (len > 4 && pass[0] != '{' && pass[0] != '$' &&
+ pass[len-1] == ']' && pass[len-4] == '[') {
+ /* password[type] - we're being libpam-pwdfile compatible
+ here. it uses 13 = DES and 34 = MD5. For backwards
+ compatibility with ourself, we have also 56 = Digest-MD5. */
+ int num = (pass[len-3] - '0') * 10 + (pass[len-2] - '0');
+
+ pass = t_strndup(pass, len-4);
+ if (num == 34) {
+ pu->password = p_strconcat(pw->pool, "{PLAIN-MD5}",
+ pass, NULL);
+ } else if (num == 56) {
+ pu->password = p_strconcat(pw->pool, "{DIGEST-MD5}",
+ pass, NULL);
+ if (strlen(pu->password) != 32 + 12) {
+ e_error(pw->event, "User %s "
+ "has invalid password", username);
+ return;
+ }
+ } else {
+ pu->password = p_strconcat(pw->pool, "{CRYPT}",
+ pass, NULL);
+ }
+ } else {
+ pu->password = p_strdup(pw->pool, pass);
+ }
+
+ pu->uid = (uid_t)-1;
+ pu->gid = (gid_t)-1;
+
+ if (*args == NULL)
+ ;
+ else if (!pw->db->userdb || **args == '\0') {
+ args++;
+ } else {
+ pu->uid = userdb_parse_uid(NULL, *args);
+ if (pu->uid == 0 || pu->uid == (uid_t)-1) {
+ e_error(pw->event, "User %s has invalid UID '%s'",
+ username, *args);
+ return;
+ }
+ args++;
+ }
+
+ if (*args == NULL) {
+ if (pw->db->userdb_warn_missing) {
+ e_error(pw->event, "User %s is missing userdb info",
+ username);
+ }
+ /* don't allow userdb lookups */
+ pu->uid = 0;
+ pu->gid = 0;
+ } else if (!pw->db->userdb || **args == '\0')
+ args++;
+ else {
+ pu->gid = userdb_parse_gid(NULL, *args);
+ if (pu->gid == 0 || pu->gid == (gid_t)-1) {
+ e_error(pw->event, "User %s has invalid GID '%s'",
+ username, *args);
+ return;
+ }
+ args++;
+ }
+
+ /* user info */
+ if (*args != NULL)
+ args++;
+
+ /* home */
+ if (*args != NULL) {
+ if (pw->db->userdb)
+ pu->home = p_strdup_empty(pw->pool, *args);
+ args++;
+ }
+
+ /* shell */
+ if (*args != NULL)
+ args++;
+
+ if (*args != NULL && **args == '\0') {
+ /* old format, this field is empty and next field may
+ contain MAIL */
+ args++;
+ if (*args != NULL && **args != '\0' && pw->db->userdb) {
+ extra_fields =
+ t_strconcat("userdb_mail=",
+ t_strarray_join(args, ":"), NULL);
+ }
+ } else if (*args != NULL) {
+ /* new format, contains a space separated list of
+ extra fields */
+ extra_fields = t_strarray_join(args, ":");
+ }
+
+ if (extra_fields != NULL) {
+ pu->extra_fields =
+ p_strsplit_spaces(pw->pool, extra_fields, " ");
+ }
+
+ hash_table_insert(pw->users, user, pu);
+}
+
+static struct passwd_file *
+passwd_file_new(struct db_passwd_file *db, const char *expanded_path)
+{
+ struct passwd_file *pw;
+
+ pw = i_new(struct passwd_file, 1);
+ pw->db = db;
+ pw->path = i_strdup(expanded_path);
+ pw->fd = -1;
+ pw->event = event_create(db->event);
+ event_set_append_log_prefix(pw->event,
+ t_strdup_printf("passwd-file %s:", pw->path));
+
+ if (hash_table_is_created(db->files))
+ hash_table_insert(db->files, pw->path, pw);
+ return pw;
+}
+
+static int passwd_file_open(struct passwd_file *pw, bool startup,
+ const char **error_r)
+{
+ const char *no_args = NULL;
+ struct istream *input;
+ const char *line;
+ struct stat st;
+ time_t start_time, end_time;
+ unsigned int time_secs;
+ int fd;
+
+ fd = open(pw->path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == EACCES)
+ *error_r = eacces_error_get("open", pw->path);
+ else {
+ *error_r = t_strdup_printf("open(%s) failed: %m",
+ pw->path);
+ }
+ return -1;
+ }
+
+ if (fstat(fd, &st) != 0) {
+ *error_r = t_strdup_printf("fstat(%s) failed: %m",
+ pw->path);
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ pw->fd = fd;
+ pw->stamp = st.st_mtime;
+ pw->size = st.st_size;
+
+ pw->pool = pool_alloconly_create(MEMPOOL_GROWING"passwd_file", 10240);
+ hash_table_create(&pw->users, pw->pool, 0, str_hash, strcmp);
+
+ start_time = time(NULL);
+ input = i_stream_create_fd(pw->fd, SIZE_MAX);
+ i_stream_set_return_partial_line(input, TRUE);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (*line == '\0' || *line == ':' || *line == '#')
+ continue; /* no username or comment */
+
+ T_BEGIN {
+ const char *const *args = t_strsplit(line, ":");
+ if (args[1] != NULL) {
+ /* at least username+password */
+ passwd_file_add(pw, args[0], args[1], args+2);
+ } else {
+ /* only username */
+ passwd_file_add(pw, args[0], NULL, &no_args);
+ }
+ } T_END;
+ }
+ i_stream_destroy(&input);
+ end_time = time(NULL);
+ time_secs = end_time - start_time;
+
+ if ((time_secs > PARSE_TIME_STARTUP_WARN_SECS && startup) ||
+ (time_secs > PARSE_TIME_RELOAD_WARN_SECS && !startup)) {
+ e_warning(pw->event, "Reading %u users took %u secs",
+ hash_table_count(pw->users), time_secs);
+ } else {
+ e_debug(pw->event, "Read %u users in %u secs",
+ hash_table_count(pw->users), time_secs);
+ }
+ return 0;
+}
+
+static void passwd_file_close(struct passwd_file *pw)
+{
+ i_close_fd_path(&pw->fd, pw->path);
+
+ hash_table_destroy(&pw->users);
+ pool_unref(&pw->pool);
+}
+
+static void passwd_file_free(struct passwd_file *pw)
+{
+ if (hash_table_is_created(pw->db->files))
+ hash_table_remove(pw->db->files, pw->path);
+
+ passwd_file_close(pw);
+ event_unref(&pw->event);
+ i_free(pw->path);
+ i_free(pw);
+}
+
+static int passwd_file_sync(struct auth_request *request,
+ struct passwd_file *pw)
+{
+ struct stat st;
+ const char *error;
+
+ if (pw->last_sync_time == ioloop_time)
+ return hash_table_is_created(pw->users) ? 1 : -1;
+ pw->last_sync_time = ioloop_time;
+
+ if (stat(pw->path, &st) < 0) {
+ /* with variables don't give hard errors, or errors about
+ nonexistent files */
+ int ret = -1;
+
+ if (errno == EACCES) {
+ e_error(authdb_event(request),
+ "%s", eacces_error_get("stat", pw->path));
+ } else if (errno == ENOENT) {
+ auth_request_log_info(request, "passwd-file",
+ "missing passwd file: %s", pw->path);
+ ret = 0;
+ } else {
+ e_error(authdb_event(request),
+ "stat(%s) failed: %m", pw->path);
+ }
+
+ if (pw->db->default_file != pw)
+ passwd_file_free(pw);
+ return ret;
+ }
+
+ if (st.st_mtime != pw->stamp || st.st_size != pw->size) {
+ passwd_file_close(pw);
+ if (passwd_file_open(pw, FALSE, &error) < 0) {
+ e_error(authdb_event(request),
+ "%s", error);
+ return -1;
+ }
+ }
+ return 1;
+}
+
+static struct db_passwd_file *db_passwd_file_find(const char *path)
+{
+ struct db_passwd_file *f;
+
+ for (f = passwd_files; f != NULL; f = f->next) {
+ if (strcmp(f->path, path) == 0)
+ return f;
+ }
+
+ return NULL;
+}
+
+static void db_passwd_file_set_userdb(struct db_passwd_file *db)
+{
+ db->userdb = TRUE;
+ /* warn about missing userdb fields only when there aren't any other
+ userdbs. */
+ db->userdb_warn_missing =
+ array_is_created(&global_auth_settings->userdbs) &&
+ array_count(&global_auth_settings->userdbs) == 1;
+}
+
+struct db_passwd_file *
+db_passwd_file_init(const char *path, bool userdb, bool debug)
+{
+ struct db_passwd_file *db;
+ const char *p;
+ bool percents = FALSE;
+
+ db = db_passwd_file_find(path);
+ if (db != NULL) {
+ db->refcount++;
+ if (userdb)
+ db_passwd_file_set_userdb(db);
+ return db;
+ }
+
+ db = i_new(struct db_passwd_file, 1);
+ db->refcount = 1;
+ if (userdb)
+ db_passwd_file_set_userdb(db);
+ db->event = event_create(auth_event);
+ event_set_forced_debug(db->event, debug);
+
+ for (p = path; *p != '\0'; p++) {
+ if (*p == '%' && p[1] != '\0') {
+ if (var_get_key(++p) == '%')
+ percents = TRUE;
+ else
+ db->vars = TRUE;
+ }
+ }
+
+ if (percents && !db->vars) {
+ /* just extra escaped % chars. remove them. */
+ struct var_expand_table empty_table[1] = {
+ { .key = '\0' },
+ };
+ string_t *dest;
+ const char *error;
+
+ dest = t_str_new(256);
+ if (var_expand(dest, path, empty_table, &error) <= 0)
+ i_unreached();
+ path = str_c(dest);
+ }
+
+ db->path = i_strdup(path);
+ if (db->vars) {
+ hash_table_create(&db->files, default_pool, 0,
+ str_hash, strcmp);
+ } else {
+ db->default_file = passwd_file_new(db, path);
+ }
+
+ db->next = passwd_files;
+ passwd_files = db;
+ return db;
+}
+
+void db_passwd_file_parse(struct db_passwd_file *db)
+{
+ const char *error;
+
+ if (db->default_file != NULL && db->default_file->stamp == 0) {
+ /* no variables, open the file immediately */
+ if (passwd_file_open(db->default_file, TRUE, &error) < 0)
+ e_error(db->default_file->event, "%s", error);
+ }
+}
+
+void db_passwd_file_unref(struct db_passwd_file **_db)
+{
+ struct db_passwd_file *db = *_db;
+ struct db_passwd_file **p;
+ struct hash_iterate_context *iter;
+ char *path;
+ struct passwd_file *file;
+
+ *_db = NULL;
+ i_assert(db->refcount >= 0);
+ if (--db->refcount > 0)
+ return;
+
+ for (p = &passwd_files; *p != NULL; p = &(*p)->next) {
+ if (*p == db) {
+ *p = db->next;
+ break;
+ }
+ }
+
+ if (db->default_file != NULL)
+ passwd_file_free(db->default_file);
+ else {
+ iter = hash_table_iterate_init(db->files);
+ while (hash_table_iterate(iter, db->files, &path, &file))
+ passwd_file_free(file);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&db->files);
+ }
+ event_unref(&db->event);
+ i_free(db->path);
+ i_free(db);
+}
+
+static const char *
+path_fix(const char *path,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ const char *p;
+
+ p = strchr(path, '/');
+ if (p == NULL)
+ return path;
+
+ /* most likely this is an invalid request. just cut off the '/' and
+ everything after it. */
+ return t_strdup_until(path, p);
+}
+
+int db_passwd_file_lookup(struct db_passwd_file *db,
+ struct auth_request *request,
+ const char *username_format,
+ struct passwd_user **user_r)
+{
+ struct passwd_file *pw;
+ string_t *username, *dest;
+ const char *error;
+ int ret;
+
+ if (!db->vars)
+ pw = db->default_file;
+ else {
+ dest = t_str_new(256);
+ if (auth_request_var_expand(dest, db->path, request, path_fix,
+ &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand passwd-file path %s: %s",
+ db->path, error);
+ return -1;
+ }
+
+ pw = hash_table_lookup(db->files, str_c(dest));
+ if (pw == NULL) {
+ /* doesn't exist yet. create lookup for it. */
+ pw = passwd_file_new(db, str_c(dest));
+ }
+ }
+
+ if ((ret = passwd_file_sync(request, pw)) <= 0) {
+ /* pw may be freed now */
+ return ret;
+ }
+
+ username = t_str_new(256);
+ if (auth_request_var_expand(username, username_format, request,
+ auth_request_str_escape, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand username_format=%s: %s",
+ username_format, error);
+ return -1;
+ }
+
+ e_debug(authdb_event(request),
+ "lookup: user=%s file=%s",
+ str_c(username), pw->path);
+
+ *user_r = hash_table_lookup(pw->users, str_c(username));
+ if (*user_r == NULL) {
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ return 0;
+ }
+ return 1;
+}
+
+#endif
diff --git a/src/auth/db-passwd-file.h b/src/auth/db-passwd-file.h
new file mode 100644
index 0000000..498f9c3
--- /dev/null
+++ b/src/auth/db-passwd-file.h
@@ -0,0 +1,58 @@
+#ifndef DB_PASSWD_FILE_H
+#define DB_PASSWD_FILE_H
+
+#include "hash.h"
+
+#define PASSWD_FILE_DEFAULT_USERNAME_FORMAT "%u"
+#define PASSWD_FILE_DEFAULT_SCHEME "CRYPT"
+
+struct passwd_user {
+ uid_t uid;
+ gid_t gid;
+
+ char *home;
+ char *password;
+ char **extra_fields;
+};
+
+struct passwd_file {
+ struct db_passwd_file *db;
+ pool_t pool;
+ int refcount;
+ struct event *event;
+
+ time_t last_sync_time;
+ char *path;
+ time_t stamp;
+ off_t size;
+ int fd;
+
+ HASH_TABLE(char *, struct passwd_user *) users;
+};
+
+struct db_passwd_file {
+ struct db_passwd_file *next;
+
+ int refcount;
+ struct event *event;
+
+ char *path;
+ HASH_TABLE(char *, struct passwd_file *) files;
+ struct passwd_file *default_file;
+
+ bool vars:1;
+ bool userdb:1;
+ bool userdb_warn_missing:1;
+};
+
+int db_passwd_file_lookup(struct db_passwd_file *db,
+ struct auth_request *request,
+ const char *username_format,
+ struct passwd_user **user_r);
+
+struct db_passwd_file *
+db_passwd_file_init(const char *path, bool userdb, bool debug);
+void db_passwd_file_parse(struct db_passwd_file *db);
+void db_passwd_file_unref(struct db_passwd_file **db);
+
+#endif
diff --git a/src/auth/db-sql.c b/src/auth/db-sql.c
new file mode 100644
index 0000000..b77edd1
--- /dev/null
+++ b/src/auth/db-sql.c
@@ -0,0 +1,178 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#if defined(PASSDB_SQL) || defined(USERDB_SQL)
+
+#include "settings.h"
+#include "auth-request.h"
+#include "auth-worker-client.h"
+#include "db-sql.h"
+
+#include <stddef.h>
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, db_sql_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, db_sql_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, db_sql_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(driver),
+ DEF_STR(connect),
+ DEF_STR(password_query),
+ DEF_STR(user_query),
+ DEF_STR(update_query),
+ DEF_STR(iterate_query),
+ DEF_STR(default_pass_scheme),
+ DEF_BOOL(userdb_warning_disable),
+
+ { 0, NULL, 0 }
+};
+
+static struct db_sql_settings default_db_sql_settings = {
+ .driver = NULL,
+ .connect = NULL,
+ .password_query = "SELECT username, domain, password FROM users WHERE username = '%n' AND domain = '%d'",
+ .user_query = "SELECT home, uid, gid FROM users WHERE username = '%n' AND domain = '%d'",
+ .update_query = "UPDATE users SET password = '%w' WHERE username = '%n' AND domain = '%d'",
+ .iterate_query = "SELECT username, domain FROM users",
+ .default_pass_scheme = "MD5",
+ .userdb_warning_disable = FALSE
+};
+
+static struct db_sql_connection *connections = NULL;
+
+static struct db_sql_connection *sql_conn_find(const char *config_path)
+{
+ struct db_sql_connection *conn;
+
+ for (conn = connections; conn != NULL; conn = conn->next) {
+ if (strcmp(conn->config_path, config_path) == 0)
+ return conn;
+ }
+
+ return NULL;
+}
+
+static const char *parse_setting(const char *key, const char *value,
+ struct db_sql_connection *conn)
+{
+ return parse_setting_from_defs(conn->pool, setting_defs,
+ &conn->set, key, value);
+}
+
+struct db_sql_connection *db_sql_init(const char *config_path, bool userdb)
+{
+ struct db_sql_connection *conn;
+ struct sql_settings set;
+ const char *error;
+ pool_t pool;
+
+ conn = sql_conn_find(config_path);
+ if (conn != NULL) {
+ if (userdb)
+ conn->userdb_used = TRUE;
+ conn->refcount++;
+ return conn;
+ }
+
+ if (*config_path == '\0')
+ i_fatal("sql: Configuration file path not given");
+
+ pool = pool_alloconly_create("db_sql_connection", 1024);
+ conn = p_new(pool, struct db_sql_connection, 1);
+ conn->pool = pool;
+ conn->userdb_used = userdb;
+
+ conn->refcount = 1;
+
+ conn->config_path = p_strdup(pool, config_path);
+ conn->set = default_db_sql_settings;
+ if (!settings_read_nosection(config_path, parse_setting, conn, &error))
+ i_fatal("sql %s: %s", config_path, error);
+
+ if (conn->set.password_query == default_db_sql_settings.password_query)
+ conn->default_password_query = TRUE;
+ if (conn->set.user_query == default_db_sql_settings.user_query)
+ conn->default_user_query = TRUE;
+ if (conn->set.update_query == default_db_sql_settings.update_query)
+ conn->default_update_query = TRUE;
+ if (conn->set.iterate_query == default_db_sql_settings.iterate_query)
+ conn->default_iterate_query = TRUE;
+
+ if (conn->set.driver == NULL) {
+ i_fatal("sql: driver not set in configuration file %s",
+ config_path);
+ }
+ if (conn->set.connect == NULL) {
+ i_fatal("sql: connect string not set in configuration file %s",
+ config_path);
+ }
+ i_zero(&set);
+ set.driver = conn->set.driver;
+ set.connect_string = conn->set.connect;
+ set.event_parent = auth_event;
+ if (sql_init_full(&set, &conn->db, &error) < 0) {
+ i_fatal("sql: %s", error);
+ }
+
+ conn->next = connections;
+ connections = conn;
+ return conn;
+}
+
+void db_sql_unref(struct db_sql_connection **_conn)
+{
+ struct db_sql_connection *conn = *_conn;
+
+ /* abort all pending auth requests before setting conn to NULL,
+ so that callbacks can still access it */
+ sql_disconnect(conn->db);
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ sql_unref(&conn->db);
+ pool_unref(&conn->pool);
+}
+
+void db_sql_connect(struct db_sql_connection *conn)
+{
+ if (sql_connect(conn->db) < 0 && worker) {
+ /* auth worker's sql connection failed. we can't do anything
+ useful until the connection works. there's no point in
+ having tons of worker processes all logging failures,
+ so tell the auth master to stop creating new workers (and
+ maybe close old ones). this handling is especially useful if
+ we reach the max. number of connections for sql server. */
+ auth_worker_client_send_error();
+ }
+}
+
+void db_sql_success(struct db_sql_connection *conn ATTR_UNUSED)
+{
+ if (worker)
+ auth_worker_client_send_success();
+}
+
+void db_sql_check_userdb_warning(struct db_sql_connection *conn)
+{
+ if (worker || conn->userdb_used || conn->set.userdb_warning_disable)
+ return;
+
+ if (strcmp(conn->set.user_query,
+ default_db_sql_settings.user_query) != 0) {
+ i_warning("sql: Ignoring changed user_query in %s, "
+ "because userdb sql not used. "
+ "(If this is intentional, set userdb_warning_disable=yes)",
+ conn->config_path);
+ } else if (strcmp(conn->set.iterate_query,
+ default_db_sql_settings.iterate_query) != 0) {
+ i_warning("sql: Ignoring changed iterate_query in %s, "
+ "because userdb sql not used. "
+ "(If this is intentional, set userdb_warning_disable=yes)",
+ conn->config_path);
+ }
+}
+
+#endif
diff --git a/src/auth/db-sql.h b/src/auth/db-sql.h
new file mode 100644
index 0000000..27e177b
--- /dev/null
+++ b/src/auth/db-sql.h
@@ -0,0 +1,42 @@
+#ifndef DB_SQL_H
+#define DB_SQL_H
+
+#include "sql-api.h"
+
+struct db_sql_settings {
+ const char *driver;
+ const char *connect;
+ const char *password_query;
+ const char *user_query;
+ const char *update_query;
+ const char *iterate_query;
+ const char *default_pass_scheme;
+ bool userdb_warning_disable;
+};
+
+struct db_sql_connection {
+ struct db_sql_connection *next;
+
+ pool_t pool;
+ int refcount;
+
+ char *config_path;
+ struct db_sql_settings set;
+ struct sql_db *db;
+
+ bool default_password_query:1;
+ bool default_user_query:1;
+ bool default_update_query:1;
+ bool default_iterate_query:1;
+ bool userdb_used:1;
+};
+
+struct db_sql_connection *db_sql_init(const char *config_path, bool userdb);
+void db_sql_unref(struct db_sql_connection **conn);
+
+void db_sql_connect(struct db_sql_connection *conn);
+void db_sql_success(struct db_sql_connection *conn);
+
+void db_sql_check_userdb_warning(struct db_sql_connection *conn);
+
+#endif
diff --git a/src/auth/main.c b/src/auth/main.c
new file mode 100644
index 0000000..5f09fca
--- /dev/null
+++ b/src/auth/main.c
@@ -0,0 +1,398 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "lib-signals.h"
+#include "restrict-access.h"
+#include "child-wait.h"
+#include "sql-api.h"
+#include "module-dir.h"
+#include "randgen.h"
+#include "process-title.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-interface.h"
+#include "dict.h"
+#include "password-scheme.h"
+#include "passdb-cache.h"
+#include "mech.h"
+#include "otp.h"
+#include "mech-otp-common.h"
+#include "auth.h"
+#include "auth-penalty.h"
+#include "auth-token.h"
+#include "auth-request-handler.h"
+#include "auth-request-stats.h"
+#include "auth-worker-server.h"
+#include "auth-worker-client.h"
+#include "auth-master-connection.h"
+#include "auth-client-connection.h"
+#include "auth-policy.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+#define AUTH_PENALTY_ANVIL_PATH "anvil-auth-penalty"
+
+enum auth_socket_type {
+ AUTH_SOCKET_UNKNOWN = 0,
+ AUTH_SOCKET_CLIENT,
+ AUTH_SOCKET_LOGIN_CLIENT,
+ AUTH_SOCKET_MASTER,
+ AUTH_SOCKET_USERDB,
+ AUTH_SOCKET_TOKEN,
+ AUTH_SOCKET_TOKEN_LOGIN
+};
+
+struct auth_socket_listener {
+ enum auth_socket_type type;
+ struct stat st;
+ char *path;
+};
+
+bool worker = FALSE, worker_restart_request = FALSE;
+time_t process_start_time;
+struct auth_penalty *auth_penalty;
+
+static pool_t auth_set_pool;
+static struct module *modules = NULL;
+static struct mechanisms_register *mech_reg;
+static ARRAY(struct auth_socket_listener) listeners;
+
+void auth_refresh_proctitle(void)
+{
+ if (!global_auth_settings->verbose_proctitle || worker)
+ return;
+
+ process_title_set(t_strdup_printf(
+ "[%u wait, %u passdb, %u userdb]",
+ auth_request_state_count[AUTH_REQUEST_STATE_NEW] +
+ auth_request_state_count[AUTH_REQUEST_STATE_MECH_CONTINUE] +
+ auth_request_state_count[AUTH_REQUEST_STATE_FINISHED],
+ auth_request_state_count[AUTH_REQUEST_STATE_PASSDB],
+ auth_request_state_count[AUTH_REQUEST_STATE_USERDB]));
+}
+
+static const char *const *read_global_settings(void)
+{
+ struct master_service_settings_output set_output;
+ const char **services;
+ unsigned int i, count;
+
+ auth_set_pool = pool_alloconly_create("auth settings", 8192);
+ global_auth_settings =
+ auth_settings_read(NULL, auth_set_pool, &set_output);
+
+ /* strdup() the service names, because they're allocated from
+ set parser pool, and we'll later clear it. */
+ count = str_array_length(set_output.specific_services);
+ services = p_new(auth_set_pool, const char *, count + 1);
+ for (i = 0; i < count; i++) {
+ services[i] = p_strdup(auth_set_pool,
+ set_output.specific_services[i]);
+ }
+ return services;
+}
+
+static enum auth_socket_type
+auth_socket_type_get(const char *path)
+{
+ const char *name, *suffix;
+
+ name = strrchr(path, '/');
+ if (name == NULL)
+ name = path;
+ else
+ name++;
+
+ suffix = strrchr(name, '-');
+ if (suffix == NULL)
+ suffix = name;
+ else
+ suffix++;
+
+ if (strcmp(suffix, "login") == 0)
+ return AUTH_SOCKET_LOGIN_CLIENT;
+ else if (strcmp(suffix, "master") == 0)
+ return AUTH_SOCKET_MASTER;
+ else if (strcmp(suffix, "userdb") == 0)
+ return AUTH_SOCKET_USERDB;
+ else if (strcmp(suffix, "token") == 0)
+ return AUTH_SOCKET_TOKEN;
+ else if (strcmp(suffix, "tokenlogin") == 0)
+ return AUTH_SOCKET_TOKEN_LOGIN;
+ else
+ return AUTH_SOCKET_CLIENT;
+}
+
+static void listeners_init(void)
+{
+ unsigned int i, n;
+ const char *path;
+
+ i_array_init(&listeners, 8);
+ n = master_service_get_socket_count(master_service);
+ for (i = 0; i < n; i++) {
+ int fd = MASTER_LISTEN_FD_FIRST + i;
+ struct auth_socket_listener *l;
+
+ l = array_idx_get_space(&listeners, fd);
+ if (net_getunixname(fd, &path) < 0) {
+ if (errno != ENOTSOCK)
+ i_fatal("getunixname(%d) failed: %m", fd);
+ /* not a unix socket, set its name and type lazily */
+ } else {
+ l->type = auth_socket_type_get(path);
+ l->path = i_strdup(path);
+ if (l->type == AUTH_SOCKET_USERDB) {
+ if (stat(path, &l->st) < 0)
+ i_error("stat(%s) failed: %m", path);
+ }
+ }
+ }
+}
+
+static bool auth_module_filter(const char *name, void *context ATTR_UNUSED)
+{
+ if (str_begins(name, "authdb_") ||
+ str_begins(name, "mech_")) {
+ /* this is lazily loaded */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void main_preinit(void)
+{
+ struct module_dir_load_settings mod_set;
+ const char *const *services;
+
+ /* Load built-in SQL drivers (if any) */
+ sql_drivers_init();
+ sql_drivers_register_all();
+
+ /* Initialize databases so their configuration files can be readable
+ only by root. Also load all modules here. */
+ passdbs_init();
+ userdbs_init();
+ /* init schemes before plugins are loaded */
+ password_schemes_init();
+
+ services = read_global_settings();
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = global_auth_settings->debug;
+ mod_set.filter_callback = auth_module_filter;
+
+ modules = module_dir_load(AUTH_MODULE_DIR, NULL, &mod_set);
+ module_dir_init(modules);
+
+ if (!worker)
+ auth_penalty = auth_penalty_init(AUTH_PENALTY_ANVIL_PATH);
+ auth_request_stats_init();
+ mech_init(global_auth_settings);
+ mech_reg = mech_register_init(global_auth_settings);
+ dict_drivers_register_builtin();
+ auths_preinit(global_auth_settings, auth_set_pool,
+ mech_reg, services);
+
+ listeners_init();
+ if (!worker)
+ auth_token_init();
+
+ /* Password lookups etc. may require roots, allow it. */
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+void auth_module_load(const char *names)
+{
+ struct module_dir_load_settings mod_set;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = global_auth_settings->debug;
+ mod_set.ignore_missing = TRUE;
+
+ modules = module_dir_load_missing(modules, AUTH_MODULE_DIR, names,
+ &mod_set);
+ module_dir_init(modules);
+}
+
+static void main_init(void)
+{
+ process_start_time = ioloop_time;
+
+ /* If auth caches aren't used, just ignore these signals */
+ lib_signals_ignore(SIGHUP, TRUE);
+ lib_signals_ignore(SIGUSR2, TRUE);
+
+ /* set proctitles before init()s, since they may set them to error */
+ auth_refresh_proctitle();
+ auth_worker_refresh_proctitle("");
+
+ child_wait_init();
+ auth_worker_server_init();
+ auths_init();
+ auth_request_handler_init();
+ auth_policy_init();
+
+ if (worker) {
+ /* workers have only a single connection from the master
+ auth process */
+ master_service_set_client_limit(master_service, 1);
+ auth_worker_set_max_service_count(
+ master_service_get_service_count(master_service));
+ /* make sure this process cycles if auth connection drops */
+ master_service_set_service_count(master_service, 1);
+ } else {
+ /* caching is handled only by the main auth process */
+ passdb_cache_init(global_auth_settings);
+ }
+}
+
+static void main_deinit(void)
+{
+ struct auth_socket_listener *l;
+
+ if (auth_penalty != NULL) {
+ /* cancel all pending anvil penalty lookups */
+ auth_penalty_deinit(&auth_penalty);
+ }
+ /* deinit auth workers, which aborts pending requests */
+ auth_worker_server_deinit();
+ /* deinit passdbs and userdbs. it aborts any pending async requests. */
+ auths_deinit();
+ /* flush pending requests */
+ auth_request_handler_deinit();
+ /* there are no more auth requests */
+ auths_free();
+ dict_drivers_unregister_builtin();
+
+ auth_token_deinit();
+
+ auth_client_connections_destroy_all();
+ auth_master_connections_destroy_all();
+ auth_worker_connections_destroy_all();
+
+ auth_policy_deinit();
+ mech_register_deinit(&mech_reg);
+ mech_otp_deinit();
+ mech_deinit(global_auth_settings);
+
+ /* allow modules to unregister their dbs/drivers/etc. before freeing
+ the whole data structures containing them. */
+ module_dir_unload(&modules);
+
+ userdbs_deinit();
+ passdbs_deinit();
+ passdb_cache_deinit();
+ password_schemes_deinit();
+ auth_request_stats_deinit();
+
+ sql_drivers_deinit();
+ child_wait_deinit();
+
+ array_foreach_modifiable(&listeners, l)
+ i_free(l->path);
+ array_free(&listeners);
+ pool_unref(&auth_set_pool);
+}
+
+static void worker_connected(struct master_service_connection *conn)
+{
+ if (auth_worker_has_client()) {
+ i_error("Auth workers can handle only a single client");
+ return;
+ }
+
+ master_service_client_connection_accept(conn);
+ (void)auth_worker_client_create(auth_default_service(), conn);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct auth_socket_listener *l;
+ struct auth *auth;
+
+ l = array_idx_modifiable(&listeners, conn->listen_fd);
+ if (l->type == AUTH_SOCKET_UNKNOWN) {
+ /* first connection from inet socket, figure out its type
+ from the listener name */
+ l->type = auth_socket_type_get(conn->name);
+ l->path = i_strdup(conn->name);
+ }
+ auth = auth_default_service();
+ switch (l->type) {
+ case AUTH_SOCKET_MASTER:
+ (void)auth_master_connection_create(auth, conn->fd,
+ l->path, NULL, FALSE);
+ break;
+ case AUTH_SOCKET_USERDB:
+ (void)auth_master_connection_create(auth, conn->fd,
+ l->path, &l->st, TRUE);
+ break;
+ case AUTH_SOCKET_LOGIN_CLIENT:
+ auth_client_connection_create(auth, conn->fd, TRUE, FALSE);
+ break;
+ case AUTH_SOCKET_CLIENT:
+ auth_client_connection_create(auth, conn->fd, FALSE, FALSE);
+ break;
+ case AUTH_SOCKET_TOKEN_LOGIN:
+ auth_client_connection_create(auth, conn->fd, TRUE, TRUE);
+ break;
+ case AUTH_SOCKET_TOKEN:
+ auth_client_connection_create(auth, conn->fd, FALSE, TRUE);
+ break;
+ default:
+ i_unreached();
+ }
+ master_service_client_connection_accept(conn);
+}
+
+static void auth_die(void)
+{
+ if (!worker) {
+ /* do nothing. auth clients should disconnect soon. */
+ } else {
+ /* ask auth master to disconnect us */
+ auth_worker_client_send_shutdown();
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_SSL_INIT;
+
+ master_service = master_service_init("auth", service_flags, &argc, &argv, "w");
+ master_service_init_log(master_service);
+
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'w':
+ master_service_init_log_with_pid(master_service);
+ worker = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ main_preinit();
+ master_service_set_die_callback(master_service, auth_die);
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, worker ? worker_connected :
+ client_connected);
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/auth/mech-anonymous.c b/src/auth/mech-anonymous.c
new file mode 100644
index 0000000..fbbfccd
--- /dev/null
+++ b/src/auth/mech-anonymous.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "mech.h"
+
+static void
+mech_anonymous_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ i_assert(*request->set->anonymous_username != '\0');
+
+ if (request->set->verbose) {
+ /* temporarily set the user to the one that was given,
+ so that the log message goes right */
+ auth_request_set_username_forced(request,
+ t_strndup(data, data_size));
+ e_info(request->mech_event, "login");
+ }
+
+ auth_request_set_username_forced(request,
+ request->set->anonymous_username);
+
+ request->passdb_success = TRUE;
+ auth_request_success(request, "", 0);
+}
+
+static struct auth_request *mech_anonymous_auth_new(void)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"anonymous_auth_request", 2048);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ return request;
+}
+
+const struct mech_module mech_anonymous = {
+ "ANONYMOUS",
+
+ .flags = MECH_SEC_ANONYMOUS | MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_anonymous_auth_new,
+ mech_generic_auth_initial,
+ mech_anonymous_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-apop.c b/src/auth/mech-apop.c
new file mode 100644
index 0000000..f28171f
--- /dev/null
+++ b/src/auth/mech-apop.c
@@ -0,0 +1,173 @@
+/*
+ * APOP (RFC-1460) authentication mechanism.
+ *
+ * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "mech.h"
+#include "passdb.h"
+#include "md5.h"
+#include "buffer.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+struct apop_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ /* requested: */
+ char *challenge;
+
+ /* received: */
+ unsigned char response_digest[16];
+};
+
+static bool verify_credentials(struct apop_auth_request *request,
+ const unsigned char *credentials, size_t size)
+{
+ unsigned char digest[16];
+ struct md5_context ctx;
+
+ md5_init(&ctx);
+ md5_update(&ctx, request->challenge, strlen(request->challenge));
+ md5_update(&ctx, credentials, size);
+ md5_final(&ctx, digest);
+
+ return mem_equals_timing_safe(digest, request->response_digest, 16);
+}
+
+static void
+apop_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *auth_request)
+{
+ struct apop_auth_request *request =
+ (struct apop_auth_request *)auth_request;
+
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ if (verify_credentials(request, credentials, size))
+ auth_request_success(auth_request, "", 0);
+ else
+ auth_request_fail(auth_request);
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(auth_request);
+ break;
+ default:
+ auth_request_fail(auth_request);
+ break;
+ }
+}
+
+static void
+mech_apop_auth_initial(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct apop_auth_request *request =
+ (struct apop_auth_request *)auth_request;
+ const unsigned char *tmp, *end, *username = NULL;
+ unsigned long pid, connect_uid, timestamp;
+ const char *error;
+
+ /* pop3-login handles sending the challenge and getting the response.
+ Our input here is: <challenge> \0 <username> \0 <response> */
+
+ if (data_size == 0) {
+ /* Should never happen */
+ e_info(auth_request->mech_event,
+ "no initial response");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ tmp = data;
+ end = data + data_size;
+
+ /* get the challenge */
+ while (tmp != end && *tmp != '\0')
+ tmp++;
+ request->challenge = p_strdup_until(request->pool, data, tmp);
+
+ if (tmp != end) {
+ /* get the username */
+ username = ++tmp;
+ while (tmp != end && *tmp != '\0')
+ tmp++;
+ } else {
+ /* should never happen */
+ e_info(auth_request->mech_event,
+ "malformed data");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ if (tmp + 1 + 16 != end) {
+ /* Should never happen */
+ e_info(auth_request->mech_event,
+ "malformed data");
+ auth_request_fail(auth_request);
+ return;
+ }
+ memcpy(request->response_digest, tmp + 1,
+ sizeof(request->response_digest));
+
+ /* the challenge must begin with trusted unique ID. we trust only
+ ourself, so make sure it matches our connection specific UID
+ which we told to client in handshake. Also require a timestamp
+ which is later than this process's start time. */
+
+ if (sscanf(request->challenge, "<%lx.%lx.%lx.",
+ &pid, &connect_uid, &timestamp) != 3 ||
+ connect_uid != auth_request->connect_uid ||
+ pid != (unsigned long)getpid() ||
+ (time_t)timestamp < process_start_time) {
+ e_info(auth_request->mech_event,
+ "invalid challenge");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ if (!auth_request_set_username(auth_request, (const char *)username,
+ &error)) {
+ e_info(auth_request->mech_event, "%s", error);
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ auth_request_lookup_credentials(auth_request, "PLAIN",
+ apop_credentials_callback);
+}
+
+static struct auth_request *mech_apop_auth_new(void)
+{
+ struct apop_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"apop_auth_request", 2048);
+ request = p_new(pool, struct apop_auth_request, 1);
+ request->pool = pool;
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+const struct mech_module mech_apop = {
+ "APOP",
+
+ .flags = MECH_SEC_PRIVATE | MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE |
+ MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_VERIFY_RESPONSE,
+
+ mech_apop_auth_new,
+ mech_apop_auth_initial,
+ NULL,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-cram-md5.c b/src/auth/mech-cram-md5.c
new file mode 100644
index 0000000..e6ab888
--- /dev/null
+++ b/src/auth/mech-cram-md5.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* CRAM-MD5 SASL authentication, see RFC-2195
+ Joshua Goodall <joshua@roughtrade.net> */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "hmac-cram-md5.h"
+#include "hmac.h"
+#include "md5.h"
+#include "randgen.h"
+#include "mech.h"
+#include "passdb.h"
+#include "hostpid.h"
+
+#include <time.h>
+
+struct cram_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ /* requested: */
+ char *challenge;
+
+ /* received: */
+ char *username;
+ char *response;
+ unsigned long maxbuf;
+};
+
+static const char *get_cram_challenge(void)
+{
+ unsigned char buf[17];
+ size_t i;
+
+ random_fill(buf, sizeof(buf)-1);
+
+ for (i = 0; i < sizeof(buf)-1; i++)
+ buf[i] = (buf[i] % 10) + '0';
+ buf[sizeof(buf)-1] = '\0';
+
+ return t_strdup_printf("<%s.%s@%s>", (const char *)buf,
+ dec2str(ioloop_time), my_hostname);
+}
+
+static bool verify_credentials(struct cram_auth_request *request,
+ const unsigned char *credentials, size_t size)
+{
+
+ unsigned char digest[MD5_RESULTLEN];
+ struct hmac_context ctx;
+ const char *response_hex;
+
+ if (size != CRAM_MD5_CONTEXTLEN) {
+ e_error(request->auth_request.mech_event,
+ "invalid credentials length");
+ return FALSE;
+ }
+
+ hmac_init(&ctx, NULL, 0, &hash_method_md5);
+ hmac_md5_set_cram_context(&ctx, credentials);
+ hmac_update(&ctx, request->challenge, strlen(request->challenge));
+ hmac_final(&ctx, digest);
+
+ response_hex = binary_to_hex(digest, sizeof(digest));
+
+ if (!mem_equals_timing_safe(response_hex, request->response, sizeof(digest)*2)) {
+ e_info(request->auth_request.mech_event,
+ AUTH_LOG_MSG_PASSWORD_MISMATCH);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool parse_cram_response(struct cram_auth_request *request,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ size_t i, space;
+
+ *error_r = NULL;
+
+ /* <username> SPACE <response>. Username may contain spaces, so assume
+ the rightmost space is the response separator. */
+ for (i = space = 0; i < size; i++) {
+ if (data[i] == '\0') {
+ *error_r = "NULs in response";
+ return FALSE;
+ }
+ if (data[i] == ' ')
+ space = i;
+ }
+
+ if (space == 0) {
+ *error_r = "missing digest";
+ return FALSE;
+ }
+
+ request->username = p_strndup(request->pool, data, space);
+ space++;
+ request->response =
+ p_strndup(request->pool, data + space, size - space);
+ return TRUE;
+}
+
+static void credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *auth_request)
+{
+ struct cram_auth_request *request =
+ (struct cram_auth_request *)auth_request;
+
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ if (verify_credentials(request, credentials, size))
+ auth_request_success(auth_request, "", 0);
+ else
+ auth_request_fail(auth_request);
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(auth_request);
+ break;
+ default:
+ auth_request_fail(auth_request);
+ break;
+ }
+}
+
+static void
+mech_cram_md5_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct cram_auth_request *request =
+ (struct cram_auth_request *)auth_request;
+ const char *error;
+
+ if (parse_cram_response(request, data, data_size, &error)) {
+ if (auth_request_set_username(auth_request, request->username,
+ &error)) {
+ auth_request_lookup_credentials(auth_request,
+ "CRAM-MD5", credentials_callback);
+ return;
+ }
+ }
+
+ if (error == NULL)
+ error = "authentication failed";
+
+ e_info(auth_request->mech_event, "%s", error);
+ auth_request_fail(auth_request);
+}
+
+static void
+mech_cram_md5_auth_initial(struct auth_request *auth_request,
+ const unsigned char *data ATTR_UNUSED,
+ size_t data_size ATTR_UNUSED)
+{
+ struct cram_auth_request *request =
+ (struct cram_auth_request *)auth_request;
+
+ request->challenge = p_strdup(request->pool, get_cram_challenge());
+ auth_request_handler_reply_continue(auth_request, request->challenge,
+ strlen(request->challenge));
+}
+
+static struct auth_request *mech_cram_md5_auth_new(void)
+{
+ struct cram_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"cram_md5_auth_request", 2048);
+ request = p_new(pool, struct cram_auth_request, 1);
+ request->pool = pool;
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+const struct mech_module mech_cram_md5 = {
+ "CRAM-MD5",
+
+ .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE,
+ .passdb_need = MECH_PASSDB_NEED_VERIFY_RESPONSE,
+
+ mech_cram_md5_auth_new,
+ mech_cram_md5_auth_initial,
+ mech_cram_md5_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-digest-md5-private.h b/src/auth/mech-digest-md5-private.h
new file mode 100644
index 0000000..fb9ff80
--- /dev/null
+++ b/src/auth/mech-digest-md5-private.h
@@ -0,0 +1,38 @@
+#ifndef MECH_DIGEST_MD5_PRIVATE_H
+#define MECH_DIGEST_MD5_PRIVATE_H
+
+#include "auth-request.h"
+
+enum qop_option {
+ QOP_AUTH = 0x01, /* authenticate */
+ QOP_AUTH_INT = 0x02, /* + integrity protection, not supported yet */
+ QOP_AUTH_CONF = 0x04, /* + encryption, not supported yet */
+
+ QOP_COUNT = 3
+};
+
+struct digest_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ /* requested: */
+ char *nonce;
+ enum qop_option qop;
+
+ /* received: */
+ char *username;
+ char *cnonce;
+ char *nonce_count;
+ char *qop_value;
+ char *digest_uri; /* may be NULL */
+ char *authzid; /* may be NULL, authorization ID */
+ unsigned char response[32];
+ unsigned long maxbuf;
+ bool nonce_found:1;
+
+ /* final reply: */
+ char *rspauth;
+};
+
+#endif
diff --git a/src/auth/mech-digest-md5.c b/src/auth/mech-digest-md5.c
new file mode 100644
index 0000000..d7f4383
--- /dev/null
+++ b/src/auth/mech-digest-md5.c
@@ -0,0 +1,592 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* Digest-MD5 SASL authentication, see RFC-2831 */
+
+#include "auth-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "mech-digest-md5-private.h"
+#include "md5.h"
+#include "randgen.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "mech.h"
+#include "passdb.h"
+
+
+#define MAX_REALM_LEN 64
+
+/* Linear whitespace */
+#define IS_LWS(c) ((c) == ' ' || (c) == '\t')
+
+static const char *qop_names[] = { "auth", "auth-int", "auth-conf" };
+
+static string_t *get_digest_challenge(struct digest_auth_request *request)
+{
+ const struct auth_settings *set = request->auth_request.set;
+ buffer_t buf;
+ string_t *str;
+ const char *const *tmp;
+ unsigned char nonce[16];
+ unsigned char nonce_base64[MAX_BASE64_ENCODED_SIZE(sizeof(nonce))+1];
+ int i;
+ bool first_qop;
+
+ /*
+ realm="hostname" (multiple allowed)
+ nonce="randomized data, at least 64bit"
+ qop="auth,auth-int,auth-conf"
+ maxbuf=number (with auth-int, auth-conf, defaults to 64k)
+ charset="utf-8" (iso-8859-1 if it doesn't exist)
+ algorithm="md5-sess"
+ cipher="3des,des,rc4-40,rc4,rc4-56" (with auth-conf)
+ */
+
+ /* get 128bit of random data as nonce */
+ random_fill(nonce, sizeof(nonce));
+
+ buffer_create_from_data(&buf, nonce_base64, sizeof(nonce_base64));
+ base64_encode(nonce, sizeof(nonce), &buf);
+ buffer_append_c(&buf, '\0');
+ request->nonce = p_strdup(request->pool, buf.data);
+
+ str = t_str_new(256);
+ if (*set->realms_arr == NULL) {
+ /* If no realms are given, at least Cyrus SASL client defaults
+ to destination host name */
+ str_append(str, "realm=\"\",");
+ } else {
+ for (tmp = set->realms_arr; *tmp != NULL; tmp++)
+ str_printfa(str, "realm=\"%s\",", *tmp);
+ }
+
+ str_printfa(str, "nonce=\"%s\",", request->nonce);
+
+ str_append(str, "qop=\""); first_qop = TRUE;
+ for (i = 0; i < QOP_COUNT; i++) {
+ if ((request->qop & (1 << i)) != 0) {
+ if (first_qop)
+ first_qop = FALSE;
+ else
+ str_append_c(str, ',');
+ str_append(str, qop_names[i]);
+ }
+ }
+ str_append(str, "\",");
+
+ str_append(str, "charset=\"utf-8\","
+ "algorithm=\"md5-sess\"");
+ return str;
+}
+
+static bool verify_credentials(struct digest_auth_request *request,
+ const unsigned char *credentials, size_t size)
+{
+ struct md5_context ctx;
+ unsigned char digest[MD5_RESULTLEN];
+ const char *a1_hex, *a2_hex, *response_hex;
+ int i;
+
+ /* get the MD5 password */
+ if (size != MD5_RESULTLEN) {
+ e_error(request->auth_request.mech_event,
+ "invalid credentials length");
+ return FALSE;
+ }
+
+ /*
+ response =
+ HEX( KD ( HEX(H(A1)),
+ { nonce-value, ":" nc-value, ":",
+ cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
+
+ and if authzid is not empty:
+
+ A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
+ ":", nonce-value, ":", cnonce-value, ":", authzid }
+
+ else:
+
+ A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
+ ":", nonce-value, ":", cnonce-value }
+
+ If the "qop" directive's value is "auth", then A2 is:
+
+ A2 = { "AUTHENTICATE:", digest-uri-value }
+
+ If the "qop" value is "auth-int" or "auth-conf" then A2 is:
+
+ A2 = { "AUTHENTICATE:", digest-uri-value,
+ ":00000000000000000000000000000000" }
+ */
+
+ /* A1 */
+ md5_init(&ctx);
+ md5_update(&ctx, credentials, size);
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->nonce, strlen(request->nonce));
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->cnonce, strlen(request->cnonce));
+ if (request->authzid != NULL) {
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->authzid, strlen(request->authzid));
+ }
+ md5_final(&ctx, digest);
+ a1_hex = binary_to_hex(digest, 16);
+
+ /* do it twice, first verify the user's response, the second is
+ sent for client as a reply */
+ for (i = 0; i < 2; i++) {
+ /* A2 */
+ md5_init(&ctx);
+ if (i == 0)
+ md5_update(&ctx, "AUTHENTICATE:", 13);
+ else
+ md5_update(&ctx, ":", 1);
+
+ if (request->digest_uri != NULL) {
+ md5_update(&ctx, request->digest_uri,
+ strlen(request->digest_uri));
+ }
+ if (request->qop == QOP_AUTH_INT ||
+ request->qop == QOP_AUTH_CONF) {
+ md5_update(&ctx, ":00000000000000000000000000000000",
+ 33);
+ }
+ md5_final(&ctx, digest);
+ a2_hex = binary_to_hex(digest, 16);
+
+ /* response */
+ md5_init(&ctx);
+ md5_update(&ctx, a1_hex, 32);
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->nonce, strlen(request->nonce));
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->nonce_count,
+ strlen(request->nonce_count));
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->cnonce, strlen(request->cnonce));
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, request->qop_value,
+ strlen(request->qop_value));
+ md5_update(&ctx, ":", 1);
+ md5_update(&ctx, a2_hex, 32);
+ md5_final(&ctx, digest);
+ response_hex = binary_to_hex(digest, 16);
+
+ if (i == 0) {
+ /* verify response */
+ if (!mem_equals_timing_safe(response_hex, request->response, 32)) {
+ auth_request_log_info(&request->auth_request,
+ AUTH_SUBSYS_MECH,
+ AUTH_LOG_MSG_PASSWORD_MISMATCH);
+ return FALSE;
+ }
+ } else {
+ request->rspauth =
+ p_strconcat(request->pool, "rspauth=",
+ response_hex, NULL);
+ }
+ }
+
+ return TRUE;
+}
+
+static bool parse_next(char **data, char **key, char **value)
+{
+ /* @UNSAFE */
+ char *p, *dest;
+
+ p = *data;
+ while (IS_LWS(*p)) p++;
+
+ /* get key */
+ *key = p;
+ while (*p != '\0' && *p != '=' && *p != ',')
+ p++;
+
+ if (*p != '=') {
+ *data = p;
+ return FALSE;
+ }
+
+ *value = p+1;
+
+ /* skip trailing whitespace in key */
+ while (p > *data && IS_LWS(p[-1]))
+ p--;
+ *p = '\0';
+
+ /* get value */
+ p = *value;
+ while (IS_LWS(*p)) p++;
+
+ if (*p != '"') {
+ while (*p != '\0' && *p != ',')
+ p++;
+
+ *data = p;
+ /* If there is more to parse, ensure it won't get skipped
+ because *p is set to NUL below */
+ if (**data != '\0') (*data)++;
+ while (IS_LWS(p[-1]))
+ p--;
+ *p = '\0';
+ } else {
+ /* quoted string */
+ *value = dest = ++p;
+ while (*p != '\0' && *p != '"') {
+ if (*p == '\\' && p[1] != '\0')
+ p++;
+ *dest++ = *p++;
+ }
+
+ *data = *p == '"' ? p+1 : p;
+ *dest = '\0';
+ }
+
+ return TRUE;
+}
+
+static bool auth_handle_response(struct digest_auth_request *request,
+ char *key, char *value, const char **error)
+{
+ unsigned int i;
+
+ (void)str_lcase(key);
+
+ if (strcmp(key, "realm") == 0) {
+ if (request->auth_request.fields.realm == NULL && *value != '\0')
+ auth_request_set_realm(&request->auth_request, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "username") == 0) {
+ if (request->username != NULL) {
+ *error = "username must not exist more than once";
+ return FALSE;
+ }
+
+ if (*value == '\0') {
+ *error = "empty username";
+ return FALSE;
+ }
+
+ request->username = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "nonce") == 0) {
+ /* nonce must be same */
+ if (strcmp(value, request->nonce) != 0) {
+ *error = "Invalid nonce";
+ return FALSE;
+ }
+
+ request->nonce_found = TRUE;
+ return TRUE;
+ }
+
+ if (strcmp(key, "cnonce") == 0) {
+ if (request->cnonce != NULL) {
+ *error = "cnonce must not exist more than once";
+ return FALSE;
+ }
+
+ if (*value == '\0') {
+ *error = "cnonce can't contain empty value";
+ return FALSE;
+ }
+
+ request->cnonce = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "nc") == 0) {
+ unsigned int nc;
+
+ if (request->nonce_count != NULL) {
+ *error = "nonce-count must not exist more than once";
+ return FALSE;
+ }
+
+ if (str_to_uint(value, &nc) < 0) {
+ *error = "nonce-count value invalid";
+ return FALSE;
+ }
+
+ if (nc != 1) {
+ *error = "re-auth not supported currently";
+ return FALSE;
+ }
+
+ request->nonce_count = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "qop") == 0) {
+ for (i = 0; i < QOP_COUNT; i++) {
+ if (strcasecmp(qop_names[i], value) == 0)
+ break;
+ }
+
+ if (i == QOP_COUNT) {
+ *error = t_strdup_printf("Unknown QoP value: %s",
+ str_sanitize(value, 32));
+ return FALSE;
+ }
+
+ request->qop &= (1 << i);
+ if (request->qop == 0) {
+ *error = "Nonallowed QoP requested";
+ return FALSE;
+ }
+
+ request->qop_value = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "digest-uri") == 0) {
+ /* type / host / serv-name */
+ const char *const *uri = t_strsplit(value, "/");
+
+ if (uri[0] == NULL || uri[1] == NULL) {
+ *error = "Invalid digest-uri";
+ return FALSE;
+ }
+
+ /* FIXME: RFC recommends that we verify the host/serv-type.
+ But isn't the realm enough already? That'd be just extra
+ configuration.. Maybe optionally list valid hosts in
+ config file? */
+ request->digest_uri = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "maxbuf") == 0) {
+ if (request->maxbuf != 0) {
+ *error = "maxbuf must not exist more than once";
+ return FALSE;
+ }
+
+ if (str_to_ulong(value, &request->maxbuf) < 0 ||
+ request->maxbuf == 0) {
+ *error = "Invalid maxbuf value";
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ if (strcmp(key, "charset") == 0) {
+ if (strcasecmp(value, "utf-8") != 0) {
+ *error = "Only utf-8 charset is allowed";
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ if (strcmp(key, "response") == 0) {
+ if (strlen(value) != 32) {
+ *error = "Invalid response value";
+ return FALSE;
+ }
+
+ memcpy(request->response, value, 32);
+ return TRUE;
+ }
+
+ if (strcmp(key, "cipher") == 0) {
+ /* not supported, ignore */
+ return TRUE;
+ }
+
+ if (strcmp(key, "authzid") == 0) {
+ if (request->authzid != NULL) {
+ *error = "authzid must not exist more than once";
+ return FALSE;
+ }
+
+ if (*value == '\0') {
+ *error = "empty authzid";
+ return FALSE;
+ }
+
+ request->authzid = p_strdup(request->pool, value);
+ return TRUE;
+ }
+
+ /* unknown key, ignore */
+ return TRUE;
+}
+
+static bool parse_digest_response(struct digest_auth_request *request,
+ const unsigned char *data, size_t size,
+ const char **error)
+{
+ char *copy, *key, *value;
+ bool failed;
+
+ /*
+ realm="realm"
+ username="username"
+ nonce="randomized data"
+ cnonce="??"
+ nc=00000001
+ qop="auth|auth-int|auth-conf"
+ digest-uri="serv-type/host[/serv-name]"
+ response=32 HEX digits
+ maxbuf=number (with auth-int, auth-conf, defaults to 64k)
+ charset="utf-8" (iso-8859-1 if it doesn't exist)
+ cipher="cipher-value"
+ authzid="authzid-value"
+ */
+
+ *error = NULL;
+ failed = FALSE;
+
+ if (size == 0) {
+ *error = "Client sent no input";
+ return FALSE;
+ }
+
+ /* treating response as NUL-terminated string also gets rid of all
+ potential problems with NUL characters in strings. */
+ copy = t_strdup_noconst(t_strndup(data, size));
+ while (*copy != '\0') {
+ if (parse_next(&copy, &key, &value)) {
+ if (!auth_handle_response(request, key, value, error)) {
+ failed = TRUE;
+ break;
+ }
+ }
+
+ if (*copy == ',')
+ copy++;
+ }
+
+ if (!failed) {
+ if (!request->nonce_found) {
+ *error = "Missing nonce parameter";
+ failed = TRUE;
+ } else if (request->cnonce == NULL) {
+ *error = "Missing cnonce parameter";
+ failed = TRUE;
+ } else if (request->username == NULL) {
+ *error = "Missing username parameter";
+ failed = TRUE;
+ }
+ }
+
+ if (request->nonce_count == NULL)
+ request->nonce_count = p_strdup(request->pool, "00000001");
+ if (request->qop_value == NULL)
+ request->qop_value = p_strdup(request->pool, "auth");
+
+ return !failed;
+}
+
+static void credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *auth_request)
+{
+ struct digest_auth_request *request =
+ (struct digest_auth_request *)auth_request;
+
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ if (!verify_credentials(request, credentials, size)) {
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ auth_request_success(auth_request, request->rspauth,
+ strlen(request->rspauth));
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(auth_request);
+ break;
+ default:
+ auth_request_fail(auth_request);
+ break;
+ }
+}
+
+static void
+mech_digest_md5_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct digest_auth_request *request =
+ (struct digest_auth_request *)auth_request;
+ const char *username, *error;
+
+ if (parse_digest_response(request, data, data_size, &error)) {
+ if (auth_request->fields.realm != NULL &&
+ strchr(request->username, '@') == NULL) {
+ username = t_strconcat(request->username, "@",
+ auth_request->fields.realm, NULL);
+ auth_request->domain_is_realm = TRUE;
+ } else {
+ username = request->username;
+ }
+
+ if (auth_request_set_username(auth_request, username, &error) &&
+ (request->authzid == NULL ||
+ auth_request_set_login_username(auth_request,
+ request->authzid,
+ &error))) {
+ auth_request_lookup_credentials(auth_request,
+ "DIGEST-MD5", credentials_callback);
+ return;
+ }
+ }
+
+ if (error != NULL)
+ e_info(auth_request->mech_event, "%s", error);
+
+ auth_request_fail(auth_request);
+}
+
+static void
+mech_digest_md5_auth_initial(struct auth_request *auth_request,
+ const unsigned char *data ATTR_UNUSED,
+ size_t data_size ATTR_UNUSED)
+{
+ struct digest_auth_request *request =
+ (struct digest_auth_request *)auth_request;
+ string_t *challenge;
+
+ /* FIXME: there's no support for subsequent authentication */
+
+ challenge = get_digest_challenge(request);
+ auth_request_handler_reply_continue(auth_request, str_data(challenge),
+ str_len(challenge));
+}
+
+static struct auth_request *mech_digest_md5_auth_new(void)
+{
+ struct digest_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"digest_md5_auth_request", 2048);
+ request = p_new(pool, struct digest_auth_request, 1);
+ request->pool = pool;
+ request->qop = QOP_AUTH;
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+const struct mech_module mech_digest_md5 = {
+ "DIGEST-MD5",
+
+ .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE |
+ MECH_SEC_MUTUAL_AUTH,
+ .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS,
+
+ mech_digest_md5_auth_new,
+ mech_digest_md5_auth_initial,
+ mech_digest_md5_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-dovecot-token.c b/src/auth/mech-dovecot-token.c
new file mode 100644
index 0000000..408ef6e
--- /dev/null
+++ b/src/auth/mech-dovecot-token.c
@@ -0,0 +1,92 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+/* Used internally by Dovecot processes to authenticate against each others
+ (e.g. imap to imap-urlauth). See auth-token.c */
+
+#include "auth-common.h"
+#include "mech.h"
+#include "safe-memset.h"
+#include "auth-token.h"
+
+static void
+mech_dovecot_token_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ const char *session_id, *username, *pid, *service, *error;
+ char *auth_token;
+ size_t i, len;
+ int count;
+
+ /* service \0 pid \0 username \0 session_id \0 auth_token */
+ service = (const char *) data;
+ session_id = username = pid = auth_token = NULL;
+ count = 0;
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\0') {
+ count++; i++;
+ if (count == 1)
+ pid = (const char *)data + i;
+ else if (count == 2)
+ username = (const char *)data + i;
+ else if (count == 3)
+ session_id = (const char *)data + i;
+ else if (count == 4) {
+ len = data_size - i;
+ auth_token = p_strndup(unsafe_data_stack_pool,
+ data+i, len);
+ }
+ else
+ break;
+ }
+ }
+
+ if (count != 4) {
+ /* invalid input */
+ e_info(request->mech_event, "invalid input");
+ auth_request_fail(request);
+ } else if (!auth_request_set_username(request, username, &error)) {
+ /* invalid username */
+ e_info(request->mech_event, "%s", error);
+ auth_request_fail(request);
+ } else {
+ const char *valid_token =
+ auth_token_get(service, pid, request->fields.user,
+ session_id);
+
+ if (auth_token != NULL &&
+ str_equals_timing_almost_safe(auth_token, valid_token)) {
+ request->passdb_success = TRUE;
+ auth_request_set_field(request, "userdb_client_service", service, "");
+ auth_request_success(request, NULL, 0);
+ } else {
+ auth_request_fail(request);
+ }
+ }
+
+ /* make sure it's cleared */
+ if (auth_token != NULL)
+ safe_memset(auth_token, 0, strlen(auth_token));
+}
+
+static struct auth_request *mech_dovecot_token_auth_new(void)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dovecot_token_auth_request", 512);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ return request;
+}
+
+const struct mech_module mech_dovecot_token = {
+ "DOVECOT-TOKEN",
+
+ .flags = MECH_SEC_PRIVATE | MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_dovecot_token_auth_new,
+ mech_generic_auth_initial,
+ mech_dovecot_token_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-external.c b/src/auth/mech-external.c
new file mode 100644
index 0000000..39809b4
--- /dev/null
+++ b/src/auth/mech-external.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+#include "mech.h"
+#include "mech-plain-common.h"
+
+static void
+mech_external_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ const char *authzid, *error;
+
+ authzid = t_strndup(data, data_size);
+ if (request->fields.user == NULL) {
+ e_info(request->mech_event,
+ "username not known");
+ auth_request_fail(request);
+ return;
+ }
+
+ /* this call is done simply to put the username through translation
+ settings */
+ if (!auth_request_set_username(request, "", &error)) {
+ e_info(request->mech_event,
+ "Invalid username");
+ auth_request_fail(request);
+ return;
+ }
+
+ if (*authzid != '\0' &&
+ !auth_request_set_login_username(request, authzid, &error)) {
+ /* invalid login username */
+ e_info(request->mech_event,
+ "login user: %s", error);
+ auth_request_fail(request);
+ } else {
+ auth_request_verify_plain(request, "",
+ plain_verify_callback);
+ }
+}
+
+static struct auth_request *mech_external_auth_new(void)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"external_auth_request", 2048);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ return request;
+}
+
+const struct mech_module mech_external = {
+ "EXTERNAL",
+
+ .flags = 0,
+ .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN,
+
+ mech_external_auth_new,
+ mech_generic_auth_initial,
+ mech_external_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-gssapi.c b/src/auth/mech-gssapi.c
new file mode 100644
index 0000000..5ed8edb
--- /dev/null
+++ b/src/auth/mech-gssapi.c
@@ -0,0 +1,797 @@
+/*
+ * GSSAPI Module
+ *
+ * Copyright (c) 2005 Jelmer Vernooij <jelmer@samba.org>
+ *
+ * Related standards:
+ * - draft-ietf-sasl-gssapi-03
+ * - RFC2222
+ *
+ * Some parts inspired by an older patch from Colin Walters
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "env-util.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hex-binary.h"
+#include "safe-memset.h"
+#include "mech.h"
+#include "passdb.h"
+
+
+#if defined(BUILTIN_GSSAPI) || defined(PLUGIN_BUILD)
+
+#ifndef HAVE___GSS_USEROK
+# define USE_KRB5_USEROK
+# include <krb5.h>
+#endif
+
+#ifdef HAVE_GSSAPI_GSSAPI_H
+# include <gssapi/gssapi.h>
+#elif defined (HAVE_GSSAPI_H)
+# include <gssapi.h>
+#endif
+
+#ifdef HAVE_GSSAPI_GSSAPI_KRB5_H
+# include <gssapi/gssapi_krb5.h>
+#elif defined (HAVE_GSSAPI_KRB5_H)
+# include <gssapi_krb5.h>
+#else
+# undef USE_KRB5_USEROK
+#endif
+
+#ifdef HAVE_GSSAPI_GSSAPI_EXT_H
+# include <gssapi/gssapi_ext.h>
+#endif
+
+#define krb5_boolean2bool(X) ((X) != 0)
+
+/* Non-zero flags defined in RFC 2222 */
+enum sasl_gssapi_qop {
+ SASL_GSSAPI_QOP_UNSPECIFIED = 0x00,
+ SASL_GSSAPI_QOP_AUTH_ONLY = 0x01,
+ SASL_GSSAPI_QOP_AUTH_INT = 0x02,
+ SASL_GSSAPI_QOP_AUTH_CONF = 0x04
+};
+
+struct gssapi_auth_request {
+ struct auth_request auth_request;
+ gss_ctx_id_t gss_ctx;
+ gss_cred_id_t service_cred;
+
+ enum {
+ GSS_STATE_SEC_CONTEXT,
+ GSS_STATE_WRAP,
+ GSS_STATE_UNWRAP
+ } sasl_gssapi_state;
+
+ gss_name_t authn_name;
+ gss_name_t authz_name;
+
+ pool_t pool;
+};
+
+static bool gssapi_initialized = FALSE;
+
+static gss_OID_desc mech_gssapi_krb5_oid =
+ { 9, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" };
+
+static int
+mech_gssapi_wrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf);
+
+static void mech_gssapi_log_error(struct auth_request *request,
+ OM_uint32 status_value, int status_type,
+ const char *description)
+{
+ OM_uint32 message_context = 0;
+ OM_uint32 minor_status;
+ gss_buffer_desc status_string;
+
+ do {
+ (void)gss_display_status(&minor_status, status_value,
+ status_type, GSS_C_NO_OID,
+ &message_context, &status_string);
+
+ e_info(request->mech_event,
+ "While %s: %s", description,
+ str_sanitize(status_string.value, SIZE_MAX));
+
+ (void)gss_release_buffer(&minor_status, &status_string);
+ } while (message_context != 0);
+}
+
+static void mech_gssapi_initialize(const struct auth_settings *set)
+{
+ const char *path = set->krb5_keytab;
+
+ if (*path != '\0') {
+ /* environment may be used by Kerberos 5 library directly */
+ env_put("KRB5_KTNAME", path);
+#ifdef HAVE_GSSKRB5_REGISTER_ACCEPTOR_IDENTITY
+ gsskrb5_register_acceptor_identity(path);
+#elif defined (HAVE_KRB5_GSS_REGISTER_ACCEPTOR_IDENTITY)
+ krb5_gss_register_acceptor_identity(path);
+#endif
+ }
+}
+
+static struct auth_request *mech_gssapi_auth_new(void)
+{
+ struct gssapi_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"gssapi_auth_request", 2048);
+ request = p_new(pool, struct gssapi_auth_request, 1);
+ request->pool = pool;
+
+ request->gss_ctx = GSS_C_NO_CONTEXT;
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+static OM_uint32
+obtain_service_credentials(struct auth_request *request, gss_cred_id_t *ret_r)
+{
+ OM_uint32 major_status, minor_status;
+ string_t *principal_name;
+ gss_buffer_desc inbuf;
+ gss_name_t gss_principal;
+ const char *service_name;
+
+ if (!gssapi_initialized) {
+ gssapi_initialized = TRUE;
+ mech_gssapi_initialize(request->set);
+ }
+
+ if (strcmp(request->set->gssapi_hostname, "$ALL") == 0) {
+ e_debug(request->mech_event,
+ "Using all keytab entries");
+ *ret_r = GSS_C_NO_CREDENTIAL;
+ return GSS_S_COMPLETE;
+ }
+
+ if (strcasecmp(request->fields.service, "POP3") == 0) {
+ /* The standard POP3 service name with GSSAPI is called
+ just "pop". */
+ service_name = "pop";
+ } else {
+ service_name = t_str_lcase(request->fields.service);
+ }
+
+ principal_name = t_str_new(128);
+ str_append(principal_name, service_name);
+ str_append_c(principal_name, '@');
+ str_append(principal_name, request->set->gssapi_hostname);
+
+ e_debug(request->mech_event,
+ "Obtaining credentials for %s", str_c(principal_name));
+
+ inbuf.length = str_len(principal_name);
+ inbuf.value = str_c_modifiable(principal_name);
+
+ major_status = gss_import_name(&minor_status, &inbuf,
+ GSS_C_NT_HOSTBASED_SERVICE,
+ &gss_principal);
+ str_free(&principal_name);
+
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE,
+ "importing principal name");
+ return major_status;
+ }
+
+ major_status = gss_acquire_cred(&minor_status, gss_principal, 0,
+ GSS_C_NULL_OID_SET, GSS_C_ACCEPT,
+ ret_r, NULL, NULL);
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE,
+ "acquiring service credentials");
+ mech_gssapi_log_error(request, minor_status, GSS_C_MECH_CODE,
+ "acquiring service credentials");
+ return major_status;
+ }
+
+ gss_release_name(&minor_status, &gss_principal);
+ return major_status;
+}
+
+static gss_name_t
+import_name(struct auth_request *request, void *str, size_t len)
+{
+ OM_uint32 major_status, minor_status;
+ gss_buffer_desc name_buf;
+ gss_name_t name;
+
+ name_buf.value = str;
+ name_buf.length = len;
+ major_status = gss_import_name(&minor_status, &name_buf,
+ GSS_C_NO_OID, &name);
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE,
+ "gss_import_name");
+ return GSS_C_NO_NAME;
+ }
+ return name;
+}
+
+static gss_name_t
+duplicate_name(struct auth_request *request, gss_name_t old)
+{
+ OM_uint32 major_status, minor_status;
+ gss_name_t new;
+
+ major_status = gss_duplicate_name(&minor_status, old, &new);
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE,
+ "gss_duplicate_name");
+ return GSS_C_NO_NAME;
+ }
+ return new;
+}
+
+static bool data_has_nuls(const void *data, size_t len)
+{
+ const unsigned char *c = data;
+ size_t i;
+
+ /* apparently all names end with NUL? */
+ if (len > 0 && c[len-1] == '\0')
+ len--;
+
+ for (i = 0; i < len; i++) {
+ if (c[i] == '\0')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int get_display_name(struct auth_request *auth_request, gss_name_t name,
+ gss_OID *name_type_r, const char **display_name_r)
+{
+ OM_uint32 major_status, minor_status;
+ gss_buffer_desc buf;
+
+ major_status = gss_display_name(&minor_status, name,
+ &buf, name_type_r);
+ if (major_status != GSS_S_COMPLETE) {
+ mech_gssapi_log_error(auth_request, major_status,
+ GSS_C_GSS_CODE, "gss_display_name");
+ return -1;
+ }
+ if (data_has_nuls(buf.value, buf.length)) {
+ e_info(auth_request->mech_event,
+ "authn_name has NULs");
+ return -1;
+ }
+ *display_name_r = t_strndup(buf.value, buf.length);
+ (void)gss_release_buffer(&minor_status, &buf);
+ return 0;
+}
+
+static bool mech_gssapi_oid_cmp(const gss_OID_desc *oid1,
+ const gss_OID_desc *oid2)
+{
+ return oid1->length == oid2->length &&
+ mem_equals_timing_safe(oid1->elements, oid2->elements, oid1->length);
+}
+
+static int
+mech_gssapi_sec_context(struct gssapi_auth_request *request,
+ gss_buffer_desc inbuf)
+{
+ struct auth_request *auth_request = &request->auth_request;
+ OM_uint32 major_status, minor_status;
+ gss_buffer_desc output_token;
+ gss_OID name_type;
+ gss_OID mech_type;
+ const char *username, *error;
+ int ret = 0;
+
+ major_status = gss_accept_sec_context (
+ &minor_status,
+ &request->gss_ctx,
+ request->service_cred,
+ &inbuf,
+ GSS_C_NO_CHANNEL_BINDINGS,
+ &request->authn_name,
+ &mech_type,
+ &output_token,
+ NULL, /* ret_flags */
+ NULL, /* time_rec */
+ NULL /* delegated_cred_handle */
+ );
+
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(auth_request, major_status,
+ GSS_C_GSS_CODE,
+ "processing incoming data");
+ mech_gssapi_log_error(auth_request, minor_status,
+ GSS_C_MECH_CODE,
+ "processing incoming data");
+ return -1;
+ }
+
+ switch (major_status) {
+ case GSS_S_COMPLETE:
+ if (!mech_gssapi_oid_cmp(mech_type, &mech_gssapi_krb5_oid)) {
+ e_info(auth_request->mech_event,
+ "GSSAPI mechanism not Kerberos5");
+ ret = -1;
+ } else if (get_display_name(auth_request, request->authn_name,
+ &name_type, &username) < 0)
+ ret = -1;
+ else if (!auth_request_set_username(auth_request, username,
+ &error)) {
+ e_info(auth_request->mech_event,
+ "authn_name: %s", error);
+ ret = -1;
+ } else {
+ request->sasl_gssapi_state = GSS_STATE_WRAP;
+ e_debug(auth_request->mech_event,
+ "security context state completed.");
+ }
+ break;
+ case GSS_S_CONTINUE_NEEDED:
+ e_debug(auth_request->mech_event,
+ "Processed incoming packet correctly, "
+ "waiting for another.");
+ break;
+ default:
+ e_error(auth_request->mech_event,
+ "Received unexpected major status %d", major_status);
+ break;
+ }
+
+ if (ret == 0) {
+ if (output_token.length > 0) {
+ auth_request_handler_reply_continue(auth_request,
+ output_token.value,
+ output_token.length);
+ } else {
+ /* If there is no output token, go straight to wrap,
+ which is expecting an empty input token. */
+ ret = mech_gssapi_wrap(request, output_token);
+ }
+ }
+ (void)gss_release_buffer(&minor_status, &output_token);
+ return ret;
+}
+
+static int
+mech_gssapi_wrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf)
+{
+ OM_uint32 major_status, minor_status;
+ gss_buffer_desc outbuf;
+ unsigned char ret[4];
+
+ /* The client's return data should be empty here */
+
+ /* Only authentication, no integrity or confidentiality
+ protection (yet?) */
+ ret[0] = (SASL_GSSAPI_QOP_UNSPECIFIED |
+ SASL_GSSAPI_QOP_AUTH_ONLY);
+ ret[1] = 0xFF;
+ ret[2] = 0xFF;
+ ret[3] = 0xFF;
+
+ inbuf.length = 4;
+ inbuf.value = ret;
+
+ major_status = gss_wrap(&minor_status, request->gss_ctx, 0,
+ GSS_C_QOP_DEFAULT, &inbuf, NULL, &outbuf);
+
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(&request->auth_request, major_status,
+ GSS_C_GSS_CODE, "sending security layer negotiation");
+ mech_gssapi_log_error(&request->auth_request, minor_status,
+ GSS_C_MECH_CODE, "sending security layer negotiation");
+ return -1;
+ }
+
+ e_debug(request->auth_request.mech_event,
+ "Negotiated security layer");
+
+ auth_request_handler_reply_continue(&request->auth_request,
+ outbuf.value, outbuf.length);
+
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ request->sasl_gssapi_state = GSS_STATE_UNWRAP;
+ return 0;
+}
+
+#ifdef USE_KRB5_USEROK
+static bool
+k5_principal_is_authorized(struct auth_request *request, const char *name)
+{
+ const char *value, *const *authorized_names, *const *tmp;
+
+ value = auth_fields_find(request->fields.extra_fields, "k5principals");
+ if (value == NULL)
+ return FALSE;
+
+ authorized_names = t_strsplit_spaces(value, ",");
+ for (tmp = authorized_names; *tmp != NULL; tmp++) {
+ if (strcmp(*tmp, name) == 0) {
+ e_debug(request->mech_event,
+ "authorized by k5principals field: %s", name);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+mech_gssapi_krb5_userok(struct gssapi_auth_request *request,
+ gss_name_t name, const char *login_user,
+ bool check_name_type)
+{
+ krb5_context ctx;
+ krb5_principal princ;
+ krb5_error_code krb5_err;
+ gss_OID name_type;
+ const char *princ_display_name;
+ bool authorized = FALSE;
+
+ /* Parse out the principal's username */
+ if (get_display_name(&request->auth_request, name, &name_type,
+ &princ_display_name) < 0)
+ return FALSE;
+
+ if (!mech_gssapi_oid_cmp(name_type, GSS_KRB5_NT_PRINCIPAL_NAME) &&
+ check_name_type) {
+ e_info(request->auth_request.mech_event,
+ "OID not kerberos principal name");
+ return FALSE;
+ }
+
+ /* Init a krb5 context and parse the principal username */
+ krb5_err = krb5_init_context(&ctx);
+ if (krb5_err != 0) {
+ e_error(request->auth_request.mech_event,
+ "krb5_init_context() failed: %d", (int)krb5_err);
+ return FALSE;
+ }
+ krb5_err = krb5_parse_name(ctx, princ_display_name, &princ);
+ if (krb5_err != 0) {
+ /* writing the error string would be better, but we probably
+ rarely get here and there doesn't seem to be a standard
+ way of getting it */
+ e_info(request->auth_request.mech_event,
+ "krb5_parse_name() failed: %d",
+ (int)krb5_err);
+ } else {
+ /* See if the principal is in the list of authorized
+ * principals for the user */
+ authorized = k5_principal_is_authorized(&request->auth_request,
+ princ_display_name);
+
+ /* See if the principal is authorized to act as the
+ specified (UNIX) user */
+ if (!authorized) {
+ authorized = krb5_boolean2bool(krb5_kuserok(ctx, princ, login_user));
+ }
+
+ krb5_free_principal(ctx, princ);
+ }
+ krb5_free_context(ctx);
+ return authorized;
+}
+#endif
+
+static int
+mech_gssapi_userok(struct gssapi_auth_request *request, const char *login_user)
+{
+ struct auth_request *auth_request = &request->auth_request;
+ OM_uint32 major_status, minor_status;
+ int equal_authn_authz;
+#ifdef HAVE___GSS_USEROK
+ int login_ok;
+#endif
+
+ /* if authn and authz names equal, don't bother checking further. */
+ major_status = gss_compare_name(&minor_status,
+ request->authn_name,
+ request->authz_name,
+ &equal_authn_authz);
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(auth_request, major_status,
+ GSS_C_GSS_CODE,
+ "gss_compare_name failed");
+ return -1;
+ }
+
+ if (equal_authn_authz != 0)
+ return 0;
+
+ /* handle cross-realm authentication */
+#ifdef HAVE___GSS_USEROK
+ /* Solaris */
+ major_status = __gss_userok(&minor_status, request->authn_name,
+ login_user, &login_ok);
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(auth_request, major_status,
+ GSS_C_GSS_CODE, "__gss_userok failed");
+ return -1;
+ }
+
+ if (login_ok == 0) {
+ e_info(auth_request->mech_event,
+ "User not authorized to log in as %s", login_user);
+ return -1;
+ }
+ return 0;
+#elif defined(USE_KRB5_USEROK)
+ if (!mech_gssapi_krb5_userok(request, request->authn_name,
+ login_user, TRUE)) {
+ e_info(auth_request->mech_event,
+ "User not authorized to log in as %s", login_user);
+ return -1;
+ }
+
+ return 0;
+#else
+ e_info(auth_request->mech_event,
+ "Cross-realm authentication not supported "
+ "(authn_name=%s, authz_name=%s)",
+ request->auth_request.fields.original_username, login_user);
+ return -1;
+#endif
+}
+
+static void
+gssapi_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ struct auth_request *request)
+{
+ struct gssapi_auth_request *gssapi_request =
+ (struct gssapi_auth_request *)request;
+
+ /* We don't care much whether the lookup succeeded or not because GSSAPI
+ * does not strictly require a passdb. But if a passdb is configured,
+ * now the k5principals field will have been filled in. */
+ switch (result) {
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(request);
+ return;
+ case PASSDB_RESULT_USER_DISABLED:
+ case PASSDB_RESULT_PASS_EXPIRED:
+ /* user is explicitly disabled, don't allow it to log in */
+ auth_request_fail(request);
+ return;
+ case PASSDB_RESULT_NEXT:
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ case PASSDB_RESULT_OK:
+ break;
+ }
+
+ if (mech_gssapi_userok(gssapi_request, request->fields.user) == 0)
+ auth_request_success(request, NULL, 0);
+ else
+ auth_request_fail(request);
+}
+
+static int
+mech_gssapi_unwrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf)
+{
+ struct auth_request *auth_request = &request->auth_request;
+ OM_uint32 major_status, minor_status;
+ gss_buffer_desc outbuf;
+ const char *login_user, *error;
+ unsigned char *name;
+ size_t name_len;
+
+ major_status = gss_unwrap(&minor_status, request->gss_ctx,
+ &inbuf, &outbuf, NULL, NULL);
+
+ if (GSS_ERROR(major_status) != 0) {
+ mech_gssapi_log_error(auth_request, major_status,
+ GSS_C_GSS_CODE,
+ "final negotiation: gss_unwrap");
+ return -1;
+ }
+
+ /* outbuf[0] contains bitmask for selected security layer,
+ outbuf[1..3] contains maximum output_message size */
+ if (outbuf.length < 4) {
+ e_error(auth_request->mech_event,
+ "Invalid response length");
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return -1;
+ }
+
+ if (outbuf.length > 4) {
+ name = (unsigned char *)outbuf.value + 4;
+ name_len = outbuf.length - 4;
+
+ if (data_has_nuls(name, name_len)) {
+ e_info(auth_request->mech_event,
+ "authz_name has NULs");
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return -1;
+ }
+
+ login_user = p_strndup(auth_request->pool, name, name_len);
+ request->authz_name = import_name(auth_request, name, name_len);
+ } else {
+ request->authz_name = duplicate_name(auth_request,
+ request->authn_name);
+ if (get_display_name(auth_request, request->authz_name,
+ NULL, &login_user) < 0) {
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return -1;
+ }
+ }
+
+ if (request->authz_name == GSS_C_NO_NAME) {
+ e_info(auth_request->mech_event,
+ "no authz_name");
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return -1;
+ }
+
+ /* Set username early, so that the credential lookup is for the
+ * authorizing user. This means the username in subsequent log
+ * messages will be the authorization name, not the authentication
+ * name, which may mean that future log messages should be adjusted
+ * to log the right thing. */
+ if (!auth_request_set_username(auth_request, login_user, &error)) {
+ e_info(auth_request->mech_event,
+ "authz_name: %s", error);
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return -1;
+ }
+
+ /* Continue in callback once auth_request is populated with passdb
+ information. */
+ auth_request->passdb_success = TRUE; /* default to success */
+ auth_request_lookup_credentials(&request->auth_request, "",
+ gssapi_credentials_callback);
+ (void)gss_release_buffer(&minor_status, &outbuf);
+ return 0;
+}
+
+static void
+mech_gssapi_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ struct gssapi_auth_request *gssapi_request =
+ (struct gssapi_auth_request *)request;
+ gss_buffer_desc inbuf;
+ int ret = -1;
+
+ inbuf.value = (void *)data;
+ inbuf.length = data_size;
+
+ switch (gssapi_request->sasl_gssapi_state) {
+ case GSS_STATE_SEC_CONTEXT:
+ ret = mech_gssapi_sec_context(gssapi_request, inbuf);
+ break;
+ case GSS_STATE_WRAP:
+ ret = mech_gssapi_wrap(gssapi_request, inbuf);
+ break;
+ case GSS_STATE_UNWRAP:
+ ret = mech_gssapi_unwrap(gssapi_request, inbuf);
+ break;
+ default:
+ i_unreached();
+ }
+ if (ret < 0)
+ auth_request_fail(request);
+}
+
+static void
+mech_gssapi_auth_initial(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ struct gssapi_auth_request *gssapi_request =
+ (struct gssapi_auth_request *)request;
+ OM_uint32 major_status;
+
+ major_status =
+ obtain_service_credentials(request,
+ &gssapi_request->service_cred);
+
+ if (GSS_ERROR(major_status) != 0) {
+ auth_request_internal_failure(request);
+ return;
+ }
+ gssapi_request->authn_name = GSS_C_NO_NAME;
+ gssapi_request->authz_name = GSS_C_NO_NAME;
+
+ gssapi_request->sasl_gssapi_state = GSS_STATE_SEC_CONTEXT;
+
+ if (data_size == 0) {
+ /* The client should go first */
+ auth_request_handler_reply_continue(request, NULL, 0);
+ } else {
+ mech_gssapi_auth_continue(request, data, data_size);
+ }
+}
+
+static void
+mech_gssapi_auth_free(struct auth_request *request)
+{
+ struct gssapi_auth_request *gssapi_request =
+ (struct gssapi_auth_request *)request;
+ OM_uint32 minor_status;
+
+ if (gssapi_request->gss_ctx != GSS_C_NO_CONTEXT) {
+ (void)gss_delete_sec_context(&minor_status,
+ &gssapi_request->gss_ctx,
+ GSS_C_NO_BUFFER);
+ }
+
+ (void)gss_release_cred(&minor_status, &gssapi_request->service_cred);
+ if (gssapi_request->authn_name != GSS_C_NO_NAME) {
+ (void)gss_release_name(&minor_status,
+ &gssapi_request->authn_name);
+ }
+ if (gssapi_request->authz_name != GSS_C_NO_NAME) {
+ (void)gss_release_name(&minor_status,
+ &gssapi_request->authz_name);
+ }
+ pool_unref(&request->pool);
+}
+
+const struct mech_module mech_gssapi = {
+ "GSSAPI",
+
+ .flags = MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_gssapi_auth_new,
+ mech_gssapi_auth_initial,
+ mech_gssapi_auth_continue,
+ mech_gssapi_auth_free
+};
+
+/* MTI Kerberos v1.5+ and Heimdal v0.7+ supports SPNEGO for Kerberos tickets
+ internally. Nothing else needs to be done here. Note however that this does
+ not support SPNEGO when the only available credential is NTLM.. */
+const struct mech_module mech_gssapi_spnego = {
+ "GSS-SPNEGO",
+
+ .flags = MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_gssapi_auth_new,
+ mech_gssapi_auth_initial,
+ mech_gssapi_auth_continue,
+ mech_gssapi_auth_free
+};
+
+#ifndef BUILTIN_GSSAPI
+void mech_gssapi_init(void);
+void mech_gssapi_deinit(void);
+
+void mech_gssapi_init(void)
+{
+ mech_register_module(&mech_gssapi);
+#ifdef HAVE_GSSAPI_SPNEGO
+ /* load if we already didn't load it using winbind */
+ if (mech_module_find(mech_gssapi_spnego.mech_name) == NULL)
+ mech_register_module(&mech_gssapi_spnego);
+#endif
+}
+
+void mech_gssapi_deinit(void)
+{
+#ifdef HAVE_GSSAPI_SPNEGO
+ const struct mech_module *mech;
+
+ mech = mech_module_find(mech_gssapi_spnego.mech_name);
+ if (mech != NULL && mech->auth_new == mech_gssapi_auth_new)
+ mech_unregister_module(&mech_gssapi_spnego);
+#endif
+ mech_unregister_module(&mech_gssapi);
+}
+#endif
+
+#endif
diff --git a/src/auth/mech-login.c b/src/auth/mech-login.c
new file mode 100644
index 0000000..6f27433
--- /dev/null
+++ b/src/auth/mech-login.c
@@ -0,0 +1,76 @@
+/*
+ * LOGIN authentication mechanism.
+ *
+ * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "mech.h"
+#include "passdb.h"
+#include "safe-memset.h"
+#include "mech-plain-common.h"
+
+
+static void
+mech_login_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ static const char prompt2[] = "Password:";
+ const char *username, *error;
+
+ if (request->fields.user == NULL) {
+ username = t_strndup(data, data_size);
+
+ if (!auth_request_set_username(request, username, &error)) {
+ e_info(request->mech_event, "%s", error);
+ auth_request_fail(request);
+ return;
+ }
+
+ auth_request_handler_reply_continue(request, prompt2,
+ strlen(prompt2));
+ } else {
+ char *pass = p_strndup(unsafe_data_stack_pool, data, data_size);
+ auth_request_verify_plain(request, pass, plain_verify_callback);
+ safe_memset(pass, 0, strlen(pass));
+ }
+}
+
+static void
+mech_login_auth_initial(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ static const char prompt1[] = "Username:";
+
+ if (data_size == 0) {
+ auth_request_handler_reply_continue(request, prompt1,
+ strlen(prompt1));
+ } else {
+ mech_login_auth_continue(request, data, data_size);
+ }
+}
+
+static struct auth_request *mech_login_auth_new(void)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"login_auth_request", 2048);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ return request;
+}
+
+const struct mech_module mech_login = {
+ "LOGIN",
+
+ .flags = MECH_SEC_PLAINTEXT,
+ .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN,
+
+ mech_login_auth_new,
+ mech_login_auth_initial,
+ mech_login_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-oauth2.c b/src/auth/mech-oauth2.c
new file mode 100644
index 0000000..dae5632
--- /dev/null
+++ b/src/auth/mech-oauth2.c
@@ -0,0 +1,339 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "mech.h"
+#include "passdb.h"
+#include "oauth2.h"
+#include "json-parser.h"
+#include <ctype.h>
+
+struct oauth2_auth_request {
+ struct auth_request auth;
+ bool failed;
+};
+
+static bool oauth2_find_oidc_url(struct auth_request *req, const char **url_r)
+{
+ struct auth_passdb *db = req->passdb;
+ if (req->openid_config_url != NULL) {
+ *url_r = req->openid_config_url;
+ return TRUE;
+ }
+
+ /* keep looking until you get a value */
+ for (; db != NULL; db = db->next) {
+ if (strcmp(db->passdb->iface.name, "oauth2") == 0) {
+ const char *url =
+ passdb_oauth2_get_oidc_url(req->passdb->passdb);
+ if (url == NULL || *url == '\0')
+ continue;
+ *url_r = url;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* RFC5801 based unescaping */
+static bool oauth2_unescape_username(const char *in, const char **username_r)
+{
+ string_t *out;
+ out = t_str_new(64);
+ for (; *in != '\0'; in++) {
+ if (in[0] == ',')
+ return FALSE;
+ if (in[0] == '=') {
+ if (in[1] == '2' && in[2] == 'C')
+ str_append_c(out, ',');
+ else if (in[1] == '3' && in[2] == 'D')
+ str_append_c(out, '=');
+ else
+ return FALSE;
+ in += 2;
+ } else {
+ str_append_c(out, *in);
+ }
+ }
+ *username_r = str_c(out);
+ return TRUE;
+}
+
+static void oauth2_verify_callback(enum passdb_result result,
+ const char *const *error_fields,
+ struct auth_request *request)
+{
+ const char *oidc_url;
+
+ i_assert(result == PASSDB_RESULT_OK || error_fields != NULL);
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ auth_request_success(request, "", 0);
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ request->internal_failure = TRUE;
+ /* fall through */
+ default:
+ /* we could get new token after this */
+ if (request->mech_password != NULL)
+ request->mech_password = NULL;
+ string_t *error = t_str_new(64);
+ str_append_c(error, '{');
+ for (unsigned int i = 0; error_fields[i] != NULL; i += 2) {
+ i_assert(error_fields[i+1] != NULL);
+ if (i > 0)
+ str_append_c(error, ',');
+ str_append_c(error, '"');
+ json_append_escaped(error, error_fields[i]);
+ str_append(error, "\":\"");
+ json_append_escaped(error, error_fields[i+1]);
+ str_append_c(error, '"');
+ }
+ /* FIXME: HORRIBLE HACK - REMOVE ME!!!
+ It is because the mech has not been implemented properly
+ that we need to pass the config url in this strange way.
+
+ This **must** be removed from here and db-oauth2 once the
+ validation result et al is handled here.
+ */
+ if (oauth2_find_oidc_url(request, &oidc_url)) {
+ if (str_len(error) > 0)
+ str_append_c(error, ',');
+ str_printfa(error, "\"openid-configuration\":\"");
+ json_append_escaped(error, oidc_url);
+ str_append_c(error, '"');
+ }
+ str_append_c(error, '}');
+ auth_request_fail_with_reply(request, str_data(error), str_len(error));
+ break;
+ }
+}
+
+static void mech_oauth2_verify_token(struct auth_request *request,
+ const char *token,
+ enum passdb_result result,
+ verify_plain_callback_t callback)
+{
+ i_assert(token != NULL);
+ if (result != PASSDB_RESULT_OK) {
+ request->passdb_result = result;
+ request->failed = TRUE;
+ }
+ auth_request_verify_plain(request, token, callback);
+}
+
+static void
+xoauth2_verify_callback(enum passdb_result result, struct auth_request *request)
+{
+ const char *const error_fields[] = {
+ "status", "401",
+ "schemes", "bearer",
+ "scope", "mail",
+ NULL
+ };
+ oauth2_verify_callback(result, error_fields, request);
+}
+
+static void
+oauthbearer_verify_callback(enum passdb_result result, struct auth_request *request)
+{
+ const char *error_fields[] = {
+ "status", "invalid_token",
+ NULL
+ };
+ oauth2_verify_callback(result, error_fields, request);
+}
+
+/* Input syntax:
+ user=Username^Aauth=Bearer token^A^A
+*/
+static void
+mech_xoauth2_auth_continue(struct auth_request *request,
+ const unsigned char *data,
+ size_t data_size)
+{
+ /* split the data from ^A */
+ bool user_given = FALSE;
+ const char *error;
+ const char *token = NULL;
+ const char *const *ptr;
+ const char *username;
+ const char *const *fields =
+ t_strsplit(t_strndup(data, data_size), "\x01");
+ for(ptr = fields; *ptr != NULL; ptr++) {
+ if (str_begins(*ptr, "user=")) {
+ /* xoauth2 does not require unescaping because the data
+ format does not contain anything to escape */
+ username = (*ptr)+5;
+ user_given = TRUE;
+ } else if (str_begins(*ptr, "auth=")) {
+ const char *value = (*ptr)+5;
+ if (strncasecmp(value, "bearer ", 7) == 0 &&
+ oauth2_valid_token(value+7)) {
+ token = value+7;
+ } else {
+ e_info(request->mech_event,
+ "Invalid continued data");
+ xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ }
+ }
+ /* do not fail on unexpected fields */
+ }
+
+ if (user_given && !auth_request_set_username(request, username, &error)) {
+ e_info(request->mech_event,
+ "%s", error);
+ xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, request);
+ return;
+ }
+
+ if (user_given && token != NULL)
+ mech_oauth2_verify_token(request, token, PASSDB_RESULT_OK,
+ xoauth2_verify_callback);
+ else {
+ e_info(request->mech_event, "Username or token missing");
+ xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, request);
+ }
+}
+
+/* Input syntax for data:
+ gs2flag,a=username,^Afield=...^Afield=...^Aauth=Bearer token^A^A
+*/
+static void
+mech_oauthbearer_auth_continue(struct auth_request *request,
+ const unsigned char *data,
+ size_t data_size)
+{
+ bool user_given = FALSE;
+ const char *error;
+ const char *username;
+ const char *const *ptr;
+ /* split the data from ^A */
+ const char **fields =
+ t_strsplit(t_strndup(data, data_size), "\x01");
+ const char *token = NULL;
+ /* ensure initial field is OK */
+ if (*fields == NULL || *(fields[0]) == '\0') {
+ e_info(request->mech_event,
+ "Invalid continued data");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ }
+
+ /* the first field is specified by RFC5801 as gs2-header */
+ for(ptr = t_strsplit_spaces(fields[0], ","); *ptr != NULL; ptr++) {
+ switch(*ptr[0]) {
+ case 'f':
+ e_info(request->mech_event,
+ "Client requested non-standard mechanism");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ case 'p':
+ /* channel binding is not supported */
+ e_info(request->mech_event,
+ "Client requested and used channel-binding");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ case 'n':
+ case 'y':
+ /* we don't need to use channel-binding */
+ continue;
+ case 'a': /* authzid */
+ if ((*ptr)[1] != '=' ||
+ !oauth2_unescape_username((*ptr)+2, &username)) {
+ e_info(request->mech_event,
+ "Invalid username escaping");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ } else {
+ user_given = TRUE;
+ }
+ break;
+ default:
+ e_info(request->mech_event,
+ "Invalid gs2-header in request");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ }
+ }
+
+ for(ptr = fields; *ptr != NULL; ptr++) {
+ if (str_begins(*ptr, "auth=")) {
+ const char *value = (*ptr)+5;
+ if (strncasecmp(value, "bearer ", 7) == 0 &&
+ oauth2_valid_token(value+7)) {
+ token = value+7;
+ } else {
+ e_info(request->mech_event,
+ "Invalid continued data");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ }
+ }
+ /* do not fail on unexpected fields */
+ }
+ if (user_given && !auth_request_set_username(request, username, &error)) {
+ e_info(request->mech_event,
+ "%s", error);
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ return;
+ }
+ if (user_given && token != NULL)
+ mech_oauth2_verify_token(request, token, PASSDB_RESULT_OK,
+ oauthbearer_verify_callback);
+ else {
+ e_info(request->mech_event, "Missing username or token");
+ oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH,
+ request);
+ }
+}
+
+static struct auth_request *mech_oauth2_auth_new(void)
+{
+ struct oauth2_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"oauth2_auth_request", 2048);
+ request = p_new(pool, struct oauth2_auth_request, 1);
+ request->auth.pool = pool;
+ return &request->auth;
+}
+
+const struct mech_module mech_oauthbearer = {
+ "OAUTHBEARER",
+
+ /* while this does not transfer plaintext password,
+ the token is still considered as password */
+ .flags = MECH_SEC_PLAINTEXT,
+ .passdb_need = 0,
+
+ mech_oauth2_auth_new,
+ mech_generic_auth_initial,
+ mech_oauthbearer_auth_continue,
+ mech_generic_auth_free
+};
+
+const struct mech_module mech_xoauth2 = {
+ "XOAUTH2",
+
+ .flags = MECH_SEC_PLAINTEXT,
+ .passdb_need = 0,
+
+ mech_oauth2_auth_new,
+ mech_generic_auth_initial,
+ mech_xoauth2_auth_continue,
+ mech_generic_auth_free
+};
+
+
diff --git a/src/auth/mech-otp-common.c b/src/auth/mech-otp-common.c
new file mode 100644
index 0000000..753fcbb
--- /dev/null
+++ b/src/auth/mech-otp-common.c
@@ -0,0 +1,71 @@
+/*
+ * Common code for OTP authentication mechanisms.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "hash.h"
+#include "mech.h"
+
+#include "otp.h"
+#include "mech-otp-common.h"
+
+static HASH_TABLE(char *, struct auth_request *) otp_lock_table;
+
+void otp_lock_init(void)
+{
+ if (hash_table_is_created(otp_lock_table))
+ return;
+
+ hash_table_create(&otp_lock_table, default_pool, 128,
+ strcase_hash, strcasecmp);
+}
+
+bool otp_try_lock(struct auth_request *auth_request)
+{
+ if (hash_table_lookup(otp_lock_table, auth_request->fields.user) != NULL)
+ return FALSE;
+
+ hash_table_insert(otp_lock_table, auth_request->fields.user, auth_request);
+ return TRUE;
+}
+
+void otp_unlock(struct auth_request *auth_request)
+{
+ struct otp_auth_request *request =
+ (struct otp_auth_request *)auth_request;
+
+ if (!request->lock)
+ return;
+
+ hash_table_remove(otp_lock_table, auth_request->fields.user);
+ request->lock = FALSE;
+}
+
+void otp_set_credentials_callback(bool success,
+ struct auth_request *auth_request)
+{
+ if (success)
+ auth_request_success(auth_request, "", 0);
+ else {
+ auth_request_internal_failure(auth_request);
+ otp_unlock(auth_request);
+ }
+
+ otp_unlock(auth_request);
+}
+
+void mech_otp_auth_free(struct auth_request *auth_request)
+{
+ otp_unlock(auth_request);
+
+ pool_unref(&auth_request->pool);
+}
+
+void mech_otp_deinit(void)
+{
+ hash_table_destroy(&otp_lock_table);
+}
diff --git a/src/auth/mech-otp-common.h b/src/auth/mech-otp-common.h
new file mode 100644
index 0000000..37a6551
--- /dev/null
+++ b/src/auth/mech-otp-common.h
@@ -0,0 +1,23 @@
+#ifndef MECH_OTP_COMMON_H
+#define MECH_OTP_COMMON_H
+
+struct otp_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ bool lock;
+
+ struct otp_state state;
+};
+
+void otp_lock_init(void);
+bool otp_try_lock(struct auth_request *auth_request);
+void otp_unlock(struct auth_request *auth_request);
+
+void otp_set_credentials_callback(bool success,
+ struct auth_request *auth_request);
+void mech_otp_auth_free(struct auth_request *auth_request);
+void mech_otp_deinit(void);
+
+#endif
diff --git a/src/auth/mech-otp.c b/src/auth/mech-otp.c
new file mode 100644
index 0000000..58f3db1
--- /dev/null
+++ b/src/auth/mech-otp.c
@@ -0,0 +1,240 @@
+/*
+ * One-Time-Password (RFC 2444) authentication mechanism.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "safe-memset.h"
+#include "hash.h"
+#include "mech.h"
+#include "passdb.h"
+#include "hex-binary.h"
+#include "otp.h"
+#include "mech-otp-common.h"
+
+static void
+otp_send_challenge(struct auth_request *auth_request,
+ const unsigned char *credentials, size_t size)
+{
+ struct otp_auth_request *request =
+ (struct otp_auth_request *)auth_request;
+ const char *answer;
+
+ if (otp_parse_dbentry(t_strndup(credentials, size),
+ &request->state) != 0) {
+ e_error(request->auth_request.mech_event,
+ "invalid OTP data in passdb");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ if (--request->state.seq < 1) {
+ e_error(request->auth_request.mech_event,
+ "sequence number < 1");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ request->lock = otp_try_lock(auth_request);
+ if (!request->lock) {
+ e_error(request->auth_request.mech_event,
+ "user is locked, race attack?");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ answer = p_strdup_printf(request->pool, "otp-%s %u %s ext",
+ digest_name(request->state.algo),
+ request->state.seq, request->state.seed);
+
+ auth_request_handler_reply_continue(auth_request, answer,
+ strlen(answer));
+}
+
+static void
+otp_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *auth_request)
+{
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ otp_send_challenge(auth_request, credentials, size);
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(auth_request);
+ break;
+ default:
+ auth_request_fail(auth_request);
+ break;
+ }
+}
+
+static void
+mech_otp_auth_phase1(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct otp_auth_request *request =
+ (struct otp_auth_request *)auth_request;
+ const char *authenid, *error;
+ size_t i, count;
+
+ /* authorization ID \0 authentication ID
+ FIXME: we'll ignore authorization ID for now. */
+ authenid = NULL;
+
+ count = 0;
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\0') {
+ if (++count == 1)
+ authenid = (const char *) data + i + 1;
+ }
+ }
+
+ if (count != 1) {
+ e_error(request->auth_request.mech_event,
+ "invalid input");
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ if (!auth_request_set_username(auth_request, authenid, &error)) {
+ e_info(auth_request->mech_event, "%s", error);
+ auth_request_fail(auth_request);
+ return;
+ }
+
+ auth_request_lookup_credentials(auth_request, "OTP",
+ otp_credentials_callback);
+}
+
+static void mech_otp_verify(struct auth_request *auth_request,
+ const char *data, bool hex)
+{
+ struct otp_auth_request *request =
+ (struct otp_auth_request *)auth_request;
+ struct otp_state *state = &request->state;
+ unsigned char hash[OTP_HASH_SIZE], cur_hash[OTP_HASH_SIZE];
+ int ret;
+
+ ret = otp_parse_response(data, hash, hex);
+ if (ret < 0) {
+ e_error(request->auth_request.mech_event,
+ "invalid response");
+ auth_request_fail(auth_request);
+ otp_unlock(auth_request);
+ return;
+ }
+
+ otp_next_hash(state->algo, hash, cur_hash);
+
+ ret = memcmp(cur_hash, state->hash, OTP_HASH_SIZE);
+ if (ret != 0) {
+ auth_request_fail(auth_request);
+ otp_unlock(auth_request);
+ return;
+ }
+
+ memcpy(state->hash, hash, sizeof(state->hash));
+
+ auth_request_set_credentials(auth_request, "OTP",
+ otp_print_dbentry(state),
+ otp_set_credentials_callback);
+}
+
+static void mech_otp_verify_init(struct auth_request *auth_request,
+ const char *data, bool hex)
+{
+ struct otp_auth_request *request =
+ (struct otp_auth_request *)auth_request;
+ struct otp_state new_state;
+ unsigned char hash[OTP_HASH_SIZE], cur_hash[OTP_HASH_SIZE];
+ const char *error;
+ int ret;
+
+ ret = otp_parse_init_response(data, &new_state, cur_hash, hex, &error);
+ if (ret < 0) {
+ e_error(request->auth_request.mech_event,
+ "invalid init response, %s", error);
+ auth_request_fail(auth_request);
+ otp_unlock(auth_request);
+ return;
+ }
+
+ otp_next_hash(request->state.algo, cur_hash, hash);
+
+ ret = memcmp(hash, request->state.hash, OTP_HASH_SIZE);
+ if (ret != 0) {
+ auth_request_fail(auth_request);
+ otp_unlock(auth_request);
+ return;
+ }
+
+ auth_request_set_credentials(auth_request, "OTP",
+ otp_print_dbentry(&new_state),
+ otp_set_credentials_callback);
+}
+
+static void
+mech_otp_auth_phase2(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ const char *str = t_strndup(data, data_size);
+
+ if (str_begins(str, "hex:")) {
+ mech_otp_verify(auth_request, str + 4, TRUE);
+ } else if (str_begins(str, "word:")) {
+ mech_otp_verify(auth_request, str + 5, FALSE);
+ } else if (str_begins(str, "init-hex:")) {
+ mech_otp_verify_init(auth_request, str + 9, TRUE);
+ } else if (str_begins(str, "init-word:")) {
+ mech_otp_verify_init(auth_request, str + 10, FALSE);
+ } else {
+ e_error(auth_request->mech_event,
+ "unsupported response type");
+ auth_request_fail(auth_request);
+ otp_unlock(auth_request);
+ }
+}
+
+static void
+mech_otp_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ if (auth_request->fields.user == NULL) {
+ mech_otp_auth_phase1(auth_request, data, data_size);
+ } else {
+ mech_otp_auth_phase2(auth_request, data, data_size);
+ }
+}
+
+static struct auth_request *mech_otp_auth_new(void)
+{
+ struct otp_auth_request *request;
+ pool_t pool;
+
+ otp_lock_init();
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"otp_auth_request", 2048);
+ request = p_new(pool, struct otp_auth_request, 1);
+ request->pool = pool;
+ request->lock = FALSE;
+
+ request->auth_request.refcount = 1;
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+const struct mech_module mech_otp = {
+ "OTP",
+
+ .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_SET_CREDENTIALS,
+
+ mech_otp_auth_new,
+ mech_generic_auth_initial,
+ mech_otp_auth_continue,
+ mech_otp_auth_free
+};
diff --git a/src/auth/mech-plain-common.c b/src/auth/mech-plain-common.c
new file mode 100644
index 0000000..2d947b9
--- /dev/null
+++ b/src/auth/mech-plain-common.c
@@ -0,0 +1,22 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "mech.h"
+#include "passdb.h"
+#include "mech-plain-common.h"
+
+void plain_verify_callback(enum passdb_result result,
+ struct auth_request *request)
+{
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ auth_request_success(request, "", 0);
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(request);
+ break;
+ default:
+ auth_request_fail(request);
+ break;
+ }
+}
diff --git a/src/auth/mech-plain-common.h b/src/auth/mech-plain-common.h
new file mode 100644
index 0000000..1dc339c
--- /dev/null
+++ b/src/auth/mech-plain-common.h
@@ -0,0 +1,7 @@
+#ifndef MECH_PLAIN_COMMON_H
+#define MECH_PLAIN_COMMON_H
+
+void plain_verify_callback(enum passdb_result result,
+ struct auth_request *request);
+
+#endif
diff --git a/src/auth/mech-plain.c b/src/auth/mech-plain.c
new file mode 100644
index 0000000..3bb715f
--- /dev/null
+++ b/src/auth/mech-plain.c
@@ -0,0 +1,88 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "safe-memset.h"
+#include "mech.h"
+#include "passdb.h"
+#include "mech-plain-common.h"
+
+static void
+mech_plain_auth_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ const char *authid, *authenid, *error;
+ char *pass;
+ size_t i, len;
+ int count;
+
+ /* authorization ID \0 authentication ID \0 pass. */
+ authid = (const char *) data;
+ authenid = NULL; pass = NULL;
+
+ count = 0;
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\0') {
+ if (++count == 1)
+ authenid = (const char *) data + i+1;
+ else if (count == 2) {
+ i++;
+ len = data_size - i;
+ pass = p_strndup(unsafe_data_stack_pool,
+ data+i, len);
+ }
+ else
+ break;
+ }
+ }
+
+ if (count == 2 && authenid != NULL && strcmp(authid, authenid) == 0) {
+ /* the login username isn't different */
+ authid = "";
+ }
+
+ if (count != 2) {
+ /* invalid input */
+ e_info(request->mech_event, "invalid input");
+ auth_request_fail(request);
+ } else if (!auth_request_set_username(request, authenid, &error)) {
+ /* invalid username */
+ e_info(request->mech_event, "%s", error);
+ auth_request_fail(request);
+ } else if (*authid != '\0' &&
+ !auth_request_set_login_username(request, authid, &error)) {
+ /* invalid login username */
+ e_info(request->mech_event,
+ "login user: %s", error);
+ auth_request_fail(request);
+ } else {
+ auth_request_verify_plain(request, pass,
+ plain_verify_callback);
+ }
+
+ /* make sure it's cleared */
+ if (pass != NULL)
+ safe_memset(pass, 0, strlen(pass));
+}
+
+static struct auth_request *mech_plain_auth_new(void)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"plain_auth_request", 2048);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ return request;
+}
+
+const struct mech_module mech_plain = {
+ "PLAIN",
+
+ .flags = MECH_SEC_PLAINTEXT | MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN,
+
+ mech_plain_auth_new,
+ mech_generic_auth_initial,
+ mech_plain_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-scram.c b/src/auth/mech-scram.c
new file mode 100644
index 0000000..a90d0d1
--- /dev/null
+++ b/src/auth/mech-scram.c
@@ -0,0 +1,550 @@
+/*
+ * SCRAM-SHA-1 SASL authentication, see RFC-5802
+ *
+ * Copyright (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include <limits.h>
+
+#include "auth-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hmac.h"
+#include "sha1.h"
+#include "sha2.h"
+#include "randgen.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "strnum.h"
+#include "password-scheme.h"
+#include "mech.h"
+#include "mech-scram.h"
+
+/* s-nonce length */
+#define SCRAM_SERVER_NONCE_LEN 64
+
+struct scram_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ const struct hash_method *hash_method;
+ const char *password_scheme;
+
+ /* sent: */
+ const char *server_first_message;
+ const char *snonce;
+
+ /* received: */
+ const char *gs2_header;
+ const char *cnonce;
+ const char *client_first_message_bare;
+ const char *client_final_message_without_proof;
+ buffer_t *proof;
+
+ /* stored */
+ unsigned char *stored_key;
+ unsigned char *server_key;
+};
+
+static const char *
+get_scram_server_first(struct scram_auth_request *request,
+ int iter, const char *salt)
+{
+ unsigned char snonce[SCRAM_SERVER_NONCE_LEN+1];
+ string_t *str;
+ size_t i;
+
+ /* RFC 5802, Section 7:
+
+ server-first-message =
+ [reserved-mext ","] nonce "," salt ","
+ iteration-count ["," extensions]
+
+ nonce = "r=" c-nonce [s-nonce]
+
+ salt = "s=" base64
+
+ iteration-count = "i=" posit-number
+ ;; A positive number.
+ */
+
+ random_fill(snonce, sizeof(snonce)-1);
+
+ /* make sure snonce is printable and does not contain ',' */
+ for (i = 0; i < sizeof(snonce)-1; i++) {
+ snonce[i] = (snonce[i] % ('~' - '!')) + '!';
+ if (snonce[i] == ',')
+ snonce[i] = '~';
+ }
+ snonce[sizeof(snonce)-1] = '\0';
+ request->snonce = p_strndup(request->pool, snonce, sizeof(snonce));
+
+ str = t_str_new(32 + strlen(request->cnonce) + sizeof(snonce) +
+ strlen(salt));
+ str_printfa(str, "r=%s%s,s=%s,i=%d", request->cnonce, request->snonce,
+ salt, iter);
+ return str_c(str);
+}
+
+static const char *get_scram_server_final(struct scram_auth_request *request)
+{
+ const struct hash_method *hmethod = request->hash_method;
+ struct hmac_context ctx;
+ const char *auth_message;
+ unsigned char server_signature[hmethod->digest_size];
+ string_t *str;
+
+ /* RFC 5802, Section 3:
+
+ AuthMessage := client-first-message-bare + "," +
+ server-first-message + "," +
+ client-final-message-without-proof
+ ServerSignature := HMAC(ServerKey, AuthMessage)
+ */
+ auth_message = t_strconcat(request->client_first_message_bare, ",",
+ request->server_first_message, ",",
+ request->client_final_message_without_proof, NULL);
+
+ hmac_init(&ctx, request->server_key, hmethod->digest_size, hmethod);
+ hmac_update(&ctx, auth_message, strlen(auth_message));
+ hmac_final(&ctx, server_signature);
+
+ /* RFC 5802, Section 7:
+
+ server-final-message = (server-error / verifier)
+ ["," extensions]
+
+ verifier = "v=" base64
+ ;; base-64 encoded ServerSignature.
+
+ */
+ str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(sizeof(server_signature)));
+ str_append(str, "v=");
+ base64_encode(server_signature, sizeof(server_signature), str);
+
+ return str_c(str);
+}
+
+static const char *scram_unescape_username(const char *in)
+{
+ string_t *out;
+
+ /* RFC 5802, Section 5.1:
+
+ The characters ',' or '=' in usernames are sent as '=2C' and '=3D'
+ respectively. If the server receives a username that contains '='
+ not followed by either '2C' or '3D', then the server MUST fail the
+ authentication.
+ */
+
+ out = t_str_new(64);
+ for (; *in != '\0'; in++) {
+ i_assert(in[0] != ','); /* strsplit should have caught this */
+
+ if (in[0] == '=') {
+ if (in[1] == '2' && in[2] == 'C')
+ str_append_c(out, ',');
+ else if (in[1] == '3' && in[2] == 'D')
+ str_append_c(out, '=');
+ else
+ return NULL;
+ in += 2;
+ } else {
+ str_append_c(out, *in);
+ }
+ }
+ return str_c(out);
+}
+
+static bool
+parse_scram_client_first(struct scram_auth_request *request,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ const char *login_username = NULL;
+ const char *data_cstr, *p;
+ const char *gs2_header, *gs2_cbind_flag, *authzid;
+ const char *cfm_bare, *username, *nonce;
+ const char *const *fields;
+
+ data_cstr = gs2_header = t_strndup(data, size);
+
+ /* RFC 5802, Section 7:
+
+ client-first-message = gs2-header client-first-message-bare
+ gs2-header = gs2-cbind-flag "," [ authzid ] ","
+
+ client-first-message-bare = [reserved-mext ","]
+ username "," nonce ["," extensions]
+
+ extensions = attr-val *("," attr-val)
+ ;; All extensions are optional,
+ ;; i.e., unrecognized attributes
+ ;; not defined in this document
+ ;; MUST be ignored.
+ attr-val = ALPHA "=" value
+ */
+ p = strchr(data_cstr, ',');
+ if (p == NULL) {
+ *error_r = "Invalid initial client message: "
+ "Missing first ',' in GS2 header";
+ return FALSE;
+ }
+ gs2_cbind_flag = t_strdup_until(data_cstr, p);
+ data_cstr = p + 1;
+
+ p = strchr(data_cstr, ',');
+ if (p == NULL) {
+ *error_r = "Invalid initial client message: "
+ "Missing second ',' in GS2 header";
+ return FALSE;
+ }
+ authzid = t_strdup_until(data_cstr, p);
+ gs2_header = t_strdup_until(gs2_header, p + 1);
+ cfm_bare = p + 1;
+
+ fields = t_strsplit(cfm_bare, ",");
+ if (str_array_length(fields) < 2) {
+ *error_r = "Invalid initial client message: "
+ "Missing nonce field";
+ return FALSE;
+ }
+ username = fields[0];
+ nonce = fields[1];
+
+ /* gs2-cbind-flag = ("p=" cb-name) / "n" / "y"
+ */
+ switch (gs2_cbind_flag[0]) {
+ case 'p':
+ *error_r = "Channel binding not supported";
+ return FALSE;
+ case 'y':
+ case 'n':
+ break;
+ default:
+ *error_r = "Invalid GS2 header";
+ return FALSE;
+ }
+
+ /* authzid = "a=" saslname
+ ;; Protocol specific.
+ */
+ if (authzid[0] == '\0')
+ ;
+ else if (authzid[0] == 'a' && authzid[1] == '=') {
+ /* Unescape authzid */
+ login_username = scram_unescape_username(authzid + 2);
+
+ if (login_username == NULL) {
+ *error_r = "authzid escaping is invalid";
+ return FALSE;
+ }
+ } else {
+ *error_r = "Invalid authzid field";
+ return FALSE;
+ }
+
+ /* reserved-mext = "m=" 1*(value-char)
+ */
+ if (username[0] == 'm') {
+ *error_r = "Mandatory extension(s) not supported";
+ return FALSE;
+ }
+ /* username = "n=" saslname
+ */
+ if (username[0] == 'n' && username[1] == '=') {
+ /* Unescape username */
+ username = scram_unescape_username(username + 2);
+ if (username == NULL) {
+ *error_r = "Username escaping is invalid";
+ return FALSE;
+ }
+ if (!auth_request_set_username(&request->auth_request,
+ username, error_r))
+ return FALSE;
+ } else {
+ *error_r = "Invalid username field";
+ return FALSE;
+ }
+ if (login_username != NULL) {
+ if (!auth_request_set_login_username(&request->auth_request,
+ login_username, error_r))
+ return FALSE;
+ }
+
+ /* nonce = "r=" c-nonce [s-nonce] */
+ if (nonce[0] == 'r' && nonce[1] == '=')
+ request->cnonce = p_strdup(request->pool, nonce+2);
+ else {
+ *error_r = "Invalid client nonce";
+ return FALSE;
+ }
+
+ request->gs2_header = p_strdup(request->pool, gs2_header);
+ request->client_first_message_bare = p_strdup(request->pool, cfm_bare);
+ return TRUE;
+}
+
+static bool verify_credentials(struct scram_auth_request *request)
+{
+ const struct hash_method *hmethod = request->hash_method;
+ struct hmac_context ctx;
+ const char *auth_message;
+ unsigned char client_key[hmethod->digest_size];
+ unsigned char client_signature[hmethod->digest_size];
+ unsigned char stored_key[hmethod->digest_size];
+ size_t i;
+
+ /* RFC 5802, Section 3:
+
+ AuthMessage := client-first-message-bare + "," +
+ server-first-message + "," +
+ client-final-message-without-proof
+ ClientSignature := HMAC(StoredKey, AuthMessage)
+ */
+ auth_message = t_strconcat(request->client_first_message_bare, ",",
+ request->server_first_message, ",",
+ request->client_final_message_without_proof, NULL);
+
+ hmac_init(&ctx, request->stored_key, hmethod->digest_size, hmethod);
+ hmac_update(&ctx, auth_message, strlen(auth_message));
+ hmac_final(&ctx, client_signature);
+
+ /* ClientProof := ClientKey XOR ClientSignature */
+ const unsigned char *proof_data = request->proof->data;
+ for (i = 0; i < sizeof(client_signature); i++)
+ client_key[i] = proof_data[i] ^ client_signature[i];
+
+ /* StoredKey := H(ClientKey) */
+ hash_method_get_digest(hmethod, client_key, sizeof(client_key),
+ stored_key);
+
+ safe_memset(client_key, 0, sizeof(client_key));
+ safe_memset(client_signature, 0, sizeof(client_signature));
+
+ return mem_equals_timing_safe(stored_key, request->stored_key,
+ sizeof(stored_key));
+}
+
+static void
+credentials_callback(enum passdb_result result,
+ const unsigned char *credentials, size_t size,
+ struct auth_request *auth_request)
+{
+ struct scram_auth_request *request =
+ (struct scram_auth_request *)auth_request;
+ const char *salt, *error;
+ unsigned int iter_count;
+
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ if (scram_scheme_parse(request->hash_method,
+ request->password_scheme,
+ credentials, size, &iter_count, &salt,
+ request->stored_key, request->server_key,
+ &error) < 0) {
+ e_info(auth_request->mech_event,
+ "%s", error);
+ auth_request_fail(auth_request);
+ break;
+ }
+
+ request->server_first_message = p_strdup(request->pool,
+ get_scram_server_first(request, iter_count, salt));
+
+ auth_request_handler_reply_continue(auth_request,
+ request->server_first_message,
+ strlen(request->server_first_message));
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ auth_request_internal_failure(auth_request);
+ break;
+ default:
+ auth_request_fail(auth_request);
+ break;
+ }
+}
+
+static bool
+parse_scram_client_final(struct scram_auth_request *request,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ const struct hash_method *hmethod = request->hash_method;
+ const char **fields, *cbind_input, *nonce_str;
+ unsigned int field_count;
+ string_t *str;
+
+ /* RFC 5802, Section 7:
+
+ client-final-message-without-proof =
+ channel-binding "," nonce [","
+ extensions]
+ client-final-message =
+ client-final-message-without-proof "," proof
+ */
+ fields = t_strsplit(t_strndup(data, size), ",");
+ field_count = str_array_length(fields);
+ if (field_count < 3) {
+ *error_r = "Invalid final client message";
+ return FALSE;
+ }
+
+ /* channel-binding = "c=" base64
+ ;; base64 encoding of cbind-input.
+
+ cbind-data = 1*OCTET
+ cbind-input = gs2-header [ cbind-data ]
+ ;; cbind-data MUST be present for
+ ;; gs2-cbind-flag of "p" and MUST be absent
+ ;; for "y" or "n".
+ */
+ cbind_input = request->gs2_header;
+ str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(strlen(cbind_input)));
+ str_append(str, "c=");
+ base64_encode(cbind_input, strlen(cbind_input), str);
+
+ if (strcmp(fields[0], str_c(str)) != 0) {
+ *error_r = "Invalid channel binding data";
+ return FALSE;
+ }
+
+ /* nonce = "r=" c-nonce [s-nonce]
+ ;; Second part provided by server.
+ c-nonce = printable
+ s-nonce = printable
+ */
+ nonce_str = t_strconcat("r=", request->cnonce, request->snonce, NULL);
+ if (strcmp(fields[1], nonce_str) != 0) {
+ *error_r = "Wrong nonce";
+ return FALSE;
+ }
+
+ /* proof = "p=" base64
+ */
+ if (fields[field_count-1][0] == 'p') {
+ size_t len = strlen(&fields[field_count-1][2]);
+
+ request->proof = buffer_create_dynamic(request->pool,
+ MAX_BASE64_DECODED_SIZE(len));
+ if (base64_decode(&fields[field_count-1][2], len, NULL,
+ request->proof) < 0) {
+ *error_r = "Invalid base64 encoding";
+ return FALSE;
+ }
+ if (request->proof->used != hmethod->digest_size) {
+ *error_r = "Invalid ClientProof length";
+ return FALSE;
+ }
+ } else {
+ *error_r = "Invalid ClientProof";
+ return FALSE;
+ }
+
+ (void)str_array_remove(fields, fields[field_count-1]);
+ request->client_final_message_without_proof =
+ p_strdup(request->pool, t_strarray_join(fields, ","));
+
+ return TRUE;
+}
+
+void mech_scram_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct scram_auth_request *request =
+ (struct scram_auth_request *)auth_request;
+ const char *error = NULL;
+ const char *server_final_message;
+ size_t len;
+
+ if (request->client_first_message_bare == NULL) {
+ /* Received client-first-message */
+ if (parse_scram_client_first(request, data,
+ data_size, &error)) {
+ auth_request_lookup_credentials(
+ &request->auth_request,
+ request->password_scheme,
+ credentials_callback);
+ return;
+ }
+ } else {
+ /* Received client-final-message */
+ if (parse_scram_client_final(request, data, data_size,
+ &error)) {
+ if (!verify_credentials(request)) {
+ e_info(auth_request->mech_event,
+ AUTH_LOG_MSG_PASSWORD_MISMATCH);
+ } else {
+ server_final_message =
+ get_scram_server_final(request);
+ len = strlen(server_final_message);
+ auth_request_success(auth_request,
+ server_final_message, len);
+ return;
+ }
+ }
+ }
+
+ if (error != NULL)
+ e_info(auth_request->mech_event, "%s", error);
+ auth_request_fail(auth_request);
+}
+
+struct auth_request *
+mech_scram_auth_new(const struct hash_method *hash_method,
+ const char *password_scheme)
+{
+ struct scram_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"scram_auth_request", 2048);
+ request = p_new(pool, struct scram_auth_request, 1);
+ request->pool = pool;
+
+ request->hash_method = hash_method;
+ request->password_scheme = password_scheme;
+
+ request->stored_key = p_malloc(pool, hash_method->digest_size);
+ request->server_key = p_malloc(pool, hash_method->digest_size);
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+static struct auth_request *mech_scram_sha1_auth_new(void)
+{
+ return mech_scram_auth_new(&hash_method_sha1, "SCRAM-SHA-1");
+}
+
+static struct auth_request *mech_scram_sha256_auth_new(void)
+{
+ return mech_scram_auth_new(&hash_method_sha256, "SCRAM-SHA-256");
+}
+
+const struct mech_module mech_scram_sha1 = {
+ "SCRAM-SHA-1",
+
+ .flags = MECH_SEC_MUTUAL_AUTH,
+ .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS,
+
+ mech_scram_sha1_auth_new,
+ mech_generic_auth_initial,
+ mech_scram_auth_continue,
+ mech_generic_auth_free
+};
+
+const struct mech_module mech_scram_sha256 = {
+ "SCRAM-SHA-256",
+
+ .flags = MECH_SEC_MUTUAL_AUTH,
+ .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS,
+
+ mech_scram_sha256_auth_new,
+ mech_generic_auth_initial,
+ mech_scram_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech-scram.h b/src/auth/mech-scram.h
new file mode 100644
index 0000000..0275aa7
--- /dev/null
+++ b/src/auth/mech-scram.h
@@ -0,0 +1,10 @@
+#ifndef MECH_SCRAM_H
+#define MECH_SCRAM_H
+
+struct auth_request *
+mech_scram_auth_new(const struct hash_method *hash_method,
+ const char *password_scheme);
+void mech_scram_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size);
+
+#endif
diff --git a/src/auth/mech-winbind.c b/src/auth/mech-winbind.c
new file mode 100644
index 0000000..d2525db
--- /dev/null
+++ b/src/auth/mech-winbind.c
@@ -0,0 +1,364 @@
+/*
+ * NTLM and Negotiate authentication mechanisms,
+ * using Samba winbind daemon
+ *
+ * Copyright (c) 2007 Dmitry Butskoy <dmitry@butskoy.name>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "lib-signals.h"
+#include "mech.h"
+#include "str.h"
+#include "buffer.h"
+#include "base64.h"
+#include "execv-const.h"
+#include "istream.h"
+#include "ostream.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+
+#define DEFAULT_WINBIND_HELPER_PATH "/usr/bin/ntlm_auth"
+
+enum helper_result {
+ HR_OK = 0, /* OK or continue */
+ HR_FAIL = -1, /* authentication failed */
+ HR_RESTART = -2 /* FAIL + try to restart helper */
+};
+
+struct winbind_helper {
+ const char *param;
+ pid_t pid;
+
+ struct istream *in_pipe;
+ struct ostream *out_pipe;
+};
+
+struct winbind_auth_request {
+ struct auth_request auth_request;
+
+ struct winbind_helper *winbind;
+ bool continued;
+};
+
+static struct winbind_helper winbind_ntlm_context = {
+ "--helper-protocol=squid-2.5-ntlmssp", -1, NULL, NULL
+};
+static struct winbind_helper winbind_spnego_context = {
+ "--helper-protocol=gss-spnego", -1, NULL, NULL
+};
+
+static bool sigchld_handler_set = FALSE;
+
+static void winbind_helper_disconnect(struct winbind_helper *winbind)
+{
+ i_stream_destroy(&winbind->in_pipe);
+ o_stream_destroy(&winbind->out_pipe);
+}
+
+static void winbind_wait_pid(struct winbind_helper *winbind)
+{
+ int status, ret;
+
+ if (winbind->pid == -1)
+ return;
+
+ /* FIXME: use child-wait.h API */
+ if ((ret = waitpid(winbind->pid, &status, WNOHANG)) <= 0) {
+ if (ret < 0 && errno != ECHILD && errno != EINTR)
+ i_error("waitpid() failed: %m");
+ return;
+ }
+
+ if (WIFSIGNALED(status)) {
+ i_error("winbind: ntlm_auth died with signal %d",
+ WTERMSIG(status));
+ } else if (WIFEXITED(status)) {
+ i_error("winbind: ntlm_auth exited with exit code %d",
+ WEXITSTATUS(status));
+ } else {
+ /* shouldn't happen */
+ i_error("winbind: ntlm_auth exited with status %d",
+ status);
+ }
+ winbind->pid = -1;
+}
+
+static void sigchld_handler(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ winbind_wait_pid(&winbind_ntlm_context);
+ winbind_wait_pid(&winbind_spnego_context);
+}
+
+static void
+winbind_helper_connect(const struct auth_settings *set,
+ struct winbind_helper *winbind,
+ struct event *event)
+{
+ int infd[2], outfd[2];
+ pid_t pid;
+
+ if (winbind->in_pipe != NULL || winbind->pid != -1)
+ return;
+
+ if (pipe(infd) < 0) {
+ e_error(event, "pipe() failed: %m");
+ return;
+ }
+ if (pipe(outfd) < 0) {
+ i_close_fd(&infd[0]); i_close_fd(&infd[1]);
+ return;
+ }
+
+ pid = fork();
+ if (pid < 0) {
+ e_error(event, "fork() failed: %m");
+ i_close_fd(&infd[0]); i_close_fd(&infd[1]);
+ i_close_fd(&outfd[0]); i_close_fd(&outfd[1]);
+ return;
+ }
+
+ if (pid == 0) {
+ /* child */
+ const char *args[3];
+
+ i_close_fd(&infd[0]);
+ i_close_fd(&outfd[1]);
+
+ if (dup2(outfd[0], STDIN_FILENO) < 0 ||
+ dup2(infd[1], STDOUT_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+
+ args[0] = set->winbind_helper_path;
+ args[1] = winbind->param;
+ args[2] = NULL;
+ execv_const(args[0], args);
+ }
+
+ /* parent */
+ i_close_fd(&infd[1]);
+ i_close_fd(&outfd[0]);
+
+ winbind->pid = pid;
+ winbind->in_pipe =
+ i_stream_create_fd_autoclose(&infd[0], AUTH_CLIENT_MAX_LINE_LENGTH);
+ winbind->out_pipe =
+ o_stream_create_fd_autoclose(&outfd[1], SIZE_MAX);
+
+ if (!sigchld_handler_set) {
+ sigchld_handler_set = TRUE;
+ lib_signals_set_handler(SIGCHLD, LIBSIG_FLAGS_SAFE,
+ sigchld_handler, NULL);
+ }
+}
+
+static enum helper_result
+do_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct winbind_auth_request *request =
+ (struct winbind_auth_request *)auth_request;
+ struct istream *in_pipe = request->winbind->in_pipe;
+ string_t *str;
+ char *answer;
+ const char **token;
+ bool gss_spnego = request->winbind == &winbind_spnego_context;
+
+ if (request->winbind->in_pipe == NULL)
+ return HR_RESTART;
+
+ str = t_str_new(MAX_BASE64_ENCODED_SIZE(data_size + 1) + 4);
+ str_printfa(str, "%s ", request->continued ? "KK" : "YR");
+ base64_encode(data, data_size, str);
+ str_append_c(str, '\n');
+
+ if (o_stream_send(request->winbind->out_pipe,
+ str_data(str), str_len(str)) < 0 ||
+ o_stream_flush(request->winbind->out_pipe) < 0) {
+ e_error(auth_request->mech_event,
+ "write(out_pipe) failed: %s",
+ o_stream_get_error(request->winbind->out_pipe));
+ return HR_RESTART;
+ }
+ request->continued = FALSE;
+
+ while ((answer = i_stream_read_next_line(in_pipe)) == NULL) {
+ if (in_pipe->stream_errno != 0 || in_pipe->eof)
+ break;
+ }
+ if (answer == NULL) {
+ if (in_pipe->stream_errno != 0) {
+ e_error(auth_request->mech_event,
+ "read(in_pipe) failed: %m");
+ } else {
+ e_error(auth_request->mech_event,
+ "read(in_pipe) failed: "
+ "unexpected end of file");
+ }
+ return HR_RESTART;
+ }
+
+ token = t_strsplit_spaces(answer, " ");
+ if (token[0] == NULL ||
+ (token[1] == NULL && strcmp(token[0], "BH") != 0) ||
+ (gss_spnego && (token[1] == NULL || token[2] == NULL))) {
+ e_error(auth_request->mech_event,
+ "Invalid input from helper: %s", answer);
+ return HR_RESTART;
+ }
+
+ /*
+ * NTLM:
+ * The child's reply contains 2 parts:
+ * - The code: TT, AF or NA
+ * - The argument:
+ * For TT it's the blob to send to the client, coded in base64
+ * For AF it's user or DOMAIN\user
+ * For NA it's the NT error code
+ *
+ * GSS-SPNEGO:
+ * The child's reply contains 3 parts:
+ * - The code: TT, AF or NA
+ * - The blob to send to the client, coded in base64
+ * - The argument:
+ * For TT it's a dummy '*'
+ * For AF it's DOMAIN\user
+ * For NA it's the NT error code
+ */
+
+ if (strcmp(token[0], "TT") == 0) {
+ buffer_t *buf;
+
+ i_assert(token[1] != NULL);
+ buf = t_base64_decode_str(token[1]);
+ auth_request_handler_reply_continue(auth_request, buf->data,
+ buf->used);
+ request->continued = TRUE;
+ return HR_OK;
+ } else if (strcmp(token[0], "NA") == 0) {
+ const char *error = gss_spnego ? token[2] : token[1];
+
+ e_info(auth_request->mech_event,
+ "user not authenticated: %s", error);
+ return HR_FAIL;
+ } else if (strcmp(token[0], "AF") == 0) {
+ const char *user, *p, *error;
+
+ user = t_strarray_join(gss_spnego ? token+2 : token+1, " ");
+ i_assert(user != NULL);
+
+ p = strchr(user, '\\');
+ if (p != NULL) {
+ /* change "DOMAIN\user" to uniform style
+ "user@DOMAIN" */
+ user = t_strconcat(p+1, "@",
+ t_strdup_until(user, p), NULL);
+ }
+
+ if (!auth_request_set_username(auth_request, user, &error)) {
+ e_info(auth_request->mech_event,
+ "%s", error);
+ return HR_FAIL;
+ }
+
+ request->auth_request.passdb_success = TRUE;
+ if (gss_spnego && strcmp(token[1], "*") != 0) {
+ buffer_t *buf;
+
+ buf = t_base64_decode_str(token[1]);
+ auth_request_success(&request->auth_request,
+ buf->data, buf->used);
+ } else {
+ auth_request_success(&request->auth_request, "", 0);
+ }
+ return HR_OK;
+ } else if (strcmp(token[0], "BH") == 0) {
+ e_info(auth_request->mech_event,
+ "ntlm_auth reports broken helper: %s",
+ token[1] != NULL ? token[1] : "");
+ return HR_RESTART;
+ } else {
+ e_error(auth_request->mech_event,
+ "Invalid input from helper: %s", answer);
+ return HR_RESTART;
+ }
+}
+
+static void
+mech_winbind_auth_initial(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct winbind_auth_request *request =
+ (struct winbind_auth_request *)auth_request;
+
+ winbind_helper_connect(auth_request->set, request->winbind,
+ auth_request->event);
+ mech_generic_auth_initial(auth_request, data, data_size);
+}
+
+static void
+mech_winbind_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size)
+{
+ struct winbind_auth_request *request =
+ (struct winbind_auth_request *)auth_request;
+ enum helper_result res;
+
+ res = do_auth_continue(auth_request, data, data_size);
+ if (res != HR_OK) {
+ if (res == HR_RESTART)
+ winbind_helper_disconnect(request->winbind);
+ auth_request_fail(auth_request);
+ }
+}
+
+static struct auth_request *do_auth_new(struct winbind_helper *winbind)
+{
+ struct winbind_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"winbind_auth_request", 2048);
+ request = p_new(pool, struct winbind_auth_request, 1);
+ request->auth_request.pool = pool;
+
+ request->winbind = winbind;
+ return &request->auth_request;
+}
+
+static struct auth_request *mech_winbind_ntlm_auth_new(void)
+{
+ return do_auth_new(&winbind_ntlm_context);
+}
+
+static struct auth_request *mech_winbind_spnego_auth_new(void)
+{
+ return do_auth_new(&winbind_spnego_context);
+}
+
+const struct mech_module mech_winbind_ntlm = {
+ "NTLM",
+
+ .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE |
+ MECH_SEC_ALLOW_NULS,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_winbind_ntlm_auth_new,
+ mech_winbind_auth_initial,
+ mech_winbind_auth_continue,
+ mech_generic_auth_free
+};
+
+const struct mech_module mech_winbind_spnego = {
+ "GSS-SPNEGO",
+
+ .flags = 0,
+ .passdb_need = MECH_PASSDB_NEED_NOTHING,
+
+ mech_winbind_spnego_auth_new,
+ mech_winbind_auth_initial,
+ mech_winbind_auth_continue,
+ mech_generic_auth_free
+};
diff --git a/src/auth/mech.c b/src/auth/mech.c
new file mode 100644
index 0000000..477f27b
--- /dev/null
+++ b/src/auth/mech.c
@@ -0,0 +1,245 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "mech.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "passdb.h"
+
+#include <ctype.h>
+
+static struct mech_module_list *mech_modules;
+
+void mech_register_module(const struct mech_module *module)
+{
+ struct mech_module_list *list;
+ i_assert(strcmp(module->mech_name, t_str_ucase(module->mech_name)) == 0);
+
+ list = i_new(struct mech_module_list, 1);
+ list->module = *module;
+
+ list->next = mech_modules;
+ mech_modules = list;
+}
+
+void mech_unregister_module(const struct mech_module *module)
+{
+ struct mech_module_list **pos, *list;
+
+ for (pos = &mech_modules; *pos != NULL; pos = &(*pos)->next) {
+ if (strcmp((*pos)->module.mech_name, module->mech_name) == 0) {
+ list = *pos;
+ *pos = (*pos)->next;
+ i_free(list);
+ break;
+ }
+ }
+}
+
+const struct mech_module *mech_module_find(const char *name)
+{
+ struct mech_module_list *list;
+ name = t_str_ucase(name);
+
+ for (list = mech_modules; list != NULL; list = list->next) {
+ if (strcmp(list->module.mech_name, name) == 0)
+ return &list->module;
+ }
+ return NULL;
+}
+
+void mech_generic_auth_initial(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ if (data == NULL) {
+ auth_request_handler_reply_continue(request, uchar_empty_ptr, 0);
+ } else {
+ /* initial reply given, even if it was 0 bytes */
+ request->mech->auth_continue(request, data, data_size);
+ }
+}
+
+void mech_generic_auth_free(struct auth_request *request)
+{
+ pool_unref(&request->pool);
+}
+
+extern const struct mech_module mech_plain;
+extern const struct mech_module mech_login;
+extern const struct mech_module mech_apop;
+extern const struct mech_module mech_cram_md5;
+extern const struct mech_module mech_digest_md5;
+extern const struct mech_module mech_external;
+extern const struct mech_module mech_otp;
+extern const struct mech_module mech_scram_sha1;
+extern const struct mech_module mech_scram_sha256;
+extern const struct mech_module mech_anonymous;
+#ifdef HAVE_GSSAPI
+extern const struct mech_module mech_gssapi;
+#endif
+#ifdef HAVE_GSSAPI_SPNEGO
+extern const struct mech_module mech_gssapi_spnego;
+#endif
+extern const struct mech_module mech_winbind_ntlm;
+extern const struct mech_module mech_winbind_spnego;
+extern const struct mech_module mech_oauthbearer;
+extern const struct mech_module mech_xoauth2;
+
+static void mech_register_add(struct mechanisms_register *reg,
+ const struct mech_module *mech)
+{
+ struct mech_module_list *list;
+
+ list = p_new(reg->pool, struct mech_module_list, 1);
+ list->module = *mech;
+
+ str_printfa(reg->handshake, "MECH\t%s", mech->mech_name);
+ if ((mech->flags & MECH_SEC_PRIVATE) != 0)
+ str_append(reg->handshake, "\tprivate");
+ if ((mech->flags & MECH_SEC_ANONYMOUS) != 0)
+ str_append(reg->handshake, "\tanonymous");
+ if ((mech->flags & MECH_SEC_PLAINTEXT) != 0)
+ str_append(reg->handshake, "\tplaintext");
+ if ((mech->flags & MECH_SEC_DICTIONARY) != 0)
+ str_append(reg->handshake, "\tdictionary");
+ if ((mech->flags & MECH_SEC_ACTIVE) != 0)
+ str_append(reg->handshake, "\tactive");
+ if ((mech->flags & MECH_SEC_FORWARD_SECRECY) != 0)
+ str_append(reg->handshake, "\tforward-secrecy");
+ if ((mech->flags & MECH_SEC_MUTUAL_AUTH) != 0)
+ str_append(reg->handshake, "\tmutual-auth");
+ str_append_c(reg->handshake, '\n');
+
+ list->next = reg->modules;
+ reg->modules = list;
+}
+
+static const char *mech_get_plugin_name(const char *name)
+{
+ string_t *str = t_str_new(32);
+
+ str_append(str, "mech_");
+ for (; *name != '\0'; name++) {
+ if (*name == '-')
+ str_append_c(str, '_');
+ else
+ str_append_c(str, i_tolower(*name));
+ }
+ return str_c(str);
+}
+
+struct mechanisms_register *
+mech_register_init(const struct auth_settings *set)
+{
+ struct mechanisms_register *reg;
+ const struct mech_module *mech;
+ const char *const *mechanisms;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mechanisms register", 1024);
+ reg = p_new(pool, struct mechanisms_register, 1);
+ reg->pool = pool;
+ reg->set = set;
+ reg->handshake = str_new(pool, 512);
+
+ mechanisms = t_strsplit_spaces(set->mechanisms, " ");
+ for (; *mechanisms != NULL; mechanisms++) {
+ const char *name = t_str_ucase(*mechanisms);
+
+ if (strcmp(name, "ANONYMOUS") == 0) {
+ if (*set->anonymous_username == '\0') {
+ i_fatal("ANONYMOUS listed in mechanisms, "
+ "but anonymous_username not set");
+ }
+ }
+ mech = mech_module_find(name);
+ if (mech == NULL) {
+ /* maybe it's a plugin. try to load it. */
+ auth_module_load(mech_get_plugin_name(name));
+ mech = mech_module_find(name);
+ }
+ if (mech == NULL)
+ i_fatal("Unknown authentication mechanism '%s'", name);
+ mech_register_add(reg, mech);
+ }
+
+ if (reg->modules == NULL)
+ i_fatal("No authentication mechanisms configured");
+ return reg;
+}
+
+void mech_register_deinit(struct mechanisms_register **_reg)
+{
+ struct mechanisms_register *reg = *_reg;
+
+ *_reg = NULL;
+ pool_unref(&reg->pool);
+}
+
+const struct mech_module *
+mech_register_find(const struct mechanisms_register *reg, const char *name)
+{
+ const struct mech_module_list *list;
+ name = t_str_ucase(name);
+
+ for (list = reg->modules; list != NULL; list = list->next) {
+ if (strcmp(list->module.mech_name, name) == 0)
+ return &list->module;
+ }
+ return NULL;
+}
+
+void mech_init(const struct auth_settings *set)
+{
+ mech_register_module(&mech_plain);
+ mech_register_module(&mech_login);
+ mech_register_module(&mech_apop);
+ mech_register_module(&mech_cram_md5);
+ mech_register_module(&mech_digest_md5);
+ mech_register_module(&mech_external);
+ if (set->use_winbind) {
+ mech_register_module(&mech_winbind_ntlm);
+ mech_register_module(&mech_winbind_spnego);
+ } else {
+#if defined(HAVE_GSSAPI_SPNEGO) && defined(BUILTIN_GSSAPI)
+ mech_register_module(&mech_gssapi_spnego);
+#endif
+ }
+ mech_register_module(&mech_otp);
+ mech_register_module(&mech_scram_sha1);
+ mech_register_module(&mech_scram_sha256);
+ mech_register_module(&mech_anonymous);
+#ifdef BUILTIN_GSSAPI
+ mech_register_module(&mech_gssapi);
+#endif
+ mech_register_module(&mech_oauthbearer);
+ mech_register_module(&mech_xoauth2);
+}
+
+void mech_deinit(const struct auth_settings *set)
+{
+ mech_unregister_module(&mech_plain);
+ mech_unregister_module(&mech_login);
+ mech_unregister_module(&mech_apop);
+ mech_unregister_module(&mech_cram_md5);
+ mech_unregister_module(&mech_digest_md5);
+ mech_unregister_module(&mech_external);
+ if (set->use_winbind) {
+ mech_unregister_module(&mech_winbind_ntlm);
+ mech_unregister_module(&mech_winbind_spnego);
+ } else {
+#if defined(HAVE_GSSAPI_SPNEGO) && defined(BUILTIN_GSSAPI)
+ mech_unregister_module(&mech_gssapi_spnego);
+#endif
+ }
+ mech_unregister_module(&mech_otp);
+ mech_unregister_module(&mech_scram_sha1);
+ mech_unregister_module(&mech_scram_sha256);
+ mech_unregister_module(&mech_anonymous);
+#ifdef BUILTIN_GSSAPI
+ mech_unregister_module(&mech_gssapi);
+#endif
+ mech_unregister_module(&mech_oauthbearer);
+ mech_unregister_module(&mech_xoauth2);
+}
diff --git a/src/auth/mech.h b/src/auth/mech.h
new file mode 100644
index 0000000..4a9f593
--- /dev/null
+++ b/src/auth/mech.h
@@ -0,0 +1,77 @@
+#ifndef MECH_H
+#define MECH_H
+
+#include "auth-client-interface.h"
+
+struct auth_settings;
+struct auth_request;
+
+#include "auth-request.h"
+#include "auth-request-handler.h"
+
+/* Used only for string sanitization. */
+#define MAX_MECH_NAME_LEN 64
+
+enum mech_passdb_need {
+ /* Mechanism doesn't need a passdb at all */
+ MECH_PASSDB_NEED_NOTHING = 0,
+ /* Mechanism just needs to verify a given plaintext password */
+ MECH_PASSDB_NEED_VERIFY_PLAIN,
+ /* Mechanism needs to verify a given challenge+response combination,
+ i.e. there is only a single response from client.
+ (Currently implemented the same as _LOOKUP_CREDENTIALS) */
+ MECH_PASSDB_NEED_VERIFY_RESPONSE,
+ /* Mechanism needs to look up credentials with appropriate scheme */
+ MECH_PASSDB_NEED_LOOKUP_CREDENTIALS,
+ /* Mechanism needs to look up credentials and also modify them */
+ MECH_PASSDB_NEED_SET_CREDENTIALS
+};
+
+struct mech_module {
+ const char *mech_name;
+
+ enum mech_security_flags flags;
+ enum mech_passdb_need passdb_need;
+
+ struct auth_request *(*auth_new)(void);
+ void (*auth_initial)(struct auth_request *request,
+ const unsigned char *data, size_t data_size);
+ void (*auth_continue)(struct auth_request *request,
+ const unsigned char *data, size_t data_size);
+ void (*auth_free)(struct auth_request *request);
+};
+
+struct mech_module_list {
+ struct mech_module_list *next;
+
+ struct mech_module module;
+};
+
+struct mechanisms_register {
+ pool_t pool;
+ const struct auth_settings *set;
+
+ struct mech_module_list *modules;
+ buffer_t *handshake;
+};
+
+extern const struct mech_module mech_dovecot_token;
+
+void mech_register_module(const struct mech_module *module);
+void mech_unregister_module(const struct mech_module *module);
+const struct mech_module *mech_module_find(const char *name);
+
+void mech_generic_auth_initial(struct auth_request *request,
+ const unsigned char *data, size_t data_size);
+void mech_generic_auth_free(struct auth_request *request);
+
+struct mechanisms_register *
+mech_register_init(const struct auth_settings *set);
+void mech_register_deinit(struct mechanisms_register **reg);
+const struct mech_module *
+mech_register_find(const struct mechanisms_register *reg, const char *name);
+
+void mech_init(const struct auth_settings *set);
+void mech_deinit(const struct auth_settings *set);
+
+#endif
diff --git a/src/auth/mycrypt.c b/src/auth/mycrypt.c
new file mode 100644
index 0000000..0bd00bc
--- /dev/null
+++ b/src/auth/mycrypt.c
@@ -0,0 +1,26 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define _XOPEN_SOURCE 4
+#define _XOPEN_SOURCE_EXTENDED 1 /* 1 needed for AIX */
+#ifndef _AIX
+# define _XOPEN_VERSION 4 /* breaks AIX */
+#endif
+#define _XPG4_2
+#ifdef CRYPT_USE_XPG6
+# define _XPG6 /* Some Solaris versions require this, some break with this */
+#endif
+#include <unistd.h>
+#ifdef HAVE_CRYPT_H
+# include <crypt.h>
+#endif
+
+#include "mycrypt.h"
+
+char *mycrypt(const char *key, const char *salt)
+{
+ return crypt(key, salt);
+}
diff --git a/src/auth/mycrypt.h b/src/auth/mycrypt.h
new file mode 100644
index 0000000..550adfc
--- /dev/null
+++ b/src/auth/mycrypt.h
@@ -0,0 +1,8 @@
+#ifndef MYCRYPT_H
+#define MYCRYPT_H
+
+/* A simple wrapper to crypt(). Problem with it is that it requires
+ _XOPEN_SOURCE define which breaks other things. */
+char *mycrypt(const char *key, const char *salt);
+
+#endif
diff --git a/src/auth/passdb-blocking.c b/src/auth/passdb-blocking.c
new file mode 100644
index 0000000..6b7030c
--- /dev/null
+++ b/src/auth/passdb-blocking.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "auth-worker-server.h"
+#include "password-scheme.h"
+#include "passdb.h"
+#include "passdb-blocking.h"
+
+
+static void
+auth_worker_reply_parse_args(struct auth_request *request,
+ const char *const *args)
+{
+ if (**args != '\0')
+ request->passdb_password = p_strdup(request->pool, *args);
+ args++;
+
+ if (*args != NULL)
+ auth_request_set_fields(request, args, NULL);
+}
+
+enum passdb_result
+passdb_blocking_auth_worker_reply_parse(struct auth_request *request, const char *reply)
+{
+ enum passdb_result ret;
+ const char *const *args;
+
+ args = t_strsplit_tabescaped(reply);
+
+ if (strcmp(*args, "OK") == 0 && args[1] != NULL && args[2] != NULL) {
+ /* OK \t user \t password [\t extra] */
+ if (args[1][0] != '\0')
+ auth_request_set_field(request, "user", args[1], NULL);
+ auth_worker_reply_parse_args(request, args + 2);
+ return PASSDB_RESULT_OK;
+ }
+
+ if (strcmp(*args, "NEXT") == 0 && args[1] != NULL) {
+ /* NEXT \t user [\t extra] */
+ if (args[1][0] != '\0')
+ auth_request_set_field(request, "user", args[1], NULL);
+ auth_worker_reply_parse_args(request, args + 1);
+ return PASSDB_RESULT_NEXT;
+ }
+
+ if (strcmp(*args, "FAIL") == 0 && args[1] != NULL) {
+ int result;
+ /* FAIL \t result [\t user \t password [\t extra]] */
+ if (str_to_int(args[1], &result) < 0) {
+ /* shouldn't happen */
+ } else {
+ ret = (enum passdb_result)result;
+ if (ret == PASSDB_RESULT_OK) {
+ /* shouldn't happen */
+ } else if (args[2] == NULL) {
+ /* internal failure most likely */
+ return ret;
+ } else if (args[3] != NULL) {
+ if (*args[2] != '\0') {
+ auth_request_set_field(request, "user",
+ args[2], NULL);
+ }
+ auth_worker_reply_parse_args(request, args + 3);
+ return ret;
+ }
+ }
+ }
+
+ e_error(authdb_event(request),
+ "Received invalid reply from worker: %s", reply);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+}
+
+static bool
+verify_plain_callback(struct auth_worker_connection *conn ATTR_UNUSED,
+ const char *reply, void *context)
+{
+ struct auth_request *request = context;
+ enum passdb_result result;
+
+ result = passdb_blocking_auth_worker_reply_parse(request, reply);
+ auth_request_verify_plain_callback(result, request);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+void passdb_blocking_verify_plain(struct auth_request *request)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "PASSV\t%u\t", request->passdb->passdb->id);
+ str_append_tabescaped(str, request->mech_password);
+ str_append_c(str, '\t');
+ auth_request_export(request, str);
+
+ auth_request_ref(request);
+ auth_worker_call(request->pool, request->fields.user, str_c(str),
+ verify_plain_callback, request);
+}
+
+static bool
+lookup_credentials_callback(struct auth_worker_connection *conn ATTR_UNUSED,
+ const char *reply, void *context)
+{
+ struct auth_request *request = context;
+ enum passdb_result result;
+ const char *password = NULL, *scheme = NULL;
+
+ result = passdb_blocking_auth_worker_reply_parse(request, reply);
+ if (result == PASSDB_RESULT_OK && request->passdb_password != NULL) {
+ password = request->passdb_password;
+ scheme = password_get_scheme(&password);
+ if (scheme == NULL) {
+ e_error(authdb_event(request),
+ "Received reply from worker without "
+ "password scheme");
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ }
+
+ passdb_handle_credentials(result, password, scheme,
+ auth_request_lookup_credentials_callback,
+ request);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+void passdb_blocking_lookup_credentials(struct auth_request *request)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "PASSL\t%u\t", request->passdb->passdb->id);
+ str_append_tabescaped(str, request->wanted_credentials_scheme);
+ str_append_c(str, '\t');
+ auth_request_export(request, str);
+
+ auth_request_ref(request);
+ auth_worker_call(request->pool, request->fields.user, str_c(str),
+ lookup_credentials_callback, request);
+}
+
+static bool
+set_credentials_callback(struct auth_worker_connection *conn ATTR_UNUSED,
+ const char *reply, void *context)
+{
+ struct auth_request *request = context;
+ bool success;
+
+ success = strcmp(reply, "OK") == 0 || str_begins(reply, "OK\t");
+ request->private_callback.set_credentials(success, request);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+void passdb_blocking_set_credentials(struct auth_request *request,
+ const char *new_credentials)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "SETCRED\t%u\t", request->passdb->passdb->id);
+ str_append_tabescaped(str, new_credentials);
+ str_append_c(str, '\t');
+ auth_request_export(request, str);
+
+ auth_request_ref(request);
+ auth_worker_call(request->pool, request->fields.user, str_c(str),
+ set_credentials_callback, request);
+}
diff --git a/src/auth/passdb-blocking.h b/src/auth/passdb-blocking.h
new file mode 100644
index 0000000..3963998
--- /dev/null
+++ b/src/auth/passdb-blocking.h
@@ -0,0 +1,11 @@
+#ifndef PASSDB_BLOCKING_H
+#define PASSDB_BLOCKING_H
+
+enum passdb_result
+passdb_blocking_auth_worker_reply_parse(struct auth_request *request, const char *reply);
+void passdb_blocking_verify_plain(struct auth_request *request);
+void passdb_blocking_lookup_credentials(struct auth_request *request);
+void passdb_blocking_set_credentials(struct auth_request *request,
+ const char *new_credentials);
+
+#endif
diff --git a/src/auth/passdb-bsdauth.c b/src/auth/passdb-bsdauth.c
new file mode 100644
index 0000000..36469c6
--- /dev/null
+++ b/src/auth/passdb-bsdauth.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_BSDAUTH
+
+#include "safe-memset.h"
+#include "auth-cache.h"
+#include "ipwd.h"
+#include "mycrypt.h"
+
+#include <login_cap.h>
+#include <bsd_auth.h>
+
+static void
+bsdauth_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passwd pw;
+ const char *type;
+ int result;
+
+ e_debug(authdb_event(request), "lookup");
+
+ switch (i_getpwnam(request->fields.user, &pw)) {
+ case -1:
+ e_error(authdb_event(request),
+ "getpwnam() failed: %m");
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ case 0:
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ callback(PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ /* check if the password is valid */
+ type = t_strdup_printf("auth-%s", request->fields.service);
+ result = auth_userokay(request->fields.user, NULL,
+ t_strdup_noconst(type),
+ t_strdup_noconst(password));
+
+ /* clear the passwords from memory */
+ safe_memset(pw.pw_passwd, 0, strlen(pw.pw_passwd));
+
+ if (result == 0) {
+ auth_request_log_password_mismatch(request, AUTH_SUBSYS_DB);
+ callback(PASSDB_RESULT_PASSWORD_MISMATCH, request);
+ return;
+ }
+
+ /* make sure we're using the username exactly as it's in the database */
+ auth_request_set_field(request, "user", pw.pw_name, NULL);
+
+ callback(PASSDB_RESULT_OK, request);
+}
+
+static struct passdb_module *
+bsdauth_preinit(pool_t pool, const char *args)
+{
+ struct passdb_module *module;
+
+ module = p_new(pool, struct passdb_module, 1);
+ module->default_pass_scheme = "PLAIN"; /* same reason as PAM */
+ module->blocking = TRUE;
+
+ if (strcmp(args, "blocking=no") == 0)
+ module->blocking = FALSE;
+ else if (str_begins(args, "cache_key="))
+ module->default_cache_key = auth_cache_parse_key(pool, args + 10);
+ else if (*args != '\0')
+ i_fatal("passdb bsdauth: Unknown setting: %s", args);
+ return module;
+}
+
+static void bsdauth_deinit(struct passdb_module *module ATTR_UNUSED)
+{
+ endpwent();
+}
+
+struct passdb_module_interface passdb_bsdauth = {
+ "bsdauth",
+
+ bsdauth_preinit,
+ NULL,
+ bsdauth_deinit,
+
+ bsdauth_verify_plain,
+ NULL,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_bsdauth = {
+ .name = "bsdauth"
+};
+#endif
diff --git a/src/auth/passdb-cache.c b/src/auth/passdb-cache.c
new file mode 100644
index 0000000..1fe3b07
--- /dev/null
+++ b/src/auth/passdb-cache.c
@@ -0,0 +1,214 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "restrict-process-size.h"
+#include "auth-request-stats.h"
+#include "auth-worker-server.h"
+#include "password-scheme.h"
+#include "passdb.h"
+#include "passdb-cache.h"
+#include "passdb-blocking.h"
+
+struct auth_cache *passdb_cache = NULL;
+
+static void
+passdb_cache_log_hit(struct auth_request *request, const char *value)
+{
+ const char *p;
+
+ if (!request->set->debug_passwords &&
+ *value != '\0' && *value != '\t') {
+ /* hide the password */
+ p = strchr(value, '\t');
+ value = t_strconcat(PASSWORD_HIDDEN_STR, p, NULL);
+ }
+ e_debug(authdb_event(request), "cache hit: %s", value);
+}
+
+static bool
+passdb_cache_lookup(struct auth_request *request, const char *key,
+ bool use_expired, struct auth_cache_node **node_r,
+ const char **value_r, bool *neg_expired_r)
+{
+ struct auth_stats *stats = auth_request_stats_get(request);
+ const char *value;
+ bool expired;
+
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+
+ /* value = password \t ... */
+ value = auth_cache_lookup(passdb_cache, request, key, node_r,
+ &expired, neg_expired_r);
+ if (value == NULL || (expired && !use_expired)) {
+ stats->auth_cache_miss_count++;
+ e_debug(authdb_event(request),
+ value == NULL ? "cache miss" :
+ "cache expired");
+ return FALSE;
+ }
+ stats->auth_cache_hit_count++;
+ passdb_cache_log_hit(request, value);
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+
+ *value_r = value;
+ return TRUE;
+}
+
+static bool
+passdb_cache_verify_plain_callback(struct auth_worker_connection *conn ATTR_UNUSED,
+ const char *reply, void *context)
+{
+ struct auth_request *request = context;
+ enum passdb_result result;
+
+ result = passdb_blocking_auth_worker_reply_parse(request, reply);
+ if (result != PASSDB_RESULT_OK)
+ auth_fields_rollback(request->fields.extra_fields);
+ auth_request_verify_plain_callback_finish(result, request);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+bool passdb_cache_verify_plain(struct auth_request *request, const char *key,
+ const char *password,
+ enum passdb_result *result_r, bool use_expired)
+{
+ const char *value, *cached_pw, *scheme, *const *list;
+ struct auth_cache_node *node;
+ enum passdb_result ret;
+ bool neg_expired;
+
+ if (passdb_cache == NULL || key == NULL)
+ return FALSE;
+
+ if (!passdb_cache_lookup(request, key, use_expired,
+ &node, &value, &neg_expired))
+ return FALSE;
+
+ if (*value == '\0') {
+ /* negative cache entry */
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ *result_r = PASSDB_RESULT_USER_UNKNOWN;
+ auth_request_verify_plain_callback_finish(*result_r, request);
+ return TRUE;
+ }
+
+ list = t_strsplit_tabescaped(value);
+
+ cached_pw = list[0];
+ if (*cached_pw == '\0') {
+ /* NULL password */
+ e_info(authdb_event(request),
+ "Cached NULL password access");
+ ret = PASSDB_RESULT_OK;
+ } else if (request->set->cache_verify_password_with_worker) {
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "PASSW\t%u\t", request->passdb->passdb->id);
+ str_append_tabescaped(str, password);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, cached_pw);
+ str_append_c(str, '\t');
+ auth_request_export(request, str);
+
+ e_debug(authdb_event(request), "cache: "
+ "validating password on worker");
+ auth_request_ref(request);
+ /* Save the extra fields already here, and take a snapshot.
+ If verification fails, roll back fields. */
+ auth_request_set_fields(request, list + 1, NULL);
+ auth_fields_snapshot(request->fields.extra_fields);
+ auth_worker_call(request->pool, request->fields.user, str_c(str),
+ passdb_cache_verify_plain_callback, request);
+ return TRUE;
+ } else {
+ scheme = password_get_scheme(&cached_pw);
+ i_assert(scheme != NULL);
+
+ ret = auth_request_password_verify_log(request, password, cached_pw,
+ scheme, AUTH_SUBSYS_DB,
+ !(node->last_success || neg_expired));
+
+ if (ret == PASSDB_RESULT_PASSWORD_MISMATCH &&
+ (node->last_success || neg_expired)) {
+ /* a) the last authentication was successful. assume
+ that the password was changed and cache is expired.
+ b) negative TTL reached, use it for password
+ mismatches too. */
+ node->last_success = FALSE;
+ return FALSE;
+ }
+ }
+ node->last_success = ret == PASSDB_RESULT_OK;
+
+ /* save the extra_fields only after we know we're using the
+ cached data */
+ auth_request_set_fields(request, list + 1, NULL);
+
+ *result_r = ret;
+
+ auth_request_verify_plain_callback_finish(*result_r, request);
+ return TRUE;
+}
+
+bool passdb_cache_lookup_credentials(struct auth_request *request,
+ const char *key, const char **password_r,
+ const char **scheme_r,
+ enum passdb_result *result_r,
+ bool use_expired)
+{
+ const char *value, *const *list;
+ struct auth_cache_node *node;
+ bool neg_expired;
+
+ if (passdb_cache == NULL)
+ return FALSE;
+
+ if (!passdb_cache_lookup(request, key, use_expired,
+ &node, &value, &neg_expired))
+ return FALSE;
+
+ if (*value == '\0') {
+ /* negative cache entry */
+ *result_r = PASSDB_RESULT_USER_UNKNOWN;
+ *password_r = NULL;
+ *scheme_r = NULL;
+ return TRUE;
+ }
+
+ list = t_strsplit_tabescaped(value);
+ auth_request_set_fields(request, list + 1, NULL);
+
+ *result_r = PASSDB_RESULT_OK;
+ *password_r = *list[0] == '\0' ? NULL : list[0];
+ *scheme_r = password_get_scheme(password_r);
+ i_assert(*scheme_r != NULL || *password_r == NULL);
+ return TRUE;
+}
+
+void passdb_cache_init(const struct auth_settings *set)
+{
+ rlim_t limit;
+
+ if (set->cache_size == 0 || set->cache_ttl == 0)
+ return;
+
+ if (restrict_get_process_size(&limit) == 0 &&
+ set->cache_size > (uoff_t)limit) {
+ i_warning("auth_cache_size (%"PRIuUOFF_T"M) is higher than "
+ "process VSZ limit (%"PRIuUOFF_T"M)",
+ set->cache_size/1024/1024,
+ (uoff_t)(limit/1024/1024));
+ }
+ passdb_cache = auth_cache_new(set->cache_size, set->cache_ttl,
+ set->cache_negative_ttl);
+}
+
+void passdb_cache_deinit(void)
+{
+ if (passdb_cache != NULL)
+ auth_cache_free(&passdb_cache);
+}
diff --git a/src/auth/passdb-cache.h b/src/auth/passdb-cache.h
new file mode 100644
index 0000000..1b9ff08
--- /dev/null
+++ b/src/auth/passdb-cache.h
@@ -0,0 +1,21 @@
+#ifndef PASSDB_CACHE_H
+#define PASSDB_CACHE_H
+
+#include "auth-cache.h"
+#include "passdb.h"
+
+extern struct auth_cache *passdb_cache;
+
+bool passdb_cache_verify_plain(struct auth_request *request, const char *key,
+ const char *password,
+ enum passdb_result *result_r, bool use_expired);
+bool passdb_cache_lookup_credentials(struct auth_request *request,
+ const char *key, const char **password_r,
+ const char **scheme_r,
+ enum passdb_result *result_r,
+ bool use_expired);
+
+void passdb_cache_init(const struct auth_settings *set);
+void passdb_cache_deinit(void);
+
+#endif
diff --git a/src/auth/passdb-checkpassword.c b/src/auth/passdb-checkpassword.c
new file mode 100644
index 0000000..101c7f0
--- /dev/null
+++ b/src/auth/passdb-checkpassword.c
@@ -0,0 +1,153 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_CHECKPASSWORD
+
+#include "password-scheme.h"
+#include "db-checkpassword.h"
+
+struct checkpassword_passdb_module {
+ struct passdb_module module;
+ struct db_checkpassword *db;
+};
+
+static void
+auth_checkpassword_callback(struct auth_request *request,
+ enum db_checkpassword_status status,
+ const char *const *extra_fields,
+ verify_plain_callback_t *callback)
+{
+ const char *scheme, *crypted_pass = NULL;
+ unsigned int i;
+
+ switch (status) {
+ case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE:
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ case DB_CHECKPASSWORD_STATUS_FAILURE:
+ callback(PASSDB_RESULT_PASSWORD_MISMATCH, request);
+ return;
+ case DB_CHECKPASSWORD_STATUS_OK:
+ break;
+ }
+ for (i = 0; extra_fields[i] != NULL; i++) {
+ if (str_begins(extra_fields[i], "password="))
+ crypted_pass = extra_fields[i]+9;
+ else if (extra_fields[i][0] != '\0') {
+ auth_request_set_field_keyvalue(request,
+ extra_fields[i], NULL);
+ }
+ }
+ if (crypted_pass != NULL) {
+ /* for cache */
+ scheme = password_get_scheme(&crypted_pass);
+ if (scheme != NULL) {
+ auth_request_set_field(request, "password",
+ crypted_pass, scheme);
+ } else {
+ e_error(authdb_event(request),
+ "password field returned without {scheme} prefix");
+ }
+ }
+ callback(PASSDB_RESULT_OK, request);
+}
+
+static void
+checkpassword_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct checkpassword_passdb_module *module =
+ (struct checkpassword_passdb_module *)_module;
+
+ db_checkpassword_call(module->db, request, password,
+ auth_checkpassword_callback, callback);
+}
+
+static void
+credentials_checkpassword_callback(struct auth_request *request,
+ enum db_checkpassword_status status,
+ const char *const *extra_fields,
+ lookup_credentials_callback_t *callback)
+{
+ const char *scheme, *crypted_pass = NULL;
+ unsigned int i;
+
+ switch (status) {
+ case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE:
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request);
+ return;
+ case DB_CHECKPASSWORD_STATUS_FAILURE:
+ callback(PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request);
+ return;
+ case DB_CHECKPASSWORD_STATUS_OK:
+ break;
+ }
+ for (i = 0; extra_fields[i] != NULL; i++) {
+ if (str_begins(extra_fields[i], "password="))
+ crypted_pass = extra_fields[i]+9;
+ else if (extra_fields[i][0] != '\0') {
+ auth_request_set_field_keyvalue(request,
+ extra_fields[i], NULL);
+ }
+ }
+ scheme = password_get_scheme(&crypted_pass);
+ if (scheme == NULL)
+ scheme = request->wanted_credentials_scheme;
+
+ passdb_handle_credentials(PASSDB_RESULT_OK, crypted_pass, scheme,
+ callback, request);
+}
+
+static void
+checkpassword_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct checkpassword_passdb_module *module =
+ (struct checkpassword_passdb_module *)_module;
+
+ db_checkpassword_call(module->db, request, NULL,
+ credentials_checkpassword_callback, callback);
+}
+
+static struct passdb_module *
+checkpassword_preinit(pool_t pool, const char *args)
+{
+ struct checkpassword_passdb_module *module;
+ const char *checkpassword_path = args;
+ const char *checkpassword_reply_path =
+ PKG_LIBEXECDIR"/checkpassword-reply";
+
+ module = p_new(pool, struct checkpassword_passdb_module, 1);
+ module->db = db_checkpassword_init(checkpassword_path,
+ checkpassword_reply_path);
+ return &module->module;
+}
+
+static void checkpassword_deinit(struct passdb_module *_module)
+{
+ struct checkpassword_passdb_module *module =
+ (struct checkpassword_passdb_module *)_module;
+
+ db_checkpassword_deinit(&module->db);
+}
+
+struct passdb_module_interface passdb_checkpassword = {
+ "checkpassword",
+
+ checkpassword_preinit,
+ NULL,
+ checkpassword_deinit,
+
+ checkpassword_verify_plain,
+ checkpassword_lookup_credentials,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_checkpassword = {
+ .name = "checkpassword"
+};
+#endif
diff --git a/src/auth/passdb-dict.c b/src/auth/passdb-dict.c
new file mode 100644
index 0000000..31eea93
--- /dev/null
+++ b/src/auth/passdb-dict.c
@@ -0,0 +1,184 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "dict.h"
+#include "password-scheme.h"
+#include "auth-cache.h"
+#include "db-dict.h"
+
+#include <string.h>
+
+struct dict_passdb_module {
+ struct passdb_module module;
+
+ struct dict_connection *conn;
+};
+
+struct passdb_dict_request {
+ struct auth_request *auth_request;
+ union {
+ verify_plain_callback_t *verify_plain;
+ lookup_credentials_callback_t *lookup_credentials;
+ } callback;
+};
+
+static int
+dict_query_save_results(struct auth_request *auth_request,
+ struct dict_connection *conn,
+ struct db_dict_value_iter *iter)
+{
+ const char *key, *value, *error;
+
+ while (db_dict_value_iter_next(iter, &key, &value)) {
+ if (value != NULL) {
+ auth_request_set_field(auth_request, key, value,
+ conn->set.default_pass_scheme);
+ }
+ }
+ if (db_dict_value_iter_deinit(&iter, &error) < 0) {
+ e_error(authdb_event(auth_request), "%s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static enum passdb_result
+passdb_dict_lookup_key(struct auth_request *auth_request,
+ struct dict_passdb_module *module)
+{
+ struct db_dict_value_iter *iter;
+ int ret;
+
+ ret = db_dict_value_iter_init(module->conn, auth_request,
+ &module->conn->set.passdb_fields,
+ &module->conn->set.parsed_passdb_objects,
+ &iter);
+ if (ret < 0)
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ else if (ret == 0) {
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ return PASSDB_RESULT_USER_UNKNOWN;
+ } else {
+ if (dict_query_save_results(auth_request, module->conn, iter) < 0)
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+
+ if (auth_request->passdb_password == NULL &&
+ !auth_fields_exists(auth_request->fields.extra_fields,
+ "nopassword")) {
+ return auth_request_password_missing(auth_request);
+ } else {
+ return PASSDB_RESULT_OK;
+ }
+ }
+}
+
+static void passdb_dict_lookup_pass(struct passdb_dict_request *dict_request)
+{
+ struct auth_request *auth_request = dict_request->auth_request;
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct dict_passdb_module *module =
+ (struct dict_passdb_module *)_module;
+ const char *password = NULL, *scheme = NULL;
+ enum passdb_result passdb_result;
+
+ if (array_count(&module->conn->set.passdb_fields) == 0 &&
+ array_count(&module->conn->set.parsed_passdb_objects) == 0) {
+ e_error(authdb_event(auth_request),
+ "No passdb_objects or passdb_fields specified");
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else {
+ passdb_result = passdb_dict_lookup_key(auth_request, module);
+ }
+
+ if (passdb_result == PASSDB_RESULT_OK) {
+ /* passdb_password may change on the way,
+ so we'll need to strdup. */
+ password = t_strdup(auth_request->passdb_password);
+ scheme = password_get_scheme(&password);
+ /* auth_request_set_field() sets scheme */
+ i_assert(password == NULL || scheme != NULL);
+ }
+
+ if (auth_request->wanted_credentials_scheme != NULL) {
+ passdb_handle_credentials(passdb_result, password, scheme,
+ dict_request->callback.lookup_credentials,
+ auth_request);
+ } else {
+ if (password != NULL) {
+ passdb_result =
+ auth_request_password_verify(auth_request,
+ auth_request->mech_password,
+ password, scheme, AUTH_SUBSYS_DB);
+ }
+
+ dict_request->callback.verify_plain(passdb_result,
+ auth_request);
+ }
+}
+
+static void dict_verify_plain(struct auth_request *request,
+ const char *password ATTR_UNUSED,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_dict_request *dict_request;
+
+ dict_request = p_new(request->pool, struct passdb_dict_request, 1);
+ dict_request->auth_request = request;
+ dict_request->callback.verify_plain = callback;
+
+ passdb_dict_lookup_pass(dict_request);
+}
+
+static void dict_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passdb_dict_request *dict_request;
+
+ dict_request = p_new(request->pool, struct passdb_dict_request, 1);
+ dict_request->auth_request = request;
+ dict_request->callback.lookup_credentials = callback;
+
+ passdb_dict_lookup_pass(dict_request);
+}
+
+static struct passdb_module *
+passdb_dict_preinit(pool_t pool, const char *args)
+{
+ struct dict_passdb_module *module;
+ struct dict_connection *conn;
+
+ module = p_new(pool, struct dict_passdb_module, 1);
+ module->conn = conn = db_dict_init(args);
+
+ module->module.blocking = TRUE;
+ module->module.default_cache_key = auth_cache_parse_key(pool,
+ db_dict_parse_cache_key(&conn->set.keys, &conn->set.passdb_fields,
+ &conn->set.parsed_passdb_objects));
+ module->module.default_pass_scheme = conn->set.default_pass_scheme;
+ return &module->module;
+}
+
+static void passdb_dict_deinit(struct passdb_module *_module)
+{
+ struct dict_passdb_module *module =
+ (struct dict_passdb_module *)_module;
+
+ db_dict_unref(&module->conn);
+}
+
+struct passdb_module_interface passdb_dict = {
+ "dict",
+
+ passdb_dict_preinit,
+ NULL,
+ passdb_dict_deinit,
+
+ dict_verify_plain,
+ dict_lookup_credentials,
+ NULL
+};
diff --git a/src/auth/passdb-imap.c b/src/auth/passdb-imap.c
new file mode 100644
index 0000000..9965732
--- /dev/null
+++ b/src/auth/passdb-imap.c
@@ -0,0 +1,241 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "passdb.h"
+#include "str.h"
+#include "imap-resp-code.h"
+#include "imapc-client.h"
+
+#define IMAP_DEFAULT_PORT 143
+#define IMAPS_DEFAULT_PORT 993
+#define DNS_CLIENT_SOCKET_NAME "dns-client"
+
+struct imap_passdb_module {
+ struct passdb_module module;
+ struct imapc_client_settings set;
+ bool set_have_vars;
+};
+
+struct imap_auth_request {
+ struct imapc_client *client;
+ struct auth_request *auth_request;
+ verify_plain_callback_t *verify_callback;
+ struct timeout *to_free;
+};
+
+static enum passdb_result
+passdb_imap_get_failure_result(const struct imapc_command_reply *reply)
+{
+ const char *key = reply->resp_text_key;
+
+ if (key == NULL)
+ return PASSDB_RESULT_PASSWORD_MISMATCH;
+
+ if (strcasecmp(key, IMAP_RESP_CODE_AUTHFAILED) == 0 ||
+ strcasecmp(key, IMAP_RESP_CODE_AUTHZFAILED) == 0)
+ return PASSDB_RESULT_PASSWORD_MISMATCH;
+ if (strcasecmp(key, IMAP_RESP_CODE_EXPIRED) == 0)
+ return PASSDB_RESULT_PASS_EXPIRED;
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+}
+
+static void passdb_imap_login_free(struct imap_auth_request *request)
+{
+ timeout_remove(&request->to_free);
+ imapc_client_deinit(&request->client);
+ auth_request_unref(&request->auth_request);
+}
+
+static void
+passdb_imap_login_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imap_auth_request *request = context;
+ enum passdb_result result = PASSDB_RESULT_INTERNAL_FAILURE;
+
+ switch (reply->state) {
+ case IMAPC_COMMAND_STATE_OK:
+ result = PASSDB_RESULT_OK;
+ break;
+ case IMAPC_COMMAND_STATE_NO:
+ result = passdb_imap_get_failure_result(reply);
+ e_info(authdb_event(request->auth_request),
+ "%s", reply->text_full);
+ break;
+ case IMAPC_COMMAND_STATE_AUTH_FAILED:
+ case IMAPC_COMMAND_STATE_BAD:
+ case IMAPC_COMMAND_STATE_DISCONNECTED:
+ e_error(authdb_event(request->auth_request),
+ "%s", reply->text_full);
+ break;
+ }
+ request->verify_callback(result, request->auth_request);
+ /* imapc_client can't be freed in this callback, so do it in a
+ separate callback. FIXME: remove this once imapc supports proper
+ refcounting. */
+ request->to_free = timeout_add_short(0, passdb_imap_login_free, request);
+}
+
+static void
+passdb_imap_verify_plain(struct auth_request *auth_request,
+ const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct imap_passdb_module *module =
+ (struct imap_passdb_module *)_module;
+ struct imap_auth_request *request;
+ struct imapc_client_settings set;
+ const char *error;
+ string_t *str;
+
+ set = module->set;
+ set.debug = event_want_debug(auth_request->event);
+ set.dns_client_socket_path =
+ t_strconcat(auth_request->set->base_dir, "/",
+ DNS_CLIENT_SOCKET_NAME, NULL);
+ set.password = password;
+ set.max_idle_time = IMAPC_DEFAULT_MAX_IDLE_TIME;
+ if (set.ssl_set.ca_dir == NULL)
+ set.ssl_set.ca_dir = auth_request->set->ssl_client_ca_dir;
+ if (set.ssl_set.ca_file == NULL)
+ set.ssl_set.ca_file = auth_request->set->ssl_client_ca_file;
+
+ if (module->set_have_vars) {
+ str = t_str_new(128);
+ if (auth_request_var_expand(str, set.username, auth_request,
+ NULL, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand username=%s: %s",
+ set.username, error);
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+ set.username = t_strdup(str_c(str));
+
+ str_truncate(str, 0);
+ if (auth_request_var_expand(str, set.host, auth_request,
+ NULL, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand host=%s: %s",
+ set.host, error);
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+ set.host = t_strdup(str_c(str));
+ }
+ e_debug(authdb_event(auth_request),
+ "lookup host=%s port=%d", set.host, set.port);
+
+ request = p_new(auth_request->pool, struct imap_auth_request, 1);
+ request->client = imapc_client_init(&set, authdb_event(auth_request));
+ request->auth_request = auth_request;
+ request->verify_callback = callback;
+
+ auth_request_ref(auth_request);
+ imapc_client_set_login_callback(request->client, passdb_imap_login_callback, request);
+ imapc_client_login(request->client);
+}
+
+static struct passdb_module *
+passdb_imap_preinit(pool_t pool, const char *args)
+{
+ struct imap_passdb_module *module;
+ char **tmp;
+ const char *key, *value;
+ bool port_set = FALSE;
+
+ module = p_new(pool, struct imap_passdb_module, 1);
+ module->module.default_pass_scheme = "PLAIN";
+ module->set.port = IMAP_DEFAULT_PORT;
+ module->set.ssl_mode = IMAPC_CLIENT_SSL_MODE_NONE;
+ module->set.username = "%u";
+ module->set.rawlog_dir = "";
+
+ for (tmp = p_strsplit(pool, args, " "); *tmp != NULL; tmp++) {
+ key = *tmp;
+ value = strchr(key, '=');
+ if (value == NULL)
+ value = "";
+ else
+ key = t_strdup_until(key, value++);
+ if (strcmp(key, "host") == 0)
+ module->set.host = value;
+ else if (strcmp(key, "port") == 0) {
+ if (net_str2port(value, &module->set.port) < 0)
+ i_fatal("passdb imap: Invalid port: %s", value);
+ port_set = TRUE;
+ } else if (strcmp(key, "username") == 0)
+ module->set.username = value;
+ else if (strcmp(key, "ssl_ca_dir") == 0)
+ module->set.ssl_set.ca_dir = value;
+ else if (strcmp(key, "ssl_ca_file") == 0)
+ module->set.ssl_set.ca_file = value;
+ else if (strcmp(key, "rawlog_dir") == 0)
+ module->set.rawlog_dir = value;
+ else if (strcmp(key, "ssl") == 0) {
+ if (strcmp(value, "imaps") == 0) {
+ if (!port_set)
+ module->set.port = IMAPS_DEFAULT_PORT;
+ module->set.ssl_mode =
+ IMAPC_CLIENT_SSL_MODE_IMMEDIATE;
+ } else if (strcmp(value, "starttls") == 0) {
+ module->set.ssl_mode =
+ IMAPC_CLIENT_SSL_MODE_STARTTLS;
+ } else {
+ i_fatal("passdb imap: Invalid ssl mode: %s",
+ value);
+ }
+ } else if (strcmp(key, "allow_invalid_cert") == 0) {
+ if (strcmp(value, "yes") == 0) {
+ module->set.ssl_set.allow_invalid_cert = TRUE;
+ } else if (strcmp(value, "no") == 0) {
+ module->set.ssl_set.allow_invalid_cert = FALSE;
+ } else {
+ i_fatal("passdb imap: Invalid allow_invalid_cert value: %s",
+ value);
+ }
+ } else {
+ i_fatal("passdb imap: Unknown parameter: %s", key);
+ }
+ }
+
+ if (!module->set.ssl_set.allow_invalid_cert && module->set.ssl_mode != IMAPC_CLIENT_SSL_MODE_NONE) {
+ if (module->set.ssl_set.ca_dir == NULL && module->set.ssl_set.ca_file == NULL)
+ i_fatal("passdb imap: Cannot verify certificate without ssl_ca_dir or ssl_ca_file setting");
+ }
+
+ if (module->set.host == NULL)
+ i_fatal("passdb imap: Missing host parameter");
+
+ module->set_have_vars =
+ strchr(module->set.username, '%') != NULL ||
+ strchr(module->set.host, '%') != NULL;
+ return &module->module;
+}
+
+static struct passdb_module_interface passdb_imap_plugin = {
+ "imap",
+
+ passdb_imap_preinit,
+ NULL,
+ NULL,
+
+ passdb_imap_verify_plain,
+ NULL,
+ NULL
+};
+
+void authdb_imap_init(void);
+void authdb_imap_deinit(void);
+
+void authdb_imap_init(void)
+{
+ passdb_register_module(&passdb_imap_plugin);
+
+}
+void authdb_imap_deinit(void)
+{
+ passdb_unregister_module(&passdb_imap_plugin);
+}
diff --git a/src/auth/passdb-ldap.c b/src/auth/passdb-ldap.c
new file mode 100644
index 0000000..1aa4b28
--- /dev/null
+++ b/src/auth/passdb-ldap.c
@@ -0,0 +1,517 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#if defined(PASSDB_LDAP) && (defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD))
+
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "password-scheme.h"
+#include "auth-cache.h"
+#include "db-ldap.h"
+
+#include <ldap.h>
+
+struct ldap_passdb_module {
+ struct passdb_module module;
+
+ struct ldap_connection *conn;
+};
+
+struct passdb_ldap_request {
+ union {
+ struct ldap_request ldap;
+ struct ldap_request_search search;
+ struct ldap_request_bind bind;
+ } request;
+ const char *dn;
+
+ union {
+ verify_plain_callback_t *verify_plain;
+ lookup_credentials_callback_t *lookup_credentials;
+ } callback;
+
+ unsigned int entries;
+ bool require_password;
+};
+
+static void
+ldap_query_save_result(struct ldap_connection *conn,
+ struct auth_request *auth_request,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res)
+{
+ struct db_ldap_result_iterate_context *ldap_iter;
+ const char *name, *const *values;
+
+ ldap_iter = db_ldap_result_iterate_init(conn, ldap_request, res, FALSE);
+ while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) {
+ if (values[0] == NULL) {
+ auth_request_set_null_field(auth_request, name);
+ continue;
+ }
+ if (values[1] != NULL) {
+ e_warning(authdb_event(auth_request),
+ "Multiple values found for '%s', "
+ "using value '%s'", name, values[0]);
+ }
+ auth_request_set_field(auth_request, name, values[0],
+ conn->set.default_pass_scheme);
+ }
+ db_ldap_result_iterate_deinit(&ldap_iter);
+}
+
+static void
+ldap_lookup_finish(struct auth_request *auth_request,
+ struct passdb_ldap_request *ldap_request,
+ LDAPMessage *res)
+{
+ enum passdb_result passdb_result;
+ const char *password = NULL, *scheme;
+
+ if (res == NULL) {
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else if (ldap_request->entries == 0) {
+ passdb_result = PASSDB_RESULT_USER_UNKNOWN;
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ } else if (ldap_request->entries > 1) {
+ e_error(authdb_event(auth_request),
+ "pass_filter matched multiple objects, aborting");
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else if (auth_request->passdb_password == NULL &&
+ ldap_request->require_password &&
+ !auth_fields_exists(auth_request->fields.extra_fields, "nopassword")) {
+ passdb_result = auth_request_password_missing(auth_request);
+ } else {
+ /* passdb_password may change on the way,
+ so we'll need to strdup. */
+ password = t_strdup(auth_request->passdb_password);
+ passdb_result = PASSDB_RESULT_OK;
+ }
+
+ scheme = password_get_scheme(&password);
+ /* auth_request_set_field() sets scheme */
+ i_assert(password == NULL || scheme != NULL);
+
+ if (auth_request->wanted_credentials_scheme != NULL) {
+ passdb_handle_credentials(passdb_result, password, scheme,
+ ldap_request->callback.lookup_credentials,
+ auth_request);
+ } else {
+ if (password != NULL) {
+ passdb_result =
+ auth_request_password_verify(auth_request,
+ auth_request->mech_password,
+ password, scheme, AUTH_SUBSYS_DB);
+ }
+
+ ldap_request->callback.verify_plain(passdb_result,
+ auth_request);
+ }
+}
+
+static void
+ldap_lookup_pass_callback(struct ldap_connection *conn,
+ struct ldap_request *request, LDAPMessage *res)
+{
+ struct passdb_ldap_request *ldap_request =
+ (struct passdb_ldap_request *)request;
+ struct auth_request *auth_request = request->auth_request;
+
+ if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) {
+ ldap_lookup_finish(auth_request, ldap_request, res);
+ auth_request_unref(&auth_request);
+ return;
+ }
+
+ if (ldap_request->entries++ == 0) {
+ /* first entry */
+ ldap_query_save_result(conn, auth_request,
+ &ldap_request->request.search, res);
+ }
+}
+
+static void
+ldap_auth_bind_callback(struct ldap_connection *conn,
+ struct ldap_request *ldap_request, LDAPMessage *res)
+{
+ struct passdb_ldap_request *passdb_ldap_request =
+ (struct passdb_ldap_request *)ldap_request;
+ struct auth_request *auth_request = ldap_request->auth_request;
+ enum passdb_result passdb_result;
+ int ret;
+
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+
+ if (res != NULL) {
+ ret = ldap_result2error(conn->ld, res, 0);
+ if (ret == LDAP_SUCCESS)
+ passdb_result = PASSDB_RESULT_OK;
+ else if (ret == LDAP_INVALID_CREDENTIALS) {
+ auth_request_log_login_failure(auth_request,
+ AUTH_SUBSYS_DB,
+ AUTH_LOG_MSG_PASSWORD_MISMATCH" (for LDAP bind)");
+ passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ } else if (ret == LDAP_NO_SUCH_OBJECT) {
+ passdb_result = PASSDB_RESULT_USER_UNKNOWN;
+ auth_request_log_unknown_user(auth_request,
+ AUTH_SUBSYS_DB);
+ } else {
+ e_error(authdb_event(auth_request),
+ "ldap_bind() failed: %s",
+ ldap_err2string(ret));
+ }
+ }
+
+ passdb_ldap_request->callback.
+ verify_plain(passdb_result, auth_request);
+ auth_request_unref(&auth_request);
+}
+
+static void ldap_auth_bind(struct ldap_connection *conn,
+ struct ldap_request_bind *brequest)
+{
+ struct passdb_ldap_request *passdb_ldap_request =
+ (struct passdb_ldap_request *)brequest;
+ struct auth_request *auth_request = brequest->request.auth_request;
+
+ if (*auth_request->mech_password == '\0') {
+ /* Assume that empty password fails. This is especially
+ important with Windows 2003 AD, which always returns success
+ with empty passwords. */
+ e_info(authdb_event(auth_request),
+ "Login attempt with empty password");
+ passdb_ldap_request->callback.
+ verify_plain(PASSDB_RESULT_PASSWORD_MISMATCH,
+ auth_request);
+ return;
+ }
+
+ brequest->request.callback = ldap_auth_bind_callback;
+ db_ldap_request(conn, &brequest->request);
+}
+
+static void passdb_ldap_request_fail(struct passdb_ldap_request *request,
+ enum passdb_result passdb_result)
+{
+ struct auth_request *auth_request = request->request.ldap.auth_request;
+
+ if (auth_request->wanted_credentials_scheme != NULL) {
+ request->callback.lookup_credentials(passdb_result, NULL, 0,
+ auth_request);
+ } else {
+ request->callback.verify_plain(passdb_result, auth_request);
+ }
+ auth_request_unref(&auth_request);
+}
+
+static void
+ldap_bind_lookup_dn_fail(struct auth_request *auth_request,
+ struct passdb_ldap_request *request,
+ LDAPMessage *res)
+{
+ enum passdb_result passdb_result;
+
+ if (res == NULL)
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ else if (request->entries == 0) {
+ passdb_result = PASSDB_RESULT_USER_UNKNOWN;
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ } else {
+ i_assert(request->entries > 1);
+ e_error(authdb_event(auth_request),
+ "pass_filter matched multiple objects, aborting");
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ passdb_ldap_request_fail(request, passdb_result);
+}
+
+static void ldap_bind_lookup_dn_callback(struct ldap_connection *conn,
+ struct ldap_request *ldap_request,
+ LDAPMessage *res)
+{
+ struct passdb_ldap_request *passdb_ldap_request =
+ (struct passdb_ldap_request *)ldap_request;
+ struct auth_request *auth_request = ldap_request->auth_request;
+ struct passdb_ldap_request *brequest;
+ char *dn;
+
+ if (res != NULL && ldap_msgtype(res) == LDAP_RES_SEARCH_ENTRY) {
+ if (passdb_ldap_request->entries++ > 0) {
+ /* too many replies */
+ return;
+ }
+
+ /* first entry */
+ ldap_query_save_result(conn, auth_request,
+ &passdb_ldap_request->request.search, res);
+
+ /* save dn */
+ dn = ldap_get_dn(conn->ld, res);
+ passdb_ldap_request->dn = p_strdup(auth_request->pool, dn);
+ ldap_memfree(dn);
+ } else if (res == NULL || passdb_ldap_request->entries != 1) {
+ /* failure */
+ ldap_bind_lookup_dn_fail(auth_request, passdb_ldap_request, res);
+ } else if (auth_request->fields.skip_password_check) {
+ /* we've already verified that the password matched -
+ we just wanted to get any extra fields */
+ passdb_ldap_request->callback.
+ verify_plain(PASSDB_RESULT_OK, auth_request);
+ auth_request_unref(&auth_request);
+ } else {
+ /* create a new bind request */
+ brequest = p_new(auth_request->pool,
+ struct passdb_ldap_request, 1);
+ brequest->dn = passdb_ldap_request->dn;
+ brequest->callback = passdb_ldap_request->callback;
+ brequest->request.bind.dn = brequest->dn;
+ brequest->request.bind.request.type = LDAP_REQUEST_TYPE_BIND;
+ brequest->request.bind.request.auth_request = auth_request;
+
+ ldap_auth_bind(conn, &brequest->request.bind);
+ }
+}
+
+static void ldap_lookup_pass(struct auth_request *auth_request,
+ struct passdb_ldap_request *request,
+ bool require_password)
+{
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ struct ldap_request_search *srequest = &request->request.search;
+ const char **attr_names = (const char **)conn->pass_attr_names;
+ const char *error;
+ string_t *str;
+
+ request->require_password = require_password;
+ srequest->request.type = LDAP_REQUEST_TYPE_SEARCH;
+
+ str = t_str_new(512);
+ if (auth_request_var_expand(str, conn->set.base, auth_request,
+ ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand base=%s: %s", conn->set.base, error);
+ passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE);
+ return;
+ }
+ srequest->base = p_strdup(auth_request->pool, str_c(str));
+
+ str_truncate(str, 0);
+ if (auth_request_var_expand(str, conn->set.pass_filter,
+ auth_request, ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand pass_filter=%s: %s",
+ conn->set.pass_filter, error);
+ passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE);
+ return;
+ }
+ srequest->filter = p_strdup(auth_request->pool, str_c(str));
+ srequest->attr_map = &conn->pass_attr_map;
+ srequest->attributes = conn->pass_attr_names;
+
+ e_debug(authdb_event(auth_request), "pass search: "
+ "base=%s scope=%s filter=%s fields=%s",
+ srequest->base, conn->set.scope,
+ srequest->filter, attr_names == NULL ? "(all)" :
+ t_strarray_join(attr_names, ","));
+
+ srequest->request.callback = ldap_lookup_pass_callback;
+ db_ldap_request(conn, &srequest->request);
+}
+
+static void ldap_bind_lookup_dn(struct auth_request *auth_request,
+ struct passdb_ldap_request *request)
+{
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ struct ldap_request_search *srequest = &request->request.search;
+ const char *error;
+ string_t *str;
+
+ srequest->request.type = LDAP_REQUEST_TYPE_SEARCH;
+
+ str = t_str_new(512);
+ if (auth_request_var_expand(str, conn->set.base, auth_request,
+ ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand base=%s: %s", conn->set.base, error);
+ passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE);
+ return;
+ }
+ srequest->base = p_strdup(auth_request->pool, str_c(str));
+
+ str_truncate(str, 0);
+ if (auth_request_var_expand(str, conn->set.pass_filter,
+ auth_request, ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand pass_filter=%s: %s",
+ conn->set.pass_filter, error);
+ passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE);
+ return;
+ }
+ srequest->filter = p_strdup(auth_request->pool, str_c(str));
+
+ /* we don't need the attributes to perform authentication, but they
+ may contain some extra parameters. if a password is returned,
+ it's just ignored. */
+ srequest->attr_map = &conn->pass_attr_map;
+ srequest->attributes = conn->pass_attr_names;
+
+ e_debug(authdb_event(auth_request),
+ "bind search: base=%s filter=%s",
+ srequest->base, srequest->filter);
+
+ srequest->request.callback = ldap_bind_lookup_dn_callback;
+ db_ldap_request(conn, &srequest->request);
+}
+
+static void
+ldap_verify_plain_auth_bind_userdn(struct auth_request *auth_request,
+ struct passdb_ldap_request *request)
+{
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ struct ldap_request_bind *brequest = &request->request.bind;
+ string_t *dn;
+ const char *error;
+
+ brequest->request.type = LDAP_REQUEST_TYPE_BIND;
+
+ dn = t_str_new(512);
+ if (auth_request_var_expand(dn, conn->set.auth_bind_userdn,
+ auth_request, ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand auth_bind_userdn=%s: %s",
+ conn->set.auth_bind_userdn, error);
+ passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE);
+ return;
+ }
+
+ brequest->dn = p_strdup(auth_request->pool, str_c(dn));
+ ldap_auth_bind(conn, brequest);
+}
+
+static void
+ldap_verify_plain(struct auth_request *request,
+ const char *password ATTR_UNUSED,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ struct passdb_ldap_request *ldap_request;
+
+ /* reconnect if needed. this is also done by db_ldap_search(), but
+ with auth binds we'll have to do it ourself */
+ if (db_ldap_connect(conn)< 0) {
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+
+ ldap_request = p_new(request->pool, struct passdb_ldap_request, 1);
+ ldap_request->callback.verify_plain = callback;
+
+ auth_request_ref(request);
+ ldap_request->request.ldap.auth_request = request;
+
+ if (!conn->set.auth_bind)
+ ldap_lookup_pass(request, ldap_request, TRUE);
+ else if (conn->set.auth_bind_userdn == NULL)
+ ldap_bind_lookup_dn(request, ldap_request);
+ else
+ ldap_verify_plain_auth_bind_userdn(request, ldap_request);
+}
+
+static void ldap_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+ struct passdb_ldap_request *ldap_request;
+ bool require_password;
+
+ ldap_request = p_new(request->pool, struct passdb_ldap_request, 1);
+ ldap_request->callback.lookup_credentials = callback;
+
+ auth_request_ref(request);
+ ldap_request->request.ldap.auth_request = request;
+
+ /* with auth_bind=yes we don't necessarily have a password.
+ this will fail actual password credentials lookups, but it's fine
+ for passdb lookups done by lmtp/doveadm */
+ require_password = !module->conn->set.auth_bind;
+ ldap_lookup_pass(request, ldap_request, require_password);
+}
+
+static struct passdb_module *
+passdb_ldap_preinit(pool_t pool, const char *args)
+{
+ struct ldap_passdb_module *module;
+ struct ldap_connection *conn;
+
+ module = p_new(pool, struct ldap_passdb_module, 1);
+ module->conn = conn = db_ldap_init(args, FALSE);
+ p_array_init(&conn->pass_attr_map, pool, 16);
+ db_ldap_set_attrs(conn, conn->set.pass_attrs, &conn->pass_attr_names,
+ &conn->pass_attr_map,
+ conn->set.auth_bind ? "password" : NULL);
+ module->module.blocking = conn->set.blocking;
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool,
+ t_strconcat(conn->set.base,
+ conn->set.pass_attrs,
+ conn->set.pass_filter, NULL));
+ module->module.default_pass_scheme = conn->set.default_pass_scheme;
+ return &module->module;
+}
+
+static void passdb_ldap_init(struct passdb_module *_module)
+{
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+
+ if (!module->module.blocking || worker)
+ db_ldap_connect_delayed(module->conn);
+}
+
+static void passdb_ldap_deinit(struct passdb_module *_module)
+{
+ struct ldap_passdb_module *module =
+ (struct ldap_passdb_module *)_module;
+
+ db_ldap_unref(&module->conn);
+}
+
+#ifndef PLUGIN_BUILD
+struct passdb_module_interface passdb_ldap =
+#else
+struct passdb_module_interface passdb_ldap_plugin =
+#endif
+{
+ "ldap",
+
+ passdb_ldap_preinit,
+ passdb_ldap_init,
+ passdb_ldap_deinit,
+
+ ldap_verify_plain,
+ ldap_lookup_credentials,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_ldap = {
+ .name = "ldap"
+};
+#endif
diff --git a/src/auth/passdb-lua.c b/src/auth/passdb-lua.c
new file mode 100644
index 0000000..13665c4
--- /dev/null
+++ b/src/auth/passdb-lua.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+#include "auth-cache.h"
+
+#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD)
+
+#include "db-lua.h"
+
+struct dlua_passdb_module {
+ struct passdb_module module;
+ struct dlua_script *script;
+ const char *file;
+ bool has_password_verify;
+};
+
+static enum passdb_result
+passdb_lua_verify_password(struct dlua_passdb_module *module,
+ struct auth_request *request, const char *password)
+{
+ const char *error = NULL;
+ enum passdb_result result =
+ auth_lua_call_password_verify(module->script, request,
+ password, &error);
+ if (result == PASSDB_RESULT_PASSWORD_MISMATCH) {
+ auth_request_log_password_mismatch(request, AUTH_SUBSYS_DB);
+ } else if (result == PASSDB_RESULT_INTERNAL_FAILURE && error != NULL) {
+ e_error(authdb_event(request), "passdb-lua: %s",
+ error);
+ }
+ return result;
+}
+
+static enum passdb_result
+passdb_lua_lookup(struct auth_request *request,
+ const char **scheme_r, const char **password_r)
+{
+ const char *error = NULL;
+ enum passdb_result result;
+ struct dlua_passdb_module *module =
+ (struct dlua_passdb_module *)request->passdb->passdb;
+
+ *scheme_r = *password_r = NULL;
+
+ result = auth_lua_call_passdb_lookup(module->script, request, scheme_r,
+ password_r, &error);
+
+ if (result == PASSDB_RESULT_INTERNAL_FAILURE && error != NULL) {
+ e_error(authdb_event(request), "db-lua: %s", error);
+ } else if (result != PASSDB_RESULT_OK) {
+ /* skip next bit */
+ } else if (!auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ if (*password_r == NULL || **password_r == '\0') {
+ result = auth_request_password_missing(request);
+ } else {
+ if (*scheme_r == NULL)
+ *scheme_r = request->passdb->passdb->default_pass_scheme;
+ auth_request_set_field(request, "password",
+ *password_r, *scheme_r);
+ }
+ } else if (*password_r != NULL && **password_r != '\0') {
+ e_info(authdb_event(request),
+ "nopassword given and password is not empty");
+ result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ }
+ return result;
+}
+
+static void
+passdb_lua_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ const char *lua_password, *lua_scheme;
+ enum passdb_result result =
+ passdb_lua_lookup(request, &lua_scheme, &lua_password);
+
+ passdb_handle_credentials(result, lua_password, lua_scheme, callback, request);
+}
+
+static void
+passdb_lua_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct dlua_passdb_module *module =
+ (struct dlua_passdb_module *)request->passdb->passdb;
+ const char *lua_password, *lua_scheme;
+ enum passdb_result result;
+
+ if (module->has_password_verify) {
+ result = passdb_lua_verify_password(module, request, password);
+ } else {
+ result = passdb_lua_lookup(request, &lua_scheme, &lua_password);
+ if (result == PASSDB_RESULT_OK) {
+ if (lua_scheme == NULL)
+ lua_scheme = "PLAIN";
+ result = auth_request_password_verify(request, password,
+ lua_password,
+ lua_scheme,
+ AUTH_SUBSYS_DB);
+ }
+ }
+ callback(result, request);
+}
+
+static struct passdb_module *
+passdb_lua_preinit(pool_t pool, const char *args)
+{
+ const char *cache_key = "%u";
+ const char *scheme = "PLAIN";
+ struct dlua_passdb_module *module;
+ bool blocking = TRUE;
+
+ module = p_new(pool, struct dlua_passdb_module, 1);
+ const char *const *fields = t_strsplit_spaces(args, " ");
+ while(*fields != NULL) {
+ if (str_begins(*fields, "file=")) {
+ module->file = p_strdup(pool, (*fields)+5);
+ } else if (str_begins(*fields, "blocking=")) {
+ const char *value = (*fields)+9;
+ if (strcmp(value, "yes") == 0) {
+ blocking = TRUE;
+ } else if (strcmp(value, "no") == 0) {
+ blocking = FALSE;
+ } else {
+ i_fatal("Invalid value %s. "
+ "Field blocking must be yes or no",
+ value);
+ }
+ } else if (str_begins(*fields, "cache_key=")) {
+ if (*((*fields)+10) != '\0')
+ cache_key = (*fields)+10;
+ else /* explicitly disable auth caching for lua */
+ cache_key = NULL;
+ } else if (str_begins(*fields, "scheme=")) {
+ scheme = p_strdup(pool, (*fields)+7);
+ } else {
+ i_fatal("Unsupported parameter %s", *fields);
+ }
+ fields++;
+ }
+
+ if (module->file == NULL)
+ i_fatal("passdb-lua: Missing mandatory file= parameter");
+
+ module->module.blocking = blocking;
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool, cache_key);
+ module->module.default_pass_scheme = scheme;
+ return &module->module;
+}
+
+static void passdb_lua_init(struct passdb_module *_module)
+{
+ struct dlua_passdb_module *module =
+ (struct dlua_passdb_module *)_module;
+ const char *error;
+
+ if (dlua_script_create_file(module->file, &module->script, auth_event, &error) < 0 ||
+ auth_lua_script_init(module->script, &error) < 0)
+ i_fatal("passdb-lua: initialization failed: %s", error);
+ module->has_password_verify =
+ dlua_script_has_function(module->script, AUTH_LUA_PASSWORD_VERIFY);
+}
+
+static void passdb_lua_deinit(struct passdb_module *_module)
+{
+ struct dlua_passdb_module *module =
+ (struct dlua_passdb_module *)_module;
+ dlua_script_unref(&module->script);
+}
+
+#ifndef PLUGIN_BUILD
+struct passdb_module_interface passdb_lua =
+#else
+struct passdb_module_interface passdb_lua_plugin =
+#endif
+{
+ "lua",
+
+ passdb_lua_preinit,
+ passdb_lua_init,
+ passdb_lua_deinit,
+
+ passdb_lua_verify_plain,
+ passdb_lua_lookup_credentials,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_lua = {
+ .name = "lua"
+};
+#endif
diff --git a/src/auth/passdb-oauth2.c b/src/auth/passdb-oauth2.c
new file mode 100644
index 0000000..e52a73f
--- /dev/null
+++ b/src/auth/passdb-oauth2.c
@@ -0,0 +1,90 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+#include "db-oauth2.h"
+
+struct oauth2_passdb_module {
+ struct passdb_module module;
+ struct db_oauth2 *db;
+};
+
+static void
+oauth2_verify_plain_continue(struct db_oauth2_request *req,
+ enum passdb_result result, const char *error,
+ struct auth_request *request)
+{
+ if (result == PASSDB_RESULT_INTERNAL_FAILURE)
+ e_error(authdb_event(request), "oauth2 failed: %s",
+ error);
+ else if (result != PASSDB_RESULT_OK)
+ e_info(authdb_event(request), "oauth2 failed: %s",
+ error);
+ else {
+ auth_request_set_field(request, "token", req->token, "PLAIN");
+ }
+ req->verify_callback(result, request);
+ auth_request_unref(&request);
+}
+
+static void
+oauth2_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct oauth2_passdb_module *module =
+ (struct oauth2_passdb_module *)request->passdb->passdb;
+ struct db_oauth2_request *req =
+ p_new(request->pool, struct db_oauth2_request, 1);
+ req->pool = request->pool;
+ req->verify_callback = callback;
+
+ auth_request_ref(request);
+
+ db_oauth2_lookup(module->db, req, password, request, oauth2_verify_plain_continue, request);
+}
+
+static struct passdb_module *
+oauth2_preinit(pool_t pool, const char *args)
+{
+ struct oauth2_passdb_module *module;
+
+ module = p_new(pool, struct oauth2_passdb_module, 1);
+ module->db = db_oauth2_init(args);
+ module->module.default_pass_scheme = "PLAIN";
+
+ if (db_oauth2_uses_password_grant(module->db)) {
+ module->module.default_cache_key = "%u";
+ } else {
+ module->module.default_cache_key = "%u%w";
+ }
+
+ return &module->module;
+}
+
+static void oauth2_deinit(struct passdb_module *passdb)
+{
+ struct oauth2_passdb_module *module = (struct oauth2_passdb_module *)passdb;
+ db_oauth2_unref(&module->db);
+}
+
+/* FIXME: Remove when oauth2 mech is fixed */
+const char *passdb_oauth2_get_oidc_url(struct passdb_module *passdb)
+{
+ struct oauth2_passdb_module *module =
+ container_of(passdb, struct oauth2_passdb_module, module);
+ if (module->db != NULL)
+ return db_oauth2_get_openid_configuration_url(module->db);
+ return NULL;
+}
+
+struct passdb_module_interface passdb_oauth2 = {
+ "oauth2",
+
+ oauth2_preinit,
+ NULL,
+ oauth2_deinit,
+
+ oauth2_verify_plain,
+ NULL,
+ NULL
+};
diff --git a/src/auth/passdb-pam.c b/src/auth/passdb-pam.c
new file mode 100644
index 0000000..6a1032d
--- /dev/null
+++ b/src/auth/passdb-pam.c
@@ -0,0 +1,401 @@
+/*
+ Based on auth_pam.c from popa3d by Solar Designer <solar@openwall.com>.
+
+ You're allowed to do whatever you like with this software (including
+ re-distribution in source and/or binary form, with or without
+ modification), provided that credit is given where it is due and any
+ modified versions are marked as such. There's absolutely no warranty.
+*/
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_PAM
+
+#include "lib-signals.h"
+#include "str.h"
+#include "net.h"
+#include "safe-memset.h"
+#include "auth-cache.h"
+
+#include <sys/stat.h>
+
+#ifdef HAVE_SECURITY_PAM_APPL_H
+# include <security/pam_appl.h>
+#elif defined(HAVE_PAM_PAM_APPL_H)
+# include <pam/pam_appl.h>
+#endif
+
+#if defined(sun) || defined(__sun__) || defined(_HPUX_SOURCE)
+# define pam_const
+#else
+# define pam_const const
+#endif
+
+typedef pam_const void *pam_item_t;
+
+#define PASSDB_PAM_DEFAULT_MAX_REQUESTS 100
+
+struct pam_passdb_module {
+ struct passdb_module module;
+
+ const char *service_name, *pam_cache_key;
+ unsigned int requests_left;
+
+ bool pam_setcred:1;
+ bool pam_session:1;
+ bool failure_show_msg:1;
+};
+
+struct pam_conv_context {
+ struct auth_request *request;
+ const char *pass;
+ const char *failure_msg;
+};
+
+static int
+pam_userpass_conv(int num_msg, pam_const struct pam_message **msg,
+ struct pam_response **resp_r, void *appdata_ptr)
+{
+ /* @UNSAFE */
+ struct pam_conv_context *ctx = appdata_ptr;
+ struct passdb_module *_passdb = ctx->request->passdb->passdb;
+ struct pam_passdb_module *passdb = (struct pam_passdb_module *)_passdb;
+ struct pam_response *resp;
+ char *string;
+ int i;
+
+ *resp_r = NULL;
+
+ resp = calloc(num_msg, sizeof(struct pam_response));
+ if (resp == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "Out of memory");
+
+ for (i = 0; i < num_msg; i++) {
+ e_debug(authdb_event(ctx->request),
+ "#%d/%d style=%d msg=%s", i+1, num_msg,
+ msg[i]->msg_style,
+ msg[i]->msg != NULL ? msg[i]->msg : "");
+ switch (msg[i]->msg_style) {
+ case PAM_PROMPT_ECHO_ON:
+ /* Assume we're asking for user. We might not ever
+ get here because PAM already knows the user. */
+ string = strdup(ctx->request->fields.user);
+ if (string == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "Out of memory");
+ break;
+ case PAM_PROMPT_ECHO_OFF:
+ /* Assume we're asking for password */
+ if (passdb->failure_show_msg)
+ ctx->failure_msg = t_strdup(msg[i]->msg);
+ string = strdup(ctx->pass);
+ if (string == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "Out of memory");
+ break;
+ case PAM_ERROR_MSG:
+ case PAM_TEXT_INFO:
+ string = NULL;
+ break;
+ default:
+ while (--i >= 0) {
+ if (resp[i].resp != NULL) {
+ safe_memset(resp[i].resp, 0,
+ strlen(resp[i].resp));
+ free(resp[i].resp);
+ }
+ }
+
+ free(resp);
+ return PAM_CONV_ERR;
+ }
+
+ resp[i].resp_retcode = PAM_SUCCESS;
+ resp[i].resp = string;
+ }
+
+ *resp_r = resp;
+ return PAM_SUCCESS;
+}
+
+static const char *
+pam_get_missing_service_file_path(const char *service ATTR_UNUSED)
+{
+#ifdef SUNPAM
+ /* Uses /etc/pam.conf - we're not going to parse that */
+ return NULL;
+#else
+ static bool service_checked = FALSE;
+ const char *path;
+ struct stat st;
+
+ if (service_checked) {
+ /* check and complain only once */
+ return NULL;
+ }
+ service_checked = TRUE;
+
+ path = t_strdup_printf("/etc/pam.d/%s", service);
+ if (stat(path, &st) < 0 && errno == ENOENT) {
+ /* looks like it's missing. but before assuming that the system
+ even uses /etc/pam.d, make sure that it exists. */
+ if (stat("/etc/pam.d", &st) == 0)
+ return path;
+ }
+ /* exists or is unknown */
+ return NULL;
+#endif
+}
+
+static int try_pam_auth(struct auth_request *request, pam_handle_t *pamh,
+ const char *service)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct pam_passdb_module *module = (struct pam_passdb_module *)_module;
+ const char *path, *str;
+ pam_item_t item;
+ int status;
+
+ if ((status = pam_authenticate(pamh, 0)) != PAM_SUCCESS) {
+ path = pam_get_missing_service_file_path(service);
+ switch (status) {
+ case PAM_USER_UNKNOWN:
+ str = "unknown user";
+ break;
+ default:
+ str = t_strconcat("pam_authenticate() failed: ",
+ pam_strerror(pamh, status), NULL);
+ break;
+ }
+ if (path != NULL) {
+ /* log this as error, since it probably is */
+ str = t_strdup_printf("%s (%s missing?)", str, path);
+ e_error(authdb_event(request), "%s", str);
+ } else if (status == PAM_AUTH_ERR) {
+ str = t_strconcat(str, " ("AUTH_LOG_MSG_PASSWORD_MISMATCH"?)", NULL);
+ if (request->set->debug_passwords) {
+ str = t_strconcat(str, " (given password: ",
+ request->mech_password,
+ ")", NULL);
+ }
+ e_info(authdb_event(request), "%s", str);
+ } else {
+ if (status == PAM_USER_UNKNOWN)
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ else {
+ e_info(authdb_event(request),
+ "%s", str);
+ }
+ }
+ return status;
+ }
+
+#ifdef HAVE_PAM_SETCRED
+ if (module->pam_setcred) {
+ if ((status = pam_setcred(pamh, PAM_ESTABLISH_CRED)) !=
+ PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_setcred() failed: %s",
+ pam_strerror(pamh, status));
+ return status;
+ }
+ }
+#endif
+
+ if ((status = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_acct_mgmt() failed: %s",
+ pam_strerror(pamh, status));
+ return status;
+ }
+
+ if (module->pam_session) {
+ if ((status = pam_open_session(pamh, 0)) != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_open_session() failed: %s",
+ pam_strerror(pamh, status));
+ return status;
+ }
+
+ if ((status = pam_close_session(pamh, 0)) != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_close_session() failed: %s",
+ pam_strerror(pamh, status));
+ return status;
+ }
+ }
+
+ status = pam_get_item(pamh, PAM_USER, &item);
+ if (status != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_get_item(PAM_USER) failed: %s",
+ pam_strerror(pamh, status));
+ return status;
+ }
+ auth_request_set_field(request, "user", item, NULL);
+ return PAM_SUCCESS;
+}
+
+static void set_pam_items(struct auth_request *request, pam_handle_t *pamh)
+{
+ const char *host;
+
+ /* These shouldn't fail, and we don't really care if they do. */
+ host = net_ip2addr(&request->fields.remote_ip);
+ if (host[0] != '\0')
+ (void)pam_set_item(pamh, PAM_RHOST, host);
+ (void)pam_set_item(pamh, PAM_RUSER, request->fields.user);
+ /* TTY is needed by eg. pam_access module */
+ (void)pam_set_item(pamh, PAM_TTY, "dovecot");
+}
+
+static enum passdb_result
+pam_verify_plain_call(struct auth_request *request, const char *service,
+ const char *password)
+{
+ pam_handle_t *pamh;
+ struct pam_conv_context ctx;
+ struct pam_conv conv;
+ enum passdb_result result;
+ int status, status2;
+
+ conv.conv = pam_userpass_conv;
+ conv.appdata_ptr = &ctx;
+
+ i_zero(&ctx);
+ ctx.request = request;
+ ctx.pass = password;
+
+ status = pam_start(service, request->fields.user, &conv, &pamh);
+ if (status != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_start() failed: %s",
+ pam_strerror(pamh, status));
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ set_pam_items(request, pamh);
+ status = try_pam_auth(request, pamh, service);
+ if ((status2 = pam_end(pamh, status)) != PAM_SUCCESS) {
+ e_error(authdb_event(request),
+ "pam_end() failed: %s",
+ pam_strerror(pamh, status2));
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ switch (status) {
+ case PAM_SUCCESS:
+ result = PASSDB_RESULT_OK;
+ break;
+ case PAM_USER_UNKNOWN:
+ result = PASSDB_RESULT_USER_UNKNOWN;
+ break;
+ case PAM_NEW_AUTHTOK_REQD:
+ case PAM_ACCT_EXPIRED:
+ result = PASSDB_RESULT_PASS_EXPIRED;
+ break;
+ default:
+ result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ break;
+ }
+
+ if (result != PASSDB_RESULT_OK && ctx.failure_msg != NULL) {
+ auth_request_set_field(request, "reason",
+ ctx.failure_msg, NULL);
+ }
+ return result;
+}
+
+static void
+pam_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct pam_passdb_module *module = (struct pam_passdb_module *)_module;
+ enum passdb_result result;
+ const char *service, *error;
+
+ if (module->requests_left > 0) {
+ if (--module->requests_left == 0)
+ worker_restart_request = TRUE;
+ }
+
+ if (t_auth_request_var_expand(module->service_name, request, NULL,
+ &service, &error) <= 0) {
+ e_debug(authdb_event(request),
+ "Failed to expand service %s: %s",
+ module->service_name, error);
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+
+ e_debug(authdb_event(request),
+ "lookup service=%s", service);
+
+ result = pam_verify_plain_call(request, service, password);
+ callback(result, request);
+}
+
+static struct passdb_module *
+pam_preinit(pool_t pool, const char *args)
+{
+ struct pam_passdb_module *module;
+ const char *const *t_args;
+ int i;
+
+ module = p_new(pool, struct pam_passdb_module, 1);
+ module->service_name = "dovecot";
+ /* we're caching the password by using directly the plaintext password
+ given by the auth mechanism */
+ module->module.default_pass_scheme = "PLAIN";
+ module->module.blocking = TRUE;
+ module->requests_left = PASSDB_PAM_DEFAULT_MAX_REQUESTS;
+
+ t_args = t_strsplit_spaces(args, " ");
+ for(i = 0; t_args[i] != NULL; i++) {
+ /* -session for backwards compatibility */
+ if (strcmp(t_args[i], "-session") == 0 ||
+ strcmp(t_args[i], "session=yes") == 0)
+ module->pam_session = TRUE;
+ else if (strcmp(t_args[i], "setcred=yes") == 0)
+ module->pam_setcred = TRUE;
+ else if (str_begins(t_args[i], "cache_key=")) {
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool, t_args[i] + 10);
+ } else if (strcmp(t_args[i], "blocking=yes") == 0) {
+ /* ignore, for backwards compatibility */
+ } else if (strcmp(t_args[i], "failure_show_msg=yes") == 0) {
+ module->failure_show_msg = TRUE;
+ } else if (strcmp(t_args[i], "*") == 0) {
+ /* for backwards compatibility */
+ module->service_name = "%Ls";
+ } else if (str_begins(t_args[i], "max_requests=")) {
+ if (str_to_uint(t_args[i] + 13,
+ &module->requests_left) < 0) {
+ i_error("pam: Invalid requests_left value: %s",
+ t_args[i] + 13);
+ }
+ } else if (t_args[i+1] == NULL) {
+ module->service_name = p_strdup(pool, t_args[i]);
+ } else {
+ i_fatal("pam: Unknown setting: %s", t_args[i]);
+ }
+ }
+ return &module->module;
+}
+
+struct passdb_module_interface passdb_pam = {
+ "pam",
+
+ pam_preinit,
+ NULL,
+ NULL,
+
+ pam_verify_plain,
+ NULL,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_pam = {
+ .name = "pam"
+};
+#endif
diff --git a/src/auth/passdb-passwd-file.c b/src/auth/passdb-passwd-file.c
new file mode 100644
index 0000000..1f45fab
--- /dev/null
+++ b/src/auth/passdb-passwd-file.c
@@ -0,0 +1,210 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_PASSWD_FILE
+
+#include "str.h"
+#include "auth-cache.h"
+#include "password-scheme.h"
+#include "db-passwd-file.h"
+
+struct passwd_file_passdb_module {
+ struct passdb_module module;
+
+ struct db_passwd_file *pwf;
+ const char *username_format;
+};
+
+static int
+passwd_file_add_extra_fields(struct auth_request *request, char *const *fields)
+{
+ string_t *str = t_str_new(512);
+ const struct var_expand_table *table;
+ const char *key, *value, *error;
+ unsigned int i;
+
+ table = auth_request_get_var_expand_table(request, NULL);
+
+ for (i = 0; fields[i] != NULL; i++) {
+ value = strchr(fields[i], '=');
+ if (value != NULL) {
+ key = t_strdup_until(fields[i], value);
+ str_truncate(str, 0);
+ if (auth_request_var_expand_with_table(str, value + 1,
+ request, table, NULL, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand extra field %s: %s",
+ fields[i], error);
+ return -1;
+ }
+ value = str_c(str);
+ } else {
+ key = fields[i];
+ value = "";
+ }
+ auth_request_set_field(request, key, value, NULL);
+ }
+ return 0;
+}
+
+static int passwd_file_save_results(struct auth_request *request,
+ const struct passwd_user *pu,
+ const char **crypted_pass_r,
+ const char **scheme_r)
+{
+ *crypted_pass_r = pu->password != NULL ? pu->password : "";
+ *scheme_r = password_get_scheme(crypted_pass_r);
+ if (*scheme_r == NULL)
+ *scheme_r = request->passdb->passdb->default_pass_scheme;
+
+ /* save the password so cache can use it */
+ auth_request_set_field(request, "password",
+ *crypted_pass_r, *scheme_r);
+
+ if (pu->extra_fields != NULL) {
+ if (passwd_file_add_extra_fields(request, pu->extra_fields) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void
+passwd_file_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct passwd_file_passdb_module *module =
+ (struct passwd_file_passdb_module *)_module;
+ struct passwd_user *pu;
+ const char *scheme, *crypted_pass;
+ enum passdb_result result;
+ int ret;
+
+ ret = db_passwd_file_lookup(module->pwf, request,
+ module->username_format, &pu);
+ if (ret <= 0) {
+ callback(ret < 0 ? PASSDB_RESULT_INTERNAL_FAILURE :
+ PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ if (passwd_file_save_results(request, pu, &crypted_pass, &scheme) < 0) {
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+
+ result = auth_request_password_verify(request, password, crypted_pass,
+ scheme, AUTH_SUBSYS_DB);
+
+ callback(result, request);
+}
+
+static void
+passwd_file_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passdb_module *_module = request->passdb->passdb;
+ struct passwd_file_passdb_module *module =
+ (struct passwd_file_passdb_module *)_module;
+ struct passwd_user *pu;
+ const char *crypted_pass, *scheme;
+ int ret;
+
+ ret = db_passwd_file_lookup(module->pwf, request,
+ module->username_format, &pu);
+ if (ret <= 0) {
+ callback(ret < 0 ? PASSDB_RESULT_INTERNAL_FAILURE :
+ PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request);
+ return;
+ }
+
+ if (passwd_file_save_results(request, pu, &crypted_pass, &scheme) < 0) {
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request);
+ return;
+ }
+
+ passdb_handle_credentials(PASSDB_RESULT_OK, crypted_pass, scheme,
+ callback, request);
+}
+
+static struct passdb_module *
+passwd_file_preinit(pool_t pool, const char *args)
+{
+ struct passwd_file_passdb_module *module;
+ const char *scheme = PASSWD_FILE_DEFAULT_SCHEME;
+ const char *format = PASSWD_FILE_DEFAULT_USERNAME_FORMAT;
+ const char *key, *value;
+
+ while (*args != '\0') {
+ if (*args == '/')
+ break;
+
+ key = args;
+ value = strchr(key, '=');
+ if (value == NULL) {
+ value = "";
+ args = strchr(key, ' ');
+ } else {
+ key = t_strdup_until(key, value);
+ args = strchr(++value, ' ');
+ if (args != NULL)
+ value = t_strdup_until(value, args);
+ }
+ if (args == NULL)
+ args = "";
+ else
+ args++;
+
+ if (strcmp(key, "scheme") == 0)
+ scheme = p_strdup(pool, value);
+ else if (strcmp(key, "username_format") == 0)
+ format = p_strdup(pool, value);
+ else
+ i_fatal("passdb passwd-file: Unknown setting: %s", key);
+ }
+
+ if (*args == '\0')
+ i_fatal("passdb passwd-file: Missing args");
+
+ module = p_new(pool, struct passwd_file_passdb_module, 1);
+ module->pwf = db_passwd_file_init(args, FALSE,
+ global_auth_settings->debug);
+ module->username_format = format;
+ module->module.default_pass_scheme = scheme;
+ return &module->module;
+}
+
+static void passwd_file_init(struct passdb_module *_module)
+{
+ struct passwd_file_passdb_module *module =
+ (struct passwd_file_passdb_module *)_module;
+
+ db_passwd_file_parse(module->pwf);
+}
+
+static void passwd_file_deinit(struct passdb_module *_module)
+{
+ struct passwd_file_passdb_module *module =
+ (struct passwd_file_passdb_module *)_module;
+
+ db_passwd_file_unref(&module->pwf);
+}
+
+struct passdb_module_interface passdb_passwd_file = {
+ "passwd-file",
+
+ passwd_file_preinit,
+ passwd_file_init,
+ passwd_file_deinit,
+
+ passwd_file_verify_plain,
+ passwd_file_lookup_credentials,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_passwd_file = {
+ .name = "passwd-file"
+};
+#endif
diff --git a/src/auth/passdb-passwd.c b/src/auth/passdb-passwd.c
new file mode 100644
index 0000000..89c54eb
--- /dev/null
+++ b/src/auth/passdb-passwd.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_PASSWD
+
+#include "safe-memset.h"
+#include "ipwd.h"
+
+#define PASSWD_CACHE_KEY "%u"
+#define PASSWD_PASS_SCHEME "CRYPT"
+
+static enum passdb_result
+passwd_lookup(struct auth_request *request, struct passwd *pw_r)
+{
+ e_debug(authdb_event(request), "lookup");
+
+ switch (i_getpwnam(request->fields.user, pw_r)) {
+ case -1:
+ e_error(authdb_event(request),
+ "getpwnam() failed: %m");
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ case 0:
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ return PASSDB_RESULT_USER_UNKNOWN;
+ }
+
+ if (!IS_VALID_PASSWD(pw_r->pw_passwd)) {
+ e_info(authdb_event(request),
+ "invalid password field '%s'", pw_r->pw_passwd);
+ return PASSDB_RESULT_USER_DISABLED;
+ }
+
+ /* save the password so cache can use it */
+ auth_request_set_field(request, "password", pw_r->pw_passwd,
+ PASSWD_PASS_SCHEME);
+ return PASSDB_RESULT_OK;
+}
+
+static void
+passwd_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct passwd pw;
+ enum passdb_result res;
+
+ res = passwd_lookup(request, &pw);
+ if (res != PASSDB_RESULT_OK) {
+ callback(res, request);
+ return;
+ }
+ /* check if the password is valid */
+ res = auth_request_password_verify(request, password, pw.pw_passwd,
+ PASSWD_PASS_SCHEME, AUTH_SUBSYS_DB);
+
+ /* clear the passwords from memory */
+ safe_memset(pw.pw_passwd, 0, strlen(pw.pw_passwd));
+
+ if (res != PASSDB_RESULT_OK) {
+ callback(res, request);
+ return;
+ }
+
+ /* make sure we're using the username exactly as it's in the database */
+ auth_request_set_field(request, "user", pw.pw_name, NULL);
+
+ callback(res, request);
+}
+
+static void
+passwd_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passwd pw;
+ enum passdb_result res;
+
+ res = passwd_lookup(request, &pw);
+ if (res != PASSDB_RESULT_OK) {
+ callback(res, NULL, 0, request);
+ return;
+ }
+ /* make sure we're using the username exactly as it's in the database */
+ auth_request_set_field(request, "user", pw.pw_name, NULL);
+ passdb_handle_credentials(PASSDB_RESULT_OK, pw.pw_passwd,
+ PASSWD_PASS_SCHEME, callback, request);
+}
+
+static struct passdb_module *
+passwd_preinit(pool_t pool, const char *args)
+{
+ struct passdb_module *module;
+
+ module = p_new(pool, struct passdb_module, 1);
+ module->blocking = TRUE;
+ if (strcmp(args, "blocking=no") == 0)
+ module->blocking = FALSE;
+ else if (*args != '\0')
+ i_fatal("passdb passwd: Unknown setting: %s", args);
+
+ module->default_cache_key = PASSWD_CACHE_KEY;
+ module->default_pass_scheme = PASSWD_PASS_SCHEME;
+ return module;
+}
+
+static void passwd_deinit(struct passdb_module *module ATTR_UNUSED)
+{
+ endpwent();
+}
+
+struct passdb_module_interface passdb_passwd = {
+ "passwd",
+
+ passwd_preinit,
+ NULL,
+ passwd_deinit,
+
+ passwd_verify_plain,
+ passwd_lookup_credentials,
+ NULL
+};
+
+#else
+struct passdb_module_interface passdb_passwd = {
+ .name = "passwd"
+};
+#endif
diff --git a/src/auth/passdb-shadow.c b/src/auth/passdb-shadow.c
new file mode 100644
index 0000000..e312b45
--- /dev/null
+++ b/src/auth/passdb-shadow.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_SHADOW
+
+#include "safe-memset.h"
+
+#include <shadow.h>
+
+#define SHADOW_CACHE_KEY "%u"
+#define SHADOW_PASS_SCHEME "CRYPT"
+
+static enum passdb_result
+shadow_lookup(struct auth_request *request, struct spwd **spw_r)
+{
+ e_debug(authdb_event(request), "lookup");
+
+ *spw_r = getspnam(request->fields.user);
+ if (*spw_r == NULL) {
+ auth_request_log_unknown_user(request, AUTH_SUBSYS_DB);
+ return PASSDB_RESULT_USER_UNKNOWN;
+ }
+
+ if (!IS_VALID_PASSWD((*spw_r)->sp_pwdp)) {
+ e_info(authdb_event(request),
+ "invalid password field");
+ return PASSDB_RESULT_USER_DISABLED;
+ }
+
+ /* save the password so cache can use it */
+ auth_request_set_field(request, "password", (*spw_r)->sp_pwdp,
+ SHADOW_PASS_SCHEME);
+ return PASSDB_RESULT_OK;
+}
+
+static void
+shadow_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct spwd *spw;
+ enum passdb_result res;
+ int ret;
+
+ res = shadow_lookup(request, &spw);
+ if (res != PASSDB_RESULT_OK) {
+ callback(res, request);
+ return;
+ }
+
+ /* check if the password is valid */
+ ret = auth_request_password_verify(request, password, spw->sp_pwdp,
+ SHADOW_PASS_SCHEME, AUTH_SUBSYS_DB);
+
+ /* clear the passwords from memory */
+ safe_memset(spw->sp_pwdp, 0, strlen(spw->sp_pwdp));
+
+ if (ret <= 0) {
+ callback(PASSDB_RESULT_PASSWORD_MISMATCH, request);
+ return;
+ }
+
+ /* make sure we're using the username exactly as it's in the database */
+ auth_request_set_field(request, "user", spw->sp_namp, NULL);
+
+ callback(PASSDB_RESULT_OK, request);
+}
+
+static void
+shadow_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct spwd *spw;
+ enum passdb_result res;
+
+ res = shadow_lookup(request, &spw);
+ if (res != PASSDB_RESULT_OK) {
+ callback(res, NULL, 0, request);
+ return;
+ }
+ /* make sure we're using the username exactly as it's in the database */
+ auth_request_set_field(request, "user", spw->sp_namp, NULL);
+ passdb_handle_credentials(PASSDB_RESULT_OK, spw->sp_pwdp,
+ SHADOW_PASS_SCHEME, callback, request);
+}
+
+static struct passdb_module *
+shadow_preinit(pool_t pool, const char *args)
+{
+ struct passdb_module *module;
+
+ module = p_new(pool, struct passdb_module, 1);
+ module->blocking = TRUE;
+ if (strcmp(args, "blocking=no") == 0)
+ module->blocking = FALSE;
+ else if (*args != '\0')
+ i_fatal("passdb shadow: Unknown setting: %s", args);
+
+ module->default_cache_key = SHADOW_CACHE_KEY;
+ module->default_pass_scheme = SHADOW_PASS_SCHEME;
+ return module;
+}
+
+static void shadow_deinit(struct passdb_module *module ATTR_UNUSED)
+{
+ endspent();
+}
+
+struct passdb_module_interface passdb_shadow = {
+ "shadow",
+
+ shadow_preinit,
+ NULL,
+ shadow_deinit,
+
+ shadow_verify_plain,
+ shadow_lookup_credentials,
+ NULL
+};
+#else
+struct passdb_module_interface passdb_shadow = {
+ .name = "shadow"
+};
+#endif
diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c
new file mode 100644
index 0000000..0b993fb
--- /dev/null
+++ b/src/auth/passdb-sql.c
@@ -0,0 +1,310 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+
+#ifdef PASSDB_SQL
+
+#include "safe-memset.h"
+#include "password-scheme.h"
+#include "auth-cache.h"
+#include "db-sql.h"
+
+#include <string.h>
+
+struct sql_passdb_module {
+ struct passdb_module module;
+
+ struct db_sql_connection *conn;
+};
+
+struct passdb_sql_request {
+ struct auth_request *auth_request;
+ union {
+ verify_plain_callback_t *verify_plain;
+ lookup_credentials_callback_t *lookup_credentials;
+ set_credentials_callback_t *set_credentials;
+ } callback;
+};
+
+static void sql_query_save_results(struct sql_result *result,
+ struct passdb_sql_request *sql_request)
+{
+ struct auth_request *auth_request = sql_request->auth_request;
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct sql_passdb_module *module = (struct sql_passdb_module *)_module;
+ unsigned int i, fields_count;
+ const char *name, *value;
+
+ fields_count = sql_result_get_fields_count(result);
+ for (i = 0; i < fields_count; i++) {
+ name = sql_result_get_field_name(result, i);
+ value = sql_result_get_field_value(result, i);
+
+ if (*name == '\0')
+ ;
+ else if (value == NULL)
+ auth_request_set_null_field(auth_request, name);
+ else {
+ auth_request_set_field(auth_request, name, value,
+ module->conn->set.default_pass_scheme);
+ }
+ }
+}
+
+static void sql_query_callback(struct sql_result *result,
+ struct passdb_sql_request *sql_request)
+{
+ struct auth_request *auth_request = sql_request->auth_request;
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct sql_passdb_module *module = (struct sql_passdb_module *)_module;
+ enum passdb_result passdb_result;
+ const char *password, *scheme;
+ int ret;
+
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ password = NULL;
+
+ ret = sql_result_next_row(result);
+ if (ret >= 0)
+ db_sql_success(module->conn);
+ if (ret < 0) {
+ if (!module->conn->default_password_query) {
+ e_error(authdb_event(auth_request),
+ "Password query failed: %s",
+ sql_result_get_error(result));
+ } else {
+ e_error(authdb_event(auth_request),
+ "Password query failed: %s "
+ "(using built-in default password_query: %s)",
+ sql_result_get_error(result),
+ module->conn->set.password_query);
+ }
+ } else if (ret == 0) {
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ passdb_result = PASSDB_RESULT_USER_UNKNOWN;
+ } else {
+ sql_query_save_results(result, sql_request);
+
+ /* Note that we really want to check if the password field is
+ found. Just checking if password is set isn't enough,
+ because with proxies we might want to return NULL as
+ password. */
+ if (sql_result_find_field(result, "password") < 0 &&
+ sql_result_find_field(result, "password_noscheme") < 0) {
+ e_error(authdb_event(auth_request),
+ "Password query must return a field named "
+ "'password'");
+ } else if (sql_result_next_row(result) > 0) {
+ e_error(authdb_event(auth_request),
+ "Password query returned multiple matches");
+ } else if (auth_request->passdb_password == NULL &&
+ !auth_fields_exists(auth_request->fields.extra_fields,
+ "nopassword")) {
+ passdb_result = auth_request_password_missing(auth_request);
+ } else {
+ /* passdb_password may change on the way,
+ so we'll need to strdup. */
+ password = t_strdup(auth_request->passdb_password);
+ passdb_result = PASSDB_RESULT_OK;
+ }
+ }
+
+ scheme = password_get_scheme(&password);
+ /* auth_request_set_field() sets scheme */
+ i_assert(password == NULL || scheme != NULL);
+
+ if (auth_request->wanted_credentials_scheme != NULL) {
+ passdb_handle_credentials(passdb_result, password, scheme,
+ sql_request->callback.lookup_credentials,
+ auth_request);
+ auth_request_unref(&auth_request);
+ return;
+ }
+
+ /* verify plain */
+ if (password == NULL) {
+ sql_request->callback.verify_plain(passdb_result, auth_request);
+ auth_request_unref(&auth_request);
+ return;
+ }
+
+ passdb_result = auth_request_password_verify(auth_request,
+ auth_request->mech_password,
+ password, scheme, AUTH_SUBSYS_DB);
+
+ sql_request->callback.verify_plain(passdb_result, auth_request);
+ auth_request_unref(&auth_request);
+}
+
+static const char *
+passdb_sql_escape(const char *str, const struct auth_request *auth_request)
+{
+ struct passdb_module *_module = auth_request->passdb->passdb;
+ struct sql_passdb_module *module = (struct sql_passdb_module *)_module;
+
+ return sql_escape_string(module->conn->db, str);
+}
+
+static void sql_lookup_pass(struct passdb_sql_request *sql_request)
+{
+ struct passdb_module *_module =
+ sql_request->auth_request->passdb->passdb;
+ struct sql_passdb_module *module = (struct sql_passdb_module *)_module;
+ const char *query, *error;
+
+ if (t_auth_request_var_expand(module->conn->set.password_query,
+ sql_request->auth_request,
+ passdb_sql_escape, &query, &error) <= 0) {
+ e_debug(authdb_event(sql_request->auth_request),
+ "Failed to expand password_query=%s: %s",
+ module->conn->set.password_query, error);
+ sql_request->callback.verify_plain(PASSDB_RESULT_INTERNAL_FAILURE,
+ sql_request->auth_request);
+ return;
+ }
+
+ e_debug(authdb_event(sql_request->auth_request),
+ "query: %s", query);
+
+ auth_request_ref(sql_request->auth_request);
+ sql_query(module->conn->db, query,
+ sql_query_callback, sql_request);
+}
+
+static void sql_verify_plain(struct auth_request *request,
+ const char *password ATTR_UNUSED,
+ verify_plain_callback_t *callback)
+{
+ struct passdb_sql_request *sql_request;
+
+ sql_request = p_new(request->pool, struct passdb_sql_request, 1);
+ sql_request->auth_request = request;
+ sql_request->callback.verify_plain = callback;
+
+ sql_lookup_pass(sql_request);
+}
+
+static void sql_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct passdb_sql_request *sql_request;
+
+ sql_request = p_new(request->pool, struct passdb_sql_request, 1);
+ sql_request->auth_request = request;
+ sql_request->callback.lookup_credentials = callback;
+
+ sql_lookup_pass(sql_request);
+}
+
+static void sql_set_credentials_callback(const struct sql_commit_result *sql_result,
+ struct passdb_sql_request *sql_request)
+{
+ struct passdb_module *_module =
+ sql_request->auth_request->passdb->passdb;
+ struct sql_passdb_module *module = (struct sql_passdb_module *)_module;
+
+ if (sql_result->error != NULL) {
+ if (!module->conn->default_update_query) {
+ auth_request_log_error(sql_request->auth_request,
+ AUTH_SUBSYS_DB,
+ "Set credentials query failed: %s", sql_result->error);
+ } else {
+ auth_request_log_error(sql_request->auth_request,
+ AUTH_SUBSYS_DB,
+ "Set credentials query failed: %s"
+ "(using built-in default update_query: %s)",
+ sql_result->error, module->conn->set.update_query);
+ }
+ }
+
+ sql_request->callback.
+ set_credentials(sql_result->error == NULL, sql_request->auth_request);
+ i_free(sql_request);
+}
+
+static void sql_set_credentials(struct auth_request *request,
+ const char *new_credentials,
+ set_credentials_callback_t *callback)
+{
+ struct sql_passdb_module *module =
+ (struct sql_passdb_module *) request->passdb->passdb;
+ struct sql_transaction_context *transaction;
+ struct passdb_sql_request *sql_request;
+ const char *query, *error;
+
+ request->mech_password = p_strdup(request->pool, new_credentials);
+
+ if (t_auth_request_var_expand(module->conn->set.update_query,
+ request, passdb_sql_escape,
+ &query, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand update_query=%s: %s",
+ module->conn->set.update_query, error);
+ callback(FALSE, request);
+ return;
+ }
+
+ sql_request = i_new(struct passdb_sql_request, 1);
+ sql_request->auth_request = request;
+ sql_request->callback.set_credentials = callback;
+
+ transaction = sql_transaction_begin(module->conn->db);
+ sql_update(transaction, query);
+ sql_transaction_commit(&transaction,
+ sql_set_credentials_callback, sql_request);
+}
+
+static struct passdb_module *
+passdb_sql_preinit(pool_t pool, const char *args)
+{
+ struct sql_passdb_module *module;
+ struct db_sql_connection *conn;
+
+ module = p_new(pool, struct sql_passdb_module, 1);
+ module->conn = conn = db_sql_init(args, FALSE);
+
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool, conn->set.password_query);
+ module->module.default_pass_scheme = conn->set.default_pass_scheme;
+ return &module->module;
+}
+
+static void passdb_sql_init(struct passdb_module *_module)
+{
+ struct sql_passdb_module *module =
+ (struct sql_passdb_module *)_module;
+ enum sql_db_flags flags;
+
+ flags = sql_get_flags(module->conn->db);
+ module->module.blocking = (flags & SQL_DB_FLAG_BLOCKING) != 0;
+
+ if (!module->module.blocking || worker)
+ db_sql_connect(module->conn);
+ db_sql_check_userdb_warning(module->conn);
+}
+
+static void passdb_sql_deinit(struct passdb_module *_module)
+{
+ struct sql_passdb_module *module =
+ (struct sql_passdb_module *)_module;
+
+ db_sql_unref(&module->conn);
+}
+
+struct passdb_module_interface passdb_sql = {
+ "sql",
+
+ passdb_sql_preinit,
+ passdb_sql_init,
+ passdb_sql_deinit,
+
+ sql_verify_plain,
+ sql_lookup_credentials,
+ sql_set_credentials
+};
+#else
+struct passdb_module_interface passdb_sql = {
+ .name = "sql"
+};
+#endif
diff --git a/src/auth/passdb-static.c b/src/auth/passdb-static.c
new file mode 100644
index 0000000..2b99fab
--- /dev/null
+++ b/src/auth/passdb-static.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "passdb.h"
+#include "passdb-template.h"
+#include "password-scheme.h"
+
+struct static_passdb_module {
+ struct passdb_module module;
+ struct passdb_template *tmpl;
+ const char *static_password_tmpl;
+};
+
+static enum passdb_result
+static_save_fields(struct auth_request *request, const char **password_r,
+ const char **scheme_r)
+{
+ struct static_passdb_module *module =
+ (struct static_passdb_module *)request->passdb->passdb;
+ const char *error;
+
+ *password_r = NULL;
+ *scheme_r = NULL;
+
+ e_debug(authdb_event(request), "lookup");
+ if (passdb_template_export(module->tmpl, request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand template: %s", error);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+
+ if (module->static_password_tmpl != NULL) {
+ if (t_auth_request_var_expand(module->static_password_tmpl,
+ request, NULL, password_r, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand password=%s: %s",
+ module->static_password_tmpl, error);
+ return PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ } else if (auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ *password_r = "";
+ } else {
+ return auth_request_password_missing(request);
+ }
+
+ *scheme_r = password_get_scheme(password_r);
+
+ if (*scheme_r == NULL)
+ *scheme_r = STATIC_PASS_SCHEME;
+
+ auth_request_set_field(request, "password",
+ *password_r, *scheme_r);
+
+ return PASSDB_RESULT_OK;
+}
+
+static void
+static_verify_plain(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback)
+{
+ enum passdb_result result;
+ const char *static_password;
+ const char *static_scheme;
+
+ result = static_save_fields(request, &static_password, &static_scheme);
+ if (result != PASSDB_RESULT_OK) {
+ callback(result, request);
+ return;
+ }
+
+ result = auth_request_password_verify(request, password, static_password,
+ static_scheme, AUTH_SUBSYS_DB);
+ callback(result, request);
+}
+
+static void
+static_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ enum passdb_result result;
+ const char *static_password;
+ const char *static_scheme;
+
+ result = static_save_fields(request, &static_password, &static_scheme);
+ passdb_handle_credentials(result, static_password,
+ static_scheme, callback, request);
+}
+
+static struct passdb_module *
+static_preinit(pool_t pool, const char *args)
+{
+ struct static_passdb_module *module;
+ const char *value;
+
+ module = p_new(pool, struct static_passdb_module, 1);
+ module->tmpl = passdb_template_build(pool, args);
+
+ if (passdb_template_remove(module->tmpl, "password", &value))
+ module->static_password_tmpl = value;
+ return &module->module;
+}
+
+struct passdb_module_interface passdb_static = {
+ "static",
+
+ static_preinit,
+ NULL,
+ NULL,
+
+ static_verify_plain,
+ static_lookup_credentials,
+ NULL
+};
diff --git a/src/auth/passdb-template.c b/src/auth/passdb-template.c
new file mode 100644
index 0000000..3bb7f54
--- /dev/null
+++ b/src/auth/passdb-template.c
@@ -0,0 +1,102 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "passdb.h"
+#include "passdb-template.h"
+
+struct passdb_template {
+ ARRAY(const char *) args;
+};
+
+struct passdb_template *passdb_template_build(pool_t pool, const char *args)
+{
+ struct passdb_template *tmpl;
+ const char *const *tmp, *key, *value;
+
+ tmpl = p_new(pool, struct passdb_template, 1);
+
+ tmp = t_strsplit_spaces(args, " ");
+ p_array_init(&tmpl->args, pool, str_array_length(tmp));
+
+ for (; *tmp != NULL; tmp++) {
+ value = strchr(*tmp, '=');
+ if (value == NULL)
+ key = *tmp;
+ else
+ key = t_strdup_until(*tmp, value++);
+
+ if (*key == '\0')
+ i_fatal("Invalid passdb template %s - key must not be empty",
+ args);
+
+ key = p_strdup(pool, key);
+ value = p_strdup(pool, value);
+ array_push_back(&tmpl->args, &key);
+ array_push_back(&tmpl->args, &value);
+ }
+ return tmpl;
+}
+
+int passdb_template_export(struct passdb_template *tmpl,
+ struct auth_request *auth_request,
+ const char **error_r)
+{
+ const struct var_expand_table *table;
+ string_t *str;
+ const char *const *args, *value;
+ unsigned int i, count;
+
+ if (passdb_template_is_empty(tmpl))
+ return 0;
+
+ str = t_str_new(256);
+ table = auth_request_get_var_expand_table(auth_request, NULL);
+
+ args = array_get(&tmpl->args, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (args[i+1] == NULL)
+ value = "";
+ else {
+ str_truncate(str, 0);
+ if (auth_request_var_expand_with_table(str, args[i+1],
+ auth_request, table, NULL, error_r) <= 0)
+ return -1;
+ value = str_c(str);
+ }
+ auth_request_set_field(auth_request, args[i], value,
+ STATIC_PASS_SCHEME);
+ }
+ return 0;
+}
+
+bool passdb_template_remove(struct passdb_template *tmpl,
+ const char *key, const char **value_r)
+{
+ const char *const *args;
+ unsigned int i, count;
+
+ args = array_get(&tmpl->args, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(args[i], key) == 0) {
+ *value_r = args[i+1];
+ array_delete(&tmpl->args, i, 2);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool passdb_template_is_empty(struct passdb_template *tmpl)
+{
+ return array_count(&tmpl->args) == 0;
+}
+
+const char *const *passdb_template_get_args(struct passdb_template *tmpl, unsigned int *count_r)
+{
+ return array_get(&tmpl->args, count_r);
+}
+
diff --git a/src/auth/passdb-template.h b/src/auth/passdb-template.h
new file mode 100644
index 0000000..b681c80
--- /dev/null
+++ b/src/auth/passdb-template.h
@@ -0,0 +1,16 @@
+#ifndef PASSDB_TEMPLATE_H
+#define PASSDB_TEMPLATE_H
+
+#define STATIC_PASS_SCHEME "PLAIN"
+
+struct passdb_template *passdb_template_build(pool_t pool, const char *args);
+int passdb_template_export(struct passdb_template *tmpl,
+ struct auth_request *auth_request,
+ const char **error_r);
+bool passdb_template_remove(struct passdb_template *tmpl,
+ const char *key, const char **value_r);
+bool passdb_template_is_empty(struct passdb_template *tmpl);
+
+const char *const *passdb_template_get_args(struct passdb_template *tmpl, unsigned int *count_r);
+
+#endif
diff --git a/src/auth/passdb.c b/src/auth/passdb.c
new file mode 100644
index 0000000..9bc2b87
--- /dev/null
+++ b/src/auth/passdb.c
@@ -0,0 +1,351 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "password-scheme.h"
+#include "auth-worker-server.h"
+#include "passdb.h"
+
+static ARRAY(struct passdb_module_interface *) passdb_interfaces;
+static ARRAY(struct passdb_module *) passdb_modules;
+
+static const struct passdb_module_interface passdb_iface_deinit = {
+ .name = "deinit"
+};
+
+static struct passdb_module_interface *passdb_interface_find(const char *name)
+{
+ struct passdb_module_interface *iface;
+
+ array_foreach_elem(&passdb_interfaces, iface) {
+ if (strcmp(iface->name, name) == 0)
+ return iface;
+ }
+ return NULL;
+}
+
+void passdb_register_module(struct passdb_module_interface *iface)
+{
+ struct passdb_module_interface *old_iface;
+
+ old_iface = passdb_interface_find(iface->name);
+ if (old_iface != NULL && old_iface->verify_plain == NULL) {
+ /* replacing a "support not compiled in" passdb */
+ passdb_unregister_module(old_iface);
+ } else if (old_iface != NULL) {
+ i_panic("passdb_register_module(%s): Already registered",
+ iface->name);
+ }
+ array_push_back(&passdb_interfaces, &iface);
+}
+
+void passdb_unregister_module(struct passdb_module_interface *iface)
+{
+ struct passdb_module_interface *const *ifaces;
+ unsigned int idx;
+
+ array_foreach(&passdb_interfaces, ifaces) {
+ if (*ifaces == iface) {
+ idx = array_foreach_idx(&passdb_interfaces, ifaces);
+ array_delete(&passdb_interfaces, idx, 1);
+ return;
+ }
+ }
+ i_panic("passdb_unregister_module(%s): Not registered", iface->name);
+}
+
+bool passdb_get_credentials(struct auth_request *auth_request,
+ const char *input, const char *input_scheme,
+ const unsigned char **credentials_r, size_t *size_r)
+{
+ const char *wanted_scheme = auth_request->wanted_credentials_scheme;
+ const char *plaintext, *error;
+ int ret;
+ struct password_generate_params pwd_gen_params;
+
+ if (auth_request->prefer_plain_credentials &&
+ password_scheme_is_alias(input_scheme, "PLAIN")) {
+ /* we've a plaintext scheme and we prefer to get it instead
+ of converting it to the fallback scheme */
+ wanted_scheme = "";
+ }
+
+ ret = password_decode(input, input_scheme,
+ credentials_r, size_r, &error);
+ if (ret <= 0) {
+ if (ret < 0) {
+ e_error(authdb_event(auth_request),
+ "Password data is not valid for scheme %s: %s",
+ input_scheme, error);
+ } else {
+ e_error(authdb_event(auth_request),
+ "Unknown scheme %s", input_scheme);
+ }
+ return FALSE;
+ }
+
+ if (*wanted_scheme == '\0') {
+ /* anything goes. change the wanted_credentials_scheme to what
+ we actually got, so blocking passdbs work. */
+ auth_request->wanted_credentials_scheme =
+ p_strdup(auth_request->pool, t_strcut(input_scheme, '.'));
+ return TRUE;
+ }
+
+ if (!password_scheme_is_alias(input_scheme, wanted_scheme)) {
+ if (!password_scheme_is_alias(input_scheme, "PLAIN")) {
+ const char *error = t_strdup_printf(
+ "Requested %s scheme, but we have only %s",
+ wanted_scheme, input_scheme);
+ if (auth_request->set->debug_passwords) {
+ error = t_strdup_printf("%s (input: %s)",
+ error, input);
+ }
+ e_info(authdb_event(auth_request),
+ "%s", error);
+ return FALSE;
+ }
+
+ /* we can generate anything out of plaintext passwords */
+ plaintext = t_strndup(*credentials_r, *size_r);
+ i_zero(&pwd_gen_params);
+ pwd_gen_params.user = auth_request->fields.original_username;
+ if (!auth_request->domain_is_realm &&
+ strchr(pwd_gen_params.user, '@') != NULL) {
+ /* domain must not be used as realm. add the @realm. */
+ pwd_gen_params.user = t_strconcat(pwd_gen_params.user, "@",
+ auth_request->fields.realm, NULL);
+ }
+ if (auth_request->set->debug_passwords) {
+ e_debug(authdb_event(auth_request),
+ "Generating %s from user '%s', password '%s'",
+ wanted_scheme, pwd_gen_params.user, plaintext);
+ }
+ if (!password_generate(plaintext, &pwd_gen_params,
+ wanted_scheme, credentials_r, size_r)) {
+ e_error(authdb_event(auth_request),
+ "Requested unknown scheme %s", wanted_scheme);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+void passdb_handle_credentials(enum passdb_result result,
+ const char *password, const char *scheme,
+ lookup_credentials_callback_t *callback,
+ struct auth_request *auth_request)
+{
+ const unsigned char *credentials = NULL;
+ size_t size = 0;
+
+ if (result != PASSDB_RESULT_OK) {
+ callback(result, NULL, 0, auth_request);
+ return;
+ } else if (auth_fields_exists(auth_request->fields.extra_fields,
+ "noauthenticate")) {
+ callback(PASSDB_RESULT_NEXT, NULL, 0, auth_request);
+ return;
+ }
+
+ if (password != NULL) {
+ if (!passdb_get_credentials(auth_request, password, scheme,
+ &credentials, &size))
+ result = PASSDB_RESULT_SCHEME_NOT_AVAILABLE;
+ } else if (*auth_request->wanted_credentials_scheme == '\0') {
+ /* We're doing a passdb lookup (not authenticating).
+ Pass through a NULL password without an error. */
+ } else if (auth_request->fields.delayed_credentials != NULL) {
+ /* We already have valid credentials from an earlier
+ passdb lookup. auth_request_lookup_credentials_finish()
+ will use them. */
+ } else {
+ e_info(authdb_event(auth_request),
+ "Requested %s scheme, but we have a NULL password",
+ auth_request->wanted_credentials_scheme);
+ result = PASSDB_RESULT_SCHEME_NOT_AVAILABLE;
+ }
+
+ callback(result, credentials, size, auth_request);
+}
+
+static struct passdb_module *
+passdb_find(const char *driver, const char *args, unsigned int *idx_r)
+{
+ struct passdb_module *const *passdbs;
+ unsigned int i, count;
+
+ passdbs = array_get(&passdb_modules, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(passdbs[i]->iface.name, driver) == 0 &&
+ strcmp(passdbs[i]->args, args) == 0) {
+ *idx_r = i;
+ return passdbs[i];
+ }
+ }
+ return NULL;
+}
+
+struct passdb_module *
+passdb_preinit(pool_t pool, const struct auth_passdb_settings *set)
+{
+ static unsigned int auth_passdb_id = 0;
+ struct passdb_module_interface *iface;
+ struct passdb_module *passdb;
+ unsigned int idx;
+
+ iface = passdb_interface_find(set->driver);
+ if (iface == NULL || iface->verify_plain == NULL) {
+ /* maybe it's a plugin. try to load it. */
+ auth_module_load(t_strconcat("authdb_", set->driver, NULL));
+ iface = passdb_interface_find(set->driver);
+ }
+ if (iface == NULL)
+ i_fatal("Unknown passdb driver '%s'", set->driver);
+ if (iface->verify_plain == NULL) {
+ i_fatal("Support not compiled in for passdb driver '%s'",
+ set->driver);
+ }
+ if (iface->preinit == NULL && iface->init == NULL &&
+ *set->args != '\0') {
+ i_fatal("passdb %s: No args are supported: %s",
+ set->driver, set->args);
+ }
+
+ passdb = passdb_find(set->driver, set->args, &idx);
+ if (passdb != NULL)
+ return passdb;
+
+ if (iface->preinit == NULL)
+ passdb = p_new(pool, struct passdb_module, 1);
+ else
+ passdb = iface->preinit(pool, set->args);
+ passdb->id = ++auth_passdb_id;
+ passdb->iface = *iface;
+ passdb->args = p_strdup(pool, set->args);
+ if (*set->mechanisms == '\0') {
+ passdb->mechanisms = NULL;
+ } else if (strcasecmp(set->mechanisms, "none") == 0) {
+ passdb->mechanisms = (const char *const[]){NULL};
+ } else {
+ passdb->mechanisms = (const char* const*)p_strsplit_spaces(pool, set->mechanisms, " ,");
+ }
+
+ if (*set->username_filter == '\0') {
+ passdb->username_filter = NULL;
+ } else {
+ passdb->username_filter = (const char* const*)p_strsplit_spaces(pool, set->username_filter, " ,");
+ }
+ array_push_back(&passdb_modules, &passdb);
+ return passdb;
+}
+
+void passdb_init(struct passdb_module *passdb)
+{
+ if (passdb->iface.init != NULL && passdb->init_refcount == 0)
+ passdb->iface.init(passdb);
+ passdb->init_refcount++;
+}
+
+void passdb_deinit(struct passdb_module *passdb)
+{
+ unsigned int idx;
+
+ i_assert(passdb->init_refcount > 0);
+
+ if (--passdb->init_refcount > 0)
+ return;
+
+ if (passdb_find(passdb->iface.name, passdb->args, &idx) == NULL)
+ i_unreached();
+ array_delete(&passdb_modules, idx, 1);
+
+ if (passdb->iface.deinit != NULL)
+ passdb->iface.deinit(passdb);
+
+ /* make sure passdb isn't accessed again */
+ passdb->iface = passdb_iface_deinit;
+}
+
+void passdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN])
+{
+ struct md5_context ctx;
+ struct passdb_module *const *passdbs;
+ unsigned int i, count;
+
+ md5_init(&ctx);
+ passdbs = array_get(&passdb_modules, &count);
+ for (i = 0; i < count; i++) {
+ md5_update(&ctx, &passdbs[i]->id, sizeof(passdbs[i]->id));
+ md5_update(&ctx, passdbs[i]->iface.name,
+ strlen(passdbs[i]->iface.name));
+ md5_update(&ctx, passdbs[i]->args, strlen(passdbs[i]->args));
+ }
+ md5_final(&ctx, md5);
+}
+
+const char *
+passdb_result_to_string(enum passdb_result result)
+{
+ switch (result) {
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ return "internal_failure";
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ return "scheme_not_available";
+ case PASSDB_RESULT_USER_UNKNOWN:
+ return "user_unknown";
+ case PASSDB_RESULT_USER_DISABLED:
+ return "user_disabled";
+ case PASSDB_RESULT_PASS_EXPIRED:
+ return "pass_expired";
+ case PASSDB_RESULT_NEXT:
+ return "next";
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ return "password_mismatch";
+ case PASSDB_RESULT_OK:
+ return "ok";
+ }
+ i_unreached();
+}
+
+extern struct passdb_module_interface passdb_passwd;
+extern struct passdb_module_interface passdb_bsdauth;
+extern struct passdb_module_interface passdb_dict;
+#ifdef HAVE_LUA
+extern struct passdb_module_interface passdb_lua;
+#endif
+extern struct passdb_module_interface passdb_shadow;
+extern struct passdb_module_interface passdb_passwd_file;
+extern struct passdb_module_interface passdb_pam;
+extern struct passdb_module_interface passdb_checkpassword;
+extern struct passdb_module_interface passdb_ldap;
+extern struct passdb_module_interface passdb_sql;
+extern struct passdb_module_interface passdb_static;
+extern struct passdb_module_interface passdb_oauth2;
+
+void passdbs_init(void)
+{
+ i_array_init(&passdb_interfaces, 16);
+ i_array_init(&passdb_modules, 16);
+ passdb_register_module(&passdb_passwd);
+ passdb_register_module(&passdb_bsdauth);
+ passdb_register_module(&passdb_dict);
+#ifdef HAVE_LUA
+ passdb_register_module(&passdb_lua);
+#endif
+ passdb_register_module(&passdb_passwd_file);
+ passdb_register_module(&passdb_pam);
+ passdb_register_module(&passdb_checkpassword);
+ passdb_register_module(&passdb_shadow);
+ passdb_register_module(&passdb_ldap);
+ passdb_register_module(&passdb_sql);
+ passdb_register_module(&passdb_static);
+ passdb_register_module(&passdb_oauth2);
+}
+
+void passdbs_deinit(void)
+{
+ array_free(&passdb_modules);
+ array_free(&passdb_interfaces);
+}
diff --git a/src/auth/passdb.h b/src/auth/passdb.h
new file mode 100644
index 0000000..f9b33ea
--- /dev/null
+++ b/src/auth/passdb.h
@@ -0,0 +1,123 @@
+#ifndef PASSDB_H
+#define PASSDB_H
+
+#include "md5.h"
+
+#define IS_VALID_PASSWD(pass) \
+ ((pass)[0] != '\0' && (pass)[0] != '*' && (pass)[0] != '!')
+
+struct auth_request;
+struct auth_passdb_settings;
+
+enum passdb_result {
+ PASSDB_RESULT_INTERNAL_FAILURE = -1,
+ PASSDB_RESULT_SCHEME_NOT_AVAILABLE = -2,
+
+ PASSDB_RESULT_USER_UNKNOWN = -3,
+ PASSDB_RESULT_USER_DISABLED = -4,
+ PASSDB_RESULT_PASS_EXPIRED = -5,
+ PASSDB_RESULT_NEXT = -6,
+
+ PASSDB_RESULT_PASSWORD_MISMATCH = 0,
+ PASSDB_RESULT_OK = 1
+};
+
+typedef void verify_plain_callback_t(enum passdb_result result,
+ struct auth_request *request);
+typedef void verify_plain_continue_callback_t(struct auth_request *request,
+ verify_plain_callback_t *callback);
+typedef void lookup_credentials_callback_t(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request);
+typedef void set_credentials_callback_t(bool success,
+ struct auth_request *request);
+
+struct passdb_module_interface {
+ const char *name;
+
+ struct passdb_module *(*preinit)(pool_t pool, const char *args);
+ void (*init)(struct passdb_module *module);
+ void (*deinit)(struct passdb_module *module);
+
+ /* Check if plaintext password matches */
+ void (*verify_plain)(struct auth_request *request, const char *password,
+ verify_plain_callback_t *callback);
+
+ /* Return authentication credentials, set in
+ auth_request->credentials. */
+ void (*lookup_credentials)(struct auth_request *request,
+ lookup_credentials_callback_t *callback);
+
+ /* Update credentials */
+ void (*set_credentials)(struct auth_request *request,
+ const char *new_credentials,
+ set_credentials_callback_t *callback);
+};
+
+struct passdb_module {
+ const char *args;
+ /* The default caching key for this module, or NULL if caching isn't
+ wanted. This is updated by settings in auth_passdb. */
+ const char *default_cache_key;
+ /* Default password scheme for this module.
+ If default_cache_key is set, must not be NULL. */
+ const char *default_pass_scheme;
+ /* Supported authentication mechanisms, NULL is all, [NULL] is none*/
+ const char *const *mechanisms;
+ /* Username filter, NULL is no filter */
+ const char *const *username_filter;
+
+ /* If blocking is set to TRUE, use child processes to access
+ this passdb. */
+ bool blocking;
+ /* id is used by blocking passdb to identify the passdb */
+ unsigned int id;
+
+ /* number of time init() has been called */
+ int init_refcount;
+
+ /* WARNING: avoid adding anything here that isn't based on args.
+ if you do, you need to change passdb.c:passdb_find() also to avoid
+ accidentally merging wrong passdbs. */
+
+ struct passdb_module_interface iface;
+};
+
+const char *passdb_result_to_string(enum passdb_result result);
+
+/* Try to get credentials in wanted scheme (request->credentials_scheme) from
+ given input. Returns FALSE if this wasn't possible (unknown scheme,
+ conversion not possible or invalid credentials).
+
+ If wanted scheme is "", the credentials are returned as-is without any
+ checks. This is useful mostly just to see if there exist any credentials
+ at all. */
+bool passdb_get_credentials(struct auth_request *auth_request,
+ const char *input, const char *input_scheme,
+ const unsigned char **credentials_r,
+ size_t *size_r);
+
+void passdb_handle_credentials(enum passdb_result result,
+ const char *password, const char *scheme,
+ lookup_credentials_callback_t *callback,
+ struct auth_request *auth_request);
+
+struct passdb_module *
+passdb_preinit(pool_t pool, const struct auth_passdb_settings *set);
+void passdb_init(struct passdb_module *passdb);
+void passdb_deinit(struct passdb_module *passdb);
+
+void passdb_register_module(struct passdb_module_interface *iface);
+void passdb_unregister_module(struct passdb_module_interface *iface);
+
+void passdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]);
+
+void passdbs_init(void);
+void passdbs_deinit(void);
+
+const char *passdb_oauth2_get_oidc_url(struct passdb_module *passdb);
+
+#include "auth-request.h"
+
+#endif
diff --git a/src/auth/password-scheme-crypt.c b/src/auth/password-scheme-crypt.c
new file mode 100644
index 0000000..34febad
--- /dev/null
+++ b/src/auth/password-scheme-crypt.c
@@ -0,0 +1,196 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mycrypt.h"
+#include "password-scheme.h"
+#include "crypt-blowfish.h"
+#include "randgen.h"
+
+/* Lengths and limits for some crypt() algorithms. */
+#define CRYPT_BLF_ROUNDS_DEFAULT 5
+#define CRYPT_BLF_ROUNDS_MIN 4
+#define CRYPT_BLF_ROUNDS_MAX 31
+#define CRYPT_BLF_SALT_LEN 16 /* raw salt */
+#define CRYPT_BLF_PREFIX_LEN (7+22+1) /* $2.$nn$ + salt */
+#define CRYPT_BLF_BUFFER_LEN 128
+#define CRYPT_BLF_PREFIX "$2y"
+#define CRYPT_SHA2_ROUNDS_DEFAULT 5000
+#define CRYPT_SHA2_ROUNDS_MIN 1000
+#define CRYPT_SHA2_ROUNDS_MAX 999999999
+#define CRYPT_SHA2_SALT_LEN 16
+
+static void
+crypt_generate_des(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+#define CRYPT_SALT_LEN 2
+ const char *password, *salt;
+
+ salt = password_generate_salt(CRYPT_SALT_LEN);
+ password = t_strdup(mycrypt(plaintext, salt));
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = strlen(password);
+}
+
+static void
+crypt_generate_blowfish(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ char salt[CRYPT_BLF_SALT_LEN];
+ char password[CRYPT_BLF_BUFFER_LEN];
+ char magic_salt[CRYPT_BLF_PREFIX_LEN];
+ unsigned int rounds = params->rounds;
+
+ if (rounds == 0)
+ rounds = CRYPT_BLF_ROUNDS_DEFAULT;
+ else if (rounds < CRYPT_BLF_ROUNDS_MIN)
+ rounds = CRYPT_BLF_ROUNDS_MIN;
+ else if (rounds > CRYPT_BLF_ROUNDS_MAX)
+ rounds = CRYPT_BLF_ROUNDS_MAX;
+
+ random_fill(salt, CRYPT_BLF_SALT_LEN);
+ if (crypt_gensalt_blowfish_rn(CRYPT_BLF_PREFIX, rounds,
+ salt, CRYPT_BLF_SALT_LEN,
+ magic_salt, CRYPT_BLF_PREFIX_LEN) == NULL)
+ i_fatal("crypt_gensalt_blowfish_rn failed: %m");
+
+ if (crypt_blowfish_rn(plaintext, magic_salt, password,
+ CRYPT_BLF_BUFFER_LEN) == NULL)
+ i_fatal("crypt_blowfish_rn failed: %m");
+
+ *raw_password_r = (const unsigned char *)t_strdup(password);
+ *size_r = strlen(password);
+}
+
+static int
+crypt_verify_blowfish(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ const char *password;
+ const char *salt;
+ char crypted[CRYPT_BLF_BUFFER_LEN];
+
+ if (size == 0) {
+ /* the default mycrypt() handler would return match */
+ return 0;
+ }
+ password = t_strndup(raw_password, size);
+
+ if (size < CRYPT_BLF_PREFIX_LEN ||
+ !str_begins(password, "$2") ||
+ password[2] < 'a' || password[2] > 'z' ||
+ password[3] != '$') {
+ *error_r = "Password is not blowfish password";
+ return -1;
+ }
+
+ salt = t_strndup(password, CRYPT_BLF_PREFIX_LEN);
+ if (crypt_blowfish_rn(plaintext, salt, crypted, CRYPT_BLF_BUFFER_LEN) == NULL) {
+ /* really shouldn't happen unless the system is broken */
+ *error_r = t_strdup_printf("crypt_blowfish_rn failed: %m");
+ return -1;
+ }
+
+ return strcmp(crypted, password) == 0 ? 1 : 0;
+}
+
+static void
+crypt_generate_sha256(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const char *password, *salt, *magic_salt;
+ unsigned int rounds = params->rounds;
+
+ if (rounds == 0)
+ rounds = CRYPT_SHA2_ROUNDS_DEFAULT;
+ else if (rounds < CRYPT_SHA2_ROUNDS_MIN)
+ rounds = CRYPT_SHA2_ROUNDS_MIN;
+ else if (rounds > CRYPT_SHA2_ROUNDS_MAX)
+ rounds = CRYPT_SHA2_ROUNDS_MAX;
+
+ salt = password_generate_salt(CRYPT_SHA2_SALT_LEN);
+ if (rounds == CRYPT_SHA2_ROUNDS_DEFAULT)
+ magic_salt = t_strdup_printf("$5$%s", salt);
+ else
+ magic_salt = t_strdup_printf("$5$rounds=%u$%s", rounds, salt);
+ password = t_strdup(mycrypt(plaintext, magic_salt));
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = strlen(password);
+}
+
+static void
+crypt_generate_sha512(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const char *password, *salt, *magic_salt;
+ unsigned int rounds = params->rounds;
+
+ if (rounds == 0)
+ rounds = CRYPT_SHA2_ROUNDS_DEFAULT;
+ else if (rounds < CRYPT_SHA2_ROUNDS_MIN)
+ rounds = CRYPT_SHA2_ROUNDS_MIN;
+ else if (rounds > CRYPT_SHA2_ROUNDS_MAX)
+ rounds = CRYPT_SHA2_ROUNDS_MAX;
+
+ salt = password_generate_salt(CRYPT_SHA2_SALT_LEN);
+ if (rounds == CRYPT_SHA2_ROUNDS_DEFAULT)
+ magic_salt = t_strdup_printf("$6$%s", salt);
+ else
+ magic_salt = t_strdup_printf("$6$rounds=%u$%s", rounds, salt);
+ password = t_strdup(mycrypt(plaintext, magic_salt));
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = strlen(password);
+}
+
+/* keep in sync with the crypt_schemes struct below */
+static const struct {
+ const char *key;
+ const char *salt;
+ const char *expected;
+} sample[] = {
+ { "08/15!test~4711", "JB", "JBOZ0DgmtucwE" },
+ { "08/15!test~4711", "$5$rounds=1000$0123456789abcdef",
+ "$5$rounds=1000$0123456789abcdef$K/DksR0DT01hGc8g/kt"
+ "9McEgrbFMKi9qrb1jehe7hn4" },
+ { "08/15!test~4711", "$6$rounds=1000$0123456789abcdef",
+ "$6$rounds=1000$0123456789abcdef$ZIAd5WqfyLkpvsVCVUU1GrvqaZTq"
+ "vhJoouxdSqJO71l9Ld3tVrfOatEjarhghvEYADkq//LpDnTeO90tcbtHR1" }
+};
+
+/* keep in sync with the sample struct above */
+static const struct password_scheme crypt_schemes[] = {
+ { "DES-CRYPT", PW_ENCODING_NONE, 0, crypt_verify,
+ crypt_generate_des },
+ { "SHA256-CRYPT", PW_ENCODING_NONE, 0, crypt_verify,
+ crypt_generate_sha256 },
+ { "SHA512-CRYPT", PW_ENCODING_NONE, 0, crypt_verify,
+ crypt_generate_sha512 }
+};
+
+static const struct password_scheme blf_crypt_scheme = {
+ "BLF-CRYPT", PW_ENCODING_NONE, 0, crypt_verify_blowfish,
+ crypt_generate_blowfish
+};
+
+static const struct password_scheme default_crypt_scheme = {
+ "CRYPT", PW_ENCODING_NONE, 0, crypt_verify,
+ crypt_generate_blowfish
+};
+
+void password_scheme_register_crypt(void)
+{
+ unsigned int i;
+ const char *crypted;
+
+ i_assert(N_ELEMENTS(crypt_schemes) == N_ELEMENTS(sample));
+
+ for (i = 0; i < N_ELEMENTS(crypt_schemes); i++) {
+ crypted = mycrypt(sample[i].key, sample[i].salt);
+ if (crypted != NULL &&
+ (strcmp(crypted, sample[i].expected) == 0))
+ password_scheme_register(&crypt_schemes[i]);
+ }
+ password_scheme_register(&blf_crypt_scheme);
+ password_scheme_register(&default_crypt_scheme);
+}
diff --git a/src/auth/password-scheme-md5crypt.c b/src/auth/password-scheme-md5crypt.c
new file mode 100644
index 0000000..a75e3b2
--- /dev/null
+++ b/src/auth/password-scheme-md5crypt.c
@@ -0,0 +1,147 @@
+/*
+ * ----------------------------------------------------------------------------
+ * "THE BEER-WARE LICENSE" (Revision 42):
+ * <phk@login.dknet.dk> wrote this file. As long as you retain this notice you
+ * can do whatever you want with this stuff. If we meet some day, and you think
+ * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
+ * ----------------------------------------------------------------------------
+ */
+
+/*
+ * Ported from FreeBSD to Linux, only minimal changes. --marekm
+ */
+
+/*
+ * Adapted from shadow-19990607 by Tudor Bosman, tudorb@jm.nu
+ */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "md5.h"
+#include "password-scheme.h"
+
+static unsigned char itoa64[] = /* 0 ... 63 => ascii - 64 */
+ "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+static char magic[] = "$1$"; /*
+ * This string is magic for
+ * this algorithm. Having
+ * it this way, we can get
+ * get better later on
+ */
+
+static void
+to64(string_t *str, unsigned long v, int n)
+{
+ while (--n >= 0) {
+ str_append_c(str, itoa64[v&0x3f]);
+ v >>= 6;
+ }
+}
+
+/*
+ * UNIX password
+ *
+ * Use MD5 for what it is best at...
+ */
+
+const char *password_generate_md5_crypt(const char *pw, const char *salt)
+{
+ const char *sp,*ep;
+ unsigned char final[MD5_RESULTLEN];
+ int sl,pl,i,j;
+ struct md5_context ctx,ctx1;
+ unsigned long l;
+ string_t *passwd;
+ size_t pw_len = strlen(pw);
+
+ /* Refine the Salt first */
+ sp = salt;
+
+ /* If it starts with the magic string, then skip that */
+ if (strncmp(sp, magic, sizeof(magic)-1) == 0)
+ sp += sizeof(magic)-1;
+
+ /* It stops at the first '$', max 8 chars */
+ for(ep=sp;*ep != '\0' && *ep != '$' && ep < (sp+8);ep++)
+ continue;
+
+ /* get the length of the true salt */
+ sl = ep - sp;
+
+ md5_init(&ctx);
+
+ /* The password first, since that is what is most unknown */
+ md5_update(&ctx,pw,pw_len);
+
+ /* Then our magic string */
+ md5_update(&ctx,magic,sizeof(magic)-1);
+
+ /* Then the raw salt */
+ md5_update(&ctx,sp,sl);
+
+ /* Then just as many characters of the MD5(pw,salt,pw) */
+ md5_init(&ctx1);
+ md5_update(&ctx1,pw,pw_len);
+ md5_update(&ctx1,sp,sl);
+ md5_update(&ctx1,pw,pw_len);
+ md5_final(&ctx1,final);
+ for(pl = pw_len; pl > 0; pl -= MD5_RESULTLEN)
+ md5_update(&ctx,final,pl>MD5_RESULTLEN ? MD5_RESULTLEN : pl);
+
+ /* Don't leave anything around in vm they could use. */
+ safe_memset(final, 0, sizeof(final));
+
+ /* Then something really weird... */
+ for (j=0,i = pw_len; i != 0; i >>= 1)
+ if ((i&1) != 0)
+ md5_update(&ctx, final+j, 1);
+ else
+ md5_update(&ctx, pw+j, 1);
+
+ /* Now make the output string */
+ passwd = t_str_new(sl + 64);
+ str_append(passwd, magic);
+ str_append_data(passwd, sp, sl);
+ str_append_c(passwd, '$');
+
+ md5_final(&ctx,final);
+
+ /*
+ * and now, just to make sure things don't run too fast
+ * On a 60 Mhz Pentium this takes 34 msec, so you would
+ * need 30 seconds to build a 1000 entry dictionary...
+ */
+ for(i=0;i<1000;i++) {
+ md5_init(&ctx1);
+ if((i & 1) != 0)
+ md5_update(&ctx1,pw,pw_len);
+ else
+ md5_update(&ctx1,final,MD5_RESULTLEN);
+
+ if((i % 3) != 0)
+ md5_update(&ctx1,sp,sl);
+
+ if((i % 7) != 0)
+ md5_update(&ctx1,pw,pw_len);
+
+ if((i & 1) != 0)
+ md5_update(&ctx1,final,MD5_RESULTLEN);
+ else
+ md5_update(&ctx1,pw,pw_len);
+ md5_final(&ctx1,final);
+ }
+
+ l = (final[ 0]<<16) | (final[ 6]<<8) | final[12]; to64(passwd,l,4);
+ l = (final[ 1]<<16) | (final[ 7]<<8) | final[13]; to64(passwd,l,4);
+ l = (final[ 2]<<16) | (final[ 8]<<8) | final[14]; to64(passwd,l,4);
+ l = (final[ 3]<<16) | (final[ 9]<<8) | final[15]; to64(passwd,l,4);
+ l = (final[ 4]<<16) | (final[10]<<8) | final[ 5]; to64(passwd,l,4);
+ l = final[11] ; to64(passwd,l,2);
+
+ /* Don't leave anything around in vm they could use. */
+ safe_memset(final, 0, sizeof(final));
+
+ return str_c(passwd);
+}
diff --git a/src/auth/password-scheme-otp.c b/src/auth/password-scheme-otp.c
new file mode 100644
index 0000000..ad7951b
--- /dev/null
+++ b/src/auth/password-scheme-otp.c
@@ -0,0 +1,40 @@
+/*
+ * OTP password scheme.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "hex-binary.h"
+#include "password-scheme.h"
+#include "randgen.h"
+#include "otp.h"
+
+int password_generate_otp(const char *pw, const char *state_data,
+ unsigned int algo, const char **result_r)
+{
+ struct otp_state state;
+
+ if (state_data != NULL) {
+ if (otp_parse_dbentry(state_data, &state) != 0)
+ return -1;
+ } else {
+ /* Generate new OTP credentials from plaintext */
+ unsigned char random_data[OTP_MAX_SEED_LEN / 2];
+ const char *random_hex;
+
+ random_fill(random_data, sizeof(random_data));
+ random_hex = binary_to_hex(random_data, sizeof(random_data));
+ if (i_strocpy(state.seed, random_hex, sizeof(state.seed)) < 0)
+ i_unreached();
+
+ state.seq = 1024;
+ state.algo = algo;
+ }
+
+ otp_hash(state.algo, state.seed, pw, state.seq, state.hash);
+ *result_r = otp_print_dbentry(&state);
+ return 0;
+}
diff --git a/src/auth/password-scheme-pbkdf2.c b/src/auth/password-scheme-pbkdf2.c
new file mode 100644
index 0000000..1e8eb3b
--- /dev/null
+++ b/src/auth/password-scheme-pbkdf2.c
@@ -0,0 +1,82 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "password-scheme.h"
+#include "hex-binary.h"
+#include "hash-method.h"
+#include "pkcs5.h"
+
+#define PBKDF2_KEY_SIZE_SHA1 20
+
+#define PBKDF2_GENERATE_SALT_LEN 16
+#define PBKDF2_ROUNDS_DEFAULT 5000
+
+static void
+pbkdf_run(const char *plaintext, const char *salt,
+ unsigned int rounds, unsigned char key_r[PBKDF2_KEY_SIZE_SHA1])
+{
+ memset(key_r, 0, PBKDF2_KEY_SIZE_SHA1);
+ buffer_t buf;
+ buffer_create_from_data(&buf, key_r, PBKDF2_KEY_SIZE_SHA1);
+
+ pkcs5_pbkdf(PKCS5_PBKDF2, hash_method_lookup("sha1"),
+ (const unsigned char *)plaintext, strlen(plaintext),
+ (const unsigned char *)salt, strlen(salt),
+ rounds, PBKDF2_KEY_SIZE_SHA1, &buf);
+}
+
+void pbkdf2_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char key[PBKDF2_KEY_SIZE_SHA1];
+ const char *salt;
+ string_t *str = t_str_new(64);
+ unsigned int rounds = params->rounds;
+
+ if (rounds == 0)
+ rounds = PBKDF2_ROUNDS_DEFAULT;
+ salt = password_generate_salt(PBKDF2_GENERATE_SALT_LEN);
+ pbkdf_run(plaintext, salt, rounds, key);
+
+ str_printfa(str, "$1$%s$%u$", salt, rounds);
+ binary_to_hex_append(str, key, sizeof(key));
+
+ *raw_password_r = str_data(str);
+ *size_r = str_len(str);
+}
+
+int pbkdf2_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ const char *const *fields;
+ const char *salt;
+ unsigned int rounds;
+ unsigned char key1[PBKDF2_KEY_SIZE_SHA1], key2[PBKDF2_KEY_SIZE_SHA1];
+ buffer_t buf;
+
+ /* $1$salt$rounds$hash */
+ if (size < 3 || memcmp(raw_password, "$1$", 3) != 0) {
+ *error_r = "Invalid PBKDF2 passdb entry prefix";
+ return -1;
+ }
+
+ fields = t_strsplit(t_strndup(raw_password + 3, size - 3), "$");
+ salt = fields[0];
+ if (str_array_length(fields) != 3 ||
+ str_to_uint(fields[1], &rounds) < 0) {
+ *error_r = "Invalid PBKDF2 passdb entry format";
+ return -1;
+ }
+ buffer_create_from_data(&buf, key1, sizeof(key1));
+ if (strlen(fields[2]) != sizeof(key1)*2 ||
+ hex_to_binary(fields[2], &buf) < 0) {
+ *error_r = "PBKDF2 hash not 160bit hex-encoded";
+ return -1;
+ }
+
+ pbkdf_run(plaintext, salt, rounds, key2);
+ return mem_equals_timing_safe(key1, key2, sizeof(key1)) ? 1 : 0;
+}
diff --git a/src/auth/password-scheme-scram.c b/src/auth/password-scheme-scram.c
new file mode 100644
index 0000000..a395074
--- /dev/null
+++ b/src/auth/password-scheme-scram.c
@@ -0,0 +1,224 @@
+/*
+ * SCRAM-SHA-1 SASL authentication, see RFC-5802
+ *
+ * Copyright (c) 2012 Florian Zeitz <florob@babelmonkeys.de>
+ *
+ * This software is released under the MIT license.
+ */
+
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hmac.h"
+#include "randgen.h"
+#include "hash-method.h"
+#include "sha1.h"
+#include "sha2.h"
+#include "str.h"
+#include "password-scheme.h"
+
+/* SCRAM allowed iteration count range. RFC says it SHOULD be at least 4096 */
+#define SCRAM_MIN_ITERATE_COUNT 4096
+#define SCRAM_MAX_ITERATE_COUNT INT_MAX
+
+#define SCRAM_DEFAULT_ITERATE_COUNT 4096
+
+static void
+Hi(const struct hash_method *hmethod, const unsigned char *str, size_t str_size,
+ const unsigned char *salt, size_t salt_size, unsigned int i,
+ unsigned char *result)
+{
+ struct hmac_context ctx;
+ unsigned char U[hmethod->digest_size];
+ unsigned int j, k;
+
+ /* Calculate U1 */
+ hmac_init(&ctx, str, str_size, hmethod);
+ hmac_update(&ctx, salt, salt_size);
+ hmac_update(&ctx, "\0\0\0\1", 4);
+ hmac_final(&ctx, U);
+
+ memcpy(result, U, hmethod->digest_size);
+
+ /* Calculate U2 to Ui and Hi */
+ for (j = 2; j <= i; j++) {
+ hmac_init(&ctx, str, str_size, hmethod);
+ hmac_update(&ctx, U, sizeof(U));
+ hmac_final(&ctx, U);
+ for (k = 0; k < hmethod->digest_size; k++)
+ result[k] ^= U[k];
+ }
+}
+
+int scram_scheme_parse(const struct hash_method *hmethod, const char *name,
+ const unsigned char *credentials, size_t size,
+ unsigned int *iter_count_r, const char **salt_r,
+ unsigned char stored_key_r[],
+ unsigned char server_key_r[], const char **error_r)
+{
+ const char *const *fields;
+ buffer_t *buf;
+
+ /* password string format: iter,salt,stored_key,server_key */
+ fields = t_strsplit(t_strndup(credentials, size), ",");
+
+ if (str_array_length(fields) != 4) {
+ *error_r = t_strdup_printf(
+ "Invalid %s passdb entry format", name);
+ return -1;
+ }
+ if (str_to_uint(fields[0], iter_count_r) < 0 ||
+ *iter_count_r < SCRAM_MIN_ITERATE_COUNT ||
+ *iter_count_r > SCRAM_MAX_ITERATE_COUNT) {
+ *error_r = t_strdup_printf(
+ "Invalid %s iteration count in passdb", name);
+ return -1;
+ }
+ *salt_r = fields[1];
+
+ buf = t_buffer_create(hmethod->digest_size);
+ if (base64_decode(fields[2], strlen(fields[2]), NULL, buf) < 0 ||
+ buf->used != hmethod->digest_size) {
+ *error_r = t_strdup_printf(
+ "Invalid %s StoredKey in passdb", name);
+ return -1;
+ }
+ memcpy(stored_key_r, buf->data, hmethod->digest_size);
+
+ buffer_set_used_size(buf, 0);
+ if (base64_decode(fields[3], strlen(fields[3]), NULL, buf) < 0 ||
+ buf->used != hmethod->digest_size) {
+ *error_r = t_strdup_printf(
+ "Invalid %s ServerKey in passdb", name);
+ return -1;
+ }
+ memcpy(server_key_r, buf->data, hmethod->digest_size);
+ return 0;
+}
+
+int scram_verify(const struct hash_method *hmethod, const char *scheme_name,
+ const char *plaintext, const unsigned char *raw_password,
+ size_t size, const char **error_r)
+{
+ struct hmac_context ctx;
+ const char *salt_base64;
+ unsigned int iter_count;
+ const unsigned char *salt;
+ size_t salt_len;
+ unsigned char salted_password[hmethod->digest_size];
+ unsigned char client_key[hmethod->digest_size];
+ unsigned char stored_key[hmethod->digest_size];
+ unsigned char calculated_stored_key[hmethod->digest_size];
+ unsigned char server_key[hmethod->digest_size];
+ int ret;
+
+ if (scram_scheme_parse(hmethod, scheme_name, raw_password, size,
+ &iter_count, &salt_base64,
+ stored_key, server_key, error_r) < 0)
+ return -1;
+
+ salt = buffer_get_data(t_base64_decode_str(salt_base64), &salt_len);
+
+ /* FIXME: credentials should be SASLprepped UTF8 data here */
+ Hi(hmethod, (const unsigned char *)plaintext, strlen(plaintext),
+ salt, salt_len, iter_count, salted_password);
+
+ /* Calculate ClientKey */
+ hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod);
+ hmac_update(&ctx, "Client Key", 10);
+ hmac_final(&ctx, client_key);
+
+ /* Calculate StoredKey */
+ hash_method_get_digest(hmethod, client_key, sizeof(client_key),
+ calculated_stored_key);
+ ret = mem_equals_timing_safe(stored_key, calculated_stored_key,
+ sizeof(stored_key)) ? 1 : 0;
+
+ safe_memset(salted_password, 0, sizeof(salted_password));
+ safe_memset(client_key, 0, sizeof(client_key));
+ safe_memset(stored_key, 0, sizeof(stored_key));
+
+ return ret;
+}
+
+void scram_generate(const struct hash_method *hmethod, const char *plaintext,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ string_t *str;
+ struct hmac_context ctx;
+ unsigned char salt[16];
+ unsigned char salted_password[hmethod->digest_size];
+ unsigned char client_key[hmethod->digest_size];
+ unsigned char server_key[hmethod->digest_size];
+ unsigned char stored_key[hmethod->digest_size];
+
+ random_fill(salt, sizeof(salt));
+
+ str = t_str_new(MAX_BASE64_ENCODED_SIZE(sizeof(salt)));
+ str_printfa(str, "%d,", SCRAM_DEFAULT_ITERATE_COUNT);
+ base64_encode(salt, sizeof(salt), str);
+
+ /* FIXME: credentials should be SASLprepped UTF8 data here */
+ Hi(hmethod, (const unsigned char *)plaintext, strlen(plaintext), salt,
+ sizeof(salt), SCRAM_DEFAULT_ITERATE_COUNT, salted_password);
+
+ /* Calculate ClientKey */
+ hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod);
+ hmac_update(&ctx, "Client Key", 10);
+ hmac_final(&ctx, client_key);
+
+ /* Calculate StoredKey */
+ hash_method_get_digest(hmethod, client_key, sizeof(client_key),
+ stored_key);
+ str_append_c(str, ',');
+ base64_encode(stored_key, sizeof(stored_key), str);
+
+ /* Calculate ServerKey */
+ hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod);
+ hmac_update(&ctx, "Server Key", 10);
+ hmac_final(&ctx, server_key);
+ str_append_c(str, ',');
+ base64_encode(server_key, sizeof(server_key), str);
+
+ safe_memset(salted_password, 0, sizeof(salted_password));
+ safe_memset(client_key, 0, sizeof(client_key));
+ safe_memset(server_key, 0, sizeof(server_key));
+ safe_memset(stored_key, 0, sizeof(stored_key));
+
+ *raw_password_r = (const unsigned char *)str_c(str);
+ *size_r = str_len(str);
+}
+
+int scram_sha1_verify(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ return scram_verify(&hash_method_sha1, "SCRAM-SHA-1", plaintext,
+ raw_password, size, error_r);
+}
+
+void scram_sha1_generate(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ scram_generate(&hash_method_sha1, plaintext, raw_password_r, size_r);
+}
+
+int scram_sha256_verify(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ return scram_verify(&hash_method_sha256, "SCRAM-SHA-256", plaintext,
+ raw_password, size, error_r);
+}
+
+void scram_sha256_generate(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ scram_generate(&hash_method_sha256, plaintext, raw_password_r, size_r);
+}
diff --git a/src/auth/password-scheme-sodium.c b/src/auth/password-scheme-sodium.c
new file mode 100644
index 0000000..3e2f6bd
--- /dev/null
+++ b/src/auth/password-scheme-sodium.c
@@ -0,0 +1,92 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "password-scheme.h"
+
+#ifdef HAVE_LIBSODIUM
+#include <sodium.h>
+
+static void
+generate_argon2i(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned long long rounds = params->rounds;
+ size_t memlimit;
+ char result[crypto_pwhash_STRBYTES];
+
+ if (rounds == 0)
+ rounds = crypto_pwhash_argon2i_OPSLIMIT_INTERACTIVE;
+
+ if (rounds >= crypto_pwhash_argon2i_OPSLIMIT_SENSITIVE)
+ memlimit = crypto_pwhash_argon2i_MEMLIMIT_SENSITIVE;
+ else if (rounds >= crypto_pwhash_argon2i_OPSLIMIT_MODERATE)
+ memlimit = crypto_pwhash_argon2i_MEMLIMIT_MODERATE;
+ else
+ memlimit = crypto_pwhash_argon2i_MEMLIMIT_INTERACTIVE;
+
+ if (crypto_pwhash_argon2i_str(result, plaintext, strlen(plaintext), rounds, memlimit) < 0)
+ i_fatal("crypto_pwhash_argon2i_str failed: %m");
+ *raw_password_r = (const unsigned char*)t_strdup(result);
+ *size_r = strlen(result);
+}
+
+#ifdef crypto_pwhash_ALG_ARGON2ID13
+static void
+generate_argon2id(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned long long rounds = params->rounds;
+ size_t memlimit;
+ char result[crypto_pwhash_argon2id_STRBYTES];
+ i_zero(&result);
+
+ if (rounds == 0)
+ rounds = crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE;
+
+ if (rounds >= crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE)
+ memlimit = crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE;
+ else if (rounds >= crypto_pwhash_argon2id_OPSLIMIT_MODERATE)
+ memlimit = crypto_pwhash_argon2id_MEMLIMIT_MODERATE;
+ else
+ memlimit = crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE;
+
+ /* XXX: Bug in sodium-1.0.13, it expects rounds to be 3 */
+ if (rounds < 3)
+ rounds = 3;
+
+ if (crypto_pwhash_argon2id_str(result, plaintext, strlen(plaintext), rounds, memlimit) < 0)
+ i_fatal("crypto_pwhash_argon2id_str failed: %m");
+ *raw_password_r = (const unsigned char*)t_strdup(result);
+ *size_r = strlen(result);
+}
+#endif
+
+static int
+verify_argon2(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r ATTR_UNUSED)
+{
+ const char *passwd = t_strndup(raw_password, size);
+ if (crypto_pwhash_str_verify(passwd, plaintext, strlen(plaintext)) < 0)
+ return 0;
+ return 1;
+}
+
+
+static const struct password_scheme sodium_schemes[] = {
+ { "ARGON2I", PW_ENCODING_NONE, 0, verify_argon2,
+ generate_argon2i },
+#ifdef crypto_pwhash_ALG_ARGON2ID13
+ { "ARGON2ID", PW_ENCODING_NONE, 0, verify_argon2,
+ generate_argon2id },
+#endif
+};
+
+void password_scheme_register_sodium(void)
+{
+ if (sodium_init() != 0)
+ i_fatal("sodium_init() failed");
+ for(size_t i = 0; i < N_ELEMENTS(sodium_schemes); i++)
+ password_scheme_register(&sodium_schemes[i]);
+}
+#endif
diff --git a/src/auth/password-scheme.c b/src/auth/password-scheme.c
new file mode 100644
index 0000000..c572cd3
--- /dev/null
+++ b/src/auth/password-scheme.c
@@ -0,0 +1,822 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "md4.h"
+#include "md5.h"
+#include "hmac.h"
+#include "hmac-cram-md5.h"
+#include "mycrypt.h"
+#include "randgen.h"
+#include "sha1.h"
+#include "sha2.h"
+#include "otp.h"
+#include "str.h"
+#include "password-scheme.h"
+
+static const char salt_chars[] =
+ "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+static HASH_TABLE(const char*, const struct password_scheme *) password_schemes;
+
+static const struct password_scheme *
+password_scheme_lookup_name(const char *name)
+{
+ return hash_table_lookup(password_schemes, name);
+}
+
+/* Lookup scheme and encoding by given name. The encoding is taken from
+ ".base64", ".b64" or ".hex" suffix if it exists, otherwise the default
+ encoding is used. */
+static const struct password_scheme *
+password_scheme_lookup(const char *name, enum password_encoding *encoding_r)
+{
+ const struct password_scheme *scheme;
+ const char *encoding = NULL;
+
+ *encoding_r = PW_ENCODING_NONE;
+ if ((encoding = strchr(name, '.')) != NULL) {
+ name = t_strdup_until(name, encoding);
+ encoding++;
+ }
+
+ scheme = password_scheme_lookup_name(name);
+ if (scheme == NULL)
+ return NULL;
+
+ if (encoding == NULL)
+ *encoding_r = scheme->default_encoding;
+ else if (strcasecmp(encoding, "b64") == 0 ||
+ strcasecmp(encoding, "base64") == 0)
+ *encoding_r = PW_ENCODING_BASE64;
+ else if (strcasecmp(encoding, "hex") == 0)
+ *encoding_r = PW_ENCODING_HEX;
+ else {
+ /* unknown encoding. treat as invalid scheme. */
+ return NULL;
+ }
+ return scheme;
+}
+
+int password_verify(const char *plaintext,
+ const struct password_generate_params *params,
+ const char *scheme, const unsigned char *raw_password,
+ size_t size, const char **error_r)
+{
+ const struct password_scheme *s;
+ enum password_encoding encoding;
+ const unsigned char *generated;
+ size_t generated_size;
+ int ret;
+
+ s = password_scheme_lookup(scheme, &encoding);
+ if (s == NULL) {
+ *error_r = "Unknown password scheme";
+ return -1;
+ }
+
+ if (s->password_verify != NULL) {
+ ret = s->password_verify(plaintext, params, raw_password, size,
+ error_r);
+ } else {
+ /* generic verification handler: generate the password and
+ compare it to the one in database */
+ s->password_generate(plaintext, params,
+ &generated, &generated_size);
+ ret = size != generated_size ? 0 :
+ mem_equals_timing_safe(generated, raw_password, size) ? 1 : 0;
+ }
+
+ if (ret == 0)
+ *error_r = AUTH_LOG_MSG_PASSWORD_MISMATCH;
+ return ret;
+}
+
+const char *password_get_scheme(const char **password)
+{
+ const char *p, *scheme;
+
+ if (*password == NULL)
+ return NULL;
+
+ if (str_begins(*password, "$1$")) {
+ /* $1$<salt>$<password>[$<ignored>] */
+ p = strchr(*password + 3, '$');
+ if (p != NULL) {
+ /* stop at next '$' after password */
+ p = strchr(p+1, '$');
+ if (p != NULL)
+ *password = t_strdup_until(*password, p);
+ return "MD5-CRYPT";
+ }
+ }
+
+ if (**password != '{')
+ return NULL;
+
+ p = strchr(*password, '}');
+ if (p == NULL)
+ return NULL;
+
+ scheme = t_strdup_until(*password + 1, p);
+ *password = p + 1;
+ return scheme;
+}
+
+int password_decode(const char *password, const char *scheme,
+ const unsigned char **raw_password_r, size_t *size_r,
+ const char **error_r)
+{
+ const struct password_scheme *s;
+ enum password_encoding encoding;
+ buffer_t *buf;
+ size_t len;
+ bool guessed_encoding;
+
+ *error_r = NULL;
+
+ s = password_scheme_lookup(scheme, &encoding);
+ if (s == NULL) {
+ *error_r = "Unknown scheme";
+ return 0;
+ }
+
+ len = strlen(password);
+ if (encoding != PW_ENCODING_NONE && s->raw_password_len != 0 &&
+ strchr(scheme, '.') == NULL) {
+ /* encoding not specified. we can guess quite well between
+ base64 and hex encodings. the only problem is distinguishing
+ 2 character strings, but there shouldn't be any that short
+ raw_password_lens. */
+ encoding = len == s->raw_password_len * 2 ?
+ PW_ENCODING_HEX : PW_ENCODING_BASE64;
+ guessed_encoding = TRUE;
+ } else {
+ guessed_encoding = FALSE;
+ }
+
+ switch (encoding) {
+ case PW_ENCODING_NONE:
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = len;
+ break;
+ case PW_ENCODING_HEX:
+ buf = t_buffer_create(len / 2 + 1);
+ if (hex_to_binary(password, buf) == 0) {
+ *raw_password_r = buf->data;
+ *size_r = buf->used;
+ break;
+ }
+ if (!guessed_encoding) {
+ *error_r = "Input isn't valid HEX encoded data";
+ return -1;
+ }
+ /* check if it's base64-encoded after all. some input lengths
+ produce matching hex and base64 encoded lengths. */
+ /* fall through */
+ case PW_ENCODING_BASE64:
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(len));
+ if (base64_decode(password, len, NULL, buf) < 0) {
+ *error_r = "Input isn't valid base64 encoded data";
+ return -1;
+ }
+
+ *raw_password_r = buf->data;
+ *size_r = buf->used;
+ break;
+ }
+ if (s->raw_password_len != *size_r && s->raw_password_len != 0) {
+ /* password has invalid length */
+ *error_r = t_strdup_printf(
+ "Input length isn't valid (%u instead of %u)",
+ (unsigned int)*size_r, s->raw_password_len);
+ return -1;
+ }
+ return 1;
+}
+
+bool password_generate(const char *plaintext, const struct password_generate_params *params,
+ const char *scheme,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const struct password_scheme *s;
+ enum password_encoding encoding;
+
+ s = password_scheme_lookup(scheme, &encoding);
+ if (s == NULL)
+ return FALSE;
+
+ s->password_generate(plaintext, params, raw_password_r, size_r);
+ return TRUE;
+}
+
+bool password_generate_encoded(const char *plaintext, const struct password_generate_params *params,
+ const char *scheme, const char **password_r)
+{
+ const struct password_scheme *s;
+ const unsigned char *raw_password;
+ enum password_encoding encoding;
+ string_t *str;
+ size_t size;
+
+ s = password_scheme_lookup(scheme, &encoding);
+ if (s == NULL)
+ return FALSE;
+
+ s->password_generate(plaintext, params, &raw_password, &size);
+ switch (encoding) {
+ case PW_ENCODING_NONE:
+ *password_r = t_strndup(raw_password, size);
+ break;
+ case PW_ENCODING_BASE64:
+ str = t_str_new(MAX_BASE64_ENCODED_SIZE(size) + 1);
+ base64_encode(raw_password, size, str);
+ *password_r = str_c(str);
+ break;
+ case PW_ENCODING_HEX:
+ *password_r = binary_to_hex(raw_password, size);
+ break;
+ }
+ return TRUE;
+}
+
+const char *password_generate_salt(size_t len)
+{
+ char *salt;
+ salt = t_malloc_no0(len + 1);
+ for (size_t i = 0; i < len; i++)
+ salt[i] = salt_chars[i_rand_limit(sizeof(salt_chars) - 1)];
+ salt[len] = '\0';
+ return salt;
+}
+
+bool password_scheme_is_alias(const char *scheme1, const char *scheme2)
+{
+ const struct password_scheme *s1 = NULL, *s2 = NULL;
+
+ if (*scheme1 == '\0' || *scheme2 == '\0')
+ return FALSE;
+
+ scheme1 = t_strcut(scheme1, '.');
+ scheme2 = t_strcut(scheme2, '.');
+
+ if (strcasecmp(scheme1, scheme2) == 0)
+ return TRUE;
+
+ s1 = hash_table_lookup(password_schemes, scheme1);
+ s2 = hash_table_lookup(password_schemes, scheme2);
+
+ /* if they've the same generate function, they're equivalent */
+ return s1 != NULL && s2 != NULL &&
+ s1->password_generate == s2->password_generate;
+}
+
+const char *
+password_scheme_detect(const char *plain_password, const char *crypted_password,
+ const struct password_generate_params *params)
+{
+ struct hash_iterate_context *ctx;
+ const char *key;
+ const struct password_scheme *scheme;
+ const unsigned char *raw_password;
+ size_t raw_password_size;
+ const char *error;
+
+ ctx = hash_table_iterate_init(password_schemes);
+ while (hash_table_iterate(ctx, password_schemes, &key, &scheme)) {
+ if (password_decode(crypted_password, scheme->name,
+ &raw_password, &raw_password_size,
+ &error) <= 0)
+ continue;
+
+ if (password_verify(plain_password, params, scheme->name,
+ raw_password, raw_password_size,
+ &error) > 0)
+ break;
+ key = NULL;
+ }
+ hash_table_iterate_deinit(&ctx);
+ return key;
+}
+
+int crypt_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ const char *password, *crypted;
+
+ if (size > 4 && raw_password[0] == '$' && raw_password[1] == '2' &&
+ raw_password[3] == '$')
+ return password_verify(plaintext, params, "BLF-CRYPT",
+ raw_password, size, error_r);
+
+ if (size == 0) {
+ /* the default mycrypt() handler would return match */
+ return 0;
+ }
+
+ password = t_strndup(raw_password, size);
+ crypted = mycrypt(plaintext, password);
+ if (crypted == NULL) {
+ /* really shouldn't happen unless the system is broken */
+ *error_r = t_strdup_printf("crypt() failed: %m");
+ return -1;
+ }
+
+ return str_equals_timing_almost_safe(crypted, password) ? 1 : 0;
+}
+
+static int
+md5_verify(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char *raw_password, size_t size, const char **error_r)
+{
+ const char *password, *str, *error;
+ const unsigned char *md5_password;
+ size_t md5_size;
+
+ password = t_strndup(raw_password, size);
+ if (str_begins(password, "$1$")) {
+ /* MD5-CRYPT */
+ str = password_generate_md5_crypt(plaintext, password);
+ return str_equals_timing_almost_safe(str, password) ? 1 : 0;
+ } else if (password_decode(password, "PLAIN-MD5",
+ &md5_password, &md5_size, &error) <= 0) {
+ *error_r = "Not a valid MD5-CRYPT or PLAIN-MD5 password";
+ return -1;
+ } else {
+ return password_verify(plaintext, params, "PLAIN-MD5",
+ md5_password, md5_size, error_r);
+ }
+}
+
+static int
+md5_crypt_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r ATTR_UNUSED)
+{
+ const char *password, *str;
+
+ password = t_strndup(raw_password, size);
+ str = password_generate_md5_crypt(plaintext, password);
+ return str_equals_timing_almost_safe(str, password) ? 1 : 0;
+}
+
+static void
+md5_crypt_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const char *password;
+ const char *salt;
+
+ salt = password_generate_salt(8);
+
+ password = password_generate_md5_crypt(plaintext, salt);
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = strlen(password);
+}
+
+static void
+sha1_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char *digest;
+
+ digest = t_malloc_no0(SHA1_RESULTLEN);
+ sha1_get_digest(plaintext, strlen(plaintext), digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA1_RESULTLEN;
+}
+
+static void
+sha256_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char *digest;
+
+ digest = t_malloc_no0(SHA256_RESULTLEN);
+ sha256_get_digest(plaintext, strlen(plaintext), digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA256_RESULTLEN;
+}
+
+static void
+sha512_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char *digest;
+
+ digest = t_malloc_no0(SHA512_RESULTLEN);
+ sha512_get_digest(plaintext, strlen(plaintext), digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA512_RESULTLEN;
+}
+
+static void
+ssha_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+#define SSHA_SALT_LEN 4
+ unsigned char *digest, *salt;
+ struct sha1_ctxt ctx;
+
+ digest = t_malloc_no0(SHA1_RESULTLEN + SSHA_SALT_LEN);
+ salt = digest + SHA1_RESULTLEN;
+ random_fill(salt, SSHA_SALT_LEN);
+
+ sha1_init(&ctx);
+ sha1_loop(&ctx, plaintext, strlen(plaintext));
+ sha1_loop(&ctx, salt, SSHA_SALT_LEN);
+ sha1_result(&ctx, digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA1_RESULTLEN + SSHA_SALT_LEN;
+}
+
+static int ssha_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ unsigned char sha1_digest[SHA1_RESULTLEN];
+ struct sha1_ctxt ctx;
+
+ /* format: <SHA1 hash><salt> */
+ if (size <= SHA1_RESULTLEN) {
+ *error_r = "SSHA password is too short";
+ return -1;
+ }
+
+ sha1_init(&ctx);
+ sha1_loop(&ctx, plaintext, strlen(plaintext));
+ sha1_loop(&ctx, raw_password + SHA1_RESULTLEN, size - SHA1_RESULTLEN);
+ sha1_result(&ctx, sha1_digest);
+ return mem_equals_timing_safe(sha1_digest, raw_password, SHA1_RESULTLEN) ? 1 : 0;
+}
+
+static void
+ssha256_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+#define SSHA256_SALT_LEN 4
+ unsigned char *digest, *salt;
+ struct sha256_ctx ctx;
+
+ digest = t_malloc_no0(SHA256_RESULTLEN + SSHA256_SALT_LEN);
+ salt = digest + SHA256_RESULTLEN;
+ random_fill(salt, SSHA256_SALT_LEN);
+
+ sha256_init(&ctx);
+ sha256_loop(&ctx, plaintext, strlen(plaintext));
+ sha256_loop(&ctx, salt, SSHA256_SALT_LEN);
+ sha256_result(&ctx, digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA256_RESULTLEN + SSHA256_SALT_LEN;
+}
+
+static int ssha256_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ unsigned char sha256_digest[SHA256_RESULTLEN];
+ struct sha256_ctx ctx;
+
+ /* format: <SHA256 hash><salt> */
+ if (size <= SHA256_RESULTLEN) {
+ *error_r = "SSHA256 password is too short";
+ return -1;
+ }
+
+ sha256_init(&ctx);
+ sha256_loop(&ctx, plaintext, strlen(plaintext));
+ sha256_loop(&ctx, raw_password + SHA256_RESULTLEN,
+ size - SHA256_RESULTLEN);
+ sha256_result(&ctx, sha256_digest);
+ return mem_equals_timing_safe(sha256_digest, raw_password,
+ SHA256_RESULTLEN) ? 1 : 0;
+}
+
+static void
+ssha512_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+#define SSHA512_SALT_LEN 4
+ unsigned char *digest, *salt;
+ struct sha512_ctx ctx;
+
+ digest = t_malloc_no0(SHA512_RESULTLEN + SSHA512_SALT_LEN);
+ salt = digest + SHA512_RESULTLEN;
+ random_fill(salt, SSHA512_SALT_LEN);
+
+ sha512_init(&ctx);
+ sha512_loop(&ctx, plaintext, strlen(plaintext));
+ sha512_loop(&ctx, salt, SSHA512_SALT_LEN);
+ sha512_result(&ctx, digest);
+
+ *raw_password_r = digest;
+ *size_r = SHA512_RESULTLEN + SSHA512_SALT_LEN;
+}
+
+static int ssha512_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ unsigned char sha512_digest[SHA512_RESULTLEN];
+ struct sha512_ctx ctx;
+
+ /* format: <SHA512 hash><salt> */
+ if (size <= SHA512_RESULTLEN) {
+ *error_r = "SSHA512 password is too short";
+ return -1;
+ }
+
+ sha512_init(&ctx);
+ sha512_loop(&ctx, plaintext, strlen(plaintext));
+ sha512_loop(&ctx, raw_password + SHA512_RESULTLEN,
+ size - SHA512_RESULTLEN);
+ sha512_result(&ctx, sha512_digest);
+ return mem_equals_timing_safe(sha512_digest, raw_password,
+ SHA512_RESULTLEN) ? 1 : 0;
+}
+
+static void
+smd5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+#define SMD5_SALT_LEN 4
+ unsigned char *digest, *salt;
+ struct md5_context ctx;
+
+ digest = t_malloc_no0(MD5_RESULTLEN + SMD5_SALT_LEN);
+ salt = digest + MD5_RESULTLEN;
+ random_fill(salt, SMD5_SALT_LEN);
+
+ md5_init(&ctx);
+ md5_update(&ctx, plaintext, strlen(plaintext));
+ md5_update(&ctx, salt, SMD5_SALT_LEN);
+ md5_final(&ctx, digest);
+
+ *raw_password_r = digest;
+ *size_r = MD5_RESULTLEN + SMD5_SALT_LEN;
+}
+
+static int smd5_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ unsigned char md5_digest[MD5_RESULTLEN];
+ struct md5_context ctx;
+
+ /* format: <MD5 hash><salt> */
+ if (size <= MD5_RESULTLEN) {
+ *error_r = "SMD5 password is too short";
+ return -1;
+ }
+
+ md5_init(&ctx);
+ md5_update(&ctx, plaintext, strlen(plaintext));
+ md5_update(&ctx, raw_password + MD5_RESULTLEN, size - MD5_RESULTLEN);
+ md5_final(&ctx, md5_digest);
+ return mem_equals_timing_safe(md5_digest, raw_password, MD5_RESULTLEN) ? 1 : 0;
+}
+
+static void
+plain_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ *raw_password_r = (const unsigned char *)plaintext,
+ *size_r = strlen(plaintext);
+}
+
+static int
+plain_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r ATTR_UNUSED)
+{
+ size_t plaintext_len = strlen(plaintext);
+
+ if (plaintext_len != size)
+ return 0;
+ return mem_equals_timing_safe(plaintext, raw_password, size) ? 1 : 0;
+}
+
+static int
+plain_trunc_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ size_t i, plaintext_len, trunc_len = 0;
+
+ /* format: <length>-<password> */
+ for (i = 0; i < size; i++) {
+ if (raw_password[i] >= '0' && raw_password[i] <= '9')
+ trunc_len = trunc_len*10 + raw_password[i]-'0';
+ else
+ break;
+ }
+ if (i == size || raw_password[i] != '-') {
+ *error_r = "PLAIN-TRUNC missing length: prefix";
+ return -1;
+ }
+ i++;
+
+ plaintext_len = strlen(plaintext);
+ if (size-i == trunc_len && plaintext_len >= trunc_len) {
+ /* possibly truncated password. allow the given password as
+ long as the prefix matches. */
+ return mem_equals_timing_safe(raw_password+i, plaintext, trunc_len) ? 1 : 0;
+ }
+ return plaintext_len == size-i &&
+ mem_equals_timing_safe(raw_password+i, plaintext, plaintext_len) ? 1 : 0;
+}
+
+static void
+cram_md5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ struct hmac_context ctx;
+ unsigned char *context_digest;
+
+ context_digest = t_malloc_no0(CRAM_MD5_CONTEXTLEN);
+ hmac_init(&ctx, (const unsigned char *)plaintext,
+ strlen(plaintext), &hash_method_md5);
+ hmac_md5_get_cram_context(&ctx, context_digest);
+
+ *raw_password_r = context_digest;
+ *size_r = CRAM_MD5_CONTEXTLEN;
+}
+
+static void
+digest_md5_generate(const char *plaintext, const struct password_generate_params *params,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const char *realm, *str, *user;
+ unsigned char *digest;
+
+ if (params->user == NULL)
+ i_fatal("digest_md5_generate(): username not given");
+
+ user = params->user;
+
+
+ /* assume user@realm format for username. If user@domain is wanted
+ in the username, allow also user@domain@realm. */
+ realm = strrchr(user, '@');
+ if (realm != NULL) {
+ user = t_strdup_until(user, realm);
+ realm++;
+ } else {
+ realm = "";
+ }
+
+ /* user:realm:passwd */
+ digest = t_malloc_no0(MD5_RESULTLEN);
+ str = t_strdup_printf("%s:%s:%s", user, realm, plaintext);
+ md5_get_digest(str, strlen(str), digest);
+
+ *raw_password_r = digest;
+ *size_r = MD5_RESULTLEN;
+}
+
+static void
+plain_md4_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char *digest;
+
+ digest = t_malloc_no0(MD4_RESULTLEN);
+ md4_get_digest(plaintext, strlen(plaintext), digest);
+
+ *raw_password_r = digest;
+ *size_r = MD4_RESULTLEN;
+}
+
+static void
+plain_md5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ unsigned char *digest;
+
+ digest = t_malloc_no0(MD5_RESULTLEN);
+ md5_get_digest(plaintext, strlen(plaintext), digest);
+
+ *raw_password_r = digest;
+ *size_r = MD5_RESULTLEN;
+}
+
+static int otp_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r)
+{
+ const char *password, *generated;
+
+ password = t_strndup(raw_password, size);
+ if (password_generate_otp(plaintext, password, UINT_MAX, &generated) < 0) {
+ *error_r = "Invalid OTP data in passdb";
+ return -1;
+ }
+
+ return strcasecmp(password, generated) == 0 ? 1 : 0;
+}
+
+static void
+otp_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r)
+{
+ const char *password;
+
+ if (password_generate_otp(plaintext, NULL, OTP_HASH_SHA1, &password) < 0)
+ i_unreached();
+ *raw_password_r = (const unsigned char *)password;
+ *size_r = strlen(password);
+}
+
+static const struct password_scheme builtin_schemes[] = {
+ { "MD5", PW_ENCODING_NONE, 0, md5_verify, md5_crypt_generate },
+ { "MD5-CRYPT", PW_ENCODING_NONE, 0,
+ md5_crypt_verify, md5_crypt_generate },
+ { "SHA", PW_ENCODING_BASE64, SHA1_RESULTLEN, NULL, sha1_generate },
+ { "SHA1", PW_ENCODING_BASE64, SHA1_RESULTLEN, NULL, sha1_generate },
+ { "SHA256", PW_ENCODING_BASE64, SHA256_RESULTLEN,
+ NULL, sha256_generate },
+ { "SHA512", PW_ENCODING_BASE64, SHA512_RESULTLEN,
+ NULL, sha512_generate },
+ { "SMD5", PW_ENCODING_BASE64, 0, smd5_verify, smd5_generate },
+ { "SSHA", PW_ENCODING_BASE64, 0, ssha_verify, ssha_generate },
+ { "SSHA256", PW_ENCODING_BASE64, 0, ssha256_verify, ssha256_generate },
+ { "SSHA512", PW_ENCODING_BASE64, 0, ssha512_verify, ssha512_generate },
+ { "PLAIN", PW_ENCODING_NONE, 0, plain_verify, plain_generate },
+ { "CLEAR", PW_ENCODING_NONE, 0, plain_verify, plain_generate },
+ { "CLEARTEXT", PW_ENCODING_NONE, 0, plain_verify, plain_generate },
+ { "PLAIN-TRUNC", PW_ENCODING_NONE, 0, plain_trunc_verify, plain_generate },
+ { "CRAM-MD5", PW_ENCODING_HEX, CRAM_MD5_CONTEXTLEN,
+ NULL, cram_md5_generate },
+ { "SCRAM-SHA-1", PW_ENCODING_NONE, 0, scram_sha1_verify,
+ scram_sha1_generate},
+ { "SCRAM-SHA-256", PW_ENCODING_NONE, 0, scram_sha256_verify,
+ scram_sha256_generate},
+ { "HMAC-MD5", PW_ENCODING_HEX, CRAM_MD5_CONTEXTLEN,
+ NULL, cram_md5_generate },
+ { "DIGEST-MD5", PW_ENCODING_HEX, MD5_RESULTLEN,
+ NULL, digest_md5_generate },
+ { "PLAIN-MD4", PW_ENCODING_HEX, MD4_RESULTLEN,
+ NULL, plain_md4_generate },
+ { "PLAIN-MD5", PW_ENCODING_HEX, MD5_RESULTLEN,
+ NULL, plain_md5_generate },
+ { "LDAP-MD5", PW_ENCODING_BASE64, MD5_RESULTLEN,
+ NULL, plain_md5_generate },
+ { "OTP", PW_ENCODING_NONE, 0, otp_verify, otp_generate },
+ { "PBKDF2", PW_ENCODING_NONE, 0, pbkdf2_verify, pbkdf2_generate },
+};
+
+void password_scheme_register(const struct password_scheme *scheme)
+{
+ if (password_scheme_lookup_name(scheme->name) != NULL) {
+ i_panic("password_scheme_register(%s): Already registered",
+ scheme->name);
+ }
+ hash_table_insert(password_schemes, scheme->name, scheme);
+}
+
+void password_scheme_unregister(const struct password_scheme *scheme)
+{
+ if (!hash_table_try_remove(password_schemes, scheme->name))
+ i_panic("password_scheme_unregister(%s): Not registered", scheme->name);
+}
+
+void password_schemes_get(ARRAY_TYPE(password_scheme_p) *schemes_r)
+{
+ struct hash_iterate_context *ctx;
+ const char *key;
+ const struct password_scheme *scheme;
+ ctx = hash_table_iterate_init(password_schemes);
+ while(hash_table_iterate(ctx, password_schemes, &key, &scheme)) {
+ array_push_back(schemes_r, &scheme);
+ }
+ hash_table_iterate_deinit(&ctx);
+}
+
+void password_schemes_init(void)
+{
+ unsigned int i;
+
+ hash_table_create(&password_schemes, default_pool,
+ N_ELEMENTS(builtin_schemes)*2, strfastcase_hash,
+ strcasecmp);
+ for (i = 0; i < N_ELEMENTS(builtin_schemes); i++)
+ password_scheme_register(&builtin_schemes[i]);
+ password_scheme_register_crypt();
+#ifdef HAVE_LIBSODIUM
+ password_scheme_register_sodium();
+#endif
+}
+
+void password_schemes_deinit(void)
+{
+ hash_table_destroy(&password_schemes);
+}
diff --git a/src/auth/password-scheme.h b/src/auth/password-scheme.h
new file mode 100644
index 0000000..b3c3f56
--- /dev/null
+++ b/src/auth/password-scheme.h
@@ -0,0 +1,147 @@
+#ifndef PASSWORD_SCHEME_H
+#define PASSWORD_SCHEME_H
+
+#define AUTH_LOG_MSG_PASSWORD_MISMATCH "Password mismatch"
+
+struct hash_method;
+
+enum password_encoding {
+ PW_ENCODING_NONE,
+ PW_ENCODING_BASE64,
+ PW_ENCODING_HEX
+};
+
+struct password_generate_params {
+ const char *user;
+ unsigned int rounds;
+};
+
+struct password_scheme {
+ const char *name;
+ enum password_encoding default_encoding;
+ /* If non-zero, this is the expected raw password length.
+ It can be used to automatically detect encoding between
+ hex and base64 encoded passwords. */
+ unsigned int raw_password_len;
+
+ int (*password_verify)(const char *plaintext,
+ const struct password_generate_params *params,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r);
+ void (*password_generate)(const char *plaintext,
+ const struct password_generate_params *params,
+ const unsigned char **raw_password_r,
+ size_t *size_r);
+};
+ARRAY_DEFINE_TYPE(password_scheme_p, const struct password_scheme *);
+void password_schemes_get(ARRAY_TYPE(password_scheme_p) *schemes_r);
+
+extern unsigned int password_scheme_encryption_rounds;
+
+/* Returns 1 = matched, 0 = didn't match, -1 = unknown scheme or invalid
+ raw_password */
+int password_verify(const char *plaintext,
+ const struct password_generate_params *params,
+ const char *scheme,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r);
+
+/* Extracts scheme from password, or returns NULL if it isn't found.
+ If auth_request is given, it's used for debug logging. */
+const char *password_get_scheme(const char **password);
+
+/* Decode encoded (base64/hex) password to raw form. Returns 1 if ok,
+ 0 if scheme is unknown, -1 if password is invalid. */
+int password_decode(const char *password, const char *scheme,
+ const unsigned char **raw_password_r, size_t *size_r,
+ const char **error_r);
+
+/* Create password with wanted scheme out of plaintext password and username.
+ Potential base64/hex directives are ignored in scheme. Returns FALSE if
+ the scheme is unknown. */
+bool password_generate(const char *plaintext,
+ const struct password_generate_params *params,
+ const char *scheme,
+ const unsigned char **raw_password_r, size_t *size_r);
+/* Like above, but generate encoded passwords. If hex/base64 directive isn't
+ specified in the scheme, the default encoding for the scheme is used.
+ Returns FALSE if the scheme is unknown. */
+bool password_generate_encoded(const char *plaintext,
+ const struct password_generate_params *params,
+ const char *scheme, const char **password_r);
+
+/* Returns TRUE if schemes are equivalent. */
+bool password_scheme_is_alias(const char *scheme1, const char *scheme2);
+
+/* Try to detect in which scheme crypted password is. Returns the scheme name
+ or NULL if nothing was found. */
+const char *
+password_scheme_detect(const char *plain_password, const char *crypted_password,
+ const struct password_generate_params *params);
+
+void password_scheme_register(const struct password_scheme *scheme);
+void password_scheme_unregister(const struct password_scheme *scheme);
+
+void password_schemes_init(void);
+void password_schemes_deinit(void);
+
+/* some password schemes/algorithms supports a variable number of
+ encryption rounds. */
+void password_set_encryption_rounds(unsigned int rounds);
+
+/* INTERNAL: */
+const char *password_generate_salt(size_t len);
+const char *password_generate_md5_crypt(const char *pw, const char *salt);
+int password_generate_otp(const char *pw, const char *state_data,
+ unsigned int algo, const char **result_r)
+ ATTR_NULL(2);
+
+int crypt_verify(const char *plaintext,
+ const struct password_generate_params *params,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r);
+
+int scram_scheme_parse(const struct hash_method *hmethod, const char *name,
+ const unsigned char *credentials, size_t size,
+ unsigned int *iter_count_r, const char **salt_r,
+ unsigned char stored_key_r[],
+ unsigned char server_key_r[], const char **error_r);
+int scram_verify(const struct hash_method *hmethod, const char *scheme_name,
+ const char *plaintext, const unsigned char *raw_password,
+ size_t size, const char **error_r);
+void scram_generate(const struct hash_method *hmethod, const char *plaintext,
+ const unsigned char **raw_password_r, size_t *size_r);
+
+int scram_sha1_verify(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r ATTR_UNUSED);
+void scram_sha1_generate(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r);
+
+int scram_sha256_verify(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r);
+void scram_sha256_generate(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r);
+
+void pbkdf2_generate(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char **raw_password_r, size_t *size_r);
+int pbkdf2_verify(const char *plaintext,
+ const struct password_generate_params *params ATTR_UNUSED,
+ const unsigned char *raw_password, size_t size,
+ const char **error_r);
+
+/* check which of the algorithms Blowfish, SHA-256 and SHA-512 are
+ supported by the used libc's/glibc's crypt() */
+void password_scheme_register_crypt(void);
+
+#ifdef HAVE_LIBSODIUM
+void password_scheme_register_sodium(void);
+#endif
+
+#endif
diff --git a/src/auth/test-auth-cache.c b/src/auth/test-auth-cache.c
new file mode 100644
index 0000000..ea71b1b
--- /dev/null
+++ b/src/auth/test-auth-cache.c
@@ -0,0 +1,82 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "auth-request.h"
+#include "auth-cache.h"
+#include "test-common.h"
+
+const struct var_expand_table
+auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT + 1] = {
+ /* these 3 must be in this order */
+ { 'u', NULL, "user" },
+ { 'n', NULL, "username" },
+ { 'd', NULL, "domain" },
+
+ { 'a', NULL, NULL },
+ { '\0', NULL, "longb" },
+ { 'c', NULL, "longc" },
+ { '\0', NULL, NULL }
+};
+
+struct var_expand_table *
+auth_request_get_var_expand_table_full(const struct auth_request *auth_request ATTR_UNUSED,
+ const char *username ATTR_UNUSED,
+ auth_request_escape_func_t *escape_func ATTR_UNUSED,
+ unsigned int *count ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+int auth_request_var_expand_with_table(string_t *dest, const char *str,
+ const struct auth_request *auth_request ATTR_UNUSED,
+ const struct var_expand_table *table ATTR_UNUSED,
+ auth_request_escape_func_t *escape_func ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return var_expand(dest, str, auth_request_var_expand_static_tab, error_r);
+}
+
+static void test_auth_cache_parse_key(void)
+{
+ static const struct {
+ const char *in, *out;
+ } tests[] = {
+ { "%n@%d", "%u" },
+ { "%{username}@%{domain}", "%u" },
+ { "%n%d%u", "%u" },
+ { "%n", "%n" },
+ { "%d", "%d" },
+ { "%a%b%u", "%u\t%a\t%b" },
+
+ { "foo%5.5Mabar", "%a" },
+ { "foo%5.5M{longb}bar", "%{longb}" },
+ { "foo%5.5Mcbar", "%c" },
+ { "foo%5.5M{longc}bar", "%c" },
+ { "%a%b", "%a\t%b" },
+ { "%a%{longb}%a", "%a\t%{longb}" },
+ { "%{longc}%c", "%c" },
+ { "%c%a%{longc}%c", "%a\t%c" },
+ { "%a%{env:foo}%{env:foo}%a", "%a\t%{env:foo}\t%{env:foo}" }
+ };
+ const char *cache_key;
+ unsigned int i;
+
+ test_begin("auth cache parse key");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ cache_key = auth_cache_parse_key(pool_datastack_create(),
+ tests[i].in);
+ test_assert(strcmp(cache_key, tests[i].out) == 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_auth_cache_parse_key,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/auth/test-auth-request-fields.c b/src/auth/test-auth-request-fields.c
new file mode 100644
index 0000000..e43a1bf
--- /dev/null
+++ b/src/auth/test-auth-request-fields.c
@@ -0,0 +1,147 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+#include "str.h"
+#include "strescape.h"
+#include "auth-request.h"
+
+struct test_auth_request_field {
+ const char *internal_name;
+ const char *event_field;
+ const char *value;
+};
+
+static const struct test_auth_request_field auth_request_field_names[] = {
+ /* use the order in auth_request_export() */
+#define PREFIX "\t\r\n\001prefix-"
+ { "user", "user", PREFIX"testuser" },
+ { "service", "service", PREFIX"testservice" },
+ { "master-user", "master_user", PREFIX"testmasteruser" },
+ { "original-username", "original_user", PREFIX"testoriguser" },
+ { "requested-login-user", "login_user", PREFIX"testloginuser" },
+ { "lip", "local_ip", "255.254.253.252" },
+ { "rip", "remote_ip", "155.154.153.152" },
+ { "lport", "local_port", "12" },
+ { "rport", "remote_port", "13" },
+ { "real_lip", "real_local_ip", "1.2.3.4" },
+ { "real_rip", "real_remote_ip", "5.6.7.8" },
+ { "real_lport", "real_local_port", "14" },
+ { "real_rport", "real_remote_port", "15" },
+ { "local_name", "local_name", PREFIX"testlocalname" },
+ { "session", "session", PREFIX"testsession" },
+ { "secured", NULL, "" },
+ { "skip-password-check", NULL, "" },
+ { "delayed-credentials", NULL, "" },
+ { "valid-client-cert", NULL, "" },
+ { "no-penalty", NULL, "" },
+ { "successful", NULL, "" },
+ { "mech", "mechanism", "TOKEN" },
+ { "client_id", "client_id", PREFIX"testclientid" },
+ { "passdb_extrafield1", NULL, PREFIX"extravalue1" },
+ { "passdb_extrafield2", NULL, PREFIX"extravalue2" },
+ { "userdb_uextrafield1", NULL, PREFIX"userextravalue1" },
+ { "userdb_uextrafield2", NULL, PREFIX"userextravalue2" },
+};
+
+static struct auth_request *
+test_auth_request_init(const struct mech_module *mech)
+{
+ struct auth_request *request;
+ pool_t pool = pool_alloconly_create("test auth request", 1024);
+
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+ request->event = event_create(NULL);
+ request->mech = mech;
+ auth_request_fields_init(request);
+
+ /* fill out fields that are always exported */
+ request->fields.user = "user";
+ request->fields.original_username = "user";
+ request->fields.service = "service";
+ return request;
+}
+
+static void test_auth_request_deinit(struct auth_request *request)
+{
+ event_unref(&request->event);
+ pool_unref(&request->pool);
+}
+
+static void test_auth_request_fields_list(void)
+{
+ struct auth_request *request =
+ test_auth_request_init(&mech_dovecot_token);
+ string_t *exported = t_str_new(512);
+ for (unsigned int i = 0; i < N_ELEMENTS(auth_request_field_names); i++) {
+ const struct test_auth_request_field *test =
+ &auth_request_field_names[i];
+ test_assert_idx(auth_request_import(request,
+ test->internal_name, test->value), i);
+
+ str_append(exported, test->internal_name);
+ if (test->value[0] != '\0') {
+ str_append_c(exported, '=');
+ str_append_tabescaped(exported, test->value);
+ }
+ str_append_c(exported, '\t');
+
+ if (test->event_field != NULL) {
+ const char *value =
+ event_find_field_recursive_str(request->event, test->event_field);
+ test_assert_idx(null_strcmp(value, test->value) == 0, i);
+ }
+ }
+ str_truncate(exported, str_len(exported)-1);
+
+ string_t *exported2 = t_str_new(512);
+ auth_request_export(request, exported2);
+ test_assert_strcmp(str_c(exported), str_c(exported2));
+
+ test_auth_request_deinit(request);
+}
+
+static bool
+test_auth_request_export_cmp(struct auth_request *request,
+ const char *key, const char *value)
+{
+ string_t *exported = t_str_new(128);
+ str_append(exported, "user=user\tservice=service\toriginal-username=user\t");
+ str_append(exported, key);
+ if (value[0] != '\0') {
+ str_append_c(exported, '=');
+ str_append_tabescaped(exported, value);
+ }
+
+ string_t *exported2 = t_str_new(128);
+ auth_request_export(request, exported2);
+ test_assert_strcmp(str_c(exported), str_c(exported2));
+ return strcmp(str_c(exported), str_c(exported2)) == 0;
+
+}
+
+static void test_auth_request_fields_secured(void)
+{
+ struct auth_request *request = test_auth_request_init(NULL);
+
+ test_assert(auth_request_import(request, "secured", ""));
+ test_assert(test_auth_request_export_cmp(request, "secured", ""));
+ test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "trusted") == 0);
+
+ test_assert(auth_request_import(request, "secured", "tls"));
+ test_assert(test_auth_request_export_cmp(request, "secured", "tls"));
+ test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "TLS") == 0);
+
+ test_assert(auth_request_import(request, "secured", "blah"));
+ test_assert(test_auth_request_export_cmp(request, "secured", ""));
+ test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "trusted") == 0);
+ test_auth_request_deinit(request);
+}
+
+void test_auth_request_fields(void)
+{
+ test_begin("auth request fields");
+ test_auth_request_fields_list();
+ test_auth_request_fields_secured();
+ test_end();
+}
diff --git a/src/auth/test-auth-request-var-expand.c b/src/auth/test-auth-request-var-expand.c
new file mode 100644
index 0000000..e54e2ba
--- /dev/null
+++ b/src/auth/test-auth-request-var-expand.c
@@ -0,0 +1,259 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+#include "str.h"
+#include "auth.h"
+#include "passdb.h"
+#include "userdb.h"
+#include "auth-request.h"
+
+static struct passdb_module test_passdb = {
+ .id = 40
+};
+static struct userdb_module test_userdb = {
+ .id = 41
+};
+
+static struct auth_passdb test_auth_passdb = {
+ .passdb = &test_passdb
+};
+static struct auth_userdb test_auth_userdb = {
+ .userdb = &test_userdb
+};
+
+static struct auth_request default_test_request = {
+ .fields = {
+ .user = "-user@+domain1@+domain2",
+ .service = "-service",
+ .local_ip = { .family = AF_INET },
+ .remote_ip = { .family = AF_INET },
+ .mech_name = "-mech",
+ .secured = AUTH_REQUEST_SECURED,
+ .local_port = 21,
+ .remote_port = 210,
+ .valid_client_cert = TRUE,
+ .requested_login_user = "-loginuser@+logindomain1@+logindomain2",
+ .session_id = "-session",
+ .real_local_ip = { .family = AF_INET },
+ .real_remote_ip = { .family = AF_INET },
+ .real_local_port = 200,
+ .real_remote_port = 201,
+ .master_user = "-masteruser@-masterdomain1@-masterdomain2",
+ .original_username = "-origuser@-origdomain1@-origdomain2",
+ },
+ .client_pid = 54321,
+ .mech_password = "-password",
+
+ .session_pid = 5000,
+
+ .passdb = &test_auth_passdb,
+ .userdb = &test_auth_userdb
+};
+
+static struct auth_request test_request;
+static struct auth_request empty_test_request = { .fields = { .user = "" } };
+
+static const char *
+test_escape(const char *string, const struct auth_request *request)
+{
+ char *dest;
+ unsigned int i;
+
+ test_assert(request == &test_request);
+
+ dest = t_strdup_noconst(string);
+ for (i = 0; dest[i] != '\0'; i++) {
+ if (dest[i] == '-')
+ dest[i] = '+';
+ }
+ return dest;
+}
+
+static bool test_empty_request(string_t *str, const char *input)
+{
+ const struct var_expand_table *tab =
+ auth_request_get_var_expand_table(&empty_test_request, NULL);
+ const char *error;
+
+ str_truncate(str, 0);
+ test_assert(var_expand(str, input, tab, &error) == 1);
+ return strspn(str_c(str), "\n0") == str_len(str);
+}
+
+static void test_auth_request_var_expand_shortlong(void)
+{
+ static const char *test_input_short =
+ "%u\n%n\n%d\n%s\n%h\n%l\n%r\n%l\n%r\n%p\n%w\n%m\n%c\n"
+ "%a\n%b\n%a\n%b\n%k\n";
+ static const char *test_input_long =
+ "%{user}\n%{username}\n%{domain}\n%{service}\n%{home}\n"
+ "%{lip}\n%{rip}\n%{local_ip}\n%{remote_ip}\n"
+ "%{pid}\n%{password}\n%{mech}\n%{secured}\n"
+ "%{lport}\n%{rport}\n%{local_port}\n%{remote_port}\n%{cert}\n";
+ static const char *test_output =
+ /* %{home} is intentionally always expanding to empty */
+ "+user@+domain1@+domain2\n+user\n+domain1@+domain2\n+service\n\n"
+ "7.91.205.21\n73.150.2.210\n7.91.205.21\n73.150.2.210\n"
+ "54321\n+password\n+mech\nsecured\n"
+ "21\n210\n21\n210\nvalid\n";
+ const struct var_expand_table *tab;
+ string_t *str = t_str_new(256);
+ const char *error;
+
+ test_begin("auth request var expand short and long");
+
+ tab = auth_request_get_var_expand_table(&test_request, test_escape);
+ test_assert(var_expand(str, test_input_short, tab, &error) == 1);
+ test_assert(strcmp(str_c(str), test_output) == 0);
+
+ str_truncate(str, 0);
+ test_assert(var_expand(str, test_input_long, tab, &error) == 1);
+ test_assert(strcmp(str_c(str), test_output) == 0);
+
+ /* test with empty input that it won't crash */
+ test_assert(test_empty_request(str, test_input_short));
+ test_assert(test_empty_request(str, test_input_long));
+
+ test_end();
+}
+
+static void test_auth_request_var_expand_flags(void)
+{
+ static const char *test_input = "%!\n%{secured}\n%{cert}\n";
+ string_t *str = t_str_new(10);
+ const char *error;
+
+ test_begin("auth request var expand flags");
+
+ test_request.userdb_lookup = FALSE;
+ test_request.fields.secured = AUTH_REQUEST_SECURED_NONE;
+ test_request.fields.valid_client_cert = FALSE;
+ test_assert(var_expand(str, test_input,
+ auth_request_get_var_expand_table(&test_request, test_escape),
+ &error) == 1);
+ test_assert(strcmp(str_c(str), "40\n\n\n") == 0);
+
+ test_request.userdb_lookup = TRUE;
+ test_request.fields.secured = AUTH_REQUEST_SECURED;
+ test_request.fields.valid_client_cert = TRUE;
+
+ str_truncate(str, 0);
+ test_assert(var_expand(str, test_input,
+ auth_request_get_var_expand_table(&test_request, test_escape),
+ &error) == 1);
+ test_assert(strcmp(str_c(str), "41\nsecured\nvalid\n") == 0);
+
+ test_assert(test_empty_request(str, test_input));
+ test_end();
+}
+
+static void test_auth_request_var_expand_long(void)
+{
+ static const char *test_input =
+ "%{login_user}\n%{login_username}\n%{login_domain}\n%{session}\n"
+ "%{real_lip}\n%{real_rip}\n%{real_lport}\n%{real_rport}\n"
+ "%{real_local_ip}\n%{real_remote_ip}\n"
+ "%{real_local_port}\n%{real_remote_port}\n"
+ "%{master_user}\n%{session_pid}\n"
+ "%{orig_user}\n%{orig_username}\n%{orig_domain}\n";
+ static const char *test_output =
+ "+loginuser@+logindomain1@+logindomain2\n+loginuser\n+logindomain1@+logindomain2\n+session\n"
+ "13.81.174.20\n13.81.174.21\n200\n201\n"
+ "13.81.174.20\n13.81.174.21\n"
+ "200\n201\n"
+ "+masteruser@+masterdomain1@+masterdomain2\n5000\n"
+ "+origuser@+origdomain1@+origdomain2\n+origuser\n+origdomain1@+origdomain2\n";
+ string_t *str = t_str_new(256);
+ const char *error;
+
+ test_begin("auth request var expand long-only");
+
+ test_assert(var_expand(str, test_input,
+ auth_request_get_var_expand_table(&test_request, test_escape),
+ &error) == 1);
+ test_assert(strcmp(str_c(str), test_output) == 0);
+
+ test_assert(test_empty_request(str, test_input));
+ test_end();
+}
+
+static void test_auth_request_var_expand_usernames(void)
+{
+ static const struct {
+ const char *username, *output;
+ } tests[] = {
+ { "-foo", "+foo\n\n\n\n+foo" },
+ { "-foo@-domain", "+foo\n+domain\n+domain\n+domain\n+foo@+domain" },
+ { "-foo@-domain1@-domain2", "+foo\n+domain1@+domain2\n+domain1\n+domain2\n+foo@+domain1@+domain2" }
+ };
+ static const char *test_input =
+ "%{username}\n%{domain}\n%{domain_first}\n%{domain_last}\n%{user}";
+ string_t *str = t_str_new(64);
+ const char *error;
+ unsigned int i;
+
+ test_begin("auth request var expand usernames");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_request.fields.user = t_strdup_noconst(tests[i].username);
+ str_truncate(str, 0);
+ test_assert(var_expand(str, test_input,
+ auth_request_get_var_expand_table(&test_request, test_escape),
+ &error) == 1);
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+ }
+ test_request.fields.user = default_test_request.fields.user;
+ test_end();
+}
+
+static void test_auth_request_var_expand_funcs(void)
+{
+ pool_t pool;
+ const char *value, *error;
+
+ test_begin("auth request var expand funcs");
+
+ pool = pool_alloconly_create("test var expand funcs", 1024);
+ test_request.fields.extra_fields = auth_fields_init(pool);
+ test_request.fields.userdb_reply = auth_fields_init(pool);
+
+ auth_fields_add(test_request.fields.extra_fields, "pkey1", "-pval1", 0);
+ auth_fields_add(test_request.fields.extra_fields, "pkey2", "", 0);
+
+ auth_fields_add(test_request.fields.userdb_reply, "ukey1", "-uval1", 0);
+ auth_fields_add(test_request.fields.userdb_reply, "ukey2", "", 0);
+
+ test_assert(t_auth_request_var_expand(
+ "%{passdb:pkey1}\n%{passdb:pkey1:default1}\n"
+ "%{passdb:pkey2}\n%{passdb:pkey2:default2}\n"
+ "%{passdb:pkey3}\n%{passdb:pkey3:default3}\n"
+ "%{passdb:ukey1}\n%{passdb:ukey1:default4}\n",
+ &test_request, test_escape, &value, &error) == 1);
+ test_assert(strcmp(value, "+pval1\n+pval1\n\n\n\ndefault3\n\ndefault4\n") == 0);
+
+ test_assert(t_auth_request_var_expand(
+ "%{userdb:ukey1}\n%{userdb:ukey1:default1}\n"
+ "%{userdb:ukey2}\n%{userdb:ukey2:default2}\n"
+ "%{userdb:ukey3}\n%{userdb:ukey3:default3}\n"
+ "%{userdb:pkey1}\n%{userdb:pkey1:default4}\n",
+ &test_request, test_escape, &value, &error) == 1);
+ test_assert(strcmp(value, "+uval1\n+uval1\n\n\n\ndefault3\n\ndefault4\n") == 0);
+
+ pool_unref(&pool);
+ test_end();
+}
+
+void test_auth_request_var_expand(void)
+{
+ default_test_request.fields.local_ip.u.ip4.s_addr = htonl(123456789);
+ default_test_request.fields.remote_ip.u.ip4.s_addr = htonl(1234567890);
+ default_test_request.fields.real_local_ip.u.ip4.s_addr = htonl(223456788);
+ default_test_request.fields.real_remote_ip.u.ip4.s_addr = htonl(223456789);
+
+ test_request = default_test_request;
+
+ test_auth_request_var_expand_shortlong();
+ test_auth_request_var_expand_flags();
+ test_auth_request_var_expand_long();
+ test_auth_request_var_expand_usernames();
+ test_auth_request_var_expand_funcs();
+}
diff --git a/src/auth/test-auth.h b/src/auth/test-auth.h
new file mode 100644
index 0000000..4641fe3
--- /dev/null
+++ b/src/auth/test-auth.h
@@ -0,0 +1,25 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef TEST_AUTH_H
+#define TEST_AUTH_H 1
+
+#define AUTH_REQUEST_FIELDS_CONST
+
+#include "lib.h"
+#include "test-common.h"
+
+struct auth_passdb;
+
+extern struct auth_passdb_settings mock_passdb_set;
+
+void test_auth_request_var_expand(void);
+void test_auth_request_fields(void);
+void test_db_dict_parse_cache_key(void);
+void test_username_filter(void);
+void test_db_lua(void);
+struct auth_passdb *passdb_mock(void);
+void passdb_mock_mod_init(void);
+void passdb_mock_mod_deinit(void);
+
+#endif
+
diff --git a/src/auth/test-db-dict.c b/src/auth/test-db-dict.c
new file mode 100644
index 0000000..59b0e78
--- /dev/null
+++ b/src/auth/test-db-dict.c
@@ -0,0 +1,43 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "auth-settings.h"
+#include "test-auth.h"
+#include "array.h"
+#include "db-dict.h"
+
+void test_db_dict_parse_cache_key(void)
+{
+ struct db_dict_key keys[] = {
+ { "key0", "%d and %n", NULL, NULL, 0 },
+ { "key1", "%{foo}%r%{bar}", NULL, NULL, 0 },
+ { "key2", "%{test1}/path", NULL, NULL, 0 },
+ { "key3", "path2/%{test2}", NULL, NULL, 0 },
+ { "key4", "%{plop}", NULL, NULL, 0 },
+ { "key5", "%{unused}", NULL, NULL, 0 }
+ };
+ struct db_dict_field fields[] = {
+ { "name1", "hello %{dict:key0} %l and %{dict:key1}" },
+ { "name2", "%{dict:key2} also %{extra} plus" }
+ };
+ const struct db_dict_key *objects[] = {
+ &keys[3], &keys[4]
+ };
+ buffer_t keybuf, fieldbuf, objectbuf;
+ ARRAY_TYPE(db_dict_key) keyarr;
+ ARRAY_TYPE(db_dict_field) fieldarr;
+ ARRAY_TYPE(db_dict_key_p) objectarr;
+
+ test_begin("db dict parse cache key");
+
+ buffer_create_from_const_data(&keybuf, keys, sizeof(keys));
+ buffer_create_from_const_data(&fieldbuf, fields, sizeof(fields));
+ buffer_create_from_const_data(&objectbuf, objects, sizeof(objects));
+ array_create_from_buffer(&keyarr, &keybuf, sizeof(keys[0]));
+ array_create_from_buffer(&fieldarr, &fieldbuf, sizeof(fields[0]));
+ array_create_from_buffer(&objectarr, &objectbuf, sizeof(objects[0]));
+
+ test_assert(strcmp(db_dict_parse_cache_key(&keyarr, &fieldarr, &objectarr),
+ "\t%d and %n\t%l\t%{foo}%r%{bar}\t%{test1}/path\t%{extra}\tpath2/%{test2}\t%{plop}") == 0);
+ test_end();
+}
diff --git a/src/auth/test-libpassword.c b/src/auth/test-libpassword.c
new file mode 100644
index 0000000..114daf6
--- /dev/null
+++ b/src/auth/test-libpassword.c
@@ -0,0 +1,137 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "password-scheme.h"
+
+#ifdef HAVE_LIBSODIUM
+#include <sodium.h>
+#endif
+
+static struct {
+ const char *scheme_generated;
+ const char *scheme_detected;
+} known_non_aliases[] = {
+ { "MD5", "DES-CRYPT" },
+ { "MD5-CRYPT", "DES-CRYPT" },
+ { "ARGON2ID", "ARGON2I" },
+};
+
+/* some algorithms are detected as something other, because they are compatible
+ but not considered aliases by dovecot. treat those here to avoid false errors. */
+static bool schemes_are_known_non_alias(const char *generated, const char *detected)
+{
+ for(size_t i = 0; i < N_ELEMENTS(known_non_aliases); i++) {
+ if (strcmp(known_non_aliases[i].scheme_generated, generated) == 0 &&
+ strcmp(known_non_aliases[i].scheme_detected, detected) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+test_password_scheme(const char *scheme, const char *crypted,
+ const char *plaintext)
+{
+ struct password_generate_params params = {
+ .user = "testuser1",
+ .rounds = 0,
+ };
+ const unsigned char *raw_password;
+ size_t siz;
+ const char *error, *scheme2;
+
+ test_begin(t_strdup_printf("password scheme(%s)", scheme));
+
+ test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0);
+ test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1);
+ test_assert(password_verify(plaintext, &params, scheme, raw_password, siz, &error) == 1);
+
+ test_assert(password_generate_encoded(plaintext, &params, scheme, &crypted));
+ crypted = t_strdup_printf("{%s}%s", scheme, crypted);
+ test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0);
+ test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1);
+ test_assert(password_verify(plaintext, &params, scheme, raw_password, siz, &error) == 1);
+
+ scheme2 = password_scheme_detect(plaintext, crypted, &params);
+
+ test_assert(scheme2 != NULL &&
+ (password_scheme_is_alias(scheme, scheme2) ||
+ schemes_are_known_non_alias(scheme, scheme2)));
+
+ test_end();
+}
+
+static void test_password_failures(void)
+{
+ const char *scheme = "PLAIN";
+ const char *crypted = "{PLAIN}invalid";
+ const char *plaintext = "test";
+
+ struct password_generate_params params = {
+ .user = "testuser1",
+ .rounds = 0,
+ };
+ const unsigned char *raw_password;
+ size_t siz;
+ const char *error;
+
+ test_begin("password scheme failures");
+
+ /* wrong password */
+ test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0);
+ test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1);
+ test_assert(password_verify(plaintext, &params, scheme, raw_password, siz, &error) == 0);
+
+ /* unknown scheme */
+ crypted = "{INVALID}invalid";
+ scheme = password_get_scheme(&crypted);
+ test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 0);
+
+ /* crypt with empty value */
+ test_assert(password_verify(plaintext, &params, "CRYPT", NULL, 0, &error) == 0);
+
+ test_end();
+}
+
+static void test_password_schemes(void)
+{
+ test_password_scheme("PLAIN", "{PLAIN}test", "test");
+ test_password_scheme("CRYPT", "{CRYPT}//EsnG9FLTKjo", "test");
+ test_password_scheme("PLAIN-MD4", "{PLAIN-MD4}db346d691d7acc4dc2625db19f9e3f52", "test");
+ test_password_scheme("MD5", "{MD5}$1$wmyrgRuV$kImF6.9MAFQNHe23kq5vI/", "test");
+ test_password_scheme("SHA1", "{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M=", "test");
+ test_password_scheme("SMD5", "{SMD5}JTu1KRwptKZJg/RLd+6Vn5GUd0M=", "test");
+ test_password_scheme("LDAP-MD5", "{LDAP-MD5}CY9rzUYh03PK3k6DJie09g==", "test");
+ test_password_scheme("SHA256", "{SHA256}n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "test");
+ test_password_scheme("SHA512", "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==", "test");
+ test_password_scheme("SSHA", "{SSHA}H/zrDv8FXUu1JmwvVYijfrYEF34jVZcO", "test");
+ test_password_scheme("MD5-CRYPT", "{MD5-CRYPT}$1$GgvxyNz8$OjZhLh4P.gF1lxYEbLZ3e/", "test");
+ test_password_scheme("OTP", "{OTP}sha1 1024 ae6b49aa481f7233 f69fc7f98b8fbf54", "test");
+ test_password_scheme("PBKDF2", "{PBKDF2}$1$bUnT4Pl7yFtYX0KU$5000$50a83cafdc517b9f46519415e53c6a858908680a", "test");
+ test_password_scheme("CRAM-MD5", "{CRAM-MD5}e02d374fde0dc75a17a557039a3a5338c7743304777dccd376f332bee68d2cf6", "test");
+ test_password_scheme("DIGEST-MD5", "{DIGEST-MD5}77c1a8c437c9b08ba2f460fe5d58db5d", "test");
+ test_password_scheme("SCRAM-SHA-1", "{SCRAM-SHA-1}4096,GetyLXdBuHzf1FWf8SLz2Q==,NA/OqmF4hhrsrB9KR7po+dliTGM=,QBiURvQaE6H6qYTmeghDHLANBFQ=", "test");
+ test_password_scheme("SCRAM-SHA-256", "{SCRAM-SHA-256}4096,LfNGSFqiFykEZ1xDAYlnKQ==,"
+ "HACNf9CII7cMz3XjRy/Oh3Ae2LHApoDyNw74d3YtFws=,"
+ "AQH0j7Hf8J12g8eNBadvzlNB2am3PxgNwFCFd3RxEaw=",
+ "test");
+ test_password_scheme("BLF-CRYPT", "{BLF-CRYPT}$2y$05$11ipvo5dR6CwkzwmhwM26OXgzXwhV2PyPuLV.Qi31ILcRcThQpEiW", "test");
+#ifdef HAVE_LIBSODIUM
+ test_password_scheme("ARGON2I", "{ARGON2I}$argon2i$v=19$m=32768,t=4,p=1$f2iuP4aUeNMrgu34fhOkkg$1XSZZMWlIs0zmE+snlUIcLADO3GXbA2O/hsQmmc317k", "test");
+#ifdef crypto_pwhash_ALG_ARGON2ID13
+ test_password_scheme("ARGON2ID", "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$vBb99oJ12p3WAdYlaMHz1A$jtFOtbo/sYV9OSlTxDo/nVNq3uArHd5GJSEx0ty85Cc", "test");
+#endif
+#endif
+}
+
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_password_schemes,
+ test_password_failures,
+ NULL
+ };
+ password_schemes_init();
+ return test_run(test_functions);
+}
diff --git a/src/auth/test-lua.c b/src/auth/test-lua.c
new file mode 100644
index 0000000..c794c73
--- /dev/null
+++ b/src/auth/test-lua.c
@@ -0,0 +1,182 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+
+#ifdef BUILTIN_LUA
+#include "istream.h"
+#include "auth-settings.h"
+#include "auth-request.h"
+#include "auth-fields.h"
+#include "db-lua.h"
+
+static struct auth_settings test_lua_auth_set = {
+ .master_user_separator = "",
+ .default_realm = "",
+ .username_format = "",
+};
+
+static struct auth_request *test_db_lua_auth_request_new(void)
+{
+ const char *error;
+ struct auth_request *req = auth_request_new_dummy(NULL);
+ req->set = global_auth_settings;
+ struct event *event = event_create(req->event);
+ array_push_back(&req->authdb_event, &event);
+ req->passdb = passdb_mock();
+ test_assert(auth_request_set_username(req, "testuser", &error));
+ return req;
+}
+
+static void test_db_lua_auth_verify(void)
+{
+ struct auth_request *req = test_db_lua_auth_request_new();
+
+ static const char *luascript =
+"function auth_password_verify(req, pass)\n"
+" req:log_debug(\"user \" .. req.user)\n"
+" if req:password_verify(\"{SHA256-CRYPT}$5$XtUywQCSjW0zAJgE$YjuPKQnsLuH4iE9kranZyy1lbil5IrRUfs7X6EyJyG1\", pass) then\n"
+" return dovecot.auth.PASSDB_RESULT_OK, {}\n"
+" end\n"
+"end\n";
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+
+ test_begin("auth db lua passdb_verify");
+
+ test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0);
+ if (script != NULL) {
+ test_assert(auth_lua_script_init(script, &error) == 0);
+ test_assert(auth_lua_call_password_verify(script, req, "password", &error) == 1);
+ dlua_script_unref(&script);
+ }
+ if (error != NULL) {
+ i_error("Test failed: %s", error);
+ }
+ i_free(req->passdb);
+
+ auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK);
+ auth_request_unref(&req);
+
+ test_end();
+}
+
+static void test_db_lua_auth_lookup_numberish_value(void)
+{
+ const char *scheme,*pass;
+
+ struct auth_request *req = test_db_lua_auth_request_new();
+
+ static const char *luascript =
+"function auth_passdb_lookup(req)\n"
+" local fields = {}\n"
+" fields[\"user\"] = \"01234\"\n"
+" return dovecot.auth.PASSDB_RESULT_OK, fields\n"
+"end\n";
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+
+ test_begin("auth db lua passdb_lookup");
+
+ test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0);
+ if (script != NULL) {
+ test_assert(auth_lua_script_init(script, &error) == 0);
+ test_assert(auth_lua_call_passdb_lookup(script, req, &scheme, &pass, &error) == 1);
+ test_assert(strcmp(req->fields.user, "01234") == 0);
+ dlua_script_unref(&script);
+ }
+ if (error != NULL) {
+ i_error("Test failed: %s", error);
+ }
+ i_free(req->passdb);
+ auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK);
+ auth_request_unref(&req);
+
+ test_end();
+}
+
+static void test_db_lua_auth_lookup(void)
+{
+ const char *scheme,*pass;
+
+ struct auth_request *req = test_db_lua_auth_request_new();
+
+ static const char *luascript =
+"function auth_passdb_lookup(req)\n"
+" req:log_debug(\"user \" .. req.user)\n"
+" return dovecot.auth.PASSDB_RESULT_OK, req:var_expand(\"password=pass\")\n"
+"end\n";
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+
+ test_begin("auth db lua passdb_lookup");
+
+ test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0);
+ if (script != NULL) {
+ test_assert(auth_lua_script_init(script, &error) == 0);
+ test_assert(auth_lua_call_passdb_lookup(script, req, &scheme, &pass, &error) == 1);
+ dlua_script_unref(&script);
+ test_assert_strcmp(pass, "pass");
+ }
+ if (error != NULL) {
+ i_error("Test failed: %s", error);
+ }
+ i_free(req->passdb);
+ auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK);
+ auth_request_unref(&req);
+
+ test_end();
+}
+
+static void test_db_lua_auth_lookup_bad_keyname(void)
+{
+ const char *scheme,*pass;
+
+ struct auth_request *req = test_db_lua_auth_request_new();
+
+ static const char *luascript =
+"function auth_passdb_lookup(req)\n"
+" req:log_debug(\"user \" .. req.user)\n"
+" return dovecot.auth.PASSDB_RESULT_OK, {"
+"[\"\"]=\"1\","
+"[\"\\t\"]=\"2\","
+"[\"\\n\"]=\"3\","
+"[\"password\"]=\"pass\""
+"}\n"
+"end\n";
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+
+ test_begin("auth db lua bad keyname");
+
+ test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0);
+ if (script != NULL) {
+ test_assert(auth_lua_script_init(script, &error) == 0);
+ test_expect_error_string_n_times("db-lua: Field key", 3);
+ test_assert(auth_lua_call_passdb_lookup(script, req, &scheme, &pass, &error) == 1);
+ dlua_script_unref(&script);
+ test_assert_strcmp(pass, "pass");
+ const ARRAY_TYPE(auth_field) *fields =
+ auth_fields_export(req->fields.extra_fields);
+ test_assert(array_count(fields) == 0);
+ }
+ if (error != NULL) {
+ i_error("Test failed: %s", error);
+ }
+ i_free(req->passdb);
+ auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK);
+ auth_request_unref(&req);
+
+ test_end();
+}
+
+void test_db_lua(void) {
+ memset(test_lua_auth_set.username_chars_map, 0xff,
+ sizeof(test_lua_auth_set.username_chars_map));
+ global_auth_settings = &test_lua_auth_set;
+ test_db_lua_auth_lookup();
+ test_db_lua_auth_lookup_numberish_value();
+ test_db_lua_auth_verify();
+ test_db_lua_auth_lookup_bad_keyname();
+}
+
+#endif
diff --git a/src/auth/test-main.c b/src/auth/test-main.c
new file mode 100644
index 0000000..762c34b
--- /dev/null
+++ b/src/auth/test-main.c
@@ -0,0 +1,39 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "auth-settings.h"
+#include "test-common.h"
+#include "test-auth.h"
+#include "password-scheme.h"
+#include "passdb.h"
+
+int main(int argc, const char *argv[])
+{
+ const char *match = "";
+ int ret;
+ static const struct named_test test_functions[] = {
+ TEST_NAMED(test_auth_request_var_expand)
+ TEST_NAMED(test_auth_request_fields)
+ TEST_NAMED(test_db_dict_parse_cache_key)
+ TEST_NAMED(test_username_filter)
+#if defined(BUILTIN_LUA)
+ TEST_NAMED(test_db_lua)
+#endif
+ { NULL, NULL }
+ };
+
+ password_schemes_init();
+ passdbs_init();
+ passdb_mock_mod_init();
+
+ if (argc > 2 && strcasecmp(argv[1], "--match") == 0)
+ match = argv[2];
+
+ ret = test_run_named(test_functions, match);
+
+ passdb_mock_mod_deinit();
+ password_schemes_deinit();
+ passdbs_deinit();
+
+ return ret;
+}
diff --git a/src/auth/test-mech.c b/src/auth/test-mech.c
new file mode 100644
index 0000000..8a2c7b3
--- /dev/null
+++ b/src/auth/test-mech.c
@@ -0,0 +1,412 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+#include "auth.h"
+#include "str.h"
+#include "auth-common.h"
+#include "auth-request.h"
+#include "auth-request-handler-private.h"
+#include "auth-settings.h"
+#include "mech-digest-md5-private.h"
+#include "otp.h"
+#include "mech-otp-common.h"
+#include "settings-parser.h"
+#include "password-scheme.h"
+#include "auth-token.h"
+
+#include <unistd.h>
+#include <time.h>
+
+#define UCHAR_LEN(str) (const unsigned char *)(str), sizeof(str)-1
+
+extern const struct mech_module mech_anonymous;
+extern const struct mech_module mech_apop;
+extern const struct mech_module mech_cram_md5;
+extern const struct mech_module mech_digest_md5;
+extern const struct mech_module mech_dovecot_token;
+extern const struct mech_module mech_external;
+extern const struct mech_module mech_login;
+extern const struct mech_module mech_oauthbearer;
+extern const struct mech_module mech_otp;
+extern const struct mech_module mech_plain;
+extern const struct mech_module mech_scram_sha1;
+extern const struct mech_module mech_scram_sha256;
+extern const struct mech_module mech_xoauth2;
+
+static struct auth_settings set;
+static struct mechanisms_register *mech_reg;
+
+struct test_case {
+ const struct mech_module *mech;
+ const unsigned char *in;
+ size_t len;
+ const char *username;
+ const char *expect_error;
+ bool success;
+ bool set_username_before_test;
+ bool set_cert_username;
+};
+
+static void
+verify_plain_continue_mock_callback(struct auth_request *request,
+ verify_plain_callback_t *callback)
+{
+ request->passdb_success = TRUE;
+ callback(PASSDB_RESULT_OK, request);
+}
+
+static void
+request_handler_reply_mock_callback(struct auth_request *request,
+ enum auth_client_result result,
+ const void *auth_reply ATTR_UNUSED,
+ size_t reply_size ATTR_UNUSED)
+{
+ request->failed = result != AUTH_CLIENT_RESULT_SUCCESS;
+
+ if (request->passdb_result == PASSDB_RESULT_OK)
+ request->failed = FALSE;
+ else if (request->mech == &mech_otp) {
+ if (null_strcmp(request->fields.user, "otp_phase_2") == 0)
+ request->failed = FALSE;
+ } else if (request->mech == &mech_oauthbearer) {
+ }
+};
+
+static void
+request_handler_reply_continue_mock_callback(struct auth_request *request,
+ const void *reply,
+ size_t reply_size)
+{
+ request->context = p_strndup(request->pool, reply, reply_size);
+}
+
+static void
+auth_client_request_mock_callback(const char *reply ATTR_UNUSED,
+ struct auth_client_connection *conn ATTR_UNUSED)
+{
+}
+
+static void test_mechs_init(void)
+{
+ const char *const services[] = {NULL};
+ process_start_time = time(NULL);
+
+ /* Copy default settings */
+ set = *(const struct auth_settings *)auth_setting_parser_info.defaults;
+ global_auth_settings = &set;
+ global_auth_settings->base_dir = ".";
+ memset((&set)->username_chars_map, 1, sizeof((&set)->username_chars_map));
+ set.username_format = "";
+
+ t_array_init(&set.passdbs, 2);
+ struct auth_passdb_settings *mock_set = t_new(struct auth_passdb_settings, 1);
+ *mock_set = mock_passdb_set;
+ array_push_back(&set.passdbs, &mock_set);
+ mock_set = t_new(struct auth_passdb_settings, 1);
+ *mock_set = mock_passdb_set;
+ mock_set->master = TRUE;
+ array_push_back(&set.passdbs, &mock_set);
+ t_array_init(&set.userdbs, 1);
+
+ /* Disable stats */
+ set.stats = FALSE;
+
+ /* For tests of digest-md5. */
+ set.realms_arr = t_strsplit_spaces("example.com ", " ");
+ /* For tests of mech-anonymous. */
+ set.anonymous_username = "anonuser";
+
+ mech_init(global_auth_settings);
+ mech_reg = mech_register_init(global_auth_settings);
+ passdbs_init();
+ userdbs_init();
+ passdb_mock_mod_init();
+ password_schemes_init();
+
+ auths_preinit(&set, pool_datastack_create(), mech_reg, services);
+ auths_init();
+ auth_token_init();
+}
+
+static void test_mech_prepare_request(struct auth_request **request_r,
+ const struct mech_module *mech,
+ struct auth_request_handler *handler,
+ unsigned int running_test,
+ const struct test_case *test_case)
+{
+ global_auth_settings->ssl_username_from_cert = test_case->set_cert_username;
+ struct auth *auth = auth_default_service();
+
+ struct auth_request *request = auth_request_new(mech, NULL);
+ request->handler = handler;
+ request->id = running_test+1;
+ request->mech_password = NULL;
+ request->state = AUTH_REQUEST_STATE_NEW;
+ request->set = global_auth_settings;
+ request->connect_uid = running_test;
+ request->passdb = auth->passdbs;
+ request->userdb = auth->userdbs;
+ handler->refcount = 1;
+
+ auth_fields_add(request->fields.extra_fields, "nodelay", "", 0);
+ auth_request_ref(request);
+ auth_request_state_count[AUTH_REQUEST_STATE_NEW] = 1;
+
+ if (test_case->set_username_before_test || test_case->set_cert_username)
+ request->fields.user = p_strdup(request->pool, test_case->username);
+ if (test_case->set_cert_username)
+ request->fields.cert_username = TRUE;
+
+ *request_r = request;
+}
+
+static void test_mech_handle_challenge(struct auth_request *request,
+ const unsigned char *in,
+ size_t in_len,
+ unsigned int running_test,
+ bool expected_success)
+{
+ string_t *out = t_str_new(16);
+ str_append_data(out, in, in_len);
+ const char *challenge = request->context;
+ if (request->mech == &mech_login) {
+ /* We do not care about any specific password just give
+ * the username input as password also in case it's wanted. */
+ if (expected_success)
+ test_assert_strcmp_idx(challenge, "Password:", running_test);
+ else
+ test_assert_strcmp_idx(challenge, "Username:", running_test);
+ } else if (request->mech == &mech_cram_md5 && *in != '\0') {
+ str_truncate(out, 0);
+ str_append(out, "testuser b913a602c7eda7a495b4e6e7334d3890");
+ } else if (request->mech == &mech_digest_md5) {
+ struct digest_auth_request *digest_request =
+ (struct digest_auth_request *) request;
+ digest_request->nonce = "OA6MG9tEQGm2hh";
+ }
+ auth_request_continue(request, out->data, out->used);
+}
+
+static inline const unsigned char *
+test_mech_construct_apop_challenge(unsigned int connect_uid, size_t *len_r)
+{
+ string_t *apop_challenge = t_str_new(128);
+
+ str_printfa(apop_challenge,"<%lx.%lx.%"PRIxTIME_T".", (unsigned long)getpid(),
+ (unsigned long)connect_uid, process_start_time+10);
+ str_append_data(apop_challenge, "\0testuser\0responseoflen16-", 26);
+ *len_r = apop_challenge->used;
+ return apop_challenge->data;
+}
+
+static void test_mechs(void)
+{
+ static struct auth_request_handler handler = {
+ .callback = auth_client_request_mock_callback,
+ .reply_callback = request_handler_reply_mock_callback,
+ .reply_continue_callback = request_handler_reply_continue_mock_callback,
+ .verify_plain_continue_callback = verify_plain_continue_mock_callback,
+ };
+
+ static struct test_case tests[] = {
+ /* Expected to be successful */
+ {&mech_anonymous, UCHAR_LEN("\0any \0 bad \0 content"), "anonuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_apop, NULL, 0, "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_cram_md5, UCHAR_LEN("testuser b913a602c7eda7a495b4e6e7334d3890"), "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\""), "testuser@example.com", NULL,TRUE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "testuser@example.com", NULL, TRUE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"test\xc3\xbaser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "test\xc3\xbaser@example.com", NULL, TRUE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"test\xc3\xbaser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",charset=\"utf-8\",cipher=unsupported,nc=00000001,digest-uri=imap/server.com,response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "test\xc3\xbaser@example.com", NULL, TRUE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"testuser\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",charset=\"utf-8\",cipher=unsupported,nc=00000001,digest-uri=imap/server.com,response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "testuser@example.com", NULL, TRUE, FALSE, FALSE},
+ {&mech_external, UCHAR_LEN(""), "testuser", NULL, TRUE, TRUE, TRUE},
+ {&mech_dovecot_token, NULL, 0, "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_login, UCHAR_LEN("testuser"), "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("\0testuser\0testpass"), "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("normaluser\0masteruser\0masterpass"), "masteruser", NULL, TRUE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("normaluser\0normaluser\0masterpass"), "normaluser", NULL, TRUE, FALSE, FALSE},
+ {&mech_otp, UCHAR_LEN("hex:5Bf0 75d9 959d 036f"), "otp_phase_2", NULL, TRUE, TRUE, FALSE},
+ {&mech_otp, UCHAR_LEN("word:BOND FOGY DRAB NE RISE MART"), "otp_phase_2", NULL, TRUE, TRUE, FALSE},
+ {&mech_otp, UCHAR_LEN("init-hex:f6bd 6b33 89b8 7203:md5 499 ke6118:23d1 b253 5ae0 2b7e"), "otp_phase_2", NULL, TRUE, TRUE, FALSE},
+ {&mech_otp, UCHAR_LEN("init-word:END KERN BALM NICK EROS WAVY:md5 499 ke1235:BABY FAIN OILY NIL TIDY DADE"), "otp_phase_2", NULL , TRUE, TRUE, FALSE},
+ {&mech_oauthbearer, UCHAR_LEN("n,a=testuser,p=cHJvb2Y=,f=nonstandart\x01host=server\x01port=143\x01""auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==\x01\x01"), "testuser", NULL, FALSE, TRUE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("n,,n=testuser,r=rOprNGfwEbeRWgbNEkqO"), "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_scram_sha256, UCHAR_LEN("n,,n=testuser,r=rOprNGfwEbeRWgbNEkqO"), "testuser", NULL, TRUE, FALSE, FALSE},
+ {&mech_xoauth2, UCHAR_LEN("user=testuser\x01""auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"), "testuser", NULL, TRUE, FALSE, FALSE},
+
+ /* Below tests are expected to fail */
+ /* Empty input tests*/
+ {&mech_apop, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_cram_md5, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_dovecot_token, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_external, UCHAR_LEN(""), "testuser", NULL, FALSE, TRUE, FALSE},
+ {&mech_external, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_login, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_otp, UCHAR_LEN(""), NULL, "invalid input", FALSE, FALSE, FALSE},
+ {&mech_otp, UCHAR_LEN(""), "testuser", "invalid input", FALSE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_oauthbearer, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_xoauth2, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha256, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE},
+
+ /* Bad input tests*/
+ {&mech_apop, UCHAR_LEN("1.1.1\0test\0user\0response"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_apop, UCHAR_LEN("1.1.1\0testuser\0tooshort"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_apop, UCHAR_LEN("1.1.1\0testuser\0responseoflen16-"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_apop, UCHAR_LEN("1.1.1"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_otp, UCHAR_LEN("somebody\0testuser"), "testuser", "otp(testuser): unsupported response type", FALSE, TRUE, FALSE},
+ {&mech_cram_md5, UCHAR_LEN("testuser\0response"), "testuser", NULL, FALSE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("testuser\0"), "testuser", NULL, FALSE, FALSE, FALSE},
+
+ /* Covering most of the digest md5 parsing */
+ {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",cnonce=\"OA6MHXh6VqTrRk\",response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("realm=\"example.com\",cnonce=\"OA6MHXh6VqTrRk\",nonce=\"OA6MG9tEQGm2hh\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\", nonce=\"OA6MG9tEQGm2hh\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("qop=\"auth-int\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("qop=\"auth-int\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("qop=\"auth-conf\",\"cipher=rc4\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("cnonce=\"OA6MHXh6VqTrRk\",cnonce=\"OA6MHXh6VqTrRk\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("cnonce=\"\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("nonce=\"not matching\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("nc=00000001,nc=00000002"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("nc=NAN"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("nc=00000002"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("cipher=unsupported"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("digest-uri="), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("username=\"a\",username=\"b\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("response=broken"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("maxbuf=32,maxbuf=1024"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("maxbuf=broken"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("authzid=\"somebody\",authzid=\"else\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("authzid=\"\""), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("charset=unsupported"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_digest_md5, UCHAR_LEN("qop=unsupported"), NULL, NULL, FALSE, FALSE, FALSE},
+
+ /* Too much nuls */
+ {&mech_dovecot_token, UCHAR_LEN("service\0pid\0fail\0se\0ssion_id\0deadbeef"), NULL , NULL, FALSE, FALSE, FALSE},
+ {&mech_login, UCHAR_LEN("test user\0user"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_oauthbearer, UCHAR_LEN("n,a==testuser,\x01""auth=Bearer token\x01\x01"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_oauthbearer, UCHAR_LEN("n,a=testuser,f=non-standard\x01""auth=Bearer token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE},
+ {&mech_oauthbearer, UCHAR_LEN("n,a=testuser\x01""auth=token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE},
+ {&mech_xoauth2, UCHAR_LEN("testuser\x01auth=Bearer token\x01\x01"), NULL, NULL, FALSE, FALSE, FALSE},
+ /* does not start with [B|b]earer */
+ {&mech_xoauth2, UCHAR_LEN("user=testuser\x01""auth=token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE},
+ /* Too much nuls */
+ {&mech_plain, UCHAR_LEN("\0fa\0il\0ing\0withthis"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("failingwiththis"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_plain, UCHAR_LEN("failing\0withthis"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_otp, UCHAR_LEN("someb\0ody\0testuser"), NULL, "invalid input", FALSE, FALSE, FALSE},
+ /* phase 2 */
+ {&mech_otp, UCHAR_LEN("someb\0ody\0testuser"), "testuser", "otp(testuser): unsupported response type", FALSE, TRUE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("iws0X8v3Bz2T0CJGbJQyF0X+HI4Ts=,,,,"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("n,a=masteruser,,"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("n,a==masteruser,,"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("n,,m=testuser,,"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha1, UCHAR_LEN("broken\0input"), NULL, NULL, FALSE, FALSE, FALSE},
+ {&mech_scram_sha256, UCHAR_LEN("broken\0input"), NULL, NULL, FALSE, FALSE, FALSE},
+ };
+
+ test_mechs_init();
+
+ string_t *d_token = t_str_new(32);
+ str_append_data(d_token, UCHAR_LEN("service\0pid\0testuser\0session\0"));
+ str_append(d_token, auth_token_get("service","pid","testuser","session"));
+
+ for (unsigned int running_test = 0; running_test < N_ELEMENTS(tests);
+ running_test++) T_BEGIN {
+ struct test_case *test_case = &tests[running_test];
+ const struct mech_module *mech = test_case->mech;
+ struct auth_request *request;
+ const char *testname = t_strdup_printf("auth mech %s %d/%zu",
+ mech->mech_name,
+ running_test+1,
+ N_ELEMENTS(tests));
+ test_begin(testname);
+
+ test_mech_prepare_request(&request, mech, &handler, running_test,
+ test_case);
+
+ if (mech == &mech_apop && test_case->in == NULL)
+ test_case->in =
+ test_mech_construct_apop_challenge(request->connect_uid,
+ &test_case->len);
+ if (mech == &mech_dovecot_token && test_case->in == NULL) {
+ test_case->in = d_token->data;
+ test_case->len = d_token->used;
+ }
+
+ if (test_case->expect_error != NULL)
+ test_expect_error_string(test_case->expect_error);
+
+ request->state = AUTH_REQUEST_STATE_NEW;
+ unsigned char *input_dup = test_case->len == 0 ? NULL :
+ i_memdup(test_case->in, test_case->len);
+ request->initial_response = input_dup;
+ request->initial_response_len = test_case->len;
+ auth_request_initial(request);
+
+ const char *challenge = request->context;
+
+ if (challenge != NULL) {
+ test_mech_handle_challenge(request, test_case->in,
+ test_case->len,
+ running_test,
+ test_case->success);
+ }
+
+ const char *username = request->fields.user;
+
+ if (request->fields.master_user != NULL)
+ username = request->fields.master_user;
+
+ if (!test_case->set_username_before_test && test_case->success) {
+ /* If the username was set by the test logic, do not
+ * compare it as it does not give any additional
+ * information */
+ test_assert_strcmp_idx(test_case->username, username,
+ running_test);
+ } else if (!test_case->set_username_before_test && !test_case->success) {
+ /* If the username is not set by the testlogic and we
+ * expect failure, verify that the mechanism failed by
+ * checking that the username is not set */
+ test_assert_idx(username == NULL, running_test);
+ }
+
+ if (test_case->success)
+ test_assert_idx(request->failed == FALSE, running_test);
+ else
+ test_assert_idx(request->failed == TRUE, running_test);
+
+ event_unref(&request->event);
+ event_unref(&request->mech_event);
+ i_free(input_dup);
+ mech->auth_free(request);
+
+ test_end();
+ } T_END;
+ mech_otp_deinit();
+ auths_deinit();
+ auth_token_deinit();
+ password_schemes_deinit();
+ passdb_mock_mod_deinit();
+ passdbs_deinit();
+ userdbs_deinit();
+ event_unref(&auth_event);
+ mech_deinit(global_auth_settings);
+ mech_register_deinit(&mech_reg);
+ auths_free();
+ i_unlink("auth-token-secret.dat");
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mechs,
+ NULL
+ };
+
+ return test_run(test_functions);
+}
diff --git a/src/auth/test-mock.c b/src/auth/test-mock.c
new file mode 100644
index 0000000..9584912
--- /dev/null
+++ b/src/auth/test-mock.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+#include "auth-common.h"
+#include "passdb.h"
+
+struct auth_penalty *auth_penalty;
+time_t process_start_time;
+bool worker, worker_restart_request;
+static struct passdb_module *mock_passdb_mod = NULL;
+static pool_t mock_pool;
+
+void auth_module_load(const char *names ATTR_UNUSED)
+{
+}
+void auth_refresh_proctitle(void) {
+}
+
+static void passdb_mock_init(struct passdb_module *module ATTR_UNUSED)
+{
+}
+static void passdb_mock_deinit(struct passdb_module *module ATTR_UNUSED)
+{
+}
+static void passdb_mock_verify_plain(struct auth_request *request, const char *password ATTR_UNUSED,
+ verify_plain_callback_t *callback)
+{
+ callback(PASSDB_RESULT_OK, request);
+}
+
+static void passdb_mock_lookup_credentials(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ passdb_handle_credentials(PASSDB_RESULT_OK, "password", "PLAIN",
+ callback, request);
+}
+
+static struct passdb_module_interface mock_interface = {
+ .name = "mock",
+ .init = passdb_mock_init,
+ .deinit = passdb_mock_deinit,
+ .verify_plain = passdb_mock_verify_plain,
+ .lookup_credentials = passdb_mock_lookup_credentials,
+};
+
+struct auth_passdb_settings mock_passdb_set = {
+ .name = "mock",
+ .driver = "mock",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+ .mechanisms = "",
+ .username_filter = "",
+ .skip = "never",
+ .result_success = "return-ok",
+ .result_failure = "continue",
+ .result_internalfail = "continue",
+ .deny = FALSE,
+ .pass = FALSE,
+ .master = FALSE,
+ .auth_verbose = "default"
+};
+
+void passdb_mock_mod_init(void)
+{
+ if (mock_passdb_mod != NULL)
+ return;
+
+ mock_pool = pool_allocfree_create("auth mock");
+
+ passdb_register_module(&mock_interface);
+
+ struct auth_passdb_settings set = {
+ .name = "mock",
+ .driver = "mock",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+ .mechanisms = "",
+ .username_filter = "",
+
+ .skip = "never",
+ .result_success = "return-ok",
+ .result_failure = "continue",
+ .result_internalfail = "continue",
+
+ .deny = FALSE,
+ .pass = FALSE,
+ .master = FALSE,
+ .auth_verbose = "default"
+ };
+ mock_passdb_mod = passdb_preinit(mock_pool, &set);
+ passdb_init(mock_passdb_mod);
+}
+
+void passdb_mock_mod_deinit(void)
+{
+ passdb_deinit(mock_passdb_mod);
+ passdb_unregister_module(&mock_interface);
+ pool_unref(&mock_pool);
+}
+
+struct auth_passdb *passdb_mock(void)
+{
+ struct auth_passdb *ret = i_new(struct auth_passdb, 1);
+ ret->set = &mock_passdb_set;
+ ret->passdb = mock_passdb_mod;
+ return ret;
+}
diff --git a/src/auth/test-username-filter.c b/src/auth/test-username-filter.c
new file mode 100644
index 0000000..4a1c221
--- /dev/null
+++ b/src/auth/test-username-filter.c
@@ -0,0 +1,50 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "auth-settings.h"
+#include "test-auth.h"
+#include "auth-request.h"
+
+void test_username_filter(void)
+{
+ const struct {
+ const char *filter;
+ const char *input;
+ bool accepted;
+ } cases[] = {
+ { "", "", TRUE },
+ { "*", "", TRUE },
+ { "", "testuser1", TRUE },
+ { "*", "testuser1", TRUE },
+ { "!*", "testuser1", FALSE },
+ { "!*", "", FALSE },
+ { "*@*", "", FALSE },
+ { "*@*", "@", TRUE },
+ { "!*@*", "@", FALSE },
+ { "!*@*", "", TRUE },
+ { "*@*", "testuser1", FALSE },
+ { "!*@*", "testuser1", TRUE },
+ { "*@*", "testuser1@testdomain", TRUE },
+ { "!*@*", "testuser1@testdomain", FALSE },
+ { "*@testdomain *@testdomain2", "testuser1@testdomain", TRUE },
+ { "*@testdomain *@testdomain2", "testuser1@testdomain2", TRUE },
+ { "*@testdomain *@testdomain2", "testuser1@testdomain3", FALSE },
+ { "!testuser@testdomain *@testdomain", "testuser@testdomain", FALSE },
+ { "!testuser@testdomain *@testdomain", "testuser2@testdomain", TRUE },
+ { "*@testdomain !testuser@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE },
+ { "*@testdomain !testuser@testdomain !testuser2@testdomain", "testuser3@testdomain", TRUE },
+ { "!testuser@testdomain !testuser2@testdomain", "testuser", TRUE },
+ { "!testuser@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE },
+ { "!testuser@testdomain *@testdomain !testuser2@testdomain", "testuser3@testdomain", TRUE },
+ { "!testuser@testdomain *@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE },
+ };
+
+ test_begin("test username_filter");
+
+ for(size_t i = 0; i < N_ELEMENTS(cases); i++) {
+ const char *const *filter = t_strsplit_spaces(cases[i].filter, " ,");
+ test_assert_idx(auth_request_username_accepted(filter, cases[i].input) == cases[i].accepted, i);
+ }
+
+ test_end();
+}
diff --git a/src/auth/userdb-blocking.c b/src/auth/userdb-blocking.c
new file mode 100644
index 0000000..9f928e7
--- /dev/null
+++ b/src/auth/userdb-blocking.c
@@ -0,0 +1,144 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "str.h"
+#include "auth-worker-server.h"
+#include "userdb.h"
+#include "userdb-blocking.h"
+
+
+struct blocking_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ struct auth_worker_connection *conn;
+ bool next;
+ bool destroyed;
+};
+
+static bool user_callback(struct auth_worker_connection *conn ATTR_UNUSED,
+ const char *reply, void *context)
+{
+ struct auth_request *request = context;
+ enum userdb_result result;
+ const char *username, *args;
+
+ if (str_begins(reply, "FAIL\t")) {
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ args = reply + 5;
+ } else if (str_begins(reply, "NOTFOUND\t")) {
+ result = USERDB_RESULT_USER_UNKNOWN;
+ args = reply + 9;
+ } else if (str_begins(reply, "OK\t")) {
+ result = USERDB_RESULT_OK;
+ username = reply + 3;
+ args = strchr(username, '\t');
+ if (args == NULL)
+ args = "";
+ else
+ username = t_strdup_until(username, args++);
+ if (username[0] != '\0' &&
+ strcmp(request->fields.user, username) != 0) {
+ auth_request_set_username_forced(request, username);
+ request->user_changed_by_lookup = TRUE;
+ }
+ } else {
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ e_error(authdb_event(request),
+ "BUG: auth-worker sent invalid user reply");
+ args = "";
+ }
+
+ if (*args != '\0') {
+ auth_fields_import(request->fields.userdb_reply, args, 0);
+ if (auth_fields_exists(request->fields.userdb_reply, "tempfail"))
+ request->userdb_lookup_tempfailed = TRUE;
+ }
+
+ auth_request_userdb_callback(result, request);
+ auth_request_unref(&request);
+ return TRUE;
+}
+
+void userdb_blocking_lookup(struct auth_request *request)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "USER\t%u\t", request->userdb->userdb->id);
+ auth_request_export(request, str);
+
+ auth_request_ref(request);
+ auth_worker_call(request->pool, request->fields.user,
+ str_c(str), user_callback, request);
+}
+
+static bool iter_callback(struct auth_worker_connection *conn,
+ const char *reply, void *context)
+{
+ struct blocking_userdb_iterate_context *ctx = context;
+
+ ctx->conn = conn;
+
+ if (str_begins(reply, "*\t")) {
+ if (ctx->destroyed)
+ return TRUE;
+ ctx->next = FALSE;
+ ctx->ctx.callback(reply + 2, ctx->ctx.context);
+ return ctx->next || ctx->destroyed;
+ }
+
+ if (strcmp(reply, "OK") != 0)
+ ctx->ctx.failed = TRUE;
+ if (!ctx->destroyed)
+ ctx->ctx.callback(NULL, ctx->ctx.context);
+ auth_request_unref(&ctx->ctx.auth_request);
+ return TRUE;
+}
+
+struct userdb_iterate_context *
+userdb_blocking_iter_init(struct auth_request *request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct blocking_userdb_iterate_context *ctx;
+ string_t *str;
+
+ str = t_str_new(128);
+ str_printfa(str, "LIST\t%u\t", request->userdb->userdb->id);
+ auth_request_export(request, str);
+
+ ctx = p_new(request->pool, struct blocking_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+
+ auth_request_ref(request);
+ auth_worker_call(request->pool, "*",
+ str_c(str), iter_callback, ctx);
+ return &ctx->ctx;
+}
+
+void userdb_blocking_iter_next(struct userdb_iterate_context *_ctx)
+{
+ struct blocking_userdb_iterate_context *ctx =
+ (struct blocking_userdb_iterate_context *)_ctx;
+
+ i_assert(ctx->conn != NULL);
+
+ ctx->next = TRUE;
+ auth_worker_server_resume_input(ctx->conn);
+}
+
+int userdb_blocking_iter_deinit(struct userdb_iterate_context **_ctx)
+{
+ struct blocking_userdb_iterate_context *ctx =
+ (struct blocking_userdb_iterate_context *)*_ctx;
+ int ret = ctx->ctx.failed ? -1 : 0;
+
+ *_ctx = NULL;
+
+ /* iter_callback() may still be called */
+ ctx->destroyed = TRUE;
+
+ if (ctx->conn != NULL)
+ auth_worker_server_resume_input(ctx->conn);
+ return ret;
+}
diff --git a/src/auth/userdb-blocking.h b/src/auth/userdb-blocking.h
new file mode 100644
index 0000000..2bbf075
--- /dev/null
+++ b/src/auth/userdb-blocking.h
@@ -0,0 +1,12 @@
+#ifndef USERDB_BLOCKING_H
+#define USERDB_BLOCKING_H
+
+void userdb_blocking_lookup(struct auth_request *request);
+
+struct userdb_iterate_context *
+userdb_blocking_iter_init(struct auth_request *request,
+ userdb_iter_callback_t *callback, void *context);
+void userdb_blocking_iter_next(struct userdb_iterate_context *ctx);
+int userdb_blocking_iter_deinit(struct userdb_iterate_context **ctx);
+
+#endif
diff --git a/src/auth/userdb-checkpassword.c b/src/auth/userdb-checkpassword.c
new file mode 100644
index 0000000..aa6455e
--- /dev/null
+++ b/src/auth/userdb-checkpassword.c
@@ -0,0 +1,92 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#ifdef USERDB_CHECKPASSWORD
+
+#include "db-checkpassword.h"
+
+struct checkpassword_userdb_module {
+ struct userdb_module module;
+ struct db_checkpassword *db;
+};
+
+static void
+userdb_checkpassword_callback(struct auth_request *request,
+ enum db_checkpassword_status status,
+ const char *const *extra_fields,
+ userdb_callback_t *callback)
+{
+ unsigned int i;
+
+ switch (status) {
+ case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE:
+ callback(USERDB_RESULT_INTERNAL_FAILURE, request);
+ break;
+ case DB_CHECKPASSWORD_STATUS_FAILURE:
+ callback(USERDB_RESULT_USER_UNKNOWN, request);
+ break;
+ case DB_CHECKPASSWORD_STATUS_OK:
+ for (i = 0; extra_fields[i] != NULL; i++) {
+ if (!str_begins(extra_fields[i], "userdb_"))
+ continue;
+ auth_request_set_field_keyvalue(request,
+ extra_fields[i], NULL);
+ }
+ callback(USERDB_RESULT_OK, request);
+ break;
+ }
+}
+
+static void
+checkpassword_lookup(struct auth_request *request, userdb_callback_t *callback)
+{
+ struct userdb_module *_module = request->userdb->userdb;
+ struct checkpassword_userdb_module *module =
+ (struct checkpassword_userdb_module *)_module;
+
+ db_checkpassword_call(module->db, request, NULL,
+ userdb_checkpassword_callback, callback);
+}
+
+static struct userdb_module *
+checkpassword_preinit(pool_t pool, const char *args)
+{
+ struct checkpassword_userdb_module *module;
+ const char *checkpassword_path = args;
+ const char *checkpassword_reply_path =
+ PKG_LIBEXECDIR"/checkpassword-reply";
+
+ module = p_new(pool, struct checkpassword_userdb_module, 1);
+ module->db = db_checkpassword_init(checkpassword_path,
+ checkpassword_reply_path);
+ return &module->module;
+}
+
+static void checkpassword_deinit(struct userdb_module *_module)
+{
+ struct checkpassword_userdb_module *module =
+ (struct checkpassword_userdb_module *)_module;
+
+ db_checkpassword_deinit(&module->db);
+}
+
+struct userdb_module_interface userdb_checkpassword = {
+ "checkpassword",
+
+ checkpassword_preinit,
+ NULL,
+ checkpassword_deinit,
+
+ checkpassword_lookup,
+
+ NULL,
+ NULL,
+ NULL
+};
+#else
+struct userdb_module_interface userdb_checkpassword = {
+ .name = "checkpassword"
+};
+#endif
diff --git a/src/auth/userdb-dict.c b/src/auth/userdb-dict.c
new file mode 100644
index 0000000..51e618e
--- /dev/null
+++ b/src/auth/userdb-dict.c
@@ -0,0 +1,205 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "auth-cache.h"
+#include "db-dict.h"
+
+#include <dict.h>
+
+struct dict_userdb_module {
+ struct userdb_module module;
+
+ struct dict_connection *conn;
+};
+
+struct dict_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+
+ userdb_callback_t *userdb_callback;
+ const char *key_prefix;
+ size_t key_prefix_len;
+ struct dict_iterate_context *iter;
+};
+
+static int
+dict_query_save_results(struct auth_request *auth_request,
+ struct db_dict_value_iter *iter)
+{
+ const char *key, *value, *error;
+
+ while (db_dict_value_iter_next(iter, &key, &value)) {
+ if (value != NULL)
+ auth_request_set_userdb_field(auth_request, key, value);
+ }
+ if (db_dict_value_iter_deinit(&iter, &error) < 0) {
+ e_error(authdb_event(auth_request), "%s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static void userdb_dict_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct dict_userdb_module *module =
+ (struct dict_userdb_module *)_module;
+ struct db_dict_value_iter *iter;
+ enum userdb_result userdb_result;
+ int ret;
+
+ if (array_count(&module->conn->set.userdb_fields) == 0 &&
+ array_count(&module->conn->set.parsed_userdb_objects) == 0) {
+ e_error(authdb_event(auth_request),
+ "No userdb_objects or userdb_fields specified");
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+
+ ret = db_dict_value_iter_init(module->conn, auth_request,
+ &module->conn->set.userdb_fields,
+ &module->conn->set.parsed_userdb_objects,
+ &iter);
+ if (ret < 0)
+ userdb_result = USERDB_RESULT_INTERNAL_FAILURE;
+ else if (ret == 0) {
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ userdb_result = USERDB_RESULT_USER_UNKNOWN;
+ } else {
+ if (dict_query_save_results(auth_request, iter) < 0)
+ userdb_result = USERDB_RESULT_INTERNAL_FAILURE;
+ else
+ userdb_result = USERDB_RESULT_OK;
+ }
+ callback(userdb_result, auth_request);
+}
+
+static struct userdb_iterate_context *
+userdb_dict_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct dict_userdb_module *module =
+ (struct dict_userdb_module *)_module;
+ struct dict_userdb_iterate_context *ctx;
+ string_t *path;
+ const char *error;
+
+ ctx = i_new(struct dict_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = auth_request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+ auth_request_ref(auth_request);
+
+ if (*module->conn->set.iterate_prefix == '\0') {
+ if (!module->conn->set.iterate_disable) {
+ e_error(authdb_event(auth_request),
+ "iterate: iterate_prefix not set");
+ ctx->ctx.failed = TRUE;
+ }
+ return &ctx->ctx;
+ }
+
+ path = t_str_new(128);
+ str_append(path, DICT_PATH_SHARED);
+ if (auth_request_var_expand(path, module->conn->set.iterate_prefix,
+ auth_request, NULL, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand iterate_prefix=%s: %s",
+ module->conn->set.iterate_prefix, error);
+ ctx->ctx.failed = TRUE;
+ return &ctx->ctx;
+ }
+ ctx->key_prefix = p_strdup(auth_request->pool, str_c(path));
+ ctx->key_prefix_len = strlen(ctx->key_prefix);
+
+ struct dict_op_settings set = {
+ .username = auth_request->fields.user,
+ };
+ ctx->iter = dict_iterate_init(module->conn->dict, &set, ctx->key_prefix, 0);
+ e_debug(authdb_event(auth_request),
+ "iterate: prefix=%s", ctx->key_prefix);
+ return &ctx->ctx;
+}
+
+static const char *
+userdb_dict_get_user(struct dict_userdb_iterate_context *ctx, const char *key)
+{
+ i_assert(strncmp(key, ctx->key_prefix, ctx->key_prefix_len) == 0);
+
+ return key + ctx->key_prefix_len;
+}
+
+static void userdb_dict_iterate_next(struct userdb_iterate_context *_ctx)
+{
+ struct dict_userdb_iterate_context *ctx =
+ (struct dict_userdb_iterate_context *)_ctx;
+ const char *key, *value;
+
+ if (ctx->iter != NULL && dict_iterate(ctx->iter, &key, &value))
+ _ctx->callback(userdb_dict_get_user(ctx, key), _ctx->context);
+ else
+ _ctx->callback(NULL, _ctx->context);
+}
+
+static int userdb_dict_iterate_deinit(struct userdb_iterate_context *_ctx)
+{
+ struct dict_userdb_iterate_context *ctx =
+ (struct dict_userdb_iterate_context *)_ctx;
+ const char *error;
+ int ret = _ctx->failed ? -1 : 0;
+
+ if (dict_iterate_deinit(&ctx->iter, &error) < 0) {
+ e_error(authdb_event(_ctx->auth_request),
+ "dict_iterate(%s) failed: %s",
+ ctx->key_prefix, error);
+ ret = -1;
+ }
+ auth_request_unref(&ctx->ctx.auth_request);
+ i_free(ctx);
+ return ret;
+}
+
+static struct userdb_module *
+userdb_dict_preinit(pool_t pool, const char *args)
+{
+ struct dict_userdb_module *module;
+ struct dict_connection *conn;
+
+ module = p_new(pool, struct dict_userdb_module, 1);
+ module->conn = conn = db_dict_init(args);
+
+ module->module.blocking = TRUE;
+ module->module.default_cache_key = auth_cache_parse_key(pool,
+ db_dict_parse_cache_key(&conn->set.keys, &conn->set.userdb_fields,
+ &conn->set.parsed_userdb_objects));
+ return &module->module;
+}
+
+static void userdb_dict_deinit(struct userdb_module *_module)
+{
+ struct dict_userdb_module *module =
+ (struct dict_userdb_module *)_module;
+
+ db_dict_unref(&module->conn);
+}
+
+struct userdb_module_interface userdb_dict =
+{
+ "dict",
+
+ userdb_dict_preinit,
+ NULL,
+ userdb_dict_deinit,
+
+ userdb_dict_lookup,
+
+ userdb_dict_iterate_init,
+ userdb_dict_iterate_next,
+ userdb_dict_iterate_deinit
+};
diff --git a/src/auth/userdb-ldap.c b/src/auth/userdb-ldap.c
new file mode 100644
index 0000000..ecbf09e
--- /dev/null
+++ b/src/auth/userdb-ldap.c
@@ -0,0 +1,343 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#if defined(USERDB_LDAP) && (defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD))
+
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "auth-cache.h"
+#include "db-ldap.h"
+
+#include <ldap.h>
+
+struct ldap_userdb_module {
+ struct userdb_module module;
+
+ struct ldap_connection *conn;
+};
+
+struct userdb_ldap_request {
+ struct ldap_request_search request;
+ userdb_callback_t *userdb_callback;
+ unsigned int entries;
+};
+
+struct userdb_iter_ldap_request {
+ struct ldap_request_search request;
+ struct ldap_userdb_iterate_context *ctx;
+ userdb_callback_t *userdb_callback;
+};
+
+struct ldap_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ struct userdb_iter_ldap_request request;
+ pool_t pool;
+ struct ldap_connection *conn;
+ bool continued, in_callback, deinitialized;
+};
+
+static void
+ldap_query_get_result(struct ldap_connection *conn,
+ struct auth_request *auth_request,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res)
+{
+ struct db_ldap_result_iterate_context *ldap_iter;
+ const char *name, *const *values;
+
+ ldap_iter = db_ldap_result_iterate_init(conn, ldap_request, res, TRUE);
+ while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) {
+ auth_request_set_userdb_field_values(auth_request,
+ name, values);
+ }
+ db_ldap_result_iterate_deinit(&ldap_iter);
+}
+
+static void
+userdb_ldap_lookup_finish(struct auth_request *auth_request,
+ struct userdb_ldap_request *urequest,
+ LDAPMessage *res)
+{
+ enum userdb_result result = USERDB_RESULT_INTERNAL_FAILURE;
+
+ if (res == NULL) {
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else if (urequest->entries == 0) {
+ result = USERDB_RESULT_USER_UNKNOWN;
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ } else if (urequest->entries > 1) {
+ e_error(authdb_event(auth_request),
+ "user_filter matched multiple objects, aborting");
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else {
+ result = USERDB_RESULT_OK;
+ }
+
+ urequest->userdb_callback(result, auth_request);
+}
+
+static void userdb_ldap_lookup_callback(struct ldap_connection *conn,
+ struct ldap_request *request,
+ LDAPMessage *res)
+{
+ struct userdb_ldap_request *urequest =
+ (struct userdb_ldap_request *) request;
+ struct auth_request *auth_request =
+ urequest->request.request.auth_request;
+
+ if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) {
+ userdb_ldap_lookup_finish(auth_request, urequest, res);
+ auth_request_unref(&auth_request);
+ return;
+ }
+
+ if (urequest->entries++ == 0) {
+ /* first entry */
+ ldap_query_get_result(conn, auth_request,
+ &urequest->request, res);
+ }
+}
+
+static void userdb_ldap_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct ldap_userdb_module *module =
+ (struct ldap_userdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ const char **attr_names = (const char **)conn->user_attr_names;
+ struct userdb_ldap_request *request;
+ const char *error;
+ string_t *str;
+
+ auth_request_ref(auth_request);
+ request = p_new(auth_request->pool, struct userdb_ldap_request, 1);
+ request->userdb_callback = callback;
+
+ str = t_str_new(512);
+ if (auth_request_var_expand(str, conn->set.base, auth_request,
+ ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand base=%s: %s", conn->set.base, error);
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+ request->request.base = p_strdup(auth_request->pool, str_c(str));
+
+ str_truncate(str, 0);
+ if (auth_request_var_expand(str, conn->set.user_filter, auth_request,
+ ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand user_filter=%s: %s",
+ conn->set.user_filter, error);
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+ request->request.filter = p_strdup(auth_request->pool, str_c(str));
+
+ request->request.attr_map = &conn->user_attr_map;
+ request->request.attributes = conn->user_attr_names;
+
+ e_debug(authdb_event(auth_request), "user search: "
+ "base=%s scope=%s filter=%s fields=%s",
+ request->request.base, conn->set.scope,
+ request->request.filter,
+ attr_names == NULL ? "(all)" :
+ t_strarray_join(attr_names, ","));
+
+ request->request.request.auth_request = auth_request;
+ request->request.request.callback = userdb_ldap_lookup_callback;
+ db_ldap_request(conn, &request->request.request);
+}
+
+static void userdb_ldap_iterate_callback(struct ldap_connection *conn,
+ struct ldap_request *request,
+ LDAPMessage *res)
+{
+ struct userdb_iter_ldap_request *urequest =
+ (struct userdb_iter_ldap_request *)request;
+ struct ldap_userdb_iterate_context *ctx = urequest->ctx;
+ struct db_ldap_result_iterate_context *ldap_iter;
+ const char *name, *const *values;
+
+ if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) {
+ if (res == NULL)
+ ctx->ctx.failed = TRUE;
+ if (!ctx->deinitialized)
+ ctx->ctx.callback(NULL, ctx->ctx.context);
+ auth_request_unref(&request->auth_request);
+ return;
+ }
+
+ if (ctx->deinitialized)
+ return;
+
+ /* the iteration can take a while. reset the request's create time so
+ it won't be aborted while it's still running */
+ request->create_time = ioloop_time;
+
+ ctx->in_callback = TRUE;
+ ldap_iter = db_ldap_result_iterate_init(conn, &urequest->request,
+ res, TRUE);
+ while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) {
+ if (strcmp(name, "user") != 0) {
+ e_warning(authdb_event(request->auth_request), "iterate: "
+ "Ignoring field not named 'user': %s", name);
+ continue;
+ }
+ for (; *values != NULL; values++) {
+ ctx->continued = FALSE;
+ ctx->ctx.callback(*values, ctx->ctx.context);
+ }
+ }
+ db_ldap_result_iterate_deinit(&ldap_iter);
+ if (!ctx->continued)
+ db_ldap_enable_input(conn, FALSE);
+ ctx->in_callback = FALSE;
+}
+
+static struct userdb_iterate_context *
+userdb_ldap_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct ldap_userdb_module *module =
+ (struct ldap_userdb_module *)_module;
+ struct ldap_connection *conn = module->conn;
+ struct ldap_userdb_iterate_context *ctx;
+ struct userdb_iter_ldap_request *request;
+ const char **attr_names = (const char **)conn->iterate_attr_names;
+ const char *error;
+ string_t *str;
+
+ ctx = p_new(auth_request->pool, struct ldap_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = auth_request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+ ctx->conn = conn;
+ request = &ctx->request;
+ request->ctx = ctx;
+
+ auth_request_ref(auth_request);
+ request->request.request.auth_request = auth_request;
+
+ str = t_str_new(512);
+ if (auth_request_var_expand(str, conn->set.base, auth_request,
+ ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand base=%s: %s", conn->set.base, error);
+ ctx->ctx.failed = TRUE;
+ }
+ request->request.base = p_strdup(auth_request->pool, str_c(str));
+
+ str_truncate(str, 0);
+ if (auth_request_var_expand(str, conn->set.iterate_filter,
+ auth_request, ldap_escape, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand iterate_filter=%s: %s",
+ conn->set.iterate_filter, error);
+ ctx->ctx.failed = TRUE;
+ }
+ request->request.filter = p_strdup(auth_request->pool, str_c(str));
+ request->request.attr_map = &conn->iterate_attr_map;
+ request->request.attributes = conn->iterate_attr_names;
+ request->request.multi_entry = TRUE;
+
+ e_debug(auth_request->event, "ldap: iterate: base=%s scope=%s filter=%s fields=%s",
+ request->request.base, conn->set.scope,
+ request->request.filter, attr_names == NULL ? "(all)" :
+ t_strarray_join(attr_names, ","));
+ request->request.request.callback = userdb_ldap_iterate_callback;
+ db_ldap_request(conn, &request->request.request);
+ return &ctx->ctx;
+}
+
+static void userdb_ldap_iterate_next(struct userdb_iterate_context *_ctx)
+{
+ struct ldap_userdb_iterate_context *ctx =
+ (struct ldap_userdb_iterate_context *)_ctx;
+
+ ctx->continued = TRUE;
+ if (!ctx->in_callback)
+ db_ldap_enable_input(ctx->conn, TRUE);
+}
+
+static int userdb_ldap_iterate_deinit(struct userdb_iterate_context *_ctx)
+{
+ struct ldap_userdb_iterate_context *ctx =
+ (struct ldap_userdb_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ db_ldap_enable_input(ctx->conn, TRUE);
+ ctx->deinitialized = TRUE;
+ return ret;
+}
+
+static struct userdb_module *
+userdb_ldap_preinit(pool_t pool, const char *args)
+{
+ struct ldap_userdb_module *module;
+ struct ldap_connection *conn;
+
+ module = p_new(pool, struct ldap_userdb_module, 1);
+ module->conn = conn = db_ldap_init(args, TRUE);
+ p_array_init(&conn->user_attr_map, pool, 16);
+ p_array_init(&conn->iterate_attr_map, pool, 16);
+
+ db_ldap_set_attrs(conn, conn->set.user_attrs, &conn->user_attr_names,
+ &conn->user_attr_map, NULL);
+ db_ldap_set_attrs(conn, conn->set.iterate_attrs,
+ &conn->iterate_attr_names,
+ &conn->iterate_attr_map, NULL);
+ module->module.blocking = conn->set.blocking;
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool,
+ t_strconcat(conn->set.base,
+ conn->set.user_attrs,
+ conn->set.user_filter, NULL));
+ return &module->module;
+}
+
+static void userdb_ldap_init(struct userdb_module *_module)
+{
+ struct ldap_userdb_module *module =
+ (struct ldap_userdb_module *)_module;
+
+ if (!module->module.blocking || worker)
+ db_ldap_connect_delayed(module->conn);
+}
+
+static void userdb_ldap_deinit(struct userdb_module *_module)
+{
+ struct ldap_userdb_module *module =
+ (struct ldap_userdb_module *)_module;
+
+ db_ldap_unref(&module->conn);
+}
+
+#ifndef PLUGIN_BUILD
+struct userdb_module_interface userdb_ldap =
+#else
+struct userdb_module_interface userdb_ldap_plugin =
+#endif
+{
+ "ldap",
+
+ userdb_ldap_preinit,
+ userdb_ldap_init,
+ userdb_ldap_deinit,
+
+ userdb_ldap_lookup,
+
+ userdb_ldap_iterate_init,
+ userdb_ldap_iterate_next,
+ userdb_ldap_iterate_deinit
+};
+#else
+struct userdb_module_interface userdb_ldap = {
+ .name = "ldap"
+};
+#endif
diff --git a/src/auth/userdb-lua.c b/src/auth/userdb-lua.c
new file mode 100644
index 0000000..6b75c0d
--- /dev/null
+++ b/src/auth/userdb-lua.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+#include "auth-cache.h"
+
+#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD)
+
+#include "db-lua.h"
+
+struct dlua_userdb_module {
+ struct userdb_module module;
+ struct dlua_script *script;
+ const char *file;
+};
+
+static void userdb_lua_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct dlua_userdb_module *module =
+ (struct dlua_userdb_module *)_module;
+ const char *error;
+ enum userdb_result result =
+ auth_lua_call_userdb_lookup(module->script, auth_request, &error);
+ if (result == USERDB_RESULT_INTERNAL_FAILURE)
+ e_error(authdb_event(auth_request),
+ "userdb-lua: %s", error);
+ callback(result, auth_request);
+}
+
+static struct userdb_module *
+userdb_lua_preinit(pool_t pool, const char *args)
+{
+ struct dlua_userdb_module *module;
+ const char *cache_key = "%u";
+ bool blocking = TRUE;
+
+ module = p_new(pool, struct dlua_userdb_module, 1);
+ const char *const *fields = t_strsplit_spaces(args, " ");
+ while(*fields != NULL) {
+ if (str_begins(*fields, "file=")) {
+ module->file = p_strdup(pool, (*fields)+5);
+ } else if (str_begins(*fields, "blocking=")) {
+ const char *value = (*fields)+9;
+ if (strcmp(value, "yes") == 0) {
+ blocking = TRUE;
+ } else if (strcmp(value, "no") == 0) {
+ blocking = FALSE;
+ } else {
+ i_fatal("Invalid value %s. "
+ "Field blocking must be yes or no",
+ value);
+ }
+ } else if (str_begins(*fields, "cache_key=")) {
+ if (*((*fields)+10) != '\0')
+ cache_key = (*fields)+10;
+ else /* explicitly disable auth caching for lua */
+ cache_key = NULL;
+ } else {
+ i_fatal("Unsupported parameter %s", *fields);
+ }
+ fields++;
+ }
+
+ if (module->file == NULL)
+ i_fatal("userdb-lua: Missing mandatory file= parameter");
+
+ module->module.blocking = blocking;
+ if (cache_key != NULL) {
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool, cache_key);
+ }
+ return &module->module;
+}
+
+static void userdb_lua_init(struct userdb_module *_module)
+{
+ struct dlua_userdb_module *module =
+ (struct dlua_userdb_module *)_module;
+ const char *error;
+
+ if (dlua_script_create_file(module->file, &module->script, auth_event, &error) < 0 ||
+ auth_lua_script_init(module->script, &error) < 0)
+ i_fatal("userdb-lua: initialization failed: %s", error);
+}
+
+static void userdb_lua_deinit(struct userdb_module *_module)
+{
+ struct dlua_userdb_module *module =
+ (struct dlua_userdb_module *)_module;
+ dlua_script_unref(&module->script);
+}
+
+static struct userdb_iterate_context *
+userdb_lua_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback,
+ void *context)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct dlua_userdb_module *module =
+ (struct dlua_userdb_module *)_module;
+ return auth_lua_call_userdb_iterate_init(module->script, auth_request,
+ callback, context);
+}
+
+static void userdb_lua_iterate_next(struct userdb_iterate_context *ctx)
+{
+ auth_lua_userdb_iterate_next(ctx);
+}
+
+static int userdb_lua_iterate_deinit(struct userdb_iterate_context *ctx)
+{
+ return auth_lua_userdb_iterate_deinit(ctx);
+}
+
+#ifndef PLUGIN_BUILD
+struct userdb_module_interface userdb_lua =
+#else
+struct userdb_module_interface userdb_lua_plugin =
+#endif
+{
+ "lua",
+
+ userdb_lua_preinit,
+ userdb_lua_init,
+ userdb_lua_deinit,
+
+ userdb_lua_lookup,
+
+ userdb_lua_iterate_init,
+ userdb_lua_iterate_next,
+ userdb_lua_iterate_deinit
+};
+#else
+struct userdb_module_interface userdb_lua = {
+ .name = "lua"
+};
+#endif
diff --git a/src/auth/userdb-passwd-file.c b/src/auth/userdb-passwd-file.c
new file mode 100644
index 0000000..1e87138
--- /dev/null
+++ b/src/auth/userdb-passwd-file.c
@@ -0,0 +1,247 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#ifdef USERDB_PASSWD_FILE
+
+#include "istream.h"
+#include "str.h"
+#include "auth-cache.h"
+#include "db-passwd-file.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+struct passwd_file_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ struct istream *input;
+ char *path;
+ bool skip_passdb_entries;
+};
+
+struct passwd_file_userdb_module {
+ struct userdb_module module;
+
+ struct db_passwd_file *pwf;
+ const char *username_format;
+};
+
+static int
+passwd_file_add_extra_fields(struct auth_request *request, char *const *fields)
+{
+ string_t *str = t_str_new(512);
+ const struct var_expand_table *table;
+ const char *key, *value, *error;
+ unsigned int i;
+
+ table = auth_request_get_var_expand_table(request, NULL);
+
+ for (i = 0; fields[i] != NULL; i++) {
+ if (!str_begins(fields[i], "userdb_"))
+ continue;
+
+ key = fields[i] + 7;
+ value = strchr(key, '=');
+ if (value != NULL) {
+ key = t_strdup_until(key, value);
+ str_truncate(str, 0);
+ if (auth_request_var_expand_with_table(str, value + 1,
+ request, table, NULL, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand extra field %s: %s",
+ fields[i], error);
+ return -1;
+ }
+ value = str_c(str);
+ } else {
+ value = "";
+ }
+ auth_request_set_userdb_field(request, key, value);
+ }
+ return 0;
+}
+
+static void passwd_file_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct passwd_file_userdb_module *module =
+ (struct passwd_file_userdb_module *)_module;
+ struct passwd_user *pu;
+ int ret;
+
+ ret = db_passwd_file_lookup(module->pwf, auth_request,
+ module->username_format, &pu);
+ if (ret <= 0 || pu->uid == 0) {
+ callback(ret < 0 ? USERDB_RESULT_INTERNAL_FAILURE :
+ USERDB_RESULT_USER_UNKNOWN, auth_request);
+ return;
+ }
+
+ if (pu->uid != (uid_t)-1) {
+ auth_request_set_userdb_field(auth_request, "uid",
+ dec2str(pu->uid));
+ }
+ if (pu->gid != (gid_t)-1) {
+ auth_request_set_userdb_field(auth_request, "gid",
+ dec2str(pu->gid));
+ }
+
+ if (pu->home != NULL)
+ auth_request_set_userdb_field(auth_request, "home", pu->home);
+
+ if (pu->extra_fields != NULL &&
+ passwd_file_add_extra_fields(auth_request, pu->extra_fields) < 0) {
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+
+ callback(USERDB_RESULT_OK, auth_request);
+}
+
+static struct userdb_iterate_context *
+passwd_file_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct passwd_file_userdb_module *module =
+ (struct passwd_file_userdb_module *)_module;
+ struct passwd_file_userdb_iterate_context *ctx;
+ int fd;
+
+ ctx = i_new(struct passwd_file_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = auth_request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+ ctx->skip_passdb_entries = !module->pwf->userdb_warn_missing;
+ if (module->pwf->default_file == NULL) {
+ e_error(authdb_event(auth_request),
+ "passwd-file: User iteration isn't currently supported "
+ "with %%variable paths");
+ ctx->ctx.failed = TRUE;
+ return &ctx->ctx;
+ }
+ ctx->path = i_strdup(module->pwf->default_file->path);
+
+ /* for now we support only a single passwd-file */
+ fd = open(ctx->path, O_RDONLY);
+ if (fd == -1) {
+ e_error(authdb_event(auth_request),
+ "open(%s) failed: %m", ctx->path);
+ ctx->ctx.failed = TRUE;
+ } else {
+ ctx->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ }
+ return &ctx->ctx;
+}
+
+static void passwd_file_iterate_next(struct userdb_iterate_context *_ctx)
+{
+ struct passwd_file_userdb_iterate_context *ctx =
+ (struct passwd_file_userdb_iterate_context *)_ctx;
+ const char *line, *p;
+
+ if (ctx->input == NULL)
+ line = NULL;
+ else {
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0' || *line == ':' || *line == '#')
+ continue; /* no username or comment */
+ if (ctx->skip_passdb_entries &&
+ ((p = i_strchr_to_next(line, ':')) == NULL ||
+ strchr(p, ':') == NULL)) {
+ /* only passdb info */
+ continue;
+ }
+ break;
+ }
+ if (line == NULL && ctx->input->stream_errno != 0) {
+ e_error(authdb_event(_ctx->auth_request),
+ "read(%s) failed: %s", ctx->path,
+ i_stream_get_error(ctx->input));
+ _ctx->failed = TRUE;
+ }
+ }
+ if (line == NULL)
+ _ctx->callback(NULL, _ctx->context);
+ else T_BEGIN {
+ _ctx->callback(t_strcut(line, ':'), _ctx->context);
+ } T_END;
+}
+
+static int passwd_file_iterate_deinit(struct userdb_iterate_context *_ctx)
+{
+ struct passwd_file_userdb_iterate_context *ctx =
+ (struct passwd_file_userdb_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ i_stream_destroy(&ctx->input);
+ i_free(ctx->path);
+ i_free(ctx);
+ return ret;
+}
+
+static struct userdb_module *
+passwd_file_preinit(pool_t pool, const char *args)
+{
+ struct passwd_file_userdb_module *module;
+ const char *format = PASSWD_FILE_DEFAULT_USERNAME_FORMAT;
+ const char *p;
+
+ if (str_begins(args, "username_format=")) {
+ args += 16;
+ p = strchr(args, ' ');
+ if (p == NULL) {
+ format = p_strdup(pool, args);
+ args = "";
+ } else {
+ format = p_strdup_until(pool, args, p);
+ args = p + 1;
+ }
+ }
+
+ if (*args == '\0')
+ i_fatal("userdb passwd-file: Missing args");
+
+ module = p_new(pool, struct passwd_file_userdb_module, 1);
+ module->pwf = db_passwd_file_init(args, TRUE,
+ global_auth_settings->debug);
+ module->username_format = format;
+ return &module->module;
+}
+
+static void passwd_file_init(struct userdb_module *_module)
+{
+ struct passwd_file_userdb_module *module =
+ (struct passwd_file_userdb_module *)_module;
+
+ db_passwd_file_parse(module->pwf);
+}
+
+static void passwd_file_deinit(struct userdb_module *_module)
+{
+ struct passwd_file_userdb_module *module =
+ (struct passwd_file_userdb_module *)_module;
+
+ db_passwd_file_unref(&module->pwf);
+}
+
+struct userdb_module_interface userdb_passwd_file = {
+ "passwd-file",
+
+ passwd_file_preinit,
+ passwd_file_init,
+ passwd_file_deinit,
+
+ passwd_file_lookup,
+
+ passwd_file_iterate_init,
+ passwd_file_iterate_next,
+ passwd_file_iterate_deinit
+};
+#else
+struct userdb_module_interface userdb_passwd_file = {
+ .name = "passwd-file"
+};
+#endif
diff --git a/src/auth/userdb-passwd.c b/src/auth/userdb-passwd.c
new file mode 100644
index 0000000..3c99102
--- /dev/null
+++ b/src/auth/userdb-passwd.c
@@ -0,0 +1,253 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#ifdef USERDB_PASSWD
+
+#include "ioloop.h"
+#include "ipwd.h"
+#include "time-util.h"
+#include "userdb-template.h"
+
+#define USER_CACHE_KEY "%u"
+#define PASSWD_SLOW_WARN_MSECS (10*1000)
+#define PASSWD_SLOW_MASTER_WARN_MSECS 50
+#define PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL 100
+#define PASSDB_SLOW_MASTER_WARN_MIN_PERCENTAGE 5
+
+struct passwd_userdb_module {
+ struct userdb_module module;
+ struct userdb_template *tmpl;
+
+ unsigned int fast_count, slow_count;
+ bool slow_warned:1;
+};
+
+struct passwd_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ struct passwd_userdb_iterate_context *next_waiting;
+};
+
+static struct passwd_userdb_iterate_context *cur_userdb_iter = NULL;
+static struct timeout *cur_userdb_iter_to = NULL;
+
+static void
+passwd_check_warnings(struct auth_request *auth_request,
+ struct passwd_userdb_module *module,
+ const struct timeval *start_tv)
+{
+ struct timeval end_tv;
+ unsigned int msecs, percentage;
+
+ i_gettimeofday(&end_tv);
+
+ msecs = timeval_diff_msecs(&end_tv, start_tv);
+ if (msecs >= PASSWD_SLOW_WARN_MSECS) {
+ e_warning(authdb_event(auth_request), "Lookup for %s took %u secs",
+ auth_request->fields.user, msecs/1000);
+ return;
+ }
+ if (worker || module->slow_warned)
+ return;
+
+ if (msecs < PASSWD_SLOW_MASTER_WARN_MSECS) {
+ module->fast_count++;
+ return;
+ }
+ module->slow_count++;
+ if (module->fast_count + module->slow_count <
+ PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL)
+ return;
+
+ percentage = module->slow_count * 100 /
+ (module->slow_count + module->fast_count);
+ if (percentage < PASSDB_SLOW_MASTER_WARN_MIN_PERCENTAGE) {
+ /* start from beginning */
+ module->slow_count = module->fast_count = 0;
+ } else {
+ e_warning(authdb_event(auth_request),
+ "%u%% of last %u lookups took over "
+ "%u milliseconds, "
+ "you may want to set blocking=yes for userdb",
+ percentage, PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL,
+ PASSWD_SLOW_MASTER_WARN_MSECS);
+ module->slow_warned = TRUE;
+ }
+}
+
+static void passwd_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct passwd_userdb_module *module =
+ (struct passwd_userdb_module *)_module;
+ struct passwd pw;
+ struct timeval start_tv;
+ const char *error;
+ int ret;
+
+ e_debug(authdb_event(auth_request), "lookup");
+
+ i_gettimeofday(&start_tv);
+ ret = i_getpwnam(auth_request->fields.user, &pw);
+ if (start_tv.tv_sec != 0)
+ passwd_check_warnings(auth_request, module, &start_tv);
+
+ switch (ret) {
+ case -1:
+ e_error(authdb_event(auth_request),
+ "getpwnam() failed: %m");
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ case 0:
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ callback(USERDB_RESULT_USER_UNKNOWN, auth_request);
+ return;
+ }
+
+ auth_request_set_field(auth_request, "user", pw.pw_name, NULL);
+
+ auth_request_set_userdb_field(auth_request, "system_groups_user",
+ pw.pw_name);
+ auth_request_set_userdb_field(auth_request, "uid", dec2str(pw.pw_uid));
+ auth_request_set_userdb_field(auth_request, "gid", dec2str(pw.pw_gid));
+ auth_request_set_userdb_field(auth_request, "home", pw.pw_dir);
+
+ if (userdb_template_export(module->tmpl, auth_request, &error) < 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand template: %s", error);
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ }
+
+ callback(USERDB_RESULT_OK, auth_request);
+}
+
+static struct userdb_iterate_context *
+passwd_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct passwd_userdb_iterate_context *ctx;
+
+ ctx = i_new(struct passwd_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = auth_request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+ setpwent();
+
+ if (cur_userdb_iter == NULL)
+ cur_userdb_iter = ctx;
+ return &ctx->ctx;
+}
+
+static bool
+passwd_iterate_want_pw(struct passwd *pw, const struct auth_settings *set)
+{
+ /* skip entries not in valid UID range.
+ they're users for daemons and such. */
+ if (pw->pw_uid < (uid_t)set->first_valid_uid)
+ return FALSE;
+ if (pw->pw_uid > (uid_t)set->last_valid_uid && set->last_valid_uid != 0)
+ return FALSE;
+ if (pw->pw_gid < (gid_t)set->first_valid_gid)
+ return FALSE;
+ if (pw->pw_gid > (gid_t)set->last_valid_gid && set->last_valid_gid != 0)
+ return FALSE;
+ return TRUE;
+}
+
+static void passwd_iterate_next(struct userdb_iterate_context *_ctx)
+{
+ struct passwd_userdb_iterate_context *ctx =
+ (struct passwd_userdb_iterate_context *)_ctx;
+ const struct auth_settings *set = _ctx->auth_request->set;
+ struct passwd *pw;
+
+ if (cur_userdb_iter != NULL && cur_userdb_iter != ctx) {
+ /* we can't support concurrent userdb iteration.
+ wait until the previous one is done */
+ ctx->next_waiting = cur_userdb_iter->next_waiting;
+ cur_userdb_iter->next_waiting = ctx;
+ return;
+ }
+
+ /* reset errno since it might have been set when we got here */
+ errno = 0;
+ while ((pw = getpwent()) != NULL) {
+ if (passwd_iterate_want_pw(pw, set)) {
+ _ctx->callback(pw->pw_name, _ctx->context);
+ return;
+ }
+ /* getpwent might set errno to something even if it
+ returns non-NULL. */
+ errno = 0;
+ }
+ if (errno != 0) {
+ e_error(authdb_event(_ctx->auth_request),
+ "getpwent() failed: %m");
+ _ctx->failed = TRUE;
+ }
+ _ctx->callback(NULL, _ctx->context);
+}
+
+static void ATTR_NULL(1)
+passwd_iterate_next_timeout(void *context ATTR_UNUSED)
+{
+ timeout_remove(&cur_userdb_iter_to);
+ passwd_iterate_next(&cur_userdb_iter->ctx);
+}
+
+static int passwd_iterate_deinit(struct userdb_iterate_context *_ctx)
+{
+ struct passwd_userdb_iterate_context *ctx =
+ (struct passwd_userdb_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ cur_userdb_iter = ctx->next_waiting;
+ i_free(ctx);
+
+ if (cur_userdb_iter != NULL) {
+ cur_userdb_iter_to = timeout_add(0, passwd_iterate_next_timeout,
+ NULL);
+ }
+ endpwent();
+ return ret;
+}
+
+static struct userdb_module *
+passwd_passwd_preinit(pool_t pool, const char *args)
+{
+ struct passwd_userdb_module *module;
+ const char *value;
+
+ module = p_new(pool, struct passwd_userdb_module, 1);
+ module->module.default_cache_key = USER_CACHE_KEY;
+ module->tmpl = userdb_template_build(pool, "passwd", args);
+ module->module.blocking = TRUE;
+
+ if (userdb_template_remove(module->tmpl, "blocking", &value))
+ module->module.blocking = strcasecmp(value, "yes") == 0;
+ /* FIXME: backwards compatibility */
+ if (!userdb_template_is_empty(module->tmpl))
+ i_warning("userdb passwd: Move templates args to override_fields setting");
+ return &module->module;
+}
+
+struct userdb_module_interface userdb_passwd = {
+ "passwd",
+
+ passwd_passwd_preinit,
+ NULL,
+ NULL,
+
+ passwd_lookup,
+
+ passwd_iterate_init,
+ passwd_iterate_next,
+ passwd_iterate_deinit
+};
+#else
+struct userdb_module_interface userdb_passwd = {
+ .name = "passwd"
+};
+#endif
diff --git a/src/auth/userdb-prefetch.c b/src/auth/userdb-prefetch.c
new file mode 100644
index 0000000..a5730b9
--- /dev/null
+++ b/src/auth/userdb-prefetch.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#ifdef USERDB_PREFETCH
+
+#include "str.h"
+#include "var-expand.h"
+
+
+static void prefetch_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ /* auth_request_set_field() should have already placed the userdb_*
+ values to userdb_reply. */
+ if (!auth_request->userdb_prefetch_set) {
+ if (auth_request_get_auth(auth_request)->userdbs->next == NULL) {
+ /* no other userdbs */
+ if (auth_request->userdb_lookup) {
+ e_error(authdb_event(auth_request),
+ "userdb lookup not possible with only userdb prefetch");
+ } else {
+ e_error(authdb_event(auth_request),
+ "passdb didn't return userdb entries");
+ }
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+ /* more userdbs, they may know the user */
+ e_debug(authdb_event(auth_request),
+ "passdb didn't return userdb entries, "
+ "trying the next userdb");
+ callback(USERDB_RESULT_USER_UNKNOWN, auth_request);
+ return;
+ }
+
+ e_debug(authdb_event(auth_request), "success");
+ callback(USERDB_RESULT_OK, auth_request);
+}
+
+struct userdb_module_interface userdb_prefetch = {
+ "prefetch",
+
+ NULL,
+ NULL,
+ NULL,
+
+ prefetch_lookup,
+
+ NULL,
+ NULL,
+ NULL
+};
+#else
+struct userdb_module_interface userdb_prefetch = {
+ .name = "prefetch"
+};
+#endif
diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c
new file mode 100644
index 0000000..3a87640
--- /dev/null
+++ b/src/auth/userdb-sql.c
@@ -0,0 +1,319 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "userdb.h"
+
+#ifdef USERDB_SQL
+
+#include "auth-cache.h"
+#include "db-sql.h"
+
+#include <string.h>
+
+struct sql_userdb_module {
+ struct userdb_module module;
+
+ struct db_sql_connection *conn;
+};
+
+struct userdb_sql_request {
+ struct auth_request *auth_request;
+ userdb_callback_t *callback;
+};
+
+struct sql_userdb_iterate_context {
+ struct userdb_iterate_context ctx;
+ struct sql_result *result;
+ bool freed:1;
+ bool call_iter:1;
+};
+
+static void userdb_sql_iterate_next(struct userdb_iterate_context *_ctx);
+static int userdb_sql_iterate_deinit(struct userdb_iterate_context *_ctx);
+
+static void
+sql_query_get_result(struct sql_result *result,
+ struct auth_request *auth_request)
+{
+ const char *name, *value;
+ unsigned int i, fields_count;
+
+ fields_count = sql_result_get_fields_count(result);
+ for (i = 0; i < fields_count; i++) {
+ name = sql_result_get_field_name(result, i);
+ value = sql_result_get_field_value(result, i);
+
+ if (*name != '\0' && value != NULL) {
+ auth_request_set_userdb_field(auth_request,
+ name, value);
+ }
+ }
+}
+
+static void sql_query_callback(struct sql_result *sql_result,
+ struct userdb_sql_request *sql_request)
+{
+ struct auth_request *auth_request = sql_request->auth_request;
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+ enum userdb_result result = USERDB_RESULT_INTERNAL_FAILURE;
+ int ret;
+
+ ret = sql_result_next_row(sql_result);
+ if (ret >= 0)
+ db_sql_success(module->conn);
+ if (ret < 0) {
+ if (!module->conn->default_user_query) {
+ e_error(authdb_event(auth_request),
+ "User query failed: %s",
+ sql_result_get_error(sql_result));
+ } else {
+ e_error(authdb_event(auth_request),
+ "User query failed: %s "
+ "(using built-in default user_query: %s)",
+ sql_result_get_error(sql_result),
+ module->conn->set.user_query);
+ }
+ } else if (ret == 0) {
+ result = USERDB_RESULT_USER_UNKNOWN;
+ auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB);
+ } else {
+ sql_query_get_result(sql_result, auth_request);
+ result = USERDB_RESULT_OK;
+ }
+
+ sql_request->callback(result, auth_request);
+ auth_request_unref(&auth_request);
+ i_free(sql_request);
+}
+
+static const char *
+userdb_sql_escape(const char *str, const struct auth_request *auth_request)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+
+ return sql_escape_string(module->conn->db, str);
+}
+
+static void userdb_sql_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+ struct userdb_sql_request *sql_request;
+ const char *query, *error;
+
+ if (t_auth_request_var_expand(module->conn->set.user_query,
+ auth_request, userdb_sql_escape,
+ &query, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand user_query=%s: %s",
+ module->conn->set.user_query, error);
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ return;
+ }
+
+ auth_request_ref(auth_request);
+ sql_request = i_new(struct userdb_sql_request, 1);
+ sql_request->callback = callback;
+ sql_request->auth_request = auth_request;
+
+ e_debug(authdb_event(auth_request), "%s", query);
+
+ sql_query(module->conn->db, query,
+ sql_query_callback, sql_request);
+}
+
+static void sql_iter_query_callback(struct sql_result *sql_result,
+ struct sql_userdb_iterate_context *ctx)
+{
+ ctx->result = sql_result;
+ sql_result_ref(sql_result);
+
+ if (ctx->freed)
+ (void)userdb_sql_iterate_deinit(&ctx->ctx);
+ else if (ctx->call_iter)
+ userdb_sql_iterate_next(&ctx->ctx);
+}
+
+static struct userdb_iterate_context *
+userdb_sql_iterate_init(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback, void *context)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+ struct sql_userdb_iterate_context *ctx;
+ const char *query, *error;
+
+ if (t_auth_request_var_expand(module->conn->set.iterate_query,
+ auth_request, userdb_sql_escape,
+ &query, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand iterate_query=%s: %s",
+ module->conn->set.iterate_query, error);
+ }
+
+ ctx = i_new(struct sql_userdb_iterate_context, 1);
+ ctx->ctx.auth_request = auth_request;
+ ctx->ctx.callback = callback;
+ ctx->ctx.context = context;
+ auth_request_ref(auth_request);
+
+ sql_query(module->conn->db, query,
+ sql_iter_query_callback, ctx);
+ e_debug(authdb_event(auth_request), "%s", query);
+ return &ctx->ctx;
+}
+
+static int userdb_sql_iterate_get_user(struct sql_userdb_iterate_context *ctx,
+ const char **user_r)
+{
+ const char *domain;
+ int idx;
+
+ /* try user first */
+ idx = sql_result_find_field(ctx->result, "user");
+ if (idx == 0) {
+ *user_r = sql_result_get_field_value(ctx->result, idx);
+ return 0;
+ }
+
+ /* username [+ domain]? */
+ idx = sql_result_find_field(ctx->result, "username");
+ if (idx < 0) {
+ /* no user or username, fail */
+ return -1;
+ }
+
+ *user_r = sql_result_get_field_value(ctx->result, idx);
+ if (*user_r == NULL)
+ return 0;
+
+ domain = sql_result_find_field_value(ctx->result, "domain");
+ if (domain != NULL)
+ *user_r = t_strconcat(*user_r, "@", domain, NULL);
+ return 0;
+}
+
+static void userdb_sql_iterate_next(struct userdb_iterate_context *_ctx)
+{
+ struct sql_userdb_iterate_context *ctx =
+ (struct sql_userdb_iterate_context *)_ctx;
+ struct userdb_module *_module = _ctx->auth_request->userdb->userdb;
+ struct sql_userdb_module *module = (struct sql_userdb_module *)_module;
+ const char *user;
+ int ret;
+
+ if (ctx->result == NULL) {
+ /* query not finished yet */
+ ctx->call_iter = TRUE;
+ return;
+ }
+
+ ret = sql_result_next_row(ctx->result);
+ if (ret >= 0)
+ db_sql_success(module->conn);
+ if (ret > 0) {
+ if (userdb_sql_iterate_get_user(ctx, &user) < 0)
+ e_error(authdb_event(_ctx->auth_request),
+ "sql: Iterate query didn't return 'user' field");
+ else if (user == NULL)
+ e_error(authdb_event(_ctx->auth_request),
+ "sql: Iterate query returned NULL user");
+ else {
+ _ctx->callback(user, _ctx->context);
+ return;
+ }
+ _ctx->failed = TRUE;
+ } else if (ret < 0) {
+ if (!module->conn->default_iterate_query) {
+ e_error(authdb_event(_ctx->auth_request),
+ "sql: Iterate query failed: %s",
+ sql_result_get_error(ctx->result));
+ } else {
+ e_error(authdb_event(_ctx->auth_request),
+ "sql: Iterate query failed: %s "
+ "(using built-in default iterate_query: %s)",
+ sql_result_get_error(ctx->result),
+ module->conn->set.iterate_query);
+ }
+ _ctx->failed = TRUE;
+ }
+ _ctx->callback(NULL, _ctx->context);
+}
+
+static int userdb_sql_iterate_deinit(struct userdb_iterate_context *_ctx)
+{
+ struct sql_userdb_iterate_context *ctx =
+ (struct sql_userdb_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ auth_request_unref(&_ctx->auth_request);
+ if (ctx->result == NULL) {
+ /* sql query hasn't finished yet */
+ ctx->freed = TRUE;
+ } else {
+ if (ctx->result != NULL)
+ sql_result_unref(ctx->result);
+ i_free(ctx);
+ }
+ return ret;
+}
+
+static struct userdb_module *
+userdb_sql_preinit(pool_t pool, const char *args)
+{
+ struct sql_userdb_module *module;
+
+ module = p_new(pool, struct sql_userdb_module, 1);
+ module->conn = db_sql_init(args, TRUE);
+
+ module->module.default_cache_key =
+ auth_cache_parse_key(pool, module->conn->set.user_query);
+ return &module->module;
+}
+
+static void userdb_sql_init(struct userdb_module *_module)
+{
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+ enum sql_db_flags flags;
+
+ flags = sql_get_flags(module->conn->db);
+ _module->blocking = (flags & SQL_DB_FLAG_BLOCKING) != 0;
+
+ if (!_module->blocking || worker)
+ db_sql_connect(module->conn);
+}
+
+static void userdb_sql_deinit(struct userdb_module *_module)
+{
+ struct sql_userdb_module *module =
+ (struct sql_userdb_module *)_module;
+
+ db_sql_unref(&module->conn);
+}
+
+struct userdb_module_interface userdb_sql = {
+ "sql",
+
+ userdb_sql_preinit,
+ userdb_sql_init,
+ userdb_sql_deinit,
+
+ userdb_sql_lookup,
+
+ userdb_sql_iterate_init,
+ userdb_sql_iterate_next,
+ userdb_sql_iterate_deinit
+};
+#else
+struct userdb_module_interface userdb_sql = {
+ .name = "sql"
+};
+#endif
diff --git a/src/auth/userdb-static.c b/src/auth/userdb-static.c
new file mode 100644
index 0000000..bed0f58
--- /dev/null
+++ b/src/auth/userdb-static.c
@@ -0,0 +1,144 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "userdb.h"
+#include "userdb-template.h"
+
+
+struct static_context {
+ userdb_callback_t *callback, *old_callback;
+ void *old_context;
+};
+
+struct static_userdb_module {
+ struct userdb_module module;
+ struct userdb_template *tmpl;
+
+ bool allow_all_users:1;
+};
+
+static void static_lookup_real(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct static_userdb_module *module =
+ (struct static_userdb_module *)_module;
+ const char *error;
+
+ if (userdb_template_export(module->tmpl, auth_request, &error) < 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand template: %s", error);
+ callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ }
+ callback(USERDB_RESULT_OK, auth_request);
+}
+
+static void
+static_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ struct auth_request *auth_request)
+{
+ struct static_context *ctx = auth_request->context;
+
+ auth_request->userdb_lookup = TRUE;
+
+ auth_request->private_callback.userdb = ctx->old_callback;
+ auth_request->context = ctx->old_context;
+ auth_request_set_state(auth_request, AUTH_REQUEST_STATE_USERDB);
+
+ switch (result) {
+ case PASSDB_RESULT_OK:
+ static_lookup_real(auth_request, ctx->callback);
+ break;
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_USER_DISABLED:
+ case PASSDB_RESULT_PASS_EXPIRED:
+ ctx->callback(USERDB_RESULT_USER_UNKNOWN, auth_request);
+ break;
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ e_error(authdb_event(auth_request),
+ "passdb doesn't support lookups, "
+ "can't verify user's existence");
+ /* fall through */
+ default:
+ ctx->callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request);
+ break;
+ }
+
+ i_free(ctx);
+}
+
+static void static_lookup(struct auth_request *auth_request,
+ userdb_callback_t *callback)
+{
+ struct userdb_module *_module = auth_request->userdb->userdb;
+ struct static_userdb_module *module =
+ (struct static_userdb_module *)_module;
+ struct static_context *ctx;
+
+ if (!auth_request->fields.successful && !module->allow_all_users) {
+ /* this is a userdb-only lookup. we need to know if this
+ users exists or not. use a passdb lookup to do that.
+ if the passdb doesn't support returning credentials, this
+ will of course fail.. */
+ ctx = i_new(struct static_context, 1);
+ ctx->old_callback = auth_request->private_callback.userdb;
+ ctx->old_context = auth_request->context;
+ ctx->callback = callback;
+
+ i_assert(auth_request->state == AUTH_REQUEST_STATE_USERDB);
+ auth_request_set_state(auth_request,
+ AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ auth_request->context = ctx;
+ if (auth_request->passdb != NULL) {
+ /* kludge: temporarily work as if we weren't doing
+ a userdb lookup. this is to get auth cache to use
+ passdb caching instead of userdb caching. */
+ auth_request->userdb_lookup = FALSE;
+ auth_request_lookup_credentials(auth_request, "",
+ static_credentials_callback);
+ } else {
+ static_credentials_callback(
+ PASSDB_RESULT_SCHEME_NOT_AVAILABLE,
+ uchar_empty_ptr, 0, auth_request);
+ }
+ } else {
+ static_lookup_real(auth_request, callback);
+ }
+}
+
+static struct userdb_module *
+static_preinit(pool_t pool, const char *args)
+{
+ struct static_userdb_module *module;
+ const char *value;
+
+ module = p_new(pool, struct static_userdb_module, 1);
+ module->tmpl = userdb_template_build(pool, "static", args);
+
+ if (userdb_template_remove(module->tmpl, "allow_all_users", &value)) {
+ module->allow_all_users = value == NULL ||
+ strcasecmp(value, "yes") == 0;
+ }
+ return &module->module;
+}
+
+struct userdb_module_interface userdb_static = {
+ "static",
+
+ static_preinit,
+ NULL,
+ NULL,
+
+ static_lookup,
+
+ NULL,
+ NULL,
+ NULL
+};
diff --git a/src/auth/userdb-template.c b/src/auth/userdb-template.c
new file mode 100644
index 0000000..a1e3011
--- /dev/null
+++ b/src/auth/userdb-template.c
@@ -0,0 +1,124 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "userdb.h"
+#include "userdb-template.h"
+
+struct userdb_template {
+ ARRAY(const char *) args;
+};
+
+struct userdb_template *
+userdb_template_build(pool_t pool, const char *userdb_name, const char *args)
+{
+ struct userdb_template *tmpl;
+ const char *const *tmp, *key, *value, *nonull_value;
+ uid_t uid;
+ gid_t gid;
+
+ tmpl = p_new(pool, struct userdb_template, 1);
+
+ tmp = t_strsplit_spaces(args, " ");
+ p_array_init(&tmpl->args, pool, str_array_length(tmp));
+
+ for (; *tmp != NULL; tmp++) {
+ value = strchr(*tmp, '=');
+ if (value == NULL)
+ key = *tmp;
+ else
+ key = t_strdup_until(*tmp, value++);
+
+
+ if (*key == '\0')
+ i_fatal("Invalid userdb template %s - key must not be empty",
+ args);
+
+ nonull_value = value == NULL ? "" : value;
+ if (strcasecmp(key, "uid") == 0) {
+ uid = userdb_parse_uid(NULL, nonull_value);
+ if (uid == (uid_t)-1) {
+ i_fatal("%s userdb: Invalid uid: %s",
+ userdb_name, nonull_value);
+ }
+ value = dec2str(uid);
+ } else if (strcasecmp(key, "gid") == 0) {
+ gid = userdb_parse_gid(NULL, nonull_value);
+ if (gid == (gid_t)-1) {
+ i_fatal("%s userdb: Invalid gid: %s",
+ userdb_name, nonull_value);
+ }
+ value = dec2str(gid);
+ } else if (*key == '\0') {
+ i_fatal("%s userdb: Empty key (=%s)",
+ userdb_name, nonull_value);
+ }
+ key = p_strdup(pool, key);
+ value = p_strdup(pool, value);
+
+ array_push_back(&tmpl->args, &key);
+ array_push_back(&tmpl->args, &value);
+ }
+ return tmpl;
+}
+
+int userdb_template_export(struct userdb_template *tmpl,
+ struct auth_request *auth_request,
+ const char **error_r)
+{
+ const struct var_expand_table *table;
+ string_t *str;
+ const char *const *args, *value;
+ unsigned int i, count;
+
+ if (userdb_template_is_empty(tmpl))
+ return 0;
+
+ str = t_str_new(256);
+ table = auth_request_get_var_expand_table(auth_request, NULL);
+
+ args = array_get(&tmpl->args, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (args[i+1] == NULL)
+ value = "";
+ else {
+ str_truncate(str, 0);
+ if (auth_request_var_expand_with_table(str, args[i+1],
+ auth_request, table, NULL, error_r) <= 0)
+ return -1;
+ value = str_c(str);
+ }
+ auth_request_set_userdb_field(auth_request, args[i], value);
+ }
+ return 0;
+}
+
+bool userdb_template_remove(struct userdb_template *tmpl,
+ const char *key, const char **value_r)
+{
+ const char *const *args;
+ unsigned int i, count;
+
+ args = array_get(&tmpl->args, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(args[i], key) == 0) {
+ *value_r = args[i+1];
+ array_delete(&tmpl->args, i, 2);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool userdb_template_is_empty(struct userdb_template *tmpl)
+{
+ return array_count(&tmpl->args) == 0;
+}
+
+const char *const *userdb_template_get_args(struct userdb_template *tmpl, unsigned int *count_r)
+{
+ return array_get(&tmpl->args, count_r);
+}
diff --git a/src/auth/userdb-template.h b/src/auth/userdb-template.h
new file mode 100644
index 0000000..a146a87
--- /dev/null
+++ b/src/auth/userdb-template.h
@@ -0,0 +1,15 @@
+#ifndef USERDB_TEMPLATE_H
+#define USERDB_TEMPLATE_H
+
+struct userdb_template *
+userdb_template_build(pool_t pool, const char *userdb_name, const char *args);
+int userdb_template_export(struct userdb_template *tmpl,
+ struct auth_request *auth_request,
+ const char **error_r);
+bool userdb_template_remove(struct userdb_template *tmpl,
+ const char *key, const char **value_r);
+bool userdb_template_is_empty(struct userdb_template *tmpl);
+const char *const *userdb_template_get_args(struct userdb_template *tmpl,
+ unsigned int *count_r);
+
+#endif
diff --git a/src/auth/userdb.c b/src/auth/userdb.c
new file mode 100644
index 0000000..21751f9
--- /dev/null
+++ b/src/auth/userdb.c
@@ -0,0 +1,256 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "ipwd.h"
+#include "auth-worker-server.h"
+#include "userdb.h"
+
+static ARRAY(struct userdb_module_interface *) userdb_interfaces;
+static ARRAY(struct userdb_module *) userdb_modules;
+
+static const struct userdb_module_interface userdb_iface_deinit = {
+ .name = "deinit"
+};
+
+static struct userdb_module_interface *userdb_interface_find(const char *name)
+{
+ struct userdb_module_interface *iface;
+
+ array_foreach_elem(&userdb_interfaces, iface) {
+ if (strcmp(iface->name, name) == 0)
+ return iface;
+ }
+ return NULL;
+}
+
+void userdb_register_module(struct userdb_module_interface *iface)
+{
+ struct userdb_module_interface *old_iface;
+
+ old_iface = userdb_interface_find(iface->name);
+ if (old_iface != NULL && old_iface->lookup == NULL) {
+ /* replacing a "support not compiled in" userdb */
+ userdb_unregister_module(old_iface);
+ } else if (old_iface != NULL) {
+ i_panic("userdb_register_module(%s): Already registered",
+ iface->name);
+ }
+ array_push_back(&userdb_interfaces, &iface);
+}
+
+void userdb_unregister_module(struct userdb_module_interface *iface)
+{
+ struct userdb_module_interface *const *ifaces;
+ unsigned int idx;
+
+ array_foreach(&userdb_interfaces, ifaces) {
+ if (*ifaces == iface) {
+ idx = array_foreach_idx(&userdb_interfaces, ifaces);
+ array_delete(&userdb_interfaces, idx, 1);
+ return;
+ }
+ }
+ i_panic("userdb_unregister_module(%s): Not registered", iface->name);
+}
+
+uid_t userdb_parse_uid(struct auth_request *request, const char *str)
+{
+ struct passwd pw;
+ uid_t uid;
+
+ if (str == NULL)
+ return (uid_t)-1;
+
+ if (str_to_uid(str, &uid) == 0)
+ return uid;
+
+ switch (i_getpwnam(str, &pw)) {
+ case -1:
+ e_error(request == NULL ? auth_event : authdb_event(request),
+ "getpwnam() failed: %m");
+ return (uid_t)-1;
+ case 0:
+ e_error(request == NULL ? auth_event : authdb_event(request),
+ "Invalid UID value '%s'", str);
+ return (uid_t)-1;
+ default:
+ return pw.pw_uid;
+ }
+}
+
+gid_t userdb_parse_gid(struct auth_request *request, const char *str)
+{
+ struct group gr;
+ gid_t gid;
+
+ if (str == NULL)
+ return (gid_t)-1;
+
+ if (str_to_gid(str, &gid) == 0)
+ return gid;
+
+ switch (i_getgrnam(str, &gr)) {
+ case -1:
+ e_error(request == NULL ? auth_event : authdb_event(request),
+ "getgrnam() failed: %m");
+ return (gid_t)-1;
+ case 0:
+ e_error(request == NULL ? auth_event : authdb_event(request),
+ "Invalid GID value '%s'", str);
+ return (gid_t)-1;
+ default:
+ return gr.gr_gid;
+ }
+}
+
+static struct userdb_module *
+userdb_find(const char *driver, const char *args, unsigned int *idx_r)
+{
+ struct userdb_module *const *userdbs;
+ unsigned int i, count;
+
+ userdbs = array_get(&userdb_modules, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(userdbs[i]->iface->name, driver) == 0 &&
+ strcmp(userdbs[i]->args, args) == 0) {
+ *idx_r = i;
+ return userdbs[i];
+ }
+ }
+ return NULL;
+}
+
+struct userdb_module *
+userdb_preinit(pool_t pool, const struct auth_userdb_settings *set)
+{
+ static unsigned int auth_userdb_id = 0;
+ struct userdb_module_interface *iface;
+ struct userdb_module *userdb;
+ unsigned int idx;
+
+ iface = userdb_interface_find(set->driver);
+ if (iface == NULL || iface->lookup == NULL) {
+ /* maybe it's a plugin. try to load it. */
+ auth_module_load(t_strconcat("authdb_", set->driver, NULL));
+ iface = userdb_interface_find(set->driver);
+ }
+ if (iface == NULL)
+ i_fatal("Unknown userdb driver '%s'", set->driver);
+ if (iface->lookup == NULL) {
+ i_fatal("Support not compiled in for userdb driver '%s'",
+ set->driver);
+ }
+ if (iface->preinit == NULL && iface->init == NULL &&
+ *set->args != '\0') {
+ i_fatal("userdb %s: No args are supported: %s",
+ set->driver, set->args);
+ }
+
+ userdb = userdb_find(set->driver, set->args, &idx);
+ if (userdb != NULL)
+ return userdb;
+
+ if (iface->preinit == NULL)
+ userdb = p_new(pool, struct userdb_module, 1);
+ else
+ userdb = iface->preinit(pool, set->args);
+ userdb->id = ++auth_userdb_id;
+ userdb->iface = iface;
+ userdb->args = p_strdup(pool, set->args);
+
+ array_push_back(&userdb_modules, &userdb);
+ return userdb;
+}
+
+void userdb_init(struct userdb_module *userdb)
+{
+ if (userdb->iface->init != NULL && userdb->init_refcount == 0)
+ userdb->iface->init(userdb);
+ userdb->init_refcount++;
+}
+
+void userdb_deinit(struct userdb_module *userdb)
+{
+ unsigned int idx;
+
+ i_assert(userdb->init_refcount > 0);
+
+ if (--userdb->init_refcount > 0)
+ return;
+
+ if (userdb_find(userdb->iface->name, userdb->args, &idx) == NULL)
+ i_unreached();
+ array_delete(&userdb_modules, idx, 1);
+
+ if (userdb->iface->deinit != NULL)
+ userdb->iface->deinit(userdb);
+
+ /* make sure userdb isn't accessed again */
+ userdb->iface = &userdb_iface_deinit;
+}
+
+void userdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN])
+{
+ struct md5_context ctx;
+ struct userdb_module *const *userdbs;
+ unsigned int i, count;
+
+ md5_init(&ctx);
+ userdbs = array_get(&userdb_modules, &count);
+ for (i = 0; i < count; i++) {
+ md5_update(&ctx, &userdbs[i]->id, sizeof(userdbs[i]->id));
+ md5_update(&ctx, userdbs[i]->iface->name,
+ strlen(userdbs[i]->iface->name));
+ md5_update(&ctx, userdbs[i]->args, strlen(userdbs[i]->args));
+ }
+ md5_final(&ctx, md5);
+}
+
+const char *userdb_result_to_string(enum userdb_result result)
+{
+ switch (result) {
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ return "internal_failure";
+ case USERDB_RESULT_USER_UNKNOWN:
+ return "user_unknown";
+ case USERDB_RESULT_OK:
+ return "ok";
+ }
+ i_unreached();
+}
+
+extern struct userdb_module_interface userdb_prefetch;
+extern struct userdb_module_interface userdb_static;
+extern struct userdb_module_interface userdb_passwd;
+extern struct userdb_module_interface userdb_passwd_file;
+extern struct userdb_module_interface userdb_ldap;
+extern struct userdb_module_interface userdb_sql;
+extern struct userdb_module_interface userdb_checkpassword;
+extern struct userdb_module_interface userdb_dict;
+#ifdef HAVE_LUA
+extern struct userdb_module_interface userdb_lua;
+#endif
+
+void userdbs_init(void)
+{
+ i_array_init(&userdb_interfaces, 16);
+ i_array_init(&userdb_modules, 16);
+ userdb_register_module(&userdb_passwd);
+ userdb_register_module(&userdb_passwd_file);
+ userdb_register_module(&userdb_prefetch);
+ userdb_register_module(&userdb_static);
+ userdb_register_module(&userdb_ldap);
+ userdb_register_module(&userdb_sql);
+ userdb_register_module(&userdb_checkpassword);
+ userdb_register_module(&userdb_dict);
+#ifdef HAVE_LUA
+ userdb_register_module(&userdb_lua);
+#endif
+}
+
+void userdbs_deinit(void)
+{
+ array_free(&userdb_modules);
+ array_free(&userdb_interfaces);
+}
diff --git a/src/auth/userdb.h b/src/auth/userdb.h
new file mode 100644
index 0000000..be64144
--- /dev/null
+++ b/src/auth/userdb.h
@@ -0,0 +1,92 @@
+#ifndef USERDB_H
+#define USERDB_H
+
+#include "md5.h"
+#include "auth-fields.h"
+
+struct auth;
+struct auth_request;
+struct auth_userdb_settings;
+
+enum userdb_result {
+ USERDB_RESULT_INTERNAL_FAILURE = -1,
+ USERDB_RESULT_USER_UNKNOWN = -2,
+
+ USERDB_RESULT_OK = 1
+};
+
+typedef void userdb_callback_t(enum userdb_result result,
+ struct auth_request *request);
+/* user=NULL when there are no more users */
+typedef void userdb_iter_callback_t(const char *user, void *context);
+
+struct userdb_module {
+ const char *args;
+ /* The default caching key for this module, or NULL if caching isn't
+ wanted. This is updated by settings in auth_userdb. */
+ const char *default_cache_key;
+
+ /* If blocking is set to TRUE, use child processes to access
+ this userdb. */
+ bool blocking;
+ /* id is used by blocking userdb to identify the userdb */
+ unsigned int id;
+
+ /* number of time init() has been called */
+ int init_refcount;
+
+ /* WARNING: avoid adding anything here that isn't based on args.
+ if you do, you need to change userdb.c:userdb_find() also to avoid
+ accidentally merging wrong userdbs. */
+
+ const struct userdb_module_interface *iface;
+};
+
+struct userdb_iterate_context {
+ struct auth_request *auth_request;
+ userdb_iter_callback_t *callback;
+ void *context;
+ bool failed;
+};
+
+struct userdb_module_interface {
+ const char *name;
+
+ struct userdb_module *(*preinit)(pool_t pool, const char *args);
+ void (*init)(struct userdb_module *module);
+ void (*deinit)(struct userdb_module *module);
+
+ void (*lookup)(struct auth_request *auth_request,
+ userdb_callback_t *callback);
+
+ struct userdb_iterate_context *
+ (*iterate_init)(struct auth_request *auth_request,
+ userdb_iter_callback_t *callback,
+ void *context);
+ void (*iterate_next)(struct userdb_iterate_context *ctx);
+ int (*iterate_deinit)(struct userdb_iterate_context *ctx);
+};
+
+const char *userdb_result_to_string(enum userdb_result result);
+
+uid_t userdb_parse_uid(struct auth_request *request, const char *str)
+ ATTR_NULL(1);
+gid_t userdb_parse_gid(struct auth_request *request, const char *str)
+ ATTR_NULL(1);
+
+struct userdb_module *
+userdb_preinit(pool_t pool, const struct auth_userdb_settings *set);
+void userdb_init(struct userdb_module *userdb);
+void userdb_deinit(struct userdb_module *userdb);
+
+void userdb_register_module(struct userdb_module_interface *iface);
+void userdb_unregister_module(struct userdb_module_interface *iface);
+
+void userdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]);
+
+void userdbs_init(void);
+void userdbs_deinit(void);
+
+#include "auth-request.h"
+
+#endif
diff --git a/src/config/Makefile.am b/src/config/Makefile.am
new file mode 100644
index 0000000..084ec0e
--- /dev/null
+++ b/src/config/Makefile.am
@@ -0,0 +1,98 @@
+pkgsysconfdir = $(sysconfdir)/dovecot
+pkglibexecdir = $(libexecdir)/dovecot
+exampledir = $(docdir)/example-config
+
+bin_PROGRAMS = doveconf
+pkglibexec_PROGRAMS = config
+
+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 \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DEXAMPLE_CONFIG_DIR=\""$(exampledir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ -DSSLDIR=\""$(ssldir)\"" \
+ -DSYSCONFDIR=\""$(pkgsysconfdir)"\" \
+ $(BINARY_CFLAGS)
+
+noinst_LTLIBRARIES = libconfig.la
+
+config_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+config_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+
+doveconf_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveconf_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+
+common = \
+ config-connection.c \
+ config-filter.c \
+ config-parser.c \
+ config-request.c \
+ old-set-parser.c \
+ sysinfo-get.c
+
+libconfig_la_SOURCES = $(common)
+
+config_SOURCES = \
+ all-settings.c \
+ main.c
+
+doveconf_SOURCES = \
+ all-settings.c \
+ doveconf.c
+
+noinst_HEADERS = \
+ all-settings.h \
+ config-connection.h \
+ old-set-parser.h \
+ sysinfo-get.h
+
+pkginclude_HEADERS = \
+ config-filter.h \
+ config-parser.h \
+ config-parser-private.h \
+ config-request.h
+
+all-settings.c: $(SETTING_FILES) $(top_srcdir)/src/config/settings-get.pl
+ $(top_srcdir)/src/config/settings-get.pl $(SETTING_FILES) > all-settings.c || rm -f all-settings.c
+
+EXTRA_DIST = \
+ config-settings.c \
+ settings-get.pl
+
+test_programs = \
+ test-config-parser
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT)
+
+test_config_parser_CFLAGS = $(AM_CPPFLAGS)
+test_config_parser_SOURCES = test-config-parser.c
+test_config_parser_LDADD = $(test_libs)
+test_config_parser_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/config/Makefile.in b/src/config/Makefile.in
new file mode 100644
index 0000000..b071404
--- /dev/null
+++ b/src/config/Makefile.in
@@ -0,0 +1,1079 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+bin_PROGRAMS = doveconf$(EXEEXT)
+pkglibexec_PROGRAMS = config$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/config
+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) \
+ $(pkginclude_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkgincludedir)"
+am__EXEEXT_1 = test-config-parser$(EXEEXT)
+PROGRAMS = $(bin_PROGRAMS) $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libconfig_la_LIBADD =
+am__objects_1 = config-connection.lo config-filter.lo config-parser.lo \
+ config-request.lo old-set-parser.lo sysinfo-get.lo
+am_libconfig_la_OBJECTS = $(am__objects_1)
+libconfig_la_OBJECTS = $(am_libconfig_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_config_OBJECTS = all-settings.$(OBJEXT) main.$(OBJEXT)
+config_OBJECTS = $(am_config_OBJECTS)
+am__DEPENDENCIES_1 =
+am_doveconf_OBJECTS = all-settings.$(OBJEXT) doveconf.$(OBJEXT)
+doveconf_OBJECTS = $(am_doveconf_OBJECTS)
+am_test_config_parser_OBJECTS = \
+ test_config_parser-test-config-parser.$(OBJEXT)
+test_config_parser_OBJECTS = $(am_test_config_parser_OBJECTS)
+am__DEPENDENCIES_2 = $(noinst_LTLIBRARIES) $(am__DEPENDENCIES_1)
+test_config_parser_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(test_config_parser_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)/all-settings.Po \
+ ./$(DEPDIR)/config-connection.Plo \
+ ./$(DEPDIR)/config-filter.Plo ./$(DEPDIR)/config-parser.Plo \
+ ./$(DEPDIR)/config-request.Plo ./$(DEPDIR)/doveconf.Po \
+ ./$(DEPDIR)/main.Po ./$(DEPDIR)/old-set-parser.Plo \
+ ./$(DEPDIR)/sysinfo-get.Plo \
+ ./$(DEPDIR)/test_config_parser-test-config-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 =
+SOURCES = $(libconfig_la_SOURCES) $(config_SOURCES) \
+ $(doveconf_SOURCES) $(test_config_parser_SOURCES)
+DIST_SOURCES = $(libconfig_la_SOURCES) $(config_SOURCES) \
+ $(doveconf_SOURCES) $(test_config_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; }; \
+ }
+HEADERS = $(noinst_HEADERS) $(pkginclude_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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
+exampledir = $(docdir)/example-config
+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 \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DEXAMPLE_CONFIG_DIR=\""$(exampledir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ -DSSLDIR=\""$(ssldir)\"" \
+ -DSYSCONFDIR=\""$(pkgsysconfdir)"\" \
+ $(BINARY_CFLAGS)
+
+noinst_LTLIBRARIES = libconfig.la
+config_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+config_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+doveconf_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(RAND_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveconf_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+common = \
+ config-connection.c \
+ config-filter.c \
+ config-parser.c \
+ config-request.c \
+ old-set-parser.c \
+ sysinfo-get.c
+
+libconfig_la_SOURCES = $(common)
+config_SOURCES = \
+ all-settings.c \
+ main.c
+
+doveconf_SOURCES = \
+ all-settings.c \
+ doveconf.c
+
+noinst_HEADERS = \
+ all-settings.h \
+ config-connection.h \
+ old-set-parser.h \
+ sysinfo-get.h
+
+pkginclude_HEADERS = \
+ config-filter.h \
+ config-parser.h \
+ config-parser-private.h \
+ config-request.h
+
+EXTRA_DIST = \
+ config-settings.c \
+ settings-get.pl
+
+test_programs = \
+ test-config-parser
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT)
+
+test_config_parser_CFLAGS = $(AM_CPPFLAGS)
+test_config_parser_SOURCES = test-config-parser.c
+test_config_parser_LDADD = $(test_libs)
+test_config_parser_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(noinst_LTLIBRARIES)
+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/config/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/config/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-binPROGRAMS: $(bin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-binPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+ @list='$(bin_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-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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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}; \
+ }
+
+libconfig.la: $(libconfig_la_OBJECTS) $(libconfig_la_DEPENDENCIES) $(EXTRA_libconfig_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libconfig_la_OBJECTS) $(libconfig_la_LIBADD) $(LIBS)
+
+config$(EXEEXT): $(config_OBJECTS) $(config_DEPENDENCIES) $(EXTRA_config_DEPENDENCIES)
+ @rm -f config$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(config_OBJECTS) $(config_LDADD) $(LIBS)
+
+doveconf$(EXEEXT): $(doveconf_OBJECTS) $(doveconf_DEPENDENCIES) $(EXTRA_doveconf_DEPENDENCIES)
+ @rm -f doveconf$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(doveconf_OBJECTS) $(doveconf_LDADD) $(LIBS)
+
+test-config-parser$(EXEEXT): $(test_config_parser_OBJECTS) $(test_config_parser_DEPENDENCIES) $(EXTRA_test_config_parser_DEPENDENCIES)
+ @rm -f test-config-parser$(EXEEXT)
+ $(AM_V_CCLD)$(test_config_parser_LINK) $(test_config_parser_OBJECTS) $(test_config_parser_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/all-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/config-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/config-filter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/config-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/config-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveconf.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/old-set-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sysinfo-get.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_config_parser-test-config-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 $@ $<
+
+test_config_parser-test-config-parser.o: test-config-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_config_parser_CFLAGS) $(CFLAGS) -MT test_config_parser-test-config-parser.o -MD -MP -MF $(DEPDIR)/test_config_parser-test-config-parser.Tpo -c -o test_config_parser-test-config-parser.o `test -f 'test-config-parser.c' || echo '$(srcdir)/'`test-config-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_config_parser-test-config-parser.Tpo $(DEPDIR)/test_config_parser-test-config-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-config-parser.c' object='test_config_parser-test-config-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) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_config_parser_CFLAGS) $(CFLAGS) -c -o test_config_parser-test-config-parser.o `test -f 'test-config-parser.c' || echo '$(srcdir)/'`test-config-parser.c
+
+test_config_parser-test-config-parser.obj: test-config-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_config_parser_CFLAGS) $(CFLAGS) -MT test_config_parser-test-config-parser.obj -MD -MP -MF $(DEPDIR)/test_config_parser-test-config-parser.Tpo -c -o test_config_parser-test-config-parser.obj `if test -f 'test-config-parser.c'; then $(CYGPATH_W) 'test-config-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-config-parser.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_config_parser-test-config-parser.Tpo $(DEPDIR)/test_config_parser-test-config-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-config-parser.c' object='test_config_parser-test-config-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) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_config_parser_CFLAGS) $(CFLAGS) -c -o test_config_parser-test-config-parser.obj `if test -f 'test-config-parser.c'; then $(CYGPATH_W) 'test-config-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-config-parser.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkgincludeHEADERS: $(pkginclude_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginclude_HEADERS)'; test -n "$(pkgincludedir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkgincludedir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkgincludedir)" || 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)$(pkgincludedir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkgincludedir)" || exit $$?; \
+ done
+
+uninstall-pkgincludeHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginclude_HEADERS)'; test -n "$(pkgincludedir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkgincludedir)'; $(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)$(bindir)" "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(pkgincludedir)"; 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-binPROGRAMS clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/all-settings.Po
+ -rm -f ./$(DEPDIR)/config-connection.Plo
+ -rm -f ./$(DEPDIR)/config-filter.Plo
+ -rm -f ./$(DEPDIR)/config-parser.Plo
+ -rm -f ./$(DEPDIR)/config-request.Plo
+ -rm -f ./$(DEPDIR)/doveconf.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/old-set-parser.Plo
+ -rm -f ./$(DEPDIR)/sysinfo-get.Plo
+ -rm -f ./$(DEPDIR)/test_config_parser-test-config-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-pkgincludeHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-binPROGRAMS install-pkglibexecPROGRAMS
+
+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)/all-settings.Po
+ -rm -f ./$(DEPDIR)/config-connection.Plo
+ -rm -f ./$(DEPDIR)/config-filter.Plo
+ -rm -f ./$(DEPDIR)/config-parser.Plo
+ -rm -f ./$(DEPDIR)/config-request.Plo
+ -rm -f ./$(DEPDIR)/doveconf.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/old-set-parser.Plo
+ -rm -f ./$(DEPDIR)/sysinfo-get.Plo
+ -rm -f ./$(DEPDIR)/test_config_parser-test-config-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-binPROGRAMS uninstall-pkgincludeHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-binPROGRAMS clean-generic \
+ clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS 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-binPROGRAMS \
+ 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-pkgincludeHEADERS \
+ install-pkglibexecPROGRAMS 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-binPROGRAMS uninstall-pkgincludeHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.PRECIOUS: Makefile
+
+
+all-settings.c: $(SETTING_FILES) $(top_srcdir)/src/config/settings-get.pl
+ $(top_srcdir)/src/config/settings-get.pl $(SETTING_FILES) > all-settings.c || rm -f all-settings.c
+
+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/config/all-settings.c b/src/config/all-settings.c
new file mode 100644
index 0000000..a76242c
--- /dev/null
+++ b/src/config/all-settings.c
@@ -0,0 +1,6112 @@
+/* WARNING: THIS FILE IS GENERATED - DO NOT PATCH!
+ It's not enough alone in any case, because the defaults may be
+ coming from the individual *-settings.c in some situations. If you
+ wish to modify defaults, change the other *-settings.c files and
+ just delete this file. This file will be automatically regenerated
+ by make. (This file is distributed in the tarball only because some
+ systems might not have Perl installed.) */
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ipwd.h"
+#include "var-expand.h"
+#include "file-lock.h"
+#include "fsync-mode.h"
+#include "hash-format.h"
+#include "net.h"
+#include "unichar.h"
+#include "hash-method.h"
+#include "settings-parser.h"
+#include "message-header-parser.h"
+#include "all-settings.h"
+#include <stddef.h>
+#include <unistd.h>
+#define CONFIG_BINARY
+extern buffer_t config_all_services_buf;/* ../../src/lib-storage/mail-storage-settings.h */
+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;
+/* <settings checks> */
+#define MAILBOX_SET_AUTO_NO "no"
+#define MAILBOX_SET_AUTO_CREATE "create"
+#define MAILBOX_SET_AUTO_SUBSCRIBE "subscribe"
+/* </settings checks> */
+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;
+};
+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;
+};
+/* ../../src/lib-storage/index/pop3c/pop3c-settings.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;
+};
+/* ../../src/lib-storage/index/mbox/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;
+};
+/* ../../src/lib-storage/index/maildir/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;
+};
+/* ../../src/lib-storage/index/imapc/imapc-settings.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> */
+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;
+};
+/* ../../src/lib-storage/index/dbox-multi/mdbox-settings.h */
+struct mdbox_settings {
+ bool mdbox_preallocate_space;
+ uoff_t mdbox_rotate_size;
+ unsigned int mdbox_rotate_interval;
+};
+/* ../../src/lib-smtp/smtp-submit-settings.h */
+extern const struct setting_parser_info smtp_submit_setting_parser_info;
+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;
+};
+/* ../../src/lib-settings/settings.h */
+#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) }
+/* ../../src/lib-master/service-settings.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 *);
+/* ../../src/lib-master/master-service-ssl-settings.h */
+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;
+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;
+};
+/* ../../src/lib-master/master-service-settings.h */
+extern const struct setting_parser_info master_service_setting_parser_info;
+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;
+};
+/* ../../src/lib-lda/lda-settings.h */
+extern const struct setting_parser_info lda_setting_parser_info;
+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;
+};
+/* ../../src/lib-dict-backend/dict-sql-settings.h */
+struct dict_sql_settings {
+ const char *connect;
+
+ unsigned int max_pattern_fields_count;
+ ARRAY(struct dict_sql_map) maps;
+};
+/* ../../src/lib-dict-backend/dict-ldap-settings.h */
+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;
+};
+/* ../../src/lib-storage/mail-storage-settings.c */
+extern const struct setting_parser_info mailbox_setting_parser_info;
+extern const struct setting_parser_info mail_namespace_setting_parser_info;
+/* <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> */
+#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
+};
+/* ../../src/lib-storage/index/pop3c/pop3c-settings.c */
+/* <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> */
+#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 = ""
+};
+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
+};
+/* ../../src/lib-storage/index/mbox/mbox-settings.c */
+#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
+};
+/* ../../src/lib-storage/index/maildir/maildir-settings.c */
+#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
+};
+/* ../../src/lib-storage/index/imapc/imapc-settings.c */
+/* <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;
+}
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imapc_settings)
+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
+};
+/* ../../src/lib-storage/index/dbox-multi/mdbox-settings.c */
+#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
+};
+/* ../../src/lib-lda/lda-settings.c */
+#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
+};
+/* ../../src/lib-dict-backend/dict-sql-settings.c */
+#define DEF_STR(name) DEF_STRUCT_STR(name, dict_sql_map)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, dict_sql_map)
+/* ../../src/lib-dict-backend/dict-ldap-settings.c */
+#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)
+/* ../../src/submission/submission-settings.h */
+extern const struct setting_parser_info submission_setting_parser_info;
+/* <settings checks> */
+enum submission_client_workarounds {
+ SUBMISSION_WORKAROUND_WHITESPACE_BEFORE_PATH = BIT(0),
+ SUBMISSION_WORKAROUND_MAILBOX_FOR_PATH = BIT(1),
+};
+/* </settings checks> */
+struct submission_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ const char *hostname;
+
+ const char *login_greeting;
+ const char *login_trusted_networks;
+
+ const char *recipient_delimiter;
+
+ /* submission: */
+ uoff_t submission_max_mail_size;
+ unsigned int submission_max_recipients;
+ const char *submission_client_workarounds;
+ const char *submission_logout_format;
+
+ /* submission backend: */
+ const char *submission_backend_capabilities;
+
+ /* submission relay: */
+ const char *submission_relay_host;
+ in_port_t submission_relay_port;
+ bool submission_relay_trusted;
+
+ const char *submission_relay_user;
+ const char *submission_relay_master_user;
+ const char *submission_relay_password;
+
+ const char *submission_relay_ssl;
+ bool submission_relay_ssl_verify;
+
+ const char *submission_relay_rawlog_dir;
+ unsigned int submission_relay_max_idle_time;
+
+ unsigned int submission_relay_connect_timeout;
+ unsigned int submission_relay_command_timeout;
+
+ /* imap urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+
+ enum submission_client_workarounds parsed_workarounds;
+};
+/* ../../src/submission-login/submission-login-settings.h */
+extern const struct setting_parser_info *submission_login_setting_roots[];
+/* <settings checks> */
+enum submission_login_client_workarounds {
+ SUBMISSION_LOGIN_WORKAROUND_IMPLICIT_AUTH_EXTERNAL = BIT(0),
+ SUBMISSION_LOGIN_WORKAROUND_EXOTIC_BACKEND = BIT(1),
+};
+/* </settings checks> */
+struct submission_login_settings {
+ const char *hostname;
+
+ /* submission: */
+ uoff_t submission_max_mail_size;
+ const char *submission_client_workarounds;
+ const char *submission_backend_capabilities;
+
+ enum submission_login_client_workarounds parsed_workarounds;
+};
+/* ../../src/stats/stats-settings.h */
+extern const struct setting_parser_info stats_setting_parser_info;
+extern const struct setting_parser_info stats_metric_setting_parser_info;
+/* <settings checks> */
+/*
+ * We allow a selection of a timestamp format.
+ *
+ * The 'time-unix' format generates a number with the number of seconds
+ * since 1970-01-01 00:00 UTC.
+ *
+ * The 'time-rfc3339' format uses the YYYY-MM-DDTHH:MM:SS.uuuuuuZ format as
+ * defined by RFC 3339.
+ *
+ * The special native format (not explicitly selectable in the config, but
+ * default if no time-* token is used) uses the format's native timestamp
+ * format. Note that not all formats have a timestamp data format.
+ *
+ * The native format and the rules below try to address the question: can a
+ * parser that doesn't have any knowledge of fields' values' types losslessly
+ * reconstruct the fields?
+ *
+ * For example, JSON only has strings and numbers, so it cannot represent a
+ * timestamp in a "context-free lossless" way. Therefore, when making a
+ * JSON blob, we need to decide which way to serialize timestamps. No
+ * matter how we do it, we incur some loss. If a decoder sees 1557232304 in
+ * a field, it cannot be certain if the field is an integer that just
+ * happens to be a reasonable timestamp, or if it actually is a timestamp.
+ * Same goes with RFC3339 - it could just be that the user supplied a string
+ * that looks like a timestamp, and that string made it into an event field.
+ *
+ * Other common serialization formats, such as CBOR, have a lossless way of
+ * encoding timestamps.
+ *
+ * Note that there are two concepts at play: native and default.
+ *
+ * The rules for how the format's timestamp formats are used:
+ *
+ * 1. The default time format is the native format.
+ * 2. The native time format may or may not exist for a given format (e.g.,
+ * in JSON)
+ * 3. If the native format doesn't exist and no time format was specified in
+ * the config, it is a config error.
+ *
+ * We went with these rules because:
+ *
+ * 1. It prevents type information loss by default.
+ * 2. It completely isolates the policy from the algorithm.
+ * 3. It defers the decision whether each format without a native timestamp
+ * type should have a default acting as native until after we've had some
+ * operational experience.
+ * 4. A future decision to add a default (via 3. point) will be 100% compatible.
+ */
+enum event_exporter_time_fmt {
+ EVENT_EXPORTER_TIME_FMT_NATIVE = 0,
+ EVENT_EXPORTER_TIME_FMT_UNIX,
+ EVENT_EXPORTER_TIME_FMT_RFC3339,
+};
+/* </settings checks> */
+/* <settings checks> */
+enum stats_metric_group_by_func {
+ STATS_METRIC_GROUPBY_DISCRETE = 0,
+ STATS_METRIC_GROUPBY_QUANTIZED,
+};
+
+/*
+ * A range covering a stats bucket. The the interval is half closed - the
+ * minimum is excluded and the maximum is included. In other words: (min, max].
+ * Because we don't have a +Inf and -Inf, we use INTMAX_MIN and INTMAX_MAX
+ * respectively.
+ */
+struct stats_metric_settings_bucket_range {
+ intmax_t min;
+ intmax_t max;
+};
+
+struct stats_metric_settings_group_by {
+ const char *field;
+ enum stats_metric_group_by_func func;
+ unsigned int num_ranges;
+ struct stats_metric_settings_bucket_range *ranges;
+};
+/* </settings checks> */
+#define STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE \
+ "name hostname timestamps categories fields"
+struct stats_exporter_settings {
+ const char *name;
+ const char *transport;
+ const char *transport_args;
+ unsigned int transport_timeout;
+ const char *format;
+ const char *format_args;
+
+ /* parsed values */
+ enum event_exporter_time_fmt parsed_time_format;
+};
+struct stats_metric_settings {
+ const char *metric_name;
+ const char *description;
+ const char *fields;
+ const char *group_by;
+ const char *filter;
+
+ ARRAY(struct stats_metric_settings_group_by) parsed_group_by;
+ struct event_filter *parsed_filter;
+
+ /* exporter related fields */
+ const char *exporter;
+ const char *exporter_include;
+};
+struct stats_settings {
+ const char *stats_http_rawlog_dir;
+
+ ARRAY(struct stats_exporter_settings *) exporters;
+ ARRAY(struct stats_metric_settings *) metrics;
+};
+/* ../../src/replication/replicator/replicator-settings.h */
+extern const struct setting_parser_info replicator_setting_parser_info;
+struct replicator_settings {
+ const char *auth_socket_path;
+ const char *doveadm_socket_path;
+ const char *replication_dsync_parameters;
+
+ unsigned int replication_full_sync_interval;
+ unsigned int replication_max_conns;
+};
+/* ../../src/replication/aggregator/aggregator-settings.h */
+extern const struct setting_parser_info aggregator_setting_parser_info;
+struct aggregator_settings {
+ const char *replicator_host;
+ in_port_t replicator_port;
+};
+/* ../../src/pop3/pop3-settings.h */
+extern const struct setting_parser_info pop3_setting_parser_info;
+/* <settings checks> */
+enum pop3_client_workarounds {
+ WORKAROUND_OUTLOOK_NO_NULS = 0x01,
+ WORKAROUND_OE_NS_EOH = 0x02
+};
+enum pop3_delete_type {
+ POP3_DELETE_TYPE_EXPUNGE = 0,
+ POP3_DELETE_TYPE_FLAG,
+};
+/* </settings checks> */
+struct pop3_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ /* pop3: */
+ bool pop3_no_flag_updates;
+ bool pop3_enable_last;
+ bool pop3_reuse_xuidl;
+ bool pop3_save_uidl;
+ bool pop3_lock_session;
+ bool pop3_fast_size_lookups;
+ const char *pop3_client_workarounds;
+ const char *pop3_logout_format;
+ const char *pop3_uidl_duplicates;
+ const char *pop3_deleted_flag;
+ const char *pop3_delete_type;
+
+ enum pop3_client_workarounds parsed_workarounds;
+ enum pop3_delete_type parsed_delete_type;
+};
+/* ../../src/pop3-login/pop3-login-settings.h */
+extern const struct setting_parser_info *pop3_login_setting_roots[];
+/* ../../src/plugins/quota/quota-status-settings.h */
+extern const struct setting_parser_info quota_status_setting_parser_info;
+struct quota_status_settings {
+ const char *recipient_delimiter;
+};
+/* ../../src/plugins/mail-crypt/fs-crypt-settings.h */
+extern const struct setting_parser_info fs_crypt_setting_parser_info;
+struct fs_crypt_settings {
+ ARRAY(const char *) plugin_envs;
+};
+/* ../../src/old-stats/stats-settings.h */
+extern const struct setting_parser_info old_stats_setting_parser_info;
+struct old_stats_settings {
+ uoff_t memory_limit;
+
+ unsigned int command_min_time;
+ unsigned int session_min_time;
+ unsigned int user_min_time;
+ unsigned int domain_min_time;
+ unsigned int ip_min_time;
+
+ unsigned int carbon_interval;
+ const char *carbon_server;
+ const char *carbon_name;
+};
+/* ../../src/master/master-settings.h */
+extern const struct setting_parser_info master_setting_parser_info;
+struct master_settings {
+ const char *base_dir;
+ const char *state_dir;
+ const char *libexec_dir;
+ const char *instance_name;
+ const char *protocols;
+ const char *listen;
+ const char *ssl;
+ const char *default_internal_user;
+ const char *default_internal_group;
+ const char *default_login_user;
+ unsigned int default_process_limit;
+ unsigned int default_client_limit;
+ unsigned int default_idle_kill;
+ uoff_t default_vsz_limit;
+
+ bool version_ignore;
+
+ unsigned int first_valid_uid, last_valid_uid;
+ unsigned int first_valid_gid, last_valid_gid;
+
+ ARRAY_TYPE(service_settings) services;
+ char **protocols_split;
+};
+/* ../../src/login-common/login-settings.h */
+extern const struct setting_parser_info **login_set_roots;
+extern const struct setting_parser_info login_setting_parser_info;
+struct login_settings {
+ const char *login_trusted_networks;
+ const char *login_source_ips;
+ const char *login_greeting;
+ const char *login_log_format_elements, *login_log_format;
+ const char *login_access_sockets;
+ const char *login_proxy_notify_path;
+ const char *login_plugin_dir;
+ const char *login_plugins;
+ unsigned int login_proxy_timeout;
+ unsigned int login_proxy_max_reconnects;
+ unsigned int login_proxy_max_disconnect_delay;
+ const char *login_proxy_rawlog_dir;
+ const char *director_username_hash;
+
+ bool auth_ssl_require_client_cert;
+ bool auth_ssl_username_from_cert;
+
+ bool disable_plaintext_auth;
+ bool auth_verbose;
+ bool auth_debug;
+ bool auth_debug_passwords;
+ bool verbose_proctitle;
+
+ unsigned int mail_max_userip_connections;
+
+ /* generated: */
+ char *const *log_format_elements_split;
+};
+/* ../../src/lmtp/lmtp-settings.h */
+extern const struct setting_parser_info lmtp_setting_parser_info;
+/* <settings checks> */
+enum lmtp_hdr_delivery_address {
+ LMTP_HDR_DELIVERY_ADDRESS_NONE,
+ LMTP_HDR_DELIVERY_ADDRESS_FINAL,
+ LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL
+};
+
+enum lmtp_client_workarounds {
+ LMTP_WORKAROUND_WHITESPACE_BEFORE_PATH = BIT(0),
+ LMTP_WORKAROUND_MAILBOX_FOR_PATH = BIT(1),
+};
+/* </settings checks> */
+struct lmtp_settings {
+ bool lmtp_proxy;
+ bool lmtp_save_to_detail_mailbox;
+ bool lmtp_rcpt_check_quota;
+ bool lmtp_add_received_header;
+ bool lmtp_verbose_replies;
+ unsigned int lmtp_user_concurrency_limit;
+ const char *lmtp_hdr_delivery_address;
+ const char *lmtp_rawlog_dir;
+ const char *lmtp_proxy_rawlog_dir;
+
+ const char *lmtp_client_workarounds;
+
+ const char *login_greeting;
+ const char *login_trusted_networks;
+
+ const char *mail_plugins;
+ const char *mail_plugin_dir;
+
+ enum lmtp_hdr_delivery_address parsed_lmtp_hdr_delivery_address;
+
+ enum lmtp_client_workarounds parsed_workarounds;
+};
+/* ../../src/imap/imap-settings.h */
+extern const struct setting_parser_info imap_setting_parser_info;
+/* <settings checks> */
+enum imap_client_workarounds {
+ WORKAROUND_DELAY_NEWMAIL = 0x01,
+ WORKAROUND_TB_EXTRA_MAILBOX_SEP = 0x08,
+ WORKAROUND_TB_LSUB_FLAGS = 0x10
+};
+
+enum imap_client_fetch_failure {
+ IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY,
+ IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER,
+ IMAP_CLIENT_FETCH_FAILURE_NO_AFTER,
+};
+/* </settings checks> */
+struct imap_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ /* imap: */
+ uoff_t imap_max_line_length;
+ unsigned int imap_idle_notify_interval;
+ const char *imap_capability;
+ const char *imap_client_workarounds;
+ const char *imap_logout_format;
+ const char *imap_id_send;
+ const char *imap_id_log;
+ const char *imap_fetch_failure;
+ bool imap_metadata;
+ bool imap_literal_minus;
+ unsigned int imap_hibernate_timeout;
+
+ /* imap urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+
+ enum imap_client_workarounds parsed_workarounds;
+ enum imap_client_fetch_failure parsed_fetch_failure;
+};
+/* ../../src/imap-urlauth/imap-urlauth-worker-settings.h */
+extern const struct setting_parser_info imap_urlauth_worker_setting_parser_info;
+struct imap_urlauth_worker_settings {
+ bool verbose_proctitle;
+
+ /* imap_urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+};
+/* ../../src/imap-urlauth/imap-urlauth-settings.h */
+extern const struct setting_parser_info imap_urlauth_setting_parser_info;
+struct imap_urlauth_settings {
+ const char *base_dir;
+
+ bool mail_debug;
+
+ bool verbose_proctitle;
+
+ /* imap_urlauth: */
+ const char *imap_urlauth_logout_format;
+
+ const char *imap_urlauth_submit_user;
+ const char *imap_urlauth_stream_user;
+};
+/* ../../src/imap-urlauth/imap-urlauth-login-settings.h */
+extern const struct setting_parser_info *imap_urlauth_login_setting_roots[];
+/* ../../src/imap-login/imap-login-settings.h */
+extern const struct setting_parser_info *imap_login_setting_roots[];
+struct imap_login_settings {
+ const char *imap_capability;
+ const char *imap_id_send;
+ const char *imap_id_log;
+ bool imap_literal_minus;
+ bool imap_id_retain;
+};
+/* ../../src/doveadm/doveadm-settings.h */
+extern const struct setting_parser_info doveadm_setting_parser_info;
+/* <settings checks> */
+enum dsync_features {
+ DSYNC_FEATURE_EMPTY_HDR_WORKAROUND = 0x1,
+ DSYNC_FEATURE_NO_HEADER_HASHES = 0x2,
+};
+/* </settings checks> */
+struct doveadm_settings {
+ const char *base_dir;
+ const char *libexec_dir;
+ const char *mail_plugins;
+ const char *mail_plugin_dir;
+ const char *mail_temp_dir;
+ bool auth_debug;
+ const char *auth_socket_path;
+ const char *doveadm_socket_path;
+ unsigned int doveadm_worker_count;
+ in_port_t doveadm_port;
+ const char *doveadm_ssl;
+ const char *doveadm_username;
+ const char *doveadm_password;
+ const char *doveadm_allowed_commands;
+ const char *dsync_alt_char;
+ const char *dsync_remote_cmd;
+ const char *director_username_hash;
+ const char *doveadm_api_key;
+ const char *dsync_features;
+ const char *dsync_hashed_headers;
+ unsigned int dsync_commit_msgs_interval;
+ const char *doveadm_http_rawlog_dir;
+ enum dsync_features parsed_features;
+ ARRAY(const char *) plugin_envs;
+};
+/* ../../src/director/director-settings.h */
+extern const struct setting_parser_info director_setting_parser_info;
+struct director_settings {
+ const char *auth_master_user_separator;
+
+ const char *director_servers;
+ const char *director_mail_servers;
+ const char *director_username_hash;
+ const char *director_flush_socket;
+
+ unsigned int director_ping_idle_timeout;
+ unsigned int director_ping_max_timeout;
+ unsigned int director_user_expire;
+ unsigned int director_user_kick_delay;
+ unsigned int director_max_parallel_moves;
+ unsigned int director_max_parallel_kicks;
+ uoff_t director_output_buffer_size;
+};
+/* ../../src/dict/dict-settings.h */
+extern const struct setting_parser_info dict_setting_parser_info;
+struct dict_server_settings {
+ const char *base_dir;
+ bool verbose_proctitle;
+
+ const char *dict_db_config;
+ ARRAY(const char *) dicts;
+};
+/* ../../src/auth/auth-settings.h */
+extern const struct setting_parser_info auth_setting_parser_info;
+struct auth_passdb_settings {
+ const char *name;
+ const char *driver;
+ const char *args;
+ const char *default_fields;
+ const char *override_fields;
+ const char *mechanisms;
+ const char *username_filter;
+
+ const char *skip;
+ const char *result_success;
+ const char *result_failure;
+ const char *result_internalfail;
+ bool deny;
+ bool pass; /* deprecated, use result_success=continue instead */
+ bool master;
+ const char *auth_verbose;
+};
+struct auth_userdb_settings {
+ const char *name;
+ const char *driver;
+ const char *args;
+ const char *default_fields;
+ const char *override_fields;
+
+ const char *skip;
+ const char *result_success;
+ const char *result_failure;
+ const char *result_internalfail;
+ const char *auth_verbose;
+};
+struct auth_settings {
+ const char *mechanisms;
+ const char *realms;
+ const char *default_realm;
+ uoff_t cache_size;
+ unsigned int cache_ttl;
+ unsigned int cache_negative_ttl;
+ bool cache_verify_password_with_worker;
+ const char *username_chars;
+ const char *username_translation;
+ const char *username_format;
+ const char *master_user_separator;
+ const char *anonymous_username;
+ const char *krb5_keytab;
+ const char *gssapi_hostname;
+ const char *winbind_helper_path;
+ const char *proxy_self;
+ unsigned int failure_delay;
+
+ const char *policy_server_url;
+ const char *policy_server_api_header;
+ unsigned int policy_server_timeout_msecs;
+ const char *policy_hash_mech;
+ const char *policy_hash_nonce;
+ const char *policy_request_attributes;
+ bool policy_reject_on_fail;
+ bool policy_check_before_auth;
+ bool policy_check_after_auth;
+ bool policy_report_after_auth;
+ bool policy_log_only;
+ unsigned int policy_hash_truncate;
+
+ bool stats;
+ bool verbose, debug, debug_passwords;
+ const char *verbose_passwords;
+ bool ssl_require_client_cert;
+ bool ssl_username_from_cert;
+ bool use_winbind;
+
+ unsigned int worker_max_count;
+
+ /* settings that don't have auth_ prefix: */
+ ARRAY(struct auth_passdb_settings *) passdbs;
+ ARRAY(struct auth_userdb_settings *) userdbs;
+
+ const char *base_dir;
+ const char *ssl_client_ca_dir;
+ const char *ssl_client_ca_file;
+
+ bool verbose_proctitle;
+ unsigned int first_valid_uid;
+ unsigned int last_valid_uid;
+ unsigned int first_valid_gid;
+ unsigned int last_valid_gid;
+
+ /* generated: */
+ char username_chars_map[256];
+ char username_translation_map[256];
+ const char *const *realms_arr;
+ const struct ip_addr *proxy_self_ips;
+};
+/* ../../src/util/tcpwrap-settings.c */
+#ifdef HAVE_LIBWRAP
+struct service_settings tcpwrap_service_settings = {
+ .name = "tcpwrap",
+ .protocol = "",
+ .type = "",
+ .executable = "tcpwrap",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#endif
+/* ../../src/util/health-check-settings.c */
+struct service_settings health_check_service_settings = {
+ .name = "health-check",
+ .protocol = "",
+ .type = "",
+ .executable = "script -p health-check.sh",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = TRUE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+/* ../../src/submission/submission-settings.c */
+/* <settings checks> */
+static struct file_listener_settings submission_unix_listeners_array[] = {
+ { "login/submission", 0666, "", "" }
+};
+static struct file_listener_settings *submission_unix_listeners[] = {
+ &submission_unix_listeners_array[0]
+};
+static buffer_t submission_unix_listeners_buf = {
+ { { submission_unix_listeners, sizeof(submission_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+struct submission_client_workaround_list {
+ const char *name;
+ enum submission_client_workarounds num;
+};
+
+/* These definitions need to be kept in sync with equivalent definitions present
+ in src/submission-login/submission-login-settings.c. Workarounds that are not
+ relevant to the submission service are defined as 0 here to prevent "Unknown
+ workaround" errors below. */
+static const struct submission_client_workaround_list
+submission_client_workaround_list[] = {
+ { "whitespace-before-path",
+ SUBMISSION_WORKAROUND_WHITESPACE_BEFORE_PATH },
+ { "mailbox-for-path",
+ SUBMISSION_WORKAROUND_MAILBOX_FOR_PATH },
+ { "implicit-auth-external", 0 },
+ { "exotic-backend", 0 },
+ { NULL, 0 }
+};
+
+static int
+submission_settings_parse_workarounds(struct submission_settings *set,
+ const char **error_r)
+{
+ enum submission_client_workarounds client_workarounds = 0;
+ const struct submission_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->submission_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = submission_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "submission_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+submission_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct submission_settings *set = _set;
+
+ if (submission_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+#ifndef CONFIG_BINARY
+ if (set->submission_relay_max_idle_time == 0) {
+ *error_r = "submission_relay_max_idle_time must not be 0";
+ return FALSE;
+ }
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+#endif
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings submission_service_settings = {
+ .name = "submission",
+ .protocol = "submission",
+ .type = "",
+ .executable = "submission",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &submission_unix_listeners_buf,
+ sizeof(submission_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct submission_settings)
+static const struct setting_define submission_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(STR, hostname),
+
+ DEF(STR, login_greeting),
+ DEF(STR, login_trusted_networks),
+
+ DEF(STR, recipient_delimiter),
+
+ DEF(SIZE, submission_max_mail_size),
+ DEF(UINT, submission_max_recipients),
+ DEF(STR, submission_client_workarounds),
+ DEF(STR, submission_logout_format),
+
+ DEF(STR, submission_backend_capabilities),
+
+ DEF(STR, submission_relay_host),
+ DEF(IN_PORT, submission_relay_port),
+ DEF(BOOL, submission_relay_trusted),
+
+ DEF(STR, submission_relay_user),
+ DEF(STR, submission_relay_master_user),
+ DEF(STR, submission_relay_password),
+
+ DEF(ENUM, submission_relay_ssl),
+ DEF(BOOL, submission_relay_ssl_verify),
+
+ DEF(STR_VARS, submission_relay_rawlog_dir),
+ DEF(TIME, submission_relay_max_idle_time),
+
+ DEF(TIME_MSECS, submission_relay_connect_timeout),
+ DEF(TIME_MSECS, submission_relay_command_timeout),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct submission_settings submission_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ .hostname = "",
+
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_trusted_networks = "",
+
+ .recipient_delimiter = "+",
+
+ .submission_max_mail_size = 40*1024*1024,
+ .submission_max_recipients = 0,
+ .submission_client_workarounds = "",
+ .submission_logout_format = "in=%i out=%o",
+
+ .submission_backend_capabilities = NULL,
+
+ .submission_relay_host = "",
+ .submission_relay_port = 25,
+ .submission_relay_trusted = FALSE,
+
+ .submission_relay_user = "",
+ .submission_relay_master_user = "",
+ .submission_relay_password = "",
+
+ .submission_relay_ssl = "no:smtps:starttls",
+ .submission_relay_ssl_verify = TRUE,
+
+ .submission_relay_rawlog_dir = "",
+ .submission_relay_max_idle_time = 60*29,
+
+ .submission_relay_connect_timeout = 30*1000,
+ .submission_relay_command_timeout = 60*5*1000,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143,
+};
+static const struct setting_parser_info *submission_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info submission_setting_parser_info = {
+ .module_name = "submission",
+ .defines = submission_setting_defines,
+ .defaults = &submission_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct submission_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = submission_settings_verify,
+ .dependencies = submission_setting_dependencies
+};
+/* ../../src/submission-login/submission-login-settings.c */
+/* <settings checks> */
+static struct inet_listener_settings submission_login_inet_listeners_array[] = {
+ { .name = "submission", .address = "", .port = 587 },
+ { .name = "submissions", .address = "", .port = 465, .ssl = TRUE }
+};
+static struct inet_listener_settings *submission_login_inet_listeners[] = {
+ &submission_login_inet_listeners_array[0]
+};
+static buffer_t submission_login_inet_listeners_buf = {
+ { { submission_login_inet_listeners,
+ sizeof(submission_login_inet_listeners) } }
+};
+
+/* </settings checks> */
+/* <settings checks> */
+struct submission_login_client_workaround_list {
+ const char *name;
+ enum submission_login_client_workarounds num;
+};
+
+/* These definitions need to be kept in sync with equivalent definitions present
+ in src/submission/submission-settings.c. Workarounds that are not relevant
+ to the submission-login service are defined as 0 here to prevent "Unknown
+ workaround" errors below. */
+static const struct submission_login_client_workaround_list
+submission_login_client_workaround_list[] = {
+ { "whitespace-before-path", 0},
+ { "mailbox-for-path", 0 },
+ { "implicit-auth-external",
+ SUBMISSION_LOGIN_WORKAROUND_IMPLICIT_AUTH_EXTERNAL },
+ { "exotic-backend",
+ SUBMISSION_LOGIN_WORKAROUND_EXOTIC_BACKEND },
+ { NULL, 0 }
+};
+
+static int
+submission_login_settings_parse_workarounds(
+ struct submission_login_settings *set, const char **error_r)
+{
+ enum submission_login_client_workarounds client_workarounds = 0;
+ const struct submission_login_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->submission_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = submission_login_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "submission_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+submission_login_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct submission_login_settings *set = _set;
+
+ if (submission_login_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+#ifndef CONFIG_BINARY
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+#endif
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings submission_login_service_settings = {
+ .name = "submission-login",
+ .protocol = "submission",
+ .type = "login",
+ .executable = "submission-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &submission_login_inet_listeners_buf,
+ sizeof(submission_login_inet_listeners[0]) } }
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct submission_login_settings)
+static const struct setting_define submission_login_setting_defines[] = {
+ DEF(STR, hostname),
+
+ DEF(SIZE, submission_max_mail_size),
+ DEF(STR, submission_client_workarounds),
+ DEF(STR, submission_backend_capabilities),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct submission_login_settings submission_login_default_settings = {
+ .hostname = "",
+
+ .submission_max_mail_size = 0,
+ .submission_client_workarounds = "",
+ .submission_backend_capabilities = NULL
+};
+static const struct setting_parser_info *submission_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info submission_login_setting_parser_info = {
+ .module_name = "submission-login",
+ .defines = submission_login_setting_defines,
+ .defaults = &submission_login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct submission_login_settings),
+ .parent_offset = SIZE_MAX,
+
+ .check_func = submission_login_settings_check,
+ .dependencies = submission_login_setting_dependencies
+};
+const struct setting_parser_info *submission_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &submission_login_setting_parser_info,
+ NULL
+};
+/* ../../src/stats/stats-settings.c */
+extern const struct setting_parser_info stats_metric_setting_parser_info;
+extern const struct setting_parser_info stats_exporter_setting_parser_info;
+/* <settings checks> */
+#include "event-filter.h"
+#include <math.h>
+/* </settings checks> */
+/* <settings checks> */
+static struct file_listener_settings stats_unix_listeners_array[] = {
+ { "stats-reader", 0600, "", "" },
+ { "stats-writer", 0660, "", "$default_internal_group" },
+ { "login/stats-writer", 0600, "$default_login_user", "" },
+};
+static struct file_listener_settings *stats_unix_listeners[] = {
+ &stats_unix_listeners_array[0],
+ &stats_unix_listeners_array[1],
+ &stats_unix_listeners_array[2],
+};
+static buffer_t stats_unix_listeners_buf = {
+ { { stats_unix_listeners, sizeof(stats_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+static bool parse_format_args_set_time(struct stats_exporter_settings *set,
+ enum event_exporter_time_fmt fmt,
+ const char **error_r)
+{
+ if ((set->parsed_time_format != EVENT_EXPORTER_TIME_FMT_NATIVE) &&
+ (set->parsed_time_format != fmt)) {
+ *error_r = t_strdup_printf("Exporter '%s' specifies multiple "
+ "time format args", set->name);
+ return FALSE;
+ }
+
+ set->parsed_time_format = fmt;
+
+ return TRUE;
+}
+
+static bool parse_format_args(struct stats_exporter_settings *set,
+ const char **error_r)
+{
+ const char *const *tmp;
+
+ /* Defaults */
+ set->parsed_time_format = EVENT_EXPORTER_TIME_FMT_NATIVE;
+
+ tmp = t_strsplit_spaces(set->format_args, " ");
+
+ /*
+ * If the config contains multiple types of the same type (e.g.,
+ * both time-rfc3339 and time-unix) we fail the config check.
+ *
+ * Note: At the moment, we have only time-* tokens. In the future
+ * when we have other tokens, they should be parsed here.
+ */
+ for (; *tmp != NULL; tmp++) {
+ enum event_exporter_time_fmt fmt;
+
+ if (strcmp(*tmp, "time-rfc3339") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_RFC3339;
+ } else if (strcmp(*tmp, "time-unix") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_UNIX;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format "
+ "arg: %s", *tmp);
+ return FALSE;
+ }
+
+ if (!parse_format_args_set_time(set, fmt, error_r))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool stats_exporter_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_exporter_settings *set = _set;
+ bool time_fmt_required;
+
+ if (set->name[0] == '\0') {
+ *error_r = "Exporter name can't be empty";
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->format[0] == '\0') {
+ *error_r = "Exporter format name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->format, "none") == 0) {
+ time_fmt_required = FALSE;
+ } else if (strcmp(set->format, "json") == 0) {
+ time_fmt_required = TRUE;
+ } else if (strcmp(set->format, "tab-text") == 0) {
+ time_fmt_required = TRUE;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format '%s'",
+ set->format);
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->transport[0] == '\0') {
+ *error_r = "Exporter transport name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->transport, "drop") == 0 ||
+ strcmp(set->transport, "http-post") == 0 ||
+ strcmp(set->transport, "log") == 0) {
+ /* no-op */
+ } else {
+ *error_r = t_strdup_printf("Unknown transport type '%s'",
+ set->transport);
+ return FALSE;
+ }
+
+ if (!parse_format_args(set, error_r))
+ return FALSE;
+
+ /* Some formats don't have a native way of serializing time stamps */
+ if (time_fmt_required &&
+ set->parsed_time_format == EVENT_EXPORTER_TIME_FMT_NATIVE) {
+ *error_r = t_strdup_printf("%s exporter format requires a "
+ "time-* argument", set->format);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_common(const char *func,
+ const char *const *params,
+ intmax_t *min_r,
+ intmax_t *max_r,
+ intmax_t *other_r,
+ const char **error_r)
+{
+ intmax_t min, max, other;
+
+ if ((str_array_length(params) != 3) ||
+ (str_to_intmax(params[0], &min) < 0) ||
+ (str_to_intmax(params[1], &max) < 0) ||
+ (str_to_intmax(params[2], &other) < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function takes "
+ "3 int args", func);
+ return FALSE;
+ }
+
+ if ((min < 0) || (max < 0) || (other < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "arguments must be >= 0", func);
+ return FALSE;
+ }
+
+ if (min >= max) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "min must be < max (%ju must be < %ju)",
+ func, min, max);
+ return FALSE;
+ }
+
+ *min_r = min;
+ *max_r = max;
+ *other_r = other;
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_exp(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, base;
+
+ if (!parse_metric_group_by_common("exponential", params, &min, &max, &base, error_r))
+ return FALSE;
+
+ if ((base != 2) && (base != 10)) {
+ *error_r = t_strdup_printf("group_by 'exponential' aggregate function "
+ "base must be one of: 2, 10 (base=%ju)",
+ base);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'base^min'. The last bucket is also special - it
+ * contains everything greater than 'base^max'.
+ *
+ * The second bucket begins at 'base^min + 1', the third bucket
+ * begins at 'base^(min + 1) + 1', and so on.
+ */
+ group_by->num_ranges = max - min + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = pow(base, min);
+ group_by->ranges[group_by->num_ranges - 1].min = pow(base, max);
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = pow(base, min + (i - 1));
+ group_by->ranges[i].max = pow(base, min + i);
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_lin(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, step;
+
+ if (!parse_metric_group_by_common("linear", params, &min, &max, &step, error_r))
+ return FALSE;
+
+ if ((min + step) > max) {
+ *error_r = t_strdup_printf("group_by 'linear' aggregate function "
+ "min+step must be <= max (%ju must be <= %ju)",
+ min + step, max);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'min'. The last bucket is also special - it contains
+ * everything greater than 'max'.
+ *
+ * The second bucket begins at 'min + 1', the third bucket begins at
+ * 'min + 1 * step + 1', the fourth at 'min + 2 * step + 1', and so on.
+ */
+ group_by->num_ranges = (max - min) / step + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = min;
+ group_by->ranges[group_by->num_ranges - 1].min = max;
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = min + (i - 1) * step;
+ group_by->ranges[i].max = min + i * step;
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by(struct stats_metric_settings *set,
+ pool_t pool, const char **error_r)
+{
+ const char *const *tmp = t_strsplit_spaces(set->group_by, " ");
+
+ if (tmp[0] == NULL)
+ return TRUE;
+
+ p_array_init(&set->parsed_group_by, pool, str_array_length(tmp));
+
+ /* For each group_by field */
+ for (; *tmp != NULL; tmp++) {
+ struct stats_metric_settings_group_by group_by;
+ const char *const *params;
+
+ i_zero(&group_by);
+
+ /* <field name>:<aggregation func>... */
+ params = t_strsplit(*tmp, ":");
+
+ if (params[1] == NULL) {
+ /* <field name> - alias for <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ } else if (strcmp(params[1], "discrete") == 0) {
+ /* <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ if (params[2] != NULL) {
+ *error_r = "group_by 'discrete' aggregate function "
+ "does not take any args";
+ return FALSE;
+ }
+ } else if (strcmp(params[1], "exponential") == 0) {
+ /* <field>:exponential:<min mag>:<max mag>:<base> */
+ if (!parse_metric_group_by_exp(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else if (strcmp(params[1], "linear") == 0) {
+ /* <field>:linear:<min val>:<max val>:<step> */
+ if (!parse_metric_group_by_lin(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else {
+ *error_r = t_strdup_printf("unknown aggregation function "
+ "'%s' on field '%s'", params[1], params[0]);
+ return FALSE;
+ }
+
+ group_by.field = p_strdup(pool, params[0]);
+
+ array_push_back(&set->parsed_group_by, &group_by);
+ }
+
+ return TRUE;
+}
+
+static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r)
+{
+ struct stats_metric_settings *set = _set;
+
+ if (set->metric_name[0] == '\0') {
+ *error_r = "Metric name can't be empty";
+ return FALSE;
+ }
+
+ if (set->filter[0] == '\0') {
+ *error_r = t_strdup_printf("metric %s { filter } is empty - "
+ "will not match anything", set->metric_name);
+ return FALSE;
+ }
+
+ set->parsed_filter = event_filter_create_fragment(pool);
+ if (event_filter_parse(set->filter, set->parsed_filter, error_r) < 0)
+ return FALSE;
+
+ if (!parse_metric_group_by(set, pool, error_r))
+ return FALSE;
+
+ return TRUE;
+}
+
+static bool stats_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_settings *set = _set;
+ struct stats_exporter_settings *exporter;
+ struct stats_metric_settings *metric;
+
+ if (!array_is_created(&set->metrics) || !array_is_created(&set->exporters))
+ return TRUE;
+
+ /* check that all metrics refer to exporters that exist */
+ array_foreach_elem(&set->metrics, metric) {
+ bool found = FALSE;
+
+ if (metric->exporter[0] == '\0')
+ continue; /* metric not exported */
+
+ array_foreach_elem(&set->exporters, exporter) {
+ if (strcmp(metric->exporter, exporter->name) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ *error_r = t_strdup_printf("metric %s refers to "
+ "non-existent exporter '%s'",
+ metric->metric_name,
+ metric->exporter);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings stats_service_settings = {
+ .name = "stats",
+ .protocol = "",
+ .type = "",
+ .executable = "stats",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &stats_unix_listeners_buf,
+ sizeof(stats_unix_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_exporter_settings)
+static const struct setting_define stats_exporter_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, transport),
+ DEF(STR, transport_args),
+ DEF(TIME_MSECS, transport_timeout),
+ DEF(STR, format),
+ DEF(STR, format_args),
+ SETTING_DEFINE_LIST_END
+};
+static const struct stats_exporter_settings stats_exporter_default_settings = {
+ .name = "",
+ .transport = "",
+ .transport_args = "",
+ .transport_timeout = 250, /* ms */
+ .format = "",
+ .format_args = "",
+};
+const struct setting_parser_info stats_exporter_setting_parser_info = {
+ .defines = stats_exporter_setting_defines,
+ .defaults = &stats_exporter_default_settings,
+
+ .type_offset = offsetof(struct stats_exporter_settings, name),
+ .struct_size = sizeof(struct stats_exporter_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_exporter_settings_check,
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_metric_settings)
+static const struct setting_define stats_metric_setting_defines[] = {
+ DEF(STR, metric_name),
+ DEF(STR, fields),
+ DEF(STR, group_by),
+ DEF(STR, filter),
+ DEF(STR, exporter),
+ DEF(STR, exporter_include),
+ DEF(STR, description),
+ SETTING_DEFINE_LIST_END
+};
+static const struct stats_metric_settings stats_metric_default_settings = {
+ .metric_name = "",
+ .fields = "",
+ .filter = "",
+ .exporter = "",
+ .group_by = "",
+ .exporter_include = STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE,
+ .description = "",
+};
+const struct setting_parser_info stats_metric_setting_parser_info = {
+ .defines = stats_metric_setting_defines,
+ .defaults = &stats_metric_default_settings,
+
+ .type_offset = offsetof(struct stats_metric_settings, metric_name),
+ .struct_size = sizeof(struct stats_metric_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_metric_settings_check,
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_settings)
+#undef DEFLIST_UNIQUE
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct stats_settings, field), \
+ .list_info = defines }
+static const struct setting_define stats_setting_defines[] = {
+ DEF(STR, stats_http_rawlog_dir),
+
+ DEFLIST_UNIQUE(metrics, "metric", &stats_metric_setting_parser_info),
+ DEFLIST_UNIQUE(exporters, "event_exporter", &stats_exporter_setting_parser_info),
+ SETTING_DEFINE_LIST_END
+};
+const struct stats_settings stats_default_settings = {
+ .stats_http_rawlog_dir = "",
+
+ .metrics = ARRAY_INIT,
+ .exporters = ARRAY_INIT,
+};
+const struct setting_parser_info stats_setting_parser_info = {
+ .module_name = "stats",
+ .defines = stats_setting_defines,
+ .defaults = &stats_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct stats_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_settings_check,
+};
+/* ../../src/replication/replicator/replicator-settings.c */
+/* <settings checks> */
+static struct file_listener_settings replicator_unix_listeners_array[] = {
+ { "replicator", 0600, "$default_internal_user", "" },
+ { "replicator-doveadm", 0, "$default_internal_user", "" }
+};
+static struct file_listener_settings *replicator_unix_listeners[] = {
+ &replicator_unix_listeners_array[0],
+ &replicator_unix_listeners_array[1]
+};
+static buffer_t replicator_unix_listeners_buf = {
+ { { replicator_unix_listeners, sizeof(replicator_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings replicator_service_settings = {
+ .name = "replicator",
+ .protocol = "",
+ .type = "",
+ .executable = "replicator",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &replicator_unix_listeners_buf,
+ sizeof(replicator_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct replicator_settings)
+static const struct setting_define replicator_setting_defines[] = {
+ DEF(STR, auth_socket_path),
+ DEF(STR, doveadm_socket_path),
+ DEF(STR, replication_dsync_parameters),
+
+ DEF(TIME, replication_full_sync_interval),
+ DEF(UINT, replication_max_conns),
+
+ SETTING_DEFINE_LIST_END
+};
+const struct replicator_settings replicator_default_settings = {
+ .auth_socket_path = "auth-userdb",
+ .doveadm_socket_path = "doveadm-server",
+ .replication_dsync_parameters = "-d -N -l 30 -U",
+
+ .replication_full_sync_interval = 60*60*24,
+ .replication_max_conns = 10
+};
+const struct setting_parser_info replicator_setting_parser_info = {
+ .module_name = "replicator",
+ .defines = replicator_setting_defines,
+ .defaults = &replicator_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct replicator_settings),
+
+ .parent_offset = SIZE_MAX
+};
+/* ../../src/replication/aggregator/aggregator-settings.c */
+/* <settings checks> */
+static struct file_listener_settings aggregator_unix_listeners_array[] = {
+ { "replication-notify", 0600, "", "" }
+};
+static struct file_listener_settings *aggregator_unix_listeners[] = {
+ &aggregator_unix_listeners_array[0]
+};
+static buffer_t aggregator_unix_listeners_buf = {
+ { { aggregator_unix_listeners, sizeof(aggregator_unix_listeners) } }
+};
+
+static struct file_listener_settings aggregator_fifo_listeners_array[] = {
+ { "replication-notify-fifo", 0600, "", "" }
+};
+static struct file_listener_settings *aggregator_fifo_listeners[] = {
+ &aggregator_fifo_listeners_array[0]
+};
+static buffer_t aggregator_fifo_listeners_buf = {
+ { { aggregator_fifo_listeners, sizeof(aggregator_fifo_listeners) } }
+};
+/* </settings checks> */
+struct service_settings aggregator_service_settings = {
+ .name = "aggregator",
+ .protocol = "",
+ .type = "",
+ .executable = "aggregator",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = ".",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &aggregator_unix_listeners_buf,
+ sizeof(aggregator_unix_listeners[0]) } },
+ .fifo_listeners = { { &aggregator_fifo_listeners_buf,
+ sizeof(aggregator_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct aggregator_settings)
+static const struct setting_define aggregator_setting_defines[] = {
+ DEF(STR, replicator_host),
+ DEF(IN_PORT, replicator_port),
+
+ SETTING_DEFINE_LIST_END
+};
+const struct aggregator_settings aggregator_default_settings = {
+ .replicator_host = "replicator",
+ .replicator_port = 0
+};
+const struct setting_parser_info aggregator_setting_parser_info = {
+ .module_name = "aggregator",
+ .defines = aggregator_setting_defines,
+ .defaults = &aggregator_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct aggregator_settings),
+
+ .parent_offset = SIZE_MAX
+};
+/* ../../src/pop3/pop3-settings.c */
+/* <settings checks> */
+static struct file_listener_settings pop3_unix_listeners_array[] = {
+ { "login/pop3", 0666, "", "" }
+};
+static struct file_listener_settings *pop3_unix_listeners[] = {
+ &pop3_unix_listeners_array[0]
+};
+static buffer_t pop3_unix_listeners_buf = {
+ { { pop3_unix_listeners, sizeof(pop3_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+struct pop3_client_workaround_list {
+ const char *name;
+ enum pop3_client_workarounds num;
+};
+
+static const struct pop3_client_workaround_list pop3_client_workaround_list[] = {
+ { "outlook-no-nuls", WORKAROUND_OUTLOOK_NO_NULS },
+ { "oe-ns-eoh", WORKAROUND_OE_NS_EOH },
+ { NULL, 0 }
+};
+
+static int
+pop3_settings_parse_workarounds(struct pop3_settings *set,
+ const char **error_r)
+{
+ enum pop3_client_workarounds client_workarounds = 0;
+ const struct pop3_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->pop3_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = pop3_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("pop3_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+pop3_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct pop3_settings *set = _set;
+
+ if (pop3_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+ if (strcmp(set->pop3_delete_type, "default") == 0) {
+ if (set->pop3_deleted_flag[0] == '\0')
+ set->parsed_delete_type = POP3_DELETE_TYPE_EXPUNGE;
+ else
+ set->parsed_delete_type = POP3_DELETE_TYPE_FLAG;
+ } else if (strcmp(set->pop3_delete_type, "expunge") == 0) {
+ set->parsed_delete_type = POP3_DELETE_TYPE_EXPUNGE;
+ } else if (strcmp(set->pop3_delete_type, "flag") == 0) {
+ if (set->pop3_deleted_flag[0] == '\0') {
+ *error_r = "pop3_delete_type=flag, but pop3_deleted_flag not set";
+ return FALSE;
+ }
+ set->parsed_delete_type = POP3_DELETE_TYPE_FLAG;
+ } else {
+ *error_r = t_strdup_printf("pop3_delete_type: Unknown value '%s'",
+ set->pop3_delete_type);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings pop3_service_settings = {
+ .name = "pop3",
+ .protocol = "pop3",
+ .type = "",
+ .executable = "pop3",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &pop3_unix_listeners_buf,
+ sizeof(pop3_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct pop3_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct pop3_settings, field), \
+ .list_info = defines }
+static const struct setting_define pop3_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(BOOL, pop3_no_flag_updates),
+ DEF(BOOL, pop3_enable_last),
+ DEF(BOOL, pop3_reuse_xuidl),
+ DEF(BOOL, pop3_save_uidl),
+ DEF(BOOL, pop3_lock_session),
+ DEF(BOOL, pop3_fast_size_lookups),
+ DEF(STR, pop3_client_workarounds),
+ DEF(STR, pop3_logout_format),
+ DEF(ENUM, pop3_uidl_duplicates),
+ DEF(STR, pop3_deleted_flag),
+ DEF(ENUM, pop3_delete_type),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct pop3_settings pop3_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ .pop3_no_flag_updates = FALSE,
+ .pop3_enable_last = FALSE,
+ .pop3_reuse_xuidl = FALSE,
+ .pop3_save_uidl = FALSE,
+ .pop3_lock_session = FALSE,
+ .pop3_fast_size_lookups = FALSE,
+ .pop3_client_workarounds = "",
+ .pop3_logout_format = "top=%t/%p, retr=%r/%b, del=%d/%m, size=%s",
+ .pop3_uidl_duplicates = "allow:rename",
+ .pop3_deleted_flag = "",
+ .pop3_delete_type = "default:expunge:flag"
+};
+static const struct setting_parser_info *pop3_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info pop3_setting_parser_info = {
+ .module_name = "pop3",
+ .defines = pop3_setting_defines,
+ .defaults = &pop3_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct pop3_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = pop3_settings_verify,
+ .dependencies = pop3_setting_dependencies
+};
+/* ../../src/pop3-login/pop3-login-settings.c */
+/* <settings checks> */
+static struct inet_listener_settings pop3_login_inet_listeners_array[] = {
+ { .name = "pop3", .address = "", .port = 110 },
+ { .name = "pop3s", .address = "", .port = 995, .ssl = TRUE }
+};
+static struct inet_listener_settings *pop3_login_inet_listeners[] = {
+ &pop3_login_inet_listeners_array[0],
+ &pop3_login_inet_listeners_array[1]
+};
+static buffer_t pop3_login_inet_listeners_buf = {
+ { { pop3_login_inet_listeners, sizeof(pop3_login_inet_listeners) } }
+};
+
+/* </settings checks> */
+struct service_settings pop3_login_service_settings = {
+ .name = "pop3-login",
+ .protocol = "pop3",
+ .type = "login",
+ .executable = "pop3-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &pop3_login_inet_listeners_buf,
+ sizeof(pop3_login_inet_listeners[0]) } }
+};
+static const struct setting_define pop3_login_setting_defines[] = {
+ SETTING_DEFINE_LIST_END
+};
+static const struct setting_parser_info *pop3_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info pop3_login_setting_parser_info = {
+ .module_name = "pop3-login",
+ .defines = pop3_login_setting_defines,
+
+ .type_offset = SIZE_MAX,
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = pop3_login_setting_dependencies
+};
+const struct setting_parser_info *pop3_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &pop3_login_setting_parser_info,
+ NULL
+};
+/* ../../src/plugins/quota/quota-status-settings.c */
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct quota_status_settings)
+static const struct setting_define quota_status_setting_defines[] = {
+ DEF(STR, recipient_delimiter),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct quota_status_settings quota_status_default_settings = {
+ .recipient_delimiter = "+",
+};
+static const struct setting_parser_info *quota_status_setting_dependencies[] = {
+ NULL
+};
+const struct setting_parser_info quota_status_setting_parser_info = {
+ .module_name = "mail",
+ .defines = quota_status_setting_defines,
+ .defaults = &quota_status_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct quota_status_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = quota_status_setting_dependencies
+};
+/* ../../src/plugins/mail-crypt/fs-crypt-settings.c */
+static const struct setting_define fs_crypt_setting_defines[] = {
+ { .type = SET_STRLIST, .key = "plugin",
+ .offset = offsetof(struct fs_crypt_settings, plugin_envs) },
+
+ SETTING_DEFINE_LIST_END
+};
+const struct fs_crypt_settings fs_crypt_default_settings = {
+ .plugin_envs = ARRAY_INIT
+};
+static const struct setting_parser_info *fs_crypt_setting_dependencies[] = {
+ NULL
+};
+const struct setting_parser_info fs_crypt_setting_parser_info = {
+ .module_name = "fs-crypt",
+ .defines = fs_crypt_setting_defines,
+ .defaults = &fs_crypt_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct fs_crypt_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = fs_crypt_setting_dependencies
+};
+/* ../../src/old-stats/stats-settings.c */
+/* <settings checks> */
+static struct file_listener_settings old_stats_unix_listeners_array[] = {
+ { "old-stats", 0600, "", "" }
+};
+static struct file_listener_settings *old_stats_unix_listeners[] = {
+ &old_stats_unix_listeners_array[0]
+};
+static buffer_t old_stats_unix_listeners_buf = {
+ { { old_stats_unix_listeners, sizeof(old_stats_unix_listeners) } }
+};
+static struct file_listener_settings old_stats_fifo_listeners_array[] = {
+ { "old-stats-mail", 0600, "", "" },
+ { "old-stats-user", 0600, "", "" }
+};
+static struct file_listener_settings *old_stats_fifo_listeners[] = {
+ &old_stats_fifo_listeners_array[0],
+ &old_stats_fifo_listeners_array[1]
+};
+static buffer_t old_stats_fifo_listeners_buf = {
+ { { old_stats_fifo_listeners, sizeof(old_stats_fifo_listeners) } }
+};
+/* </settings checks> */
+struct service_settings old_stats_service_settings = {
+ .name = "old-stats",
+ .protocol = "",
+ .type = "",
+ .executable = "old-stats",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &old_stats_unix_listeners_buf,
+ sizeof(old_stats_unix_listeners[0]) } },
+ .fifo_listeners = { { &old_stats_fifo_listeners_buf,
+ sizeof(old_stats_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type("old_stats_"#name, name, struct old_stats_settings)
+static const struct setting_define old_stats_setting_defines[] = {
+ DEF(SIZE, memory_limit),
+ DEF(TIME, command_min_time),
+ DEF(TIME, session_min_time),
+ DEF(TIME, user_min_time),
+ DEF(TIME, domain_min_time),
+ DEF(TIME, ip_min_time),
+ DEF(STR, carbon_server),
+ DEF(TIME, carbon_interval),
+ DEF(STR, carbon_name),
+ SETTING_DEFINE_LIST_END
+};
+const struct old_stats_settings old_stats_default_settings = {
+ .memory_limit = 1024*1024*16,
+
+ .command_min_time = 60,
+ .session_min_time = 60*15,
+ .user_min_time = 60*60,
+ .domain_min_time = 60*60*12,
+ .ip_min_time = 60*60*12,
+
+ .carbon_interval = 30,
+ .carbon_server = "",
+ .carbon_name = ""
+};
+const struct setting_parser_info old_stats_setting_parser_info = {
+ .module_name = "stats",
+ .defines = old_stats_setting_defines,
+ .defaults = &old_stats_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct old_stats_settings),
+
+ .parent_offset = SIZE_MAX
+};
+/* ../../src/master/master-settings.c */
+extern const struct setting_parser_info service_setting_parser_info;
+extern const struct setting_parser_info service_setting_parser_info;
+/* <settings checks> */
+static void
+expand_user(const char **user, enum service_user_default *default_r,
+ const struct master_settings *set)
+{
+ /* $variable expansion is typically done by doveconf, but these
+ variables can come from built-in settings, so we need to expand
+ them here */
+ if (strcmp(*user, "$default_internal_user") == 0) {
+ *user = set->default_internal_user;
+ *default_r = SERVICE_USER_DEFAULT_INTERNAL;
+ } else if (strcmp(*user, "$default_login_user") == 0) {
+ *user = set->default_login_user;
+ *default_r = SERVICE_USER_DEFAULT_LOGIN;
+ } else {
+ *default_r = SERVICE_USER_DEFAULT_NONE;
+ }
+}
+
+static void
+expand_group(const char **group, const struct master_settings *set)
+{
+ /* $variable expansion is typically done by doveconf, but these
+ variables can come from built-in settings, so we need to expand
+ them here */
+ if (strcmp(*group, "$default_internal_group") == 0)
+ *group = set->default_internal_group;
+}
+
+static bool
+fix_file_listener_paths(ARRAY_TYPE(file_listener_settings) *l,
+ pool_t pool, const struct master_settings *master_set,
+ ARRAY_TYPE(const_string) *all_listeners,
+ const char **error_r)
+{
+ struct file_listener_settings *set;
+ size_t base_dir_len = strlen(master_set->base_dir);
+ enum service_user_default user_default;
+
+ if (!array_is_created(l))
+ return TRUE;
+
+ array_foreach_elem(l, set) {
+ if (set->path[0] == '\0') {
+ *error_r = "path must not be empty";
+ return FALSE;
+ }
+
+ expand_user(&set->user, &user_default, master_set);
+ expand_group(&set->group, master_set);
+ if (*set->path != '/') {
+ set->path = p_strconcat(pool, master_set->base_dir, "/",
+ set->path, NULL);
+ } else if (strncmp(set->path, master_set->base_dir,
+ base_dir_len) == 0 &&
+ set->path[base_dir_len] == '/') {
+ i_warning("You should remove base_dir prefix from "
+ "unix_listener: %s", set->path);
+ }
+ if (set->mode != 0)
+ array_push_back(all_listeners, &set->path);
+ }
+ return TRUE;
+}
+
+static void add_inet_listeners(ARRAY_TYPE(inet_listener_settings) *l,
+ ARRAY_TYPE(const_string) *all_listeners)
+{
+ struct inet_listener_settings *set;
+ const char *str;
+
+ if (!array_is_created(l))
+ return;
+
+ array_foreach_elem(l, set) {
+ if (set->port != 0) {
+ str = t_strdup_printf("%u:%s", set->port, set->address);
+ array_push_back(all_listeners, &str);
+ }
+ }
+}
+
+static bool master_settings_parse_type(struct service_settings *set,
+ const char **error_r)
+{
+ if (*set->type == '\0')
+ set->parsed_type = SERVICE_TYPE_UNKNOWN;
+ else if (strcmp(set->type, "log") == 0)
+ set->parsed_type = SERVICE_TYPE_LOG;
+ else if (strcmp(set->type, "config") == 0)
+ set->parsed_type = SERVICE_TYPE_CONFIG;
+ else if (strcmp(set->type, "anvil") == 0)
+ set->parsed_type = SERVICE_TYPE_ANVIL;
+ else if (strcmp(set->type, "login") == 0)
+ set->parsed_type = SERVICE_TYPE_LOGIN;
+ else if (strcmp(set->type, "startup") == 0)
+ set->parsed_type = SERVICE_TYPE_STARTUP;
+ else if (strcmp(set->type, "worker") == 0)
+ set->parsed_type = SERVICE_TYPE_WORKER;
+ else {
+ *error_r = t_strconcat("Unknown service type: ",
+ set->type, NULL);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void service_set_login_dump_core(struct service_settings *set)
+{
+ const char *p;
+
+ if (set->parsed_type != SERVICE_TYPE_LOGIN)
+ return;
+
+ p = strstr(set->executable, " -D");
+ if (p != NULL && (p[3] == '\0' || p[3] == ' '))
+ set->login_dump_core = TRUE;
+}
+
+static bool
+services_have_protocol(struct master_settings *set, const char *name)
+{
+ struct service_settings *service;
+
+ array_foreach_elem(&set->services, service) {
+ if (strcmp(service->protocol, name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+#ifdef CONFIG_BINARY
+static const struct service_settings *
+master_default_settings_get_service(const char *name)
+{
+ extern struct master_settings master_default_settings;
+ struct service_settings *set;
+
+ array_foreach_elem(&master_default_settings.services, set) {
+ if (strcmp(set->name, name) == 0)
+ return set;
+ }
+ return NULL;
+}
+#endif
+
+static unsigned int
+service_get_client_limit(struct master_settings *set, const char *name)
+{
+ struct service_settings *service;
+
+ array_foreach_elem(&set->services, service) {
+ if (strcmp(service->name, name) == 0) {
+ if (service->client_limit != 0)
+ return service->client_limit;
+ else
+ return set->default_client_limit;
+ }
+ }
+ return set->default_client_limit;
+}
+
+static bool
+master_settings_verify(void *_set, pool_t pool, const char **error_r)
+{
+ static bool warned_auth = FALSE, warned_anvil = FALSE;
+ struct master_settings *set = _set;
+ struct service_settings *const *services;
+ const char *const *strings;
+ ARRAY_TYPE(const_string) all_listeners;
+ struct passwd pw;
+ unsigned int i, j, count, client_limit, process_limit;
+ unsigned int max_auth_client_processes, max_anvil_client_processes;
+ string_t *max_auth_client_processes_reason = t_str_new(64);
+ string_t *max_anvil_client_processes_reason = t_str_new(64);
+ size_t len;
+#ifdef CONFIG_BINARY
+ const struct service_settings *default_service;
+#else
+ rlim_t fd_limit;
+ const char *max_client_limit_source = "default_client_limit";
+ unsigned int max_client_limit = set->default_client_limit;
+#endif
+
+ if (*set->listen == '\0') {
+ *error_r = "listen can't be set empty";
+ return FALSE;
+ }
+
+ len = strlen(set->base_dir);
+ if (len > 0 && set->base_dir[len-1] == '/') {
+ /* drop trailing '/' */
+ set->base_dir = p_strndup(pool, set->base_dir, len - 1);
+ }
+
+ if (set->last_valid_uid != 0 &&
+ set->first_valid_uid > set->last_valid_uid) {
+ *error_r = "first_valid_uid can't be larger than last_valid_uid";
+ return FALSE;
+ }
+ if (set->last_valid_gid != 0 &&
+ set->first_valid_gid > set->last_valid_gid) {
+ *error_r = "first_valid_gid can't be larger than last_valid_gid";
+ return FALSE;
+ }
+
+ if (i_getpwnam(set->default_login_user, &pw) == 0) {
+ *error_r = t_strdup_printf("default_login_user doesn't exist: %s",
+ set->default_login_user);
+ return FALSE;
+ }
+ if (i_getpwnam(set->default_internal_user, &pw) == 0) {
+ *error_r = t_strdup_printf("default_internal_user doesn't exist: %s",
+ set->default_internal_user);
+ return FALSE;
+ }
+
+ /* check that we have at least one service. the actual service
+ structure validity is checked later while creating them. */
+ if (!array_is_created(&set->services) ||
+ array_count(&set->services) == 0) {
+ *error_r = "No services defined";
+ return FALSE;
+ }
+ services = array_get(&set->services, &count);
+ for (i = 0; i < count; i++) {
+ struct service_settings *service = services[i];
+
+ if (*service->name == '\0') {
+ *error_r = t_strdup_printf(
+ "Service #%d is missing name", i);
+ return FALSE;
+ }
+ if (!master_settings_parse_type(service, error_r))
+ return FALSE;
+ for (j = 0; j < i; j++) {
+ if (strcmp(service->name, services[j]->name) == 0) {
+ *error_r = t_strdup_printf(
+ "Duplicate service name: %s",
+ service->name);
+ return FALSE;
+ }
+ }
+ expand_user(&service->user, &service->user_default, set);
+ expand_group(&service->extra_groups, set);
+ service_set_login_dump_core(service);
+ }
+ set->protocols_split = p_strsplit_spaces(pool, set->protocols, " ");
+ if (set->protocols_split[0] != NULL &&
+ strcmp(set->protocols_split[0], "none") == 0 &&
+ set->protocols_split[1] == NULL)
+ set->protocols_split[0] = NULL;
+
+ for (i = 0; set->protocols_split[i] != NULL; i++) {
+ if (!services_have_protocol(set, set->protocols_split[i])) {
+ *error_r = t_strdup_printf("protocols: "
+ "Unknown protocol: %s",
+ set->protocols_split[i]);
+ return FALSE;
+ }
+ }
+ t_array_init(&all_listeners, 64);
+ max_auth_client_processes = 0;
+ max_anvil_client_processes = 2; /* blocking, nonblocking pipes */
+ for (i = 0; i < count; i++) {
+ struct service_settings *service = services[i];
+
+ if (*service->protocol != '\0' &&
+ !str_array_find((const char **)set->protocols_split,
+ service->protocol)) {
+ /* protocol not enabled, ignore its settings */
+ continue;
+ }
+
+ if (*service->executable != '/' &&
+ *service->executable != '\0') {
+ service->executable =
+ p_strconcat(pool, set->libexec_dir, "/",
+ service->executable, NULL);
+ }
+ if (*service->chroot != '/' && *service->chroot != '\0') {
+ service->chroot =
+ p_strconcat(pool, set->base_dir, "/",
+ service->chroot, NULL);
+ }
+ if (service->drop_priv_before_exec &&
+ *service->chroot != '\0') {
+ *error_r = t_strdup_printf("service(%s): "
+ "drop_priv_before_exec=yes can't be "
+ "used with chroot", service->name);
+ return FALSE;
+ }
+ process_limit = service->process_limit;
+ if (process_limit == 0)
+ process_limit = set->default_process_limit;
+ if (service->process_min_avail > process_limit) {
+ *error_r = t_strdup_printf("service(%s): "
+ "process_min_avail is higher than process_limit",
+ service->name);
+ return FALSE;
+ }
+ if (service->vsz_limit < 1024*1024 && service->vsz_limit != 0) {
+ *error_r = t_strdup_printf("service(%s): "
+ "vsz_limit is too low", service->name);
+ return FALSE;
+ }
+
+#ifdef CONFIG_BINARY
+ default_service =
+ master_default_settings_get_service(service->name);
+ if (default_service != NULL &&
+ default_service->process_limit_1 && process_limit > 1) {
+ *error_r = t_strdup_printf("service(%s): "
+ "process_limit must be 1", service->name);
+ return FALSE;
+ }
+#else
+ if (max_client_limit < service->client_limit) {
+ max_client_limit = service->client_limit;
+ max_client_limit_source = t_strdup_printf(
+ "service %s { client_limit }", service->name);
+ }
+#endif
+
+ if (*service->protocol != '\0') {
+ /* each imap/pop3/lmtp process can use up a connection,
+ although if service_count=1 it's only temporary.
+ imap-hibernate doesn't do any auth lookups. */
+ if ((service->service_count != 1 ||
+ strcmp(service->type, "login") == 0) &&
+ strcmp(service->name, "imap-hibernate") != 0) {
+ str_printfa(max_auth_client_processes_reason,
+ " + service %s { process_limit=%u }",
+ service->name, process_limit);
+ max_auth_client_processes += process_limit;
+ }
+ }
+ if (strcmp(service->type, "login") == 0 ||
+ strcmp(service->name, "auth") == 0) {
+ max_anvil_client_processes += process_limit;
+ str_printfa(max_anvil_client_processes_reason,
+ " + service %s { process_limit=%u }",
+ service->name, process_limit);
+ }
+
+ if (!fix_file_listener_paths(&service->unix_listeners, pool,
+ set, &all_listeners, error_r)) {
+ *error_r = t_strdup_printf("service(%s): unix_listener: %s",
+ service->name, *error_r);
+ return FALSE;
+ }
+ if (!fix_file_listener_paths(&service->fifo_listeners, pool,
+ set, &all_listeners, error_r)) {
+ *error_r = t_strdup_printf("service(%s): fifo_listener: %s",
+ service->name, *error_r);
+ return FALSE;
+ }
+ add_inet_listeners(&service->inet_listeners, &all_listeners);
+ }
+
+ client_limit = service_get_client_limit(set, "auth");
+ if (client_limit < max_auth_client_processes && !warned_auth) {
+ warned_auth = TRUE;
+ str_delete(max_auth_client_processes_reason, 0, 3);
+ i_warning("service auth { client_limit=%u } is lower than "
+ "required under max. load (%u). "
+ "Counted for protocol services with service_count != 1: %s",
+ client_limit, max_auth_client_processes,
+ str_c(max_auth_client_processes_reason));
+ }
+
+ client_limit = service_get_client_limit(set, "anvil");
+ if (client_limit < max_anvil_client_processes && !warned_anvil) {
+ warned_anvil = TRUE;
+ str_delete(max_anvil_client_processes_reason, 0, 3);
+ i_warning("service anvil { client_limit=%u } is lower than "
+ "required under max. load (%u). Counted with: %s",
+ client_limit, max_anvil_client_processes,
+ str_c(max_anvil_client_processes_reason));
+ }
+#ifndef CONFIG_BINARY
+ if (restrict_get_fd_limit(&fd_limit) == 0 &&
+ fd_limit < (rlim_t)max_client_limit) {
+ i_warning("fd limit (ulimit -n) is lower than required "
+ "under max. load (%u < %u), because of %s",
+ (unsigned int)fd_limit, max_client_limit,
+ max_client_limit_source);
+ }
+#endif
+
+ /* check for duplicate listeners */
+ array_sort(&all_listeners, i_strcmp_p);
+ strings = array_get(&all_listeners, &count);
+ for (i = 1; i < count; i++) {
+ if (strcmp(strings[i-1], strings[i]) == 0) {
+ *error_r = t_strdup_printf("duplicate listener: %s",
+ strings[i]);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+/* </settings checks> */
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct file_listener_settings)
+static const struct setting_define file_listener_setting_defines[] = {
+ DEF(STR, path),
+ DEF(UINT_OCT, mode),
+ DEF(STR, user),
+ DEF(STR, group),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct file_listener_settings file_listener_default_settings = {
+ .path = "",
+ .mode = 0600,
+ .user = "",
+ .group = "",
+};
+static const struct setting_parser_info file_listener_setting_parser_info = {
+ .defines = file_listener_setting_defines,
+ .defaults = &file_listener_default_settings,
+
+ .type_offset = offsetof(struct file_listener_settings, path),
+ .struct_size = sizeof(struct file_listener_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &service_setting_parser_info
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct inet_listener_settings)
+static const struct setting_define inet_listener_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, address),
+ DEF(IN_PORT, port),
+ DEF(BOOL, ssl),
+ DEF(BOOL, reuse_port),
+ DEF(BOOL, haproxy),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct inet_listener_settings inet_listener_default_settings = {
+ .name = "",
+ .address = "",
+ .port = 0,
+ .ssl = FALSE,
+ .reuse_port = FALSE,
+ .haproxy = FALSE
+};
+static const struct setting_parser_info inet_listener_setting_parser_info = {
+ .defines = inet_listener_setting_defines,
+ .defaults = &inet_listener_default_settings,
+
+ .type_offset = offsetof(struct inet_listener_settings, name),
+ .struct_size = sizeof(struct inet_listener_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &service_setting_parser_info
+};
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct service_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct service_settings, field), \
+ .list_info = defines }
+static const struct setting_define service_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, protocol),
+ DEF(STR, type),
+ DEF(STR, executable),
+ DEF(STR, user),
+ DEF(STR, group),
+ DEF(STR, privileged_group),
+ DEF(STR, extra_groups),
+ DEF(STR, chroot),
+
+ DEF(BOOL, drop_priv_before_exec),
+
+ DEF(UINT, process_min_avail),
+ DEF(UINT, process_limit),
+ DEF(UINT, client_limit),
+ DEF(UINT, service_count),
+ DEF(TIME, idle_kill),
+ DEF(SIZE, vsz_limit),
+
+ DEFLIST_UNIQUE(unix_listeners, "unix_listener",
+ &file_listener_setting_parser_info),
+ DEFLIST_UNIQUE(fifo_listeners, "fifo_listener",
+ &file_listener_setting_parser_info),
+ DEFLIST_UNIQUE(inet_listeners, "inet_listener",
+ &inet_listener_setting_parser_info),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct service_settings service_default_settings = {
+ .name = "",
+ .protocol = "",
+ .type = "",
+ .executable = "",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+const struct setting_parser_info service_setting_parser_info = {
+ .defines = service_setting_defines,
+ .defaults = &service_default_settings,
+
+ .type_offset = offsetof(struct service_settings, name),
+ .struct_size = sizeof(struct service_settings),
+
+ .parent_offset = offsetof(struct service_settings, master_set),
+ .parent = &master_setting_parser_info
+};
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct master_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct master_settings, field), \
+ .list_info = defines }
+static const struct setting_define master_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, state_dir),
+ DEF(STR, libexec_dir),
+ DEF(STR, instance_name),
+ DEF(STR, protocols),
+ DEF(STR, listen),
+ DEF(ENUM, ssl),
+ DEF(STR, default_internal_user),
+ DEF(STR, default_internal_group),
+ DEF(STR, default_login_user),
+ DEF(UINT, default_process_limit),
+ DEF(UINT, default_client_limit),
+ DEF(TIME, default_idle_kill),
+ DEF(SIZE, default_vsz_limit),
+
+ DEF(BOOL, version_ignore),
+
+ DEF(UINT, first_valid_uid),
+ DEF(UINT, last_valid_uid),
+ DEF(UINT, first_valid_gid),
+ DEF(UINT, last_valid_gid),
+
+ DEFLIST_UNIQUE(services, "service", &service_setting_parser_info),
+
+ SETTING_DEFINE_LIST_END
+};
+struct master_settings master_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .state_dir = PKG_STATEDIR,
+ .libexec_dir = PKG_LIBEXECDIR,
+ .instance_name = PACKAGE,
+ .protocols = "imap pop3 lmtp",
+ .listen = "*, ::",
+ .ssl = "yes:no:required",
+ .default_internal_user = "dovecot",
+ .default_internal_group = "dovecot",
+ .default_login_user = "dovenull",
+ .default_process_limit = 100,
+ .default_client_limit = 1000,
+ .default_idle_kill = 60,
+ .default_vsz_limit = 256*1024*1024,
+
+ .version_ignore = FALSE,
+
+ .first_valid_uid = 500,
+ .last_valid_uid = 0,
+ .first_valid_gid = 1,
+ .last_valid_gid = 0,
+
+#ifndef CONFIG_BINARY
+ .services = ARRAY_INIT
+#else
+ .services = { { &config_all_services_buf,
+ sizeof(struct service_settings *) } },
+#endif
+};
+const struct setting_parser_info master_setting_parser_info = {
+ .module_name = "master",
+ .defines = master_setting_defines,
+ .defaults = &master_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct master_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = master_settings_verify
+};
+/* ../../src/login-common/login-settings.c */
+/* <settings checks> */
+static bool login_settings_check(void *_set, pool_t pool,
+ const char **error_r ATTR_UNUSED)
+{
+ struct login_settings *set = _set;
+
+ set->log_format_elements_split =
+ p_strsplit(pool, set->login_log_format_elements, " ");
+
+ if (set->auth_debug_passwords)
+ set->auth_debug = TRUE;
+ if (set->auth_debug)
+ set->auth_verbose = TRUE;
+ return TRUE;
+}
+/* </settings checks> */
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct login_settings)
+static const struct setting_define login_setting_defines[] = {
+ DEF(STR, login_trusted_networks),
+ DEF(STR, login_source_ips),
+ DEF(STR_VARS, login_greeting),
+ DEF(STR, login_log_format_elements),
+ DEF(STR, login_log_format),
+ DEF(STR, login_access_sockets),
+ DEF(STR_VARS, login_proxy_notify_path),
+ DEF(STR, login_plugin_dir),
+ DEF(STR, login_plugins),
+ DEF(TIME_MSECS, login_proxy_timeout),
+ DEF(UINT, login_proxy_max_reconnects),
+ DEF(TIME, login_proxy_max_disconnect_delay),
+ DEF(STR, login_proxy_rawlog_dir),
+ DEF(STR, director_username_hash),
+
+ DEF(BOOL, auth_ssl_require_client_cert),
+ DEF(BOOL, auth_ssl_username_from_cert),
+
+ DEF(BOOL, disable_plaintext_auth),
+ DEF(BOOL, auth_verbose),
+ DEF(BOOL, auth_debug),
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(UINT, mail_max_userip_connections),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct login_settings login_default_settings = {
+ .login_trusted_networks = "",
+ .login_source_ips = "",
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c session=<%{session}>",
+ .login_log_format = "%$: %s",
+ .login_access_sockets = "",
+ .login_proxy_notify_path = "proxy-notify",
+ .login_plugin_dir = MODULEDIR"/login",
+ .login_plugins = "",
+ .login_proxy_timeout = 30*1000,
+ .login_proxy_max_reconnects = 3,
+ .login_proxy_max_disconnect_delay = 0,
+ .login_proxy_rawlog_dir = "",
+ .director_username_hash = "%Lu",
+
+ .auth_ssl_require_client_cert = FALSE,
+ .auth_ssl_username_from_cert = FALSE,
+
+ .disable_plaintext_auth = TRUE,
+ .auth_verbose = FALSE,
+ .auth_debug = FALSE,
+ .verbose_proctitle = FALSE,
+
+ .mail_max_userip_connections = 10
+};
+const struct setting_parser_info login_setting_parser_info = {
+ .module_name = "login",
+ .defines = login_setting_defines,
+ .defaults = &login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct login_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = login_settings_check
+};
+/* ../../src/log/log-settings.c */
+/* <settings checks> */
+static struct file_listener_settings log_unix_listeners_array[] = {
+ { "log-errors", 0600, "", "" }
+};
+static struct file_listener_settings *log_unix_listeners[] = {
+ &log_unix_listeners_array[0]
+};
+static buffer_t log_unix_listeners_buf = {
+ { { log_unix_listeners, sizeof(log_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings log_service_settings = {
+ .name = "log",
+ .protocol = "",
+ .type = "log",
+ .executable = "log",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &log_unix_listeners_buf,
+ sizeof(log_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+/* ../../src/lmtp/lmtp-settings.c */
+/* <settings checks> */
+static struct file_listener_settings lmtp_unix_listeners_array[] = {
+ { "lmtp", 0666, "", "" }
+};
+static struct file_listener_settings *lmtp_unix_listeners[] = {
+ &lmtp_unix_listeners_array[0]
+};
+static buffer_t lmtp_unix_listeners_buf = {
+ { { lmtp_unix_listeners, sizeof(lmtp_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+struct lmtp_client_workaround_list {
+ const char *name;
+ enum lmtp_client_workarounds num;
+};
+
+static const struct lmtp_client_workaround_list
+lmtp_client_workaround_list[] = {
+ { "whitespace-before-path", LMTP_WORKAROUND_WHITESPACE_BEFORE_PATH },
+ { "mailbox-for-path", LMTP_WORKAROUND_MAILBOX_FOR_PATH },
+ { NULL, 0 }
+};
+
+static int
+lmtp_settings_parse_workarounds(struct lmtp_settings *set,
+ const char **error_r)
+{
+ enum lmtp_client_workarounds client_workarounds = 0;
+ const struct lmtp_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->lmtp_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = lmtp_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "lmtp_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool lmtp_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct lmtp_settings *set = _set;
+
+ if (lmtp_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+ if (strcmp(set->lmtp_hdr_delivery_address, "none") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_NONE;
+ } else if (strcmp(set->lmtp_hdr_delivery_address, "final") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_FINAL;
+ } else if (strcmp(set->lmtp_hdr_delivery_address, "original") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL;
+ } else {
+ *error_r = t_strdup_printf("Unknown lmtp_hdr_delivery_address: %s",
+ set->lmtp_hdr_delivery_address);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings lmtp_service_settings = {
+ .name = "lmtp",
+ .protocol = "lmtp",
+ .type = "",
+ .executable = "lmtp",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &lmtp_unix_listeners_buf,
+ sizeof(lmtp_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct lmtp_settings)
+static const struct setting_define lmtp_setting_defines[] = {
+ DEF(BOOL, lmtp_proxy),
+ DEF(BOOL, lmtp_save_to_detail_mailbox),
+ DEF(BOOL, lmtp_rcpt_check_quota),
+ DEF(BOOL, lmtp_add_received_header),
+ DEF(BOOL, lmtp_verbose_replies),
+ DEF(UINT, lmtp_user_concurrency_limit),
+ DEF(ENUM, lmtp_hdr_delivery_address),
+ DEF(STR_VARS, lmtp_rawlog_dir),
+ DEF(STR_VARS, lmtp_proxy_rawlog_dir),
+
+ DEF(STR, lmtp_client_workarounds),
+
+ DEF(STR_VARS, login_greeting),
+ DEF(STR, login_trusted_networks),
+
+ DEF(STR, mail_plugins),
+ DEF(STR, mail_plugin_dir),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct lmtp_settings lmtp_default_settings = {
+ .lmtp_proxy = FALSE,
+ .lmtp_save_to_detail_mailbox = FALSE,
+ .lmtp_rcpt_check_quota = FALSE,
+ .lmtp_add_received_header = TRUE,
+ .lmtp_verbose_replies = FALSE,
+ .lmtp_user_concurrency_limit = 0,
+ .lmtp_hdr_delivery_address = "final:none:original",
+ .lmtp_rawlog_dir = "",
+ .lmtp_proxy_rawlog_dir = "",
+
+ .lmtp_client_workarounds = "",
+
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_trusted_networks = "",
+
+ .mail_plugins = "",
+ .mail_plugin_dir = MODULEDIR,
+};
+static const struct setting_parser_info *lmtp_setting_dependencies[] = {
+ &lda_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info lmtp_setting_parser_info = {
+ .module_name = "lmtp",
+ .defines = lmtp_setting_defines,
+ .defaults = &lmtp_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct lmtp_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = lmtp_settings_check,
+ .dependencies = lmtp_setting_dependencies
+};
+/* ../../src/ipc/ipc-settings.c */
+/* <settings checks> */
+static struct file_listener_settings ipc_unix_listeners_array[] = {
+ { "ipc", 0600, "$default_internal_user", "" },
+ { "login/ipc-proxy", 0600, "$default_login_user", "" }
+};
+static struct file_listener_settings *ipc_unix_listeners[] = {
+ &ipc_unix_listeners_array[0],
+ &ipc_unix_listeners_array[1]
+};
+static buffer_t ipc_unix_listeners_buf = {
+ { { ipc_unix_listeners, sizeof(ipc_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings ipc_service_settings = {
+ .name = "ipc",
+ .protocol = "",
+ .type = "",
+ .executable = "ipc",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &ipc_unix_listeners_buf,
+ sizeof(ipc_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+/* ../../src/indexer/indexer-worker-settings.c */
+/* <settings checks> */
+static struct file_listener_settings indexer_worker_unix_listeners_array[] = {
+ { "indexer-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *indexer_worker_unix_listeners[] = {
+ &indexer_worker_unix_listeners_array[0]
+};
+static buffer_t indexer_worker_unix_listeners_buf = {
+ { { indexer_worker_unix_listeners, sizeof(indexer_worker_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings indexer_worker_service_settings = {
+ .name = "indexer-worker",
+ .protocol = "",
+ .type = "worker",
+ .executable = "indexer-worker",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 10,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &indexer_worker_unix_listeners_buf,
+ sizeof(indexer_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+/* ../../src/indexer/indexer-settings.c */
+extern const struct setting_parser_info service_setting_parser_info;
+/* <settings checks> */
+static struct file_listener_settings indexer_unix_listeners_array[] = {
+ { "indexer", 0666, "", "" }
+};
+static struct file_listener_settings *indexer_unix_listeners[] = {
+ &indexer_unix_listeners_array[0]
+};
+static buffer_t indexer_unix_listeners_buf = {
+ { { indexer_unix_listeners, sizeof(indexer_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings indexer_service_settings = {
+ .name = "indexer",
+ .protocol = "",
+ .type = "",
+ .executable = "indexer",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &indexer_unix_listeners_buf,
+ sizeof(indexer_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+/* ../../src/imap/imap-settings.c */
+/* <settings checks> */
+static struct file_listener_settings imap_unix_listeners_array[] = {
+ { "login/imap", 0666, "", "" },
+ { "imap-master", 0600, "", "" }
+};
+static struct file_listener_settings *imap_unix_listeners[] = {
+ &imap_unix_listeners_array[0],
+ &imap_unix_listeners_array[1]
+};
+static buffer_t imap_unix_listeners_buf = {
+ { { imap_unix_listeners, sizeof(imap_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+struct imap_client_workaround_list {
+ const char *name;
+ enum imap_client_workarounds num;
+};
+
+static const struct imap_client_workaround_list imap_client_workaround_list[] = {
+ { "delay-newmail", WORKAROUND_DELAY_NEWMAIL },
+ { "tb-extra-mailbox-sep", WORKAROUND_TB_EXTRA_MAILBOX_SEP },
+ { "tb-lsub-flags", WORKAROUND_TB_LSUB_FLAGS },
+ { NULL, 0 }
+};
+
+static int
+imap_settings_parse_workarounds(struct imap_settings *set,
+ const char **error_r)
+{
+ enum imap_client_workarounds client_workarounds = 0;
+ const struct imap_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->imap_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = imap_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("imap_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+
+static bool
+imap_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct imap_settings *set = _set;
+
+ if (imap_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+ if (strcmp(set->imap_fetch_failure, "disconnect-immediately") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY;
+ else if (strcmp(set->imap_fetch_failure, "disconnect-after") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER;
+ else if (strcmp(set->imap_fetch_failure, "no-after") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_NO_AFTER;
+ else {
+ *error_r = t_strdup_printf("Unknown imap_fetch_failure: %s",
+ set->imap_fetch_failure);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings imap_service_settings = {
+ .name = "imap",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_unix_listeners_buf,
+ sizeof(imap_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct imap_settings, field), \
+ .list_info = defines }
+static const struct setting_define imap_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(SIZE, imap_max_line_length),
+ DEF(TIME, imap_idle_notify_interval),
+ DEF(STR, imap_capability),
+ DEF(STR, imap_client_workarounds),
+ DEF(STR, imap_logout_format),
+ DEF(STR, imap_id_send),
+ DEF(STR, imap_id_log),
+ DEF(ENUM, imap_fetch_failure),
+ DEF(BOOL, imap_metadata),
+ DEF(BOOL, imap_literal_minus),
+ DEF(TIME, imap_hibernate_timeout),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct imap_settings imap_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ /* RFC-2683 recommends at least 8000 bytes. Some clients however don't
+ break large message sets to multiple commands, so we're pretty
+ liberal by default. */
+ .imap_max_line_length = 64*1024,
+ .imap_idle_notify_interval = 2*60,
+ .imap_capability = "",
+ .imap_client_workarounds = "",
+ .imap_logout_format = "in=%i out=%o deleted=%{deleted} "
+ "expunged=%{expunged} trashed=%{trashed} "
+ "hdr_count=%{fetch_hdr_count} hdr_bytes=%{fetch_hdr_bytes} "
+ "body_count=%{fetch_body_count} body_bytes=%{fetch_body_bytes}",
+ .imap_id_send = "name *",
+ .imap_id_log = "",
+ .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after",
+ .imap_metadata = FALSE,
+ .imap_literal_minus = FALSE,
+ .imap_hibernate_timeout = 0,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143
+};
+static const struct setting_parser_info *imap_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ &smtp_submit_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info imap_setting_parser_info = {
+ .module_name = "imap",
+ .defines = imap_setting_defines,
+ .defaults = &imap_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = imap_settings_verify,
+ .dependencies = imap_setting_dependencies
+};
+/* ../../src/imap-urlauth/imap-urlauth-worker-settings.c */
+/* <settings checks> */
+static struct file_listener_settings imap_urlauth_worker_unix_listeners_array[] = {
+ { "imap-urlauth-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *imap_urlauth_worker_unix_listeners[] = {
+ &imap_urlauth_worker_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_worker_unix_listeners_buf = {
+ { { imap_urlauth_worker_unix_listeners,
+ sizeof(imap_urlauth_worker_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings imap_urlauth_worker_service_settings = {
+ .name = "imap-urlauth-worker",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-urlauth-worker",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_worker_unix_listeners_buf,
+ sizeof(imap_urlauth_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_urlauth_worker_settings)
+static const struct setting_define imap_urlauth_worker_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+const struct imap_urlauth_worker_settings imap_urlauth_worker_default_settings = {
+ .verbose_proctitle = FALSE,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143
+};
+static const struct setting_parser_info *imap_urlauth_worker_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info imap_urlauth_worker_setting_parser_info = {
+ .module_name = "imap-urlauth-worker",
+ .defines = imap_urlauth_worker_setting_defines,
+ .defaults = &imap_urlauth_worker_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_urlauth_worker_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_worker_setting_dependencies
+};
+/* ../../src/imap-urlauth/imap-urlauth-settings.c */
+/* <settings checks> */
+static struct file_listener_settings imap_urlauth_unix_listeners_array[] = {
+ { "token-login/imap-urlauth", 0666, "", "" }
+};
+static struct file_listener_settings *imap_urlauth_unix_listeners[] = {
+ &imap_urlauth_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_unix_listeners_buf = {
+ { { imap_urlauth_unix_listeners, sizeof(imap_urlauth_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings imap_urlauth_service_settings = {
+ .name = "imap-urlauth",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-urlauth",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_unix_listeners_buf,
+ sizeof(imap_urlauth_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_urlauth_settings)
+static const struct setting_define imap_urlauth_setting_defines[] = {
+ DEF(STR, base_dir),
+
+ DEF(BOOL, mail_debug),
+
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, imap_urlauth_logout_format),
+ DEF(STR, imap_urlauth_submit_user),
+ DEF(STR, imap_urlauth_stream_user),
+
+ SETTING_DEFINE_LIST_END
+};
+const struct imap_urlauth_settings imap_urlauth_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .mail_debug = FALSE,
+
+ .verbose_proctitle = FALSE,
+
+ .imap_urlauth_logout_format = "in=%i out=%o",
+ .imap_urlauth_submit_user = NULL,
+ .imap_urlauth_stream_user = NULL
+};
+static const struct setting_parser_info *imap_urlauth_setting_dependencies[] = {
+ NULL
+};
+const struct setting_parser_info imap_urlauth_setting_parser_info = {
+ .module_name = "imap-urlauth",
+ .defines = imap_urlauth_setting_defines,
+ .defaults = &imap_urlauth_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_urlauth_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_setting_dependencies
+};
+/* ../../src/imap-urlauth/imap-urlauth-login-settings.c */
+/* <settings checks> */
+static struct file_listener_settings
+imap_urlauth_login_unix_listeners_array[] = {
+ { "imap-urlauth", 0666, "", "" }
+};
+static struct file_listener_settings *imap_urlauth_login_unix_listeners[] = {
+ &imap_urlauth_login_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_login_unix_listeners_buf = {
+ { { imap_urlauth_login_unix_listeners,
+ sizeof(imap_urlauth_login_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings imap_urlauth_login_service_settings = {
+ .name = "imap-urlauth-login",
+ .protocol = "imap",
+ .type = "login",
+ .executable = "imap-urlauth-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "token-login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_login_unix_listeners_buf,
+ sizeof(imap_urlauth_login_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+static const struct setting_define imap_urlauth_login_setting_defines[] = {
+ SETTING_DEFINE_LIST_END
+};
+static const struct setting_parser_info *imap_urlauth_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info imap_urlauth_login_setting_parser_info = {
+ .module_name = "imap-urlauth-login",
+ .defines = imap_urlauth_login_setting_defines,
+
+ .type_offset = SIZE_MAX,
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_login_setting_dependencies
+};
+const struct setting_parser_info *imap_urlauth_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &imap_urlauth_login_setting_parser_info,
+ NULL
+};
+/* ../../src/imap-login/imap-login-settings.c */
+/* <settings checks> */
+static struct inet_listener_settings imap_login_inet_listeners_array[] = {
+ { .name = "imap", .address = "", .port = 143 },
+ { .name = "imaps", .address = "", .port = 993, .ssl = TRUE }
+};
+static struct inet_listener_settings *imap_login_inet_listeners[] = {
+ &imap_login_inet_listeners_array[0],
+ &imap_login_inet_listeners_array[1]
+};
+static buffer_t imap_login_inet_listeners_buf = {
+ { { imap_login_inet_listeners, sizeof(imap_login_inet_listeners) } }
+};
+/* </settings checks> */
+struct service_settings imap_login_service_settings = {
+ .name = "imap-login",
+ .protocol = "imap",
+ .type = "login",
+ .executable = "imap-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &imap_login_inet_listeners_buf,
+ sizeof(imap_login_inet_listeners[0]) } }
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_login_settings)
+static const struct setting_define imap_login_setting_defines[] = {
+ DEF(STR, imap_capability),
+ DEF(STR, imap_id_send),
+ DEF(STR, imap_id_log),
+ DEF(BOOL, imap_literal_minus),
+ DEF(BOOL, imap_id_retain),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct imap_login_settings imap_login_default_settings = {
+ .imap_capability = "",
+ .imap_id_send = "name *",
+ .imap_id_log = "",
+ .imap_literal_minus = FALSE,
+ .imap_id_retain = FALSE,
+};
+static const struct setting_parser_info *imap_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+static const struct setting_parser_info imap_login_setting_parser_info = {
+ .module_name = "imap-login",
+ .defines = imap_login_setting_defines,
+ .defaults = &imap_login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_login_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = imap_login_setting_dependencies
+};
+const struct setting_parser_info *imap_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &imap_login_setting_parser_info,
+ NULL
+};
+/* ../../src/imap-hibernate/imap-hibernate-settings.c */
+/* <settings checks> */
+static struct file_listener_settings imap_hibernate_unix_listeners_array[] = {
+ { "imap-hibernate", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *imap_hibernate_unix_listeners[] = {
+ &imap_hibernate_unix_listeners_array[0]
+};
+static buffer_t imap_hibernate_unix_listeners_buf = {
+ { { imap_hibernate_unix_listeners, sizeof(imap_hibernate_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings imap_hibernate_service_settings = {
+ .name = "imap-hibernate",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-hibernate",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_hibernate_unix_listeners_buf,
+ sizeof(imap_hibernate_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+/* ../../src/doveadm/doveadm-settings.c */
+/* <settings checks> */
+static struct file_listener_settings doveadm_unix_listeners_array[] = {
+ { "doveadm-server", 0600, "", "" }
+};
+static struct file_listener_settings *doveadm_unix_listeners[] = {
+ &doveadm_unix_listeners_array[0]
+};
+static buffer_t doveadm_unix_listeners_buf = {
+ { { doveadm_unix_listeners, sizeof(doveadm_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+struct dsync_feature_list {
+ const char *name;
+ enum dsync_features num;
+};
+
+static const struct dsync_feature_list dsync_feature_list[] = {
+ { "empty-header-workaround", DSYNC_FEATURE_EMPTY_HDR_WORKAROUND },
+ { "no-header-hashes", DSYNC_FEATURE_NO_HEADER_HASHES },
+ { NULL, 0 }
+};
+
+static int
+dsync_settings_parse_features(struct doveadm_settings *set,
+ const char **error_r)
+{
+ enum dsync_features features = 0;
+ const struct dsync_feature_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->dsync_features, " ,");
+ for (; *str != NULL; str++) {
+ list = dsync_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("dsync_features: "
+ "Unknown feature: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_features = features;
+ return 0;
+}
+
+static bool doveadm_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct doveadm_settings *set = _set;
+
+#ifndef CONFIG_BINARY
+ fix_base_path(set, pool, &set->auth_socket_path);
+ fix_base_path(set, pool, &set->doveadm_socket_path);
+#endif
+ if (*set->dsync_hashed_headers == '\0') {
+ *error_r = "dsync_hashed_headers must not be empty";
+ return FALSE;
+ }
+ if (*set->dsync_alt_char == '\0') {
+ *error_r = "dsync_alt_char must not be empty";
+ return FALSE;
+ }
+ if (dsync_settings_parse_features(set, error_r) != 0)
+ return FALSE;
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings doveadm_service_settings = {
+ .name = "doveadm",
+ .protocol = "",
+ .type = "",
+ .executable = "doveadm-server",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &doveadm_unix_listeners_buf,
+ sizeof(doveadm_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct doveadm_settings)
+static const struct setting_define doveadm_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, libexec_dir),
+ DEF(STR, mail_plugins),
+ DEF(STR, mail_plugin_dir),
+ DEF(STR_VARS, mail_temp_dir),
+ DEF(BOOL, auth_debug),
+ DEF(STR, auth_socket_path),
+ DEF(STR, doveadm_socket_path),
+ DEF(UINT, doveadm_worker_count),
+ DEF(IN_PORT, doveadm_port),
+ { .type = SET_ALIAS, .key = "doveadm_proxy_port" },
+ DEF(ENUM, doveadm_ssl),
+ DEF(STR, doveadm_username),
+ DEF(STR, doveadm_password),
+ DEF(STR, doveadm_allowed_commands),
+ DEF(STR, dsync_alt_char),
+ DEF(STR, dsync_remote_cmd),
+ DEF(STR, director_username_hash),
+ DEF(STR, doveadm_api_key),
+ DEF(STR, dsync_features),
+ DEF(UINT, dsync_commit_msgs_interval),
+ DEF(STR, doveadm_http_rawlog_dir),
+ DEF(STR, dsync_hashed_headers),
+
+ { .type = SET_STRLIST, .key = "plugin",
+ .offset = offsetof(struct doveadm_settings, plugin_envs) },
+
+ SETTING_DEFINE_LIST_END
+};
+const struct doveadm_settings doveadm_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .libexec_dir = PKG_LIBEXECDIR,
+ .mail_plugins = "",
+ .mail_plugin_dir = MODULEDIR,
+ .mail_temp_dir = "/tmp",
+ .auth_debug = FALSE,
+ .auth_socket_path = "auth-userdb",
+ .doveadm_socket_path = "doveadm-server",
+ .doveadm_worker_count = 0,
+ .doveadm_port = 0,
+ .doveadm_ssl = "no:ssl:starttls",
+ .doveadm_username = "doveadm",
+ .doveadm_password = "",
+ .doveadm_allowed_commands = "",
+ .dsync_alt_char = "_",
+ .dsync_remote_cmd = "ssh -l%{login} %{host} doveadm dsync-server -u%u -U",
+ .dsync_features = "",
+ .dsync_hashed_headers = "Date Message-ID",
+ .dsync_commit_msgs_interval = 100,
+ .director_username_hash = "%Lu",
+ .doveadm_api_key = "",
+ .doveadm_http_rawlog_dir = "",
+
+ .plugin_envs = ARRAY_INIT
+};
+static const struct setting_parser_info *doveadm_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info doveadm_setting_parser_info = {
+ .module_name = "doveadm",
+ .defines = doveadm_setting_defines,
+ .defaults = &doveadm_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct doveadm_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = doveadm_settings_check,
+ .dependencies = doveadm_setting_dependencies
+};
+/* ../../src/dns/dns-client-settings.c */
+/* <settings checks> */
+static struct file_listener_settings dns_client_unix_listeners_array[] = {
+ { "dns-client", 0666, "", "" },
+ { "login/dns-client", 0666, "", "" },
+};
+static struct file_listener_settings *dns_client_unix_listeners[] = {
+ &dns_client_unix_listeners_array[0],
+ &dns_client_unix_listeners_array[1],
+};
+static buffer_t dns_client_unix_listeners_buf = {
+ { { dns_client_unix_listeners, sizeof(dns_client_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings dns_client_service_settings = {
+ .name = "dns-client",
+ .protocol = "",
+ .type = "",
+ .executable = "dns-client",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dns_client_unix_listeners_buf,
+ sizeof(dns_client_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+/* ../../src/director/director-settings.c */
+/* <settings checks> */
+static bool director_settings_verify(void *_set, pool_t pool, const char **error_r);
+
+static struct file_listener_settings director_unix_listeners_array[] = {
+ { "login/director", 0, "", "" },
+ { "director-admin", 0600, "", "" }
+};
+static struct file_listener_settings *director_unix_listeners[] = {
+ &director_unix_listeners_array[0],
+ &director_unix_listeners_array[1]
+};
+static buffer_t director_unix_listeners_buf = {
+ { { director_unix_listeners, sizeof(director_unix_listeners) } }
+};
+static struct file_listener_settings director_fifo_listeners_array[] = {
+ { "login/proxy-notify", 0, "", "" }
+};
+static struct file_listener_settings *director_fifo_listeners[] = {
+ &director_fifo_listeners_array[0]
+};
+static buffer_t director_fifo_listeners_buf = {
+ { { director_fifo_listeners, sizeof(director_fifo_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+static bool
+director_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct director_settings *set = _set;
+
+ if (set->director_user_expire < 10) {
+ *error_r = "director_user_expire is too low";
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings director_service_settings = {
+ .name = "director",
+ .protocol = "",
+ .type = "",
+ .executable = "director",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = ".",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &director_unix_listeners_buf,
+ sizeof(director_unix_listeners[0]) } },
+ .fifo_listeners = { { &director_fifo_listeners_buf,
+ sizeof(director_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct director_settings)
+static const struct setting_define director_setting_defines[] = {
+ DEF(STR, auth_master_user_separator),
+
+ DEF(STR, director_servers),
+ DEF(STR, director_mail_servers),
+ DEF(STR, director_username_hash),
+ DEF(STR, director_flush_socket),
+ DEF(TIME, director_ping_idle_timeout),
+ DEF(TIME, director_ping_max_timeout),
+ DEF(TIME, director_user_expire),
+ DEF(TIME, director_user_kick_delay),
+ DEF(UINT, director_max_parallel_moves),
+ DEF(UINT, director_max_parallel_kicks),
+ DEF(SIZE, director_output_buffer_size),
+
+ SETTING_DEFINE_LIST_END
+};
+const struct director_settings director_default_settings = {
+ .auth_master_user_separator = "",
+
+ .director_servers = "",
+ .director_mail_servers = "",
+ .director_username_hash = "%Lu",
+ .director_flush_socket = "",
+ .director_ping_idle_timeout = 30,
+ .director_ping_max_timeout = 60,
+ .director_user_expire = 60*15,
+ .director_user_kick_delay = 2,
+ .director_max_parallel_moves = 100,
+ .director_max_parallel_kicks = 100,
+ .director_output_buffer_size = 10 * 1024 * 1024,
+};
+const struct setting_parser_info director_setting_parser_info = {
+ .module_name = "director",
+ .defines = director_setting_defines,
+ .defaults = &director_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct director_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = director_settings_verify
+};
+/* ../../src/dict/dict-settings.c */
+/* <settings checks> */
+static struct file_listener_settings dict_unix_listeners_array[] = {
+ { "dict", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *dict_unix_listeners[] = {
+ &dict_unix_listeners_array[0]
+};
+static buffer_t dict_unix_listeners_buf = {
+ { { dict_unix_listeners, sizeof(dict_unix_listeners) } }
+};
+
+static struct file_listener_settings dict_async_unix_listeners_array[] = {
+ { "dict-async", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *dict_async_unix_listeners[] = {
+ &dict_async_unix_listeners_array[0]
+};
+static buffer_t dict_async_unix_listeners_buf = {
+ { { dict_async_unix_listeners, sizeof(dict_async_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings dict_service_settings = {
+ .name = "dict",
+ .protocol = "",
+ .type = "",
+ .executable = "dict",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dict_unix_listeners_buf,
+ sizeof(dict_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+struct service_settings dict_async_service_settings = {
+ .name = "dict-async",
+ .protocol = "",
+ .type = "",
+ .executable = "dict",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dict_async_unix_listeners_buf,
+ sizeof(dict_async_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct dict_server_settings)
+static const struct setting_define dict_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, dict_db_config),
+ { .type = SET_STRLIST, .key = "dict",
+ .offset = offsetof(struct dict_server_settings, dicts) },
+
+ SETTING_DEFINE_LIST_END
+};
+const struct dict_server_settings dict_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .verbose_proctitle = FALSE,
+
+ .dict_db_config = "",
+ .dicts = ARRAY_INIT
+};
+const struct setting_parser_info dict_setting_parser_info = {
+ .module_name = "dict",
+ .defines = dict_setting_defines,
+ .defaults = &dict_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct dict_server_settings),
+
+ .parent_offset = SIZE_MAX
+};
+/* ../../src/config/config-settings.c */
+/* <settings checks> */
+static struct file_listener_settings config_unix_listeners_array[] = {
+ { "config", 0600, "", "" }
+};
+static struct file_listener_settings *config_unix_listeners[] = {
+ &config_unix_listeners_array[0]
+};
+static buffer_t config_unix_listeners_buf = {
+ { { config_unix_listeners, sizeof(config_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings config_service_settings = {
+ .name = "config",
+ .protocol = "",
+ .type = "config",
+ .executable = "config",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &config_unix_listeners_buf,
+ sizeof(config_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+/* ../../src/auth/auth-settings.c */
+extern const struct setting_parser_info auth_passdb_setting_parser_info;
+extern const struct setting_parser_info auth_userdb_setting_parser_info;
+/* <settings checks> */
+static struct file_listener_settings auth_unix_listeners_array[] = {
+ { "login/login", 0666, "", "" },
+ { "token-login/tokenlogin", 0666, "", "" },
+ { "auth-login", 0600, "$default_internal_user", "" },
+ { "auth-client", 0600, "$default_internal_user", "" },
+ { "auth-userdb", 0666, "$default_internal_user", "" },
+ { "auth-master", 0600, "", "" }
+};
+static struct file_listener_settings *auth_unix_listeners[] = {
+ &auth_unix_listeners_array[0],
+ &auth_unix_listeners_array[1],
+ &auth_unix_listeners_array[2],
+ &auth_unix_listeners_array[3],
+ &auth_unix_listeners_array[4],
+ &auth_unix_listeners_array[5]
+};
+static buffer_t auth_unix_listeners_buf = {
+ { { auth_unix_listeners, sizeof(auth_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+static struct file_listener_settings auth_worker_unix_listeners_array[] = {
+ { "auth-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *auth_worker_unix_listeners[] = {
+ &auth_worker_unix_listeners_array[0]
+};
+static buffer_t auth_worker_unix_listeners_buf = {
+ { { auth_worker_unix_listeners, sizeof(auth_worker_unix_listeners) } }
+};
+/* </settings checks> */
+/* <settings checks> */
+static bool
+auth_settings_set_self_ips(struct auth_settings *set, pool_t pool,
+ const char **error_r)
+{
+ const char *const *tmp;
+ ARRAY(struct ip_addr) ips_array;
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int ret;
+
+ if (*set->proxy_self == '\0') {
+ set->proxy_self_ips = p_new(pool, struct ip_addr, 1);
+ return TRUE;
+ }
+
+ p_array_init(&ips_array, pool, 4);
+ tmp = t_strsplit_spaces(set->proxy_self, " ");
+ for (; *tmp != NULL; tmp++) {
+ ret = net_gethostbyname(*tmp, &ips, &ips_count);
+ if (ret != 0) {
+ *error_r = t_strdup_printf("auth_proxy_self_ips: "
+ "gethostbyname(%s) failed: %s",
+ *tmp, net_gethosterror(ret));
+ }
+ array_append(&ips_array, ips, ips_count);
+ }
+ array_append_zero(&ips_array);
+ set->proxy_self_ips = array_front(&ips_array);
+ return TRUE;
+}
+
+static bool
+auth_verify_verbose_password(struct auth_settings *set,
+ const char **error_r)
+{
+ const char *p, *value = set->verbose_passwords;
+ unsigned int num;
+
+ p = strchr(value, ':');
+ if (p != NULL) {
+ if (str_to_uint(p+1, &num) < 0 || num == 0) {
+ *error_r = t_strdup_printf("auth_verbose_passwords: "
+ "Invalid truncation number: '%s'", p+1);
+ return FALSE;
+ }
+ value = t_strdup_until(value, p);
+ }
+ if (strcmp(value, "no") == 0)
+ return TRUE;
+ else if (strcmp(value, "plain") == 0)
+ return TRUE;
+ else if (strcmp(value, "sha1") == 0)
+ return TRUE;
+ else if (strcmp(value, "yes") == 0) {
+ /* just use it as alias for "plain" */
+ set->verbose_passwords = "plain";
+ return TRUE;
+ } else {
+ *error_r = "auth_verbose_passwords: Invalid value";
+ return FALSE;
+ }
+}
+
+static bool auth_settings_check(void *_set, pool_t pool,
+ const char **error_r)
+{
+ struct auth_settings *set = _set;
+ const char *p;
+
+ if (set->debug_passwords)
+ set->debug = TRUE;
+ if (set->debug)
+ set->verbose = TRUE;
+
+ if (set->worker_max_count == 0) {
+ *error_r = "auth_worker_max_count must be above zero";
+ return FALSE;
+ }
+
+ if (set->cache_size > 0 && set->cache_size < 1024) {
+ /* probably a configuration error.
+ older versions used megabyte numbers */
+ *error_r = t_strdup_printf("auth_cache_size value is too small "
+ "(%"PRIuUOFF_T" bytes)",
+ set->cache_size);
+ return FALSE;
+ }
+
+ if (!auth_verify_verbose_password(set, error_r))
+ return FALSE;
+
+ if (*set->username_chars == '\0') {
+ /* all chars are allowed */
+ memset(set->username_chars_map, 1,
+ sizeof(set->username_chars_map));
+ } else {
+ for (p = set->username_chars; *p != '\0'; p++)
+ set->username_chars_map[(int)(uint8_t)*p] = 1;
+ }
+
+ if (*set->username_translation != '\0') {
+ p = set->username_translation;
+ for (; *p != '\0' && p[1] != '\0'; p += 2)
+ set->username_translation_map[(int)(uint8_t)*p] = p[1];
+ }
+ set->realms_arr =
+ (const char *const *)p_strsplit_spaces(pool, set->realms, " ");
+
+ if (*set->policy_server_url != '\0') {
+ if (*set->policy_hash_nonce == '\0') {
+
+ *error_r = "auth_policy_hash_nonce must be set when policy server is used";
+ return FALSE;
+ }
+ const struct hash_method *digest = hash_method_lookup(set->policy_hash_mech);
+ if (digest == NULL) {
+ *error_r = "invalid auth_policy_hash_mech given";
+ return FALSE;
+ }
+ if (set->policy_hash_truncate > 0 && set->policy_hash_truncate >= digest->digest_size*8) {
+ *error_r = t_strdup_printf("policy_hash_truncate is not smaller than digest size (%u >= %u)",
+ set->policy_hash_truncate,
+ digest->digest_size*8);
+ return FALSE;
+ }
+ }
+
+ if (!auth_settings_set_self_ips(set, pool, error_r))
+ return FALSE;
+ return TRUE;
+}
+
+static bool
+auth_passdb_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct auth_passdb_settings *set = _set;
+
+ if (set->driver == NULL || *set->driver == '\0') {
+ *error_r = "passdb is missing driver";
+ return FALSE;
+ }
+ if (set->pass && strcmp(set->result_success, "return-ok") != 0) {
+ *error_r = "Obsolete pass=yes setting mixed with non-default result_success";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+auth_userdb_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct auth_userdb_settings *set = _set;
+
+ if (set->driver == NULL || *set->driver == '\0') {
+ *error_r = "userdb is missing driver";
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+struct service_settings auth_service_settings = {
+ .name = "auth",
+ .protocol = "",
+ .type = "",
+ .executable = "auth",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &auth_unix_listeners_buf,
+ sizeof(auth_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+struct service_settings auth_worker_service_settings = {
+ .name = "auth-worker",
+ .protocol = "",
+ .type = "worker",
+ .executable = "auth -w",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &auth_worker_unix_listeners_buf,
+ sizeof(auth_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_passdb_settings)
+static const struct setting_define auth_passdb_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, driver),
+ DEF(STR, args),
+ DEF(STR, default_fields),
+ DEF(STR, override_fields),
+ DEF(STR, mechanisms),
+ DEF(STR, username_filter),
+
+ DEF(ENUM, skip),
+ DEF(ENUM, result_success),
+ DEF(ENUM, result_failure),
+ DEF(ENUM, result_internalfail),
+
+ DEF(BOOL, deny),
+ DEF(BOOL, pass),
+ DEF(BOOL, master),
+ DEF(ENUM, auth_verbose),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct auth_passdb_settings auth_passdb_default_settings = {
+ .name = "",
+ .driver = "",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+ .mechanisms = "",
+ .username_filter = "",
+
+ .skip = "never:authenticated:unauthenticated",
+ .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail",
+ .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+ .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+
+ .deny = FALSE,
+ .pass = FALSE,
+ .master = FALSE,
+ .auth_verbose = "default:yes:no"
+};
+const struct setting_parser_info auth_passdb_setting_parser_info = {
+ .defines = auth_passdb_setting_defines,
+ .defaults = &auth_passdb_default_settings,
+
+ .type_offset = offsetof(struct auth_passdb_settings, name),
+ .struct_size = sizeof(struct auth_passdb_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &auth_setting_parser_info,
+
+ .check_func = auth_passdb_settings_check
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_userdb_settings)
+static const struct setting_define auth_userdb_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, driver),
+ DEF(STR, args),
+ DEF(STR, default_fields),
+ DEF(STR, override_fields),
+
+ DEF(ENUM, skip),
+ DEF(ENUM, result_success),
+ DEF(ENUM, result_failure),
+ DEF(ENUM, result_internalfail),
+
+ DEF(ENUM, auth_verbose),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct auth_userdb_settings auth_userdb_default_settings = {
+ /* NOTE: when adding fields, update also auth.c:userdb_dummy_set */
+ .name = "",
+ .driver = "",
+ .args = "",
+ .default_fields = "",
+ .override_fields = "",
+
+ .skip = "never:found:notfound",
+ .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail",
+ .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+ .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail",
+
+ .auth_verbose = "default:yes:no"
+};
+const struct setting_parser_info auth_userdb_setting_parser_info = {
+ .defines = auth_userdb_setting_defines,
+ .defaults = &auth_userdb_default_settings,
+
+ .type_offset = offsetof(struct auth_userdb_settings, name),
+ .struct_size = sizeof(struct auth_userdb_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &auth_setting_parser_info,
+
+ .check_func = auth_userdb_settings_check
+};
+#undef DEF
+#undef DEF_NOPREFIX
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type("auth_"#name, name, struct auth_settings)
+#define DEF_NOPREFIX(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct auth_settings, field), \
+ .list_info = defines }
+static const struct setting_define auth_setting_defines[] = {
+ DEF(STR, mechanisms),
+ DEF(STR, realms),
+ DEF(STR, default_realm),
+ DEF(SIZE, cache_size),
+ DEF(TIME, cache_ttl),
+ DEF(TIME, cache_negative_ttl),
+ DEF(BOOL, cache_verify_password_with_worker),
+ DEF(STR, username_chars),
+ DEF(STR, username_translation),
+ DEF(STR, username_format),
+ DEF(STR, master_user_separator),
+ DEF(STR, anonymous_username),
+ DEF(STR, krb5_keytab),
+ DEF(STR, gssapi_hostname),
+ DEF(STR, winbind_helper_path),
+ DEF(STR, proxy_self),
+ DEF(TIME, failure_delay),
+
+ DEF(STR, policy_server_url),
+ DEF(STR, policy_server_api_header),
+ DEF(UINT, policy_server_timeout_msecs),
+ DEF(STR, policy_hash_mech),
+ DEF(STR, policy_hash_nonce),
+ DEF(STR, policy_request_attributes),
+ DEF(BOOL, policy_reject_on_fail),
+ DEF(BOOL, policy_check_before_auth),
+ DEF(BOOL, policy_check_after_auth),
+ DEF(BOOL, policy_report_after_auth),
+ DEF(BOOL, policy_log_only),
+ DEF(UINT, policy_hash_truncate),
+
+ DEF(BOOL, stats),
+ DEF(BOOL, verbose),
+ DEF(BOOL, debug),
+ DEF(BOOL, debug_passwords),
+ DEF(STR, verbose_passwords),
+ DEF(BOOL, ssl_require_client_cert),
+ DEF(BOOL, ssl_username_from_cert),
+ DEF(BOOL, use_winbind),
+
+ DEF(UINT, worker_max_count),
+
+ DEFLIST(passdbs, "passdb", &auth_passdb_setting_parser_info),
+ DEFLIST(userdbs, "userdb", &auth_userdb_setting_parser_info),
+
+ DEF_NOPREFIX(STR, base_dir),
+ DEF_NOPREFIX(BOOL, verbose_proctitle),
+ DEF_NOPREFIX(UINT, first_valid_uid),
+ DEF_NOPREFIX(UINT, last_valid_uid),
+ DEF_NOPREFIX(UINT, first_valid_gid),
+ DEF_NOPREFIX(UINT, last_valid_gid),
+
+ DEF_NOPREFIX(STR, ssl_client_ca_dir),
+ DEF_NOPREFIX(STR, ssl_client_ca_file),
+
+ SETTING_DEFINE_LIST_END
+};
+static const struct auth_settings auth_default_settings = {
+ .mechanisms = "plain",
+ .realms = "",
+ .default_realm = "",
+ .cache_size = 0,
+ .cache_ttl = 60*60,
+ .cache_negative_ttl = 60*60,
+ .cache_verify_password_with_worker = FALSE,
+ .username_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@",
+ .username_translation = "",
+ .username_format = "%Lu",
+ .master_user_separator = "",
+ .anonymous_username = "anonymous",
+ .krb5_keytab = "",
+ .gssapi_hostname = "",
+ .winbind_helper_path = "/usr/bin/ntlm_auth",
+ .proxy_self = "",
+ .failure_delay = 2,
+
+ .policy_server_url = "",
+ .policy_server_api_header = "",
+ .policy_server_timeout_msecs = 2000,
+ .policy_hash_mech = "sha256",
+ .policy_hash_nonce = "",
+ .policy_request_attributes = "login=%{requested_username} pwhash=%{hashed_password} remote=%{rip} device_id=%{client_id} protocol=%s session_id=%{session}",
+ .policy_reject_on_fail = FALSE,
+ .policy_check_before_auth = TRUE,
+ .policy_check_after_auth = TRUE,
+ .policy_report_after_auth = TRUE,
+ .policy_log_only = FALSE,
+ .policy_hash_truncate = 12,
+
+ .stats = FALSE,
+ .verbose = FALSE,
+ .debug = FALSE,
+ .debug_passwords = FALSE,
+ .verbose_passwords = "no",
+ .ssl_require_client_cert = FALSE,
+ .ssl_username_from_cert = FALSE,
+ .ssl_client_ca_dir = "",
+ .ssl_client_ca_file = "",
+
+ .use_winbind = FALSE,
+
+ .worker_max_count = 30,
+
+ .passdbs = ARRAY_INIT,
+ .userdbs = ARRAY_INIT,
+
+ .base_dir = PKG_RUNDIR,
+ .verbose_proctitle = FALSE,
+ .first_valid_uid = 500,
+ .last_valid_uid = 0,
+ .first_valid_gid = 1,
+ .last_valid_gid = 0,
+};
+const struct setting_parser_info auth_setting_parser_info = {
+ .module_name = "auth",
+ .defines = auth_setting_defines,
+ .defaults = &auth_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct auth_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = auth_settings_check
+};
+/* ../../src/anvil/anvil-settings.c */
+/* <settings checks> */
+static struct file_listener_settings anvil_unix_listeners_array[] = {
+ { "anvil", 0600, "", "" },
+ { "anvil-auth-penalty", 0600, "", "" }
+};
+static struct file_listener_settings *anvil_unix_listeners[] = {
+ &anvil_unix_listeners_array[0],
+ &anvil_unix_listeners_array[1]
+};
+static buffer_t anvil_unix_listeners_buf = {
+ { { anvil_unix_listeners, sizeof(anvil_unix_listeners) } }
+};
+/* </settings checks> */
+struct service_settings anvil_service_settings = {
+ .name = "anvil",
+ .protocol = "",
+ .type = "anvil",
+ .executable = "anvil",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 1,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &anvil_unix_listeners_buf,
+ sizeof(anvil_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+static struct service_settings *config_all_services[] = {
+#ifdef HAVE_LIBWRAP
+ &tcpwrap_service_settings,
+#endif
+ &health_check_service_settings,
+ &submission_service_settings,
+ &submission_login_service_settings,
+ &stats_service_settings,
+ &replicator_service_settings,
+ &aggregator_service_settings,
+ &pop3_service_settings,
+ &pop3_login_service_settings,
+ &old_stats_service_settings,
+ &log_service_settings,
+ &lmtp_service_settings,
+ &ipc_service_settings,
+ &indexer_worker_service_settings,
+ &indexer_service_settings,
+ &imap_service_settings,
+ &imap_urlauth_worker_service_settings,
+ &imap_urlauth_service_settings,
+ &imap_urlauth_login_service_settings,
+ &imap_login_service_settings,
+ &imap_hibernate_service_settings,
+ &doveadm_service_settings,
+ &dns_client_service_settings,
+ &director_service_settings,
+ &dict_service_settings,
+ &dict_async_service_settings,
+ &config_service_settings,
+ &auth_service_settings,
+ &auth_worker_service_settings,
+ &anvil_service_settings,
+};
+buffer_t config_all_services_buf = {
+ { { config_all_services, sizeof(config_all_services) } }
+};
+const struct setting_parser_info *all_default_roots[] = {
+ &master_service_setting_parser_info,
+ &master_service_ssl_setting_parser_info,
+ &master_service_ssl_server_setting_parser_info,
+ &smtp_submit_setting_parser_info,
+ &aggregator_setting_parser_info,
+ &auth_setting_parser_info,
+ &dict_setting_parser_info,
+ &director_setting_parser_info,
+ &doveadm_setting_parser_info,
+ &fs_crypt_setting_parser_info,
+ &imap_login_setting_parser_info,
+ &imap_setting_parser_info,
+ &imap_urlauth_login_setting_parser_info,
+ &imap_urlauth_setting_parser_info,
+ &imap_urlauth_worker_setting_parser_info,
+ &imapc_setting_parser_info,
+ &lda_setting_parser_info,
+ &lmtp_setting_parser_info,
+ &login_setting_parser_info,
+ &mail_storage_setting_parser_info,
+ &mail_user_setting_parser_info,
+ &maildir_setting_parser_info,
+ &master_setting_parser_info,
+ &mbox_setting_parser_info,
+ &mdbox_setting_parser_info,
+ &old_stats_setting_parser_info,
+ &pop3_login_setting_parser_info,
+ &pop3_setting_parser_info,
+ &pop3c_setting_parser_info,
+ &quota_status_setting_parser_info,
+ &replicator_setting_parser_info,
+ &stats_setting_parser_info,
+ &submission_login_setting_parser_info,
+ &submission_setting_parser_info,
+ NULL
+};
+const struct setting_parser_info *const *all_roots = all_default_roots;
+ARRAY_TYPE(service_settings) *default_services = &master_default_settings.services;
diff --git a/src/config/all-settings.h b/src/config/all-settings.h
new file mode 100644
index 0000000..96ac7bd
--- /dev/null
+++ b/src/config/all-settings.h
@@ -0,0 +1,8 @@
+#ifndef ALL_SETTINGS_H
+#define ALL_SETTINGS_H
+
+extern const struct setting_parser_info *const *all_roots;
+extern const struct setting_parser_info *all_default_roots[];
+extern ARRAY_TYPE(service_settings) *default_services;
+
+#endif
diff --git a/src/config/config-connection.c b/src/config/config-connection.c
new file mode 100644
index 0000000..bd3db86
--- /dev/null
+++ b/src/config/config-connection.c
@@ -0,0 +1,267 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "config-request.h"
+#include "config-parser.h"
+#include "config-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE 1024
+
+#define CONFIG_CLIENT_PROTOCOL_MAJOR_VERSION 2
+#define CONFIG_CLIENT_PROTOCOL_MINOR_VERSION 0
+
+struct config_connection {
+ struct config_connection *prev, *next;
+
+ int fd;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+
+ bool version_received:1;
+ bool handshaked:1;
+};
+
+static struct config_connection *config_connections = NULL;
+
+static const char *const *
+config_connection_next_line(struct config_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return NULL;
+
+ return t_strsplit_tabescaped(line);
+}
+
+static void
+config_request_output(const char *key, const char *value,
+ enum config_key_type type ATTR_UNUSED, void *context)
+{
+ struct ostream *output = context;
+ const char *p;
+
+ o_stream_nsend_str(output, key);
+ o_stream_nsend_str(output, "=");
+ while ((p = strchr(value, '\n')) != NULL) {
+ o_stream_nsend(output, value, p-value);
+ o_stream_nsend(output, SETTING_STREAM_LF_CHAR, 1);
+ value = p+1;
+ }
+ o_stream_nsend_str(output, value);
+ o_stream_nsend_str(output, "\n");
+}
+
+static int config_connection_request(struct config_connection *conn,
+ const char *const *args)
+{
+ struct config_export_context *ctx;
+ struct master_service_settings_output output;
+ struct config_filter filter;
+ const char *path, *error, *module, *const *wanted_modules;
+ ARRAY(const char *) modules;
+ ARRAY(const char *) exclude_settings;
+ bool is_master = FALSE;
+
+ /* [<args>] */
+ t_array_init(&modules, 4);
+ t_array_init(&exclude_settings, 4);
+ i_zero(&filter);
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "service="))
+ filter.service = *args + 8;
+ else if (str_begins(*args, "module=")) {
+ module = *args + 7;
+ if (strcmp(module, "master") == 0)
+ is_master = TRUE;
+ array_push_back(&modules, &module);
+ } else if (str_begins(*args, "exclude=")) {
+ const char *value = *args + 8;
+ array_push_back(&exclude_settings, &value);
+ } else if (str_begins(*args, "lname="))
+ filter.local_name = *args + 6;
+ else if (str_begins(*args, "lip=")) {
+ if (net_addr2ip(*args + 4, &filter.local_net) == 0) {
+ filter.local_bits =
+ IPADDR_IS_V4(&filter.local_net) ?
+ 32 : 128;
+ }
+ } else if (str_begins(*args, "rip=")) {
+ if (net_addr2ip(*args + 4, &filter.remote_net) == 0) {
+ filter.remote_bits =
+ IPADDR_IS_V4(&filter.remote_net) ?
+ 32 : 128;
+ }
+ }
+ }
+ array_append_zero(&modules);
+ wanted_modules = array_count(&modules) == 1 ? NULL :
+ array_front(&modules);
+ array_append_zero(&exclude_settings);
+
+ if (is_master) {
+ /* master reads configuration only when reloading settings */
+ path = master_service_get_config_path(master_service);
+ if (config_parse_file(path, TRUE, NULL, &error) <= 0) {
+ o_stream_nsend_str(conn->output,
+ t_strconcat("\nERROR ", error, "\n", NULL));
+ config_connection_destroy(conn);
+ return -1;
+ }
+ }
+
+ o_stream_cork(conn->output);
+
+ ctx = config_export_init(wanted_modules,
+ array_count(&exclude_settings) == 1 ? NULL :
+ array_front(&exclude_settings),
+ CONFIG_DUMP_SCOPE_SET, 0,
+ config_request_output, conn->output);
+ config_export_by_filter(ctx, &filter);
+ config_export_get_output(ctx, &output);
+
+ if (output.specific_services != NULL) {
+ const char *const *s;
+
+ for (s = output.specific_services; *s != NULL; s++) {
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("service=%s\t", *s));
+ }
+ }
+ if (output.service_uses_local)
+ o_stream_nsend_str(conn->output, "service-uses-local\t");
+ if (output.service_uses_remote)
+ o_stream_nsend_str(conn->output, "service-uses-remote\t");
+ if (output.used_local)
+ o_stream_nsend_str(conn->output, "used-local\t");
+ if (output.used_remote)
+ o_stream_nsend_str(conn->output, "used-remote\t");
+ o_stream_nsend_str(conn->output, "\n");
+
+ if (config_export_finish(&ctx) < 0) {
+ config_connection_destroy(conn);
+ return -1;
+ }
+ o_stream_nsend_str(conn->output, "\n");
+ o_stream_uncork(conn->output);
+ return 0;
+}
+
+static int config_filters_request(struct config_connection *conn)
+{
+ struct config_filter_parser *const *filters = config_filter_get_all(config_filter);
+ o_stream_cork(conn->output);
+ while(*filters != NULL) {
+ const struct config_filter *filter = &(*filters)->filter;
+ o_stream_nsend_str(conn->output, "FILTER");
+ if (filter->service != NULL)
+ o_stream_nsend_str(conn->output, t_strdup_printf("\tservice=%s",
+ str_tabescape(filter->service)));
+ if (filter->local_name != NULL)
+ o_stream_nsend_str(conn->output, t_strdup_printf("\tlocal-name=%s",
+ str_tabescape(filter->local_name)));
+ if (filter->local_bits > 0)
+ o_stream_nsend_str(conn->output, t_strdup_printf("\tlocal-net=%s/%u",
+ net_ip2addr(&filter->local_net),
+ filter->local_bits));
+ if (filter->remote_bits > 0)
+ o_stream_nsend_str(conn->output, t_strdup_printf("\tremote-net=%s/%u",
+ net_ip2addr(&filter->remote_net),
+ filter->remote_bits));
+ o_stream_nsend_str(conn->output, "\n");
+ filters++;
+ }
+ o_stream_nsend_str(conn->output, "\n");
+ o_stream_uncork(conn->output);
+ return 0;
+}
+
+
+static void config_connection_input(struct config_connection *conn)
+{
+ const char *const *args, *line;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ i_error("BUG: Config client connection sent too much data");
+ config_connection_destroy(conn);
+ return;
+ case -1:
+ config_connection_destroy(conn);
+ return;
+ }
+
+ if (!conn->version_received) {
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return;
+
+ if (!version_string_verify(line, "config",
+ CONFIG_CLIENT_PROTOCOL_MAJOR_VERSION)) {
+ i_error("Config client not compatible with this server "
+ "(mixed old and new binaries?)");
+ config_connection_destroy(conn);
+ return;
+ }
+ conn->version_received = TRUE;
+ }
+
+ while ((args = config_connection_next_line(conn)) != NULL) {
+ if (args[0] == NULL)
+ continue;
+ if (strcmp(args[0], "REQ") == 0) {
+ if (config_connection_request(conn, args + 1) < 0)
+ break;
+ }
+ if (strcmp(args[0], "FILTERS") == 0) {
+ if (config_filters_request(conn) < 0)
+ break;
+ }
+ }
+}
+
+struct config_connection *config_connection_create(int fd)
+{
+ struct config_connection *conn;
+
+ conn = i_new(struct config_connection, 1);
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ conn->io = io_add(fd, IO_READ, config_connection_input, conn);
+ DLLIST_PREPEND(&config_connections, conn);
+ return conn;
+}
+
+void config_connection_destroy(struct config_connection *conn)
+{
+ DLLIST_REMOVE(&config_connections, conn);
+
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(config conn) failed: %m");
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void config_connections_destroy_all(void)
+{
+ while (config_connections != NULL)
+ config_connection_destroy(config_connections);
+}
diff --git a/src/config/config-connection.h b/src/config/config-connection.h
new file mode 100644
index 0000000..7983fff
--- /dev/null
+++ b/src/config/config-connection.h
@@ -0,0 +1,9 @@
+#ifndef CONFIG_CONNECTION_H
+#define CONFIG_CONNECTION_H
+
+struct config_connection *config_connection_create(int fd);
+void config_connection_destroy(struct config_connection *conn);
+
+void config_connections_destroy_all(void);
+
+#endif
diff --git a/src/config/config-filter.c b/src/config/config-filter.c
new file mode 100644
index 0000000..336380d
--- /dev/null
+++ b/src/config/config-filter.c
@@ -0,0 +1,401 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "settings-parser.h"
+#include "master-service-settings.h"
+#include "config-parser.h"
+#include "config-filter.h"
+#include "dns-util.h"
+
+struct config_filter_context {
+ pool_t pool;
+ struct config_filter_parser *const *parsers;
+};
+
+static bool config_filter_match_service(const struct config_filter *mask,
+ const struct config_filter *filter)
+{
+ if (mask->service != NULL) {
+ if (filter->service == NULL)
+ return FALSE;
+ if (mask->service[0] == '!') {
+ /* not service */
+ if (strcmp(filter->service, mask->service + 1) == 0)
+ return FALSE;
+ } else {
+ if (strcmp(filter->service, mask->service) != 0)
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+config_filter_match_local_name(const struct config_filter *mask,
+ 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, *local_name = mask->local_name;
+ while((ptr = strchr(local_name, ' ')) != NULL) {
+ if (dns_match_wildcard(filter_local_name,
+ t_strdup_until(local_name, ptr)) == 0)
+ return TRUE;
+ local_name = ptr+1;
+ }
+ return dns_match_wildcard(filter_local_name, local_name) == 0;
+}
+
+static bool config_filter_match_rest(const struct config_filter *mask,
+ const struct config_filter *filter)
+{
+ bool matched;
+
+ if (mask->local_name != NULL) {
+ if (filter->local_name == NULL)
+ return FALSE;
+ T_BEGIN {
+ matched = config_filter_match_local_name(mask, filter->local_name);
+ } T_END;
+ if (!matched)
+ return FALSE;
+ }
+ /* FIXME: it's not comparing full masks */
+ if (mask->remote_bits != 0) {
+ if (filter->remote_bits == 0)
+ return FALSE;
+ if (!net_is_in_network(&filter->remote_net, &mask->remote_net,
+ mask->remote_bits))
+ return FALSE;
+ }
+ if (mask->local_bits != 0) {
+ if (filter->local_bits == 0)
+ return FALSE;
+ if (!net_is_in_network(&filter->local_net, &mask->local_net,
+ mask->local_bits))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool config_filter_match(const struct config_filter *mask,
+ const struct config_filter *filter)
+{
+ if (!config_filter_match_service(mask, filter))
+ return FALSE;
+
+ return config_filter_match_rest(mask, filter);
+}
+
+bool config_filters_equal(const struct config_filter *f1,
+ const struct config_filter *f2)
+{
+ if (null_strcmp(f1->service, f2->service) != 0)
+ return FALSE;
+
+ if (f1->remote_bits != f2->remote_bits)
+ return FALSE;
+ if (!net_ip_compare(&f1->remote_net, &f2->remote_net))
+ return FALSE;
+
+ if (f1->local_bits != f2->local_bits)
+ return FALSE;
+ if (!net_ip_compare(&f1->local_net, &f2->local_net))
+ return FALSE;
+
+ if (null_strcasecmp(f1->local_name, f2->local_name) != 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+struct config_filter_context *config_filter_init(pool_t pool)
+{
+ struct config_filter_context *ctx;
+
+ ctx = p_new(pool, struct config_filter_context, 1);
+ ctx->pool = pool;
+ return ctx;
+}
+
+void config_filter_deinit(struct config_filter_context **_ctx)
+{
+ struct config_filter_context *ctx = *_ctx;
+ unsigned int i;
+
+ *_ctx = NULL;
+
+ for (i = 0; ctx->parsers[i] != NULL; i++)
+ config_filter_parsers_free(ctx->parsers[i]->parsers);
+ pool_unref(&ctx->pool);
+}
+
+void config_filter_add_all(struct config_filter_context *ctx,
+ struct config_filter_parser *const *parsers)
+{
+ ctx->parsers = parsers;
+}
+
+static int
+config_filter_parser_cmp(struct config_filter_parser *const *p1,
+ struct config_filter_parser *const *p2)
+{
+ const struct config_filter *f1 = &(*p1)->filter, *f2 = &(*p2)->filter;
+
+ /* remote and locals are first, although it doesn't really
+ matter which one comes first */
+ if (f1->local_name != NULL && f2->local_name == NULL)
+ return -1;
+ if (f1->local_name == NULL && f2->local_name != NULL)
+ return 1;
+
+ if (f1->local_bits > f2->local_bits)
+ return -1;
+ if (f1->local_bits < f2->local_bits)
+ return 1;
+
+ if (f1->remote_bits > f2->remote_bits)
+ return -1;
+ if (f1->remote_bits < f2->remote_bits)
+ return 1;
+
+ if (f1->service != NULL && f2->service == NULL)
+ return -1;
+ if (f1->service == NULL && f2->service != NULL)
+ return 1;
+ return 0;
+}
+
+static int
+config_filter_parser_cmp_rev(struct config_filter_parser *const *p1,
+ struct config_filter_parser *const *p2)
+{
+ return -config_filter_parser_cmp(p1, p2);
+}
+
+static bool str_array_contains(ARRAY_TYPE(const_string) *arr, const char *str)
+{
+ const char *p;
+
+ array_foreach_elem(arr, p) {
+ if (strcmp(p, str) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool have_changed_settings(const struct config_filter_parser *parser,
+ const char *const *modules)
+{
+ const unsigned char *changes;
+ unsigned int i, j, size;
+
+ for (i = 0; parser->parsers[i].root != NULL; i++) {
+ if (!config_module_want_parser(config_module_parsers,
+ modules, parser->parsers[i].root))
+ continue;
+
+ changes = settings_parser_get_changes(parser->parsers[i].parser);
+ size = parser->parsers[i].root->struct_size;
+ for (j = 0; j < size; j++) {
+ if (changes[j] != 0)
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static struct config_filter_parser *const *
+config_filter_find_all(struct config_filter_context *ctx, pool_t pool,
+ const char *const *modules,
+ const struct config_filter *filter,
+ struct master_service_settings_output *output_r)
+{
+ ARRAY_TYPE(config_filter_parsers) matches;
+ ARRAY_TYPE(const_string) service_names;
+ unsigned int i;
+
+ i_zero(output_r);
+
+ p_array_init(&matches, pool, 8);
+ p_array_init(&service_names, pool, 8);
+ for (i = 0; ctx->parsers[i] != NULL; i++) {
+ const struct config_filter *mask = &ctx->parsers[i]->filter;
+
+ if (!config_filter_match_service(mask, filter)) {
+ if (!str_array_contains(&service_names, mask->service) &&
+ have_changed_settings(ctx->parsers[i], modules))
+ array_push_back(&service_names,
+ &mask->service);
+ continue;
+ }
+
+ if (mask->local_bits > 0 || mask->local_name != NULL)
+ output_r->service_uses_local = TRUE;
+ if (mask->remote_bits > 0)
+ output_r->service_uses_remote = TRUE;
+ if (config_filter_match_rest(mask, filter)) {
+ if (mask->local_bits > 0 || mask->local_name != NULL)
+ output_r->used_local = TRUE;
+ if (mask->remote_bits > 0)
+ output_r->used_remote = TRUE;
+ array_push_back(&matches, &ctx->parsers[i]);
+ }
+ }
+ if (filter->service == NULL) {
+ array_append_zero(&service_names);
+ output_r->specific_services = array_front(&service_names);
+ }
+
+ array_sort(&matches, config_filter_parser_cmp);
+ array_append_zero(&matches);
+ return array_front(&matches);
+}
+
+struct config_filter_parser *const *
+config_filter_get_all(struct config_filter_context *ctx)
+{
+ ARRAY_TYPE(config_filter_parsers) filters;
+ unsigned int i;
+
+ t_array_init(&filters, 8);
+ for (i = 0; ctx->parsers[i] != NULL; i++) {
+ array_push_back(&filters, &ctx->parsers[i]);
+ }
+ array_sort(&filters, config_filter_parser_cmp_rev);
+ array_append_zero(&filters);
+ return array_front(&filters);
+}
+
+struct config_filter_parser *const *
+config_filter_find_subset(struct config_filter_context *ctx,
+ const struct config_filter *filter)
+{
+ ARRAY_TYPE(config_filter_parsers) matches;
+ struct config_filter tmp_mask;
+ unsigned int i;
+
+ t_array_init(&matches, 8);
+ for (i = 0; ctx->parsers[i] != NULL; i++) {
+ const struct config_filter *mask = &ctx->parsers[i]->filter;
+
+ if (filter->service != NULL) {
+ if (!config_filter_match_service(mask, filter))
+ continue;
+ }
+
+ tmp_mask = *mask;
+ if (filter->local_name == NULL)
+ tmp_mask.local_name = NULL;
+ if (filter->local_bits == 0)
+ tmp_mask.local_bits = 0;
+ if (filter->remote_bits == 0)
+ tmp_mask.remote_bits = 0;
+
+ if (config_filter_match_rest(&tmp_mask, filter))
+ array_push_back(&matches, &ctx->parsers[i]);
+ }
+ array_sort(&matches, config_filter_parser_cmp_rev);
+ array_append_zero(&matches);
+ return array_front(&matches);
+}
+
+static bool
+config_filter_is_superset(const struct config_filter *sup,
+ const struct config_filter *filter)
+{
+ /* assume that both of the filters match the same subset, so we don't
+ need to compare IPs and service name. */
+ if (sup->local_bits > filter->local_bits)
+ return FALSE;
+ if (sup->remote_bits > filter->remote_bits)
+ return FALSE;
+ if (sup->local_name != NULL && filter->local_name == NULL) {
+ i_warning("%s", sup->local_name);
+ return FALSE;
+ }
+ if (sup->service != NULL && filter->service == NULL)
+ return FALSE;
+ return TRUE;
+}
+
+static int
+config_module_parser_apply_changes(struct config_module_parser *dest,
+ const struct config_filter_parser *src,
+ pool_t pool, const char **error_r)
+{
+ const char *conflict_key;
+ unsigned int i;
+
+ for (i = 0; dest[i].root != NULL; i++) {
+ if (settings_parser_apply_changes(dest[i].parser,
+ src->parsers[i].parser, pool,
+ error_r == NULL ? NULL :
+ &conflict_key) < 0) {
+ i_assert(error_r != NULL);
+ *error_r = t_strdup_printf("Conflict in setting %s "
+ "found from filter at %s", conflict_key,
+ src->file_and_line);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int config_filter_parsers_get(struct config_filter_context *ctx, pool_t pool,
+ const char *const *modules,
+ const struct config_filter *filter,
+ struct config_module_parser **parsers_r,
+ struct master_service_settings_output *output_r,
+ const char **error_r)
+{
+ struct config_filter_parser *const *src;
+ struct config_module_parser *dest;
+ const char *error = NULL, **error_p;
+ unsigned int i, count;
+
+ /* get the matching filters. the most specific ones are handled first,
+ so that if more generic filters try to override settings we'll fail
+ with an error. Merging SET_STRLIST types requires
+ settings_parser_apply_changes() to work a bit unintuitively by
+ letting the destination settings override the source settings. */
+ src = config_filter_find_all(ctx, pool, modules, filter, output_r);
+
+ /* all of them should have the same number of parsers.
+ duplicate our initial parsers from the first match */
+ for (count = 0; src[0]->parsers[count].root != NULL; count++) ;
+ dest = p_new(pool, struct config_module_parser, count + 1);
+ for (i = 0; i < count; i++) {
+ dest[i] = src[0]->parsers[i];
+ dest[i].parser =
+ settings_parser_dup(src[0]->parsers[i].parser, pool);
+ }
+
+ /* apply the changes from rest of the matches */
+ for (i = 1; src[i] != NULL; i++) {
+ if (config_filter_is_superset(&src[i]->filter,
+ &src[i-1]->filter))
+ error_p = NULL;
+ else
+ error_p = &error;
+
+ if (config_module_parser_apply_changes(dest, src[i], pool,
+ error_p) < 0) {
+ i_assert(error != NULL);
+ config_filter_parsers_free(dest);
+ *error_r = error;
+ return -1;
+ }
+ }
+ *parsers_r = dest;
+ return 0;
+}
+
+void config_filter_parsers_free(struct config_module_parser *parsers)
+{
+ unsigned int i;
+
+ for (i = 0; parsers[i].root != NULL; i++)
+ settings_parser_deinit(&parsers[i].parser);
+}
diff --git a/src/config/config-filter.h b/src/config/config-filter.h
new file mode 100644
index 0000000..fda3182
--- /dev/null
+++ b/src/config/config-filter.h
@@ -0,0 +1,58 @@
+#ifndef CONFIG_FILTER_H
+#define CONFIG_FILTER_H
+
+#include "net.h"
+
+struct master_service_settings_output;
+
+struct config_filter {
+ const char *service;
+ /* local_name is for TLS SNI requests.
+ both local_name and local_bits can't be set at the same time. */
+ const char *local_name;
+ /* the hosts are used only in doveconf output */
+ const char *local_host, *remote_host;
+ struct ip_addr local_net, remote_net;
+ unsigned int local_bits, remote_bits;
+};
+
+struct config_filter_parser {
+ struct config_filter filter;
+ const char *file_and_line;
+ /* NULL-terminated array of parsers */
+ struct config_module_parser *parsers;
+};
+ARRAY_DEFINE_TYPE(config_filter_parsers, struct config_filter_parser *);
+
+struct config_filter_context *config_filter_init(pool_t pool);
+void config_filter_deinit(struct config_filter_context **ctx);
+
+/* Replace filter's parsers with given parser list. */
+void config_filter_add_all(struct config_filter_context *ctx,
+ struct config_filter_parser *const *parsers);
+
+/* Build new parsers from all existing ones matching the given filter. */
+int config_filter_parsers_get(struct config_filter_context *ctx, pool_t pool,
+ const char *const *modules,
+ const struct config_filter *filter,
+ struct config_module_parser **parsers_r,
+ struct master_service_settings_output *output_r,
+ const char **error_r) ATTR_NULL(3);
+void config_filter_parsers_free(struct config_module_parser *parsers);
+
+/* Return a list of filters that are a subset of the given filter. */
+struct config_filter_parser *const *
+config_filter_find_subset(struct config_filter_context *ctx,
+ const struct config_filter *filter);
+
+struct config_filter_parser *const *
+config_filter_get_all(struct config_filter_context *ctx);
+
+/* Returns TRUE if filter matches mask. */
+bool config_filter_match(const struct config_filter *mask,
+ const struct config_filter *filter);
+/* Returns TRUE if two filters are fully equal. */
+bool config_filters_equal(const struct config_filter *f1,
+ const struct config_filter *f2);
+
+#endif
diff --git a/src/config/config-parser-private.h b/src/config/config-parser-private.h
new file mode 100644
index 0000000..9fe98ce
--- /dev/null
+++ b/src/config/config-parser-private.h
@@ -0,0 +1,75 @@
+#ifndef CONFIG_PARSER_PRIVATE_H
+#define CONFIG_PARSER_PRIVATE_H
+
+#include "config-parser.h"
+#include "config-filter.h"
+
+enum config_line_type {
+ CONFIG_LINE_TYPE_SKIP,
+ CONFIG_LINE_TYPE_CONTINUE,
+ CONFIG_LINE_TYPE_ERROR,
+ CONFIG_LINE_TYPE_KEYVALUE,
+ CONFIG_LINE_TYPE_KEYFILE,
+ CONFIG_LINE_TYPE_KEYVARIABLE,
+ CONFIG_LINE_TYPE_SECTION_BEGIN,
+ CONFIG_LINE_TYPE_SECTION_END,
+ CONFIG_LINE_TYPE_INCLUDE,
+ CONFIG_LINE_TYPE_INCLUDE_TRY
+};
+
+struct config_section_stack {
+ struct config_section_stack *prev;
+ const char *key;
+
+ struct config_filter filter;
+ /* root=NULL-terminated list of parsers */
+ struct config_module_parser *parsers;
+ size_t pathlen;
+
+ const char *open_path;
+ unsigned int open_linenum;
+ bool is_filter;
+};
+
+struct input_stack {
+ struct input_stack *prev;
+
+ struct istream *input;
+ const char *path;
+ unsigned int linenum;
+};
+
+struct config_parser_context {
+ pool_t pool;
+ const char *path;
+ const char *const *modules;
+
+ ARRAY(struct config_filter_parser *) all_parsers;
+ struct config_module_parser *root_parsers;
+ struct config_section_stack *cur_section;
+ struct input_stack *cur_input;
+
+ string_t *str;
+ size_t pathlen;
+ unsigned int section_counter;
+ const char *error;
+
+ struct old_set_parser *old;
+
+ HASH_TABLE(const char *, const char *) seen_settings;
+ struct config_filter_context *filter;
+ bool expand_values:1;
+ bool hide_errors:1;
+};
+
+extern void (*hook_config_parser_begin)(struct config_parser_context *ctx);
+extern int (*hook_config_parser_end)(struct config_parser_context *ctx,
+ const char **error_r);
+
+int config_apply_line(struct config_parser_context *ctx, const char *key,
+ const char *line, const char *section_name) ATTR_NULL(4);
+void config_parser_apply_line(struct config_parser_context *ctx,
+ enum config_line_type type,
+ const char *key, const char *value);
+
+#endif
diff --git a/src/config/config-parser.c b/src/config/config-parser.c
new file mode 100644
index 0000000..9d36796
--- /dev/null
+++ b/src/config/config-parser.c
@@ -0,0 +1,1212 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "strescape.h"
+#include "istream.h"
+#include "module-dir.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "all-settings.h"
+#include "old-set-parser.h"
+#include "config-request.h"
+#include "config-parser-private.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#ifdef HAVE_GLOB_H
+# include <glob.h>
+#endif
+
+#ifndef GLOB_BRACE
+# define GLOB_BRACE 0
+#endif
+
+#define DNS_LOOKUP_TIMEOUT_SECS 30
+#define DNS_LOOKUP_WARN_SECS 5
+
+ARRAY_DEFINE_TYPE(setting_parser_info_p, const struct setting_parser_info *);
+
+static const enum settings_parser_flags settings_parser_flags =
+ SETTINGS_PARSER_FLAG_IGNORE_UNKNOWN_KEYS |
+ SETTINGS_PARSER_FLAG_TRACK_CHANGES;
+
+struct config_module_parser *config_module_parsers;
+struct config_filter_context *config_filter;
+struct module *modules;
+void (*hook_config_parser_begin)(struct config_parser_context *ctx);
+int (*hook_config_parser_end)(struct config_parser_context *ctx,
+ const char **error_r);
+
+static ARRAY_TYPE(service_settings) services_free_at_deinit = ARRAY_INIT;
+static ARRAY_TYPE(setting_parser_info_p) roots_free_at_deinit = ARRAY_INIT;
+
+static const char *info_type_name_find(const struct setting_parser_info *info)
+{
+ unsigned int i;
+
+ for (i = 0; info->defines[i].key != NULL; i++) {
+ if (info->defines[i].offset == info->type_offset)
+ return info->defines[i].key;
+ }
+ i_panic("setting parser: Invalid type_offset value");
+ return NULL;
+}
+
+static int config_add_type(struct setting_parser_context *parser,
+ const char *line, const char *section_name)
+{
+ const struct setting_parser_info *info;
+ const char *p;
+ string_t *str;
+ int ret;
+
+ info = settings_parse_get_prev_info(parser);
+ if (info == NULL) {
+ /* section inside strlist */
+ return -1;
+ }
+ if (info->type_offset == SIZE_MAX)
+ return 0;
+
+ str = t_str_new(256);
+ p = strchr(line, '=');
+ str_append_data(str, line, p-line);
+ str_append_c(str, SETTINGS_SEPARATOR);
+ str_append(str, p+1);
+ if (info != NULL) {
+ str_append_c(str, SETTINGS_SEPARATOR);
+ str_append(str, info_type_name_find(info));
+ }
+
+ ret = settings_parse_keyvalue(parser, str_c(str), section_name);
+ i_assert(ret > 0);
+ return 0;
+}
+
+static bool
+config_parser_is_in_localremote(struct config_section_stack *section)
+{
+ const struct config_filter *filter = &section->filter;
+
+ return filter->local_name != NULL || filter->local_bits > 0 ||
+ filter->remote_bits > 0;
+}
+
+static void
+section_stack_write(string_t *str, struct config_section_stack *section)
+{
+ if (section == NULL)
+ return;
+
+ section_stack_write(str, section->prev);
+ if (!section->is_filter && section->key != NULL)
+ str_printfa(str, "%s { ", section->key);
+}
+
+static const char *
+get_setting_full_path(struct config_parser_context *ctx, const char *key)
+{
+ string_t *str = t_str_new(128);
+
+ section_stack_write(str, ctx->cur_section);
+ str_append(str, key);
+ return str_c(str);
+}
+
+int config_apply_line(struct config_parser_context *ctx, const char *key,
+ const char *line, const char *section_name)
+{
+ struct config_module_parser *l;
+ bool found = FALSE;
+ int ret;
+
+ for (l = ctx->cur_section->parsers; l->root != NULL; l++) {
+ ret = settings_parse_line(l->parser, line);
+ if (ret > 0) {
+ found = TRUE;
+ /* FIXME: remove once auth does support these. */
+ if (strcmp(l->root->module_name, "auth") == 0 &&
+ config_parser_is_in_localremote(ctx->cur_section)) {
+ ctx->error = p_strconcat(ctx->pool,
+ "Auth settings not supported inside local/remote blocks: ",
+ key, NULL);
+ return -1;
+ }
+ if (section_name != NULL) {
+ if (config_add_type(l->parser, line, section_name) < 0) {
+ ctx->error = "Section not allowed here";
+ return -1;
+ }
+ }
+ } else if (ret < 0) {
+ ctx->error = settings_parser_get_error(l->parser);
+ return -1;
+ }
+ }
+ if (!found) {
+ ctx->error = p_strconcat(ctx->pool, "Unknown setting: ",
+ get_setting_full_path(ctx, key), NULL);
+ return -1;
+ }
+ return 0;
+}
+
+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 struct config_module_parser *
+config_module_parsers_init(pool_t pool)
+{
+ struct config_module_parser *dest;
+ unsigned int i, count;
+
+ for (count = 0; all_roots[count] != NULL; count++) ;
+
+ dest = p_new(pool, struct config_module_parser, count + 1);
+ for (i = 0; i < count; i++) {
+ dest[i].root = all_roots[i];
+ dest[i].parser = settings_parser_init(pool, all_roots[i],
+ settings_parser_flags);
+ }
+ return dest;
+}
+
+static void
+config_add_new_parser(struct config_parser_context *ctx)
+{
+ struct config_section_stack *cur_section = ctx->cur_section;
+ struct config_filter_parser *parser;
+
+ parser = p_new(ctx->pool, struct config_filter_parser, 1);
+ parser->filter = cur_section->filter;
+ if (ctx->cur_input->linenum == 0) {
+ parser->file_and_line =
+ p_strdup(ctx->pool, ctx->cur_input->path);
+ } else {
+ parser->file_and_line =
+ p_strdup_printf(ctx->pool, "%s:%d",
+ ctx->cur_input->path,
+ ctx->cur_input->linenum);
+ }
+ parser->parsers = cur_section->prev == NULL ? ctx->root_parsers :
+ config_module_parsers_init(ctx->pool);
+ array_push_back(&ctx->all_parsers, &parser);
+
+ cur_section->parsers = parser->parsers;
+}
+
+static struct config_section_stack *
+config_add_new_section(struct config_parser_context *ctx)
+{
+ struct config_section_stack *section;
+
+ section = p_new(ctx->pool, struct config_section_stack, 1);
+ section->prev = ctx->cur_section;
+ section->filter = ctx->cur_section->filter;
+ section->parsers = ctx->cur_section->parsers;
+
+ section->open_path = p_strdup(ctx->pool, ctx->cur_input->path);
+ section->open_linenum = ctx->cur_input->linenum;
+ return section;
+}
+
+static struct config_filter_parser *
+config_filter_parser_find(struct config_parser_context *ctx,
+ const struct config_filter *filter)
+{
+ struct config_filter_parser *parser;
+
+ array_foreach_elem(&ctx->all_parsers, parser) {
+ if (config_filters_equal(&parser->filter, filter))
+ return parser;
+ }
+ return NULL;
+}
+
+int config_parse_net(const char *value, struct ip_addr *ip_r,
+ unsigned int *bits_r, const char **error_r)
+{
+ struct ip_addr *ips;
+ const char *p;
+ unsigned int ip_count, bits, max_bits;
+ time_t t1, t2;
+ int ret;
+
+ if (net_parse_range(value, ip_r, bits_r) == 0)
+ return 0;
+
+ p = strchr(value, '/');
+ if (p != NULL) {
+ value = t_strdup_until(value, p);
+ p++;
+ }
+
+ t1 = time(NULL);
+ alarm(DNS_LOOKUP_TIMEOUT_SECS);
+ ret = net_gethostbyname(value, &ips, &ip_count);
+ alarm(0);
+ t2 = time(NULL);
+ if (ret != 0) {
+ *error_r = t_strdup_printf("gethostbyname(%s) failed: %s",
+ value, net_gethosterror(ret));
+ return -1;
+ }
+ *ip_r = ips[0];
+
+ if (t2 - t1 >= DNS_LOOKUP_WARN_SECS) {
+ i_warning("gethostbyname(%s) took %d seconds",
+ value, (int)(t2-t1));
+ }
+
+ max_bits = IPADDR_IS_V4(&ips[0]) ? 32 : 128;
+ if (p == NULL)
+ *bits_r = max_bits;
+ else if (str_to_uint(p, &bits) == 0 && bits <= max_bits)
+ *bits_r = bits;
+ else {
+ *error_r = t_strdup_printf("Invalid network mask: %s", p);
+ return -1;
+ }
+ return 0;
+}
+
+static bool
+config_filter_add_new_filter(struct config_parser_context *ctx,
+ const char *key, const char *value)
+{
+ struct config_filter *filter = &ctx->cur_section->filter;
+ struct config_filter *parent = &ctx->cur_section->prev->filter;
+ struct config_filter_parser *parser;
+ const char *error;
+
+ if (strcmp(key, "protocol") == 0) {
+ if (parent->service != NULL)
+ ctx->error = "Nested protocol { protocol { .. } } block not allowed";
+ else
+ filter->service = p_strdup(ctx->pool, value);
+ } else if (strcmp(key, "local") == 0) {
+ if (parent->remote_bits > 0)
+ ctx->error = "remote { local { .. } } not allowed (use local { remote { .. } } instead)";
+ else if (parent->service != NULL)
+ ctx->error = "protocol { local { .. } } not allowed (use local { protocol { .. } } instead)";
+ else if (parent->local_name != NULL)
+ ctx->error = "local_name { local { .. } } not allowed (use local { local_name { .. } } instead)";
+ else if (config_parse_net(value, &filter->local_net,
+ &filter->local_bits, &error) < 0)
+ ctx->error = p_strdup(ctx->pool, error);
+ else if (parent->local_bits > filter->local_bits ||
+ (parent->local_bits > 0 &&
+ !net_is_in_network(&filter->local_net,
+ &parent->local_net,
+ parent->local_bits)))
+ ctx->error = "local net1 { local net2 { .. } } requires net2 to be inside net1";
+ else
+ filter->local_host = p_strdup(ctx->pool, value);
+ } else if (strcmp(key, "local_name") == 0) {
+ if (parent->remote_bits > 0)
+ ctx->error = "remote { local_name { .. } } not allowed (use local_name { remote { .. } } instead)";
+ else if (parent->service != NULL)
+ ctx->error = "protocol { local_name { .. } } not allowed (use local_name { protocol { .. } } instead)";
+ else
+ filter->local_name = p_strdup(ctx->pool, value);
+ } else if (strcmp(key, "remote") == 0) {
+ if (parent->service != NULL)
+ ctx->error = "protocol { remote { .. } } not allowed (use remote { protocol { .. } } instead)";
+ else if (config_parse_net(value, &filter->remote_net,
+ &filter->remote_bits, &error) < 0)
+ ctx->error = p_strdup(ctx->pool, error);
+ else if (parent->remote_bits > filter->remote_bits ||
+ (parent->remote_bits > 0 &&
+ !net_is_in_network(&filter->remote_net,
+ &parent->remote_net,
+ parent->remote_bits)))
+ ctx->error = "remote net1 { remote net2 { .. } } requires net2 to be inside net1";
+ else
+ filter->remote_host = p_strdup(ctx->pool, value);
+ } else {
+ return FALSE;
+ }
+
+ parser = config_filter_parser_find(ctx, filter);
+ if (parser != NULL)
+ ctx->cur_section->parsers = parser->parsers;
+ else
+ config_add_new_parser(ctx);
+ ctx->cur_section->is_filter = TRUE;
+ return TRUE;
+}
+
+static int
+config_filter_parser_check(struct config_parser_context *ctx,
+ const struct config_module_parser *p,
+ const char **error_r)
+{
+ const char *error = NULL;
+ bool ok;
+
+ for (; p->root != NULL; p++) {
+ /* skip checking settings we don't care about */
+ if (!config_module_want_parser(ctx->root_parsers,
+ ctx->modules, p->root))
+ continue;
+
+ settings_parse_var_skip(p->parser);
+ T_BEGIN {
+ ok = settings_parser_check(p->parser, ctx->pool, &error);
+ } T_END_PASS_STR_IF(!ok, &error);
+ if (!ok) {
+ /* be sure to assert-crash early if error is missing */
+ i_assert(error != NULL);
+ *error_r = error;
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static const char *
+get_str_setting(struct config_filter_parser *parser, const char *key,
+ const char *default_value)
+{
+ struct config_module_parser *module_parser;
+ const char *const *set_value;
+ enum setting_type set_type;
+
+ module_parser = parser->parsers;
+ for (; module_parser->parser != NULL; module_parser++) {
+ set_value = settings_parse_get_value(module_parser->parser,
+ key, &set_type);
+ if (set_value != NULL &&
+ settings_parse_is_changed(module_parser->parser, key)) {
+ i_assert(set_type == SET_STR || set_type == SET_ENUM);
+ return *set_value;
+ }
+ }
+ return default_value;
+}
+
+static int
+config_all_parsers_check(struct config_parser_context *ctx,
+ struct config_filter_context *new_filter,
+ const char **error_r)
+{
+ struct config_filter_parser *const *parsers;
+ struct config_module_parser *tmp_parsers;
+ struct master_service_settings_output output;
+ unsigned int i, count;
+ const char *ssl_set, *global_ssl_set;
+ pool_t tmp_pool;
+ bool ssl_warned = FALSE;
+ int ret = 0;
+
+ if (ctx->cur_section->prev != NULL) {
+ *error_r = t_strdup_printf(
+ "Missing '}' (section started at %s:%u)",
+ ctx->cur_section->open_path,
+ ctx->cur_section->open_linenum);
+ return -1;
+ }
+
+ tmp_pool = pool_alloconly_create(MEMPOOL_GROWING"config parsers check", 1024*64);
+ parsers = array_get(&ctx->all_parsers, &count);
+ i_assert(count > 0 && parsers[count-1] == NULL);
+ count--;
+
+ global_ssl_set = get_str_setting(parsers[0], "ssl", "");
+ for (i = 0; i < count && ret == 0; i++) {
+ if (config_filter_parsers_get(new_filter, tmp_pool, NULL,
+ &parsers[i]->filter,
+ &tmp_parsers, &output,
+ error_r) < 0) {
+ ret = -1;
+ break;
+ }
+
+ ssl_set = get_str_setting(parsers[i], "ssl", global_ssl_set);
+ if (strcmp(ssl_set, "no") != 0 &&
+ strcmp(global_ssl_set, "no") == 0 && !ssl_warned) {
+ i_warning("SSL is disabled because global ssl=no, "
+ "ignoring ssl=%s for subsection", ssl_set);
+ ssl_warned = TRUE;
+ }
+
+ ret = config_filter_parser_check(ctx, tmp_parsers, error_r);
+ config_filter_parsers_free(tmp_parsers);
+ p_clear(tmp_pool);
+ }
+ pool_unref(&tmp_pool);
+ return ret;
+}
+
+static int
+str_append_file(string_t *str, const char *key, const char *path,
+ const char **error_r)
+{
+ unsigned char buf[1024];
+ int fd;
+ ssize_t ret;
+
+ *error_r = NULL;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ *error_r = t_strdup_printf("%s: Can't open file %s: %m",
+ key, path);
+ return -1;
+ }
+ while ((ret = read(fd, buf, sizeof(buf))) > 0)
+ str_append_data(str, buf, ret);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("%s: read(%s) failed: %m",
+ key, path);
+ }
+ i_close_fd(&fd);
+ return ret < 0 ? -1 : 0;
+}
+
+static int settings_add_include(struct config_parser_context *ctx, const char *path,
+ bool ignore_errors, const char **error_r)
+{
+ struct input_stack *tmp, *new_input;
+ int fd;
+
+ for (tmp = ctx->cur_input; 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 = p_new(ctx->pool, struct input_stack, 1);
+ new_input->prev = ctx->cur_input;
+ new_input->path = p_strdup(ctx->pool, path);
+ new_input->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ i_stream_set_return_partial_line(new_input->input, TRUE);
+ ctx->cur_input = new_input;
+ return 0;
+}
+
+static int
+settings_include(struct config_parser_context *ctx, const char *pattern,
+ bool ignore_errors)
+{
+ const char *error;
+#ifdef HAVE_GLOB
+ glob_t globbers;
+ unsigned int i;
+
+ switch (glob(pattern, GLOB_BRACE, NULL, &globbers)) {
+ case 0:
+ break;
+ case GLOB_NOSPACE:
+ ctx->error = "glob() failed: Not enough memory";
+ return -1;
+ case GLOB_ABORTED:
+ ctx->error = "glob() failed: Read error";
+ return -1;
+ case GLOB_NOMATCH:
+ if (ignore_errors)
+ return 0;
+ ctx->error = "No matches";
+ return -1;
+ default:
+ ctx->error = "glob() failed: Unknown error";
+ return -1;
+ }
+
+ /* iterate through the different files matching the globbing */
+ for (i = globbers.gl_pathc; i > 0; i--) {
+ if (settings_add_include(ctx, globbers.gl_pathv[i-1],
+ ignore_errors, &error) < 0) {
+ ctx->error = p_strdup(ctx->pool, error);
+ return -1;
+ }
+ }
+ globfree(&globbers);
+ return 0;
+#else
+ if (settings_add_include(ctx, pattern, ignore_errors, &error) < 0) {
+ ctx->error = p_strdup(ctx->pool, error);
+ return -1;
+ }
+ return 0;
+#endif
+}
+
+static enum config_line_type
+config_parse_line(struct config_parser_context *ctx,
+ char *line, string_t *full_line,
+ const char **key_r, const char **value_r)
+{
+ const char *key;
+ size_t len;
+ char *p;
+
+ *key_r = NULL;
+ *value_r = NULL;
+
+ /* @UNSAFE: line is modified */
+
+ /* skip whitespace */
+ while (IS_WHITE(*line))
+ line++;
+
+ /* ignore comments or empty lines */
+ if (*line == '#' || *line == '\0')
+ return CONFIG_LINE_TYPE_SKIP;
+
+ /* strip away comments. pretty kludgy way really.. */
+ for (p = line; *p != '\0'; p++) {
+ if (*p == '\'' || *p == '"') {
+ char 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.",
+ ctx->cur_input->path,
+ ctx->cur_input->linenum);
+ }
+ *p = '\0';
+ break;
+ }
+ }
+
+ /* remove whitespace from end of line */
+ len = strlen(line);
+ while (len >= 1) {
+ if(!IS_WHITE(line[len-1]))
+ break;
+ len--;
+ }
+ line[len] = '\0';
+
+ if (len >= 1 && line[len-1] == '\\') {
+ /* continues in next line */
+ len--;
+ while (len >= 1) {
+ if(!IS_WHITE(line[len-1]))
+ break;
+ len--;
+ }
+ if(len >= 1) {
+ str_append_data(full_line, line, len);
+ str_append_c(full_line, ' ');
+ }
+ return CONFIG_LINE_TYPE_CONTINUE;
+ }
+ if (str_len(full_line) > 0) {
+ str_append(full_line, line);
+ line = str_c_modifiable(full_line);
+ }
+
+ /* 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++;
+ }
+ *key_r = key;
+ *value_r = line;
+
+ if (strcmp(key, "!include") == 0)
+ return CONFIG_LINE_TYPE_INCLUDE;
+ if (strcmp(key, "!include_try") == 0)
+ return CONFIG_LINE_TYPE_INCLUDE_TRY;
+
+ if (*line == '=') {
+ /* a) */
+ *line++ = '\0';
+ while (IS_WHITE(*line)) line++;
+
+ if (*line == '<') {
+ while (IS_WHITE(line[1])) line++;
+ *value_r = line + 1;
+ return CONFIG_LINE_TYPE_KEYFILE;
+ }
+ if (*line != '\'' && *line != '"' && strchr(line, '$') != NULL) {
+ *value_r = line;
+ return CONFIG_LINE_TYPE_KEYVARIABLE;
+ }
+
+ len = strlen(line);
+ if (len > 0 &&
+ ((*line == '"' && line[len-1] == '"') ||
+ (*line == '\'' && line[len-1] == '\''))) {
+ line[len-1] = '\0';
+ line = str_unescape(line+1);
+ }
+ *value_r = line;
+ return CONFIG_LINE_TYPE_KEYVALUE;
+ }
+
+ if (strcmp(key, "}") == 0 && *line == '\0')
+ return CONFIG_LINE_TYPE_SECTION_END;
+
+ /* b) + errors */
+ line[-1] = '\0';
+
+ if (*line == '{')
+ *value_r = "";
+ else {
+ /* get section name */
+ if (*line != '"') {
+ *value_r = line;
+ while (!IS_WHITE(*line) && *line != '\0')
+ line++;
+ if (*line != '\0') {
+ *line++ = '\0';
+ while (IS_WHITE(*line))
+ line++;
+ }
+ } else {
+ char *value = ++line;
+ while (*line != '"' && *line != '\0')
+ line++;
+ if (*line == '"') {
+ *line++ = '\0';
+ while (IS_WHITE(*line))
+ line++;
+ *value_r = str_unescape(value);
+ }
+ }
+ if (*line != '{') {
+ *value_r = "Expecting '{'";
+ return CONFIG_LINE_TYPE_ERROR;
+ }
+ }
+ if (line[1] != '\0') {
+ *value_r = "Garbage after '{'";
+ return CONFIG_LINE_TYPE_ERROR;
+ }
+ return CONFIG_LINE_TYPE_SECTION_BEGIN;
+}
+
+static int config_parse_finish(struct config_parser_context *ctx, const char **error_r)
+{
+ struct config_filter_context *new_filter;
+ const char *error;
+ int ret = 0;
+
+ if (hook_config_parser_end != NULL)
+ ret = hook_config_parser_end(ctx, error_r);
+
+ new_filter = config_filter_init(ctx->pool);
+ array_append_zero(&ctx->all_parsers);
+ config_filter_add_all(new_filter, array_front(&ctx->all_parsers));
+
+ if (ret < 0)
+ ;
+ else if (ctx->hide_errors)
+ ret = 0;
+ else if ((ret = config_all_parsers_check(ctx, new_filter, &error)) < 0) {
+ *error_r = t_strdup_printf("Error in configuration file %s: %s",
+ ctx->path, error);
+ }
+
+ if (config_filter != NULL)
+ config_filter_deinit(&config_filter);
+ config_module_parsers = ctx->root_parsers;
+ config_filter = new_filter;
+ return ret;
+}
+
+static const void *
+config_get_value(struct config_section_stack *section, const char *key,
+ bool expand_parent, enum setting_type *type_r)
+{
+ struct config_module_parser *l;
+ const void *value;
+
+ for (l = section->parsers; l->root != NULL; l++) {
+ value = settings_parse_get_value(l->parser, key, type_r);
+ if (value != NULL) {
+ if (!expand_parent || section->prev == NULL ||
+ settings_parse_is_changed(l->parser, key))
+ return value;
+
+ /* not changed by this parser. maybe parent has. */
+ return config_get_value(section->prev,
+ key, TRUE, type_r);
+ }
+ }
+ return NULL;
+}
+
+static bool
+config_require_key(struct config_parser_context *ctx, const char *key)
+{
+ struct config_module_parser *l;
+
+ if (ctx->modules == NULL)
+ return TRUE;
+
+ for (l = ctx->cur_section->parsers; l->root != NULL; l++) {
+ if (config_module_want_parser(ctx->root_parsers,
+ ctx->modules, l->root) &&
+ settings_parse_is_valid_key(l->parser, key))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int config_write_keyvariable(struct config_parser_context *ctx,
+ const char *key, const char *value,
+ string_t *str)
+{
+ const char *var_end, *p_start = value;
+ bool dump;
+ while (value != NULL) {
+ const char *var_name;
+ bool expand_parent;
+ var_end = strchr(value, ' ');
+
+ /* expand_parent=TRUE for "key = $key stuff".
+ we'll always expand it so that doveconf -n can give
+ usable output */
+ if (var_end == NULL)
+ var_name = value;
+ else
+ var_name = t_strdup_until(value, var_end);
+ expand_parent = strcmp(key, var_name +
+ (*var_name == '$' ? 1 : 0)) == 0;
+
+ if (!str_begins(var_name, "$") ||
+ (value > p_start && !IS_WHITE(value[-1]))) {
+ str_append(str, var_name);
+ } else if (!ctx->expand_values && !expand_parent) {
+ str_append(str, var_name);
+ } else if (str_begins(var_name, "$ENV:")) {
+ /* use environment variable */
+ const char *envval = getenv(var_name+5);
+ if (envval != NULL)
+ str_append(str, envval);
+ } else {
+ const char *var_value;
+ enum setting_type var_type;
+
+ i_assert(var_name[0] == '$');
+ var_name++;
+
+ var_value = config_get_value(ctx->cur_section, var_name,
+ expand_parent, &var_type);
+ if (var_value == NULL) {
+ ctx->error = p_strconcat(ctx->pool,
+ "Unknown variable: $",
+ var_name, NULL);
+ return -1;
+ }
+ if (!config_export_type(str, var_value, NULL,
+ var_type, TRUE, &dump)) {
+ ctx->error = p_strconcat(ctx->pool,
+ "Invalid variable: $",
+ var_name, NULL);
+ return -1;
+ }
+ }
+
+ if (var_end == NULL)
+ break;
+
+ str_append_c(str, ' ');
+
+ /* find next token */
+ while (*var_end != '\0' && IS_WHITE(*var_end)) var_end++;
+ value = var_end;
+ while (*var_end != '\0' && !IS_WHITE(*var_end)) var_end++;
+ }
+
+ return 0;
+}
+
+static int config_write_value(struct config_parser_context *ctx,
+ enum config_line_type type,
+ const char *key, const char *value)
+{
+ string_t *str = ctx->str;
+ const char *error, *path, *full_key;
+
+ switch (type) {
+ case CONFIG_LINE_TYPE_KEYVALUE:
+ str_append(str, value);
+ break;
+ case CONFIG_LINE_TYPE_KEYFILE:
+ full_key = t_strndup(str_data(ctx->str), str_len(str)-1);
+ if (!ctx->expand_values) {
+ str_append_c(str, '<');
+ str_append(str, value);
+ } else {
+ if (!config_require_key(ctx, full_key)) {
+ /* don't even try to open the file */
+ } else {
+ path = fix_relative_path(value, ctx->cur_input);
+ if (str_append_file(str, full_key, path, &error) < 0) {
+ /* file reading failed */
+ ctx->error = p_strdup(ctx->pool, error);
+ return -1;
+ }
+ }
+ }
+ break;
+ case CONFIG_LINE_TYPE_KEYVARIABLE:
+ if (config_write_keyvariable(ctx, key, value, str) < 0)
+ return -1;
+ break;
+ default:
+ i_unreached();
+ }
+ return 0;
+}
+
+static void
+config_parser_check_warnings(struct config_parser_context *ctx, const char *key)
+{
+ const char *path, *first_pos;
+
+ first_pos = hash_table_lookup(ctx->seen_settings, str_c(ctx->str));
+ if (ctx->cur_section->prev == NULL) {
+ /* changing a root setting. if we've already seen it inside
+ filters, log a warning. */
+ if (first_pos == NULL)
+ return;
+ i_warning("%s line %u: Global setting %s won't change the setting inside an earlier filter at %s "
+ "(if this is intentional, avoid this warning by moving the global setting before %s)",
+ ctx->cur_input->path, ctx->cur_input->linenum,
+ key, first_pos, first_pos);
+ return;
+ }
+ if (first_pos != NULL)
+ return;
+ first_pos = p_strdup_printf(ctx->pool, "%s line %u",
+ ctx->cur_input->path, ctx->cur_input->linenum);
+ path = p_strdup(ctx->pool, str_c(ctx->str));
+ hash_table_insert(ctx->seen_settings, path, first_pos);
+}
+
+void config_parser_apply_line(struct config_parser_context *ctx,
+ enum config_line_type type,
+ const char *key, const char *value)
+{
+ const char *section_name;
+
+ str_truncate(ctx->str, ctx->pathlen);
+ switch (type) {
+ case CONFIG_LINE_TYPE_SKIP:
+ break;
+ case CONFIG_LINE_TYPE_CONTINUE:
+ i_unreached();
+ case CONFIG_LINE_TYPE_ERROR:
+ ctx->error = p_strdup(ctx->pool, value);
+ break;
+ case CONFIG_LINE_TYPE_KEYVALUE:
+ case CONFIG_LINE_TYPE_KEYFILE:
+ case CONFIG_LINE_TYPE_KEYVARIABLE:
+ str_append(ctx->str, key);
+ config_parser_check_warnings(ctx, key);
+ str_append_c(ctx->str, '=');
+
+ if (config_write_value(ctx, type, key, value) < 0)
+ break;
+ (void)config_apply_line(ctx, key, str_c(ctx->str), NULL);
+ break;
+ case CONFIG_LINE_TYPE_SECTION_BEGIN:
+ ctx->cur_section = config_add_new_section(ctx);
+ ctx->cur_section->pathlen = ctx->pathlen;
+ ctx->cur_section->key = p_strdup(ctx->pool, key);
+
+ if (config_filter_add_new_filter(ctx, key, value)) {
+ /* new filter */
+ break;
+ }
+
+ /* new config section */
+ if (*value == '\0') {
+ /* no section name, use a counter */
+ section_name = dec2str(ctx->section_counter++);
+ } else {
+ section_name = settings_section_escape(value);
+ }
+ str_append(ctx->str, key);
+ ctx->pathlen = str_len(ctx->str);
+
+ str_append_c(ctx->str, '=');
+ str_append(ctx->str, section_name);
+
+ if (config_apply_line(ctx, key, str_c(ctx->str), value) < 0)
+ break;
+
+ str_truncate(ctx->str, ctx->pathlen);
+ str_append_c(ctx->str, SETTINGS_SEPARATOR);
+ str_append(ctx->str, section_name);
+ str_append_c(ctx->str, SETTINGS_SEPARATOR);
+ ctx->pathlen = str_len(ctx->str);
+ break;
+ case CONFIG_LINE_TYPE_SECTION_END:
+ if (ctx->cur_section->prev == NULL)
+ ctx->error = "Unexpected '}'";
+ else {
+ ctx->pathlen = ctx->cur_section->pathlen;
+ ctx->cur_section = ctx->cur_section->prev;
+ }
+ break;
+ case CONFIG_LINE_TYPE_INCLUDE:
+ case CONFIG_LINE_TYPE_INCLUDE_TRY:
+ (void)settings_include(ctx, fix_relative_path(value, ctx->cur_input),
+ type == CONFIG_LINE_TYPE_INCLUDE_TRY);
+ break;
+ }
+}
+
+int config_parse_file(const char *path, bool expand_values,
+ const char *const *modules, const char **error_r)
+{
+ struct input_stack root;
+ struct config_parser_context ctx;
+ unsigned int i, count;
+ const char *key, *value;
+ string_t *full_line;
+ enum config_line_type type;
+ char *line;
+ int fd, ret = 0;
+ bool handled;
+
+ if (path == NULL) {
+ path = "<defaults>";
+ fd = -1;
+ } else {
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ *error_r = t_strdup_printf("open(%s) failed: %m", path);
+ return 0;
+ }
+ }
+
+ i_zero(&ctx);
+ ctx.pool = pool_alloconly_create(MEMPOOL_GROWING"config file parser", 1024*256);
+ ctx.path = path;
+ ctx.hide_errors = fd == -1;
+
+ for (count = 0; all_roots[count] != NULL; count++) ;
+ ctx.root_parsers =
+ p_new(ctx.pool, struct config_module_parser, count+1);
+ for (i = 0; i < count; i++) {
+ ctx.root_parsers[i].root = all_roots[i];
+ ctx.root_parsers[i].parser =
+ settings_parser_init(ctx.pool, all_roots[i],
+ settings_parser_flags);
+ }
+
+ i_zero(&root);
+ root.path = path;
+ ctx.cur_input = &root;
+ ctx.expand_values = expand_values;
+ ctx.modules = modules;
+ hash_table_create(&ctx.seen_settings, ctx.pool, 0, str_hash, strcmp);
+
+ p_array_init(&ctx.all_parsers, ctx.pool, 128);
+ ctx.cur_section = p_new(ctx.pool, struct config_section_stack, 1);
+ config_add_new_parser(&ctx);
+
+ ctx.str = str_new(ctx.pool, 256);
+ full_line = str_new(default_pool, 512);
+ ctx.cur_input->input = fd != -1 ?
+ i_stream_create_fd_autoclose(&fd, SIZE_MAX) :
+ i_stream_create_from_data("", 0);
+ i_stream_set_return_partial_line(ctx.cur_input->input, TRUE);
+ old_settings_init(&ctx);
+ if (hook_config_parser_begin != NULL) T_BEGIN {
+ hook_config_parser_begin(&ctx);
+ } T_END;
+
+prevfile:
+ while ((line = i_stream_read_next_line(ctx.cur_input->input)) != NULL) {
+ ctx.cur_input->linenum++;
+ type = config_parse_line(&ctx, line, full_line,
+ &key, &value);
+ str_truncate(ctx.str, ctx.pathlen);
+ if (type == CONFIG_LINE_TYPE_CONTINUE)
+ continue;
+
+ T_BEGIN {
+ handled = old_settings_handle(&ctx, type, key, value);
+ if (!handled)
+ config_parser_apply_line(&ctx, type, key, value);
+ } T_END;
+
+ if (ctx.error != NULL) {
+ *error_r = t_strdup_printf(
+ "Error in configuration file %s line %d: %s",
+ ctx.cur_input->path, ctx.cur_input->linenum,
+ ctx.error);
+ ret = -2;
+ break;
+ }
+ str_truncate(full_line, 0);
+ }
+
+ i_stream_destroy(&ctx.cur_input->input);
+ ctx.cur_input = ctx.cur_input->prev;
+ if (line == NULL && ctx.cur_input != NULL)
+ goto prevfile;
+
+ hash_table_destroy(&ctx.seen_settings);
+ str_free(&full_line);
+ if (ret == 0)
+ ret = config_parse_finish(&ctx, error_r);
+ return ret < 0 ? ret : 1;
+}
+
+void config_parse_load_modules(void)
+{
+ struct module_dir_load_settings mod_set;
+ struct module *m;
+ const struct setting_parser_info **roots;
+ ARRAY_TYPE(setting_parser_info_p) new_roots;
+ ARRAY_TYPE(service_settings) new_services;
+ struct service_settings *const *services, *service_set;
+ unsigned int i, count;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ modules = module_dir_load(CONFIG_MODULE_DIR, NULL, &mod_set);
+ module_dir_init(modules);
+
+ i_array_init(&new_roots, 64);
+ i_array_init(&new_services, 64);
+ for (m = modules; m != NULL; m = m->next) {
+ roots = module_get_symbol_quiet(m,
+ t_strdup_printf("%s_set_roots", m->name));
+ if (roots != NULL) {
+ for (i = 0; roots[i] != NULL; i++)
+ array_push_back(&new_roots, &roots[i]);
+ }
+
+ services = module_get_symbol_quiet(m,
+ t_strdup_printf("%s_service_settings_array", m->name));
+ if (services != NULL) {
+ for (count = 0; services[count] != NULL; count++) ;
+ array_append(&new_services, services, count);
+ } else {
+ service_set = module_get_symbol_quiet(m,
+ t_strdup_printf("%s_service_settings", m->name));
+ if (service_set != NULL)
+ array_push_back(&new_services, &service_set);
+ }
+ }
+ if (array_count(&new_roots) > 0) {
+ /* modules added new settings. add the defaults and start
+ using the new list. */
+ for (i = 0; all_roots[i] != NULL; i++)
+ array_push_back(&new_roots, &all_roots[i]);
+ array_append_zero(&new_roots);
+ all_roots = array_front(&new_roots);
+ roots_free_at_deinit = new_roots;
+ } else {
+ array_free(&new_roots);
+ }
+ if (array_count(&new_services) > 0) {
+ /* module added new services. update the defaults. */
+ services = array_get(default_services, &count);
+ for (i = 0; i < count; i++)
+ array_push_back(&new_services, &services[i]);
+ *default_services = new_services;
+ services_free_at_deinit = new_services;
+ } else {
+ array_free(&new_services);
+ }
+}
+
+static bool parsers_are_connected(const struct setting_parser_info *root,
+ const struct setting_parser_info *info)
+{
+ const struct setting_parser_info *p;
+ const struct setting_parser_info *const *dep;
+
+ /* we're trying to find info or its parents from root's dependencies. */
+
+ for (p = info; p != NULL; p = p->parent) {
+ if (p == root)
+ return TRUE;
+ }
+
+ if (root->dependencies != NULL) {
+ for (dep = root->dependencies; *dep != NULL; dep++) {
+ if (parsers_are_connected(*dep, info))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool config_module_want_parser(struct config_module_parser *parsers,
+ const char *const *modules,
+ const struct setting_parser_info *root)
+{
+ struct config_module_parser *l;
+
+ if (modules == NULL)
+ return TRUE;
+ if (root == &master_service_setting_parser_info) {
+ /* everyone wants master service settings */
+ return TRUE;
+ }
+
+ for (l = parsers; l->root != NULL; l++) {
+ if (!str_array_find(modules, l->root->module_name))
+ continue;
+
+ /* see if we can find a way to get from the original parser
+ to this parser */
+ if (parsers_are_connected(l->root, root))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void config_parser_deinit(void)
+{
+ if (array_is_created(&services_free_at_deinit))
+ array_free(&services_free_at_deinit);
+ if (array_is_created(&roots_free_at_deinit))
+ array_free(&roots_free_at_deinit);
+}
diff --git a/src/config/config-parser.h b/src/config/config-parser.h
new file mode 100644
index 0000000..e0a0a5b
--- /dev/null
+++ b/src/config/config-parser.h
@@ -0,0 +1,33 @@
+#ifndef CONFIG_PARSER_H
+#define CONFIG_PARSER_H
+
+#define CONFIG_MODULE_DIR MODULEDIR"/settings"
+
+#define IS_WHITE(c) ((c) == ' ' || (c) == '\t')
+
+struct config_module_parser {
+ const struct setting_parser_info *root;
+ struct setting_parser_context *parser;
+ void *settings;
+};
+ARRAY_DEFINE_TYPE(config_module_parsers, struct config_module_parser *);
+
+extern struct config_module_parser *config_module_parsers;
+extern struct config_filter_context *config_filter;
+extern struct module *modules;
+
+int config_parse_net(const char *value, struct ip_addr *ip_r,
+ unsigned int *bits_r, const char **error_r);
+int config_parse_file(const char *path, bool expand_values,
+ const char *const *modules, const char **error_r)
+ ATTR_NULL(3);
+
+void config_parse_load_modules(void);
+
+bool config_module_want_parser(struct config_module_parser *parsers,
+ const char *const *modules,
+ const struct setting_parser_info *root)
+ ATTR_NULL(2);
+void config_parser_deinit(void);
+
+#endif
diff --git a/src/config/config-request.c b/src/config/config-request.c
new file mode 100644
index 0000000..7428367
--- /dev/null
+++ b/src/config/config-request.c
@@ -0,0 +1,524 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "ostream.h"
+#include "settings-parser.h"
+#include "master-service-settings.h"
+#include "all-settings.h"
+#include "config-parser.h"
+#include "config-request.h"
+#include "old-set-parser.h"
+
+struct config_export_context {
+ pool_t pool;
+ string_t *value;
+ string_t *prefix;
+ HASH_TABLE(char *, char *) keys;
+ enum config_dump_scope scope;
+
+ config_request_callback_t *callback;
+ void *context;
+
+ const char *const *modules;
+ const char *const *exclude_settings;
+ enum config_dump_flags flags;
+ const struct config_module_parser *parsers;
+ struct config_module_parser *dup_parsers;
+ struct master_service_settings_output output;
+
+ bool failed;
+};
+
+static void config_export_size(string_t *str, uoff_t size)
+{
+ static const char suffixes[] = { 'B', 'k', 'M', 'G', 'T' };
+ char suffix = suffixes[0];
+ unsigned int i;
+
+ if (size == 0) {
+ str_append_c(str, '0');
+ return;
+ }
+ for (i = 1; i < N_ELEMENTS(suffixes) && (size % 1024) == 0; i++) {
+ suffix = suffixes[i];
+ size /= 1024;
+ }
+ str_printfa(str, "%"PRIuUOFF_T" %c", size, suffix);
+}
+
+static void config_export_time(string_t *str, unsigned int stamp)
+{
+ const char *suffix = "secs";
+
+ if (stamp == 0) {
+ str_append_c(str, '0');
+ return;
+ }
+
+ if (stamp % 60 == 0) {
+ stamp /= 60;
+ suffix = "mins";
+ if (stamp % 60 == 0) {
+ stamp /= 60;
+ suffix = "hours";
+ if (stamp % 24 == 0) {
+ stamp /= 24;
+ suffix = "days";
+ if (stamp % 7 == 0) {
+ stamp /= 7;
+ suffix = "weeks";
+ }
+ }
+ }
+ }
+
+ str_printfa(str, "%u %s", stamp, suffix);
+}
+
+static void config_export_time_msecs(string_t *str, unsigned int stamp_msecs)
+{
+ if ((stamp_msecs % 1000) == 0)
+ config_export_time(str, stamp_msecs/1000);
+ else
+ str_printfa(str, "%u ms", stamp_msecs);
+}
+
+bool config_export_type(string_t *str, const void *value,
+ const void *default_value,
+ enum setting_type type, bool dump_default,
+ bool *dump_r)
+{
+ switch (type) {
+ case SET_BOOL: {
+ const bool *val = value, *dval = default_value;
+
+ if (dump_default || dval == NULL || *val != *dval)
+ str_append(str, *val ? "yes" : "no");
+ break;
+ }
+ case SET_SIZE: {
+ const uoff_t *val = value, *dval = default_value;
+
+ if (dump_default || dval == NULL || *val != *dval)
+ config_export_size(str, *val);
+ break;
+ }
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS: {
+ const unsigned int *val = value, *dval = default_value;
+
+ if (dump_default || dval == NULL || *val != *dval) {
+ switch (type) {
+ case SET_UINT_OCT:
+ str_printfa(str, "0%o", *val);
+ break;
+ case SET_TIME:
+ config_export_time(str, *val);
+ break;
+ case SET_TIME_MSECS:
+ config_export_time_msecs(str, *val);
+ break;
+ default:
+ str_printfa(str, "%u", *val);
+ break;
+ }
+ }
+ break;
+ }
+ case SET_IN_PORT: {
+ const in_port_t *val = value, *dval = default_value;
+
+ if (dump_default || dval == NULL || *val != *dval)
+ str_printfa(str, "%u", *val);
+ break;
+ }
+ case SET_STR_VARS: {
+ const char *const *val = value, *sval;
+ const char *const *_dval = default_value;
+ const char *dval = _dval == NULL ? NULL : *_dval;
+
+ i_assert(*val == NULL ||
+ **val == SETTING_STRVAR_UNEXPANDED[0]);
+
+ sval = *val == NULL ? NULL : (*val + 1);
+ if ((dump_default || null_strcmp(sval, dval) != 0) &&
+ sval != NULL) {
+ str_append(str, sval);
+ *dump_r = TRUE;
+ }
+ break;
+ }
+ case SET_STR: {
+ const char *const *val = value;
+ const char *const *_dval = default_value;
+ const char *dval = _dval == NULL ? NULL : *_dval;
+
+ if ((dump_default || null_strcmp(*val, dval) != 0) &&
+ *val != NULL) {
+ str_append(str, *val);
+ *dump_r = TRUE;
+ }
+ break;
+ }
+ case SET_ENUM: {
+ const char *const *val = value;
+ size_t len = strlen(*val);
+
+ if (dump_default)
+ str_append(str, *val);
+ else {
+ const char *const *_dval = default_value;
+ const char *dval = _dval == NULL ? NULL : *_dval;
+
+ i_assert(dval != NULL);
+ if (strncmp(*val, dval, len) != 0 ||
+ ((*val)[len] != ':' && (*val)[len] != '\0'))
+ str_append(str, *val);
+ }
+ break;
+ }
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+setting_export_section_name(string_t *str, const struct setting_define *def,
+ const void *set, unsigned int idx)
+{
+ const char *const *name;
+ size_t name_offset;
+
+ if (def->type != SET_DEFLIST_UNIQUE) {
+ /* not unique, use the index */
+ str_printfa(str, "%u", idx);
+ return;
+ }
+ name_offset = def->list_info->type_offset;
+ i_assert(name_offset != SIZE_MAX);
+
+ name = CONST_PTR_OFFSET(set, name_offset);
+ if (*name == NULL || **name == '\0') {
+ /* no name, this one isn't unique. use the index. */
+ str_printfa(str, "%u", idx);
+ } else T_BEGIN {
+ str_append(str, settings_section_escape(*name));
+ } T_END;
+}
+
+static void
+settings_export(struct config_export_context *ctx,
+ const struct setting_parser_info *info,
+ bool parent_unique_deflist,
+ const void *set, const void *change_set)
+{
+ const struct setting_define *def;
+ const void *value, *default_value, *change_value;
+ void *const *children, *const *change_children = NULL;
+ unsigned int i, count, count2;
+ size_t prefix_len;
+ const char *str;
+ char *key;
+ bool dump, dump_default = FALSE;
+
+ for (def = info->defines; def->key != NULL; def++) {
+ if (ctx->exclude_settings != NULL &&
+ str_array_find(ctx->exclude_settings, def->key))
+ continue;
+
+ value = CONST_PTR_OFFSET(set, def->offset);
+ default_value = info->defaults == NULL ? NULL :
+ CONST_PTR_OFFSET(info->defaults, def->offset);
+ change_value = CONST_PTR_OFFSET(change_set, def->offset);
+ switch (ctx->scope) {
+ case CONFIG_DUMP_SCOPE_ALL_WITH_HIDDEN:
+ dump_default = TRUE;
+ break;
+ case CONFIG_DUMP_SCOPE_ALL_WITHOUT_HIDDEN:
+ if ((def->flags & SET_FLAG_HIDDEN) == 0) {
+ /* not hidden - dump it */
+ dump_default = TRUE;
+ break;
+ }
+ /* hidden - dump default only if it's explicitly set */
+ /* fall through */
+ case CONFIG_DUMP_SCOPE_SET:
+ dump_default = *((const char *)change_value) != 0;
+ break;
+ case CONFIG_DUMP_SCOPE_CHANGED:
+ dump_default = FALSE;
+ break;
+ }
+ if (!parent_unique_deflist ||
+ (ctx->flags & CONFIG_DUMP_FLAG_HIDE_LIST_DEFAULTS) == 0) {
+ /* .. */
+ } else if (*((const char *)change_value) == 0 &&
+ def->offset != info->type_offset) {
+ /* this is mainly for service {} blocks. if value
+ hasn't changed, it's the default. even if
+ info->defaults has a different value. */
+ default_value = value;
+ } else {
+ /* value is set explicitly, but we don't know the
+ default here. assume it's not the default. */
+ dump_default = TRUE;
+ }
+
+ dump = FALSE;
+ count = 0; children = NULL;
+ str_truncate(ctx->value, 0);
+ switch (def->type) {
+ case SET_BOOL:
+ case SET_SIZE:
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS:
+ case SET_IN_PORT:
+ case SET_STR_VARS:
+ case SET_STR:
+ case SET_ENUM:
+ if (!config_export_type(ctx->value, value,
+ default_value, def->type,
+ dump_default, &dump))
+ i_unreached();
+ break;
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE: {
+ const ARRAY_TYPE(void_array) *val = value;
+ const ARRAY_TYPE(void_array) *change_val = change_value;
+
+ if (!array_is_created(val))
+ break;
+
+ children = array_get(val, &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(ctx->value, ' ');
+ setting_export_section_name(ctx->value, def, children[i], i);
+ }
+ change_children = array_get(change_val, &count2);
+ i_assert(count == count2);
+ break;
+ }
+ case SET_STRLIST: {
+ const ARRAY_TYPE(const_string) *val = value;
+ const char *const *strings;
+
+ if (!array_is_created(val))
+ break;
+
+ key = p_strconcat(ctx->pool, str_c(ctx->prefix),
+ def->key, NULL);
+
+ if (hash_table_lookup(ctx->keys, key) != NULL) {
+ /* already added all of these */
+ break;
+ }
+ hash_table_insert(ctx->keys, key, key);
+ /* for doveconf -n to see this KEY_LIST */
+ ctx->callback(key, "", CONFIG_KEY_LIST, ctx->context);
+
+ strings = array_get(val, &count);
+ i_assert(count % 2 == 0);
+ for (i = 0; i < count; i += 2) {
+ str = p_strdup_printf(ctx->pool, "%s%s%c%s",
+ str_c(ctx->prefix),
+ def->key,
+ SETTINGS_SEPARATOR,
+ strings[i]);
+ ctx->callback(str, strings[i+1],
+ CONFIG_KEY_NORMAL, ctx->context);
+ }
+ count = 0;
+ break;
+ }
+ case SET_ALIAS:
+ break;
+ }
+ if (str_len(ctx->value) > 0 || dump) {
+ key = p_strconcat(ctx->pool, str_c(ctx->prefix),
+ def->key, NULL);
+ if (hash_table_lookup(ctx->keys, key) == NULL) {
+ enum config_key_type type;
+
+ if (def->offset == info->type_offset &&
+ parent_unique_deflist)
+ type = CONFIG_KEY_UNIQUE_KEY;
+ else if (SETTING_TYPE_IS_DEFLIST(def->type))
+ type = CONFIG_KEY_LIST;
+ else
+ type = CONFIG_KEY_NORMAL;
+ ctx->callback(key, str_c(ctx->value), type,
+ ctx->context);
+ hash_table_insert(ctx->keys, key, key);
+ }
+ }
+
+ i_assert(count == 0 || children != NULL);
+ prefix_len = str_len(ctx->prefix);
+ for (i = 0; i < count; i++) {
+ str_append(ctx->prefix, def->key);
+ str_append_c(ctx->prefix, SETTINGS_SEPARATOR);
+ setting_export_section_name(ctx->prefix, def, children[i], i);
+ str_append_c(ctx->prefix, SETTINGS_SEPARATOR);
+ settings_export(ctx, def->list_info,
+ def->type == SET_DEFLIST_UNIQUE,
+ children[i], change_children[i]);
+
+ str_truncate(ctx->prefix, prefix_len);
+ }
+ }
+}
+
+struct config_export_context *
+config_export_init(const char *const *modules,
+ const char *const *exclude_settings,
+ enum config_dump_scope scope,
+ enum config_dump_flags flags,
+ config_request_callback_t *callback, void *context)
+{
+ struct config_export_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"config export", 1024*64);
+ ctx = p_new(pool, struct config_export_context, 1);
+ ctx->pool = pool;
+
+ ctx->modules = modules == NULL ? NULL : p_strarray_dup(pool, modules);
+ ctx->exclude_settings = exclude_settings == NULL ? NULL :
+ p_strarray_dup(pool, exclude_settings);
+ ctx->flags = flags;
+ ctx->callback = callback;
+ ctx->context = context;
+ ctx->scope = scope;
+ ctx->value = str_new(pool, 256);
+ ctx->prefix = str_new(pool, 64);
+ hash_table_create(&ctx->keys, ctx->pool, 0, str_hash, strcmp);
+ return ctx;
+}
+
+void config_export_by_filter(struct config_export_context *ctx,
+ const struct config_filter *filter)
+{
+ const char *error;
+
+ if (config_filter_parsers_get(config_filter, ctx->pool,
+ ctx->modules, filter,
+ &ctx->dup_parsers, &ctx->output,
+ &error) < 0) {
+ i_error("%s", error);
+ ctx->failed = TRUE;
+ }
+ ctx->parsers = ctx->dup_parsers;
+}
+
+void config_export_parsers(struct config_export_context *ctx,
+ const struct config_module_parser *parsers)
+{
+ ctx->parsers = parsers;
+}
+
+void config_export_get_output(struct config_export_context *ctx,
+ struct master_service_settings_output *output_r)
+{
+ *output_r = ctx->output;
+}
+
+const char *
+config_export_get_import_environment(struct config_export_context *ctx)
+{
+ enum setting_type stype;
+ unsigned int i;
+
+ for (i = 0; ctx->parsers[i].root != NULL; i++) {
+ if (ctx->parsers[i].root == &master_service_setting_parser_info) {
+ const char *const *value =
+ settings_parse_get_value(ctx->parsers[i].parser,
+ "import_environment", &stype);
+ i_assert(value != NULL);
+ return *value;
+ }
+ }
+ i_unreached();
+}
+
+static void config_export_free(struct config_export_context *ctx)
+{
+ if (ctx->dup_parsers != NULL)
+ config_filter_parsers_free(ctx->dup_parsers);
+ hash_table_destroy(&ctx->keys);
+ pool_unref(&ctx->pool);
+}
+
+int config_export_finish(struct config_export_context **_ctx)
+{
+ struct config_export_context *ctx = *_ctx;
+ const struct config_module_parser *parser;
+ const char *error;
+ unsigned int i;
+ int ret = 0;
+
+ *_ctx = NULL;
+
+ if (ctx->failed) {
+ config_export_free(ctx);
+ return -1;
+ }
+
+ for (i = 0; ctx->parsers[i].root != NULL; i++) {
+ parser = &ctx->parsers[i];
+ if (!config_module_want_parser(config_module_parsers,
+ ctx->modules, parser->root))
+ continue;
+
+ T_BEGIN {
+ enum setting_type stype;
+ const char *const *value = settings_parse_get_value(parser->parser, "ssl", &stype);
+
+ if ((ctx->flags & CONFIG_DUMP_FLAG_IN_SECTION) == 0 &&
+ value != NULL && strcmp(*value, "no") != 0 &&
+ settings_parse_is_valid_key(parser->parser, "ssl_dh")) {
+ value = settings_parse_get_value(parser->parser,
+ "ssl_dh", &stype);
+
+ if (value == NULL || **value == '\0') {
+ const char *newval;
+ if (old_settings_ssl_dh_load(&newval, &error)) {
+ if (newval != NULL)
+ settings_parse_line(parser->parser, t_strdup_printf("%s=%s", "ssl_dh", newval));
+ } else {
+ i_error("%s", error);
+ ret = -1;
+ }
+ }
+ }
+ settings_export(ctx, parser->root, FALSE,
+ settings_parser_get(parser->parser),
+ settings_parser_get_changes(parser->parser));
+ } T_END;
+
+ if ((ctx->flags & CONFIG_DUMP_FLAG_CHECK_SETTINGS) != 0) {
+ settings_parse_var_skip(parser->parser);
+ if (!settings_parser_check(parser->parser, ctx->pool,
+ &error)) {
+ if ((ctx->flags & CONFIG_DUMP_FLAG_CALLBACK_ERRORS) != 0) {
+ ctx->callback(NULL, error, CONFIG_KEY_ERROR,
+ ctx->context);
+ } else {
+ i_error("%s", error);
+ ret = -1;
+ break;
+ }
+ }
+ }
+ }
+ config_export_free(ctx);
+ return ret;
+}
diff --git a/src/config/config-request.h b/src/config/config-request.h
new file mode 100644
index 0000000..c91e836
--- /dev/null
+++ b/src/config/config-request.h
@@ -0,0 +1,61 @@
+#ifndef CONFIG_REQUEST_H
+#define CONFIG_REQUEST_H
+
+#include "settings-parser.h"
+#include "config-filter.h"
+
+struct master_service_settings_output;
+
+enum config_dump_scope {
+ /* Dump all settings, including hidden settings */
+ CONFIG_DUMP_SCOPE_ALL_WITH_HIDDEN,
+ /* Dump all non-hidden settings */
+ CONFIG_DUMP_SCOPE_ALL_WITHOUT_HIDDEN,
+ /* Dump all that have explicitly been set */
+ CONFIG_DUMP_SCOPE_SET,
+ /* Dump only settings that differ from defaults */
+ CONFIG_DUMP_SCOPE_CHANGED
+};
+
+enum config_dump_flags {
+ CONFIG_DUMP_FLAG_CHECK_SETTINGS = 0x01,
+ CONFIG_DUMP_FLAG_HIDE_LIST_DEFAULTS = 0x02,
+ /* Errors are reported using callback and they don't stop handling */
+ CONFIG_DUMP_FLAG_CALLBACK_ERRORS = 0x04,
+ /* Set if dumping a section and not top level config */
+ CONFIG_DUMP_FLAG_IN_SECTION = 0x08,
+};
+
+enum config_key_type {
+ CONFIG_KEY_NORMAL,
+ CONFIG_KEY_LIST,
+ CONFIG_KEY_UNIQUE_KEY,
+ /* error message is in value */
+ CONFIG_KEY_ERROR
+};
+
+typedef void config_request_callback_t(const char *key, const char *value,
+ enum config_key_type type, void *context);
+
+bool config_export_type(string_t *str, const void *value,
+ const void *default_value,
+ enum setting_type type, bool dump_default,
+ bool *dump_r) ATTR_NULL(3);
+struct config_export_context *
+config_export_init(const char *const *modules,
+ const char *const *exclude_settings,
+ enum config_dump_scope scope,
+ enum config_dump_flags flags,
+ config_request_callback_t *callback, void *context)
+ ATTR_NULL(1, 5);
+void config_export_by_filter(struct config_export_context *ctx,
+ const struct config_filter *filter);
+void config_export_parsers(struct config_export_context *ctx,
+ const struct config_module_parser *parsers);
+void config_export_get_output(struct config_export_context *ctx,
+ struct master_service_settings_output *output_r);
+const char *
+config_export_get_import_environment(struct config_export_context *ctx);
+int config_export_finish(struct config_export_context **ctx);
+
+#endif
diff --git a/src/config/config-settings.c b/src/config/config-settings.c
new file mode 100644
index 0000000..d19fe4e
--- /dev/null
+++ b/src/config/config-settings.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings config_unix_listeners_array[] = {
+ { "config", 0600, "", "" }
+};
+static struct file_listener_settings *config_unix_listeners[] = {
+ &config_unix_listeners_array[0]
+};
+static buffer_t config_unix_listeners_buf = {
+ { { config_unix_listeners, sizeof(config_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings config_service_settings = {
+ .name = "config",
+ .protocol = "",
+ .type = "config",
+ .executable = "config",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &config_unix_listeners_buf,
+ sizeof(config_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
diff --git a/src/config/doveconf.c b/src/config/doveconf.c
new file mode 100644
index 0000000..79ea9e8
--- /dev/null
+++ b/src/config/doveconf.c
@@ -0,0 +1,1072 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "path-util.h"
+#include "module-dir.h"
+#include "env-util.h"
+#include "guid.h"
+#include "hash.h"
+#include "hostpid.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "settings-parser.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "all-settings.h"
+#include "sysinfo-get.h"
+#include "old-set-parser.h"
+#include "config-connection.h"
+#include "config-parser.h"
+#include "config-request.h"
+#include "dovecot-version.h"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <sysexits.h>
+
+struct prefix_stack {
+ unsigned int prefix_idx;
+ unsigned int str_pos;
+};
+ARRAY_DEFINE_TYPE(prefix_stack, struct prefix_stack);
+
+struct config_dump_human_context {
+ pool_t pool;
+ string_t *list_prefix;
+ ARRAY_TYPE(const_string) strings;
+ ARRAY_TYPE(const_string) errors;
+ struct config_export_context *export_ctx;
+
+ bool list_prefix_sent:1;
+};
+
+#define LIST_KEY_PREFIX "\001"
+#define UNIQUE_KEY_SUFFIX "\xff"
+
+static const char *indent_str = " !!!!";
+
+static const char *const secrets[] = {
+ "key",
+ "secret",
+ "pass",
+ "http://",
+ "https://",
+ "ftp://",
+ NULL
+};
+
+
+static void
+config_request_get_strings(const char *key, const char *value,
+ enum config_key_type type, void *context)
+{
+ struct config_dump_human_context *ctx = context;
+ const char *p;
+
+ switch (type) {
+ case CONFIG_KEY_NORMAL:
+ value = p_strdup_printf(ctx->pool, "%s=%s", key, value);
+ break;
+ case CONFIG_KEY_LIST:
+ value = p_strdup_printf(ctx->pool, LIST_KEY_PREFIX"%s=%s",
+ key, value);
+ break;
+ case CONFIG_KEY_UNIQUE_KEY:
+ p = strrchr(key, '/');
+ i_assert(p != NULL);
+ value = p_strdup_printf(ctx->pool, "%.*s/"UNIQUE_KEY_SUFFIX"%s=%s",
+ (int)(p - key), key, p + 1, value);
+ break;
+ case CONFIG_KEY_ERROR:
+ value = p_strdup(ctx->pool, value);
+ array_push_back(&ctx->errors, &value);
+ return;
+ }
+ array_push_back(&ctx->strings, &value);
+}
+
+static int config_string_cmp(const char *const *p1, const char *const *p2)
+{
+ const char *s1 = *p1, *s2 = *p2;
+ unsigned int i = 0;
+
+ while (s1[i] == s2[i]) {
+ if (s1[i] == '\0' || s1[i] == '=')
+ return 0;
+ i++;
+ }
+
+ if (s1[i] == '=')
+ return -1;
+ if (s2[i] == '=')
+ return 1;
+
+ return s1[i] - s2[i];
+}
+
+static struct prefix_stack prefix_stack_pop(ARRAY_TYPE(prefix_stack) *stack)
+{
+ const struct prefix_stack *s;
+ struct prefix_stack sc;
+ unsigned int count;
+
+ s = array_get(stack, &count);
+ i_assert(count > 0);
+ if (count == 1) {
+ sc.prefix_idx = UINT_MAX;
+ } else {
+ sc.prefix_idx = s[count-2].prefix_idx;
+ }
+ sc.str_pos = s[count-1].str_pos;
+ array_delete(stack, count-1, 1);
+ return sc;
+}
+
+static void prefix_stack_reset_str(ARRAY_TYPE(prefix_stack) *stack)
+{
+ struct prefix_stack *s;
+
+ array_foreach_modifiable(stack, s)
+ s->str_pos = UINT_MAX;
+}
+
+static struct config_dump_human_context *
+config_dump_human_init(const char *const *modules, enum config_dump_scope scope,
+ bool check_settings, bool in_section)
+{
+ struct config_dump_human_context *ctx;
+ enum config_dump_flags flags;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"config human strings", 1024*32);
+ ctx = p_new(pool, struct config_dump_human_context, 1);
+ ctx->pool = pool;
+ ctx->list_prefix = str_new(ctx->pool, 128);
+ i_array_init(&ctx->strings, 256);
+ i_array_init(&ctx->errors, 256);
+
+ flags = CONFIG_DUMP_FLAG_HIDE_LIST_DEFAULTS |
+ CONFIG_DUMP_FLAG_CALLBACK_ERRORS;
+ if (check_settings)
+ flags |= CONFIG_DUMP_FLAG_CHECK_SETTINGS;
+ if (in_section)
+ flags |= CONFIG_DUMP_FLAG_IN_SECTION;
+ ctx->export_ctx = config_export_init(modules, NULL, scope, flags,
+ config_request_get_strings, ctx);
+ return ctx;
+}
+
+static void config_dump_human_deinit(struct config_dump_human_context *ctx)
+{
+ array_free(&ctx->strings);
+ array_free(&ctx->errors);
+ pool_unref(&ctx->pool);
+}
+
+static bool value_need_quote(const char *value)
+{
+ size_t len = strlen(value);
+
+ if (len == 0)
+ return FALSE;
+
+ if (strchr(value, '#') != NULL)
+ return TRUE;
+ if (IS_WHITE(value[0]) || IS_WHITE(value[len-1]))
+ return TRUE;
+ return FALSE;
+}
+
+static const char *find_next_secret(const char *input, const char **secret_r)
+{
+ const char *const *secret;
+ const char *ptr = NULL;
+ *secret_r = NULL;
+ for(secret = secrets; *secret != NULL; secret++) {
+ const char *cptr;
+ if ((cptr = strstr(input, *secret)) != NULL) {
+ if (ptr == NULL || cptr < ptr) {
+ *secret_r = *secret;
+ ptr = cptr;
+ }
+ }
+ }
+ i_assert(*secret_r != NULL || ptr == NULL);
+ return ptr;
+}
+
+static bool
+hide_url_userpart_from_value(struct ostream *output, const char **_ptr,
+ const char **optr, bool quote)
+{
+ const char *ptr = *_ptr;
+ const char *start_of_user = ptr;
+ const char *start_of_host = NULL;
+ string_t *quoted = NULL;
+
+ if (quote)
+ quoted = t_str_new(256);
+
+ /* it's a URL, see if there is a userpart */
+ while(*ptr != '\0' && !i_isspace(*ptr) && *ptr != '/') {
+ if (*ptr == '@') {
+ start_of_host = ptr;
+ break;
+ }
+ ptr++;
+ }
+
+ if (quote) {
+ str_truncate(quoted, 0);
+ str_append_escaped(quoted, *optr, start_of_user - (*optr));
+ o_stream_nsend(output, quoted->data, quoted->used);
+ } else {
+ o_stream_nsend(output, *optr, start_of_user - (*optr));
+ }
+
+ if (start_of_host != NULL && start_of_host != start_of_user) {
+ o_stream_nsend_str(output, "#hidden_use-P_to_show#");
+ } else if (quote) {
+ str_truncate(quoted, 0);
+ str_append_escaped(quoted, start_of_user, ptr - start_of_user);
+ o_stream_nsend(output, quoted->data, quoted->used);
+ } else {
+ o_stream_nsend(output, start_of_user, ptr - start_of_user);
+ }
+
+ *optr = ptr;
+ *_ptr = ptr;
+ return TRUE;
+}
+
+static inline bool key_ends_with(const char *key, const char *eptr,
+ const char *suffix)
+{
+ /* take = into account */
+ size_t n = strlen(suffix)+1;
+ return (eptr-key > (ptrdiff_t)n && str_begins(eptr-n, suffix));
+}
+
+static bool
+hide_secrets_from_value(struct ostream *output, const char *key,
+ const char *value)
+{
+ bool ret = FALSE, quote = value_need_quote(value);
+ const char *ptr, *optr, *secret;
+ if (*value != '\0' &&
+ (key_ends_with(key, value, "_password") ||
+ key_ends_with(key, value, "_key") ||
+ key_ends_with(key, value, "_nonce") ||
+ str_begins(key, "ssl_dh"))) {
+ o_stream_nsend_str(output, "# hidden, use -P to show it");
+ return TRUE;
+ }
+
+ /* Check if we can find anything that has prefix of any of the
+ secrets. It should match things like secret_api_key or pass or password,
+ etc. but not something like nonsecret. */
+ optr = ptr = value;
+ while((ptr = find_next_secret(ptr, &secret)) != NULL) {
+ if (strstr(secret, "://") != NULL) {
+ ptr += strlen(secret);
+ if ((ret = hide_url_userpart_from_value(output, &ptr, &optr, quote)))
+ continue;
+ }
+ /* we have found something that we hide, and will deal with output
+ here. */
+ ret = TRUE;
+ if (ptr == value ||
+ (ptr > value && !i_isalnum(ptr[-1]))) {
+ size_t len;
+ while(*ptr != '\0') {
+ if (*ptr == '=' || i_isspace(*ptr))
+ break;
+ ptr++;
+ }
+ while(i_isspace(*ptr))
+ ptr++;
+ len = (size_t)(ptr-optr);
+ if (quote) {
+ string_t *quoted = t_str_new(len*2);
+ str_append_escaped(quoted, optr, len);
+ o_stream_nsend(output,
+ quoted->data, quoted->used);
+ } else {
+ o_stream_nsend(output, optr, len);
+ }
+ if (*ptr == '=') {
+ o_stream_nsend(output, ptr, 1);
+ o_stream_nsend_str(output, "#hidden_use-P_to_show#");
+ while(*ptr != '\0' && !i_isspace(*ptr) &&
+ *ptr != ';' && *ptr != ':')
+ ptr++;
+ }
+ optr = ptr;
+ } else {
+ /* "secret" is prefixed with alphanumeric character,
+ e.g. "nopassword". So it's not really a secret.
+ Skip forward to avoid infinite loop. */
+ ptr++;
+ }
+ };
+ /* if we are dealing with output, send rest here */
+ if (ret) {
+ if (quote)
+ o_stream_nsend_str(output, str_escape(optr));
+ else
+ o_stream_nsend_str(output, optr);
+ }
+ return ret;
+}
+
+static int ATTR_NULL(4)
+config_dump_human_output(struct config_dump_human_context *ctx,
+ struct ostream *output, unsigned int indent,
+ const char *setting_name_filter, bool hide_passwords)
+{
+ ARRAY_TYPE(const_string) prefixes_arr;
+ ARRAY_TYPE(prefix_stack) prefix_stack;
+ struct prefix_stack prefix;
+ const char *const *strings, *const *args, *p, *str, *const *prefixes;
+ const char *key, *key2, *value;
+ unsigned int i, j, count, prefix_count;
+ unsigned int prefix_idx = UINT_MAX;
+ size_t len, skip_len, setting_name_filter_len;
+ bool unique_key;
+ int ret = 0;
+
+ setting_name_filter_len = setting_name_filter == NULL ? 0 :
+ strlen(setting_name_filter);
+ if (config_export_finish(&ctx->export_ctx) < 0)
+ return -1;
+
+ array_sort(&ctx->strings, config_string_cmp);
+ strings = array_get(&ctx->strings, &count);
+
+ /* strings are sorted so that all lists come first */
+ p_array_init(&prefixes_arr, ctx->pool, 32);
+ for (i = 0; i < count && strings[i][0] == LIST_KEY_PREFIX[0]; i++) T_BEGIN {
+ p = strchr(strings[i], '=');
+ i_assert(p != NULL);
+ if (p[1] == '\0') {
+ /* "strlist=" */
+ str = p_strdup_printf(ctx->pool, "%s/",
+ t_strcut(strings[i]+1, '='));
+ array_push_back(&prefixes_arr, &str);
+ } else {
+ /* string is in format: "list=0 1 2" */
+ for (args = t_strsplit(p + 1, " "); *args != NULL; args++) {
+ str = p_strdup_printf(ctx->pool, "%s/%s/",
+ t_strcut(strings[i]+1, '='),
+ *args);
+ array_push_back(&prefixes_arr, &str);
+ }
+ }
+ } T_END;
+ prefixes = array_get(&prefixes_arr, &prefix_count);
+
+ p_array_init(&prefix_stack, ctx->pool, 8);
+ for (; i < count; i++) T_BEGIN {
+ value = strchr(strings[i], '=');
+ i_assert(value != NULL);
+
+ key = t_strdup_until(strings[i], value++);
+ unique_key = FALSE;
+
+ p = strrchr(key, '/');
+ if (p != NULL && p[1] == UNIQUE_KEY_SUFFIX[0]) {
+ key = t_strconcat(t_strdup_until(key, p + 1),
+ p + 2, NULL);
+ unique_key = TRUE;
+ }
+ if (setting_name_filter_len > 0) {
+ /* see if this setting matches the name filter */
+ if (!(strncmp(setting_name_filter, key,
+ setting_name_filter_len) == 0 &&
+ (key[setting_name_filter_len] == '/' ||
+ key[setting_name_filter_len] == '\0')))
+ goto end;
+ }
+ again:
+ j = 0;
+ /* if there are open sections and this key isn't in it,
+ close the sections */
+ while (prefix_idx != UINT_MAX) {
+ len = strlen(prefixes[prefix_idx]);
+ if (strncmp(prefixes[prefix_idx], key, len) != 0) {
+ prefix = prefix_stack_pop(&prefix_stack);
+ indent--;
+ if (prefix.str_pos != UINT_MAX)
+ str_truncate(ctx->list_prefix, prefix.str_pos);
+ else {
+ o_stream_nsend(output, indent_str, indent*2);
+ o_stream_nsend_str(output, "}\n");
+ }
+ prefix_idx = prefix.prefix_idx;
+ } else {
+ /* keep the prefix */
+ j = prefix_idx + 1;
+ break;
+ }
+ }
+ /* see if this key is in some section */
+ for (; j < prefix_count; j++) {
+ len = strlen(prefixes[j]);
+ if (strncmp(prefixes[j], key, len) == 0) {
+ key2 = key + (prefix_idx == UINT_MAX ? 0 :
+ strlen(prefixes[prefix_idx]));
+ prefix.str_pos = !unique_key ? UINT_MAX :
+ str_len(ctx->list_prefix);
+ prefix_idx = j;
+ prefix.prefix_idx = prefix_idx;
+ array_push_back(&prefix_stack, &prefix);
+
+ str_append_max(ctx->list_prefix, indent_str, indent*2);
+ p = strchr(key2, '/');
+ if (p != NULL)
+ str_append_data(ctx->list_prefix, key2, p - key2);
+ else
+ str_append(ctx->list_prefix, key2);
+ if (unique_key && *value != '\0') {
+ if (strchr(value, ' ') == NULL)
+ str_printfa(ctx->list_prefix, " %s", value);
+ else
+ str_printfa(ctx->list_prefix, " \"%s\"", str_escape(value));
+ }
+ str_append(ctx->list_prefix, " {\n");
+ indent++;
+
+ if (unique_key)
+ goto end;
+ else
+ goto again;
+ }
+ }
+ o_stream_nsend(output, str_data(ctx->list_prefix), str_len(ctx->list_prefix));
+ str_truncate(ctx->list_prefix, 0);
+ prefix_stack_reset_str(&prefix_stack);
+ ctx->list_prefix_sent = TRUE;
+
+ skip_len = prefix_idx == UINT_MAX ? 0 : strlen(prefixes[prefix_idx]);
+ i_assert(skip_len == 0 ||
+ strncmp(prefixes[prefix_idx], strings[i], skip_len) == 0);
+ o_stream_nsend(output, indent_str, indent*2);
+ key = strings[i] + skip_len;
+ if (unique_key) key++;
+ value = strchr(key, '=');
+ i_assert(value != NULL);
+ o_stream_nsend(output, key, value-key);
+ o_stream_nsend_str(output, " = ");
+ if (hide_passwords &&
+ hide_secrets_from_value(output, key, value+1))
+ /* sent */
+ ;
+ else if (!value_need_quote(value+1))
+ o_stream_nsend_str(output, value+1);
+ else {
+ o_stream_nsend(output, "\"", 1);
+ o_stream_nsend_str(output, str_escape(value+1));
+ o_stream_nsend(output, "\"", 1);
+ }
+ o_stream_nsend(output, "\n", 1);
+ end: ;
+ } T_END;
+
+ while (prefix_idx != UINT_MAX) {
+ prefix = prefix_stack_pop(&prefix_stack);
+ if (prefix.str_pos != UINT_MAX)
+ break;
+ prefix_idx = prefix.prefix_idx;
+ indent--;
+ o_stream_nsend(output, indent_str, indent*2);
+ o_stream_nsend_str(output, "}\n");
+ }
+
+ /* flush output before writing errors */
+ o_stream_uncork(output);
+ array_foreach_elem(&ctx->errors, str) {
+ i_error("%s", str);
+ ret = -1;
+ }
+ return ret;
+}
+
+static unsigned int
+config_dump_filter_begin(string_t *str,
+ const struct config_filter *filter)
+{
+ unsigned int indent = 0;
+
+ if (filter->local_bits > 0) {
+ str_printfa(str, "local %s", net_ip2addr(&filter->local_net));
+
+ if (IPADDR_IS_V4(&filter->local_net)) {
+ if (filter->local_bits != 32)
+ str_printfa(str, "/%u", filter->local_bits);
+ } else {
+ if (filter->local_bits != 128)
+ str_printfa(str, "/%u", filter->local_bits);
+ }
+ str_append(str, " {\n");
+ indent++;
+ }
+
+ if (filter->local_name != NULL) {
+ str_append_max(str, indent_str, indent*2);
+ str_printfa(str, "local_name %s {\n", filter->local_name);
+ indent++;
+ }
+
+ if (filter->remote_bits > 0) {
+ str_append_max(str, indent_str, indent*2);
+ str_printfa(str, "remote %s", net_ip2addr(&filter->remote_net));
+
+ if (IPADDR_IS_V4(&filter->remote_net)) {
+ if (filter->remote_bits != 32)
+ str_printfa(str, "/%u", filter->remote_bits);
+ } else {
+ if (filter->remote_bits != 128)
+ str_printfa(str, "/%u", filter->remote_bits);
+ }
+ str_append(str, " {\n");
+ indent++;
+ }
+ if (filter->service != NULL) {
+ str_append_max(str, indent_str, indent*2);
+ str_printfa(str, "protocol %s {\n", filter->service);
+ indent++;
+ }
+ return indent;
+}
+
+static void
+config_dump_filter_end(struct ostream *output, unsigned int indent)
+{
+ while (indent > 0) {
+ indent--;
+ o_stream_nsend(output, indent_str, indent*2);
+ o_stream_nsend(output, "}\n", 2);
+ }
+}
+
+static int
+config_dump_human_sections(struct ostream *output,
+ const struct config_filter *filter,
+ const char *const *modules, bool hide_passwords)
+{
+ struct config_filter_parser *const *filters;
+ static struct config_dump_human_context *ctx;
+ unsigned int indent;
+ int ret = 0;
+
+ filters = config_filter_find_subset(config_filter, filter);
+
+ /* first filter should be the global one */
+ i_assert(filters[0] != NULL && filters[0]->filter.service == NULL);
+ filters++;
+
+ for (; *filters != NULL; filters++) {
+ ctx = config_dump_human_init(modules, CONFIG_DUMP_SCOPE_SET,
+ FALSE, TRUE);
+ indent = config_dump_filter_begin(ctx->list_prefix,
+ &(*filters)->filter);
+ config_export_parsers(ctx->export_ctx, (*filters)->parsers);
+ if (config_dump_human_output(ctx, output, indent, NULL, hide_passwords) < 0)
+ ret = -1;
+ if (ctx->list_prefix_sent)
+ config_dump_filter_end(output, indent);
+ config_dump_human_deinit(ctx);
+ }
+ return ret;
+}
+
+static int ATTR_NULL(4)
+config_dump_human(const struct config_filter *filter, const char *const *modules,
+ enum config_dump_scope scope, const char *setting_name_filter,
+ bool hide_passwords)
+{
+ static struct config_dump_human_context *ctx;
+ struct ostream *output;
+ int ret;
+
+ output = o_stream_create_fd(STDOUT_FILENO, 0);
+ o_stream_set_no_error_handling(output, TRUE);
+ o_stream_cork(output);
+
+ ctx = config_dump_human_init(modules, scope, TRUE, FALSE);
+ config_export_by_filter(ctx->export_ctx, filter);
+ ret = config_dump_human_output(ctx, output, 0, setting_name_filter, hide_passwords);
+ config_dump_human_deinit(ctx);
+
+ if (setting_name_filter == NULL)
+ ret = config_dump_human_sections(output, filter, modules, hide_passwords);
+
+ o_stream_uncork(output);
+ o_stream_destroy(&output);
+ return ret;
+}
+
+static int
+config_dump_one(const struct config_filter *filter, bool hide_key,
+ enum config_dump_scope scope, const char *setting_name_filter,
+ bool hide_passwords)
+{
+ static struct config_dump_human_context *ctx;
+ const char *str;
+ size_t len;
+ bool dump_section = FALSE;
+
+ ctx = config_dump_human_init(NULL, scope, FALSE, FALSE);
+ config_export_by_filter(ctx->export_ctx, filter);
+ if (config_export_finish(&ctx->export_ctx) < 0)
+ return -1;
+
+ len = strlen(setting_name_filter);
+ array_foreach_elem(&ctx->strings, str) {
+ if (strncmp(str, setting_name_filter, len) != 0)
+ continue;
+
+ if (str[len] == '=') {
+ if (hide_key)
+ printf("%s\n", str + len+1);
+ else {
+ printf("%s = %s\n", setting_name_filter,
+ str + len+1);
+ }
+ dump_section = FALSE;
+ break;
+ } else if (str[len] == '/') {
+ dump_section = TRUE;
+ }
+ }
+ config_dump_human_deinit(ctx);
+
+ if (dump_section)
+ (void)config_dump_human(filter, NULL, scope, setting_name_filter, hide_passwords);
+ return 0;
+}
+
+static void config_request_simple_stdout(const char *key, const char *value,
+ enum config_key_type type ATTR_UNUSED,
+ void *context)
+{
+ char **setting_name_filters = context;
+ unsigned int i;
+ size_t filter_len;
+
+ if (setting_name_filters == NULL) {
+ printf("%s=%s\n", key, value);
+ return;
+ }
+
+ for (i = 0; setting_name_filters[i] != NULL; i++) {
+ filter_len = strlen(setting_name_filters[i]);
+ if (strncmp(setting_name_filters[i], key, filter_len) == 0 &&
+ (key[filter_len] == '\0' || key[filter_len] == '/'))
+ printf("%s=%s\n", key, value);
+ }
+}
+
+static void config_request_putenv(const char *key, const char *value,
+ enum config_key_type type ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ T_BEGIN {
+ env_put(t_str_ucase(key), value);
+ } T_END;
+}
+
+static const char *get_setting(const char *module, const char *name)
+{
+ struct config_module_parser *l;
+ const struct setting_define *def;
+ const char *const *value;
+ const void *set;
+
+ for (l = config_module_parsers; l->root != NULL; l++) {
+ if (strcmp(l->root->module_name, module) != 0)
+ continue;
+
+ set = settings_parser_get(l->parser);
+ for (def = l->root->defines; def->key != NULL; def++) {
+ if (strcmp(def->key, name) == 0) {
+ value = CONST_PTR_OFFSET(set, def->offset);
+ return *value;
+ }
+ }
+ }
+ return "";
+}
+
+static void filter_parse_arg(struct config_filter *filter, const char *arg)
+{
+ const char *key, *value, *error;
+
+ value = strchr(arg, '=');
+ if (value != NULL)
+ key = t_strdup_until(arg, value++);
+ else {
+ key = arg;
+ value = "";
+ }
+
+ if (strcmp(key, "service") == 0)
+ filter->service = value;
+ else if (strcmp(key, "protocol") == 0)
+ filter->service = value;
+ else if (strcmp(key, "lname") == 0)
+ filter->local_name = value;
+ else if (strcmp(key, "local") == 0) {
+ if (config_parse_net(value, &filter->local_net,
+ &filter->local_bits, &error) < 0)
+ i_fatal("local filter: %s", error);
+ } else if (strcmp(key, "remote") == 0) {
+ if (config_parse_net(value, &filter->remote_net,
+ &filter->remote_bits, &error) < 0)
+ i_fatal("remote filter: %s", error);
+ } else {
+ i_fatal("Unknown filter argument: %s", arg);
+ }
+}
+
+struct hostname_format {
+ const char *prefix, *suffix;
+ unsigned int numcount;
+ bool zeropadding;
+};
+
+static void
+hostname_format_write(string_t *str, const struct hostname_format *fmt,
+ unsigned int num)
+{
+ str_truncate(str, 0);
+ str_append(str, fmt->prefix);
+ if (!fmt->zeropadding)
+ str_printfa(str, "%d", num);
+ else
+ str_printfa(str, "%0*d", fmt->numcount, num);
+ str_append(str, fmt->suffix);
+}
+
+static void hostname_verify_format(const char *arg)
+{
+ struct hostname_format fmt;
+ const char *p;
+ unsigned char hash[GUID_128_HOST_HASH_SIZE];
+ unsigned int n, limit;
+ HASH_TABLE(void *, void *) hosts;
+ void *key, *value;
+ string_t *host;
+ const char *host2;
+ bool duplicates = FALSE;
+
+ i_zero(&fmt);
+ if (arg != NULL) {
+ /* host%d, host%2d, host%02d */
+ p = strchr(arg, '%');
+ if (p == NULL)
+ i_fatal("Host parameter missing %%d");
+ fmt.prefix = t_strdup_until(arg, p++);
+ if (*p == '0') {
+ fmt.zeropadding = TRUE;
+ p++;
+ }
+ if (!i_isdigit(*p))
+ fmt.numcount = 1;
+ else
+ fmt.numcount = *p++ - '0';
+ if (*p++ != 'd')
+ i_fatal("Host parameter missing %%d");
+ fmt.suffix = p;
+ } else {
+ /* detect host1[suffix] vs host01[suffix] */
+ size_t len = strlen(my_hostname);
+ while (len > 0 && !i_isdigit(my_hostname[len-1]))
+ len--;
+ fmt.suffix = my_hostname + len;
+ fmt.numcount = 0;
+ while (len > 0 && i_isdigit(my_hostname[len-1])) {
+ len--;
+ fmt.numcount++;
+ }
+ if (my_hostname[len] == '0')
+ fmt.zeropadding = TRUE;
+ fmt.prefix = t_strndup(my_hostname, len);
+ if (fmt.numcount == 0) {
+ i_fatal("Hostname '%s' has no digits, can't verify",
+ my_hostname);
+ }
+ }
+ for (n = 0, limit = 1; n < fmt.numcount; n++)
+ limit *= 10;
+ host = t_str_new(128);
+ hash_table_create_direct(&hosts, default_pool, limit);
+ for (n = 0; n < limit; n++) {
+ hostname_format_write(host, &fmt, n);
+
+ guid_128_host_hash_get(str_c(host), hash);
+ i_assert(sizeof(key) >= sizeof(hash));
+ key = NULL; memcpy(&key, hash, sizeof(hash));
+
+ value = hash_table_lookup(hosts, key);
+ if (value != NULL) {
+ host2 = t_strdup(str_c(host));
+ hostname_format_write(host, &fmt,
+ POINTER_CAST_TO(value, unsigned int)-1);
+ i_error("Duplicate host hashes: %s and %s",
+ str_c(host), host2);
+ duplicates = TRUE;
+ } else {
+ hash_table_insert(hosts, key, POINTER_CAST(n+1));
+ }
+ }
+ hash_table_destroy(&hosts);
+
+ if (duplicates)
+ lib_exit(EX_CONFIG);
+ else {
+ host2 = t_strdup(str_c(host));
+ hostname_format_write(host, &fmt, 0);
+ printf("No duplicate host hashes in %s .. %s\n",
+ str_c(host), host2);
+ lib_exit(0);
+ }
+}
+
+static void check_wrong_config(const char *config_path)
+{
+ const char *base_dir, *symlink_path, *prev_path, *error;
+
+ base_dir = get_setting("master", "base_dir");
+ symlink_path = t_strconcat(base_dir, "/"PACKAGE".conf", NULL);
+ if (t_readlink(symlink_path, &prev_path, &error) < 0) {
+ if (errno != ENOENT)
+ i_error("t_readlink(%s) failed: %s", symlink_path, error);
+ return;
+ }
+
+ if (strcmp(prev_path, config_path) != 0) {
+ i_warning("Dovecot was last started using %s, "
+ "but this config is %s", prev_path, config_path);
+ }
+}
+
+static void failure_exit_callback(int *status)
+{
+ /* don't use EX_CONFIG, because it often causes MTAs to bounce
+ the mails back. */
+ *status = EX_TEMPFAIL;
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags master_service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME;
+ enum config_dump_scope scope = CONFIG_DUMP_SCOPE_ALL_WITHOUT_HIDDEN;
+ const char *orig_config_path, *config_path, *module;
+ ARRAY(const char *) module_names;
+ struct config_filter filter;
+ const char *const *wanted_modules, *error;
+ char **exec_args = NULL, **setting_name_filters = NULL;
+ unsigned int i;
+ int c, ret, ret2;
+ bool config_path_specified, expand_vars = FALSE, hide_key = FALSE;
+ bool parse_full_config = FALSE, simple_output = FALSE;
+ bool dump_defaults = FALSE, host_verify = FALSE;
+ bool print_plugin_banner = FALSE, hide_passwords = TRUE;
+
+ if (getenv("USE_SYSEXITS") != NULL) {
+ /* we're coming from (e.g.) LDA */
+ i_set_failure_exit_callback(failure_exit_callback);
+ }
+
+ i_zero(&filter);
+ master_service = master_service_init("config", master_service_flags,
+ &argc, &argv, "adf:hHm:nNpPexsS");
+ orig_config_path = t_strdup(master_service_get_config_path(master_service));
+
+ i_set_failure_prefix("doveconf: ");
+ t_array_init(&module_names, 4);
+ while ((c = master_getopt(master_service)) > 0) {
+ if (c == 'e') {
+ expand_vars = TRUE;
+ break;
+ }
+ switch (c) {
+ case 'a':
+ break;
+ case 'd':
+ dump_defaults = TRUE;
+ break;
+ case 'f':
+ filter_parse_arg(&filter, optarg);
+ break;
+ case 'h':
+ hide_key = TRUE;
+ break;
+ case 'H':
+ host_verify = TRUE;
+ break;
+ case 'm':
+ module = t_strdup(optarg);
+ array_push_back(&module_names, &module);
+ break;
+ case 'n':
+ scope = CONFIG_DUMP_SCOPE_CHANGED;
+ break;
+ case 'N':
+ scope = CONFIG_DUMP_SCOPE_SET;
+ break;
+ case 'p':
+ parse_full_config = TRUE;
+ break;
+ case 'P':
+ hide_passwords = FALSE;
+ break;
+ case 's':
+ scope = CONFIG_DUMP_SCOPE_ALL_WITH_HIDDEN;
+ break;
+ case 'S':
+ simple_output = TRUE;
+ break;
+ case 'x':
+ expand_vars = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ array_append_zero(&module_names);
+ wanted_modules = array_count(&module_names) == 1 ? NULL :
+ array_front(&module_names);
+
+ config_path = master_service_get_config_path(master_service);
+ /* use strcmp() instead of !=, because dovecot -n always gives us
+ -c parameter */
+ config_path_specified = strcmp(config_path, orig_config_path) != 0;
+
+ if (host_verify)
+ hostname_verify_format(argv[optind]);
+
+ if (c == 'e') {
+ if (argv[optind] == NULL)
+ i_fatal("Missing command for -e");
+ exec_args = &argv[optind];
+ } else if (argv[optind] != NULL) {
+ /* print only a single config setting */
+ setting_name_filters = argv+optind;
+ if (scope == CONFIG_DUMP_SCOPE_ALL_WITHOUT_HIDDEN)
+ scope = CONFIG_DUMP_SCOPE_ALL_WITH_HIDDEN;
+ } else if (!simple_output) {
+ /* print the config file path before parsing it, so in case
+ of errors it's still shown */
+ printf("# "DOVECOT_VERSION_FULL": %s\n", config_path);
+ print_plugin_banner = TRUE;
+ fflush(stdout);
+ }
+ master_service_init_finish(master_service);
+ config_parse_load_modules();
+
+ if (print_plugin_banner) {
+ struct module *m;
+
+ for (m = modules; m != NULL; m = m->next) {
+ const char **str = module_get_symbol_quiet(m,
+ t_strdup_printf("%s_doveconf_banner", m->name));
+ if (str != NULL)
+ printf("# %s\n", *str);
+ }
+ }
+
+ if ((ret = config_parse_file(dump_defaults ? NULL : config_path,
+ expand_vars,
+ parse_full_config ? NULL : wanted_modules,
+ &error)) == 0 &&
+ access(EXAMPLE_CONFIG_DIR, X_OK) == 0) {
+ i_fatal("%s (copy example configs from "EXAMPLE_CONFIG_DIR"/)",
+ error);
+ }
+
+ if ((ret == -1 && exec_args != NULL) || ret == 0 || ret == -2)
+ i_fatal("%s", error);
+
+ if (simple_output) {
+ struct config_export_context *ctx;
+
+ ctx = config_export_init(wanted_modules, NULL, scope,
+ CONFIG_DUMP_FLAG_CHECK_SETTINGS,
+ config_request_simple_stdout,
+ setting_name_filters);
+ config_export_by_filter(ctx, &filter);
+ ret2 = config_export_finish(&ctx);
+ } else if (setting_name_filters != NULL) {
+ ret2 = 0;
+ /* ignore settings-check failures in configuration. this allows
+ using doveconf to lookup settings for things like install or
+ uninstall scripts where the configuration might
+ (temporarily) not be fully usable */
+ ret = 0;
+ for (i = 0; setting_name_filters[i] != NULL; i++) {
+ if (config_dump_one(&filter, hide_key, scope,
+ setting_name_filters[i], hide_passwords) < 0)
+ ret2 = -1;
+ }
+ } else if (exec_args == NULL) {
+ const char *info;
+
+ info = sysinfo_get(get_setting("mail", "mail_location"));
+ if (*info != '\0')
+ printf("# %s\n", info);
+ printf("# Hostname: %s\n", my_hostdomain());
+ if (!config_path_specified)
+ check_wrong_config(config_path);
+ if (scope == CONFIG_DUMP_SCOPE_ALL_WITHOUT_HIDDEN)
+ printf("# NOTE: Send doveconf -n output instead when asking for help.\n");
+ fflush(stdout);
+ ret2 = config_dump_human(&filter, wanted_modules, scope, NULL, hide_passwords);
+ } else {
+ struct config_export_context *ctx;
+
+ ctx = config_export_init(wanted_modules, NULL, CONFIG_DUMP_SCOPE_SET,
+ CONFIG_DUMP_FLAG_CHECK_SETTINGS,
+ config_request_putenv, NULL);
+ config_export_by_filter(ctx, &filter);
+
+ if (getenv(DOVECOT_PRESERVE_ENVS_ENV) != NULL) {
+ /* Standalone binary is getting its configuration via
+ doveconf. Clean the environment before calling it.
+ Do this only if the environment exists, because
+ lib-master doesn't set it if it doesn't want the
+ environment to be cleaned (e.g. -k parameter). */
+ const char *import_environment =
+ config_export_get_import_environment(ctx);
+ master_service_import_environment(import_environment);
+ master_service_env_clean();
+ }
+
+ env_put("DOVECONF_ENV", "1");
+ if (config_export_finish(&ctx) < 0)
+ i_fatal("Invalid configuration");
+ execvp(exec_args[0], exec_args);
+ i_fatal("execvp(%s) failed: %m", exec_args[0]);
+ }
+
+ if (ret < 0) {
+ /* delayed error */
+ i_fatal("%s", error);
+ }
+ if (ret2 < 0)
+ i_fatal("Errors in configuration");
+
+ config_filter_deinit(&config_filter);
+ old_settings_deinit_global();
+ module_dir_unload(&modules);
+ config_parser_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/config/main.c b/src/config/main.c
new file mode 100644
index 0000000..01a80ba
--- /dev/null
+++ b/src/config/main.c
@@ -0,0 +1,53 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "old-set-parser.h"
+#include "config-connection.h"
+#include "config-parser.h"
+#include "config-request.h"
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ (void)config_connection_create(conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ const char *path, *error;
+
+ master_service = master_service_init("config", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ master_service_init_log(master_service);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ config_parse_load_modules();
+
+ path = master_service_get_config_path(master_service);
+ if (config_parse_file(path, TRUE, NULL, &error) <= 0)
+ i_fatal("%s", error);
+
+ /* notify about our success only after successfully parsing the
+ config file, so if the parsing fails, master won't immediately
+ just recreate this process (and fail again and so on). */
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+ config_connections_destroy_all();
+
+ config_filter_deinit(&config_filter);
+ old_settings_deinit_global();
+ module_dir_unload(&modules);
+ config_parser_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/config/old-set-parser.c b/src/config/old-set-parser.c
new file mode 100644
index 0000000..d5302f8
--- /dev/null
+++ b/src/config/old-set-parser.c
@@ -0,0 +1,824 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "settings-parser.h"
+#include "config-parser-private.h"
+#include "old-set-parser.h"
+#include "istream.h"
+#include "base64.h"
+
+static bool seen_ssl_parameters_dat;
+static const char *ssl_dh_parameters;
+
+#define config_apply_line (void)config_apply_line
+
+struct socket_set {
+ const char *path, *mode, *user, *group;
+ bool master;
+};
+
+struct old_set_parser {
+ const char *base_dir;
+ /* 1 when in auth {} section, >1 when inside auth { .. { .. } } */
+ unsigned int auth_section;
+ /* 1 when in socket listen {}, >1 when inside more of its sections */
+ unsigned int socket_listen_section;
+ bool seen_auth_section;
+ struct socket_set socket_set;
+};
+
+static const struct config_filter any_filter = {
+ .service = NULL
+};
+
+static const struct config_filter imap_filter = {
+ .service = "imap"
+};
+static const struct config_filter pop3_filter = {
+ .service = "pop3"
+};
+static const struct config_filter managesieve_filter = {
+ .service = "sieve"
+};
+
+static char *ssl_dh_value = NULL;
+static bool ssl_dh_loaded = FALSE;
+
+static void ATTR_FORMAT(2, 3)
+obsolete(struct config_parser_context *ctx, const char *str, ...)
+{
+ static bool seen_obsoletes = FALSE;
+ va_list args;
+
+ if (!seen_obsoletes) {
+ i_warning("NOTE: You can get a new clean config file with: "
+ "doveconf -Pn > dovecot-new.conf");
+ seen_obsoletes = TRUE;
+ }
+
+ va_start(args, str);
+ i_warning("Obsolete setting in %s:%u: %s",
+ ctx->cur_input->path, ctx->cur_input->linenum,
+ t_strdup_vprintf(str, args));
+ va_end(args);
+}
+
+static void set_rename(struct config_parser_context *ctx,
+ const char *old_key, const char *key, const char *value)
+{
+ obsolete(ctx, "%s has been renamed to %s", old_key, key);
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE, key, value);
+}
+
+static bool old_settings_ssl_dh_read(const char **value, const char **error_r)
+{
+
+ if (ssl_dh_parameters != NULL) *value = ssl_dh_parameters;
+
+ const char *fn = t_strconcat(PKG_STATEDIR, "/ssl-parameters.dat", NULL);
+ buffer_t *data = t_buffer_create(300);
+ string_t *b64_data = t_str_new(500);
+ size_t siz;
+ unsigned short keysize;
+ unsigned int off=0;
+
+ /* try read it */
+ struct istream *is = i_stream_create_file(fn, IO_BLOCK_SIZE);
+
+ if (is->stream_errno == ENOENT) {
+ /* this is given because the ssl-parameters.dat file is no more there
+ and we don't want to to make go searching for the file
+ this code is only ever reached if ssl_dh_parameters is empty anyways
+ */
+ /* check moved to correct place from here */
+ *value = NULL;
+ i_stream_unref(&is);
+ return TRUE;
+ } else if (is->stream_errno != 0) {
+ *error_r = t_strdup(i_stream_get_error(is));
+ i_stream_unref(&is);
+ return FALSE;
+ }
+
+ /* then try to read the rest of the data */
+ while(i_stream_read(is) > 0) {
+ const unsigned char *buf = i_stream_get_data(is, &siz);
+ if (siz < 88) break;
+ memcpy(&keysize, buf, 2);
+ if (keysize == 512) {
+ memcpy(&off, buf+4, 4);
+ off += 16; // skip headers
+ } else {
+ off = 8; // skip header
+ }
+ if (off > siz) break;
+ buffer_append(data, buf+off, siz);
+ break;
+ }
+
+ const void *tmp = buffer_get_data(data, &siz);
+
+ if (siz > 4) {
+ str_append(b64_data, "-----BEGIN DH PARAMETERS-----\n");
+ base64_encode(tmp, siz-4, b64_data);
+ /* need to wrap the string nicely */
+ for(size_t i = 29+65; i < str_len(b64_data); i+=64) /* start at header + first 64 */
+ {
+ str_insert(b64_data, i++, "\n");
+ }
+ str_append_c(b64_data,'\n');
+ str_append(b64_data, "-----END DH PARAMETERS-----");
+ ssl_dh_parameters = i_strdup(str_c(b64_data));
+ *value = ssl_dh_parameters;
+
+ if (!seen_ssl_parameters_dat) {
+ i_warning("please set ssl_dh=<%s", SYSCONFDIR"/dh.pem");
+ i_warning("You can generate it with: dd if=%s bs=1 skip=%u | openssl dhparam -inform der > %s", fn, off, SYSCONFDIR"/dh.pem");
+ seen_ssl_parameters_dat = TRUE;
+ }
+ } else if (is->stream_errno == ENOENT) {
+ /* check for empty ssl_dh elsewhere */
+ *value = NULL;
+ i_stream_unref(&is);
+ return TRUE;
+ } else {
+ *error_r = "ssl enabled, but ssl_dh not set";
+ i_stream_unref(&is);
+ return FALSE;
+ }
+ i_stream_unref(&is);
+
+ return TRUE;
+}
+
+bool old_settings_ssl_dh_load(const char **value, const char **error_r)
+{
+ if (ssl_dh_loaded) {
+ *value = ssl_dh_value;
+ return TRUE;
+ }
+ if (!old_settings_ssl_dh_read(value, error_r))
+ return FALSE;
+ ssl_dh_value = i_strdup(*value);
+ ssl_dh_loaded = TRUE;
+ return TRUE;
+}
+
+/* FIXME: Remove ssl_protocols_to_min_protocol() in v2.4 */
+static int ssl_protocols_to_min_protocol(const char *ssl_protocols,
+ const char **min_protocol_r,
+ const char **error_r)
+{
+ static const char *protocol_versions[] = {
+ "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2",
+ };
+ /* Array where -1 = disable, 0 = not found, 1 = enable */
+ int protos[N_ELEMENTS(protocol_versions)];
+ memset(protos, 0, sizeof(protos));
+ bool explicit_enable = FALSE;
+
+ const char *const *tmp = t_strsplit_spaces(ssl_protocols, ", ");
+ for (; *tmp != NULL; tmp++) {
+ const char *p = *tmp;
+ bool enable = TRUE;
+ if (p[0] == '!') {
+ enable = FALSE;
+ ++p;
+ }
+ for (unsigned i = 0; i < N_ELEMENTS(protocol_versions); i++) {
+ if (strcmp(p, protocol_versions[i]) == 0) {
+ if (enable) {
+ protos[i] = 1;
+ explicit_enable = TRUE;
+ } else {
+ protos[i] = -1;
+ }
+ goto found;
+ }
+ }
+ *error_r = t_strdup_printf("Unrecognized protocol '%s'", p);
+ return -1;
+
+ found:;
+ }
+
+ unsigned min = N_ELEMENTS(protocol_versions);
+ for (unsigned i = 0; i < N_ELEMENTS(protocol_versions); i++) {
+ if (explicit_enable) {
+ if (protos[i] > 0)
+ min = I_MIN(min, i);
+ } else if (protos[i] == 0)
+ min = I_MIN(min, i);
+ }
+ if (min == N_ELEMENTS(protocol_versions)) {
+ *error_r = "All protocols disabled";
+ return -1;
+ }
+
+ *min_protocol_r = protocol_versions[min];
+ return 0;
+}
+
+static bool
+old_settings_handle_root(struct config_parser_context *ctx,
+ const char *key, const char *value)
+{
+ const char *p;
+ size_t len;
+
+ if (strcmp(key, "base_dir") == 0) {
+ len = strlen(value);
+ if (len > 0 && value[len-1] == '/')
+ value = t_strndup(value, len-1);
+ ctx->old->base_dir = p_strdup(ctx->pool, value);
+ }
+ if (strcmp(key, "protocols") == 0) {
+ char **protos, **s;
+ bool have_imap = FALSE, have_imaps = FALSE;
+ bool have_pop3 = FALSE, have_pop3s = FALSE;
+
+ protos = p_strsplit_spaces(pool_datastack_create(), value, " ");
+ for (s = protos; *s != NULL; s++) {
+ if (strcmp(*s, "imap") == 0)
+ have_imap = TRUE;
+ else if (strcmp(*s, "imaps") == 0) {
+ *s = "";
+ have_imaps = TRUE;
+ } else if (strcmp(*s, "pop3") == 0)
+ have_pop3 = TRUE;
+ else if (strcmp(*s, "pop3s") == 0) {
+ *s = "";
+ have_pop3s = TRUE;
+ } else if (strcmp(*s, "managesieve") == 0) {
+ *s = "sieve";
+ obsolete(ctx, "protocols=managesieve has been renamed to protocols=sieve");
+ }
+ }
+ value = t_strarray_join((const char *const *)protos, " ");
+ /* ugly way to drop extra spaces.. */
+ protos = p_strsplit_spaces(pool_datastack_create(), value, " ");
+ value = t_strarray_join((const char *const *)protos, " ");
+
+ if (have_imaps && !have_imap) {
+ obsolete(ctx, "'imaps' protocol can no longer be specified (use protocols=imap). to disable non-ssl imap, use service imap-login { inet_listener imap { port=0 } }");
+ value = t_strconcat(value, " imap", NULL);
+ config_apply_line(ctx, "port",
+ "service/imap-login/inet_listener/imap/port=0", NULL);
+ } else if (have_imaps)
+ obsolete(ctx, "'imaps' protocol is no longer necessary, remove it");
+ if (have_pop3s && !have_pop3) {
+ obsolete(ctx, "'pop3s' protocol can no longer be specified (use protocols=pop3). to disable non-ssl pop3, use service pop3-login { inet_listener pop3 { port=0 } }");
+ value = t_strconcat(value, " pop3", NULL);
+ config_apply_line(ctx, "port",
+ "service/pop3-login/inet_listener/pop3/port=0", NULL);
+ } else if (have_pop3s)
+ obsolete(ctx, "'pop3s' protocol is no longer necessary, remove it");
+
+ if (*value == ' ') value++;
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ key, value);
+ return TRUE;
+ }
+ if (strcmp(key, "ssl_cert_file") == 0 ||
+ strcmp(key, "ssl_key_file") == 0 ||
+ strcmp(key, "ssl_ca_file") == 0) {
+ if (*value == '\0')
+ return TRUE;
+ p = t_strdup_until(key, strrchr(key, '_'));
+ obsolete(ctx, "%s has been replaced by %s = <file", key, p);
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYFILE,
+ p, value);
+ return TRUE;
+ }
+ if (strcmp(key, "ssl_disable") == 0) {
+ if (strcasecmp(value, "yes") == 0)
+ value = "no";
+ else if (strcasecmp(value, "no") == 0)
+ value = "yes";
+ set_rename(ctx, key, "ssl", value);
+ return TRUE;
+ }
+ if (strcmp(key, "ssl_parameters_regenerate") == 0 ||
+ strcmp(key, "ssl_dh_parameters_length") == 0) {
+ obsolete(ctx, "%s is no longer needed", key);
+ return TRUE;
+ }
+ if (strcmp(key, "ssl_protocols") == 0) {
+ obsolete(ctx, "%s has been replaced by ssl_min_protocol", key);
+ const char *min_protocol, *error;
+ if (ssl_protocols_to_min_protocol(value, &min_protocol, &error) < 0) {
+ i_error("Could not find a minimum ssl_min_protocol "
+ "setting from ssl_protocols = %s: %s",
+ value, error);
+ return TRUE;
+ }
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ "ssl_min_protocol", min_protocol);
+ return TRUE;
+ }
+ if (strcmp(key, "sieve") == 0 ||
+ strcmp(key, "sieve_storage") == 0) {
+ if (strcmp(key, "sieve_storage") == 0)
+ obsolete(ctx, "sieve_storage has been moved into plugin { sieve_dir }");
+ else
+ obsolete(ctx, "%s has been moved into plugin {} section", key);
+
+ config_apply_line(ctx, "", "plugin=", NULL);
+ config_apply_line(ctx, key,
+ t_strdup_printf("plugin/%s=%s", key, value), NULL);
+ return TRUE;
+ }
+ if (strcmp(key, "fsync_disable") == 0) {
+ if (strcasecmp(value, "yes") == 0)
+ value = "never";
+ else if (strcasecmp(value, "no") == 0)
+ value = "optimized";
+ set_rename(ctx, key, "mail_fsync", value);
+ return TRUE;
+ }
+ if (strcmp(key, "dbox_rotate_size") == 0) {
+ set_rename(ctx, key, "mdbox_rotate_size", value);
+ return TRUE;
+ }
+ if (str_begins(key, "mail_cache_compress_")) {
+ const char *new_key = t_strconcat("mail_cache_purge_", key+20, NULL);
+ set_rename(ctx, key, new_key, value);
+ return TRUE;
+ }
+ if (strcmp(key, "imap_client_workarounds") == 0) {
+ char **args, **arg;
+
+ args = p_strsplit_spaces(pool_datastack_create(), value, " ,");
+ for (arg = args; *arg != NULL; arg++) {
+ if (strcmp(*arg, "outlook-idle") == 0) {
+ *arg = "";
+ obsolete(ctx, "imap_client_workarounds=outlook-idle is no longer necessary");
+ } else if (strcmp(*arg, "netscape-eoh") == 0) {
+ *arg = "";
+ obsolete(ctx, "imap_client_workarounds=netscape-eoh is no longer supported");
+ }
+ }
+ value = t_strarray_join((void *)args, " ");
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ key, value);
+ return TRUE;
+ }
+
+ if (strcmp(key, "login_dir") == 0 ||
+ strcmp(key, "dbox_rotate_min_size") == 0 ||
+ strcmp(key, "dbox_rotate_days") == 0 ||
+ strcmp(key, "director_consistent_hashing") == 0 ||
+ strcmp(key, "mail_log_max_lines_per_sec") == 0 ||
+ strcmp(key, "maildir_copy_preserve_filename") == 0) {
+ obsolete(ctx, "%s has been removed", key);
+ return TRUE;
+ }
+ if (ctx->old->auth_section == 1) {
+ if (!str_begins(key, "auth_"))
+ key = t_strconcat("auth_", key, NULL);
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ key, value);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+config_apply_login_set(struct config_parser_context *ctx,
+ struct config_section_stack *old_section,
+ const char *old_key, const char *key, const char *value)
+{
+ obsolete(ctx, "%s has been replaced by service { %s }", old_key, key);
+
+ if (config_filter_match(&old_section->filter, &imap_filter)) {
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/imap-login/%s=%s", key, value), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &pop3_filter)) {
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/pop3-login/%s=%s", key, value), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &managesieve_filter)) {
+ /* if pigeonhole isn't installed, this fails.
+ just ignore it then.. */
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/managesieve-login/%s=%s", key, value), NULL);
+ ctx->error = NULL;
+ }
+}
+
+static void
+config_apply_mail_set(struct config_parser_context *ctx,
+ struct config_section_stack *old_section,
+ const char *old_key, const char *key, const char *value)
+{
+ obsolete(ctx, "%s has been replaced by service { %s }", old_key, key);
+
+ if (config_filter_match(&old_section->filter, &imap_filter)) {
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/imap/%s=%s", key,value), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &pop3_filter)) {
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/pop3/%s=%s", key,value), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &managesieve_filter)) {
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/managesieve/%s=%s", key,value), NULL);
+ ctx->error = NULL;
+ }
+}
+
+static void
+config_apply_auth_set(struct config_parser_context *ctx,
+ const char *old_key, const char *key, const char *value)
+{
+ obsolete(ctx, "%s has been replaced by service auth { %s }", old_key, key);
+ config_apply_line(ctx, key,
+ t_strdup_printf("service/auth/%s=%s", key,value), NULL);
+}
+
+static bool listen_has_port(const char *str)
+{
+ const char *const *addrs;
+
+ if (strchr(str, ':') == NULL)
+ return FALSE;
+
+ addrs = t_strsplit_spaces(str, ", ");
+ for (; *addrs != NULL; addrs++) {
+ if (strcmp(*addrs, "*") != 0 &&
+ strcmp(*addrs, "::") != 0 &&
+ strcmp(*addrs, "[::]") != 0 &&
+ !is_ipv4_address(*addrs) &&
+ !is_ipv6_address(*addrs))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+old_settings_handle_proto(struct config_parser_context *ctx,
+ const char *key, const char *value)
+{
+ struct config_section_stack *old_section = ctx->cur_section;
+ const char *p;
+ uoff_t size;
+ bool root;
+
+ while (ctx->cur_section->prev != NULL)
+ ctx->cur_section = ctx->cur_section->prev;
+
+ root = config_filter_match(&old_section->filter, &any_filter);
+
+ if (strcmp(key, "ssl_listen") == 0 ||
+ (strcmp(key, "listen") == 0 &&
+ (listen_has_port(value) || !root))) {
+ const char *ssl = strcmp(key, "ssl_listen") == 0 ? "s" : "";
+
+ if (*value == '\0') {
+ /* default */
+ return TRUE;
+ }
+ p = strrchr(value, ':');
+ if (p != NULL && listen_has_port(value)) {
+ obsolete(ctx, "%s=..:port has been replaced by service { inet_listener { port } }", key);
+ value = t_strdup_until(value, p++);
+ if (config_filter_match(&old_section->filter, &imap_filter)) {
+ config_apply_line(ctx, "port",
+ t_strdup_printf("service/imap-login/inet_listener/imap%s/port=%s", ssl, p), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &pop3_filter)) {
+ config_apply_line(ctx, "port",
+ t_strdup_printf("service/pop3-login/inet_listener/pop3%s/port=%s", ssl, p), NULL);
+ }
+ if (*ssl == '\0' &&
+ config_filter_match(&old_section->filter, &managesieve_filter)) {
+ config_apply_line(ctx, "port",
+ t_strdup_printf("service/managesieve-login/inet_listener/managesieve/port=%s", p), NULL);
+ ctx->error = NULL;
+ }
+ }
+ if (root && *ssl == '\0') {
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ key, value);
+ } else {
+ obsolete(ctx, "protocol { %s } has been replaced by service { inet_listener { address } }", key);
+ if (config_filter_match(&old_section->filter, &imap_filter)) {
+ config_apply_line(ctx, "address",
+ t_strdup_printf("service/imap-login/inet_listener/imap%s/address=%s", ssl, value), NULL);
+ }
+ if (config_filter_match(&old_section->filter, &pop3_filter)) {
+ config_apply_line(ctx, "address",
+ t_strdup_printf("service/pop3-login/inet_listener/pop3%s/address=%s", ssl, value), NULL);
+ }
+ if (*ssl == '\0' &&
+ config_filter_match(&old_section->filter, &managesieve_filter)) {
+ config_apply_line(ctx, "address",
+ t_strdup_printf("service/managesieve-login/inet_listener/managesieve/address=%s", value), NULL);
+ ctx->error = NULL;
+ }
+ }
+ return TRUE;
+ }
+ if (strcmp(key, "login_chroot") == 0) {
+ if (strcmp(value, "no") == 0)
+ value = "";
+ else
+ value = "login";
+
+ config_apply_login_set(ctx, old_section, key, "chroot", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_user") == 0) {
+ config_apply_login_set(ctx, old_section, key, "user", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_executable") == 0) {
+ config_apply_login_set(ctx, old_section, key, "executable", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_process_size") == 0) {
+ config_apply_login_set(ctx, old_section, key, "vsz_limit",
+ t_strconcat(value, " M", NULL));
+ return TRUE;
+ }
+ if (strcmp(key, "login_process_per_connection") == 0) {
+ config_apply_login_set(ctx, old_section, key, "service_count",
+ strcmp(value, "no") == 0 ? "0" : "1");
+ return TRUE;
+ }
+ if (strcmp(key, "login_processes_count") == 0) {
+ config_apply_login_set(ctx, old_section, key, "process_min_avail", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_max_processes_count") == 0) {
+ config_apply_login_set(ctx, old_section, key, "process_limit", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_max_connections") == 0) {
+ config_apply_login_set(ctx, old_section, key, "client_limit", value);
+ return TRUE;
+ }
+ if (strcmp(key, "login_process_size") == 0) {
+ config_apply_login_set(ctx, old_section, key, "vsz_limit",
+ t_strconcat(value, " M", NULL));
+ return TRUE;
+ }
+
+ if (strcmp(key, "max_mail_processes") == 0) {
+ config_apply_mail_set(ctx, old_section, key, "process_limit", value);
+ return TRUE;
+ }
+ if (strcmp(key, "mail_executable") == 0) {
+ config_apply_mail_set(ctx, old_section, key, "executable", value);
+ return TRUE;
+ }
+ if (strcmp(key, "mail_process_size") == 0) {
+ config_apply_mail_set(ctx, old_section, key, "vsz_limit",
+ t_strconcat(value, " M", NULL));
+ return TRUE;
+ }
+ if (strcmp(key, "mail_drop_priv_before_exec") == 0) {
+ config_apply_mail_set(ctx, old_section, key, "drop_priv_before_exec", value);
+ return TRUE;
+ }
+
+ if (ctx->old->auth_section == 1) {
+ if (!str_begins(key, "auth_"))
+ key = t_strconcat("auth_", key, NULL);
+ }
+
+ if (strcmp(key, "auth_executable") == 0) {
+ config_apply_auth_set(ctx, key, "executable", value);
+ return TRUE;
+ }
+ if (strcmp(key, "auth_process_size") == 0) {
+ config_apply_auth_set(ctx, key, "vsz_limit",
+ t_strconcat(value, " M", NULL));
+ return TRUE;
+ }
+ if (strcmp(key, "auth_user") == 0) {
+ config_apply_auth_set(ctx, key, "user", value);
+ return TRUE;
+ }
+ if (strcmp(key, "auth_chroot") == 0) {
+ config_apply_auth_set(ctx, key, "chroot", value);
+ return TRUE;
+ }
+ if (strcmp(key, "auth_cache_size") == 0 &&
+ str_to_uoff(value, &size) == 0 && size > 0 && size < 1024) {
+ obsolete(ctx, "auth_cache_size value no longer defaults to "
+ "megabytes. Use %sM", value);
+ config_apply_line(ctx, key,
+ t_strdup_printf("%s=%sM", key, value), NULL);
+ return TRUE;
+ }
+ if (strcmp(key, "auth_count") == 0) {
+ if (strcmp(value, "count") == 0)
+ obsolete(ctx, "auth_count has been removed");
+ else
+ obsolete(ctx, "auth_count has been removed, and its value must be 1");
+ return TRUE;
+ }
+ if (ctx->old->socket_listen_section == 2) {
+ const char **p = NULL;
+
+ if (strcmp(key, "path") == 0)
+ p = &ctx->old->socket_set.path;
+ else if (strcmp(key, "mode") == 0)
+ p = &ctx->old->socket_set.mode;
+ else if (strcmp(key, "user") == 0)
+ p = &ctx->old->socket_set.user;
+ else if (strcmp(key, "group") == 0)
+ p = &ctx->old->socket_set.group;
+
+ if (p != NULL) {
+ *p = p_strdup(ctx->pool, value);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool old_auth_section(struct config_parser_context *ctx,
+ const char *key, const char *value)
+{
+ if (ctx->old->auth_section == 0 && ctx->old->seen_auth_section) {
+ obsolete(ctx, "Multiple auth sections are no longer supported");
+ return FALSE;
+ }
+ ctx->old->seen_auth_section = TRUE;
+ i_zero(&ctx->old->socket_set);
+
+ ctx->old->auth_section++;
+ if ((strcmp(key, "passdb") == 0 || strcmp(key, "userdb") == 0) &&
+ ctx->old->auth_section == 2) {
+ obsolete(ctx, "%s %s {} has been replaced by %s { driver=%s }",
+ key, value, key, value);
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_SECTION_BEGIN, key, "");
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_KEYVALUE,
+ "driver", value);
+ return TRUE;
+ }
+ if (strcmp(key, "socket") == 0 && ctx->old->auth_section == 2) {
+ if (strcmp(value, "connect") == 0) {
+ obsolete(ctx, "socket connect {} is no longer supported, configure external auth server separately");
+ return FALSE;
+ }
+ if (strcmp(value, "listen") != 0)
+ return FALSE;
+
+ /* socket listen { .. } */
+ ctx->old->socket_listen_section++;
+ return TRUE;
+ }
+
+ if (ctx->old->socket_listen_section > 0)
+ ctx->old->socket_listen_section++;
+ if ((strcmp(key, "master") == 0 || strcmp(key, "client") == 0) &&
+ ctx->old->socket_listen_section == 2) {
+ ctx->old->socket_set.master = strcmp(key, "master") == 0;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void socket_apply(struct config_parser_context *ctx)
+{
+ const struct socket_set *set = &ctx->old->socket_set;
+ const char *path, *prefix;
+ size_t len;
+ bool master_suffix;
+
+ if (set->path == NULL) {
+ ctx->error = "socket listen {} is missing path";
+ return;
+ }
+ path = set->path;
+ len = strlen(ctx->old->base_dir);
+ if (str_begins(path, ctx->old->base_dir) &&
+ path[len] == '/')
+ path += len + 1;
+
+ len = strlen(path);
+ master_suffix = len >= 7 &&
+ (strcmp(path + len - 7, "-master") == 0 ||
+ strcmp(path + len - 7, "-userdb") == 0);
+
+ if (set->master && !master_suffix) {
+ ctx->error = "socket listen { master { path=.. } } must end with -master (or -userdb) suffix";
+ return;
+ } else if (!set->master && master_suffix) {
+ ctx->error = "socket listen { client { path=.. } } must end not with -master or -userdb suffix";
+ return;
+ }
+
+ config_apply_line(ctx, "unix_listener",
+ t_strdup_printf("service/auth/unix_listener=%s", settings_section_escape(path)), path);
+ prefix = t_strdup_printf("service/auth/unix_listener/%s", settings_section_escape(path));
+ if (set->mode != NULL) {
+ config_apply_line(ctx, "mode",
+ t_strdup_printf("%s/mode=%s", prefix, set->mode), NULL);
+ }
+ if (set->user != NULL) {
+ config_apply_line(ctx, "user",
+ t_strdup_printf("%s/user=%s", prefix, set->user), NULL);
+ }
+ if (set->group != NULL) {
+ config_apply_line(ctx, "group",
+ t_strdup_printf("%s/group=%s", prefix, set->group), NULL);
+ }
+ i_zero(&ctx->old->socket_set);
+}
+
+static bool
+old_settings_handle_path(struct config_parser_context *ctx,
+ const char *key, const char *value)
+{
+ if (strcmp(str_c(ctx->str), "plugin/0/") == 0) {
+ if (strcmp(key, "imap_zlib_compress_level") == 0) {
+ obsolete(ctx, "%s has been replaced by imap_compress_deflate_level", key);
+ config_apply_line(ctx, key,
+ t_strdup_printf("plugin/0/imap_compress_deflate_level=%s", value), NULL);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool old_settings_handle(struct config_parser_context *ctx,
+ enum config_line_type type,
+ const char *key, const char *value)
+{
+ switch (type) {
+ case CONFIG_LINE_TYPE_SKIP:
+ case CONFIG_LINE_TYPE_CONTINUE:
+ case CONFIG_LINE_TYPE_ERROR:
+ case CONFIG_LINE_TYPE_INCLUDE:
+ case CONFIG_LINE_TYPE_INCLUDE_TRY:
+ case CONFIG_LINE_TYPE_KEYFILE:
+ case CONFIG_LINE_TYPE_KEYVARIABLE:
+ break;
+ case CONFIG_LINE_TYPE_KEYVALUE:
+ if (ctx->pathlen == 0) {
+ struct config_section_stack *old_section =
+ ctx->cur_section;
+ bool ret;
+
+ ret = old_settings_handle_proto(ctx, key, value);
+ ctx->cur_section = old_section;
+ if (ret)
+ return TRUE;
+
+ return old_settings_handle_root(ctx, key, value);
+ }
+ return old_settings_handle_path(ctx, key, value);
+ case CONFIG_LINE_TYPE_SECTION_BEGIN:
+ if (ctx->old->auth_section > 0)
+ return old_auth_section(ctx, key, value);
+ else if (ctx->pathlen == 0 && strcmp(key, "auth") == 0) {
+ obsolete(ctx, "add auth_ prefix to all settings inside auth {} and remove the auth {} section completely");
+ ctx->old->auth_section = 1;
+ return TRUE;
+ } else if (ctx->pathlen == 0 && strcmp(key, "protocol") == 0 &&
+ strcmp(value, "managesieve") == 0) {
+ obsolete(ctx, "protocol managesieve {} has been replaced by protocol sieve { }");
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_SECTION_BEGIN,
+ "protocol", "sieve");
+ return TRUE;
+ } else if (ctx->pathlen == 0 && strcmp(key, "service") == 0 &&
+ strcmp(value, "dns_client") == 0) {
+ obsolete(ctx, "service dns_client {} has been replaced by service dns-client { }");
+ config_parser_apply_line(ctx, CONFIG_LINE_TYPE_SECTION_BEGIN,
+ "service", "dns-client");
+ return TRUE;
+ }
+ break;
+ case CONFIG_LINE_TYPE_SECTION_END:
+ if (ctx->old->auth_section > 0) {
+ if (--ctx->old->auth_section == 0)
+ return TRUE;
+ }
+ if (ctx->old->socket_listen_section > 0) {
+ if (ctx->old->socket_listen_section == 2)
+ socket_apply(ctx);
+ ctx->old->socket_listen_section--;
+ return TRUE;
+ }
+ break;
+ }
+ return FALSE;
+}
+
+void old_settings_init(struct config_parser_context *ctx)
+{
+ ctx->old = p_new(ctx->pool, struct old_set_parser, 1);
+ ctx->old->base_dir = PKG_RUNDIR;
+}
+
+void old_settings_deinit_global(void)
+{
+ i_free(ssl_dh_value);
+}
diff --git a/src/config/old-set-parser.h b/src/config/old-set-parser.h
new file mode 100644
index 0000000..e95b3e1
--- /dev/null
+++ b/src/config/old-set-parser.h
@@ -0,0 +1,16 @@
+#ifndef OLD_SET_PARSER_H
+#define OLD_SET_PARSER_H
+
+#include "config-parser-private.h"
+
+struct config_parser_context;
+
+bool old_settings_ssl_dh_load(const char **value, const char **error_r);
+
+bool old_settings_handle(struct config_parser_context *ctx,
+ enum config_line_type type,
+ const char *key, const char *value);
+void old_settings_init(struct config_parser_context *ctx);
+void old_settings_deinit_global(void);
+
+#endif
diff --git a/src/config/settings-get.pl b/src/config/settings-get.pl
new file mode 100755
index 0000000..ca124eb
--- /dev/null
+++ b/src/config/settings-get.pl
@@ -0,0 +1,162 @@
+#!/usr/bin/env perl
+use strict;
+
+print "/* WARNING: THIS FILE IS GENERATED - DO NOT PATCH!\n";
+print " It's not enough alone in any case, because the defaults may be\n";
+print " coming from the individual *-settings.c in some situations. If you\n";
+print " wish to modify defaults, change the other *-settings.c files and\n";
+print " just delete this file. This file will be automatically regenerated\n";
+print " by make. (This file is distributed in the tarball only because some\n";
+print " systems might not have Perl installed.) */\n";
+print '#include "lib.h"'."\n";
+print '#include "array.h"'."\n";
+print '#include "str.h"'."\n";
+print '#include "ipwd.h"'."\n";
+print '#include "var-expand.h"'."\n";
+print '#include "file-lock.h"'."\n";
+print '#include "fsync-mode.h"'."\n";
+print '#include "hash-format.h"'."\n";
+print '#include "net.h"'."\n";
+print '#include "unichar.h"'."\n";
+print '#include "hash-method.h"'."\n";
+print '#include "settings-parser.h"'."\n";
+print '#include "message-header-parser.h"'."\n";
+print '#include "all-settings.h"'."\n";
+print '#include <stddef.h>'."\n";
+print '#include <unistd.h>'."\n";
+print '#define CONFIG_BINARY'."\n";
+print 'extern buffer_t config_all_services_buf;';
+
+my @services = ();
+my @service_ifdefs = ();
+my %parsers = {};
+
+foreach my $file (@ARGV) {
+ my $f;
+ open($f, $file) || die "Can't open $file: $@";
+
+ my $state = 0;
+ my $file_contents = "";
+ my $externs = "";
+ my $code = "";
+ my %funcs;
+ my $cur_name = "";
+ my $ifdef = "";
+ my $state_ifdef = 0;
+
+ while (<$f>) {
+ my $write = 0;
+ if ($state == 0) {
+ if (/struct .*_settings \{/ ||
+ /struct setting_define.*\{/ ||
+ /struct .*_default_settings = \{/) {
+ $state++;
+ } elsif (/^struct service_settings (.*) = \{/) {
+ $state++;
+ if ($ifdef eq "") {
+ $state_ifdef = 0;
+ } else {
+ $_ = $ifdef."\n".$_;
+ $state_ifdef = 1;
+ }
+ push @services, $1;
+ push @service_ifdefs, $ifdef;
+ } elsif (/^(static )?const struct setting_parser_info (.*) = \{/) {
+ $cur_name = $2;
+ $state++ if ($cur_name !~ /^\*default_/);
+ } elsif (/^extern const struct setting_parser_info (.*);/) {
+ $externs .= "extern const struct setting_parser_info $1;\n";
+ } elsif (/\/\* <settings checks> \*\//) {
+ $state = 4;
+ $code .= $_;
+ }
+
+ if (/(^#ifdef .*)$/ || /^(#if .*)$/) {
+ $ifdef = $1;
+ } else {
+ $ifdef = "";
+ }
+
+ if (/#define.*DEF/ || /^#undef.*DEF/ || /ARRAY_DEFINE_TYPE.*_settings/) {
+ $write = 1;
+ $state = 2 if (/\\$/);
+ }
+ } elsif ($state == 2) {
+ $write = 1;
+ $state = 0 if (!/\\$/);
+ } elsif ($state == 4) {
+ $code .= $_;
+ $state = 0 if (/\/\* <\/settings checks> \*\//);
+ }
+
+ if ($state == 1 || $state == 3) {
+ if ($state == 1) {
+ if (/\.module_name = "(.*)"/) {
+ $parsers{$cur_name} = $1;
+ }
+ if (/DEFLIST.*".*",(.*)$/) {
+ my $value = $1;
+ if ($value =~ /.*&(.*)\)/) {
+ $parsers{$1} = 0;
+ $externs .= "extern const struct setting_parser_info $1;\n";
+ } else {
+ $state = 3;
+ }
+ }
+ } elsif ($state == 3) {
+ if (/.*&(.*)\)/) {
+ $parsers{$1} = 0;
+ }
+ }
+
+ s/^static const (struct master_settings master_default_settings)/$1/;
+
+ $write = 1;
+ if (/};/) {
+ $state = 0;
+ if ($state_ifdef) {
+ $_ .= "#endif\n";
+ $state_ifdef = 0;
+ }
+ }
+ }
+
+ $file_contents .= $_ if ($write);
+ }
+
+ print "/* $file */\n";
+ print $externs;
+ print $code;
+ print $file_contents;
+
+ close $f;
+}
+
+print "static struct service_settings *config_all_services[] = {\n";
+
+for (my $i = 0; $i < scalar(@services); $i++) {
+ my $ifdef = $service_ifdefs[$i];
+ print "$ifdef\n" if ($ifdef ne "");
+ print "\t&".$services[$i].",\n";
+ print "#endif\n" if ($ifdef ne "");
+}
+print "};\n";
+print "buffer_t config_all_services_buf = {\n";
+print "\t{ { config_all_services, sizeof(config_all_services) } }\n";
+print "};\n";
+
+print "const struct setting_parser_info *all_default_roots[] = {\n";
+print "\t&master_service_setting_parser_info,\n";
+print "\t&master_service_ssl_setting_parser_info,\n";
+print "\t&master_service_ssl_server_setting_parser_info,\n";
+print "\t&smtp_submit_setting_parser_info,\n";
+foreach my $name (sort(keys %parsers)) {
+ my $module = $parsers{$name};
+ next if (!$module);
+
+ print "\t&".$name.", \n";
+}
+print "\tNULL\n";
+print "};\n";
+print "const struct setting_parser_info *const *all_roots = all_default_roots;\n";
+print "ARRAY_TYPE(service_settings) *default_services = &master_default_settings.services;\n";
diff --git a/src/config/sysinfo-get.c b/src/config/sysinfo-get.c
new file mode 100644
index 0000000..dffebe6
--- /dev/null
+++ b/src/config/sysinfo-get.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mountpoint.h"
+#include "strescape.h"
+#include "sysinfo-get.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_UTSNAME_H
+# include <sys/utsname.h>
+#endif
+
+static bool readfile(const char *path, const char **data_r)
+{
+ char buf[1024];
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return FALSE;
+ ret = read(fd, buf, sizeof(buf));
+ i_close_fd(&fd);
+ if (ret <= 0)
+ return FALSE;
+
+ *data_r = t_strndup(buf, ret);
+ return TRUE;
+}
+
+static bool lsb_distro_get(const char *path, const char **name_r)
+{
+ const char *data, *const *p, *str, *end;
+
+ if (!readfile(path, &data))
+ return FALSE;
+
+ for (p = t_strsplit(data, "\n"); *p != NULL; p++) {
+ if (str_begins(*p, "DISTRIB_DESCRIPTION="))
+ break;
+ }
+ if (*p == NULL)
+ return FALSE;
+
+ str = t_strcut(*p + 20, '\n');
+ if (*str != '"')
+ *name_r = str;
+ else {
+ end = strrchr(++str, '"');
+ *name_r = str_unescape(p_strdup_until(unsafe_data_stack_pool,
+ str, end));
+ }
+ return TRUE;
+}
+
+static const char *distro_get(void)
+{
+ static const char *files[] = {
+ "", "/etc/redhat-release",
+ "", "/etc/SuSE-release",
+ "", "/etc/mandriva-release",
+ "", "/etc/fedora-release",
+ "", "/etc/sourcemage-release",
+ "", "/etc/slackware-version",
+ "", "/etc/gentoo-release",
+ "Debian ", "/etc/debian_version",
+ NULL
+ };
+ const char *name;
+ unsigned int i;
+
+ if (lsb_distro_get("/etc/lsb-release", &name))
+ return name;
+ for (i = 0; files[i] != NULL; i += 2) {
+ if (readfile(files[i+1], &name)) {
+ return t_strconcat(files[i], t_strcut(name, '\n'),
+ NULL);
+ }
+ }
+ return "";
+}
+
+static const char *filesystem_get(const char *mail_location)
+{
+ struct mountpoint mp;
+ const char *path;
+
+ path = strchr(mail_location, ':');
+ if (path == NULL)
+ path = mail_location;
+ else
+ path = t_strcut(path + 1, ':');
+ if (*path == '~') {
+ /* we don't know where users' home dirs are */
+ return "";
+ }
+ path = t_strcut(path, '%');
+ if (strlen(path) <= 1)
+ return "";
+
+ /* all in all it seems we can support only /<path>/%u style location */
+ if (mountpoint_get(path, pool_datastack_create(), &mp) < 0)
+ return "";
+ return mp.type == NULL ? "" : mp.type;
+}
+
+const char *sysinfo_get(const char *mail_location)
+{
+ const char *distro = "", *fs, *uname_info = "";
+#ifdef HAVE_SYS_UTSNAME_H
+ struct utsname u;
+
+ if (uname(&u) < 0)
+ i_error("uname() failed: %m");
+ else {
+ uname_info = t_strdup_printf("%s %s %s",
+ u.sysname, u.release, u.machine);
+ }
+ if (strcmp(u.sysname, "Linux") == 0)
+ distro = distro_get();
+#endif
+ fs = filesystem_get(mail_location);
+ if (*uname_info == '\0' && *distro == '\0' && *fs == '\0')
+ return "";
+ return t_strdup_printf("OS: %s %s %s %s %s", u.sysname, u.release, u.machine, distro, fs);
+}
diff --git a/src/config/sysinfo-get.h b/src/config/sysinfo-get.h
new file mode 100644
index 0000000..0f319cb
--- /dev/null
+++ b/src/config/sysinfo-get.h
@@ -0,0 +1,6 @@
+#ifndef SYSINFO_GET_H
+#define SYSINFO_GET_H
+
+const char *sysinfo_get(const char *mail_location);
+
+#endif
diff --git a/src/config/test-config-parser.c b/src/config/test-config-parser.c
new file mode 100644
index 0000000..baf91a1
--- /dev/null
+++ b/src/config/test-config-parser.c
@@ -0,0 +1,170 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream.h"
+#include "service-settings.h"
+#include "settings-parser.h"
+#include "config-filter.h"
+#include "test-common.h"
+#include "all-settings.h"
+#include "config-parser.h"
+
+#define TEST_CONFIG_FILE ".test-config"
+
+static ARRAY_TYPE(service_settings) services = ARRAY_INIT;
+ARRAY_TYPE(service_settings) *default_services = &services;
+
+struct test_settings {
+ const char *key;
+ const char *key2;
+ const char *key3;
+ const char *key4;
+ const char *key5;
+ const char *pop3_deleted_flag;
+ const char *env_key;
+ const char *env_key2;
+ const char *env_key3;
+ const char *env_key4;
+ const char *env_key5;
+ const char *protocols;
+};
+
+static const struct setting_define test_settings_defs[] = {
+ SETTING_DEFINE_STRUCT_STR("key", key, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("key2", key2, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("key3", key3, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("key4", key4, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("key5", key5, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("pop3_deleted_flag", pop3_deleted_flag, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("env_key", env_key, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("env_key2", env_key2, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("env_key3", env_key3, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("env_key4", env_key4, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("env_key5", env_key5, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("protocols", protocols, struct test_settings),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct test_settings test_settings_defaults = {
+ .key = "",
+ .key2 = "",
+ .key3 = "",
+ .key4 = "",
+ .key5 = "",
+ .pop3_deleted_flag = "",
+ .env_key = "",
+ .env_key2 = "",
+ .env_key3 = "",
+ .env_key4 = "",
+ .env_key5 = "",
+ .protocols = "pop3",
+};
+
+const struct setting_parser_info test_settings_root = {
+ .module_name = "test",
+ .defines = test_settings_defs,
+ .defaults = &test_settings_defaults,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct test_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = NULL,
+};
+
+static const struct setting_parser_info *const roots[] = {
+ &test_settings_root,
+ NULL
+};
+
+const struct setting_parser_info *const *all_roots = roots;
+
+static void write_config_file(const char *contents)
+{
+ struct ostream *os = o_stream_create_file(TEST_CONFIG_FILE, 0, 0600, 0);
+ o_stream_nsend_str(os, contents);
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_unref(&os);
+}
+
+static void test_config_parser(void)
+{
+ const char *error = NULL;
+
+ test_begin("config_parse_file");
+
+ write_config_file(
+"# comment\n"
+"key=value\n"
+"key2 = \\$escape \\escape \\\"escape\\\"\n"
+"key3 = value\n"
+"key3 = $key3 nothervalue\n"
+"key3 = yetanother value $key3 right here\n"
+"key4 = \" $key3 \"\n"
+"key5 = ' $key4 '\n"
+"pop3_deleted_flag = \"$Deleted\"\n"
+"env_key=$ENV:foo\n"
+"env_key=$env_key $ENV:bar\n"
+"env_key=$env_key \"$env_key\"\n"
+"env_key2 = foo$ENV:FOO bar\n"
+"env_key3 = $ENV:FOO$ENV:FOO bar\n"
+"env_key4 = $ENV:foo $ENV:bar $key\n"
+"env_key5 = $ENV:foo $ENV:foo\n"
+"protocols = $protocols imap\n"
+ );
+
+ putenv("foo=test1");
+ putenv("bar=test2");
+ putenv("FOO$ENV:FOO=works");
+
+ test_assert(config_parse_file(TEST_CONFIG_FILE, TRUE, NULL, &error) == 1);
+ if (error != NULL)
+ i_error("config_parse_file(): %s", error);
+
+ /* get the parsed output */
+ const struct test_settings *set =
+ settings_parser_get(config_module_parsers[0].parser);
+ test_assert_strcmp(set->key, "value");
+ test_assert_strcmp(set->key2, "\\$escape \\escape \\\"escape\\\"");
+ test_assert_strcmp(set->key3, "yetanother value value nothervalue right here");
+ test_assert_strcmp(set->key4, " $key3 ");
+ test_assert_strcmp(set->key5, " $key4 ");
+ test_assert_strcmp(set->pop3_deleted_flag, "$Deleted");
+ test_assert_strcmp(set->env_key, "test1 test2 \"$env_key\"");
+ test_assert_strcmp(set->env_key2, "foo$ENV:FOO bar");
+ test_assert_strcmp(set->env_key3, "works bar");
+ test_assert_strcmp(set->env_key4, "test1 test2 value");
+ test_assert_strcmp(set->env_key5, "test1 test1");
+ test_assert_strcmp(set->protocols, "pop3 imap");
+
+ /* try again unexpanded */
+ test_assert(config_parse_file(TEST_CONFIG_FILE, FALSE, NULL, &error) == 1);
+ set = settings_parser_get(config_module_parsers[0].parser);
+
+ test_assert_strcmp(set->key, "value");
+ test_assert_strcmp(set->key2, "\\$escape \\escape \\\"escape\\\"");
+ test_assert_strcmp(set->key3, "yetanother value value nothervalue right here");
+ test_assert_strcmp(set->key4, " $key3 ");
+ test_assert_strcmp(set->key5, " $key4 ");
+ test_assert_strcmp(set->pop3_deleted_flag, "$Deleted");
+ test_assert_strcmp(set->env_key, "$ENV:foo $ENV:bar \"$env_key\"");
+ test_assert_strcmp(set->env_key2, "foo$ENV:FOO bar");
+ test_assert_strcmp(set->env_key3, "$ENV:FOO$ENV:FOO bar");
+ test_assert_strcmp(set->env_key4, "$ENV:foo $ENV:bar $key");
+ test_assert_strcmp(set->env_key5, "$ENV:foo $ENV:foo");
+ test_assert_strcmp(set->protocols, "pop3 imap");
+
+ config_filter_deinit(&config_filter);
+ config_parser_deinit();
+ i_unlink_if_exists(TEST_CONFIG_FILE);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_config_parser,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/dict/Makefile.am b/src/dict/Makefile.am
new file mode 100644
index 0000000..e408046
--- /dev/null
+++ b/src/dict/Makefile.am
@@ -0,0 +1,43 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = dict
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-sql \
+ -DDICT_MODULE_DIR=\""$(moduledir)/dict"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+dict_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+libs = \
+ ../lib-dict-backend/libdict_backend.la \
+ $(LIBDOVECOT_SQL)
+
+dict_LDADD = \
+ $(libs) \
+ $(LIBDOVECOT) \
+ $(DICT_LIBS) \
+ $(SQL_LIBS) \
+ -lm
+
+dict_DEPENDENCIES = $(libs) $(LIBDOVECOT_DEPS)
+
+dict_SOURCES = \
+ dict-connection.c \
+ dict-commands.c \
+ dict-settings.c \
+ dict-init-cache.c \
+ main.c
+
+noinst_HEADERS = \
+ dict-connection.h \
+ dict-commands.h \
+ dict-settings.h \
+ dict-init-cache.h \
+ main.h
diff --git a/src/dict/Makefile.in b/src/dict/Makefile.in
new file mode 100644
index 0000000..f4b323f
--- /dev/null
+++ b/src/dict/Makefile.in
@@ -0,0 +1,847 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = dict$(EXEEXT)
+subdir = src/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 $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_dict_OBJECTS = dict-connection.$(OBJEXT) dict-commands.$(OBJEXT) \
+ dict-settings.$(OBJEXT) dict-init-cache.$(OBJEXT) \
+ main.$(OBJEXT)
+dict_OBJECTS = $(am_dict_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = ../lib-dict-backend/libdict_backend.la \
+ $(am__DEPENDENCIES_1)
+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 =
+dict_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(dict_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-commands.Po \
+ ./$(DEPDIR)/dict-connection.Po ./$(DEPDIR)/dict-init-cache.Po \
+ ./$(DEPDIR)/dict-settings.Po ./$(DEPDIR)/main.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 = $(dict_SOURCES)
+DIST_SOURCES = $(dict_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-sql \
+ -DDICT_MODULE_DIR=\""$(moduledir)/dict"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+dict_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+libs = \
+ ../lib-dict-backend/libdict_backend.la \
+ $(LIBDOVECOT_SQL)
+
+dict_LDADD = \
+ $(libs) \
+ $(LIBDOVECOT) \
+ $(DICT_LIBS) \
+ $(SQL_LIBS) \
+ -lm
+
+dict_DEPENDENCIES = $(libs) $(LIBDOVECOT_DEPS)
+dict_SOURCES = \
+ dict-connection.c \
+ dict-commands.c \
+ dict-settings.c \
+ dict-init-cache.c \
+ main.c
+
+noinst_HEADERS = \
+ dict-connection.h \
+ dict-commands.h \
+ dict-settings.h \
+ dict-init-cache.h \
+ main.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/dict/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+dict$(EXEEXT): $(dict_OBJECTS) $(dict_DEPENDENCIES) $(EXTRA_dict_DEPENDENCIES)
+ @rm -f dict$(EXEEXT)
+ $(AM_V_CCLD)$(dict_LINK) $(dict_OBJECTS) $(dict_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-init-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dict-commands.Po
+ -rm -f ./$(DEPDIR)/dict-connection.Po
+ -rm -f ./$(DEPDIR)/dict-init-cache.Po
+ -rm -f ./$(DEPDIR)/dict-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+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-commands.Po
+ -rm -f ./$(DEPDIR)/dict-connection.Po
+ -rm -f ./$(DEPDIR)/dict-init-cache.Po
+ -rm -f ./$(DEPDIR)/dict-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/dict/dict-commands.c b/src/dict/dict-commands.c
new file mode 100644
index 0000000..da6acce
--- /dev/null
+++ b/src/dict/dict-commands.c
@@ -0,0 +1,797 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "stats-dist.h"
+#include "time-util.h"
+#include "dict-private.h"
+#include "dict-client.h"
+#include "dict-settings.h"
+#include "dict-connection.h"
+#include "dict-commands.h"
+#include "main.h"
+
+#define DICT_CLIENT_PROTOCOL_TIMINGS_MIN_VERSION 1
+#define DICT_CLIENT_PROTOCOL_UNORDERED_MIN_VERSION 1
+#define DICT_OUTPUT_OPTIMAL_SIZE 1024
+
+struct dict_cmd_func {
+ enum dict_protocol_cmd cmd;
+ int (*func)(struct dict_connection_cmd *cmd, const char *const *args);
+};
+
+struct dict_connection_cmd {
+ const struct dict_cmd_func *cmd;
+ struct dict_connection *conn;
+ struct timeval start_timeval;
+ struct event *event;
+ char *reply;
+
+ struct dict_iterate_context *iter;
+ enum dict_iterate_flags iter_flags;
+
+ unsigned int async_reply_id;
+ unsigned int trans_id; /* obsolete */
+ unsigned int rows;
+
+ bool uncork_pending;
+};
+
+struct dict_command_stats cmd_stats;
+
+static int cmd_iterate_flush(struct dict_connection_cmd *cmd);
+
+static bool dict_connection_cmd_output_more(struct dict_connection_cmd *cmd);
+
+static void dict_connection_cmd_free(struct dict_connection_cmd *cmd)
+{
+ const char *error;
+
+ if (dict_iterate_deinit(&cmd->iter, &error) < 0)
+ e_error(cmd->event, "dict_iterate() failed: %s", error);
+ i_free(cmd->reply);
+ if (cmd->uncork_pending)
+ o_stream_uncork(cmd->conn->conn.output);
+
+ if (dict_connection_unref(cmd->conn) && !cmd->conn->destroyed)
+ connection_input_resume(&cmd->conn->conn);
+ event_unref(&cmd->event);
+ i_free(cmd);
+}
+
+static void dict_connection_cmd_remove(struct dict_connection_cmd *cmd)
+{
+ struct dict_connection_cmd *const *cmds;
+ unsigned int i, count;
+
+ cmds = array_get(&cmd->conn->cmds, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i] == cmd) {
+ array_delete(&cmd->conn->cmds, i, 1);
+ dict_connection_cmd_free(cmd);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static void dict_connection_cmds_flush(struct dict_connection *conn)
+{
+ struct dict_connection_cmd *cmd, *const *first_cmdp;
+
+ i_assert(conn->conn.minor_version < DICT_CLIENT_PROTOCOL_UNORDERED_MIN_VERSION);
+
+ dict_connection_ref(conn);
+ while (array_count(&conn->cmds) > 0) {
+ first_cmdp = array_front(&conn->cmds);
+ cmd = *first_cmdp;
+
+ i_assert(cmd->async_reply_id == 0);
+
+ /* we may be able to start outputting iterations now. */
+ if (cmd->iter != NULL)
+ (void)cmd_iterate_flush(cmd);
+
+ if (cmd->reply == NULL) {
+ /* command not finished yet */
+ break;
+ }
+
+ o_stream_nsend_str(conn->conn.output, cmd->reply);
+ dict_connection_cmd_remove(cmd);
+ }
+ dict_connection_unref_safe(conn);
+}
+
+static void dict_connection_cmd_try_flush(struct dict_connection_cmd **_cmd)
+{
+ struct dict_connection_cmd *cmd = *_cmd;
+
+ *_cmd = NULL;
+ if (cmd->conn->conn.minor_version < DICT_CLIENT_PROTOCOL_UNORDERED_MIN_VERSION) {
+ dict_connection_cmds_flush(cmd->conn);
+ return;
+ }
+ i_assert(cmd->async_reply_id != 0);
+ i_assert(cmd->reply != NULL);
+
+ o_stream_nsend_str(cmd->conn->conn.output, t_strdup_printf("%c%u\t%s",
+ DICT_PROTOCOL_REPLY_ASYNC_REPLY,
+ cmd->async_reply_id, cmd->reply));
+ dict_connection_cmd_remove(cmd);
+}
+
+static void dict_connection_cmd_async(struct dict_connection_cmd *cmd)
+{
+ if (cmd->conn->conn.minor_version < DICT_CLIENT_PROTOCOL_UNORDERED_MIN_VERSION)
+ return;
+
+ i_assert(cmd->async_reply_id == 0);
+ cmd->async_reply_id = ++cmd->conn->async_id_counter;
+ if (cmd->async_reply_id == 0)
+ cmd->async_reply_id = ++cmd->conn->async_id_counter;
+ o_stream_nsend_str(cmd->conn->conn.output, t_strdup_printf("%c%u\n",
+ DICT_PROTOCOL_REPLY_ASYNC_ID, cmd->async_reply_id));
+}
+
+static void
+cmd_stats_update(struct dict_connection_cmd *cmd, struct stats_dist *stats)
+{
+ long long diff;
+
+ if (!dict_settings->verbose_proctitle)
+ return;
+
+ diff = timeval_diff_usecs(&ioloop_timeval, &cmd->start_timeval);
+ if (diff < 0)
+ diff = 0;
+ stats_dist_add(stats, diff);
+ dict_proctitle_update_later();
+}
+
+static void
+dict_cmd_reply_handle_stats(struct dict_connection_cmd *cmd,
+ string_t *str, struct stats_dist *stats)
+{
+ io_loop_time_refresh();
+ cmd_stats_update(cmd, stats);
+
+ if (cmd->conn->conn.minor_version < DICT_CLIENT_PROTOCOL_TIMINGS_MIN_VERSION)
+ return;
+ str_printfa(str, "\t%ld\t%u\t%ld\t%u",
+ (long)cmd->start_timeval.tv_sec,
+ (unsigned int)cmd->start_timeval.tv_usec,
+ (long)ioloop_timeval.tv_sec,
+ (unsigned int)ioloop_timeval.tv_usec);
+}
+
+static void
+cmd_lookup_write_reply(struct dict_connection_cmd *cmd,
+ const char *const *values, string_t *str)
+{
+ string_t *tmp;
+
+ i_assert(values[0] != NULL);
+
+ if (cmd->conn->conn.minor_version < DICT_CLIENT_PROTOCOL_VERSION_MIN_MULTI_OK ||
+ values[1] == NULL) {
+ str_append_c(str, DICT_PROTOCOL_REPLY_OK);
+ str_append_tabescaped(str, values[0]);
+ return;
+ }
+ /* the results get double-tabescaped so they end up becoming a single
+ parameter */
+ tmp = t_str_new(128);
+ for (unsigned int i = 0; values[i] != NULL; i++) {
+ str_append_c(tmp, '\t');
+ str_append_tabescaped(tmp, values[i]);
+ }
+ str_append_c(str, DICT_PROTOCOL_REPLY_MULTI_OK);
+ str_append_tabescaped(str, str_c(tmp) + 1);
+}
+
+static void
+cmd_lookup_callback(const struct dict_lookup_result *result,
+ struct dict_connection_cmd *cmd)
+{
+ string_t *str = t_str_new(128);
+
+ event_set_name(cmd->event, "dict_server_lookup_finished");
+ if (result->ret > 0) {
+ cmd_lookup_write_reply(cmd, result->values, str);
+ e_debug(cmd->event, "Lookup finished");
+ } else if (result->ret == 0) {
+ event_add_str(cmd->event, "key_not_found", "yes");
+ str_append_c(str, DICT_PROTOCOL_REPLY_NOTFOUND);
+ e_debug(cmd->event, "Lookup finished without results");
+ } else {
+ event_add_str(cmd->event, "error", result->error);
+ e_error(cmd->event, "Lookup failed: %s", result->error);
+ str_append_c(str, DICT_PROTOCOL_REPLY_FAIL);
+ str_append_tabescaped(str, result->error);
+ }
+ dict_cmd_reply_handle_stats(cmd, str, cmd_stats.lookups);
+ str_append_c(str, '\n');
+
+ cmd->reply = i_strdup(str_c(str));
+ dict_connection_cmd_try_flush(&cmd);
+}
+
+static int cmd_lookup(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ const char *username;
+
+ if (str_array_length(args) < 1) {
+ e_error(cmd->event, "LOOKUP: broken input");
+ return -1;
+ }
+ username = args[1];
+
+ /* <key> [<username>] */
+ dict_connection_cmd_async(cmd);
+ event_add_str(cmd->event, "key", args[0]);
+ event_add_str(cmd->event, "user", username);
+ const struct dict_op_settings set = {
+ .username = username,
+ };
+ dict_lookup_async(cmd->conn->dict, &set, args[0], cmd_lookup_callback, cmd);
+ return 1;
+}
+
+static bool dict_connection_flush_if_full(struct dict_connection *conn)
+{
+ if (o_stream_get_buffer_used_size(conn->conn.output) >
+ DICT_OUTPUT_OPTIMAL_SIZE) {
+ if (o_stream_flush(conn->conn.output) <= 0) {
+ /* continue later when there's more space
+ in output buffer */
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+ conn->iter_flush_pending = TRUE;
+ return FALSE;
+ }
+ /* flushed everything, continue */
+ }
+ return TRUE;
+}
+
+static void
+cmd_iterate_flush_finish(struct dict_connection_cmd *cmd, string_t *str)
+{
+ const char *error;
+
+ event_set_name(cmd->event, "dict_server_iteration_finished");
+ str_truncate(str, 0);
+ if (dict_iterate_deinit(&cmd->iter, &error) < 0) {
+ event_add_str(cmd->event, "error", error);
+ e_error(cmd->event, "dict_iterate() failed: %s", error);
+ str_printfa(str, "%c%s", DICT_PROTOCOL_REPLY_FAIL, error);
+ } else {
+ event_add_int(cmd->event, "rows", cmd->rows);
+ e_debug(cmd->event, "Iteration finished");
+ }
+ dict_cmd_reply_handle_stats(cmd, str, cmd_stats.iterations);
+ str_append_c(str, '\n');
+
+ cmd->reply = i_strdup(str_c(str));
+}
+
+static int cmd_iterate_flush(struct dict_connection_cmd *cmd)
+{
+ string_t *str = t_str_new(256);
+ const char *key, *const *values;
+
+ if (cmd->conn->destroyed) {
+ cmd_iterate_flush_finish(cmd, str);
+ return 1;
+ }
+
+ if (!dict_connection_flush_if_full(cmd->conn))
+ return 0;
+
+ while (dict_iterate_values(cmd->iter, &key, &values)) {
+ cmd->rows++;
+ str_truncate(str, 0);
+ if (cmd->async_reply_id != 0) {
+ str_append_c(str, DICT_PROTOCOL_REPLY_ASYNC_REPLY);
+ str_printfa(str, "%u\t", cmd->async_reply_id);
+ }
+ str_append_c(str, DICT_PROTOCOL_REPLY_OK);
+ str_append_tabescaped(str, key);
+ str_append_c(str, '\t');
+ if ((cmd->iter_flags & DICT_ITERATE_FLAG_NO_VALUE) == 0) {
+ str_append_tabescaped(str, values[0]);
+ for (unsigned int i = 1; values[i] != NULL; i++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, values[i]);
+ }
+ }
+ str_append_c(str, '\n');
+ o_stream_nsend(cmd->conn->conn.output, str_data(str), str_len(str));
+
+ if (!dict_connection_flush_if_full(cmd->conn))
+ return 0;
+ }
+ if (dict_iterate_has_more(cmd->iter)) {
+ /* wait for the next iteration callback */
+ return 0;
+ }
+
+ cmd_iterate_flush_finish(cmd, str);
+ return 1;
+}
+
+static void cmd_iterate_callback(struct dict_connection_cmd *cmd)
+{
+ struct dict_connection *conn = cmd->conn;
+
+ dict_connection_ref(conn);
+ o_stream_cork(conn->conn.output);
+ /* Don't uncork if we're just waiting for more input from the dict
+ driver. Some dict drivers (e.g. dict-client) don't do any kind of
+ buffering internally, so this callback can write out only a single
+ iteration. By leaving the ostream corked it doesn't result in many
+ tiny writes. However, we could be here also because the connection
+ output buffer is full already, in which case don't want to leave a
+ cork. */
+ conn->iter_flush_pending = FALSE;
+ cmd->uncork_pending = FALSE;
+ if (dict_connection_cmd_output_more(cmd)) {
+ /* NOTE: cmd may be freed now */
+ o_stream_uncork(conn->conn.output);
+ } else if (conn->iter_flush_pending) {
+ /* Don't leave the stream uncorked or we might get stuck. */
+ o_stream_uncork(conn->conn.output);
+ } else {
+ /* It's possible that the command gets finished via some other
+ code path. To make sure this doesn't cause hangs, uncork the
+ output when command gets freed. */
+ cmd->uncork_pending = TRUE;
+ }
+ dict_connection_unref_safe(conn);
+}
+
+static int cmd_iterate(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ const char *username;
+ unsigned int flags;
+ uint64_t max_rows;
+
+ if (str_array_length(args) < 3 ||
+ str_to_uint(args[0], &flags) < 0 ||
+ str_to_uint64(args[1], &max_rows) < 0) {
+ e_error(cmd->event, "ITERATE: broken input");
+ return -1;
+ }
+ dict_connection_cmd_async(cmd);
+ username = args[3];
+
+ const struct dict_op_settings set = {
+ .username = username,
+ };
+
+ /* <flags> <max_rows> <path> [<username>] */
+ flags |= DICT_ITERATE_FLAG_ASYNC;
+ event_add_str(cmd->event, "key", args[2]);
+ event_add_str(cmd->event, "user", username);
+ cmd->iter = dict_iterate_init(cmd->conn->dict, &set, args[2], flags);
+ cmd->iter_flags = flags;
+ if (max_rows > 0)
+ dict_iterate_set_limit(cmd->iter, max_rows);
+ dict_iterate_set_async_callback(cmd->iter, cmd_iterate_callback, cmd);
+ (void)dict_connection_cmd_output_more(cmd);
+ return 1;
+}
+
+static struct dict_connection_transaction *
+dict_connection_transaction_lookup(struct dict_connection *conn,
+ unsigned int id)
+{
+ struct dict_connection_transaction *transaction;
+
+ if (!array_is_created(&conn->transactions))
+ return NULL;
+
+ array_foreach_modifiable(&conn->transactions, transaction) {
+ if (transaction->id == id)
+ return transaction;
+ }
+ return NULL;
+}
+
+static void
+dict_connection_transaction_array_remove(struct dict_connection *conn,
+ unsigned int id)
+{
+ const struct dict_connection_transaction *transactions;
+ unsigned int i, count;
+
+ transactions = array_get(&conn->transactions, &count);
+ for (i = 0; i < count; i++) {
+ if (transactions[i].id == id) {
+ i_assert(transactions[i].ctx == NULL);
+ array_delete(&conn->transactions, i, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static int cmd_begin(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+ unsigned int id;
+ const char *username;
+
+ if (str_array_length(args) < 1) {
+ e_error(cmd->event, "BEGIN: broken input");
+ return -1;
+ }
+ username = args[1];
+
+ /* <id> [<username>] */
+ if (str_to_uint(args[0], &id) < 0) {
+ e_error(cmd->event, "Invalid transaction ID %s", args[0]);
+ return -1;
+ }
+ if (dict_connection_transaction_lookup(cmd->conn, id) != NULL) {
+ e_error(cmd->event, "Transaction ID %u already exists", id);
+ return -1;
+ }
+
+ if (!array_is_created(&cmd->conn->transactions))
+ i_array_init(&cmd->conn->transactions, 4);
+
+ struct dict_op_settings set = {
+ .username = username,
+ };
+ trans = array_append_space(&cmd->conn->transactions);
+ trans->id = id;
+ trans->conn = cmd->conn;
+ trans->ctx = dict_transaction_begin(cmd->conn->dict, &set);
+ return 0;
+}
+
+static int
+dict_connection_transaction_lookup_parse(struct dict_connection *conn,
+ const char *id_str,
+ struct dict_connection_transaction **trans_r)
+{
+ unsigned int id;
+
+ if (str_to_uint(id_str, &id) < 0) {
+ e_error(conn->conn.event, "Invalid transaction ID %s", id_str);
+ return -1;
+ }
+ *trans_r = dict_connection_transaction_lookup(conn, id);
+ if (*trans_r == NULL) {
+ e_error(conn->conn.event, "Transaction ID %u doesn't exist", id);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+cmd_commit_finish(struct dict_connection_cmd *cmd,
+ const struct dict_commit_result *result, bool async)
+{
+ string_t *str = t_str_new(64);
+ char chr;
+
+ event_set_name(cmd->event, "dict_server_transaction_finished");
+ switch (result->ret) {
+ case DICT_COMMIT_RET_OK:
+ chr = DICT_PROTOCOL_REPLY_OK;
+ break;
+ case DICT_COMMIT_RET_NOTFOUND:
+ event_add_str(cmd->event, "key_not_found", "yes");
+ chr = DICT_PROTOCOL_REPLY_NOTFOUND;
+ break;
+ case DICT_COMMIT_RET_WRITE_UNCERTAIN:
+ i_assert(result->error != NULL);
+ event_add_str(cmd->event, "write_uncertain", "yes");
+ event_add_str(cmd->event, "error", result->error);
+ chr = DICT_PROTOCOL_REPLY_WRITE_UNCERTAIN;
+ break;
+ case DICT_COMMIT_RET_FAILED:
+ default:
+ i_assert(result->error != NULL);
+ event_add_str(cmd->event, "error", result->error);
+ chr = DICT_PROTOCOL_REPLY_FAIL;
+ break;
+ }
+ if (async)
+ str_append_c(str, DICT_PROTOCOL_REPLY_ASYNC_COMMIT);
+ str_printfa(str, "%c%u", chr, cmd->trans_id);
+ if (chr != DICT_PROTOCOL_REPLY_OK &&
+ chr != DICT_PROTOCOL_REPLY_NOTFOUND) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, result->error);
+ }
+ dict_cmd_reply_handle_stats(cmd, str, cmd_stats.commits);
+ str_append_c(str, '\n');
+ cmd->reply = i_strdup(str_c(str));
+
+ if (result->ret < 0)
+ e_debug(cmd->event, "Transaction finished: %s", result->error);
+ else
+ e_debug(cmd->event, "Transaction finished");
+ dict_connection_transaction_array_remove(cmd->conn, cmd->trans_id);
+ dict_connection_cmd_try_flush(&cmd);
+}
+
+static void cmd_commit_callback(const struct dict_commit_result *result,
+ struct dict_connection_cmd *cmd)
+{
+ cmd_commit_finish(cmd, result, FALSE);
+}
+
+static void cmd_commit_callback_async(const struct dict_commit_result *result,
+ struct dict_connection_cmd *cmd)
+{
+ cmd_commit_finish(cmd, result, TRUE);
+}
+
+static int
+cmd_commit(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+ cmd->trans_id = trans->id;
+ event_add_str(cmd->event, "user", trans->ctx->set.username);
+
+ dict_connection_cmd_async(cmd);
+ dict_transaction_commit_async(&trans->ctx, cmd_commit_callback, cmd);
+ return 1;
+}
+
+static int
+cmd_commit_async(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+ cmd->trans_id = trans->id;
+ event_add_str(cmd->event, "user", trans->ctx->set.username);
+
+ dict_connection_cmd_async(cmd);
+ dict_transaction_commit_async(&trans->ctx, cmd_commit_callback_async, cmd);
+ return 1;
+}
+
+static int
+cmd_rollback(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+
+ event_add_str(cmd->event, "user", trans->ctx->set.username);
+ dict_transaction_rollback(&trans->ctx);
+ dict_connection_transaction_array_remove(cmd->conn, trans->id);
+ return 0;
+}
+
+static int cmd_set(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+
+ /* <id> <key> <value> */
+ if (str_array_length(args) != 3) {
+ e_error(cmd->event, "SET: broken input");
+ return -1;
+ }
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+ event_add_str(cmd->event, "user", trans->ctx->set.username);
+ dict_set(trans->ctx, args[1], args[2]);
+ return 0;
+}
+
+static int cmd_unset(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+
+ /* <id> <key> */
+ if (str_array_length(args) != 2) {
+ e_error(cmd->event, "UNSET: broken input");
+ return -1;
+ }
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+ dict_unset(trans->ctx, args[1]);
+ return 0;
+}
+
+static int
+cmd_atomic_inc(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+ long long diff;
+
+ /* <id> <key> <diff> */
+ if (str_array_length(args) != 3 ||
+ str_to_llong(args[2], &diff) < 0) {
+ e_error(cmd->event, "ATOMIC_INC: broken input");
+ return -1;
+ }
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+
+ dict_atomic_inc(trans->ctx, args[1], diff);
+ return 0;
+}
+
+static int cmd_timestamp(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+ long long tv_sec;
+ unsigned int tv_nsec;
+
+ /* <id> <secs> <nsecs> */
+ if (str_array_length(args) != 3 ||
+ str_to_llong(args[1], &tv_sec) < 0 ||
+ str_to_uint(args[2], &tv_nsec) < 0) {
+ e_error(cmd->event, "TIMESTAMP: broken input");
+ return -1;
+ }
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+
+ struct timespec ts = {
+ .tv_sec = tv_sec,
+ .tv_nsec = tv_nsec
+ };
+ dict_transaction_set_timestamp(trans->ctx, &ts);
+ return 0;
+}
+
+static int
+cmd_hide_log_values(struct dict_connection_cmd *cmd, const char *const *args)
+{
+ struct dict_connection_transaction *trans;
+ bool value;
+
+ /* <id> <hide_log_values> */
+ if (str_array_length(args) != 2 ) {
+ e_error(cmd->event, "HIDE_LOG_VALUES: broken input");
+ return -1;
+ }
+ if (strcasecmp(args[1], "yes") == 0 || strcasecmp(args[1], "y") == 0 ||
+ strcmp(args[1], "1") == 0)
+ value = TRUE;
+ else if (strcasecmp(args[1], "no") == 0)
+ value = FALSE;
+ else {
+ e_error(cmd->event, "HIDE_LOG_VALUES: broken input");
+ return -1;
+ }
+
+ if (dict_connection_transaction_lookup_parse(cmd->conn, args[0], &trans) < 0)
+ return -1;
+
+ dict_transaction_set_hide_log_values(trans->ctx, value);
+ return 0;
+}
+
+static const struct dict_cmd_func cmds[] = {
+ { DICT_PROTOCOL_CMD_LOOKUP, cmd_lookup },
+ { DICT_PROTOCOL_CMD_ITERATE, cmd_iterate },
+ { DICT_PROTOCOL_CMD_BEGIN, cmd_begin },
+ { DICT_PROTOCOL_CMD_COMMIT, cmd_commit },
+ { DICT_PROTOCOL_CMD_COMMIT_ASYNC, cmd_commit_async },
+ { DICT_PROTOCOL_CMD_ROLLBACK, cmd_rollback },
+ { DICT_PROTOCOL_CMD_SET, cmd_set },
+ { DICT_PROTOCOL_CMD_UNSET, cmd_unset },
+ { DICT_PROTOCOL_CMD_ATOMIC_INC, cmd_atomic_inc },
+ { DICT_PROTOCOL_CMD_TIMESTAMP, cmd_timestamp },
+ { DICT_PROTOCOL_CMD_HIDE_LOG_VALUES, cmd_hide_log_values },
+
+ { 0, NULL }
+};
+
+static const struct dict_cmd_func *dict_command_find(enum dict_protocol_cmd cmd)
+{
+ unsigned int i;
+
+ for (i = 0; cmds[i].cmd != '\0'; i++) {
+ if (cmds[i].cmd == cmd)
+ return &cmds[i];
+ }
+ return NULL;
+}
+
+int dict_command_input(struct dict_connection *conn, const char *line)
+{
+ const struct dict_cmd_func *cmd_func;
+ struct dict_connection_cmd *cmd;
+ int ret;
+ const char *const *args;
+
+ cmd_func = dict_command_find((enum dict_protocol_cmd)*line);
+ if (cmd_func == NULL) {
+ e_error(conn->conn.event, "Unknown command %c", *line);
+ return -1;
+ }
+
+ cmd = i_new(struct dict_connection_cmd, 1);
+ cmd->conn = conn;
+ cmd->event = event_create(cmd->conn->conn.event);
+ cmd->cmd = cmd_func;
+ cmd->start_timeval = ioloop_timeval;
+ array_push_back(&conn->cmds, &cmd);
+ dict_connection_ref(conn);
+
+ args = t_strsplit_tabescaped(line + 1);
+ if ((ret = cmd_func->func(cmd, args)) <= 0) {
+ dict_connection_cmd_remove(cmd);
+ return ret;
+ }
+ return 0;
+}
+
+static bool dict_connection_cmds_try_output_more(struct dict_connection *conn)
+{
+ struct dict_connection_cmd *cmd;
+
+ /* only iterators may be returning a lot of data */
+ array_foreach_elem(&conn->cmds, cmd) {
+ if (cmd->iter == NULL) {
+ /* not an iterator */
+ } else if (cmd_iterate_flush(cmd) == 0) {
+ /* unfinished */
+ } else {
+ dict_connection_cmd_try_flush(&cmd);
+ /* cmd should be freed now, restart output */
+ return TRUE;
+ }
+ if (conn->conn.minor_version < DICT_CLIENT_PROTOCOL_TIMINGS_MIN_VERSION)
+ break;
+ /* try to flush the rest */
+ }
+ return FALSE;
+}
+
+void dict_connection_cmds_output_more(struct dict_connection *conn)
+{
+ while (array_count(&conn->cmds) > 0) {
+ if (!dict_connection_cmds_try_output_more(conn))
+ break;
+ }
+}
+
+static bool dict_connection_cmd_output_more(struct dict_connection_cmd *cmd)
+{
+ struct dict_connection_cmd *const *first_cmdp;
+
+ if (cmd->conn->conn.minor_version < DICT_CLIENT_PROTOCOL_TIMINGS_MIN_VERSION) {
+ first_cmdp = array_front(&cmd->conn->cmds);
+ if (*first_cmdp != cmd)
+ return TRUE;
+ }
+ return dict_connection_cmds_try_output_more(cmd->conn);
+}
+
+void dict_commands_init(void)
+{
+ cmd_stats.lookups = stats_dist_init();
+ cmd_stats.iterations = stats_dist_init();
+ cmd_stats.commits = stats_dist_init();
+}
+
+void dict_commands_deinit(void)
+{
+ stats_dist_deinit(&cmd_stats.lookups);
+ stats_dist_deinit(&cmd_stats.iterations);
+ stats_dist_deinit(&cmd_stats.commits);
+}
diff --git a/src/dict/dict-commands.h b/src/dict/dict-commands.h
new file mode 100644
index 0000000..ce4a04f
--- /dev/null
+++ b/src/dict/dict-commands.h
@@ -0,0 +1,21 @@
+#ifndef DICT_COMMANDS_H
+#define DICT_COMMANDS_H
+
+struct dict_connection;
+
+struct dict_command_stats {
+ struct stats_dist *lookups;
+ struct stats_dist *iterations;
+ struct stats_dist *commits;
+};
+
+extern struct dict_command_stats cmd_stats;
+
+int dict_command_input(struct dict_connection *conn, const char *line);
+
+void dict_connection_cmds_output_more(struct dict_connection *conn);
+
+void dict_commands_init(void);
+void dict_commands_deinit(void);
+
+#endif
diff --git a/src/dict/dict-connection.c b/src/dict/dict-connection.c
new file mode 100644
index 0000000..6a87c27
--- /dev/null
+++ b/src/dict/dict-connection.c
@@ -0,0 +1,272 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "dict-client.h"
+#include "dict-settings.h"
+#include "dict-commands.h"
+#include "dict-connection.h"
+#include "dict-init-cache.h"
+
+#include <unistd.h>
+
+#define DICT_CONN_MAX_PENDING_COMMANDS 1000
+
+static int dict_connection_dict_init(struct dict_connection *conn);
+static void dict_connection_destroy(struct connection *_conn);
+struct connection_list *dict_connections = NULL;
+
+static struct event_category dict_server_event_category = {
+ .name = "dict-server",
+};
+
+static int dict_connection_handshake_args(struct connection *_conn,
+ const char *const *args)
+{
+ unsigned int major, value_type_num;
+ struct dict_connection *conn =
+ container_of(_conn, struct dict_connection, conn);
+
+ /* protocol handshake is Hmajor minor value_type */
+ if (str_array_length(args) < 5 || **args != 'H')
+ return -1;
+
+ /* check major version which comes right after 'H' in the
+ first parameter, store minor version. */
+ if (str_to_uint(args[0]+1, &major) < 0 ||
+ str_to_uint(args[1], &conn->conn.minor_version) < 0 ||
+ major != DICT_CLIENT_PROTOCOL_MAJOR_VERSION)
+ return -1;
+
+ /* check value type */
+ if (str_to_uint(args[2], &value_type_num) < 0 ||
+ value_type_num >= DICT_DATA_TYPE_LAST)
+ return -1;
+
+ conn->value_type = (enum dict_data_type)value_type_num;
+ conn->name = i_strdup(args[4]);
+
+ /* try initialize the given dict */
+ if (dict_connection_dict_init(conn) < 0)
+ return -1;
+
+ return 1;
+}
+
+static int dict_connection_handshake_line(struct connection *conn,
+ const char *line)
+{
+ const char *const *args = t_strsplit_tabescaped(line);
+ return dict_connection_handshake_args(conn, args);
+}
+
+static int dict_connection_dict_init(struct dict_connection *conn)
+{
+ struct dict_settings dict_set;
+ const char *const *strlist;
+ unsigned int i, count;
+ const char *uri, *error;
+
+ if (!array_is_created(&dict_settings->dicts)) {
+ e_error(conn->conn.event, "No dictionaries configured");
+ return -1;
+ }
+ strlist = array_get(&dict_settings->dicts, &count);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(strlist[i], conn->name) == 0)
+ break;
+ }
+
+ if (i == count) {
+ e_error(conn->conn.event, "Unconfigured dictionary name '%s'",
+ conn->name);
+ return -1;
+ }
+ event_set_append_log_prefix(conn->conn.event,
+ t_strdup_printf("%s: ", conn->name));
+ event_add_str(conn->conn.event, "dict_name", conn->name);
+ uri = strlist[i+1];
+
+ i_zero(&dict_set);
+ dict_set.base_dir = dict_settings->base_dir;
+ dict_set.event_parent = conn->conn.event;
+ if (dict_init_cache_get(conn->name, uri, &dict_set, &conn->dict, &error) < 0) {
+ /* dictionary initialization failed */
+ e_error(conn->conn.event, "Failed to initialize dictionary '%s': %s",
+ conn->name, error);
+ return -1;
+ }
+ return 0;
+}
+
+static int dict_connection_output(struct connection *_conn)
+{
+ struct dict_connection *conn = container_of(_conn, struct dict_connection, conn);
+ int ret;
+
+ if ((ret = o_stream_flush(conn->conn.output)) < 0) {
+ dict_connection_destroy(&conn->conn);
+ return 1;
+ }
+ if (ret > 0)
+ dict_connection_cmds_output_more(conn);
+ return ret;
+}
+
+struct dict_connection *
+dict_connection_create(struct master_service_connection *master_conn)
+{
+ struct dict_connection *conn;
+
+ conn = i_new(struct dict_connection, 1);
+ conn->refcount = 1;
+
+ connection_init_server(dict_connections, &conn->conn, master_conn->name,
+ master_conn->fd, master_conn->fd);
+ event_add_category(conn->conn.event, &dict_server_event_category);
+
+ o_stream_set_flush_callback(conn->conn.output, dict_connection_output,
+ &conn->conn);
+
+ i_array_init(&conn->cmds, DICT_CONN_MAX_PENDING_COMMANDS);
+
+ return conn;
+}
+
+void dict_connection_ref(struct dict_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ conn->refcount++;
+}
+
+bool dict_connection_unref(struct dict_connection *conn)
+{
+ struct dict_connection_transaction *transaction;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ i_assert(array_count(&conn->cmds) == 0);
+
+ /* we should have only transactions that haven't been committed or
+ rollbacked yet. close those before dict is deinitialized. */
+ if (array_is_created(&conn->transactions)) {
+ array_foreach_modifiable(&conn->transactions, transaction)
+ dict_transaction_rollback(&transaction->ctx);
+ }
+
+ if (conn->dict != NULL)
+ dict_init_cache_unref(&conn->dict);
+
+ if (array_is_created(&conn->transactions))
+ array_free(&conn->transactions);
+
+ array_free(&conn->cmds);
+
+ connection_deinit(&conn->conn);
+
+ i_free(conn->name);
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+ return FALSE;
+}
+
+static int dict_connection_input_line(struct connection *_conn, const char *line)
+{
+ struct dict_connection *conn =
+ container_of(_conn, struct dict_connection, conn);
+
+ i_assert(conn->dict != NULL);
+
+ if (dict_command_input(conn, line) < 0)
+ return -1;
+
+ if (array_count(&conn->cmds) >= DICT_CONN_MAX_PENDING_COMMANDS) {
+ connection_input_halt(_conn);
+ return 0;
+ }
+
+ return 1;
+}
+
+static void dict_connection_unref_safe_callback(struct dict_connection *conn)
+{
+ timeout_remove(&conn->to_unref);
+ (void)dict_connection_unref(conn);
+}
+
+void dict_connection_unref_safe(struct dict_connection *conn)
+{
+ if (conn->refcount == 1) {
+ /* delayed unref to make sure we don't try to call
+ dict_deinit() from a dict-callback. that's too much trouble
+ for each dict driver to be able to handle. */
+ if (conn->to_unref == NULL) {
+ conn->to_unref = timeout_add_short(0,
+ dict_connection_unref_safe_callback, conn);
+ }
+ } else {
+ (void)dict_connection_unref(conn);
+ }
+}
+
+static void dict_connection_destroy(struct connection *_conn)
+{
+ struct dict_connection *conn = container_of(_conn, struct dict_connection, conn);
+
+ /* If there are commands still running, we delay disconnecting can may
+ come back here. Track this so we unreference the connection only
+ once. */
+ if (conn->destroyed)
+ return;
+ conn->destroyed = TRUE;
+
+ /* the connection is closed, but there may still be commands left
+ running. finish them, even if the calling client can't be notified
+ about whether they succeeded (clients may not even care).
+
+ flush the command output here in case we were waiting on iteration
+ output. */
+ i_stream_close(conn->conn.input);
+ o_stream_close(conn->conn.output);
+ dict_connection_cmds_output_more(conn);
+
+ io_remove(&conn->conn.io);
+ dict_connection_unref(conn);
+}
+
+unsigned int dict_connections_current_count(void)
+{
+ return dict_connections->connections_count;
+}
+
+void dict_connections_destroy_all(void)
+{
+ connection_list_deinit(&dict_connections);
+}
+
+static struct connection_settings dict_connections_set = {
+ .dont_send_version = TRUE,
+ .input_max_size = DICT_CLIENT_MAX_LINE_LENGTH,
+ .output_max_size = 128*1024,
+};
+
+static struct connection_vfuncs dict_connections_vfuncs = {
+ .destroy = dict_connection_destroy,
+ .handshake_line = dict_connection_handshake_line,
+ .input_line = dict_connection_input_line,
+};
+
+void dict_connections_init(void)
+{
+ dict_connections = connection_list_init(&dict_connections_set,
+ &dict_connections_vfuncs);
+}
diff --git a/src/dict/dict-connection.h b/src/dict/dict-connection.h
new file mode 100644
index 0000000..2241dad
--- /dev/null
+++ b/src/dict/dict-connection.h
@@ -0,0 +1,48 @@
+#ifndef DICT_CONNECTION_H
+#define DICT_CONNECTION_H
+
+#include "dict.h"
+#include "connection.h"
+
+struct dict_connection_transaction {
+ unsigned int id;
+ struct dict_connection *conn;
+ struct dict_transaction_context *ctx;
+};
+
+struct dict_connection {
+ struct connection conn;
+ struct dict_connection *prev, *next;
+ struct dict_server *server;
+ int refcount;
+
+ char *name;
+ struct dict *dict;
+ enum dict_data_type value_type;
+
+ struct timeout *to_unref;
+
+ /* There are only a few transactions per client, so keeping them in
+ array is fast enough */
+ ARRAY(struct dict_connection_transaction) transactions;
+ ARRAY(struct dict_connection_cmd *) cmds;
+ unsigned int async_id_counter;
+
+ bool iter_flush_pending;
+ bool destroyed;
+};
+
+struct master_service_connection;
+
+struct dict_connection *
+dict_connection_create(struct master_service_connection *master_conn);
+
+void dict_connection_ref(struct dict_connection *conn);
+bool dict_connection_unref(struct dict_connection *conn);
+void dict_connection_unref_safe(struct dict_connection *conn);
+
+unsigned int dict_connections_current_count(void);
+void dict_connections_init(void);
+void dict_connections_destroy_all(void);
+
+#endif
diff --git a/src/dict/dict-init-cache.c b/src/dict/dict-init-cache.c
new file mode 100644
index 0000000..ed76940
--- /dev/null
+++ b/src/dict/dict-init-cache.c
@@ -0,0 +1,164 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dict.h"
+#include "dict-private.h"
+#include "dict-init-cache.h"
+#include "llist.h"
+
+/* How many seconds to keep dict opened for reuse after it's been closed */
+#define DICT_CACHE_TIMEOUT_SECS 30
+/* How many closed dicts to keep */
+#define DICT_CACHE_MAX_COUNT 10
+
+struct dict_init_cache_list {
+ struct dict_init_cache_list *prev, *next;
+
+ struct dict *dict;
+ char *dict_name;
+ int refcount;
+
+ time_t destroy_time;
+};
+
+static struct dict_init_cache_list *dicts = NULL;
+static struct timeout *to_dict = NULL;
+
+static struct dict_init_cache_list *
+dict_init_cache_add(const char *dict_name, struct dict *dict)
+{
+ struct dict_init_cache_list *list;
+
+ list = i_new(struct dict_init_cache_list, 1);
+ list->refcount = 1;
+ list->dict = dict;
+ list->dict_name = i_strdup(dict_name);
+
+ DLLIST_PREPEND(&dicts, list);
+
+ return list;
+}
+
+static void dict_init_cache_list_free(struct dict_init_cache_list *list)
+{
+ i_assert(list->refcount == 0);
+
+ DLLIST_REMOVE(&dicts, list);
+ dict_deinit(&list->dict);
+ i_free(list->dict_name);
+ i_free(list);
+}
+
+static struct dict_init_cache_list *dict_init_cache_find(const char *dict_name)
+{
+ struct dict_init_cache_list *listp = dicts, *next = NULL, *match = NULL;
+ unsigned int ref0_count = 0;
+
+ while (listp != NULL) {
+ next = listp->next;
+ if (match != NULL) {
+ /* already found the dict. we're just going through
+ the rest of them to drop 0 refcounts */
+ } else if (strcmp(dict_name, listp->dict_name) == 0)
+ match = listp;
+
+ if (listp->refcount == 0 && listp != match) {
+ if (listp->destroy_time <= ioloop_time ||
+ ref0_count >= DICT_CACHE_MAX_COUNT - 1)
+ dict_init_cache_list_free(listp);
+ else
+ ref0_count++;
+ }
+ listp = next;
+ }
+ return match;
+}
+
+int dict_init_cache_get(const char *dict_name, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct dict_init_cache_list *match;
+ int ret = 0;
+
+ match = dict_init_cache_find(dict_name);
+ if (match == NULL) {
+ if (dict_init(uri, set, dict_r, error_r) < 0)
+ return -1;
+ match = dict_init_cache_add(dict_name, *dict_r);
+ } else {
+ match->refcount++;
+ *dict_r = match->dict;
+ }
+ i_assert(match->dict != NULL);
+ return ret;
+}
+
+static void destroy_unrefed(void)
+{
+ struct dict_init_cache_list *listp, *next = NULL;
+ bool seen_ref0 = FALSE;
+
+ for (listp = dicts; listp != NULL; listp = next) {
+ next = listp->next;
+
+ i_assert(listp->refcount >= 0);
+ if (listp->refcount > 0)
+ ;
+ else if (listp->destroy_time <= ioloop_time)
+ dict_init_cache_list_free(listp);
+ else
+ seen_ref0 = TRUE;
+ }
+
+ if (!seen_ref0 && to_dict != NULL)
+ timeout_remove(&to_dict);
+}
+
+static void dict_removal_timeout(void *context ATTR_UNUSED)
+{
+ destroy_unrefed();
+}
+
+void dict_init_cache_unref(struct dict **_dict)
+{
+ struct dict *dict = *_dict;
+ struct dict_init_cache_list *listp;
+
+ if (dict == NULL)
+ return;
+
+ *_dict = NULL;
+ for (listp = dicts; listp != NULL; listp = listp->next) {
+ if (listp->dict == dict)
+ break;
+ }
+
+ i_assert(listp != NULL && listp->dict == dict);
+ i_assert(listp->refcount > 0);
+
+ listp->refcount--;
+ listp->destroy_time = ioloop_time + DICT_CACHE_TIMEOUT_SECS;
+
+ if (to_dict == NULL) {
+ to_dict = timeout_add_to(io_loop_get_root(),
+ DICT_CACHE_TIMEOUT_SECS*1000/2,
+ dict_removal_timeout, NULL);
+ }
+}
+
+void dict_init_cache_wait_all(void)
+{
+ struct dict_init_cache_list *listp;
+
+ for (listp = dicts; listp != NULL; listp = listp->next)
+ dict_wait(listp->dict);
+}
+
+void dict_init_cache_destroy_all(void)
+{
+ timeout_remove(&to_dict);
+ while (dicts != NULL)
+ dict_init_cache_list_free(dicts);
+}
diff --git a/src/dict/dict-init-cache.h b/src/dict/dict-init-cache.h
new file mode 100644
index 0000000..1342ca3
--- /dev/null
+++ b/src/dict/dict-init-cache.h
@@ -0,0 +1,12 @@
+#ifndef DICT_INIT_CACHE_H
+#define DICT_INIT_CACHE_H
+
+int dict_init_cache_get(const char *dict_name, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r);
+void dict_init_cache_unref(struct dict **dict);
+
+void dict_init_cache_wait_all(void);
+void dict_init_cache_destroy_all(void);
+
+#endif
diff --git a/src/dict/dict-settings.c b/src/dict/dict-settings.c
new file mode 100644
index 0000000..23c33f6
--- /dev/null
+++ b/src/dict/dict-settings.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "dict-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings dict_unix_listeners_array[] = {
+ { "dict", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *dict_unix_listeners[] = {
+ &dict_unix_listeners_array[0]
+};
+static buffer_t dict_unix_listeners_buf = {
+ { { dict_unix_listeners, sizeof(dict_unix_listeners) } }
+};
+
+static struct file_listener_settings dict_async_unix_listeners_array[] = {
+ { "dict-async", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *dict_async_unix_listeners[] = {
+ &dict_async_unix_listeners_array[0]
+};
+static buffer_t dict_async_unix_listeners_buf = {
+ { { dict_async_unix_listeners, sizeof(dict_async_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings dict_service_settings = {
+ .name = "dict",
+ .protocol = "",
+ .type = "",
+ .executable = "dict",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dict_unix_listeners_buf,
+ sizeof(dict_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+struct service_settings dict_async_service_settings = {
+ .name = "dict-async",
+ .protocol = "",
+ .type = "",
+ .executable = "dict",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dict_async_unix_listeners_buf,
+ sizeof(dict_async_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct dict_server_settings)
+
+static const struct setting_define dict_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, dict_db_config),
+ { .type = SET_STRLIST, .key = "dict",
+ .offset = offsetof(struct dict_server_settings, dicts) },
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct dict_server_settings dict_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .verbose_proctitle = FALSE,
+
+ .dict_db_config = "",
+ .dicts = ARRAY_INIT
+};
+
+const struct setting_parser_info dict_setting_parser_info = {
+ .module_name = "dict",
+ .defines = dict_setting_defines,
+ .defaults = &dict_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct dict_server_settings),
+
+ .parent_offset = SIZE_MAX
+};
+
+const struct dict_server_settings *dict_settings;
diff --git a/src/dict/dict-settings.h b/src/dict/dict-settings.h
new file mode 100644
index 0000000..3c37589
--- /dev/null
+++ b/src/dict/dict-settings.h
@@ -0,0 +1,15 @@
+#ifndef DICT_SETTINGS_H
+#define DICT_SETTINGS_H
+
+struct dict_server_settings {
+ const char *base_dir;
+ bool verbose_proctitle;
+
+ const char *dict_db_config;
+ ARRAY(const char *) dicts;
+};
+
+extern const struct setting_parser_info dict_setting_parser_info;
+extern const struct dict_server_settings *dict_settings;
+
+#endif
diff --git a/src/dict/main.c b/src/dict/main.c
new file mode 100644
index 0000000..14b1cea
--- /dev/null
+++ b/src/dict/main.c
@@ -0,0 +1,179 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "ioloop.h"
+#include "randgen.h"
+#include "str.h"
+#include "stats-dist.h"
+#include "process-title.h"
+#include "env-util.h"
+#include "module-dir.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "sql-api.h"
+#include "dict.h"
+#include "dict-client.h"
+#include "dict-commands.h"
+#include "dict-connection.h"
+#include "dict-settings.h"
+#include "dict-init-cache.h"
+#include "main.h"
+
+#include <math.h>
+
+static struct module *modules;
+static struct timeout *to_proctitle;
+static bool proctitle_updated;
+static struct ioloop *main_ioloop;
+
+static void
+add_stats_string(string_t *str, struct stats_dist *stats, const char *name)
+{
+ uint64_t min, max, p95;
+ double avg;
+
+ min = stats_dist_get_min(stats);
+ avg = stats_dist_get_avg(stats);
+ p95 = stats_dist_get_95th(stats);
+ max = stats_dist_get_max(stats);
+
+ str_printfa(str, ", %u %s:%llu/%lld/%llu/%llu",
+ stats_dist_get_count(stats), name,
+ (unsigned long long)min/1000, llrint(avg/1000),
+ (unsigned long long)p95/1000,
+ (unsigned long long)max/1000);
+ stats_dist_reset(stats);
+}
+
+static void dict_proctitle_update(void *context ATTR_UNUSED)
+{
+ string_t *str = t_str_new(128);
+
+ if (!proctitle_updated)
+ timeout_remove(&to_proctitle);
+
+ str_printfa(str, "[%u clients", dict_connections_current_count());
+
+ add_stats_string(str, cmd_stats.lookups, "lookups");
+ add_stats_string(str, cmd_stats.iterations, "iters");
+ add_stats_string(str, cmd_stats.commits, "commits");
+ str_append_c(str, ']');
+
+ process_title_set(str_c(str));
+ proctitle_updated = FALSE;
+}
+
+void dict_proctitle_update_later(void)
+{
+ if (!dict_settings->verbose_proctitle)
+ return;
+
+ if (to_proctitle == NULL)
+ to_proctitle = timeout_add_to(main_ioloop, 1000, dict_proctitle_update, NULL);
+ proctitle_updated = TRUE;
+}
+
+static void dict_die(void)
+{
+ /* hope that other processes relying on us will die first. */
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ (void)dict_connection_create(conn);
+}
+
+static void main_preinit(void)
+{
+ /* Load built-in SQL drivers (if any) */
+ sql_drivers_init();
+ sql_drivers_register_all();
+#ifdef HAVE_CDB
+ dict_driver_register(&dict_driver_cdb);
+#endif
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_init(void)
+{
+ struct module_dir_load_settings mod_set;
+ void **sets;
+
+ sets = master_service_settings_get_others(master_service);
+ dict_settings = sets[0];
+
+ if (*dict_settings->dict_db_config != '\0') {
+ /* for berkeley db library */
+ env_put("DB_CONFIG", dict_settings->dict_db_config);
+ }
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+
+ modules = module_dir_load(DICT_MODULE_DIR, NULL, &mod_set);
+ module_dir_init(modules);
+
+ /* Register only after loading modules. They may contain SQL drivers,
+ which we'll need to register. */
+ dict_drivers_register_all();
+ dict_commands_init();
+ dict_connections_init();
+
+ if (dict_settings->verbose_proctitle)
+ dict_proctitle_update(NULL);
+}
+
+static void main_deinit(void)
+{
+ /* wait for all dict operations to finish */
+ dict_init_cache_wait_all();
+ /* connections should no longer have any extra refcounts */
+ dict_connections_destroy_all();
+ dict_init_cache_destroy_all();
+
+ dict_drivers_unregister_all();
+ dict_commands_deinit();
+
+ module_dir_unload(&modules);
+
+ sql_drivers_deinit();
+ timeout_remove(&to_proctitle);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags = 0;
+ const struct setting_parser_info *set_roots[] = {
+ &dict_setting_parser_info,
+ NULL
+ };
+ const char *error;
+
+ master_service = master_service_init("dict", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ master_service_init_log_with_pid(master_service);
+ main_preinit();
+ master_service_set_die_callback(master_service, dict_die);
+
+ main_ioloop = current_ioloop;
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+
+ /* clean up cached dicts */
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/dict/main.h b/src/dict/main.h
new file mode 100644
index 0000000..e505e13
--- /dev/null
+++ b/src/dict/main.h
@@ -0,0 +1,6 @@
+#ifndef MAIN_H
+#define MAIN_H
+
+void dict_proctitle_update_later(void);
+
+#endif
diff --git a/src/director/Makefile.am b/src/director/Makefile.am
new file mode 100644
index 0000000..c202353
--- /dev/null
+++ b/src/director/Makefile.am
@@ -0,0 +1,70 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = director
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-program-client \
+ $(BINARY_CFLAGS)
+
+director_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+director_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+director_SOURCES = \
+ main.c \
+ auth-connection.c \
+ director.c \
+ director-connection.c \
+ director-host.c \
+ director-request.c \
+ director-settings.c \
+ doveadm-connection.c \
+ login-connection.c \
+ mail-host.c \
+ notify-connection.c \
+ user-directory.c
+
+noinst_HEADERS = \
+ auth-connection.h \
+ director.h \
+ director-connection.h \
+ director-host.h \
+ director-request.h \
+ director-settings.h \
+ doveadm-connection.h \
+ login-connection.h \
+ mail-host.h \
+ notify-connection.h \
+ user-directory.h
+
+noinst_PROGRAMS = director-test $(test_programs)
+
+director_test_LDADD = $(LIBDOVECOT)
+director_test_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+director_test_SOURCES = \
+ director-test.c
+
+test_programs = \
+ test-user-directory
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_user_directory_SOURCES = test-user-directory.c
+test_user_directory_LDADD = user-directory.o $(test_libs)
+test_user_directory_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/director/Makefile.in b/src/director/Makefile.in
new file mode 100644
index 0000000..9ae08ff
--- /dev/null
+++ b/src/director/Makefile.in
@@ -0,0 +1,928 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = director$(EXEEXT)
+noinst_PROGRAMS = director-test$(EXEEXT) $(am__EXEEXT_1)
+subdir = src/director
+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-user-directory$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am_director_OBJECTS = main.$(OBJEXT) auth-connection.$(OBJEXT) \
+ director.$(OBJEXT) director-connection.$(OBJEXT) \
+ director-host.$(OBJEXT) director-request.$(OBJEXT) \
+ director-settings.$(OBJEXT) doveadm-connection.$(OBJEXT) \
+ login-connection.$(OBJEXT) mail-host.$(OBJEXT) \
+ notify-connection.$(OBJEXT) user-directory.$(OBJEXT)
+director_OBJECTS = $(am_director_OBJECTS)
+am__DEPENDENCIES_1 =
+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_director_test_OBJECTS = director-test.$(OBJEXT)
+director_test_OBJECTS = $(am_director_test_OBJECTS)
+am_test_user_directory_OBJECTS = test-user-directory.$(OBJEXT)
+test_user_directory_OBJECTS = $(am_test_user_directory_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)/auth-connection.Po \
+ ./$(DEPDIR)/director-connection.Po \
+ ./$(DEPDIR)/director-host.Po ./$(DEPDIR)/director-request.Po \
+ ./$(DEPDIR)/director-settings.Po ./$(DEPDIR)/director-test.Po \
+ ./$(DEPDIR)/director.Po ./$(DEPDIR)/doveadm-connection.Po \
+ ./$(DEPDIR)/login-connection.Po ./$(DEPDIR)/mail-host.Po \
+ ./$(DEPDIR)/main.Po ./$(DEPDIR)/notify-connection.Po \
+ ./$(DEPDIR)/test-user-directory.Po \
+ ./$(DEPDIR)/user-directory.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 = $(director_SOURCES) $(director_test_SOURCES) \
+ $(test_user_directory_SOURCES)
+DIST_SOURCES = $(director_SOURCES) $(director_test_SOURCES) \
+ $(test_user_directory_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-auth \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-program-client \
+ $(BINARY_CFLAGS)
+
+director_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+director_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+director_SOURCES = \
+ main.c \
+ auth-connection.c \
+ director.c \
+ director-connection.c \
+ director-host.c \
+ director-request.c \
+ director-settings.c \
+ doveadm-connection.c \
+ login-connection.c \
+ mail-host.c \
+ notify-connection.c \
+ user-directory.c
+
+noinst_HEADERS = \
+ auth-connection.h \
+ director.h \
+ director-connection.h \
+ director-host.h \
+ director-request.h \
+ director-settings.h \
+ doveadm-connection.h \
+ login-connection.h \
+ mail-host.h \
+ notify-connection.h \
+ user-directory.h
+
+director_test_LDADD = $(LIBDOVECOT)
+director_test_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+director_test_SOURCES = \
+ director-test.c
+
+test_programs = \
+ test-user-directory
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_user_directory_SOURCES = test-user-directory.c
+test_user_directory_LDADD = user-directory.o $(test_libs)
+test_user_directory_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(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/director/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/director/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+director$(EXEEXT): $(director_OBJECTS) $(director_DEPENDENCIES) $(EXTRA_director_DEPENDENCIES)
+ @rm -f director$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(director_OBJECTS) $(director_LDADD) $(LIBS)
+
+director-test$(EXEEXT): $(director_test_OBJECTS) $(director_test_DEPENDENCIES) $(EXTRA_director_test_DEPENDENCIES)
+ @rm -f director-test$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(director_test_OBJECTS) $(director_test_LDADD) $(LIBS)
+
+test-user-directory$(EXEEXT): $(test_user_directory_OBJECTS) $(test_user_directory_DEPENDENCIES) $(EXTRA_test_user_directory_DEPENDENCIES)
+ @rm -f test-user-directory$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_user_directory_OBJECTS) $(test_user_directory_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-host.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-request.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-test.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/login-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-host.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-user-directory.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/user-directory.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/auth-connection.Po
+ -rm -f ./$(DEPDIR)/director-connection.Po
+ -rm -f ./$(DEPDIR)/director-host.Po
+ -rm -f ./$(DEPDIR)/director-request.Po
+ -rm -f ./$(DEPDIR)/director-settings.Po
+ -rm -f ./$(DEPDIR)/director-test.Po
+ -rm -f ./$(DEPDIR)/director.Po
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/login-connection.Po
+ -rm -f ./$(DEPDIR)/mail-host.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/test-user-directory.Po
+ -rm -f ./$(DEPDIR)/user-directory.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-pkglibexecPROGRAMS
+
+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-connection.Po
+ -rm -f ./$(DEPDIR)/director-connection.Po
+ -rm -f ./$(DEPDIR)/director-host.Po
+ -rm -f ./$(DEPDIR)/director-request.Po
+ -rm -f ./$(DEPDIR)/director-settings.Po
+ -rm -f ./$(DEPDIR)/director-test.Po
+ -rm -f ./$(DEPDIR)/director.Po
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/login-connection.Po
+ -rm -f ./$(DEPDIR)/mail-host.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/test-user-directory.Po
+ -rm -f ./$(DEPDIR)/user-directory.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-pkglibexecPROGRAMS
+
+.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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/director/auth-connection.c b/src/director/auth-connection.c
new file mode 100644
index 0000000..33e58ce
--- /dev/null
+++ b/src/director/auth-connection.c
@@ -0,0 +1,141 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "llist.h"
+#include "safe-memset.h"
+#include "auth-client-interface.h"
+#include "director.h"
+#include "auth-connection.h"
+
+#include <unistd.h>
+
+struct auth_connection {
+ struct auth_connection *prev, *next;
+
+ struct director *dir;
+ char *path;
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ auth_input_callback *callback;
+ void *context;
+};
+
+static struct auth_connection *auth_connections;
+
+static void auth_connection_disconnected(struct auth_connection **conn);
+
+static void auth_connection_input(struct auth_connection *conn)
+{
+ char *line;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ e_error(conn->dir->event, "Auth server disconnected unexpectedly");
+ auth_connection_disconnected(&conn);
+ return;
+ case -2:
+ /* buffer full */
+ e_error(conn->dir->event,
+ "BUG: Auth server sent us more than %d bytes",
+ (int)AUTH_CLIENT_MAX_LINE_LENGTH);
+ auth_connection_disconnected(&conn);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ conn->callback(line, conn->context);
+ safe_memset(line, 0, strlen(line));
+ } T_END;
+ }
+}
+
+struct auth_connection *
+auth_connection_init(struct director *dir, const char *path)
+{
+ struct auth_connection *conn;
+
+ conn = i_new(struct auth_connection, 1);
+ conn->dir = dir;
+ conn->fd = -1;
+ conn->path = i_strdup(path);
+ DLLIST_PREPEND(&auth_connections, conn);
+ return conn;
+}
+
+void auth_connection_set_callback(struct auth_connection *conn,
+ auth_input_callback *callback, void *context)
+{
+ conn->callback = callback;
+ conn->context = context;
+}
+
+int auth_connection_connect(struct auth_connection *conn)
+{
+ i_assert(conn->fd == -1);
+
+ conn->fd = net_connect_unix_with_retries(conn->path, 1000);
+ if (conn->fd == -1) {
+ e_error(conn->dir->event, "connect(%s) failed: %m", conn->path);
+ return -1;
+ }
+
+ conn->input = i_stream_create_fd(conn->fd, AUTH_CLIENT_MAX_LINE_LENGTH);
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ conn->io = io_add(conn->fd, IO_READ, auth_connection_input, conn);
+ return 0;
+}
+
+void auth_connection_deinit(struct auth_connection **_conn)
+{
+ struct auth_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ DLLIST_REMOVE(&auth_connections, conn);
+ if (conn->fd != -1) {
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(auth connection) failed: %m");
+ }
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void auth_connection_disconnected(struct auth_connection **_conn)
+{
+ struct auth_connection *conn = *_conn;
+
+ *_conn = NULL;
+ /* notify callback. it should deinit this connection */
+ conn->callback(NULL, conn->context);
+}
+
+struct ostream *auth_connection_get_output(struct auth_connection *conn)
+{
+ i_assert(conn->output != NULL);
+ return conn->output;
+}
+
+void auth_connections_deinit(void)
+{
+ while (auth_connections != NULL) {
+ struct auth_connection *conn = auth_connections;
+
+ auth_connection_disconnected(&conn);
+ }
+}
diff --git a/src/director/auth-connection.h b/src/director/auth-connection.h
new file mode 100644
index 0000000..d2e12e6
--- /dev/null
+++ b/src/director/auth-connection.h
@@ -0,0 +1,24 @@
+#ifndef AUTH_CONNECTION_H
+#define AUTH_CONNECTION_H
+
+struct director;
+
+/* Called for each input line. This is also called with line=NULL if
+ connection gets disconnected. */
+typedef void auth_input_callback(const char *line, void *context);
+
+struct auth_connection *
+auth_connection_init(struct director *dir, const char *path);
+void auth_connection_deinit(struct auth_connection **conn);
+
+void auth_connection_set_callback(struct auth_connection *conn,
+ auth_input_callback *callback, void *context);
+
+/* Start connecting. Returns 0 if ok, -1 if connect failed. */
+int auth_connection_connect(struct auth_connection *conn);
+/* Get auth connection's output stream. */
+struct ostream *auth_connection_get_output(struct auth_connection *conn);
+
+void auth_connections_deinit(void);
+
+#endif
diff --git a/src/director/director-connection.c b/src/director/director-connection.c
new file mode 100644
index 0000000..a89cc2e
--- /dev/null
+++ b/src/director/director-connection.c
@@ -0,0 +1,2712 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Handshaking:
+
+ Incoming director connections send:
+
+ VERSION
+ ME
+ <wait for DONE from remote handshake>
+ DONE
+ <make this connection our "left" connection, potentially disconnecting
+ another one>
+
+ Outgoing director connections send:
+
+ VERSION
+ ME
+ [0..n] DIRECTOR
+ HOST-HAND-START
+ [0..n] HOST
+ HOST-HAND-END
+ [0..n] USER
+ <possibly other non-handshake commands between USERs>
+ DONE
+ <wait for DONE from remote>
+ <make this connection our "right" connection, potentially disconnecting
+ another one>
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "director-connection.h"
+
+#include <unistd.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#define MAX_INBUF_SIZE 1024
+#define OUTBUF_FLUSH_THRESHOLD (1024*128)
+/* Max time to wait for connect() to finish before aborting */
+#define DIRECTOR_CONNECTION_CONNECT_TIMEOUT_MSECS (10*1000)
+/* Max idling time before "ME" command must have been received,
+ or we'll disconnect. */
+#define DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS (10*1000)
+/* Max time to wait for USERs in handshake to be sent. With a lot of users the
+ kernel may quickly eat up everything we send, while the receiver is busy
+ parsing the data. */
+#define DIRECTOR_CONNECTION_SEND_USERS_TIMEOUT_MSECS (30*1000)
+/* Max idling time before "DONE" command must have been received,
+ or we'll disconnect. Use a slightly larger value than for _SEND_USERS_ so
+ that we'll get a better error if the sender decides to disconnect. */
+#define DIRECTOR_CONNECTION_DONE_TIMEOUT_MSECS (40*1000)
+/* How long to wait to send PING when connection is idle */
+#define DIRECTOR_CONNECTION_PING_INTERVAL_MSECS (15*1000)
+/* How long to wait before sending PING while waiting for SYNC reply */
+#define DIRECTOR_CONNECTION_PING_SYNC_INTERVAL_MSECS 1000
+/* Log a warning if PING reply or PONG response takes longer than this */
+#define DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS (5*1000)
+/* If outgoing director connection exists for less than this many seconds,
+ mark the host as failed so we won't try to reconnect to it immediately */
+#define DIRECTOR_SUCCESS_MIN_CONNECT_SECS 40
+/* If USER request doesn't have a timestamp, user isn't refreshed if it was
+ already refreshed director_user_expire/4 seconds ago. This value is the
+ hardcoded maximum for that value. */
+#define DIRECTOR_SKIP_RECENT_REFRESH_MAX_SECS 15
+#define DIRECTOR_RECONNECT_AFTER_WRONG_CONNECT_MSECS 1000
+#define DIRECTOR_WAIT_DISCONNECT_SECS 10
+#define DIRECTOR_HANDSHAKE_WARN_SECS 29
+#define DIRECTOR_HANDSHAKE_BYTES_LOG_MIN_SECS (60*30)
+#define DIRECTOR_MAX_SYNC_SEQ_DUPLICATES 4
+/* If we receive SYNCs with a timestamp this many seconds higher than the last
+ valid received SYNC timestamp, assume that we lost the director's restart
+ notification and reset the last_sync_seq */
+#define DIRECTOR_SYNC_STALE_TIMESTAMP_RESET_SECS (60*2)
+#define DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS 1
+/* How many USER entries to send during handshake before going back to ioloop
+ to see if there's other work to be done as well. */
+#define DIRECTOR_HANDSHAKE_MAX_USERS_SENT_PER_FLUSH 10000
+
+#define CMD_IS_USER_HANDSHAKE(minor_version, args) \
+ ((minor_version) < DIRECTOR_VERSION_HANDSHAKE_U_CMD && \
+ str_array_length(args) > 2)
+
+#define DIRECTOR_OPT_CONSISTENT_HASHING "consistent-hashing"
+
+struct director_connection {
+ int refcount;
+ struct director *dir;
+ struct event *event;
+ char *name;
+ struct timeval created, connected_time, me_received_time;
+ struct timeval connected_user_cpu;
+ unsigned int minor_version;
+
+ struct timeval last_input, last_output;
+ size_t peak_bytes_buffered;
+
+ struct timeval ping_sent_time;
+ size_t ping_sent_buffer_size;
+ struct timeval ping_sent_user_cpu;
+ uoff_t ping_sent_input_offset, ping_sent_output_offset;
+ unsigned int last_ping_msecs;
+
+ /* for incoming connections the director host isn't known until
+ ME-line is received */
+ struct director_host *host;
+ /* this is set only for wrong connections: */
+ struct director_host *connect_request_to;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_disconnect, *to_ping, *to_pong;
+
+ struct director_user_iter *user_iter;
+ unsigned int users_received, handshake_users_received;
+ unsigned int handshake_users_sent;
+
+ /* set during command execution */
+ const char *cur_cmd, *const *cur_args;
+
+ bool in:1;
+ bool connected:1;
+ bool version_received:1;
+ bool me_received:1;
+ bool handshake_received:1;
+ bool ignore_host_events:1;
+ bool handshake_sending_hosts:1;
+ bool ping_waiting:1;
+ bool synced:1;
+ bool wrong_host:1;
+ bool verifying_left:1;
+ bool users_unsorted:1;
+ bool connected_user_cpu_set:1;
+};
+
+static bool director_connection_unref(struct director_connection *conn);
+static void director_finish_sending_handshake(struct director_connection *conn);
+static void director_connection_disconnected(struct director_connection **conn,
+ const char *reason);
+static void director_connection_reconnect(struct director_connection **conn,
+ const char *reason);
+static void
+director_connection_log_disconnect(struct director_connection *conn, int err,
+ const char *errstr);
+static int director_connection_send_done(struct director_connection *conn);
+
+static void
+director_connection_set_name(struct director_connection *conn, const char *name)
+{
+ char *old_name = conn->name;
+ conn->name = i_strdup(name);
+ i_free(old_name);
+
+ event_set_append_log_prefix(conn->event,
+ t_strdup_printf("director(%s): ", conn->name));
+}
+
+static void ATTR_FORMAT(2, 3)
+director_cmd_error(struct director_connection *conn, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ e_error(conn->event, "Command %s: %s (input: %s)",
+ conn->cur_cmd, t_strdup_vprintf(fmt, args),
+ t_strarray_join(conn->cur_args, "\t"));
+ va_end(args);
+
+ if (conn->host != NULL)
+ conn->host->last_protocol_failure = ioloop_time;
+}
+
+static void
+director_connection_append_stats(struct director_connection *conn, string_t *str)
+{
+ struct rusage usage;
+
+ str_printfa(str, "bytes in=%"PRIuUOFF_T", bytes out=%"PRIuUOFF_T,
+ conn->input->v_offset, conn->output->offset);
+ str_printfa(str, ", %u+%u USERs received",
+ conn->handshake_users_received, conn->users_received);
+ if (conn->handshake_users_sent > 0) {
+ str_printfa(str, ", %u USERs sent in handshake",
+ conn->handshake_users_sent);
+ }
+ if (conn->last_input.tv_sec > 0) {
+ int input_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->last_input);
+ str_printfa(str, ", last input %u.%03u s ago",
+ input_msecs/1000, input_msecs%1000);
+ }
+ if (conn->last_output.tv_sec > 0) {
+ int output_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->last_output);
+ str_printfa(str, ", last output %u.%03u s ago",
+ output_msecs/1000, output_msecs%1000);
+ }
+ if (conn->connected) {
+ int connected_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connected_time);
+ str_printfa(str, ", connected %u.%03u s ago",
+ connected_msecs/1000, connected_msecs%1000);
+ }
+ if (o_stream_get_buffer_used_size(conn->output) > 0) {
+ str_printfa(str, ", %zu bytes in output buffer",
+ o_stream_get_buffer_used_size(conn->output));
+ }
+ str_printfa(str, ", %zu peak output buffer size",
+ conn->peak_bytes_buffered);
+ if (conn->connected_user_cpu_set &&
+ getrusage(RUSAGE_SELF, &usage) == 0) {
+ /* this isn't measuring the CPU usage used by the connection
+ itself, but it can still be a useful measurement */
+ int diff = timeval_diff_msecs(&usage.ru_utime,
+ &conn->connected_user_cpu);
+ str_printfa(str, ", %d.%03d CPU secs since connected",
+ diff / 1000, diff % 1000);
+ }
+}
+
+static void
+director_connection_init_timeout(struct director_connection *conn)
+{
+ struct timeval start_time;
+ string_t *reason = t_str_new(128);
+
+ if (!conn->connected) {
+ start_time = conn->created;
+ str_append(reason, "Connect timed out");
+ } else if (!conn->me_received) {
+ start_time = conn->connected_time;
+ str_append(reason, "Handshaking ME timed out");
+ } else if (!conn->in) {
+ start_time = conn->me_received_time;
+ str_append(reason, "Sending handshake timed out");
+ } else {
+ start_time = conn->me_received_time;
+ str_append(reason, "Handshaking DONE timed out");
+ }
+ int msecs = timeval_diff_msecs(&ioloop_timeval, &start_time);
+ str_printfa(reason, " (%u.%03u secs, ", msecs/1000, msecs%1000);
+ director_connection_append_stats(conn, reason);
+ str_append_c(reason, ')');
+
+ e_error(conn->event, "%s", str_c(reason));
+ director_connection_disconnected(&conn, "Handshake timeout");
+}
+
+static void
+director_connection_set_ping_timeout(struct director_connection *conn)
+{
+ unsigned int msecs;
+
+ msecs = conn->synced || !conn->handshake_received ?
+ DIRECTOR_CONNECTION_PING_INTERVAL_MSECS :
+ DIRECTOR_CONNECTION_PING_SYNC_INTERVAL_MSECS;
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(msecs, director_connection_ping, conn);
+}
+
+static void director_connection_wait_timeout(struct director_connection *conn)
+{
+ director_connection_log_disconnect(conn, ETIMEDOUT, "");
+ director_connection_deinit(&conn,
+ "Timeout waiting for disconnect after CONNECT");
+}
+
+static void director_connection_send_connect(struct director_connection *conn,
+ struct director_host *host)
+{
+ const char *connect_str;
+
+ if (conn->to_disconnect != NULL)
+ return;
+
+ connect_str = t_strdup_printf("CONNECT\t%s\t%u\n",
+ host->ip_str, host->port);
+ director_connection_send(conn, connect_str);
+ o_stream_uncork(conn->output);
+
+ /* wait for a while for the remote to disconnect, so it will hopefully
+ see our CONNECT command. we'll also log the warning later to avoid
+ multiple log lines about it. */
+ conn->connect_request_to = host;
+ director_host_ref(conn->connect_request_to);
+
+ conn->to_disconnect =
+ timeout_add(DIRECTOR_WAIT_DISCONNECT_SECS*1000,
+ director_connection_wait_timeout, conn);
+}
+
+static void director_connection_assigned(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ if (dir->left != NULL && dir->right != NULL) {
+ /* we're connected to both directors. see if the ring is
+ finished by sending a SYNC. if we get it back, it's done. */
+ dir->sync_seq++;
+ director_set_ring_unsynced(dir);
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+ }
+ director_connection_set_ping_timeout(conn);
+}
+
+static bool director_connection_assign_left(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ i_assert(conn->in);
+ i_assert(dir->left != conn);
+
+ /* make sure this is the correct incoming connection */
+ if (conn->host->self) {
+ e_error(conn->event, "Connection from self, dropping");
+ return FALSE;
+ } else if (dir->left == NULL) {
+ /* no conflicts yet */
+ } else if (dir->left->host == conn->host) {
+ e_warning(conn->event,
+ "Replacing left director connection %s with %s",
+ dir->left->host->name, conn->host->name);
+ director_connection_deinit(&dir->left, t_strdup_printf(
+ "Replacing with %s", conn->host->name));
+ } else if (dir->left->verifying_left) {
+ /* we're waiting to verify if our current left is still
+ working. if we don't receive a PONG, the current left
+ gets disconnected and a new left gets assigned. if we do
+ receive a PONG, we'll wait until the current left
+ disconnects us and then reassign the new left. */
+ return TRUE;
+ } else if (director_host_cmp_to_self(dir->left->host, conn->host,
+ dir->self_host) < 0) {
+ /* the old connection is the correct one.
+ refer the client there (FIXME: do we ever get here?) */
+ director_connection_send_connect(conn, dir->left->host);
+ return TRUE;
+ } else {
+ /* this new connection is the correct one, but wait until the
+ old connection gets disconnected before using this one.
+ that guarantees that the director inserting itself into
+ the ring has finished handshaking its left side, so the
+ switch will be fast. */
+ return TRUE;
+ }
+ dir->left = conn;
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/left", conn->host->name));
+ director_connection_assigned(conn);
+ return TRUE;
+}
+
+static void director_assign_left(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (conn->in && conn->handshake_received &&
+ conn->to_disconnect == NULL && conn != dir->left) {
+ /* either use this or disconnect it */
+ if (!director_connection_assign_left(conn)) {
+ /* we don't want this */
+ director_connection_deinit(&conn,
+ "Unwanted incoming connection");
+ director_assign_left(dir);
+ break;
+ }
+ }
+ }
+}
+
+static bool director_has_outgoing_connections(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (!conn->in && conn->to_disconnect == NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void director_send_delayed_syncs(struct director *dir)
+{
+ struct director_host *host;
+
+ i_assert(dir->right != NULL);
+
+ e_debug(dir->right->event, "Sending delayed SYNCs");
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (host->delayed_sync_seq == 0)
+ continue;
+
+ director_sync_send(dir, host, host->delayed_sync_seq,
+ host->delayed_sync_minor_version,
+ host->delayed_sync_timestamp,
+ host->delayed_sync_hosts_hash);
+ host->delayed_sync_seq = 0;
+ }
+}
+
+static bool director_connection_assign_right(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ i_assert(!conn->in);
+
+ if (dir->right != NULL) {
+ /* see if we should disconnect or keep the existing
+ connection. */
+ if (director_host_cmp_to_self(conn->host, dir->right->host,
+ dir->self_host) <= 0) {
+ /* the old connection is the correct one */
+ e_warning(conn->event,
+ "Aborting incorrect outgoing connection to %s "
+ "(already connected to correct one: %s)",
+ conn->host->name, dir->right->host->name);
+ conn->wrong_host = TRUE;
+ return FALSE;
+ }
+ e_warning(conn->event,
+ "Replacing right director connection %s with %s",
+ dir->right->host->name, conn->host->name);
+ director_connection_deinit(&dir->right, t_strdup_printf(
+ "Replacing with %s", conn->host->name));
+ }
+ dir->right = conn;
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/right", conn->host->name));
+ director_connection_assigned(conn);
+ director_send_delayed_syncs(dir);
+ return TRUE;
+}
+
+static bool
+director_args_parse_ip_port(struct director_connection *conn,
+ const char *const *args,
+ struct ip_addr *ip_r, in_port_t *port_r)
+{
+ if (args[0] == NULL || args[1] == NULL) {
+ director_cmd_error(conn, "Missing IP+port parameters");
+ return FALSE;
+ }
+ if (net_addr2ip(args[0], ip_r) < 0) {
+ director_cmd_error(conn, "Invalid IP address: %s", args[0]);
+ return FALSE;
+ }
+ if (net_str2port(args[1], port_r) < 0) {
+ director_cmd_error(conn, "Invalid port: %s", args[1]);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool director_cmd_me(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ const char *connect_str;
+ struct ip_addr ip;
+ in_port_t port;
+ time_t next_comm_attempt;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+ if (conn->me_received) {
+ director_cmd_error(conn, "Duplicate ME");
+ return FALSE;
+ }
+
+ if (!conn->in && (!net_ip_compare(&conn->host->ip, &ip) ||
+ conn->host->port != port)) {
+ e_error(conn->event,
+ "Remote director thinks it's someone else "
+ "(connected to %s:%u, remote says it's %s:%u)",
+ conn->host->ip_str, conn->host->port,
+ net_ip2addr(&ip), port);
+ return FALSE;
+ }
+ conn->me_received = TRUE;
+ conn->me_received_time = ioloop_timeval;
+
+ if (args[2] != NULL) {
+ time_t remote_time;
+ int diff;
+
+ if (str_to_time(args[2], &remote_time) < 0) {
+ director_cmd_error(conn, "Invalid ME timestamp");
+ return FALSE;
+ }
+ diff = ioloop_time - remote_time;
+ if (diff > DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS ||
+ (diff < 0 && -diff > DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS)) {
+ e_warning(conn->event,
+ "Director %s clock differs from ours by %d secs",
+ conn->name, diff);
+ }
+ }
+
+ timeout_remove(&conn->to_ping);
+ if (conn->in) {
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_DONE_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ } else {
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_SEND_USERS_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ }
+
+ if (!conn->in)
+ return TRUE;
+
+ /* Incoming connection:
+
+ a) we don't have an established ring yet. make sure we're connecting
+ to our right side (which might become our left side).
+
+ b) it's our current "left" connection. the previous connection
+ is most likely dead.
+
+ c) we have an existing ring. tell our current "left" to connect to
+ it with CONNECT command.
+
+ d) the incoming connection doesn't belong to us at all, refer it
+ elsewhere with CONNECT. however, before disconnecting it verify
+ first that our left side is actually still functional.
+ */
+ i_assert(conn->host == NULL);
+ conn->host = director_host_get(dir, &ip, port);
+ /* the host shouldn't be removed at this point, but if for some
+ reason it is we don't want to crash */
+ conn->host->removed = FALSE;
+ director_host_ref(conn->host);
+ /* make sure we don't keep old sequence values across restarts */
+ director_host_restarted(conn->host);
+
+ next_comm_attempt = conn->host->last_protocol_failure +
+ DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS;
+ if (next_comm_attempt > ioloop_time) {
+ /* the director recently sent invalid protocol data,
+ don't try retrying yet */
+ e_error(conn->event,
+ "Remote sent invalid protocol data recently, "
+ "waiting %u secs before allowing further communication",
+ (unsigned int)(next_comm_attempt-ioloop_time));
+ return FALSE;
+ } else if (dir->left == NULL) {
+ /* a) - just in case the left is also our right side reset
+ its failed state, so we can connect to it */
+ conn->host->last_network_failure = 0;
+ if (!director_has_outgoing_connections(dir))
+ director_connect(dir, "Connecting to left");
+ } else if (dir->left->host == conn->host) {
+ /* b) */
+ i_assert(dir->left != conn);
+ director_connection_deinit(&dir->left,
+ "Replacing with new incoming connection");
+ } else if (director_host_cmp_to_self(conn->host, dir->left->host,
+ dir->self_host) < 0) {
+ /* c) */
+ connect_str = t_strdup_printf("CONNECT\t%s\t%u\n",
+ conn->host->ip_str,
+ conn->host->port);
+ director_connection_send(dir->left, connect_str);
+ } else {
+ /* d) */
+ dir->left->verifying_left = TRUE;
+ director_connection_ping(dir->left);
+ }
+ return TRUE;
+}
+
+static inline bool
+user_need_refresh(struct director *dir, struct user *user,
+ time_t timestamp, bool unknown_timestamp)
+{
+ if (timestamp <= (time_t)user->timestamp) {
+ /* we already have this timestamp */
+ return FALSE;
+ }
+ if (unknown_timestamp) {
+ /* Old director sent USER command without timestamp. We don't
+ know what it is exactly, but we can assume that it's very
+ close to the current time (which timestamp parameter is
+ already set to). However, try to break USER loops here when
+ director ring latency is >1sec, but below skip_recent_secs
+ by just not refreshing the user. */
+ time_t skip_recent_secs =
+ I_MIN(dir->set->director_user_expire/4,
+ DIRECTOR_SKIP_RECENT_REFRESH_MAX_SECS);
+ if ((time_t)user->timestamp + skip_recent_secs >= timestamp)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+director_user_refresh(struct director_connection *conn,
+ unsigned int username_hash, struct mail_host *host,
+ time_t timestamp, bool weak, bool *forced_r,
+ struct user **user_r)
+{
+ struct director *dir = conn->dir;
+ struct user *user;
+ bool ret = FALSE, unset_weak_user = FALSE;
+ struct user_directory *users = host->tag->users;
+ bool unknown_timestamp = (timestamp == (time_t)-1);
+
+ *forced_r = FALSE;
+
+ if (unknown_timestamp) {
+ /* Old director version sent USER without timestamp. */
+ timestamp = ioloop_time;
+ }
+
+ if (timestamp + (time_t)dir->set->director_user_expire <= ioloop_time) {
+ /* Ignore this refresh entirely, regardless of whether the
+ user already exists or not. */
+ e_debug(conn->event,
+ "user refresh: %u has expired timestamp %"PRIdTIME_T,
+ username_hash, timestamp);
+ return -1;
+ }
+
+ user = user_directory_lookup(users, username_hash);
+ if (user == NULL) {
+ *user_r = user_directory_add(users, username_hash,
+ host, timestamp);
+ (*user_r)->weak = weak;
+ e_debug(conn->event, "user refresh: %u added", username_hash);
+ return 1;
+ }
+
+ if (user->weak) {
+ if (!weak) {
+ /* removing user's weakness */
+ e_debug(conn->event, "user refresh: %u weakness removed",
+ username_hash);
+ unset_weak_user = TRUE;
+ user->weak = FALSE;
+ ret = TRUE;
+ } else {
+ /* weak user marked again as weak */
+ }
+ } else if (weak &&
+ !user_directory_user_is_recently_updated(users, user)) {
+ /* mark the user as weak */
+ e_debug(conn->event, "user refresh: %u set weak", username_hash);
+ user->weak = TRUE;
+ ret = TRUE;
+ } else if (weak) {
+ e_debug(conn->event,
+ "user refresh: %u weak update to %s ignored, "
+ "we recently changed it to %s",
+ username_hash, host->ip_str,
+ user->host->ip_str);
+ host = user->host;
+ ret = TRUE;
+ } else if (user->host == host) {
+ /* update to the same host */
+ } else if (user_directory_user_is_near_expiring(users, user)) {
+ /* host conflict for a user that is already near expiring. we can
+ assume that the other director had already dropped this user
+ and we should have as well. use the new host. */
+ e_debug(conn->event, "user refresh: %u is nearly expired, "
+ "replacing host %s with %s", username_hash,
+ user->host->ip_str, host->ip_str);
+ ret = TRUE;
+ } else if (USER_IS_BEING_KILLED(user)) {
+ /* user is still being moved - ignore conflicting host updates
+ from other directors who don't yet know about the move. */
+ e_debug(conn->event, "user refresh: %u is being moved, "
+ "preserve its host %s instead of replacing with %s",
+ username_hash, user->host->ip_str, host->ip_str);
+ host = user->host;
+ } else {
+ /* non-weak user received a non-weak update with
+ conflicting host. this shouldn't happen. */
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "User hash %u "
+ "is being redirected to two hosts: %s and %s",
+ username_hash, user->host->ip_str, host->ip_str);
+ str_printfa(str, " (old_ts=%ld", (long)user->timestamp);
+
+ if (!conn->handshake_received) {
+ str_printfa(str, ",handshaking,recv_ts=%ld",
+ (long)timestamp);
+ }
+ if (USER_IS_BEING_KILLED(user)) {
+ if (user->kill_ctx->to_move != NULL)
+ str_append(str, ",moving");
+ str_printfa(str, ",kill_state=%s",
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ }
+ str_append_c(str, ')');
+ e_error(conn->event, "%s", str_c(str));
+
+ /* we want all the directors to redirect the user to same
+ server, but we don't want two directors fighting over which
+ server it belongs to, so always use the lower IP address */
+ if (net_ip_cmp(&user->host->ip, &host->ip) > 0) {
+ /* change the host. we'll also need to remove the user
+ from the old host's user_count, because we can't
+ keep track of the user for more than one host.
+
+ send the updated USER back to the sender as well. */
+ *forced_r = TRUE;
+ } else {
+ /* keep the host */
+ host = user->host;
+ }
+ /* especially IMAP connections can take a long time to die.
+ make sure we kill off the connections in the wrong
+ backends. */
+ director_kick_user_hash(dir, dir->self_host, NULL,
+ username_hash, &host->ip);
+ ret = TRUE;
+ }
+ if (user->host != host) {
+ user->host->user_count--;
+ user->host = host;
+ user->host->user_count++;
+ ret = TRUE;
+ }
+ /* Update user's timestamp if it's higher than the current one. Note
+ that we'll preserve the original timestamp. This is important when
+ the director ring is slow and a single USER can traverse through
+ the ring more than a second. We don't want to get into a loop where
+ the same USER goes through the ring forever. */
+ if (user_need_refresh(dir, user, timestamp, unknown_timestamp)) {
+ /* NOTE: This makes the users list somewhat out-of-order.
+ It's not a big problem - most likely it's only a few seconds
+ difference. The worst that can happen is that some users
+ take up memory that should have been freed already. */
+ e_debug(conn->event, "user refresh: %u refreshed timestamp from %u to %"PRIdTIME_T,
+ username_hash, user->timestamp, timestamp);
+ user_directory_refresh(users, user);
+ user->timestamp = timestamp;
+ ret = TRUE;
+ } else {
+ e_debug(conn->event, "user refresh: %u ignored timestamp %"PRIdTIME_T" (we have %u)",
+ username_hash, timestamp, user->timestamp);
+ }
+
+ if (unset_weak_user) {
+ /* user is no longer weak. handle pending requests for
+ this user if there are any */
+ director_set_state_changed(conn->dir);
+ }
+
+ *user_r = user;
+ return ret ? 1 : 0;
+}
+
+static bool
+director_handshake_cmd_user(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash, timestamp;
+ struct ip_addr ip;
+ struct mail_host *host;
+ struct user *user;
+ bool weak, forced;
+
+ if (str_array_length(args) < 3 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0 ||
+ str_to_uint(args[2], &timestamp) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ weak = args[3] != NULL && args[3][0] == 'w';
+ conn->handshake_users_received++;
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ e_error(conn->event, "USER used unknown host %s in handshake",
+ args[1]);
+ return FALSE;
+ }
+
+ if ((time_t)timestamp > ioloop_time) {
+ /* The other director's clock seems to be into the future
+ compared to us. Don't set any of our users' timestamps into
+ future though. It's most likely only 1 second difference. */
+ timestamp = ioloop_time;
+ }
+ conn->dir->num_incoming_requests++;
+ if (director_user_refresh(conn, username_hash, host,
+ timestamp, weak, &forced, &user) < 0) {
+ /* user expired - ignore */
+ return TRUE;
+ }
+ /* Possibilities:
+
+ a) The user didn't exist yet, and it was added with the given
+ timestamp.
+
+ b) The user existed, but with an older timestamp. The timestamp
+ wasn't yet updated, so do it here below.
+
+ c) The user existed with a newer timestamp. This is either because
+ we already received a non-handshake USER update for this user, or
+ our director saw a login for this user. Ignore this update.
+
+ (We never want to change the user's timestamp to be older, because
+ that could result in directors going to a loop fighting each others
+ over a flipping timestamp.) */
+ if (user->timestamp < timestamp)
+ user->timestamp = timestamp;
+ /* always sort users after handshaking to make sure the order
+ is correct */
+ conn->users_unsorted = TRUE;
+ return TRUE;
+}
+
+static bool
+director_cmd_user(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash;
+ struct ip_addr ip;
+ struct mail_host *host;
+ struct user *user;
+ bool forced;
+ time_t timestamp = (time_t)-1;
+
+ if (str_array_length(args) < 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0 ||
+ (args[2] != NULL && str_to_time(args[2], &timestamp) < 0)) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ /* could this before it's potentially ignored */
+ conn->dir->num_incoming_requests++;
+
+ conn->users_received++;
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ /* we probably just removed this host. */
+ return TRUE;
+ }
+
+ if (director_user_refresh(conn, username_hash,
+ host, timestamp, FALSE, &forced, &user) > 0) {
+ /* user changed - forward the USER in the ring */
+ struct director_host *src_host =
+ forced ? conn->dir->self_host : conn->host;
+ i_assert(!user->weak);
+ director_update_user(conn->dir, src_host, user);
+ }
+ return TRUE;
+}
+
+static bool director_cmd_director(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ bool log_add = FALSE;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host != NULL) {
+ if (host == conn->dir->self_host) {
+ /* ignore updates to ourself */
+ return TRUE;
+ }
+ if (host->removed) {
+ /* ignore re-adds of removed directors */
+ return TRUE;
+ }
+
+ /* already have this. just reset its last_network_failure
+ timestamp, since it might be up now, but only if this
+ isn't part of the handshake. (if it was, reseting the
+ timestamp could cause us to rapidly keep trying to connect
+ to it) */
+ if (conn->handshake_received)
+ host->last_network_failure = 0;
+ /* it also may have been restarted, reset its state */
+ director_host_restarted(host);
+ } else {
+ /* save the director and forward it */
+ host = director_host_add(conn->dir, &ip, port);
+ log_add = TRUE;
+ }
+ /* just forward this to the entire ring until it reaches back to
+ itself. some hosts may see this twice, but that's the only way to
+ guarantee that it gets seen by everyone. resetting the host multiple
+ times may cause us to handle its commands multiple times, but the
+ commands can handle that. however, we need to also handle a
+ situation where the added director never comes back - we don't want
+ to send the director information in a loop forever. */
+ if (conn->dir->right != NULL &&
+ director_host_cmp_to_self(host, conn->dir->right->host,
+ conn->dir->self_host) > 0) {
+ e_debug(conn->event,
+ "Received DIRECTOR update for a host where we should be connected to. "
+ "Not forwarding it since it's probably crashed.");
+ } else {
+ director_notify_ring_added(host,
+ director_connection_get_host(conn), log_add);
+ }
+ return TRUE;
+}
+
+static bool director_cmd_director_remove(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host != NULL && !host->removed)
+ director_ring_remove(host, director_connection_get_host(conn));
+ return TRUE;
+}
+
+static bool
+director_cmd_host_hand_start(struct director_connection *conn,
+ const char *const *args)
+{
+ const ARRAY_TYPE(mail_host) *hosts;
+ struct mail_host *const *hostp;
+ unsigned int remote_ring_completed;
+
+ if (args[0] == NULL ||
+ str_to_uint(args[0], &remote_ring_completed) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ if (remote_ring_completed != 0 && !conn->dir->ring_handshaked) {
+ /* clear everything we have and use only what remote sends us */
+ e_debug(conn->event, "We're joining a ring - replace all hosts");
+ hosts = mail_hosts_get(conn->dir->mail_hosts);
+ while (array_count(hosts) > 0) {
+ hostp = array_front(hosts);
+ director_remove_host(conn->dir, NULL, NULL, *hostp);
+ }
+ } else if (remote_ring_completed == 0 && conn->dir->ring_handshaked) {
+ /* ignore whatever remote sends */
+ e_debug(conn->event, "Remote is joining our ring - "
+ "ignore all remote HOSTs");
+ conn->ignore_host_events = TRUE;
+ } else {
+ e_debug(conn->event, "Merge rings' hosts");
+ }
+ conn->handshake_sending_hosts = TRUE;
+ return TRUE;
+}
+
+static int
+director_cmd_is_seen_full(struct director_connection *conn,
+ const char *const **_args, unsigned int *seq_r,
+ struct director_host **host_r)
+{
+ const char *const *args = *_args;
+ struct ip_addr ip;
+ in_port_t port;
+ unsigned int seq;
+ struct director_host *host;
+
+ if (str_array_length(args) < 3 ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ net_str2port(args[1], &port) < 0 ||
+ str_to_uint(args[2], &seq) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return -1;
+ }
+ *_args = args + 3;
+ *seq_r = seq;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host == NULL || host->removed) {
+ /* director is already gone, but we can't be sure if this
+ command was sent everywhere. re-send it as if it was from
+ ourself. */
+ *host_r = NULL;
+ } else {
+ *host_r = host;
+ if (seq <= host->last_seq) {
+ /* already seen this */
+ return 1;
+ }
+ host->last_seq = seq;
+ }
+ return 0;
+}
+
+static int
+director_cmd_is_seen(struct director_connection *conn,
+ const char *const **_args,
+ struct director_host **host_r)
+{
+ unsigned int seq;
+
+ return director_cmd_is_seen_full(conn, _args, &seq, host_r);
+}
+
+static bool
+director_cmd_user_weak(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct ip_addr ip;
+ unsigned int username_hash;
+ struct mail_host *host;
+ struct user *user;
+ struct director_host *src_host = conn->host;
+ bool weak = TRUE, weak_forward = FALSE, forced;
+ int ret;
+
+ /* note that unlike other commands we don't want to just ignore
+ duplicate commands */
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) < 0)
+ return FALSE;
+
+ /* could this before it's potentially ignored */
+ conn->dir->num_incoming_requests++;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ /* we probably just removed this host. */
+ return TRUE;
+ }
+
+ if (ret == 0) {
+ /* First time we're seeing this - forward it to others also.
+ We'll want to do it even if the user was already marked as
+ weak, because otherwise if two directors mark the user weak
+ at the same time both the USER-WEAK notifications reach
+ only half the directors until they collide and neither one
+ finishes going through the whole ring marking the user
+ non-weak. */
+ weak_forward = TRUE;
+ } else if (dir_host == conn->dir->self_host) {
+ /* We originated this USER-WEAK request. The entire ring has seen
+ it and there weren't any conflicts. Make the user non-weak. */
+ e_debug(conn->event,
+ "user refresh: %u Our USER-WEAK seen by the entire ring",
+ username_hash);
+ src_host = conn->dir->self_host;
+ weak = FALSE;
+ } else {
+ /* The original USER-WEAK sender will send a new non-weak USER
+ update saying what really happened. We'll still need to forward
+ this around the ring to the origin so it also knows it has
+ travelled through the ring. */
+ e_debug(conn->event,
+ "user refresh: %u Remote USER-WEAK from %s seen by the entire ring, ignoring",
+ username_hash, dir_host->ip_str);
+ weak_forward = TRUE;
+ }
+
+ ret = director_user_refresh(conn, username_hash,
+ host, ioloop_time, weak, &forced, &user);
+ /* user is refreshed with ioloop_time, it can't be expired already */
+ i_assert(ret >= 0);
+ if (ret > 0 || weak_forward) {
+ /* user changed, or we've decided that we need to forward
+ the weakness notification to the rest of the ring even
+ though we already knew it. */
+ if (forced)
+ src_host = conn->dir->self_host;
+ if (!user->weak)
+ director_update_user(conn->dir, src_host, user);
+ else {
+ director_update_user_weak(conn->dir, src_host, conn,
+ dir_host, user);
+ }
+ }
+ return TRUE;
+}
+
+static bool ATTR_NULL(3)
+director_cmd_host_int(struct director_connection *conn, const char *const *args,
+ struct director_host *dir_host)
+{
+ struct director_host *src_host = conn->host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ const char *tag = "", *host_tag, *hostname = NULL;
+ unsigned int arg_count, vhost_count;
+ bool update, down = FALSE, tag_changed = FALSE;
+ time_t last_updown_change = 0;
+
+ arg_count = str_array_length(args);
+ if (arg_count < 2 ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ str_to_uint(args[1], &vhost_count) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 3)
+ tag = args[2];
+ if (arg_count >= 4) {
+ if ((args[3][0] != 'D' && args[3][0] != 'U') ||
+ str_to_time(args[3]+1, &last_updown_change) < 0) {
+ director_cmd_error(conn, "Invalid updown parameters");
+ return FALSE;
+ }
+ down = args[3][0] == 'D';
+ }
+ if (arg_count >= 5)
+ hostname = args[4];
+ if (conn->ignore_host_events) {
+ /* remote is sending hosts in a handshake, but it doesn't have
+ a completed ring and we do. */
+ i_assert(conn->handshake_sending_hosts);
+ return TRUE;
+ }
+ if (tag[0] != '\0' && conn->minor_version < DIRECTOR_VERSION_TAGS_V2) {
+ director_cmd_error(conn, "Received a host tag from older director version with incompatible tagging support");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ host = mail_host_add_hostname(conn->dir->mail_hosts,
+ hostname, &ip, tag);
+ update = TRUE;
+ } else {
+ host_tag = mail_host_get_tag(host);
+ tag_changed = strcmp(tag, host_tag) != 0;
+ update = host->vhost_count != vhost_count ||
+ host->down != down || tag_changed;
+
+ if (update && host->desynced) {
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "Host %s is being updated before previous update had finished (",
+ host->ip_str);
+ if (host->down != down &&
+ host->last_updown_change > last_updown_change) {
+ /* our host has a newer change. preserve it. */
+ down = host->down;
+ }
+ if (host->down != down) {
+ if (host->down)
+ str_append(str, "down -> up");
+ else
+ str_append(str, "up -> down");
+ }
+ if (host->vhost_count != vhost_count) {
+ if (host->down != down)
+ str_append(str, ", ");
+ str_printfa(str, "vhosts %u -> %u",
+ host->vhost_count, vhost_count);
+ }
+ str_append(str, ") - ");
+
+ vhost_count = I_MIN(vhost_count, host->vhost_count);
+ str_printfa(str, "setting to state=%s vhosts=%u",
+ down ? "down" : "up", vhost_count);
+ e_warning(conn->event, "%s", str_c(str));
+ /* make the change appear to come from us, so it
+ reaches the full ring */
+ dir_host = NULL;
+ src_host = conn->dir->self_host;
+ }
+ if (update) {
+ /* Make sure the host's timestamp never shrinks.
+ Otherwise we might get into a loop where the up/down
+ state keeps switching. */
+ last_updown_change = I_MAX(last_updown_change,
+ host->last_updown_change);
+ }
+ }
+
+ if (update) {
+ const char *log_prefix = t_strdup_printf("director(%s): ",
+ conn->name);
+ if (tag_changed) {
+ e_error(conn->event,
+ "Host %s changed tag from '%s' to '%s'",
+ host->ip_str,
+ mail_host_get_tag(host), tag);
+ mail_host_set_tag(host, tag);
+ }
+ mail_host_set_down(host, down, last_updown_change, log_prefix);
+ mail_host_set_vhost_count(host, vhost_count, log_prefix);
+ director_update_host(conn->dir, src_host, dir_host, host);
+ } else {
+ e_debug(conn->event,
+ "Ignoring host %s update vhost_count=%u "
+ "down=%d last_updown_change=%ld (hosts_hash=%u)",
+ net_ip2addr(&ip), vhost_count, down ? 1 : 0,
+ (long)last_updown_change,
+ mail_hosts_hash(conn->dir->mail_hosts));
+ }
+ return TRUE;
+}
+
+static bool
+director_cmd_host_handshake(struct director_connection *conn,
+ const char *const *args)
+{
+ return director_cmd_host_int(conn, args, NULL);
+}
+
+static bool
+director_cmd_host(struct director_connection *conn, const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+ return director_cmd_host_int(conn, args, dir_host);
+}
+
+static bool
+director_cmd_host_remove(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1 ||
+ net_addr2ip(args[0], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL)
+ director_remove_host(conn->dir, conn->host, dir_host, host);
+ return TRUE;
+}
+
+static bool
+director_cmd_host_flush(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1 ||
+ net_addr2ip(args[0], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL)
+ director_flush_host(conn->dir, conn->host, dir_host, host);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_move(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ unsigned int username_hash;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL) {
+ director_move_user(conn->dir, conn->host, dir_host,
+ username_hash, host);
+ }
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user(conn->dir, conn->host, dir_host, args[0]);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick_alt(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user_alt(conn->dir, conn->host, dir_host, args[0], args[1]);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick_hash(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ unsigned int username_hash;
+ struct ip_addr except_ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &except_ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user_hash(conn->dir, conn->host, dir_host,
+ username_hash, &except_ip);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_killed(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash;
+
+ if (str_array_length(args) != 1 ||
+ str_to_uint(args[0], &username_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_user_killed(conn->dir, username_hash);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_killed_everywhere(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ unsigned int seq, username_hash;
+ int ret;
+
+ if ((ret = director_cmd_is_seen_full(conn, &args, &seq, &dir_host)) < 0)
+ return FALSE;
+
+ if (str_array_length(args) != 1 ||
+ str_to_uint(args[0], &username_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ if (ret > 0) {
+ i_assert(dir_host != NULL);
+ e_debug(conn->event,
+ "User %u - ignoring already seen USER-KILLED-EVERYWHERE "
+ "with seq=%u <= %s.last_seq=%u", username_hash,
+ seq, dir_host->name, dir_host->last_seq);
+ return TRUE;
+ }
+
+ director_user_killed_everywhere(conn->dir, conn->host,
+ dir_host, username_hash);
+ return TRUE;
+}
+
+static bool director_handshake_cmd_done(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+ int handshake_msecs = timeval_diff_msecs(&ioloop_timeval, &conn->connected_time);
+ string_t *str;
+
+ if (conn->users_unsorted && conn->user_iter == NULL) {
+ /* we sent our user list before receiving remote's */
+ conn->users_unsorted = FALSE;
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "Handshake finished in %u.%03u secs (",
+ handshake_msecs/1000, handshake_msecs%1000);
+ director_connection_append_stats(conn, str);
+ str_append_c(str, ')');
+ if (handshake_msecs >= DIRECTOR_HANDSHAKE_WARN_SECS*1000)
+ e_warning(conn->event, "%s", str_c(str));
+ else
+ e_info(conn->event, "%s", str_c(str));
+
+ /* the host is up now, make sure we can connect to it immediately
+ if needed */
+ conn->host->last_network_failure = 0;
+
+ conn->handshake_received = TRUE;
+ if (conn->in) {
+ /* handshaked to left side. tell it we've received the
+ whole handshake. */
+ director_connection_send(conn, "DONE\n");
+
+ /* tell the "right" director about the "left" one */
+ director_update_send(dir, director_connection_get_host(conn),
+ t_strdup_printf("DIRECTOR\t%s\t%u\n",
+ conn->host->ip_str,
+ conn->host->port));
+ /* this is our "left" side. */
+ return director_connection_assign_left(conn);
+ } else {
+ /* handshaked to "right" side. */
+ return director_connection_assign_right(conn);
+ }
+}
+
+static int
+director_handshake_cmd_options(struct director_connection *conn,
+ const char *const *args)
+{
+ bool consistent_hashing = FALSE;
+ unsigned int i;
+
+ for (i = 0; args[i] != NULL; i++) {
+ if (strcmp(args[i], DIRECTOR_OPT_CONSISTENT_HASHING) == 0)
+ consistent_hashing = TRUE;
+ }
+ if (!consistent_hashing) {
+ e_error(conn->event, "director_consistent_hashing settings "
+ "differ between directors. Set "
+ "director_consistent_hashing=yes on old directors");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+director_connection_handle_handshake(struct director_connection *conn,
+ const char *cmd, const char *const *args)
+{
+ unsigned int major_version;
+
+ /* both incoming and outgoing connections get VERSION and ME */
+ if (strcmp(cmd, "VERSION") == 0 && str_array_length(args) >= 3) {
+ if (strcmp(args[0], DIRECTOR_VERSION_NAME) != 0) {
+ e_error(conn->event, "Wrong protocol in socket "
+ "(%s vs %s)",
+ args[0], DIRECTOR_VERSION_NAME);
+ return -1;
+ } else if (str_to_uint(args[1], &major_version) < 0 ||
+ str_to_uint(args[2], &conn->minor_version) < 0) {
+ e_error(conn->event, "Invalid protocol version: "
+ "%s.%s", args[1], args[2]);
+ return -1;
+ } else if (major_version != DIRECTOR_VERSION_MAJOR) {
+ e_error(conn->event, "Incompatible protocol version: "
+ "%u vs %u", major_version,
+ DIRECTOR_VERSION_MAJOR);
+ return -1;
+ }
+ if (conn->minor_version < DIRECTOR_VERSION_TAGS_V2 &&
+ mail_hosts_have_tags(conn->dir->mail_hosts)) {
+ e_error(conn->event, "Director version supports incompatible tags");
+ return -1;
+ }
+ conn->version_received = TRUE;
+ director_finish_sending_handshake(conn);
+ return 1;
+ }
+ if (!conn->version_received) {
+ director_cmd_error(conn, "Incompatible protocol");
+ return -1;
+ }
+
+ if (strcmp(cmd, "ME") == 0)
+ return director_cmd_me(conn, args) ? 1 : -1;
+ if (!conn->me_received) {
+ director_cmd_error(conn, "Expecting ME command first");
+ return -1;
+ }
+
+ /* incoming connections get a HOST list */
+ if (conn->handshake_sending_hosts) {
+ if (strcmp(cmd, "HOST") == 0)
+ return director_cmd_host_handshake(conn, args) ? 1 : -1;
+ if (strcmp(cmd, "HOST-HAND-END") == 0) {
+ conn->ignore_host_events = FALSE;
+ conn->handshake_sending_hosts = FALSE;
+ return 1;
+ }
+ director_cmd_error(conn, "Unexpected command during host list");
+ return -1;
+ }
+ if (strcmp(cmd, "OPTIONS") == 0)
+ return director_handshake_cmd_options(conn, args);
+ if (strcmp(cmd, "HOST-HAND-START") == 0) {
+ if (!conn->in) {
+ director_cmd_error(conn,
+ "Host list is only for incoming connections");
+ return -1;
+ }
+ return director_cmd_host_hand_start(conn, args) ? 1 : -1;
+ }
+
+ if (conn->in &&
+ (strcmp(cmd, "U") == 0 ||
+ (strcmp(cmd, "USER") == 0 &&
+ CMD_IS_USER_HANDSHAKE(conn->minor_version, args))))
+ return director_handshake_cmd_user(conn, args) ? 1 : -1;
+
+ /* both get DONE */
+ if (strcmp(cmd, "DONE") == 0)
+ return director_handshake_cmd_done(conn) ? 1 : -1;
+ return 0;
+}
+
+static bool
+director_connection_sync_host(struct director_connection *conn,
+ struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash)
+{
+ struct director *dir = conn->dir;
+
+ if (minor_version > DIRECTOR_VERSION_MINOR) {
+ /* we're not up to date */
+ minor_version = DIRECTOR_VERSION_MINOR;
+ }
+
+ if (host->self) {
+ if (dir->sync_seq != seq) {
+ /* stale SYNC event */
+ return FALSE;
+ }
+ /* sync_seq increases when we get disconnected, so we must be
+ successfully connected to both directions */
+ i_assert(dir->left != NULL && dir->right != NULL);
+
+ if (hosts_hash != 0 &&
+ hosts_hash != mail_hosts_hash(conn->dir->mail_hosts)) {
+ e_error(conn->event, "Hosts unexpectedly changed during SYNC reply - resending"
+ "(seq=%u, old hosts_hash=%u, new hosts_hash=%u)",
+ seq, hosts_hash,
+ mail_hosts_hash(dir->mail_hosts));
+ (void)director_resend_sync(dir);
+ return FALSE;
+ }
+
+ dir->ring_min_version = minor_version;
+ if (!dir->ring_handshaked) {
+ /* the ring is handshaked */
+ director_set_ring_handshaked(dir);
+ } else if (dir->ring_synced) {
+ /* duplicate SYNC (which was sent just in case the
+ previous one got lost) */
+ } else {
+ e_debug(conn->event, "Ring is synced (%s sent seq=%u, hosts_hash=%u)",
+ conn->name, seq,
+ mail_hosts_hash(dir->mail_hosts));
+ int sync_msecs =
+ timeval_diff_msecs(&ioloop_timeval, &dir->last_sync_start_time);
+ if (sync_msecs >= 0)
+ dir->last_sync_msecs = sync_msecs;
+ director_set_ring_synced(dir);
+ }
+ } else {
+ if (seq < host->last_sync_seq &&
+ timestamp < host->last_sync_timestamp +
+ DIRECTOR_SYNC_STALE_TIMESTAMP_RESET_SECS) {
+ /* stale SYNC event */
+ e_debug(conn->event, "Ignore stale SYNC event for %s "
+ "(seq %u < %u, timestamp=%u)",
+ host->name, seq, host->last_sync_seq,
+ timestamp);
+ return FALSE;
+ } else if (seq < host->last_sync_seq) {
+ e_warning(conn->event,
+ "Last SYNC seq for %s appears to be stale, resetting "
+ "(seq=%u, timestamp=%u -> seq=%u, timestamp=%u)",
+ host->name, host->last_sync_seq,
+ host->last_sync_timestamp, seq, timestamp);
+ host->last_sync_seq = seq;
+ host->last_sync_timestamp = timestamp;
+ host->last_sync_seq_counter = 1;
+ } else if (seq > host->last_sync_seq ||
+ timestamp > host->last_sync_timestamp) {
+ host->last_sync_seq = seq;
+ host->last_sync_timestamp = timestamp;
+ host->last_sync_seq_counter = 1;
+ e_debug(conn->event, "Update SYNC for %s "
+ "(seq=%u, timestamp=%u -> seq=%u, timestamp=%u)",
+ host->name, host->last_sync_seq,
+ host->last_sync_timestamp, seq, timestamp);
+ } else if (++host->last_sync_seq_counter >
+ DIRECTOR_MAX_SYNC_SEQ_DUPLICATES) {
+ /* we've received this too many times already */
+ e_debug(conn->event, "Ignore duplicate #%u SYNC event for %s "
+ "(seq=%u, timestamp %u <= %u)",
+ host->last_sync_seq_counter, host->name, seq,
+ timestamp, host->last_sync_timestamp);
+ return FALSE;
+ }
+
+ if (hosts_hash != 0 &&
+ hosts_hash != mail_hosts_hash(conn->dir->mail_hosts)) {
+ if (host->desynced_hosts_hash != hosts_hash) {
+ e_debug(conn->event, "Ignore director %s stale SYNC request whose hosts don't match us "
+ "(seq=%u, remote hosts_hash=%u, my hosts_hash=%u)",
+ host->ip_str, seq, hosts_hash,
+ mail_hosts_hash(dir->mail_hosts));
+ host->desynced_hosts_hash = hosts_hash;
+ return FALSE;
+ }
+ /* we'll get here only if we received a SYNC twice
+ with the same wrong hosts_hash. FIXME: this gets
+ triggered unnecessarily sometimes if hosts are
+ changing rapidly. */
+ e_error(conn->event, "Director %s SYNC request hosts don't match us - resending hosts "
+ "(seq=%u, remote hosts_hash=%u, my hosts_hash=%u)",
+ host->ip_str, seq,
+ hosts_hash, mail_hosts_hash(dir->mail_hosts));
+ director_resend_hosts(dir);
+ return FALSE;
+ }
+ host->desynced_hosts_hash = 0;
+ if (dir->right != NULL) {
+ /* forward it to the connection on right */
+ director_sync_send(dir, host, seq, minor_version,
+ timestamp, hosts_hash);
+ } else {
+ e_debug(conn->event, "We have no right connection - "
+ "delay replying to SYNC until finished");
+ host->delayed_sync_seq = seq;
+ host->delayed_sync_minor_version = minor_version;
+ host->delayed_sync_timestamp = timestamp;
+ host->delayed_sync_hosts_hash = hosts_hash;
+ }
+ }
+ return TRUE;
+}
+
+static bool director_connection_sync(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ unsigned int arg_count, seq, minor_version = 0, timestamp = ioloop_time;
+ unsigned int hosts_hash = 0;
+
+ arg_count = str_array_length(args);
+ if (arg_count < 3 ||
+ !director_args_parse_ip_port(conn, args, &ip, &port) ||
+ str_to_uint(args[2], &seq) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 4 && str_to_uint(args[3], &minor_version) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 5 && str_to_uint(args[4], &timestamp) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 6 && str_to_uint(args[5], &hosts_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ /* find the originating director. if we don't see it, it was already
+ removed and we can ignore this sync. */
+ host = director_host_lookup(dir, &ip, port);
+ if (host != NULL) {
+ if (!director_connection_sync_host(conn, host, seq,
+ minor_version, timestamp,
+ hosts_hash))
+ return TRUE;
+ }
+
+ /* If directors got disconnected while we were waiting a SYNC reply,
+ it might have gotten lost. If we've received a DIRECTOR update since
+ the last time we sent a SYNC, retry sending it here to make sure
+ it doesn't get stuck. We don't want to do this too eagerly because
+ it may trigger desynced_hosts_hash != hosts_hash mismatch, which
+ causes unnecessary error logging and hosts-resending. */
+ if ((host == NULL || !host->self) &&
+ dir->last_sync_sent_ring_change_counter != dir->ring_change_counter &&
+ (time_t)dir->self_host->last_sync_timestamp != ioloop_time)
+ (void)director_resend_sync(dir);
+ return TRUE;
+}
+
+static void director_disconnect_timeout(struct director_connection *conn)
+{
+ director_connection_deinit(&conn, "CONNECT requested");
+}
+
+static void
+director_reconnect_after_wrong_connect_timeout(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(&conn, "Wrong CONNECT requested");
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after wrong CONNECT request");
+}
+
+static void
+director_reconnect_after_wrong_connect(struct director_connection *conn)
+{
+ if (conn->to_disconnect != NULL)
+ return;
+ conn->to_disconnect =
+ timeout_add_short(DIRECTOR_RECONNECT_AFTER_WRONG_CONNECT_MSECS,
+ director_reconnect_after_wrong_connect_timeout, conn);
+}
+
+static bool director_cmd_connect(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ const char *right_state;
+
+ if (str_array_length(args) != 2 ||
+ !director_args_parse_ip_port(conn, args, &ip, &port)) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = director_host_get(conn->dir, &ip, port);
+
+ /* remote suggests us to connect elsewhere */
+ if (dir->right != NULL &&
+ director_host_cmp_to_self(host, dir->right->host,
+ dir->self_host) <= 0) {
+ /* the old connection is the correct one */
+ e_debug(conn->event, "Ignoring CONNECT request to %s (current right is %s)",
+ host->name, dir->right->name);
+ director_reconnect_after_wrong_connect(conn);
+ return TRUE;
+ }
+ if (host->removed) {
+ e_debug(conn->event, "Ignoring CONNECT request to %s (director is removed)",
+ host->name);
+ director_reconnect_after_wrong_connect(conn);
+ return TRUE;
+ }
+
+ /* reset failure timestamp so we'll actually try to connect there. */
+ host->last_network_failure = 0;
+ /* reset removed-flag, so we don't crash */
+ host->removed = FALSE;
+
+ if (dir->right == NULL) {
+ right_state = "initializing right";
+ } else {
+ right_state = t_strdup_printf("replacing current right %s",
+ dir->right->name);
+ /* disconnect from right side immediately - it's not accepting
+ any further commands from us. */
+ if (conn->dir->right != conn)
+ director_connection_deinit(&conn->dir->right, "CONNECT requested");
+ else if (conn->to_disconnect == NULL) {
+ conn->to_disconnect =
+ timeout_add_short(0, director_disconnect_timeout, conn);
+ }
+ }
+
+ /* connect here */
+ (void)director_connect_host(dir, host, t_strdup_printf(
+ "Received CONNECT request from %s - %s",
+ conn->name, right_state));
+ return TRUE;
+}
+
+static void director_disconnect_wrong_lefts(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (conn->in && conn != dir->left && conn->me_received &&
+ conn->to_disconnect == NULL &&
+ director_host_cmp_to_self(dir->left->host, conn->host,
+ dir->self_host) < 0)
+ director_connection_send_connect(conn, dir->left->host);
+ }
+}
+
+static bool director_cmd_ping(struct director_connection *conn,
+ const char *const *args)
+{
+ time_t sent_time;
+ uintmax_t send_buffer_size;
+
+ if (str_array_length(args) >= 2 &&
+ str_to_time(args[0], &sent_time) == 0 &&
+ str_to_uintmax(args[1], &send_buffer_size) == 0) {
+ int diff_secs = ioloop_time - sent_time;
+ if (diff_secs*1000+500 > DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS) {
+ e_warning(conn->event,
+ "PING response took %d secs to receive "
+ "(send buffer was %ju bytes)",
+ diff_secs, send_buffer_size);
+ }
+ }
+ director_connection_send(conn,
+ t_strdup_printf("PONG\t%"PRIdTIME_T"\t%zu\n",
+ ioloop_time, o_stream_get_buffer_used_size(conn->output)));
+ return TRUE;
+}
+
+static void
+director_ping_append_extra(struct director_connection *conn, string_t *str,
+ time_t pong_sent_time,
+ uintmax_t pong_send_buffer_size)
+{
+ struct rusage usage;
+
+ str_printfa(str, "buffer size at PING was %zu bytes", conn->ping_sent_buffer_size);
+ if (pong_sent_time != 0) {
+ str_printfa(str, ", remote sent it %"PRIdTIME_T" secs ago",
+ ioloop_time - pong_sent_time);
+ }
+ if (pong_send_buffer_size != (uintmax_t)-1) {
+ str_printfa(str, ", remote buffer size at PONG was %ju bytes",
+ pong_send_buffer_size);
+ }
+ if (conn->ping_sent_user_cpu.tv_sec != (time_t)-1 &&
+ getrusage(RUSAGE_SELF, &usage) == 0) {
+ int diff = timeval_diff_msecs(&usage.ru_utime,
+ &conn->ping_sent_user_cpu);
+ str_printfa(str, ", %u.%03u CPU secs since PING was sent",
+ diff/1000, diff%1000);
+ }
+ str_printfa(str, ", %"PRIuUOFF_T" bytes input",
+ conn->input->v_offset - conn->ping_sent_input_offset);
+ str_printfa(str, ", %"PRIuUOFF_T" bytes output",
+ conn->output->offset - conn->ping_sent_output_offset);
+}
+
+static bool director_cmd_pong(struct director_connection *conn,
+ const char *const *args)
+{
+ time_t sent_time;
+ uintmax_t send_buffer_size;
+
+ if (!conn->ping_waiting)
+ return TRUE;
+ conn->ping_waiting = FALSE;
+ timeout_remove(&conn->to_pong);
+
+ if (str_array_length(args) < 2 ||
+ str_to_time(args[0], &sent_time) < 0 ||
+ str_to_uintmax(args[1], &send_buffer_size) < 0) {
+ sent_time = 0;
+ send_buffer_size = (uintmax_t)-1;
+ }
+
+ int ping_msecs = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+ if (ping_msecs >= 0) {
+ if (ping_msecs > DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS) {
+ string_t *extra = t_str_new(128);
+ director_ping_append_extra(conn, extra, sent_time, send_buffer_size);
+ e_warning(conn->event,
+ "PONG response took %u.%03u secs (%s)",
+ ping_msecs/1000, ping_msecs%1000,
+ str_c(extra));
+ }
+ conn->last_ping_msecs = ping_msecs;
+ }
+
+ if (conn->verifying_left) {
+ conn->verifying_left = FALSE;
+ if (conn == conn->dir->left) {
+ /* our left side is functional. tell all the wrong
+ incoming connections to connect to it instead. */
+ director_disconnect_wrong_lefts(conn->dir);
+ }
+ }
+
+ director_connection_set_ping_timeout(conn);
+ return TRUE;
+}
+
+static bool
+director_connection_handle_cmd(struct director_connection *conn,
+ const char *cmd, const char *const *args)
+{
+ int ret;
+
+ if (!conn->handshake_received) {
+ ret = director_connection_handle_handshake(conn, cmd, args);
+ if (ret > 0)
+ return TRUE;
+ if (ret < 0) {
+ /* invalid commands during handshake,
+ we probably don't want to reconnect here */
+ return FALSE;
+ }
+ /* allow also other commands during handshake */
+ }
+
+ if (strcmp(cmd, "PING") == 0)
+ return director_cmd_ping(conn, args);
+ if (strcmp(cmd, "PONG") == 0)
+ return director_cmd_pong(conn, args);
+ if (strcmp(cmd, "USER") == 0)
+ return director_cmd_user(conn, args);
+ if (strcmp(cmd, "USER-WEAK") == 0)
+ return director_cmd_user_weak(conn, args);
+ if (strcmp(cmd, "HOST") == 0)
+ return director_cmd_host(conn, args);
+ if (strcmp(cmd, "HOST-REMOVE") == 0)
+ return director_cmd_host_remove(conn, args);
+ if (strcmp(cmd, "HOST-FLUSH") == 0)
+ return director_cmd_host_flush(conn, args);
+ if (strcmp(cmd, "USER-MOVE") == 0)
+ return director_cmd_user_move(conn, args);
+ if (strcmp(cmd, "USER-KICK") == 0)
+ return director_cmd_user_kick(conn, args);
+ if (strcmp(cmd, "USER-KICK-ALT") == 0)
+ return director_cmd_user_kick_alt(conn, args);
+ if (strcmp(cmd, "USER-KICK-HASH") == 0)
+ return director_cmd_user_kick_hash(conn, args);
+ if (strcmp(cmd, "USER-KILLED") == 0)
+ return director_cmd_user_killed(conn, args);
+ if (strcmp(cmd, "USER-KILLED-EVERYWHERE") == 0)
+ return director_cmd_user_killed_everywhere(conn, args);
+ if (strcmp(cmd, "DIRECTOR") == 0)
+ return director_cmd_director(conn, args);
+ if (strcmp(cmd, "DIRECTOR-REMOVE") == 0)
+ return director_cmd_director_remove(conn, args);
+ if (strcmp(cmd, "SYNC") == 0)
+ return director_connection_sync(conn, args);
+ if (strcmp(cmd, "CONNECT") == 0)
+ return director_cmd_connect(conn, args);
+ if (strcmp(cmd, "QUIT") == 0) {
+ e_warning(conn->event,
+ "Director %s disconnected us with reason: %s",
+ conn->name, t_strarray_join(args, " "));
+ return FALSE;
+ }
+
+ director_cmd_error(conn, "Unknown command %s", cmd);
+ return FALSE;
+}
+
+static bool
+director_connection_handle_line(struct director_connection *conn,
+ char *line)
+{
+ const char *cmd, *const *args;
+ bool ret;
+
+ e_debug(conn->event, "input: %s", line);
+
+ args = t_strsplit_tabescaped_inplace(line);
+ cmd = args[0];
+ if (cmd == NULL) {
+ director_cmd_error(conn, "Received empty line");
+ return FALSE;
+ }
+
+ conn->cur_cmd = cmd;
+ conn->cur_args = args;
+ ret = director_connection_handle_cmd(conn, cmd, args+1);
+ conn->cur_cmd = NULL;
+ conn->cur_args = NULL;
+ return ret;
+}
+
+static void
+director_connection_log_disconnect(struct director_connection *conn, int err,
+ const char *errstr)
+{
+ string_t *str = t_str_new(128);
+
+ i_assert(conn->connected);
+
+ if (conn->connect_request_to != NULL) {
+ e_warning(conn->event,
+ "Director %s tried to connect to us, "
+ "should use %s instead",
+ conn->name, conn->connect_request_to->name);
+ return;
+ }
+
+ str_printfa(str, "Director %s disconnected: ", conn->name);
+ str_append(str, "Connection closed");
+ if (err != 0 && err != EPIPE) {
+ errno = err;
+ if (errstr[0] == '\0')
+ str_printfa(str, ": %m");
+ else
+ str_printfa(str, ": %s", errstr);
+ }
+
+ str_append(str, " (");
+ director_connection_append_stats(conn, str);
+
+ if (!conn->me_received)
+ str_append(str, ", handshake ME not received");
+ else if (!conn->handshake_received)
+ str_append(str, ", handshake DONE not received");
+ if (conn->synced)
+ str_append(str, ", synced");
+ str_append_c(str, ')');
+ e_error(conn->event, "%s", str_c(str));
+}
+
+static void director_connection_input(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+ char *line;
+ uoff_t prev_offset;
+ bool ret;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ director_connection_log_disconnect(conn, conn->input->stream_errno,
+ i_stream_get_error(conn->input));
+ director_connection_disconnected(&conn, i_stream_get_error(conn->input));
+ return;
+ case -2:
+ /* buffer full */
+ director_cmd_error(conn, "Director sent us more than %d bytes",
+ MAX_INBUF_SIZE);
+ director_connection_reconnect(&conn, "Too long input line");
+ return;
+ }
+
+ if (conn->to_disconnect != NULL) {
+ /* just read everything the remote sends, and wait for it
+ to disconnect. we mainly just want the remote to read the
+ CONNECT we sent it. */
+ i_stream_skip(conn->input, i_stream_get_data_size(conn->input));
+ return;
+ }
+ conn->last_input = ioloop_timeval;
+ conn->refcount++;
+
+ director_sync_freeze(dir);
+ prev_offset = conn->input->v_offset;
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ dir->ring_traffic_input += conn->input->v_offset - prev_offset;
+ prev_offset = conn->input->v_offset;
+
+ T_BEGIN {
+ ret = director_connection_handle_line(conn, line);
+ } T_END;
+
+ if (!ret) {
+ if (!director_connection_unref(conn))
+ break;
+ director_connection_reconnect(&conn, t_strdup_printf(
+ "Invalid input: %s", line));
+ break;
+ }
+ }
+ director_sync_thaw(dir);
+ if (conn != NULL) {
+ if (director_connection_unref(conn))
+ timeout_reset(conn->to_ping);
+ }
+}
+
+static void director_connection_send_directors(struct director_connection *conn)
+{
+ struct director_host *host;
+ string_t *str = t_str_new(64);
+
+ array_foreach_elem(&conn->dir->dir_hosts, host) {
+ if (host->removed)
+ continue;
+
+ str_truncate(str, 0);
+ str_printfa(str, "DIRECTOR\t%s\t%u\n",
+ host->ip_str, host->port);
+ director_connection_send(conn, str_c(str));
+ }
+}
+
+static void
+director_connection_send_hosts(struct director_connection *conn)
+{
+ struct mail_host *host;
+ bool send_updowns;
+ string_t *str = t_str_new(128);
+
+ i_assert(conn->version_received);
+
+ send_updowns = conn->minor_version >= DIRECTOR_VERSION_UPDOWN;
+
+ str_printfa(str, "HOST-HAND-START\t%u\n",
+ conn->dir->ring_handshaked ? 1 : 0);
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ const char *host_tag = mail_host_get_tag(host);
+
+ str_printfa(str, "HOST\t%s\t%u",
+ host->ip_str, host->vhost_count);
+ if (host_tag[0] != '\0' || send_updowns) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, host_tag);
+ }
+ if (send_updowns) {
+ str_printfa(str, "\t%c%ld\t", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ if (host->hostname != NULL)
+ str_append_tabescaped(str, host->hostname);
+ }
+ str_append_c(str, '\n');
+ director_connection_send(conn, str_c(str));
+ str_truncate(str, 0);
+ }
+ str_printfa(str, "HOST-HAND-END\t%u\n",
+ conn->dir->ring_handshaked ? 1 : 0);
+ director_connection_send(conn, str_c(str));
+}
+
+static int director_connection_send_done(struct director_connection *conn)
+{
+ i_assert(conn->version_received);
+
+ if (conn->minor_version >= DIRECTOR_VERSION_OPTIONS) {
+ director_connection_send(conn,
+ "OPTIONS\t"DIRECTOR_OPT_CONSISTENT_HASHING"\n");
+ } else {
+ e_error(conn->event, "Director version is too old for supporting director_consistent_hashing=yes");
+ return -1;
+ }
+ director_connection_send(conn, "DONE\n");
+ return 0;
+}
+
+static int director_connection_send_users(struct director_connection *conn)
+{
+ struct user *user;
+ string_t *str = t_str_new(128);
+ char dec_buf[MAX_INT_STRLEN];
+ unsigned int sent_count = 0;
+ int ret;
+
+ i_assert(conn->version_received);
+
+ /* with new versions use "U" for sending the handshake users, because
+ otherwise their parameters may look identical and can't be
+ distinguished. */
+ if (director_connection_get_minor_version(conn) >= DIRECTOR_VERSION_HANDSHAKE_U_CMD)
+ str_append(str, "U\t");
+ else
+ str_append(str, "USER\t");
+ size_t cmd_prefix_len = str_len(str);
+ while ((user = director_iterate_users_next(conn->user_iter)) != NULL) {
+ str_truncate(str, cmd_prefix_len);
+ str_append(str, dec2str_buf(dec_buf, user->username_hash));
+ str_append_c(str, '\t');
+ str_append(str, user->host->ip_str);
+ str_append_c(str, '\t');
+ str_append(str, dec2str_buf(dec_buf, user->timestamp));
+ if (user->weak)
+ str_append(str, "\tw");
+ str_append_c(str, '\n');
+
+ conn->handshake_users_sent++;
+ director_connection_send(conn, str_c(str));
+ if (++sent_count >= DIRECTOR_HANDSHAKE_MAX_USERS_SENT_PER_FLUSH) {
+ /* Don't send too much at once to avoid hangs */
+ timeout_reset(conn->to_ping);
+ return 0;
+ }
+
+ if (o_stream_get_buffer_used_size(conn->output) >= OUTBUF_FLUSH_THRESHOLD) {
+ if ((ret = o_stream_flush(conn->output)) <= 0) {
+ /* continue later */
+ timeout_reset(conn->to_ping);
+ return ret;
+ }
+ }
+ }
+ director_iterate_users_deinit(&conn->user_iter);
+ if (director_connection_send_done(conn) < 0)
+ return -1;
+
+ if (conn->users_unsorted && conn->handshake_received) {
+ /* we received remote's list of users before sending ours */
+ conn->users_unsorted = FALSE;
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ ret = o_stream_flush(conn->output);
+ timeout_reset(conn->to_ping);
+ return ret;
+}
+
+static int director_connection_output(struct director_connection *conn)
+{
+ int ret;
+
+ conn->last_output = ioloop_timeval;
+ if (conn->user_iter != NULL) {
+ /* still handshaking USER list */
+ ret = director_connection_send_users(conn);
+ if (ret < 0) {
+ director_connection_log_disconnect(conn, conn->output->stream_errno,
+ o_stream_get_error(conn->output));
+ director_connection_disconnected(&conn,
+ o_stream_get_error(conn->output));
+ } else {
+ o_stream_set_flush_pending(conn->output, TRUE);
+ }
+ return ret;
+ }
+ return o_stream_flush(conn->output);
+}
+
+static struct director_connection *
+director_connection_init_common(struct director *dir, int fd)
+{
+ struct director_connection *conn;
+
+ conn = i_new(struct director_connection, 1);
+ conn->refcount = 1;
+ conn->created = ioloop_timeval;
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->event = event_create(dir->event);
+ conn->input = i_stream_create_fd(conn->fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(conn->fd, dir->set->director_output_buffer_size);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ array_push_back(&dir->connections, &conn);
+ return conn;
+}
+
+static void director_connection_send_handshake(struct director_connection *conn)
+{
+ director_connection_send(conn, t_strdup_printf(
+ "VERSION\t"DIRECTOR_VERSION_NAME"\t%u\t%u\n"
+ "ME\t%s\t%u\t%lld\n",
+ DIRECTOR_VERSION_MAJOR, DIRECTOR_VERSION_MINOR,
+ net_ip2addr(&conn->dir->self_ip), conn->dir->self_port,
+ (long long)time(NULL)));
+}
+
+static void director_connection_set_connected(struct director_connection *conn)
+{
+ struct rusage usage;
+
+ conn->connected = TRUE;
+ conn->connected_time = ioloop_timeval;
+
+ if (getrusage(RUSAGE_SELF, &usage) == 0) {
+ conn->connected_user_cpu_set = TRUE;
+ conn->connected_user_cpu = usage.ru_utime;
+ }
+}
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd,
+ const struct ip_addr *ip)
+{
+ struct director_connection *conn;
+
+ conn = director_connection_init_common(dir, fd);
+ conn->in = TRUE;
+ director_connection_set_connected(conn);
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/in", net_ip2addr(ip)));
+ conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+
+ e_info(conn->event, "Incoming connection from director %s", conn->name);
+ director_connection_send_handshake(conn);
+ return conn;
+}
+
+static void director_connection_connected(struct director_connection *conn)
+{
+ int err;
+
+ if ((err = net_geterror(conn->fd)) != 0) {
+ e_error(conn->event, "connect() failed: %s", strerror(err));
+ director_connection_disconnected(&conn, strerror(err));
+ return;
+ }
+ director_connection_set_connected(conn);
+ o_stream_set_flush_callback(conn->output,
+ director_connection_output, conn);
+
+ io_remove(&conn->io);
+ conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+
+ o_stream_cork(conn->output);
+ director_connection_send_handshake(conn);
+ director_connection_send_directors(conn);
+ o_stream_uncork(conn->output);
+ /* send the rest of the handshake after we've received the remote's
+ version number */
+}
+
+static void director_finish_sending_handshake(struct director_connection *conn)
+{
+ if (
+ conn->in) {
+ /* only outgoing connections send hosts & users */
+ return;
+ }
+ o_stream_cork(conn->output);
+ director_connection_send_hosts(conn);
+
+ i_assert(conn->user_iter == NULL);
+ /* Iterate only through users that aren't refreshed since the
+ iteration started. The refreshed users will already be sent as
+ regular USER updates, so they don't need to be sent again.
+
+ We especially don't want to send these users again, because
+ otherwise in a rapidly changing director we might never end up
+ sending all the users when they constantly keep being added to the
+ end of the list. (The iteration lists users in order from older to
+ newer.) */
+ conn->user_iter = director_iterate_users_init(conn->dir, TRUE);
+
+ if (director_connection_send_users(conn) == 0)
+ o_stream_set_flush_pending(conn->output, TRUE);
+
+ o_stream_uncork(conn->output);
+}
+
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+ struct director_host *host)
+{
+ struct director_connection *conn;
+
+ i_assert(!host->removed);
+
+ /* make sure we don't keep old sequence values across restarts */
+ director_host_restarted(host);
+
+ conn = director_connection_init_common(dir, fd);
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/out", host->name));
+ conn->host = host;
+ director_host_ref(host);
+ conn->io = io_add(conn->fd, IO_WRITE,
+ director_connection_connected, conn);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_CONNECT_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ return conn;
+}
+
+void director_connection_deinit(struct director_connection **_conn,
+ const char *remote_reason)
+{
+ struct director_connection *const *conns, *conn = *_conn;
+ struct director *dir = conn->dir;
+ unsigned int i, count;
+
+ *_conn = NULL;
+
+ i_assert(conn->fd != -1);
+
+ if (conn->host != NULL) {
+ e_debug(conn->event, "Disconnecting from %s: %s",
+ conn->host->name, remote_reason);
+ }
+ if (*remote_reason != '\0' &&
+ conn->minor_version >= DIRECTOR_VERSION_QUIT) {
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "QUIT\t%s\n", remote_reason));
+ }
+
+ conns = array_get(&dir->connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i] == conn) {
+ array_delete(&dir->connections, i, 1);
+ break;
+ }
+ }
+ i_assert(i < count);
+ if (dir->left == conn) {
+ dir->left = NULL;
+ /* if there is already another handshaked incoming connection,
+ use it as the new "left" */
+ director_assign_left(dir);
+ }
+ if (dir->right == conn)
+ dir->right = NULL;
+
+ if (conn->users_unsorted) {
+ /* Users were received, but handshake didn't finish.
+ Finish sorting so the users won't stay in wrong order. */
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ if (conn->connect_request_to != NULL) {
+ director_host_unref(conn->connect_request_to);
+ conn->connect_request_to = NULL;
+ }
+ if (conn->user_iter != NULL)
+ director_iterate_users_deinit(&conn->user_iter);
+ timeout_remove(&conn->to_disconnect);
+ timeout_remove(&conn->to_pong);
+ timeout_remove(&conn->to_ping);
+ io_remove(&conn->io);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ i_close_fd(&conn->fd);
+
+ if (conn->in)
+ master_service_client_connection_destroyed(master_service);
+ director_connection_unref(conn);
+
+ if (dir->left == NULL || dir->right == NULL) {
+ /* we aren't synced until we're again connected to a ring */
+ dir->sync_seq++;
+ director_set_ring_unsynced(dir);
+ }
+}
+
+static bool director_connection_unref(struct director_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ if (conn->host != NULL)
+ director_host_unref(conn->host);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ event_unref(&conn->event);
+ i_free(conn->name);
+ i_free(conn);
+ return FALSE;
+}
+
+static void director_connection_disconnected(struct director_connection **_conn,
+ const char *reason)
+{
+ struct director_connection *conn = *_conn;
+ struct director *dir = conn->dir;
+
+ if ((conn->connected_time.tv_sec == 0 ||
+ conn->connected_time.tv_sec + DIRECTOR_SUCCESS_MIN_CONNECT_SECS > ioloop_time) &&
+ conn->host != NULL) {
+ /* connection didn't exist for very long, assume it has a
+ network problem */
+ conn->host->last_network_failure = ioloop_time;
+ }
+
+ director_connection_deinit(_conn, reason);
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after disconnection");
+}
+
+static void director_connection_reconnect(struct director_connection **_conn,
+ const char *reason)
+{
+ struct director_connection *conn = *_conn;
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(_conn, reason);
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after error");
+}
+
+static void director_disconnect_write_error(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(&conn, "write failure");
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after write failure");
+}
+
+void director_connection_send(struct director_connection *conn,
+ const char *data)
+{
+ size_t len = strlen(data);
+ off_t ret;
+
+ if (conn->output->closed || !conn->connected)
+ return;
+
+ if (event_want_debug(conn->event)) T_BEGIN {
+ const char *const *lines = t_strsplit(data, "\n");
+ for (; lines[1] != NULL; lines++)
+ e_debug(conn->event, "output: %s", *lines);
+ } T_END;
+ ret = o_stream_send(conn->output, data, len);
+ if (ret != (off_t)len) {
+ if (ret < 0) {
+ director_connection_log_disconnect(conn,
+ conn->output->stream_errno,
+ t_strdup_printf("write() failed: %s",
+ o_stream_get_error(conn->output)));
+ } else {
+ director_connection_log_disconnect(conn, EINVAL,
+ t_strdup_printf("Output buffer full at %zu",
+ o_stream_get_buffer_used_size(conn->output)));
+ }
+ o_stream_close(conn->output);
+ /* closing the stream when output buffer is full doesn't cause
+ disconnection itself. */
+ timeout_remove(&conn->to_disconnect);
+ conn->to_disconnect =
+ timeout_add_short(0, director_disconnect_write_error, conn);
+ } else {
+ conn->dir->ring_traffic_output += len;
+ conn->last_output = ioloop_timeval;
+ conn->peak_bytes_buffered =
+ I_MAX(conn->peak_bytes_buffered,
+ o_stream_get_buffer_used_size(conn->output));
+ }
+}
+
+static void
+director_connection_ping_idle_timeout(struct director_connection *conn)
+{
+ string_t *str = t_str_new(128);
+ int diff = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+
+ str_printfa(str, "Ping timed out in %u.%03u secs: ",
+ diff/1000, diff%1000);
+ director_ping_append_extra(conn, str, 0, (uintmax_t)-1);
+ director_connection_log_disconnect(conn, EINVAL, str_c(str));
+ director_connection_disconnected(&conn, "Ping timeout");
+}
+
+static void director_connection_pong_timeout(struct director_connection *conn)
+{
+ int diff = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+ const char *errstr;
+
+ errstr = t_strdup_printf(
+ "PONG reply not received in %u.%03u secs, "
+ "although other input keeps coming",
+ diff/1000, diff%1000);
+ director_connection_log_disconnect(conn, EINVAL, errstr);
+ director_connection_disconnected(&conn, "Pong timeout");
+}
+
+void director_connection_ping(struct director_connection *conn)
+{
+ if (conn->ping_waiting)
+ return;
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(conn->dir->set->director_ping_idle_timeout*1000,
+ director_connection_ping_idle_timeout, conn);
+ conn->to_pong = timeout_add(conn->dir->set->director_ping_max_timeout*1000,
+ director_connection_pong_timeout, conn);
+ conn->ping_waiting = TRUE;
+ conn->ping_sent_time = ioloop_timeval;
+ conn->ping_sent_buffer_size = o_stream_get_buffer_used_size(conn->output);
+ conn->ping_sent_input_offset = conn->input->v_offset;
+ conn->ping_sent_output_offset = conn->output->offset;
+
+ struct rusage usage;
+ if (getrusage(RUSAGE_SELF, &usage) == 0)
+ conn->ping_sent_user_cpu = usage.ru_utime;
+ else
+ conn->ping_sent_user_cpu.tv_sec = (time_t)-1;
+ /* send it after getting the buffer size */
+ director_connection_send(conn, t_strdup_printf(
+ "PING\t%"PRIdTIME_T"\t%zu\n", ioloop_time,
+ conn->ping_sent_buffer_size));
+}
+
+const char *director_connection_get_name(struct director_connection *conn)
+{
+ return conn->name;
+}
+
+struct director_host *
+director_connection_get_host(struct director_connection *conn)
+{
+ return conn->host;
+}
+
+bool director_connection_is_handshaked(struct director_connection *conn)
+{
+ return conn->handshake_received;
+}
+
+bool director_connection_is_synced(struct director_connection *conn)
+{
+ return conn->synced;
+}
+
+bool director_connection_is_incoming(struct director_connection *conn)
+{
+ return conn->in;
+}
+
+unsigned int
+director_connection_get_minor_version(struct director_connection *conn)
+{
+ return conn->minor_version;
+}
+
+void director_connection_cork(struct director_connection *conn)
+{
+ o_stream_cork(conn->output);
+}
+
+void director_connection_uncork(struct director_connection *conn)
+{
+ o_stream_uncork(conn->output);
+}
+
+void director_connection_set_synced(struct director_connection *conn,
+ bool synced)
+{
+ if (conn->synced == synced)
+ return;
+ conn->synced = synced;
+
+ /* switch ping timeout, unless we're already waiting for PONG */
+ if (conn->ping_waiting)
+ return;
+
+ director_connection_set_ping_timeout(conn);
+}
+
+void director_connection_get_status(struct director_connection *conn,
+ struct director_connection_status *status_r)
+{
+ i_zero(status_r);
+ status_r->bytes_read = conn->input->v_offset;
+ status_r->bytes_sent = conn->output->offset;
+ status_r->bytes_buffered = o_stream_get_buffer_used_size(conn->output);
+ status_r->peak_bytes_buffered = conn->peak_bytes_buffered;
+ status_r->last_input = conn->last_input;
+ status_r->last_output = conn->last_output;
+ status_r->last_ping_msecs = conn->last_ping_msecs;
+ status_r->handshake_users_sent = conn->handshake_users_sent;
+ status_r->handshake_users_received = conn->handshake_users_received;
+}
diff --git a/src/director/director-connection.h b/src/director/director-connection.h
new file mode 100644
index 0000000..2b8bf0f
--- /dev/null
+++ b/src/director/director-connection.h
@@ -0,0 +1,47 @@
+#ifndef DIRECTOR_CONNECTION_H
+#define DIRECTOR_CONNECTION_H
+
+struct director_connection_status {
+ uoff_t bytes_read, bytes_sent;
+ size_t bytes_buffered, peak_bytes_buffered;
+ struct timeval last_input, last_output;
+ unsigned int last_ping_msecs;
+
+ unsigned int handshake_users_received;
+ unsigned int handshake_users_sent;
+};
+
+struct director_host;
+struct director;
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd,
+ const struct ip_addr *ip);
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+ struct director_host *host);
+void director_connection_deinit(struct director_connection **conn,
+ const char *remote_reason);
+
+void director_connection_send(struct director_connection *conn,
+ const char *data);
+void director_connection_set_synced(struct director_connection *conn,
+ bool synced);
+void director_connection_ping(struct director_connection *conn);
+
+const char *director_connection_get_name(struct director_connection *conn);
+struct director_host *
+director_connection_get_host(struct director_connection *conn);
+bool director_connection_is_handshaked(struct director_connection *conn);
+bool director_connection_is_synced(struct director_connection *conn);
+bool director_connection_is_incoming(struct director_connection *conn);
+unsigned int
+director_connection_get_minor_version(struct director_connection *conn);
+
+void director_connection_cork(struct director_connection *conn);
+void director_connection_uncork(struct director_connection *conn);
+
+void director_connection_get_status(struct director_connection *conn,
+ struct director_connection_status *status_r);
+
+#endif
diff --git a/src/director/director-host.c b/src/director/director-host.c
new file mode 100644
index 0000000..215ad0d
--- /dev/null
+++ b/src/director/director-host.c
@@ -0,0 +1,190 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "director.h"
+#include "director-host.h"
+
+static int director_host_cmp(const struct director_host *b1,
+ const struct director_host *b2)
+{
+ int ret;
+
+ ret = net_ip_cmp(&b1->ip, &b2->ip);
+ if (ret != 0)
+ return ret;
+ return (int)b1->port - (int)b2->port;
+}
+
+int director_host_cmp_p(struct director_host *const *host1,
+ struct director_host *const *host2)
+{
+ return director_host_cmp(*host1, *host2);
+}
+
+struct director_host *
+director_host_add(struct director *dir,
+ const struct ip_addr *ip, in_port_t port)
+{
+ struct director_host *host;
+
+ i_assert(director_host_lookup(dir, ip, port) == NULL);
+
+ host = i_new(struct director_host, 1);
+ host->dir = dir;
+ host->refcount = 1;
+ host->ip = *ip;
+ host->ip_str = i_strdup(net_ip2addr(&host->ip));
+ host->port = port;
+ host->name = i_strdup_printf("%s:%u", host->ip_str, port);
+
+ array_push_back(&dir->dir_hosts, &host);
+
+ /* there are few enough directors that sorting after each
+ addition should be fine */
+ array_sort(&dir->dir_hosts, director_host_cmp_p);
+ return host;
+}
+
+void director_host_free(struct director_host **_host)
+{
+ struct director_host *host = *_host;
+
+ i_assert(host->refcount == 1);
+
+ *_host = NULL;
+ director_host_unref(host);
+}
+
+void director_host_ref(struct director_host *host)
+{
+ i_assert(host->refcount > 0);
+ host->refcount++;
+}
+
+void director_host_unref(struct director_host *host)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ i_assert(host->refcount > 0);
+
+ if (--host->refcount > 0)
+ return;
+
+ hosts = array_get(&host->dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == host) {
+ array_delete(&host->dir->dir_hosts, i, 1);
+ break;
+ }
+ }
+ i_free(host->name);
+ i_free(host->ip_str);
+ i_free(host);
+}
+
+void director_host_restarted(struct director_host *host)
+{
+ host->last_seq = 0;
+ host->last_sync_seq = 0;
+ host->last_sync_seq_counter = 0;
+ host->last_sync_timestamp = 0;
+}
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+ in_port_t port)
+{
+ struct director_host *host;
+
+ host = director_host_lookup(dir, ip, port);
+ if (host == NULL)
+ host = director_host_add(dir, ip, port);
+ return host;
+}
+
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+ in_port_t port)
+{
+ struct director_host *host;
+
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (net_ip_compare(&host->ip, ip) && host->port == port)
+ return host;
+ }
+ return NULL;
+}
+
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip)
+{
+ struct director_host *host;
+
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (net_ip_compare(&host->ip, ip))
+ return host;
+ }
+ return NULL;
+}
+
+int director_host_cmp_to_self(const struct director_host *b1,
+ const struct director_host *b2,
+ const struct director_host *self)
+{
+ int ret;
+
+ if ((ret = director_host_cmp(b1, b2)) >= 0)
+ return ret == 0 ? 0 : -director_host_cmp_to_self(b2, b1, self);
+
+ /* order -> return:
+ self, b1, b2 -> b2
+ b1, self, b2 -> b1
+ b1, b2, self -> b2
+ */
+ if (director_host_cmp(self, b1) < 0)
+ return 1; /* self, b1, b2 */
+ if (director_host_cmp(self, b2) < 0)
+ return -1; /* b1, self, b2 */
+ return 1; /* b1, b2, self */
+}
+
+static void director_host_add_string(struct director *dir, const char *host)
+{
+ struct ip_addr *ips;
+ in_port_t port;
+ unsigned int i, ips_count;
+
+ if (net_str2hostport(host, dir->self_port, &host, &port) < 0)
+ i_fatal("Invalid director host:port in '%s'", host);
+
+ if (net_gethostbyname(host, &ips, &ips_count) < 0)
+ i_fatal("Unknown director host: %s", host);
+
+ for (i = 0; i < ips_count; i++) {
+ if (director_host_lookup(dir, &ips[i], port) == NULL)
+ (void)director_host_add(dir, &ips[i], port);
+ }
+}
+
+void director_host_add_from_string(struct director *dir, const char *hosts)
+{
+ T_BEGIN {
+ const char *const *tmp;
+
+ tmp = t_strsplit_spaces(hosts, " ");
+ for (; *tmp != NULL; tmp++)
+ director_host_add_string(dir, *tmp);
+ } T_END;
+
+ if (array_count(&dir->dir_hosts) == 0) {
+ /* standalone director */
+ struct ip_addr ip;
+
+ if (net_addr2ip("127.0.0.1", &ip) < 0)
+ i_unreached();
+ dir->self_host = director_host_add(dir, &ip, 0);
+ dir->self_host->self = TRUE;
+ }
+}
diff --git a/src/director/director-host.h b/src/director/director-host.h
new file mode 100644
index 0000000..3bf05d2
--- /dev/null
+++ b/src/director/director-host.h
@@ -0,0 +1,81 @@
+#ifndef DIRECTOR_HOST_H
+#define DIRECTOR_HOST_H
+
+#include "net.h"
+
+struct director;
+
+struct director_host {
+ struct director *dir;
+ int refcount;
+
+ struct ip_addr ip;
+ char *ip_str;
+ in_port_t port;
+
+ /* name contains "ip:port" */
+ char *name;
+ /* change commands each have originating host and originating sequence.
+ we'll keep track of the highest sequence we've seen from the host.
+ if we find a lower sequence, we've already handled the command and
+ it can be ignored (or: it must be ignored to avoid potential command
+ loops) */
+ unsigned int last_seq;
+ /* use these to avoid infinitely sending SYNCs for directors that
+ aren't connected in the ring. */
+ unsigned int last_sync_seq, last_sync_seq_counter, last_sync_timestamp;
+ /* whenever we receive a SYNC with stale hosts_hash, set this. if it's
+ already set and equals the current hosts_hash, re-send our hosts to
+ everybody in case they somehow got out of sync. */
+ unsigned int desynced_hosts_hash;
+ /* Last time host was detected to be down */
+ time_t last_network_failure;
+ time_t last_protocol_failure;
+
+ /* When we finish getting a right connection, send a SYNC with these
+ parameters (if delayed_sync_seq != 0) */
+ uint32_t delayed_sync_seq;
+ unsigned int delayed_sync_minor_version;
+ unsigned int delayed_sync_timestamp;
+ unsigned int delayed_sync_hosts_hash;
+
+ /* we are this director */
+ bool self:1;
+ bool removed:1;
+};
+
+struct director_host *
+director_host_add(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+void director_host_free(struct director_host **host);
+
+void director_host_ref(struct director_host *host);
+void director_host_unref(struct director_host *host);
+
+void director_host_restarted(struct director_host *host);
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip);
+
+/* Returns 0 if b1 equals b2.
+ -1 if b1 is closer to our left side than b2 or
+ -1 if b2 is closer to our right side than b1
+ 1 vice versa */
+int director_host_cmp_to_self(const struct director_host *b1,
+ const struct director_host *b2,
+ const struct director_host *self);
+/* Compare directors by IP/port. */
+int director_host_cmp_p(struct director_host *const *host1,
+ struct director_host *const *host2);
+
+/* Parse hosts list (e.g. "host1:port host2 host3:port") and them as
+ directors */
+void director_host_add_from_string(struct director *dir, const char *hosts);
+
+#endif
diff --git a/src/director/director-request.c b/src/director/director-request.c
new file mode 100644
index 0000000..af41f7f
--- /dev/null
+++ b/src/director/director-request.c
@@ -0,0 +1,354 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-request.h"
+
+#define DIRECTOR_REQUEST_TIMEOUT_SECS 30
+#define RING_NOCONN_WARNING_DELAY_MSECS (2*1000)
+
+enum director_request_delay_reason {
+ REQUEST_DELAY_NONE = 0,
+ REQUEST_DELAY_RINGNOTHANDSHAKED,
+ REQUEST_DELAY_RINGNOTSYNCED,
+ REQUEST_DELAY_NOHOSTS,
+ REQUEST_DELAY_WEAK,
+ REQUEST_DELAY_KILL
+};
+
+static const char *delay_reason_strings[] = {
+ "unknown",
+ "ring not handshaked",
+ "ring not synced",
+ "no hosts",
+ "weak user",
+ "kill waiting"
+};
+
+struct director_request {
+ struct director *dir;
+
+ struct event *event;
+ time_t create_time;
+ unsigned int username_hash;
+ enum director_request_delay_reason delay_reason;
+ char *username_tag;
+
+ director_request_callback *callback;
+ void *context;
+};
+
+static void director_request_free(struct director_request *request)
+{
+ event_unref(&request->event);
+ i_free(request->username_tag);
+ i_free(request);
+}
+
+static const char *
+director_request_get_timeout_error(struct director_request *request,
+ struct user *user, string_t *str)
+{
+ unsigned int secs;
+
+ str_truncate(str, 0);
+ str_printfa(str, "Timeout because %s - queued for %u secs (",
+ delay_reason_strings[request->delay_reason],
+ (unsigned int)(ioloop_time - request->create_time));
+
+ if (request->dir->ring_last_sync_time == 0)
+ str_append(str, "Ring has never been synced");
+ else {
+ secs = ioloop_time - request->dir->ring_last_sync_time;
+ if (request->dir->ring_synced)
+ str_printfa(str, "Ring synced for %u secs", secs);
+ else
+ str_printfa(str, "Ring not synced for %u secs", secs);
+ }
+
+ if (user != NULL) {
+ if (user->weak)
+ str_append(str, ", weak user");
+ str_printfa(str, ", user refreshed %u secs ago",
+ (unsigned int)(ioloop_time - user->timestamp));
+ }
+ str_printfa(str, ", hash=%u", request->username_hash);
+ if (request->username_tag != NULL)
+ str_printfa(str, ", tag=%s", request->username_tag);
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void director_request_timeout(struct director *dir)
+{
+ struct director_request **requestp, *request;
+ struct user *user;
+ const char *errormsg;
+ string_t *str = t_str_new(128);
+
+ while (array_count(&dir->pending_requests) > 0) {
+ requestp = array_front_modifiable(&dir->pending_requests);
+ request = *requestp;
+
+ if (request->create_time +
+ DIRECTOR_REQUEST_TIMEOUT_SECS > ioloop_time)
+ break;
+
+ const char *tag_name = request->username_tag == NULL ? "" :
+ request->username_tag;
+ struct mail_tag *tag = mail_tag_find(dir->mail_hosts, tag_name);
+ user = tag == NULL ? NULL :
+ user_directory_lookup(tag->users, request->username_hash);
+
+ errormsg = director_request_get_timeout_error(request,
+ user, str);
+ if (user != NULL &&
+ request->delay_reason == REQUEST_DELAY_WEAK) {
+ /* weakness appears to have gotten stuck. this is a
+ bug, but try to fix it for future requests by
+ removing the weakness. */
+ user->weak = FALSE;
+ }
+
+ i_assert(dir->requests_delayed_count > 0);
+ dir->requests_delayed_count--;
+
+ array_pop_front(&dir->pending_requests);
+ T_BEGIN {
+ request->callback(NULL, NULL, errormsg, request->context);
+ } T_END;
+ director_request_free(request);
+ }
+
+ if (array_count(&dir->pending_requests) == 0 && dir->to_request != NULL)
+ timeout_remove(&dir->to_request);
+}
+
+void director_request(struct director *dir, const char *username,
+ const char *tag,
+ director_request_callback *callback, void *context)
+{
+ struct director_request *request;
+ unsigned int username_hash;
+
+ if (!director_get_username_hash(dir, username,
+ &username_hash)) {
+ callback(NULL, NULL, "Failed to expand director_username_hash", context);
+ return;
+ }
+
+ dir->num_requests++;
+
+ request = i_new(struct director_request, 1);
+ request->dir = dir;
+ request->create_time = ioloop_time;
+ request->username_hash = username_hash;
+ request->username_tag = tag[0] == '\0' ? NULL : i_strdup(tag);
+ request->callback = callback;
+ request->context = context;
+ request->event = event_create(dir->event);
+ event_set_append_log_prefix(request->event,
+ t_strdup_printf("request: Hash %u ", username_hash));
+
+ if (director_request_continue(request))
+ return;
+
+ /* need to queue it */
+ if (dir->to_request == NULL) {
+ dir->to_request =
+ timeout_add(DIRECTOR_REQUEST_TIMEOUT_SECS * 1000,
+ director_request_timeout, dir);
+ }
+ array_push_back(&dir->pending_requests, &request);
+}
+
+static void ring_noconn_warning(struct director *dir)
+{
+ if (!dir->ring_handshaked) {
+ e_warning(dir->event, "Delaying all requests "
+ "until all directors have connected");
+ } else {
+ e_warning(dir->event,
+ "Delaying new user requests until ring is synced");
+ }
+ dir->ring_handshake_warning_sent = TRUE;
+ timeout_remove(&dir->to_handshake_warning);
+}
+
+static void ring_log_delayed_warning(struct director *dir)
+{
+ if (dir->ring_handshake_warning_sent ||
+ dir->to_handshake_warning != NULL)
+ return;
+
+ dir->to_handshake_warning = timeout_add(RING_NOCONN_WARNING_DELAY_MSECS,
+ ring_noconn_warning, dir);
+}
+
+static bool
+director_request_existing(struct director_request *request, struct user *user)
+{
+ struct director *dir = request->dir;
+ struct mail_host *host;
+
+ if (USER_IS_BEING_KILLED(user)) {
+ /* delay processing this user's connections until
+ its existing connections have been killed */
+ request->delay_reason = REQUEST_DELAY_KILL;
+ e_debug(request->event, "waiting for kill to finish");
+ return FALSE;
+ }
+ if (dir->right == NULL && dir->ring_synced) {
+ /* looks like all the other directors have died. we can do
+ whatever we want without breaking anything. remove the
+ user's weakness just in case it was set to TRUE when we
+ had more directors. */
+ user->weak = FALSE;
+ return TRUE;
+ }
+
+ if (user->weak) {
+ /* wait for user to become non-weak */
+ request->delay_reason = REQUEST_DELAY_WEAK;
+ e_debug(request->event, "waiting for weakness");
+ return FALSE;
+ }
+ if (!user_directory_user_is_near_expiring(user->host->tag->users, user))
+ return TRUE;
+
+ /* user is close to being expired. another director may have
+ already expired it. */
+ host = mail_host_get_by_hash(dir->mail_hosts, user->username_hash,
+ user->host->tag->name);
+ if (!dir->ring_synced) {
+ /* try again later once ring is synced */
+ request->delay_reason = REQUEST_DELAY_RINGNOTSYNCED;
+ e_debug(request->event, "waiting for sync for making weak");
+ return FALSE;
+ }
+ if (user->host == host) {
+ /* doesn't matter, other directors would
+ assign the user the same way regardless */
+ e_debug(request->event, "would be weak, but host doesn't change");
+ return TRUE;
+ }
+
+ /* We have to worry about two separate timepoints in here:
+
+ a) some directors think the user isn't expiring, and
+ others think the user is near expiring
+
+ b) some directors think the user is near expiring, and
+ others think the user has already expired
+
+ What we don't have to worry about is:
+
+ !c) some directors think the user isn't expiring, and
+ others think the user has already expired
+
+ If !c) happens, the user might get redirected to different backends.
+ We'll use a large enough timeout between a) and b) states, so that
+ !c) should never happen.
+
+ So what we'll do here is:
+
+ 1. Send a USER-WEAK notification to all directors with the new host.
+ 2. Each director receiving USER-WEAK refreshes the user's timestamp
+ and host, but marks the user as being weak.
+ 3. Once USER-WEAK has reached all directors, a real USER update is
+ sent, which removes the weak-flag.
+ 4. If a director ever receives a USER update for a weak user, the
+ USER update overrides the host and removes the weak-flag.
+ 5. Director doesn't let any weak user log in, until the weak-flag
+ gets removed.
+ */
+ if (dir->ring_min_version < DIRECTOR_VERSION_WEAK_USERS) {
+ /* weak users not supported by ring currently */
+ return TRUE;
+ } else {
+ user->weak = TRUE;
+ director_update_user_weak(dir, dir->self_host, NULL, NULL, user);
+ request->delay_reason = REQUEST_DELAY_WEAK;
+ e_debug(request->event, "set to weak");
+ return FALSE;
+ }
+}
+
+static bool director_request_continue_real(struct director_request *request)
+{
+ struct director *dir = request->dir;
+ struct mail_host *host;
+ struct user *user;
+ const char *tag;
+ struct mail_tag *mail_tag;
+
+ if (!dir->ring_handshaked) {
+ /* delay requests until ring handshaking is complete */
+ e_debug(request->event, "waiting for handshake");
+ ring_log_delayed_warning(dir);
+ request->delay_reason = REQUEST_DELAY_RINGNOTHANDSHAKED;
+ return FALSE;
+ }
+
+ tag = request->username_tag == NULL ? "" : request->username_tag;
+ mail_tag = mail_tag_find(dir->mail_hosts, tag);
+ user = mail_tag == NULL ? NULL :
+ user_directory_lookup(mail_tag->users, request->username_hash);
+
+ if (user != NULL) {
+ i_assert(user->host->tag == mail_tag);
+ if (!director_request_existing(request, user))
+ return FALSE;
+ user_directory_refresh(mail_tag->users, user);
+ e_debug(request->event, "refreshed timeout to %u",
+ user->timestamp);
+ } else {
+ if (!dir->ring_synced) {
+ /* delay adding new users until ring is again synced */
+ ring_log_delayed_warning(dir);
+ request->delay_reason = REQUEST_DELAY_RINGNOTSYNCED;
+ e_debug(request->event, "waiting for sync for adding");
+ return FALSE;
+ }
+ host = mail_host_get_by_hash(dir->mail_hosts,
+ request->username_hash, tag);
+ if (host == NULL) {
+ /* all hosts have been removed */
+ request->delay_reason = REQUEST_DELAY_NOHOSTS;
+ e_debug(request->event, "waiting for hosts");
+ return FALSE;
+ }
+ user = user_directory_add(host->tag->users,
+ request->username_hash,
+ host, ioloop_time);
+ e_debug(request->event, "added timeout to %u (hosts_hash=%u)",
+ user->timestamp, mail_hosts_hash(dir->mail_hosts));
+ }
+
+ i_assert(!user->weak);
+ director_update_user(dir, dir->self_host, user);
+ T_BEGIN {
+ request->callback(user->host, user->host->hostname,
+ NULL, request->context);
+ } T_END;
+ director_request_free(request);
+ return TRUE;
+}
+
+bool director_request_continue(struct director_request *request)
+{
+ if (request->delay_reason != REQUEST_DELAY_NONE) {
+ i_assert(request->dir->requests_delayed_count > 0);
+ request->dir->requests_delayed_count--;
+ }
+ if (!director_request_continue_real(request)) {
+ i_assert(request->delay_reason != REQUEST_DELAY_NONE);
+ request->dir->requests_delayed_count++;
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/src/director/director-request.h b/src/director/director-request.h
new file mode 100644
index 0000000..e6fff80
--- /dev/null
+++ b/src/director/director-request.h
@@ -0,0 +1,16 @@
+#ifndef DIRECTOR_REQUEST_H
+#define DIRECTOR_REQUEST_H
+
+struct director;
+struct director_request;
+
+typedef void
+director_request_callback(const struct mail_host *host, const char *hostname,
+ const char *errormsg, void *context);
+
+void director_request(struct director *dir, const char *username,
+ const char *tag,
+ director_request_callback *callback, void *context);
+bool director_request_continue(struct director_request *request);
+
+#endif
diff --git a/src/director/director-settings.c b/src/director/director-settings.c
new file mode 100644
index 0000000..10f2f9e
--- /dev/null
+++ b/src/director/director-settings.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "director-settings.h"
+
+/* <settings checks> */
+static bool director_settings_verify(void *_set, pool_t pool, const char **error_r);
+
+static struct file_listener_settings director_unix_listeners_array[] = {
+ { "login/director", 0, "", "" },
+ { "director-admin", 0600, "", "" }
+};
+static struct file_listener_settings *director_unix_listeners[] = {
+ &director_unix_listeners_array[0],
+ &director_unix_listeners_array[1]
+};
+static buffer_t director_unix_listeners_buf = {
+ { { director_unix_listeners, sizeof(director_unix_listeners) } }
+};
+static struct file_listener_settings director_fifo_listeners_array[] = {
+ { "login/proxy-notify", 0, "", "" }
+};
+static struct file_listener_settings *director_fifo_listeners[] = {
+ &director_fifo_listeners_array[0]
+};
+static buffer_t director_fifo_listeners_buf = {
+ { { director_fifo_listeners, sizeof(director_fifo_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings director_service_settings = {
+ .name = "director",
+ .protocol = "",
+ .type = "",
+ .executable = "director",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = ".",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &director_unix_listeners_buf,
+ sizeof(director_unix_listeners[0]) } },
+ .fifo_listeners = { { &director_fifo_listeners_buf,
+ sizeof(director_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct director_settings)
+
+static const struct setting_define director_setting_defines[] = {
+ DEF(STR, auth_master_user_separator),
+
+ DEF(STR, director_servers),
+ DEF(STR, director_mail_servers),
+ DEF(STR, director_username_hash),
+ DEF(STR, director_flush_socket),
+ DEF(TIME, director_ping_idle_timeout),
+ DEF(TIME, director_ping_max_timeout),
+ DEF(TIME, director_user_expire),
+ DEF(TIME, director_user_kick_delay),
+ DEF(UINT, director_max_parallel_moves),
+ DEF(UINT, director_max_parallel_kicks),
+ DEF(SIZE, director_output_buffer_size),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct director_settings director_default_settings = {
+ .auth_master_user_separator = "",
+
+ .director_servers = "",
+ .director_mail_servers = "",
+ .director_username_hash = "%Lu",
+ .director_flush_socket = "",
+ .director_ping_idle_timeout = 30,
+ .director_ping_max_timeout = 60,
+ .director_user_expire = 60*15,
+ .director_user_kick_delay = 2,
+ .director_max_parallel_moves = 100,
+ .director_max_parallel_kicks = 100,
+ .director_output_buffer_size = 10 * 1024 * 1024,
+};
+
+const struct setting_parser_info director_setting_parser_info = {
+ .module_name = "director",
+ .defines = director_setting_defines,
+ .defaults = &director_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct director_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = director_settings_verify
+};
+
+/* <settings checks> */
+static bool
+director_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct director_settings *set = _set;
+
+ if (set->director_user_expire < 10) {
+ *error_r = "director_user_expire is too low";
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/director/director-settings.h b/src/director/director-settings.h
new file mode 100644
index 0000000..2c5fff6
--- /dev/null
+++ b/src/director/director-settings.h
@@ -0,0 +1,25 @@
+#ifndef DIRECTOR_SETTINGS_H
+#define DIRECTOR_SETTINGS_H
+
+#include "net.h"
+
+struct director_settings {
+ const char *auth_master_user_separator;
+
+ const char *director_servers;
+ const char *director_mail_servers;
+ const char *director_username_hash;
+ const char *director_flush_socket;
+
+ unsigned int director_ping_idle_timeout;
+ unsigned int director_ping_max_timeout;
+ unsigned int director_user_expire;
+ unsigned int director_user_kick_delay;
+ unsigned int director_max_parallel_moves;
+ unsigned int director_max_parallel_kicks;
+ uoff_t director_output_buffer_size;
+};
+
+extern const struct setting_parser_info director_setting_parser_info;
+
+#endif
diff --git a/src/director/director-test.c b/src/director/director-test.c
new file mode 100644
index 0000000..ab632b4
--- /dev/null
+++ b/src/director/director-test.c
@@ -0,0 +1,605 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ This program accepts incoming unauthenticated IMAP connections from
+ port 14300. If the same user is connecting to multiple different local IPs,
+ it logs an error (i.e. director is not working right then).
+
+ This program also accepts incoming director connections on port 9091 and
+ forwards them to local_ip:9090. To make this work properly, director
+ executable must be given -t 9091 parameter. The idea is that this test tool
+ hooks between all director connections and can then add delays or break the
+ connections.
+
+ Finally, this program connects to director-admin socket where it adds
+ and removes mail hosts.
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "hash.h"
+#include "llist.h"
+#include "strescape.h"
+#include "imap-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "director-settings.h"
+
+#include <unistd.h>
+
+#define IMAP_PORT 14300
+#define DIRECTOR_IN_PORT 9091
+#define DIRECTOR_OUT_PORT 9090
+#define USER_TIMEOUT_MSECS (1000*10) /* FIXME: this should be based on director_user_expire */
+#define ADMIN_RANDOM_TIMEOUT_MSECS 500
+#define DIRECTOR_CONN_MAX_DELAY_MSECS 100
+#define DIRECTOR_DISCONNECT_TIMEOUT_SECS 10
+
+struct host {
+ int refcount;
+
+ struct ip_addr ip;
+ unsigned int vhost_count;
+};
+
+struct user {
+ char *username;
+ struct host *host;
+
+ time_t last_seen;
+ unsigned int connections;
+
+ struct timeout *to;
+};
+
+struct imap_client {
+ struct imap_client *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct imap_parser *parser;
+ struct user *user;
+
+ char *username;
+};
+
+struct director_connection {
+ struct director_connection *prev, *next;
+
+ int in_fd, out_fd;
+ struct io *in_io, *out_io;
+ struct istream *in_input, *out_input;
+ struct ostream *in_output, *out_output;
+ struct timeout *to_delay;
+};
+
+struct admin_connection {
+ char *path;
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct timeout *to_random;
+ bool pending_command;
+};
+
+static struct imap_client *imap_clients;
+static struct director_connection *director_connections;
+static HASH_TABLE(char *, struct user *) users;
+static HASH_TABLE(struct ip_addr *, struct host *) hosts;
+static ARRAY(struct host *) hosts_array;
+static struct admin_connection *admin;
+static struct timeout *to_disconnect;
+
+static void imap_client_destroy(struct imap_client **client);
+static void director_connection_destroy(struct director_connection **conn);
+static void director_connection_timeout(struct director_connection *conn);
+
+static void host_unref(struct host **_host)
+{
+ struct host *host = *_host;
+
+ *_host = NULL;
+
+ i_assert(host->refcount > 0);
+ if (--host->refcount > 0)
+ return;
+
+ i_free(host);
+}
+
+static void client_username_check(struct imap_client *client)
+{
+ struct user *user;
+ struct host *host;
+ struct ip_addr local_ip;
+
+ if (net_getsockname(client->fd, &local_ip, NULL) < 0)
+ i_fatal("net_getsockname() failed: %m");
+
+ host = hash_table_lookup(hosts, &local_ip);
+ if (host == NULL) {
+ i_error("User logging into unknown host %s",
+ net_ip2addr(&local_ip));
+ host = i_new(struct host, 1);
+ host->refcount = 1;
+ host->ip = local_ip;
+ host->vhost_count = 100;
+ hash_table_insert(hosts, &host->ip, host);
+ array_push_back(&hosts_array, &host);
+ }
+
+ user = hash_table_lookup(users, client->username);
+ if (user == NULL) {
+ user = i_new(struct user, 1);
+ user->username = i_strdup(client->username);
+ hash_table_insert(users, user->username, user);
+ } else if (user->host != host) {
+ i_error("user %s: old connection from %s, new from %s. "
+ "%u old connections, last was %u secs ago",
+ user->username, net_ip2addr(&user->host->ip),
+ net_ip2addr(&host->ip), user->connections,
+ (unsigned int)(ioloop_time - user->last_seen));
+ host_unref(&user->host);
+ }
+ client->user = user;
+ user->host = host;
+ user->connections++;
+ user->last_seen = ioloop_time;
+ user->host->refcount++;
+
+ timeout_remove(&user->to);
+}
+
+static void user_free(struct user *user)
+{
+ host_unref(&user->host);
+ timeout_remove(&user->to);
+ hash_table_remove(users, user->username);
+ i_free(user->username);
+ i_free(user);
+}
+
+static int imap_client_parse_input(struct imap_client *client)
+{
+ const char *tag, *cmd, *str;
+ const struct imap_arg *args;
+ int ret;
+
+ ret = imap_parser_read_args(client->parser, 0, 0, &args);
+ if (ret < 0) {
+ if (ret == -2)
+ return 0;
+ return -1;
+ }
+
+ if (!imap_arg_get_atom(args, &tag))
+ return -1;
+ args++;
+
+ if (!imap_arg_get_atom(args, &cmd))
+ return -1;
+ args++;
+
+ if (strcasecmp(cmd, "login") == 0) {
+ if (client->username != NULL)
+ return -1;
+
+ if (!imap_arg_get_astring(args, &str))
+ return -1;
+
+ o_stream_nsend_str(client->output,
+ t_strconcat(tag, " OK Logged in.\r\n", NULL));
+ client->username = i_strdup(str);
+ client_username_check(client);
+ } else if (strcasecmp(cmd, "logout") == 0) {
+ o_stream_nsend_str(client->output, t_strconcat(
+ "* BYE Out.\r\n",
+ tag, " OK Logged out.\r\n", NULL));
+ imap_client_destroy(&client);
+ return 0;
+ } else if (strcasecmp(cmd, "capability") == 0) {
+ o_stream_nsend_str(client->output,
+ t_strconcat("* CAPABILITY IMAP4rev1\r\n",
+ tag, " OK Done.\r\n", NULL));
+ } else {
+ o_stream_nsend_str(client->output,
+ t_strconcat(tag, " BAD Not supported.\r\n", NULL));
+ }
+
+ (void)i_stream_read_next_line(client->input); /* eat away LF */
+ imap_parser_reset(client->parser);
+ return 1;
+}
+
+static void imap_client_input(struct imap_client *client)
+{
+ int ret;
+
+ switch (i_stream_read(client->input)) {
+ case -2:
+ i_error("imap: Too much input");
+ imap_client_destroy(&client);
+ return;
+ case -1:
+ imap_client_destroy(&client);
+ return;
+ default:
+ break;
+ }
+
+ while ((ret = imap_client_parse_input(client)) > 0) ;
+ if (ret < 0) {
+ i_error("imap: Invalid input");
+ imap_client_destroy(&client);
+ }
+}
+
+static void imap_client_create(int fd)
+{
+ struct imap_client *client;
+
+ client = i_new(struct imap_client, 1);
+ client->fd = fd;
+ client->input = i_stream_create_fd(fd, 4096);
+ client->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ client->io = io_add(fd, IO_READ, imap_client_input, client);
+ client->parser =
+ imap_parser_create(client->input, client->output, 4096);
+ o_stream_nsend_str(client->output,
+ "* OK [CAPABILITY IMAP4rev1] director-test ready.\r\n");
+ DLLIST_PREPEND(&imap_clients, client);
+}
+
+static void imap_client_destroy(struct imap_client **_client)
+{
+ struct imap_client *client = *_client;
+ struct user *user = client->user;
+
+ *_client = NULL;
+
+ if (user != NULL) {
+ i_assert(user->connections > 0);
+ if (--user->connections == 0) {
+ i_assert(user->to == NULL);
+ user->to = timeout_add(USER_TIMEOUT_MSECS, user_free,
+ user);
+ }
+ user->last_seen = ioloop_time;
+ }
+
+ DLLIST_REMOVE(&imap_clients, client);
+ imap_parser_unref(&client->parser);
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ net_disconnect(client->fd);
+ i_free(client->username);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+director_connection_input(struct director_connection *conn,
+ struct istream *input, struct ostream *output)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read_more(input, &data, &size) == -1) {
+ director_connection_destroy(&conn);
+ return;
+ }
+
+ o_stream_nsend(output, data, size);
+ i_stream_skip(input, size);
+
+ if (i_rand_limit(3) == 0 && conn->to_delay == NULL) {
+ conn->to_delay =
+ timeout_add(i_rand_limit(DIRECTOR_CONN_MAX_DELAY_MSECS),
+ director_connection_timeout, conn);
+ io_remove(&conn->in_io);
+ io_remove(&conn->out_io);
+ }
+}
+
+static void director_connection_in_input(struct director_connection *conn)
+{
+ director_connection_input(conn, conn->in_input, conn->out_output);
+}
+
+static void director_connection_out_input(struct director_connection *conn)
+{
+ director_connection_input(conn, conn->out_input, conn->in_output);
+}
+
+static void director_connection_timeout(struct director_connection *conn)
+{
+ timeout_remove(&conn->to_delay);
+ conn->in_io = io_add(conn->in_fd, IO_READ,
+ director_connection_in_input, conn);
+ conn->out_io = io_add(conn->out_fd, IO_READ,
+ director_connection_out_input, conn);
+}
+
+static void
+director_connection_create(int in_fd, const struct ip_addr *local_ip,
+ const struct ip_addr *remote_ip)
+{
+ struct director_connection *conn;
+ int out_fd;
+
+ out_fd = net_connect_ip(local_ip, DIRECTOR_OUT_PORT, remote_ip);
+ if (out_fd == -1) {
+ i_close_fd(&in_fd);
+ return;
+ }
+
+ conn = i_new(struct director_connection, 1);
+ conn->in_fd = in_fd;
+ conn->in_input = i_stream_create_fd(conn->in_fd, SIZE_MAX);
+ conn->in_output = o_stream_create_fd(conn->in_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->in_output, TRUE);
+ conn->in_io = io_add(conn->in_fd, IO_READ,
+ director_connection_in_input, conn);
+
+ conn->out_fd = out_fd;
+ conn->out_input = i_stream_create_fd(conn->out_fd, SIZE_MAX);
+ conn->out_output = o_stream_create_fd(conn->out_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->out_output, TRUE);
+ conn->out_io = io_add(conn->out_fd, IO_READ,
+ director_connection_out_input, conn);
+
+ DLLIST_PREPEND(&director_connections, conn);
+}
+
+static void director_connection_destroy(struct director_connection **_conn)
+{
+ struct director_connection *conn = *_conn;
+
+ DLLIST_REMOVE(&director_connections, conn);
+
+ timeout_remove(&conn->to_delay);
+
+ io_remove(&conn->in_io);
+ i_stream_unref(&conn->in_input);
+ o_stream_unref(&conn->in_output);
+ net_disconnect(conn->in_fd);
+
+ io_remove(&conn->out_io);
+ i_stream_unref(&conn->out_input);
+ o_stream_unref(&conn->out_output);
+ net_disconnect(conn->out_fd);
+
+ i_free(conn);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port;
+
+ if (net_getsockname(conn->fd, &local_ip, &local_port) < 0)
+ i_fatal("net_getsockname() failed: %m");
+ if (net_getpeername(conn->fd, &remote_ip, NULL) < 0)
+ i_fatal("net_getsockname() failed: %m");
+
+ if (local_port == IMAP_PORT)
+ imap_client_create(conn->fd);
+ else if (local_port == DIRECTOR_IN_PORT)
+ director_connection_create(conn->fd, &local_ip, &remote_ip);
+ else {
+ i_error("Connection to unknown port %u", local_port);
+ return;
+ }
+ master_service_client_connection_accept(conn);
+}
+
+static void
+admin_send(struct admin_connection *conn, const char *data)
+{
+ if (write_full(i_stream_get_fd(conn->input), data, strlen(data)) < 0)
+ i_fatal("write(%s) failed: %m", conn->path);
+}
+
+static void admin_input(struct admin_connection *conn)
+{
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (strcmp(line, "OK") != 0)
+ i_error("director-doveadm: Unexpected input: %s", line);
+ conn->pending_command = FALSE;
+ }
+ if (conn->input->stream_errno != 0 || conn->input->eof)
+ i_fatal("director-doveadm: Connection lost");
+}
+
+static void admin_random_action(struct admin_connection *conn)
+{
+ struct host *const *hosts;
+ unsigned int i, count;
+
+ if (conn->pending_command)
+ return;
+
+ hosts = array_get(&hosts_array, &count);
+ i = i_rand_limit(count);
+
+ hosts[i]->vhost_count = i_rand_limit(20) * 10;
+
+ admin_send(conn, t_strdup_printf("HOST-SET\t%s\t%u\n",
+ net_ip2addr(&hosts[i]->ip), hosts[i]->vhost_count));
+ conn->pending_command = TRUE;
+}
+
+static struct admin_connection *admin_connect(const char *path)
+{
+#define DIRECTOR_ADMIN_HANDSHAKE "VERSION\tdirector-doveadm\t1\t0\n"
+ struct admin_connection *conn;
+ const char *line;
+
+ conn = i_new(struct admin_connection, 1);
+ conn->path = i_strdup(path);
+ conn->fd = net_connect_unix(path);
+ if (conn->fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ conn->io = io_add(conn->fd, IO_READ, admin_input, conn);
+ conn->to_random = timeout_add_short(ADMIN_RANDOM_TIMEOUT_MSECS,
+ admin_random_action, conn);
+
+ net_set_nonblock(conn->fd, FALSE);
+ conn->input = i_stream_create_fd(conn->fd, SIZE_MAX);
+ admin_send(conn, DIRECTOR_ADMIN_HANDSHAKE);
+
+ line = i_stream_read_next_line(conn->input);
+ if (line == NULL)
+ i_fatal("%s disconnected", conn->path);
+ if (!version_string_verify(line, "director-doveadm", 1)) {
+ i_fatal("%s not a compatible director-doveadm socket",
+ conn->path);
+ }
+ net_set_nonblock(conn->fd, TRUE);
+ return conn;
+}
+
+static void admin_disconnect(struct admin_connection **_conn)
+{
+ struct admin_connection *conn = *_conn;
+
+ *_conn = NULL;
+ timeout_remove(&conn->to_random);
+ i_stream_destroy(&conn->input);
+ io_remove(&conn->io);
+ net_disconnect(conn->fd);
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void admin_read_hosts(struct admin_connection *conn)
+{
+ const char *line;
+
+ net_set_nonblock(admin->fd, FALSE);
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ /* ip vhost-count user-count */
+ T_BEGIN {
+ const char *const *args = t_strsplit_tabescaped(line);
+ struct host *host;
+
+ host = i_new(struct host, 1);
+ host->refcount = 1;
+ if (net_addr2ip(args[0], &host->ip) < 0 ||
+ str_to_uint(args[1], &host->vhost_count) < 0)
+ i_fatal("host list broken");
+ hash_table_insert(hosts, &host->ip, host);
+ array_push_back(&hosts_array, &host);
+ } T_END;
+ }
+ if (line == NULL)
+ i_fatal("Couldn't read hosts list");
+ net_set_nonblock(admin->fd, TRUE);
+}
+
+static void ATTR_NULL(1)
+director_connection_disconnect_timeout(void *context ATTR_UNUSED)
+{
+ struct director_connection *conn;
+ unsigned int i, count = 0;
+
+ for (conn = director_connections; conn != NULL; conn = conn->next)
+ count++;
+
+ if (count != 0) {
+ i = 0; count = i_rand_limit(count);
+ for (conn = director_connections; i < count; conn = conn->next) {
+ i_assert(conn != NULL);
+ i++;
+ }
+ i_assert(conn != NULL);
+ director_connection_destroy(&conn);
+ }
+}
+
+static void main_init(const char *admin_path)
+{
+ hash_table_create(&users, default_pool, 0, str_hash, strcmp);
+ hash_table_create(&hosts, default_pool, 0, net_ip_hash, net_ip_cmp);
+ i_array_init(&hosts_array, 256);
+
+ admin = admin_connect(admin_path);
+ admin_send(admin, "HOST-LIST\n");
+ admin_read_hosts(admin);
+
+ to_disconnect =
+ timeout_add(1000 * i_rand_minmax(5, 5 + DIRECTOR_DISCONNECT_TIMEOUT_SECS - 1),
+ director_connection_disconnect_timeout, NULL);
+}
+
+static void main_deinit(void)
+{
+ struct hash_iterate_context *iter;
+ char *username;
+ struct ip_addr *ip;
+ struct user *user;
+ struct host *host;
+
+ while (imap_clients != NULL) {
+ struct imap_client *client = imap_clients;
+ imap_client_destroy(&client);
+ }
+
+ timeout_remove(&to_disconnect);
+ while (director_connections != NULL) {
+ struct director_connection *conn = director_connections;
+ director_connection_destroy(&conn);
+ }
+
+ iter = hash_table_iterate_init(users);
+ while (hash_table_iterate(iter, users, &username, &user))
+ user_free(user);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&users);
+
+ iter = hash_table_iterate_init(hosts);
+ while (hash_table_iterate(iter, hosts, &ip, &host))
+ host_unref(&host);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&hosts);
+ array_free(&hosts_array);
+
+ admin_disconnect(&admin);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ const char *admin_path;
+
+ master_service = master_service_init("director-test", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ admin_path = argv[optind];
+ if (admin_path == NULL)
+ i_fatal("director-doveadm socket path missing");
+
+ master_service_init_log(master_service);
+
+ main_init(admin_path);
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/director/director.c b/src/director/director.c
new file mode 100644
index 0000000..317cff1
--- /dev/null
+++ b/src/director/director.c
@@ -0,0 +1,1589 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "log-throttle.h"
+#include "ipc-client.h"
+#include "program-client.h"
+#include "var-expand.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "mail-user-hash.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director.h"
+
+#define DIRECTOR_IPC_PROXY_PATH "ipc"
+#define DIRECTOR_DNS_SOCKET_PATH "dns-client"
+#define DIRECTOR_RECONNECT_RETRY_SECS 60
+#define DIRECTOR_RECONNECT_TIMEOUT_MSECS (30*1000)
+#define DIRECTOR_USER_MOVE_TIMEOUT_MSECS (30*1000)
+#define DIRECTOR_SYNC_TIMEOUT_MSECS (5*1000)
+#define DIRECTOR_RING_MIN_WAIT_SECS 20
+#define DIRECTOR_QUICK_RECONNECT_TIMEOUT_MSECS 1000
+#define DIRECTOR_DELAYED_DIR_REMOVE_MSECS (1000*30)
+
+const char *user_kill_state_names[USER_KILL_STATE_DELAY+1] = {
+ "none",
+ "killing",
+ "notify-received",
+ "waiting-for-notify",
+ "waiting-for-everyone",
+ "flushing",
+ "delay",
+};
+
+static struct event_category event_category_director = {
+ .name = "director",
+};
+
+static struct log_throttle *user_move_throttle;
+static struct log_throttle *user_kill_fail_throttle;
+
+static void director_hosts_purge_removed(struct director *dir);
+
+static const struct log_throttle_settings director_log_throttle_settings = {
+ .throttle_at_max_per_interval = 100,
+ .unthrottle_at_max_per_interval = 2,
+};
+
+static void
+director_user_kill_finish_delayed(struct director *dir, struct user *user,
+ bool skip_delay);
+
+static bool director_is_self_ip_set(struct director *dir)
+{
+ if (net_ip_compare(&dir->self_ip, &net_ip4_any))
+ return FALSE;
+
+ if (net_ip_compare(&dir->self_ip, &net_ip6_any))
+ return FALSE;
+
+ return TRUE;
+}
+
+static void director_find_self_ip(struct director *dir)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (net_try_bind(&hosts[i]->ip) == 0) {
+ dir->self_ip = hosts[i]->ip;
+ return;
+ }
+ }
+ i_fatal("director_servers doesn't list ourself");
+}
+
+void director_find_self(struct director *dir)
+{
+ if (dir->self_host != NULL)
+ return;
+
+ if (!director_is_self_ip_set(dir))
+ director_find_self_ip(dir);
+
+ dir->self_host = director_host_lookup(dir, &dir->self_ip,
+ dir->self_port);
+ if (dir->self_host == NULL) {
+ i_fatal("director_servers doesn't list ourself (%s:%u)",
+ net_ip2addr(&dir->self_ip), dir->self_port);
+ }
+ dir->self_host->self = TRUE;
+}
+
+static unsigned int director_find_self_idx(struct director *dir)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ i_assert(dir->self_host != NULL);
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == dir->self_host)
+ return i;
+ }
+ i_unreached();
+}
+
+static bool
+director_has_outgoing_connection(struct director *dir,
+ struct director_host *host)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) == host &&
+ !director_connection_is_incoming(conn))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+director_log_connect(struct director *dir, struct director_host *host,
+ const char *reason)
+{
+ string_t *str = t_str_new(128);
+
+ if (host->last_network_failure > 0) {
+ str_printfa(str, ", last network failure %ds ago",
+ (int)(ioloop_time - host->last_network_failure));
+ }
+ if (host->last_protocol_failure > 0) {
+ str_printfa(str, ", last protocol failure %ds ago",
+ (int)(ioloop_time - host->last_protocol_failure));
+ }
+ e_info(dir->event, "Connecting to %s:%u (as %s%s): %s",
+ host->ip_str, host->port,
+ net_ip2addr(&dir->self_ip), str_c(str), reason);
+}
+
+int director_connect_host(struct director *dir, struct director_host *host,
+ const char *reason)
+{
+ in_port_t port;
+ int fd;
+
+ if (director_has_outgoing_connection(dir, host))
+ return 0;
+
+ director_log_connect(dir, host, reason);
+ port = dir->test_port != 0 ? dir->test_port : host->port;
+ fd = net_connect_ip(&host->ip, port, &dir->self_ip);
+ if (fd == -1) {
+ host->last_network_failure = ioloop_time;
+ e_error(dir->event, "connect(%s) failed: %m", host->name);
+ return -1;
+ }
+ /* Reset timestamp so that director_connect() won't skip this host
+ while we're still trying to connect to it */
+ host->last_network_failure = 0;
+
+ (void)director_connection_init_out(dir, fd, host);
+ return 0;
+}
+
+static struct director_host *
+director_get_preferred_right_host(struct director *dir)
+{
+ struct director_host *const *hosts, *host;
+ unsigned int i, count, self_idx;
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ if (count == 1) {
+ /* self */
+ return NULL;
+ }
+
+ self_idx = director_find_self_idx(dir);
+ for (i = 0; i < count; i++) {
+ host = hosts[(self_idx + i + 1) % count];
+ if (!host->removed)
+ return host;
+ }
+ /* self, with some removed hosts */
+ return NULL;
+}
+
+static void director_quick_reconnect_retry(struct director *dir)
+{
+ director_connect(dir, "Alone in director ring - trying to connect to others");
+}
+
+static bool director_wait_for_others(struct director *dir)
+{
+ struct director_host *host;
+
+ /* don't assume we're alone until we've attempted to connect
+ to others for a while */
+ if (dir->ring_first_alone != 0 &&
+ ioloop_time - dir->ring_first_alone > DIRECTOR_RING_MIN_WAIT_SECS)
+ return FALSE;
+
+ if (dir->ring_first_alone == 0)
+ dir->ring_first_alone = ioloop_time;
+ /* reset all failures and try again */
+ array_foreach_elem(&dir->dir_hosts, host) {
+ host->last_network_failure = 0;
+ host->last_protocol_failure = 0;
+ }
+ timeout_remove(&dir->to_reconnect);
+ dir->to_reconnect = timeout_add(DIRECTOR_QUICK_RECONNECT_TIMEOUT_MSECS,
+ director_quick_reconnect_retry, dir);
+ return TRUE;
+}
+
+void director_connect(struct director *dir, const char *reason)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count, self_idx;
+
+ self_idx = director_find_self_idx(dir);
+
+ /* try to connect to first working server on our right side.
+ the left side is supposed to connect to us. */
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 1; i < count; i++) {
+ unsigned int idx = (self_idx + i) % count;
+
+ if (hosts[idx]->removed)
+ continue;
+
+ if (hosts[idx]->last_network_failure +
+ DIRECTOR_RECONNECT_RETRY_SECS > ioloop_time) {
+ /* connection failed recently, don't try retrying here */
+ continue;
+ }
+ if (hosts[idx]->last_protocol_failure +
+ DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS > ioloop_time) {
+ /* the director recently sent invalid protocol data,
+ don't try retrying yet */
+ continue;
+ }
+
+ if (director_connect_host(dir, hosts[idx], reason) == 0) {
+ /* success */
+ return;
+ }
+ }
+
+ if (count > 1 && director_wait_for_others(dir))
+ return;
+
+ /* we're the only one */
+ if (count > 1) {
+ e_warning(dir->event,
+ "director: Couldn't connect to right side, "
+ "we must be the only director left");
+ }
+ if (dir->left != NULL) {
+ /* since we couldn't connect to it,
+ it must have failed recently */
+ e_warning(dir->event,
+ "director: Assuming %s is dead, disconnecting",
+ director_connection_get_name(dir->left));
+ director_connection_deinit(&dir->left,
+ "This connection is dead?");
+ }
+ dir->ring_min_version = DIRECTOR_VERSION_MINOR;
+ if (!dir->ring_handshaked)
+ director_set_ring_handshaked(dir);
+ else if (!dir->ring_synced)
+ director_set_ring_synced(dir);
+}
+
+void director_set_ring_handshaked(struct director *dir)
+{
+ i_assert(!dir->ring_handshaked);
+
+ timeout_remove(&dir->to_handshake_warning);
+ if (dir->ring_handshake_warning_sent) {
+ e_warning(dir->event,
+ "Directors have been connected, "
+ "continuing delayed requests");
+ dir->ring_handshake_warning_sent = FALSE;
+ }
+ e_debug(dir->event, "Director ring handshaked");
+
+ dir->ring_handshaked = TRUE;
+ director_set_ring_synced(dir);
+}
+
+static void director_reconnect_timeout(struct director *dir)
+{
+ struct director_host *cur_host, *preferred_host =
+ director_get_preferred_right_host(dir);
+
+ cur_host = dir->right == NULL ? NULL :
+ director_connection_get_host(dir->right);
+
+ if (preferred_host == NULL) {
+ /* all directors have been removed, try again later */
+ } else if (cur_host != preferred_host) {
+ (void)director_connect_host(dir, preferred_host,
+ "Reconnect attempt to preferred director");
+ } else {
+ /* the connection hasn't finished sync yet.
+ keep this timeout for now. */
+ }
+}
+
+void director_set_ring_synced(struct director *dir)
+{
+ struct director_host *host;
+
+ i_assert(!dir->ring_synced);
+ i_assert((dir->left != NULL && dir->right != NULL) ||
+ (dir->left == NULL && dir->right == NULL));
+
+ timeout_remove(&dir->to_handshake_warning);
+ if (dir->ring_handshake_warning_sent) {
+ e_warning(dir->event,
+ "Ring is synced, continuing delayed requests "
+ "(syncing took %d secs, hosts_hash=%u)",
+ (int)(ioloop_time - dir->ring_last_sync_time),
+ mail_hosts_hash(dir->mail_hosts));
+ dir->ring_handshake_warning_sent = FALSE;
+ }
+
+ host = dir->right == NULL ? NULL :
+ director_connection_get_host(dir->right);
+
+ timeout_remove(&dir->to_reconnect);
+ if (host != director_get_preferred_right_host(dir)) {
+ /* try to reconnect to preferred host later */
+ dir->to_reconnect =
+ timeout_add(DIRECTOR_RECONNECT_TIMEOUT_MSECS,
+ director_reconnect_timeout, dir);
+ }
+
+ if (dir->left != NULL)
+ director_connection_set_synced(dir->left, TRUE);
+ if (dir->right != NULL)
+ director_connection_set_synced(dir->right, TRUE);
+ timeout_remove(&dir->to_sync);
+ dir->ring_synced = TRUE;
+ dir->ring_last_sync_time = ioloop_time;
+ /* If there are any director hosts still marked as "removed", we can
+ safely remove those now. The entire director cluster knows about the
+ removal now. */
+ director_hosts_purge_removed(dir);
+ mail_hosts_set_synced(dir->mail_hosts);
+ director_set_state_changed(dir);
+}
+
+void director_sync_send(struct director *dir, struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash)
+{
+ string_t *str;
+
+ if (host == dir->self_host) {
+ dir->last_sync_sent_ring_change_counter = dir->ring_change_counter;
+ dir->last_sync_start_time = ioloop_timeval;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "SYNC\t%s\t%u\t%u",
+ host->ip_str, host->port, seq);
+ if (minor_version > 0 &&
+ director_connection_get_minor_version(dir->right) > 0) {
+ /* only minor_version>0 supports extra parameters */
+ str_printfa(str, "\t%u\t%u\t%u", minor_version,
+ timestamp, hosts_hash);
+ }
+ str_append_c(str, '\n');
+ director_connection_send(dir->right, str_c(str));
+
+ /* ping our connections in case either of them are hanging.
+ if they are, we want to know it fast. */
+ if (dir->left != NULL)
+ director_connection_ping(dir->left);
+ director_connection_ping(dir->right);
+}
+
+static bool
+director_has_any_outgoing_connections(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (!director_connection_is_incoming(conn))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool director_resend_sync(struct director *dir)
+{
+ if (dir->ring_synced) {
+ /* everything ok, no need to do anything */
+ return FALSE;
+ }
+
+ if (dir->right == NULL) {
+ /* right side connection is missing. make sure we're not
+ hanging due to some bug. */
+ if (dir->to_reconnect == NULL &&
+ !director_has_any_outgoing_connections(dir)) {
+ e_warning(dir->event,
+ "Right side connection is unexpectedly lost, reconnecting");
+ director_connect(dir, "Right side connection lost");
+ }
+ } else if (dir->left != NULL) {
+ /* send a new SYNC in case the previous one got dropped */
+ dir->self_host->last_sync_timestamp = ioloop_time;
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+ if (dir->to_sync != NULL)
+ timeout_reset(dir->to_sync);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void director_sync_timeout(struct director *dir)
+{
+ i_assert(!dir->ring_synced);
+
+ if (director_resend_sync(dir))
+ e_error(dir->event, "Ring SYNC seq=%u appears to have got lost, resending", dir->sync_seq);
+}
+
+void director_set_ring_unsynced(struct director *dir)
+{
+ if (dir->ring_synced) {
+ dir->ring_synced = FALSE;
+ dir->ring_last_sync_time = ioloop_time;
+ }
+
+ if (dir->to_sync == NULL) {
+ dir->to_sync = timeout_add(DIRECTOR_SYNC_TIMEOUT_MSECS,
+ director_sync_timeout, dir);
+ } else {
+ timeout_reset(dir->to_sync);
+ }
+}
+
+static void director_sync(struct director *dir)
+{
+ /* we're synced again when we receive this SYNC back */
+ dir->sync_seq++;
+ if (dir->right == NULL && dir->left == NULL) {
+ /* we're alone. if we're already synced,
+ don't become unsynced. */
+ return;
+ }
+ director_set_ring_unsynced(dir);
+
+ if (dir->sync_frozen) {
+ dir->sync_pending = TRUE;
+ return;
+ }
+ if (dir->right == NULL) {
+ i_assert(!dir->ring_synced ||
+ (dir->left == NULL && dir->right == NULL));
+ e_debug(dir->event, "Ring is desynced (seq=%u, no right connection)",
+ dir->sync_seq);
+ return;
+ }
+
+ e_debug(dir->event, "Ring is desynced (seq=%u, sending SYNC to %s)",
+ dir->sync_seq, dir->right == NULL ? "(nowhere)" :
+ director_connection_get_name(dir->right));
+
+ /* send PINGs to our connections more rapidly until we've synced again.
+ if the connection has actually died, we don't need to wait (and
+ delay requests) for as long to detect it */
+ if (dir->left != NULL)
+ director_connection_set_synced(dir->left, FALSE);
+ director_connection_set_synced(dir->right, FALSE);
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+}
+
+void director_sync_freeze(struct director *dir)
+{
+ struct director_connection *conn;
+
+ i_assert(!dir->sync_frozen);
+ i_assert(!dir->sync_pending);
+
+ array_foreach_elem(&dir->connections, conn)
+ director_connection_cork(conn);
+ dir->sync_frozen = TRUE;
+}
+
+void director_sync_thaw(struct director *dir)
+{
+ struct director_connection *conn;
+
+ i_assert(dir->sync_frozen);
+
+ dir->sync_frozen = FALSE;
+ if (dir->sync_pending) {
+ dir->sync_pending = FALSE;
+ director_sync(dir);
+ }
+ array_foreach_elem(&dir->connections, conn)
+ director_connection_uncork(conn);
+}
+
+void director_notify_ring_added(struct director_host *added_host,
+ struct director_host *src, bool log)
+{
+ const char *cmd;
+
+ if (log) {
+ e_info(added_host->dir->event,
+ "Adding director %s to ring (requested by %s)",
+ added_host->name, src->name);
+ }
+
+ added_host->dir->ring_change_counter++;
+ cmd = t_strdup_printf("DIRECTOR\t%s\t%u\n",
+ added_host->ip_str, added_host->port);
+ director_update_send(added_host->dir, src, cmd);
+}
+
+static void director_hosts_purge_removed(struct director *dir)
+{
+ struct director_host *const *hosts, *host;
+ unsigned int i, count;
+
+ timeout_remove(&dir->to_remove_dirs);
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; ) {
+ if (hosts[i]->removed) {
+ host = hosts[i];
+ director_host_free(&host);
+ hosts = array_get(&dir->dir_hosts, &count);
+ } else {
+ i++;
+ }
+ }
+}
+
+void director_ring_remove(struct director_host *removed_host,
+ struct director_host *src)
+{
+ struct director *dir = removed_host->dir;
+ struct director_connection *const *conns, *conn;
+ unsigned int i, count;
+ const char *cmd;
+
+ e_info(dir->event, "Removing director %s from ring (requested by %s)",
+ removed_host->name, src->name);
+
+ if (removed_host->self && !src->self) {
+ /* others will just disconnect us */
+ return;
+ }
+
+ if (!removed_host->self) {
+ /* mark the host as removed and fully remove it later. this
+ delay is needed, because the removal may trigger director
+ reconnections, which may send the director back and we don't
+ want to re-add it */
+ removed_host->removed = TRUE;
+ if (dir->to_remove_dirs == NULL) {
+ dir->to_remove_dirs =
+ timeout_add(DIRECTOR_DELAYED_DIR_REMOVE_MSECS,
+ director_hosts_purge_removed, dir);
+ }
+ }
+
+ /* if our left or ride side gets removed, notify them first
+ before disconnecting. */
+ cmd = t_strdup_printf("DIRECTOR-REMOVE\t%s\t%u\n",
+ removed_host->ip_str, removed_host->port);
+ director_update_send_version(dir, src,
+ DIRECTOR_VERSION_RING_REMOVE, cmd);
+
+ /* disconnect any connections to the host */
+ conns = array_get(&dir->connections, &count);
+ for (i = 0; i < count; ) {
+ conn = conns[i];
+ if (director_connection_get_host(conn) != removed_host ||
+ removed_host->self)
+ i++;
+ else {
+ director_connection_deinit(&conn, "Removing from ring");
+ conns = array_get(&dir->connections, &count);
+ }
+ }
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after director was removed");
+ director_sync(dir);
+}
+
+static void
+director_send_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ const char *host_tag = mail_host_get_tag(host);
+ string_t *str;
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "HOST\t%s\t%u\t%u\t%s\t%u",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ host->ip_str, host->vhost_count);
+ if (dir->ring_min_version >= DIRECTOR_VERSION_TAGS_V2) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, host_tag);
+ } else if (host_tag[0] != '\0' &&
+ dir->ring_min_version < DIRECTOR_VERSION_TAGS_V2) {
+ if (dir->ring_min_version < DIRECTOR_VERSION_TAGS) {
+ e_error(dir->event, "Ring has directors that don't support tags - removing host %s with tag '%s'",
+ host->ip_str, host_tag);
+ } else {
+ e_error(dir->event, "Ring has directors that support mixed versions of tags - removing host %s with tag '%s'",
+ host->ip_str, host_tag);
+ }
+ director_remove_host(dir, NULL, NULL, host);
+ return;
+ }
+ if (dir->ring_min_version >= DIRECTOR_VERSION_UPDOWN) {
+ str_printfa(str, "\t%c%ld\t", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ /* add any further version checks here - these directors ignore
+ any extra unknown arguments */
+ if (host->hostname != NULL)
+ str_append_tabescaped(str, host->hostname);
+ }
+ str_append_c(str, '\n');
+ director_update_send(dir, src, str_c(str));
+}
+
+void director_resend_hosts(struct director *dir)
+{
+ struct mail_host *host;
+
+ array_foreach_elem(mail_hosts_get(dir->mail_hosts), host)
+ director_send_host(dir, dir->self_host, NULL, host);
+}
+
+void director_update_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ /* update state in case this is the first mail host being added */
+ director_set_state_changed(dir);
+
+ e_debug(dir->event, "Updating host %s vhost_count=%u "
+ "down=%d last_updown_change=%ld (hosts_hash=%u)",
+ host->ip_str, host->vhost_count, host->down ? 1 : 0,
+ (long)host->last_updown_change,
+ mail_hosts_hash(dir->mail_hosts));
+
+ director_send_host(dir, src, orig_src, host);
+
+ /* mark the host desynced until ring is synced again. except if we're
+ alone in the ring that never happens. */
+ if (dir->right != NULL || dir->left != NULL)
+ host->desynced = TRUE;
+ director_sync(dir);
+}
+
+void director_remove_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+
+ if (src != NULL) {
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ director_update_send(dir, src, t_strdup_printf(
+ "HOST-REMOVE\t%s\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port,
+ orig_src->last_seq, host->ip_str));
+ }
+
+ user_directory_remove_host(users, host);
+ mail_host_remove(host);
+ director_sync(dir);
+}
+
+void director_flush_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ director_update_send(dir, src, t_strdup_printf(
+ "HOST-FLUSH\t%s\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ host->ip_str));
+ user_directory_remove_host(users, host);
+ director_sync(dir);
+}
+
+void director_update_user(struct director *dir, struct director_host *src,
+ struct user *user)
+{
+ struct director_connection *conn;
+
+ i_assert(src != NULL);
+ i_assert(!user->weak);
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) == src)
+ continue;
+
+ if (director_connection_get_minor_version(conn) >= DIRECTOR_VERSION_USER_TIMESTAMP) {
+ director_connection_send(conn, t_strdup_printf(
+ "USER\t%u\t%s\t%u\n", user->username_hash, user->host->ip_str,
+ user->timestamp));
+ } else {
+ director_connection_send(conn, t_strdup_printf(
+ "USER\t%u\t%s\n", user->username_hash, user->host->ip_str));
+ }
+ }
+}
+
+void director_update_user_weak(struct director *dir, struct director_host *src,
+ struct director_connection *src_conn,
+ struct director_host *orig_src,
+ struct user *user)
+{
+ const char *cmd;
+
+ i_assert(src != NULL);
+ i_assert(user->weak);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ cmd = t_strdup_printf("USER-WEAK\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ user->username_hash, user->host->ip_str);
+
+ if (src != dir->self_host && dir->left != NULL && dir->right != NULL &&
+ director_connection_get_host(dir->left) ==
+ director_connection_get_host(dir->right)) {
+ /* only two directors in this ring and we're forwarding
+ USER-WEAK from one director back to itself via another
+ so it sees we've received it. we can't use
+ director_update_send() for this, because it doesn't send
+ data back to the source. */
+ if (dir->right == src_conn)
+ director_connection_send(dir->left, cmd);
+ else if (dir->left == src_conn)
+ director_connection_send(dir->right, cmd);
+ else
+ i_unreached();
+ } else {
+ director_update_send(dir, src, cmd);
+ }
+}
+
+static void
+director_flush_user_continue(enum program_client_exit_status result,
+ struct director_kill_context *ctx)
+{
+ struct director *dir = ctx->dir;
+ ctx->callback_pending = FALSE;
+
+ struct user *user = user_directory_lookup(ctx->tag->users,
+ ctx->username_hash);
+
+ if (result == PROGRAM_CLIENT_EXIT_STATUS_FAILURE) {
+ struct istream *is = iostream_temp_finish(&ctx->reply, SIZE_MAX);
+ char *data;
+ i_stream_set_return_partial_line(is, TRUE);
+ data = i_stream_read_next_line(is);
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ ctx->username_hash,
+ net_ip2addr(&ctx->host_ip),
+ data == NULL ? "(no output to stdout)" : data);
+ while((data = i_stream_read_next_line(is)) != NULL) {
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ ctx->username_hash,
+ net_ip2addr(&ctx->host_ip), data);
+ }
+ i_stream_unref(&is);
+ } else {
+ o_stream_unref(&ctx->reply);
+ }
+ program_client_destroy(&ctx->pclient);
+
+ if (!DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx)) {
+ /* user was already freed - ignore */
+ e_debug(dir->event, "User %u freed while flushing, result=%d",
+ ctx->username_hash, result);
+ i_assert(ctx->to_move == NULL);
+ i_free(ctx);
+ } else {
+ /* ctx is freed later via user->kill_ctx */
+ e_debug(dir->event, "Flushing user %u finished, result=%d",
+ ctx->username_hash, result);
+ director_user_kill_finish_delayed(dir, user,
+ result == PROGRAM_CLIENT_EXIT_STATUS_SUCCESS);
+ }
+}
+
+static void
+director_flush_user(struct director *dir, struct user *user)
+{
+ struct director_kill_context *ctx = user->kill_ctx;
+ struct var_expand_table tab[] = {
+ { 'i', user->host->ip_str, "ip" },
+ { 'h', user->host->hostname, "host" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+
+ /* Execute flush script, if set. Only the director that started the
+ user moving will call the flush script. Having each director do it
+ would be redundant since they're all supposed to be performing the
+ same flush task to the same backend.
+
+ Flushing is also not triggered if we're moving a user that we just
+ created due to the user move. This means that the user doesn't have
+ an old host, so we couldn't really even perform any flushing on the
+ backend. */
+ if (*dir->set->director_flush_socket == '\0' ||
+ ctx->old_host_ip.family == 0 ||
+ !ctx->kill_is_self_initiated) {
+ director_user_kill_finish_delayed(dir, user, FALSE);
+ return;
+ }
+
+ ctx->host_ip = user->host->ip;
+
+ string_t *s_sock = str_new(default_pool, 32);
+ if (var_expand(s_sock, dir->set->director_flush_socket, tab, &error) <= 0) {
+ e_error(dir->event, "Failed to expand director_flush_socket=%s: %s",
+ dir->set->director_flush_socket, error);
+ director_user_kill_finish_delayed(dir, user, FALSE);
+ return;
+ }
+ ctx->socket_path = str_free_without_data(&s_sock);
+
+ struct program_client_settings set = {
+ .client_connect_timeout_msecs = 10000,
+ .dns_client_socket_path = DIRECTOR_DNS_SOCKET_PATH,
+ };
+
+ restrict_access_init(&set.restrict_set);
+
+ const char *const args[] = {
+ "FLUSH",
+ t_strdup_printf("%u", user->username_hash),
+ net_ip2addr(&ctx->old_host_ip),
+ user->host->ip_str,
+ ctx->old_host_down ? "down" : "up",
+ dec2str(ctx->old_host_vhost_count),
+ NULL
+ };
+
+ ctx->kill_state = USER_KILL_STATE_FLUSHING;
+ e_debug(dir->event, "Flushing user %u via %s", user->username_hash,
+ ctx->socket_path);
+
+ if ((program_client_create(ctx->socket_path, args, &set, FALSE,
+ &ctx->pclient, &error)) != 0) {
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ user->username_hash,
+ user->host->ip_str,
+ error);
+ director_flush_user_continue(PROGRAM_CLIENT_EXIT_STATUS_FAILURE,
+ ctx);
+ return;
+ }
+
+ ctx->reply =
+ iostream_temp_create_named("/tmp", 0,
+ t_strdup_printf("flush response from %s",
+ user->host->ip_str));
+ o_stream_set_no_error_handling(ctx->reply, TRUE);
+ program_client_set_output(ctx->pclient, ctx->reply);
+ ctx->callback_pending = TRUE;
+ program_client_run_async(ctx->pclient, director_flush_user_continue, ctx);
+}
+
+static void director_user_move_finished(struct director *dir)
+{
+ i_assert(dir->users_moving_count > 0);
+ dir->users_moving_count--;
+
+ director_set_state_changed(dir);
+}
+
+static void director_user_move_free(struct user *user)
+{
+ struct director *dir = user->kill_ctx->dir;
+ struct director_kill_context *kill_ctx = user->kill_ctx;
+
+ i_assert(kill_ctx != NULL);
+
+ e_debug(dir->event, "User %u move finished at state=%s", user->username_hash,
+ user_kill_state_names[kill_ctx->kill_state]);
+
+ if (kill_ctx->ipc_cmd != NULL)
+ ipc_client_cmd_abort(dir->ipc_proxy, &kill_ctx->ipc_cmd);
+ timeout_remove(&kill_ctx->to_move);
+ i_free(kill_ctx->socket_path);
+ i_free(kill_ctx);
+ user->kill_ctx = NULL;
+
+ director_user_move_finished(dir);
+}
+
+static void
+director_user_kill_finish_delayed_to(struct user *user)
+{
+ i_assert(user->kill_ctx != NULL);
+ i_assert(user->kill_ctx->kill_state == USER_KILL_STATE_DELAY);
+
+ director_user_move_free(user);
+}
+
+static void
+director_user_kill_finish_delayed(struct director *dir, struct user *user,
+ bool skip_delay)
+{
+ if (skip_delay) {
+ user->kill_ctx->kill_state = USER_KILL_STATE_NONE;
+ director_user_move_free(user);
+ return;
+ }
+
+ user->kill_ctx->kill_state = USER_KILL_STATE_DELAY;
+
+ /* wait for a while for the kills to finish in the backend server,
+ so there are no longer any processes running for the user before we
+ start letting new in connections to the new server. */
+ timeout_remove(&user->kill_ctx->to_move);
+ user->kill_ctx->to_move =
+ timeout_add(dir->set->director_user_kick_delay * 1000,
+ director_user_kill_finish_delayed_to, user);
+}
+
+static void
+director_finish_user_kill(struct director *dir, struct user *user, bool self)
+{
+ struct director_kill_context *kill_ctx = user->kill_ctx;
+
+ i_assert(kill_ctx != NULL);
+ i_assert(kill_ctx->kill_state != USER_KILL_STATE_FLUSHING);
+ i_assert(kill_ctx->kill_state != USER_KILL_STATE_DELAY);
+
+ e_debug(dir->event, "User %u kill finished - %sstate=%s", user->username_hash,
+ self ? "we started it " : "",
+ user_kill_state_names[kill_ctx->kill_state]);
+
+ if (dir->right == NULL) {
+ /* we're alone */
+ director_flush_user(dir, user);
+ } else if (self ||
+ kill_ctx->kill_state == USER_KILL_STATE_KILLING_NOTIFY_RECEIVED) {
+ director_connection_send(dir->right, t_strdup_printf(
+ "USER-KILLED\t%u\n", user->username_hash));
+ kill_ctx->kill_state = USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE;
+ } else {
+ i_assert(kill_ctx->kill_state == USER_KILL_STATE_KILLING);
+ kill_ctx->kill_state = USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY;
+ }
+}
+
+static void director_user_kill_fail_throttled(unsigned int new_events_count,
+ void *context ATTR_UNUSED)
+{
+ i_error("Failed to kill %u users' connections", new_events_count);
+}
+
+static void director_kill_user_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context)
+{
+ struct director_kill_context *ctx = context;
+ struct user *user;
+
+ /* don't try to abort the IPC command anymore */
+ ctx->ipc_cmd = NULL;
+
+ /* this is an asynchronous notification about user being killed.
+ there are no guarantees about what might have happened to the user
+ in the mean time. */
+ switch (state) {
+ case IPC_CLIENT_CMD_STATE_REPLY:
+ /* shouldn't get here. the command reply isn't finished yet. */
+ e_error(ctx->dir->event,
+ "login process sent unexpected reply to kick: %s", data);
+ return;
+ case IPC_CLIENT_CMD_STATE_OK:
+ break;
+ case IPC_CLIENT_CMD_STATE_ERROR:
+ if (log_throttle_accept(user_kill_fail_throttle)) {
+ e_error(ctx->dir->event,
+ "Failed to kill user %u connections: %s",
+ ctx->username_hash, data);
+ }
+ /* we can't really do anything but continue anyway */
+ break;
+ }
+
+ i_assert(ctx->dir->users_kicking_count > 0);
+ ctx->dir->users_kicking_count--;
+ if (ctx->dir->kick_callback != NULL)
+ ctx->dir->kick_callback(ctx->dir);
+
+ user = user_directory_lookup(ctx->tag->users, ctx->username_hash);
+ if (!DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx)) {
+ /* user was already freed - ignore */
+ i_assert(ctx->to_move == NULL);
+ director_user_move_finished(ctx->dir);
+ i_free(ctx);
+ } else {
+ i_assert(ctx->kill_state == USER_KILL_STATE_KILLING ||
+ ctx->kill_state == USER_KILL_STATE_KILLING_NOTIFY_RECEIVED);
+ /* we were still waiting for the kill notification */
+ director_finish_user_kill(ctx->dir, user, ctx->kill_is_self_initiated);
+ }
+}
+
+static void director_user_move_throttled(unsigned int new_events_count,
+ void *context ATTR_UNUSED)
+{
+ i_error("%u users' move timed out, their state may now be inconsistent",
+ new_events_count);
+}
+
+static void director_user_move_timeout(struct user *user)
+{
+ i_assert(user->kill_ctx != NULL);
+ i_assert(user->kill_ctx->kill_state != USER_KILL_STATE_DELAY);
+
+ if (log_throttle_accept(user_move_throttle)) {
+ e_error(user->kill_ctx->dir->event,
+ "Finishing user %u move timed out, "
+ "its state may now be inconsistent (state=%s)",
+ user->username_hash,
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ }
+ if (user->kill_ctx->kill_state == USER_KILL_STATE_FLUSHING) {
+ o_stream_unref(&user->kill_ctx->reply);
+ program_client_destroy(&user->kill_ctx->pclient);
+ }
+ director_user_move_free(user);
+}
+
+void director_kill_user(struct director *dir, struct director_host *src,
+ struct user *user, struct mail_tag *tag,
+ struct mail_host *old_host, bool forced_kick)
+{
+ struct director_kill_context *ctx;
+ const char *cmd;
+
+ if (USER_IS_BEING_KILLED(user)) {
+ /* User is being moved again before the previous move
+ finished. We'll just continue wherever we left off
+ earlier. */
+ e_debug(dir->event, "User %u move restarted - previous kill_state=%s",
+ user->username_hash,
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ return;
+ }
+
+ user->kill_ctx = ctx = i_new(struct director_kill_context, 1);
+ ctx->dir = dir;
+ ctx->tag = tag;
+ ctx->username_hash = user->username_hash;
+ ctx->kill_is_self_initiated = src->self;
+ if (old_host != NULL) {
+ ctx->old_host_ip = old_host->ip;
+ ctx->old_host_down = old_host->down;
+ ctx->old_host_vhost_count = old_host->vhost_count;
+ }
+
+ dir->users_moving_count++;
+ ctx->to_move = timeout_add(DIRECTOR_USER_MOVE_TIMEOUT_MSECS,
+ director_user_move_timeout, user);
+ ctx->kill_state = USER_KILL_STATE_KILLING;
+
+ if ((old_host != NULL && old_host != user->host) || forced_kick) {
+ cmd = t_strdup_printf("proxy\t*\tKICK-DIRECTOR-HASH\t%u",
+ user->username_hash);
+ dir->users_kicking_count++;
+ ctx->ipc_cmd = ipc_client_cmd(dir->ipc_proxy, cmd,
+ director_kill_user_callback, ctx);
+ } else {
+ /* a) we didn't even know about the user before now.
+ don't bother performing a local kick, since it wouldn't
+ kick anything.
+ b) our host was already correct. notify others that we have
+ killed the user, but don't really do it. */
+ director_finish_user_kill(ctx->dir, user,
+ ctx->kill_is_self_initiated);
+ }
+}
+
+void director_move_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash, struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+ struct mail_host *old_host = NULL;
+ struct user *user;
+
+ /* 1. move this user's host, and set its "killing" flag to delay all of
+ its future connections until all directors have killed the
+ connections and notified us about it.
+
+ 2. tell the other directors about the move
+
+ 3. once user kill callback is called, tell the other directors
+ with USER-KILLED that we're done killing the user.
+
+ 4. when some director gets a duplicate USER-KILLED, it's
+ responsible for notifying all directors that user is completely
+ killed.
+
+ 5. after receiving USER-KILLED-EVERYWHERE notification,
+ new connections are again allowed for the user.
+ */
+ user = user_directory_lookup(users, username_hash);
+ if (user == NULL) {
+ e_debug(dir->event, "User %u move started: User was nonexistent",
+ username_hash);
+ user = user_directory_add(users, username_hash,
+ host, ioloop_time);
+ } else if (user->host == host) {
+ /* User is already in the wanted host, but another director
+ didn't think so. We'll need to finish the move without
+ killing any of our connections. */
+ old_host = user->host;
+ user->timestamp = ioloop_time;
+ e_debug(dir->event, "User %u move forwarded: host is already %s",
+ username_hash, host->ip_str);
+ } else {
+ /* user is looked up via the new host's tag, so if it's found
+ the old tag has to be the same. */
+ i_assert(user->host->tag == host->tag);
+
+ old_host = user->host;
+ user->host->user_count--;
+ user->host = host;
+ user->host->user_count++;
+ user->timestamp = ioloop_time;
+ e_debug(dir->event, "User %u move started: host %s -> %s",
+ username_hash, old_host->ip_str,
+ host->ip_str);
+ }
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ director_update_send(dir, src, t_strdup_printf(
+ "USER-MOVE\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ user->username_hash, user->host->ip_str));
+ /* kill the user only after sending the USER-MOVE, because the kill
+ may finish instantly. */
+ director_kill_user(dir, src, user, host->tag, old_host, FALSE);
+}
+
+static void
+director_kick_user_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context)
+{
+ struct director *dir = context;
+
+ if (state == IPC_CLIENT_CMD_STATE_REPLY) {
+ /* shouldn't get here. the command reply isn't finished yet. */
+ e_error(dir->event, "login process sent unexpected reply to kick: %s", data);
+ return;
+ }
+
+ i_assert(dir->users_kicking_count > 0);
+ dir->users_kicking_count--;
+ if (dir->kick_callback != NULL)
+ dir->kick_callback(dir);
+}
+
+void director_kick_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src, const char *username)
+{
+ string_t *cmd = t_str_new(64);
+
+ str_append(cmd, "proxy\t*\tKICK\t");
+ str_append_tabescaped(cmd, username);
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, str_c(cmd),
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ str_truncate(cmd, 0);
+ str_printfa(cmd, "USER-KICK\t%s\t%u\t%u\t",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq);
+ str_append_tabescaped(cmd, username);
+ str_append_c(cmd, '\n');
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK, str_c(cmd));
+}
+
+void director_kick_user_alt(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ const char *field, const char *value)
+{
+ string_t *cmd = t_str_new(64);
+
+ str_append(cmd, "proxy\t*\tKICK-ALT\t");
+ str_append_tabescaped(cmd, field);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, value);
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, str_c(cmd),
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ str_truncate(cmd, 0);
+ str_printfa(cmd, "USER-KICK-ALT\t%s\t%u\t%u\t",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq);
+ str_append_tabescaped(cmd, field);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, value);
+ str_append_c(cmd, '\n');
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK_ALT, str_c(cmd));
+}
+
+void director_kick_user_hash(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash,
+ const struct ip_addr *except_ip)
+{
+ const char *cmd;
+
+ cmd = t_strdup_printf("proxy\t*\tKICK-DIRECTOR-HASH\t%u\t%s",
+ username_hash, net_ip2addr(except_ip));
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, cmd,
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ cmd = t_strdup_printf("USER-KICK-HASH\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ username_hash, net_ip2addr(except_ip));
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK, cmd);
+}
+
+static void
+director_send_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ director_update_send(dir, src, t_strdup_printf(
+ "USER-KILLED-EVERYWHERE\t%s\t%u\t%u\t%u\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ username_hash));
+}
+
+static void
+director_user_tag_killed(struct director *dir, struct mail_tag *tag,
+ unsigned int username_hash)
+{
+ struct user *user;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL || !USER_IS_BEING_KILLED(user))
+ return;
+
+ switch (user->kill_ctx->kill_state) {
+ case USER_KILL_STATE_KILLING:
+ user->kill_ctx->kill_state = USER_KILL_STATE_KILLING_NOTIFY_RECEIVED;
+ break;
+ case USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY:
+ director_finish_user_kill(dir, user, TRUE);
+ break;
+ case USER_KILL_STATE_KILLING_NOTIFY_RECEIVED:
+ e_debug(dir->event, "User %u kill_state=%s - ignoring USER-KILLED",
+ username_hash, user_kill_state_names[user->kill_ctx->kill_state]);
+ break;
+ case USER_KILL_STATE_NONE:
+ case USER_KILL_STATE_FLUSHING:
+ case USER_KILL_STATE_DELAY:
+ /* move restarted. state=none can also happen if USER-MOVE was
+ sent while we were still moving. send back
+ USER-KILLED-EVERYWHERE to avoid hangs. */
+ director_send_user_killed_everywhere(dir, dir->self_host, NULL,
+ username_hash);
+ break;
+ case USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE:
+ director_user_killed_everywhere(dir, dir->self_host,
+ NULL, username_hash);
+ break;
+ }
+}
+
+void director_user_killed(struct director *dir, unsigned int username_hash)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(mail_hosts_get_tags(dir->mail_hosts), tag)
+ director_user_tag_killed(dir, tag, username_hash);
+}
+
+static void
+director_user_tag_killed_everywhere(struct director *dir,
+ struct mail_tag *tag,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ struct user *user;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL) {
+ e_debug(dir->event, "User %u no longer exists - ignoring USER-KILLED-EVERYWHERE",
+ username_hash);
+ return;
+ }
+ if (!USER_IS_BEING_KILLED(user)) {
+ e_debug(dir->event, "User %u is no longer being killed - ignoring USER-KILLED-EVERYWHERE",
+ username_hash);
+ return;
+ }
+ if (user->kill_ctx->kill_state != USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE) {
+ e_debug(dir->event, "User %u kill_state=%s - ignoring USER-KILLED-EVERYWHERE",
+ username_hash, user_kill_state_names[user->kill_ctx->kill_state]);
+ return;
+ }
+
+ director_flush_user(dir, user);
+ director_send_user_killed_everywhere(dir, src, orig_src, username_hash);
+}
+
+void director_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(mail_hosts_get_tags(dir->mail_hosts), tag) {
+ director_user_tag_killed_everywhere(dir, tag, src, orig_src,
+ username_hash);
+ }
+}
+
+static void director_state_callback_timeout(struct director *dir)
+{
+ timeout_remove(&dir->to_callback);
+ dir->state_change_callback(dir);
+}
+
+void director_set_state_changed(struct director *dir)
+{
+ /* we may get called to here from various places. use a timeout to
+ make sure the state callback is called with a clean state. */
+ if (dir->to_callback == NULL) {
+ dir->to_callback =
+ timeout_add(0, director_state_callback_timeout, dir);
+ }
+}
+
+void director_update_send(struct director *dir, struct director_host *src,
+ const char *cmd)
+{
+ director_update_send_version(dir, src, 0, cmd);
+}
+
+void director_update_send_version(struct director *dir,
+ struct director_host *src,
+ unsigned int min_version, const char *cmd)
+{
+ struct director_connection *conn;
+
+ i_assert(src != NULL);
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) != src &&
+ director_connection_get_minor_version(conn) >= min_version)
+ director_connection_send(conn, cmd);
+ }
+}
+
+static void director_user_freed(struct user *user)
+{
+ if (user->kill_ctx != NULL) {
+ /* director_user_expire is very short. user expired before
+ moving the user finished or timed out. */
+ if (user->kill_ctx->callback_pending) {
+ /* kill_ctx is used as a callback parameter.
+ only remove the timeout and finish the free later. */
+ timeout_remove(&user->kill_ctx->to_move);
+ } else {
+ director_user_move_free(user);
+ }
+ }
+}
+
+struct director *
+director_init(const struct director_settings *set,
+ const struct ip_addr *listen_ip, in_port_t listen_port,
+ director_state_change_callback_t *callback,
+ director_kick_callback_t *kick_callback)
+{
+ struct director *dir;
+
+ dir = i_new(struct director, 1);
+ dir->set = set;
+ dir->self_port = listen_port;
+ dir->self_ip = *listen_ip;
+ dir->state_change_callback = callback;
+ dir->kick_callback = kick_callback;
+ dir->event = event_create(NULL);
+ event_add_category(dir->event, &event_category_director);
+ i_array_init(&dir->dir_hosts, 16);
+ i_array_init(&dir->pending_requests, 16);
+ i_array_init(&dir->connections, 8);
+ dir->mail_hosts = mail_hosts_init(dir, set->director_user_expire,
+ director_user_freed);
+
+ dir->ipc_proxy = ipc_client_init(DIRECTOR_IPC_PROXY_PATH);
+ dir->ring_min_version = DIRECTOR_VERSION_MINOR;
+ return dir;
+}
+
+void director_deinit(struct director **_dir)
+{
+ struct director *dir = *_dir;
+ struct director_host *const *hostp, *host;
+ struct director_connection *conn, *const *connp;
+
+ *_dir = NULL;
+
+ while (array_count(&dir->connections) > 0) {
+ connp = array_front(&dir->connections);
+ conn = *connp;
+ director_connection_deinit(&conn, "Shutting down");
+ }
+
+ mail_hosts_deinit(&dir->mail_hosts);
+ mail_hosts_deinit(&dir->orig_config_hosts);
+
+ ipc_client_deinit(&dir->ipc_proxy);
+ timeout_remove(&dir->to_reconnect);
+ timeout_remove(&dir->to_handshake_warning);
+ timeout_remove(&dir->to_request);
+ timeout_remove(&dir->to_sync);
+ timeout_remove(&dir->to_remove_dirs);
+ timeout_remove(&dir->to_callback);
+ while (array_count(&dir->dir_hosts) > 0) {
+ hostp = array_front(&dir->dir_hosts);
+ host = *hostp;
+ director_host_free(&host);
+ }
+ array_free(&dir->pending_requests);
+ array_free(&dir->dir_hosts);
+ array_free(&dir->connections);
+ event_unref(&dir->event);
+ i_free(dir);
+}
+
+struct director_user_iter {
+ struct director *dir;
+ unsigned int tag_idx;
+ struct user_directory_iter *user_iter;
+ bool iter_until_current_tail;
+};
+
+struct director_user_iter *
+director_iterate_users_init(struct director *dir, bool iter_until_current_tail)
+{
+ struct director_user_iter *iter = i_new(struct director_user_iter, 1);
+ iter->dir = dir;
+ iter->iter_until_current_tail = iter_until_current_tail;
+ return iter;
+}
+
+struct user *director_iterate_users_next(struct director_user_iter *iter)
+{
+ const ARRAY_TYPE(mail_tag) *tags;
+ struct user *user;
+
+ i_assert(iter != NULL);
+
+ if (iter->user_iter == NULL) {
+ tags = mail_hosts_get_tags(iter->dir->mail_hosts);
+ if (iter->tag_idx >= array_count(tags))
+ return NULL;
+ struct mail_tag *tag = array_idx_elem(tags, iter->tag_idx);
+ iter->user_iter = user_directory_iter_init(tag->users,
+ iter->iter_until_current_tail);
+ }
+ user = user_directory_iter_next(iter->user_iter);
+ if (user == NULL) {
+ user_directory_iter_deinit(&iter->user_iter);
+ iter->tag_idx++;
+ return director_iterate_users_next(iter);
+ } else
+ return user;
+}
+
+void director_iterate_users_deinit(struct director_user_iter **_iter)
+{
+ i_assert(_iter != NULL && *_iter != NULL);
+ struct director_user_iter *iter = *_iter;
+ *_iter = NULL;
+ if (iter->user_iter != NULL)
+ user_directory_iter_deinit(&iter->user_iter);
+ i_free(iter);
+}
+
+bool
+director_get_username_hash(struct director *dir, const char *username,
+ unsigned int *hash_r)
+{
+ const char *error;
+
+ if (mail_user_hash(username, dir->set->director_username_hash, hash_r,
+ &error))
+ return TRUE;
+ e_error(dir->event, "Failed to expand director_username_hash=%s: %s",
+ dir->set->director_username_hash, error);
+ return FALSE;
+}
+
+void directors_init(void)
+{
+ user_move_throttle =
+ log_throttle_init(&director_log_throttle_settings,
+ director_user_move_throttled, NULL);
+ user_kill_fail_throttle =
+ log_throttle_init(&director_log_throttle_settings,
+ director_user_kill_fail_throttled, NULL);
+}
+
+void directors_deinit(void)
+{
+ log_throttle_deinit(&user_move_throttle);
+ log_throttle_deinit(&user_kill_fail_throttle);
+}
diff --git a/src/director/director.h b/src/director/director.h
new file mode 100644
index 0000000..8f2d2a1
--- /dev/null
+++ b/src/director/director.h
@@ -0,0 +1,274 @@
+#ifndef DIRECTOR_H
+#define DIRECTOR_H
+
+#include "net.h"
+#include "director-settings.h"
+
+#define DIRECTOR_VERSION_NAME "director"
+#define DIRECTOR_VERSION_MAJOR 1
+#define DIRECTOR_VERSION_MINOR 9
+
+/* weak users supported in protocol */
+#define DIRECTOR_VERSION_WEAK_USERS 1
+/* director ring remove supported */
+#define DIRECTOR_VERSION_RING_REMOVE 2
+/* quit reason supported */
+#define DIRECTOR_VERSION_QUIT 3
+/* user-kick supported */
+#define DIRECTOR_VERSION_USER_KICK 4
+/* options supported in handshake */
+#define DIRECTOR_VERSION_OPTIONS 5
+/* user tags supported */
+#define DIRECTOR_VERSION_TAGS 5
+/* up/down state is tracked */
+#define DIRECTOR_VERSION_UPDOWN 6
+/* user tag version 2 supported */
+#define DIRECTOR_VERSION_TAGS_V2 7
+/* user-kick-alt supported */
+#define DIRECTOR_VERSION_USER_KICK_ALT 8
+/* Users are sent as "U" command in handshake */
+#define DIRECTOR_VERSION_HANDSHAKE_U_CMD 9
+/* USER event with timestamp supported */
+#define DIRECTOR_VERSION_USER_TIMESTAMP 9
+
+/* Minimum time between even attempting to communicate with a director that
+ failed due to a protocol error. */
+#define DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS 60
+
+struct director;
+struct mail_host;
+struct user;
+struct director_user_init;
+
+enum user_kill_state {
+ /* User isn't being killed */
+ USER_KILL_STATE_NONE,
+ /* We're still killing the user's connections */
+ USER_KILL_STATE_KILLING,
+ /* Like above, but our left side already announced it was finished
+ with killing its user connections */
+ USER_KILL_STATE_KILLING_NOTIFY_RECEIVED,
+ /* We're done killing, but we have to wait for the left side to
+ finish killing its user connections before sending USER-KILLED to
+ our right side */
+ USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY,
+ /* We're done killing, but waiting for USER-KILLED-EVERYWHERE
+ notification until this state gets reset. */
+ USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE,
+ /* Waiting for the flush socket to finish. */
+ USER_KILL_STATE_FLUSHING,
+ /* Wait for a while for the user connections to actually die. Note that
+ only at this stage we can be sure that all the directors know about
+ the user move (although it could be earlier if we added a new
+ USER-MOVED notification). */
+ USER_KILL_STATE_DELAY
+ /* NOTE: remember to update also user_kill_state_names[] */
+};
+extern const char *user_kill_state_names[USER_KILL_STATE_DELAY+1];
+
+typedef void director_state_change_callback_t(struct director *dir);
+typedef director_state_change_callback_t director_kick_callback_t;
+
+/* When a user gets freed, the kill_ctx may still be left alive. It's also
+ possible for the user to come back, in which case the kill_ctx is usually
+ NULL, but another kill could have also started. The previous kill_ctx is
+ valid only if it matches the current user's kill_ctx. */
+#define DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx) \
+ ((user) != NULL && (user)->kill_ctx == ctx)
+
+struct director_kill_context {
+ struct director *dir;
+ struct mail_tag *tag;
+ unsigned int username_hash;
+ struct ip_addr old_host_ip;
+ unsigned int old_host_vhost_count;
+ bool old_host_down;
+ bool kill_is_self_initiated;
+ bool callback_pending;
+
+ enum user_kill_state kill_state;
+ /* Move timeout to make sure user's connections won't silently hang
+ indefinitely if there is some trouble moving it. */
+ struct timeout *to_move;
+ /* IPC command to kick the user */
+ struct ipc_client_cmd *ipc_cmd;
+
+ /* these are set only for director_flush_socket handling: */
+ struct ip_addr host_ip;
+ struct program_client *pclient;
+ struct ostream *reply;
+ char *socket_path;
+};
+
+struct director {
+ struct event *event;
+ const struct director_settings *set;
+
+ /* IP and port of this director. self_host->ip/port must equal these. */
+ struct ip_addr self_ip;
+ in_port_t self_port;
+
+ in_port_t test_port;
+
+ struct director_host *self_host;
+ /* left and right connections are set only after they have finished
+ handshaking. until then they're in the connections list, although
+ updates are still sent to them during handshaking if the USER list
+ is long. */
+ struct director_connection *left, *right;
+ /* all director connections */
+ ARRAY(struct director_connection *) connections;
+ struct timeout *to_reconnect;
+ struct timeout *to_sync;
+ struct timeout *to_callback;
+
+ /* current mail hosts */
+ struct mail_host_list *mail_hosts;
+ /* original mail hosts configured in config file.
+ this is used only for doveadm lookups */
+ struct mail_host_list *orig_config_hosts;
+ /* Number of users currently being moved */
+ unsigned int users_moving_count;
+ /* Number of users currently being kicked */
+ unsigned int users_kicking_count;
+ /* Number of requests currently delayed */
+ unsigned int requests_delayed_count;
+
+ /* these requests are waiting for directors to be in synced */
+ ARRAY(struct director_request *) pending_requests;
+ struct timeout *to_request;
+ struct timeout *to_handshake_warning;
+
+ director_state_change_callback_t *state_change_callback;
+ director_kick_callback_t *kick_callback;
+
+ /* director hosts are sorted by IP (and port) */
+ ARRAY(struct director_host *) dir_hosts;
+ struct timeout *to_remove_dirs;
+
+ struct ipc_client *ipc_proxy;
+ unsigned int sync_seq;
+ unsigned int ring_change_counter;
+ unsigned int last_sync_sent_ring_change_counter;
+ /* Timestamp when the last SYNC was initiated by us */
+ struct timeval last_sync_start_time;
+ /* the lowest minor version supported by the ring */
+ unsigned int ring_min_version;
+ /* Timestamp when ring became synced or unsynced the last time */
+ time_t ring_last_sync_time;
+ /* How many milliseconds it took for the last SYNC to travel through
+ the ring. */
+ unsigned int last_sync_msecs;
+
+ time_t ring_first_alone;
+
+ uint64_t num_requests, num_incoming_requests;
+ uint64_t ring_traffic_input, ring_traffic_output;
+
+ /* director ring handshaking is complete.
+ director can start serving clients. */
+ bool ring_handshaked:1;
+ bool ring_handshake_warning_sent:1;
+ bool ring_synced:1;
+ bool sync_frozen:1;
+ bool sync_pending:1;
+};
+
+/* Create a new director. If listen_ip specifies an actual IP, it's used with
+ listen_port for finding ourself from the director_servers setting.
+ listen_port is used regardless by director_host_add_from_string() for hosts
+ without specified port. */
+struct director *
+director_init(const struct director_settings *set,
+ const struct ip_addr *listen_ip, in_port_t listen_port,
+ director_state_change_callback_t *callback,
+ director_kick_callback_t *kick_callback);
+void director_deinit(struct director **dir);
+void director_find_self(struct director *dir);
+
+/* Start connecting to other directors */
+void director_connect(struct director *dir, const char *reason);
+
+void director_set_ring_handshaked(struct director *dir);
+void director_set_ring_synced(struct director *dir);
+void director_set_ring_unsynced(struct director *dir);
+void director_set_state_changed(struct director *dir);
+void director_sync_send(struct director *dir, struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash);
+bool director_resend_sync(struct director *dir);
+
+void director_notify_ring_added(struct director_host *added_host,
+ struct director_host *src, bool log);
+void director_ring_remove(struct director_host *removed_host,
+ struct director_host *src);
+
+void director_update_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(3);
+void director_resend_hosts(struct director *dir);
+void director_remove_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(2, 3);
+void director_flush_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(3);
+void director_update_user(struct director *dir, struct director_host *src,
+ struct user *user);
+void director_update_user_weak(struct director *dir, struct director_host *src,
+ struct director_connection *src_conn,
+ struct director_host *orig_src,
+ struct user *user) ATTR_NULL(3);
+void director_kill_user(struct director *dir, struct director_host *src,
+ struct user *user, struct mail_tag *tag,
+ struct mail_host *old_host, bool forced_kick);
+void director_move_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash, struct mail_host *host)
+ ATTR_NULL(3);
+void director_kick_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src, const char *username)
+ ATTR_NULL(3);
+void director_kick_user_alt(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ const char *field, const char *value)
+ ATTR_NULL(3);
+void director_kick_user_hash(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash,
+ const struct ip_addr *except_ip)
+ ATTR_NULL(3);
+void director_user_killed(struct director *dir, unsigned int username_hash);
+void director_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash) ATTR_NULL(3);
+void director_user_weak(struct director *dir, struct user *user);
+
+void director_sync_freeze(struct director *dir);
+void director_sync_thaw(struct director *dir);
+
+/* Send data to all directors using both left and right connections
+ (unless they're the same). */
+void director_update_send(struct director *dir, struct director_host *src,
+ const char *cmd);
+void director_update_send_version(struct director *dir,
+ struct director_host *src,
+ unsigned int min_version, const char *cmd);
+
+int director_connect_host(struct director *dir, struct director_host *host,
+ const char *reason);
+
+bool
+director_get_username_hash(struct director *dir, const char *username,
+ unsigned int *hash_r);
+
+void directors_init(void);
+void directors_deinit(void);
+
+struct director_user_iter *
+director_iterate_users_init(struct director *dir, bool iter_until_current_tail);
+struct user *director_iterate_users_next(struct director_user_iter *iter);
+void director_iterate_users_deinit(struct director_user_iter **_iter);
+
+#endif
diff --git a/src/director/doveadm-connection.c b/src/director/doveadm-connection.c
new file mode 100644
index 0000000..c840536
--- /dev/null
+++ b/src/director/doveadm-connection.c
@@ -0,0 +1,1196 @@
+/* Copyright (c) 2010-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 "str.h"
+#include "strescape.h"
+#include "llist.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "director-connection.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+#define DOVEADM_PROTOCOL_VERSION_MAJOR 1
+#define DOVEADM_HANDSHAKE "VERSION\tdirector-doveadm\t1\t0\n"
+
+#define MAX_VALID_VHOST_COUNT 1000
+
+#define DOVEADM_CONNECTION_RING_SYNC_TIMEOUT_MSECS (30*1000)
+
+enum doveadm_director_cmd_ret {
+ DOVEADM_DIRECTOR_CMD_RET_FAIL = -1,
+ DOVEADM_DIRECTOR_CMD_RET_UNFINISHED = 0,
+ DOVEADM_DIRECTOR_CMD_RET_OK = 1,
+ DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK,
+};
+
+enum doveadm_director_cmd_flag {
+ DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC = 0x01,
+};
+
+typedef void
+doveadm_connection_ring_sync_callback_t(struct doveadm_connection *);
+
+struct director_reset_cmd {
+ struct director_reset_cmd *prev, *next;
+
+ struct director *dir;
+ struct doveadm_connection *_conn;
+ struct timeval start_time;
+
+ struct director_user_iter *iter;
+ unsigned int host_start_idx, host_idx, hosts_count;
+ unsigned int max_moving_users;
+ unsigned int reset_count;
+ bool users_killed;
+};
+
+struct director_kick_cmd {
+ struct director_kick_cmd *prev, *next;
+
+ struct doveadm_connection *_conn;
+ struct director *dir;
+ char *mask, *field, *value;
+ bool alt:1;
+};
+
+struct doveadm_connection {
+ struct doveadm_connection *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct director *dir;
+
+ struct timeout *to_ring_sync_abort;
+ struct director_reset_cmd *reset_cmd;
+ struct director_kick_cmd *kick_cmd;
+ doveadm_connection_ring_sync_callback_t *ring_sync_callback;
+
+ const char **cmd_pending_args;
+ unsigned int cmd_pending_idx;
+
+ bool handshaked:1;
+};
+
+static struct doveadm_connection *doveadm_connections;
+static struct doveadm_connection *doveadm_ring_sync_pending_connections;
+static struct director_reset_cmd *reset_cmds = NULL;
+static struct director_kick_cmd *kick_cmds = NULL;
+
+static void doveadm_connection_set_io(struct doveadm_connection *conn);
+static void doveadm_connection_deinit(struct doveadm_connection **_conn);
+static void
+doveadm_connection_ring_sync_list_move(struct doveadm_connection *conn);
+static void doveadm_connection_cmd_run_synced(struct doveadm_connection *conn);
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_list(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct mail_host *host;
+ string_t *str = t_str_new(1024);
+
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ str_printfa(str, "%s\t%u\t%u\t",
+ host->ip_str, host->vhost_count,
+ host->user_count);
+ str_append_tabescaped(str, mail_host_get_tag(host));
+ str_printfa(str, "\t%c\t%ld", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ str_append_c(str, '\n');
+ }
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_list_removed(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct mail_host_list *orig_hosts_list;
+ struct mail_host *const *orig_hosts, *const *cur_hosts;
+ unsigned int i, j, orig_hosts_count, cur_hosts_count;
+ string_t *str = t_str_new(1024);
+ int ret;
+
+ orig_hosts_list = mail_hosts_init(conn->dir,
+ conn->dir->set->director_user_expire,
+ NULL);
+ (void)mail_hosts_parse_and_add(orig_hosts_list,
+ conn->dir->set->director_mail_servers);
+
+ orig_hosts = array_get(mail_hosts_get(orig_hosts_list),
+ &orig_hosts_count);
+ cur_hosts = array_get(mail_hosts_get(conn->dir->mail_hosts),
+ &cur_hosts_count);
+
+ /* the hosts are sorted by IP */
+ for (i = j = 0; i < orig_hosts_count && j < cur_hosts_count; ) {
+ ret = net_ip_cmp(&orig_hosts[i]->ip, &cur_hosts[j]->ip);
+ if (ret == 0)
+ i++, j++;
+ else if (ret > 0)
+ j++;
+ else {
+ str_printfa(str, "%s\n", orig_hosts[i]->ip_str);
+ i++;
+ }
+ }
+ for (; i < orig_hosts_count; i++)
+ str_printfa(str, "%s\n", orig_hosts[i]->ip_str);
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+
+ mail_hosts_deinit(&orig_hosts_list);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static void
+doveadm_director_host_append_status(const struct director_host *host,
+ const char *type, string_t *str)
+{
+ time_t last_failed = I_MAX(host->last_network_failure,
+ host->last_protocol_failure);
+ str_printfa(str, "%s\t%u\t%s\t%"PRIdTIME_T"\t",
+ host->ip_str, host->port, type,
+ last_failed);
+}
+
+static void doveadm_director_append_status(struct director *dir, string_t *str)
+{
+ if (!dir->ring_handshaked)
+ str_append(str, "ring handshaking");
+ else if (dir->ring_synced)
+ str_append(str, "ring synced");
+ else {
+ str_printfa(str, "ring syncing - last sync %d secs ago",
+ (int)(ioloop_time - dir->ring_last_sync_time));
+ }
+ str_printfa(str, "\t%u", dir->last_sync_msecs);
+}
+
+static void
+doveadm_director_connection_append_status(struct director_connection *conn,
+ string_t *str)
+{
+ struct director_connection_status status;
+
+ director_connection_get_status(conn, &status);
+ if (!director_connection_is_handshaked(conn)) {
+ str_append(str, "handshaking - ");
+ if (director_connection_is_incoming(conn))
+ str_printfa(str, "%u USERs received", status.handshake_users_received);
+ else
+ str_printfa(str, "%u USERs sent", status.handshake_users_sent);
+ } else if (director_connection_is_synced(conn))
+ str_append(str, "synced");
+ else
+ str_append(str, "syncing");
+
+ str_printfa(str, "\t%u\t%"PRIuUOFF_T"\t%"PRIuUOFF_T"\t%zu\t%zu\t"
+ "%"PRIdTIME_T"\t%"PRIdTIME_T, status.last_ping_msecs,
+ status.bytes_read, status.bytes_sent,
+ status.bytes_buffered, status.peak_bytes_buffered,
+ status.last_input.tv_sec, status.last_output.tv_sec);
+}
+
+static void
+doveadm_director_connection_append(struct director *dir,
+ struct director_connection *conn,
+ const struct director_host *host,
+ string_t *str)
+{
+ const char *type;
+
+ if (conn == dir->left)
+ type = "left";
+ else if (conn == dir->right)
+ type = "right";
+ else if (director_connection_is_incoming(conn))
+ type = "in";
+ else
+ type = "out";
+
+ if (host != NULL)
+ doveadm_director_host_append_status(host, type, str);
+ doveadm_director_connection_append_status(conn, str);
+ str_append_c(str, '\n');
+}
+
+static void
+doveadm_director_host_append(struct director *dir,
+ const struct director_host *host, string_t *str)
+{
+ const char *type;
+
+ if (host->removed)
+ type = "removed";
+ else if (dir->self_host == host)
+ type = "self";
+ else
+ type = "";
+
+ doveadm_director_host_append_status(host, type, str);
+ if (dir->self_host == host)
+ doveadm_director_append_status(dir, str);
+ str_append_c(str, '\n');
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_list(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ string_t *str = t_str_new(1024);
+ struct director_connection *dir_conn;
+ ARRAY(struct director_host *) hosts;
+
+ t_array_init(&hosts, array_count(&dir->dir_hosts));
+ array_append_array(&hosts, &dir->dir_hosts);
+ array_sort(&hosts, director_host_cmp_p);
+
+ /* first show incoming connections that have no known host yet */
+ array_foreach_elem(&dir->connections, dir_conn) {
+ if (director_connection_get_host(dir_conn) == NULL)
+ doveadm_director_connection_append(dir, dir_conn, NULL, str);
+ }
+
+ /* show other connections and host without connections sorted by host */
+ array_foreach_elem(&hosts, host) {
+ bool have_connections = FALSE;
+
+ array_foreach_elem(&dir->connections, dir_conn) {
+ const struct director_host *conn_host =
+ director_connection_get_host(dir_conn);
+ if (conn_host != host)
+ continue;
+ have_connections = TRUE;
+ doveadm_director_connection_append(dir, dir_conn, host, str);
+ }
+ if (!have_connections)
+ doveadm_director_host_append(dir, host, str);
+ }
+
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_add(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port = conn->dir->self_port;
+
+ if (args[0] == NULL ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ (args[1] != NULL && net_str2port(args[1], &port) < 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid DIRECTOR-ADD parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (director_host_lookup(conn->dir, &ip, port) == NULL) {
+ host = director_host_add(conn->dir, &ip, port);
+ director_notify_ring_added(host, conn->dir->self_host, TRUE);
+ }
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_remove(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port = 0;
+
+ if (args[0] == NULL ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ (args[1] != NULL && net_str2port(args[1], &port) < 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid DIRECTOR-REMOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ host = port != 0 ?
+ director_host_lookup(conn->dir, &ip, port) :
+ director_host_lookup_ip(conn->dir, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_ring_remove(host, conn->dir->self_host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_set_or_update(struct doveadm_connection *conn,
+ const char *const *args, bool update)
+{
+ struct director *dir = conn->dir;
+ const char *ip_str, *tag = "";
+ struct mail_host *host;
+ struct ip_addr ip;
+ unsigned int vhost_count = UINT_MAX;
+
+ ip_str = args[0];
+ if (ip_str != NULL) {
+ tag = strchr(ip_str, '@');
+ if (tag == NULL)
+ tag = "";
+ else
+ ip_str = t_strdup_until(ip_str, tag++);
+ }
+ if (ip_str == NULL || net_addr2ip(ip_str, &ip) < 0 ||
+ (args[1] != NULL && str_to_uint(args[1], &vhost_count) < 0) ||
+ (args[1] == NULL && update)) {
+ e_error(conn->dir->event, "doveadm sent invalid %s parameters",
+ update ? "HOST-UPDATE" : "HOST-SET");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ if (vhost_count > MAX_VALID_VHOST_COUNT && vhost_count != UINT_MAX) {
+ o_stream_nsend_str(conn->output, "vhost count too large\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ host = mail_host_lookup(dir->mail_hosts, &ip);
+ if (host == NULL) {
+ if (update) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ host = mail_host_add_ip(dir->mail_hosts, &ip, tag);
+ } else if (tag[0] != '\0' && strcmp(mail_host_get_tag(host), tag) != 0) {
+ o_stream_nsend_str(conn->output, "host tag can't be changed\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else if (host->desynced) {
+ o_stream_nsend_str(conn->output,
+ "host is already being updated - try again later\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ if (vhost_count != UINT_MAX)
+ mail_host_set_vhost_count(host, vhost_count, "doveadm: ");
+ /* NOTE: we don't support changing a tag for an existing host.
+ it needs to be removed first. otherwise it would be a bit ugly to
+ handle. */
+ director_update_host(dir, dir->self_host, NULL, host);
+
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_set(struct doveadm_connection *conn, const char *const *args)
+{
+ return doveadm_cmd_host_set_or_update(conn, args, FALSE);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_update(struct doveadm_connection *conn, const char *const *args)
+{
+ return doveadm_cmd_host_set_or_update(conn, args, TRUE);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_updown(struct doveadm_connection *conn, bool down,
+ const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid %s parameters: %s",
+ down ? "HOST-DOWN" : "HOST-UP",
+ args[0] == NULL ? "" : args[0]);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ if (host->down == down) {
+ o_stream_nsend_str(conn->output, "OK\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else if (host->desynced) {
+ o_stream_nsend_str(conn->output,
+ "host is already being updated - try again later\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ mail_host_set_down(host, down, ioloop_time, "doveadm: ");
+ director_update_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_up(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ return doveadm_cmd_host_updown(conn, FALSE, args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_down(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ return doveadm_cmd_host_updown(conn, TRUE, args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_remove(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-REMOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_remove_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static void
+doveadm_cmd_host_flush_all(struct doveadm_connection *conn)
+{
+ struct mail_host *host;
+ unsigned int total_user_count = 0;
+
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ total_user_count += host->user_count;
+ director_flush_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ }
+ e_warning(conn->dir->event,
+ "Flushed all backend hosts with %u users. This is an unsafe "
+ "operation and may cause the same users to end up in multiple backends.",
+ total_user_count);
+ o_stream_nsend(conn->output, "OK\n", 3);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_flush(struct doveadm_connection *conn, const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || args[0][0] == '\0') {
+ doveadm_cmd_host_flush_all(conn);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-FLUSH parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_flush_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static void doveadm_reset_cmd_free(struct director_reset_cmd *cmd)
+{
+ DLLIST_REMOVE(&reset_cmds, cmd);
+
+ if (cmd->iter != NULL)
+ director_iterate_users_deinit(&cmd->iter);
+ if (cmd->_conn != NULL)
+ cmd->_conn->reset_cmd = NULL;
+ i_free(cmd);
+}
+
+static bool
+director_host_reset_users(struct director_reset_cmd *cmd,
+ struct mail_host *host)
+{
+ struct director *dir = cmd->dir;
+ struct user *user;
+ struct mail_host *new_host;
+
+ if (dir->users_moving_count >= cmd->max_moving_users)
+ return FALSE;
+
+ if (dir->right != NULL)
+ director_connection_cork(dir->right);
+
+ if (cmd->iter == NULL) {
+ cmd->iter = director_iterate_users_init(dir, FALSE);
+ cmd->users_killed = FALSE;
+ }
+
+ while ((user = director_iterate_users_next(cmd->iter)) != NULL) {
+ if (user->host != host)
+ continue;
+
+ new_host = mail_host_get_by_hash(dir->mail_hosts,
+ user->username_hash,
+ mail_host_get_tag(host));
+ if (new_host != host) T_BEGIN {
+ if (new_host != NULL) {
+ director_move_user(dir, dir->self_host, NULL,
+ user->username_hash, new_host);
+ } else {
+ /* there are no more available backends.
+ kick the user instead. */
+ director_kill_user(dir, dir->self_host, user,
+ user->host->tag, user->host,
+ TRUE);
+ cmd->users_killed = TRUE;
+ }
+ cmd->reset_count++;
+ } T_END;
+ if (dir->users_moving_count >= cmd->max_moving_users)
+ break;
+ }
+ if (user == NULL) {
+ int msecs = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ e_info(dir->event,
+ "Moved %u users in %u hosts in %u.%03u secs (max parallel=%u)",
+ cmd->reset_count, cmd->hosts_count - cmd->host_start_idx,
+ msecs / 1000, msecs % 1000, cmd->max_moving_users);
+ director_iterate_users_deinit(&cmd->iter);
+ if (cmd->users_killed) {
+ /* no more backends. we already sent kills. now remove
+ the users entirely from the host. */
+ director_flush_host(dir, dir->self_host, NULL, host);
+ }
+ }
+ if (dir->right != NULL)
+ director_connection_uncork(dir->right);
+ return user == NULL;
+}
+
+static bool
+director_reset_cmd_run(struct director_reset_cmd *cmd)
+{
+ struct mail_host *const *hosts;
+ unsigned int count;
+
+ hosts = array_get(mail_hosts_get(cmd->dir->mail_hosts), &count);
+ if (count > cmd->hosts_count)
+ count = cmd->hosts_count;
+ while (cmd->host_idx < count) {
+ if (!director_host_reset_users(cmd, hosts[cmd->host_idx]))
+ return FALSE;
+ cmd->host_idx++;
+ }
+ if (cmd->_conn != NULL) {
+ struct doveadm_connection *conn = cmd->_conn;
+
+ o_stream_nsend(conn->output, "OK\n", 3);
+ if (conn->io == NULL)
+ doveadm_connection_set_io(conn);
+ }
+ doveadm_reset_cmd_free(cmd);
+ return TRUE;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_reset_users(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_reset_cmd *cmd;
+ struct ip_addr ip;
+ struct mail_host *const *hosts;
+ unsigned int i = 0, count;
+ unsigned int max_moving_users =
+ conn->dir->set->director_max_parallel_moves;
+
+ if (args[0] != NULL && args[1] != NULL &&
+ (str_to_uint(args[1], &max_moving_users) < 0 ||
+ max_moving_users == 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-RESET-USERS parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ hosts = array_get(mail_hosts_get(conn->dir->mail_hosts), &count);
+ if (args[0] != NULL && args[0][0] != '\0') {
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-RESET-USERS ip: %s",
+ args[0]);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (net_ip_compare(&hosts[i]->ip, &ip))
+ break;
+ }
+ if (i == count) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ count = i+1;
+ }
+
+ conn->reset_cmd = cmd = i_new(struct director_reset_cmd, 1);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+ cmd->max_moving_users = max_moving_users;
+ cmd->host_start_idx = i;
+ cmd->host_idx = i;
+ cmd->hosts_count = count;
+ cmd->start_time = ioloop_timeval;
+ DLLIST_PREPEND(&reset_cmds, cmd);
+
+ if (!director_reset_cmd_run(cmd)) {
+ /* we still have work to do. don't handle any more doveadm
+ input until we're finished. */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ }
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_lookup(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct user *user;
+ struct mail_host *host;
+ const char *username, *tag;
+ unsigned int username_hash;
+ struct mail_tag *mail_tag;
+ string_t *str = t_str_new(256);
+
+ if (args[0] == NULL) {
+ username = "";
+ tag = "";
+ } else {
+ username = args[0];
+ tag = args[1] != NULL ? args[1] : "";
+ }
+ if (str_to_uint(username, &username_hash) < 0) {
+ if (!director_get_username_hash(conn->dir,
+ username, &username_hash)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ }
+
+ /* get user's current host */
+ mail_tag = mail_tag_find(conn->dir->mail_hosts, tag);
+ user = mail_tag == NULL ? NULL :
+ user_directory_lookup(mail_tag->users, username_hash);
+ if (user == NULL)
+ str_append(str, "\t0");
+ else {
+ str_printfa(str, "%s\t%u", user->host->ip_str,
+ user->timestamp +
+ conn->dir->set->director_user_expire);
+ }
+
+ /* get host if it wasn't in user directory */
+ host = mail_host_get_by_hash(conn->dir->mail_hosts, username_hash, tag);
+ if (host == NULL)
+ str_append(str, "\t");
+ else
+ str_printfa(str, "\t%s", host->ip_str);
+
+ /* get host with default configuration */
+ host = mail_host_get_by_hash(conn->dir->orig_config_hosts,
+ username_hash, tag);
+ if (host == NULL)
+ str_append(str, "\t\n");
+ else
+ str_printfa(str, "\t%s\n", host->ip_str);
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_list(struct doveadm_connection *conn, const char *const *args)
+{
+ struct director_user_iter *iter;
+ struct user *user;
+ struct ip_addr ip;
+
+ if (args[0] != NULL && args[0][0] != '\0') {
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-LIST parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ } else {
+ ip.family = 0;
+ }
+
+ iter = director_iterate_users_init(conn->dir, FALSE);
+ while ((user = director_iterate_users_next(iter)) != NULL) {
+ if (ip.family == 0 ||
+ net_ip_compare(&ip, &user->host->ip)) T_BEGIN {
+ unsigned int expire_time = user->timestamp +
+ conn->dir->set->director_user_expire;
+
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "%u\t%u\t%s\n",
+ user->username_hash, expire_time,
+ user->host->ip_str));
+ } T_END;
+ }
+ director_iterate_users_deinit(&iter);
+ o_stream_nsend(conn->output, "\n", 1);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_move(struct doveadm_connection *conn, const char *const *args)
+{
+ unsigned int username_hash;
+ struct user *user;
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ net_addr2ip(args[1], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-MOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (str_to_uint(args[0], &username_hash) < 0) {
+ if (!director_get_username_hash(conn->dir,
+ args[0], &username_hash)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ }
+
+ user = user_directory_lookup(host->tag->users, username_hash);
+ if (user != NULL && USER_IS_BEING_KILLED(user)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (user == NULL || user->host != host) {
+ director_move_user(conn->dir, conn->dir->self_host, NULL,
+ username_hash, host);
+ } else {
+ /* already the correct host. reset the user's timeout. */
+ user_directory_refresh(host->tag->users, user);
+ director_update_user(conn->dir, conn->dir->self_host, user);
+ }
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static void doveadm_kick_cmd_free(struct director_kick_cmd **_cmd)
+{
+ struct director_kick_cmd *cmd = *_cmd;
+ *_cmd = NULL;
+
+ if (cmd->_conn != NULL)
+ cmd->_conn->kick_cmd = NULL;
+
+ i_free(cmd->field);
+ i_free(cmd->value);
+ i_free(cmd->mask);
+ i_free(cmd);
+}
+
+static bool doveadm_cmd_user_kick_run(struct director_kick_cmd *cmd)
+{
+ if (cmd->dir->users_kicking_count >=
+ cmd->dir->set->director_max_parallel_kicks)
+ return FALSE;
+
+ if (cmd->alt)
+ director_kick_user_alt(cmd->dir, cmd->dir->self_host,
+ NULL, cmd->field, cmd->value);
+ else
+ director_kick_user(cmd->dir, cmd->dir->self_host,
+ NULL, cmd->mask);
+ if (cmd->_conn != NULL) {
+ struct doveadm_connection *conn = cmd->_conn;
+
+ o_stream_nsend(conn->output, "OK\n", 3);
+ if (conn->io == NULL)
+ doveadm_connection_set_io(conn);
+ }
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ return TRUE;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_kick(struct doveadm_connection *conn, const char *const *args)
+{
+ struct director_kick_cmd *cmd;
+ bool wait = TRUE;
+
+ if (args[0] == NULL) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-KICK parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (null_strcmp(args[1], "nowait") == 0)
+ wait = FALSE;
+
+ cmd = conn->kick_cmd = i_new(struct director_kick_cmd, 1);
+ cmd->alt = FALSE;
+ cmd->mask = i_strdup(args[0]);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+
+ DLLIST_PREPEND(&kick_cmds, cmd);
+
+ if (!doveadm_cmd_user_kick_run(cmd)) {
+ if (wait) {
+ /* we have work to do, wait until it finishes */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ } else {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ /* need to remove it here */
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ }
+ }
+
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_kick_alt(struct doveadm_connection *conn, const char *const *args)
+{
+ bool wait = TRUE;
+ struct director_kick_cmd *cmd;
+
+ if (str_array_length(args) < 2) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-KICK-ALT parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (null_strcmp(args[2], "nowait") == 0)
+ wait = FALSE;
+
+ conn->kick_cmd = cmd = i_new(struct director_kick_cmd, 1);
+ cmd->alt = TRUE;
+ cmd->field = i_strdup(args[0]);
+ cmd->value = i_strdup(args[1]);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+
+ DLLIST_PREPEND(&kick_cmds, cmd);
+
+ if (!doveadm_cmd_user_kick_run(cmd)) {
+ if (wait) {
+ /* we have work to do, wait until it finishes */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ } else {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ }
+ }
+
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+struct {
+ const char *name;
+ enum doveadm_director_cmd_ret (*cmd)
+ (struct doveadm_connection *conn, const char *const *args);
+ enum doveadm_director_cmd_flag flags;
+} doveadm_director_commands[] = {
+ { "HOST-LIST", doveadm_cmd_host_list, 0 },
+ { "HOST-LIST-REMOVED", doveadm_cmd_host_list_removed, 0 },
+ { "DIRECTOR-LIST", doveadm_cmd_director_list, 0 },
+ { "DIRECTOR-ADD", doveadm_cmd_director_add, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "DIRECTOR-REMOVE", doveadm_cmd_director_remove, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-SET", doveadm_cmd_host_set, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-UPDATE", doveadm_cmd_host_update, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-UP", doveadm_cmd_host_up, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-DOWN", doveadm_cmd_host_down, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-REMOVE", doveadm_cmd_host_remove, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-FLUSH", doveadm_cmd_host_flush, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-RESET-USERS", doveadm_cmd_host_reset_users, 0 },
+ { "USER-LOOKUP", doveadm_cmd_user_lookup, 0 },
+ { "USER-LIST", doveadm_cmd_user_list, 0 },
+ { "USER-MOVE", doveadm_cmd_user_move, 0 },
+ { "USER-KICK", doveadm_cmd_user_kick, 0 },
+ { "USER-KICK-ALT", doveadm_cmd_user_kick_alt, 0 },
+};
+
+static void
+doveadm_connection_ring_sync_timeout(struct doveadm_connection *conn)
+{
+ doveadm_connection_ring_sync_list_move(conn);
+ o_stream_nsend_str(conn->output, "Ring sync timed out\n");
+
+ i_assert(conn->io == NULL);
+ doveadm_connection_set_io(conn);
+ io_set_pending(conn->io);
+
+ i_free_and_null(conn->cmd_pending_args);
+}
+
+static void
+doveadm_connection_set_ring_sync_callback(struct doveadm_connection *conn,
+ doveadm_connection_ring_sync_callback_t *callback)
+{
+ i_assert(conn->ring_sync_callback == NULL);
+ i_assert(conn->to_ring_sync_abort == NULL);
+
+ conn->ring_sync_callback = callback;
+ io_remove(&conn->io);
+ DLLIST_REMOVE(&doveadm_connections, conn);
+ DLLIST_PREPEND(&doveadm_ring_sync_pending_connections, conn);
+ conn->to_ring_sync_abort =
+ timeout_add(DOVEADM_CONNECTION_RING_SYNC_TIMEOUT_MSECS,
+ doveadm_connection_ring_sync_timeout, conn);
+}
+
+static void doveadm_connection_ret_ok(struct doveadm_connection *conn)
+{
+ o_stream_nsend(conn->output, "OK\n", 3);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_connection_cmd_run(struct doveadm_connection *conn,
+ const char *const *args, unsigned int i)
+{
+ enum doveadm_director_cmd_ret ret;
+
+ if ((doveadm_director_commands[i].flags &
+ DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC) != 0 &&
+ !conn->dir->ring_synced) {
+ /* wait for ring to be synced before running the command */
+ conn->cmd_pending_args = p_strarray_dup(default_pool, args);
+ conn->cmd_pending_idx = i;
+ doveadm_connection_set_ring_sync_callback(conn,
+ doveadm_connection_cmd_run_synced);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ }
+
+ ret = doveadm_director_commands[i].cmd(conn, args);
+ if (ret != DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK)
+ return ret;
+ /* Delay sending OK until ring is synced. This way doveadm will know
+ whether the call actually succeeded or not. */
+ if (conn->dir->ring_synced) {
+ /* director is alone */
+ i_assert(conn->dir->right == NULL && conn->dir->left == NULL);
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ doveadm_connection_set_ring_sync_callback(conn, doveadm_connection_ret_ok);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+}
+
+static void doveadm_connection_cmd_run_synced(struct doveadm_connection *conn)
+{
+ const char **args = conn->cmd_pending_args;
+
+ conn->cmd_pending_args = NULL;
+ (void)doveadm_connection_cmd_run(conn, args, conn->cmd_pending_idx);
+ i_free(args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_connection_cmd(struct doveadm_connection *conn, const char *line)
+{
+ const char *cmd, *const *args;
+
+ args = t_strsplit_tabescaped(line);
+ if (args[0] == NULL) {
+ e_error(conn->dir->event, "doveadm sent empty command line");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ cmd = args[0];
+ args++;
+
+ for (unsigned int i = 0; i < N_ELEMENTS(doveadm_director_commands); i++) {
+ if (strcmp(doveadm_director_commands[i].name, cmd) == 0)
+ return doveadm_connection_cmd_run(conn, args, i);
+ }
+ e_error(conn->dir->event, "doveadm sent unknown command: %s", line);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+}
+
+static void doveadm_connection_input(struct doveadm_connection *conn)
+{
+ const char *line;
+ enum doveadm_director_cmd_ret ret = DOVEADM_DIRECTOR_CMD_RET_OK;
+
+ if (!conn->handshaked) {
+ if ((line = i_stream_read_next_line(conn->input)) == NULL) {
+ if (conn->input->eof || conn->input->stream_errno != 0)
+ doveadm_connection_deinit(&conn);
+ return;
+ }
+
+ if (!version_string_verify(line, "director-doveadm",
+ DOVEADM_PROTOCOL_VERSION_MAJOR)) {
+ e_error(conn->dir->event, "doveadm not compatible with this server "
+ "(mixed old and new binaries?)");
+ doveadm_connection_deinit(&conn);
+ return;
+ }
+ conn->handshaked = TRUE;
+ }
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL &&
+ ret == DOVEADM_DIRECTOR_CMD_RET_OK) {
+ T_BEGIN {
+ ret = doveadm_connection_cmd(conn, line);
+ } T_END;
+ }
+ /* Delay deinit if io was removed, even if the client
+ already disconnected. */
+ if (conn->io != NULL &&
+ (conn->input->eof || conn->input->stream_errno != 0 ||
+ ret == DOVEADM_DIRECTOR_CMD_RET_FAIL))
+ doveadm_connection_deinit(&conn);
+}
+
+static void doveadm_connection_set_io(struct doveadm_connection *conn)
+{
+ conn->io = io_add(conn->fd, IO_READ, doveadm_connection_input, conn);
+}
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd)
+{
+ struct doveadm_connection *conn;
+
+ conn = i_new(struct doveadm_connection, 1);
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->input = i_stream_create_fd(conn->fd, 1024);
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ doveadm_connection_set_io(conn);
+ o_stream_nsend_str(conn->output, DOVEADM_HANDSHAKE);
+
+ DLLIST_PREPEND(&doveadm_connections, conn);
+ return conn;
+}
+
+static void doveadm_connection_deinit(struct doveadm_connection **_conn)
+{
+ struct doveadm_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->to_ring_sync_abort == NULL);
+
+ if (conn->reset_cmd != NULL) {
+ /* finish the move even if doveadm disconnected */
+ conn->reset_cmd->_conn = NULL;
+ }
+ if (conn->kick_cmd != NULL) {
+ /* finish the kick even if doveadm disconnected */
+ conn->kick_cmd->_conn = NULL;
+ }
+
+ DLLIST_REMOVE(&doveadm_connections, conn);
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(doveadm connection) failed: %m");
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+doveadm_connection_ring_sync_list_move(struct doveadm_connection *conn)
+{
+ timeout_remove(&conn->to_ring_sync_abort);
+ DLLIST_REMOVE(&doveadm_ring_sync_pending_connections, conn);
+ DLLIST_PREPEND(&doveadm_connections, conn);
+}
+
+void doveadm_connections_deinit(void)
+{
+ while (reset_cmds != NULL)
+ doveadm_reset_cmd_free(reset_cmds);
+
+ unsigned int pending_count = 0;
+ while (doveadm_ring_sync_pending_connections != NULL) {
+ doveadm_connection_ring_sync_list_move(doveadm_ring_sync_pending_connections);
+ pending_count++;
+ }
+ if (pending_count > 0)
+ i_warning("Shutting down while %u doveadm connections were waiting for ring sync", pending_count);
+ while (doveadm_connections != NULL) {
+ struct doveadm_connection *conn = doveadm_connections;
+
+ doveadm_connection_deinit(&conn);
+ }
+}
+
+void doveadm_connections_kick_callback(struct director *dir ATTR_UNUSED)
+{
+ while(kick_cmds != NULL)
+ if (!doveadm_cmd_user_kick_run(kick_cmds))
+ break;
+}
+
+static void doveadm_connections_continue_reset_cmds(void)
+{
+ while (reset_cmds != NULL) {
+ if (!director_reset_cmd_run(reset_cmds))
+ break;
+ }
+}
+
+void doveadm_connections_ring_synced(struct director *dir)
+{
+ /* Note that it's not possible for a single connection to be multiple
+ times in doveadm_ring_sync_pending_connections. This is prevented
+ by removing input IO from the connection whenever it's added to the
+ list. */
+ while (doveadm_ring_sync_pending_connections != NULL &&
+ dir->ring_synced) {
+ struct doveadm_connection *conn =
+ doveadm_ring_sync_pending_connections;
+ doveadm_connection_ring_sync_callback_t *callback =
+ conn->ring_sync_callback;
+
+ conn->ring_sync_callback = NULL;
+ doveadm_connection_ring_sync_list_move(conn);
+ doveadm_connection_set_io(conn);
+ io_set_pending(conn->io);
+ callback(conn);
+ }
+ if (dir->ring_synced)
+ doveadm_connections_continue_reset_cmds();
+}
diff --git a/src/director/doveadm-connection.h b/src/director/doveadm-connection.h
new file mode 100644
index 0000000..04904b6
--- /dev/null
+++ b/src/director/doveadm-connection.h
@@ -0,0 +1,13 @@
+#ifndef DOVEADM_CONNECTION_H
+#define DOVEADM_CONNECTION_H
+
+struct director;
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd);
+void doveadm_connections_deinit(void);
+
+void doveadm_connections_kick_callback(struct director *dir);
+void doveadm_connections_ring_synced(struct director *dir);
+
+#endif
diff --git a/src/director/login-connection.c b/src/director/login-connection.c
new file mode 100644
index 0000000..8156078
--- /dev/null
+++ b/src/director/login-connection.c
@@ -0,0 +1,346 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "director.h"
+#include "director-request.h"
+#include "mail-host.h"
+#include "auth-client-interface.h"
+#include "auth-connection.h"
+#include "login-connection.h"
+
+#include <unistd.h>
+
+#define AUTHREPLY_PROTOCOL_MAJOR_VERSION 1
+#define AUTHREPLY_PROTOCOL_MINOR_VERSION 0
+
+struct login_connection {
+ struct login_connection *prev, *next;
+
+ int refcount;
+ enum login_connection_type type;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct auth_connection *auth;
+ struct director *dir;
+
+ bool handshaked:1;
+ bool destroyed:1;
+};
+
+struct login_host_request {
+ struct login_connection *conn;
+ char *line, *username;
+
+ struct ip_addr local_ip;
+ in_port_t local_port;
+ in_port_t dest_port;
+ bool director_proxy_maybe;
+};
+
+static struct login_connection *login_connections;
+
+static void auth_input_line(const char *line, void *context);
+static void login_connection_unref(struct login_connection **_conn);
+
+static void login_connection_input(struct login_connection *conn)
+{
+ struct ostream *output;
+ unsigned char buf[4096];
+ ssize_t ret;
+
+ ret = read(conn->fd, buf, sizeof(buf));
+ if (ret <= 0) {
+ if (ret < 0) {
+ if (errno == EAGAIN)
+ return;
+ if (errno != ECONNRESET)
+ e_error(conn->dir->event, "read(login connection) failed: %m");
+ }
+ login_connection_deinit(&conn);
+ return;
+ }
+ output = auth_connection_get_output(conn->auth);
+ o_stream_nsend(output, buf, ret);
+}
+
+static void login_connection_authreply_input(struct login_connection *conn)
+{
+ bool bail = FALSE;
+ const char *line;
+
+ while (!bail && (line = i_stream_read_next_line(conn->input)) != NULL) T_BEGIN {
+ if (!conn->handshaked) {
+ if (!version_string_verify(line, "director-authreply-client",
+ AUTHREPLY_PROTOCOL_MAJOR_VERSION)) {
+ e_error(conn->dir->event, "authreply client sent invalid handshake: %s", line);
+ login_connection_deinit(&conn);
+ bail = TRUE; /* don't return from within a T_BEGIN {...} T_END */
+ } else {
+ conn->handshaked = TRUE;
+ }
+ } else {
+ auth_input_line(line, conn);
+ }
+ } T_END;
+
+ if (bail)
+ return;
+
+ if (conn->input->eof) {
+ if (conn->input->stream_errno != 0 &&
+ conn->input->stream_errno != ECONNRESET) {
+ e_error(conn->dir->event, "read(authreply connection) failed: %s",
+ i_stream_get_error(conn->input));
+ }
+ login_connection_deinit(&conn);
+ }
+}
+
+static void
+login_connection_send_line(struct login_connection *conn, const char *line)
+{
+ struct const_iovec iov[2];
+
+ if (conn->destroyed)
+ return;
+
+ iov[0].iov_base = line;
+ iov[0].iov_len = strlen(line);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ o_stream_nsendv(conn->output, iov, N_ELEMENTS(iov));
+}
+
+static bool login_host_request_is_self(struct login_host_request *request,
+ const struct ip_addr *dest_ip)
+{
+ if (!net_ip_compare(dest_ip, &request->local_ip))
+ return FALSE;
+ if (request->dest_port != 0 && request->local_port != 0 &&
+ request->dest_port != request->local_port)
+ return FALSE;
+ return TRUE;
+}
+
+static void
+login_host_callback(const struct mail_host *host, const char *hostname,
+ const char *errormsg, void *context)
+{
+ struct login_host_request *request = context;
+ struct director *dir = request->conn->dir;
+ const char *line, *line_params;
+ unsigned int secs;
+
+ if (host == NULL) {
+ if (str_begins(request->line, "OK\t"))
+ line_params = request->line + 3;
+ else if (str_begins(request->line, "PASS\t"))
+ line_params = request->line + 5;
+ else
+ i_panic("BUG: Unexpected line: %s", request->line);
+
+ e_error(dir->event, "director: User %s host lookup failed: %s",
+ request->username, errormsg);
+ line = t_strconcat("FAIL\t", t_strcut(line_params, '\t'),
+ "\tcode="AUTH_CLIENT_FAIL_CODE_TEMPFAIL, NULL);
+ } else if (request->director_proxy_maybe &&
+ login_host_request_is_self(request, &host->ip)) {
+ line = request->line;
+ } else {
+ string_t *str = t_str_new(64);
+ char secs_buf[MAX_INT_STRLEN];
+
+ secs = dir->set->director_user_expire / 2;
+ str_append(str, request->line);
+ str_append(str, "\tproxy_refresh=");
+ str_append(str, dec2str_buf(secs_buf, secs));
+ str_append(str, "\thost=");
+ if (hostname == NULL || hostname[0] == '\0')
+ str_append(str, host->ip_str);
+ else {
+ str_append(str, hostname);
+ str_append(str, "\thostip=");
+ str_append(str, host->ip_str);
+ }
+ line = str_c(str);
+ }
+ login_connection_send_line(request->conn, line);
+
+ login_connection_unref(&request->conn);
+ i_free(request->username);
+ i_free(request->line);
+ i_free(request);
+}
+
+static void auth_input_line(const char *line, void *context)
+{
+ struct login_connection *conn = context;
+ struct login_host_request *request, temp_request;
+ const char *const *args, *line_params, *username = NULL, *tag = "";
+ bool proxy = FALSE, host = FALSE;
+
+ if (line == NULL) {
+ /* auth connection died -> kill also this login connection */
+ login_connection_deinit(&conn);
+ return;
+ }
+ if (conn->type != LOGIN_CONNECTION_TYPE_USERDB &&
+ str_begins(line, "OK\t"))
+ line_params = line + 3;
+ else if (conn->type == LOGIN_CONNECTION_TYPE_USERDB &&
+ str_begins(line, "PASS\t"))
+ line_params = line + 5;
+ else {
+ login_connection_send_line(conn, line);
+ return;
+ }
+
+ /* OK <id> [<parameters>] */
+ args = t_strsplit_tabescaped(line_params);
+ if (*args != NULL) {
+ /* we should always get here, but in case we don't just
+ forward as-is and let login process handle the error. */
+ args++;
+ }
+
+ i_zero(&temp_request);
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "proxy") &&
+ ((*args)[5] == '=' || (*args)[5] == '\0'))
+ proxy = TRUE;
+ else if (str_begins(*args, "host="))
+ host = TRUE;
+ else if (str_begins(*args, "lip=")) {
+ if (net_addr2ip((*args) + 4, &temp_request.local_ip) < 0)
+ e_error(conn->dir->event, "auth sent invalid lip field: %s", (*args) + 6);
+ } else if (str_begins(*args, "lport=")) {
+ if (net_str2port((*args) + 6, &temp_request.local_port) < 0)
+ e_error(conn->dir->event, "auth sent invalid lport field: %s", (*args) + 6);
+ } else if (str_begins(*args, "port=")) {
+ if (net_str2port((*args) + 5, &temp_request.dest_port) < 0)
+ e_error(conn->dir->event, "auth sent invalid port field: %s", (*args) + 6);
+ } else if (str_begins(*args, "destuser="))
+ username = *args + 9;
+ else if (str_begins(*args, "director_tag="))
+ tag = *args + 13;
+ else if (str_begins(*args, "director_proxy_maybe") &&
+ ((*args)[20] == '=' || (*args)[20] == '\0'))
+ temp_request.director_proxy_maybe = TRUE;
+ else if (str_begins(*args, "user=")) {
+ if (username == NULL)
+ username = *args + 5;
+ }
+ }
+ if ((!proxy && !temp_request.director_proxy_maybe) ||
+ host || username == NULL) {
+ login_connection_send_line(conn, line);
+ return;
+ }
+ if (*conn->dir->set->auth_master_user_separator != '\0') {
+ /* with master user logins we still want to use only the
+ login username */
+ username = t_strcut(username,
+ *conn->dir->set->auth_master_user_separator);
+ }
+
+ /* we need to add the host. the lookup might be asynchronous */
+ request = i_new(struct login_host_request, 1);
+ *request = temp_request;
+ request->conn = conn;
+ request->line = i_strdup(line);
+ request->username = i_strdup(username);
+
+ conn->refcount++;
+ director_request(conn->dir, username, tag, login_host_callback, request);
+}
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+ struct auth_connection *auth,
+ enum login_connection_type type)
+{
+ struct login_connection *conn;
+
+ conn = i_new(struct login_connection, 1);
+ conn->refcount = 1;
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ if (type != LOGIN_CONNECTION_TYPE_AUTHREPLY) {
+ i_assert(auth != NULL);
+ conn->auth = auth;
+ conn->io = io_add(conn->fd, IO_READ,
+ login_connection_input, conn);
+ auth_connection_set_callback(conn->auth, auth_input_line, conn);
+ } else {
+ i_assert(auth == NULL);
+ conn->input = i_stream_create_fd(conn->fd, IO_BLOCK_SIZE);
+ conn->io = io_add(conn->fd, IO_READ,
+ login_connection_authreply_input, conn);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "VERSION\tdirector-authreply-server\t%d\t%d\n",
+ AUTHREPLY_PROTOCOL_MAJOR_VERSION,
+ AUTHREPLY_PROTOCOL_MINOR_VERSION));
+ }
+ conn->type = type;
+
+ DLLIST_PREPEND(&login_connections, conn);
+ return conn;
+}
+
+void login_connection_deinit(struct login_connection **_conn)
+{
+ struct login_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->destroyed)
+ return;
+ conn->destroyed = TRUE;
+
+ DLLIST_REMOVE(&login_connections, conn);
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(login connection) failed: %m");
+ conn->fd = -1;
+
+ if (conn->auth != NULL)
+ auth_connection_deinit(&conn->auth);
+ login_connection_unref(&conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void login_connection_unref(struct login_connection **_conn)
+{
+ struct login_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount == 0)
+ i_free(conn);
+}
+
+void login_connections_deinit(void)
+{
+ while (login_connections != NULL) {
+ struct login_connection *conn = login_connections;
+
+ login_connection_deinit(&conn);
+ }
+}
diff --git a/src/director/login-connection.h b/src/director/login-connection.h
new file mode 100644
index 0000000..f977ec6
--- /dev/null
+++ b/src/director/login-connection.h
@@ -0,0 +1,20 @@
+#ifndef LOGIN_CONNECTION_H
+#define LOGIN_CONNECTION_H
+
+struct director;
+
+enum login_connection_type {
+ LOGIN_CONNECTION_TYPE_AUTH,
+ LOGIN_CONNECTION_TYPE_USERDB,
+ LOGIN_CONNECTION_TYPE_AUTHREPLY
+};
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+ struct auth_connection *auth,
+ enum login_connection_type type);
+void login_connection_deinit(struct login_connection **conn);
+
+void login_connections_deinit(void);
+
+#endif
diff --git a/src/director/mail-host.c b/src/director/mail-host.c
new file mode 100644
index 0000000..50966f2
--- /dev/null
+++ b/src/director/mail-host.c
@@ -0,0 +1,560 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "crc32.h"
+#include "md5.h"
+#include "director.h"
+#include "user-directory.h"
+#include "mail-host.h"
+
+#define VHOST_MULTIPLIER 100
+
+struct mail_host_list {
+ struct director *dir;
+ ARRAY_TYPE(mail_tag) tags;
+ ARRAY_TYPE(mail_host) hosts;
+ user_free_hook_t *user_free_hook;
+ unsigned int hosts_hash;
+ unsigned int user_expire_secs;
+ bool vhosts_unsorted;
+ bool have_vhosts;
+};
+
+static int
+mail_host_cmp(struct mail_host *const *h1, struct mail_host *const *h2)
+{
+ return net_ip_cmp(&(*h1)->ip, &(*h2)->ip);
+}
+
+static int
+mail_vhost_cmp(const struct mail_vhost *h1, const struct mail_vhost *h2)
+{
+ if (h1->hash < h2->hash)
+ return -1;
+ else if (h1->hash > h2->hash)
+ return 1;
+ /* hash collision. not ideal, but we'll need to keep the order
+ consistent across directors so compare the IPs next. */
+ return net_ip_cmp(&h1->host->ip, &h2->host->ip);
+}
+
+static int
+mail_vhost_hash_cmp(const unsigned int *hash, const struct mail_vhost *vhost)
+{
+ if (vhost->hash < *hash)
+ return 1;
+ else if (vhost->hash > *hash)
+ return -1;
+ else
+ return 0;
+}
+
+static void mail_vhost_add(struct mail_tag *tag, struct mail_host *host)
+{
+ struct mail_vhost *vhost;
+ struct md5_context md5_ctx, md5_ctx2;
+ unsigned char md5[MD5_RESULTLEN];
+ char num_str[MAX_INT_STRLEN];
+ unsigned int i, j;
+
+ if (host->down || host->tag != tag)
+ return;
+
+ md5_init(&md5_ctx);
+ md5_update(&md5_ctx, host->ip_str, strlen(host->ip_str));
+
+ for (i = 0; i < host->vhost_count; i++) {
+ md5_ctx2 = md5_ctx;
+ i_snprintf(num_str, sizeof(num_str), "-%u", i);
+ md5_update(&md5_ctx2, num_str, strlen(num_str));
+ md5_final(&md5_ctx2, md5);
+
+ vhost = array_append_space(&tag->vhosts);
+ vhost->host = host;
+ for (j = 0; j < sizeof(vhost->hash); j++)
+ vhost->hash = (vhost->hash << CHAR_BIT) | md5[j];
+ }
+}
+
+static void
+mail_tag_vhosts_sort_ring(struct mail_host_list *list, struct mail_tag *tag)
+{
+ struct mail_host *host;
+
+ /* rebuild vhosts */
+ array_clear(&tag->vhosts);
+ array_foreach_elem(&list->hosts, host)
+ mail_vhost_add(tag, host);
+ array_sort(&tag->vhosts, mail_vhost_cmp);
+}
+
+static void
+mail_hosts_sort(struct mail_host_list *list)
+{
+ struct mail_host *host;
+ struct mail_tag *tag;
+ uint32_t num;
+
+ array_sort(&list->hosts, mail_host_cmp);
+
+ list->have_vhosts = FALSE;
+ array_foreach_elem(&list->tags, tag) {
+ mail_tag_vhosts_sort_ring(list, tag);
+ if (array_count(&tag->vhosts) > 0)
+ list->have_vhosts = TRUE;
+ }
+ list->vhosts_unsorted = FALSE;
+
+ /* recalculate the hosts_hash */
+ list->hosts_hash = 0;
+ array_foreach_elem(&list->hosts, host) {
+ num = (host->down ? 1 : 0) ^ host->vhost_count;
+ list->hosts_hash = crc32_data_more(list->hosts_hash,
+ &num, sizeof(num));
+ num = net_ip_hash(&host->ip);
+ list->hosts_hash = crc32_data_more(list->hosts_hash,
+ &num, sizeof(num));
+ list->hosts_hash = crc32_str_more(list->hosts_hash,
+ host->tag->name);
+ }
+}
+
+struct mail_tag *
+mail_tag_find(struct mail_host_list *list, const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(&list->tags, tag) {
+ if (strcmp(tag->name, tag_name) == 0)
+ return tag;
+ }
+ return NULL;
+}
+
+static struct mail_tag *
+mail_tag_get(struct mail_host_list *list, const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ tag = mail_tag_find(list, tag_name);
+ if (tag == NULL) {
+ tag = i_new(struct mail_tag, 1);
+ tag->name = i_strdup(tag_name);
+ i_array_init(&tag->vhosts, 16*VHOST_MULTIPLIER);
+ tag->users = user_directory_init(list->dir,
+ list->user_expire_secs,
+ list->user_free_hook);
+ array_push_back(&list->tags, &tag);
+ }
+ return tag;
+}
+
+static void mail_tag_free(struct mail_tag *tag)
+{
+ user_directory_deinit(&tag->users);
+ array_free(&tag->vhosts);
+ i_free(tag->name);
+ i_free(tag);
+}
+
+struct mail_host *
+mail_host_add_ip(struct mail_host_list *list, const struct ip_addr *ip,
+ const char *tag_name)
+{
+ struct mail_host *host;
+
+ i_assert(tag_name != NULL);
+
+ host = i_new(struct mail_host, 1);
+ host->list = list;
+ host->vhost_count = VHOST_MULTIPLIER;
+ host->ip = *ip;
+ host->ip_str = i_strdup(net_ip2addr(ip));
+ host->tag = mail_tag_get(list, tag_name);
+ array_push_back(&list->hosts, &host);
+
+ list->vhosts_unsorted = TRUE;
+ return host;
+}
+
+struct mail_host *
+mail_host_add_hostname(struct mail_host_list *list, const char *hostname,
+ const struct ip_addr *ip, const char *tag_name)
+{
+ struct mail_host *host;
+
+ host = mail_host_add_ip(list, ip, tag_name);
+ if (hostname != NULL && hostname[0] != '\0')
+ host->hostname = i_strdup(hostname);
+ return host;
+}
+
+static int
+mail_host_add(struct mail_host_list *list, const char *hostname,
+ const char *tag_name)
+{
+ struct ip_addr *ips, ip;
+ unsigned int i, ips_count;
+
+ if (net_addr2ip(hostname, &ip) == 0) {
+ (void)mail_host_add_ip(list, &ip, tag_name);
+ return 0;
+ }
+
+ if (net_gethostbyname(hostname, &ips, &ips_count) < 0) {
+ e_error(list->dir->event, "Unknown mail host: %s", hostname);
+ return -1;
+ }
+
+ for (i = 0; i < ips_count; i++)
+ (void)mail_host_add_hostname(list, hostname, &ips[i], tag_name);
+ return 0;
+}
+
+static int
+mail_hosts_add_range(struct mail_host_list *list,
+ struct ip_addr ip1, struct ip_addr ip2,
+ const char *tag_name)
+{
+ uint32_t *ip1_arr, *ip2_arr;
+ uint32_t i1, i2;
+ unsigned int i, j, max_bits, last_bits;
+
+ if (ip1.family != ip2.family) {
+ e_error(list->dir->event, "IP address family mismatch: %s vs %s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ if (net_ip_cmp(&ip1, &ip2) > 0) {
+ e_error(list->dir->event, "IP addresses reversed: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ if (IPADDR_IS_V4(&ip1)) {
+ ip1_arr = &ip1.u.ip4.s_addr;
+ ip2_arr = &ip2.u.ip4.s_addr;
+ max_bits = 32;
+ last_bits = 8;
+ } else {
+ ip1_arr = (void *)&ip1.u.ip6;
+ ip2_arr = (void *)&ip2.u.ip6;
+ max_bits = 128;
+ last_bits = 16;
+ }
+
+ /* make sure initial bits match */
+ for (i = 0; i < (max_bits-last_bits)/32; i++) {
+ if (ip1_arr[i] != ip2_arr[i]) {
+ e_error(list->dir->event, "IP address range too large: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ }
+ i1 = htonl(ip1_arr[i]);
+ i2 = htonl(ip2_arr[i]);
+
+ for (j = last_bits; j < 32; j++) {
+ if ((i1 & (1U << j)) != (i2 & (1U << j))) {
+ e_error(list->dir->event, "IP address range too large: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ }
+
+ /* create hosts from the final bits */
+ do {
+ ip1_arr[i] = ntohl(i1);
+ (void)mail_host_add_ip(list, &ip1, tag_name);
+ i1++;
+ } while (ip1_arr[i] != ip2_arr[i]);
+ return 0;
+}
+
+int mail_hosts_parse_and_add(struct mail_host_list *list,
+ const char *hosts_string)
+{
+ int ret = 0;
+
+ T_BEGIN {
+ const char *const *tmp, *p, *host1, *host2;
+ struct ip_addr ip1, ip2;
+
+ tmp = t_strsplit_spaces(hosts_string, " ");
+ for (; *tmp != NULL; tmp++) {
+ const char *tag, *value = *tmp;
+
+ p = strchr(value, '@');
+ if (p == NULL)
+ tag = "";
+ else {
+ value = t_strdup_until(value, p++);
+ tag = p;
+ }
+ p = strchr(value, '-');
+ if (p != NULL) {
+ /* see if this is ip1-ip2 range */
+ host1 = t_strdup_until(value, p);
+ host2 = p + 1;
+ if (net_addr2ip(host1, &ip1) == 0 &&
+ net_addr2ip(host2, &ip2) == 0) {
+ if (mail_hosts_add_range(list, ip1, ip2,
+ tag) < 0)
+ ret = -1;
+ continue;
+ }
+ }
+
+ if (mail_host_add(list, value, tag) < 0)
+ ret = -1;
+ }
+ } T_END;
+
+ if (array_count(&list->hosts) == 0) {
+ if (ret < 0)
+ e_error(list->dir->event, "No valid servers specified");
+ else
+ e_error(list->dir->event, "Empty server list");
+ ret = -1;
+ }
+ return ret;
+}
+
+const char *mail_host_get_tag(const struct mail_host *host)
+{
+ return host->tag->name;
+}
+
+void mail_host_set_tag(struct mail_host *host, const char *tag_name)
+{
+ i_assert(tag_name != NULL);
+
+ /* If the host already has users, forget all of them. Otherwise state
+ becomes inconsistent, since tag->users won't match
+ user->host->tag. */
+ user_directory_remove_host(host->tag->users, host);
+
+ host->tag = mail_tag_get(host->list, tag_name);
+ host->list->vhosts_unsorted = TRUE;
+}
+
+void mail_host_set_down(struct mail_host *host, bool down,
+ time_t timestamp, const char *log_prefix)
+{
+ if (host->down != down) {
+ const char *updown = down ? "down" : "up";
+ e_info(host->list->dir->event, "%sHost %s changed %s "
+ "(vhost_count=%u last_updown_change=%ld)",
+ log_prefix, host->ip_str, updown,
+ host->vhost_count, (long)host->last_updown_change);
+
+ host->down = down;
+ host->last_updown_change = timestamp;
+ host->list->vhosts_unsorted = TRUE;
+ }
+}
+
+void mail_host_set_vhost_count(struct mail_host *host, unsigned int vhost_count,
+ const char *log_prefix)
+{
+ e_info(host->list->dir->event,
+ "%sHost %s vhost count changed from %u to %u",
+ log_prefix, host->ip_str,
+ host->vhost_count, vhost_count);
+
+ host->vhost_count = vhost_count;
+ host->list->vhosts_unsorted = TRUE;
+}
+
+static void mail_host_free(struct mail_host *host)
+{
+ i_free(host->hostname);
+ i_free(host->ip_str);
+ i_free(host);
+}
+
+void mail_host_remove(struct mail_host *host)
+{
+ struct mail_host_list *list = host->list;
+ struct mail_host *const *hosts;
+ unsigned int i, count;
+
+ hosts = array_get(&list->hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == host) {
+ array_delete(&host->list->hosts, i, 1);
+ break;
+ }
+ }
+ mail_host_free(host);
+ list->vhosts_unsorted = TRUE;
+}
+
+struct mail_host *
+mail_host_lookup(struct mail_host_list *list, const struct ip_addr *ip)
+{
+ struct mail_host *host;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ array_foreach_elem(&list->hosts, host) {
+ if (net_ip_compare(&host->ip, ip))
+ return host;
+ }
+ return NULL;
+}
+
+static struct mail_host *
+mail_host_get_by_hash_ring(struct mail_tag *tag, unsigned int hash)
+{
+ const struct mail_vhost *vhosts;
+ unsigned int count, idx;
+
+ vhosts = array_get(&tag->vhosts, &count);
+ (void)array_bsearch_insert_pos(&tag->vhosts, &hash,
+ mail_vhost_hash_cmp, &idx);
+ i_assert(idx <= count);
+ if (idx == count) {
+ if (count == 0)
+ return NULL;
+ idx = 0;
+ }
+ return vhosts[idx % count].host;
+}
+
+struct mail_host *
+mail_host_get_by_hash(struct mail_host_list *list, unsigned int hash,
+ const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ tag = mail_tag_find(list, tag_name);
+ if (tag == NULL)
+ return NULL;
+
+ return mail_host_get_by_hash_ring(tag, hash);
+}
+
+void mail_hosts_set_synced(struct mail_host_list *list)
+{
+ struct mail_host *host;
+
+ array_foreach_elem(&list->hosts, host)
+ host->desynced = FALSE;
+}
+
+unsigned int mail_hosts_hash(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ /* don't retun 0 as hash, since we're using it as "doesn't exist" in
+ some places. */
+ return list->hosts_hash == 0 ? 1 : list->hosts_hash;
+}
+
+bool mail_hosts_have_usable(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ return list->have_vhosts;
+}
+
+const ARRAY_TYPE(mail_host) *mail_hosts_get(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ return &list->hosts;
+}
+
+bool mail_hosts_have_tags(struct mail_host_list *list)
+{
+ struct mail_tag *tag;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ array_foreach_elem(&list->tags, tag) {
+ if (tag->name[0] != '\0' && array_count(&tag->vhosts) > 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+const ARRAY_TYPE(mail_tag) *mail_hosts_get_tags(struct mail_host_list *list)
+{
+ return &list->tags;
+}
+
+struct mail_host_list *
+mail_hosts_init(struct director *dir,
+ unsigned int user_expire_secs,
+ user_free_hook_t *user_free_hook)
+{
+ struct mail_host_list *list;
+
+ list = i_new(struct mail_host_list, 1);
+ list->dir = dir;
+ list->user_expire_secs = user_expire_secs;
+ list->user_free_hook = user_free_hook;
+
+ i_array_init(&list->hosts, 16);
+ i_array_init(&list->tags, 4);
+ return list;
+}
+
+void mail_hosts_deinit(struct mail_host_list **_list)
+{
+ struct mail_host_list *list = *_list;
+ struct mail_host *host;
+ struct mail_tag *tag;
+
+ *_list = NULL;
+
+ array_foreach_elem(&list->tags, tag)
+ mail_tag_free(tag);
+ array_foreach_elem(&list->hosts, host)
+ mail_host_free(host);
+ array_free(&list->hosts);
+ array_free(&list->tags);
+ i_free(list);
+}
+
+static struct mail_host *
+mail_host_dup(struct mail_host_list *dest_list, const struct mail_host *src)
+{
+ struct mail_host *dest;
+
+ dest = i_new(struct mail_host, 1);
+ *dest = *src;
+ dest->tag = mail_tag_get(dest_list, src->tag->name);
+ dest->ip_str = i_strdup(src->ip_str);
+ dest->hostname = i_strdup(src->hostname);
+ return dest;
+}
+
+struct mail_host_list *mail_hosts_dup(const struct mail_host_list *src)
+{
+ struct mail_host_list *dest;
+ struct mail_host *host, *dest_host;
+
+ dest = mail_hosts_init(src->dir, src->user_expire_secs, src->user_free_hook);
+ array_foreach_elem(&src->hosts, host) {
+ dest_host = mail_host_dup(dest, host);
+ array_push_back(&dest->hosts, &dest_host);
+ }
+ mail_hosts_sort(dest);
+ return dest;
+}
+
+void mail_hosts_sort_users(struct mail_host_list *list)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(&list->tags, tag)
+ user_directory_sort(tag->users);
+}
diff --git a/src/director/mail-host.h b/src/director/mail-host.h
new file mode 100644
index 0000000..3efc528
--- /dev/null
+++ b/src/director/mail-host.h
@@ -0,0 +1,90 @@
+#ifndef MAIL_HOST_H
+#define MAIL_HOST_H
+
+#include "net.h"
+#include "user-directory.h"
+
+struct director;
+struct mail_host_list;
+
+struct mail_vhost {
+ unsigned int hash;
+ struct mail_host *host;
+};
+
+/* mail_tags aren't removed/freed before mail_hosts_deinit(), so it's safe
+ to add pointers to them. */
+struct mail_tag {
+ /* "" = no tag */
+ char *name;
+ ARRAY(struct mail_vhost) vhosts;
+ /* temporary user -> host associations */
+ struct user_directory *users;
+};
+ARRAY_DEFINE_TYPE(mail_tag, struct mail_tag *);
+
+struct mail_host {
+ struct mail_host_list *list;
+
+ unsigned int user_count;
+ unsigned int vhost_count;
+ /* server up/down. down=TRUE has effectively the same result as if
+ vhost_count=0. */
+ bool down;
+ time_t last_updown_change;
+
+ struct ip_addr ip;
+ char *ip_str;
+ char *hostname;
+ struct mail_tag *tag;
+
+ /* host was recently changed and ring hasn't synced yet since */
+ bool desynced:1;
+};
+ARRAY_DEFINE_TYPE(mail_host, struct mail_host *);
+
+struct mail_host *
+mail_host_add_ip(struct mail_host_list *list, const struct ip_addr *ip,
+ const char *tag_name);
+struct mail_host *
+mail_host_add_hostname(struct mail_host_list *list, const char *hostname,
+ const struct ip_addr *ip, const char *tag_name);
+struct mail_host *
+mail_host_lookup(struct mail_host_list *list, const struct ip_addr *ip);
+struct mail_host *
+mail_host_get_by_hash(struct mail_host_list *list, unsigned int hash,
+ const char *tag_name);
+
+int mail_hosts_parse_and_add(struct mail_host_list *list,
+ const char *hosts_string);
+const char *mail_host_get_tag(const struct mail_host *host);
+void mail_host_set_tag(struct mail_host *host, const char *tag_name);
+void mail_host_set_down(struct mail_host *host, bool down, time_t timestamp,
+ const char *log_prefix);
+void mail_host_set_vhost_count(struct mail_host *host, unsigned int vhost_count,
+ const char *log_prefix);
+void mail_host_remove(struct mail_host *host);
+
+void mail_hosts_set_synced(struct mail_host_list *list);
+unsigned int mail_hosts_hash(struct mail_host_list *list);
+bool mail_hosts_have_usable(struct mail_host_list *list);
+const ARRAY_TYPE(mail_host) *mail_hosts_get(struct mail_host_list *list);
+bool mail_hosts_have_tags(struct mail_host_list *list);
+
+const ARRAY_TYPE(mail_tag) *mail_hosts_get_tags(struct mail_host_list *list);
+struct mail_tag *
+mail_tag_find(struct mail_host_list *list, const char *tag_name);
+struct user *
+mail_hosts_find_user(struct mail_host_list *list, const char *tag_name,
+ unsigned int username_hash);
+
+struct mail_host_list *
+mail_hosts_init(struct director *dir,
+ unsigned int user_expire_secs,
+ user_free_hook_t *user_free_hook);
+void mail_hosts_deinit(struct mail_host_list **list);
+
+struct mail_host_list *mail_hosts_dup(const struct mail_host_list *src);
+void mail_hosts_sort_users(struct mail_host_list *list);
+
+#endif
diff --git a/src/director/main.c b/src/director/main.c
new file mode 100644
index 0000000..d55a38a
--- /dev/null
+++ b/src/director/main.c
@@ -0,0 +1,366 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "restrict-access.h"
+#include "process-title.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "auth-connection.h"
+#include "doveadm-connection.h"
+#include "login-connection.h"
+#include "notify-connection.h"
+#include "user-directory.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director-request.h"
+#include "mail-host.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define AUTH_SOCKET_PATH "auth-login"
+#define AUTH_USERDB_SOCKET_PATH "auth-userdb"
+
+enum director_socket_type {
+ DIRECTOR_SOCKET_TYPE_UNKNOWN = 0,
+ DIRECTOR_SOCKET_TYPE_AUTH,
+ DIRECTOR_SOCKET_TYPE_USERDB,
+ DIRECTOR_SOCKET_TYPE_AUTHREPLY,
+ DIRECTOR_SOCKET_TYPE_RING,
+ DIRECTOR_SOCKET_TYPE_DOVEADM,
+ DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY,
+};
+
+static struct director *director;
+static struct timeout *to_proctitle_refresh;
+static ARRAY(enum director_socket_type) listener_socket_types;
+
+static unsigned int director_total_users_count(void)
+{
+ struct mail_tag *tag;
+ unsigned int count = 0;
+
+ array_foreach_elem(mail_hosts_get_tags(director->mail_hosts), tag)
+ count += user_directory_count(tag->users);
+ return count;
+}
+
+static void director_refresh_proctitle_timeout(void *context ATTR_UNUSED)
+{
+ static uint64_t prev_requests = 0, prev_input = 0, prev_output;
+ static uint64_t prev_incoming_requests = 0;
+ string_t *str;
+
+ str = t_str_new(64);
+ str_printfa(str, "[%u users", director_total_users_count());
+ if (director->requests_delayed_count > 0)
+ str_printfa(str, ", %u delayed", director->requests_delayed_count);
+ if (director->users_moving_count > 0)
+ str_printfa(str, ", %u moving", director->users_moving_count);
+ if (director->users_kicking_count > 0)
+ str_printfa(str, ", %u kicking", director->users_kicking_count);
+ str_printfa(str, ", %"PRIu64"+%"PRIu64" req/s",
+ director->num_requests - prev_requests,
+ director->num_incoming_requests - prev_incoming_requests);
+ str_printfa(str, ", %"PRIu64"+%"PRIu64" kB/s",
+ (director->ring_traffic_input - prev_input)/1024,
+ (director->ring_traffic_output - prev_output)/1024);
+ str_append_c(str, ']');
+
+ prev_requests = director->num_requests;
+ prev_incoming_requests = director->num_incoming_requests;
+ prev_input = director->ring_traffic_input;
+ prev_output = director->ring_traffic_output;
+
+ process_title_set(str_c(str));
+}
+
+static enum director_socket_type
+director_socket_type_get_from_name(const char *path)
+{
+ const char *name, *suffix;
+
+ name = strrchr(path, '/');
+ if (name == NULL)
+ name = path;
+ else
+ name++;
+
+ suffix = strrchr(name, '-');
+ if (suffix == NULL)
+ suffix = name;
+ else
+ suffix++;
+
+ if (strcmp(suffix, "auth") == 0)
+ return DIRECTOR_SOCKET_TYPE_AUTH;
+ else if (strcmp(suffix, "userdb") == 0)
+ return DIRECTOR_SOCKET_TYPE_USERDB;
+ else if (strcmp(suffix, "authreply") == 0)
+ return DIRECTOR_SOCKET_TYPE_AUTHREPLY;
+ else if (strcmp(suffix, "ring") == 0)
+ return DIRECTOR_SOCKET_TYPE_RING;
+ else if (strcmp(suffix, "admin") == 0 ||
+ strcmp(suffix, "doveadm") == 0)
+ return DIRECTOR_SOCKET_TYPE_DOVEADM;
+ else if (strcmp(suffix, "notify") == 0)
+ return DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY;
+ else
+ return DIRECTOR_SOCKET_TYPE_UNKNOWN;
+}
+
+static enum director_socket_type
+listener_get_socket_type_fallback(int listen_fd)
+{
+ in_port_t local_port;
+
+ if (net_getsockname(listen_fd, NULL, &local_port) == 0 &&
+ local_port != 0) {
+ /* TCP/IP connection */
+ return DIRECTOR_SOCKET_TYPE_RING;
+ }
+ return DIRECTOR_SOCKET_TYPE_AUTH;
+}
+
+static void listener_sockets_init(struct ip_addr *listen_ip_r,
+ in_port_t *listen_port_r)
+{
+ const char *name;
+ unsigned int i, socket_count;
+ struct ip_addr ip;
+ in_port_t port;
+ enum director_socket_type type;
+
+ *listen_port_r = 0;
+
+ i_array_init(&listener_socket_types, 8);
+ socket_count = master_service_get_socket_count(master_service);
+ for (i = 0; i < socket_count; i++) {
+ int listen_fd = MASTER_LISTEN_FD_FIRST + i;
+
+ name = master_service_get_socket_name(master_service, listen_fd);
+ type = director_socket_type_get_from_name(name);
+ if (type == DIRECTOR_SOCKET_TYPE_UNKNOWN) {
+ /* mainly for backwards compatibility */
+ type = listener_get_socket_type_fallback(listen_fd);
+ }
+ if (type == DIRECTOR_SOCKET_TYPE_RING && *listen_port_r == 0 &&
+ net_getsockname(listen_fd, &ip, &port) == 0 && port > 0) {
+ *listen_ip_r = ip;
+ *listen_port_r = port;
+ }
+ array_idx_set(&listener_socket_types, listen_fd, &type);
+ }
+}
+
+static int director_client_connected(int fd, const struct ip_addr *ip)
+{
+ struct director_host *host;
+
+ host = director_host_lookup_ip(director, ip);
+ if (host == NULL || host->removed) {
+ e_warning(director->event,
+ "Connection from %s: Server not listed in "
+ "director_servers, dropping", net_ip2addr(ip));
+ return -1;
+ }
+
+ (void)director_connection_init_in(director, fd, ip);
+ return 0;
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct auth_connection *auth;
+ const char *socket_path;
+ const enum director_socket_type *typep;
+ bool userdb;
+
+ if (conn->fifo) {
+ master_service_client_connection_accept(conn);
+ notify_connection_init(director, conn->fd, TRUE);
+ return;
+ }
+
+ typep = array_idx(&listener_socket_types, conn->listen_fd);
+ switch (*typep) {
+ case DIRECTOR_SOCKET_TYPE_UNKNOWN:
+ i_unreached();
+ case DIRECTOR_SOCKET_TYPE_AUTH:
+ case DIRECTOR_SOCKET_TYPE_USERDB:
+ /* a) userdb connection, probably for lmtp proxy
+ b) login connection
+ Both of them are handled exactly the same, except for which
+ auth socket they connect to. */
+ userdb = *typep == DIRECTOR_SOCKET_TYPE_USERDB;
+ socket_path = userdb ? AUTH_USERDB_SOCKET_PATH :
+ AUTH_SOCKET_PATH;
+ auth = auth_connection_init(director, socket_path);
+ if (auth_connection_connect(auth) < 0) {
+ auth_connection_deinit(&auth);
+ break;
+ }
+ master_service_client_connection_accept(conn);
+ (void)login_connection_init(director, conn->fd, auth,
+ userdb ? LOGIN_CONNECTION_TYPE_USERDB :
+ LOGIN_CONNECTION_TYPE_AUTH);
+ break;
+ case DIRECTOR_SOCKET_TYPE_AUTHREPLY:
+ master_service_client_connection_accept(conn);
+ (void)login_connection_init(director, conn->fd, NULL,
+ LOGIN_CONNECTION_TYPE_AUTHREPLY);
+ break;
+ case DIRECTOR_SOCKET_TYPE_RING:
+ if (director_client_connected(conn->fd, &conn->remote_ip) == 0)
+ master_service_client_connection_accept(conn);
+ break;
+ case DIRECTOR_SOCKET_TYPE_DOVEADM:
+ master_service_client_connection_accept(conn);
+ (void)doveadm_connection_init(director, conn->fd);
+ break;
+ case DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY:
+ master_service_client_connection_accept(conn);
+ notify_connection_init(director, conn->fd, FALSE);
+ break;
+ }
+}
+
+static void director_state_changed(struct director *dir)
+{
+ struct director_request *request;
+ ARRAY(struct director_request *) new_requests;
+ bool ret;
+
+ if (!dir->ring_synced)
+ return;
+
+ /* if there are any pending client requests, finish them now */
+ t_array_init(&new_requests, 8);
+ array_foreach_elem(&dir->pending_requests, request) {
+ ret = director_request_continue(request);
+ if (!ret) {
+ /* a) request for a user being killed
+ b) user is weak */
+ array_push_back(&new_requests, &request);
+ }
+ }
+ array_clear(&dir->pending_requests);
+ array_append_array(&dir->pending_requests, &new_requests);
+
+ if (dir->to_request != NULL && array_count(&new_requests) == 0)
+ timeout_remove(&dir->to_request);
+ doveadm_connections_ring_synced(dir);
+}
+
+static void main_preinit(void)
+{
+ const struct director_settings *set;
+ struct ip_addr listen_ip;
+ in_port_t listen_port;
+
+ /* make sure we die with master even with shutdown_clients=no.
+ otherwise there will be two director processes and everything is
+ broken. it's only the login processes that need to stay alive. */
+ master_service_set_die_with_master(master_service, TRUE);
+
+ if (master_service_settings_get(master_service)->verbose_proctitle) {
+ to_proctitle_refresh =
+ timeout_add(1000, director_refresh_proctitle_timeout,
+ NULL);
+ }
+ set = master_service_settings_get_others(master_service)[0];
+
+ listener_sockets_init(&listen_ip, &listen_port);
+ if (listen_port == 0 && *set->director_servers != '\0') {
+ i_fatal("No inet_listeners defined for director service "
+ "(for standalone keep director_servers empty)");
+ }
+
+ directors_init();
+ director = director_init(set, &listen_ip, listen_port,
+ director_state_changed,
+ doveadm_connections_kick_callback);
+ director_host_add_from_string(director, set->director_servers);
+ director_find_self(director);
+ if (mail_hosts_parse_and_add(director->mail_hosts,
+ set->director_mail_servers) < 0)
+ i_fatal("Invalid value for director_mail_servers setting");
+ director->orig_config_hosts = mail_hosts_dup(director->mail_hosts);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_deinit(void)
+{
+ timeout_remove(&to_proctitle_refresh);
+ notify_connections_deinit();
+ /* deinit doveadm connections before director, so it can clean up
+ its pending work, such as abort user moves. */
+ doveadm_connections_deinit();
+ director_deinit(&director);
+ directors_deinit();
+ login_connections_deinit();
+ auth_connections_deinit();
+ array_free(&listener_socket_types);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &director_setting_parser_info,
+ NULL
+ };
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE;
+ in_port_t test_port = 0;
+ const char *error;
+ bool debug = FALSE;
+ int c;
+
+ master_service = master_service_init("director", service_flags,
+ &argc, &argv, "Dt:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ case 't':
+ if (net_str2port(optarg, &test_port) < 0)
+ i_fatal("-t: Not a port number: %s", optarg);
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ master_service_init_log(master_service);
+
+ main_preinit();
+ director->test_port = test_port;
+ event_set_forced_debug(director->event, debug);
+ director_connect(director, "Initial connection");
+
+ if (director->test_port != 0) {
+ /* we're testing, possibly writing to same log file.
+ make it clear which director we are. */
+ master_service_init_log_with_prefix(master_service,
+ t_strdup_printf("director(%s): ",
+ net_ip2addr(&director->self_ip)));
+ }
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+ main_deinit();
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/director/notify-connection.c b/src/director/notify-connection.c
new file mode 100644
index 0000000..d750c83
--- /dev/null
+++ b/src/director/notify-connection.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "master-service.h"
+#include "director.h"
+#include "mail-host.h"
+#include "notify-connection.h"
+
+#include <unistd.h>
+
+struct notify_connection {
+ struct notify_connection *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct director *dir;
+
+ bool fifo:1;
+};
+
+static struct notify_connection *notify_connections = NULL;
+
+static void notify_connection_deinit(struct notify_connection **_conn);
+
+static void notify_update_user(struct director *dir, struct mail_tag *tag,
+ const char *username, unsigned int username_hash)
+{
+ struct user *user;
+ int diff;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL)
+ return;
+
+ diff = ioloop_time - user->timestamp;
+ if (diff >= (int)dir->set->director_user_expire) {
+ e_warning(dir->event,
+ "notify: User %s refreshed too late (%d secs)",
+ username, diff);
+ }
+ user_directory_refresh(tag->users, user);
+ director_update_user(dir, dir->self_host, user);
+}
+
+static void notify_connection_input(struct notify_connection *conn)
+{
+ struct mail_tag *tag;
+ const char *line;
+ unsigned int hash;
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (!director_get_username_hash(conn->dir, line, &hash))
+ continue;
+ array_foreach_elem(mail_hosts_get_tags(conn->dir->mail_hosts), tag)
+ notify_update_user(conn->dir, tag, line, hash);
+ }
+ if (conn->input->eof) {
+ if (conn->fifo)
+ e_error(conn->dir->event,
+ "notify: read() unexpectedly returned EOF");
+ notify_connection_deinit(&conn);
+ } else if (conn->input->stream_errno != 0) {
+ e_error(conn->dir->event, "notify: read() failed: %s",
+ i_stream_get_error(conn->input));
+ notify_connection_deinit(&conn);
+ }
+}
+
+void notify_connection_init(struct director *dir, int fd, bool fifo)
+{
+ struct notify_connection *conn;
+
+ conn = i_new(struct notify_connection, 1);
+ conn->fd = fd;
+ conn->fifo = fifo;
+ conn->dir = dir;
+ conn->input = i_stream_create_fd(conn->fd, 1024);
+ conn->io = io_add(conn->fd, IO_READ, notify_connection_input, conn);
+ DLLIST_PREPEND(&notify_connections, conn);
+}
+
+static void notify_connection_deinit(struct notify_connection **_conn)
+{
+ struct notify_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ DLLIST_REMOVE(&notify_connections, conn);
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(notify connection) failed: %m");
+ if (!conn->fifo)
+ master_service_client_connection_destroyed(master_service);
+ i_free(conn);
+}
+
+void notify_connections_deinit(void)
+{
+ while (notify_connections != NULL) {
+ struct notify_connection *conn = notify_connections;
+ notify_connection_deinit(&conn);
+ }
+}
diff --git a/src/director/notify-connection.h b/src/director/notify-connection.h
new file mode 100644
index 0000000..8f1bd02
--- /dev/null
+++ b/src/director/notify-connection.h
@@ -0,0 +1,9 @@
+#ifndef NOTIFY_CONNECTION_H
+#define NOTIFY_CONNECTION_H
+
+struct director;
+
+void notify_connection_init(struct director *dir, int fd, bool fifo);
+void notify_connections_deinit(void);
+
+#endif
diff --git a/src/director/test-user-directory.c b/src/director/test-user-directory.c
new file mode 100644
index 0000000..88f40a8
--- /dev/null
+++ b/src/director/test-user-directory.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mail-user-hash.h"
+#include "mail-host.h"
+#include "test-common.h"
+
+
+#define USER_DIR_TIMEOUT 1000000
+
+bool mail_user_hash(const char *username ATTR_UNUSED,
+ const char *format ATTR_UNUSED,
+ unsigned int *hash_r, const char **error_r ATTR_UNUSED)
+{
+ *hash_r = 0;
+ return TRUE;
+}
+
+static void
+verify_user_directory(struct user_directory *dir, unsigned int user_count)
+{
+ struct user_directory_iter *iter;
+ struct user *user, *prev = NULL;
+ unsigned int prev_stamp = 0, iter_count = 0;
+
+ iter = user_directory_iter_init(dir, FALSE);
+ while ((user = user_directory_iter_next(iter)) != NULL) {
+ test_assert(prev_stamp <= user->timestamp);
+ test_assert(user->prev == prev);
+ test_assert(prev == NULL || user->prev->next == user);
+
+ iter_count++;
+ prev = user;
+ }
+ test_assert(prev == NULL || prev->next == NULL);
+ user_directory_iter_deinit(&iter);
+ test_assert(iter_count == user_count);
+}
+
+static void test_user_directory_ascending(void)
+{
+ const unsigned int count = 100000;
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ unsigned int i;
+
+ test_begin("user directory ascending");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+ (void)user_directory_add(dir, 1, host, ioloop_time + count+1);
+
+ for (i = 0; i < count; i++)
+ (void)user_directory_add(dir, i+2, host, ioloop_time + i);
+ verify_user_directory(dir, count+1);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+static void test_user_directory_descending(void)
+{
+ const unsigned int count = 1000;
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ unsigned int i;
+
+ test_begin("user directory descending");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+
+ for (i = 0; i < count; i++)
+ (void)user_directory_add(dir, i+1, host, ioloop_time - i);
+ verify_user_directory(dir, count);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+static void test_user_directory_random(void)
+{
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ time_t timestamp;
+ unsigned int i, count = i_rand_minmax(10000, 19999);
+
+ test_begin("user directory random");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+ for (i = 0; i < count; i++) {
+ if (i_rand_limit(10) == 0)
+ timestamp = ioloop_time;
+ else
+ timestamp = ioloop_time-i_rand_limit(100);
+ (void)user_directory_add(dir, i+1, host, timestamp);
+ }
+ verify_user_directory(dir, count);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_user_directory_ascending,
+ test_user_directory_descending,
+ test_user_directory_random,
+ NULL
+ };
+ struct ioloop *ioloop = io_loop_create();
+ int ret = test_run(test_functions);
+ io_loop_destroy(&ioloop);
+ return ret;
+}
diff --git a/src/director/user-directory.c b/src/director/user-directory.c
new file mode 100644
index 0000000..ea7f6e0
--- /dev/null
+++ b/src/director/user-directory.c
@@ -0,0 +1,349 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hash.h"
+#include "llist.h"
+#include "mail-host.h"
+#include "director.h"
+
+/* n% of timeout_secs */
+#define USER_NEAR_EXPIRING_PERCENTAGE 10
+/* but min/max. of this many secs */
+#define USER_NEAR_EXPIRING_MIN 3
+#define USER_NEAR_EXPIRING_MAX 30
+/* This shouldn't matter what it is exactly, just try it sometimes later. */
+#define USER_BEING_KILLED_EXPIRE_RETRY_SECS 60
+
+struct user_directory_iter {
+ struct user_directory *dir;
+ struct user *pos, *stop_after_tail;
+};
+
+struct user_directory {
+ struct director *director;
+
+ /* unsigned int username_hash => user */
+ HASH_TABLE(void *, struct user *) hash;
+ /* sorted by time. may be unsorted while handshakes are going on. */
+ struct user *head, *tail;
+
+ ARRAY(struct user_directory_iter *) iters;
+ user_free_hook_t *user_free_hook;
+
+ unsigned int timeout_secs;
+ /* If user's expire time is less than this many seconds away,
+ don't assume that other directors haven't yet expired it */
+ unsigned int user_near_expiring_secs;
+ struct timeout *to_expire;
+ time_t to_expire_timestamp;
+
+ bool sort_pending;
+};
+
+static void user_move_iters(struct user_directory *dir, struct user *user)
+{
+ struct user_directory_iter *iter;
+
+ array_foreach_elem(&dir->iters, iter) {
+ if (iter->pos == user)
+ iter->pos = user->next;
+ if (iter->stop_after_tail == user) {
+ iter->stop_after_tail =
+ user->prev != NULL ? user->prev : user->next;
+ }
+ }
+}
+
+static void user_free(struct user_directory *dir, struct user *user)
+{
+ i_assert(user->host->user_count > 0);
+ user->host->user_count--;
+
+ if (dir->user_free_hook != NULL)
+ dir->user_free_hook(user);
+ user_move_iters(dir, user);
+
+ hash_table_remove(dir->hash, POINTER_CAST(user->username_hash));
+ DLLIST2_REMOVE(&dir->head, &dir->tail, user);
+ i_free(user);
+}
+
+static bool user_directory_user_has_connections(struct user_directory *dir,
+ struct user *user,
+ time_t *expire_timestamp_r)
+{
+ time_t expire_timestamp = user->timestamp + dir->timeout_secs;
+
+ if (expire_timestamp > ioloop_time) {
+ *expire_timestamp_r = expire_timestamp;
+ return TRUE;
+ }
+
+ if (USER_IS_BEING_KILLED(user)) {
+ /* don't free this user until the kill is finished */
+ *expire_timestamp_r = ioloop_time +
+ USER_BEING_KILLED_EXPIRE_RETRY_SECS;
+ return TRUE;
+ }
+
+ if (user->weak) {
+ if (expire_timestamp + USER_NEAR_EXPIRING_MAX > ioloop_time) {
+ *expire_timestamp_r = expire_timestamp +
+ USER_NEAR_EXPIRING_MAX;
+ return TRUE;
+ }
+
+ e_warning(dir->director->event,
+ "User %u weakness appears to be stuck, removing it",
+ user->username_hash);
+ }
+ return FALSE;
+}
+
+static void user_directory_drop_expired(struct user_directory *dir)
+{
+ time_t expire_timestamp = 0;
+
+ while (dir->head != NULL &&
+ !user_directory_user_has_connections(dir, dir->head, &expire_timestamp)) {
+ user_free(dir, dir->head);
+ expire_timestamp = 0;
+ }
+ i_assert(expire_timestamp > ioloop_time || expire_timestamp == 0);
+
+ if (expire_timestamp != dir->to_expire_timestamp) {
+ timeout_remove(&dir->to_expire);
+ if (expire_timestamp != 0) {
+ struct timeval tv = { .tv_sec = expire_timestamp };
+ dir->to_expire_timestamp = tv.tv_sec;
+ dir->to_expire = timeout_add_absolute(&tv,
+ user_directory_drop_expired, dir);
+ }
+ }
+}
+
+unsigned int user_directory_count(struct user_directory *dir)
+{
+ return hash_table_count(dir->hash);
+}
+
+struct user *user_directory_lookup(struct user_directory *dir,
+ unsigned int username_hash)
+{
+ struct user *user;
+ time_t expire_timestamp;
+
+ user_directory_drop_expired(dir);
+ user = hash_table_lookup(dir->hash, POINTER_CAST(username_hash));
+ if (user != NULL && !user_directory_user_has_connections(dir, user, &expire_timestamp)) {
+ user_free(dir, user);
+ user = NULL;
+ }
+ return user;
+}
+
+struct user *
+user_directory_add(struct user_directory *dir, unsigned int username_hash,
+ struct mail_host *host, time_t timestamp)
+{
+ struct user *user;
+
+ /* make sure we don't add timestamps higher than ioloop time */
+ if (timestamp > ioloop_time)
+ timestamp = ioloop_time;
+
+ user = i_new(struct user, 1);
+ user->username_hash = username_hash;
+ user->host = host;
+ user->host->user_count++;
+ user->timestamp = timestamp;
+ DLLIST2_APPEND(&dir->head, &dir->tail, user);
+
+ if (dir->to_expire == NULL) {
+ struct timeval tv = { .tv_sec = ioloop_time + dir->timeout_secs };
+ dir->to_expire_timestamp = tv.tv_sec;
+ dir->to_expire = timeout_add_absolute(&tv, user_directory_drop_expired, dir);
+ }
+ hash_table_insert(dir->hash, POINTER_CAST(user->username_hash), user);
+ return user;
+}
+
+void user_directory_refresh(struct user_directory *dir, struct user *user)
+{
+ user_move_iters(dir, user);
+
+ user->timestamp = ioloop_time;
+ DLLIST2_REMOVE(&dir->head, &dir->tail, user);
+ DLLIST2_APPEND(&dir->head, &dir->tail, user);
+}
+
+void user_directory_remove_host(struct user_directory *dir,
+ struct mail_host *host)
+{
+ struct user *user, *next;
+
+ for (user = dir->head; user != NULL; user = next) {
+ next = user->next;
+
+ if (user->host == host)
+ user_free(dir, user);
+ }
+}
+
+static int user_timestamp_cmp(struct user *const *user1,
+ struct user *const *user2)
+{
+ if ((*user1)->timestamp < (*user2)->timestamp)
+ return -1;
+ if ((*user1)->timestamp > (*user2)->timestamp)
+ return 1;
+ return 0;
+}
+
+void user_directory_sort(struct user_directory *dir)
+{
+ ARRAY(struct user *) users;
+ struct user *user;
+ unsigned int i, users_count = hash_table_count(dir->hash);
+
+ dir->sort_pending = FALSE;
+
+ if (users_count == 0) {
+ i_assert(dir->head == NULL);
+ return;
+ }
+
+ if (array_count(&dir->iters) > 0) {
+ /* We can't sort the directory while there are iterators
+ or they'll skip users. Do the sort after there are no more
+ iterators. */
+ dir->sort_pending = TRUE;
+ return;
+ }
+
+ /* place all users into array and sort it */
+ i_array_init(&users, users_count);
+ user = dir->head;
+ for (i = 0; i < users_count; i++, user = user->next)
+ array_push_back(&users, &user);
+ i_assert(user == NULL);
+ array_sort(&users, user_timestamp_cmp);
+
+ /* recreate the linked list */
+ dir->head = dir->tail = NULL;
+ array_foreach_elem(&users, user)
+ DLLIST2_APPEND(&dir->head, &dir->tail, user);
+ i_assert(dir->head != NULL &&
+ dir->head->timestamp <= dir->tail->timestamp);
+ array_free(&users);
+}
+
+bool user_directory_user_is_recently_updated(struct user_directory *dir,
+ struct user *user)
+{
+ return (time_t)(user->timestamp + dir->timeout_secs/2) >= ioloop_time;
+}
+
+bool user_directory_user_is_near_expiring(struct user_directory *dir,
+ struct user *user)
+{
+ time_t expire_timestamp;
+
+ expire_timestamp = user->timestamp +
+ (dir->timeout_secs - dir->user_near_expiring_secs);
+ return expire_timestamp < ioloop_time;
+}
+
+struct user_directory *
+user_directory_init(struct director *director, unsigned int timeout_secs,
+ user_free_hook_t *user_free_hook)
+{
+ struct user_directory *dir;
+
+ i_assert(timeout_secs > USER_NEAR_EXPIRING_MIN);
+
+ dir = i_new(struct user_directory, 1);
+ dir->director = director;
+ dir->timeout_secs = timeout_secs;
+ dir->user_near_expiring_secs =
+ timeout_secs * USER_NEAR_EXPIRING_PERCENTAGE / 100;
+ dir->user_near_expiring_secs =
+ I_MIN(dir->user_near_expiring_secs, USER_NEAR_EXPIRING_MAX);
+ dir->user_near_expiring_secs =
+ I_MAX(dir->user_near_expiring_secs, USER_NEAR_EXPIRING_MIN);
+ i_assert(dir->timeout_secs/2 > dir->user_near_expiring_secs);
+
+ dir->user_free_hook = user_free_hook;
+ hash_table_create_direct(&dir->hash, default_pool, 0);
+ i_array_init(&dir->iters, 8);
+ return dir;
+}
+
+void user_directory_deinit(struct user_directory **_dir)
+{
+ struct user_directory *dir = *_dir;
+
+ *_dir = NULL;
+
+ i_assert(array_count(&dir->iters) == 0);
+
+ while (dir->head != NULL)
+ user_free(dir, dir->head);
+ timeout_remove(&dir->to_expire);
+ hash_table_destroy(&dir->hash);
+ array_free(&dir->iters);
+ i_free(dir);
+}
+
+struct user_directory_iter *
+user_directory_iter_init(struct user_directory *dir,
+ bool iter_until_current_tail)
+{
+ struct user_directory_iter *iter;
+
+ iter = i_new(struct user_directory_iter, 1);
+ iter->dir = dir;
+ iter->pos = dir->head;
+ iter->stop_after_tail = iter_until_current_tail ? dir->tail : NULL;
+ array_push_back(&dir->iters, &iter);
+ user_directory_drop_expired(dir);
+ return iter;
+}
+
+struct user *user_directory_iter_next(struct user_directory_iter *iter)
+{
+ struct user *user;
+
+ user = iter->pos;
+ if (user == NULL)
+ return NULL;
+
+ iter->pos = user->next;
+ if (user == iter->stop_after_tail) {
+ /* this is the last user we want to iterate */
+ iter->pos = NULL;
+ }
+ return user;
+}
+
+void user_directory_iter_deinit(struct user_directory_iter **_iter)
+{
+ struct user_directory_iter *iter = *_iter;
+ struct user_directory_iter *const *iters;
+ unsigned int i, count;
+
+ *_iter = NULL;
+
+ iters = array_get(&iter->dir->iters, &count);
+ for (i = 0; i < count; i++) {
+ if (iters[i] == iter) {
+ array_delete(&iter->dir->iters, i, 1);
+ break;
+ }
+ }
+ if (array_count(&iter->dir->iters) == 0 && iter->dir->sort_pending)
+ user_directory_sort(iter->dir);
+ i_free(iter);
+}
diff --git a/src/director/user-directory.h b/src/director/user-directory.h
new file mode 100644
index 0000000..04f28cd
--- /dev/null
+++ b/src/director/user-directory.h
@@ -0,0 +1,79 @@
+#ifndef USER_DIRECTORY_H
+#define USER_DIRECTORY_H
+
+struct director;
+
+#define USER_IS_BEING_KILLED(user) \
+ ((user)->kill_ctx != NULL)
+
+struct user {
+ /* Approximately sorted by time (except during handshaking).
+ The sorting order may be constantly wrong a few seconds here and
+ there. */
+ struct user *prev, *next;
+
+ /* first 32 bits of MD5(username). collisions are quite unlikely, but
+ even if they happen it doesn't matter - the users are just
+ redirected to same server */
+ unsigned int username_hash;
+ unsigned int timestamp;
+
+ struct mail_host *host;
+
+ /* If non-NULL, don't allow new connections until all
+ directors have killed the user's connections. */
+ struct director_kill_context *kill_ctx;
+
+ /* TRUE, if the user's timestamp was close to being expired and we're
+ now doing a ring-wide sync for this user to make sure we don't
+ assign conflicting hosts to it */
+ bool weak:1;
+};
+
+typedef void user_free_hook_t(struct user *);
+
+/* Create a new directory. Users are dropped if their time gets older
+ than timeout_secs. */
+struct user_directory *
+user_directory_init(struct director *director, unsigned int timeout_secs,
+ user_free_hook_t *user_free_hook);
+void user_directory_deinit(struct user_directory **dir);
+
+/* Returns the number of users currently in directory. */
+unsigned int user_directory_count(struct user_directory *dir);
+/* Look up username from directory. Returns NULL if not found. */
+struct user *user_directory_lookup(struct user_directory *dir,
+ unsigned int username_hash);
+/* Add a user to directory and return it. */
+struct user *
+user_directory_add(struct user_directory *dir, unsigned int username_hash,
+ struct mail_host *host, time_t timestamp);
+/* Refresh user's timestamp */
+void user_directory_refresh(struct user_directory *dir, struct user *user);
+
+/* Remove all users that have pointers to given host */
+void user_directory_remove_host(struct user_directory *dir,
+ struct mail_host *host);
+/* Sort users based on the timestamp. This is called only after updating
+ timestamps based on remote director's user list after handshake. */
+void user_directory_sort(struct user_directory *dir);
+
+bool user_directory_user_is_recently_updated(struct user_directory *dir,
+ struct user *user);
+bool user_directory_user_is_near_expiring(struct user_directory *dir,
+ struct user *user);
+
+/* Iterate through users in the directory. It's safe to modify user directory
+ while iterators are running. The removed users will just be skipped over.
+ Users that are refreshed (= moved to end of list) may be processed twice.
+
+ Using iter_until_current_tail=TRUE causes the iterator to not iterate
+ through any users that were added/refreshed since the iteration began.
+ Note that this may skip some users entirely. */
+struct user_directory_iter *
+user_directory_iter_init(struct user_directory *dir,
+ bool iter_until_current_tail);
+struct user *user_directory_iter_next(struct user_directory_iter *iter);
+void user_directory_iter_deinit(struct user_directory_iter **iter);
+
+#endif
diff --git a/src/dns/Makefile.am b/src/dns/Makefile.am
new file mode 100644
index 0000000..87f4b6c
--- /dev/null
+++ b/src/dns/Makefile.am
@@ -0,0 +1,17 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = dns-client
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-settings \
+ $(BINARY_CFLAGS)
+
+dns_client_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dns_client_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+dns_client_SOURCES = \
+ dns-client.c \
+ dns-client-settings.c
diff --git a/src/dns/Makefile.in b/src/dns/Makefile.in
new file mode 100644
index 0000000..462671a
--- /dev/null
+++ b/src/dns/Makefile.in
@@ -0,0 +1,803 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = dns-client$(EXEEXT)
+subdir = src/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 $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_dns_client_OBJECTS = dns-client.$(OBJEXT) \
+ dns-client-settings.$(OBJEXT)
+dns_client_OBJECTS = $(am_dns_client_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/dns-client-settings.Po \
+ ./$(DEPDIR)/dns-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 = $(dns_client_SOURCES)
+DIST_SOURCES = $(dns_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__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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-master \
+ -I$(top_srcdir)/src/lib-settings \
+ $(BINARY_CFLAGS)
+
+dns_client_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dns_client_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+dns_client_SOURCES = \
+ dns-client.c \
+ dns-client-settings.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/dns/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+dns-client$(EXEEXT): $(dns_client_OBJECTS) $(dns_client_DEPENDENCIES) $(EXTRA_dns_client_DEPENDENCIES)
+ @rm -f dns-client$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(dns_client_OBJECTS) $(dns_client_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dns-client-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dns-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
+
+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 $(PROGRAMS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dns-client-settings.Po
+ -rm -f ./$(DEPDIR)/dns-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-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS
+
+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-client-settings.Po
+ -rm -f ./$(DEPDIR)/dns-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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/dns/dns-client-settings.c b/src/dns/dns-client-settings.c
new file mode 100644
index 0000000..082e735
--- /dev/null
+++ b/src/dns/dns-client-settings.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings dns_client_unix_listeners_array[] = {
+ { "dns-client", 0666, "", "" },
+ { "login/dns-client", 0666, "", "" },
+};
+static struct file_listener_settings *dns_client_unix_listeners[] = {
+ &dns_client_unix_listeners_array[0],
+ &dns_client_unix_listeners_array[1],
+};
+static buffer_t dns_client_unix_listeners_buf = {
+ { { dns_client_unix_listeners, sizeof(dns_client_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings dns_client_service_settings = {
+ .name = "dns-client",
+ .protocol = "",
+ .type = "",
+ .executable = "dns-client",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &dns_client_unix_listeners_buf,
+ sizeof(dns_client_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
diff --git a/src/dns/dns-client.c b/src/dns/dns-client.c
new file mode 100644
index 0000000..381c2ea
--- /dev/null
+++ b/src/dns/dns-client.c
@@ -0,0 +1,161 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "array.h"
+#include "strfuncs.h"
+#include "connection.h"
+#include "restrict-access.h"
+#include "master-service.h"
+
+#include <unistd.h>
+
+static struct event_category event_category_dns = {
+ .name = "dns-worker"
+};
+
+static struct connection_list *dns_clients = NULL;
+
+static int dns_client_input_args(struct connection *client, const char *const *args)
+{
+ struct ip_addr *ips, ip;
+ const char *name;
+ struct event *event;
+ unsigned int i, ips_count;
+ int ret;
+ struct event_passthrough *e;
+
+ if (strcmp(args[0], "QUIT") == 0) {
+ return -1;
+ } else if (args[1] == NULL) {
+ e_error(client->event, "Got empty request");
+ return -1;
+ }
+
+ event = event_create(client->event);
+ event_set_append_log_prefix(event, t_strconcat(args[1], ": ", NULL));
+
+ e = event_create_passthrough(event)->
+ set_name("dns_worker_request_started")->
+ add_str("name", args[1]);
+ e_debug(e->event(), "Resolving");
+
+ e = event_create_passthrough(event)->
+ set_name("dns_worker_request_finished")->
+ add_str("name", args[1]);
+
+ if (strcmp(args[0], "IP") == 0) {
+ ret = net_gethostbyname(args[1], &ips, &ips_count);
+ if (ret == 0 && ips_count == 0) {
+ /* shouldn't happen, but fix it anyway.. */
+ ret = EAI_NONAME;
+ }
+ /* update timestamp after hostname lookup so the event duration
+ field gets set correctly */
+ io_loop_time_refresh();
+ if (ret != 0) {
+ const char *err = net_gethosterror(ret);
+ e->add_int("error_code", ret);
+ e->add_str("error", err);
+ e_debug(e->event(), "Resolve failed: %s", err);
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("%d\t%s\n", ret, err));
+ } else {
+ ARRAY_TYPE(const_string) tmp;
+ t_array_init(&tmp, ips_count);
+ o_stream_nsend_str(client->output, "0\t");
+ for (i = 0; i < ips_count; i++) {
+ const char *ip = net_ip2addr(&ips[i]);
+ array_push_back(&tmp, &ip);
+ }
+ array_append_zero(&tmp);
+ e_debug(e->event(), "Resolve success: %s",
+ t_strarray_join(array_front(&tmp), ", "));
+ o_stream_nsend_str(client->output,
+ t_strarray_join(array_front(&tmp), "\t"));
+ o_stream_nsend_str(client->output, "\n");
+ }
+ } else if (strcmp(args[0], "NAME") == 0) {
+ if (net_addr2ip(args[1], &ip) < 0) {
+ e->add_int("error_code", EAI_FAIL);
+ e->add_str("error", "Not an IP");
+ e_debug(e->event(), "Resolve failed: Not an IP");
+ o_stream_nsend_str(client->output, "-1\tNot an IP\n");
+ } else if ((ret = net_gethostbyaddr(&ip, &name)) != 0) {
+ const char *err = net_gethosterror(ret);
+ e->add_int("error_code", ret);
+ e->add_str("error", err);
+ e_debug(e->event(), "Resolve failed: %s", err);
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("%d\t%s\n", ret, err));
+ } else {
+ e_debug(e->event(), "Resolve success: %s", name);
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("0\t%s\n", name));
+ }
+ } else {
+ e->add_str("error", "Unknown command");
+ e_error(e->event(), "Unknown command '%s'", args[0]);
+ o_stream_nsend_str(client->output, "-1\tUnknown command\n");
+ }
+
+ event_unref(&event);
+
+ return 1;
+}
+
+static void dns_client_destroy(struct connection *client)
+{
+ connection_deinit(client);
+ event_unref(&client->event);
+ i_free(client);
+ master_service_client_connection_destroyed(master_service);
+}
+
+static const struct connection_vfuncs dns_client_vfuncs = {
+ .input_args = dns_client_input_args,
+ .destroy = dns_client_destroy
+};
+
+static const struct connection_settings dns_client_set = {
+ .service_name_in = "dns-client",
+ .service_name_out = "dns",
+ .major_version = 1,
+ .minor_version = 0,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX
+};
+
+static void client_connected(struct master_service_connection *master_conn)
+{
+ struct connection *conn = i_new(struct connection, 1);
+ master_service_client_connection_accept(master_conn);
+ connection_init_server(dns_clients, conn, master_conn->name,
+ master_conn->fd, master_conn->fd);
+ event_add_category(conn->event, &event_category_dns);
+}
+
+int main(int argc, char *argv[])
+{
+ master_service = master_service_init("dns-client", 0,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ master_service_init_log(master_service);
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ /* setup connection list */
+ dns_clients = connection_list_init(&dns_client_set, &dns_client_vfuncs);
+
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+
+ /* disconnect all clients */
+ connection_list_deinit(&dns_clients);
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/doveadm/Makefile.am b/src/doveadm/Makefile.am
new file mode 100644
index 0000000..440af73
--- /dev/null
+++ b/src/doveadm/Makefile.am
@@ -0,0 +1,201 @@
+doveadm_moduledir = $(moduledir)/doveadm
+pkglibexecdir = $(libexecdir)/dovecot
+
+SUBDIRS = dsync
+
+bin_PROGRAMS = doveadm
+pkglibexec_PROGRAMS = doveadm-server
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-compression \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -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-imap-storage \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/auth \
+ -I$(top_srcdir)/src/stats \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ -DAUTH_MODULE_DIR=\""$(moduledir)/auth"\" \
+ -DDOVEADM_MODULEDIR=\""$(doveadm_moduledir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DMANDIR=\""$(mandir)"\" \
+ $(BINARY_CFLAGS)
+
+cmd_pw_libs = \
+ ../auth/libpassword.la \
+ ../lib-otp/libotp.la
+
+libs = \
+ dsync/libdsync.la \
+ ../lib-compression/libcompression.la
+
+doveadm_LDADD = \
+ $(libs) \
+ $(cmd_pw_libs) \
+ $(CRYPT_LIBS) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(LIBSODIUM_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveadm_DEPENDENCIES = \
+ $(libs) \
+ $(cmd_pw_libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+doveadm_server_LDADD = \
+ $(libs) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveadm_server_DEPENDENCIES = \
+ $(libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+doveadm_common_cmds = \
+ doveadm-auth.c \
+ doveadm-dict.c \
+ doveadm-director.c \
+ doveadm-fs.c \
+ doveadm-instance.c \
+ doveadm-kick.c \
+ doveadm-log.c \
+ doveadm-master.c \
+ doveadm-mutf7.c \
+ doveadm-penalty.c \
+ doveadm-proxy.c \
+ doveadm-replicator.c \
+ doveadm-sis.c \
+ doveadm-stats.c \
+ doveadm-oldstats.c \
+ doveadm-who.c
+
+doveadm_common_mail_cmds = \
+ doveadm-dsync.c \
+ doveadm-mail.c \
+ doveadm-mail-altmove.c \
+ doveadm-mail-batch.c \
+ doveadm-mail-deduplicate.c \
+ doveadm-mail-expunge.c \
+ doveadm-mail-fetch.c \
+ doveadm-mail-flags.c \
+ doveadm-mail-import.c \
+ doveadm-mail-index.c \
+ doveadm-mail-iter.c \
+ doveadm-mail-mailbox.c \
+ doveadm-mail-mailbox-metadata.c \
+ doveadm-mail-mailbox-status.c \
+ doveadm-mail-copymove.c \
+ doveadm-mailbox-list-iter.c \
+ doveadm-mail-save.c \
+ doveadm-mail-search.c \
+ doveadm-mail-server.c \
+ doveadm-mail-mailbox-cache.c \
+ doveadm-mail-rebuild.c
+
+# these aren't actually useful in doveadm-server, but plugins may implement
+# both dumping and some other commands inside a single plugin. not having the
+# dump functions in doveadm-server fails to load such plugins.
+doveadm_common_dump_cmds = \
+ doveadm-dump.c \
+ doveadm-dump-dbox.c \
+ doveadm-dump-index.c \
+ doveadm-dump-log.c \
+ doveadm-dump-mailboxlog.c \
+ doveadm-dump-thread.c \
+ doveadm-dump-dcrypt-file.c \
+ doveadm-dump-dcrypt-key.c \
+ doveadm-zlib.c
+
+common = \
+ $(doveadm_common_cmds) \
+ $(doveadm_common_mail_cmds) \
+ $(doveadm_common_dump_cmds) \
+ doveadm-cmd.c \
+ doveadm-print.c \
+ doveadm-settings.c \
+ doveadm-util.c \
+ server-connection.c \
+ doveadm-print-formatted.c
+
+doveadm_SOURCES = \
+ $(common) \
+ doveadm.c \
+ doveadm-print-flow.c \
+ doveadm-print-pager.c \
+ doveadm-print-tab.c \
+ doveadm-print-table.c \
+ doveadm-print-json.c \
+ doveadm-pw.c
+
+doveadm_server_SOURCES = \
+ $(common) \
+ doveadm-auth-server.c \
+ client-connection.c \
+ client-connection-tcp.c \
+ client-connection-http.c \
+ doveadm-print-server.c \
+ doveadm-print-json.c \
+ main.c
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ doveadm.h \
+ doveadm-cmd.h \
+ doveadm-dsync.h \
+ doveadm-dump.h \
+ doveadm-mail.h \
+ doveadm-mail-iter.h \
+ doveadm-mailbox-list-iter.h \
+ doveadm-print.h \
+ doveadm-print-private.h \
+ doveadm-settings.h \
+ doveadm-util.h
+
+noinst_HEADERS = \
+ client-connection.h \
+ client-connection-private.h \
+ server-connection.h \
+ doveadm-server.h \
+ doveadm-who.h
+
+install-exec-local:
+ rm -f $(DESTDIR)$(bindir)/dsync
+ $(LN_S) doveadm $(DESTDIR)$(bindir)/dsync
+
+test_programs = \
+ test-doveadm-util
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_doveadm_util_SOURCES = doveadm-util.c test-doveadm-util.c
+test_doveadm_util_LDADD = $(test_libs) $(MODULE_LIBS)
+test_doveadm_util_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/doveadm/Makefile.in b/src/doveadm/Makefile.in
new file mode 100644
index 0000000..33795c8
--- /dev/null
+++ b/src/doveadm/Makefile.in
@@ -0,0 +1,1515 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+bin_PROGRAMS = doveadm$(EXEEXT)
+pkglibexec_PROGRAMS = doveadm-server$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/doveadm
+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__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+am__EXEEXT_1 = test-doveadm-util$(EXEEXT)
+PROGRAMS = $(bin_PROGRAMS) $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am__objects_1 = doveadm-auth.$(OBJEXT) doveadm-dict.$(OBJEXT) \
+ doveadm-director.$(OBJEXT) doveadm-fs.$(OBJEXT) \
+ doveadm-instance.$(OBJEXT) doveadm-kick.$(OBJEXT) \
+ doveadm-log.$(OBJEXT) doveadm-master.$(OBJEXT) \
+ doveadm-mutf7.$(OBJEXT) doveadm-penalty.$(OBJEXT) \
+ doveadm-proxy.$(OBJEXT) doveadm-replicator.$(OBJEXT) \
+ doveadm-sis.$(OBJEXT) doveadm-stats.$(OBJEXT) \
+ doveadm-oldstats.$(OBJEXT) doveadm-who.$(OBJEXT)
+am__objects_2 = doveadm-dsync.$(OBJEXT) doveadm-mail.$(OBJEXT) \
+ doveadm-mail-altmove.$(OBJEXT) doveadm-mail-batch.$(OBJEXT) \
+ doveadm-mail-deduplicate.$(OBJEXT) \
+ doveadm-mail-expunge.$(OBJEXT) doveadm-mail-fetch.$(OBJEXT) \
+ doveadm-mail-flags.$(OBJEXT) doveadm-mail-import.$(OBJEXT) \
+ doveadm-mail-index.$(OBJEXT) doveadm-mail-iter.$(OBJEXT) \
+ doveadm-mail-mailbox.$(OBJEXT) \
+ doveadm-mail-mailbox-metadata.$(OBJEXT) \
+ doveadm-mail-mailbox-status.$(OBJEXT) \
+ doveadm-mail-copymove.$(OBJEXT) \
+ doveadm-mailbox-list-iter.$(OBJEXT) \
+ doveadm-mail-save.$(OBJEXT) doveadm-mail-search.$(OBJEXT) \
+ doveadm-mail-server.$(OBJEXT) \
+ doveadm-mail-mailbox-cache.$(OBJEXT) \
+ doveadm-mail-rebuild.$(OBJEXT)
+am__objects_3 = doveadm-dump.$(OBJEXT) doveadm-dump-dbox.$(OBJEXT) \
+ doveadm-dump-index.$(OBJEXT) doveadm-dump-log.$(OBJEXT) \
+ doveadm-dump-mailboxlog.$(OBJEXT) \
+ doveadm-dump-thread.$(OBJEXT) \
+ doveadm-dump-dcrypt-file.$(OBJEXT) \
+ doveadm-dump-dcrypt-key.$(OBJEXT) doveadm-zlib.$(OBJEXT)
+am__objects_4 = $(am__objects_1) $(am__objects_2) $(am__objects_3) \
+ doveadm-cmd.$(OBJEXT) doveadm-print.$(OBJEXT) \
+ doveadm-settings.$(OBJEXT) doveadm-util.$(OBJEXT) \
+ server-connection.$(OBJEXT) doveadm-print-formatted.$(OBJEXT)
+am_doveadm_OBJECTS = $(am__objects_4) doveadm.$(OBJEXT) \
+ doveadm-print-flow.$(OBJEXT) doveadm-print-pager.$(OBJEXT) \
+ doveadm-print-tab.$(OBJEXT) doveadm-print-table.$(OBJEXT) \
+ doveadm-print-json.$(OBJEXT) doveadm-pw.$(OBJEXT)
+doveadm_OBJECTS = $(am_doveadm_OBJECTS)
+am__DEPENDENCIES_1 =
+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_doveadm_server_OBJECTS = $(am__objects_4) \
+ doveadm-auth-server.$(OBJEXT) client-connection.$(OBJEXT) \
+ client-connection-tcp.$(OBJEXT) \
+ client-connection-http.$(OBJEXT) \
+ doveadm-print-server.$(OBJEXT) doveadm-print-json.$(OBJEXT) \
+ main.$(OBJEXT)
+doveadm_server_OBJECTS = $(am_doveadm_server_OBJECTS)
+am_test_doveadm_util_OBJECTS = doveadm-util.$(OBJEXT) \
+ test-doveadm-util.$(OBJEXT)
+test_doveadm_util_OBJECTS = $(am_test_doveadm_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)/client-connection-http.Po \
+ ./$(DEPDIR)/client-connection-tcp.Po \
+ ./$(DEPDIR)/client-connection.Po \
+ ./$(DEPDIR)/doveadm-auth-server.Po ./$(DEPDIR)/doveadm-auth.Po \
+ ./$(DEPDIR)/doveadm-cmd.Po ./$(DEPDIR)/doveadm-dict.Po \
+ ./$(DEPDIR)/doveadm-director.Po ./$(DEPDIR)/doveadm-dsync.Po \
+ ./$(DEPDIR)/doveadm-dump-dbox.Po \
+ ./$(DEPDIR)/doveadm-dump-dcrypt-file.Po \
+ ./$(DEPDIR)/doveadm-dump-dcrypt-key.Po \
+ ./$(DEPDIR)/doveadm-dump-index.Po \
+ ./$(DEPDIR)/doveadm-dump-log.Po \
+ ./$(DEPDIR)/doveadm-dump-mailboxlog.Po \
+ ./$(DEPDIR)/doveadm-dump-thread.Po ./$(DEPDIR)/doveadm-dump.Po \
+ ./$(DEPDIR)/doveadm-fs.Po ./$(DEPDIR)/doveadm-instance.Po \
+ ./$(DEPDIR)/doveadm-kick.Po ./$(DEPDIR)/doveadm-log.Po \
+ ./$(DEPDIR)/doveadm-mail-altmove.Po \
+ ./$(DEPDIR)/doveadm-mail-batch.Po \
+ ./$(DEPDIR)/doveadm-mail-copymove.Po \
+ ./$(DEPDIR)/doveadm-mail-deduplicate.Po \
+ ./$(DEPDIR)/doveadm-mail-expunge.Po \
+ ./$(DEPDIR)/doveadm-mail-fetch.Po \
+ ./$(DEPDIR)/doveadm-mail-flags.Po \
+ ./$(DEPDIR)/doveadm-mail-import.Po \
+ ./$(DEPDIR)/doveadm-mail-index.Po \
+ ./$(DEPDIR)/doveadm-mail-iter.Po \
+ ./$(DEPDIR)/doveadm-mail-mailbox-cache.Po \
+ ./$(DEPDIR)/doveadm-mail-mailbox-metadata.Po \
+ ./$(DEPDIR)/doveadm-mail-mailbox-status.Po \
+ ./$(DEPDIR)/doveadm-mail-mailbox.Po \
+ ./$(DEPDIR)/doveadm-mail-rebuild.Po \
+ ./$(DEPDIR)/doveadm-mail-save.Po \
+ ./$(DEPDIR)/doveadm-mail-search.Po \
+ ./$(DEPDIR)/doveadm-mail-server.Po ./$(DEPDIR)/doveadm-mail.Po \
+ ./$(DEPDIR)/doveadm-mailbox-list-iter.Po \
+ ./$(DEPDIR)/doveadm-master.Po ./$(DEPDIR)/doveadm-mutf7.Po \
+ ./$(DEPDIR)/doveadm-oldstats.Po ./$(DEPDIR)/doveadm-penalty.Po \
+ ./$(DEPDIR)/doveadm-print-flow.Po \
+ ./$(DEPDIR)/doveadm-print-formatted.Po \
+ ./$(DEPDIR)/doveadm-print-json.Po \
+ ./$(DEPDIR)/doveadm-print-pager.Po \
+ ./$(DEPDIR)/doveadm-print-server.Po \
+ ./$(DEPDIR)/doveadm-print-tab.Po \
+ ./$(DEPDIR)/doveadm-print-table.Po \
+ ./$(DEPDIR)/doveadm-print.Po ./$(DEPDIR)/doveadm-proxy.Po \
+ ./$(DEPDIR)/doveadm-pw.Po ./$(DEPDIR)/doveadm-replicator.Po \
+ ./$(DEPDIR)/doveadm-settings.Po ./$(DEPDIR)/doveadm-sis.Po \
+ ./$(DEPDIR)/doveadm-stats.Po ./$(DEPDIR)/doveadm-util.Po \
+ ./$(DEPDIR)/doveadm-who.Po ./$(DEPDIR)/doveadm-zlib.Po \
+ ./$(DEPDIR)/doveadm.Po ./$(DEPDIR)/main.Po \
+ ./$(DEPDIR)/server-connection.Po \
+ ./$(DEPDIR)/test-doveadm-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 = $(doveadm_SOURCES) $(doveadm_server_SOURCES) \
+ $(test_doveadm_util_SOURCES)
+DIST_SOURCES = $(doveadm_SOURCES) $(doveadm_server_SOURCES) \
+ $(test_doveadm_util_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; }; \
+ }
+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"
+pkglibexecdir = $(libexecdir)/dovecot
+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@
+doveadm_moduledir = $(moduledir)/doveadm
+SUBDIRS = dsync
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-compression \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -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-imap-storage \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/auth \
+ -I$(top_srcdir)/src/stats \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ -DAUTH_MODULE_DIR=\""$(moduledir)/auth"\" \
+ -DDOVEADM_MODULEDIR=\""$(doveadm_moduledir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DMANDIR=\""$(mandir)"\" \
+ $(BINARY_CFLAGS)
+
+cmd_pw_libs = \
+ ../auth/libpassword.la \
+ ../lib-otp/libotp.la
+
+libs = \
+ dsync/libdsync.la \
+ ../lib-compression/libcompression.la
+
+doveadm_LDADD = \
+ $(libs) \
+ $(cmd_pw_libs) \
+ $(CRYPT_LIBS) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(LIBSODIUM_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveadm_DEPENDENCIES = \
+ $(libs) \
+ $(cmd_pw_libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+doveadm_server_LDADD = \
+ $(libs) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+doveadm_server_DEPENDENCIES = \
+ $(libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+doveadm_common_cmds = \
+ doveadm-auth.c \
+ doveadm-dict.c \
+ doveadm-director.c \
+ doveadm-fs.c \
+ doveadm-instance.c \
+ doveadm-kick.c \
+ doveadm-log.c \
+ doveadm-master.c \
+ doveadm-mutf7.c \
+ doveadm-penalty.c \
+ doveadm-proxy.c \
+ doveadm-replicator.c \
+ doveadm-sis.c \
+ doveadm-stats.c \
+ doveadm-oldstats.c \
+ doveadm-who.c
+
+doveadm_common_mail_cmds = \
+ doveadm-dsync.c \
+ doveadm-mail.c \
+ doveadm-mail-altmove.c \
+ doveadm-mail-batch.c \
+ doveadm-mail-deduplicate.c \
+ doveadm-mail-expunge.c \
+ doveadm-mail-fetch.c \
+ doveadm-mail-flags.c \
+ doveadm-mail-import.c \
+ doveadm-mail-index.c \
+ doveadm-mail-iter.c \
+ doveadm-mail-mailbox.c \
+ doveadm-mail-mailbox-metadata.c \
+ doveadm-mail-mailbox-status.c \
+ doveadm-mail-copymove.c \
+ doveadm-mailbox-list-iter.c \
+ doveadm-mail-save.c \
+ doveadm-mail-search.c \
+ doveadm-mail-server.c \
+ doveadm-mail-mailbox-cache.c \
+ doveadm-mail-rebuild.c
+
+
+# these aren't actually useful in doveadm-server, but plugins may implement
+# both dumping and some other commands inside a single plugin. not having the
+# dump functions in doveadm-server fails to load such plugins.
+doveadm_common_dump_cmds = \
+ doveadm-dump.c \
+ doveadm-dump-dbox.c \
+ doveadm-dump-index.c \
+ doveadm-dump-log.c \
+ doveadm-dump-mailboxlog.c \
+ doveadm-dump-thread.c \
+ doveadm-dump-dcrypt-file.c \
+ doveadm-dump-dcrypt-key.c \
+ doveadm-zlib.c
+
+common = \
+ $(doveadm_common_cmds) \
+ $(doveadm_common_mail_cmds) \
+ $(doveadm_common_dump_cmds) \
+ doveadm-cmd.c \
+ doveadm-print.c \
+ doveadm-settings.c \
+ doveadm-util.c \
+ server-connection.c \
+ doveadm-print-formatted.c
+
+doveadm_SOURCES = \
+ $(common) \
+ doveadm.c \
+ doveadm-print-flow.c \
+ doveadm-print-pager.c \
+ doveadm-print-tab.c \
+ doveadm-print-table.c \
+ doveadm-print-json.c \
+ doveadm-pw.c
+
+doveadm_server_SOURCES = \
+ $(common) \
+ doveadm-auth-server.c \
+ client-connection.c \
+ client-connection-tcp.c \
+ client-connection-http.c \
+ doveadm-print-server.c \
+ doveadm-print-json.c \
+ main.c
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ doveadm.h \
+ doveadm-cmd.h \
+ doveadm-dsync.h \
+ doveadm-dump.h \
+ doveadm-mail.h \
+ doveadm-mail-iter.h \
+ doveadm-mailbox-list-iter.h \
+ doveadm-print.h \
+ doveadm-print-private.h \
+ doveadm-settings.h \
+ doveadm-util.h
+
+noinst_HEADERS = \
+ client-connection.h \
+ client-connection-private.h \
+ server-connection.h \
+ doveadm-server.h \
+ doveadm-who.h
+
+test_programs = \
+ test-doveadm-util
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_doveadm_util_SOURCES = doveadm-util.c test-doveadm-util.c
+test_doveadm_util_LDADD = $(test_libs) $(MODULE_LIBS)
+test_doveadm_util_DEPENDENCIES = $(test_deps)
+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/doveadm/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/doveadm/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-binPROGRAMS: $(bin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-binPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+ @list='$(bin_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-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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+doveadm$(EXEEXT): $(doveadm_OBJECTS) $(doveadm_DEPENDENCIES) $(EXTRA_doveadm_DEPENDENCIES)
+ @rm -f doveadm$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(doveadm_OBJECTS) $(doveadm_LDADD) $(LIBS)
+
+doveadm-server$(EXEEXT): $(doveadm_server_OBJECTS) $(doveadm_server_DEPENDENCIES) $(EXTRA_doveadm_server_DEPENDENCIES)
+ @rm -f doveadm-server$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(doveadm_server_OBJECTS) $(doveadm_server_LDADD) $(LIBS)
+
+test-doveadm-util$(EXEEXT): $(test_doveadm_util_OBJECTS) $(test_doveadm_util_DEPENDENCIES) $(EXTRA_test_doveadm_util_DEPENDENCIES)
+ @rm -f test-doveadm-util$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_doveadm_util_OBJECTS) $(test_doveadm_util_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-connection-http.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-connection-tcp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-auth-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-auth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-cmd.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dict.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-director.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dsync.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-dbox.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-dcrypt-file.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-dcrypt-key.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-index.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-log.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-mailboxlog.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-thread.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-fs.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-instance.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-kick.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-log.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-altmove.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-batch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-copymove.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-deduplicate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-expunge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-fetch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-flags.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-import.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-index.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-iter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-mailbox-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-mailbox-metadata.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-mailbox-status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-mailbox.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-rebuild.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-save.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-search.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mailbox-list-iter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-master.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mutf7.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-oldstats.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-penalty.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-flow.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-formatted.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-json.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-pager.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-tab.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print-table.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-print.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-proxy.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-pw.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-replicator.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-sis.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-stats.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-who.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-zlib.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/server-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-doveadm-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 $@ $<
+
+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) $(HEADERS)
+installdirs: installdirs-recursive
+installdirs-am:
+ for dir in "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkglibexecdir)" "$(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-binPROGRAMS clean-generic clean-libtool \
+ clean-noinstPROGRAMS clean-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f ./$(DEPDIR)/client-connection-http.Po
+ -rm -f ./$(DEPDIR)/client-connection-tcp.Po
+ -rm -f ./$(DEPDIR)/client-connection.Po
+ -rm -f ./$(DEPDIR)/doveadm-auth-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-auth.Po
+ -rm -f ./$(DEPDIR)/doveadm-cmd.Po
+ -rm -f ./$(DEPDIR)/doveadm-dict.Po
+ -rm -f ./$(DEPDIR)/doveadm-director.Po
+ -rm -f ./$(DEPDIR)/doveadm-dsync.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dbox.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dcrypt-file.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dcrypt-key.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-index.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-log.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-mailboxlog.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-thread.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump.Po
+ -rm -f ./$(DEPDIR)/doveadm-fs.Po
+ -rm -f ./$(DEPDIR)/doveadm-instance.Po
+ -rm -f ./$(DEPDIR)/doveadm-kick.Po
+ -rm -f ./$(DEPDIR)/doveadm-log.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-altmove.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-batch.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-copymove.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-deduplicate.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-expunge.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-fetch.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-flags.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-import.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-index.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-iter.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-cache.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-metadata.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-status.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-rebuild.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-save.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-search.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail.Po
+ -rm -f ./$(DEPDIR)/doveadm-mailbox-list-iter.Po
+ -rm -f ./$(DEPDIR)/doveadm-master.Po
+ -rm -f ./$(DEPDIR)/doveadm-mutf7.Po
+ -rm -f ./$(DEPDIR)/doveadm-oldstats.Po
+ -rm -f ./$(DEPDIR)/doveadm-penalty.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-flow.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-formatted.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-json.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-pager.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-tab.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-table.Po
+ -rm -f ./$(DEPDIR)/doveadm-print.Po
+ -rm -f ./$(DEPDIR)/doveadm-proxy.Po
+ -rm -f ./$(DEPDIR)/doveadm-pw.Po
+ -rm -f ./$(DEPDIR)/doveadm-replicator.Po
+ -rm -f ./$(DEPDIR)/doveadm-settings.Po
+ -rm -f ./$(DEPDIR)/doveadm-sis.Po
+ -rm -f ./$(DEPDIR)/doveadm-stats.Po
+ -rm -f ./$(DEPDIR)/doveadm-util.Po
+ -rm -f ./$(DEPDIR)/doveadm-who.Po
+ -rm -f ./$(DEPDIR)/doveadm-zlib.Po
+ -rm -f ./$(DEPDIR)/doveadm.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/server-connection.Po
+ -rm -f ./$(DEPDIR)/test-doveadm-util.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-binPROGRAMS install-exec-local \
+ install-pkglibexecPROGRAMS
+
+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)/client-connection-http.Po
+ -rm -f ./$(DEPDIR)/client-connection-tcp.Po
+ -rm -f ./$(DEPDIR)/client-connection.Po
+ -rm -f ./$(DEPDIR)/doveadm-auth-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-auth.Po
+ -rm -f ./$(DEPDIR)/doveadm-cmd.Po
+ -rm -f ./$(DEPDIR)/doveadm-dict.Po
+ -rm -f ./$(DEPDIR)/doveadm-director.Po
+ -rm -f ./$(DEPDIR)/doveadm-dsync.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dbox.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dcrypt-file.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-dcrypt-key.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-index.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-log.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-mailboxlog.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump-thread.Po
+ -rm -f ./$(DEPDIR)/doveadm-dump.Po
+ -rm -f ./$(DEPDIR)/doveadm-fs.Po
+ -rm -f ./$(DEPDIR)/doveadm-instance.Po
+ -rm -f ./$(DEPDIR)/doveadm-kick.Po
+ -rm -f ./$(DEPDIR)/doveadm-log.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-altmove.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-batch.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-copymove.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-deduplicate.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-expunge.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-fetch.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-flags.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-import.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-index.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-iter.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-cache.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-metadata.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox-status.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-mailbox.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-rebuild.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-save.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-search.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-mail.Po
+ -rm -f ./$(DEPDIR)/doveadm-mailbox-list-iter.Po
+ -rm -f ./$(DEPDIR)/doveadm-master.Po
+ -rm -f ./$(DEPDIR)/doveadm-mutf7.Po
+ -rm -f ./$(DEPDIR)/doveadm-oldstats.Po
+ -rm -f ./$(DEPDIR)/doveadm-penalty.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-flow.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-formatted.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-json.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-pager.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-server.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-tab.Po
+ -rm -f ./$(DEPDIR)/doveadm-print-table.Po
+ -rm -f ./$(DEPDIR)/doveadm-print.Po
+ -rm -f ./$(DEPDIR)/doveadm-proxy.Po
+ -rm -f ./$(DEPDIR)/doveadm-pw.Po
+ -rm -f ./$(DEPDIR)/doveadm-replicator.Po
+ -rm -f ./$(DEPDIR)/doveadm-settings.Po
+ -rm -f ./$(DEPDIR)/doveadm-sis.Po
+ -rm -f ./$(DEPDIR)/doveadm-stats.Po
+ -rm -f ./$(DEPDIR)/doveadm-util.Po
+ -rm -f ./$(DEPDIR)/doveadm-who.Po
+ -rm -f ./$(DEPDIR)/doveadm-zlib.Po
+ -rm -f ./$(DEPDIR)/doveadm.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/server-connection.Po
+ -rm -f ./$(DEPDIR)/test-doveadm-util.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-binPROGRAMS uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.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-binPROGRAMS clean-generic clean-libtool \
+ clean-noinstPROGRAMS clean-pkglibexecPROGRAMS 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-binPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-binPROGRAMS uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.PRECIOUS: Makefile
+
+
+install-exec-local:
+ rm -f $(DESTDIR)$(bindir)/dsync
+ $(LN_S) doveadm $(DESTDIR)$(bindir)/dsync
+
+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/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c
new file mode 100644
index 0000000..c59d9dd
--- /dev/null
+++ b/src/doveadm/client-connection-http.c
@@ -0,0 +1,1227 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "compat.h"
+#include "lib-signals.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "settings-parser.h"
+#include "iostream-ssl.h"
+#include "iostream-temp.h"
+#include "istream-seekable.h"
+#include "master-service.h"
+#include "master-service-ssl.h"
+#include "master-service-settings.h"
+#include "mail-storage-service.h"
+#include "http-server.h"
+#include "http-request.h"
+#include "http-response.h"
+#include "http-url.h"
+#include "doveadm-util.h"
+#include "doveadm-server.h"
+#include "doveadm-mail.h"
+#include "doveadm-print.h"
+#include "doveadm-settings.h"
+#include "client-connection-private.h"
+#include "json-parser.h"
+
+#include <unistd.h>
+#include <ctype.h>
+
+enum client_request_parse_state {
+ CLIENT_REQUEST_PARSE_INIT,
+ CLIENT_REQUEST_PARSE_CMD,
+ CLIENT_REQUEST_PARSE_CMD_NAME,
+ CLIENT_REQUEST_PARSE_CMD_PARAMS,
+ CLIENT_REQUEST_PARSE_CMD_PARAM_KEY,
+ CLIENT_REQUEST_PARSE_CMD_PARAM_VALUE,
+ CLIENT_REQUEST_PARSE_CMD_PARAM_ARRAY,
+ CLIENT_REQUEST_PARSE_CMD_PARAM_ISTREAM,
+ CLIENT_REQUEST_PARSE_CMD_ID,
+ CLIENT_REQUEST_PARSE_CMD_DONE,
+ CLIENT_REQUEST_PARSE_DONE
+};
+
+struct client_request_http {
+ pool_t pool;
+ struct client_connection_http *conn;
+
+ struct http_server_request *http_request;
+
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ struct json_parser *json_parser;
+
+ const struct doveadm_cmd_ver2 *cmd;
+ struct doveadm_cmd_param *cmd_param;
+ struct ioloop *ioloop;
+ ARRAY_TYPE(doveadm_cmd_param_arr_t) pargv;
+ int method_err;
+ char *method_id;
+ bool first_row;
+ bool value_is_array;
+
+ enum client_request_parse_state parse_state;
+};
+
+struct client_connection_http {
+ struct client_connection conn;
+
+ struct http_server_connection *http_conn;
+
+ struct client_request_http *request;
+};
+
+typedef void doveadm_server_handler_t(struct client_request_http *req);
+
+struct doveadm_http_server_mount {
+ const char *verb;
+ const char *path;
+ doveadm_server_handler_t *handler;
+ bool auth;
+};
+
+static struct http_server *doveadm_http_server;
+
+static void doveadm_http_server_send_response(struct client_request_http *req);
+
+/*
+ * API
+ */
+
+static void doveadm_http_server_options_handler(struct client_request_http *);
+static void doveadm_http_server_print_mounts(struct client_request_http *);
+static void doveadm_http_server_send_api_v1(struct client_request_http *);
+static void doveadm_http_server_read_request_v1(struct client_request_http *);
+
+static struct doveadm_http_server_mount doveadm_http_server_mounts[] = {
+{
+ .verb = "OPTIONS",
+ .path = NULL,
+ .handler = doveadm_http_server_options_handler,
+ .auth = FALSE
+},{
+ .verb = "GET",
+ .path = "/",
+ .handler = doveadm_http_server_print_mounts,
+ .auth = TRUE
+},{
+ .verb = "GET",
+ .path = "/doveadm/v1",
+ .handler = doveadm_http_server_send_api_v1,
+ .auth = TRUE
+},{
+ .verb = "POST",
+ .path = "/doveadm/v1",
+ .handler = doveadm_http_server_read_request_v1,
+ .auth = TRUE
+}
+};
+
+static void doveadm_http_server_json_error(void *context, const char *error)
+{
+ struct client_request_http *req = context;
+ struct ostream *output = req->output;
+ string_t *escaped;
+
+ escaped = str_new(req->pool, 10);
+
+ o_stream_nsend_str(output, "[\"error\",{\"type\":\"");
+ json_append_escaped(escaped, error);
+ o_stream_nsend_str(output, str_c(escaped));
+ o_stream_nsend_str(output, "\", \"exitCode\":");
+ str_truncate(escaped,0);
+ str_printfa(escaped, "%d", doveadm_exit_code);
+ o_stream_nsend_str(output, str_c(escaped));
+ o_stream_nsend_str(output, "},\"");
+ str_truncate(escaped,0);
+ if (req->method_id != NULL) {
+ json_append_escaped(escaped, req->method_id);
+ o_stream_nsend_str(output, str_c(escaped));
+ }
+ o_stream_nsend_str(output, "\"]");
+}
+
+static void doveadm_http_server_json_success(void *context, struct istream *result)
+{
+ struct client_request_http *req = context;
+ struct ostream *output = req->output;
+ string_t *escaped;
+
+ escaped = str_new(req->pool, 10);
+
+ o_stream_nsend_str(output, "[\"doveadmResponse\",");
+ o_stream_nsend_istream(output, result);
+ o_stream_nsend_str(output, ",\"");
+ if (req->method_id != NULL) {
+ json_append_escaped(escaped, req->method_id);
+ o_stream_nsend_str(output, str_c(escaped));
+ }
+ o_stream_nsend_str(output, "\"]");
+}
+
+static void
+doveadm_http_server_command_execute(struct client_request_http *req)
+{
+ struct client_connection_http *conn = req->conn;
+ struct doveadm_cmd_context cctx;
+ struct istream *is;
+ const char *user;
+ struct ioloop *ioloop, *prev_ioloop;
+
+ /* final preflight check */
+ if (req->method_err == 0 &&
+ !doveadm_client_is_allowed_command(conn->conn.set,
+ req->cmd->name))
+ req->method_err = 403;
+ if (req->method_err != 0) {
+ if (req->method_err == 404) {
+ doveadm_http_server_json_error(req, "unknownMethod");
+ } else if (req->method_err == 403) {
+ doveadm_http_server_json_error(req, "unAuthorized");
+ } else if (req->method_err == 400) {
+ doveadm_http_server_json_error(req, "invalidRequest");
+ } else {
+ doveadm_http_server_json_error(req, "internalError");
+ }
+ return;
+ }
+
+ prev_ioloop = current_ioloop;
+ i_zero(&cctx);
+ cctx.conn_type = conn->conn.type;
+ cctx.input = req->input;
+ cctx.output = req->output;
+
+ // create iostream
+ doveadm_print_ostream = iostream_temp_create("/tmp/doveadm.", 0);
+ cctx.cmd = req->cmd;
+
+ if ((cctx.cmd->flags & CMD_FLAG_NO_PRINT) == 0)
+ doveadm_print_init(DOVEADM_PRINT_TYPE_JSON);
+
+ /* then call it */
+ doveadm_cmd_params_null_terminate_arrays(&req->pargv);
+ cctx.argv = array_get(&req->pargv, (unsigned int*)&cctx.argc);
+ ioloop = io_loop_create();
+ doveadm_exit_code = 0;
+
+ cctx.local_ip = conn->conn.local_ip;
+ cctx.local_port = conn->conn.local_port;
+ cctx.remote_ip = conn->conn.remote_ip;
+ cctx.remote_port = conn->conn.remote_port;
+
+ if (doveadm_cmd_param_str(&cctx, "user", &user))
+ i_info("Executing command '%s' as '%s'", cctx.cmd->name, user);
+ else
+ i_info("Executing command '%s'", cctx.cmd->name);
+ client_connection_set_proctitle(&conn->conn, cctx.cmd->name);
+ cctx.cmd->cmd(&cctx);
+ client_connection_set_proctitle(&conn->conn, "");
+
+ o_stream_switch_ioloop_to(req->output, prev_ioloop);
+ io_loop_destroy(&ioloop);
+
+ if ((cctx.cmd->flags & CMD_FLAG_NO_PRINT) == 0)
+ doveadm_print_deinit();
+ if (o_stream_finish(doveadm_print_ostream) < 0) {
+ i_info("Error writing output in command %s: %s",
+ req->cmd->name,
+ o_stream_get_error(req->output));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+
+ is = iostream_temp_finish(&doveadm_print_ostream, 4096);
+
+ if (req->first_row == TRUE)
+ req->first_row = FALSE;
+ else
+ o_stream_nsend_str(req->output,",");
+
+ if (doveadm_exit_code != 0) {
+ if (doveadm_exit_code == 0 || doveadm_exit_code == EX_TEMPFAIL)
+ i_error("Command %s failed", req->cmd->name);
+ doveadm_http_server_json_error(req, "exitCode");
+ } else {
+ doveadm_http_server_json_success(req, is);
+ }
+ i_stream_unref(&is);
+}
+
+static int
+request_json_parse_init(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type != JSON_TYPE_ARRAY) {
+ /* request must be a JSON array */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Request must be a JSON array");
+ return -1;
+ }
+ req->first_row = TRUE;
+ o_stream_nsend_str(req->output,"[");
+
+ /* next: parse the next command */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD;
+ return 1;
+}
+
+static int
+request_json_parse_cmd(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type == JSON_TYPE_ARRAY_END) {
+ /* end of command list */
+ req->parse_state = CLIENT_REQUEST_PARSE_DONE;
+ return 1;
+ }
+ if (type != JSON_TYPE_ARRAY) {
+ /* command must be an array */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Command must be a JSON array");
+ return -1;
+ }
+ req->method_err = 0;
+ p_free_and_null(req->pool, req->method_id);
+ req->cmd = NULL;
+ doveadm_cmd_params_clean(&req->pargv);
+
+ /* next: parse the command name */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_NAME;
+ return 1;
+}
+
+static int
+request_json_parse_cmd_name(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ const struct doveadm_cmd_ver2 *ccmd;
+ struct doveadm_cmd_param *param;
+ bool found;
+ int pargc, ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type != JSON_TYPE_STRING) {
+ /* command name must be a string */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Command name must be a string");
+ return -1;
+ }
+
+ /* see if we can find it */
+ found = FALSE;
+ array_foreach(&doveadm_cmds_ver2, ccmd) {
+ if (i_strccdascmp(ccmd->name, value) == 0) {
+ req->cmd = ccmd;
+ found = TRUE;
+ break;
+ }
+ }
+ if (!found) {
+ /* command not found; skip to the command ID */
+ json_parse_skip_next(req->json_parser);
+ req->method_err = 404;
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_ID;
+ return 1;
+ }
+
+ /* initialize pargv */
+ for (pargc = 0; req->cmd->parameters[pargc].name != NULL; pargc++) {
+ param = array_append_space(&req->pargv);
+ *param = req->cmd->parameters[pargc];
+ param->value_set = FALSE;
+ }
+
+ /* next: parse the command parameters */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAMS;
+ return 1;
+}
+
+static int
+request_json_parse_cmd_params(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type == JSON_TYPE_OBJECT_END) {
+ /* empty command parameters object; parse command ID next */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_ID;
+ return 1;
+ }
+ if (type != JSON_TYPE_OBJECT) {
+ /* parameters must be contained in an object */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Parameters must be contained in a JSON object");
+ return -1;
+ }
+
+ /* next: parse parameter key */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+}
+
+static int
+request_json_parse_cmd_param_key(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ struct doveadm_cmd_param *par;
+ bool found;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type == JSON_TYPE_OBJECT_END) {
+ /* end of parameters; parse command ID next */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_ID;
+ return 1;
+ }
+ i_assert(type == JSON_TYPE_OBJECT_KEY);
+ /* find the parameter */
+ found = FALSE;
+ array_foreach_modifiable(&req->pargv, par) {
+ if (i_strccdascmp(par->name, value) == 0) {
+ req->cmd_param = par;
+ found = TRUE;
+ break;
+ }
+ }
+ if (found && req->cmd_param->value_set) {
+ /* it's already set, cannot have same key twice in json */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Parameter `%s' is duplicated",
+ req->cmd_param->name);
+ return -1;
+ }
+ /* skip remaining parameters if error has already occurred */
+ if (!found || req->method_err != 0) {
+ json_parse_skip_next(req->json_parser);
+ req->method_err = 400;
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+ }
+
+ /* next: parse parameter value */
+ req->value_is_array = FALSE;
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_VALUE;
+ return 1;
+}
+
+static int
+request_json_parse_param_value(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if (req->cmd_param->type == CMD_PARAM_ISTREAM) {
+ struct istream* is[2] = {0};
+
+ /* read the value as a stream */
+ ret = json_parse_next_stream(req->json_parser, &is[0]);
+ if (ret <= 0)
+ return ret;
+
+ req->cmd_param->value.v_istream =
+ i_stream_create_seekable_path(is,
+ IO_BLOCK_SIZE, "/tmp/doveadm.");
+ i_stream_unref(&is[0]);
+ req->cmd_param->value_set = TRUE;
+
+ /* read the seekable stream to its end so that the underlying
+ json istream is read to its conclusion. */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_ISTREAM;
+ return 1;
+ }
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (req->cmd_param->type == CMD_PARAM_ARRAY) {
+ const char *tmp;
+
+ /* expects either a singular value or an array of values */
+ p_array_init(&req->cmd_param->value.v_array, req->pool, 1);
+ req->cmd_param->value_set = TRUE;
+ if (type == JSON_TYPE_ARRAY) {
+ /* start of array */
+ req->value_is_array = TRUE;
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_ARRAY;
+ return 1;
+ }
+ /* singular value */
+ if (type != JSON_TYPE_STRING) {
+ /* FIXME: should handle other than string too */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Parameter `%s' must be string or array",
+ req->cmd_param->name);
+ return -1;
+ }
+ tmp = p_strdup(req->pool, value);
+ array_push_back(&req->cmd_param->value.v_array, &tmp);
+
+ /* next: continue with the next parameter */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+ }
+
+ /* expects just a value */
+ req->cmd_param->value_set = TRUE;
+ switch(req->cmd_param->type) {
+ case CMD_PARAM_BOOL:
+ req->cmd_param->value.v_bool = (strcmp(value, "true") == 0);
+ break;
+ case CMD_PARAM_INT64:
+ if (str_to_int64(value, &req->cmd_param->value.v_int64) != 0) {
+ req->method_err = 400;
+ }
+ break;
+ case CMD_PARAM_IP:
+ if (net_addr2ip(value, &req->cmd_param->value.v_ip) != 0) {
+ req->method_err = 400;
+ }
+ break;
+ case CMD_PARAM_STR:
+ req->cmd_param->value.v_string = p_strdup(req->pool, value);
+ break;
+ default:
+ break;
+ }
+
+ /* next: continue with the next parameter */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+}
+
+static int
+request_json_parse_param_array(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type == JSON_TYPE_ARRAY_END) {
+ /* end of array: continue with next parameter */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+ }
+ if (type != JSON_TYPE_STRING) {
+ /* array items must be string */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Command parameter array can only contain"
+ "string values");
+ return -1;
+ }
+
+ /* record entry */
+ value = p_strdup(req->pool, value);
+ array_push_back(&req->cmd_param->value.v_array, &value);
+
+ /* next: continue with the next array item */
+ return 1;
+}
+
+static int
+request_json_parse_param_istream(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ struct istream *v_input = req->cmd_param->value.v_istream;
+ const unsigned char *data;
+ size_t size;
+
+ while (i_stream_read_more(v_input, &data, &size) > 0)
+ i_stream_skip(v_input, size);
+ if (!v_input->eof) {
+ /* more to read */
+ return 0;
+ }
+
+ if (v_input->stream_errno != 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(v_input),
+ i_stream_get_error(v_input));
+ req->method_err = 400;
+ if (req->input->stream_errno == 0) {
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Failed to read command parameter data");
+ }
+ return -1;
+ }
+
+ /* next: continue with the next parameter */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_PARAM_KEY;
+ return 1;
+}
+
+static int
+request_json_parse_cmd_id(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type != JSON_TYPE_STRING) {
+ /* command ID must be a string */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Command ID must be a string");
+ return -1;
+ }
+
+ /* next: parse end of command */
+ req->method_id = p_strdup(req->pool, value);
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD_DONE;
+ return 1;
+}
+
+static int
+request_json_parse_cmd_done(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ if (type != JSON_TYPE_ARRAY_END) {
+ /* command array must end here */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Unexpected JSON element at end of command");
+ return -1;
+ }
+
+ /* execute command */
+ doveadm_http_server_command_execute(req);
+
+ /* next: parse next command */
+ req->parse_state = CLIENT_REQUEST_PARSE_CMD;
+ return 1;
+}
+
+static int
+request_json_parse_done(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ if ((ret=json_parse_next(req->json_parser, &type, &value)) <= 0)
+ return ret;
+ /* only gets here when there is spurious additional JSON */
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "Unexpected JSON element in input");
+ return -1;
+}
+
+static int
+doveadm_http_server_json_parse_v1(struct client_request_http *req)
+{
+ /* parser state machine */
+ switch (req->parse_state) {
+ /* command list: '[' */
+ case CLIENT_REQUEST_PARSE_INIT:
+ return request_json_parse_init(req);
+ /* command begin: '[' */
+ case CLIENT_REQUEST_PARSE_CMD:
+ return request_json_parse_cmd(req);
+ /* command name: string */
+ case CLIENT_REQUEST_PARSE_CMD_NAME:
+ return request_json_parse_cmd_name(req);
+ /* command parameters: '{' */
+ case CLIENT_REQUEST_PARSE_CMD_PARAMS:
+ return request_json_parse_cmd_params(req);
+ /* parameter key: string */
+ case CLIENT_REQUEST_PARSE_CMD_PARAM_KEY:
+ return request_json_parse_cmd_param_key(req);
+ /* parameter value */
+ case CLIENT_REQUEST_PARSE_CMD_PARAM_VALUE:
+ return request_json_parse_param_value(req);
+ /* parameter array value */
+ case CLIENT_REQUEST_PARSE_CMD_PARAM_ARRAY:
+ return request_json_parse_param_array(req);
+ /* parameter istream value */
+ case CLIENT_REQUEST_PARSE_CMD_PARAM_ISTREAM:
+ return request_json_parse_param_istream(req);
+ /* command ID: string */
+ case CLIENT_REQUEST_PARSE_CMD_ID:
+ return request_json_parse_cmd_id(req);
+ /* command end: ']' */
+ case CLIENT_REQUEST_PARSE_CMD_DONE:
+ return request_json_parse_cmd_done(req);
+ /* finished parsing request (seen final ']') */
+ case CLIENT_REQUEST_PARSE_DONE:
+ return request_json_parse_done(req);
+ default:
+ break;
+ }
+ i_unreached();
+}
+
+static void
+doveadm_http_server_read_request_v1(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ const char *error;
+ int ret;
+
+ if (req->json_parser == NULL) {
+ req->json_parser = json_parser_init_flags(
+ req->input, JSON_PARSER_NO_ROOT_OBJECT);
+ }
+
+ while ((ret=doveadm_http_server_json_parse_v1(req)) > 0);
+
+ if (http_server_request_get_response(http_sreq) != NULL) {
+ /* already responded */
+ io_remove(&req->io);
+ i_stream_destroy(&req->input);
+ return;
+ }
+ if (!req->input->eof && ret == 0)
+ return;
+ io_remove(&req->io);
+
+ doveadm_cmd_params_clean(&req->pargv);
+
+ if (req->input->stream_errno != 0) {
+ http_server_request_fail_close(http_sreq,
+ 400, "Client disconnected");
+ i_info("read(%s) failed: %s",
+ i_stream_get_name(req->input),
+ i_stream_get_error(req->input));
+ return;
+ }
+
+ if (json_parser_deinit(&req->json_parser, &error) != 0) {
+ http_server_request_fail_text(http_sreq,
+ 400, "Bad Request",
+ "JSON parse error: %s", error);
+ return;
+ }
+
+ i_stream_destroy(&req->input);
+ o_stream_nsend_str(req->output,"]");
+
+ doveadm_http_server_send_response(req);
+}
+
+static void doveadm_http_server_camelcase_value(string_t *value)
+{
+ size_t i, k;
+ char *ptr = str_c_modifiable(value);
+
+ for (i = 0, k = 0; i < strlen(ptr);) {
+ if (ptr[i] == ' ' || ptr[i] == '-') {
+ i++;
+ ptr[k++] = i_toupper(ptr[i++]);
+ } else {
+ ptr[k++] = ptr[i++];
+ }
+ }
+ str_truncate(value, k);
+}
+
+static void
+doveadm_http_server_send_api_v1(struct client_request_http *req)
+{
+ struct ostream *output = req->output;
+ const struct doveadm_cmd_ver2 *cmd;
+ const struct doveadm_cmd_param *par;
+ unsigned int i, k;
+ string_t *tmp;
+ bool sent;
+
+ tmp = str_new(req->pool, 8);
+
+ o_stream_nsend_str(output,"[\n");
+ for (i = 0; i < array_count(&doveadm_cmds_ver2); i++) {
+ cmd = array_idx(&doveadm_cmds_ver2, i);
+ if (i > 0)
+ o_stream_nsend_str(output, ",\n");
+ o_stream_nsend_str(output, "\t{\"command\":\"");
+ json_append_escaped(tmp, cmd->name);
+ doveadm_http_server_camelcase_value(tmp);
+ o_stream_nsend_str(output, str_c(tmp));
+ o_stream_nsend_str(output, "\", \"parameters\":[");
+
+ sent = FALSE;
+ for (k = 0; cmd->parameters[k].name != NULL; k++) {
+ str_truncate(tmp, 0);
+ par = &(cmd->parameters[k]);
+ if ((par->flags & CMD_PARAM_FLAG_DO_NOT_EXPOSE) != 0)
+ continue;
+ if (sent)
+ o_stream_nsend_str(output, ",\n");
+ else
+ o_stream_nsend_str(output, "\n");
+ sent = TRUE;
+ o_stream_nsend_str(output, "\t\t{\"name\":\"");
+ json_append_escaped(tmp, par->name);
+ doveadm_http_server_camelcase_value(tmp);
+ o_stream_nsend_str(output, str_c(tmp));
+ o_stream_nsend_str(output, "\",\"type\":\"");
+ switch(par->type) {
+ case CMD_PARAM_BOOL:
+ o_stream_nsend_str(output, "boolean");
+ break;
+ case CMD_PARAM_INT64:
+ o_stream_nsend_str(output, "integer");
+ break;
+ case CMD_PARAM_ARRAY:
+ o_stream_nsend_str(output, "array");
+ break;
+ case CMD_PARAM_IP:
+ case CMD_PARAM_ISTREAM:
+ case CMD_PARAM_STR:
+ o_stream_nsend_str(output, "string");
+ }
+ o_stream_nsend_str(output, "\"}");
+ }
+ if (k > 0)
+ o_stream_nsend_str(output,"\n\t");
+ o_stream_nsend_str(output,"]}");
+ str_truncate(tmp, 0);
+ }
+ o_stream_nsend_str(output,"\n]");
+ doveadm_http_server_send_response(req);
+}
+
+static void
+doveadm_http_server_options_handler(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ struct http_server_response *http_resp;
+
+ http_resp = http_server_response_create(http_sreq, 200, "OK");
+ http_server_response_add_header(http_resp,
+ "Access-Control-Allow-Origin", "*");
+ http_server_response_add_header(http_resp,
+ "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ http_server_response_add_header(http_resp,
+ "Access-Control-Allow-Request-Headers",
+ "Content-Type, X-API-Key, Authorization");
+ http_server_response_add_header(http_resp,
+ "Access-Control-Allow-Headers",
+ "Content-Type, WWW-Authenticate");
+ http_server_response_submit(http_resp);
+}
+
+static void
+doveadm_http_server_print_mounts(struct client_request_http *req)
+{
+ struct ostream *output = req->output;
+ unsigned int i;
+
+ o_stream_nsend_str(output, "[\n");
+ for (i = 0; i < N_ELEMENTS(doveadm_http_server_mounts); i++) {
+ if (i > 0)
+ o_stream_nsend_str(output, ",\n");
+ o_stream_nsend_str(output, "{\"method\":\"");
+ if (doveadm_http_server_mounts[i].verb == NULL)
+ o_stream_nsend_str(output, "*");
+ else {
+ o_stream_nsend_str(output,
+ doveadm_http_server_mounts[i].verb);
+ }
+ o_stream_nsend_str(output, "\",\"path\":\"");
+ if (doveadm_http_server_mounts[i].path == NULL)
+ o_stream_nsend_str(output, "*");
+ else {
+ o_stream_nsend_str(output,
+ doveadm_http_server_mounts[i].path);
+ }
+ o_stream_nsend_str(output, "\"}");
+ }
+ o_stream_nsend_str(output, "\n]");
+ doveadm_http_server_send_response(req);
+}
+
+/*
+ * Request
+ */
+
+static void doveadm_http_server_send_response(struct client_request_http *req)
+{
+ struct http_server_request *http_sreq = req->http_request;
+ struct http_server_response *http_resp;
+ struct istream *payload = NULL;
+
+ if (req->output != NULL) {
+ if (o_stream_finish(req->output) == -1) {
+ i_info("error writing output: %s",
+ o_stream_get_error(req->output));
+ o_stream_destroy(&req->output);
+ http_server_request_fail(http_sreq,
+ 500, "Internal server error");
+ return;
+ }
+
+ payload = iostream_temp_finish(&req->output,
+ IO_BLOCK_SIZE);
+ }
+
+ http_resp = http_server_response_create(http_sreq, 200, "OK");
+ http_server_response_add_header(http_resp, "Content-Type",
+ "application/json; charset=utf-8");
+
+ if (payload != NULL) {
+ http_server_response_set_payload(http_resp, payload);
+ i_stream_unref(&payload);
+ }
+
+ http_server_response_submit(http_resp);
+}
+
+static void
+doveadm_http_server_request_destroy(struct client_request_http *req)
+{
+ struct client_connection_http *conn = req->conn;
+ struct http_server_request *http_sreq = req->http_request;
+ const struct http_request *http_req =
+ http_server_request_get(http_sreq);
+ struct http_server_response *http_resp =
+ http_server_request_get_response(http_sreq);
+
+ i_assert(conn->request == req);
+
+ if (http_resp != NULL) {
+ const char *agent, *url, *reason;
+ uoff_t size;
+ int status;
+
+ http_server_response_get_status(http_resp, &status, &reason);
+ size = http_server_response_get_total_size(http_resp);
+ agent = http_request_header_get(http_req, "User-Agent");
+ if (agent == NULL) agent = "";
+
+ url = http_url_create(http_req->target.url);
+ i_info("doveadm: %s %s %s \"%s %s "
+ "HTTP/%d.%d\" %d %"PRIuUOFF_T" \"%s\" \"%s\"",
+ net_ip2addr(&conn->conn.remote_ip), "-", "-",
+ http_req->method, http_req->target.url->path,
+ http_req->version_major, http_req->version_minor,
+ status, size, url, agent);
+ }
+ if (req->json_parser != NULL) {
+ const char *error ATTR_UNUSED;
+ (void)json_parser_deinit(&req->json_parser, &error);
+ // we've already failed, ignore error
+ }
+ if (req->output != NULL)
+ o_stream_set_no_error_handling(req->output, TRUE);
+ io_remove(&req->io);
+ o_stream_destroy(&req->output);
+ i_stream_destroy(&req->input);
+
+ http_server_request_unref(&req->http_request);
+ http_server_switch_ioloop(doveadm_http_server);
+
+ pool_unref(&req->pool);
+ conn->request = NULL;
+}
+
+static bool
+doveadm_http_server_auth_basic(struct client_request_http *req,
+ const struct http_auth_credentials *creds)
+{
+ struct client_connection_http *conn = req->conn;
+ const struct doveadm_settings *set = conn->conn.set;
+ string_t *b64_value;
+ char *value;
+
+ if (*set->doveadm_password == '\0') {
+ i_error("Invalid authentication attempt to HTTP API: "
+ "Basic authentication scheme not enabled");
+ return FALSE;
+ }
+
+ b64_value = str_new(conn->conn.pool, 32);
+ value = p_strdup_printf(conn->conn.pool,
+ "doveadm:%s", set->doveadm_password);
+ base64_encode(value, strlen(value), b64_value);
+ if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
+ return TRUE;
+
+ i_error("Invalid authentication attempt to HTTP API "
+ "(using Basic authentication scheme)");
+ return FALSE;
+}
+
+static bool
+doveadm_http_server_auth_api_key(struct client_request_http *req,
+ const struct http_auth_credentials *creds)
+{
+ struct client_connection_http *conn = req->conn;
+ const struct doveadm_settings *set = doveadm_settings;
+ string_t *b64_value;
+
+ if (*set->doveadm_api_key == '\0') {
+ i_error("Invalid authentication attempt to HTTP API: "
+ "X-Dovecot-API authentication scheme not enabled");
+ return FALSE;
+ }
+
+ b64_value = str_new(conn->conn.pool, 32);
+ base64_encode(set->doveadm_api_key,
+ strlen(set->doveadm_api_key), b64_value);
+ if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
+ return TRUE;
+
+ i_error("Invalid authentication attempt to HTTP API "
+ "(using X-Dovecot-API authentication scheme)");
+ return FALSE;
+}
+
+
+static bool
+doveadm_http_server_auth_verify(struct client_request_http *req,
+ const struct http_auth_credentials *creds)
+{
+ /* see if the mech is supported */
+ if (strcasecmp(creds->scheme, "Basic") == 0)
+ return doveadm_http_server_auth_basic(req, creds);
+ if (strcasecmp(creds->scheme, "X-Dovecot-API") == 0)
+ return doveadm_http_server_auth_api_key(req, creds);
+
+ i_error("Unsupported authentication scheme to HTTP API: %s",
+ str_sanitize(creds->scheme, 128));
+ return FALSE;
+}
+
+static bool
+doveadm_http_server_authorize_request(struct client_request_http *req)
+{
+ struct client_connection_http *conn = req->conn;
+ struct http_server_request *http_sreq = req->http_request;
+ bool auth = FALSE;
+ struct http_auth_credentials creds;
+
+ /* no authentication specified */
+ if (doveadm_settings->doveadm_api_key[0] == '\0' &&
+ *conn->conn.set->doveadm_password == '\0') {
+ http_server_request_fail(http_sreq,
+ 500, "Internal Server Error");
+ i_error("No authentication defined in configuration. "
+ "Add API key or password");
+ return FALSE;
+ }
+ if (http_server_request_get_auth(http_sreq, &creds) > 0)
+ auth = doveadm_http_server_auth_verify(req, &creds);
+ if (!auth) {
+ struct http_server_response *http_resp;
+
+ http_resp = http_server_response_create(http_sreq,
+ 401, "Authentication required");
+ if (doveadm_settings->doveadm_api_key[0] != '\0') {
+ http_server_response_add_header(http_resp,
+ "WWW-Authenticate", "X-Dovecot-API"
+ );
+ }
+ if (*conn->conn.set->doveadm_password != '\0') {
+ http_server_response_add_header(http_resp,
+ "WWW-Authenticate", "Basic Realm=\"doveadm\""
+ );
+ }
+ http_server_response_submit(http_resp);
+ }
+ return auth;
+}
+
+static void
+doveadm_http_server_handle_request(void *context,
+ struct http_server_request *http_sreq)
+{
+ struct client_connection_http *conn = context;
+ struct client_request_http *req;
+ const struct http_request *http_req =
+ http_server_request_get(http_sreq);
+ struct doveadm_http_server_mount *ep = NULL;
+ pool_t pool;
+ unsigned int i;
+
+ /* no pipelining possible due to synchronous handling of requests */
+ i_assert(conn->request == NULL);
+
+ pool = pool_alloconly_create("doveadm request", 1024*16);
+ req = p_new(pool, struct client_request_http, 1);
+ req->pool = pool;
+ req->conn = conn;
+
+ req->http_request = http_sreq;
+ http_server_request_ref(req->http_request);
+
+ http_server_request_connection_close(http_sreq, TRUE);
+ http_server_request_set_destroy_callback(http_sreq,
+ doveadm_http_server_request_destroy, req);
+
+ conn->request = req;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_http_server_mounts); i++) {
+ if (doveadm_http_server_mounts[i].verb == NULL ||
+ strcmp(http_req->method,
+ doveadm_http_server_mounts[i].verb) == 0) {
+ if (doveadm_http_server_mounts[i].path == NULL ||
+ strcmp(http_req->target.url->path,
+ doveadm_http_server_mounts[i].path) == 0) {
+ ep = &doveadm_http_server_mounts[i];
+ break;
+ }
+ }
+ }
+
+ if (ep == NULL) {
+ http_server_request_fail(http_sreq,
+ 404, "Path Not Found");
+ return;
+ }
+
+ if (ep->auth == TRUE && !doveadm_http_server_authorize_request(req))
+ return;
+
+ if (strcmp(http_req->method, "POST") == 0) {
+ /* handle request */
+ req->input = http_req->payload;
+ i_stream_set_name(req->input,
+ net_ip2addr(&conn->conn.remote_ip));
+ i_stream_ref(req->input);
+ req->io = io_add_istream(req->input, *ep->handler, req);
+ req->output = iostream_temp_create_named(
+ "/tmp/doveadm.", 0, net_ip2addr(&conn->conn.remote_ip));
+ p_array_init(&req->pargv, req->pool, 5);
+ ep->handler(req);
+ } else {
+ req->output = iostream_temp_create_named(
+ "/tmp/doveadm.", 0, net_ip2addr(&conn->conn.remote_ip));
+ ep->handler(req);
+ }
+}
+
+/*
+ * Connection
+ */
+
+static void doveadm_http_server_connection_destroy(void *context,
+ const char *reason);
+
+static const struct http_server_callbacks doveadm_http_callbacks = {
+ .connection_destroy = doveadm_http_server_connection_destroy,
+ .handle_request = doveadm_http_server_handle_request
+};
+
+static void
+client_connection_http_free(struct client_connection *_conn)
+{
+ struct client_connection_http *conn =
+ (struct client_connection_http *)_conn;
+
+ if (conn->http_conn != NULL) {
+ /* We're not in the lib-http/server's connection destroy
+ callback. */
+ http_server_connection_close(&conn->http_conn,
+ "Server shutting down");
+ }
+}
+
+struct client_connection *
+client_connection_http_create(int fd, bool ssl)
+{
+ struct client_connection_http *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("doveadm client", 1024);
+ conn = p_new(pool, struct client_connection_http, 1);
+
+ if (client_connection_init(&conn->conn,
+ DOVEADM_CONNECTION_TYPE_HTTP, pool, fd) < 0) {
+ pool_unref(&conn->conn.pool);
+ return NULL;
+ }
+ conn->conn.free = client_connection_http_free;
+
+ conn->http_conn = http_server_connection_create(doveadm_http_server,
+ fd, fd, ssl, &doveadm_http_callbacks, conn);
+ return &conn->conn;
+}
+
+static void
+doveadm_http_server_connection_destroy(void *context,
+ const char *reason ATTR_UNUSED)
+{
+ struct client_connection_http *conn =
+ (struct client_connection_http *)context;
+ struct client_connection *bconn = &conn->conn;
+
+ if (conn->http_conn == NULL) {
+ /* already destroying client directly */
+ return;
+ }
+
+ /* HTTP connection is destroyed already now */
+ conn->http_conn = NULL;
+
+ /* destroy the connection itself */
+ client_connection_destroy(&bconn);
+}
+
+/*
+ * Server
+ */
+
+void doveadm_http_server_init(void)
+{
+ struct http_server_settings http_set = {
+ .rawlog_dir = doveadm_settings->doveadm_http_rawlog_dir,
+ };
+
+ doveadm_http_server = http_server_init(&http_set);
+}
+
+void doveadm_http_server_deinit(void)
+{
+ http_server_deinit(&doveadm_http_server);
+}
diff --git a/src/doveadm/client-connection-private.h b/src/doveadm/client-connection-private.h
new file mode 100644
index 0000000..8205531
--- /dev/null
+++ b/src/doveadm/client-connection-private.h
@@ -0,0 +1,22 @@
+#ifndef CLIENT_CONNECTION_PRIVATE_H
+#define CLIENT_CONNECTION_PRIVATE_H
+
+#include "client-connection.h"
+
+bool doveadm_client_is_allowed_command(const struct doveadm_settings *set,
+ const char *cmd_name);
+
+int client_connection_init(struct client_connection *conn,
+ enum doveadm_client_type type, pool_t pool, int fd);
+void client_connection_destroy(struct client_connection **_conn);
+
+void client_connection_set_proctitle(struct client_connection *conn,
+ const char *text);
+
+void doveadm_http_server_init(void);
+void doveadm_http_server_deinit(void);
+
+void doveadm_server_init(void);
+void doveadm_server_deinit(void);
+
+#endif
diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c
new file mode 100644
index 0000000..b80d674
--- /dev/null
+++ b/src/doveadm/client-connection-tcp.c
@@ -0,0 +1,558 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "str.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "iostream-ssl.h"
+#include "ostream-multiplex.h"
+#include "master-service.h"
+#include "master-service-ssl.h"
+#include "mail-storage-service.h"
+#include "doveadm-util.h"
+#include "doveadm-mail.h"
+#include "doveadm-print.h"
+#include "doveadm-server.h"
+#include "client-connection-private.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE (1024*1024)
+
+struct client_connection_tcp {
+ struct client_connection conn;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct ostream *log_out;
+ struct ssl_iostream *ssl_iostream;
+ struct ioloop *ioloop;
+
+ bool handshaked:1;
+ bool preauthenticated:1;
+ bool authenticated:1;
+ bool io_setup:1;
+ bool use_multiplex:1;
+};
+
+static void
+client_connection_tcp_input(struct client_connection_tcp *conn);
+static void
+client_connection_tcp_send_auth_handshake(struct client_connection_tcp *conn);
+static void
+client_connection_tcp_destroy(struct client_connection_tcp **_conn);
+static int
+client_connection_tcp_init_ssl(struct client_connection_tcp *conn);
+
+static failure_callback_t *orig_error_callback, *orig_fatal_callback;
+static failure_callback_t *orig_info_callback, *orig_debug_callback = NULL;
+
+static bool log_recursing = FALSE;
+
+static void ATTR_FORMAT(2, 0)
+doveadm_server_log_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ struct client_connection_tcp *conn = NULL;
+
+ if (doveadm_client != NULL &&
+ doveadm_client->type == DOVEADM_CONNECTION_TYPE_TCP)
+ conn = (struct client_connection_tcp *)doveadm_client;
+
+ if (!log_recursing && conn != NULL &&
+ conn->log_out != NULL) T_BEGIN {
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct ostream *log_out = conn->log_out;
+ char c;
+ const char *ptr;
+ bool corked;
+ va_list va;
+
+ /* prevent re-entering this code if
+ any of the following code causes logging */
+ log_recursing = TRUE;
+
+ /* since we can get here from just about anywhere, make sure
+ the log ostream uses the connection's ioloop. */
+ if (conn->ioloop != NULL)
+ io_loop_set_current(conn->ioloop);
+
+ const char *log_prefix =
+ ctx->log_prefix != NULL ? ctx->log_prefix :
+ i_get_failure_prefix();
+ size_t log_prefix_len = strlen(log_prefix);
+ c = doveadm_log_type_to_char(ctx->type);
+ corked = o_stream_is_corked(log_out);
+
+ va_copy(va, args);
+ const char *str = t_strdup_vprintf(format, va);
+ va_end(va);
+
+ if (!corked)
+ o_stream_cork(log_out);
+ for (;;) {
+ ptr = strchr(str, '\n');
+ size_t len = ptr == NULL ? strlen(str) :
+ (size_t)(ptr - str);
+
+ o_stream_nsend(log_out, &c, 1);
+ o_stream_nsend(log_out, log_prefix, log_prefix_len);
+ o_stream_nsend(log_out, str, len);
+ o_stream_nsend(log_out, "\n", 1);
+
+ if (ptr == NULL)
+ break;
+ str = ptr+1;
+ }
+ o_stream_uncork(log_out);
+ if (corked)
+ o_stream_cork(log_out);
+ io_loop_set_current(prev_ioloop);
+
+ log_recursing = FALSE;
+ } T_END;
+
+ switch(ctx->type) {
+ case LOG_TYPE_DEBUG:
+ orig_debug_callback(ctx, format, args);
+ break;
+ case LOG_TYPE_INFO:
+ orig_info_callback(ctx, format, args);
+ break;
+ case LOG_TYPE_WARNING:
+ case LOG_TYPE_ERROR:
+ orig_error_callback(ctx, format, args);
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void doveadm_server_capture_logs(void)
+{
+ i_assert(orig_debug_callback == NULL);
+ i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback,
+ &orig_info_callback, &orig_debug_callback);
+ i_set_error_handler(doveadm_server_log_handler);
+ i_set_info_handler(doveadm_server_log_handler);
+ i_set_debug_handler(doveadm_server_log_handler);
+}
+
+static void doveadm_server_restore_logs(void)
+{
+ i_assert(orig_debug_callback != NULL);
+ i_set_error_handler(orig_error_callback);
+ i_set_info_handler(orig_info_callback);
+ i_set_debug_handler(orig_debug_callback);
+ orig_fatal_callback = NULL;
+ orig_error_callback = NULL;
+ orig_info_callback = NULL;
+ orig_debug_callback = NULL;
+}
+
+static void
+doveadm_cmd_server_post(struct client_connection_tcp *conn, const char *cmd_name)
+{
+ const char *str = NULL;
+
+ if (doveadm_exit_code == 0) {
+ o_stream_nsend(conn->output, "\n+\n", 3);
+ return;
+ }
+
+ str = doveadm_exit_code_to_str(doveadm_exit_code);
+
+ if (str != NULL) {
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("\n-%s\n", str));
+ } else {
+ o_stream_nsend_str(conn->output, "\n-\n");
+ i_error("BUG: Command '%s' returned unknown error code %d",
+ cmd_name, doveadm_exit_code);
+ }
+}
+
+static void
+doveadm_cmd_server_run_ver2(struct client_connection_tcp *conn,
+ int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx)
+{
+ i_getopt_reset();
+ if (doveadm_cmd_run_ver2(argc, argv, cctx) < 0)
+ doveadm_exit_code = EX_USAGE;
+ doveadm_cmd_server_post(conn, cctx->cmd->name);
+}
+
+static int doveadm_cmd_handle(struct client_connection_tcp *conn,
+ const char *cmd_name,
+ int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx)
+{
+ struct ioloop *prev_ioloop = current_ioloop;
+ const struct doveadm_cmd_ver2 *cmd_ver2;
+
+ if ((cmd_ver2 = doveadm_cmd_find_with_args_ver2(cmd_name, &argc, &argv)) == NULL) {
+ i_error("doveadm: Client sent unknown command: %s", cmd_name);
+ return -1;
+ }
+ cctx->cmd = cmd_ver2;
+
+ /* some commands will want to call io_loop_run(), but we're already
+ running one and we can't call the original one recursively, so
+ create a new ioloop. */
+ conn->ioloop = io_loop_create();
+ o_stream_switch_ioloop(conn->output);
+ if (conn->log_out != NULL)
+ o_stream_switch_ioloop(conn->log_out);
+
+ doveadm_cmd_server_run_ver2(conn, argc, argv, cctx);
+
+ o_stream_switch_ioloop_to(conn->output, prev_ioloop);
+ if (conn->log_out != NULL)
+ o_stream_switch_ioloop_to(conn->log_out, prev_ioloop);
+ io_loop_destroy(&conn->ioloop);
+
+ /* clear all headers */
+ doveadm_print_deinit();
+ doveadm_print_init(DOVEADM_PRINT_TYPE_SERVER);
+
+ /* We already sent the success/failure reply to the client. Return 0
+ so caller never adds another failure reply. */
+ return 0;
+}
+
+static bool client_handle_command(struct client_connection_tcp *conn,
+ const char *const *args)
+{
+ struct doveadm_cmd_context cctx;
+ const char *flags, *cmd_name;
+ unsigned int argc = str_array_length(args);
+
+ if (argc < 3) {
+ i_error("doveadm client: No command given");
+ return FALSE;
+ }
+ i_zero(&cctx);
+ cctx.conn_type = conn->conn.type;
+ cctx.input = conn->input;
+ cctx.output = conn->output;
+ cctx.local_ip = conn->conn.local_ip;
+ cctx.remote_ip = conn->conn.remote_ip;
+ cctx.local_port = conn->conn.local_port;
+ cctx.remote_port = conn->conn.remote_port;
+ doveadm_exit_code = 0;
+
+ flags = args[0];
+ cctx.username = args[1];
+ cmd_name = args[2];
+
+ doveadm_debug = FALSE;
+ doveadm_verbose = FALSE;
+
+ for (; *flags != '\0'; flags++) {
+ switch (*flags) {
+ case 'D':
+ doveadm_debug = TRUE;
+ doveadm_verbose = TRUE;
+ break;
+ case 'v':
+ doveadm_verbose = TRUE;
+ break;
+ default:
+ i_error("doveadm client: Unknown flag: %c", *flags);
+ return FALSE;
+ }
+ }
+
+ if (!doveadm_client_is_allowed_command(conn->conn.set, cmd_name)) {
+ i_error("doveadm client isn't allowed to use command: %s",
+ cmd_name);
+ return FALSE;
+ }
+
+ client_connection_set_proctitle(&conn->conn, cmd_name);
+ o_stream_cork(conn->output);
+ /* Disable IO while running a command. This is required for commands
+ that do IO themselves (e.g. dsync-server). */
+ io_remove(&conn->io);
+ if (doveadm_cmd_handle(conn, cmd_name, argc-2, args+2, &cctx) < 0)
+ o_stream_nsend(conn->output, "\n-\n", 3);
+ o_stream_uncork(conn->output);
+ conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn);
+ client_connection_set_proctitle(&conn->conn, "");
+
+ /* Try to flush the output. It might finish later. */
+ (void)o_stream_flush(conn->output);
+ return TRUE;
+}
+
+static int
+client_connection_tcp_authenticate(struct client_connection_tcp *conn)
+{
+ const struct doveadm_settings *set = conn->conn.set;
+ const char *line, *pass;
+ buffer_t *plain;
+ const unsigned char *data;
+ size_t size;
+
+ if ((line = i_stream_read_next_line(conn->input)) == NULL) {
+ if (conn->input->eof)
+ return -1;
+ return 0;
+ }
+
+ if (*set->doveadm_password == '\0') {
+ i_error("doveadm_password not set, "
+ "remote authentication disabled");
+ return -1;
+ }
+
+ if (strcmp(line, "STARTTLS") == 0) {
+ io_remove(&conn->io);
+ if (client_connection_tcp_init_ssl(conn) < 0)
+ return -1;
+ conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn);
+ return 0;
+ }
+
+ /* FIXME: some day we should probably let auth process do this and
+ support all kinds of authentication */
+ if (!str_begins(line, "PLAIN\t")) {
+ i_error("doveadm client attempted non-PLAIN authentication: %s", line);
+ return -1;
+ }
+
+ plain = t_buffer_create(128);
+ if (base64_decode(line + 6, strlen(line + 6), NULL, plain) < 0) {
+ i_error("doveadm client sent invalid base64 auth PLAIN data");
+ return -1;
+ }
+ data = plain->data;
+ size = plain->used;
+
+ if (size < 10 || data[0] != '\0' ||
+ memcmp(data+1, "doveadm", 7) != 0 || data[8] != '\0') {
+ i_error("doveadm client didn't authenticate as 'doveadm'");
+ return -1;
+ }
+ pass = t_strndup(data + 9, size - 9);
+ if (strlen(pass) != strlen(set->doveadm_password) ||
+ !mem_equals_timing_safe(pass, set->doveadm_password,
+ strlen(pass))) {
+ i_error("doveadm client authenticated with wrong password");
+ return -1;
+ }
+ return 1;
+}
+
+static void client_log_disconnect_error(struct client_connection_tcp *conn)
+{
+ const char *error;
+
+ error = conn->ssl_iostream == NULL ? NULL :
+ ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (error == NULL) {
+ error = conn->input->stream_errno == 0 ? "EOF" :
+ strerror(conn->input->stream_errno);
+ }
+ i_error("doveadm client disconnected before handshake: %s", error);
+}
+
+static void
+client_connection_tcp_input(struct client_connection_tcp *conn)
+{
+ const char *line;
+ bool ok = TRUE;
+ int ret;
+ unsigned int minor;
+
+ if (!conn->handshaked) {
+ if ((line = i_stream_read_next_line(conn->input)) == NULL) {
+ if (conn->input->eof || conn->input->stream_errno != 0) {
+ client_log_disconnect_error(conn);
+ client_connection_tcp_destroy(&conn);
+ }
+ return;
+ }
+ if (!version_string_verify_full(line, "doveadm-server",
+ DOVEADM_SERVER_PROTOCOL_VERSION_MAJOR, &minor)) {
+ i_error("doveadm client not compatible with this server "
+ "(mixed old and new binaries?)");
+ client_connection_tcp_destroy(&conn);
+ return;
+ }
+ if (minor > 0) {
+ /* send version reply */
+ o_stream_nsend_str(conn->output,
+ DOVEADM_CLIENT_PROTOCOL_VERSION_LINE"\n");
+ conn->use_multiplex = TRUE;
+ }
+ client_connection_tcp_send_auth_handshake(conn);
+ conn->handshaked = TRUE;
+ }
+ if (!conn->authenticated) {
+ if ((ret = client_connection_tcp_authenticate(conn)) <= 0) {
+ if (ret < 0) {
+ o_stream_nsend(conn->output, "-\n", 2);
+ client_connection_tcp_destroy(&conn);
+ }
+ return;
+ }
+ o_stream_nsend(conn->output, "+\n", 2);
+ conn->authenticated = TRUE;
+ }
+
+ if (!conn->io_setup) {
+ conn->io_setup = TRUE;
+ if (conn->use_multiplex) {
+ struct ostream *os = conn->output;
+ conn->output = o_stream_create_multiplex(os, SIZE_MAX);
+ o_stream_set_name(conn->output, o_stream_get_name(os));
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ o_stream_unref(&os);
+ conn->log_out =
+ o_stream_multiplex_add_channel(conn->output,
+ DOVEADM_LOG_CHANNEL_ID);
+ o_stream_set_no_error_handling(conn->log_out, TRUE);
+ o_stream_set_name(conn->log_out, t_strdup_printf("%s (log)",
+ o_stream_get_name(conn->output)));
+ doveadm_server_capture_logs();
+ }
+ doveadm_print_ostream = conn->output;
+ }
+
+ while (ok && !conn->input->closed &&
+ (line = i_stream_read_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ const char *const *args;
+
+ args = t_strsplit_tabescaped(line);
+ ok = client_handle_command(conn, args);
+ } T_END;
+ }
+ if (conn->input->eof || conn->input->stream_errno != 0 || !ok)
+ client_connection_tcp_destroy(&conn);
+}
+
+static int
+client_connection_tcp_init_ssl(struct client_connection_tcp *conn)
+{
+ const char *error;
+
+ if (master_service_ssl_init(master_service,
+ &conn->input, &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;
+ }
+ return 0;
+}
+
+static bool
+client_connection_is_preauthenticated(int listen_fd)
+{
+ const char *listen_path;
+ struct stat st;
+
+ /* we'll have to do this with stat(), because at least in Linux
+ fstat() always returns mode as 0777 */
+ return net_getunixname(listen_fd, &listen_path) == 0 &&
+ stat(listen_path, &st) == 0 && S_ISSOCK(st.st_mode) &&
+ (st.st_mode & 0777) == 0600;
+}
+
+static void
+client_connection_tcp_send_auth_handshake(struct client_connection_tcp *conn)
+{
+ if (conn->preauthenticated) {
+ /* no need for client to authenticate */
+ conn->authenticated = TRUE;
+ o_stream_nsend(conn->output, "+\n", 2);
+ } else {
+ o_stream_nsend(conn->output, "-\n", 2);
+ }
+}
+
+static void
+client_connection_tcp_free(struct client_connection *_conn)
+{
+ struct client_connection_tcp *conn =
+ (struct client_connection_tcp *)_conn;
+
+ i_assert(_conn->type == DOVEADM_CONNECTION_TYPE_TCP);
+
+ doveadm_print_deinit();
+ doveadm_print_ostream = NULL;
+
+ if (conn->log_out != NULL) {
+ doveadm_server_restore_logs();
+ o_stream_unref(&conn->log_out);
+ }
+ ssl_iostream_destroy(&conn->ssl_iostream);
+
+ io_remove(&conn->io);
+ o_stream_destroy(&conn->output);
+ i_stream_destroy(&conn->input);
+ i_close_fd(&conn->fd);
+}
+
+struct client_connection *
+client_connection_tcp_create(int fd, int listen_fd, bool ssl)
+{
+ struct client_connection_tcp *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("doveadm client", 1024*16);
+ conn = p_new(pool, struct client_connection_tcp, 1);
+ conn->fd = fd;
+
+ if (client_connection_init(&conn->conn,
+ DOVEADM_CONNECTION_TYPE_TCP, pool, fd) < 0) {
+ client_connection_tcp_destroy(&conn);
+ return NULL;
+ }
+ conn->conn.free = client_connection_tcp_free;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_SERVER);
+
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ i_stream_set_name(conn->input, conn->conn.name);
+ o_stream_set_name(conn->output, conn->conn.name);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ if (ssl) {
+ if (client_connection_tcp_init_ssl(conn) < 0) {
+ client_connection_tcp_destroy(&conn);
+ return NULL;
+ }
+ }
+ /* add IO after SSL istream is created */
+ conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn);
+ conn->preauthenticated =
+ client_connection_is_preauthenticated(listen_fd);
+ client_connection_set_proctitle(&conn->conn, "");
+
+ return &conn->conn;
+}
+
+static void
+client_connection_tcp_destroy(struct client_connection_tcp **_conn)
+{
+ struct client_connection_tcp *conn = *_conn;
+ struct client_connection *bconn = &conn->conn;
+
+ *_conn = NULL;
+ client_connection_destroy(&bconn);
+}
diff --git a/src/doveadm/client-connection.c b/src/doveadm/client-connection.c
new file mode 100644
index 0000000..02a64a6
--- /dev/null
+++ b/src/doveadm/client-connection.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "process-title.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "doveadm.h"
+#include "doveadm-settings.h"
+#include "doveadm-server.h"
+#include "client-connection-private.h"
+
+bool doveadm_client_is_allowed_command(const struct doveadm_settings *set,
+ const char *cmd_name)
+{
+ bool ret = FALSE;
+
+ if (*set->doveadm_allowed_commands == '\0')
+ return TRUE;
+
+ T_BEGIN {
+ const char *const *cmds =
+ t_strsplit(set->doveadm_allowed_commands, ",");
+ for (; *cmds != NULL; cmds++) {
+ if (strcmp(*cmds, cmd_name) == 0) {
+ ret = TRUE;
+ break;
+ }
+ }
+ } T_END;
+ return ret;
+}
+
+static int client_connection_read_settings(struct client_connection *conn)
+{
+ const struct setting_parser_info *set_roots[] = {
+ &doveadm_setting_parser_info,
+ NULL
+ };
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const char *error;
+ void *set;
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.service = "doveadm";
+ input.local_ip = conn->local_ip;
+ input.remote_ip = conn->remote_ip;
+
+ if (master_service_settings_read(master_service, &input,
+ &output, &error) < 0) {
+ i_error("Error reading configuration: %s", error);
+ return -1;
+ }
+ set = master_service_settings_get_others(master_service)[0];
+ conn->set = settings_dup(&doveadm_setting_parser_info, set, conn->pool);
+ return 0;
+}
+
+int client_connection_init(struct client_connection *conn,
+ enum doveadm_client_type type, pool_t pool, int fd)
+{
+ const char *ip;
+
+ i_assert(type != DOVEADM_CONNECTION_TYPE_CLI);
+
+ conn->type = type;
+ conn->pool = pool;
+
+ (void)net_getsockname(fd, &conn->local_ip, &conn->local_port);
+ (void)net_getpeername(fd, &conn->remote_ip, &conn->remote_port);
+
+ ip = net_ip2addr(&conn->remote_ip);
+ if (ip[0] != '\0')
+ i_set_failure_prefix("doveadm(%s): ", ip);
+
+ conn->name = conn->remote_ip.family == 0 ? "<local>" :
+ p_strdup(pool, net_ip2addr(&conn->remote_ip));
+
+ return client_connection_read_settings(conn);
+}
+
+void client_connection_destroy(struct client_connection **_conn)
+{
+ struct client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->free != NULL)
+ conn->free(conn);
+
+ doveadm_client = NULL;
+ master_service_client_connection_destroyed(master_service);
+
+ if (doveadm_verbose_proctitle)
+ process_title_set("[idling]");
+
+ pool_unref(&conn->pool);
+}
+
+void client_connection_set_proctitle(struct client_connection *conn,
+ const char *text)
+{
+ const char *str;
+
+ if (!doveadm_verbose_proctitle)
+ return;
+
+ if (text[0] == '\0')
+ str = t_strdup_printf("[%s]", conn->name);
+ else
+ str = t_strdup_printf("[%s %s]", conn->name, text);
+ process_title_set(str);
+}
+
+void doveadm_server_init(void)
+{
+ doveadm_http_server_init();
+}
+
+void doveadm_server_deinit(void)
+{
+ if (doveadm_client != NULL)
+ client_connection_destroy(&doveadm_client);
+ doveadm_http_server_deinit();
+}
diff --git a/src/doveadm/client-connection.h b/src/doveadm/client-connection.h
new file mode 100644
index 0000000..bb3797e
--- /dev/null
+++ b/src/doveadm/client-connection.h
@@ -0,0 +1,26 @@
+#ifndef CLIENT_CONNECTION_H
+#define CLIENT_CONNECTION_H
+
+#include "net.h"
+
+#define DOVEADM_LOG_CHANNEL_ID 'L'
+
+struct client_connection {
+ pool_t pool;
+ enum doveadm_client_type type;
+ const char *name;
+
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+
+ const struct doveadm_settings *set;
+
+ void (*free)(struct client_connection *conn);
+};
+
+struct client_connection *
+client_connection_tcp_create(int fd, int listen_fd, bool ssl);
+struct client_connection *
+client_connection_http_create(int fd, bool ssl);
+
+#endif
diff --git a/src/doveadm/doveadm-auth-server.c b/src/doveadm/doveadm-auth-server.c
new file mode 100644
index 0000000..f50fa5f
--- /dev/null
+++ b/src/doveadm/doveadm-auth-server.c
@@ -0,0 +1,517 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "wildcard-match.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "auth-client.h"
+#include "auth-master.h"
+#include "master-auth.h"
+#include "master-login-auth.h"
+#include "mail-storage-service.h"
+#include "mail-user.h"
+#include "ostream.h"
+#include "json-parser.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+struct authtest_input {
+ pool_t pool;
+ const char *username;
+ const char *master_user;
+ const char *password;
+ struct auth_user_info info;
+ bool success;
+
+ struct auth_client_request *request;
+ struct master_auth_request master_auth_req;
+
+ unsigned int auth_id;
+ unsigned int auth_pid;
+ const char *auth_cookie;
+
+};
+
+static struct auth_master_connection *
+doveadm_get_auth_master_conn(const char *auth_socket_path)
+{
+ enum auth_master_flags flags = 0;
+
+ if (doveadm_debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+ return auth_master_init(auth_socket_path, flags);
+}
+
+static int
+cmd_user_input(struct auth_master_connection *conn,
+ const struct authtest_input *input,
+ const char *show_field, bool userdb)
+{
+ const char *lookup_name = userdb ? "userdb lookup" : "passdb lookup";
+ pool_t pool;
+ const char *updated_username = NULL, *const *fields, *p;
+ int ret;
+
+ pool = pool_alloconly_create("auth master lookup", 1024);
+
+ if (userdb) {
+ ret = auth_master_user_lookup(conn, input->username, &input->info,
+ pool, &updated_username, &fields);
+ } else {
+ ret = auth_master_pass_lookup(conn, input->username, &input->info,
+ pool, &fields);
+ }
+ if (ret < 0) {
+ const char *msg;
+ if (fields[0] == NULL) {
+ msg = t_strdup_printf("\"error\":\"%s failed\"",
+ lookup_name);
+ } else {
+ msg = t_strdup_printf("\"error\":\"%s failed: %s\"",
+ lookup_name,
+ fields[0]);
+ }
+ o_stream_nsend_str(doveadm_print_ostream, msg);
+ ret = -1;
+ } else if (ret == 0) {
+ o_stream_nsend_str(doveadm_print_ostream,
+ t_strdup_printf("\"error\":\"%s: user doesn't exist\"",
+ lookup_name));
+ } else if (show_field != NULL) {
+ size_t show_field_len = strlen(show_field);
+ string_t *json_field = t_str_new(show_field_len+1);
+ json_append_escaped(json_field, show_field);
+ o_stream_nsend_str(doveadm_print_ostream, t_strdup_printf("\"%s\":", str_c(json_field)));
+ for (; *fields != NULL; fields++) {
+ if (strncmp(*fields, show_field, show_field_len) == 0 &&
+ (*fields)[show_field_len] == '=') {
+ string_t *jsonval = t_str_new(32);
+ json_append_escaped(jsonval, *fields + show_field_len + 1);
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jsonval));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ }
+ }
+ } else {
+ string_t *jsonval = t_str_new(64);
+ o_stream_nsend_str(doveadm_print_ostream, "\"source\":\"");
+ o_stream_nsend_str(doveadm_print_ostream, userdb ? "userdb\"" : "passdb\"");
+
+ if (updated_username != NULL) {
+ o_stream_nsend_str(doveadm_print_ostream, ",\"updated_username\":\"");
+ str_truncate(jsonval, 0);
+ json_append_escaped(jsonval, updated_username);
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jsonval));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ }
+ for (; *fields != NULL; fields++) {
+ const char *field = *fields;
+ if (*field == '\0') continue;
+ p = strchr(*fields, '=');
+ str_truncate(jsonval, 0);
+ if (p != NULL) {
+ field = t_strcut(*fields, '=');
+ }
+ str_truncate(jsonval, 0);
+ json_append_escaped(jsonval, field);
+ o_stream_nsend_str(doveadm_print_ostream, ",\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jsonval));
+ o_stream_nsend_str(doveadm_print_ostream, "\":");
+ if (p != NULL) {
+ str_truncate(jsonval, 0);
+ json_append_escaped(jsonval, p+1);
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jsonval));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ } else {
+ o_stream_nsend_str(doveadm_print_ostream, "true");
+ }
+ }
+ }
+ return ret;
+}
+
+static void auth_user_info_parse(struct auth_user_info *info, const char *arg)
+{
+ if (str_begins(arg, "service="))
+ info->service = arg + 8;
+ else if (str_begins(arg, "lip=")) {
+ if (net_addr2ip(arg + 4, &info->local_ip) < 0)
+ i_fatal("lip: Invalid ip");
+ } else if (str_begins(arg, "rip=")) {
+ if (net_addr2ip(arg + 4, &info->remote_ip) < 0)
+ i_fatal("rip: Invalid ip");
+ } else if (str_begins(arg, "lport=")) {
+ if (net_str2port(arg + 6, &info->local_port) < 0)
+ i_fatal("lport: Invalid port number");
+ } else if (str_begins(arg, "rport=")) {
+ if (net_str2port(arg + 6, &info->remote_port) < 0)
+ i_fatal("rport: Invalid port number");
+ } else {
+ i_fatal("Unknown -x argument: %s", arg);
+ }
+}
+
+static void
+cmd_user_list(struct auth_master_connection *conn,
+ const struct authtest_input *input,
+ char *const *users)
+{
+ struct auth_master_user_list_ctx *ctx;
+ const char *username, *user_mask = "*";
+ string_t *escaped = t_str_new(256);
+ bool first = TRUE;
+ unsigned int i;
+
+ if (users[0] != NULL && users[1] == NULL)
+ user_mask = users[0];
+
+ o_stream_nsend_str(doveadm_print_ostream, "{\"userList\":[");
+
+ ctx = auth_master_user_list_init(conn, user_mask, &input->info);
+ while ((username = auth_master_user_list_next(ctx)) != NULL) {
+ for (i = 0; users[i] != NULL; i++) {
+ if (wildcard_match_icase(username, users[i]))
+ break;
+ }
+ if (users[i] != NULL) {
+ if (first)
+ first = FALSE;
+ else
+ o_stream_nsend_str(doveadm_print_ostream, ",");
+ str_truncate(escaped, 0);
+ str_append_c(escaped, '"');
+ json_append_escaped(escaped, username);
+ str_append_c(escaped, '"');
+ o_stream_nsend(doveadm_print_ostream, escaped->data, escaped->used);
+ }
+ }
+ if (auth_master_user_list_deinit(&ctx) < 0) {
+ i_error("user listing failed");
+ doveadm_exit_code = EX_DATAERR;
+ }
+
+ o_stream_nsend_str(doveadm_print_ostream, "]}");
+}
+
+static void cmd_auth_cache_flush(struct doveadm_cmd_context *cctx)
+{
+ const char *master_socket_path, *const *users;
+ struct auth_master_connection *conn;
+ unsigned int count;
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &master_socket_path)) {
+ master_socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/auth-master", NULL);
+ }
+ if (!doveadm_cmd_param_array(cctx, "user", &users))
+ i_fatal("Missing user parameter");
+
+ conn = doveadm_get_auth_master_conn(master_socket_path);
+ if (auth_master_cache_flush(conn, users, &count) < 0) {
+ i_error("Cache flush failed");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else {
+ doveadm_print_init("formatted");
+ doveadm_print_formatted_set_format("%{entries} cache entries flushed\n");
+ doveadm_print_header_simple("entries");
+ doveadm_print_num(count);
+ }
+ auth_master_deinit(&conn);
+}
+
+static void cmd_user_mail_input_field(const char *key, const char *value,
+ const char *show_field, bool *first)
+{
+ string_t *jvalue = t_str_new(128);
+ if (show_field != NULL && strcmp(show_field, key) != 0) return;
+ /* do not emit comma on first field. we need to keep track
+ of when the first field actually gets printed as it
+ might change due to show_field */
+ if (!*first)
+ o_stream_nsend_str(doveadm_print_ostream, ",");
+ *first = FALSE;
+ json_append_escaped(jvalue, key);
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jvalue));
+ o_stream_nsend_str(doveadm_print_ostream, "\":\"");
+ str_truncate(jvalue, 0);
+ json_append_escaped(jvalue, value);
+ o_stream_nsend_str(doveadm_print_ostream, str_c(jvalue));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+}
+
+static void
+cmd_user_mail_print_fields(const struct authtest_input *input,
+ struct mail_user *user,
+ const char *const *userdb_fields,
+ const char *show_field)
+{
+ const struct mail_storage_settings *mail_set;
+ const char *key, *value;
+ unsigned int i;
+ bool first = TRUE;
+
+ if (strcmp(input->username, user->username) != 0)
+ cmd_user_mail_input_field("user", user->username, show_field, &first);
+ cmd_user_mail_input_field("uid", user->set->mail_uid, show_field, &first);
+ cmd_user_mail_input_field("gid", user->set->mail_gid, show_field, &first);
+ cmd_user_mail_input_field("home", user->set->mail_home, show_field, &first);
+
+ mail_set = mail_user_set_get_storage_set(user);
+ cmd_user_mail_input_field("mail", mail_set->mail_location, show_field, &first);
+
+ if (userdb_fields != NULL) {
+ for (i = 0; userdb_fields[i] != NULL; i++) {
+ value = strchr(userdb_fields[i], '=');
+ if (value != NULL)
+ key = t_strdup_until(userdb_fields[i], value++);
+ else {
+ key = userdb_fields[i];
+ value = "";
+ }
+ if (strcmp(key, "uid") != 0 &&
+ strcmp(key, "gid") != 0 &&
+ strcmp(key, "home") != 0 &&
+ strcmp(key, "mail") != 0 &&
+ *key != '\0') {
+ cmd_user_mail_input_field(key, value, show_field, &first);
+ }
+ }
+ }
+}
+
+static int
+cmd_user_mail_input(struct mail_storage_service_ctx *storage_service,
+ const struct authtest_input *input,
+ const char *show_field, const char *expand_field)
+{
+ struct mail_storage_service_input service_input;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ const char *error, *const *userdb_fields;
+ pool_t pool;
+ int ret;
+
+ i_zero(&service_input);
+ service_input.module = "mail";
+ service_input.service = input->info.service;
+ service_input.username = input->username;
+ service_input.local_ip = input->info.local_ip;
+ service_input.local_port = input->info.local_port;
+ service_input.remote_ip = input->info.remote_ip;
+ service_input.remote_port = input->info.remote_port;
+ service_input.debug = input->info.debug;
+
+ pool = pool_alloconly_create("userdb fields", 1024);
+ mail_storage_service_save_userdb_fields(storage_service, pool,
+ &userdb_fields);
+
+ if ((ret = mail_storage_service_lookup_next(storage_service, &service_input,
+ &service_user, &user,
+ &error)) <= 0) {
+ pool_unref(&pool);
+ if (ret < 0)
+ return -1;
+ string_t *username = t_str_new(32);
+ json_append_escaped(username, input->username);
+ o_stream_nsend_str(doveadm_print_ostream,
+ t_strdup_printf("\"error\":\"userdb lookup: user %s doesn't exist\"", str_c(username))
+ );
+ return 0;
+ }
+
+ if (expand_field == NULL)
+ cmd_user_mail_print_fields(input, user, userdb_fields, show_field);
+ else {
+ string_t *str = t_str_new(128);
+ if (var_expand_with_funcs(str, expand_field,
+ mail_user_var_expand_table(user),
+ mail_user_var_expand_func_table, user,
+ &error) <= 0) {
+ string_t *str = t_str_new(128);
+ str_printfa(str, "\"error\":\"Failed to expand field: ");
+ json_append_escaped(str, error);
+ str_append_c(str, '"');
+ o_stream_nsend(doveadm_print_ostream, str_data(str), str_len(str));
+ } else {
+ string_t *value = t_str_new(128);
+ json_append_escaped(value, expand_field);
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(value));
+ o_stream_nsend_str(doveadm_print_ostream, "\":\"");
+ str_truncate(value, 0);
+ json_append_escaped(value, str_c(str));
+ o_stream_nsend_str(doveadm_print_ostream, str_c(value));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ }
+
+ }
+
+ mail_user_deinit(&user);
+ mail_storage_service_user_unref(&service_user);
+ pool_unref(&pool);
+ return 1;
+}
+
+static void cmd_user_ver2(struct doveadm_cmd_context *cctx)
+{
+ const char * const *optval;
+
+ const char *auth_socket_path = NULL;
+ struct auth_master_connection *conn;
+ struct authtest_input input;
+ const char *show_field = NULL, *expand_field = NULL;
+ struct mail_storage_service_ctx *storage_service = NULL;
+ bool have_wildcards, userdb_only = FALSE, first = TRUE;
+ int ret;
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &auth_socket_path))
+ auth_socket_path = doveadm_settings->auth_socket_path;
+
+ (void)doveadm_cmd_param_str(cctx, "expand-field", &expand_field);
+ (void)doveadm_cmd_param_str(cctx, "field", &show_field);
+ (void)doveadm_cmd_param_bool(cctx, "userdb-only", &userdb_only);
+
+ i_zero(&input);
+ if (doveadm_cmd_param_array(cctx, "auth-info", &optval))
+ for(;*optval != NULL; optval++)
+ auth_user_info_parse(&input.info, *optval);
+
+ if (!doveadm_cmd_param_array(cctx, "user-mask", &optval)) {
+ doveadm_exit_code = EX_USAGE;
+ i_error("No user(s) specified");
+ return;
+ }
+
+ if (expand_field != NULL && userdb_only) {
+ i_error("-e can't be used with -u");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+ if (expand_field != NULL && show_field != NULL) {
+ i_error("-e can't be used with -f");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ conn = doveadm_get_auth_master_conn(auth_socket_path);
+
+ have_wildcards = FALSE;
+
+ for(const char *const *val = optval; *val != NULL; val++) {
+ if (strchr(*val, '*') != NULL ||
+ strchr(*val, '?') != NULL) {
+ have_wildcards = TRUE;
+ break;
+ }
+ }
+
+ if (have_wildcards) {
+ cmd_user_list(conn, &input, (char*const*)optval);
+ auth_master_deinit(&conn);
+ return;
+ }
+
+ if (!userdb_only) {
+ storage_service = mail_storage_service_init(master_service, NULL,
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES |
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS);
+ mail_storage_service_set_auth_conn(storage_service, conn);
+ conn = NULL;
+ }
+
+ string_t *json = t_str_new(64);
+ o_stream_nsend_str(doveadm_print_ostream, "{");
+
+ input.info.local_ip = cctx->local_ip;
+ input.info.local_port = cctx->local_port;
+ input.info.remote_ip = cctx->remote_ip;
+ input.info.remote_port = cctx->remote_port;
+
+ for(const char *const *val = optval; *val != NULL; val++) {
+ str_truncate(json, 0);
+ json_append_escaped(json, *val);
+
+ input.username = *val;
+ if (first)
+ first = FALSE;
+ else
+ o_stream_nsend_str(doveadm_print_ostream, ",");
+
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, str_c(json));
+ o_stream_nsend_str(doveadm_print_ostream, "\"");
+ o_stream_nsend_str(doveadm_print_ostream, ":{");
+
+ ret = !userdb_only ?
+ cmd_user_mail_input(storage_service, &input, show_field, expand_field) :
+ cmd_user_input(conn, &input, show_field, TRUE);
+
+ o_stream_nsend_str(doveadm_print_ostream, "}");
+
+ switch (ret) {
+ case -1:
+ doveadm_exit_code = EX_TEMPFAIL;
+ break;
+ case 0:
+ doveadm_exit_code = EX_NOUSER;
+ break;
+ }
+ }
+
+ o_stream_nsend_str(doveadm_print_ostream,"}");
+
+ if (storage_service != NULL)
+ mail_storage_service_deinit(&storage_service);
+ if (conn != NULL)
+ auth_master_deinit(&conn);
+}
+
+static
+struct doveadm_cmd_ver2 doveadm_cmd_auth_server[] = {
+{
+ .name = "auth cache flush",
+ .cmd = cmd_auth_cache_flush,
+ .usage = "[-a <master socket path>] [<user> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "user",
+ .cmd = cmd_user_ver2,
+ .usage = "[-a <userdb socket path>] [-x <auth info>] [-f field] [-e <value>] [-u] <user mask> [...]",
+ .flags = CMD_FLAG_NO_PRINT,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('x', "auth-info", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('f', "field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('e', "expand-field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('u', "userdb-only", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "user-mask", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+void doveadm_register_auth_server_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_auth_server); i++) {
+ doveadm_cmd_register_ver2(&doveadm_cmd_auth_server[i]);
+ }
+}
diff --git a/src/doveadm/doveadm-auth.c b/src/doveadm/doveadm-auth.c
new file mode 100644
index 0000000..d89befe
--- /dev/null
+++ b/src/doveadm/doveadm-auth.c
@@ -0,0 +1,787 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "askpass.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "wildcard-match.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "auth-client.h"
+#include "auth-master.h"
+#include "master-auth.h"
+#include "master-login-auth.h"
+#include "mail-storage-service.h"
+#include "mail-user.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static struct event_category event_category_auth = {
+ .name = "auth",
+};
+
+struct authtest_input {
+ pool_t pool;
+ const char *username;
+ const char *master_user;
+ const char *password;
+ struct auth_user_info info;
+ bool success;
+
+ struct auth_client_request *request;
+ struct master_auth_request master_auth_req;
+
+ unsigned int auth_id;
+ unsigned int auth_pid;
+ const char *auth_cookie;
+
+};
+
+static void auth_cmd_help(struct doveadm_cmd_context *cctx);
+
+static struct auth_master_connection *
+doveadm_get_auth_master_conn(const char *auth_socket_path)
+{
+ enum auth_master_flags flags = 0;
+
+ if (doveadm_debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+ return auth_master_init(auth_socket_path, flags);
+}
+
+static int
+cmd_user_input(struct auth_master_connection *conn,
+ const struct authtest_input *input,
+ const char *show_field, bool userdb)
+{
+ const char *lookup_name = userdb ? "userdb lookup" : "passdb lookup";
+ pool_t pool;
+ const char *updated_username = NULL, *const *fields, *p;
+ int ret;
+
+ pool = pool_alloconly_create("auth master lookup", 1024);
+
+ if (userdb) {
+ ret = auth_master_user_lookup(conn, input->username, &input->info,
+ pool, &updated_username, &fields);
+ } else {
+ ret = auth_master_pass_lookup(conn, input->username, &input->info,
+ pool, &fields);
+ }
+ if (ret < 0) {
+ if (fields[0] == NULL)
+ i_error("%s failed for %s", lookup_name, input->username);
+ else {
+ i_error("%s failed for %s: %s", lookup_name,
+ input->username, fields[0]);
+ }
+ ret = -1;
+ } else if (ret == 0) {
+ fprintf(show_field == NULL ? stdout : stderr,
+ "%s: user %s doesn't exist\n", lookup_name,
+ input->username);
+ } else if (show_field != NULL) {
+ size_t show_field_len = strlen(show_field);
+
+ for (; *fields != NULL; fields++) {
+ if (strncmp(*fields, show_field, show_field_len) == 0 &&
+ (*fields)[show_field_len] == '=')
+ printf("%s\n", *fields + show_field_len + 1);
+ }
+ } else {
+ printf("%s: %s\n", userdb ? "userdb" : "passdb", input->username);
+
+ if (updated_username != NULL)
+ printf(" %-10s: %s\n", "user", updated_username);
+ for (; *fields != NULL; fields++) {
+ p = strchr(*fields, '=');
+ if (p == NULL)
+ printf(" %-10s\n", *fields);
+ else {
+ printf(" %-10s: %s\n",
+ t_strcut(*fields, '='), p + 1);
+ }
+ }
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+static void
+auth_callback(struct auth_client_request *request ATTR_UNUSED,
+ enum auth_request_status status,
+ const char *data_base64 ATTR_UNUSED,
+ const char *const *args, void *context)
+{
+ struct authtest_input *input = context;
+
+ input->request = NULL;
+ input->auth_id = auth_client_request_get_id(request);
+ input->auth_pid = auth_client_request_get_server_pid(request);
+ input->auth_cookie = input->pool == NULL ? NULL :
+ p_strdup(input->pool, auth_client_request_get_cookie(request));
+
+ if (!io_loop_is_running(current_ioloop))
+ return;
+
+ if (status == 0)
+ i_fatal("passdb expects SASL continuation");
+
+ switch (status) {
+ case AUTH_REQUEST_STATUS_ABORT:
+ i_unreached();
+ case AUTH_REQUEST_STATUS_INTERNAL_FAIL:
+ case AUTH_REQUEST_STATUS_FAIL:
+ printf("passdb: %s auth failed\n", input->username);
+ break;
+ case AUTH_REQUEST_STATUS_CONTINUE:
+ printf("passdb: %s auth unexpectedly requested continuation\n",
+ input->username);
+ break;
+ case AUTH_REQUEST_STATUS_OK:
+ input->success = TRUE;
+ printf("passdb: %s auth succeeded\n", input->username);
+ break;
+ }
+
+ if (args != NULL && *args != NULL) {
+ printf("extra fields:\n");
+ for (; *args != NULL; args++)
+ printf(" %s\n", *args);
+ }
+ io_loop_stop(current_ioloop);
+}
+
+static void auth_connected(struct auth_client *client,
+ bool connected, void *context)
+{
+ struct event *event_auth;
+ struct authtest_input *input = context;
+ struct auth_request_info info;
+ string_t *init_resp, *base64_resp;
+
+ if (!connected)
+ i_fatal("Couldn't connect to auth socket");
+ event_auth = event_create(NULL);
+ event_add_category(event_auth, &event_category_auth);
+
+ init_resp = t_str_new(128);
+ str_append(init_resp, input->username);
+ str_append_c(init_resp, '\0');
+ if (input->master_user != NULL)
+ str_append(init_resp, input->master_user);
+ else
+ str_append(init_resp, input->username);
+ str_append_c(init_resp, '\0');
+ str_append(init_resp, input->password);
+
+ base64_resp = t_str_new(128);
+ base64_encode(str_data(init_resp), str_len(init_resp), base64_resp);
+
+ i_zero(&info);
+ info.mech = "PLAIN";
+ info.service = input->info.service;
+ info.session_id = input->info.session_id;
+ info.local_name = input->info.local_name;
+ info.local_ip = input->info.local_ip;
+ info.local_port = input->info.local_port;
+ info.remote_ip = input->info.remote_ip;
+ info.remote_port = input->info.remote_port;
+ info.real_local_ip = input->info.real_local_ip;
+ info.real_remote_ip = input->info.real_remote_ip;
+ info.real_local_port = input->info.real_local_port;
+ info.real_remote_port = input->info.real_remote_port;
+ info.extra_fields = input->info.extra_fields;
+ info.forward_fields = input->info.forward_fields;
+ info.initial_resp_base64 = str_c(base64_resp);
+ if (doveadm_settings->auth_debug ||
+ event_want_debug_log(event_auth))
+ info.flags |= AUTH_REQUEST_FLAG_DEBUG;
+
+ input->request = auth_client_request_new(client, &info,
+ auth_callback, input);
+ event_unref(&event_auth);
+}
+
+static void
+cmd_auth_input(const char *auth_socket_path, struct authtest_input *input)
+{
+ struct auth_client *client;
+
+ if (auth_socket_path == NULL) {
+ auth_socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/auth-client", NULL);
+ }
+
+ client = auth_client_init(auth_socket_path, getpid(), FALSE);
+ auth_client_connect(client);
+ auth_client_set_connect_notify(client, auth_connected, input);
+
+ if (!auth_client_is_disconnected(client))
+ io_loop_run(current_ioloop);
+
+ auth_client_set_connect_notify(client, NULL, NULL);
+ auth_client_deinit(&client);
+}
+
+static void
+auth_user_info_parse_arg(struct auth_user_info *info, const char *arg)
+{
+ if (str_begins(arg, "service="))
+ info->service = arg + 8;
+ else if (str_begins(arg, "session="))
+ info->session_id = arg + 8;
+ else if (str_begins(arg, "local_name="))
+ info->local_name = arg + 11;
+ else if (str_begins(arg, "lip=")) {
+ if (net_addr2ip(arg + 4, &info->local_ip) < 0)
+ i_fatal("lip: Invalid ip");
+ } else if (str_begins(arg, "rip=")) {
+ if (net_addr2ip(arg + 4, &info->remote_ip) < 0)
+ i_fatal("rip: Invalid ip");
+ } else if (str_begins(arg, "lport=")) {
+ if (net_str2port(arg + 6, &info->local_port) < 0)
+ i_fatal("lport: Invalid port number");
+ } else if (str_begins(arg, "rport=")) {
+ if (net_str2port(arg + 6, &info->remote_port) < 0)
+ i_fatal("rport: Invalid port number");
+ } else if (str_begins(arg, "real_lip=")) {
+ if (net_addr2ip(arg + 9, &info->real_local_ip) < 0)
+ i_fatal("real_lip: Invalid ip");
+ } else if (str_begins(arg, "real_rip=")) {
+ if (net_addr2ip(arg + 9, &info->real_remote_ip) < 0)
+ i_fatal("real_rip: Invalid ip");
+ } else if (str_begins(arg, "real_lport=")) {
+ if (net_str2port(arg + 11, &info->real_local_port) < 0)
+ i_fatal("real_lport: Invalid port number");
+ } else if (str_begins(arg, "real_rport=")) {
+ if (net_str2port(arg + 11, &info->real_remote_port) < 0)
+ i_fatal("real_rport: Invalid port number");
+ } else if (str_begins(arg, "forward_")) {
+ const char *key = arg+8;
+ const char *value = strchr(arg+8, '=');
+
+ if (value == NULL)
+ value = "";
+ else
+ key = t_strdup_until(key, value++);
+ key = str_tabescape(key);
+ value = str_tabescape(value);
+ if (info->forward_fields == NULL) {
+ info->forward_fields =
+ t_strdup_printf("%s=%s", key, value);
+ } else {
+ info->forward_fields =
+ t_strdup_printf("%s\t%s=%s", info->forward_fields, key, value);
+ }
+ } else {
+ if (!array_is_created(&info->extra_fields))
+ t_array_init(&info->extra_fields, 4);
+ array_push_back(&info->extra_fields, &arg);
+ }
+}
+
+static void
+auth_user_info_parse(struct auth_user_info *info, const char *const *args)
+{
+ for (unsigned int i = 0; args[i] != NULL; i++)
+ auth_user_info_parse_arg(info, args[i]);
+}
+
+static void
+cmd_user_list(struct auth_master_connection *conn,
+ const struct authtest_input *input,
+ const char *const *users)
+{
+ struct auth_master_user_list_ctx *ctx;
+ const char *username, *user_mask = "*";
+ unsigned int i;
+
+ if (users[0] != NULL && users[1] == NULL)
+ user_mask = users[0];
+
+ ctx = auth_master_user_list_init(conn, user_mask, &input->info);
+ while ((username = auth_master_user_list_next(ctx)) != NULL) {
+ for (i = 0; users[i] != NULL; i++) {
+ if (wildcard_match_icase(username, users[i]))
+ break;
+ }
+ if (users[i] != NULL)
+ printf("%s\n", username);
+ }
+ if (auth_master_user_list_deinit(&ctx) < 0)
+ i_fatal("user listing failed");
+}
+
+static void cmd_auth_cache_flush(struct doveadm_cmd_context *cctx)
+{
+ const char *master_socket_path;
+ struct auth_master_connection *conn;
+ const char *const *users = NULL;
+ unsigned int count;
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &master_socket_path)) {
+ master_socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/auth-master", NULL);
+ }
+ (void)doveadm_cmd_param_array(cctx, "user", &users);
+
+ conn = doveadm_get_auth_master_conn(master_socket_path);
+ if (auth_master_cache_flush(conn, users, &count) < 0) {
+ i_error("Cache flush failed");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else {
+ printf("%u cache entries flushed\n", count);
+ }
+ auth_master_deinit(&conn);
+}
+
+static void authtest_input_init(struct authtest_input *input)
+{
+ i_zero(input);
+ input->info.service = "doveadm";
+ input->info.debug = doveadm_settings->auth_debug;
+}
+
+static void cmd_auth_test(struct doveadm_cmd_context *cctx)
+{
+ const char *auth_socket_path = NULL;
+ const char *const *auth_info;
+ struct authtest_input input;
+
+ authtest_input_init(&input);
+ (void)doveadm_cmd_param_str(cctx, "socket-path", &auth_socket_path);
+ (void)doveadm_cmd_param_str(cctx, "master-user", &input.master_user);
+ if (doveadm_cmd_param_array(cctx, "auth-info", &auth_info))
+ auth_user_info_parse(&input.info, auth_info);
+
+ if (!doveadm_cmd_param_str(cctx, "user", &input.username))
+ auth_cmd_help(cctx);
+ if (!doveadm_cmd_param_str(cctx, "password", &input.password))
+ input.password = t_askpass("Password: ");
+ cmd_auth_input(auth_socket_path, &input);
+ if (!input.success)
+ doveadm_exit_code = EX_NOPERM;
+}
+
+static void
+master_auth_callback(const char *const *auth_args,
+ const char *errormsg, void *context)
+{
+ struct authtest_input *input = context;
+ unsigned int i;
+
+ io_loop_stop(current_ioloop);
+ if (errormsg != NULL) {
+ i_error("userdb lookup failed: %s", errormsg);
+ return;
+ }
+ printf("userdb extra fields:\n");
+ for (i = 0; auth_args[i] != NULL; i++)
+ printf(" %s\n", auth_args[i]);
+ input->success = TRUE;
+}
+
+static void
+cmd_auth_master_input(const char *auth_master_socket_path,
+ struct authtest_input *input)
+{
+ struct master_login_auth *master_auth;
+ struct master_auth_request master_auth_req;
+ buffer_t buf;
+
+ i_zero(&master_auth_req);
+ master_auth_req.tag = 1;
+ master_auth_req.auth_pid = input->auth_pid;
+ master_auth_req.auth_id = input->auth_id;
+ master_auth_req.client_pid = getpid();
+ master_auth_req.local_ip = input->info.local_ip;
+ master_auth_req.remote_ip = input->info.remote_ip;
+
+ buffer_create_from_data(&buf, master_auth_req.cookie,
+ sizeof(master_auth_req.cookie));
+ if (strlen(input->auth_cookie) == MASTER_AUTH_COOKIE_SIZE*2)
+ (void)hex_to_binary(input->auth_cookie, &buf);
+
+ input->success = FALSE;
+ master_auth = master_login_auth_init(auth_master_socket_path, FALSE);
+ io_loop_set_running(current_ioloop);
+ master_login_auth_request(master_auth, &master_auth_req,
+ master_auth_callback, input);
+ if (io_loop_is_running(current_ioloop))
+ io_loop_run(current_ioloop);
+ master_login_auth_deinit(&master_auth);
+}
+
+static void cmd_auth_login(struct doveadm_cmd_context *cctx)
+{
+ const char *auth_login_socket_path, *auth_master_socket_path;
+ const char *const *auth_info;
+ struct auth_client *auth_client;
+ struct authtest_input input;
+
+ authtest_input_init(&input);
+ if (!doveadm_cmd_param_str(cctx, "auth-login-socket-path",
+ &auth_login_socket_path)) {
+ auth_login_socket_path =
+ t_strconcat(doveadm_settings->base_dir,
+ "/auth-login", NULL);
+ }
+ if (!doveadm_cmd_param_str(cctx, "auth-master-socket-path",
+ &auth_master_socket_path)) {
+ auth_master_socket_path =
+ t_strconcat(doveadm_settings->base_dir,
+ "/auth-master", NULL);
+ }
+ (void)doveadm_cmd_param_str(cctx, "master-user", &input.master_user);
+ if (doveadm_cmd_param_array(cctx, "auth-info", &auth_info))
+ auth_user_info_parse(&input.info, auth_info);
+ if (!doveadm_cmd_param_str(cctx, "user", &input.username))
+ auth_cmd_help(cctx);
+ if (!doveadm_cmd_param_str(cctx, "password", &input.password))
+ input.password = t_askpass("Password: ");
+
+ input.pool = pool_alloconly_create("auth login", 256);
+ /* authenticate */
+ auth_client = auth_client_init(auth_login_socket_path, getpid(), FALSE);
+ auth_client_connect(auth_client);
+ auth_client_set_connect_notify(auth_client, auth_connected, &input);
+ if (!auth_client_is_disconnected(auth_client))
+ io_loop_run(current_ioloop);
+ auth_client_set_connect_notify(auth_client, NULL, NULL);
+ /* finish login with userdb lookup */
+ if (input.success)
+ cmd_auth_master_input(auth_master_socket_path, &input);
+ if (!input.success)
+ doveadm_exit_code = EX_NOPERM;
+ auth_client_deinit(&auth_client);
+ pool_unref(&input.pool);
+}
+
+static void cmd_auth_lookup(struct doveadm_cmd_context *cctx)
+{
+ const char *auth_socket_path;
+ struct auth_master_connection *conn;
+ struct authtest_input input;
+ const char *show_field = NULL;
+ const char *const *auth_info, *const *users;
+ bool first = TRUE;
+ int ret;
+
+ authtest_input_init(&input);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &auth_socket_path))
+ auth_socket_path = doveadm_settings->auth_socket_path;
+ (void)doveadm_cmd_param_str(cctx, "field", &show_field);
+ if (doveadm_cmd_param_array(cctx, "auth-info", &auth_info))
+ auth_user_info_parse(&input.info, auth_info);
+ if (!doveadm_cmd_param_array(cctx, "user", &users))
+ auth_cmd_help(cctx);
+
+ conn = doveadm_get_auth_master_conn(auth_socket_path);
+ for (unsigned int i = 0; users[i] != NULL; i++) {
+ input.username = users[i];
+ if (first)
+ first = FALSE;
+ else
+ putchar('\n');
+
+ ret = cmd_user_input(conn, &input, show_field, FALSE);
+ switch (ret) {
+ case -1:
+ doveadm_exit_code = EX_TEMPFAIL;
+ break;
+ case 0:
+ doveadm_exit_code = EX_NOUSER;
+ break;
+ }
+ }
+ auth_master_deinit(&conn);
+}
+
+static void cmd_user_mail_input_field(const char *key, const char *value,
+ const char *show_field)
+{
+ if (show_field == NULL) {
+ doveadm_print(key);
+ doveadm_print(value);
+ } else if (strcmp(show_field, key) == 0) {
+ printf("%s\n", value);
+ }
+}
+
+static void
+cmd_user_mail_print_fields(const struct authtest_input *input,
+ struct mail_user *user,
+ const char *const *userdb_fields,
+ const char *show_field)
+{
+ const struct mail_storage_settings *mail_set;
+ const char *key, *value;
+ unsigned int i;
+
+ if (strcmp(input->username, user->username) != 0)
+ cmd_user_mail_input_field("user", user->username, show_field);
+ cmd_user_mail_input_field("uid", user->set->mail_uid, show_field);
+ cmd_user_mail_input_field("gid", user->set->mail_gid, show_field);
+ cmd_user_mail_input_field("home", user->set->mail_home, show_field);
+
+ mail_set = mail_user_set_get_storage_set(user);
+ cmd_user_mail_input_field("mail", mail_set->mail_location, show_field);
+
+ if (userdb_fields != NULL) {
+ for (i = 0; userdb_fields[i] != NULL; i++) {
+ value = strchr(userdb_fields[i], '=');
+ if (value != NULL)
+ key = t_strdup_until(userdb_fields[i], value++);
+ else {
+ key = userdb_fields[i];
+ value = "";
+ }
+ if (strcmp(key, "uid") != 0 &&
+ strcmp(key, "gid") != 0 &&
+ strcmp(key, "home") != 0 &&
+ strcmp(key, "mail") != 0)
+ cmd_user_mail_input_field(key, value, show_field);
+ }
+ }
+}
+
+static int
+cmd_user_mail_input(struct mail_storage_service_ctx *storage_service,
+ const struct authtest_input *input,
+ const char *show_field, const char *expand_field)
+{
+ struct mail_storage_service_input service_input;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ const char *error, *const *userdb_fields;
+ pool_t pool;
+ int ret;
+
+ i_zero(&service_input);
+ service_input.module = "mail";
+ service_input.service = input->info.service;
+ service_input.username = input->username;
+ service_input.local_ip = input->info.local_ip;
+ service_input.local_port = input->info.local_port;
+ service_input.remote_ip = input->info.remote_ip;
+ service_input.remote_port = input->info.remote_port;
+ service_input.debug = input->info.debug;
+
+ pool = pool_alloconly_create("userdb fields", 1024);
+ mail_storage_service_save_userdb_fields(storage_service, pool,
+ &userdb_fields);
+
+ if ((ret = mail_storage_service_lookup_next(storage_service, &service_input,
+ &service_user, &user,
+ &error)) <= 0) {
+ pool_unref(&pool);
+ if (ret < 0)
+ return -1;
+ fprintf(show_field == NULL && expand_field == NULL ? stdout : stderr,
+ "\nuserdb lookup: user %s doesn't exist\n",
+ input->username);
+ return 0;
+ }
+
+ if (expand_field == NULL)
+ cmd_user_mail_print_fields(input, user, userdb_fields, show_field);
+ else {
+ string_t *str = t_str_new(128);
+ if (var_expand_with_funcs(str, expand_field,
+ mail_user_var_expand_table(user),
+ mail_user_var_expand_func_table, user,
+ &error) <= 0) {
+ i_error("Failed to expand %s: %s", expand_field, error);
+ } else {
+ printf("%s\n", str_c(str));
+ }
+ }
+
+ mail_user_deinit(&user);
+ mail_storage_service_user_unref(&service_user);
+ pool_unref(&pool);
+ return 1;
+}
+
+static void cmd_user(struct doveadm_cmd_context *cctx)
+{
+ const char *auth_socket_path;
+ struct auth_master_connection *conn;
+ struct authtest_input input;
+ const char *show_field = NULL, *expand_field = NULL;
+ const char *const *user_masks, *const *auth_info;
+ struct mail_storage_service_ctx *storage_service = NULL;
+ unsigned int i;
+ bool have_wildcards, userdb_only = FALSE, first = TRUE;
+ int ret;
+
+ authtest_input_init(&input);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &auth_socket_path))
+ auth_socket_path = doveadm_settings->auth_socket_path;
+ (void)doveadm_cmd_param_str(cctx, "field", &show_field);
+ (void)doveadm_cmd_param_str(cctx, "expand-field", &expand_field);
+ (void)doveadm_cmd_param_bool(cctx, "userdb-only", &userdb_only);
+ if (doveadm_cmd_param_array(cctx, "auth-info", &auth_info))
+ auth_user_info_parse(&input.info, auth_info);
+ if (!doveadm_cmd_param_array(cctx, "user-mask", &user_masks))
+ auth_cmd_help(cctx);
+
+ if (expand_field != NULL && userdb_only) {
+ i_error("-e can't be used with -u");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+ if (expand_field != NULL && show_field != NULL) {
+ i_error("-e can't be used with -f");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ conn = doveadm_get_auth_master_conn(auth_socket_path);
+
+ have_wildcards = FALSE;
+ for (i = 0; user_masks[i] != NULL; i++) {
+ if (strchr(user_masks[i], '*') != NULL ||
+ strchr(user_masks[i], '?') != NULL) {
+ have_wildcards = TRUE;
+ break;
+ }
+ }
+
+ if (have_wildcards) {
+ cmd_user_list(conn, &input, user_masks);
+ auth_master_deinit(&conn);
+ return;
+ }
+
+ if (!userdb_only) {
+ storage_service = mail_storage_service_init(master_service, NULL,
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES |
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS);
+ mail_storage_service_set_auth_conn(storage_service, conn);
+ conn = NULL;
+ if (show_field == NULL && expand_field == NULL) {
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TAB);
+ doveadm_print_header_simple("field");
+ doveadm_print_header_simple("value");
+ }
+ }
+
+ for (i = 0; user_masks[i] != NULL; i++) {
+ input.username = user_masks[i];
+ if (first)
+ first = FALSE;
+ else
+ putchar('\n');
+
+ ret = !userdb_only ?
+ cmd_user_mail_input(storage_service, &input, show_field, expand_field) :
+ cmd_user_input(conn, &input, show_field, TRUE);
+ switch (ret) {
+ case -1:
+ doveadm_exit_code = EX_TEMPFAIL;
+ break;
+ case 0:
+ doveadm_exit_code = EX_NOUSER;
+ break;
+ }
+ }
+ if (storage_service != NULL)
+ mail_storage_service_deinit(&storage_service);
+ if (conn != NULL)
+ auth_master_deinit(&conn);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_auth[] = {
+{
+ .cmd = cmd_auth_test,
+ .name = "auth test",
+ .usage = "[-a <auth socket path>] [-x <auth info>] [-M <master user>] <user> [<password>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('x', "auth-info", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('M', "master-user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "password", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .cmd = cmd_auth_login,
+ .name = "auth login",
+ .usage = "[-a <auth-login socket path>] [-m <auth-master socket path>] [-x <auth info>] [-M <master user>] <user> [<password>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "auth-login-socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('m', "auth-master-socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('x', "auth-info", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('M', "master-user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "password", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .cmd = cmd_auth_lookup,
+ .name = "auth lookup",
+ .usage = "[-a <userdb socket path>] [-x <auth info>] [-f field] <user> [<user> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('x', "auth-info", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('f', "field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .cmd = cmd_auth_cache_flush,
+ .name = "auth cache flush",
+ .usage = "[-a <master socket path>] [<user> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .cmd = cmd_user,
+ .name = "user",
+ .usage = "[-a <userdb socket path>] [-x <auth info>] [-f field] [-e <value>] [-u] <user mask> [<user mask> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('x', "auth-info", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('f', "field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('e', "expand-field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('u', "userdb-only", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "user-mask", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void auth_cmd_help(struct doveadm_cmd_context *cctx)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_auth); i++) {
+ if (doveadm_cmd_auth[i].cmd == cctx->cmd->cmd)
+ help_ver2(&doveadm_cmd_auth[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_auth_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_auth); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_auth[i]);
+}
diff --git a/src/doveadm/doveadm-cmd.c b/src/doveadm/doveadm-cmd.c
new file mode 100644
index 0000000..5de976d
--- /dev/null
+++ b/src/doveadm/doveadm-cmd.c
@@ -0,0 +1,469 @@
+/* Copyright (c) 2009-2r016 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "str.h"
+#include "net.h"
+#include "doveadm.h"
+#include "doveadm-cmd.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <getopt.h>
+
+static struct doveadm_cmd_ver2 *doveadm_commands_ver2[] = {
+ &doveadm_cmd_mailbox_mutf7,
+ &doveadm_cmd_service_stop_ver2,
+ &doveadm_cmd_service_status_ver2,
+ &doveadm_cmd_sis_deduplicate,
+ &doveadm_cmd_sis_find,
+ &doveadm_cmd_process_status_ver2,
+ &doveadm_cmd_stop_ver2,
+ &doveadm_cmd_reload_ver2,
+ &doveadm_cmd_stats_dump_ver2,
+ &doveadm_cmd_stats_add_ver2,
+ &doveadm_cmd_stats_remove_ver2,
+ &doveadm_cmd_oldstats_dump_ver2,
+ &doveadm_cmd_oldstats_reset_ver2,
+ &doveadm_cmd_penalty_ver2,
+ &doveadm_cmd_kick_ver2,
+ &doveadm_cmd_who_ver2
+};
+
+static const struct exit_code_str {
+ int code;
+ const char *str;
+} exit_code_strings[] = {
+ { DOVEADM_EX_UNKNOWN, "UNKNOWN" },
+ { EX_TEMPFAIL, "TEMPFAIL" },
+ { EX_USAGE, "USAGE" },
+ { EX_NOUSER, "NOUSER" },
+ { EX_NOPERM, "NOPERM" },
+ { EX_PROTOCOL, "PROTOCOL" },
+ { EX_DATAERR, "DATAERR" },
+ { DOVEADM_EX_NOREPLICATE, "NOREPLICATE" },
+ { DOVEADM_EX_NOTFOUND, "NOTFOUND" }
+};
+
+ARRAY_TYPE(doveadm_cmd_ver2) doveadm_cmds_ver2;
+ARRAY_DEFINE_TYPE(getopt_option_array, struct option);
+
+const char *doveadm_exit_code_to_str(int code)
+{
+ for(size_t i = 0; i < N_ELEMENTS(exit_code_strings); i++) {
+ const struct exit_code_str *ptr = &exit_code_strings[i];
+ if (ptr->code == code)
+ return ptr->str;
+ }
+ return "UNKNOWN";
+}
+
+int doveadm_str_to_exit_code(const char *reason)
+{
+ for(size_t i = 0; i < N_ELEMENTS(exit_code_strings); i++) {
+ const struct exit_code_str *ptr = &exit_code_strings[i];
+ if (strcmp(ptr->str, reason) == 0)
+ return ptr->code;
+ }
+ return DOVEADM_EX_UNKNOWN;
+}
+
+void doveadm_cmd_register_ver2(struct doveadm_cmd_ver2 *cmd)
+{
+ if (cmd->cmd == NULL) {
+ if (cmd->mail_cmd != NULL)
+ cmd->cmd = doveadm_cmd_ver2_to_mail_cmd_wrapper;
+ else i_unreached();
+ }
+ array_push_back(&doveadm_cmds_ver2, cmd);
+}
+
+const struct doveadm_cmd_ver2 *doveadm_cmd_find_ver2(const char *cmd_name)
+{
+ const struct doveadm_cmd_ver2 *cmd;
+
+ array_foreach(&doveadm_cmds_ver2, cmd) {
+ if (strcmp(cmd_name, cmd->name) == 0)
+ return cmd;
+ }
+ return NULL;
+}
+
+const struct doveadm_cmd_ver2 *
+doveadm_cmd_find_with_args_ver2(const char *cmd_name, int *argc,
+ const char *const *argv[])
+{
+ int i, k;
+ const struct doveadm_cmd_ver2 *cmd;
+ const char *cptr;
+
+ for (i = 0; i < *argc; i++) {
+ if (strcmp((*argv)[i], cmd_name) == 0)
+ break;
+ }
+
+ i_assert(i != *argc);
+
+ array_foreach(&doveadm_cmds_ver2, cmd) {
+ cptr = cmd->name;
+ /* cannot reuse i here because this needs be
+ done more than once */
+ for (k = 0; *cptr != '\0' && i + k < *argc; k++) {
+ size_t alen = strlen((*argv)[i + k]);
+ /* make sure we don't overstep */
+ if (strlen(cptr) < alen)
+ break;
+ /* did not match */
+ if (strncmp(cptr, (*argv)[i+k], alen) != 0)
+ break;
+ /* do not accept abbreviations */
+ if (cptr[alen] != ' ' && cptr[alen] != '\0')
+ break;
+ cptr += alen;
+ if (*cptr != '\0')
+ cptr++; /* consume space */
+ }
+ /* name was fully consumed */
+ if (*cptr == '\0') {
+ if (k > 1) {
+ *argc -= k-1;
+ *argv += k-1;
+ }
+ return cmd;
+ }
+ }
+
+ return NULL;
+}
+
+void doveadm_cmds_init(void)
+{
+ unsigned int i;
+
+ i_array_init(&doveadm_cmds_ver2, 2);
+
+ for (i = 0; i < N_ELEMENTS(doveadm_commands_ver2); i++)
+ doveadm_cmd_register_ver2(doveadm_commands_ver2[i]);
+
+ doveadm_register_director_commands();
+ doveadm_register_instance_commands();
+ doveadm_register_proxy_commands();
+ doveadm_register_log_commands();
+ doveadm_register_replicator_commands();
+ doveadm_register_dict_commands();
+ doveadm_register_fs_commands();
+}
+
+void doveadm_cmds_deinit(void)
+{
+ array_free(&doveadm_cmds_ver2);
+}
+
+static const struct doveadm_cmd_param *
+doveadm_cmd_param_get(const struct doveadm_cmd_context *cctx,
+ const char *name)
+{
+ i_assert(cctx != NULL);
+ i_assert(cctx->argv != NULL);
+ for(int i = 0; i < cctx->argc; i++) {
+ if (strcmp(cctx->argv[i].name, name) == 0 &&
+ cctx->argv[i].value_set)
+ return &cctx->argv[i];
+ }
+ return NULL;
+}
+
+bool doveadm_cmd_param_bool(const struct doveadm_cmd_context *cctx,
+ const char *name, bool *value_r)
+{
+ const struct doveadm_cmd_param *param;
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+
+ if (param->type == CMD_PARAM_BOOL) {
+ *value_r = param->value.v_bool;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool doveadm_cmd_param_int64(const struct doveadm_cmd_context *cctx,
+ const char *name, int64_t *value_r)
+{
+ const struct doveadm_cmd_param *param;
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+
+ if (param->type == CMD_PARAM_INT64) {
+ *value_r = param->value.v_int64;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool doveadm_cmd_param_str(const struct doveadm_cmd_context *cctx,
+ const char *name, const char **value_r)
+{
+ const struct doveadm_cmd_param *param;
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+
+ if (param->type == CMD_PARAM_STR) {
+ *value_r = param->value.v_string;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool doveadm_cmd_param_ip(const struct doveadm_cmd_context *cctx,
+ const char *name, struct ip_addr *value_r)
+{
+ const struct doveadm_cmd_param *param;
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+
+ if (param->type == CMD_PARAM_IP) {
+ memcpy(value_r, &param->value.v_ip, sizeof(struct ip_addr));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool doveadm_cmd_param_array(const struct doveadm_cmd_context *cctx,
+ const char *name, const char *const **value_r)
+{
+ const struct doveadm_cmd_param *param;
+ unsigned int count;
+
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+ if (param->type == CMD_PARAM_ARRAY) {
+ *value_r = array_get(&param->value.v_array, &count);
+ /* doveadm_cmd_params_null_terminate_arrays() should have been
+ called, which guarantees that we're NULL-terminated */
+ i_assert((*value_r)[count] == NULL);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool doveadm_cmd_param_istream(const struct doveadm_cmd_context *cctx,
+ const char *name, struct istream **value_r)
+{
+ const struct doveadm_cmd_param *param;
+ if ((param = doveadm_cmd_param_get(cctx, name)) == NULL)
+ return FALSE;
+
+ if (param->type == CMD_PARAM_ISTREAM) {
+ *value_r = param->value.v_istream;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void doveadm_cmd_params_clean(ARRAY_TYPE(doveadm_cmd_param_arr_t) *pargv)
+{
+ struct doveadm_cmd_param *param;
+
+ array_foreach_modifiable(pargv, param) {
+ if (param->type == CMD_PARAM_ISTREAM &&
+ param->value.v_istream != NULL)
+ i_stream_destroy(&param->value.v_istream);
+ }
+ array_clear(pargv);
+}
+
+void doveadm_cmd_params_null_terminate_arrays(
+ ARRAY_TYPE(doveadm_cmd_param_arr_t) *pargv)
+{
+ struct doveadm_cmd_param *param;
+
+ array_foreach_modifiable(pargv, param) {
+ if (param->type == CMD_PARAM_ARRAY &&
+ array_is_created(&param->value.v_array)) {
+ array_append_zero(&param->value.v_array);
+ array_pop_back(&param->value.v_array);
+ }
+ }
+}
+
+static void
+doveadm_build_options(const struct doveadm_cmd_param par[],
+ string_t *shortopts,
+ ARRAY_TYPE(getopt_option_array) *longopts)
+{
+ for (size_t i = 0; par[i].name != NULL; i++) {
+ struct option longopt;
+
+ i_zero(&longopt);
+ longopt.name = par[i].name;
+ if (par[i].short_opt != '\0') {
+ longopt.val = par[i].short_opt;
+ str_append_c(shortopts, par[i].short_opt);
+ if (par[i].type != CMD_PARAM_BOOL)
+ str_append_c(shortopts, ':');
+ }
+ if (par[i].type != CMD_PARAM_BOOL)
+ longopt.has_arg = 1;
+ array_push_back(longopts, &longopt);
+ }
+ array_append_zero(longopts);
+}
+
+static void
+doveadm_fill_param(struct doveadm_cmd_param *param,
+ const char *value, pool_t pool)
+{
+ param->value_set = TRUE;
+ switch (param->type) {
+ case CMD_PARAM_BOOL:
+ param->value.v_bool = TRUE;
+ break;
+ case CMD_PARAM_INT64:
+ if (str_to_int64(value, &param->value.v_int64) != 0)
+ param->value_set = FALSE;
+ break;
+ case CMD_PARAM_IP:
+ if (net_addr2ip(value, &param->value.v_ip) != 0)
+ param->value_set = FALSE;
+ break;
+ case CMD_PARAM_STR:
+ param->value.v_string = p_strdup(pool, value);
+ break;
+ case CMD_PARAM_ARRAY:
+ if (!array_is_created(&param->value.v_array))
+ p_array_init(&param->value.v_array, pool, 8);
+ const char *val = p_strdup(pool, value);
+ array_push_back(&param->value.v_array, &val);
+ break;
+ case CMD_PARAM_ISTREAM: {
+ struct istream *is;
+ if (strcmp(value,"-") == 0)
+ is = i_stream_create_fd(STDIN_FILENO, IO_BLOCK_SIZE);
+ else
+ is = i_stream_create_file(value, IO_BLOCK_SIZE);
+ param->value.v_istream = is;
+ break;
+ }
+ }
+}
+
+bool doveadm_cmd_try_run_ver2(const char *cmd_name,
+ int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx)
+{
+ const struct doveadm_cmd_ver2 *cmd;
+
+ cmd = doveadm_cmd_find_with_args_ver2(cmd_name, &argc, &argv);
+ if (cmd == NULL)
+ return FALSE;
+
+ cctx->cmd = cmd;
+ if (doveadm_cmd_run_ver2(argc, argv, cctx) < 0)
+ doveadm_exit_code = EX_USAGE;
+ return TRUE;
+}
+
+static int
+doveadm_cmd_process_options(int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx, pool_t pool,
+ ARRAY_TYPE(doveadm_cmd_param_arr_t) *pargv)
+{
+ struct doveadm_cmd_param *param;
+ ARRAY_TYPE(getopt_option_array) opts;
+ string_t *optbuf = str_new(pool, 64);
+
+ p_array_init(&opts, pool, 4);
+
+ // build parameters
+ if ((cctx->cmd->flags & CMD_FLAG_NO_UNORDERED_OPTIONS) != 0)
+ str_append_c(optbuf, '+');
+ doveadm_build_options(cctx->cmd->parameters, optbuf, &opts);
+
+ unsigned int pargc;
+ for (pargc = 0; cctx->cmd->parameters[pargc].name != NULL; pargc++) {
+ param = array_append_space(pargv);
+ memcpy(param, &cctx->cmd->parameters[pargc],
+ sizeof(struct doveadm_cmd_param));
+ param->value_set = FALSE;
+ }
+ i_assert(pargc == array_count(&opts)-1); /* opts is NULL-terminated */
+
+ if ((cctx->cmd->flags & CMD_FLAG_NO_OPTIONS) != 0) {
+ /* process -parameters as if they were regular parameters */
+ optind = 1;
+ return 0;
+ }
+
+ int c, li;
+ while ((c = getopt_long(argc, (char *const *)argv, str_c(optbuf),
+ array_front(&opts), &li)) > -1) {
+ switch (c) {
+ case 0:
+ for (unsigned int i = 0; i < array_count(pargv); i++) {
+ const struct option *opt = array_idx(&opts, li);
+ param = array_idx_modifiable(pargv, i);
+ if (opt->name == param->name)
+ doveadm_fill_param(param, optarg, pool);
+ }
+ break;
+ case '?':
+ case ':':
+ doveadm_cmd_params_clean(pargv);
+ return -1;
+ default:
+ // hunt the option
+ for (unsigned int i = 0; i < pargc; i++) {
+ const struct option *longopt =
+ array_idx(&opts, i);
+ if (longopt->val == c)
+ doveadm_fill_param(array_idx_modifiable(pargv, i),
+ optarg, pool);
+ }
+ }
+ }
+ return 0;
+}
+
+int doveadm_cmd_run_ver2(int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx)
+{
+ ARRAY_TYPE(doveadm_cmd_param_arr_t) pargv;
+ unsigned int pargc;
+ pool_t pool = pool_datastack_create();
+
+ p_array_init(&pargv, pool, 20);
+ if (doveadm_cmd_process_options(argc, argv, cctx, pool, &pargv) < 0)
+ return -1;
+
+ /* process positional arguments */
+ for (; optind < argc; optind++) {
+ struct doveadm_cmd_param *ptr;
+ bool found = FALSE;
+ array_foreach_modifiable(&pargv, ptr) {
+ if ((ptr->flags & CMD_PARAM_FLAG_POSITIONAL) != 0 &&
+ (ptr->value_set == FALSE ||
+ ptr->type == CMD_PARAM_ARRAY)) {
+ doveadm_fill_param(ptr, argv[optind], pool);
+ found = TRUE;
+ break;
+ }
+ }
+ if (!found) {
+ i_error("Extraneous arguments found: %s",
+ t_strarray_join(argv + optind, " "));
+ doveadm_cmd_params_clean(&pargv);
+ return -1;
+ }
+ }
+
+ doveadm_cmd_params_null_terminate_arrays(&pargv);
+ cctx->argv = array_get_modifiable(&pargv, &pargc);
+ cctx->argc = pargc;
+
+ cctx->cmd->cmd(cctx);
+
+ doveadm_cmd_params_clean(&pargv);
+ return 0;
+}
diff --git a/src/doveadm/doveadm-cmd.h b/src/doveadm/doveadm-cmd.h
new file mode 100644
index 0000000..a988575
--- /dev/null
+++ b/src/doveadm/doveadm-cmd.h
@@ -0,0 +1,155 @@
+#ifndef DOVEADM_CMD_H
+#define DOVEADM_CMD_H
+
+#include "net.h"
+
+#define DOVEADM_CMD_PARAMS_START .parameters = (const struct doveadm_cmd_param[]){
+#define DOVEADM_CMD_PARAM(optP, nameP, typeP, flagP ) { .short_opt = optP, .name = nameP, .type = typeP, .flags = flagP },
+#define DOVEADM_CMD_PARAMS_END { .short_opt = '\0', .name = NULL, .type = CMD_PARAM_BOOL, .flags = CMD_PARAM_FLAG_NONE } }
+
+struct doveadm_cmd_ver2;
+struct doveadm_cmd_context;
+struct doveadm_mail_cmd_context;
+
+typedef void doveadm_command_t(int argc, char *argv[]);
+
+typedef enum {
+ CMD_PARAM_BOOL = 0, /* value will contain 1 (not pointer) */
+ CMD_PARAM_INT64, /* ditto but contains number (not pointer) */
+ CMD_PARAM_IP, /* value contains struct ip_addr */
+ CMD_PARAM_STR, /* value contains const char* */
+ CMD_PARAM_ARRAY, /* value contains const char*[] */
+ CMD_PARAM_ISTREAM /* value contains struct istream* */
+} doveadm_cmd_param_t;
+
+typedef enum {
+ CMD_PARAM_FLAG_NONE = 0x0,
+ CMD_PARAM_FLAG_POSITIONAL = 0x1,
+ CMD_PARAM_FLAG_DO_NOT_EXPOSE = 0x2,
+} doveadm_cmd_param_flag_t;
+
+typedef enum {
+ CMD_FLAG_NONE = 0x0,
+ CMD_FLAG_HIDDEN = 0x1,
+ CMD_FLAG_NO_PRINT = 0x2,
+ /* Don't parse any -options for the command. */
+ CMD_FLAG_NO_OPTIONS = 0x4,
+ /* Prevent GNU getopt() from finding options after the first
+ non-option is seen (e.g. "-1 arg -2" would parse -1 but not -2
+ as option). */
+ CMD_FLAG_NO_UNORDERED_OPTIONS = 0x8,
+} doveadm_cmd_flag_t;
+
+struct doveadm_cmd_param {
+ char short_opt;
+ const char *name;
+ doveadm_cmd_param_t type;
+ bool value_set;
+ struct {
+ bool v_bool;
+ int64_t v_int64;
+ const char* v_string;
+ ARRAY_TYPE(const_string) v_array;
+ struct ip_addr v_ip;
+ struct istream* v_istream;
+ } value;
+ doveadm_cmd_param_flag_t flags;
+};
+ARRAY_DEFINE_TYPE(doveadm_cmd_param_arr_t, struct doveadm_cmd_param);
+
+typedef void doveadm_command_ver2_t(struct doveadm_cmd_context *cctx);
+
+struct doveadm_cmd_ver2 {
+ doveadm_command_ver2_t *cmd;
+ struct doveadm_mail_cmd_context *(*mail_cmd)(void);
+ const char *name;
+ const char *usage;
+ doveadm_cmd_flag_t flags;
+ const struct doveadm_cmd_param *parameters;
+};
+
+struct doveadm_cmd_context {
+ const struct doveadm_cmd_ver2 *cmd; /* for help */
+
+ int argc;
+ const struct doveadm_cmd_param *argv;
+
+ const char *username;
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+
+ enum doveadm_client_type conn_type;
+ struct istream *input;
+ struct ostream *output;
+};
+
+ARRAY_DEFINE_TYPE(doveadm_cmd_ver2, struct doveadm_cmd_ver2);
+extern ARRAY_TYPE(doveadm_cmd_ver2) doveadm_cmds_ver2;
+
+void doveadm_register_auth_commands(void);
+void doveadm_register_auth_server_commands(void);
+void doveadm_register_director_commands(void);
+void doveadm_register_proxy_commands(void);
+void doveadm_register_log_commands(void);
+void doveadm_register_instance_commands(void);
+void doveadm_register_mount_commands(void);
+void doveadm_register_replicator_commands(void);
+void doveadm_register_dict_commands(void);
+void doveadm_register_fs_commands(void);
+
+void doveadm_cmds_init(void);
+void doveadm_cmds_deinit(void);
+
+void doveadm_cmd_ver2_to_mail_cmd_wrapper(struct doveadm_cmd_context *cctx);
+
+void doveadm_cmd_register_ver2(struct doveadm_cmd_ver2 *cmd);
+const struct doveadm_cmd_ver2 *
+doveadm_cmd_find_with_args_ver2(const char *cmd_name, int *argc,
+ const char *const *argv[]);
+const struct doveadm_cmd_ver2 *doveadm_cmd_find_ver2(const char *cmd_name);
+/* Returns FALSE if cmd_name doesn't exist, TRUE if it exists. */
+bool doveadm_cmd_try_run_ver2(const char *cmd_name,
+ int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx);
+/* Returns 0 if success, -1 if parameters were invalid. */
+int doveadm_cmd_run_ver2(int argc, const char *const argv[],
+ struct doveadm_cmd_context *cctx);
+
+bool doveadm_cmd_param_bool(const struct doveadm_cmd_context *cctx,
+ const char *name, bool *value_r);
+bool doveadm_cmd_param_int64(const struct doveadm_cmd_context *cctx,
+ const char *name, int64_t *value_r);
+bool doveadm_cmd_param_str(const struct doveadm_cmd_context *cctx,
+ const char *name, const char **value_r);
+bool doveadm_cmd_param_ip(const struct doveadm_cmd_context *cctx,
+ const char *name, struct ip_addr *value_r);
+bool doveadm_cmd_param_array(const struct doveadm_cmd_context *cctx,
+ const char *name, const char *const **value_r);
+bool doveadm_cmd_param_istream(const struct doveadm_cmd_context *cctx,
+ const char *name, struct istream **value_r);
+
+void doveadm_cmd_params_clean(ARRAY_TYPE(doveadm_cmd_param_arr_t) *pargv);
+void doveadm_cmd_params_null_terminate_arrays(ARRAY_TYPE(doveadm_cmd_param_arr_t) *pargv);
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_dump;
+extern struct doveadm_cmd_ver2 doveadm_cmd_service_stop_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_service_status_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_process_status_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_stop_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_reload_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_stats_dump_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_stats_add_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_stats_remove_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_mutf7;
+extern struct doveadm_cmd_ver2 doveadm_cmd_oldstats_reset_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_oldstats_dump_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_oldstats_top_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_penalty_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_pw;
+extern struct doveadm_cmd_ver2 doveadm_cmd_kick_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_who_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_sis_deduplicate;
+extern struct doveadm_cmd_ver2 doveadm_cmd_sis_find;
+extern struct doveadm_cmd_ver2 doveadm_cmd_zlibconnect;
+
+#endif
diff --git a/src/doveadm/doveadm-dict.c b/src/doveadm/doveadm-dict.c
new file mode 100644
index 0000000..12b901a
--- /dev/null
+++ b/src/doveadm/doveadm-dict.c
@@ -0,0 +1,329 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dict.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static int
+cmd_dict_init_full(struct doveadm_cmd_context *cctx,
+ doveadm_command_ver2_t *cmd ATTR_UNUSED, enum dict_iterate_flags *iter_flags,
+ struct dict **dict_r, struct dict_op_settings *dopset_r)
+{
+ struct dict_settings dict_set;
+ struct dict *dict;
+ bool set = FALSE;
+ const char *dict_uri, *error, *key, *username = "";
+ i_zero(dopset_r);
+
+ if (doveadm_cmd_param_bool(cctx, "exact", &set) && set)
+ *iter_flags |= DICT_ITERATE_FLAG_EXACT_KEY;
+ if (doveadm_cmd_param_bool(cctx, "recurse", &set) && set)
+ *iter_flags |= DICT_ITERATE_FLAG_RECURSE;
+ if (doveadm_cmd_param_bool(cctx, "no-value", &set) && set)
+ *iter_flags |= DICT_ITERATE_FLAG_NO_VALUE;
+ (void)doveadm_cmd_param_str(cctx, "user", &username);
+ dopset_r->username = username;
+
+ if (!doveadm_cmd_param_str(cctx, "dict-uri", &dict_uri)) {
+ i_error("dictionary URI must be specified");
+ doveadm_exit_code = EX_USAGE;
+ return -1;
+ }
+
+ if (!doveadm_cmd_param_str(cctx, "prefix", &key) &&
+ !doveadm_cmd_param_str(cctx, "key", &key))
+ key = "";
+
+ if (!str_begins(key, DICT_PATH_PRIVATE) &&
+ !str_begins(key, DICT_PATH_SHARED)) {
+ i_error("Key must begin with '"DICT_PATH_PRIVATE
+ "' or '"DICT_PATH_SHARED"': %s", key);
+ doveadm_exit_code = EX_USAGE;
+ return -1;
+ }
+ if (username[0] == '\0' &&
+ str_begins(key, DICT_PATH_PRIVATE)) {
+ i_error("-u must be specified for "DICT_PATH_PRIVATE" keys");
+ doveadm_exit_code = EX_USAGE;
+ return -1;
+ }
+
+ dict_drivers_register_builtin();
+ i_zero(&dict_set);
+ dict_set.base_dir = doveadm_settings->base_dir;
+ if (dict_init(dict_uri, &dict_set, &dict, &error) < 0) {
+ i_error("dict_init(%s) failed: %s", dict_uri, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ return -1;
+ }
+ *dict_r = dict;
+ return 0;
+}
+
+static int
+cmd_dict_init(struct doveadm_cmd_context *cctx,
+ doveadm_command_ver2_t *cmd, struct dict **dict_r,
+ struct dict_op_settings *set_r)
+{
+ enum dict_iterate_flags iter_flags = 0;
+ return cmd_dict_init_full(cctx, cmd, &iter_flags, dict_r, set_r);
+}
+
+struct doveadm_dict_ctx {
+ pool_t pool;
+ int ret;
+ const char *const *values;
+ const char *error;
+};
+
+static void dict_lookup_callback(const struct dict_lookup_result *result,
+ struct doveadm_dict_ctx *ctx)
+{
+ ctx->ret = result->ret;
+ ctx->values = result->values == NULL ? NULL :
+ p_strarray_dup(ctx->pool, result->values);
+ ctx->error = p_strdup(ctx->pool, result->error);
+}
+
+static void cmd_dict_get(struct doveadm_cmd_context *cctx)
+{
+ struct doveadm_dict_ctx ctx;
+ struct dict *dict;
+ const char *key;
+ struct dict_op_settings set;
+
+ if (!doveadm_cmd_param_str(cctx, "key", &key)) {
+ i_error("dict-get: Missing key");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ if (cmd_dict_init(cctx, cmd_dict_get, &dict, &set) < 0)
+ return;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("value", "", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ i_zero(&ctx);
+ ctx.pool = pool_alloconly_create("doveadm dict lookup", 512);
+ ctx.ret = -2;
+ dict_lookup_async(dict, &set, key, dict_lookup_callback, &ctx);
+ while (ctx.ret == -2)
+ dict_wait(dict);
+ if (ctx.ret < 0) {
+ i_error("dict_lookup(%s) failed: %s", key, ctx.error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (ctx.ret == 0) {
+ i_error("%s doesn't exist", key);
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ unsigned int i, values_count = str_array_length(ctx.values);
+
+ for (i = 1; i < values_count; i++)
+ doveadm_print_header("value", "", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ for (i = 0; i < values_count; i++)
+ doveadm_print(ctx.values[i]);
+ }
+ pool_unref(&ctx.pool);
+ dict_deinit(&dict);
+}
+
+static void cmd_dict_set(struct doveadm_cmd_context *cctx)
+{
+ struct dict *dict;
+ struct dict_transaction_context *trans;
+ const char *error;
+ const char *key, *value = "";
+ struct dict_op_settings set;
+
+ if (!doveadm_cmd_param_str(cctx, "key", &key) ||
+ !doveadm_cmd_param_str(cctx, "value", &value)) {
+ i_error("dict set: Missing parameters");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ if (cmd_dict_init(cctx, cmd_dict_set, &dict, &set) < 0)
+ return;
+
+ trans = dict_transaction_begin(dict, &set);
+ dict_set(trans, key, value);
+ if (dict_transaction_commit(&trans, &error) <= 0) {
+ i_error("dict_transaction_commit() failed: %s", error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ dict_deinit(&dict);
+}
+
+static void cmd_dict_unset(struct doveadm_cmd_context *cctx)
+{
+ struct dict *dict;
+ struct dict_transaction_context *trans;
+ const char *error;
+ const char *key;
+ struct dict_op_settings set;
+
+ if (!doveadm_cmd_param_str(cctx, "key", &key)) {
+ i_error("dict unset: Missing key");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ if (cmd_dict_init(cctx, cmd_dict_unset, &dict, &set) < 0)
+ return;
+
+ trans = dict_transaction_begin(dict, &set);
+ dict_unset(trans, key);
+ if (dict_transaction_commit(&trans, &error) <= 0) {
+ i_error("dict_transaction_commit() failed: %s", error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ dict_deinit(&dict);
+}
+
+static void cmd_dict_inc(struct doveadm_cmd_context *cctx)
+{
+ struct dict *dict;
+ struct dict_transaction_context *trans;
+ const char *error;
+ const char *key;
+ int64_t diff;
+ int ret;
+ struct dict_op_settings set;
+
+ if (!doveadm_cmd_param_str(cctx, "key", &key) ||
+ !doveadm_cmd_param_int64(cctx, "difference", &diff)) {
+ i_error("dict-inc: Missing parameters");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ if (cmd_dict_init(cctx, cmd_dict_inc, &dict, &set) < 0)
+ return;
+
+ trans = dict_transaction_begin(dict, &set);
+ dict_atomic_inc(trans, key, diff);
+ ret = dict_transaction_commit(&trans, &error);
+ if (ret < 0) {
+ i_error("dict_transaction_commit() failed: %s", error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (ret == 0) {
+ i_error("%s doesn't exist", key);
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ }
+ dict_deinit(&dict);
+}
+
+static void cmd_dict_iter(struct doveadm_cmd_context *cctx)
+{
+ struct dict *dict;
+ struct dict_iterate_context *iter;
+ enum dict_iterate_flags iter_flags = 0;
+ const char *prefix, *key, *const *values, *error;
+ bool header_printed = FALSE;
+ struct dict_op_settings set;
+
+ if (!doveadm_cmd_param_str(cctx, "prefix", &prefix)) {
+ i_error("dict-iter: Missing prefix");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ if (cmd_dict_init_full(cctx, cmd_dict_iter, &iter_flags, &dict, &set) < 0)
+ return;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TAB);
+ doveadm_print_header_simple("key");
+ if ((iter_flags & DICT_ITERATE_FLAG_NO_VALUE) == 0)
+ doveadm_print_header_simple("value");
+
+ iter = dict_iterate_init(dict, &set, prefix, iter_flags);
+ while (dict_iterate_values(iter, &key, &values)) {
+ unsigned int values_count = str_array_length(values);
+ if (!header_printed) {
+ for (unsigned int i = 1; i < values_count; i++)
+ doveadm_print_header_simple("value");
+ header_printed = TRUE;
+ }
+ doveadm_print(key);
+ if ((iter_flags & DICT_ITERATE_FLAG_NO_VALUE) == 0) {
+ for (unsigned int i = 0; i < values_count; i++)
+ doveadm_print(values[i]);
+ }
+ }
+ if (dict_iterate_deinit(&iter, &error) < 0) {
+ i_error("dict_iterate_deinit(%s) failed: %s", prefix, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ dict_deinit(&dict);
+}
+
+static struct doveadm_cmd_ver2 doveadm_cmd_dict[] = {
+{
+ .name = "dict get",
+ .cmd = cmd_dict_get,
+ .usage = "[-u <user>] <dict uri> <key>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "dict-uri", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "dict set",
+ .cmd = cmd_dict_set,
+ .usage = "[-u <user>] <dict uri> <key> <value>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "dict-uri", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "value", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "dict unset",
+ .cmd = cmd_dict_unset,
+ .usage = "[-u <user>] <dict uri> <key>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "dict-uri", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "dict inc",
+ .cmd = cmd_dict_inc,
+ .usage = "[-u <user>] <dict uri> <key> <diff>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "dict-uri", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "difference", CMD_PARAM_INT64, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "dict iter",
+ .cmd = cmd_dict_iter,
+ .usage = "[-u <user>] [-1RV] <dict uri> <prefix>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('1', "exact", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('R', "recurse", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('V', "no-value", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "dict-uri", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "prefix", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+void doveadm_register_dict_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_dict); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_dict[i]);
+}
diff --git a/src/doveadm/doveadm-director.c b/src/doveadm/doveadm-director.c
new file mode 100644
index 0000000..167b091
--- /dev/null
+++ b/src/doveadm/doveadm-director.c
@@ -0,0 +1,1084 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md5.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "net.h"
+#include "istream.h"
+#include "write-full.h"
+#include "master-service.h"
+#include "auth-master.h"
+#include "mail-user-hash.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+struct director_context {
+ const char *socket_path;
+ const char *users_path;
+ const char *tag;
+ const char *user;
+ const char *host;
+ const char *ip;
+ const char *port;
+ const char *vhost_count;
+ const char *passdb_field;
+
+ struct istream *users_input;
+ struct istream *input;
+ bool explicit_socket_path;
+ bool hash_map, user_map, force_flush;
+ int64_t max_parallel;
+};
+
+struct user_list {
+ struct user_list *next;
+ const char *name;
+};
+
+HASH_TABLE_DEFINE_TYPE(user_list, void *, struct user_list *);
+
+static void director_cmd_help(const struct doveadm_cmd_ver2 *);
+static int director_get_host(const char *host, struct ip_addr **ips_r,
+ unsigned int *ips_count_r) ATTR_WARN_UNUSED_RESULT;
+static void
+director_send(struct director_context *ctx, const char *data)
+{
+ if (write_full(i_stream_get_fd(ctx->input), data, strlen(data)) < 0)
+ i_fatal("write(%s) failed: %m", ctx->socket_path);
+}
+
+static void director_connect(struct director_context *ctx)
+{
+#define DIRECTOR_HANDSHAKE "VERSION\tdirector-doveadm\t1\t0\n"
+ const char *line;
+ int fd;
+
+ fd = doveadm_connect(ctx->socket_path);
+ net_set_nonblock(fd, FALSE);
+
+ ctx->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ director_send(ctx, DIRECTOR_HANDSHAKE);
+
+ alarm(5);
+ line = i_stream_read_next_line(ctx->input);
+ alarm(0);
+ if (line == NULL) {
+ if (ctx->input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->socket_path,
+ i_stream_get_error(ctx->input));
+ } else if (ctx->input->eof) {
+ i_fatal("%s disconnected", ctx->socket_path);
+ } else {
+ i_fatal("read(%s) timed out (is director configured?)",
+ ctx->socket_path);
+ }
+ }
+ if (!version_string_verify(line, "director-doveadm", 1)) {
+ i_fatal_status(EX_PROTOCOL,
+ "%s not a compatible director-doveadm socket",
+ ctx->socket_path);
+ }
+}
+
+static void director_disconnect(struct director_context *ctx)
+{
+ if (ctx->input != NULL) {
+ if (ctx->input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->socket_path,
+ i_stream_get_error(ctx->input));
+ }
+ i_stream_destroy(&ctx->input);
+ }
+}
+
+static struct director_context *
+cmd_director_init(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ ctx = t_new(struct director_context, 1);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &(ctx->socket_path)))
+ ctx->socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/director-admin", NULL);
+ else
+ ctx->explicit_socket_path = TRUE;
+ if (!doveadm_cmd_param_bool(cctx, "user-map", &(ctx->user_map)))
+ ctx->user_map = FALSE;
+ if (!doveadm_cmd_param_bool(cctx, "hash-map", &(ctx->hash_map)))
+ ctx->hash_map = FALSE;
+ if (!doveadm_cmd_param_bool(cctx, "force-flush", &(ctx->force_flush)))
+ ctx->force_flush = FALSE;
+ if (!doveadm_cmd_param_istream(cctx, "users-file", &(ctx->users_input)))
+ ctx->users_input = NULL;
+ if (!doveadm_cmd_param_str(cctx, "tag", &(ctx->tag)))
+ ctx->tag = NULL;
+ if (!doveadm_cmd_param_str(cctx, "user", &(ctx->user)))
+ ctx->user = NULL;
+ if (!doveadm_cmd_param_str(cctx, "host", &(ctx->host)))
+ ctx->host = NULL;
+ if (!doveadm_cmd_param_str(cctx, "ip", &(ctx->ip)))
+ ctx->ip = NULL;
+ if (!doveadm_cmd_param_str(cctx, "port", &(ctx->port)))
+ ctx->port = NULL;
+ if (!doveadm_cmd_param_str(cctx, "vhost-count", &(ctx->vhost_count)))
+ ctx->vhost_count = NULL;
+ if (!doveadm_cmd_param_str(cctx, "passdb-field", &(ctx->passdb_field)))
+ ctx->passdb_field = NULL;
+ if (!doveadm_cmd_param_int64(cctx, "max-parallel", &(ctx->max_parallel)))
+ ctx->max_parallel = 0;
+ if (!ctx->user_map)
+ director_connect(ctx);
+ return ctx;
+}
+
+static void director_disconnected(struct director_context *ctx)
+{
+ i_assert(ctx->input->eof);
+ if (ctx->input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", ctx->socket_path,
+ i_stream_get_error(ctx->input));
+ } else {
+ i_error("%s unexpectedly disconnected", ctx->socket_path);
+ }
+ doveadm_exit_code = EX_TEMPFAIL;
+}
+
+static void
+cmd_director_status_user(struct director_context *ctx)
+{
+ const char *line, *const *args;
+ time_t expires;
+
+ director_send(ctx, t_strdup_printf("USER-LOOKUP\t%s\t%s\n", ctx->user,
+ ctx->tag != NULL ? ctx->tag : ""));
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ director_disconnected(ctx);
+ return;
+ }
+
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) != 4 ||
+ str_to_time(args[1], &expires) < 0) {
+ i_error("Invalid reply from director");
+ doveadm_exit_code = EX_PROTOCOL;
+ return;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+
+ doveadm_print_header_simple("status");
+ doveadm_print_header_simple("expires");
+ doveadm_print_header_simple("hashed");
+ doveadm_print_header_simple("initial-config");
+
+ doveadm_print_formatted_set_format("Current: %{status} (expires %{expires})\n" \
+ "Hashed: %{hashed}\n" \
+ "Initial config: %{initial-config}\n");
+
+ if (args[0][0] != '\0') {
+ doveadm_print(args[0]);
+ doveadm_print(unixdate2str(expires));
+ } else {
+ doveadm_print("n/a");
+ doveadm_print("-1");
+ }
+ doveadm_print(args[2]);
+ doveadm_print(args[3]);
+
+ director_disconnect(ctx);
+}
+
+static void cmd_director_status(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ const char *line, *const *args;
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->user != NULL) {
+ cmd_director_status_user(ctx);
+ return;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header_simple("mail server ip");
+ doveadm_print_header_simple("tag");
+ doveadm_print_header_simple("vhosts");
+ doveadm_print_header_simple("state");
+ doveadm_print_header("state-changed", "state changed", 0);
+ doveadm_print_header_simple("users");
+
+ director_send(ctx, "HOST-LIST\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ unsigned int arg_count;
+ time_t ts;
+
+ args = t_strsplit_tabescaped(line);
+ arg_count = str_array_length(args);
+ if (arg_count >= 6) {
+ /* ip vhosts users tag updown updown-ts */
+ doveadm_print(args[0]);
+ doveadm_print(args[3]);
+ doveadm_print(args[1]);
+ doveadm_print(args[4][0] == 'D' ? "down" : "up");
+ if (str_to_time(args[5], &ts) < 0 ||
+ ts <= 0)
+ doveadm_print("-");
+ else
+ doveadm_print(unixdate2str(ts));
+ doveadm_print(args[2]);
+ }
+ } T_END;
+ }
+ if (line == NULL)
+ director_disconnected(ctx);
+ director_disconnect(ctx);
+}
+
+static bool user_hash_expand(const char *username, unsigned int *hash_r)
+{
+ const char *error;
+
+ if (!mail_user_hash(username, doveadm_settings->director_username_hash,
+ hash_r, &error)) {
+ i_error("Failed to expand director_username_hash=%s: %s",
+ doveadm_settings->director_username_hash, error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+user_list_add(const char *username, pool_t pool,
+ HASH_TABLE_TYPE(user_list) users)
+{
+ struct user_list *user, *old_user;
+ unsigned int user_hash;
+
+ if (!user_hash_expand(username, &user_hash))
+ return;
+
+ user = p_new(pool, struct user_list, 1);
+ user->name = p_strdup(pool, username);
+
+ old_user = hash_table_lookup(users, POINTER_CAST(user_hash));
+ if (old_user != NULL)
+ user->next = old_user;
+ hash_table_update(users, POINTER_CAST(user_hash), user);
+}
+
+static void ATTR_NULL(1)
+userdb_get_user_list(const char *auth_socket_path, pool_t pool,
+ HASH_TABLE_TYPE(user_list) users)
+{
+ struct auth_master_user_list_ctx *ctx;
+ struct auth_master_connection *conn;
+ const char *username;
+
+ if (auth_socket_path == NULL) {
+ auth_socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/auth-userdb", NULL);
+ }
+
+ conn = auth_master_init(auth_socket_path, 0);
+ ctx = auth_master_user_list_init(conn, "", NULL);
+ while ((username = auth_master_user_list_next(ctx)) != NULL)
+ user_list_add(username, pool, users);
+ if (auth_master_user_list_deinit(&ctx) < 0) {
+ i_error("user listing failed");
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ auth_master_deinit(&conn);
+}
+
+static void
+user_file_get_user_list(struct istream *input, pool_t pool,
+ HASH_TABLE_TYPE(user_list) users)
+{
+ const char *username;
+
+ while ((username = i_stream_read_next_line(input)) != NULL)
+ user_list_add(username, pool, users);
+}
+
+static int director_get_host(const char *host, struct ip_addr **ips_r,
+ unsigned int *ips_count_r)
+{
+ struct ip_addr ip;
+ int ret = 0;
+
+ if (net_addr2ip(host, &ip) == 0) {
+ *ips_r = t_new(struct ip_addr, 1);
+ **ips_r = ip;
+ *ips_count_r = 1;
+ } else {
+ ret = net_gethostbyname(host, ips_r, ips_count_r);
+ if (ret != 0) {
+ i_error("gethostname(%s) failed: %s", host,
+ net_gethosterror(ret));
+ doveadm_exit_code = EX_TEMPFAIL;
+ return ret;
+ }
+ }
+
+ return ret;
+}
+
+static bool ip_find(const struct ip_addr *ips, unsigned int ips_count,
+ const struct ip_addr *match_ip)
+{
+ unsigned int i;
+
+ for (i = 0; i < ips_count; i++) {
+ if (net_ip_compare(&ips[i], match_ip))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void cmd_director_map(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ const char *line, *const *args;
+ struct ip_addr *ips, user_ip;
+ pool_t pool;
+ HASH_TABLE_TYPE(user_list) users;
+ struct user_list *user;
+ unsigned int ips_count, user_hash;
+ time_t expires;
+
+ ctx = cmd_director_init(cctx);
+
+ if ((ctx->hash_map || ctx->user_map) && ctx->host == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ if (ctx->user_map) {
+ /* user -> hash mapping */
+ if (user_hash_expand(ctx->host, &user_hash)) {
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("hash", "hash", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ doveadm_print(t_strdup_printf("%u", user_hash));
+ }
+ director_disconnect(ctx);
+ return;
+ }
+
+ if (ctx->host == NULL || ctx->hash_map)
+ ips_count = 0;
+ else if (director_get_host(ctx->host, &ips, &ips_count) != 0) {
+ director_disconnect(ctx);
+ return;
+ }
+
+ pool = pool_alloconly_create("director map users", 1024*128);
+ hash_table_create_direct(&users, pool, 0);
+ if (ctx->users_input == NULL)
+ userdb_get_user_list(NULL, pool, users);
+ else
+ user_file_get_user_list(ctx->users_input, pool, users);
+
+ if (ctx->hash_map) {
+ /* hash -> usernames mapping */
+ if (str_to_uint(ctx->host, &user_hash) < 0)
+ i_fatal("Invalid username hash: %s", ctx->host);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("user", "user", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ user = hash_table_lookup(users, POINTER_CAST(user_hash));
+ for (; user != NULL; user = user->next)
+ doveadm_print(user->name);
+ goto deinit;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("user", "user", DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header_simple("hash");
+ doveadm_print_header_simple("mail server ip");
+ doveadm_print_header_simple("expire time");
+
+ if (ips_count != 1)
+ director_send(ctx, "USER-LIST\n");
+ else {
+ director_send(ctx, t_strdup_printf(
+ "USER-LIST\t%s\n", net_ip2addr(&ips[0])));
+ }
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 3 ||
+ str_to_uint(args[0], &user_hash) < 0 ||
+ str_to_time(args[1], &expires) < 0 ||
+ net_addr2ip(args[2], &user_ip) < 0) {
+ i_error("Invalid USER-LIST reply: %s", line);
+ doveadm_exit_code = EX_PROTOCOL;
+ } else if (ips_count == 0 ||
+ ip_find(ips, ips_count, &user_ip)) {
+ user = hash_table_lookup(users,
+ POINTER_CAST(user_hash));
+ if (user == NULL) {
+ doveadm_print("<unknown>");
+ doveadm_print(args[0]);
+ doveadm_print(args[2]);
+ doveadm_print(unixdate2str(expires));
+ }
+ for (; user != NULL; user = user->next) {
+ doveadm_print(user->name);
+ doveadm_print(args[0]);
+ doveadm_print(args[2]);
+ doveadm_print(unixdate2str(expires));
+ }
+ }
+ } T_END;
+ }
+ if (line == NULL)
+ director_disconnected(ctx);
+deinit:
+ director_disconnect(ctx);
+ hash_table_destroy(&users);
+ pool_unref(&pool);
+}
+
+static void
+cmd_director_add_or_update(struct doveadm_cmd_context *cctx, bool update)
+{
+ const char *director_cmd = update ? "HOST-UPDATE" : "HOST-SET";
+ struct director_context *ctx;
+ struct ip_addr *ips;
+ unsigned int i, ips_count, vhost_count = UINT_MAX;
+ const char *line, *host;
+ string_t *cmd;
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->tag != NULL && ctx->tag[0] == '\0')
+ ctx->tag = NULL;
+ if (ctx->host == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+ if (ctx->vhost_count != NULL) {
+ if (str_to_uint(ctx->vhost_count, &vhost_count) < 0) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+ } else if (update) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+ if (str_to_uint(ctx->host, &i) == 0) {
+ /* host is a number. this would translate to an IP address,
+ which is probably a mistake. */
+ i_error("Invalid host '%s'", ctx->host);
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ host = ctx->host;
+ if (ctx->tag == NULL) {
+ ctx->tag = strchr(ctx->host, '@');
+ if (ctx->tag != NULL)
+ host = t_strdup_until(ctx->host, ctx->tag++);
+ }
+ if (director_get_host(host, &ips, &ips_count) != 0) {
+ director_disconnect(ctx);
+ return;
+ }
+ cmd = t_str_new(128);
+ for (i = 0; i < ips_count; i++) {
+ str_truncate(cmd, 0);
+ str_printfa(cmd, "%s\t%s", director_cmd, net_ip2addr(&ips[i]));
+ if (ctx->tag != NULL)
+ str_printfa(cmd, "@%s", ctx->tag);
+ if (vhost_count != UINT_MAX)
+ str_printfa(cmd, "\t%u", vhost_count);
+ str_append_c(cmd, '\n');
+ director_send(ctx, str_c(cmd));
+ }
+ for (i = 0; i < ips_count; i++) {
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL)
+ director_disconnected(ctx);
+ else if (strcmp(line, "OK") != 0) {
+ i_error("%s: %s", net_ip2addr(&ips[i]),
+ strcmp(line, "NOTFOUND") == 0 ?
+ "doesn't exist" : line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (doveadm_verbose) {
+ i_info("%s: OK", net_ip2addr(&ips[i]));
+ }
+ }
+ director_disconnect(ctx);
+}
+
+static void cmd_director_add(struct doveadm_cmd_context *cctx)
+{
+ cmd_director_add_or_update(cctx, FALSE);
+}
+
+static void cmd_director_update(struct doveadm_cmd_context *cctx)
+{
+ cmd_director_add_or_update(cctx, TRUE);
+}
+
+static void
+cmd_director_ipcmd(const char *cmd_name, const char *success_result,
+ struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ struct ip_addr *ips;
+ unsigned int i, ips_count;
+ const char *host, *line;
+
+ ctx = cmd_director_init(cctx);
+ host = ctx->host;
+ if (host == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ if (director_get_host(host, &ips, &ips_count) != 0) {
+ director_disconnect(ctx);
+ return;
+ }
+ for (i = 0; i < ips_count; i++) {
+ director_send(ctx, t_strdup_printf(
+ "%s\t%s\n", cmd_name, net_ip2addr(&ips[i])));
+ }
+ for (i = 0; i < ips_count; i++) {
+ line = i_stream_read_next_line(ctx->input);
+ if (line != NULL && strcmp(line, "NOTFOUND") == 0) {
+ i_error("%s: doesn't exist",
+ net_ip2addr(&ips[i]));
+ if (doveadm_exit_code == 0)
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "OK") != 0) {
+ i_error("%s: %s", net_ip2addr(&ips[i]), line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (doveadm_verbose) {
+ i_info("%s: %s", net_ip2addr(&ips[i]), success_result);
+ }
+ }
+ director_disconnect(ctx);
+}
+
+static void cmd_director_remove(struct doveadm_cmd_context *cctx)
+{
+ cmd_director_ipcmd("HOST-REMOVE", "removed", cctx);
+}
+
+static void cmd_director_up(struct doveadm_cmd_context *cctx)
+{
+ cmd_director_ipcmd("HOST-UP", "up", cctx);
+}
+
+static void cmd_director_down(struct doveadm_cmd_context *cctx)
+{
+ cmd_director_ipcmd("HOST-DOWN", "down", cctx);
+}
+
+static void cmd_director_move(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ struct ip_addr *ips;
+ unsigned int ips_count, user_hash;
+ const char *line, *ip_str;
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->user == NULL || ctx->host == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ if (!user_hash_expand(ctx->user, &user_hash) ||
+ director_get_host(ctx->host, &ips, &ips_count) != 0) {
+ director_disconnect(ctx);
+ return;
+ }
+ ip_str = net_ip2addr(&ips[0]);
+ director_send(ctx, t_strdup_printf(
+ "USER-MOVE\t%u\t%s\n", user_hash, ip_str));
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "OK") == 0) {
+ if (doveadm_verbose)
+ i_info("User hash %u moved to %s", user_hash, ip_str);
+ } else if (strcmp(line, "NOTFOUND") == 0) {
+ i_error("Host '%s' doesn't exist", ip_str);
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (strcmp(line, "TRYAGAIN") == 0) {
+ i_error("User is already being moved, "
+ "wait a while for it to be finished");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else {
+ i_error("failed: %s", line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ director_disconnect(ctx);
+}
+
+static void cmd_director_kick(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ const char *line;
+ string_t *cmd = t_str_new(64);
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->user == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ if (ctx->passdb_field == NULL) {
+ str_append(cmd, "USER-KICK\t");
+ str_append_tabescaped(cmd, ctx->user);
+ str_append_c(cmd, '\n');
+ } else {
+ str_append(cmd, "USER-KICK-ALT\t");
+ str_append_tabescaped(cmd, ctx->passdb_field);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, ctx->user);
+ str_append_c(cmd, '\n');
+ }
+ director_send(ctx, str_c(cmd));
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "OK") == 0) {
+ if (doveadm_verbose)
+ i_info("User %s kicked", ctx->user);
+ } else {
+ i_error("failed: %s", line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ director_disconnect(ctx);
+}
+
+static void cmd_director_flush_all(struct director_context *ctx)
+{
+ const char *line;
+
+ if (ctx->force_flush)
+ line = "HOST-FLUSH\n";
+ else if (ctx->max_parallel > 0) {
+ line = t_strdup_printf("HOST-RESET-USERS\t\t%lld\n",
+ (long long)ctx->max_parallel);
+ } else {
+ line = "HOST-RESET-USERS\n";
+ }
+ director_send(ctx, line);
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "OK") != 0) {
+ i_error("failed: %s", line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (doveadm_verbose)
+ i_info("flushed");
+ director_disconnect(ctx);
+}
+
+static void cmd_director_flush(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ struct ip_addr *ips;
+ unsigned int i, ips_count;
+ struct ip_addr ip;
+ const char *line;
+ string_t *cmd;
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->host == NULL) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ if (strcmp(ctx->host, "all") == 0) {
+ cmd_director_flush_all(ctx);
+ return;
+ }
+ if (net_addr2ip(ctx->host, &ip) == 0) {
+ ips = &ip;
+ ips_count = 1;
+ } else if (director_get_host(ctx->host, &ips, &ips_count) != 0) {
+ director_disconnect(ctx);
+ return;
+ }
+
+ cmd = t_str_new(64);
+ for (i = 0; i < ips_count; i++) {
+ ip = ips[i];
+ str_truncate(cmd, 0);
+ if (ctx->force_flush)
+ str_printfa(cmd, "HOST-FLUSH\t%s\n", net_ip2addr(&ip));
+ else {
+ str_printfa(cmd, "HOST-RESET-USERS\t%s", net_ip2addr(&ip));
+ if (ctx->max_parallel > 0) {
+ str_printfa(cmd, "\t%lld",
+ (long long)ctx->max_parallel);
+ }
+ str_append_c(cmd, '\n');
+ }
+ director_send(ctx, str_c(cmd));
+ }
+ for (i = 0; i < ips_count; i++) {
+ line = i_stream_read_next_line(ctx->input);
+ if (line != NULL && strcmp(line, "NOTFOUND") == 0) {
+ i_warning("%s: doesn't exist",
+ net_ip2addr(&ips[i]));
+ if (doveadm_exit_code == 0)
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "OK") != 0) {
+ i_warning("%s: %s", net_ip2addr(&ips[i]), line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (doveadm_verbose) {
+ i_info("%s: flushed", net_ip2addr(&ips[i]));
+ }
+ }
+ director_disconnect(ctx);
+}
+
+static void cmd_director_dump(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ const char *line, *const *args;
+
+ ctx = cmd_director_init(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ if (ctx->explicit_socket_path)
+ doveadm_print_formatted_set_format("doveadm director %{command} -a %{socket-path} %{host} %{vhost_count}\n");
+ else
+ doveadm_print_formatted_set_format("doveadm director %{command} %{host} %{vhost_count}\n");
+
+ doveadm_print_header_simple("command");
+ doveadm_print_header_simple("socket-path");
+ doveadm_print_header_simple("host");
+ doveadm_print_header_simple("vhost_count");
+
+ director_send(ctx, "HOST-LIST\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) >= 2) {
+ const char *host = args[0];
+ const char *tag = args[3];
+ /* this is guaranteed to be at least NULL */
+ if (tag != NULL &&
+ *tag != '\0')
+ host = t_strdup_printf("%s@%s", host,
+ tag);
+ doveadm_print("add");
+ doveadm_print(ctx->socket_path);
+ doveadm_print(host);
+ doveadm_print(args[1]);
+ }
+ } T_END;
+ }
+
+ director_send(ctx, "HOST-LIST-REMOVED\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ doveadm_print("remove");
+ doveadm_print(ctx->socket_path);
+ doveadm_print(line);
+ doveadm_print("");
+ }
+ if (line == NULL)
+ director_disconnected(ctx);
+ director_disconnect(ctx);
+}
+
+
+static void director_read_ok_reply(struct director_context *ctx)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ director_disconnected(ctx);
+ } else if (strcmp(line, "NOTFOUND") == 0) {
+ i_error("Not found");
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (strcmp(line, "OK") != 0) {
+ i_error("Failed: %s", line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+}
+
+static void cmd_director_ring_add(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ struct ip_addr ip;
+ in_port_t port = 0;
+ string_t *str = t_str_new(64);
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->ip == NULL ||
+ net_addr2ip(ctx->ip, &ip) < 0 ||
+ (ctx->port != 0 && net_str2port(ctx->port, &port) < 0)) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ str_printfa(str, "DIRECTOR-ADD\t%s", net_ip2addr(&ip));
+ if (port != 0)
+ str_printfa(str, "\t%u", port);
+ str_append_c(str, '\n');
+ director_send(ctx, str_c(str));
+ director_read_ok_reply(ctx);
+ director_disconnect(ctx);
+}
+
+static void cmd_director_ring_remove(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ struct ip_addr ip;
+ string_t *str = t_str_new(64);
+ in_port_t port = 0;
+
+ ctx = cmd_director_init(cctx);
+ if (ctx->ip == NULL ||
+ net_addr2ip(ctx->ip, &ip) < 0 ||
+ (ctx->port != NULL && net_str2port(ctx->port, &port) < 0)) {
+ director_cmd_help(cctx->cmd);
+ return;
+ }
+
+ str_printfa(str, "DIRECTOR-REMOVE\t%s", net_ip2addr(&ip));
+ if (port != 0)
+ str_printfa(str, "\t%u", port);
+ str_append_c(str, '\n');
+ director_send(ctx, str_c(str));
+ director_read_ok_reply(ctx);
+ director_disconnect(ctx);
+}
+
+static void cmd_director_ring_status(struct doveadm_cmd_context *cctx)
+{
+ struct director_context *ctx;
+ const char *line, *const *args;
+
+ ctx = cmd_director_init(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header_simple("director ip");
+ doveadm_print_header_simple("port");
+ doveadm_print_header_simple("type");
+ doveadm_print_header_simple("last failed");
+ doveadm_print_header_simple("status");
+ doveadm_print_header_simple("ping ms");
+ doveadm_print_header_simple("input");
+ doveadm_print_header_simple("output");
+ doveadm_print_header_simple("buffered");
+ doveadm_print_header_simple("buffered peak");
+ doveadm_print_header_simple("last read");
+ doveadm_print_header_simple("last write");
+
+ director_send(ctx, "DIRECTOR-LIST\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ unsigned int i;
+ time_t ts;
+
+ args = t_strsplit_tabescaped(line);
+ for (i = 0; i < 12 && args[i] != NULL; i++) {
+ if ((i == 3 || i == 10 || i == 11) &&
+ str_to_time(args[i], &ts) == 0) {
+ if (ts == 0)
+ doveadm_print("never");
+ else
+ doveadm_print(unixdate2str(ts));
+ } else {
+ doveadm_print(args[i]);
+ }
+ }
+ for (; i < 12; i++)
+ doveadm_print("-");
+ } T_END;
+ }
+ if (line == NULL)
+ director_disconnected(ctx);
+ director_disconnect(ctx);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_director[] = {
+{
+ .name = "director status",
+ .cmd = cmd_director_status,
+ .usage = "[-a <director socket path>] [<user>] [<tag>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "tag", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director map",
+ .cmd = cmd_director_map,
+ .usage = "[-a <director socket path>] [-f <users file>] [-h | -u] [<host>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('f', "users-file", CMD_PARAM_ISTREAM, 0)
+DOVEADM_CMD_PARAM('h', "hash-map", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('u', "user-map", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director add",
+ .cmd = cmd_director_add,
+ .usage = "[-a <director socket path>] [-t <tag>] <host> [<vhost count>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('t', "tag", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "vhost-count", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director update",
+ .cmd = cmd_director_update,
+ .usage = "[-a <director socket path>] <host> <vhost count>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "vhost-count", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director up",
+ .cmd = cmd_director_up,
+ .usage = "[-a <director socket path>] <host>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director down",
+ .cmd = cmd_director_down,
+ .usage = "[-a <director socket path>] <host>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director remove",
+ .cmd = cmd_director_remove,
+ .usage = "[-a <director socket path>] <host>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director move",
+ .cmd = cmd_director_move,
+ .usage = "[-a <director socket path>] <user> <host>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director kick",
+ .cmd = cmd_director_kick,
+ .usage = "[-a <director socket path>] [-f <passdb field>] <user>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('f', "passdb-field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director flush",
+ .cmd = cmd_director_flush,
+ .usage = "[-a <director socket path>] [-F] [--max-parallel <n>] <host>|all",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('F', "force-flush", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "max-parallel", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director dump",
+ .cmd = cmd_director_dump,
+ .usage = "[-a <director socket path>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director ring add",
+ .cmd = cmd_director_ring_add,
+ .usage = "[-a <director socket path>] <ip> [<port>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "ip", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "port", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director ring remove",
+ .cmd = cmd_director_ring_remove,
+ .usage = "[-a <director socket path>] <ip> [<port>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "ip", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "port", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "director ring status",
+ .cmd = cmd_director_ring_status,
+ .usage = "[-a <director socket path>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void director_cmd_help(const struct doveadm_cmd_ver2 *cmd)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_director); i++) {
+ if (doveadm_cmd_director[i].cmd == cmd->cmd)
+ help_ver2(&doveadm_cmd_director[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_director_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_director); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_director[i]);
+}
diff --git a/src/doveadm/doveadm-dsync.c b/src/doveadm/doveadm-dsync.c
new file mode 100644
index 0000000..b0b896c
--- /dev/null
+++ b/src/doveadm/doveadm-dsync.c
@@ -0,0 +1,1567 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "array.h"
+#include "execv-const.h"
+#include "child-wait.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "iostream-rawlog.h"
+#include "write-full.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "process-title.h"
+#include "settings-parser.h"
+#include "imap-util.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mailbox-list-private.h"
+#include "doveadm-settings.h"
+#include "doveadm-mail.h"
+#include "doveadm-print.h"
+#include "doveadm-server.h"
+#include "client-connection.h"
+#include "server-connection.h"
+#include "dsync/dsync-brain.h"
+#include "dsync/dsync-ibc.h"
+#include "doveadm-dsync.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <sys/wait.h>
+
+#define DSYNC_COMMON_GETOPT_ARGS "+1a:dDEfg:I:l:m:n:NO:Pr:Rs:t:e:T:Ux:"
+#define DSYNC_REMOTE_CMD_EXIT_WAIT_SECS 30
+/* The default vname_escape_char to use unless overridden by BROKENCHAR
+ setting. Note that it's only used for internal dsync names, so it won't end
+ up in permanent storage names. The only requirement for it is that it's not
+ the same as the hierarchy separator. */
+#define DSYNC_LIST_VNAME_ESCAPE_CHAR '%'
+/* In case DSYNC_LIST_VNAME_ESCAPE_CHAR is the hierarchy separator,
+ use this instead. */
+#define DSYNC_LIST_VNAME_ALT_ESCAPE_CHAR '~'
+
+#define DSYNC_DEFAULT_IO_STREAM_TIMEOUT_SECS (60*10)
+
+enum dsync_run_type {
+ DSYNC_RUN_TYPE_LOCAL,
+ DSYNC_RUN_TYPE_STREAM,
+ DSYNC_RUN_TYPE_CMD
+};
+
+struct dsync_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ enum dsync_brain_sync_type sync_type;
+ const char *mailbox;
+ const char *sync_flags;
+ const char *virtual_all_box;
+ guid_128_t mailbox_guid;
+ const char *state_input, *rawlog_path;
+ ARRAY_TYPE(const_string) exclude_mailboxes;
+ ARRAY_TYPE(const_string) namespace_prefixes;
+ time_t sync_since_timestamp;
+ time_t sync_until_timestamp;
+ uoff_t sync_max_size;
+ unsigned int io_timeout_secs;
+
+ const char *remote_name;
+ const char *local_location;
+ pid_t remote_pid;
+ const char *const *remote_cmd_args;
+ struct child_wait *child_wait;
+ int exit_status;
+
+ int fd_in, fd_out, fd_err;
+ struct io *io_err;
+ struct istream *input, *err_stream;
+ struct ostream *output;
+ size_t input_orig_bufsize, output_orig_bufsize;
+
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream *ssl_iostream;
+
+ enum dsync_run_type run_type;
+ struct server_connection *tcp_conn;
+ const char *error;
+
+ unsigned int lock_timeout;
+ unsigned int import_commit_msgs_interval;
+
+ bool lock:1;
+ bool purge_remote:1;
+ bool sync_visible_namespaces:1;
+ bool default_replica_location:1;
+ bool oneway:1;
+ bool backup:1;
+ bool reverse_backup:1;
+ bool remote_user_prefix:1;
+ bool no_mail_sync:1;
+ bool local_location_from_arg:1;
+ bool replicator_notify:1;
+ bool exited:1;
+ bool empty_hdr_workaround:1;
+ bool no_header_hashes:1;
+};
+
+static bool legacy_dsync = FALSE;
+
+static void dsync_cmd_switch_ioloop_to(struct dsync_cmd_context *ctx,
+ struct ioloop *ioloop)
+{
+ if (ctx->input != NULL)
+ i_stream_switch_ioloop_to(ctx->input, ioloop);
+ if (ctx->output != NULL)
+ o_stream_switch_ioloop_to(ctx->output, ioloop);
+}
+
+static void remote_error_input(struct dsync_cmd_context *ctx)
+{
+ const unsigned char *data;
+ size_t size;
+ const char *line;
+
+ switch (i_stream_read(ctx->err_stream)) {
+ case -2:
+ data = i_stream_get_data(ctx->err_stream, &size);
+ fprintf(stderr, "%.*s", (int)size, data);
+ i_stream_skip(ctx->err_stream, size);
+ break;
+ case -1:
+ io_remove(&ctx->io_err);
+ break;
+ default:
+ while ((line = i_stream_next_line(ctx->err_stream)) != NULL)
+ fprintf(stderr, "%s\n", line);
+ break;
+ }
+}
+
+static void
+run_cmd(struct dsync_cmd_context *ctx, const char *const *args)
+{
+ struct doveadm_cmd_context *cctx = ctx->ctx.cctx;
+ int fd_in[2], fd_out[2], fd_err[2];
+
+ ctx->remote_cmd_args = p_strarray_dup(ctx->ctx.pool, args);
+
+ if (pipe(fd_in) < 0 || pipe(fd_out) < 0 || pipe(fd_err) < 0)
+ i_fatal("pipe() failed: %m");
+
+ ctx->remote_pid = fork();
+ switch (ctx->remote_pid) {
+ case -1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ /* child, which will execute the proxy server. stdin/stdout
+ goes to pipes which we'll pass to proxy client. */
+ if (dup2(fd_in[0], STDIN_FILENO) < 0 ||
+ dup2(fd_out[1], STDOUT_FILENO) < 0 ||
+ dup2(fd_err[1], STDERR_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+
+ i_close_fd(&fd_in[0]);
+ i_close_fd(&fd_in[1]);
+ i_close_fd(&fd_out[0]);
+ i_close_fd(&fd_out[1]);
+ i_close_fd(&fd_err[0]);
+ i_close_fd(&fd_err[1]);
+
+ execvp_const(args[0], args);
+ default:
+ /* parent */
+ break;
+ }
+
+ i_close_fd(&fd_in[0]);
+ i_close_fd(&fd_out[1]);
+ i_close_fd(&fd_err[1]);
+ ctx->fd_in = fd_out[0];
+ ctx->fd_out = fd_in[1];
+ ctx->fd_err = fd_err[0];
+
+ if (ctx->remote_user_prefix) {
+ const char *prefix =
+ t_strdup_printf("%s\n", cctx->username);
+ if (write_full(ctx->fd_out, prefix, strlen(prefix)) < 0)
+ i_fatal("write(remote out) failed: %m");
+ }
+
+ fd_set_nonblock(ctx->fd_err, TRUE);
+ ctx->err_stream = i_stream_create_fd(ctx->fd_err, IO_BLOCK_SIZE);
+ i_stream_set_return_partial_line(ctx->err_stream, TRUE);
+}
+
+static void
+mirror_get_remote_cmd_line(const char *const *argv,
+ const char *const **cmd_args_r)
+{
+ ARRAY_TYPE(const_string) cmd_args;
+ unsigned int i;
+ const char *p;
+
+ i_assert(argv[0] != NULL);
+
+ t_array_init(&cmd_args, 16);
+ for (i = 0; argv[i] != NULL; i++) {
+ p = argv[i];
+ array_push_back(&cmd_args, &p);
+ }
+
+ if (legacy_dsync) {
+ /* we're executing dsync */
+ p = "server";
+ } else if (i > 0 && strcmp(argv[i-1], "dsync-server") == 0) {
+ /* Caller already specified dsync-server in parameters.
+ This is a common misconfiguration, so just allow it. */
+ p = NULL;
+ } else {
+ /* we're executing doveadm */
+ p = "dsync-server";
+ }
+ if (p != NULL)
+ array_push_back(&cmd_args, &p);
+ array_append_zero(&cmd_args);
+ *cmd_args_r = array_front(&cmd_args);
+}
+
+static const char *const *
+get_ssh_cmd_args(const char *host, const char *login, const char *mail_user)
+{
+ static struct var_expand_table static_tab[] = {
+ { 'u', NULL, "user" },
+ { '\0', NULL, "login" },
+ { '\0', NULL, "host" },
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+ ARRAY_TYPE(const_string) cmd_args;
+ string_t *str, *str2;
+ const char *value, *const *args, *error;
+
+ tab = t_malloc_no0(sizeof(static_tab));
+ memcpy(tab, static_tab, sizeof(static_tab));
+
+ tab[0].value = mail_user;
+ tab[1].value = login;
+ tab[2].value = host;
+
+ t_array_init(&cmd_args, 8);
+ str = t_str_new(128);
+ str2 = t_str_new(128);
+ args = t_strsplit(doveadm_settings->dsync_remote_cmd, " ");
+ for (; *args != NULL; args++) {
+ if (strchr(*args, '%') == NULL)
+ value = *args;
+ else {
+ /* some automation: if parameter's all %variables
+ expand to empty, but the %variable isn't the only
+ text in the parameter, skip it. */
+ str_truncate(str, 0);
+ str_truncate(str2, 0);
+ if (var_expand(str, *args, tab, &error) <= 0 ||
+ var_expand(str2, *args, static_tab, &error) <= 0) {
+ i_error("Failed to expand dsync_remote_cmd=%s: %s",
+ *args, error);
+ }
+ if (strcmp(str_c(str), str_c(str2)) == 0 &&
+ str_len(str) > 0)
+ continue;
+ value = t_strdup(str_c(str));
+ }
+ array_push_back(&cmd_args, &value);
+ }
+ array_append_zero(&cmd_args);
+ return array_front(&cmd_args);
+}
+
+static bool mirror_get_remote_cmd(struct dsync_cmd_context *ctx,
+ const char *user,
+ const char *const **cmd_args_r)
+{
+ const char *p, *host, *const *argv = ctx->ctx.args;
+
+ if (argv[1] != NULL) {
+ /* more than one parameter, so it contains a full command
+ (e.g. ssh host dsync) */
+ mirror_get_remote_cmd_line(argv, cmd_args_r);
+ return TRUE;
+ }
+
+ /* if it begins with /[a-z0-9]+:/, it's a mail location
+ (e.g. mdbox:~/mail) */
+ for (p = argv[0]; *p != '\0'; p++) {
+ if (!i_isalnum(*p)) {
+ if (*p == ':')
+ return FALSE;
+ break;
+ }
+ }
+
+ if (strchr(argv[0], ' ') != NULL || strchr(argv[0], '/') != NULL) {
+ /* a) the whole command is in one string. this is mainly for
+ backwards compatibility.
+ b) script/path */
+ mirror_get_remote_cmd_line(t_strsplit(argv[0], " "),
+ cmd_args_r);
+ return TRUE;
+ }
+
+ /* [user@]host */
+ host = strchr(argv[0], '@');
+ if (host != NULL)
+ user = t_strdup_until(argv[0], host++);
+ else
+ host = argv[0];
+
+ /* we'll assume virtual users, so in user@host it really means not to
+ give ssh a username, but to give dsync -u user parameter. */
+ *cmd_args_r = get_ssh_cmd_args(host, "", user);
+ return TRUE;
+}
+
+static void doveadm_user_init_dsync(struct mail_user *user)
+{
+ struct mail_namespace *ns;
+ char ns_sep = mail_namespaces_get_root_sep(user->namespaces);
+
+ user->dsyncing = TRUE;
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ struct dsync_mailbox_list *dlist =
+ p_new(ns->list->pool, struct dsync_mailbox_list, 1);
+ MODULE_CONTEXT_SET(ns->list, dsync_mailbox_list_module, dlist);
+
+ if (ns->list->set.vname_escape_char == '\0') {
+ ns->list->set.vname_escape_char =
+ ns_sep != DSYNC_LIST_VNAME_ESCAPE_CHAR ?
+ DSYNC_LIST_VNAME_ESCAPE_CHAR :
+ DSYNC_LIST_VNAME_ALT_ESCAPE_CHAR;
+ } else {
+ dlist->have_orig_escape_char = TRUE;
+ }
+ }
+}
+
+static bool
+paths_are_equal(struct mail_namespace *ns1, struct mail_namespace *ns2,
+ enum mailbox_list_path_type type)
+{
+ const char *path1, *path2;
+
+ return mailbox_list_get_root_path(ns1->list, type, &path1) &&
+ mailbox_list_get_root_path(ns2->list, type, &path2) &&
+ strcmp(path1, path2) == 0;
+}
+
+static int
+get_dsync_verify_namespace(struct dsync_cmd_context *ctx,
+ struct mail_user *user, struct mail_namespace **ns_r)
+{
+ struct mail_namespace *ns;
+
+ /* Use the first -n namespace if given */
+ if (array_count(&ctx->namespace_prefixes) > 0) {
+ const char *prefix =
+ array_idx_elem(&ctx->namespace_prefixes, 0);
+ ns = mail_namespace_find(user->namespaces, prefix);
+ if (ns == NULL) {
+ i_error("Namespace not found: '%s'", prefix);
+ ctx->ctx.exit_code = DOVEADM_EX_NOTFOUND;
+ return -1;
+ }
+ *ns_r = ns;
+ return 0;
+ }
+
+ /* Prefer prefix="" namespace over inbox=yes namespace. Either it uses
+ the global mail_location, which is good, or it might have
+ overwritten location in case of e.g. using subscriptions file for
+ all namespaces. This isn't necessarily obvious, so lets make it
+ clearer by failing if it happens. */
+ if ((user->namespaces->flags & NAMESPACE_FLAG_UNUSABLE) == 0) {
+ *ns_r = user->namespaces;
+ i_assert((*ns_r)->prefix_len == 0);
+ } else {
+ /* fallback to inbox=yes */
+ *ns_r = mail_namespace_find_inbox(user->namespaces);
+ }
+ return 0;
+}
+
+static int
+cmd_dsync_run_local(struct dsync_cmd_context *ctx, struct mail_user *user,
+ struct dsync_brain *brain, struct dsync_ibc *ibc2,
+ const char **changes_during_sync_r,
+ enum mail_error *mail_error_r)
+{
+ struct dsync_brain *brain2;
+ struct mail_user *user2;
+ struct mail_namespace *ns, *ns2;
+ struct setting_parser_context *set_parser;
+ const char *location, *error;
+ bool brain1_running, brain2_running, changed1, changed2;
+ bool remote_only_changes;
+ int ret;
+
+ *mail_error_r = 0;
+
+ if (ctx->local_location_from_arg)
+ location = ctx->ctx.args[0];
+ else {
+ i_assert(ctx->local_location != NULL);
+ location = ctx->local_location;
+ }
+
+ i_set_failure_prefix("dsync(%s): ", user->username);
+
+ /* update mail_location and create another user for the
+ second location. */
+ set_parser = mail_storage_service_user_get_settings_parser(ctx->ctx.cur_service_user);
+ if (settings_parse_keyvalue(set_parser, "mail_location", location) < 0)
+ i_unreached();
+ ret = mail_storage_service_next(ctx->ctx.storage_service,
+ ctx->ctx.cur_service_user,
+ &user2, &error);
+ if (ret < 0) {
+ i_error("Failed to initialize user: %s", error);
+ ctx->ctx.exit_code = ret == -1 ? EX_TEMPFAIL : EX_CONFIG;
+ return -1;
+ }
+ doveadm_user_init_dsync(user2);
+
+ if (get_dsync_verify_namespace(ctx, user, &ns) < 0 ||
+ get_dsync_verify_namespace(ctx, user2, &ns2) < 0)
+ return -1;
+ if (mail_namespace_get_sep(ns) != mail_namespace_get_sep(ns2)) {
+ i_error("Mail locations must use the same hierarchy separator "
+ "(specify namespace prefix=\"%s\" "
+ "{ separator } explicitly)", ns->prefix);
+ ctx->ctx.exit_code = EX_CONFIG;
+ mail_user_deinit(&user2);
+ return -1;
+ }
+ if (paths_are_equal(ns, ns2, MAILBOX_LIST_PATH_TYPE_MAILBOX) &&
+ paths_are_equal(ns, ns2, MAILBOX_LIST_PATH_TYPE_INDEX)) {
+ i_error("Both source and destination mail_location "
+ "points to same directory: %s (namespace "
+ "prefix=\"%s\" { location } is set explicitly?)",
+ mailbox_list_get_root_forced(user->namespaces->list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX),
+ ns->prefix);
+ ctx->ctx.exit_code = EX_CONFIG;
+ mail_user_deinit(&user2);
+ return -1;
+ }
+
+ brain2 = dsync_brain_slave_init(user2, ibc2, TRUE, "",
+ doveadm_settings->dsync_alt_char[0]);
+ mail_user_unref(&user2);
+
+ brain1_running = brain2_running = TRUE;
+ changed1 = changed2 = TRUE;
+ while (brain1_running || brain2_running) {
+ if (dsync_brain_has_failed(brain) ||
+ dsync_brain_has_failed(brain2))
+ break;
+ if (doveadm_is_killed()) {
+ i_warning("Killed with signal %d", doveadm_killed_signo());
+ break;
+ }
+
+ i_assert(changed1 || changed2);
+ brain1_running = dsync_brain_run(brain, &changed1);
+ brain2_running = dsync_brain_run(brain2, &changed2);
+ }
+ *changes_during_sync_r = t_strdup(dsync_brain_get_unexpected_changes_reason(brain2, &remote_only_changes));
+ if (dsync_brain_deinit(&brain2, mail_error_r) < 0)
+ return -1;
+ return doveadm_is_killed() ? -1 : 0;
+}
+
+static void cmd_dsync_remote_exited(const struct child_wait_status *status,
+ struct dsync_cmd_context *ctx)
+{
+ ctx->exited = TRUE;
+ ctx->exit_status = status->status;
+ io_loop_stop(current_ioloop);
+}
+
+static void cmd_dsync_wait_remote(struct dsync_cmd_context *ctx)
+{
+ struct timeout *to;
+
+ /* wait in ioloop for the remote process to die. while we're running
+ we're also reading and printing all errors that still coming from
+ it. */
+ to = timeout_add(DSYNC_REMOTE_CMD_EXIT_WAIT_SECS*1000,
+ io_loop_stop, current_ioloop);
+ io_loop_run(current_ioloop);
+ timeout_remove(&to);
+
+ /* io_loop_run() deactivates the context - put it back */
+ mail_storage_service_io_activate_user(ctx->ctx.cur_service_user);
+
+ if (!ctx->exited) {
+ i_error("Remote command process isn't dying, killing it");
+ if (kill(ctx->remote_pid, SIGKILL) < 0 && errno != ESRCH) {
+ i_error("kill(%ld, SIGKILL) failed: %m",
+ (long)ctx->remote_pid);
+ }
+ }
+}
+
+static void cmd_dsync_log_remote_status(int status, bool remote_errors_logged,
+ const char *const *remote_cmd_args)
+{
+ if (status == -1)
+ ;
+ else if (WIFSIGNALED(status)) {
+ i_error("Remote command died with signal %d: %s", WTERMSIG(status),
+ t_strarray_join(remote_cmd_args, " "));
+ } else if (!WIFEXITED(status)) {
+ i_error("Remote command failed with status %d: %s", status,
+ t_strarray_join(remote_cmd_args, " "));
+ } else if (WEXITSTATUS(status) == EX_TEMPFAIL && remote_errors_logged) {
+ /* remote most likely already logged the error.
+ don't bother logging another line about it */
+ } else if (WEXITSTATUS(status) != 0) {
+ i_error("Remote command returned error %d: %s", WEXITSTATUS(status),
+ t_strarray_join(remote_cmd_args, " "));
+ }
+}
+
+static void cmd_dsync_run_remote(struct mail_user *user)
+{
+ i_set_failure_prefix("dsync-local(%s)<%s>: ", user->username, user->session_id);
+ io_loop_run(current_ioloop);
+}
+
+static const char *const *
+parse_ssh_location(const char *location, const char *username)
+{
+ const char *host, *login;
+
+ host = strrchr(location, '@');
+ if (host != NULL)
+ login = t_strdup_until(location, host++);
+ else {
+ host = location;
+ login = "";
+ }
+ return get_ssh_cmd_args(host, login, username);
+}
+
+static struct dsync_ibc *
+cmd_dsync_ibc_stream_init(struct dsync_cmd_context *ctx,
+ const char *name, const char *temp_prefix)
+{
+ if (ctx->input == NULL) {
+ fd_set_nonblock(ctx->fd_in, TRUE);
+ fd_set_nonblock(ctx->fd_out, TRUE);
+ ctx->input = i_stream_create_fd(ctx->fd_in, SIZE_MAX);
+ ctx->output = o_stream_create_fd(ctx->fd_out, SIZE_MAX);
+ } else {
+ i_assert(ctx->fd_in == -1 && ctx->fd_out == -1);
+ ctx->fd_in = i_stream_get_fd(ctx->input);
+ ctx->fd_out = o_stream_get_fd(ctx->output);
+ ctx->input_orig_bufsize = i_stream_get_max_buffer_size(ctx->input);
+ ctx->output_orig_bufsize = o_stream_get_max_buffer_size(ctx->output);
+ i_stream_set_max_buffer_size(ctx->input, SIZE_MAX);
+ o_stream_set_max_buffer_size(ctx->output, SIZE_MAX);
+ }
+ if (ctx->rawlog_path != NULL) {
+ iostream_rawlog_create_path(ctx->rawlog_path,
+ &ctx->input, &ctx->output);
+ }
+ return dsync_ibc_init_stream(ctx->input, ctx->output,
+ name, temp_prefix, ctx->io_timeout_secs);
+}
+
+static void
+dsync_replicator_notify(struct dsync_cmd_context *ctx,
+ enum dsync_brain_sync_type sync_type,
+ const char *state_str)
+{
+#define REPLICATOR_HANDSHAKE "VERSION\treplicator-doveadm-client\t1\t0\n"
+ const char *path;
+ string_t *str;
+ int fd;
+
+ path = t_strdup_printf("%s/replicator-doveadm",
+ ctx->ctx.cur_mail_user->set->base_dir);
+ fd = net_connect_unix(path);
+ if (fd == -1) {
+ if (errno == ECONNREFUSED || errno == ENOENT) {
+ /* replicator not running on this server. ignore. */
+ return;
+ }
+ i_error("net_connect_unix(%s) failed: %m", path);
+ return;
+ }
+ fd_set_nonblock(fd, FALSE);
+
+ str = t_str_new(128);
+ str_append(str, REPLICATOR_HANDSHAKE"NOTIFY\t");
+ str_append_tabescaped(str, ctx->ctx.cur_mail_user->username);
+ str_append_c(str, '\t');
+ if (sync_type == DSYNC_BRAIN_SYNC_TYPE_FULL)
+ str_append_c(str, 'f');
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, state_str);
+ str_append_c(str, '\n');
+ if (write_full(fd, str_data(str), str_len(str)) < 0)
+ i_error("write(%s) failed: %m", path);
+ /* we only wanted to notify replicator. we don't care enough about the
+ answer to wait for it. */
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", path);
+}
+
+static int
+cmd_dsync_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+ struct doveadm_cmd_context *cctx = _ctx->cctx;
+ struct dsync_ibc *ibc, *ibc2 = NULL;
+ struct dsync_brain *brain;
+ struct dsync_brain_settings set;
+ struct mail_namespace *ns;
+ const char *const *strp;
+ enum dsync_brain_flags brain_flags;
+ enum mail_error mail_error = 0, mail_error2;
+ bool remote_errors_logged = FALSE;
+ bool cli = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+ const char *changes_during_sync, *changes_during_sync2 = NULL;
+ bool remote_only_changes;
+ int ret = 0;
+
+ /* replicator_notify indicates here automated attempt,
+ we still want to allow manual sync/backup */
+ if (!cli && ctx->replicator_notify &&
+ mail_user_plugin_getenv_bool(_ctx->cur_mail_user, "noreplicate")) {
+ ctx->ctx.exit_code = DOVEADM_EX_NOREPLICATE;
+ return -1;
+ }
+
+ i_zero(&set);
+ if (cctx->remote_ip.family != 0) {
+ /* include the doveadm client's IP address in the ps output */
+ set.process_title_prefix = t_strdup_printf(
+ "%s ", net_ip2addr(&cctx->remote_ip));
+ }
+ set.sync_since_timestamp = ctx->sync_since_timestamp;
+ set.sync_until_timestamp = ctx->sync_until_timestamp;
+
+ if (set.sync_since_timestamp > 0 && set.sync_until_timestamp > 0 &&
+ set.sync_since_timestamp > set.sync_until_timestamp) {
+ i_fatal("start date is later than end date");
+ }
+
+ set.sync_max_size = ctx->sync_max_size;
+ set.sync_box = ctx->mailbox;
+ set.sync_flag = ctx->sync_flags;
+ set.virtual_all_box = ctx->virtual_all_box;
+ memcpy(set.sync_box_guid, ctx->mailbox_guid, sizeof(set.sync_box_guid));
+ set.lock_timeout_secs = ctx->lock_timeout;
+ set.import_commit_msgs_interval = ctx->import_commit_msgs_interval;
+ set.state = ctx->state_input;
+ set.mailbox_alt_char = doveadm_settings->dsync_alt_char[0];
+ if (*doveadm_settings->dsync_hashed_headers == '\0') {
+ i_error("dsync_hashed_headers must not be empty");
+ ctx->ctx.exit_code = EX_USAGE;
+ return -1;
+ }
+ set.hashed_headers =
+ t_strsplit_spaces(doveadm_settings->dsync_hashed_headers, " ,");
+ if (array_count(&ctx->exclude_mailboxes) > 0) {
+ /* array is NULL-terminated in init() */
+ set.exclude_mailboxes = array_front(&ctx->exclude_mailboxes);
+ }
+ doveadm_user_init_dsync(user);
+
+ t_array_init(&set.sync_namespaces, array_count(&ctx->namespace_prefixes));
+ array_foreach(&ctx->namespace_prefixes, strp) {
+ ns = mail_namespace_find(user->namespaces, *strp);
+ if (ns == NULL) {
+ i_error("Namespace not found: '%s'", *strp);
+ ctx->ctx.exit_code = EX_USAGE;
+ return -1;
+ }
+ array_push_back(&set.sync_namespaces, &ns);
+ }
+
+ if (ctx->run_type == DSYNC_RUN_TYPE_LOCAL)
+ dsync_ibc_init_pipe(&ibc, &ibc2);
+ else {
+ string_t *temp_prefix = t_str_new(64);
+ mail_user_set_get_temp_prefix(temp_prefix, user->set);
+ ibc = cmd_dsync_ibc_stream_init(ctx, ctx->remote_name,
+ str_c(temp_prefix));
+ if (ctx->fd_err != -1) {
+ ctx->io_err = io_add(ctx->fd_err, IO_READ,
+ remote_error_input, ctx);
+ }
+ }
+
+ brain_flags = DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS;
+ if (ctx->sync_visible_namespaces)
+ brain_flags |= DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES;
+ if (ctx->purge_remote)
+ brain_flags |= DSYNC_BRAIN_FLAG_PURGE_REMOTE;
+
+ if (ctx->backup) {
+ if (ctx->reverse_backup)
+ brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_RECV;
+ else
+ brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_SEND;
+ }
+
+ if (ctx->no_mail_sync)
+ brain_flags |= DSYNC_BRAIN_FLAG_NO_MAIL_SYNC;
+ if (ctx->oneway)
+ brain_flags |= DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE;
+ if (ctx->empty_hdr_workaround)
+ brain_flags |= DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND;
+ if (ctx->no_header_hashes)
+ brain_flags |= DSYNC_BRAIN_FLAG_NO_HEADER_HASHES;
+ if (doveadm_debug)
+ brain_flags |= DSYNC_BRAIN_FLAG_DEBUG;
+
+ child_wait_init();
+ brain = dsync_brain_master_init(user, ibc, ctx->sync_type,
+ brain_flags, &set);
+
+ switch (ctx->run_type) {
+ case DSYNC_RUN_TYPE_LOCAL:
+ if (cmd_dsync_run_local(ctx, user, brain, ibc2,
+ &changes_during_sync2, &mail_error) < 0)
+ ret = -1;
+ break;
+ case DSYNC_RUN_TYPE_CMD:
+ ctx->child_wait = child_wait_new_with_pid(ctx->remote_pid,
+ cmd_dsync_remote_exited, ctx);
+ /* fall through */
+ case DSYNC_RUN_TYPE_STREAM:
+ cmd_dsync_run_remote(user);
+ /* io_loop_run() deactivates the context - put it back */
+ mail_storage_service_io_activate_user(ctx->ctx.cur_service_user);
+ break;
+ }
+
+ if (ctx->state_input != NULL) {
+ string_t *state_str = t_str_new(128);
+ dsync_brain_get_state(brain, state_str);
+ doveadm_print(str_c(state_str));
+ }
+
+ changes_during_sync = dsync_brain_get_unexpected_changes_reason(brain, &remote_only_changes);
+ if (changes_during_sync != NULL || changes_during_sync2 != NULL) {
+ /* don't log a warning when running via doveadm server
+ (e.g. called by replicator) */
+ if (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI) {
+ i_warning("Mailbox changes caused a desync. "
+ "You may want to run dsync again: %s",
+ changes_during_sync == NULL ||
+ (remote_only_changes && changes_during_sync2 != NULL) ?
+ changes_during_sync2 : changes_during_sync);
+ }
+ ctx->ctx.exit_code = 2;
+ }
+ if (dsync_brain_deinit(&brain, &mail_error2) < 0)
+ ret = -1;
+ if (ret < 0) {
+ /* tempfail is the default error. prefer to use a non-tempfail
+ if that exists. */
+ if (mail_error2 != 0 &&
+ (mail_error == 0 || mail_error == MAIL_ERROR_TEMP))
+ mail_error = mail_error2;
+ doveadm_mail_failed_error(&ctx->ctx, mail_error);
+ }
+ dsync_ibc_deinit(&ibc);
+ if (ibc2 != NULL)
+ dsync_ibc_deinit(&ibc2);
+ ssl_iostream_destroy(&ctx->ssl_iostream);
+ if (ctx->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&ctx->ssl_ctx);
+ if (ctx->input != NULL) {
+ i_stream_set_max_buffer_size(ctx->input, ctx->input_orig_bufsize);
+ i_stream_unref(&ctx->input);
+ }
+ if (ctx->output != NULL) {
+ o_stream_set_max_buffer_size(ctx->output, ctx->output_orig_bufsize);
+ o_stream_unref(&ctx->output);
+ }
+ if (ctx->fd_in != -1) {
+ if (ctx->fd_out != ctx->fd_in)
+ i_close_fd(&ctx->fd_out);
+ i_close_fd(&ctx->fd_in);
+ }
+ /* print any final errors after the process has died. not closing
+ stdin/stdout before wait() may cause the process to hang, but stderr
+ shouldn't (at least with ssh) and we need stderr to be open to be
+ able to print the final errors */
+ if (ctx->run_type == DSYNC_RUN_TYPE_CMD) {
+ cmd_dsync_wait_remote(ctx);
+ remote_error_input(ctx);
+ remote_errors_logged = ctx->err_stream->v_offset > 0;
+ i_stream_destroy(&ctx->err_stream);
+ cmd_dsync_log_remote_status(ctx->exit_status, remote_errors_logged,
+ ctx->remote_cmd_args);
+ } else {
+ i_assert(ctx->err_stream == NULL);
+ }
+ io_remove(&ctx->io_err);
+ i_close_fd(&ctx->fd_err);
+
+ if (ctx->child_wait != NULL)
+ child_wait_free(&ctx->child_wait);
+ child_wait_deinit();
+ return ret;
+}
+
+static void dsync_connected_callback(int exit_code, const char *error,
+ void *context)
+{
+ struct dsync_cmd_context *ctx = context;
+
+ ctx->ctx.exit_code = exit_code;
+ switch (exit_code) {
+ case 0:
+ server_connection_extract(ctx->tcp_conn, &ctx->input,
+ &ctx->output, &ctx->ssl_iostream);
+ break;
+ case SERVER_EXIT_CODE_DISCONNECTED:
+ ctx->ctx.exit_code = EX_TEMPFAIL;
+ ctx->error = p_strdup_printf(ctx->ctx.pool,
+ "Disconnected from remote: %s", error);
+ break;
+ case EX_NOUSER:
+ ctx->error = "Unknown user in remote";
+ break;
+ case DOVEADM_EX_NOREPLICATE:
+ if (doveadm_debug)
+ i_debug("user is disabled for replication");
+ break;
+ default:
+ ctx->error = p_strdup_printf(ctx->ctx.pool,
+ "Failed to start remote dsync-server command: "
+ "Remote exit_code=%u %s",
+ exit_code, error == NULL ? "" : error);
+ break;
+ }
+ io_loop_stop(current_ioloop);
+}
+
+static int dsync_init_ssl_ctx(struct dsync_cmd_context *ctx,
+ const struct master_service_ssl_settings *ssl_set,
+ const char **error_r)
+{
+ struct ssl_iostream_settings ssl_ctx_set;
+
+ if (ctx->ssl_ctx != NULL)
+ return 0;
+
+ master_service_ssl_client_settings_to_iostream_set(ssl_set,
+ pool_datastack_create(), &ssl_ctx_set);
+ return ssl_iostream_client_context_cache_get(&ssl_ctx_set,
+ &ctx->ssl_ctx, error_r);
+}
+
+static void dsync_server_run_command(struct dsync_cmd_context *ctx,
+ struct server_connection *conn)
+{
+ struct doveadm_cmd_context *cctx = ctx->ctx.cctx;
+ /* <flags> <username> <command> [<args>] */
+ string_t *cmd = t_str_new(256);
+ if (doveadm_debug)
+ str_append_c(cmd, 'D');
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, cctx->username);
+ str_append(cmd, "\tdsync-server\t-u");
+ str_append_tabescaped(cmd, cctx->username);
+ if (ctx->replicator_notify)
+ str_append(cmd, "\t-U");
+ str_append_c(cmd, '\n');
+
+ ctx->tcp_conn = conn;
+ server_connection_cmd(conn, str_c(cmd), NULL,
+ dsync_connected_callback, ctx);
+ io_loop_run(current_ioloop);
+ ctx->tcp_conn = NULL;
+}
+
+static int
+dsync_connect_tcp(struct dsync_cmd_context *ctx,
+ const struct master_service_ssl_settings *ssl_set,
+ const char *target, bool ssl, const char **error_r)
+{
+ struct doveadm_server *server;
+ struct server_connection *conn;
+ struct ioloop *prev_ioloop, *ioloop;
+ const char *p, *error;
+
+ server = p_new(ctx->ctx.pool, struct doveadm_server, 1);
+ server->name = p_strdup(ctx->ctx.pool, target);
+ p = strrchr(server->name, ':');
+ server->hostname = p == NULL ? server->name :
+ p_strdup_until(ctx->ctx.pool, server->name, p);
+ if (ssl) {
+ if (dsync_init_ssl_ctx(ctx, ssl_set, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL context: %s", error);
+ return -1;
+ }
+ server->ssl_flags = PROXY_SSL_FLAG_YES;
+ server->ssl_ctx = ctx->ssl_ctx;
+ }
+ p_array_init(&server->connections, ctx->ctx.pool, 1);
+ p_array_init(&server->queue, ctx->ctx.pool, 1);
+
+ prev_ioloop = current_ioloop;
+ ioloop = io_loop_create();
+ dsync_cmd_switch_ioloop_to(ctx, ioloop);
+
+ if (doveadm_verbose_proctitle) {
+ process_title_set(t_strdup_printf(
+ "[dsync - connecting to %s]", server->name));
+ }
+ if (server_connection_create(server, &conn, &error) < 0) {
+ ctx->error = p_strdup_printf(ctx->ctx.pool,
+ "Couldn't create server connection: %s", error);
+ } else {
+ if (doveadm_verbose_proctitle) {
+ process_title_set(t_strdup_printf(
+ "[dsync - running dsync-server on %s]", server->name));
+ }
+
+ dsync_server_run_command(ctx, conn);
+ }
+
+ if (array_count(&server->connections) > 0)
+ server_connection_destroy(&conn);
+
+ dsync_cmd_switch_ioloop_to(ctx, prev_ioloop);
+ io_loop_destroy(&ioloop);
+
+ if (ctx->error != NULL) {
+ ssl_iostream_context_unref(&ctx->ssl_ctx);
+ *error_r = ctx->error;
+ ctx->error = NULL;
+ return -1;
+ }
+ ctx->run_type = DSYNC_RUN_TYPE_STREAM;
+ return 0;
+}
+
+static int
+parse_location(struct dsync_cmd_context *ctx,
+ const struct master_service_ssl_settings *ssl_set,
+ const char *location,
+ const char *const **remote_cmd_args_r, const char **error_r)
+{
+ struct doveadm_cmd_context *cctx = ctx->ctx.cctx;
+
+ if (str_begins(location, "tcp:")) {
+ /* TCP connection to remote dsync */
+ ctx->remote_name = location+4;
+ return dsync_connect_tcp(ctx, ssl_set, ctx->remote_name,
+ FALSE, error_r);
+ }
+ if (str_begins(location, "tcps:")) {
+ /* TCP+SSL connection to remote dsync */
+ ctx->remote_name = location+5;
+ return dsync_connect_tcp(ctx, ssl_set, ctx->remote_name,
+ TRUE, error_r);
+ }
+
+ if (str_begins(location, "remote:")) {
+ /* this is a remote (ssh) command */
+ ctx->remote_name = location+7;
+ } else if (str_begins(location, "remoteprefix:")) {
+ /* this is a remote (ssh) command with a "user\n"
+ prefix sent before dsync actually starts */
+ ctx->remote_name = location+13;
+ ctx->remote_user_prefix = TRUE;
+ } else {
+ /* local with e.g. maildir:path */
+ ctx->remote_name = NULL;
+ return 0;
+ }
+ *remote_cmd_args_r =
+ parse_ssh_location(ctx->remote_name, cctx->username);
+ return 0;
+}
+
+static int cmd_dsync_prerun(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_storage_service_user *service_user,
+ const char **error_r)
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+ struct doveadm_cmd_context *cctx = _ctx->cctx;
+ const char *const *remote_cmd_args = NULL;
+ const struct mail_user_settings *user_set;
+ const struct master_service_ssl_settings *ssl_set;
+ const char *username = "";
+
+ user_set = mail_storage_service_user_get_set(service_user)[0];
+ ssl_set = mail_storage_service_user_get_ssl_settings(service_user);
+
+ ctx->fd_in = -1;
+ ctx->fd_out = -1;
+ ctx->fd_err = -1;
+ ctx->run_type = DSYNC_RUN_TYPE_LOCAL;
+ ctx->remote_name = "remote";
+
+ if (ctx->default_replica_location) {
+ ctx->local_location =
+ mail_user_set_plugin_getenv(user_set, "mail_replica");
+ if (ctx->local_location == NULL ||
+ *ctx->local_location == '\0') {
+ *error_r = "User has no mail_replica in userdb";
+ _ctx->exit_code = DOVEADM_EX_NOTFOUND;
+ return -1;
+ }
+ } else {
+ /* if we're executing remotely, give -u parameter if we also
+ did a userdb lookup. */
+ if ((_ctx->service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0)
+ username = cctx->username;
+
+ if (!mirror_get_remote_cmd(ctx, username, &remote_cmd_args)) {
+ /* it's a mail_location */
+ if (_ctx->args[1] != NULL)
+ doveadm_mail_help_name(_ctx->cmd->name);
+ ctx->local_location = _ctx->args[0];
+ ctx->local_location_from_arg = TRUE;
+ }
+ }
+
+ if (remote_cmd_args == NULL && ctx->local_location != NULL) {
+ if (parse_location(ctx, ssl_set, ctx->local_location,
+ &remote_cmd_args, error_r) < 0)
+ return -1;
+ }
+
+ if (remote_cmd_args != NULL) {
+ /* do this before mail_storage_service_next() in case it
+ drops process privileges */
+ run_cmd(ctx, remote_cmd_args);
+ ctx->run_type = DSYNC_RUN_TYPE_CMD;
+ }
+
+ if (ctx->sync_visible_namespaces &&
+ ctx->run_type == DSYNC_RUN_TYPE_LOCAL)
+ i_fatal("-N parameter requires syncing with remote host");
+ return 0;
+}
+
+static void cmd_dsync_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+
+ if (ctx->default_replica_location) {
+ if (args[0] != NULL)
+ i_error("Don't give mail location with -d parameter");
+ } else {
+ if (args[0] == NULL)
+ doveadm_mail_help_name(_ctx->cmd->name);
+ }
+ if (array_count(&ctx->exclude_mailboxes) > 0)
+ array_append_zero(&ctx->exclude_mailboxes);
+
+ lib_signals_ignore(SIGHUP, TRUE);
+}
+
+static void cmd_dsync_preinit(struct doveadm_mail_cmd_context *ctx)
+{
+ if ((ctx->service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) == 0)
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR;
+}
+
+static bool
+cmd_mailbox_dsync_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+ const char *str, *error;
+ bool utc;
+
+ switch (c) {
+ case '1':
+ ctx->oneway = TRUE;
+ ctx->backup = TRUE;
+ break;
+ case 'a':
+ ctx->virtual_all_box = optarg;
+ break;
+ case 'd':
+ ctx->default_replica_location = TRUE;
+ break;
+ case 'E':
+ /* dsync wrapper detection flag */
+ legacy_dsync = TRUE;
+ break;
+ case 'f':
+ ctx->sync_type = DSYNC_BRAIN_SYNC_TYPE_FULL;
+ break;
+ case 'O': {
+ const char *str = optarg;
+
+ if (strchr(str, ' ') != NULL)
+ i_fatal("-O parameter doesn't support multiple flags currently");
+ if (str[0] == '-')
+ str++;
+ if (str[0] == '\\' && imap_parse_system_flag(str) == 0)
+ i_fatal("Invalid system flag given for -O parameter: '%s'", str);
+ ctx->sync_flags = optarg;
+ break;
+ }
+ case 'g':
+ if (optarg[0] == '\0')
+ ctx->no_mail_sync = TRUE;
+ else if (guid_128_from_string(optarg, ctx->mailbox_guid) < 0 ||
+ guid_128_is_empty(ctx->mailbox_guid))
+ i_fatal("Invalid -g parameter: %s", optarg);
+ break;
+ case 'l':
+ ctx->lock = TRUE;
+ if (str_to_uint(optarg, &ctx->lock_timeout) < 0)
+ i_fatal("Invalid -l parameter: %s", optarg);
+ break;
+ case 'm':
+ if (optarg[0] == '\0')
+ ctx->no_mail_sync = TRUE;
+ else
+ ctx->mailbox = optarg;
+ break;
+ case 'x':
+ str = optarg;
+ array_push_back(&ctx->exclude_mailboxes, &str);
+ break;
+ case 'n':
+ str = optarg;
+ array_push_back(&ctx->namespace_prefixes, &str);
+ break;
+ case 'N':
+ ctx->sync_visible_namespaces = TRUE;
+ break;
+ case 'P':
+ ctx->purge_remote = TRUE;
+ break;
+ case 'r':
+ ctx->rawlog_path = optarg;
+ break;
+ case 'R':
+ ctx->reverse_backup = TRUE;
+ break;
+ case 's':
+ if (ctx->sync_type != DSYNC_BRAIN_SYNC_TYPE_FULL &&
+ *optarg != '\0')
+ ctx->sync_type = DSYNC_BRAIN_SYNC_TYPE_STATE;
+ ctx->state_input = optarg;
+ break;
+ case 't':
+ if (mail_parse_human_timestamp(optarg, &ctx->sync_since_timestamp, &utc) < 0)
+ i_fatal("Invalid -t parameter: %s", optarg);
+ break;
+ case 'e':
+ if (mail_parse_human_timestamp(optarg, &ctx->sync_until_timestamp, &utc) < 0)
+ i_fatal("Invalid -e parameter: %s", optarg);
+ break;
+ case 'I':
+ if (settings_get_size(optarg, &ctx->sync_max_size, &error) < 0)
+ i_fatal("Invalid -I parameter '%s': %s", optarg, error);
+ break;
+ case 'T':
+ if (str_to_uint(optarg, &ctx->io_timeout_secs) < 0)
+ i_fatal("Invalid -T parameter: %s", optarg);
+ break;
+ case 'U':
+ ctx->replicator_notify = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_dsync_alloc(void)
+{
+ struct dsync_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct dsync_cmd_context);
+ ctx->io_timeout_secs = DSYNC_DEFAULT_IO_STREAM_TIMEOUT_SECS;
+ ctx->ctx.getopt_args = DSYNC_COMMON_GETOPT_ARGS;
+ ctx->ctx.v.parse_arg = cmd_mailbox_dsync_parse_arg;
+ ctx->ctx.v.preinit = cmd_dsync_preinit;
+ ctx->ctx.v.init = cmd_dsync_init;
+ ctx->ctx.v.prerun = cmd_dsync_prerun;
+ ctx->ctx.v.run = cmd_dsync_run;
+ ctx->sync_type = DSYNC_BRAIN_SYNC_TYPE_CHANGED;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ doveadm_print_header("state", "state",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ p_array_init(&ctx->exclude_mailboxes, ctx->ctx.pool, 4);
+ p_array_init(&ctx->namespace_prefixes, ctx->ctx.pool, 4);
+ if ((doveadm_settings->parsed_features & DSYNC_FEATURE_EMPTY_HDR_WORKAROUND) != 0)
+ ctx->empty_hdr_workaround = TRUE;
+ if ((doveadm_settings->parsed_features & DSYNC_FEATURE_NO_HEADER_HASHES) != 0)
+ ctx->no_header_hashes = TRUE;
+ ctx->import_commit_msgs_interval = doveadm_settings->dsync_commit_msgs_interval;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_dsync_backup_alloc(void)
+{
+ struct doveadm_mail_cmd_context *_ctx;
+ struct dsync_cmd_context *ctx;
+
+ _ctx = cmd_dsync_alloc();
+ ctx = (struct dsync_cmd_context *)_ctx;
+ ctx->backup = TRUE;
+ return _ctx;
+}
+
+static int
+cmd_dsync_server_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+ struct doveadm_cmd_context *cctx = _ctx->cctx;
+ bool cli = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+ struct dsync_ibc *ibc;
+ struct dsync_brain *brain;
+ string_t *temp_prefix, *state_str = NULL;
+ enum dsync_brain_sync_type sync_type;
+ const char *name, *process_title_prefix = "";
+ enum mail_error mail_error;
+
+ if (!cli) {
+ /* replicator_notify indicates here automated attempt,
+ we still want to allow manual sync/backup */
+ if (ctx->replicator_notify &&
+ mail_user_plugin_getenv_bool(_ctx->cur_mail_user, "noreplicate")) {
+ _ctx->exit_code = DOVEADM_EX_NOREPLICATE;
+ return -1;
+ }
+
+ /* doveadm-server connection. start with a success reply.
+ after that follows the regular dsync protocol. */
+ ctx->fd_in = ctx->fd_out = -1;
+ ctx->input = cctx->input;
+ ctx->output = cctx->output;
+ i_stream_ref(ctx->input);
+ o_stream_ref(ctx->output);
+ o_stream_set_finish_also_parent(ctx->output, FALSE);
+ o_stream_nsend(ctx->output, "\n+\n", 3);
+ i_set_failure_prefix("dsync-server(%s): ", user->username);
+ name = i_stream_get_name(ctx->input);
+
+ if (cctx->remote_ip.family != 0) {
+ /* include the doveadm client's IP address in the ps output */
+ process_title_prefix = t_strdup_printf(
+ "%s ", net_ip2addr(&cctx->remote_ip));
+ }
+ } else {
+ /* the log messages go via stderr to the remote dsync,
+ so the names are reversed */
+ i_set_failure_prefix("dsync-remote(%s)<%s>: ", user->username, user->session_id);
+ name = "local";
+ }
+
+ doveadm_user_init_dsync(user);
+
+ temp_prefix = t_str_new(64);
+ mail_user_set_get_temp_prefix(temp_prefix, user->set);
+
+ ibc = cmd_dsync_ibc_stream_init(ctx, name, str_c(temp_prefix));
+ brain = dsync_brain_slave_init(user, ibc, FALSE, process_title_prefix,
+ doveadm_settings->dsync_alt_char[0]);
+
+ io_loop_run(current_ioloop);
+ /* io_loop_run() deactivates the context - put it back */
+ mail_storage_service_io_activate_user(ctx->ctx.cur_service_user);
+
+ if (ctx->replicator_notify) {
+ state_str = t_str_new(128);
+ dsync_brain_get_state(brain, state_str);
+ }
+ sync_type = dsync_brain_get_sync_type(brain);
+
+ if (dsync_brain_deinit(&brain, &mail_error) < 0)
+ doveadm_mail_failed_error(_ctx, mail_error);
+ dsync_ibc_deinit(&ibc);
+
+ if (!cli) {
+ /* make sure nothing more is written by the generic doveadm
+ connection code */
+ o_stream_close(cctx->output);
+ }
+ i_stream_unref(&ctx->input);
+ o_stream_unref(&ctx->output);
+
+ if (ctx->replicator_notify && _ctx->exit_code == 0)
+ dsync_replicator_notify(ctx, sync_type, str_c(state_str));
+ return _ctx->exit_code == 0 ? 0 : -1;
+}
+
+static bool
+cmd_mailbox_dsync_server_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct dsync_cmd_context *ctx = (struct dsync_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'E':
+ /* dsync wrapper detection flag */
+ legacy_dsync = TRUE;
+ break;
+ case 'r':
+ ctx->rawlog_path = optarg;
+ break;
+ case 'T':
+ if (str_to_uint(optarg, &ctx->io_timeout_secs) < 0)
+ i_fatal("Invalid -T parameter: %s", optarg);
+ break;
+ case 'U':
+ ctx->replicator_notify = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_dsync_server_alloc(void)
+{
+ struct dsync_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct dsync_cmd_context);
+ ctx->io_timeout_secs = DSYNC_DEFAULT_IO_STREAM_TIMEOUT_SECS;
+ ctx->ctx.getopt_args = "Er:T:U";
+ ctx->ctx.v.parse_arg = cmd_mailbox_dsync_server_parse_arg;
+ ctx->ctx.v.run = cmd_dsync_server_run;
+ ctx->sync_type = DSYNC_BRAIN_SYNC_TYPE_CHANGED;
+
+ ctx->fd_in = STDIN_FILENO;
+ ctx->fd_out = STDOUT_FILENO;
+ return &ctx->ctx;
+}
+
+#define DSYNC_COMMON_PARAMS \
+DOVEADM_CMD_MAIL_COMMON \
+DOVEADM_CMD_PARAM('f', "full-sync", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('P', "purge-remote", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('R', "reverse-sync", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('U', "replicator-notify", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('l', "lock-timeout", CMD_PARAM_INT64, 0) \
+DOVEADM_CMD_PARAM('r', "rawlog", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('m', "mailbox", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('g', "mailbox-guid", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('n', "namespace", CMD_PARAM_ARRAY, 0) \
+DOVEADM_CMD_PARAM('N', "all-namespaces", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('x', "exclude-mailbox", CMD_PARAM_ARRAY, 0) \
+DOVEADM_CMD_PARAM('a', "all-mailbox", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('s', "state", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('t', "sync-since-time", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('e', "sync-until-time", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('O', "sync-flags", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('I', "sync-max-size", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('T', "timeout", CMD_PARAM_INT64, 0) \
+DOVEADM_CMD_PARAM('d', "default-destination", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('E', "legacy-dsync", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('\0', "destination", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+
+#define DSYNC_COMMON_USAGE \
+ "[-l <secs>] [-r <rawlog path>] " \
+ "[-m <mailbox>] [-g <mailbox guid>] [-n <namespace> | -N] " \
+ "[-x <exclude>] [-a <all mailbox>] [-s <state>] [-T <secs>] " \
+ "[-t <start date>] [-e <end date>] [-O <sync flag>] [-I <max size>] " \
+ "-d|<dest>"
+
+struct doveadm_cmd_ver2 doveadm_cmd_dsync_mirror = {
+ .mail_cmd = cmd_dsync_alloc,
+ .name = "sync",
+ .usage = "[-1fPRU] "DSYNC_COMMON_USAGE,
+ .flags = CMD_FLAG_NO_UNORDERED_OPTIONS,
+DOVEADM_CMD_PARAMS_START
+DSYNC_COMMON_PARAMS
+DOVEADM_CMD_PARAM('1', "oneway-sync", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAMS_END
+};
+struct doveadm_cmd_ver2 doveadm_cmd_dsync_backup = {
+ .mail_cmd = cmd_dsync_backup_alloc,
+ .name = "backup",
+ .usage = "[-fPRU] "DSYNC_COMMON_USAGE,
+ .flags = CMD_FLAG_NO_UNORDERED_OPTIONS,
+DOVEADM_CMD_PARAMS_START
+DSYNC_COMMON_PARAMS
+DOVEADM_CMD_PARAMS_END
+};
+struct doveadm_cmd_ver2 doveadm_cmd_dsync_server = {
+ .mail_cmd = cmd_dsync_server_alloc,
+ .name = "dsync-server",
+ .usage = "[-E] [-r <rawlog path>] [-T <timeout secs>] [-U]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('E', "legacy-dsync", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('r', "rawlog", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('T', "timeout", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('U', "replicator-notify", CMD_PARAM_BOOL, 0)
+/* previously dsync-server could have been added twice to the parameters */
+DOVEADM_CMD_PARAM('\0', "ignore-arg", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+void doveadm_dsync_main(int *_argc, char **_argv[])
+{
+ int argc = *_argc;
+ const char *getopt_str;
+ char **argv = *_argv;
+ char **new_argv, *mailbox = NULL, *alt_char = NULL, *username = NULL;
+ char *p, *dup, new_flags[6];
+ int max_argc, src, dest, i, j;
+ bool flag_f = FALSE, flag_R = FALSE, flag_m, flag_u, flag_C, has_arg;
+ bool dsync_server = FALSE;
+
+ p = strrchr(argv[0], '/');
+ if (p == NULL) p = argv[0];
+ if (strstr(p, "dsync") == NULL)
+ return;
+
+ /* @UNSAFE: this is called when the "doveadm" binary is called as
+ "dsync" (for backwards compatibility) */
+ max_argc = argc + 7;
+ new_argv = t_new(char *, max_argc);
+ new_argv[0] = argv[0];
+ dest = 1;
+ getopt_str = master_service_getopt_string();
+
+ /* add global doveadm flags */
+ for (src = 1; src < argc; src++) {
+ if (argv[src][0] != '-')
+ break;
+
+ flag_m = FALSE; flag_C = FALSE; has_arg = FALSE; flag_u = FALSE;
+ dup = t_strdup_noconst(argv[src]);
+ for (i = j = 1; argv[src][i] != '\0'; i++) {
+ switch (argv[src][i]) {
+ case 'C':
+ flag_C = TRUE;
+ break;
+ case 'f':
+ flag_f = TRUE;
+ break;
+ case 'R':
+ flag_R = TRUE;
+ break;
+ case 'm':
+ flag_m = TRUE;
+ break;
+ case 'u':
+ flag_u = TRUE;
+ break;
+ default:
+ p = strchr(getopt_str, argv[src][i]);
+ if (p != NULL && p[1] == ':')
+ has_arg = TRUE;
+ dup[j++] = argv[src][i];
+ break;
+ }
+ }
+ if (j > 1) {
+ dup[j++] = '\0';
+ new_argv[dest++] = dup;
+ if (has_arg && src+1 < argc)
+ new_argv[dest++] = argv[++src];
+ }
+ if (flag_m) {
+ if (src+1 == argc)
+ i_fatal("-m missing parameter");
+ mailbox = argv[++src];
+ }
+ if (flag_u) {
+ if (src+1 == argc)
+ i_fatal("-u missing parameter");
+ username = argv[++src];
+ }
+ if (flag_C) {
+ if (src+1 == argc)
+ i_fatal("-C missing parameter");
+ alt_char = argv[++src];
+ }
+ }
+ if (alt_char != NULL) {
+ new_argv[dest++] = "-o";
+ new_argv[dest++] =
+ p_strconcat(pool_datastack_create(),
+ "dsync_alt_char=", alt_char, NULL);
+ }
+
+ /* mirror|backup|server */
+ if (src == argc)
+ i_fatal("Missing mirror or backup parameter");
+ if (strcmp(argv[src], "sync") == 0 ||
+ strcmp(argv[src], "dsync-server") == 0) {
+ /* we're re-executing dsync due to doveconf.
+ "backup" re-exec detection is later. */
+ return;
+ }
+ if (strcmp(argv[src], "mirror") == 0)
+ new_argv[dest] = "sync";
+ else if (strcmp(argv[src], "backup") == 0)
+ new_argv[dest] = "backup";
+ else if (strcmp(argv[src], "server") == 0) {
+ new_argv[dest] = "dsync-server";
+ dsync_server = TRUE;
+ } else
+ i_fatal("Invalid parameter: %s", argv[src]);
+ src++; dest++;
+
+ if (src < argc && str_begins(argv[src], "-E")) {
+ /* we're re-executing dsync due to doveconf */
+ return;
+ }
+
+ /* dsync flags */
+ new_flags[0] = '-';
+ new_flags[1] = 'E'; i = 2;
+ if (!dsync_server) {
+ if (flag_f)
+ new_flags[i++] = 'f';
+ if (flag_R)
+ new_flags[i++] = 'R';
+ if (mailbox != NULL)
+ new_flags[i++] = 'm';
+ }
+ i_assert((unsigned int)i < sizeof(new_flags));
+ new_flags[i] = '\0';
+
+ if (i > 1) {
+ new_argv[dest++] = strdup(new_flags);
+ if (mailbox != NULL)
+ new_argv[dest++] = mailbox;
+ }
+ if (username != NULL) {
+ new_argv[dest++] = "-u";
+ new_argv[dest++] = username;
+ }
+
+ /* rest of the parameters */
+ for (; src < argc; src++)
+ new_argv[dest++] = argv[src];
+ i_assert(dest < max_argc);
+ new_argv[dest] = NULL;
+
+ legacy_dsync = TRUE;
+ *_argc = dest;
+ *_argv = new_argv;
+ i_getopt_reset();
+}
diff --git a/src/doveadm/doveadm-dsync.h b/src/doveadm/doveadm-dsync.h
new file mode 100644
index 0000000..c4bf08b
--- /dev/null
+++ b/src/doveadm/doveadm-dsync.h
@@ -0,0 +1,10 @@
+#ifndef DOVEADM_DSYNC_H
+#define DOVEADM_DSYNC_H
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_dsync_mirror;
+extern struct doveadm_cmd_ver2 doveadm_cmd_dsync_backup;
+extern struct doveadm_cmd_ver2 doveadm_cmd_dsync_server;
+
+void doveadm_dsync_main(int *_argc, char **_argv[]);
+
+#endif
diff --git a/src/doveadm/doveadm-dump-dbox.c b/src/doveadm/doveadm-dump-dbox.c
new file mode 100644
index 0000000..429b4a1
--- /dev/null
+++ b/src/doveadm/doveadm-dump-dbox.c
@@ -0,0 +1,231 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hex-dec.h"
+#include "istream.h"
+#include "index/dbox-common/dbox-file.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static void
+dump_timestamp(struct istream *input, const char *name, const char *value)
+{
+ time_t t;
+
+ if (strcmp(value, "0") == 0)
+ t = 0;
+ else {
+ t = hex2dec((const void *)value, strlen(value));
+ if (t == 0) {
+ i_fatal("Invalid %s at %"PRIuUOFF_T": %s",
+ name, input->v_offset, value);
+ }
+ }
+ printf("%s = %ld (%s)\n", name, (long)t, unixdate2str(t));
+}
+
+static uoff_t
+dump_size(struct istream *input, const char *name, const char *value)
+{
+ uoff_t size;
+
+ if (strcmp(value, "0") == 0)
+ size = 0;
+ else {
+ size = hex2dec((const void *)value, strlen(value));
+ if (size == 0) {
+ i_fatal("Invalid %s at %"PRIuUOFF_T": %s",
+ name, input->v_offset, value);
+ }
+ }
+ printf("%s = %"PRIuUOFF_T"\n", name, size);
+ return size;
+}
+
+static unsigned int dump_file_hdr(struct istream *input)
+{
+ const char *line, *const *arg, *version;
+ unsigned int msg_hdr_size = 0;
+
+ if ((line = i_stream_read_next_line(input)) == NULL)
+ i_fatal("Empty file");
+ arg = t_strsplit(line, " ");
+
+ /* check version */
+ version = *arg;
+ if (version == NULL || !str_is_numeric(version, ' '))
+ i_fatal("%s is not a dbox file", i_stream_get_name(input));
+ if (strcmp(version, "2") != 0)
+ i_fatal("Unsupported dbox file version %s", version);
+ arg++;
+
+ for (; *arg != NULL; arg++) {
+ switch (**arg) {
+ case DBOX_HEADER_MSG_HEADER_SIZE:
+ msg_hdr_size = hex2dec((const void *)(*arg + 1),
+ strlen(*arg + 1));
+ if (msg_hdr_size == 0) {
+ i_fatal("Invalid msg_header_size header: %s",
+ *arg + 1);
+ }
+ printf("file.msg_header_size = %u\n", msg_hdr_size);
+ break;
+ case DBOX_HEADER_CREATE_STAMP:
+ dump_timestamp(input, "file.create_stamp", *arg + 1);
+ break;
+ default:
+ printf("file.unknown-%c = %s\n", **arg, *arg + 1);
+ break;
+ }
+ }
+ if (msg_hdr_size == 0)
+ i_fatal("Missing msg_header_size in file header");
+ return msg_hdr_size;
+}
+
+static bool
+dump_msg_hdr(struct istream *input, unsigned int hdr_size, uoff_t *msg_size_r)
+{
+ struct dbox_message_header hdr;
+ const unsigned char *data;
+ size_t size;
+ uoff_t msg_size;
+
+ if (i_stream_read_bytes(input, &data, &size, hdr_size) <= 0) {
+ if (size == 0)
+ return FALSE;
+ i_fatal("Partial message header read at %"PRIuUOFF_T": %zu bytes",
+ input->v_offset, size);
+ }
+ printf("offset %"PRIuUOFF_T":\n", input->v_offset);
+
+ if (hdr_size < sizeof(hdr))
+ i_fatal("file.hdr_size too small: %u", hdr_size);
+ memcpy(&hdr, data, sizeof(hdr));
+
+ if (memcmp(hdr.magic_pre, DBOX_MAGIC_PRE, sizeof(hdr.magic_pre)) != 0)
+ i_fatal("dbox wrong pre-magic at %"PRIuUOFF_T, input->v_offset);
+
+ msg_size = dump_size(input, "msg.size",
+ t_strndup(hdr.message_size_hex, sizeof(hdr.message_size_hex)));
+
+ i_stream_skip(input, hdr_size);
+ *msg_size_r = msg_size;
+ return TRUE;
+}
+
+static void dump_msg_metadata(struct istream *input)
+{
+ struct dbox_metadata_header hdr;
+ const unsigned char *data;
+ size_t size;
+ const char *line;
+
+ /* verify magic */
+ if (i_stream_read_bytes(input, &data, &size, sizeof(hdr)) <= 0) {
+ i_fatal("dbox missing metadata at %"PRIuUOFF_T,
+ input->v_offset);
+ }
+ memcpy(&hdr, data, sizeof(hdr));
+ if (memcmp(hdr.magic_post, DBOX_MAGIC_POST, sizeof(hdr.magic_post)) != 0)
+ i_fatal("dbox wrong post-magic at %"PRIuUOFF_T, input->v_offset);
+ i_stream_skip(input, sizeof(hdr));
+
+ /* dump the metadata */
+ for (;;) {
+ if ((line = i_stream_read_next_line(input)) == NULL)
+ i_fatal("dbox metadata ended unexpectedly at EOF");
+ if (*line == '\0')
+ break;
+
+ switch (*line) {
+ case DBOX_METADATA_GUID:
+ printf("msg.guid = %s\n", line + 1);
+ break;
+ case DBOX_METADATA_POP3_UIDL:
+ printf("msg.pop3-uidl = %s\n", line + 1);
+ break;
+ case DBOX_METADATA_POP3_ORDER:
+ printf("msg.pop3-order = %s\n", line + 1);
+ break;
+ case DBOX_METADATA_RECEIVED_TIME:
+ dump_timestamp(input, "msg.received", line + 1);
+ break;
+ case DBOX_METADATA_PHYSICAL_SIZE:
+ (void)dump_size(input, "msg.physical-size", line + 1);
+ break;
+ case DBOX_METADATA_VIRTUAL_SIZE:
+ (void)dump_size(input, "msg.virtual-size", line + 1);
+ break;
+ case DBOX_METADATA_EXT_REF:
+ printf("msg.ext-ref = %s\n", line + 1);
+ break;
+ case DBOX_METADATA_ORIG_MAILBOX:
+ printf("msg.orig-mailbox = %s\n", line + 1);
+ break;
+
+ case DBOX_METADATA_OLDV1_EXPUNGED:
+ case DBOX_METADATA_OLDV1_FLAGS:
+ case DBOX_METADATA_OLDV1_KEYWORDS:
+ case DBOX_METADATA_OLDV1_SAVE_TIME:
+ case DBOX_METADATA_OLDV1_SPACE:
+ printf("msg.obsolete-%c = %s\n", *line, line + 1);
+ break;
+ }
+ }
+}
+
+static bool dump_msg(struct istream *input, unsigned int hdr_size)
+{
+ uoff_t msg_size;
+
+ if (!dump_msg_hdr(input, hdr_size, &msg_size))
+ return FALSE;
+ i_stream_skip(input, msg_size);
+ dump_msg_metadata(input);
+ return TRUE;
+}
+
+static void cmd_dump_dbox(const char *path, const char *const *args ATTR_UNUSED)
+{
+ struct istream *input;
+ int fd;
+ unsigned int hdr_size;
+ bool ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(%s) failed: %m", path);
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ i_stream_set_name(input, path);
+ hdr_size = dump_file_hdr(input);
+ do {
+ printf("\n");
+ T_BEGIN {
+ ret = dump_msg(input, hdr_size);
+ } T_END;
+ } while (ret);
+ i_stream_destroy(&input);
+}
+
+static bool test_dump_dbox(const char *path)
+{
+ const char *p;
+
+ p = strrchr(path, '/');
+ if (p == NULL)
+ p = path;
+ else
+ p++;
+ return str_begins(p, "m.") || str_begins(p, "u.");
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_dbox = {
+ "dbox",
+ test_dump_dbox,
+ cmd_dump_dbox
+};
diff --git a/src/doveadm/doveadm-dump-dcrypt-file.c b/src/doveadm/doveadm-dump-dcrypt-file.c
new file mode 100644
index 0000000..3703bf4
--- /dev/null
+++ b/src/doveadm/doveadm-dump-dcrypt-file.c
@@ -0,0 +1,93 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dcrypt.h"
+#include "istream.h"
+#include "istream-decrypt.h"
+#include "dcrypt-iostream.h"
+#include "doveadm-dump.h"
+#include <stdio.h>
+
+static int get_digest(const char *digest,
+ struct dcrypt_private_key **priv_key_r ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED,
+ void *context)
+{
+ const char **digest_r = (const char**)context;
+ *digest_r = t_strdup(digest);
+ return 0;
+}
+
+static void dcrypt_istream_dump_metadata(const struct istream *stream)
+{
+ enum io_stream_encrypt_flags flags = i_stream_encrypt_get_flags(stream);
+ if ((flags & IO_STREAM_ENC_INTEGRITY_HMAC) != 0)
+ printf("flags: IO_STREAM_ENC_INTEGRITY_HMAC\n");
+ if ((flags & IO_STREAM_ENC_INTEGRITY_AEAD) != 0)
+ printf("flags: IO_STREAM_ENC_INTEGRITY_AEAD\n");
+ if ((flags & IO_STREAM_ENC_INTEGRITY_NONE) != 0)
+ printf("flags: IO_STREAM_ENC_INTEGRITY_NONE\n");
+ if ((flags & IO_STREAM_ENC_VERSION_1) != 0)
+ printf("flags: IO_STREAM_ENC_VERSION_1\n");
+
+ enum decrypt_istream_format format = i_stream_encrypt_get_format(stream);
+ switch (format) {
+ case DECRYPT_FORMAT_V1:
+ printf("format: DECRYPT_FORMAT_V1\n");
+ break;
+ case DECRYPT_FORMAT_V2:
+ printf("format: DECRYPT_FORMAT_V2\n");
+ break;
+ }
+}
+
+static bool dcrypt_file_dump_metadata(const char *filename, bool print)
+{
+ bool ret = FALSE;
+ struct istream *is = i_stream_create_file(filename, IO_BLOCK_SIZE);
+ const char *key_digest = NULL;
+ struct istream *ds = i_stream_create_decrypt_callback(is,
+ get_digest, &key_digest);
+
+ ssize_t size = i_stream_read(ds);
+ i_assert(size < 0);
+
+ if (key_digest != NULL) {
+ ret = TRUE;
+ if (print) {
+ dcrypt_istream_dump_metadata(ds);
+ printf("decrypt key digest: %s\n", key_digest);
+ }
+ } else if (print && ds->stream_errno != 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(ds),
+ i_stream_get_error(ds));
+ }
+
+ i_stream_unref(&ds);
+ i_stream_unref(&is);
+ return ret;
+}
+
+static bool test_dump_dcrypt_file(const char *path)
+{
+ if (!dcrypt_initialize("openssl", NULL, NULL))
+ return FALSE;
+ bool ret = dcrypt_file_dump_metadata(path, FALSE);
+ return ret;
+}
+
+static void
+cmd_dump_dcrypt_file(const char *path, const char *const *args ATTR_UNUSED)
+{
+ const char *error = NULL;
+ if (!dcrypt_initialize("openssl", NULL, &error))
+ i_fatal("dcrypt_initialize failed: %s", error);
+ (void)dcrypt_file_dump_metadata(path, TRUE);
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_dcrypt_file = {
+ "dcrypt-file",
+ test_dump_dcrypt_file,
+ cmd_dump_dcrypt_file
+};
diff --git a/src/doveadm/doveadm-dump-dcrypt-key.c b/src/doveadm/doveadm-dump-dcrypt-key.c
new file mode 100644
index 0000000..cecd27f
--- /dev/null
+++ b/src/doveadm/doveadm-dump-dcrypt-key.c
@@ -0,0 +1,215 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dcrypt.h"
+#include "dcrypt-iostream.h"
+#include "ostream-encrypt.h"
+#include "istream-private.h"
+#include "istream-decrypt.h"
+#include "doveadm-dump.h"
+#include "hex-binary.h"
+#include "str.h"
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#define KEY_BUF_SIZE 4096
+
+static void dcrypt_dump_public_key_metadata(const char *buf)
+{
+ const char *error = NULL;
+ struct dcrypt_public_key *pub_key;
+
+ bool ret = dcrypt_key_load_public(&pub_key, buf, &error);
+ if (ret == FALSE) {
+ i_error("dcrypt_key_load_public failed: %s", error);
+ return;
+ }
+ enum dcrypt_key_type key_type = dcrypt_key_type_public(pub_key);
+ if (key_type == DCRYPT_KEY_RSA)
+ printf("key type: DCRYPT_KEY_RSA\n");
+ else if (key_type == DCRYPT_KEY_EC)
+ printf("key type: DCRYPT_KEY_EC\n");
+
+ string_t *hash = t_str_new(128);
+ if (!dcrypt_key_id_public(pub_key, "sha256", hash, &error)) {
+ i_error("dcrypt_key_id_public failed: %s", error);
+ } else {
+ const char *v2_hash = binary_to_hex(hash->data, hash->used);
+ printf("v2 hash: %s\n", v2_hash);
+
+ if (key_type == DCRYPT_KEY_EC) {
+ buffer_set_used_size(hash, 0);
+ if (!dcrypt_key_id_public_old(pub_key, hash, &error)) {
+ i_error("dcrypt_key_id_public_old failed: %s",
+ error);
+ } else {
+ const char *v1_hash = binary_to_hex(hash->data,
+ hash->used);
+ printf("v1 hash: %s\n", v1_hash);
+ }
+ }
+ }
+ dcrypt_key_unref_public(&pub_key);
+}
+
+static void dcrypt_dump_private_key_metadata(const char *buf)
+{
+ const char *error = NULL;
+ struct dcrypt_private_key *priv_key;
+
+ bool ret = dcrypt_key_load_private(&priv_key, buf, NULL, NULL,
+ &error);
+ if (ret == FALSE) {
+ i_error("dcrypt_key_load_private failed: %s", error);
+ return;
+ }
+ enum dcrypt_key_type key_type = dcrypt_key_type_private(priv_key);
+ if (key_type == DCRYPT_KEY_RSA)
+ printf("key type: DCRYPT_KEY_RSA\n");
+ else if (key_type == DCRYPT_KEY_EC)
+ printf("key type: DCRYPT_KEY_EC\n");
+
+ string_t *hash = t_str_new(128);
+ if (!dcrypt_key_id_private(priv_key, "sha256", hash, &error)) {
+ i_error("dcrypt_key_id_private failed: %s", error);
+ } else {
+ const char *v2_hash = binary_to_hex(hash->data, hash->used);
+ printf("v2 hash: %s\n", v2_hash);
+
+ if (key_type == DCRYPT_KEY_EC) {
+ buffer_set_used_size(hash, 0);
+ if (!dcrypt_key_id_private_old(priv_key, hash, &error)) {
+ i_error("dcrypt_key_id_private_old failed: %s", error);
+ } else {
+ const char *v1_hash = binary_to_hex(hash->data,
+ hash->used);
+ printf("v1 hash: %s\n", v1_hash);
+ }
+ }
+ }
+ dcrypt_key_unref_private(&priv_key);
+}
+
+static bool dcrypt_key_dump_metadata(const char *filename, bool print)
+{
+ bool ret = TRUE;
+ int fd = open(filename, O_RDONLY);
+ if (fd < 0) {
+ if (print) i_error("open(%s) failed: %m", filename);
+ return FALSE;
+ }
+
+ char buf[KEY_BUF_SIZE+1];
+ ssize_t res = read(fd, buf, KEY_BUF_SIZE);
+ if (res < 0) {
+ if (print) i_error("read(%s) failed: %m", filename);
+ i_close_fd(&fd);
+ return FALSE;
+ }
+ i_close_fd(&fd);
+
+ buf[res] = '\0';
+ 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;
+ const char *error;
+
+ ret = dcrypt_key_string_get_info(buf, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+ if (ret == FALSE) {
+ if (print) i_error("dcrypt_key_string_get_info failed: %s", error);
+ return FALSE;
+ }
+ if (!print) return TRUE;
+
+ switch (format) {
+ case DCRYPT_FORMAT_PEM:
+ printf("format: DCRYPT_FORMAT_PEM\n");
+ break;
+ case DCRYPT_FORMAT_DOVECOT:
+ printf("format: DCRYPT_FORMAT_DOVECOT\n");
+ break;
+ case DCRYPT_FORMAT_JWK:
+ printf("format: DCRYPT_FORMAT_JWK\n");
+ }
+
+ switch (version) {
+ case DCRYPT_KEY_VERSION_1:
+ printf("version: DCRYPT_KEY_VERSION_1\n");
+ break;
+ case DCRYPT_KEY_VERSION_2:
+ printf("version: DCRYPT_KEY_VERSION_2\n");
+ break;
+ case DCRYPT_KEY_VERSION_NA:
+ printf("version: DCRYPT_KEY_VERSION_NA\n");
+ break;
+ }
+
+ switch (kind) {
+ case DCRYPT_KEY_KIND_PUBLIC:
+ printf("kind: DCRYPT_KEY_KIND_PUBLIC\n");
+ break;
+ case DCRYPT_KEY_KIND_PRIVATE:
+ printf("kind: DCRYPT_KEY_KIND_PRIVATE\n");
+ break;
+ }
+
+ switch (encryption_type) {
+ case DCRYPT_KEY_ENCRYPTION_TYPE_NONE:
+ printf("encryption_type: DCRYPT_KEY_ENCRYPTION_TYPE_NONE\n");
+ break;
+ case DCRYPT_KEY_ENCRYPTION_TYPE_KEY:
+ printf("encryption_type: DCRYPT_KEY_ENCRYPTION_TYPE_KEY\n");
+ break;
+ case DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD:
+ printf("encryption_type: DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD\n");
+ break;
+ }
+
+ if (encryption_key_hash != NULL)
+ printf("encryption_key_hash: %s\n", encryption_key_hash);
+ if (key_hash != NULL)
+ printf("key_hash: %s\n", key_hash);
+
+ const char *data = t_str_rtrim(buf, "\r\n\t ");
+ switch (kind) {
+ case DCRYPT_KEY_KIND_PUBLIC:
+ dcrypt_dump_public_key_metadata(data);
+ break;
+ case DCRYPT_KEY_KIND_PRIVATE:
+ if (encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE)
+ dcrypt_dump_private_key_metadata(data);
+ break;
+ }
+ return TRUE;
+}
+
+static bool test_dump_dcrypt_key(const char *path)
+{
+ if (!dcrypt_initialize("openssl", NULL, NULL))
+ return FALSE;
+ bool ret = dcrypt_key_dump_metadata(path, FALSE);
+ return ret;
+}
+
+static void
+cmd_dump_dcrypt_key(const char *path, const char *const *args ATTR_UNUSED)
+{
+ const char *error = NULL;
+ if (!dcrypt_initialize("openssl", NULL, &error))
+ i_fatal("dcrypt_initialize: %s", error);
+ (void)dcrypt_key_dump_metadata(path, TRUE);
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_dcrypt_key = {
+ "dcrypt-key",
+ test_dump_dcrypt_key,
+ cmd_dump_dcrypt_key
+};
diff --git a/src/doveadm/doveadm-dump-index.c b/src/doveadm/doveadm-dump-index.c
new file mode 100644
index 0000000..ec21406
--- /dev/null
+++ b/src/doveadm/doveadm-dump-index.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "file-lock.h"
+#include "message-parser.h"
+#include "message-part-serialize.h"
+#include "mail-cache-private.h"
+#include "mail-index-modseq.h"
+#include "mail-storage-private.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <time.h>
+
+struct index_vsize_header {
+ uint64_t vsize;
+ uint32_t highest_uid;
+ uint32_t message_count;
+};
+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 mbox_index_header {
+ uint64_t sync_size;
+ uint32_t sync_mtime;
+ uint8_t dirty_flag;
+ uint8_t unused[3];
+ uint8_t mailbox_guid[16];
+};
+struct sdbox_index_header {
+ uint32_t rebuild_count;
+ guid_128_t mailbox_guid;
+ uint8_t flags;
+ uint8_t unused[3];
+};
+struct mdbox_index_header {
+ uint32_t map_uid_validity;
+ guid_128_t mailbox_guid;
+ uint8_t flags;
+ uint8_t unused[3];
+};
+struct mdbox_mail_index_record {
+ uint32_t map_uid;
+ uint32_t save_date;
+};
+struct obox_mail_index_record {
+ unsigned char guid[GUID_128_SIZE];
+ unsigned char oid[GUID_128_SIZE];
+};
+struct mobox_mail_index_header {
+ uint32_t rebuild_count;
+ uint32_t map_uid_validity;
+ uint8_t unused[4];
+ guid_128_t mailbox_guid;
+};
+struct mobox_mail_index_record {
+ uint32_t map_uid;
+ uint32_t save_date;
+};
+struct mobox_map_mail_index_header {
+ uint32_t rebuild_count;
+};
+
+struct mobox_map_mail_index_record {
+ uint32_t offset;
+ uint32_t size;
+ guid_128_t oid;
+};
+struct mailbox_list_index_header {
+ uint8_t refresh_flag;
+ /* array of { uint32_t id; char name[]; } */
+};
+struct mailbox_list_index_record {
+ uint32_t name_id;
+ uint32_t parent_uid;
+ 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 fts_index_header {
+ uint32_t last_indexed_uid;
+ uint32_t settings_checksum;
+ uint32_t unused;
+};
+struct virtual_mail_index_header {
+ uint32_t change_counter;
+ uint32_t mailbox_count;
+ uint32_t highest_mailbox_id;
+ uint32_t search_args_crc32;
+};
+struct virtual_mail_index_mailbox_record {
+ uint32_t id;
+ uint32_t name_len;
+ uint32_t uid_validity;
+ uint32_t next_uid;
+ uint64_t highest_modseq;
+};
+struct virtual_mail_index_record {
+ uint32_t mailbox_id;
+ uint32_t real_uid;
+};
+
+struct mdbox_mail_index_map_record {
+ uint32_t file_id;
+ uint32_t offset;
+ uint32_t size;
+};
+
+static void dump_hdr(struct mail_index *index)
+{
+ const struct mail_index_header *hdr = &index->map->hdr;
+ unsigned int i;
+
+ printf("version .................. = %u.%u\n", hdr->major_version, hdr->minor_version);
+ printf("base header size ......... = %u\n", hdr->base_header_size);
+ printf("header size .............. = %u\n", hdr->header_size);
+ printf("record size .............. = %u\n", hdr->record_size);
+ printf("compat flags ............. = %u\n", hdr->compat_flags);
+ printf("index id ................. = %u (%s)\n", hdr->indexid, unixdate2str(hdr->indexid));
+ printf("flags .................... = %u\n", hdr->flags);
+ printf("uid validity ............. = %u (%s)\n", hdr->uid_validity, unixdate2str(hdr->uid_validity));
+ printf("next uid ................. = %u\n", hdr->next_uid);
+ printf("messages count ........... = %u\n", hdr->messages_count);
+ printf("seen messages count ...... = %u\n", hdr->seen_messages_count);
+ printf("deleted messages count ... = %u\n", hdr->deleted_messages_count);
+ printf("first recent uid ......... = %u\n", hdr->first_recent_uid);
+ printf("first unseen uid lowwater = %u\n", hdr->first_unseen_uid_lowwater);
+ printf("first deleted uid lowwater = %u\n", hdr->first_deleted_uid_lowwater);
+ printf("log file seq ............. = %u\n", hdr->log_file_seq);
+ if (hdr->minor_version == 0) {
+ printf("log file int offset ...... = %u\n", hdr->log_file_tail_offset);
+ printf("log file ext offset ...... = %u\n", hdr->log_file_head_offset);
+ } else {
+ printf("log file tail offset ..... = %u\n", hdr->log_file_tail_offset);
+ printf("log file head offset ..... = %u\n", hdr->log_file_head_offset);
+ }
+ if (hdr->minor_version >= 3) {
+ printf("log2 rotate time ......... = %u (%s)\n", hdr->log2_rotate_time, unixdate2str(hdr->log2_rotate_time));
+ printf("last temp file scan ...... = %u (%s)\n", hdr->last_temp_file_scan, unixdate2str(hdr->last_temp_file_scan));
+ }
+ printf("day stamp ................ = %u (%s)\n", hdr->day_stamp, unixdate2str(hdr->day_stamp));
+ for (i = 0; i < N_ELEMENTS(hdr->day_first_uid); i++)
+ printf("day first uid[%u] ......... = %u\n", i, hdr->day_first_uid[i]);
+}
+
+static void dump_list_header(const void *data, size_t size)
+{
+ const struct mailbox_list_index_header *hdr = data;
+ const void *name_start, *p;
+ size_t i, len;
+ uint32_t id;
+
+ printf(" - refresh_flag = %d\n", hdr->refresh_flag);
+ for (i = sizeof(*hdr); i < size; ) {
+ /* get id */
+ if (i + sizeof(id) > size) {
+ printf(" - corrupted\n");
+ break;
+ }
+ memcpy(&id, CONST_PTR_OFFSET(data, i), sizeof(id));
+ i += sizeof(id);
+
+ if (id == 0)
+ break;
+
+ /* get name */
+ p = memchr(CONST_PTR_OFFSET(data, i), '\0', size-i);
+ if (p == NULL) {
+ printf(" - corrupted\n");
+ break;
+ }
+ name_start = CONST_PTR_OFFSET(data, i);
+ len = (const char *)p - (const char *)name_start;
+
+ printf(" - %d : %.*s\n", id, (int)len, (const char *)name_start);
+
+ i += len + 1;
+ }
+}
+
+static void dump_box_name_header(const void *data, size_t size)
+{
+ char *dest = t_malloc0(size + 1);
+ memcpy(dest, data, size);
+ for (size_t i = 0; i < size; i++) {
+ if (dest[i] == '\0')
+ dest[i] = '\n';
+ }
+ printf(" %s\n", t_strarray_join(t_strsplit(dest, "\n"), "\n "));
+}
+
+static void dump_extension_header(struct mail_index *index,
+ const struct mail_index_ext *ext)
+{
+ const void *data;
+ void *buf;
+
+ if (strcmp(ext->name, MAIL_INDEX_EXT_KEYWORDS) == 0)
+ return;
+
+ /* add some padding, since we don't bother to handle undersized
+ headers correctly */
+ buf = t_malloc0(MALLOC_ADD(ext->hdr_size, 128));
+ data = MAIL_INDEX_MAP_HDR_OFFSET(index->map, ext->hdr_offset);
+ memcpy(buf, data, ext->hdr_size);
+ data = buf;
+
+ if (strcmp(ext->name, "hdr-vsize") == 0) {
+ const struct index_vsize_header *hdr = data;
+
+ printf("header\n");
+ printf(" - highest uid . = %u\n", hdr->highest_uid);
+ printf(" - message count = %u\n", hdr->message_count);
+ printf(" - vsize ....... = %"PRIu64"\n", hdr->vsize);
+ } else if (strcmp(ext->name, "maildir") == 0) {
+ const struct maildir_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - new_check_time .... = %s\n", unixdate2str(hdr->new_check_time));
+ printf(" - new_mtime ......... = %s\n", unixdate2str(hdr->new_mtime));
+ printf(" - new_mtime_nsecs ... = %u\n", hdr->new_mtime_nsecs);
+ printf(" - cur_check_time .... = %s\n", unixdate2str(hdr->cur_check_time));
+ printf(" - cur_mtime ......... = %s\n", unixdate2str(hdr->cur_mtime));
+ printf(" - cur_mtime_nsecs.... = %u\n", hdr->cur_mtime_nsecs);
+ printf(" - uidlist_mtime ..... = %s\n", unixdate2str(hdr->uidlist_mtime));
+ printf(" - uidlist_mtime_nsecs = %u\n", hdr->uidlist_mtime_nsecs);
+ printf(" - uidlist_size ...... = %u\n", hdr->uidlist_size);
+ } else if (strcmp(ext->name, "mbox") == 0) {
+ const struct mbox_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - sync_mtime . = %s\n", unixdate2str(hdr->sync_mtime));
+ printf(" - sync_size .. = %"PRIu64"\n", hdr->sync_size);
+ printf(" - dirty_flag . = %d\n", hdr->dirty_flag);
+ printf(" - mailbox_guid = %s\n",
+ guid_128_to_string(hdr->mailbox_guid));
+ } else if (strcmp(ext->name, "mdbox-hdr") == 0) {
+ const struct mdbox_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - map_uid_validity .. = %u\n", hdr->map_uid_validity);
+ printf(" - mailbox_guid ...... = %s\n",
+ guid_128_to_string(hdr->mailbox_guid));
+ printf(" - flags ............. = 0x%x\n", hdr->flags);
+ } else if (strcmp(ext->name, "dbox-hdr") == 0) {
+ const struct sdbox_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - rebuild_count . = %u\n", hdr->rebuild_count);
+ printf(" - mailbox_guid .. = %s\n",
+ guid_128_to_string(hdr->mailbox_guid));
+ printf(" - flags ......... = 0x%x\n", hdr->flags);
+ } else if (strcmp(ext->name, "mobox-hdr") == 0) {
+ const struct mobox_mail_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - rebuild_count .. = %u\n", hdr->rebuild_count);
+ printf(" - map_uid_validity .. = %u\n", hdr->map_uid_validity);
+ printf(" - mailbox_guid ...... = %s\n",
+ guid_128_to_string(hdr->mailbox_guid));
+ } else if (strcmp(ext->name, "mobox-map") == 0) {
+ const struct mobox_map_mail_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - rebuild_count .. = %u\n", hdr->rebuild_count);
+ } else if (strcmp(ext->name, "modseq") == 0) {
+ const struct mail_index_modseq_header *hdr = data;
+
+ printf("header\n");
+ printf(" - highest_modseq = %"PRIu64"\n", hdr->highest_modseq);
+ printf(" - log_seq ...... = %u\n", hdr->log_seq);
+ printf(" - log_offset ... = %u\n", hdr->log_offset);
+ } else if (strcmp(ext->name, "fts") == 0) {
+ const struct fts_index_header *hdr = data;
+
+ printf("header\n");
+ printf(" - last_indexed_uid ..... = %u\n",
+ hdr->last_indexed_uid);
+ printf(" - settings_checksum .... = %u\n",
+ hdr->settings_checksum);
+ } else if (strcmp(ext->name, "virtual") == 0) {
+ const struct virtual_mail_index_header *hdr = data;
+ const struct virtual_mail_index_mailbox_record *rec;
+ const unsigned char *name;
+ unsigned int i;
+
+ printf("header\n");
+ printf(" - change_counter ... = %u\n", hdr->change_counter);
+ printf(" - mailbox_count .... = %u\n", hdr->mailbox_count);
+ printf(" - highest_mailbox_id = %u\n", hdr->highest_mailbox_id);
+ printf(" - search_args_crc32 = %u\n", hdr->search_args_crc32);
+
+ rec = CONST_PTR_OFFSET(hdr, sizeof(*hdr));
+ name = CONST_PTR_OFFSET(rec, sizeof(*rec) * hdr->mailbox_count);
+ for (i = 0; i < hdr->mailbox_count; i++, rec++) {
+ printf("mailbox %s:\n", t_strndup(name, rec->name_len));
+ printf(" - id ........... = %u\n", rec->id);
+ printf(" - uid_validity . = %u\n", rec->uid_validity);
+ printf(" - next_uid ..... = %u\n", rec->next_uid);
+ printf(" - highest_modseq = %"PRIu64"\n",
+ rec->highest_modseq);
+
+ name += rec->name_len;
+ }
+ } else if (strcmp(ext->name, "list") == 0) {
+ printf("header ........ = %s\n",
+ binary_to_hex(data, ext->hdr_size));
+ dump_list_header(data, ext->hdr_size);
+ } else if (strcmp(ext->name, "box-name") == 0) {
+ printf("header ........ = %s\n",
+ binary_to_hex(data, ext->hdr_size));
+ dump_box_name_header(data, ext->hdr_size);
+ } else if (strcmp(ext->name, "hdr-pop3-uidl") == 0) {
+ const struct mailbox_index_pop3_uidl *hdr = data;
+
+ printf("header\n");
+ printf(" - max_uid_with_pop3_uidl = %u\n",
+ hdr->max_uid_with_pop3_uidl);
+ } else {
+ printf("header ........ = %s\n",
+ binary_to_hex(data, ext->hdr_size));
+ }
+}
+
+static void dump_extensions(struct mail_index *index)
+{
+ const struct mail_index_ext *extensions;
+ unsigned int i, count;
+
+ if (array_is_created(&index->map->extensions))
+ extensions = array_get(&index->map->extensions, &count);
+ else
+ count = 0;
+ if (count == 0) {
+ printf("no extensions\n");
+ return;
+ }
+
+ for (i = 0; i < count; i++) {
+ const struct mail_index_ext *ext = &extensions[i];
+
+ printf("-- Extension %u --\n", i);
+ printf("name ........ = %s\n", ext->name);
+ printf("hdr_size .... = %u\n", ext->hdr_size);
+ printf("reset_id .... = %u\n", ext->reset_id);
+ printf("record_offset = %u\n", ext->record_offset);
+ printf("record_size . = %u\n", ext->record_size);
+ printf("record_align = %u\n", ext->record_align);
+ if (ext->hdr_size > 0) T_BEGIN {
+ dump_extension_header(index, ext);
+ } T_END;
+ }
+}
+
+static void dump_keywords(struct mail_index *index)
+{
+ const unsigned int *kw_indexes;
+ const char *const *keywords;
+ unsigned int i, count;
+
+ printf("-- Keywords --\n");
+ if (!array_is_created(&index->map->keyword_idx_map))
+ return;
+
+ kw_indexes = array_get(&index->map->keyword_idx_map, &count);
+ if (count == 0)
+ return;
+
+ keywords = array_front(&index->keywords);
+ for (i = 0; i < count; i++)
+ printf("%3u = %s\n", i, keywords[kw_indexes[i]]);
+}
+
+static const char *cache_decision2str(enum mail_cache_decision_type type)
+{
+ const char *str;
+
+ switch (type & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) {
+ case MAIL_CACHE_DECISION_NO:
+ str = "no";
+ break;
+ case MAIL_CACHE_DECISION_TEMP:
+ str = "tmp";
+ break;
+ case MAIL_CACHE_DECISION_YES:
+ str = "yes";
+ break;
+ default:
+ return t_strdup_printf("0x%x", type);
+ }
+
+ if ((type & MAIL_CACHE_DECISION_FORCED) != 0)
+ str = t_strconcat(str, "!", NULL);
+ return str;
+}
+
+#define CACHE_TYPE_IS_FIXED_SIZE(type) \
+ ((type) == MAIL_CACHE_FIELD_FIXED_SIZE || \
+ (type) == MAIL_CACHE_FIELD_BITMASK)
+static const char *cache_type2str(enum mail_cache_field_type type)
+{
+ switch (type) {
+ case MAIL_CACHE_FIELD_FIXED_SIZE:
+ return "fix";
+ case MAIL_CACHE_FIELD_VARIABLE_SIZE:
+ return "var";
+ case MAIL_CACHE_FIELD_STRING:
+ return "str";
+ case MAIL_CACHE_FIELD_BITMASK:
+ return "bit";
+ case MAIL_CACHE_FIELD_HEADER:
+ return "hdr";
+ default:
+ return t_strdup_printf("0x%x", type);
+ }
+}
+
+static void dump_cache_hdr(struct mail_cache *cache)
+{
+ const struct mail_cache_header *hdr;
+ const struct mail_cache_field *fields, *field;
+ unsigned int i, count, cache_idx;
+
+ (void)mail_cache_open_and_verify(cache);
+ if (MAIL_CACHE_IS_UNUSABLE(cache)) {
+ printf("cache is unusable\n");
+ return;
+ }
+
+ hdr = cache->hdr;
+ printf("major version ........ = %u\n", hdr->major_version);
+ printf("minor version ........ = %u\n", hdr->minor_version);
+ printf("indexid .............. = %u (%s)\n", hdr->indexid, unixdate2str(hdr->indexid));
+ printf("file_seq ............. = %u (%s) (%d purges)\n",
+ hdr->file_seq, unixdate2str(hdr->file_seq),
+ hdr->file_seq - hdr->indexid);
+ printf("continued_record_count = %u\n", hdr->continued_record_count);
+ printf("record_count ......... = %u\n", hdr->record_count);
+ printf("used_file_size (old) . = %u\n", hdr->backwards_compat_used_file_size);
+ printf("deleted_record_count . = %u\n", hdr->deleted_record_count);
+ printf("field_header_offset .. = %u (0x%08x nontranslated)\n",
+ mail_index_offset_to_uint32(hdr->field_header_offset),
+ hdr->field_header_offset);
+
+ printf("-- Cache fields --\n");
+ fields = mail_cache_register_get_list(cache, pool_datastack_create(),
+ &count);
+ printf(
+" # Name Type Size Dec Last used\n");
+ for (i = 0; i < cache->file_fields_count; i++) {
+ cache_idx = cache->file_field_map[i];
+ field = &fields[cache_idx];
+
+ printf("%2u: %-44s %-4s ", i, field->name,
+ cache_type2str(field->type));
+ if (field->field_size != (uint32_t)-1 ||
+ CACHE_TYPE_IS_FIXED_SIZE(field->type))
+ printf("%4u ", field->field_size);
+ else
+ printf(" - ");
+ printf("%-4s %.16s\n",
+ cache_decision2str(field->decision),
+ unixdate2str(field->last_used));
+ }
+}
+
+static void dump_message_part(string_t *str, const struct message_part *part)
+{
+ for (; part != NULL; part = part->next) {
+ str_append_c(str, '(');
+ str_printfa(str, "pos=%"PRIuUOFF_T" ", part->physical_pos);
+ str_printfa(str, "hdr.p=%"PRIuUOFF_T" ", part->header_size.physical_size);
+ str_printfa(str, "hdr.v=%"PRIuUOFF_T" ", part->header_size.virtual_size);
+ str_printfa(str, "body.p=%"PRIuUOFF_T" ", part->body_size.physical_size);
+ str_printfa(str, "body.v=%"PRIuUOFF_T" ", part->body_size.virtual_size);
+ str_printfa(str, "flags=%x", part->flags);
+ if (part->children != NULL) {
+ str_append_c(str, ' ');
+ dump_message_part(str, part->children);
+ }
+ str_append_c(str, ')');
+ }
+}
+
+static void
+dump_cache_mime_parts(string_t *str, const void *data, unsigned int size)
+{
+ const struct message_part *part;
+ const char *error;
+
+ str_append_c(str, ' ');
+
+ part = message_part_deserialize(pool_datastack_create(), data, size, &error);
+ if (part == NULL) {
+ str_printfa(str, "error: %s", error);
+ return;
+ }
+
+ dump_message_part(str, part);
+}
+
+static void
+dump_cache_append_string(string_t *str, const unsigned char *data,
+ unsigned int size)
+{
+ /* cached strings end with NUL */
+ if (size > 0 && data[size-1] == '\0')
+ size--;
+ str_append_data(str, data, size);
+}
+
+static void
+dump_cache_snippet(string_t *str, const unsigned char *data, unsigned int size)
+{
+ if (size == 0)
+ return;
+ str_printfa(str, " (version=%u: ", data[0]);
+ dump_cache_append_string(str, data+1, size-1);
+ str_append_c(str, ')');
+}
+
+static void dump_cache(struct mail_cache_view *cache_view, unsigned int seq)
+{
+ struct mail_cache_lookup_iterate_ctx iter;
+ const struct mail_cache_record *prev_rec = NULL;
+ const struct mail_cache_field *field;
+ struct mail_cache_iterate_field iter_field;
+ const void *data;
+ unsigned int size;
+ string_t *str;
+ int ret;
+
+ str = t_str_new(512);
+ mail_cache_lookup_iter_init(cache_view, seq, &iter);
+ while ((ret = mail_cache_lookup_iter_next(&iter, &iter_field)) > 0) {
+ if (iter.rec != prev_rec) {
+ printf(" - cache offset=%u size=%u, prev_offset = %u\n",
+ iter.offset, iter.rec->size,
+ iter.rec->prev_offset);
+ prev_rec = iter.rec;
+ }
+
+ field = &cache_view->cache->fields[iter_field.field_idx].field;
+ data = iter_field.data;
+ size = iter_field.size;
+
+ str_truncate(str, 0);
+ str_printfa(str, " - %s: ", field->name);
+ switch (field->type) {
+ case MAIL_CACHE_FIELD_FIXED_SIZE:
+ if (size == sizeof(uint32_t)) {
+ uint32_t value;
+ memcpy(&value, data, sizeof(value));
+ str_printfa(str, "%u ", value);
+ } else if (size == sizeof(uint64_t)) {
+ uint64_t value;
+ memcpy(&value, data, sizeof(value));
+ str_printfa(str, "%"PRIu64, value);
+ }
+ /* fall through */
+ case MAIL_CACHE_FIELD_VARIABLE_SIZE:
+ case MAIL_CACHE_FIELD_BITMASK:
+ str_printfa(str, "(%s)", binary_to_hex(data, size));
+ if (strcmp(field->name, "mime.parts") == 0)
+ dump_cache_mime_parts(str, data, size);
+ else if (strcmp(field->name, "body.snippet") == 0)
+ dump_cache_snippet(str, data, size);
+ break;
+ case MAIL_CACHE_FIELD_STRING:
+ dump_cache_append_string(str, data, size);
+ break;
+ case MAIL_CACHE_FIELD_HEADER: {
+ const uint32_t *lines = data;
+ int i;
+
+ for (i = 0;; i++) {
+ if (size < sizeof(uint32_t)) {
+ if (i == 0 && size == 0) {
+ /* header doesn't exist */
+ break;
+ }
+
+ str_append(str, "\n - BROKEN: header field doesn't end with 0 line");
+ size = 0;
+ break;
+ }
+
+ size -= sizeof(uint32_t);
+ data = CONST_PTR_OFFSET(data, sizeof(uint32_t));
+ if (lines[i] == 0)
+ break;
+
+ if (i > 0)
+ str_append(str, ", ");
+ str_printfa(str, "%u", lines[i]);
+ }
+
+ if (i == 1 && size > 0 &&
+ ((const char *)data)[size-1] == '\n')
+ size--;
+ if (size > 0)
+ str_printfa(str, ": %.*s", (int)size, (const char *)data);
+ break;
+ }
+ case MAIL_CACHE_FIELD_COUNT:
+ i_unreached();
+ break;
+ }
+
+ fwrite(str_data(str), 1, str_len(str), stdout);
+ putchar('\n');
+ }
+ if (ret < 0)
+ printf(" - broken cache\n");
+}
+
+static const char *flags2str(enum mail_flags flags)
+{
+ string_t *str;
+
+ str = t_str_new(64);
+ str_append_c(str, '(');
+ if ((flags & MAIL_SEEN) != 0)
+ str_append(str, "Seen ");
+ if ((flags & MAIL_ANSWERED) != 0)
+ str_append(str, "Answered ");
+ if ((flags & MAIL_FLAGGED) != 0)
+ str_append(str, "Flagged ");
+ if ((flags & MAIL_DELETED) != 0)
+ str_append(str, "Deleted ");
+ if ((flags & MAIL_DRAFT) != 0)
+ str_append(str, "Draft ");
+ if (str_len(str) == 1)
+ return "";
+
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void dump_record(struct mail_index_view *view, unsigned int seq)
+{
+ struct mail_index *index = mail_index_view_get_index(view);
+ const struct mail_index_record *rec;
+ const struct mail_index_registered_ext *ext;
+ const void *data;
+ unsigned int i, ext_count;
+ string_t *str;
+ bool expunged;
+
+ rec = mail_index_lookup(view, seq);
+ printf("RECORD: seq=%u, uid=%u, flags=0x%02x %s\n",
+ seq, rec->uid, rec->flags, flags2str(rec->flags));
+
+ str = t_str_new(256);
+ ext = array_get(&index->extensions, &ext_count);
+ for (i = 0; i < ext_count; i++) {
+ mail_index_lookup_ext(view, seq, i, &data, &expunged);
+ if (data == NULL || ext[i].record_size == 0)
+ continue;
+
+ str_truncate(str, 0);
+ str_printfa(str, " - ext %d %-10s: ", i, ext[i].name);
+ if (ext[i].record_size == sizeof(uint16_t) &&
+ ext[i].record_align == sizeof(uint16_t))
+ str_printfa(str, "%10u", *((const uint16_t *)data));
+ else if (ext[i].record_size == sizeof(uint32_t) &&
+ ext[i].record_align == sizeof(uint32_t))
+ str_printfa(str, "%10u", *((const uint32_t *)data));
+ else if (ext[i].record_size == sizeof(uint64_t) &&
+ ext[i].record_align == sizeof(uint64_t)) {
+ uint64_t value = *((const uint64_t *)data);
+ str_printfa(str, "%10"PRIu64, value);
+ } else {
+ str_append(str, " ");
+ }
+ str_printfa(str, " (%s)",
+ binary_to_hex(data, ext[i].record_size));
+ printf("%s\n", str_c(str));
+ if (strcmp(ext[i].name, "virtual") == 0) {
+ const struct virtual_mail_index_record *vrec = data;
+ printf(" : mailbox_id = %u\n", vrec->mailbox_id);
+ printf(" : real_uid = %u\n", vrec->real_uid);
+ } else if (strcmp(ext[i].name, "map") == 0) {
+ const struct mdbox_mail_index_map_record *mrec = data;
+ printf(" : file_id = %u\n", mrec->file_id);
+ printf(" : offset = %u\n", mrec->offset);
+ printf(" : size = %u\n", mrec->size);
+ } else if (strcmp(ext[i].name, "mdbox") == 0) {
+ const struct mdbox_mail_index_record *drec = data;
+ printf(" : map_uid = %u\n", drec->map_uid);
+ printf(" : save_date = %u (%s)\n", drec->save_date, unixdate2str(drec->save_date));
+ } else if (strcmp(ext[i].name, "obox") == 0) {
+ const struct obox_mail_index_record *orec = data;
+ printf(" : guid = %s\n", guid_128_to_string(orec->guid));
+ printf(" : oid = %s\n", binary_to_hex(orec->oid, ext[i].record_size - sizeof(orec->guid)));
+ } else if (strcmp(ext[i].name, "mobox") == 0) {
+ const struct mobox_mail_index_record *orec = data;
+ printf(" : map_uid = %u\n", orec->map_uid);
+ printf(" : save_date = %u (%s)\n", orec->save_date, unixdate2str(orec->save_date));
+ } else if (strcmp(ext[i].name, "mobox-map") == 0) {
+ const struct mobox_map_mail_index_record *orec = data;
+ printf(" : offset = %u\n", orec->offset);
+ printf(" : size = %u\n", orec->size);
+ printf(" : oid = %s\n", guid_128_to_string(orec->oid));
+ } else if (strcmp(ext[i].name, "list") == 0) {
+ const struct mailbox_list_index_record *lrec = data;
+ printf(" : name_id = %u\n", lrec->name_id);
+ printf(" : parent_uid = %u\n", lrec->parent_uid);
+ printf(" : guid = %s\n", guid_128_to_string(lrec->guid));
+ printf(" : uid_validity = %u\n", lrec->uid_validity);
+ } else if (strcmp(ext[i].name, "msgs") == 0) {
+ const struct mailbox_list_index_msgs_record *lrec = data;
+ printf(" : messages = %u\n", lrec->messages);
+ printf(" : unseen = %u\n", lrec->unseen);
+ printf(" : recent = %u\n", lrec->recent);
+ printf(" : uidnext = %u\n", lrec->uidnext);
+ } else if (strcmp(ext[i].name, "vsize") == 0 &&
+ ext[i].record_size >= sizeof(struct mailbox_index_vsize)) {
+ /* this is "vsize" in dovecot.list.index, not the
+ 32bit "vsize" in dovecot.index */
+ const struct mailbox_index_vsize *vrec = data;
+ printf(" : vsize = %"PRIu64"\n", vrec->vsize);
+ printf(" : highest_uid = %u\n", vrec->highest_uid);
+ printf(" : message_count = %u\n", vrec->message_count);
+ }
+ }
+}
+
+static bool dir_has_index(const char *dir, const char *name)
+{
+ struct stat st;
+
+ return stat(t_strconcat(dir, "/", name, NULL), &st) == 0 ||
+ stat(t_strconcat(dir, "/", name, ".log", NULL), &st) == 0;
+}
+
+static struct mail_index *path_open_index(const char *path)
+{
+ struct stat st;
+ const char *p;
+
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ if (dir_has_index(path, "dovecot.index"))
+ return mail_index_alloc(NULL, path, "dovecot.index");
+ else if (dir_has_index(path, "dovecot.map.index"))
+ return mail_index_alloc(NULL, path, "dovecot.map.index");
+ else
+ return NULL;
+ } else if ((p = strrchr(path, '/')) != NULL)
+ return mail_index_alloc(NULL, t_strdup_until(path, p), p + 1);
+ else
+ return mail_index_alloc(NULL, ".", path);
+}
+
+static void
+cmd_dump_index(const char *path, const char *const *args)
+{
+ struct mail_index *index;
+ struct mail_index_view *view;
+ struct mail_cache_view *cache_view;
+ unsigned int seq, uid = 0;
+
+ index = path_open_index(path);
+ if (index == NULL ||
+ mail_index_open(index, MAIL_INDEX_OPEN_FLAG_READONLY) <= 0)
+ i_fatal("Couldn't open index %s", path);
+ if (args[0] != NULL) {
+ if (str_to_uint(args[0], &uid) < 0)
+ i_fatal("Invalid uid number %s", args[0]);
+ }
+
+ view = mail_index_view_open(index);
+ cache_view = mail_cache_view_open(index->cache, view);
+
+ if (uid == 0) {
+ printf("-- INDEX: %s\n", index->filepath);
+ dump_hdr(index);
+ dump_extensions(index);
+ dump_keywords(index);
+
+ printf("\n-- CACHE: %s\n", index->cache->filepath);
+ dump_cache_hdr(index->cache);
+
+ printf("\n-- RECORDS: %u\n", index->map->hdr.messages_count);
+ }
+ for (seq = 1; seq <= index->map->hdr.messages_count; seq++) {
+ if (uid == 0 || mail_index_lookup(view, seq)->uid == uid) {
+ T_BEGIN {
+ dump_record(view, seq);
+ dump_cache(cache_view, seq);
+ printf("\n");
+ } T_END;
+ }
+ }
+ mail_cache_view_close(&cache_view);
+ mail_index_view_close(&view);
+ mail_index_close(index);
+ mail_index_free(&index);
+}
+
+static bool test_dump_index(const char *path)
+{
+ struct mail_index *index;
+ bool ret;
+
+ index = path_open_index(path);
+ if (index == NULL)
+ return FALSE;
+
+ ret = mail_index_open(index, MAIL_INDEX_OPEN_FLAG_READONLY) > 0;
+ if (ret)
+ mail_index_close(index);
+ mail_index_free(&index);
+ return ret;
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_index = {
+ "index",
+ test_dump_index,
+ cmd_dump_index
+};
diff --git a/src/doveadm/doveadm-dump-log.c b/src/doveadm/doveadm-dump-log.c
new file mode 100644
index 0000000..0725b28
--- /dev/null
+++ b/src/doveadm/doveadm-dump-log.c
@@ -0,0 +1,568 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "hex-binary.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+
+static struct mail_transaction_ext_intro prev_intro;
+
+static void dump_hdr(struct istream *input, uint64_t *modseq_r,
+ unsigned int *version_r)
+{
+ struct mail_transaction_log_header hdr;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(input, &data, &size, sizeof(hdr));
+ if (ret < 0 && input->stream_errno != 0)
+ i_fatal("read() failed: %s", i_stream_get_error(input));
+ if (ret <= 0) {
+ i_fatal("file hdr read() %zu != %zu",
+ size, sizeof(hdr));
+ }
+ memcpy(&hdr, data, sizeof(hdr));
+ if (hdr.hdr_size < sizeof(hdr)) {
+ memset(PTR_OFFSET(&hdr, hdr.hdr_size), 0,
+ sizeof(hdr) - hdr.hdr_size);
+ }
+ i_stream_skip(input, hdr.hdr_size);
+
+ printf("version = %u.%u\n", hdr.major_version, hdr.minor_version);
+ printf("hdr size = %u\n", hdr.hdr_size);
+ printf("index id = %u\n", hdr.indexid);
+ printf("file seq = %u\n", hdr.file_seq);
+ printf("prev file = %u/%u\n", hdr.prev_file_seq, hdr.prev_file_offset);
+ printf("create stamp = %u\n", hdr.create_stamp);
+ printf("initial modseq = %"PRIu64"\n", hdr.initial_modseq);
+ printf("compat flags = %x\n", hdr.compat_flags);
+ *modseq_r = hdr.initial_modseq;
+ *version_r = MAIL_TRANSACTION_LOG_HDR_VERSION(&hdr);
+}
+
+static const char *log_record_type(unsigned int type)
+{
+ const char *name;
+
+ switch (type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE|MAIL_TRANSACTION_EXPUNGE_PROT:
+ name = "expunge";
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID|MAIL_TRANSACTION_EXPUNGE_PROT:
+ name = "expunge-guid";
+ break;
+ case MAIL_TRANSACTION_APPEND:
+ name = "append";
+ break;
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ name = "flag-update";
+ break;
+ case MAIL_TRANSACTION_HEADER_UPDATE:
+ name = "header-update";
+ break;
+ case MAIL_TRANSACTION_EXT_INTRO:
+ name = "ext-intro";
+ break;
+ case MAIL_TRANSACTION_EXT_RESET:
+ name = "ext-reset";
+ break;
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE:
+ name = "ext-hdr";
+ break;
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE32:
+ name = "ext-hdr32";
+ break;
+ case MAIL_TRANSACTION_EXT_REC_UPDATE:
+ name = "ext-rec";
+ break;
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ name = "keyword-update";
+ break;
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ name = "keyword-reset";
+ break;
+ case MAIL_TRANSACTION_EXT_ATOMIC_INC:
+ name = "ext-atomic-inc";
+ break;
+ case MAIL_TRANSACTION_MODSEQ_UPDATE:
+ name = "modseq-update";
+ break;
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ name = "index-deleted";
+ break;
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ name = "index-undeleted";
+ break;
+ case MAIL_TRANSACTION_BOUNDARY:
+ name = "boundary";
+ break;
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ name = "attribute-update";
+ break;
+ default:
+ name = t_strdup_printf("unknown: %x", type);
+ break;
+ }
+
+ if ((type & MAIL_TRANSACTION_EXTERNAL) != 0)
+ name = t_strconcat(name, " (ext)", NULL);
+ if ((type & MAIL_TRANSACTION_SYNC) != 0)
+ name = t_strconcat(name, " (sync)", NULL);
+ return name;
+}
+
+static void print_data(const void *data, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++)
+ printf("%02x", ((const unsigned char *)data)[i]);
+ if (size == 4) {
+ const uint32_t *n = (const uint32_t *)data;
+
+ printf(" (dec=%u)", *n);
+ }
+}
+
+static void print_try_uint(const void *data, size_t size)
+{
+ size_t i;
+
+ switch (size) {
+ case 1: {
+ const uint8_t *n = data;
+ printf("%u", *n);
+ break;
+ }
+ case 2: {
+ const uint16_t *n = data;
+ uint32_t n16;
+
+ memcpy(&n16, n, sizeof(n16));
+ printf("%u", n16);
+ break;
+ }
+ case 4: {
+ const uint32_t *n = data;
+ uint32_t n32;
+
+ memcpy(&n32, n, sizeof(n32));
+ printf("%u", n32);
+ break;
+ }
+ case 8: {
+ const uint64_t *n = data;
+ uint64_t n64;
+
+ memcpy(&n64, n, sizeof(n64));
+ printf("%"PRIu64, n64);
+ break;
+ }
+ default:
+ for (i = 0; i < size; i++)
+ printf("%02x", ((const unsigned char *)data)[i]);
+ }
+}
+
+#define HDRF(field) { \
+ #field, offsetof(struct mail_index_header, field), \
+ sizeof(((struct mail_index_header *)0)->field) }
+
+static struct {
+ const char *name;
+ unsigned int offset, size;
+} header_fields[] = {
+ HDRF(minor_version),
+ HDRF(base_header_size),
+ HDRF(header_size),
+ HDRF(record_size),
+ HDRF(compat_flags),
+ HDRF(indexid),
+ HDRF(flags),
+ HDRF(uid_validity),
+ HDRF(next_uid),
+ HDRF(messages_count),
+ HDRF(unused_old_recent_messages_count),
+ HDRF(seen_messages_count),
+ HDRF(deleted_messages_count),
+ HDRF(first_recent_uid),
+ HDRF(first_unseen_uid_lowwater),
+ HDRF(first_deleted_uid_lowwater),
+ HDRF(log_file_seq),
+ HDRF(log_file_tail_offset),
+ HDRF(log_file_head_offset),
+ HDRF(day_stamp)
+};
+
+static void log_header_update(const struct mail_transaction_header_update *u,
+ size_t data_size)
+{
+ const void *data = u + 1;
+ unsigned int offset = u->offset, size = u->size;
+ unsigned int i;
+
+ if (sizeof(*u) + size > data_size) {
+ printf(" - offset = %u, size = %u (too large)\n", offset, size);
+ return;
+ }
+
+ while (size > 0) {
+ /* don't bother trying to handle header updates that include
+ unknown/unexpected fields offsets/sizes */
+ for (i = 0; i < N_ELEMENTS(header_fields); i++) {
+ if (header_fields[i].offset == offset &&
+ header_fields[i].size <= size)
+ break;
+ }
+
+ if (i == N_ELEMENTS(header_fields)) {
+ printf(" - offset = %u, size = %u: ", offset, size);
+ print_data(data, size);
+ printf("\n");
+ break;
+ }
+
+ printf(" - %s = ", header_fields[i].name);
+ print_try_uint(data, header_fields[i].size);
+ printf("\n");
+
+ data = CONST_PTR_OFFSET(data, header_fields[i].size);
+ offset += header_fields[i].size;
+ size -= header_fields[i].size;
+ }
+}
+
+static void log_record_print(const struct mail_transaction_header *hdr,
+ const void *data, size_t data_size,
+ uint64_t *modseq)
+{
+ unsigned int size = mail_index_offset_to_uint32(hdr->size) - sizeof(*hdr);
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE|MAIL_TRANSACTION_EXPUNGE_PROT: {
+ const struct mail_transaction_expunge *exp = data;
+
+ printf(" - uids=");
+ for (; size > 0; size -= sizeof(*exp), exp++) {
+ printf("%u-%u,", exp->uid1, exp->uid2);
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_EXPUNGE_GUID|MAIL_TRANSACTION_EXPUNGE_PROT: {
+ const struct mail_transaction_expunge_guid *exp = data;
+
+ for (; size > 0; size -= sizeof(*exp), exp++) {
+ printf(" - uid=%u (guid ", exp->uid);
+ print_data(exp->guid_128, sizeof(exp->guid_128));
+ printf(")\n");
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_APPEND: {
+ const struct mail_index_record *rec = data;
+
+ printf(" - uids=");
+ for (; size > 0; size -= sizeof(*rec), rec++) {
+ printf("%u", rec->uid);
+ if (rec->flags != 0)
+ printf(" (flags=%x)", rec->flags);
+ printf(",");
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_FLAG_UPDATE: {
+ const struct mail_transaction_flag_update *u = data;
+
+ for (; size > 0; size -= sizeof(*u), u++) {
+ printf(" - uids=%u-%u (flags +%x-%x, modseq_inc_flag=%d)\n",
+ u->uid1, u->uid2, u->add_flags, u->remove_flags, u->modseq_inc_flag);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_HEADER_UPDATE: {
+ const struct mail_transaction_header_update *u = data;
+
+ log_header_update(u, data_size);
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_INTRO: {
+ const struct mail_transaction_ext_intro *intro = data;
+
+ prev_intro = *intro;
+ printf(" - ext_id = %u\n", intro->ext_id);
+ printf(" - reset_id = %u\n", intro->reset_id);
+ printf(" - hdr_size = %u\n", intro->hdr_size);
+ printf(" - record_size = %u\n", intro->record_size);
+ printf(" - record_align = %u\n", intro->record_align);
+ printf(" - flags = %u\n", intro->flags);
+ printf(" - name_size = %u\n", intro->name_size);
+ if (intro->name_size > 0) {
+ const char *name = (const char *)(intro+1);
+
+ printf(" - name = '%.*s'\n", intro->name_size, name);
+ if (*modseq == 0 && intro->name_size == 6 &&
+ memcmp(name, "modseq", 6) == 0)
+ *modseq = 1;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_RESET: {
+ const struct mail_transaction_ext_reset *reset = data;
+
+ printf(" - new_reset_id = %u\n", reset->new_reset_id);
+ printf(" - preserve_data = %u\n", reset->preserve_data);
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE: {
+ const struct mail_transaction_ext_hdr_update *u = data;
+
+ printf(" - offset = %u, size = %u", u->offset, u->size);
+ if (sizeof(*u) + u->size <= data_size) {
+ printf(": ");
+ print_data(u + 1, u->size);
+ } else {
+ printf(" (too large)");
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE32: {
+ const struct mail_transaction_ext_hdr_update32 *u = data;
+
+ printf(" - offset = %u, size = %u", u->offset, u->size);
+ if (sizeof(*u) + u->size <= data_size) {
+ printf(": ");
+ print_data(u + 1, u->size);
+ } else {
+ printf(" (too large)");
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_REC_UPDATE: {
+ const struct mail_transaction_ext_rec_update *rec = data, *end;
+ size_t record_size;
+
+ end = CONST_PTR_OFFSET(data, size);
+ record_size = (sizeof(*rec) + prev_intro.record_size + 3) & ~3U;
+ while (rec < end) {
+ printf(" - uid=%u: ", rec->uid);
+ size_t bytes_left = (const char *)end - (const char *)(rec + 1);
+ if (prev_intro.record_size <= bytes_left)
+ print_data(rec + 1, prev_intro.record_size);
+ else
+ printf("(record_size too large)");
+ printf("\n");
+ rec = CONST_PTR_OFFSET(rec, record_size);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_ATOMIC_INC: {
+ const struct mail_transaction_ext_atomic_inc *rec = data, *end;
+
+ end = CONST_PTR_OFFSET(data, size);
+ for (; rec < end; rec++) {
+ printf(" - uid=%u: ", rec->uid);
+ if (rec->diff > 0)
+ printf("+%d\n", rec->diff);
+ else
+ printf("%d\n", rec->diff);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_UPDATE: {
+ const struct mail_transaction_keyword_update *u = data;
+ const uint32_t *uid;
+ unsigned int uid_offset;
+
+ printf(" - modify=%d, name=%.*s, uids=",
+ u->modify_type, u->name_size, (const char *)(u+1));
+
+ uid_offset = sizeof(*u) + u->name_size +
+ ((u->name_size % 4) == 0 ? 0 : 4 - (u->name_size%4));
+ uid = (const uint32_t *)((const char *)u + uid_offset);
+ size -= uid_offset;
+
+ for (; size > 0; size -= sizeof(*uid)*2, uid += 2) {
+ printf("%u-%u,", uid[0], uid[1]);
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_RESET: {
+ const struct mail_transaction_keyword_reset *u = data;
+
+ printf(" - uids=");
+ for (; size > 0; size -= sizeof(*u), u++) {
+ printf("%u-%u, ", u->uid1, u->uid2);
+ }
+ printf("\n");
+ break;
+ }
+ case MAIL_TRANSACTION_MODSEQ_UPDATE: {
+ const struct mail_transaction_modseq_update *rec, *end;
+
+ end = CONST_PTR_OFFSET(data, size);
+ for (rec = data; rec < end; rec++) {
+ printf(" - uid=%u modseq=%"PRIu64"\n", rec->uid,
+ ((uint64_t)rec->modseq_high32 << 32) |
+ rec->modseq_low32);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ break;
+ case MAIL_TRANSACTION_BOUNDARY: {
+ const struct mail_transaction_boundary *rec = data;
+
+ printf(" - size=%u\n", rec->size);
+ break;
+ }
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE: {
+ const char *keys = data;
+ const uint32_t *extra;
+ unsigned int i, extra_pos, extra_count = 0;
+
+ for (i = 0; i < size && keys[i] != '\0'; ) {
+ if (keys[i] == '+')
+ extra_count++;
+ extra_count++;
+ i += strlen(keys+i) + 1;
+ }
+ if (i % sizeof(uint32_t) != 0)
+ i += sizeof(uint32_t) - i%sizeof(uint32_t);
+ extra = (const void *)(keys+i);
+
+ if ((size-i) != extra_count*sizeof(uint32_t)) {
+ printf(" - broken entry\n");
+ break;
+ }
+
+ extra_pos = 0;
+ for (i = 0; i < size && keys[i] != '\0'; ) {
+ printf(" - %s: %s/%s : timestamp=%s",
+ keys[i] == '+' ? "add" : keys[i] == '-' ? "remove" : "?",
+ keys[i+1] == 'p' ? "private" :
+ keys[i+1] == 's' ? "shared" : "?error?",
+ keys+i+2, unixdate2str(extra[extra_pos++]));
+ if (keys[i] == '+')
+ printf(" value_len=%u", extra[extra_pos++]);
+ printf("\n");
+ i += strlen(keys+i) + 1;
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static int dump_record(struct istream *input, uint64_t *modseq,
+ unsigned int version)
+{
+ struct mail_transaction_header hdr;
+ unsigned int hdr_size;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(input, &data, &size, sizeof(hdr));
+ if (ret < 0 && input->stream_errno != 0)
+ i_fatal("read() failed: %s", i_stream_get_error(input));
+ if (ret <= 0) {
+ if (size == 0)
+ return 0;
+ i_fatal("rec hdr read() %zu != %zu",
+ size, sizeof(hdr));
+ }
+ memcpy(&hdr, data, sizeof(hdr));
+
+ hdr_size = mail_index_offset_to_uint32(hdr.size);
+ if (hdr_size < sizeof(hdr)) {
+ printf("record: offset=%"PRIuUOFF_T", "
+ "type=%s, size=broken (%x)\n",
+ input->v_offset, log_record_type(hdr.type), hdr.size);
+ return 0;
+ }
+
+ printf("record: offset=%"PRIuUOFF_T", type=%s, size=%u",
+ input->v_offset, log_record_type(hdr.type), hdr_size);
+
+ i_stream_skip(input, sizeof(hdr));
+ size_t data_size = hdr_size - sizeof(hdr);
+ ret = i_stream_read_bytes(input, &data, &size, data_size);
+ if (ret < 0 && input->stream_errno != 0)
+ i_fatal("read() failed: %s", i_stream_get_error(input));
+ if (ret <= 0) {
+ i_fatal("rec data read() %zu != %zu",
+ size, data_size);
+ }
+
+ uint64_t prev_modseq = *modseq;
+ mail_transaction_update_modseq(&hdr, data, modseq, version);
+ if (*modseq > prev_modseq)
+ printf(", modseq=%"PRIu64, *modseq);
+ printf("\n");
+
+ log_record_print(&hdr, data, data_size, modseq);
+ i_stream_skip(input, data_size);
+ return 1;
+}
+
+static void cmd_dump_log(const char *path, const char *const *args ATTR_UNUSED)
+{
+ struct istream *input;
+ uint64_t modseq;
+ unsigned int version;
+ int ret;
+
+ input = i_stream_create_file(path, SIZE_MAX);
+ dump_hdr(input, &modseq, &version);
+ do {
+ T_BEGIN {
+ ret = dump_record(input, &modseq, version);
+ } T_END;
+ } while (ret > 0);
+ i_stream_unref(&input);
+}
+
+static bool test_dump_log(const char *path)
+{
+ struct mail_transaction_log_header hdr;
+ const char *p;
+ bool ret = FALSE;
+ int fd;
+
+ p = strrchr(path, '/');
+ if (p == NULL)
+ p = path;
+ p = strstr(p, ".log");
+ if (p == NULL || !(p[4] == '\0' || p[4] == '.'))
+ return FALSE;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return FALSE;
+
+ if (read(fd, &hdr, sizeof(hdr)) >= MAIL_TRANSACTION_LOG_HEADER_MIN_SIZE &&
+ hdr.major_version == MAIL_TRANSACTION_LOG_MAJOR_VERSION &&
+ hdr.hdr_size >= MAIL_TRANSACTION_LOG_HEADER_MIN_SIZE)
+ ret = TRUE;
+ i_close_fd(&fd);
+ return ret;
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_log = {
+ "log",
+ test_dump_log,
+ cmd_dump_log
+};
diff --git a/src/doveadm/doveadm-dump-mailboxlog.c b/src/doveadm/doveadm-dump-mailboxlog.c
new file mode 100644
index 0000000..8ff5a54
--- /dev/null
+++ b/src/doveadm/doveadm-dump-mailboxlog.c
@@ -0,0 +1,114 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hex-binary.h"
+#include "mailbox-log.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+
+static int dump_record(int fd)
+{
+ off_t offset;
+ ssize_t ret;
+ struct mailbox_log_record rec;
+ time_t timestamp;
+
+ offset = lseek(fd, 0, SEEK_CUR);
+
+ ret = read(fd, &rec, sizeof(rec));
+ if (ret == 0)
+ return 0;
+
+ if (ret != sizeof(rec)) {
+ i_fatal("rec read() %zu != %zu",
+ ret, sizeof(rec));
+ }
+
+ printf("#%"PRIuUOFF_T": ", offset);
+ switch (rec.type) {
+ case MAILBOX_LOG_RECORD_DELETE_MAILBOX:
+ printf("delete-mailbox");
+ break;
+ case MAILBOX_LOG_RECORD_DELETE_DIR:
+ printf("delete-dir");
+ break;
+ case MAILBOX_LOG_RECORD_RENAME:
+ printf("rename");
+ break;
+ case MAILBOX_LOG_RECORD_SUBSCRIBE:
+ printf("subscribe");
+ break;
+ case MAILBOX_LOG_RECORD_UNSUBSCRIBE:
+ printf("unsubscribe");
+ break;
+ case MAILBOX_LOG_RECORD_CREATE_DIR:
+ printf("create-dir");
+ break;
+ }
+ printf(" %s", binary_to_hex(rec.mailbox_guid,
+ sizeof(rec.mailbox_guid)));
+
+ timestamp = be32_to_cpu_unaligned(rec.timestamp);
+ printf(" (%s)\n", unixdate2str(timestamp));
+ return 1;
+}
+
+static void
+cmd_dump_mailboxlog(const char *path, const char *const *args ATTR_UNUSED)
+{
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(%s) failed: %m", path);
+
+ do {
+ T_BEGIN {
+ ret = dump_record(fd);
+ } T_END;
+ } while (ret > 0);
+ i_close_fd(&fd);
+}
+
+static bool test_dump_mailboxlog(const char *path)
+{
+ const char *p;
+ int fd;
+ struct mailbox_log_record rec;
+ bool ret = FALSE;
+
+ p = strrchr(path, '.');
+ if (p == NULL || strcmp(p, ".log") != 0)
+ return FALSE;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return FALSE;
+
+ if (read(fd, &rec, sizeof(rec)) == sizeof(rec) &&
+ rec.padding[0] == 0 && rec.padding[1] == 0 && rec.padding[2] == 0) {
+ enum mailbox_log_record_type type = rec.type;
+ switch (type) {
+ case MAILBOX_LOG_RECORD_DELETE_MAILBOX:
+ case MAILBOX_LOG_RECORD_DELETE_DIR:
+ case MAILBOX_LOG_RECORD_RENAME:
+ case MAILBOX_LOG_RECORD_SUBSCRIBE:
+ case MAILBOX_LOG_RECORD_UNSUBSCRIBE:
+ case MAILBOX_LOG_RECORD_CREATE_DIR:
+ ret = TRUE;
+ break;
+ }
+ }
+ i_close_fd(&fd);
+ return ret;
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_mailboxlog = {
+ "mailboxlog",
+ test_dump_mailboxlog,
+ cmd_dump_mailboxlog
+};
diff --git a/src/doveadm/doveadm-dump-thread.c b/src/doveadm/doveadm-dump-thread.c
new file mode 100644
index 0000000..026b07f
--- /dev/null
+++ b/src/doveadm/doveadm-dump-thread.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mmap-util.h"
+#include "mail-index-private.h"
+#include "mail-index-strmap.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static uint32_t max_likely_index;
+
+static size_t dump_hdr(const struct mail_index_strmap_header *hdr)
+{
+ printf("version = %u\n", hdr->version);
+ printf("uid validity = %u\n", hdr->uid_validity);
+ return sizeof(*hdr);
+}
+
+static int dump_record(const uint8_t **p, const uint8_t *end, uint32_t *uid)
+{
+ uint32_t uid_diff, n, i, count, crc32, idx;
+ size_t size;
+
+ /* <uid diff> <n> <crc32>*count <str_idx>*count */
+ if (mail_index_unpack_num(p, end, &uid_diff) < 0)
+ return -1;
+ *uid += uid_diff;
+
+ if (mail_index_unpack_num(p, end, &n) < 0)
+ return -1;
+ printf(" - uid %u: n=%u\n", *uid, n);
+
+ count = n < 2 ? n + 1 : n;
+ size = sizeof(crc32)*count + sizeof(idx)*count;
+ if (*p + size > end)
+ return -1;
+ for (i = 0; i < count; i++) {
+ if (i == 0)
+ printf(" - message-id: ");
+ else if (i == 1) {
+ if (n == 1)
+ printf(" - in-reply-to: ");
+ else
+ printf(" - references[1]: ");
+ } else {
+ printf(" - references[%u]: ", i);
+ }
+ memcpy(&crc32, *p + sizeof(crc32)*i, sizeof(crc32));
+ memcpy(&idx, *p + sizeof(crc32)*count + sizeof(idx)*i, sizeof(idx));
+ printf("crc32=%08x index=%u\n", crc32, idx);
+ if (idx > max_likely_index)
+ printf(" - index probably broken\n");
+ }
+ *p += size;
+ return 0;
+}
+
+static int dump_block(const uint8_t *data, const uint8_t *end, uint32_t *uid)
+{
+ const uint8_t *p;
+ uint32_t block_size;
+
+ if (data + 4 >= end)
+ return -1;
+
+ memcpy(&block_size, data, sizeof(block_size));
+ block_size = mail_index_offset_to_uint32(block_size) >> 2;
+ printf(" - block_size=%u\n", block_size);
+ if (block_size == 0) {
+ /* finished */
+ return -1;
+ }
+ if (data + sizeof(block_size) + block_size > end) {
+ printf(" - broken!\n");
+ return -1;
+ }
+ p = data + sizeof(block_size);
+ end = p + block_size;
+
+ *uid += 1;
+ while (p != end) {
+ if (dump_record(&p, end, uid) < 0) {
+ printf(" - broken\n");
+ return -1;
+ }
+ }
+ return p - data;
+}
+
+static void
+cmd_dump_thread(const char *path, const char *const *args ATTR_UNUSED)
+{
+ unsigned int pos;
+ const void *map, *end;
+ struct stat st;
+ uint32_t uid;
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(%s) failed: %m", path);
+
+ if (fstat(fd, &st) < 0)
+ i_fatal("fstat(%s) failed: %m", path);
+ max_likely_index = (st.st_size / 8) * 2;
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map == MAP_FAILED)
+ i_fatal("mmap() failed: %m");
+ end = CONST_PTR_OFFSET(map, st.st_size);
+ pos = dump_hdr(map);
+ uid = 0;
+ do {
+ printf("block at offset %u:\n", pos);
+ T_BEGIN {
+ ret = dump_block(CONST_PTR_OFFSET(map, pos), end, &uid);
+ pos += ret;
+ } T_END;
+ } while (ret > 0);
+ i_close_fd(&fd);
+}
+
+static bool test_dump_thread(const char *path)
+{
+ const char *p;
+
+ p = strrchr(path, '.');
+ return p != NULL && strcmp(p, ".thread") == 0;
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_thread = {
+ "thread",
+ test_dump_thread,
+ cmd_dump_thread
+};
diff --git a/src/doveadm/doveadm-dump.c b/src/doveadm/doveadm-dump.c
new file mode 100644
index 0000000..eb08e96
--- /dev/null
+++ b/src/doveadm/doveadm-dump.c
@@ -0,0 +1,162 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-multiplex.h"
+#include "doveadm.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static ARRAY(const struct doveadm_cmd_dump *) dumps;
+
+void doveadm_dump_register(const struct doveadm_cmd_dump *dump)
+{
+ array_push_back(&dumps, &dump);
+}
+
+static const struct doveadm_cmd_dump *
+dump_find_name(const char *name)
+{
+ const struct doveadm_cmd_dump *dump;
+
+ array_foreach_elem(&dumps, dump) {
+ if (strcmp(dump->name, name) == 0)
+ return dump;
+ }
+ return NULL;
+}
+
+static const struct doveadm_cmd_dump *
+dump_find_test(const char *path)
+{
+ const struct doveadm_cmd_dump *dump;
+
+ array_foreach_elem(&dumps, dump) {
+ if (dump->test != NULL && dump->test(path))
+ return dump;
+ }
+ return NULL;
+}
+
+static void cmd_dump(struct doveadm_cmd_context *cctx)
+{
+ const struct doveadm_cmd_dump *dump;
+ const char *path, *type = NULL, *const *args = NULL;
+ const char *no_args = NULL;
+
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ help_ver2(&doveadm_cmd_dump);
+ (void)doveadm_cmd_param_str(cctx, "type", &type);
+ (void)doveadm_cmd_param_array(cctx, "args", &args);
+
+ dump = type != NULL ? dump_find_name(type) : dump_find_test(path);
+ if (dump == NULL) {
+ if (type != NULL) {
+ print_dump_types();
+ i_fatal_status(EX_USAGE, "Unknown type: %s", type);
+ } else {
+ i_fatal_status(EX_DATAERR,
+ "Can't autodetect file type: %s", path);
+ }
+ } else {
+ if (type == NULL)
+ printf("Detected file type: %s\n", dump->name);
+ }
+ dump->cmd(path, args != NULL ? args : &no_args);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_dump = {
+ .name = "dump",
+ .cmd = cmd_dump,
+ .usage = "[-t <type>] <path> [<type-specific args>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('t', "type", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "args", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+static void
+cmd_dump_multiplex(const char *path, const char *const *args ATTR_UNUSED)
+{
+ const unsigned int channels_count = 256;
+ struct istream *file_input, *channels[channels_count];
+ const unsigned char *data;
+ size_t size;
+ unsigned int i;
+
+ file_input = i_stream_create_file(path, IO_BLOCK_SIZE);
+ /* A bit kludgy: istream-multiplex returns 0 if a wrong channel is
+ being read from. This causes a panic with blocking istreams.
+ Work around this by assuming that the file istream isn't blocking. */
+ file_input->blocking = FALSE;
+ channels[0] = i_stream_create_multiplex(file_input, IO_BLOCK_SIZE);
+ i_stream_unref(&file_input);
+
+ for (i = 1; i < channels_count; i++)
+ channels[i] = i_stream_multiplex_add_channel(channels[0], i);
+
+ bool have_input;
+ do {
+ have_input = FALSE;
+ for (i = 0; i < channels_count; i++) {
+ if (i_stream_read_more(channels[i], &data, &size) > 0) {
+ printf("CHANNEL %u: %zu bytes:\n", i, size);
+ fwrite(data, 1, size, stdout);
+ printf("\n");
+ have_input = TRUE;
+ i_stream_skip(channels[i], size);
+ }
+ }
+ } while (have_input);
+
+ if (channels[0]->stream_errno != 0)
+ i_error("read() failed: %s", i_stream_get_error(channels[0]));
+ for (i = 0; i < channels_count; i++)
+ i_stream_unref(&channels[i]);
+}
+
+struct doveadm_cmd_dump doveadm_cmd_dump_multiplex = {
+ "multiplex",
+ NULL,
+ cmd_dump_multiplex
+};
+
+static const struct doveadm_cmd_dump *dumps_builtin[] = {
+ &doveadm_cmd_dump_dbox,
+ &doveadm_cmd_dump_index,
+ &doveadm_cmd_dump_log,
+ &doveadm_cmd_dump_mailboxlog,
+ &doveadm_cmd_dump_thread,
+ &doveadm_cmd_dump_zlib,
+ &doveadm_cmd_dump_dcrypt_file,
+ &doveadm_cmd_dump_dcrypt_key,
+ &doveadm_cmd_dump_multiplex,
+};
+
+void print_dump_types(void)
+{
+ unsigned int i;
+
+ fprintf(stderr, "Available dump types: %s", dumps_builtin[0]->name);
+ for (i = 1; i < N_ELEMENTS(dumps_builtin); i++)
+ fprintf(stderr, " %s", dumps_builtin[i]->name);
+ fprintf(stderr, "\n");
+}
+
+void doveadm_dump_init(void)
+{
+ unsigned int i;
+
+ i_array_init(&dumps, N_ELEMENTS(dumps_builtin) + 8);
+ for (i = 0; i < N_ELEMENTS(dumps_builtin); i++)
+ doveadm_dump_register(dumps_builtin[i]);
+}
+
+void doveadm_dump_deinit(void)
+{
+ array_free(&dumps);
+}
diff --git a/src/doveadm/doveadm-dump.h b/src/doveadm/doveadm-dump.h
new file mode 100644
index 0000000..5776260
--- /dev/null
+++ b/src/doveadm/doveadm-dump.h
@@ -0,0 +1,27 @@
+#ifndef DOVEADM_DUMP_H
+#define DOVEADM_DUMP_H
+
+#include "doveadm.h"
+
+struct doveadm_cmd_dump {
+ const char *name;
+ bool (*test)(const char *path);
+ void (*cmd)(const char *path, const char *const *args);
+};
+
+extern struct doveadm_cmd_dump doveadm_cmd_dump_dbox;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_index;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_log;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_mailboxlog;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_thread;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_zlib;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_dcrypt_file;
+extern struct doveadm_cmd_dump doveadm_cmd_dump_dcrypt_key;
+
+void doveadm_dump_register(const struct doveadm_cmd_dump *dump);
+
+void print_dump_types(void);
+void doveadm_dump_init(void);
+void doveadm_dump_deinit(void);
+
+#endif
diff --git a/src/doveadm/doveadm-fs.c b/src/doveadm/doveadm-fs.c
new file mode 100644
index 0000000..17f6129
--- /dev/null
+++ b/src/doveadm/doveadm-fs.c
@@ -0,0 +1,608 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "md5.h"
+#include "sha2.h"
+#include "hash-method.h"
+#include "hex-binary.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "fs-api.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static void fs_cmd_help(struct doveadm_cmd_context *cctx) ATTR_NORETURN;
+static void cmd_fs_delete(struct doveadm_cmd_context *cctx);
+
+static struct fs *
+cmd_fs_init(struct doveadm_cmd_context *cctx)
+{
+ struct ssl_iostream_settings ssl_set;
+ struct fs_settings fs_set;
+ struct fs *fs;
+ const char *fs_driver, *fs_args, *error;
+
+ if (!doveadm_cmd_param_str(cctx, "fs-driver", &fs_driver) ||
+ !doveadm_cmd_param_str(cctx, "fs-args", &fs_args))
+ fs_cmd_help(cctx);
+
+ doveadm_get_ssl_settings(&ssl_set, pool_datastack_create());
+ ssl_set.verbose = doveadm_debug;
+ i_zero(&fs_set);
+ fs_set.ssl_client_set = &ssl_set;
+ fs_set.temp_dir = doveadm_settings->mail_temp_dir;
+ fs_set.base_dir = doveadm_settings->base_dir;
+ fs_set.debug = doveadm_debug;
+
+ if (fs_init(fs_driver, fs_args, &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+ return fs;
+}
+
+static void cmd_fs_get(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ struct istream *input;
+ const char *path;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+ doveadm_print_header("content", "content", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ input = fs_read_stream(file, IO_BLOCK_SIZE);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ doveadm_print_stream(data, size);
+ i_stream_skip(input, size);
+ }
+ doveadm_print_stream("", 0);
+ i_assert(ret == -1);
+ if (input->stream_errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ i_stream_get_error(input));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", fs_file_path(file),
+ i_stream_get_error(input));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ i_stream_unref(&input);
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_put(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ enum fs_properties props;
+ const char *hash_str, *src_path, *dest_path;
+ struct fs_file *file;
+ struct istream *input;
+ struct ostream *output;
+ buffer_t *hash = NULL;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "input-path", &src_path) ||
+ !doveadm_cmd_param_str(cctx, "path", &dest_path))
+ fs_cmd_help(cctx);
+ if (doveadm_cmd_param_str(cctx, "hash", &hash_str)) {
+ hash = t_buffer_create(32);
+ if (hex_to_binary(optarg, hash) < 0)
+ i_fatal("Invalid -h parameter: Hash not in hex");
+ }
+
+ file = fs_file_init(fs, dest_path, FS_OPEN_MODE_REPLACE);
+ props = fs_get_properties(fs);
+ if (hash == NULL)
+ ;
+ else if (hash->used == hash_method_md5.digest_size) {
+ if ((props & FS_PROPERTY_WRITE_HASH_MD5) == 0)
+ i_fatal("fs backend doesn't support MD5 hashes");
+ fs_write_set_hash(file,
+ hash_method_lookup(hash_method_md5.name), hash->data);
+ } else if (hash->used == hash_method_sha256.digest_size) {
+ if ((props & FS_PROPERTY_WRITE_HASH_SHA256) == 0)
+ i_fatal("fs backend doesn't support SHA256 hashes");
+ fs_write_set_hash(file,
+ hash_method_lookup(hash_method_sha256.name), hash->data);
+ }
+
+ output = fs_write_stream(file);
+ input = i_stream_create_file(src_path, IO_BLOCK_SIZE);
+ o_stream_nsend_istream(output, input);
+ i_stream_destroy(&input);
+ if (fs_write_stream_finish(file, &output) < 0) {
+ i_error("fs_write_stream_finish() failed: %s",
+ fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_copy(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *src_file, *dest_file;
+ const char *src_path, *dest_path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "source-path", &src_path) ||
+ !doveadm_cmd_param_str(cctx, "destination-path", &dest_path))
+ fs_cmd_help(cctx);
+
+ src_file = fs_file_init(fs, src_path, FS_OPEN_MODE_READONLY);
+ dest_file = fs_file_init(fs, dest_path, FS_OPEN_MODE_REPLACE);
+ if (fs_copy(src_file, dest_file) == 0) ;
+ else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", src_path,
+ fs_file_last_error(dest_file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_copy(%s, %s) failed: %s",
+ src_path, dest_path, fs_file_last_error(dest_file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&src_file);
+ fs_file_deinit(&dest_file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_stat(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ struct stat st;
+ const char *path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{path} size=%{size}");
+ doveadm_print_header_simple("path");
+ doveadm_print_header("size", "size", DOVEADM_PRINT_HEADER_FLAG_NUMBER);
+
+ if (fs_stat(file, &st) == 0) {
+ doveadm_print(fs_file_path(file));
+ doveadm_print(dec2str(st.st_size));
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ fs_file_last_error(file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_stat(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_metadata(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ const struct fs_metadata *m;
+ const ARRAY_TYPE(fs_metadata) *metadata;
+ const char *path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{key}=%{value}\n");
+ doveadm_print_header_simple("key");
+ doveadm_print_header_simple("value");
+
+ if (fs_get_metadata(file, &metadata) == 0) {
+ array_foreach(metadata, m) {
+ doveadm_print(m->key);
+ doveadm_print(m->value);
+ }
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ fs_file_last_error(file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_stat(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+struct fs_delete_ctx {
+ struct fs *fs;
+ const char *path_prefix;
+
+ unsigned int files_count;
+ struct fs_file **files;
+};
+
+static int cmd_fs_delete_ctx_run(struct fs_delete_ctx *ctx)
+{
+ unsigned int i;
+ int ret = 0;
+
+ for (i = 0; i < ctx->files_count; i++) {
+ if (ctx->files[i] == NULL)
+ ;
+ else if (fs_delete(ctx->files[i]) == 0)
+ fs_file_deinit(&ctx->files[i]);
+ else if (errno == EAGAIN) {
+ if (ret == 0)
+ ret = 1;
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(ctx->files[i]),
+ fs_file_last_error(ctx->files[i]));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ ret = -1;
+ } else {
+ i_error("fs_delete(%s) failed: %s",
+ fs_file_path(ctx->files[i]),
+ fs_file_last_error(ctx->files[i]));
+ doveadm_exit_code = EX_TEMPFAIL;
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int doveadm_fs_delete_async_fname(struct fs_delete_ctx *ctx,
+ const char *fname)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < ctx->files_count; i++) {
+ if (ctx->files[i] != NULL)
+ continue;
+
+ ctx->files[i] = fs_file_init(ctx->fs,
+ t_strdup_printf("%s%s", ctx->path_prefix, fname),
+ FS_OPEN_MODE_READONLY | FS_OPEN_FLAG_ASYNC |
+ FS_OPEN_FLAG_ASYNC_NOQUEUE);
+ fname = NULL;
+ break;
+ }
+ if ((ret = cmd_fs_delete_ctx_run(ctx)) < 0)
+ return -1;
+ if (fname != NULL) {
+ if (ret > 0)
+ fs_wait_async(ctx->fs);
+ return doveadm_fs_delete_async_fname(ctx, fname);
+ }
+ return 0;
+}
+
+static void doveadm_fs_delete_async_finish(struct fs_delete_ctx *ctx)
+{
+ unsigned int i;
+
+ while (doveadm_exit_code == 0 && cmd_fs_delete_ctx_run(ctx) > 0) {
+ fs_wait_async(ctx->fs);
+ }
+ for (i = 0; i < ctx->files_count; i++) {
+ fs_file_deinit(&ctx->files[i]);
+ }
+}
+
+static void
+cmd_fs_delete_dir_recursive(struct fs *fs, unsigned int async_count,
+ const char *path_prefix)
+{
+ struct fs_iter *iter;
+ ARRAY_TYPE(const_string) fnames;
+ struct fs_delete_ctx ctx;
+ const char *fname, *error;
+ int ret;
+
+ i_zero(&ctx);
+ ctx.fs = fs;
+ ctx.path_prefix = path_prefix;
+ ctx.files_count = I_MAX(async_count, 1);
+ ctx.files = t_new(struct fs_file *, ctx.files_count);
+
+ /* delete subdirs first. all fs backends can't handle recursive
+ lookups, so save the list first. */
+ t_array_init(&fnames, 8);
+ iter = fs_iter_init(fs, path_prefix, FS_ITER_FLAG_DIRS);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ /* append "/" so that if FS_PROPERTY_DIRECTORIES is set,
+ we'll include the "/" suffix in the filename when deleting
+ it. */
+ fname = t_strconcat(fname, "/", NULL);
+ array_push_back(&fnames, &fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s",
+ path_prefix, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ array_foreach_elem(&fnames, fname) T_BEGIN {
+ cmd_fs_delete_dir_recursive(fs, async_count,
+ t_strdup_printf("%s%s", path_prefix, fname));
+ } T_END;
+
+ /* delete files. again because we're doing this asynchronously finish
+ the iteration first. */
+ if ((fs_get_properties(fs) & FS_PROPERTY_DIRECTORIES) != 0) {
+ /* we need to explicitly delete also the directories */
+ } else {
+ array_clear(&fnames);
+ }
+ iter = fs_iter_init(fs, path_prefix, 0);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ fname = t_strdup(fname);
+ array_push_back(&fnames, &fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s",
+ path_prefix, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+
+ array_foreach_elem(&fnames, fname) {
+ T_BEGIN {
+ ret = doveadm_fs_delete_async_fname(&ctx, fname);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ doveadm_fs_delete_async_finish(&ctx);
+}
+
+static void cmd_fs_delete_recursive_path(struct fs *fs, const char *path,
+ unsigned int async_count)
+{
+ struct fs_file *file;
+ size_t path_len;
+
+ path_len = strlen(path);
+ if (path_len > 0 && path[path_len-1] != '/')
+ path = t_strconcat(path, "/", NULL);
+
+ cmd_fs_delete_dir_recursive(fs, async_count, path);
+ if ((fs_get_properties(fs) & FS_PROPERTY_DIRECTORIES) != 0) {
+ /* delete the root itself */
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ if (fs_delete(file) < 0) {
+ i_error("fs_delete(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ }
+}
+
+static void
+cmd_fs_delete_recursive(struct doveadm_cmd_context *cctx,
+ unsigned int async_count)
+{
+ struct fs *fs;
+ const char *const *paths;
+ unsigned int i;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_array(cctx, "path", &paths))
+ fs_cmd_help(cctx);
+
+ for (i = 0; paths[i] != NULL; i++)
+ cmd_fs_delete_recursive_path(fs, paths[i], async_count);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_delete_paths(struct doveadm_cmd_context *cctx,
+ unsigned int async_count)
+{
+ struct fs *fs;
+ struct fs_delete_ctx ctx;
+ const char *const *paths;
+ unsigned int i;
+ int ret;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_array(cctx, "path", &paths))
+ fs_cmd_help(cctx);
+
+ i_zero(&ctx);
+ ctx.fs = fs;
+ ctx.path_prefix = "";
+ ctx.files_count = I_MAX(async_count, 1);
+ ctx.files = t_new(struct fs_file *, ctx.files_count);
+
+ for (i = 0; paths[i] != NULL; i++) {
+ T_BEGIN {
+ ret = doveadm_fs_delete_async_fname(&ctx, paths[i]);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ doveadm_fs_delete_async_finish(&ctx);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_delete(struct doveadm_cmd_context *cctx)
+{
+ bool recursive = FALSE;
+ int64_t async_count = 0;
+
+ (void)doveadm_cmd_param_bool(cctx, "recursive", &recursive);
+ (void)doveadm_cmd_param_int64(cctx, "max-parallel", &async_count);
+
+ if (recursive)
+ cmd_fs_delete_recursive(cctx, async_count);
+ else
+ cmd_fs_delete_paths(cctx, async_count);
+}
+
+static void cmd_fs_iter_full(struct doveadm_cmd_context *cctx,
+ enum fs_iter_flags flags)
+{
+ struct fs *fs;
+ struct fs_iter *iter;
+ const char *path, *fname, *error;
+ bool b;
+
+ if (doveadm_cmd_param_bool(cctx, "no-cache", &b) && b)
+ flags |= FS_ITER_FLAG_NOCACHE;
+ if (doveadm_cmd_param_bool(cctx, "object-ids", &b) && b)
+ flags |= FS_ITER_FLAG_OBJECTIDS;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{path}\n");
+ doveadm_print_header_simple("path");
+
+ iter = fs_iter_init(fs, path, flags);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ doveadm_print(fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s", path, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_iter(struct doveadm_cmd_context *cctx)
+{
+ cmd_fs_iter_full(cctx, 0);
+}
+
+static void cmd_fs_iter_dirs(struct doveadm_cmd_context *cctx)
+{
+ cmd_fs_iter_full(cctx, FS_ITER_FLAG_DIRS);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_fs[] = {
+{
+ .name = "fs get",
+ .cmd = cmd_fs_get,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs put",
+ .cmd = cmd_fs_put,
+ .usage = "[-h <hash>] <fs-driver> <fs-args> <input path> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('h', "hash", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "input-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs copy",
+ .cmd = cmd_fs_copy,
+ .usage = "<fs-driver> <fs-args> <source path> <dest path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "destination-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs stat",
+ .cmd = cmd_fs_stat,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs metadata",
+ .cmd = cmd_fs_metadata,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs delete",
+ .cmd = cmd_fs_delete,
+ .usage = "[-R] [-n <count>] <fs-driver> <fs-args> <path> [<path> ...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('R', "recursive", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('n', "max-parallel", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs iter",
+ .cmd = cmd_fs_iter,
+ .usage = "[--no-cache] [--object-ids] <fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('C', "no-cache", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('O', "object-ids", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs iter-dirs",
+ .cmd = cmd_fs_iter_dirs,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void fs_cmd_help(struct doveadm_cmd_context *cctx)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_fs); i++) {
+ if (doveadm_cmd_fs[i].cmd == cctx->cmd->cmd)
+ help_ver2(&doveadm_cmd_fs[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_fs_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_fs); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_fs[i]);
+}
diff --git a/src/doveadm/doveadm-instance.c b/src/doveadm/doveadm-instance.c
new file mode 100644
index 0000000..cd3d55e
--- /dev/null
+++ b/src/doveadm/doveadm-instance.c
@@ -0,0 +1,155 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "master-instance.h"
+#include "master-service-settings.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <signal.h>
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_instance[];
+
+static void instance_cmd_help(const struct doveadm_cmd_ver2 *cmd) ATTR_NORETURN;
+
+static bool pid_file_read(const char *path)
+{
+ char buf[32];
+ int fd;
+ ssize_t ret;
+ pid_t pid;
+ bool found = FALSE;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno != ENOENT)
+ i_error("open(%s) failed: %m", path);
+ return FALSE;
+ }
+
+ ret = read(fd, buf, sizeof(buf));
+ if (ret < 0)
+ i_error("read(%s) failed: %m", path);
+ else if (ret > 0 && buf[ret-1] == '\n') {
+ buf[ret-1] = '\0';
+ if (str_to_pid(buf, &pid) == 0) {
+ found = !(pid == getpid() ||
+ (kill(pid, 0) < 0 && errno == ESRCH));
+ }
+ }
+ i_close_fd(&fd);
+ return found;
+}
+
+static void cmd_instance_list(struct doveadm_cmd_context *cctx)
+{
+ struct master_instance_list *list;
+ struct master_instance_list_iter *iter;
+ const struct master_instance *inst;
+ const char *instance_path, *pidfile_path;
+ bool show_config = FALSE;
+ const char *name = NULL;
+
+ (void)doveadm_cmd_param_bool(cctx, "show-config", &show_config);
+ (void)doveadm_cmd_param_str(cctx, "name", &name);
+
+ if (!show_config) {
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("path", "path", DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header_simple("name");
+ doveadm_print_header_simple("last used");
+ doveadm_print_header_simple("running");
+ }
+
+ instance_path = t_strconcat(service_set->state_dir,
+ "/"MASTER_INSTANCE_FNAME, NULL);
+ list = master_instance_list_init(instance_path);
+ iter = master_instance_list_iterate_init(list);
+ while ((inst = master_instance_iterate_list_next(iter)) != NULL) {
+ if (name != NULL && strcmp(name, inst->name) != 0)
+ continue;
+
+ if (show_config) {
+ printf("%s\n", inst->config_path == NULL ? "" :
+ inst->config_path);
+ continue;
+ }
+ doveadm_print(inst->base_dir);
+ doveadm_print(inst->name);
+ doveadm_print(unixdate2str(inst->last_used));
+ pidfile_path = t_strconcat(inst->base_dir, "/master.pid", NULL);
+ if (pid_file_read(pidfile_path))
+ doveadm_print("yes");
+ else
+ doveadm_print("no");
+ }
+ master_instance_iterate_list_deinit(&iter);
+ master_instance_list_deinit(&list);
+}
+
+static void cmd_instance_remove(struct doveadm_cmd_context *cctx)
+{
+ struct master_instance_list *list;
+ const struct master_instance *inst;
+ const char *base_dir, *instance_path, *name;
+ int ret;
+
+ if (!doveadm_cmd_param_str(cctx, "name", &name))
+ instance_cmd_help(cctx->cmd);
+
+ instance_path = t_strconcat(service_set->state_dir,
+ "/"MASTER_INSTANCE_FNAME, NULL);
+ list = master_instance_list_init(instance_path);
+ inst = master_instance_list_find_by_name(list, name);
+ base_dir = inst != NULL ? inst->base_dir : name;
+ if ((ret = master_instance_list_remove(list, base_dir)) < 0) {
+ i_error("Failed to remove instance");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (ret == 0) {
+ i_error("Instance already didn't exist");
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ }
+ master_instance_list_deinit(&list);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_instance[] = {
+{
+ .name = "instance list",
+ .cmd = cmd_instance_list,
+ .usage = "[-c] [<name>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('c', "show-config", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "name", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "instance remove",
+ .cmd = cmd_instance_remove,
+ .usage = "<name> | <base dir>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "name", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void instance_cmd_help(const struct doveadm_cmd_ver2 *cmd)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_instance); i++) {
+ if (doveadm_cmd_instance[i].cmd == cmd->cmd)
+ help_ver2(&doveadm_cmd_instance[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_instance_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_instance); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_instance[i]);
+}
diff --git a/src/doveadm/doveadm-kick.c b/src/doveadm/doveadm-kick.c
new file mode 100644
index 0000000..a0b06f3
--- /dev/null
+++ b/src/doveadm/doveadm-kick.c
@@ -0,0 +1,235 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "hash.h"
+#include "doveadm.h"
+#include "doveadm-who.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <signal.h>
+
+struct kick_user {
+ const char *username;
+ bool kick_me; /* true if username and/or ip[/mask] matches.
+ ignored when the -f switch is given. */
+};
+
+struct kick_pid {
+ pid_t pid;
+ ARRAY(struct kick_user) users;
+ bool kick;
+};
+
+struct kick_context {
+ struct who_context who;
+ HASH_TABLE(void *, struct kick_pid *) pids;
+ enum doveadm_client_type conn_type;
+ bool force_kick;
+ ARRAY(const char *) kicked_users;
+};
+
+static void
+kick_aggregate_line(struct who_context *_ctx, const struct who_line *line)
+{
+ struct kick_context *ctx = (struct kick_context *)_ctx;
+ const bool user_match = who_line_filter_match(line, &ctx->who.filter);
+ struct kick_pid *k_pid;
+ struct kick_user new_user, *user;
+
+ i_zero(&new_user);
+
+ k_pid = hash_table_lookup(ctx->pids, POINTER_CAST(line->pid));
+ if (k_pid == NULL) {
+ k_pid = p_new(ctx->who.pool, struct kick_pid, 1);
+ k_pid->pid = line->pid;
+ p_array_init(&k_pid->users, ctx->who.pool, 5);
+ hash_table_insert(ctx->pids, POINTER_CAST(line->pid), k_pid);
+ }
+
+ array_foreach_modifiable(&k_pid->users, user) {
+ if (strcmp(line->username, user->username) == 0) {
+ if (user_match)
+ user->kick_me = TRUE;
+ return;
+ }
+ }
+ new_user.username = p_strdup(ctx->who.pool, line->username);
+ new_user.kick_me = user_match;
+ array_push_back(&k_pid->users, &new_user);
+}
+
+static bool
+kick_pid_want_kicked(struct kick_context *ctx, const struct kick_pid *k_pid,
+ bool *show_warning)
+{
+ unsigned int kick_count = 0;
+ const struct kick_user *user;
+
+ if (array_count(&k_pid->users) == 1) {
+ user = array_front(&k_pid->users);
+ if (!user->kick_me)
+ return FALSE;
+ } else {
+ array_foreach(&k_pid->users, user) {
+ if (user->kick_me)
+ kick_count++;
+ }
+ if (kick_count == 0)
+ return FALSE;
+ if (kick_count < array_count(&k_pid->users) &&
+ !ctx->force_kick) {
+ array_foreach(&k_pid->users, user) {
+ if (!user->kick_me) {
+ array_push_back(&ctx->kicked_users,
+ &user->username);
+ }
+ }
+ *show_warning = TRUE;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void
+kick_print_kicked(struct kick_context *ctx, const bool show_warning)
+{
+ unsigned int i, count;
+ const char *const *users;
+ bool cli = (ctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+
+ if (array_count(&ctx->kicked_users) == 0) {
+ if (cli)
+ printf("no users kicked\n");
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ return;
+ }
+
+ if (cli) {
+ if (show_warning) {
+ printf("warning: other connections would also be "
+ "kicked from following users:\n");
+ } else {
+ printf("kicked connections from the following users:\n");
+ }
+ }
+
+ array_sort(&ctx->kicked_users, i_strcmp_p);
+ users = array_get(&ctx->kicked_users, &count);
+ doveadm_print(users[0]);
+ for (i = 1; i < count; i++) {
+ if (strcmp(users[i-1], users[i]) != 0)
+ doveadm_print(users[i]);
+ }
+
+ doveadm_print_flush();
+
+ if (cli)
+ printf("\n");
+
+ if (show_warning)
+ printf("Use the '-f' option to enforce the disconnect.\n");
+}
+
+static void kick_users(struct kick_context *ctx)
+{
+ bool show_enforce_warning = FALSE;
+ struct hash_iterate_context *iter;
+ void *key;
+ struct kick_pid *k_pid;
+ const struct kick_user *user;
+
+ p_array_init(&ctx->kicked_users, ctx->who.pool, 10);
+
+ iter = hash_table_iterate_init(ctx->pids);
+ while (hash_table_iterate(iter, ctx->pids, &key, &k_pid)) {
+ if (kick_pid_want_kicked(ctx, k_pid, &show_enforce_warning))
+ k_pid->kick = TRUE;
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (show_enforce_warning) {
+ kick_print_kicked(ctx, show_enforce_warning);
+ return;
+ }
+
+ iter = hash_table_iterate_init(ctx->pids);
+ while (hash_table_iterate(iter, ctx->pids, &key, &k_pid)) {
+ if (!k_pid->kick)
+ continue;
+
+ if (kill(k_pid->pid, SIGTERM) < 0 && errno != ESRCH) {
+ fprintf(stderr, "kill(%s, SIGTERM) failed: %m\n",
+ dec2str(k_pid->pid));
+ } else {
+ array_foreach(&k_pid->users, user) {
+ array_push_back(&ctx->kicked_users,
+ &user->username);
+ }
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ kick_print_kicked(ctx, show_enforce_warning);
+}
+
+static void cmd_kick(struct doveadm_cmd_context *cctx)
+{
+ const char *const *masks;
+ struct kick_context ctx;
+
+ i_zero(&ctx);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &(ctx.who.anvil_path)))
+ ctx.who.anvil_path = t_strconcat(doveadm_settings->base_dir, "/anvil", NULL);
+ (void)doveadm_cmd_param_bool(cctx, "force", &(ctx.force_kick));
+ if (!doveadm_cmd_param_array(cctx, "mask", &masks)) {
+ doveadm_exit_code = EX_USAGE;
+ i_error("user and/or ip[/bits] must be specified.");
+ return;
+ }
+ ctx.conn_type = cctx->conn_type;
+ if (ctx.conn_type != DOVEADM_CONNECTION_TYPE_CLI) {
+ /* force-kick is a pretty ugly option. its output can't be
+ nicely translated to an API reply. it also wouldn't be very
+ useful in scripts, only for preventing a new admin from
+ accidentally kicking too many users. it's also useful only
+ in a non-recommended setup where processes are handling
+ multiple connections. so for now we'll preserve the option
+ for CLI, but always do a force-kick with non-CLI. */
+ ctx.force_kick = TRUE;
+ }
+ ctx.who.pool = pool_alloconly_create("kick pids", 10240);
+ hash_table_create_direct(&ctx.pids, ctx.who.pool, 0);
+
+ if (who_parse_args(&ctx.who, masks)!=0) {
+ hash_table_destroy(&ctx.pids);
+ pool_unref(&ctx.who.pool);
+ return;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{result} ");
+ doveadm_print_header_simple("result");
+
+ who_lookup(&ctx.who, kick_aggregate_line);
+ kick_users(&ctx);
+
+ hash_table_destroy(&ctx.pids);
+ pool_unref(&ctx.who.pool);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_kick_ver2 = {
+ .name = "kick",
+ .cmd = cmd_kick,
+ .usage = "[-a <anvil socket path>] <user mask>[|]<ip/bits>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a',"socket-path",CMD_PARAM_STR,0)
+DOVEADM_CMD_PARAM('f',"force",CMD_PARAM_BOOL,0)
+DOVEADM_CMD_PARAM('\0',"mask",CMD_PARAM_ARRAY,CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-log.c b/src/doveadm/doveadm-log.c
new file mode 100644
index 0000000..2654d9b
--- /dev/null
+++ b/src/doveadm/doveadm-log.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <signal.h>
+#include <sys/stat.h>
+
+#define LAST_LOG_TYPE LOG_TYPE_PANIC
+#define TEST_LOG_MSG_PREFIX "This is Dovecot's "
+#define LOG_ERRORS_FNAME "log-errors"
+#define LOG_TIMESTAMP_FORMAT "%b %d %H:%M:%S"
+
+static void ATTR_NULL(2)
+cmd_log_test(struct doveadm_cmd_context *cctx ATTR_UNUSED)
+{
+ struct failure_context ctx;
+ unsigned int i;
+
+ master_service->log_initialized = FALSE;
+ master_service->flags |= MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR;
+ master_service_init_log(master_service);
+
+ i_zero(&ctx);
+ for (i = 0; i < LAST_LOG_TYPE; i++) {
+ const char *prefix = failure_log_type_prefixes[i];
+
+ /* add timestamp so that syslog won't just write
+ "repeated message" text */
+ ctx.type = i;
+ i_log_type(&ctx, TEST_LOG_MSG_PREFIX"%s log (%u)",
+ t_str_lcase(t_strcut(prefix, ':')),
+ (unsigned int)ioloop_time);
+ }
+}
+
+static void cmd_log_reopen(struct doveadm_cmd_context *cctx ATTR_UNUSED)
+{
+ doveadm_master_send_signal(SIGUSR1);
+}
+
+struct log_find_file {
+ const char *path;
+ uoff_t size;
+
+ /* 1 << enum log_type */
+ unsigned int mask;
+};
+
+struct log_find_context {
+ pool_t pool;
+ HASH_TABLE(char *, struct log_find_file *) files;
+};
+
+static void cmd_log_find_add(struct log_find_context *ctx,
+ const char *path, enum log_type type)
+{
+ struct log_find_file *file;
+ char *key;
+
+ file = hash_table_lookup(ctx->files, path);
+ if (file == NULL) {
+ file = p_new(ctx->pool, struct log_find_file, 1);
+ file->path = key = p_strdup(ctx->pool, path);
+ hash_table_insert(ctx->files, key, file);
+ }
+
+ file->mask |= 1 << type;
+}
+
+static void
+cmd_log_find_syslog_files(struct log_find_context *ctx, const char *path)
+{
+ struct log_find_file *file;
+ DIR *dir;
+ struct dirent *d;
+ struct stat st;
+ char *key;
+ string_t *full_path;
+ size_t dir_len;
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ i_error("opendir(%s) failed: %m", path);
+ return;
+ }
+
+ full_path = t_str_new(256);
+ str_append(full_path, path);
+ str_append_c(full_path, '/');
+ dir_len = str_len(full_path);
+
+ while ((d = readdir(dir)) != NULL) {
+ if (d->d_name[0] == '.')
+ continue;
+
+ str_truncate(full_path, dir_len);
+ str_append(full_path, d->d_name);
+ if (stat(str_c(full_path), &st) < 0)
+ continue;
+
+ if (S_ISDIR(st.st_mode)) {
+ /* recursively go through all subdirectories */
+ cmd_log_find_syslog_files(ctx, str_c(full_path));
+ } else if (hash_table_lookup(ctx->files,
+ str_c(full_path)) == NULL) {
+ file = p_new(ctx->pool, struct log_find_file, 1);
+ file->size = st.st_size;
+ file->path = key =
+ p_strdup(ctx->pool, str_c(full_path));
+ hash_table_insert(ctx->files, key, file);
+ }
+ }
+
+ (void)closedir(dir);
+}
+
+static bool log_type_find(const char *str, enum log_type *type_r)
+{
+ unsigned int i;
+ size_t len = strlen(str);
+
+ for (i = 0; i < LAST_LOG_TYPE; i++) {
+ if (strncasecmp(str, failure_log_type_prefixes[i], len) == 0 &&
+ failure_log_type_prefixes[i][len] == ':') {
+ *type_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void cmd_log_find_syslog_file_messages(struct log_find_file *file)
+{
+ struct istream *input;
+ const char *line, *p;
+ enum log_type type;
+ int fd;
+
+ fd = open(file->path, O_RDONLY);
+ if (fd == -1)
+ return;
+
+ input = i_stream_create_fd_autoclose(&fd, 1024);
+ i_stream_seek(input, file->size);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ p = strstr(line, TEST_LOG_MSG_PREFIX);
+ if (p == NULL)
+ continue;
+ p += strlen(TEST_LOG_MSG_PREFIX);
+
+ /* <type> log */
+ T_BEGIN {
+ if (log_type_find(t_strcut(p, ' '), &type))
+ file->mask |= 1 << type;
+ } T_END;
+ }
+ i_stream_destroy(&input);
+}
+
+static void cmd_log_find_syslog_messages(struct log_find_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ struct stat st;
+ char *key;
+ struct log_find_file *file;
+
+ iter = hash_table_iterate_init(ctx->files);
+ while (hash_table_iterate(iter, ctx->files, &key, &file)) {
+ if (stat(file->path, &st) < 0 ||
+ (uoff_t)st.st_size <= file->size)
+ continue;
+
+ cmd_log_find_syslog_file_messages(file);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static void
+cmd_log_find_syslog(struct log_find_context *ctx,
+ struct doveadm_cmd_context *cctx)
+{
+ const char *log_dir;
+ struct stat st;
+
+ if (doveadm_cmd_param_str(cctx, "log-dir", &log_dir))
+ ;
+ else if (stat("/var/log", &st) == 0 && S_ISDIR(st.st_mode))
+ log_dir = "/var/log";
+ else if (stat("/var/adm", &st) == 0 && S_ISDIR(st.st_mode))
+ log_dir = "/var/adm";
+ else
+ return;
+
+ printf("Looking for log files from %s\n", log_dir);
+ cmd_log_find_syslog_files(ctx, log_dir);
+ cmd_log_test(cctx);
+
+ /* give syslog some time to write the messages to files */
+ sleep(1);
+ cmd_log_find_syslog_messages(ctx);
+}
+
+static void cmd_log_find(struct doveadm_cmd_context *cctx)
+{
+ const struct master_service_settings *set;
+ const char *log_file_path;
+ struct log_find_context ctx;
+ unsigned int i;
+
+ i_zero(&ctx);
+ ctx.pool = pool_alloconly_create("log file", 1024*32);
+ hash_table_create(&ctx.files, ctx.pool, 0, str_hash, strcmp);
+
+ /* first get the paths that we know are used */
+ set = master_service_settings_get(master_service);
+ log_file_path = set->log_path;
+ if (strcmp(log_file_path, "syslog") == 0)
+ log_file_path = "";
+ if (*log_file_path != '\0') {
+ cmd_log_find_add(&ctx, log_file_path, LOG_TYPE_WARNING);
+ cmd_log_find_add(&ctx, log_file_path, LOG_TYPE_ERROR);
+ cmd_log_find_add(&ctx, log_file_path, LOG_TYPE_FATAL);
+ }
+
+ if (strcmp(set->info_log_path, "syslog") != 0) {
+ if (*set->info_log_path != '\0')
+ log_file_path = set->info_log_path;
+ if (*log_file_path != '\0')
+ cmd_log_find_add(&ctx, log_file_path, LOG_TYPE_INFO);
+ }
+
+ if (strcmp(set->debug_log_path, "syslog") != 0) {
+ if (*set->debug_log_path != '\0')
+ log_file_path = set->debug_log_path;
+ if (*log_file_path != '\0')
+ cmd_log_find_add(&ctx, log_file_path, LOG_TYPE_DEBUG);
+ }
+
+ if (*set->log_path == '\0' ||
+ strcmp(set->log_path, "syslog") == 0 ||
+ strcmp(set->info_log_path, "syslog") == 0 ||
+ strcmp(set->debug_log_path, "syslog") == 0) {
+ /* at least some logs were logged via syslog */
+ cmd_log_find_syslog(&ctx, cctx);
+ }
+
+ /* print them */
+ for (i = 0; i < LAST_LOG_TYPE; i++) {
+ struct hash_iterate_context *iter;
+ char *key;
+ struct log_find_file *file;
+ bool found = FALSE;
+
+ iter = hash_table_iterate_init(ctx.files);
+ while (hash_table_iterate(iter, ctx.files, &key, &file)) {
+ if ((file->mask & (1 << i)) != 0) {
+ printf("%s%s\n", failure_log_type_prefixes[i],
+ file->path);
+ found = TRUE;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (!found)
+ printf("%sNot found\n", failure_log_type_prefixes[i]);
+ }
+ hash_table_destroy(&ctx.files);
+ pool_unref(&ctx.pool);
+}
+
+static const char *t_cmd_log_error_trim(const char *orig)
+{
+ size_t pos;
+
+ /* Trim whitespace from suffix and remove ':' if it exists */
+ for (pos = strlen(orig); pos > 0; pos--) {
+ if (orig[pos-1] != ' ') {
+ if (orig[pos-1] == ':')
+ pos--;
+ break;
+ }
+ }
+ return orig[pos] == '\0' ? orig : t_strndup(orig, pos);
+}
+
+static void cmd_log_error_write(const char *const *args, time_t min_timestamp)
+{
+ /* <type> <timestamp> <prefix> <text> */
+ const char *type_prefix = "?";
+ unsigned int type;
+ time_t t;
+
+ /* find type's prefix */
+ for (type = 0; type < LOG_TYPE_COUNT; type++) {
+ if (strcmp(args[0], failure_log_type_names[type]) == 0) {
+ type_prefix = failure_log_type_prefixes[type];
+ break;
+ }
+ }
+
+ if (str_to_time(args[1], &t) < 0) {
+ i_error("Invalid timestamp: %s", args[1]);
+ t = 0;
+ }
+ if (t >= min_timestamp) {
+ doveadm_print(t_strflocaltime(LOG_TIMESTAMP_FORMAT, t));
+ doveadm_print(t_cmd_log_error_trim(args[2]));
+ doveadm_print(t_cmd_log_error_trim(type_prefix));
+ doveadm_print(args[3]);
+ }
+}
+
+static void cmd_log_errors(struct doveadm_cmd_context *cctx)
+{
+ struct istream *input;
+ const char *path, *line, *const *args;
+ time_t min_timestamp = 0;
+ int64_t since_int64;
+ int fd;
+
+ if (doveadm_cmd_param_int64(cctx, "since", &since_int64))
+ min_timestamp = since_int64;
+
+ path = t_strconcat(doveadm_settings->base_dir,
+ "/"LOG_ERRORS_FNAME, NULL);
+ fd = net_connect_unix(path);
+ if (fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ net_set_nonblock(fd, FALSE);
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{timestamp} %{type}: %{prefix}: %{text}\n");
+
+ doveadm_print_header_simple("timestamp");
+ doveadm_print_header_simple("prefix");
+ doveadm_print_header_simple("type");
+ doveadm_print_header_simple("text");
+
+ while ((line = i_stream_read_next_line(input)) != NULL) T_BEGIN {
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) == 4)
+ cmd_log_error_write(args, min_timestamp);
+ else {
+ i_error("Invalid input from log: %s", line);
+ doveadm_exit_code = EX_PROTOCOL;
+ }
+ } T_END;
+ i_stream_destroy(&input);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_log[] = {
+{
+ .name = "log test",
+ .cmd = cmd_log_test,
+ .usage = "",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "log reopen",
+ .cmd = cmd_log_reopen,
+ .usage = "",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "log find",
+ .cmd = cmd_log_find,
+ .usage = "[<dir>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "log-dir", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "log errors",
+ .usage = "[-s <min_timestamp>]",
+ .cmd = cmd_log_errors,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('s', "since", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+void doveadm_register_log_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_log); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_log[i]);
+}
diff --git a/src/doveadm/doveadm-mail-altmove.c b/src/doveadm/doveadm-mail-altmove.c
new file mode 100644
index 0000000..7bcf7b0
--- /dev/null
+++ b/src/doveadm/doveadm-mail-altmove.c
@@ -0,0 +1,162 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+struct altmove_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool reverse;
+};
+
+static int
+cmd_altmove_box(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args, bool reverse)
+{
+ struct doveadm_mail_iter *iter;
+ struct mail *mail;
+ enum modify_type modify_type =
+ !reverse ? MODIFY_ADD : MODIFY_REMOVE;
+
+ if (doveadm_mail_iter_init(ctx, info, search_args, 0, NULL, 0,
+ &iter) < 0)
+ return -1;
+
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ if (doveadm_debug) {
+ i_debug("altmove: box=%s uid=%u",
+ info->vname, mail->uid);
+ }
+ mail_update_flags(mail, modify_type,
+ (enum mail_flags)MAIL_INDEX_MAIL_FLAG_BACKEND);
+ }
+ return doveadm_mail_iter_deinit_sync(&iter);
+}
+
+static int
+ns_purge(struct doveadm_mail_cmd_context *ctx, struct mail_namespace *ns,
+ struct mail_storage *storage)
+{
+ if (mail_storage_purge(storage) < 0) {
+ i_error("Purging namespace '%s' failed: %s", ns->prefix,
+ mail_storage_get_last_internal_error(storage, NULL));
+ doveadm_mail_failed_storage(ctx, storage);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+cmd_altmove_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct altmove_cmd_context *ctx = (struct altmove_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ struct mail_namespace *ns, *prev_ns = NULL;
+ ARRAY(struct mail_storage *) purged_storages;
+ struct mail_storage *const *storages, *ns_storage, *prev_storage = NULL;
+ unsigned int i, count;
+ int ret = 0;
+
+ t_array_init(&purged_storages, 8);
+ iter = doveadm_mailbox_list_iter_init(_ctx, user, _ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ ns_storage = mail_namespace_get_default_storage(info->ns);
+ if (ns_storage != prev_storage) {
+ if (prev_storage != NULL) {
+ if (ns_purge(_ctx, prev_ns, prev_storage) < 0)
+ ret = -1;
+ array_push_back(&purged_storages,
+ &prev_storage);
+ }
+ prev_storage = ns_storage;
+ prev_ns = info->ns;
+ }
+ if (cmd_altmove_box(_ctx, info, _ctx->search_args, ctx->reverse) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+
+ if (prev_storage != NULL) {
+ if (ns_purge(_ctx, prev_ns, prev_storage) < 0)
+ ret = -1;
+ array_push_back(&purged_storages, &prev_storage);
+ }
+
+ /* make sure all private storages have been purged */
+ storages = array_get(&purged_storages, &count);
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->type != MAIL_NAMESPACE_TYPE_PRIVATE)
+ continue;
+
+ ns_storage = mail_namespace_get_default_storage(ns);
+ for (i = 0; i < count; i++) {
+ if (ns_storage == storages[i])
+ break;
+ }
+ if (i == count) {
+ if (ns_purge(_ctx, ns, ns_storage) < 0)
+ ret = -1;
+ array_push_back(&purged_storages, &ns_storage);
+ storages = array_get(&purged_storages, &count);
+ }
+ }
+ return ret;
+}
+
+static void cmd_altmove_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("altmove");
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+static bool
+cmd_mailbox_altmove_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct altmove_cmd_context *ctx = (struct altmove_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'r':
+ ctx->reverse = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_altmove_alloc(void)
+{
+ struct altmove_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct altmove_cmd_context);
+ ctx->ctx.getopt_args = "r";
+ ctx->ctx.v.parse_arg = cmd_mailbox_altmove_parse_arg;
+ ctx->ctx.v.init = cmd_altmove_init;
+ ctx->ctx.v.run = cmd_altmove_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_altmove_ver2 = {
+ .name = "altmove",
+ .mail_cmd = cmd_altmove_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-r] <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('r', "reverse", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-batch.c b/src/doveadm/doveadm-mail-batch.c
new file mode 100644
index 0000000..2dbbb89
--- /dev/null
+++ b/src/doveadm/doveadm-mail-batch.c
@@ -0,0 +1,186 @@
+/* Copyright (c) 2012-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "doveadm-mail.h"
+
+#include <unistd.h>
+
+struct batch_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ ARRAY(struct doveadm_mail_cmd_context *) commands;
+};
+
+static int cmd_batch_prerun(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_storage_service_user *service_user,
+ const char **error_r)
+{
+ struct batch_cmd_context *ctx = (struct batch_cmd_context *)_ctx;
+ struct doveadm_mail_cmd_context *cmd;
+ int ret = 0;
+
+ array_foreach_elem(&ctx->commands, cmd) {
+ if (cmd->v.prerun != NULL &&
+ cmd->v.prerun(cmd, service_user, error_r) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ return ret;
+}
+
+static int cmd_batch_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct batch_cmd_context *ctx = (struct batch_cmd_context *)_ctx;
+ struct doveadm_mail_cmd_context *cmd;
+ int ret = 0;
+
+ array_foreach_elem(&ctx->commands, cmd) {
+ cmd->cur_mail_user = user;
+ const char *reason_code =
+ event_reason_code_prefix("doveadm", "cmd_",
+ cmd->cmd->name);
+ struct event_reason *reason = event_reason_begin(reason_code);
+ ret = cmd->v.run(cmd, user);
+ event_reason_end(&reason);
+ if (ret < 0) {
+ i_assert(cmd->exit_code != 0);
+ _ctx->exit_code = cmd->exit_code;
+ break;
+ }
+ cmd->cur_mail_user = NULL;
+ }
+ return ret;
+}
+
+static void
+cmd_batch_add(struct batch_cmd_context *batchctx,
+ int argc, const char *const *argv)
+{
+ struct doveadm_mail_cmd_context *subctx;
+ const struct doveadm_cmd_ver2 *cmd_ver2;
+ const struct doveadm_mail_cmd *cmd;
+ const char *getopt_args;
+ int c;
+
+ cmd_ver2 = doveadm_cmd_find_with_args_ver2(argv[0], &argc, &argv);
+ if (cmd_ver2 == NULL)
+ i_fatal_status(EX_USAGE, "doveadm batch: '%s' mail command doesn't exist", argv[0]);
+
+ struct doveadm_mail_cmd *dyncmd =
+ p_new(batchctx->ctx.pool, struct doveadm_mail_cmd, 1);
+ dyncmd->usage_args = cmd_ver2->usage;
+ dyncmd->name = cmd_ver2->name;
+ dyncmd->alloc = cmd_ver2->mail_cmd;
+ cmd = dyncmd;
+
+ subctx = doveadm_mail_cmd_init(cmd, doveadm_settings);
+ subctx->full_args = argv + 1;
+ subctx->service_flags |= batchctx->ctx.service_flags;
+
+ i_getopt_reset();
+ getopt_args = subctx->getopt_args != NULL ? subctx->getopt_args : "";
+ while ((c = getopt(argc, (void *)argv, getopt_args)) > 0) {
+ if (subctx->v.parse_arg == NULL ||
+ !subctx->v.parse_arg(subctx, c))
+ doveadm_mail_help(cmd);
+ }
+ argv += optind;
+ if (argv[0] != NULL && cmd->usage_args == NULL) {
+ i_fatal_status(EX_USAGE, "doveadm %s: Unknown parameter: %s",
+ cmd->name, argv[0]);
+ }
+ subctx->args = argv;
+ if (subctx->v.preinit != NULL)
+ subctx->v.preinit(subctx);
+ array_push_back(&batchctx->commands, &subctx);
+}
+
+static void
+cmd_batch_preinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ const char *const *args = _ctx->args;
+ struct batch_cmd_context *ctx = (struct batch_cmd_context *)_ctx;
+ ARRAY_TYPE(const_string) sep_args;
+ const char *sep = args[0];
+ unsigned int i, start;
+ int argc;
+ const char *const *argv;
+
+ if (sep == NULL || args[1] == NULL)
+ doveadm_mail_help_name("batch");
+ args++;
+
+ p_array_init(&ctx->commands, _ctx->pool, 8);
+ p_array_init(&sep_args, _ctx->pool, 16);
+ for (i = start = 0;; i++) {
+ if (args[i] != NULL && strcmp(args[i], sep) != 0) {
+ array_push_back(&sep_args, &args[i]);
+ continue;
+ }
+ if (i > start) {
+ (void)array_append_space(&sep_args);
+ argc = i - start;
+ argv = array_idx(&sep_args, start);
+ cmd_batch_add(ctx, argc, argv);
+ start = i+1;
+ }
+ if (args[i] == NULL)
+ break;
+ }
+ (void)array_append_space(&sep_args);
+}
+
+static void
+cmd_batch_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[] ATTR_UNUSED)
+{
+ struct batch_cmd_context *ctx = (struct batch_cmd_context *)_ctx;
+ struct doveadm_mail_cmd_context *cmd;
+ struct batch_cmd_context *subctx;
+
+ array_foreach_elem(&ctx->commands, cmd) {
+ subctx = (struct batch_cmd_context *)cmd;
+ subctx->ctx.storage_service = _ctx->storage_service;
+ if (subctx->ctx.v.init != NULL)
+ subctx->ctx.v.init(&subctx->ctx, subctx->ctx.args);
+ }
+}
+
+static void cmd_batch_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct batch_cmd_context *ctx = (struct batch_cmd_context *)_ctx;
+ struct doveadm_mail_cmd_context *cmd;
+
+ array_foreach_elem(&ctx->commands, cmd) {
+ doveadm_mail_cmd_deinit(cmd);
+ doveadm_mail_cmd_free(cmd);
+ }
+}
+
+static struct doveadm_mail_cmd_context *cmd_batch_alloc(void)
+{
+ struct batch_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct batch_cmd_context);
+ ctx->ctx.getopt_args = "+"; /* disable processing -args in the middle */
+ ctx->ctx.v.preinit = cmd_batch_preinit;
+ ctx->ctx.v.init = cmd_batch_init;
+ ctx->ctx.v.prerun = cmd_batch_prerun;
+ ctx->ctx.v.run = cmd_batch_run;
+ ctx->ctx.v.deinit = cmd_batch_deinit;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_batch = {
+ .name = "batch",
+ .mail_cmd = cmd_batch_alloc,
+ .usage = "<sep> <cmd1> [<sep> <cmd2> [..]]",
+ .flags = CMD_FLAG_NO_UNORDERED_OPTIONS,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "separator", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "args", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-copymove.c b/src/doveadm/doveadm-mail-copymove.c
new file mode 100644
index 0000000..7e26ed8
--- /dev/null
+++ b/src/doveadm/doveadm-mail-copymove.c
@@ -0,0 +1,224 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "doveadm-print.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+struct copy_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ const char *source_username;
+ struct mail_storage_service_user *source_service_user;
+ struct mail_user *source_user;
+
+ const char *destname;
+ bool move;
+};
+
+static int
+cmd_copy_box(struct copy_cmd_context *ctx, struct mailbox *destbox,
+ const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox_transaction_context *desttrans;
+ struct mail_save_context *save_ctx;
+ struct mail *mail;
+ int ret = 0, ret2;
+
+ if (doveadm_mail_iter_init(&ctx->ctx, info, ctx->ctx.search_args, 0,
+ NULL, 0, &iter) < 0)
+ return -1;
+
+ /* use a separately committed transaction for each mailbox.
+ this guarantees that mails aren't expunged without actually having
+ been copied. */
+ desttrans = mailbox_transaction_begin(destbox,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ ctx->ctx.transaction_flags, __func__);
+
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ save_ctx = mailbox_save_alloc(desttrans);
+ mailbox_save_copy_flags(save_ctx, mail);
+ if (ctx->move)
+ ret2 = mailbox_move(&save_ctx, mail);
+ else
+ ret2 = mailbox_copy(&save_ctx, mail);
+ if (ret2 < 0) {
+ i_error("%s message UID %u from '%s' failed: %s",
+ ctx->move ? "Moving" : "Copying",
+ mail->uid, info->vname,
+ mailbox_get_last_internal_error(destbox, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, destbox);
+ ret = -1;
+ }
+ }
+
+ if (mailbox_transaction_commit(&desttrans) < 0) {
+ i_error("Committing %s mails failed: %s",
+ ctx->move ? "moved" : "copied",
+ mailbox_get_last_internal_error(destbox, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, destbox);
+ /* rollback expunges */
+ doveadm_mail_iter_deinit_rollback(&iter);
+ ret = -1;
+ } else {
+ if (doveadm_mail_iter_deinit_sync(&iter) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+static void
+cmd_copy_alloc_source_user(struct copy_cmd_context *ctx)
+{
+ struct mail_storage_service_input input;
+ const char *error;
+
+ input = ctx->ctx.storage_service_input;
+ input.username = ctx->source_username;
+
+ mail_storage_service_io_deactivate_user(ctx->ctx.cur_service_user);
+ if (mail_storage_service_lookup_next(ctx->ctx.storage_service, &input,
+ &ctx->source_service_user,
+ &ctx->source_user,
+ &error) < 0)
+ i_fatal("Couldn't lookup user %s: %s", input.username, error);
+ mail_storage_service_io_deactivate_user(ctx->source_service_user);
+ mail_storage_service_io_activate_user(ctx->ctx.cur_service_user);
+}
+
+static int
+cmd_copy_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct copy_cmd_context *ctx = (struct copy_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ struct mail_user *src_user;
+ struct mail_namespace *ns;
+ struct mailbox *destbox;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ if (ctx->source_username != NULL && ctx->source_user == NULL)
+ cmd_copy_alloc_source_user(ctx);
+
+ ns = mail_namespace_find(user->namespaces, ctx->destname);
+ destbox = mailbox_alloc(ns->list, ctx->destname, MAILBOX_FLAG_SAVEONLY);
+ if (mailbox_open(destbox) < 0) {
+ i_error("Can't open mailbox '%s': %s", ctx->destname,
+ mailbox_get_last_internal_error(destbox, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, destbox);
+ mailbox_free(&destbox);
+ return -1;
+ }
+
+ src_user = ctx->source_user != NULL ? ctx->source_user : user;
+ iter = doveadm_mailbox_list_iter_init(_ctx, src_user, _ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_copy_box(ctx, destbox, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+
+ if (mailbox_sync(destbox, 0) < 0) {
+ i_error("Syncing mailbox '%s' failed: %s", ctx->destname,
+ mailbox_get_last_internal_error(destbox, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, destbox);
+ ret = -1;
+ }
+ mailbox_free(&destbox);
+ return ret;
+}
+
+static void cmd_copy_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct copy_cmd_context *ctx = (struct copy_cmd_context *)_ctx;
+ const char *destname = args[0], *cmdname = ctx->move ? "move" : "copy";
+
+ if (destname == NULL || args[1] == NULL)
+ doveadm_mail_help_name(cmdname);
+ args++;
+
+ if (args[0] != NULL && args[1] != NULL &&
+ strcasecmp(args[0], "user") == 0) {
+ if ((_ctx->service_flags &
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) == 0)
+ i_fatal("Use -u parameter to specify destination user");
+
+ ctx->source_username = p_strdup(_ctx->pool, args[1]);
+ args += 2;
+ }
+
+ ctx->destname = p_strdup(ctx->ctx.pool, destname);
+ _ctx->search_args = doveadm_mail_build_search_args(args);
+ if (ctx->move)
+ expunge_search_args_check(ctx->ctx.search_args, cmdname);
+}
+
+static void cmd_copy_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct copy_cmd_context *ctx = (struct copy_cmd_context *)_ctx;
+
+ if (ctx->source_user != NULL) {
+ mail_storage_service_user_unref(&ctx->source_service_user);
+ mail_user_deinit(&ctx->source_user);
+ }
+}
+
+static struct doveadm_mail_cmd_context *cmd_copy_alloc(void)
+{
+ struct copy_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct copy_cmd_context);
+ ctx->ctx.v.init = cmd_copy_init;
+ ctx->ctx.v.deinit = cmd_copy_deinit;
+ ctx->ctx.v.run = cmd_copy_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_move_alloc(void)
+{
+ struct copy_cmd_context *ctx;
+
+ ctx = (struct copy_cmd_context *)cmd_copy_alloc();
+ ctx->move = TRUE;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_copy_ver2 = {
+ .name = "copy",
+ .mail_cmd = cmd_copy_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<destination> [user <source user>] <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "destination-mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-type", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_move_ver2 = {
+ .name = "move",
+ .mail_cmd = cmd_move_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<destination> [user <source user>] <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "destination-mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-type", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-deduplicate.c b/src/doveadm/doveadm-mail-deduplicate.c
new file mode 100644
index 0000000..f990287
--- /dev/null
+++ b/src/doveadm/doveadm-mail-deduplicate.c
@@ -0,0 +1,146 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "mail-storage.h"
+#include "mail-search-build.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+struct deduplicate_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool by_msgid;
+};
+
+static int
+cmd_deduplicate_box(struct doveadm_mail_cmd_context *_ctx,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args)
+{
+ struct deduplicate_cmd_context *ctx =
+ (struct deduplicate_cmd_context *)_ctx;
+ struct doveadm_mail_iter *iter;
+ struct mail *mail;
+ enum mail_error error;
+ pool_t pool;
+ HASH_TABLE(const char *, void *) hash;
+ const char *key, *errstr;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(_ctx, info, search_args, 0, NULL, 0,
+ &iter) < 0)
+ return -1;
+
+ pool = pool_alloconly_create("deduplicate", 10240);
+ hash_table_create(&hash, pool, 0, str_hash, strcmp);
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ if (ctx->by_msgid) {
+ if (mail_get_first_header(mail, "Message-ID", &key) < 0) {
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ if (error == MAIL_ERROR_NOTFOUND)
+ continue;
+ i_error("Couldn't lookup Message-ID: for UID=%u: %s",
+ mail->uid, errstr);
+ doveadm_mail_failed_error(_ctx, error);
+ ret = -1;
+ break;
+ }
+ } else {
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &key) < 0) {
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ if (error == MAIL_ERROR_NOTFOUND)
+ continue;
+ i_error("Couldn't lookup GUID: for UID=%u: %s",
+ mail->uid, errstr);
+ doveadm_mail_failed_error(_ctx, error);
+ ret = -1;
+ break;
+ }
+ }
+ if (key != NULL && *key != '\0') {
+ if (hash_table_lookup(hash, key) != NULL)
+ mail_expunge(mail);
+ else {
+ key = p_strdup(pool, key);
+ hash_table_insert(hash, key, POINTER_CAST(1));
+ }
+ }
+ }
+
+ if (doveadm_mail_iter_deinit_sync(&iter) < 0)
+ ret = -1;
+
+ hash_table_destroy(&hash);
+ pool_unref(&pool);
+ return ret;
+}
+
+static int
+cmd_deduplicate_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_deduplicate_box(ctx, info, ctx->search_args) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_deduplicate_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("deduplicate");
+
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+static bool
+cmd_deduplicate_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct deduplicate_cmd_context *ctx =
+ (struct deduplicate_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'm':
+ ctx->by_msgid = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_deduplicate_alloc(void)
+{
+ struct deduplicate_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct deduplicate_cmd_context);
+ ctx->ctx.getopt_args = "m";
+ ctx->ctx.v.parse_arg = cmd_deduplicate_parse_arg;
+ ctx->ctx.v.init = cmd_deduplicate_init;
+ ctx->ctx.v.run = cmd_deduplicate_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_deduplicate_ver2 = {
+ .name = "deduplicate",
+ .mail_cmd = cmd_deduplicate_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-m] <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('m', "by-msgid", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-expunge.c b/src/doveadm/doveadm-mail-expunge.c
new file mode 100644
index 0000000..4ab7e3d
--- /dev/null
+++ b/src/doveadm/doveadm-mail-expunge.c
@@ -0,0 +1,286 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index.h"
+#include "mail-storage.h"
+#include "mail-search.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+struct expunge_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool delete_empty_mailbox;
+};
+
+static int
+cmd_expunge_box(struct doveadm_mail_cmd_context *_ctx,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args)
+{
+ struct expunge_cmd_context *ctx = (struct expunge_cmd_context *)_ctx;
+ struct doveadm_mail_iter *iter;
+ struct mailbox *box;
+ struct mail *mail;
+ enum mail_error error;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(_ctx, info, search_args, 0, NULL, 0,
+ &iter) < 0)
+ return -1;
+
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ if (doveadm_debug) {
+ i_debug("expunge: box=%s uid=%u",
+ info->vname, mail->uid);
+ }
+ mail_expunge(mail);
+ }
+
+ if (doveadm_mail_iter_deinit_keep_box(&iter, &box) < 0)
+ ret = -1;
+ else if (mailbox_sync(box, 0) < 0) {
+ i_error("Syncing mailbox '%s' failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+
+ if (ctx->delete_empty_mailbox && ret == 0) {
+ if (mailbox_delete_empty(box) < 0) {
+ error = mailbox_get_last_mail_error(box);
+ if (error != MAIL_ERROR_EXISTS) {
+ i_error("Deleting mailbox '%s' failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ } else {
+ if (mailbox_set_subscribed(box, FALSE) < 0) {
+ i_error("Unsubscribing mailbox '%s' failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ }
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static bool
+expunge_search_args_is_mailbox_ok(struct mail_search_arg *args);
+
+static bool
+expunge_search_args_is_mailbox_or_ok(struct mail_search_arg *args)
+{
+ struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_OR:
+ if (!expunge_search_args_is_mailbox_or_ok(arg->value.subargs))
+ return FALSE;
+ break;
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (!expunge_search_args_is_mailbox_ok(arg->value.subargs))
+ return FALSE;
+ break;
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ break;
+ default:
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+expunge_search_args_is_mailbox_ok(struct mail_search_arg *args)
+{
+ struct mail_search_arg *arg;
+ bool have_or = FALSE;
+
+ /* a) we find one mailbox here in the SUB block */
+ for (arg = args; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ return TRUE;
+ case SEARCH_OR:
+ have_or = TRUE;
+ break;
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (expunge_search_args_is_mailbox_ok(arg->value.subargs))
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* b) there is at least one OR block, and all of the ORs must have
+ mailbox */
+ if (!have_or)
+ return FALSE;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (arg->type == SEARCH_OR &&
+ !expunge_search_args_is_mailbox_or_ok(arg->value.subargs))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+expunge_search_args_is_msgset_ok(struct mail_search_arg *args);
+
+static bool
+expunge_search_args_is_msgset_or_ok(struct mail_search_arg *args)
+{
+ struct mail_search_arg *arg;
+
+ /* we're done if all OR branches contain something else besides
+ MAILBOXes */
+ for (arg = args; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ return FALSE;
+ case SEARCH_OR:
+ if (!expunge_search_args_is_msgset_or_ok(arg->value.subargs))
+ return FALSE;
+ break;
+ case SEARCH_SUB:
+ if (!expunge_search_args_is_msgset_ok(arg->value.subargs))
+ return FALSE;
+ break;
+ default:
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+expunge_search_args_is_msgset_ok(struct mail_search_arg *args)
+{
+ struct mail_search_arg *arg;
+
+ /* all args can't be just MAILBOXes */
+ for (arg = args; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ break;
+ case SEARCH_OR:
+ /* if each OR branch has something else than just
+ MAILBOXes, we're ok */
+ if (expunge_search_args_is_msgset_or_ok(arg->value.subargs))
+ return TRUE;
+ break;
+ case SEARCH_SUB:
+ if (expunge_search_args_is_msgset_ok(arg->value.subargs))
+ return TRUE;
+ break;
+ default:
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static int
+cmd_expunge_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_expunge_box(ctx, info, ctx->search_args) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+void expunge_search_args_check(struct mail_search_args *args, const char *cmd)
+{
+ if (!expunge_search_args_is_mailbox_ok(args->args)) {
+ i_fatal_status(EX_USAGE,
+ "%s: To avoid accidents, search query "
+ "must contain MAILBOX in all search branches", cmd);
+ }
+ if (!expunge_search_args_is_msgset_ok(args->args)) {
+ i_fatal_status(EX_USAGE,
+ "%s: To avoid accidents, each branch in search query "
+ "must contain something else besides MAILBOX "
+ "(e.g. just add \"all\" if you want everything)", cmd);
+ }
+ mail_search_args_simplify(args);
+}
+
+static void cmd_expunge_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("expunge");
+
+ ctx->search_args = doveadm_mail_build_search_args(args);
+ expunge_search_args_check(ctx->search_args, "expunge");
+}
+
+static bool cmd_expunge_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct expunge_cmd_context *ctx = (struct expunge_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'd':
+ ctx->delete_empty_mailbox = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_expunge_alloc(void)
+{
+ struct expunge_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct expunge_cmd_context);
+ ctx->ctx.getopt_args = "d";
+ ctx->ctx.v.parse_arg = cmd_expunge_parse_arg;
+ ctx->ctx.v.init = cmd_expunge_init;
+ ctx->ctx.v.run = cmd_expunge_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_expunge_ver2 = {
+ .name = "expunge",
+ .mail_cmd = cmd_expunge_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-m] <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('d', "delete-empty-mailbox", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-fetch.c b/src/doveadm/doveadm-mail-fetch.c
new file mode 100644
index 0000000..932a54d
--- /dev/null
+++ b/src/doveadm/doveadm-mail-fetch.c
@@ -0,0 +1,683 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "message-address.h"
+#include "message-size.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "message-decoder.h"
+#include "imap-util.h"
+#include "mail-user.h"
+#include "mail-storage.h"
+#include "mail-search.h"
+#include "mail-namespace.h"
+#include "imap-msgpart.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+
+#include <stdio.h>
+
+struct fetch_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ struct mail *mail;
+
+ ARRAY(struct fetch_field) fields;
+ ARRAY_TYPE(const_string) header_fields;
+ enum mail_fetch_field wanted_fields;
+
+ const struct fetch_field *cur_field;
+ /* if print() returns -1, log this error if non-NULL. otherwise log
+ the storage error. */
+ const char *print_error;
+};
+
+struct fetch_field {
+ const char *name;
+ enum mail_fetch_field wanted_fields;
+ int (*print)(struct fetch_cmd_context *ctx);
+};
+
+static int fetch_user(struct fetch_cmd_context *ctx)
+{
+ doveadm_print(ctx->ctx.cur_mail_user->username);
+ return 0;
+}
+
+static int fetch_mailbox(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_MAILBOX_NAME, &value) < 0)
+ return -1;
+
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_mailbox_guid(struct fetch_cmd_context *ctx)
+{
+ struct mailbox_metadata metadata;
+
+ if (mailbox_get_metadata(ctx->mail->box, MAILBOX_METADATA_GUID,
+ &metadata) < 0)
+ return -1;
+ doveadm_print(guid_128_to_string(metadata.guid));
+ return 0;
+}
+
+static int fetch_seq(struct fetch_cmd_context *ctx)
+{
+ doveadm_print_num(ctx->mail->seq);
+ return 0;
+}
+
+static int fetch_uid(struct fetch_cmd_context *ctx)
+{
+ doveadm_print_num(ctx->mail->uid);
+ return 0;
+}
+
+static int fetch_guid(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_GUID, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_flags(struct fetch_cmd_context *ctx)
+{
+ string_t *str = t_str_new(64);
+
+ imap_write_flags(str, mail_get_flags(ctx->mail),
+ mail_get_keywords(ctx->mail));
+ doveadm_print(str_c(str));
+ return 0;
+}
+
+static int fetch_modseq(struct fetch_cmd_context *ctx)
+{
+ doveadm_print_num(mail_get_modseq(ctx->mail));
+ return 0;
+}
+
+static void
+fetch_set_istream_error(struct fetch_cmd_context *ctx, struct istream *input)
+{
+ ctx->print_error = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+}
+
+static int fetch_hdr(struct fetch_cmd_context *ctx)
+{
+ struct istream *input;
+ struct message_size hdr_size;
+ int ret;
+
+ if (mail_get_hdr_stream(ctx->mail, &hdr_size, &input) < 0)
+ return -1;
+
+ input = i_stream_create_limit(input, hdr_size.physical_size);
+ if ((ret = doveadm_print_istream(input)) < 0)
+ fetch_set_istream_error(ctx, input);
+ i_stream_unref(&input);
+ return ret;
+}
+
+static int fetch_hdr_field(struct fetch_cmd_context *ctx)
+{
+ const char *const *value, *filter, *name = ctx->cur_field->name;
+ string_t *str = t_str_new(256);
+ bool add_lf = FALSE;
+
+ filter = strchr(name, '.');
+ if (filter != NULL)
+ name = t_strdup_until(name, filter++);
+
+ if (filter != NULL && strcmp(filter, "utf8") == 0) {
+ if (mail_get_headers_utf8(ctx->mail, name, &value) < 0)
+ return -1;
+ } else {
+ if (mail_get_headers(ctx->mail, name, &value) < 0)
+ return -1;
+ }
+
+ for (; *value != NULL; value++) {
+ if (add_lf)
+ str_append_c(str, '\n');
+ str_append(str, *value);
+ add_lf = TRUE;
+ }
+
+ if (filter == NULL || strcmp(filter, "utf8") == 0) {
+ /* print the header as-is */
+ } else if (strcmp(filter, "address") == 0 ||
+ strcmp(filter, "address_name") == 0 ||
+ strcmp(filter, "address_name.utf8") == 0) {
+ struct message_address *addr;
+
+ addr = message_address_parse(pool_datastack_create(),
+ str_data(str), str_len(str),
+ UINT_MAX, 0);
+ str_truncate(str, 0);
+ add_lf = FALSE;
+ for (; addr != NULL; addr = addr->next) {
+ if (add_lf)
+ str_append_c(str, '\n');
+ if (strcmp(filter, "address") == 0) {
+ if (addr->mailbox != NULL)
+ str_append(str, addr->mailbox);
+ if (addr->domain != NULL) {
+ str_append_c(str, '@');
+ str_append(str, addr->domain);
+ }
+ } else if (addr->name != NULL) {
+ if (strcmp(filter, "address_name") == 0)
+ str_append(str, addr->name);
+ else {
+ message_header_decode_utf8(
+ (const void *)addr->name,
+ strlen(addr->name), str, NULL);
+ }
+ }
+ add_lf = TRUE;
+ }
+ } else {
+ i_fatal("Unknown header filter: %s", filter);
+ }
+ doveadm_print(str_c(str));
+ return 0;
+}
+
+static int fetch_body_field(struct fetch_cmd_context *ctx)
+{
+ const char *name = ctx->cur_field->name;
+ struct imap_msgpart *msgpart;
+ struct imap_msgpart_open_result result;
+ bool binary;
+ int ret;
+
+ binary = str_begins(name, "binary.");
+ name += binary ? 7 : 5;
+ if (imap_msgpart_parse(name, &msgpart) < 0)
+ i_unreached(); /* we already verified this was ok */
+ if (binary)
+ imap_msgpart_set_decode_to_binary(msgpart);
+
+ if (imap_msgpart_open(ctx->mail, msgpart, &result) < 0) {
+ imap_msgpart_free(&msgpart);
+ return -1;
+ }
+ if ((ret = doveadm_print_istream(result.input)) < 0)
+ fetch_set_istream_error(ctx, result.input);
+ i_stream_unref(&result.input);
+ imap_msgpart_free(&msgpart);
+ return ret;
+}
+
+static int fetch_body(struct fetch_cmd_context *ctx)
+{
+ struct istream *input;
+ struct message_size hdr_size;
+ int ret;
+
+ if (mail_get_stream(ctx->mail, &hdr_size, NULL, &input) < 0)
+ return -1;
+
+ i_stream_skip(input, hdr_size.physical_size);
+ if ((ret = doveadm_print_istream(input)) < 0)
+ fetch_set_istream_error(ctx, input);
+ return ret;
+}
+
+static int fetch_body_snippet(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_BODY_SNIPPET, &value) < 0)
+ return -1;
+ /* [0] contains the snippet algorithm, skip over it */
+ i_assert(value[0] != '\0');
+ doveadm_print(value + 1);
+ return 0;
+}
+
+static int fetch_text(struct fetch_cmd_context *ctx)
+{
+ struct istream *input;
+ int ret;
+
+ if (mail_get_stream(ctx->mail, NULL, NULL, &input) < 0)
+ return -1;
+ if ((ret = doveadm_print_istream(input)) < 0)
+ fetch_set_istream_error(ctx, input);
+ return ret;
+}
+
+static int fetch_text_utf8(struct fetch_cmd_context *ctx)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE,
+ };
+ struct istream *input;
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_block raw_block, block;
+ struct message_part *parts;
+ int ret = 0;
+
+ if (mail_get_stream(ctx->mail, NULL, NULL, &input) < 0)
+ return -1;
+
+ 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 (!message_decoder_decode_next_block(decoder, &raw_block,
+ &block))
+ continue;
+
+ if (block.hdr == NULL) {
+ if (block.size > 0)
+ doveadm_print_stream(block.data, block.size);
+ } else if (block.hdr->eoh)
+ doveadm_print_stream("\n", 1);
+ else {
+ i_assert(block.hdr->name_len > 0);
+ doveadm_print_stream(block.hdr->name,
+ block.hdr->name_len);
+ doveadm_print_stream(": ", 2);
+ if (block.hdr->full_value_len > 0) {
+ doveadm_print_stream(block.hdr->full_value,
+ block.hdr->full_value_len);
+ }
+ doveadm_print_stream("\n", 1);
+ }
+ }
+ i_assert(ret != 0);
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+
+ doveadm_print_stream("", 0);
+ if (input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ return -1;
+ }
+ return 0;
+}
+
+static int fetch_size_physical(struct fetch_cmd_context *ctx)
+{
+ uoff_t size;
+
+ if (mail_get_physical_size(ctx->mail, &size) < 0)
+ return -1;
+ doveadm_print_num(size);
+ return 0;
+}
+
+static int fetch_size_virtual(struct fetch_cmd_context *ctx)
+{
+ uoff_t size;
+
+ if (mail_get_virtual_size(ctx->mail, &size) < 0)
+ return -1;
+ doveadm_print_num(size);
+ return 0;
+}
+
+static int fetch_date_received(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+
+ if (mail_get_received_date(ctx->mail, &t) < 0)
+ return -1;
+ doveadm_print(unixdate2str(t));
+ return 0;
+}
+
+static int fetch_date_sent(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+ int tz;
+ char chr;
+
+ if (mail_get_date(ctx->mail, &t, &tz) < 0)
+ return -1;
+
+ chr = tz < 0 ? '-' : '+';
+ if (tz < 0) tz = -tz;
+ doveadm_print(t_strdup_printf("%s (%c%02u%02u)", unixdate2str(t),
+ chr, tz/60, tz%60));
+ return 0;
+}
+
+static int fetch_date_saved(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+
+ if (mail_get_save_date(ctx->mail, &t) < 0)
+ return -1;
+ doveadm_print(unixdate2str(t));
+ return 0;
+}
+
+static int fetch_date_received_unixtime(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+
+ if (mail_get_received_date(ctx->mail, &t) < 0)
+ return -1;
+ doveadm_print(dec2str(t));
+ return 0;
+}
+
+static int fetch_date_sent_unixtime(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+ int tz;
+
+ if (mail_get_date(ctx->mail, &t, &tz) < 0)
+ return -1;
+
+ doveadm_print(dec2str(t));
+ return 0;
+}
+
+static int fetch_date_saved_unixtime(struct fetch_cmd_context *ctx)
+{
+ time_t t;
+
+ if (mail_get_save_date(ctx->mail, &t) < 0)
+ return -1;
+ doveadm_print(dec2str(t));
+ return 0;
+}
+
+static int fetch_imap_envelope(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_IMAP_ENVELOPE, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_imap_body(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_IMAP_BODY, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_imap_bodystructure(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_IMAP_BODYSTRUCTURE, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+static int fetch_pop3_uidl(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_UIDL_BACKEND, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_pop3_order(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_POP3_ORDER, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_refcount(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_REFCOUNT, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static int fetch_storageid(struct fetch_cmd_context *ctx)
+{
+ const char *value;
+
+ if (mail_get_special(ctx->mail, MAIL_FETCH_STORAGE_ID, &value) < 0)
+ return -1;
+ doveadm_print(value);
+ return 0;
+}
+
+static const struct fetch_field fetch_fields[] = {
+ { "user", 0, fetch_user },
+ { "mailbox", 0, fetch_mailbox },
+ { "mailbox-guid", 0, fetch_mailbox_guid },
+ { "seq", 0, fetch_seq },
+ { "uid", 0, fetch_uid },
+ { "guid", 0, fetch_guid },
+ { "flags", MAIL_FETCH_FLAGS, fetch_flags },
+ { "modseq", 0, fetch_modseq },
+ { "hdr", MAIL_FETCH_STREAM_HEADER, fetch_hdr },
+ { "body", MAIL_FETCH_STREAM_BODY, fetch_body },
+ { "body.preview", MAIL_FETCH_BODY_SNIPPET, fetch_body_snippet },
+ { "body.snippet", MAIL_FETCH_BODY_SNIPPET, fetch_body_snippet },
+ { "text", MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY, fetch_text },
+ { "text.utf8", MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY, fetch_text_utf8 },
+ { "size.physical", MAIL_FETCH_PHYSICAL_SIZE, fetch_size_physical },
+ { "size.virtual", MAIL_FETCH_VIRTUAL_SIZE, fetch_size_virtual },
+ { "date.received", MAIL_FETCH_RECEIVED_DATE, fetch_date_received },
+ { "date.sent", MAIL_FETCH_DATE, fetch_date_sent },
+ { "date.saved", MAIL_FETCH_SAVE_DATE, fetch_date_saved },
+ { "date.received.unixtime", MAIL_FETCH_RECEIVED_DATE, fetch_date_received_unixtime },
+ { "date.sent.unixtime", MAIL_FETCH_DATE, fetch_date_sent_unixtime },
+ { "date.saved.unixtime", MAIL_FETCH_SAVE_DATE, fetch_date_saved_unixtime },
+ { "imap.envelope", MAIL_FETCH_IMAP_ENVELOPE, fetch_imap_envelope },
+ { "imap.body", MAIL_FETCH_IMAP_BODY, fetch_imap_body },
+ { "imap.bodystructure", MAIL_FETCH_IMAP_BODYSTRUCTURE, fetch_imap_bodystructure },
+ { "pop3.uidl", MAIL_FETCH_UIDL_BACKEND, fetch_pop3_uidl },
+ { "pop3.order", MAIL_FETCH_POP3_ORDER, fetch_pop3_order },
+ { "refcount", MAIL_FETCH_REFCOUNT, fetch_refcount },
+ { "storageid", MAIL_FETCH_STORAGE_ID, fetch_storageid }
+};
+
+static const struct fetch_field *fetch_field_find(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(fetch_fields); i++) {
+ if (strcmp(fetch_fields[i].name, name) == 0)
+ return &fetch_fields[i];
+ }
+ return NULL;
+}
+
+static void print_fetch_fields(void)
+{
+ unsigned int i;
+
+ fprintf(stderr, "Available fetch fields: hdr.<name> body.<section> binary.<section> %s", fetch_fields[0].name);
+ for (i = 1; i < N_ELEMENTS(fetch_fields); i++)
+ fprintf(stderr, " %s", fetch_fields[i].name);
+ fprintf(stderr, "\n");
+}
+
+static void parse_fetch_fields(struct fetch_cmd_context *ctx, const char *str)
+{
+ const char *const *fields, *name;
+ const struct fetch_field *field;
+ struct fetch_field hdr_field, body_field;
+ struct imap_msgpart *msgpart;
+
+ i_zero(&hdr_field);
+ hdr_field.print = fetch_hdr_field;
+
+ i_zero(&body_field);
+ body_field.print = fetch_body_field;
+
+ t_array_init(&ctx->fields, 32);
+ t_array_init(&ctx->header_fields, 32);
+ fields = t_strsplit_spaces(str, " ");
+ for (; *fields != NULL; fields++) {
+ name = t_str_lcase(*fields);
+
+ doveadm_print_header_simple(name);
+ if ((field = fetch_field_find(name)) != NULL) {
+ ctx->wanted_fields |= field->wanted_fields;
+ array_push_back(&ctx->fields, field);
+ } else if (str_begins(name, "hdr.")) {
+ name += 4;
+ hdr_field.name = name;
+ array_push_back(&ctx->fields, &hdr_field);
+ name = t_strcut(name, '.');
+ array_push_back(&ctx->header_fields, &name);
+ } else if (str_begins(name, "body.") ||
+ str_begins(name, "binary.")) {
+ bool binary = str_begins(name, "binary.");
+ body_field.name = t_strarray_join(t_strsplit(name, ","), " ");
+
+ name += binary ? 7 : 5;
+ if (imap_msgpart_parse(name, &msgpart) < 0) {
+ print_fetch_fields();
+ i_fatal("Unknown fetch section: %s", name);
+ }
+ array_push_back(&ctx->fields, &body_field);
+ ctx->wanted_fields |= imap_msgpart_get_fetch_data(msgpart);
+ imap_msgpart_free(&msgpart);
+ } else {
+ print_fetch_fields();
+ i_fatal("Unknown fetch field: %s", name);
+ }
+ }
+ array_append_zero(&ctx->header_fields);
+}
+
+static int cmd_fetch_mail(struct fetch_cmd_context *ctx)
+{
+ const struct fetch_field *field;
+ struct mail *mail = ctx->mail;
+ int ret = 0;
+
+ array_foreach(&ctx->fields, field) {
+ ctx->cur_field = field;
+ if (field->print(ctx) < 0) {
+ i_error("fetch(%s) failed for box=%s uid=%u: %s",
+ field->name, mailbox_get_vname(mail->box),
+ mail->uid,
+ ctx->print_error != NULL ? ctx->print_error :
+ mailbox_get_last_internal_error(mail->box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, mail->box);
+ ctx->print_error = NULL;
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int
+cmd_fetch_box(struct fetch_cmd_context *ctx, const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(&ctx->ctx, info, ctx->ctx.search_args,
+ ctx->wanted_fields,
+ array_front(&ctx->header_fields),
+ DOVEADM_MAIL_ITER_FLAG_STOP_WITH_CLIENT,
+ &iter) < 0)
+ return -1;
+
+ while (doveadm_mail_iter_next(iter, &ctx->mail)) {
+ T_BEGIN {
+ if (cmd_fetch_mail(ctx) < 0)
+ ret = -1;
+ } T_END;
+ }
+ if (doveadm_mail_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int
+cmd_fetch_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct fetch_cmd_context *ctx = (struct fetch_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(_ctx, user, _ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_fetch_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_fetch_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct fetch_cmd_context *ctx = (struct fetch_cmd_context *)_ctx;
+ const char *fetch_fields = args[0];
+
+ if (fetch_fields == NULL || args[1] == NULL)
+ doveadm_mail_help_name("fetch");
+
+ parse_fetch_fields(ctx, fetch_fields);
+ _ctx->search_args = doveadm_mail_build_search_args(args + 1);
+}
+
+static struct doveadm_mail_cmd_context *cmd_fetch_alloc(void)
+{
+ struct fetch_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct fetch_cmd_context);
+ ctx->ctx.v.init = cmd_fetch_init;
+ ctx->ctx.v.run = cmd_fetch_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_fetch_ver2 = {
+ .name = "fetch",
+ .mail_cmd = cmd_fetch_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<fields> <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "field", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('\0', "fieldstr", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL | CMD_PARAM_FLAG_DO_NOT_EXPOSE) /* FIXME: horrible hack, remove me when possible */
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-flags.c b/src/doveadm/doveadm-mail-flags.c
new file mode 100644
index 0000000..7c252a7
--- /dev/null
+++ b/src/doveadm/doveadm-mail-flags.c
@@ -0,0 +1,178 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-util.h"
+#include "mail-storage.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+struct flags_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ enum modify_type modify_type;
+ enum mail_flags flags;
+ const char *const *keywords;
+};
+
+static int
+cmd_flags_run_box(struct flags_cmd_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox *box;
+ struct mail *mail;
+ struct mail_keywords *kw = NULL;
+
+ if (doveadm_mail_iter_init(&ctx->ctx, info, ctx->ctx.search_args,
+ 0, NULL, 0, &iter) < 0)
+ return -1;
+ box = doveadm_mail_iter_get_mailbox(iter);
+
+ if (ctx->keywords != NULL) {
+ if (mailbox_keywords_create(box, ctx->keywords, &kw) < 0) {
+ i_error("Invalid keywords: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ (void)doveadm_mail_iter_deinit(&iter);
+ ctx->ctx.exit_code = DOVEADM_EX_NOTPOSSIBLE;
+ return -1;
+ }
+ }
+
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ mail_update_flags(mail, ctx->modify_type, ctx->flags);
+ if (kw != NULL)
+ mail_update_keywords(mail, ctx->modify_type, kw);
+ }
+ if (kw != NULL)
+ mailbox_keywords_unref(&kw);
+ return doveadm_mail_iter_deinit_sync(&iter);
+}
+
+static int
+cmd_flags_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct flags_cmd_context *ctx = (struct flags_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(_ctx, user, _ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_flags_run_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_flags_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct flags_cmd_context *ctx = (struct flags_cmd_context *)_ctx;
+ const char *const *tmp;
+ enum mail_flags flag;
+ ARRAY_TYPE(const_string) keywords;
+
+ if (args[0] == NULL || args[1] == NULL) {
+ switch (ctx->modify_type) {
+ case MODIFY_ADD:
+ doveadm_mail_help_name("flags add");
+ case MODIFY_REMOVE:
+ doveadm_mail_help_name("flags remove");
+ case MODIFY_REPLACE:
+ doveadm_mail_help_name("flags replace");
+ }
+ i_unreached();
+ }
+
+ p_array_init(&keywords, _ctx->pool, 8);
+ for (tmp = t_strsplit(args[0], " "); *tmp != NULL; tmp++) {
+ const char *str = *tmp;
+
+ if (str[0] == '\\') {
+ flag = imap_parse_system_flag(str);
+ if (flag == 0)
+ i_fatal("Invalid system flag: %s", str);
+ ctx->flags |= flag;
+ } else {
+ str = p_strdup(_ctx->pool, str);
+ array_push_back(&keywords, &str);
+ }
+ }
+ if (array_count(&keywords) > 0 || ctx->modify_type == MODIFY_REPLACE) {
+ array_append_zero(&keywords);
+ ctx->keywords = array_front(&keywords);
+ }
+
+ _ctx->search_args = doveadm_mail_build_search_args(args+1);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_flag_alloc(enum modify_type modify_type)
+{
+ struct flags_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct flags_cmd_context);
+ ctx->modify_type = modify_type;
+ ctx->ctx.v.init = cmd_flags_init;
+ ctx->ctx.v.run = cmd_flags_run;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_flags_add_alloc(void)
+{
+ return cmd_flag_alloc(MODIFY_ADD);
+}
+
+static struct doveadm_mail_cmd_context *cmd_flags_remove_alloc(void)
+{
+ return cmd_flag_alloc(MODIFY_REMOVE);
+}
+
+static struct doveadm_mail_cmd_context *cmd_flags_replace_alloc(void)
+{
+ return cmd_flag_alloc(MODIFY_REPLACE);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_flags_add_ver2 = {
+ .name = "flags add",
+ .mail_cmd = cmd_flags_add_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<flags> <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "flag", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('\0', "flagstr", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL|CMD_PARAM_FLAG_DO_NOT_EXPOSE)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_flags_remove_ver2 = {
+ .name = "flags remove",
+ .mail_cmd = cmd_flags_remove_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<flags> <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "flag", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('\0', "flagstr", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL|CMD_PARAM_FLAG_DO_NOT_EXPOSE)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_flags_replace_ver2 = {
+ .name = "flags replace",
+ .mail_cmd = cmd_flags_replace_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<flags> <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "flag", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('\0', "flagstr", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL|CMD_PARAM_FLAG_DO_NOT_EXPOSE)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-import.c b/src/doveadm/doveadm-mail-import.c
new file mode 100644
index 0000000..bd6ef6d
--- /dev/null
+++ b/src/doveadm/doveadm-mail-import.c
@@ -0,0 +1,276 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+struct import_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ const char *src_location;
+ const char *src_username;
+ struct mail_user *src_user;
+ const char *dest_parent;
+ bool subscribe;
+};
+
+static const char *
+convert_vname_separators(const char *vname, char src_sep, char dest_sep)
+{
+ string_t *str = t_str_new(128);
+
+ for (; *vname != '\0'; vname++) {
+ if (*vname == src_sep)
+ str_append_c(str, dest_sep);
+ else if (*vname == dest_sep)
+ str_append_c(str, '_');
+ else
+ str_append_c(str, *vname);
+ }
+ return str_c(str);
+}
+
+static int
+dest_mailbox_open_or_create(struct import_cmd_context *ctx,
+ struct mail_user *user,
+ const struct mailbox_info *info,
+ struct mailbox **box_r)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ enum mail_error error;
+ const char *name, *errstr;
+
+ if (*ctx->dest_parent != '\0') {
+ /* prefix destination mailbox name with given parent mailbox */
+ ns = mail_namespace_find(user->namespaces, ctx->dest_parent);
+ } else {
+ ns = mail_namespace_find(user->namespaces, info->vname);
+ }
+ name = convert_vname_separators(info->vname,
+ mail_namespace_get_sep(info->ns),
+ mail_namespace_get_sep(ns));
+
+ if (*ctx->dest_parent != '\0') {
+ name = t_strdup_printf("%s%c%s", ctx->dest_parent,
+ mail_namespace_get_sep(ns), name);
+ }
+
+ box = mailbox_alloc(ns->list, name, MAILBOX_FLAG_SAVEONLY);
+ if (mailbox_create(box, NULL, FALSE) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_EXISTS) {
+ i_error("Couldn't create mailbox %s: %s", name, errstr);
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+ }
+ if (ctx->subscribe) {
+ if (mailbox_set_subscribed(box, TRUE) < 0) {
+ i_error("Couldn't subscribe to mailbox %s: %s",
+ name, mailbox_get_last_internal_error(box, NULL));
+ }
+ }
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("Syncing mailbox %s failed: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+ *box_r = box;
+ return 0;
+}
+
+static int
+cmd_import_box_contents(struct doveadm_mail_cmd_context *ctx,
+ struct doveadm_mail_iter *iter, struct mail *src_mail,
+ struct mailbox *dest_box)
+{
+ struct mail_save_context *save_ctx;
+ struct mailbox_transaction_context *dest_trans;
+ const char *mailbox = mailbox_get_vname(dest_box);
+ int ret = 0;
+
+ dest_trans = mailbox_transaction_begin(dest_box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ ctx->transaction_flags, __func__);
+ do {
+ if (doveadm_debug) {
+ i_debug("import: box=%s uid=%u",
+ mailbox, src_mail->uid);
+ }
+ save_ctx = mailbox_save_alloc(dest_trans);
+ mailbox_save_copy_flags(save_ctx, src_mail);
+ if (mailbox_copy(&save_ctx, src_mail) < 0) {
+ i_error("Copying box=%s uid=%u failed: %s",
+ mailbox, src_mail->uid,
+ mailbox_get_last_internal_error(dest_box, NULL));
+ ret = -1;
+ }
+ } while (doveadm_mail_iter_next(iter, &src_mail));
+
+ if (mailbox_transaction_commit(&dest_trans) < 0) {
+ i_error("Committing copied mails to %s failed: %s", mailbox,
+ mailbox_get_last_internal_error(dest_box, NULL));
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+cmd_import_box(struct import_cmd_context *ctx, struct mail_user *dest_user,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox *box;
+ struct mail *mail;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(&ctx->ctx, info, search_args, 0, NULL,
+ DOVEADM_MAIL_ITER_FLAG_READONLY,
+ &iter) < 0)
+ return -1;
+
+ if (doveadm_mail_iter_next(iter, &mail)) {
+ /* at least one mail matches in this mailbox */
+ if (dest_mailbox_open_or_create(ctx, dest_user, info, &box) < 0)
+ ret = -1;
+ else {
+ if (cmd_import_box_contents(&ctx->ctx, iter, mail, box) < 0) {
+ doveadm_mail_failed_mailbox(&ctx->ctx, mail->box);
+ ret = -1;
+ }
+ mailbox_free(&box);
+ }
+ }
+ if (doveadm_mail_iter_deinit_sync(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_import_init_source_user(struct import_cmd_context *ctx, struct mail_user *dest_user)
+{
+ struct mail_storage_service_input input;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ const char *error;
+
+ /* create a user for accessing the source storage */
+ i_zero(&input);
+ input.module = "mail";
+ input.username = ctx->src_username != NULL ?
+ ctx->src_username :
+ dest_user->username;
+
+ mail_storage_service_io_deactivate_user(ctx->ctx.cur_service_user);
+ input.flags_override_add = MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES |
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS;
+ if (mail_storage_service_lookup_next(ctx->ctx.storage_service, &input,
+ &service_user, &user, &error) < 0)
+ i_fatal("Import user initialization failed: %s", error);
+ if (mail_namespaces_init_location(user, ctx->src_location, &error) < 0)
+ i_fatal("Import namespace initialization failed: %s", error);
+
+ ctx->src_user = user;
+ mail_storage_service_io_deactivate_user(service_user);
+ mail_storage_service_user_unref(&service_user);
+ mail_storage_service_io_activate_user(ctx->ctx.cur_service_user);
+}
+
+static int
+cmd_import_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct import_cmd_context *ctx = (struct import_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ if (ctx->src_user == NULL)
+ cmd_import_init_source_user(ctx, user);
+
+ iter = doveadm_mailbox_list_iter_init(_ctx, ctx->src_user,
+ _ctx->search_args, iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_import_box(ctx, user, info, _ctx->search_args) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_import_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct import_cmd_context *ctx = (struct import_cmd_context *)_ctx;
+
+ if (str_array_length(args) < 3)
+ doveadm_mail_help_name("import");
+ ctx->src_location = p_strdup(_ctx->pool, args[0]);
+ ctx->dest_parent = p_strdup(_ctx->pool, args[1]);
+ ctx->ctx.search_args = doveadm_mail_build_search_args(args+2);
+}
+
+static void cmd_import_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct import_cmd_context *ctx = (struct import_cmd_context *)_ctx;
+
+ if (ctx->src_user != NULL)
+ mail_user_deinit(&ctx->src_user);
+}
+
+static bool cmd_import_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct import_cmd_context *ctx = (struct import_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'U':
+ ctx->src_username = p_strdup(_ctx->pool, optarg);
+ break;
+ case 's':
+ ctx->subscribe = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_import_alloc(void)
+{
+ struct import_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct import_cmd_context);
+ ctx->ctx.getopt_args = "s";
+ ctx->ctx.v.parse_arg = cmd_import_parse_arg;
+ ctx->ctx.v.init = cmd_import_init;
+ ctx->ctx.v.deinit = cmd_import_deinit;
+ ctx->ctx.v.run = cmd_import_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_import_ver2 = {
+ .name = "import",
+ .mail_cmd = cmd_import_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-U source-user] [-s] <source mail location> <dest parent mailbox> <search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('U', "source-user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('s', "subscribe", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "source-location", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "dest-parent-mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-index.c b/src/doveadm/doveadm-mail-index.c
new file mode 100644
index 0000000..524f373
--- /dev/null
+++ b/src/doveadm/doveadm-mail-index.c
@@ -0,0 +1,300 @@
+/* Copyright (c) 2010-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-namespace.h"
+#include "mail-storage.h"
+#include "mail-search-build.h"
+#include "mailbox-list-iter.h"
+#include "doveadm-settings.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+#define INDEXER_SOCKET_NAME "indexer"
+#define INDEXER_HANDSHAKE "VERSION\tindexer\t1\t0\n"
+
+struct index_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ int queue_fd;
+ unsigned int max_recent_msgs;
+ bool queue:1;
+ bool have_wildcards:1;
+};
+
+static int cmd_index_box_precache(struct doveadm_mail_cmd_context *dctx,
+ struct mailbox *box)
+{
+ struct mailbox_status status;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct mailbox_metadata metadata;
+ uint32_t seq;
+ unsigned int counter = 0, max;
+ int ret = 0;
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_PRECACHE_FIELDS,
+ &metadata) < 0) {
+ i_error("Mailbox %s: Precache-fields lookup failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ if (mailbox_get_status(box, STATUS_MESSAGES | STATUS_LAST_CACHED_SEQ,
+ &status) < 0) {
+ i_error("Mailbox %s: Status lookup failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+
+ seq = status.last_cached_seq + 1;
+ if (seq > status.messages) {
+ if (doveadm_verbose) {
+ i_info("%s: Cache is already up to date",
+ mailbox_get_vname(box));
+ }
+ return 0;
+ }
+ if (doveadm_verbose) {
+ i_info("%s: Caching mails seq=%u..%u",
+ mailbox_get_vname(box), seq, status.messages);
+ }
+
+ trans = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_NO_CACHE_DEC |
+ dctx->transaction_flags, __func__);
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq, status.messages);
+ ctx = mailbox_search_init(trans, search_args, NULL,
+ metadata.precache_fields, NULL);
+ mail_search_args_unref(&search_args);
+
+ max = status.messages - seq + 1;
+ while (mailbox_search_next(ctx, &mail)) {
+ if (mail_precache(mail) < 0) {
+ i_error("Mailbox %s: Precache for UID=%u failed: %s",
+ mailbox_get_vname(box), mail->uid,
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ break;
+ }
+ if (doveadm_verbose && ++counter % 100 == 0) {
+ printf("\r%u/%u", counter, max);
+ fflush(stdout);
+ }
+ }
+ if (doveadm_verbose)
+ printf("\r%u/%u\n", counter, max);
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_error("Mailbox %s: Mail search failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ if (mailbox_transaction_commit(&trans) < 0) {
+ i_error("Mailbox %s: Transaction commit failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+cmd_index_box(struct index_cmd_context *ctx, const struct mailbox_info *info)
+{
+ struct mailbox *box;
+ struct mailbox_status status;
+ int ret = 0;
+
+ box = mailbox_alloc(info->ns->list, info->vname,
+ MAILBOX_FLAG_IGNORE_ACLS);
+ if (ctx->max_recent_msgs != 0) {
+ /* index only if there aren't too many recent messages.
+ don't bother syncing the mailbox, that alone can take a
+ while with large maildirs. */
+ if (mailbox_open(box) < 0) {
+ i_error("Opening mailbox %s failed: %s", info->vname,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+
+ mailbox_get_open_status(box, STATUS_RECENT, &status);
+ if (status.recent > ctx->max_recent_msgs) {
+ mailbox_free(&box);
+ return 0;
+ }
+ }
+
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("Syncing mailbox %s failed: %s", info->vname,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ ret = -1;
+ } else {
+ if (cmd_index_box_precache(&ctx->ctx, box) < 0) {
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ ret = -1;
+ }
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static void index_queue_connect(struct index_cmd_context *ctx)
+{
+ const char *path;
+
+ path = t_strconcat(doveadm_settings->base_dir,
+ "/"INDEXER_SOCKET_NAME, NULL);
+ ctx->queue_fd = net_connect_unix(path);
+ if (ctx->queue_fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ if (write_full(ctx->queue_fd, INDEXER_HANDSHAKE,
+ strlen(INDEXER_HANDSHAKE)) < 0)
+ i_fatal("write(indexer) failed: %m");
+}
+
+static void cmd_index_queue(struct index_cmd_context *ctx,
+ struct mail_user *user, const char *mailbox)
+{
+ if (ctx->queue_fd == -1)
+ index_queue_connect(ctx);
+ i_assert(ctx->queue_fd != -1);
+
+ T_BEGIN {
+ string_t *str = t_str_new(256);
+
+ str_append(str, "APPEND\t0\t");
+ str_append_tabescaped(str, user->username);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, mailbox);
+ str_printfa(str, "\t%u\n", ctx->max_recent_msgs);
+ if (write_full(ctx->queue_fd, str_data(str), str_len(str)) < 0)
+ i_fatal("write(indexer) failed: %m");
+ } T_END;
+}
+
+static int
+cmd_index_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct index_cmd_context *ctx = (struct index_cmd_context *)_ctx;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS |
+ MAILBOX_LIST_ITER_STAR_WITHIN_NS;
+ const enum mail_namespace_type ns_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ unsigned int i;
+ int ret = 0;
+
+ if (ctx->queue && !ctx->have_wildcards) {
+ /* we can do this quickly without going through the mailboxes */
+ for (i = 0; _ctx->args[i] != NULL; i++)
+ cmd_index_queue(ctx, user, _ctx->args[i]);
+ return 0;
+ }
+
+ iter = mailbox_list_iter_init_namespaces(user->namespaces, _ctx->args,
+ ns_mask, iter_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags & (MAILBOX_NOSELECT |
+ MAILBOX_NONEXISTENT)) == 0) T_BEGIN {
+ if (ctx->queue)
+ cmd_index_queue(ctx, user, info->vname);
+ else {
+ if (cmd_index_box(ctx, info) < 0)
+ ret = -1;
+ }
+ } T_END;
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ i_error("Listing mailboxes failed: %s",
+ mailbox_list_get_last_internal_error(user->namespaces->list, NULL));
+ doveadm_mail_failed_error(_ctx, MAIL_ERROR_TEMP);
+ ret = -1;
+ }
+ return ret;
+}
+
+static void cmd_index_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct index_cmd_context *ctx = (struct index_cmd_context *)_ctx;
+ unsigned int i;
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("index");
+ for (i = 0; args[i] != NULL; i++) {
+ if (strchr(args[i], '*') != NULL ||
+ strchr(args[i], '%') != NULL) {
+ ctx->have_wildcards = TRUE;
+ break;
+ }
+ }
+}
+
+static void cmd_index_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct index_cmd_context *ctx = (struct index_cmd_context *)_ctx;
+
+ if (ctx->queue_fd != -1) {
+ net_disconnect(ctx->queue_fd);
+ ctx->queue_fd = -1;
+ }
+}
+
+static bool
+cmd_index_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct index_cmd_context *ctx = (struct index_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'q':
+ ctx->queue = TRUE;
+ break;
+ case 'n':
+ if (str_to_uint(optarg, &ctx->max_recent_msgs) < 0) {
+ i_fatal_status(EX_USAGE,
+ "Invalid -n parameter number: %s", optarg);
+ }
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_index_alloc(void)
+{
+ struct index_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct index_cmd_context);
+ ctx->queue_fd = -1;
+ ctx->ctx.getopt_args = "qn:";
+ ctx->ctx.v.parse_arg = cmd_index_parse_arg;
+ ctx->ctx.v.init = cmd_index_init;
+ ctx->ctx.v.deinit = cmd_index_deinit;
+ ctx->ctx.v.run = cmd_index_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_index_ver2 = {
+ .name = "index",
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-q] [-n <max recent>] <mailbox mask>",
+ .mail_cmd = cmd_index_alloc,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('q',"queue",CMD_PARAM_BOOL,0)
+DOVEADM_CMD_PARAM('n',"max-recent",CMD_PARAM_STR,0)
+DOVEADM_CMD_PARAM('\0',"mailbox-mask",CMD_PARAM_STR,CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-iter.c b/src/doveadm/doveadm-mail-iter.c
new file mode 100644
index 0000000..eca5421
--- /dev/null
+++ b/src/doveadm/doveadm-mail-iter.c
@@ -0,0 +1,176 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "mail-search.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+#include "doveadm-mail-iter.h"
+
+struct doveadm_mail_iter {
+ struct doveadm_mail_cmd_context *ctx;
+ struct mail_search_args *search_args;
+ enum doveadm_mail_iter_flags flags;
+
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ struct mail_search_context *search_ctx;
+ bool killed;
+};
+
+int doveadm_mail_iter_init(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args,
+ enum mail_fetch_field wanted_fields,
+ const char *const *wanted_headers,
+ enum doveadm_mail_iter_flags flags,
+ struct doveadm_mail_iter **iter_r)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ const char *errstr;
+ enum mail_error error;
+
+ enum mailbox_flags readonly_flag =
+ (flags & DOVEADM_MAIL_ITER_FLAG_READONLY) != 0 ?
+ MAILBOX_FLAG_READONLY : 0;
+
+ iter = i_new(struct doveadm_mail_iter, 1);
+ iter->ctx = ctx;
+ iter->flags = flags;
+ iter->box = mailbox_alloc(info->ns->list, info->vname,
+ MAILBOX_FLAG_IGNORE_ACLS | readonly_flag);
+ iter->search_args = search_args;
+
+ if (mailbox_sync(iter->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ errstr = mailbox_get_last_internal_error(iter->box, &error);
+ if (error == MAIL_ERROR_NOTFOUND) {
+ /* just ignore this mailbox */
+ *iter_r = iter;
+ return 0;
+ }
+ i_error("Syncing mailbox %s failed: %s", info->vname, errstr);
+ doveadm_mail_failed_mailbox(ctx, iter->box);
+ mailbox_free(&iter->box);
+ i_free(iter);
+ return -1;
+ }
+
+ headers_ctx = wanted_headers == NULL || wanted_headers[0] == NULL ?
+ NULL : mailbox_header_lookup_init(iter->box, wanted_headers);
+
+ mail_search_args_init(search_args, iter->box, FALSE, NULL);
+ iter->t = mailbox_transaction_begin(iter->box, ctx->transaction_flags,
+ ctx->cmd->name);
+ iter->search_ctx = mailbox_search_init(iter->t, search_args, NULL,
+ wanted_fields, headers_ctx);
+ mailbox_header_lookup_unref(&headers_ctx);
+ *iter_r = iter;
+ return 0;
+}
+
+static int
+doveadm_mail_iter_deinit_transaction(struct doveadm_mail_iter *iter,
+ bool commit)
+{
+ int ret = 0;
+
+ if (iter->search_ctx != NULL) {
+ if (mailbox_search_deinit(&iter->search_ctx) < 0) {
+ i_error("Searching mailbox %s failed: %s",
+ mailbox_get_vname(iter->box),
+ mailbox_get_last_internal_error(iter->box, NULL));
+ ret = -1;
+ }
+ }
+ if (iter->t == NULL)
+ ;
+ else if (commit) {
+ if (mailbox_transaction_commit(&iter->t) < 0) {
+ i_error("Committing mailbox %s failed: %s",
+ mailbox_get_vname(iter->box),
+ mailbox_get_last_internal_error(iter->box, NULL));
+ ret = -1;
+ }
+ } else {
+ mailbox_transaction_rollback(&iter->t);
+ }
+ mail_search_args_deinit(iter->search_args);
+ return ret;
+}
+
+static int
+doveadm_mail_iter_deinit_full(struct doveadm_mail_iter **_iter,
+ bool sync, bool commit, bool keep_box)
+{
+ struct doveadm_mail_iter *iter = *_iter;
+ int ret;
+
+ *_iter = NULL;
+
+ ret = doveadm_mail_iter_deinit_transaction(iter, commit);
+ if (ret == 0 && sync) {
+ ret = mailbox_sync(iter->box, 0);
+ if (ret < 0) {
+ i_error("Mailbox %s: Mailbox sync failed: %s",
+ mailbox_get_vname(iter->box),
+ mailbox_get_last_internal_error(iter->box, NULL));
+ }
+ }
+ if (ret < 0)
+ doveadm_mail_failed_mailbox(iter->ctx, iter->box);
+ else if (iter->killed) {
+ iter->ctx->exit_code = EX_TEMPFAIL;
+ ret = -1;
+ }
+ if (!keep_box)
+ mailbox_free(&iter->box);
+ i_free(iter);
+ return ret;
+}
+
+int doveadm_mail_iter_deinit(struct doveadm_mail_iter **_iter)
+{
+ return doveadm_mail_iter_deinit_full(_iter, FALSE, TRUE, FALSE);
+}
+
+int doveadm_mail_iter_deinit_sync(struct doveadm_mail_iter **_iter)
+{
+ return doveadm_mail_iter_deinit_full(_iter, TRUE, TRUE, FALSE);
+}
+
+int doveadm_mail_iter_deinit_keep_box(struct doveadm_mail_iter **iter,
+ struct mailbox **box_r)
+{
+ *box_r = (*iter)->box;
+ return doveadm_mail_iter_deinit_full(iter, FALSE, TRUE, TRUE);
+}
+
+void doveadm_mail_iter_deinit_rollback(struct doveadm_mail_iter **_iter)
+{
+ (void)doveadm_mail_iter_deinit_full(_iter, FALSE, FALSE, FALSE);
+}
+
+bool doveadm_mail_iter_next(struct doveadm_mail_iter *iter,
+ struct mail **mail_r)
+{
+ if (iter->search_ctx == NULL)
+ return FALSE;
+ if (doveadm_is_killed()) {
+ iter->killed = TRUE;
+ return FALSE;
+ }
+ if ((iter->flags & DOVEADM_MAIL_ITER_FLAG_STOP_WITH_CLIENT) != 0 &&
+ doveadm_print_ostream->stream_errno != 0) {
+ iter->killed = TRUE;
+ return FALSE;
+ }
+ return mailbox_search_next(iter->search_ctx, mail_r);
+}
+
+struct mailbox *doveadm_mail_iter_get_mailbox(struct doveadm_mail_iter *iter)
+{
+ return iter->box;
+}
diff --git a/src/doveadm/doveadm-mail-iter.h b/src/doveadm/doveadm-mail-iter.h
new file mode 100644
index 0000000..32cc232
--- /dev/null
+++ b/src/doveadm/doveadm-mail-iter.h
@@ -0,0 +1,34 @@
+#ifndef DOVEADM_MAIL_ITER_H
+#define DOVEADM_MAIL_ITER_H
+
+#include "mailbox-list-iter.h"
+
+enum doveadm_mail_iter_flags {
+ /* Open the mailbox with MAILBOX_FLAG_READONLY */
+ DOVEADM_MAIL_ITER_FLAG_READONLY = BIT(0),
+ /* Stop the iteration if client is detected to be disconnected. */
+ DOVEADM_MAIL_ITER_FLAG_STOP_WITH_CLIENT = BIT(1),
+};
+
+struct doveadm_mail_iter;
+struct doveadm_mail_cmd_context;
+
+int doveadm_mail_iter_init(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info,
+ struct mail_search_args *search_args,
+ enum mail_fetch_field wanted_fields,
+ const char *const *wanted_headers,
+ enum doveadm_mail_iter_flags flags,
+ struct doveadm_mail_iter **iter_r) ATTR_NULL(6);
+int doveadm_mail_iter_deinit(struct doveadm_mail_iter **iter);
+int doveadm_mail_iter_deinit_sync(struct doveadm_mail_iter **iter);
+int doveadm_mail_iter_deinit_keep_box(struct doveadm_mail_iter **iter,
+ struct mailbox **box_r);
+void doveadm_mail_iter_deinit_rollback(struct doveadm_mail_iter **iter);
+struct mailbox *doveadm_mail_iter_get_mailbox(struct doveadm_mail_iter *iter);
+
+bool doveadm_mail_iter_next(struct doveadm_mail_iter *iter,
+ struct mail **mail_r);
+
+#endif
+
diff --git a/src/doveadm/doveadm-mail-mailbox-cache.c b/src/doveadm/doveadm-mail-mailbox-cache.c
new file mode 100644
index 0000000..718fca4
--- /dev/null
+++ b/src/doveadm/doveadm-mail-mailbox-cache.c
@@ -0,0 +1,440 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "time-util.h"
+#include "mail-index-private.h"
+#include "mail-cache-private.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "doveadm-print.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail.h"
+
+struct mailbox_cache_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ const char *const *boxes;
+ const char *const *fields;
+ uint64_t last_used;
+ enum mail_cache_decision_type decision;
+ bool all_fields;
+ bool set_decision;
+ bool set_last_used;
+ bool remove;
+};
+
+static int cmd_mailbox_cache_open_box(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ const char *boxname,
+ struct mailbox **box_r)
+{
+ struct mailbox *box = doveadm_mailbox_find(user, boxname);
+
+ if (mailbox_open(box) < 0 || mailbox_sync(box, 0) < 0) {
+ i_error("Cannot open mailbox %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+
+ *box_r = box;
+
+ return 0;
+}
+
+static void cmd_mailbox_cache_decision_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+ const char *fields;
+
+ doveadm_print_header("mailbox", "mailbox", DOVEADM_PRINT_HEADER_FLAG_STICKY);
+ doveadm_print_header_simple("field");
+ doveadm_print_header_simple("decision");
+ doveadm_print_header_simple("last-used");
+
+ if (!ctx->all_fields &&
+ !doveadm_cmd_param_str(_ctx->cctx, "fieldstr", &fields)) {
+ i_fatal("Missing fields parameter");
+ } else if (!ctx->all_fields) {
+ ctx->fields = t_strsplit_spaces(fields, ", ");
+ }
+
+ ctx->boxes = args;
+}
+
+static bool
+cmd_mailbox_cache_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+
+ switch(c) {
+ case 'a':
+ ctx->all_fields = TRUE;
+ return TRUE;
+ /* this is handled in doveadm-mail as 'fieldstr' field */
+ case 'f':
+ return TRUE;
+ case 'l':
+ if (str_to_uint64(optarg, &ctx->last_used) < 0) {
+ i_error("Invalid last-used '%s': not a number", optarg);
+ return FALSE;
+ }
+ ctx->set_last_used = TRUE;
+ return TRUE;
+ case 'd':
+ if (ctx->set_decision) {
+ i_error("Only one decision flag allowed");
+ return FALSE;
+ }
+ if (strcmp(optarg, "no") == 0) {
+ ctx->decision = MAIL_CACHE_DECISION_NO;
+ } else if (strcmp(optarg, "temp") == 0) {
+ ctx->decision = MAIL_CACHE_DECISION_TEMP;
+ } else if (strcmp(optarg, "yes") == 0) {
+ ctx->decision = MAIL_CACHE_DECISION_YES;
+ } else {
+ i_error("Invalid decision '%s': " \
+ "must be one of yes, temp, no",
+ optarg);
+ return FALSE;
+ }
+ ctx->set_decision = TRUE;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static const char *
+cmd_mailbox_cache_decision_to_str(enum mail_cache_decision_type decision)
+{
+ string_t *ret = t_str_new(10);
+ switch((decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED))) {
+ case MAIL_CACHE_DECISION_NO:
+ str_append(ret, "no");
+ break;
+ case MAIL_CACHE_DECISION_TEMP:
+ str_append(ret, "temp");
+ break;
+ case MAIL_CACHE_DECISION_YES:
+ str_append(ret, "yes");
+ break;
+ }
+ return str_c(ret);
+}
+
+static void
+cmd_mailbox_cache_decision_process_field(struct mailbox_cache_cmd_context *ctx,
+ struct mail_cache_field_private *field)
+{
+ if (ctx->set_decision) {
+ field->field.decision = ctx->decision;
+ field->decision_dirty = TRUE;
+ }
+
+ if (ctx->set_last_used) {
+ field->field.last_used = (time_t)ctx->last_used;
+ field->decision_dirty = TRUE;
+ }
+
+ doveadm_print(cmd_mailbox_cache_decision_to_str(field->field.decision));
+ doveadm_print(t_strflocaltime("%F %T %Z", field->field.last_used));
+}
+
+static void
+cmd_mailbox_cache_decision_run_per_field(struct mailbox_cache_cmd_context *ctx,
+ struct mail_cache *cache)
+{
+ const char *const *field_name;
+ for(field_name = ctx->fields; *field_name != NULL; field_name++) {
+ doveadm_print(*field_name);
+ /* see if the field exists */
+ unsigned int idx = mail_cache_register_lookup(cache,
+ *field_name);
+ if (idx == UINT_MAX) {
+ doveadm_print("<not found>");
+ doveadm_print("");
+ continue;
+ }
+
+ cmd_mailbox_cache_decision_process_field(ctx, &cache->fields[idx]);
+ }
+}
+
+static void
+cmd_mailbox_cache_decision_run_all_fields(struct mailbox_cache_cmd_context *ctx,
+ struct mail_cache *cache)
+{
+ /* get all fields */
+ for(unsigned int i = 0; i < cache->fields_count; i++) {
+ doveadm_print(cache->fields[i].field.name);
+ cmd_mailbox_cache_decision_process_field(ctx, &cache->fields[i]);
+ }
+}
+
+static int cmd_mailbox_cache_decision_run_box(struct mailbox_cache_cmd_context *ctx,
+ struct mailbox *box)
+{
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box, ctx->ctx.transaction_flags,
+ "mailbox cache decision");
+ struct mail_cache *cache = t->box->cache;
+ struct mail_cache_view *view;
+
+ if (mail_cache_open_and_verify(cache) < 0 ||
+ MAIL_CACHE_IS_UNUSABLE(cache)) {
+ mailbox_transaction_rollback(&t);
+ i_error("Cache is unusable");
+ ctx->ctx.exit_code = EX_TEMPFAIL;
+ return -1;
+ }
+
+ view = mail_cache_view_open(cache, t->box->view);
+
+ if (ctx->all_fields)
+ cmd_mailbox_cache_decision_run_all_fields(ctx, cache);
+ else
+ cmd_mailbox_cache_decision_run_per_field(ctx, cache);
+
+ /* update headers */
+ if (ctx->set_decision || ctx->set_last_used)
+ mail_cache_header_fields_update(cache);
+
+ mail_cache_view_close(&view);
+
+ if (mailbox_transaction_commit(&t) < 0) {
+ i_error("mailbox_transaction_commit() failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ return -1;
+ }
+ return 0;
+}
+
+static int cmd_mailbox_cache_decision_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+ const char *const *boxname;
+ int ret = 0;
+
+ if (_ctx->exit_code != 0)
+ return -1;
+
+ for(boxname = ctx->boxes; ret == 0 && *boxname != NULL; boxname++) {
+ struct mailbox *box;
+ if ((ret = cmd_mailbox_cache_open_box(_ctx, user, *boxname, &box)) < 0)
+ break;
+ doveadm_print_sticky("mailbox", mailbox_get_vname(box));
+ ret = cmd_mailbox_cache_decision_run_box(ctx, box);
+ mailbox_free(&box);
+ }
+
+ return ret;
+}
+
+static int cmd_mailbox_cache_remove_box(struct mailbox_cache_cmd_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox *box;
+ struct mail *mail;
+ void *empty = NULL;
+ int ret = 0, count = 0;
+
+ if (doveadm_mail_iter_init(&ctx->ctx, info, ctx->ctx.search_args,
+ 0, NULL, 0, &iter) < 0)
+ return -1;
+
+ box = doveadm_mail_iter_get_mailbox(iter);
+
+ struct mail_index_transaction *t =
+ mail_index_transaction_begin(box->view, MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ struct mail_cache_view *view =
+ mail_cache_view_open(box->cache, box->view);
+
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ count++;
+ doveadm_print(mailbox_get_vname(box));
+ doveadm_print(dec2str(mail->uid));
+ /* drop cache pointer */
+ mail_index_update_ext(t, mail->seq, view->cache->ext_id, &empty, NULL);
+ doveadm_print("ok");
+ }
+
+ if (mail_index_transaction_commit(&t) < 0) {
+ i_error("mail_index_transaction_commit() failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ ret = -1;
+ } else {
+ mail_cache_expunge_count(view->cache, count);
+ }
+
+ mail_cache_view_close(&view);
+
+ if (doveadm_mail_iter_deinit(&iter) < 0)
+ ret = -1;
+
+ return ret;
+}
+
+static int cmd_mailbox_cache_remove_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(&ctx->ctx, user, ctx->ctx.search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_mailbox_cache_remove_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_mailbox_cache_remove_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("mailbox cache remove");
+
+ doveadm_print_header_simple("mailbox");
+ doveadm_print_header_simple("uid");
+ doveadm_print_header_simple("result");
+
+ ctx->ctx.search_args = doveadm_mail_build_search_args(args);
+}
+
+static int cmd_mailbox_cache_purge_run_box(struct mailbox_cache_cmd_context *ctx,
+ struct mailbox *box)
+{
+ if (mail_cache_purge(box->cache, (uint32_t)-1,
+ "doveadm mailbox cache purge") < 0) {
+ mailbox_set_index_error(box);
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ return -1;
+ }
+ return 0;
+}
+
+static int cmd_mailbox_cache_purge_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+ const char *const *boxname;
+ int ret = 0;
+
+ if (_ctx->exit_code != 0)
+ return -1;
+
+ for(boxname = ctx->boxes; ret == 0 && *boxname != NULL; boxname++) {
+ struct mailbox *box;
+ if ((ret = cmd_mailbox_cache_open_box(_ctx, user, *boxname, &box)) < 0)
+ break;
+ ret = cmd_mailbox_cache_purge_run_box(ctx, box);
+ mailbox_free(&box);
+ }
+
+ return ret;
+}
+
+static void cmd_mailbox_cache_purge_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct mailbox_cache_cmd_context *ctx =
+ container_of(_ctx, struct mailbox_cache_cmd_context, ctx);
+
+ ctx->boxes = args;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_cache_decision_alloc(void)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ doveadm_mail_cmd_alloc(struct mailbox_cache_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_cache_decision_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_cache_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_cache_decision_run;
+ ctx->ctx.getopt_args = "al:f:d:";
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_cache_remove_alloc(void)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ doveadm_mail_cmd_alloc(struct mailbox_cache_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_cache_remove_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_cache_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_cache_remove_run;
+ ctx->ctx.getopt_args = "";
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_cache_purge_alloc(void)
+{
+ struct mailbox_cache_cmd_context *ctx =
+ doveadm_mail_cmd_alloc(struct mailbox_cache_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_cache_purge_init;
+ ctx->ctx.v.run = cmd_mailbox_cache_purge_run;
+ ctx->ctx.getopt_args = "";
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_decision = {
+ .name = "mailbox cache decision",
+ .mail_cmd = cmd_mailbox_cache_decision_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"--all --fields <fields> " \
+ "--last-used <timestamp> --decision <decision> " \
+ "<mailbox> [<mailbox> ... ]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('a', "all", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('f', "fieldstr", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('l', "last-used", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('d', "decision", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_remove = {
+ .name = "mailbox cache remove",
+ .mail_cmd = cmd_mailbox_cache_remove_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<search string>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_purge = {
+ .name = "mailbox cache purge",
+ .mail_cmd = cmd_mailbox_cache_purge_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-mailbox-metadata.c b/src/doveadm/doveadm-mail-mailbox-metadata.c
new file mode 100644
index 0000000..593555b
--- /dev/null
+++ b/src/doveadm/doveadm-mail-mailbox-metadata.c
@@ -0,0 +1,425 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "imap-metadata.h"
+
+enum doveadm_metadata_op {
+ DOVEADM_METADATA_OP_SET = 0,
+ DOVEADM_METADATA_OP_GET,
+ DOVEADM_METADATA_OP_LIST,
+};
+
+const char *doveadm_metadata_op_names[] = {
+ "set attribute",
+ "get attribute",
+ "list attribute",
+};
+
+struct metadata_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ const char *mailbox;
+ enum mail_attribute_type key_type;
+ const char *key;
+ struct mail_attribute_value value;
+ bool empty_mailbox_name;
+ bool allow_empty_mailbox_name;
+ bool prepend_prefix;
+};
+
+static int
+cmd_mailbox_metadata_get_mailbox(struct metadata_cmd_context *mctx,
+ struct mail_user *user,
+ enum doveadm_metadata_op op,
+ struct mail_namespace **ns_r,
+ struct mailbox **box_r)
+{
+ mctx->empty_mailbox_name = mctx->mailbox[0] == '\0';
+
+ if (mctx->empty_mailbox_name) {
+ if (!mctx->allow_empty_mailbox_name) {
+ const char *op_str = doveadm_metadata_op_names[op];
+ i_error("Failed to %s: %s", op_str,
+ "mailbox name cannot be empty");
+ mctx->ctx.exit_code = EX_USAGE;
+ return -1;
+ }
+
+ /* Server attribute. It shouldn't depend on INBOX's ACLs,
+ so ignore them. */
+ *ns_r = mail_namespace_find_inbox(user->namespaces);
+ *box_r = mailbox_alloc((*ns_r)->list, "INBOX",
+ MAILBOX_FLAG_IGNORE_ACLS |
+ MAILBOX_FLAG_ATTRIBUTE_SESSION);
+
+ mctx->key = t_strconcat(MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER,
+ mctx->key, NULL);
+ } else {
+ /* mailbox attributes */
+ *ns_r = mail_namespace_find(user->namespaces, mctx->mailbox);
+ *box_r = mailbox_alloc((*ns_r)->list, mctx->mailbox,
+ MAILBOX_FLAG_ATTRIBUTE_SESSION);
+ }
+
+ if (op == DOVEADM_METADATA_OP_SET &&
+ mailbox_open(*box_r) < 0) {
+ i_error("Failed to open mailbox: %s",
+ mailbox_get_last_internal_error(*box_r, NULL));
+ doveadm_mail_failed_mailbox(&mctx->ctx, *box_r);
+ mailbox_free(box_r);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+cmd_mailbox_metadata_set_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mailbox_transaction_context *trans;
+ int ret;
+
+ ret = cmd_mailbox_metadata_get_mailbox(ctx, user, DOVEADM_METADATA_OP_SET,
+ &ns, &box);
+ if (ret != 0)
+ return ret;
+
+ trans = mailbox_transaction_begin(box, (ctx->empty_mailbox_name ?
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL : 0) |
+ ctx->ctx.transaction_flags, __func__);
+
+ ret = ctx->value.value == NULL ?
+ mailbox_attribute_unset(trans, ctx->key_type, ctx->key) :
+ mailbox_attribute_set(trans, ctx->key_type, ctx->key, &ctx->value);
+ if (ret < 0) {
+ i_error("Failed to set attribute: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ mailbox_transaction_rollback(&trans);
+ } else if (mailbox_transaction_commit(&trans) < 0) {
+ i_error("Failed to commit transaction: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+static void
+cmd_mailbox_metadata_parse_key(const char *arg,
+ enum mail_attribute_type *type_r,
+ const char **key_r)
+{
+ arg = t_str_lcase(arg);
+
+ if (str_begins(arg, "/private/")) {
+ *type_r = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ *key_r = arg + 9;
+ } else if (str_begins(arg, "/shared/")) {
+ *type_r = MAIL_ATTRIBUTE_TYPE_SHARED;
+ *key_r = arg + 8;
+ } else if (strcmp(arg, "/private") == 0) {
+ *type_r = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ *key_r = "";
+ } else if (strcmp(arg, "/shared") == 0) {
+ *type_r = MAIL_ATTRIBUTE_TYPE_SHARED;
+ *key_r = "";
+ } else {
+ i_fatal_status(EX_USAGE, "Invalid metadata key '%s': "
+ "Must begin with /private or /shared", arg);
+ }
+}
+
+static void
+cmd_mailbox_metadata_set_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ const char *key;
+
+ if (str_array_length(args) != 3)
+ doveadm_mail_help_name("mailbox metadata set");
+ cmd_mailbox_metadata_parse_key(args[1], &ctx->key_type, &key);
+
+ ctx->mailbox = p_strdup(_ctx->pool, args[0]);
+ ctx->key = p_strdup(_ctx->pool, key);
+ ctx->value.value = p_strdup(_ctx->pool, args[2]);
+}
+
+static bool
+cmd_mailbox_metadata_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct metadata_cmd_context *ctx =
+ (struct metadata_cmd_context *)_ctx;
+
+ switch (c) {
+ case 's':
+ ctx->allow_empty_mailbox_name = TRUE;
+ break;
+ case 'p':
+ ctx->prepend_prefix = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_metadata_set_alloc(void)
+{
+ struct metadata_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct metadata_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_metadata_set_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_metadata_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_metadata_set_run;
+ return &ctx->ctx;
+}
+
+static void
+cmd_mailbox_metadata_unset_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ const char *key;
+
+ if (str_array_length(args) != 2)
+ doveadm_mail_help_name("mailbox metadata unset");
+ cmd_mailbox_metadata_parse_key(args[1], &ctx->key_type, &key);
+
+ ctx->mailbox = p_strdup(_ctx->pool, args[0]);
+ ctx->key = p_strdup(_ctx->pool, key);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_metadata_unset_alloc(void)
+{
+ struct metadata_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct metadata_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_metadata_unset_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_metadata_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_metadata_set_run;
+ return &ctx->ctx;
+}
+
+static int
+cmd_mailbox_metadata_get_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mail_attribute_value value;
+ int ret;
+
+ ret = cmd_mailbox_metadata_get_mailbox(ctx, user, DOVEADM_METADATA_OP_GET,
+ &ns, &box);
+ if (ret != 0)
+ return ret;
+
+ ret = mailbox_attribute_get_stream(box, ctx->key_type, ctx->key, &value);
+ if (ret < 0) {
+ i_error("Failed to get attribute: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ } else if (ret == 0) {
+ /* not found, print as empty */
+ doveadm_print("");
+ } else if (value.value_stream != NULL) {
+ if (doveadm_print_istream(value.value_stream) < 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(value.value_stream),
+ i_stream_get_error(value.value_stream));
+ ret = -1;
+ }
+ } else {
+ doveadm_print(value.value);
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+static void
+cmd_mailbox_metadata_get_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ const char *key;
+
+ if (str_array_length(args) != 2)
+ doveadm_mail_help_name("mailbox metadata get");
+ cmd_mailbox_metadata_parse_key(args[1], &ctx->key_type, &key);
+
+ ctx->mailbox = p_strdup(_ctx->pool, args[0]);
+ ctx->key = p_strdup(_ctx->pool, key);
+ doveadm_print_header("value", "value",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_metadata_get_alloc(void)
+{
+ struct metadata_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct metadata_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_metadata_get_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_metadata_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_metadata_get_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx;
+}
+
+static int
+cmd_mailbox_metadata_list_run_iter(struct metadata_cmd_context *ctx,
+ struct mailbox *box,
+ enum mail_attribute_type type)
+{
+ struct mailbox_attribute_iter *iter;
+ const char *key;
+ string_t *outp = t_str_new(64);
+
+ iter = mailbox_attribute_iter_init(box, type, ctx->key);
+ while ((key = mailbox_attribute_iter_next(iter)) != NULL) {
+ if (ctx->prepend_prefix) {
+ if (type == MAIL_ATTRIBUTE_TYPE_PRIVATE)
+ str_append(outp, IMAP_METADATA_PRIVATE_PREFIX"/");
+ else if (type == MAIL_ATTRIBUTE_TYPE_SHARED)
+ str_append(outp, IMAP_METADATA_SHARED_PREFIX"/");
+ else
+ i_unreached();
+ str_append(outp, key);
+ doveadm_print(str_c(outp));
+ str_truncate(outp, 0);
+ } else {
+ doveadm_print(key);
+ }
+ }
+ if (mailbox_attribute_iter_deinit(&iter) < 0) {
+ i_error("Mailbox %s: Failed to iterate mailbox attributes: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+cmd_mailbox_metadata_list_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ int ret = 0;
+
+ ret = cmd_mailbox_metadata_get_mailbox(ctx, user, DOVEADM_METADATA_OP_LIST,
+ &ns, &box);
+ if (ret != 0)
+ return ret;
+
+ if (ctx->key[0] == '\0' || ctx->key_type == MAIL_ATTRIBUTE_TYPE_PRIVATE) {
+ if (cmd_mailbox_metadata_list_run_iter(ctx, box, MAIL_ATTRIBUTE_TYPE_PRIVATE) < 0) {
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ }
+ if (ctx->key[0] == '\0' || ctx->key_type == MAIL_ATTRIBUTE_TYPE_SHARED) {
+ if (cmd_mailbox_metadata_list_run_iter(ctx, box, MAIL_ATTRIBUTE_TYPE_SHARED) < 0) {
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static void
+cmd_mailbox_metadata_list_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct metadata_cmd_context *ctx = (struct metadata_cmd_context *)_ctx;
+ const char *key = NULL;
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("mailbox metadata list");
+ if (args[1] != NULL)
+ cmd_mailbox_metadata_parse_key(args[1], &ctx->key_type, &key);
+ ctx->mailbox = p_strdup(_ctx->pool, args[0]);
+ ctx->key = key == NULL ? "" : p_strdup(_ctx->pool, key);
+ doveadm_print_header("key", "key",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_metadata_list_alloc(void)
+{
+ struct metadata_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct metadata_cmd_context);
+ ctx->ctx.v.init = cmd_mailbox_metadata_list_init;
+ ctx->ctx.v.parse_arg = cmd_mailbox_metadata_parse_arg;
+ ctx->ctx.v.run = cmd_mailbox_metadata_list_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_set_ver2 = {
+ .name = "mailbox metadata set",
+ .mail_cmd = cmd_mailbox_metadata_set_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] <mailbox> <key> <value>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "allow-empty-mailbox-name", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "value", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_unset_ver2 = {
+ .name = "mailbox metadata unset",
+ .mail_cmd = cmd_mailbox_metadata_unset_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] <mailbox> <key>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "allow-empty-mailbox-name", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_get_ver2 = {
+ .name = "mailbox metadata get",
+ .mail_cmd = cmd_mailbox_metadata_get_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] <mailbox> <key>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "allow-empty-mailbox-name", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_list_ver2 = {
+ .name = "mailbox metadata list",
+ .mail_cmd = cmd_mailbox_metadata_list_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] [-p] <mailbox> [<key prefix>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "allow-empty-mailbox-name", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('p', "prepend-prefix", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "key-prefix", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-mailbox-status.c b/src/doveadm/doveadm-mail-mailbox-status.c
new file mode 100644
index 0000000..9f47095
--- /dev/null
+++ b/src/doveadm/doveadm-mail-mailbox-status.c
@@ -0,0 +1,275 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-search.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+#include "doveadm-mailbox-list-iter.h"
+
+#define ALL_STATUS_ITEMS \
+ (STATUS_MESSAGES | STATUS_RECENT | \
+ STATUS_UIDNEXT | STATUS_UIDVALIDITY | \
+ STATUS_UNSEEN | STATUS_HIGHESTMODSEQ)
+#define ALL_METADATA_ITEMS \
+ (MAILBOX_METADATA_VIRTUAL_SIZE | MAILBOX_METADATA_GUID | \
+ MAILBOX_METADATA_FIRST_SAVE_DATE)
+
+#define TOTAL_STATUS_ITEMS \
+ (STATUS_MESSAGES | STATUS_RECENT | STATUS_UNSEEN)
+#define TOTAL_METADATA_ITEMS \
+ (MAILBOX_METADATA_VIRTUAL_SIZE)
+
+struct status_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ struct mail_search_args *search_args;
+
+ enum mailbox_status_items status_items;
+ enum mailbox_metadata_items metadata_items;
+ struct mailbox_status total_status;
+ struct mailbox_metadata total_metadata;
+
+ bool total_sum:1;
+};
+
+static void status_parse_fields(struct status_cmd_context *ctx,
+ const char *const *fields)
+{
+ if (*fields == NULL)
+ i_fatal_status(EX_USAGE, "No status fields");
+
+ for (; *fields != NULL; fields++) {
+ const char *field = *fields;
+
+ if (strcmp(field, "all") == 0) {
+ if (ctx->total_sum) {
+ ctx->status_items |= TOTAL_STATUS_ITEMS;
+ ctx->metadata_items |= TOTAL_METADATA_ITEMS;
+ } else {
+ ctx->status_items |= ALL_STATUS_ITEMS;
+ ctx->metadata_items |= ALL_METADATA_ITEMS;
+ }
+ } else if (strcmp(field, "messages") == 0)
+ ctx->status_items |= STATUS_MESSAGES;
+ else if (strcmp(field, "recent") == 0)
+ ctx->status_items |= STATUS_RECENT;
+ else if (strcmp(field, "uidnext") == 0)
+ ctx->status_items |= STATUS_UIDNEXT;
+ else if (strcmp(field, "uidvalidity") == 0)
+ ctx->status_items |= STATUS_UIDVALIDITY;
+ else if (strcmp(field, "unseen") == 0)
+ ctx->status_items |= STATUS_UNSEEN;
+ else if (strcmp(field, "highestmodseq") == 0)
+ ctx->status_items |= STATUS_HIGHESTMODSEQ;
+ else if (strcmp(field, "vsize") == 0)
+ ctx->metadata_items |= MAILBOX_METADATA_VIRTUAL_SIZE;
+ else if (strcmp(field, "guid") == 0)
+ ctx->metadata_items |= MAILBOX_METADATA_GUID;
+ else if (strcmp(field, "firstsaved") == 0)
+ ctx->metadata_items |= MAILBOX_METADATA_FIRST_SAVE_DATE;
+ else {
+ i_fatal_status(EX_USAGE,
+ "Unknown status field: %s", field);
+ }
+
+ if (ctx->total_sum &&
+ ((ctx->status_items & ENUM_NEGATE(TOTAL_STATUS_ITEMS)) != 0 ||
+ (ctx->metadata_items & ENUM_NEGATE(TOTAL_METADATA_ITEMS)) != 0)) {
+ i_fatal_status(EX_USAGE,
+ "Status field %s can't be used with -t", field);
+ }
+ }
+}
+
+static void ATTR_NULL(2)
+status_output(struct status_cmd_context *ctx, struct mailbox *box,
+ const struct mailbox_status *status,
+ const struct mailbox_metadata *metadata)
+{
+ if (box != NULL)
+ doveadm_print(mailbox_get_vname(box));
+
+ if ((ctx->status_items & STATUS_MESSAGES) != 0)
+ doveadm_print_num(status->messages);
+ if ((ctx->status_items & STATUS_RECENT) != 0)
+ doveadm_print_num(status->recent);
+ if ((ctx->status_items & STATUS_UIDNEXT) != 0)
+ doveadm_print_num(status->uidnext);
+ if ((ctx->status_items & STATUS_UIDVALIDITY) != 0)
+ doveadm_print_num(status->uidvalidity);
+ if ((ctx->status_items & STATUS_UNSEEN) != 0)
+ doveadm_print_num(status->unseen);
+ if ((ctx->status_items & STATUS_HIGHESTMODSEQ) != 0)
+ doveadm_print_num(status->highest_modseq);
+ if ((ctx->metadata_items & MAILBOX_METADATA_VIRTUAL_SIZE) != 0)
+ doveadm_print_num(metadata->virtual_size);
+ if ((ctx->metadata_items & MAILBOX_METADATA_GUID) != 0)
+ doveadm_print(guid_128_to_string(metadata->guid));
+ if ((ctx->metadata_items & MAILBOX_METADATA_FIRST_SAVE_DATE) > 0) {
+ if (metadata->first_save_date > -1)
+ doveadm_print_num(metadata->first_save_date);
+ else
+ doveadm_print("never");
+ }
+}
+
+static void
+status_sum(struct status_cmd_context *ctx,
+ const struct mailbox_status *status,
+ const struct mailbox_metadata *metadata)
+{
+ struct mailbox_status *dest = &ctx->total_status;
+
+ dest->messages += status->messages;
+ dest->recent += status->recent;
+ dest->unseen += status->unseen;
+ ctx->total_metadata.virtual_size += metadata->virtual_size;
+}
+
+static int
+status_mailbox(struct status_cmd_context *ctx, const struct mailbox_info *info)
+{
+ struct mailbox *box;
+ struct mailbox_status status;
+ struct mailbox_metadata metadata;
+
+ box = doveadm_mailbox_find(ctx->ctx.cur_mail_user, info->vname);
+ if (mailbox_get_status(box, ctx->status_items, &status) < 0 ||
+ mailbox_get_metadata(box, ctx->metadata_items, &metadata) < 0) {
+ i_error("Mailbox %s: Failed to lookup mailbox status: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(&ctx->ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (!ctx->total_sum)
+ status_output(ctx, box, &status, &metadata);
+ else
+ status_sum(ctx, &status, &metadata);
+ mailbox_free(&box);
+ return 0;
+}
+
+static int
+cmd_mailbox_status_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct status_cmd_context *ctx = (struct status_cmd_context *)_ctx;
+ enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ i_zero(&ctx->total_status);
+ i_zero(&ctx->total_metadata);
+
+ iter = doveadm_mailbox_list_iter_init(_ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) {
+ T_BEGIN {
+ if (status_mailbox(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ }
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+
+ if (ctx->total_sum) {
+ status_output(ctx, NULL, &ctx->total_status,
+ &ctx->total_metadata);
+ }
+ return ret;
+}
+
+static void cmd_mailbox_status_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct status_cmd_context *ctx = (struct status_cmd_context *)_ctx;
+ const char *fields = args[0];
+
+ if (fields == NULL || args[1] == NULL)
+ doveadm_mail_help_name("mailbox status");
+
+ status_parse_fields(ctx, t_strsplit_spaces(fields, " "));
+ ctx->search_args = doveadm_mail_mailbox_search_args_build(args+1);
+
+ if (!ctx->total_sum) {
+ doveadm_print_header("mailbox", "mailbox",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ }
+ if ((ctx->status_items & STATUS_MESSAGES) != 0)
+ doveadm_print_header_simple("messages");
+ if ((ctx->status_items & STATUS_RECENT) != 0)
+ doveadm_print_header_simple("recent");
+ if ((ctx->status_items & STATUS_UIDNEXT) != 0)
+ doveadm_print_header_simple("uidnext");
+ if ((ctx->status_items & STATUS_UIDVALIDITY) != 0)
+ doveadm_print_header_simple("uidvalidity");
+ if ((ctx->status_items & STATUS_UNSEEN) != 0)
+ doveadm_print_header_simple("unseen");
+ if ((ctx->status_items & STATUS_HIGHESTMODSEQ) != 0)
+ doveadm_print_header_simple("highestmodseq");
+ if ((ctx->metadata_items & MAILBOX_METADATA_VIRTUAL_SIZE) != 0)
+ doveadm_print_header_simple("vsize");
+ if ((ctx->metadata_items & MAILBOX_METADATA_GUID) != 0)
+ doveadm_print_header_simple("guid");
+ if ((ctx->metadata_items & MAILBOX_METADATA_FIRST_SAVE_DATE) != 0)
+ doveadm_print_header_simple("firstsaved");
+}
+
+static void cmd_mailbox_status_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct status_cmd_context *ctx = (struct status_cmd_context *)_ctx;
+
+ if (ctx->search_args != NULL)
+ mail_search_args_unref(&ctx->search_args);
+}
+
+static bool
+cmd_mailbox_status_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct status_cmd_context *ctx = (struct status_cmd_context *)_ctx;
+
+ switch (c) {
+ case 't':
+ ctx->total_sum = TRUE;
+ break;
+ case 'f':
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_status_alloc(void)
+{
+ struct status_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct status_cmd_context);
+ ctx->ctx.getopt_args = "t";
+ ctx->ctx.v.parse_arg = cmd_mailbox_status_parse_arg;
+ ctx->ctx.v.init = cmd_mailbox_status_init;
+ ctx->ctx.v.deinit = cmd_mailbox_status_deinit;
+ ctx->ctx.v.run = cmd_mailbox_status_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_status_ver2 = {
+ .name = "mailbox status",
+ .mail_cmd = cmd_mailbox_status_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<fields> <mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('t', "total-sum", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('f', "field", CMD_PARAM_ARRAY, 0)
+DOVEADM_CMD_PARAM('\0', "fieldstr", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL | CMD_PARAM_FLAG_DO_NOT_EXPOSE) /* FIXME: horrible hack, remove me when possible */
+DOVEADM_CMD_PARAM('\0', "mailbox-mask", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-mailbox.c b/src/doveadm/doveadm-mail-mailbox.c
new file mode 100644
index 0000000..2b2c453
--- /dev/null
+++ b/src/doveadm/doveadm-mail-mailbox.c
@@ -0,0 +1,857 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "imap-utf7.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-search-build.h"
+#include "doveadm-print.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+struct doveadm_mailbox_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool subscriptions;
+};
+
+struct mailbox_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ ARRAY_TYPE(const_string) mailboxes;
+};
+
+struct create_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ ARRAY_TYPE(const_string) mailboxes;
+ struct mailbox_update update;
+};
+
+struct delete_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ ARRAY_TYPE(const_string) mailboxes;
+ bool recursive;
+ bool require_empty;
+ bool unsafe;
+};
+
+struct rename_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ const char *oldname, *newname;
+};
+
+struct list_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ struct mail_search_args *search_args;
+ bool mutf7;
+};
+
+struct update_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ const char *mailbox;
+ struct mailbox_update update;
+};
+
+struct path_cmd_context {
+ struct doveadm_mailbox_cmd_context ctx;
+ const char *mailbox;
+ enum mailbox_list_path_type path_type;
+};
+
+static const char *mailbox_list_path_type_names[] = {
+ "dir", "alt-dir", "mailbox", "alt-mailbox",
+ "control", "index", "index-private"
+};
+
+void doveadm_mailbox_args_check(const char *const args[])
+{
+ unsigned int i;
+
+ for (i = 0; args[i] != NULL; i++) {
+ if (!uni_utf8_str_is_valid(args[i])) {
+ i_fatal_status(EX_DATAERR,
+ "Mailbox name not valid UTF-8: %s", args[i]);
+ }
+ }
+}
+
+static bool cmd_mailbox_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct doveadm_mailbox_cmd_context *ctx =
+ (struct doveadm_mailbox_cmd_context *)_ctx;
+
+ switch (c) {
+ case 's':
+ ctx->subscriptions = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+#define doveadm_mailbox_cmd_alloc(type) \
+ (type *)doveadm_mailbox_cmd_alloc_size(sizeof(type))
+static struct doveadm_mail_cmd_context *
+doveadm_mailbox_cmd_alloc_size(size_t size)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc_size(size);
+ ctx->getopt_args = "s";
+ ctx->v.parse_arg = cmd_mailbox_parse_arg;
+ return ctx;
+}
+
+static bool
+cmd_mailbox_list_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct list_cmd_context *ctx = (struct list_cmd_context *)_ctx;
+
+ switch (c) {
+ case '7':
+ ctx->mutf7 = TRUE;
+ break;
+ case '8':
+ ctx->mutf7 = FALSE;
+ break;
+ case 's':
+ ctx->ctx.subscriptions = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+cmd_mailbox_list_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct list_cmd_context *ctx = (struct list_cmd_context *)_ctx;
+ enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ string_t *str = t_str_new(256);
+
+ if (ctx->ctx.subscriptions)
+ iter_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED;
+
+ iter = doveadm_mailbox_list_iter_full_init(_ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) {
+ if (!ctx->mutf7)
+ doveadm_print(info->vname);
+ else {
+ str_truncate(str, 0);
+ if (imap_utf8_to_utf7(info->vname, str) < 0)
+ i_unreached();
+ doveadm_print(str_c(str));
+ }
+ }
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ return -1;
+ return 0;
+}
+
+struct mail_search_args *
+doveadm_mail_mailbox_search_args_build(const char *const args[])
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *arg;
+ enum mail_search_arg_type type;
+ unsigned int i;
+
+ doveadm_mailbox_args_check(args);
+ search_args = mail_search_build_init();
+ for (i = 0; args[i] != NULL; i++) {
+ if (strchr(args[i], '*') != NULL ||
+ strchr(args[i], '%') != NULL)
+ type = SEARCH_MAILBOX_GLOB;
+ else
+ type = SEARCH_MAILBOX;
+ arg = mail_search_build_add(search_args, type);
+ arg->value.str = p_strdup(search_args->pool, args[i]);
+ }
+ if (i > 1) {
+ struct mail_search_arg *subargs = search_args->args;
+
+ search_args->args = NULL;
+ arg = mail_search_build_add(search_args, SEARCH_OR);
+ arg->value.subargs = subargs;
+ }
+ return search_args;
+}
+
+static void cmd_mailbox_list_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct list_cmd_context *ctx = (struct list_cmd_context *)_ctx;
+
+ doveadm_print_header("mailbox", "mailbox",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ ctx->search_args = doveadm_mail_mailbox_search_args_build(args);
+}
+
+static void cmd_mailbox_list_deinit(struct doveadm_mail_cmd_context *_ctx)
+{
+ struct list_cmd_context *ctx = (struct list_cmd_context *)_ctx;
+
+ if (ctx->search_args != NULL)
+ mail_search_args_unref(&ctx->search_args);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_list_alloc(void)
+{
+ struct list_cmd_context *ctx;
+
+ ctx = doveadm_mailbox_cmd_alloc(struct list_cmd_context);
+ ctx->ctx.ctx.v.init = cmd_mailbox_list_init;
+ ctx->ctx.ctx.v.deinit = cmd_mailbox_list_deinit;
+ ctx->ctx.ctx.v.run = cmd_mailbox_list_run;
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_list_parse_arg;
+ ctx->ctx.ctx.getopt_args = "78s";
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx.ctx;
+}
+
+static int
+cmd_mailbox_create_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct create_cmd_context *ctx = (struct create_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *name;
+ int ret = 0;
+
+ array_foreach_elem(&ctx->mailboxes, name) {
+ size_t len;
+ bool directory = FALSE;
+
+ ns = mail_namespace_find(user->namespaces, name);
+ len = strlen(name);
+ if (len > 0 && name[len-1] == mail_namespace_get_sep(ns)) {
+ name = t_strndup(name, len-1);
+ directory = TRUE;
+ }
+
+ box = mailbox_alloc(ns->list, name, 0);
+ if (mailbox_create(box, &ctx->update, directory) < 0) {
+ i_error("Can't create mailbox %s: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ if (ctx->ctx.subscriptions) {
+ if (mailbox_set_subscribed(box, TRUE) < 0) {
+ i_error("Can't subscribe to mailbox %s: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ }
+ mailbox_free(&box);
+ }
+ return ret;
+}
+
+static void cmd_mailbox_create_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct mailbox_cmd_context *ctx = (struct mailbox_cmd_context *)_ctx;
+ const char *name;
+ unsigned int i;
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("mailbox create");
+ doveadm_mailbox_args_check(args);
+
+ for (i = 0; args[i] != NULL; i++) {
+ name = p_strdup(ctx->ctx.ctx.pool, args[i]);
+ array_push_back(&ctx->mailboxes, &name);
+ }
+}
+
+static bool
+cmd_mailbox_create_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct create_cmd_context *ctx = (struct create_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'g':
+ if (guid_128_from_string(optarg, ctx->update.mailbox_guid) < 0)
+ doveadm_mail_help_name("mailbox create");
+ break;
+ case 's':
+ ctx->ctx.subscriptions = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_create_alloc(void)
+{
+ struct create_cmd_context *ctx;
+
+ ctx = doveadm_mailbox_cmd_alloc(struct create_cmd_context);
+ ctx->ctx.ctx.v.init = cmd_mailbox_create_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_create_run;
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_create_parse_arg;
+ ctx->ctx.ctx.getopt_args = "g:s";
+ p_array_init(&ctx->mailboxes, ctx->ctx.ctx.pool, 16);
+ return &ctx->ctx.ctx;
+}
+
+static int i_strcmp_reverse_p(const char *const *s1, const char *const *s2)
+{
+ return -strcmp(*s1, *s2);
+}
+
+static int
+get_child_mailboxes(struct mail_user *user, ARRAY_TYPE(const_string) *mailboxes,
+ const char *name)
+{
+ struct mailbox_list_iterate_context *iter;
+ struct mail_namespace *ns;
+ const struct mailbox_info *info;
+ const char *pattern, *child_name;
+
+ ns = mail_namespace_find(user->namespaces, name);
+ pattern = name[0] == '\0' ? "*" :
+ t_strdup_printf("%s%c*", name, mail_namespace_get_sep(ns));
+ iter = mailbox_list_iter_init(ns->list, pattern,
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ child_name = t_strdup(info->vname);
+ array_push_back(mailboxes, &child_name);
+ }
+ return mailbox_list_iter_deinit(&iter);
+}
+
+static int
+cmd_mailbox_delete_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct delete_cmd_context *ctx = (struct delete_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mail_storage *storage;
+ const char *name;
+ ARRAY_TYPE(const_string) recursive_mailboxes;
+ const ARRAY_TYPE(const_string) *mailboxes = &ctx->mailboxes;
+ enum mailbox_flags mailbox_flags = 0;
+ int ret = 0, ret2;
+
+ if (ctx->unsafe)
+ mailbox_flags |= MAILBOX_FLAG_DELETE_UNSAFE;
+ if (ctx->recursive) {
+ t_array_init(&recursive_mailboxes, 32);
+ array_foreach_elem(&ctx->mailboxes, name) {
+ if (get_child_mailboxes(user, &recursive_mailboxes,
+ name) < 0) {
+ doveadm_mail_failed_error(_ctx, MAIL_ERROR_TEMP);
+ ret = -1;
+ }
+ if (name[0] != '\0')
+ array_push_back(&recursive_mailboxes, &name);
+ }
+ array_sort(&recursive_mailboxes, i_strcmp_reverse_p);
+ mailboxes = &recursive_mailboxes;
+ }
+
+ array_foreach_elem(mailboxes, name) {
+ ns = mail_namespace_find(user->namespaces, name);
+ box = mailbox_alloc(ns->list, name, mailbox_flags);
+ storage = mailbox_get_storage(box);
+ ret2 = ctx->require_empty ? mailbox_delete_empty(box) :
+ mailbox_delete(box);
+ if (ret2 < 0) {
+ i_error("Can't delete mailbox %s: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ if (ctx->ctx.subscriptions) {
+ if (mailbox_set_subscribed(box, FALSE) < 0) {
+ i_error("Can't unsubscribe mailbox %s: %s", name,
+ mail_storage_get_last_internal_error(storage, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ }
+ mailbox_free(&box);
+ }
+ return ret;
+}
+
+static void cmd_mailbox_delete_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct delete_cmd_context *ctx = (struct delete_cmd_context *)_ctx;
+ const char *name;
+ unsigned int i;
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("mailbox delete");
+ doveadm_mailbox_args_check(args);
+
+ for (i = 0; args[i] != NULL; i++) {
+ name = p_strdup(ctx->ctx.ctx.pool, args[i]);
+ array_push_back(&ctx->mailboxes, &name);
+ }
+ array_sort(&ctx->mailboxes, i_strcmp_reverse_p);
+}
+
+static bool
+cmd_mailbox_delete_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct delete_cmd_context *ctx = (struct delete_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'r':
+ ctx->recursive = TRUE;
+ break;
+ case 's':
+ ctx->ctx.subscriptions = TRUE;
+ break;
+ case 'e':
+ ctx->require_empty = TRUE;
+ break;
+ case 'Z':
+ ctx->unsafe = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_delete_alloc(void)
+{
+ struct delete_cmd_context *ctx;
+
+ ctx = doveadm_mailbox_cmd_alloc(struct delete_cmd_context);
+ ctx->ctx.ctx.v.init = cmd_mailbox_delete_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_delete_run;
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_delete_parse_arg;
+ ctx->ctx.ctx.getopt_args = "ersZ";
+ p_array_init(&ctx->mailboxes, ctx->ctx.ctx.pool, 16);
+ return &ctx->ctx.ctx;
+}
+
+static int
+cmd_mailbox_rename_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct rename_cmd_context *ctx = (struct rename_cmd_context *)_ctx;
+ struct mail_namespace *oldns, *newns;
+ struct mailbox *oldbox, *newbox;
+ const char *oldname = ctx->oldname;
+ const char *newname = ctx->newname;
+ int ret = 0;
+
+ oldns = mail_namespace_find(user->namespaces, oldname);
+ newns = mail_namespace_find(user->namespaces, newname);
+ oldbox = mailbox_alloc(oldns->list, oldname, 0);
+ newbox = mailbox_alloc(newns->list, newname, 0);
+ if (mailbox_rename(oldbox, newbox) < 0) {
+ i_error("Can't rename mailbox %s to %s: %s", oldname, newname,
+ mailbox_get_last_internal_error(oldbox, NULL));
+ doveadm_mail_failed_mailbox(_ctx, oldbox);
+ ret = -1;
+ }
+ if (ctx->ctx.subscriptions) {
+ if (mailbox_set_subscribed(oldbox, FALSE) < 0) {
+ i_error("Can't unsubscribe mailbox %s: %s", ctx->oldname,
+ mailbox_get_last_internal_error(oldbox, NULL));
+ doveadm_mail_failed_mailbox(_ctx, oldbox);
+ ret = -1;
+ }
+ if (mailbox_set_subscribed(newbox, TRUE) < 0) {
+ i_error("Can't subscribe to mailbox %s: %s", ctx->newname,
+ mailbox_get_last_internal_error(newbox, NULL));
+ doveadm_mail_failed_mailbox(_ctx, newbox);
+ ret = -1;
+ }
+ }
+
+ mailbox_free(&oldbox);
+ mailbox_free(&newbox);
+ return ret;
+}
+
+static void cmd_mailbox_rename_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct rename_cmd_context *ctx = (struct rename_cmd_context *)_ctx;
+
+ if (str_array_length(args) != 2)
+ doveadm_mail_help_name("mailbox rename");
+ doveadm_mailbox_args_check(args);
+
+ ctx->oldname = p_strdup(ctx->ctx.ctx.pool, args[0]);
+ ctx->newname = p_strdup(ctx->ctx.ctx.pool, args[1]);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_rename_alloc(void)
+{
+ struct rename_cmd_context *ctx;
+
+ ctx = doveadm_mailbox_cmd_alloc(struct rename_cmd_context);
+ ctx->ctx.ctx.v.init = cmd_mailbox_rename_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_rename_run;
+ return &ctx->ctx.ctx;
+}
+
+static int
+cmd_mailbox_subscribe_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mailbox_cmd_context *ctx = (struct mailbox_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *name;
+ int ret = 0;
+
+ array_foreach_elem(&ctx->mailboxes, name) {
+ ns = mail_namespace_find(user->namespaces, name);
+ box = mailbox_alloc(ns->list, name, 0);
+ if (mailbox_set_subscribed(box, ctx->ctx.subscriptions) < 0) {
+ i_error("Can't %s mailbox %s: %s", name,
+ ctx->ctx.subscriptions ? "subscribe to" :
+ "unsubscribe",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ mailbox_free(&box);
+ }
+ return ret;
+}
+
+static void cmd_mailbox_subscribe_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct mailbox_cmd_context *ctx = (struct mailbox_cmd_context *)_ctx;
+ const char *name;
+ unsigned int i;
+
+ if (args[0] == NULL) {
+ doveadm_mail_help_name(ctx->ctx.subscriptions ?
+ "mailbox subscribe" :
+ "mailbox unsubscribe");
+ }
+ doveadm_mailbox_args_check(args);
+
+ for (i = 0; args[i] != NULL; i++) {
+ name = p_strdup(ctx->ctx.ctx.pool, args[i]);
+ array_push_back(&ctx->mailboxes, &name);
+ }
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_mailbox_subscriptions_alloc(bool subscriptions)
+{
+ struct mailbox_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct mailbox_cmd_context);
+ ctx->ctx.subscriptions = subscriptions;
+
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_parse_arg;
+ ctx->ctx.ctx.v.init = cmd_mailbox_subscribe_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_subscribe_run;
+ p_array_init(&ctx->mailboxes, ctx->ctx.ctx.pool, 16);
+ return &ctx->ctx.ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_subscribe_alloc(void)
+{
+ return cmd_mailbox_subscriptions_alloc(TRUE);
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_unsubscribe_alloc(void)
+{
+ return cmd_mailbox_subscriptions_alloc(FALSE);
+}
+
+static
+void cmd_mailbox_update_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct update_cmd_context *ctx = (struct update_cmd_context *)_ctx;
+
+ if (str_array_length(args) != 1)
+ doveadm_mail_help_name("mailbox update");
+
+ doveadm_mailbox_args_check(args);
+
+ ctx->mailbox = args[0];
+
+ if ((ctx->update.min_first_recent_uid != 0 ||
+ ctx->update.min_next_uid != 0) &&
+ ctx->update.min_first_recent_uid > ctx->update.min_next_uid) {
+ i_fatal_status(EX_DATAERR,
+ "min_first_recent_uid > min_next_uid");
+ }
+}
+
+static
+bool cmd_mailbox_update_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct update_cmd_context *ctx = (struct update_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'g':
+ if (guid_128_from_string(optarg, ctx->update.mailbox_guid) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ case 'V':
+ if (str_to_uint32(optarg, &(ctx->update.uid_validity)) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ case 'N':
+ if (str_to_uint32(optarg, &(ctx->update.min_next_uid)) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ case 'R':
+ if (str_to_uint32(optarg, &(ctx->update.min_first_recent_uid)) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ case 'H':
+ if (str_to_uint64(optarg, &(ctx->update.min_highest_modseq)) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ case 'P':
+ if (str_to_uint64(optarg, &(ctx->update.min_highest_pvt_modseq)) < 0)
+ doveadm_mail_help_name("mailbox update");
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static
+int cmd_mailbox_update_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct update_cmd_context *ctx = (struct update_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ enum mail_error mail_error;
+ int ret = 0;
+
+ ns = mail_namespace_find(user->namespaces, ctx->mailbox);
+ box = mailbox_alloc(ns->list, ctx->mailbox, 0);
+
+ if ((ret = mailbox_update(box, &(ctx->update))) != 0) {
+ i_error("Cannot update %s: %s",
+ ctx->mailbox,
+ mailbox_get_last_internal_error(box, &mail_error));
+ doveadm_mail_failed_error(_ctx, mail_error);
+ }
+
+ mailbox_free(&box);
+
+ return ret;
+}
+
+static
+struct doveadm_mail_cmd_context *cmd_mailbox_update_alloc(void)
+{
+ struct update_cmd_context *ctx;
+ ctx = doveadm_mail_cmd_alloc(struct update_cmd_context);
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_update_parse_arg;
+ ctx->ctx.ctx.v.init = cmd_mailbox_update_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_update_run;
+ return &ctx->ctx.ctx;
+}
+
+static void
+cmd_mailbox_path_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct update_cmd_context *ctx = (struct update_cmd_context *)_ctx;
+
+ if (str_array_length(args) != 1)
+ doveadm_mail_help_name("mailbox path");
+
+ doveadm_mailbox_args_check(args);
+
+ ctx->mailbox = args[0];
+ doveadm_print_header("path", "path", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+}
+
+static bool
+mailbox_list_path_type_name_parse(const char *name,
+ enum mailbox_list_path_type *type_r)
+{
+ enum mailbox_list_path_type type;
+
+ for (type = 0; type < N_ELEMENTS(mailbox_list_path_type_names); type++) {
+ if (strcmp(mailbox_list_path_type_names[type], name) == 0) {
+ *type_r = type;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+cmd_mailbox_path_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct path_cmd_context *ctx = (struct path_cmd_context *)_ctx;
+
+ switch (c) {
+ case 't':
+ if (!mailbox_list_path_type_name_parse(optarg, &ctx->path_type))
+ doveadm_mail_help_name("mailbox path");
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+cmd_mailbox_path_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct path_cmd_context *ctx = (struct path_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ enum mail_error mail_error;
+ const char *storage_name, *path;
+ int ret;
+
+ ns = mail_namespace_find(user->namespaces, ctx->mailbox);
+ storage_name = mailbox_list_get_storage_name(ns->list, ctx->mailbox);
+ ret = mailbox_list_get_path(ns->list, storage_name, ctx->path_type, &path);
+ if (ret < 0) {
+ i_error("Failed to lookup mailbox %s path: %s",
+ ctx->mailbox,
+ mailbox_list_get_last_internal_error(ns->list, &mail_error));
+ doveadm_mail_failed_error(_ctx, mail_error);
+ } else if (ret > 0) {
+ doveadm_print(path);
+ }
+ return ret;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mailbox_path_alloc(void)
+{
+ struct path_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct path_cmd_context);
+ ctx->path_type = MAILBOX_LIST_PATH_TYPE_INDEX;
+ ctx->ctx.ctx.v.parse_arg = cmd_mailbox_path_parse_arg;
+ ctx->ctx.ctx.v.init = cmd_mailbox_path_init;
+ ctx->ctx.ctx.v.run = cmd_mailbox_path_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return &ctx->ctx.ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_list_ver2 = {
+ .name = "mailbox list",
+ .mail_cmd = cmd_mailbox_list_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-7|-8] [-s] [<mailbox mask> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('7', "mutf7", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('8', "utf8", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('s', "subscriptions", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox-mask", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_create_ver2 = {
+ .name = "mailbox create",
+ .mail_cmd = cmd_mailbox_create_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] [-g <guid>] <mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "subscriptions", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('g', "guid", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_delete_ver2 = {
+ .name = "mailbox delete",
+ .mail_cmd = cmd_mailbox_delete_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-e] [-r] [-s] [-Z] <mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('e', "require-empty", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('s', "subscriptions", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('r', "recursive", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('Z', "unsafe", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_rename_ver2 = {
+ .name = "mailbox rename",
+ .mail_cmd = cmd_mailbox_rename_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-s] <old name> <new name>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('s', "subscriptions", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "new-name", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_subscribe_ver2 = {
+ .name = "mailbox subscribe",
+ .mail_cmd = cmd_mailbox_subscribe_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_unsubscribe_ver2 = {
+ .name = "mailbox unsubscribe",
+ .mail_cmd = cmd_mailbox_unsubscribe_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"<mailbox> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_update_ver2 = {
+ .name = "mailbox update",
+ .mail_cmd = cmd_mailbox_update_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[--mailbox-guid guid] [--uid-validity uid] [--min-next-uid uid] [--min-first-recent-uid uid] [--min-highest-modseq seq] [--min-highest-pvt-modseq seq] <mailbox>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('g', "mailbox-guid", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('V', "uid-validity", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('N', "min-next-uid", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('R', "min-first-recent-uid", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('H', "min-highest-modseq", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('P', "min-highest-pvt-modseq", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_path_ver2 = {
+ .name = "mailbox path",
+ .mail_cmd = cmd_mailbox_path_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-t <type>] <mailbox>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('t', "type", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+};
+
diff --git a/src/doveadm/doveadm-mail-rebuild.c b/src/doveadm/doveadm-mail-rebuild.c
new file mode 100644
index 0000000..211c3c3
--- /dev/null
+++ b/src/doveadm/doveadm-mail-rebuild.c
@@ -0,0 +1,101 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "doveadm-print.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+#include "mail-storage-private.h"
+
+static int
+cmd_rebuild_attachment_box(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ struct mail *mail;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(ctx, info, ctx->search_args,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE|
+ MAIL_FETCH_MESSAGE_PARTS, NULL, 0,
+ &iter) < 0)
+ return -1;
+
+ while (doveadm_mail_iter_next(iter, &mail) && ret >= 0) {
+ T_BEGIN {
+ doveadm_print(dec2str(mail->uid));
+ switch(mail_set_attachment_keywords(mail)) {
+ case -1:
+ doveadm_print("error");
+ doveadm_mail_failed_mailbox(ctx, mail->box);
+ ret = -1;
+ break;
+ case 0:
+ doveadm_print("no");
+ break;
+ case 1:
+ doveadm_print("yes");
+ break;
+ default:
+ i_unreached();
+ }
+ } T_END;
+ }
+
+ if (doveadm_mail_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int
+cmd_rebuild_attachment_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_rebuild_attachment_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_rebuild_attachment_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ doveadm_print_header_simple("uid");
+ doveadm_print_header_simple("attachment");
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+
+static struct doveadm_mail_cmd_context *cmd_rebuild_attachment_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.init = cmd_rebuild_attachment_init;
+ ctx->v.run = cmd_rebuild_attachment_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_rebuild_attachments = {
+ .name = "rebuild attachments",
+ .mail_cmd = cmd_rebuild_attachment_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-save.c b/src/doveadm/doveadm-mail-save.c
new file mode 100644
index 0000000..d8312c8
--- /dev/null
+++ b/src/doveadm/doveadm-mail-save.c
@@ -0,0 +1,142 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "mail-storage.h"
+#include "doveadm-mail.h"
+
+struct save_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ const char *mailbox;
+};
+
+static int
+cmd_save_to_mailbox(struct save_cmd_context *ctx, struct mailbox *box,
+ struct istream *input)
+{
+ struct mail_storage *storage = mailbox_get_storage(box);
+ struct mailbox_transaction_context *trans;
+ struct mail_save_context *save_ctx;
+ ssize_t ret;
+ bool save_failed = FALSE;
+
+ if (input->stream_errno != 0) {
+ i_error("open(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ ctx->ctx.exit_code = EX_TEMPFAIL;
+ return -1;
+ }
+
+ if (mailbox_open(box) < 0) {
+ i_error("Failed to open mailbox %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_storage(&ctx->ctx, storage);
+ return -1;
+ }
+
+ trans = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ ctx->ctx.transaction_flags, __func__);
+ save_ctx = mailbox_save_alloc(trans);
+ if (mailbox_save_begin(&save_ctx, input) < 0) {
+ i_error("Saving failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_storage(&ctx->ctx, storage);
+ mailbox_transaction_rollback(&trans);
+ return -1;
+ }
+ do {
+ if (mailbox_save_continue(save_ctx) < 0) {
+ save_failed = TRUE;
+ ret = -1;
+ break;
+ }
+ } while ((ret = i_stream_read(input)) > 0);
+ i_assert(ret == -1);
+
+ if (input->stream_errno != 0) {
+ i_error("read(msg input) failed: %s", i_stream_get_error(input));
+ doveadm_mail_failed_error(&ctx->ctx, MAIL_ERROR_TEMP);
+ } else if (save_failed) {
+ i_error("Saving failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_storage(&ctx->ctx, storage);
+ } else if (mailbox_save_finish(&save_ctx) < 0) {
+ i_error("Saving failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_storage(&ctx->ctx, storage);
+ } else if (mailbox_transaction_commit(&trans) < 0) {
+ i_error("Save transaction commit failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_storage(&ctx->ctx, storage);
+ } else {
+ ret = 0;
+ }
+ if (save_ctx != NULL)
+ mailbox_save_cancel(&save_ctx);
+ if (trans != NULL)
+ mailbox_transaction_rollback(&trans);
+ i_assert(input->eof);
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+cmd_save_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct save_cmd_context *ctx = (struct save_cmd_context *)_ctx;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ int ret;
+
+ ns = mail_namespace_find(user->namespaces, ctx->mailbox);
+ box = mailbox_alloc(ns->list, ctx->mailbox, MAILBOX_FLAG_SAVEONLY);
+ ret = cmd_save_to_mailbox(ctx, box, _ctx->cmd_input);
+ mailbox_free(&box);
+ return ret;
+}
+
+static void cmd_save_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[] ATTR_UNUSED)
+{
+ doveadm_mail_get_input(_ctx);
+}
+
+static bool
+cmd_mailbox_save_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct save_cmd_context *ctx = (struct save_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'm':
+ ctx->mailbox = optarg;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_save_alloc(void)
+{
+ struct save_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct save_cmd_context);
+ ctx->ctx.getopt_args = "m:";
+ ctx->ctx.v.parse_arg = cmd_mailbox_save_parse_arg;
+ ctx->ctx.v.init = cmd_save_init;
+ ctx->ctx.v.run = cmd_save_run;
+ ctx->mailbox = "INBOX";
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_save_ver2 = {
+ .name = "save",
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX"[-m mailbox]",
+ .mail_cmd = cmd_save_alloc,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('m', "mailbox", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "file", CMD_PARAM_ISTREAM, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-search.c b/src/doveadm/doveadm-mail-search.c
new file mode 100644
index 0000000..37c47e3
--- /dev/null
+++ b/src/doveadm/doveadm-mail-search.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "doveadm-print.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-mail-iter.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+static int
+cmd_search_box(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct doveadm_mail_iter *iter;
+ struct mailbox *box;
+ struct mail *mail;
+ struct mailbox_metadata metadata;
+ const char *guid_str;
+ int ret = 0;
+
+ if (doveadm_mail_iter_init(ctx, info, ctx->search_args, 0, NULL,
+ DOVEADM_MAIL_ITER_FLAG_STOP_WITH_CLIENT,
+ &iter) < 0)
+ return -1;
+ box = doveadm_mail_iter_get_mailbox(iter);
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ i_error("Couldn't get mailbox '%s' GUID: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ doveadm_mail_failed_mailbox(ctx, box);
+ } else {
+ guid_str = guid_128_to_string(metadata.guid);
+ while (doveadm_mail_iter_next(iter, &mail)) {
+ doveadm_print(guid_str);
+ T_BEGIN {
+ doveadm_print(dec2str(mail->uid));
+ } T_END;
+ }
+ }
+ if (doveadm_mail_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int
+cmd_search_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_search_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void cmd_search_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("search");
+
+ doveadm_print_header("mailbox-guid", "mailbox-guid",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ doveadm_print_header("uid", "uid",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+static struct doveadm_mail_cmd_context *cmd_search_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.init = cmd_search_init;
+ ctx->v.run = cmd_search_run;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ return ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_search_ver2 = {
+ .name = "search",
+ .mail_cmd = cmd_search_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mail-server.c b/src/doveadm/doveadm-mail-server.c
new file mode 100644
index 0000000..a829a24
--- /dev/null
+++ b/src/doveadm/doveadm-mail-server.c
@@ -0,0 +1,404 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "ioloop.h"
+#include "master-service.h"
+#include "iostream-ssl.h"
+#include "auth-master.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "server-connection.h"
+#include "doveadm-settings.h"
+#include "doveadm-print.h"
+#include "doveadm-server.h"
+#include "doveadm-mail.h"
+
+#define DOVEADM_SERVER_CONNECTIONS_MAX 4
+#define DOVEADM_SERVER_QUEUE_MAX 16
+
+#define DOVEADM_MAIL_SERVER_FAILED() \
+ (internal_failure || master_service_is_killed(master_service))
+
+struct doveadm_mail_server_cmd {
+ struct server_connection *conn;
+ char *username;
+};
+
+static HASH_TABLE(char *, struct doveadm_server *) servers;
+static pool_t server_pool;
+static struct doveadm_mail_cmd_context *cmd_ctx;
+static bool internal_failure = FALSE;
+
+static void doveadm_mail_server_handle(struct server_connection *conn,
+ const char *username);
+
+static struct doveadm_server *
+doveadm_server_get(struct doveadm_mail_cmd_context *ctx, const char *name)
+{
+ struct doveadm_server *server;
+ const char *p;
+ char *dup_name;
+
+ if (!hash_table_is_created(servers)) {
+ server_pool = pool_alloconly_create("doveadm servers", 1024*16);
+ hash_table_create(&servers, server_pool, 0, str_hash, strcmp);
+ }
+ server = hash_table_lookup(servers, name);
+ if (server == NULL) {
+ server = p_new(server_pool, struct doveadm_server, 1);
+ server->name = dup_name = p_strdup(server_pool, name);
+ p = strrchr(server->name, ':');
+ server->hostname = p == NULL ? server->name :
+ p_strdup_until(server_pool, server->name, p);
+
+ p_array_init(&server->connections, server_pool,
+ ctx->set->doveadm_worker_count);
+ p_array_init(&server->queue, server_pool,
+ DOVEADM_SERVER_QUEUE_MAX);
+ hash_table_insert(servers, dup_name, server);
+ }
+ return server;
+}
+
+static struct server_connection *
+doveadm_server_find_unused_conn(struct doveadm_server *server)
+{
+ struct server_connection *conn;
+
+ array_foreach_elem(&server->connections, conn) {
+ if (server_connection_is_idle(conn))
+ return conn;
+ }
+ return NULL;
+}
+
+static bool doveadm_server_have_used_connections(struct doveadm_server *server)
+{
+ struct server_connection *conn;
+
+ array_foreach_elem(&server->connections, conn) {
+ if (!server_connection_is_idle(conn))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void doveadm_cmd_callback(int exit_code, const char *error,
+ void *context)
+{
+ struct doveadm_mail_server_cmd *servercmd = context;
+ struct doveadm_server *server =
+ server_connection_get_server(servercmd->conn);
+ const char *username = t_strdup(servercmd->username);
+
+ i_free(servercmd->username);
+ i_free(servercmd);
+
+ switch (exit_code) {
+ case 0:
+ break;
+ case SERVER_EXIT_CODE_DISCONNECTED:
+ i_error("%s: Command %s failed for %s: %s",
+ server->name, cmd_ctx->cmd->name, username, error);
+ internal_failure = TRUE;
+ io_loop_stop(current_ioloop);
+ return;
+ case EX_NOUSER:
+ i_error("%s: No such user: %s", server->name, username);
+ if (cmd_ctx->exit_code == 0)
+ cmd_ctx->exit_code = EX_NOUSER;
+ break;
+ default:
+ if (cmd_ctx->exit_code == 0 || exit_code == EX_TEMPFAIL)
+ cmd_ctx->exit_code = exit_code;
+ break;
+ }
+
+ if (array_count(&server->queue) > 0) {
+ struct server_connection *conn;
+ char *const *usernamep = array_front(&server->queue);
+ char *username = *usernamep;
+
+ conn = doveadm_server_find_unused_conn(server);
+ if (conn != NULL) {
+ array_pop_front(&server->queue);
+ doveadm_mail_server_handle(conn, username);
+ i_free(username);
+ }
+ }
+
+ io_loop_stop(current_ioloop);
+}
+
+static void doveadm_mail_server_handle(struct server_connection *conn,
+ const char *username)
+{
+ struct doveadm_mail_server_cmd *servercmd;
+ string_t *cmd;
+ unsigned int i;
+
+ /* <flags> <username> <command> [<args>] */
+ cmd = t_str_new(256);
+ if (doveadm_debug)
+ str_append_c(cmd, 'D');
+ else if (doveadm_verbose)
+ str_append_c(cmd, 'v');
+ str_append_c(cmd, '\t');
+
+ str_append_tabescaped(cmd, username);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, cmd_ctx->cmd->name);
+ for (i = 0; cmd_ctx->full_args[i] != NULL; i++) {
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, cmd_ctx->full_args[i]);
+ }
+ str_append_c(cmd, '\n');
+
+ servercmd = i_new(struct doveadm_mail_server_cmd, 1);
+ servercmd->conn = conn;
+ servercmd->username = i_strdup(username);
+ server_connection_cmd(conn, str_c(cmd), cmd_ctx->cmd_input,
+ doveadm_cmd_callback, servercmd);
+}
+
+static void doveadm_server_flush_one(struct doveadm_server *server)
+{
+ unsigned int count = array_count(&server->queue);
+
+ do {
+ io_loop_run(current_ioloop);
+ } while (array_count(&server->queue) == count &&
+ doveadm_server_have_used_connections(server) &&
+ !DOVEADM_MAIL_SERVER_FAILED());
+}
+
+static int
+doveadm_mail_server_user_get_host(struct doveadm_mail_cmd_context *ctx,
+ const struct mail_storage_service_input *input,
+ const char **user_r, const char **host_r,
+ struct ip_addr *hostip_r, in_port_t *port_r,
+ enum doveadm_proxy_ssl_flags *ssl_flags_r,
+ const char **error_r)
+{
+ struct auth_master_connection *auth_conn;
+ struct auth_user_info info;
+ pool_t pool;
+ const char *auth_socket_path, *proxy_host, *proxy_hostip, *const *fields;
+ unsigned int i;
+ in_port_t proxy_port;
+ bool proxying;
+ int ret;
+
+ *user_r = input->username;
+ *host_r = ctx->set->doveadm_socket_path;
+ *port_r = ctx->set->doveadm_port;
+
+ if (ctx->set->doveadm_port == 0)
+ return 0;
+
+ if (strcmp(ctx->set->doveadm_ssl, "ssl") == 0)
+ *ssl_flags_r |= PROXY_SSL_FLAG_YES;
+ else if (strcmp(ctx->set->doveadm_ssl, "starttls") == 0)
+ *ssl_flags_r |= PROXY_SSL_FLAG_YES | PROXY_SSL_FLAG_STARTTLS;
+
+ /* make sure we have an auth connection */
+ mail_storage_service_init_settings(ctx->storage_service, input);
+
+ i_zero(&info);
+ info.service = master_service_get_name(master_service);
+ info.local_ip = input->local_ip;
+ info.remote_ip = input->remote_ip;
+ info.local_port = input->local_port;
+ info.remote_port = input->remote_port;
+
+ pool = pool_alloconly_create("auth lookup", 1024);
+ auth_conn = mail_storage_service_get_auth_conn(ctx->storage_service);
+ auth_socket_path = auth_master_get_socket_path(auth_conn);
+ ret = auth_master_pass_lookup(auth_conn, input->username, &info,
+ pool, &fields);
+ if (ret < 0) {
+ *error_r = fields[0] != NULL ?
+ t_strdup(fields[0]) : "passdb lookup failed";
+ *error_r = t_strdup_printf("%s: %s (to see if user is proxied, "
+ "because doveadm_port is set)",
+ auth_socket_path, *error_r);
+ } else if (ret == 0) {
+ /* user not found from passdb. it could be in userdb though,
+ so just continue with the default host */
+ } else {
+ proxy_host = NULL; proxy_hostip = NULL; proxying = FALSE;
+ proxy_port = ctx->set->doveadm_port;
+ for (i = 0; fields[i] != NULL; i++) {
+ if (str_begins(fields[i], "proxy") &&
+ (fields[i][5] == '\0' || fields[i][5] == '='))
+ proxying = TRUE;
+ else if (str_begins(fields[i], "host="))
+ proxy_host = fields[i]+5;
+ else if (str_begins(fields[i], "hostip="))
+ proxy_hostip = fields[i]+7;
+ else if (str_begins(fields[i], "user="))
+ *user_r = t_strdup(fields[i]+5);
+ else if (str_begins(fields[i], "destuser="))
+ *user_r = t_strdup(fields[i]+9);
+ else if (str_begins(fields[i], "port=")) {
+ if (net_str2port(fields[i]+5, &proxy_port) < 0)
+ proxy_port = 0;
+ } else if (str_begins(fields[i], "ssl=")) {
+ *ssl_flags_r |= PROXY_SSL_FLAG_YES;
+ if (strcmp(fields[i]+4, "any-cert") == 0)
+ *ssl_flags_r |= PROXY_SSL_FLAG_ANY_CERT;
+ } else if (str_begins(fields[i], "starttls=")) {
+ *ssl_flags_r |= PROXY_SSL_FLAG_YES |
+ PROXY_SSL_FLAG_STARTTLS;
+ if (strcmp(fields[i]+9, "any-cert") == 0)
+ *ssl_flags_r |= PROXY_SSL_FLAG_ANY_CERT;
+ }
+ }
+ if (proxy_hostip != NULL &&
+ net_addr2ip(proxy_hostip, hostip_r) < 0) {
+ *error_r = t_strdup_printf("%s Invalid hostip value '%s'",
+ auth_socket_path, proxy_hostip);
+ ret = -1;
+ }
+ if (!proxying)
+ ret = 0;
+ else if (proxy_host == NULL) {
+ *error_r = t_strdup_printf("%s: Proxy is missing destination host",
+ auth_socket_path);
+ if (strstr(auth_socket_path, "/auth-userdb") != NULL) {
+ *error_r = t_strdup_printf(
+ "%s (maybe set auth_socket_path=director-userdb)",
+ *error_r);
+ }
+ ret = -1;
+ } else {
+ *port_r = proxy_port;
+ *host_r = t_strdup_printf("%s:%u", proxy_host, proxy_port);
+ }
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+int doveadm_mail_server_user(struct doveadm_mail_cmd_context *ctx,
+ const struct mail_storage_service_input *input,
+ const char **error_r)
+{
+ struct doveadm_server *server;
+ struct server_connection *conn;
+ const char *user, *host;
+ struct ip_addr hostip;
+ enum doveadm_proxy_ssl_flags ssl_flags = 0;
+ char *username_dup;
+ int ret;
+ in_port_t port;
+
+ i_assert(cmd_ctx == ctx || cmd_ctx == NULL);
+ cmd_ctx = ctx;
+
+ i_zero(&hostip);
+ ret = doveadm_mail_server_user_get_host(ctx, input, &user, &host, &hostip,
+ &port, &ssl_flags, error_r);
+ if (ret < 0)
+ return ret;
+ if (ret == 0 &&
+ (ctx->set->doveadm_worker_count == 0 || doveadm_server)) {
+ /* run it ourself */
+ return 0;
+ }
+
+ /* server sends the sticky headers for each row as well,
+ so undo any sticks we might have added already */
+ doveadm_print_unstick_headers();
+
+ server = doveadm_server_get(ctx, host);
+ server->ip = hostip;
+ server->ssl_flags = ssl_flags;
+ server->port = port;
+ conn = doveadm_server_find_unused_conn(server);
+ if (conn != NULL)
+ doveadm_mail_server_handle(conn, user);
+ else if (array_count(&server->connections) <
+ I_MAX(ctx->set->doveadm_worker_count, 1)) {
+ if (server_connection_create(server, &conn, error_r) < 0) {
+ internal_failure = TRUE;
+ return -1;
+ } else {
+ doveadm_mail_server_handle(conn, user);
+ }
+ } else {
+ if (array_count(&server->queue) >= DOVEADM_SERVER_QUEUE_MAX)
+ doveadm_server_flush_one(server);
+
+ username_dup = i_strdup(user);
+ array_push_back(&server->queue, &username_dup);
+ }
+ *error_r = "doveadm server failure";
+ return DOVEADM_MAIL_SERVER_FAILED() ? -1 : 1;
+}
+
+static struct doveadm_server *doveadm_server_find_used(void)
+{
+ struct hash_iterate_context *iter;
+ struct doveadm_server *ret = NULL;
+ char *key;
+ struct doveadm_server *server;
+
+ iter = hash_table_iterate_init(servers);
+ while (hash_table_iterate(iter, servers, &key, &server)) {
+ if (doveadm_server_have_used_connections(server)) {
+ ret = server;
+ break;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+ return ret;
+}
+
+static void doveadm_servers_destroy_all_connections(void)
+{
+ struct hash_iterate_context *iter;
+ char *key;
+ struct doveadm_server *server;
+
+ iter = hash_table_iterate_init(servers);
+ while (hash_table_iterate(iter, servers, &key, &server)) {
+ while (array_count(&server->connections) > 0) {
+ struct server_connection *const *connp, *conn;
+
+ connp = array_front(&server->connections);
+ conn = *connp;
+ server_connection_destroy(&conn);
+ }
+ ssl_iostream_context_unref(&server->ssl_ctx);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+void doveadm_mail_server_flush(void)
+{
+ struct doveadm_server *server;
+
+ if (!hash_table_is_created(servers)) {
+ cmd_ctx = NULL;
+ return;
+ }
+
+ while ((server = doveadm_server_find_used()) != NULL &&
+ !DOVEADM_MAIL_SERVER_FAILED())
+ doveadm_server_flush_one(server);
+
+ doveadm_servers_destroy_all_connections();
+ if (master_service_is_killed(master_service))
+ i_error("Aborted");
+ if (DOVEADM_MAIL_SERVER_FAILED())
+ doveadm_mail_failed_error(cmd_ctx, MAIL_ERROR_TEMP);
+
+ hash_table_destroy(&servers);
+ pool_unref(&server_pool);
+ cmd_ctx = NULL;
+}
diff --git a/src/doveadm/doveadm-mail.c b/src/doveadm/doveadm-mail.c
new file mode 100644
index 0000000..b085231
--- /dev/null
+++ b/src/doveadm/doveadm-mail.c
@@ -0,0 +1,998 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "lib-signals.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-dot.h"
+#include "istream-seekable.h"
+#include "str.h"
+#include "unichar.h"
+#include "module-dir.h"
+#include "wildcard-match.h"
+#include "master-service.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "mail-storage-service.h"
+#include "mail-storage-hooks.h"
+#include "mail-search-build.h"
+#include "mail-search-parser.h"
+#include "mailbox-list-iter.h"
+#include "doveadm.h"
+#include "client-connection.h"
+#include "doveadm-settings.h"
+#include "doveadm-print.h"
+#include "doveadm-dsync.h"
+#include "doveadm-mail.h"
+
+#include <stdio.h>
+
+#define DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS (5*60*1000)
+
+struct force_resync_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool fsck;
+};
+
+void (*hook_doveadm_mail_init)(struct doveadm_mail_cmd_context *ctx);
+struct doveadm_mail_cmd_module_register
+ doveadm_mail_cmd_module_register = { 0 };
+char doveadm_mail_cmd_hide = '\0';
+
+static int killed_signo = 0;
+
+bool doveadm_is_killed(void)
+{
+ return killed_signo != 0;
+}
+
+int doveadm_killed_signo(void)
+{
+ return killed_signo;
+}
+
+void doveadm_mail_failed_error(struct doveadm_mail_cmd_context *ctx,
+ enum mail_error error)
+{
+ int exit_code = EX_TEMPFAIL;
+
+ switch (error) {
+ case MAIL_ERROR_NONE:
+ i_unreached();
+ case MAIL_ERROR_TEMP:
+ case MAIL_ERROR_UNAVAILABLE:
+ break;
+ case MAIL_ERROR_NOTPOSSIBLE:
+ case MAIL_ERROR_EXISTS:
+ case MAIL_ERROR_CONVERSION:
+ case MAIL_ERROR_INVALIDDATA:
+ exit_code = DOVEADM_EX_NOTPOSSIBLE;
+ break;
+ case MAIL_ERROR_PARAMS:
+ exit_code = EX_USAGE;
+ break;
+ case MAIL_ERROR_PERM:
+ exit_code = EX_NOPERM;
+ break;
+ case MAIL_ERROR_NOQUOTA:
+ exit_code = EX_CANTCREAT;
+ break;
+ case MAIL_ERROR_NOTFOUND:
+ exit_code = DOVEADM_EX_NOTFOUND;
+ break;
+ case MAIL_ERROR_EXPUNGED:
+ break;
+ case MAIL_ERROR_INUSE:
+ case MAIL_ERROR_LIMIT:
+ exit_code = DOVEADM_EX_NOTPOSSIBLE;
+ break;
+ case MAIL_ERROR_LOOKUP_ABORTED:
+ break;
+ }
+ /* tempfail overrides all other exit codes, otherwise use whatever
+ error happened first */
+ if (ctx->exit_code == 0 || exit_code == EX_TEMPFAIL)
+ ctx->exit_code = exit_code;
+}
+
+void doveadm_mail_failed_storage(struct doveadm_mail_cmd_context *ctx,
+ struct mail_storage *storage)
+{
+ enum mail_error error;
+
+ mail_storage_get_last_error(storage, &error);
+ doveadm_mail_failed_error(ctx, error);
+}
+
+void doveadm_mail_failed_mailbox(struct doveadm_mail_cmd_context *ctx,
+ struct mailbox *box)
+{
+ doveadm_mail_failed_storage(ctx, mailbox_get_storage(box));
+}
+
+void doveadm_mail_failed_list(struct doveadm_mail_cmd_context *ctx,
+ struct mailbox_list *list)
+{
+ enum mail_error error;
+
+ mailbox_list_get_last_error(list, &error);
+ doveadm_mail_failed_error(ctx, error);
+}
+
+struct doveadm_mail_cmd_context *
+doveadm_mail_cmd_alloc_size(size_t size)
+{
+ struct doveadm_mail_cmd_context *ctx;
+ pool_t pool;
+
+ i_assert(size >= sizeof(struct doveadm_mail_cmd_context));
+
+ pool = pool_alloconly_create("doveadm mail cmd", 1024);
+ ctx = p_malloc(pool, size);
+ ctx->pool = pool;
+ ctx->cmd_input_fd = -1;
+ return ctx;
+}
+
+static int
+cmd_purge_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ struct mail_namespace *ns;
+ struct mail_storage *storage;
+ int ret = 0;
+
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->type != MAIL_NAMESPACE_TYPE_PRIVATE ||
+ ns->alias_for != NULL)
+ continue;
+
+ storage = mail_namespace_get_default_storage(ns);
+ if (mail_storage_purge(storage) < 0) {
+ i_error("Purging namespace '%s' failed: %s", ns->prefix,
+ mail_storage_get_last_internal_error(storage, NULL));
+ doveadm_mail_failed_storage(ctx, storage);
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static struct doveadm_mail_cmd_context *cmd_purge_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_purge_run;
+ return ctx;
+}
+
+static void doveadm_mail_cmd_input_input(struct doveadm_mail_cmd_context *ctx)
+{
+ const unsigned char *data;
+ size_t size;
+
+ while (i_stream_read_more(ctx->cmd_input, &data, &size) > 0)
+ i_stream_skip(ctx->cmd_input, size);
+ if (!ctx->cmd_input->eof)
+ return;
+
+ if (ctx->cmd_input->stream_errno != 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(ctx->cmd_input),
+ i_stream_get_error(ctx->cmd_input));
+ }
+ io_loop_stop(current_ioloop);
+}
+
+static void doveadm_mail_cmd_input_timeout(struct doveadm_mail_cmd_context *ctx)
+{
+ struct istream *input;
+
+ input = i_stream_create_error_str(ETIMEDOUT, "Timed out in %u secs",
+ DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS/1000);
+ i_stream_set_name(input, i_stream_get_name(ctx->cmd_input));
+ i_stream_destroy(&ctx->cmd_input);
+ ctx->cmd_input = input;
+ ctx->exit_code = EX_TEMPFAIL;
+ io_loop_stop(current_ioloop);
+}
+
+static void doveadm_mail_cmd_input_read(struct doveadm_mail_cmd_context *ctx)
+{
+ struct ioloop *ioloop;
+ struct io *io;
+ struct timeout *to;
+
+ ioloop = io_loop_create();
+ /* Read the pending input from stream. Delay adding the IO in case
+ we're reading from a file. That would cause a panic with epoll. */
+ io_loop_set_running(ioloop);
+ doveadm_mail_cmd_input_input(ctx);
+ if (io_loop_is_running(ioloop)) {
+ io = io_add(ctx->cmd_input_fd, IO_READ,
+ doveadm_mail_cmd_input_input, ctx);
+ to = timeout_add(DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS,
+ doveadm_mail_cmd_input_timeout, ctx);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ timeout_remove(&to);
+ }
+ io_loop_destroy(&ioloop);
+
+ i_assert(ctx->cmd_input->eof);
+ i_stream_seek(ctx->cmd_input, 0);
+}
+
+void doveadm_mail_get_input(struct doveadm_mail_cmd_context *ctx)
+{
+ const struct doveadm_cmd_context *cctx = ctx->cctx;
+ bool cli = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+ struct istream *inputs[2];
+
+ if (ctx->cmd_input != NULL)
+ return;
+
+ if (!cli && cctx->input == NULL) {
+ ctx->cmd_input = i_stream_create_error_str(EINVAL, "Input stream missing (provide with file parameter)");
+ return;
+ }
+
+ if (!cli)
+ inputs[0] = i_stream_create_dot(cctx->input, FALSE);
+ else {
+ inputs[0] = i_stream_create_fd(STDIN_FILENO, 1024*1024);
+ i_stream_set_name(inputs[0], "stdin");
+ }
+ inputs[1] = NULL;
+ ctx->cmd_input_fd = i_stream_get_fd(inputs[0]);
+ ctx->cmd_input = i_stream_create_seekable_path(inputs, 1024*256,
+ "/tmp/doveadm.");
+ i_stream_set_name(ctx->cmd_input, i_stream_get_name(inputs[0]));
+ i_stream_unref(&inputs[0]);
+
+ doveadm_mail_cmd_input_read(ctx);
+}
+
+struct mailbox *
+doveadm_mailbox_find(struct mail_user *user, const char *mailbox)
+{
+ struct mail_namespace *ns;
+
+ if (!uni_utf8_str_is_valid(mailbox)) {
+ i_fatal_status(EX_DATAERR,
+ "Mailbox name not valid UTF-8: %s", mailbox);
+ }
+
+ ns = mail_namespace_find(user->namespaces, mailbox);
+ return mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_IGNORE_ACLS);
+}
+
+struct mail_search_args *
+doveadm_mail_build_search_args(const char *const args[])
+{
+ struct mail_search_parser *parser;
+ struct mail_search_args *sargs;
+ const char *error, *charset = "UTF-8";
+
+ parser = mail_search_parser_init_cmdline(args);
+ if (mail_search_build(mail_search_register_get_human(),
+ parser, &charset, &sargs, &error) < 0)
+ i_fatal("%s", error);
+ mail_search_parser_deinit(&parser);
+ return sargs;
+}
+
+static int cmd_force_resync_box(struct doveadm_mail_cmd_context *_ctx,
+ const struct mailbox_info *info)
+{
+ struct force_resync_cmd_context *ctx =
+ (struct force_resync_cmd_context *)_ctx;
+ enum mailbox_flags flags = MAILBOX_FLAG_IGNORE_ACLS;
+ struct mailbox *box;
+ int ret = 0;
+
+ if (ctx->fsck)
+ flags |= MAILBOX_FLAG_FSCK;
+
+ box = mailbox_alloc(info->ns->list, info->vname, flags);
+ if (mailbox_open(box) < 0) {
+ i_error("Opening mailbox %s failed: %s", info->vname,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ } else if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FORCE_RESYNC |
+ MAILBOX_SYNC_FLAG_FIX_INCONSISTENT) < 0) {
+ i_error("Forcing a resync on mailbox %s failed: %s",
+ info->vname, mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(_ctx, box);
+ ret = -1;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int cmd_force_resync_prerun(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ struct mail_storage_service_user *service_user,
+ const char **error_r)
+{
+ if (mail_storage_service_user_set_setting(service_user,
+ "mailbox_list_index_very_dirty_syncs",
+ "no",
+ error_r) <= 0)
+ i_unreached();
+ return 0;
+}
+
+static int cmd_force_resync_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS |
+ MAILBOX_LIST_ITER_STAR_WITHIN_NS;
+ const enum mail_namespace_type ns_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = mailbox_list_iter_init_namespaces(user->namespaces, ctx->args,
+ ns_mask, iter_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags & (MAILBOX_NOSELECT |
+ MAILBOX_NONEXISTENT)) == 0) T_BEGIN {
+ if (cmd_force_resync_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ i_error("Listing mailboxes failed: %s",
+ mailbox_list_get_last_internal_error(user->namespaces->list, NULL));
+ doveadm_mail_failed_list(ctx, user->namespaces->list);
+ ret = -1;
+ }
+ return ret;
+}
+
+static void
+cmd_force_resync_init(struct doveadm_mail_cmd_context *_ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("force-resync");
+}
+
+static bool
+cmd_force_resync_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct force_resync_cmd_context *ctx =
+ (struct force_resync_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'f':
+ ctx->fsck = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *cmd_force_resync_alloc(void)
+{
+ struct force_resync_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct force_resync_cmd_context);
+ ctx->ctx.getopt_args = "f";
+ ctx->ctx.v.parse_arg = cmd_force_resync_parse_arg;
+ ctx->ctx.v.init = cmd_force_resync_init;
+ ctx->ctx.v.run = cmd_force_resync_run;
+ ctx->ctx.v.prerun = cmd_force_resync_prerun;
+ return &ctx->ctx;
+}
+
+static void
+doveadm_cctx_to_storage_service_input(const struct doveadm_cmd_context *cctx,
+ struct mail_storage_service_input *input_r)
+{
+ i_zero(input_r);
+ input_r->service = "doveadm";
+ input_r->remote_ip = cctx->remote_ip;
+ input_r->remote_port = cctx->remote_port;
+ input_r->local_ip = cctx->local_ip;
+ input_r->local_port = cctx->local_port;
+ input_r->username = cctx->username;
+}
+
+static int
+doveadm_mail_next_user(struct doveadm_mail_cmd_context *ctx,
+ const char **error_r)
+{
+ const struct doveadm_cmd_context *cctx = ctx->cctx;
+ struct mail_storage_service_input input;
+ const char *error, *ip;
+ int ret;
+
+ i_assert(cctx != NULL);
+
+ ip = net_ip2addr(&cctx->remote_ip);
+ if (ip[0] == '\0')
+ i_set_failure_prefix("doveadm(%s): ", cctx->username);
+ else
+ i_set_failure_prefix("doveadm(%s,%s): ", ip, cctx->username);
+ doveadm_cctx_to_storage_service_input(cctx, &input);
+ if (ctx->cmd_input != NULL)
+ i_stream_seek(ctx->cmd_input, 0);
+
+ /* see if we want to execute this command via (another)
+ doveadm server */
+ ret = doveadm_mail_server_user(ctx, &input, error_r);
+ if (ret != 0)
+ return ret;
+
+ ret = mail_storage_service_lookup(ctx->storage_service, &input,
+ &ctx->cur_service_user, &error);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("User lookup failed: %s",
+ error);
+ }
+ return ret;
+ }
+
+ if (ctx->v.prerun != NULL) {
+ if (ctx->v.prerun(ctx, ctx->cur_service_user, error_r) < 0) {
+ mail_storage_service_user_unref(&ctx->cur_service_user);
+ return -1;
+ }
+ }
+
+ ret = mail_storage_service_next(ctx->storage_service,
+ ctx->cur_service_user,
+ &ctx->cur_mail_user, error_r);
+ if (ret < 0) {
+ mail_storage_service_user_unref(&ctx->cur_service_user);
+ return ret;
+ }
+
+ /* Create the event outside the active ioloop context, so if run()
+ switches the ioloop context it won't try to pop out the event_reason
+ from global events. */
+ struct ioloop_context *cur_ctx =
+ io_loop_get_current_context(current_ioloop);
+ io_loop_context_deactivate(cur_ctx);
+ struct event_reason *reason =
+ event_reason_begin(event_reason_code_prefix("doveadm", "cmd_",
+ ctx->cmd->name));
+ io_loop_context_activate(cur_ctx);
+
+ T_BEGIN {
+ if (ctx->v.run(ctx, ctx->cur_mail_user) < 0) {
+ i_assert(ctx->exit_code != 0);
+ }
+ } T_END;
+ mail_user_deinit(&ctx->cur_mail_user);
+ mail_storage_service_user_unref(&ctx->cur_service_user);
+ /* User deinit may still do some work, so finish the reason after it.
+ Also, this needs to be after the ioloop context is deactivated. */
+ event_reason_end(&reason);
+ return 1;
+}
+
+static void sig_die(const siginfo_t *si, void *context ATTR_UNUSED)
+{
+ killed_signo = si->si_signo;
+}
+
+int doveadm_mail_single_user(struct doveadm_mail_cmd_context *ctx,
+ const char **error_r)
+{
+ const struct doveadm_cmd_context *cctx = ctx->cctx;
+
+ i_assert(cctx->username != NULL);
+
+ doveadm_cctx_to_storage_service_input(cctx, &ctx->storage_service_input);
+ ctx->storage_service = mail_storage_service_init(master_service, NULL,
+ ctx->service_flags);
+ ctx->v.init(ctx, ctx->args);
+ if (hook_doveadm_mail_init != NULL)
+ hook_doveadm_mail_init(ctx);
+
+ lib_signals_set_handler(SIGINT, 0, sig_die, NULL);
+ lib_signals_set_handler(SIGTERM, 0, sig_die, NULL);
+
+ return doveadm_mail_next_user(ctx, error_r);
+}
+
+static void
+doveadm_mail_all_users(struct doveadm_mail_cmd_context *ctx,
+ const char *wildcard_user)
+{
+ struct doveadm_cmd_context *cctx = ctx->cctx;
+ unsigned int user_idx;
+ const char *ip, *user, *error;
+ int ret;
+
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+
+ doveadm_cctx_to_storage_service_input(cctx, &ctx->storage_service_input);
+ ctx->storage_service = mail_storage_service_init(master_service, NULL,
+ ctx->service_flags);
+ lib_signals_set_handler(SIGINT, 0, sig_die, NULL);
+ lib_signals_set_handler(SIGTERM, 0, sig_die, NULL);
+
+ ctx->v.init(ctx, ctx->args);
+
+ mail_storage_service_all_init_mask(ctx->storage_service,
+ wildcard_user != NULL ? wildcard_user : "");
+
+ if (hook_doveadm_mail_init != NULL)
+ hook_doveadm_mail_init(ctx);
+
+ user_idx = 0;
+ while ((ret = ctx->v.get_next_user(ctx, &user)) > 0) {
+ if (wildcard_user != NULL) {
+ if (!wildcard_match_icase(user, wildcard_user))
+ continue;
+ }
+ cctx->username = user;
+ doveadm_print_sticky("username", user);
+ T_BEGIN {
+ ret = doveadm_mail_next_user(ctx, &error);
+ if (ret < 0)
+ i_error("%s", error);
+ else if (ret == 0)
+ i_info("User no longer exists, skipping");
+ } T_END;
+ if (ret == -1)
+ break;
+ if (doveadm_verbose) {
+ if (++user_idx % 100 == 0) {
+ printf("\r%d", user_idx);
+ fflush(stdout);
+ }
+ }
+ if (killed_signo != 0) {
+ i_warning("Killed with signal %d", killed_signo);
+ ret = -1;
+ break;
+ }
+ }
+ if (doveadm_verbose)
+ printf("\n");
+ ip = net_ip2addr(&cctx->remote_ip);
+ if (ip[0] == '\0')
+ i_set_failure_prefix("doveadm: ");
+ else
+ i_set_failure_prefix("doveadm(%s): ", ip);
+ if (ret < 0) {
+ i_error("Failed to iterate through some users");
+ ctx->exit_code = EX_TEMPFAIL;
+ }
+}
+
+static void
+doveadm_mail_cmd_init_noop(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[] ATTR_UNUSED)
+{
+}
+
+static int
+doveadm_mail_cmd_get_next_user(struct doveadm_mail_cmd_context *ctx,
+ const char **username_r)
+{
+ if (ctx->users_list_input == NULL)
+ return mail_storage_service_all_next(ctx->storage_service, username_r);
+
+ *username_r = i_stream_read_next_line(ctx->users_list_input);
+ if (ctx->users_list_input->stream_errno != 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(ctx->users_list_input),
+ i_stream_get_error(ctx->users_list_input));
+ return -1;
+ }
+ return *username_r != NULL ? 1 : 0;
+}
+
+static void
+doveadm_mail_cmd_deinit_noop(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED)
+{
+}
+
+struct doveadm_mail_cmd_context *
+doveadm_mail_cmd_init(const struct doveadm_mail_cmd *cmd,
+ const struct doveadm_settings *set)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = cmd->alloc();
+ ctx->set = set;
+ ctx->cmd = cmd;
+ if (ctx->v.init == NULL)
+ ctx->v.init = doveadm_mail_cmd_init_noop;
+ if (ctx->v.get_next_user == NULL)
+ ctx->v.get_next_user = doveadm_mail_cmd_get_next_user;
+ if (ctx->v.deinit == NULL)
+ ctx->v.deinit = doveadm_mail_cmd_deinit_noop;
+
+ p_array_init(&ctx->module_contexts, ctx->pool, 5);
+ return ctx;
+}
+
+static struct doveadm_mail_cmd_context *
+doveadm_mail_cmdline_init(const struct doveadm_mail_cmd *cmd)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_init(cmd, doveadm_settings);
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
+ if (doveadm_debug)
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_DEBUG;
+ return ctx;
+}
+
+static void
+doveadm_mail_cmd_exec(struct doveadm_mail_cmd_context *ctx,
+ const char *wildcard_user)
+{
+ const struct doveadm_cmd_context *cctx = ctx->cctx;
+ bool cli = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+ int ret;
+ const char *error;
+
+ if (ctx->v.preinit != NULL)
+ ctx->v.preinit(ctx);
+
+ ctx->iterate_single_user =
+ !ctx->iterate_all_users && wildcard_user == NULL;
+ if (doveadm_print_is_initialized() &&
+ (!ctx->iterate_single_user || ctx->add_username_header)) {
+ doveadm_print_header("username", "Username",
+ DOVEADM_PRINT_HEADER_FLAG_STICKY |
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ }
+
+ if (ctx->iterate_single_user) {
+ if (cctx->username == NULL)
+ i_fatal_status(EX_USAGE, "USER environment is missing and -u option not used");
+ if (!cli) {
+ /* we may access multiple users */
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP;
+ }
+
+ if (ctx->add_username_header)
+ doveadm_print_sticky("username", cctx->username);
+ ret = doveadm_mail_single_user(ctx, &error);
+ if (ret < 0) {
+ /* user lookup/init failed somehow */
+ doveadm_exit_code = EX_TEMPFAIL;
+ i_error("%s", error);
+ } else if (ret == 0) {
+ doveadm_exit_code = EX_NOUSER;
+ i_error("User doesn't exist");
+ }
+ } else {
+ ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP;
+ doveadm_mail_all_users(ctx, wildcard_user);
+ }
+ doveadm_mail_server_flush();
+ doveadm_mail_cmd_deinit(ctx);
+ doveadm_print_flush();
+
+ /* service deinit unloads mail plugins, so do it late */
+ mail_storage_service_deinit(&ctx->storage_service);
+
+ if (ctx->exit_code != 0)
+ doveadm_exit_code = ctx->exit_code;
+}
+
+void doveadm_mail_cmd_deinit(struct doveadm_mail_cmd_context *ctx)
+{
+ ctx->v.deinit(ctx);
+ if (ctx->search_args != NULL)
+ mail_search_args_unref(&ctx->search_args);
+}
+
+void doveadm_mail_cmd_free(struct doveadm_mail_cmd_context *ctx)
+{
+ i_stream_unref(&ctx->users_list_input);
+ i_stream_unref(&ctx->cmd_input);
+ pool_unref(&ctx->pool);
+}
+
+void doveadm_mail_help(const struct doveadm_mail_cmd *cmd)
+{
+ fprintf(stderr, "doveadm %s "DOVEADM_CMD_MAIL_USAGE_PREFIX" %s\n",
+ cmd->name, cmd->usage_args == NULL ? "" : cmd->usage_args);
+ lib_exit(EX_USAGE);
+}
+
+void doveadm_mail_try_help_name(const char *cmd_name)
+{
+ const struct doveadm_cmd_ver2 *cmd2;
+
+ cmd2 = doveadm_cmd_find_ver2(cmd_name);
+ if (cmd2 != NULL)
+ help_ver2(cmd2);
+}
+
+void doveadm_mail_help_name(const char *cmd_name)
+{
+ doveadm_mail_try_help_name(cmd_name);
+ i_fatal("Missing help for command %s", cmd_name);
+}
+
+static struct doveadm_cmd_ver2 doveadm_cmd_force_resync_ver2 = {
+ .name = "force-resync",
+ .mail_cmd = cmd_force_resync_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-f] <mailbox mask>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('f', "fsck", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox-mask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+static struct doveadm_cmd_ver2 doveadm_cmd_purge_ver2 = {
+ .name = "purge",
+ .mail_cmd = cmd_purge_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAMS_END
+};
+
+static struct doveadm_cmd_ver2 *mail_commands_ver2[] = {
+ &doveadm_cmd_batch,
+ &doveadm_cmd_dsync_backup,
+ &doveadm_cmd_dsync_mirror,
+ &doveadm_cmd_dsync_server,
+ &doveadm_cmd_mailbox_metadata_set_ver2,
+ &doveadm_cmd_mailbox_metadata_unset_ver2,
+ &doveadm_cmd_mailbox_metadata_get_ver2,
+ &doveadm_cmd_mailbox_metadata_list_ver2,
+ &doveadm_cmd_mailbox_status_ver2,
+ &doveadm_cmd_mailbox_list_ver2,
+ &doveadm_cmd_mailbox_create_ver2,
+ &doveadm_cmd_mailbox_delete_ver2,
+ &doveadm_cmd_mailbox_rename_ver2,
+ &doveadm_cmd_mailbox_subscribe_ver2,
+ &doveadm_cmd_mailbox_unsubscribe_ver2,
+ &doveadm_cmd_mailbox_update_ver2,
+ &doveadm_cmd_mailbox_path_ver2,
+ &doveadm_cmd_fetch_ver2,
+ &doveadm_cmd_save_ver2,
+ &doveadm_cmd_index_ver2,
+ &doveadm_cmd_altmove_ver2,
+ &doveadm_cmd_deduplicate_ver2,
+ &doveadm_cmd_expunge_ver2,
+ &doveadm_cmd_flags_add_ver2,
+ &doveadm_cmd_flags_remove_ver2,
+ &doveadm_cmd_flags_replace_ver2,
+ &doveadm_cmd_import_ver2,
+ &doveadm_cmd_force_resync_ver2,
+ &doveadm_cmd_purge_ver2,
+ &doveadm_cmd_search_ver2,
+ &doveadm_cmd_copy_ver2,
+ &doveadm_cmd_move_ver2,
+ &doveadm_cmd_mailbox_cache_decision,
+ &doveadm_cmd_mailbox_cache_remove,
+ &doveadm_cmd_mailbox_cache_purge,
+ &doveadm_cmd_rebuild_attachments,
+};
+
+void doveadm_mail_init(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(mail_commands_ver2); i++)
+ doveadm_cmd_register_ver2(mail_commands_ver2[i]);
+}
+
+void doveadm_mail_init_finish(void)
+{
+ struct module_dir_load_settings mod_set;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = doveadm_debug;
+ mod_set.binary_name = "doveadm";
+
+ /* load all configured mail plugins */
+ mail_storage_service_modules =
+ module_dir_load_missing(mail_storage_service_modules,
+ doveadm_settings->mail_plugin_dir,
+ doveadm_settings->mail_plugins,
+ &mod_set);
+ /* keep mail_storage_init() referenced so that its _deinit() doesn't
+ try to free doveadm plugins' hooks too early. */
+ mail_storage_init();
+}
+
+void doveadm_mail_deinit(void)
+{
+ mail_storage_deinit();
+ module_dir_unload(&mail_storage_service_modules);
+}
+
+static int doveadm_cmd_parse_arg(struct doveadm_mail_cmd_context *mctx,
+ const struct doveadm_cmd_param *arg,
+ ARRAY_TYPE(const_string) *full_args)
+{
+ const char *short_opt_str =
+ p_strdup_printf(mctx->pool, "-%c", arg->short_opt);
+ const char *arg_value = NULL;
+
+ switch (arg->type) {
+ case CMD_PARAM_BOOL:
+ break;
+ case CMD_PARAM_INT64:
+ arg_value = dec2str(arg->value.v_int64);
+ break;
+ case CMD_PARAM_IP:
+ arg_value = net_ip2addr(&arg->value.v_ip);
+ break;
+ case CMD_PARAM_STR:
+ arg_value = arg->value.v_string;
+ break;
+ case CMD_PARAM_ARRAY: {
+ const char *str;
+
+ array_foreach_elem(&arg->value.v_array, str) {
+ optarg = (char *)str;
+ if (!mctx->v.parse_arg(mctx, arg->short_opt))
+ return -1;
+ array_push_back(full_args, &short_opt_str);
+ array_push_back(full_args, &str);
+ }
+ return 0;
+ }
+ default:
+ i_panic("Cannot convert parameter %s to short opt", arg->name);
+ }
+
+ optarg = (char *)arg_value;
+ if (!mctx->v.parse_arg(mctx, arg->short_opt))
+ return -1;
+
+ array_push_back(full_args, &short_opt_str);
+ if (arg_value != NULL)
+ array_push_back(full_args, &arg_value);
+ return 0;
+}
+
+void
+doveadm_cmd_ver2_to_mail_cmd_wrapper(struct doveadm_cmd_context *cctx)
+{
+ struct doveadm_mail_cmd_context *mctx;
+ const char *wildcard_user;
+ const char *fieldstr;
+ ARRAY_TYPE(const_string) pargv, full_args;
+ int i;
+ bool cli = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+ bool tcp_server = (cctx->conn_type == DOVEADM_CONNECTION_TYPE_TCP);
+ struct doveadm_mail_cmd mail_cmd = {
+ cctx->cmd->mail_cmd, cctx->cmd->name, cctx->cmd->usage
+ };
+
+ if (!cli) {
+ mctx = doveadm_mail_cmd_init(&mail_cmd, doveadm_settings);
+ /* doveadm-server always does userdb lookups */
+ mctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ } else {
+ mctx = doveadm_mail_cmdline_init(&mail_cmd);
+ }
+ mctx->cctx = cctx;
+ mctx->iterate_all_users = FALSE;
+ wildcard_user = NULL;
+ p_array_init(&full_args, mctx->pool, 8);
+ p_array_init(&pargv, mctx->pool, 8);
+
+ for(i=0;i<cctx->argc;i++) {
+ const struct doveadm_cmd_param *arg = &cctx->argv[i];
+
+ if (!arg->value_set)
+ continue;
+
+ if (strcmp(arg->name, "all-users") == 0) {
+ if (tcp_server)
+ mctx->add_username_header = TRUE;
+ else
+ mctx->iterate_all_users = arg->value.v_bool;
+ fieldstr = "-A";
+ array_push_back(&full_args, &fieldstr);
+ } else if (strcmp(arg->name, "socket-path") == 0) {
+ doveadm_settings->doveadm_socket_path = arg->value.v_string;
+ if (doveadm_settings->doveadm_worker_count == 0)
+ doveadm_settings->doveadm_worker_count = 1;
+ } else if (strcmp(arg->name, "user") == 0) {
+ mctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ if (!tcp_server)
+ cctx->username = arg->value.v_string;
+
+ fieldstr = "-u";
+ array_push_back(&full_args, &fieldstr);
+ array_push_back(&full_args, &arg->value.v_string);
+ if (strchr(arg->value.v_string, '*') != NULL ||
+ strchr(arg->value.v_string, '?') != NULL) {
+ if (tcp_server)
+ mctx->add_username_header = TRUE;
+ else {
+ wildcard_user = arg->value.v_string;
+ cctx->username = NULL;
+ }
+ }
+ } else if (strcmp(arg->name, "user-file") == 0) {
+ mctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ wildcard_user = "*";
+ mctx->users_list_input = arg->value.v_istream;
+ fieldstr = "-F";
+ array_push_back(&full_args, &fieldstr);
+ fieldstr = ""; /* value doesn't really matter */
+ array_push_back(&full_args, &fieldstr);
+ i_stream_ref(mctx->users_list_input);
+ } else if (strcmp(arg->name, "field") == 0 ||
+ strcmp(arg->name, "flag") == 0) {
+ /* mailbox status, fetch, flags: convert an array into a
+ single space-separated parameter (alternative to
+ fieldstr) */
+ fieldstr = p_array_const_string_join(mctx->pool,
+ &arg->value.v_array, " ");
+ array_push_back(&pargv, &fieldstr);
+ } else if (strcmp(arg->name, "file") == 0) {
+ /* input for doveadm_mail_get_input(),
+ used by e.g. save */
+ if (mctx->cmd_input != NULL) {
+ i_error("Only one file input allowed: %s", arg->name);
+ doveadm_mail_cmd_free(mctx);
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+ mctx->cmd_input = arg->value.v_istream;
+ i_stream_ref(mctx->cmd_input);
+
+ } else if (strcmp(arg->name, "trans-flags") == 0) {
+ /* This parameter allows to set additional
+ * mailbox transaction flags. */
+ mctx->transaction_flags = arg->value.v_int64;
+
+ /* Keep all named special parameters above this line */
+
+ } else if (mctx->v.parse_arg != NULL && arg->short_opt != '\0') {
+ if (doveadm_cmd_parse_arg(mctx, arg, &full_args) < 0) {
+ i_error("Invalid parameter %c", arg->short_opt);
+ doveadm_mail_cmd_free(mctx);
+ doveadm_exit_code = EX_USAGE;
+ }
+ } else if ((arg->flags & CMD_PARAM_FLAG_POSITIONAL) != 0) {
+ /* feed this into pargv */
+ if (arg->type == CMD_PARAM_ARRAY)
+ array_append_array(&pargv, &arg->value.v_array);
+ else if (arg->type == CMD_PARAM_STR)
+ array_push_back(&pargv, &arg->value.v_string);
+ } else {
+ doveadm_exit_code = EX_USAGE;
+ i_error("invalid parameter: %s", arg->name);
+ doveadm_mail_cmd_free(mctx);
+ return;
+ }
+ }
+
+ const char *dashdash = "--";
+ array_push_back(&full_args, &dashdash);
+
+ array_append_zero(&pargv);
+ /* All the -parameters need to be included in full_args so that
+ they're sent to doveadm-server. */
+ unsigned int args_pos = array_count(&full_args);
+ array_append_array(&full_args, &pargv);
+
+ mctx->args = array_idx(&full_args, args_pos);
+ mctx->full_args = array_front(&full_args);
+
+ doveadm_mail_cmd_exec(mctx, wildcard_user);
+ doveadm_mail_cmd_free(mctx);
+}
diff --git a/src/doveadm/doveadm-mail.h b/src/doveadm/doveadm-mail.h
new file mode 100644
index 0000000..af5d0fa
--- /dev/null
+++ b/src/doveadm/doveadm-mail.h
@@ -0,0 +1,206 @@
+#ifndef DOVEADM_MAIL_H
+#define DOVEADM_MAIL_H
+
+#include <stdio.h>
+#include "doveadm.h"
+#include "doveadm-util.h"
+#include "module-context.h"
+#include "mail-error.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+
+struct mailbox;
+struct mailbox_list;
+struct mail_storage;
+struct mail_user;
+struct doveadm_mail_cmd_context;
+
+struct doveadm_mail_cmd_vfuncs {
+ /* Parse one getopt() parameter. This is called for each parameter. */
+ bool (*parse_arg)(struct doveadm_mail_cmd_context *ctx, int c);
+ /* Usually not needed. The preinit() is called just after parsing all
+ parameters, but before any userdb lookups are done. This allows the
+ preinit() to alter the userdb lookup behavior (especially
+ service_flags). */
+ void (*preinit)(struct doveadm_mail_cmd_context *ctx);
+ /* Initialize the command. Most importantly if the function prints
+ anything, this should initialize the headers. It shouldn't however
+ do any actual work. The init() is called also when doveadm is
+ performing the work via doveadm-server, which could be running
+ remotely with completely different Dovecot configuration. */
+ void (*init)(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[]);
+ /* Usually not needed. When iterating through multiple users, use this
+ function to get the next username. Overriding this is usually done
+ only when there's a known username filter, such as the expire
+ plugin. */
+ int (*get_next_user)(struct doveadm_mail_cmd_context *ctx,
+ const char **username_r);
+ /* Usually not needed. This is called between
+ mail_storage_service_lookup() and mail_storage_service_next() for
+ each user. */
+ int (*prerun)(struct doveadm_mail_cmd_context *ctx,
+ struct mail_storage_service_user *service_user,
+ const char **error_r);
+ /* This is the main function which performs all the work for the
+ command. This is called once per each user. */
+ int (*run)(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *mail_user);
+ /* Deinitialize the command. Called once at the end - even if
+ preinit() or init() was never called. */
+ void (*deinit)(struct doveadm_mail_cmd_context *ctx);
+};
+
+struct doveadm_mail_cmd_module_register {
+ unsigned int id;
+};
+
+union doveadm_mail_cmd_module_context {
+ struct doveadm_mail_cmd_vfuncs super;
+ struct doveadm_mail_cmd_module_register *reg;
+};
+
+struct doveadm_mail_cmd_context {
+ pool_t pool;
+ struct doveadm_cmd_context *cctx;
+ const struct doveadm_mail_cmd *cmd;
+ const char *const *args;
+ /* args including -options */
+ const char *const *full_args;
+
+ const char *getopt_args;
+ const struct doveadm_settings *set;
+ enum mail_storage_service_flags service_flags;
+ enum mailbox_transaction_flags transaction_flags;
+ struct mail_storage_service_ctx *storage_service;
+ struct mail_storage_service_input storage_service_input;
+ /* search args aren't set for all mail commands */
+ struct mail_search_args *search_args;
+ struct istream *users_list_input;
+
+ struct mail_storage_service_user *cur_service_user;
+ struct mail_user *cur_mail_user;
+ struct doveadm_mail_cmd_vfuncs v;
+
+ struct istream *cmd_input;
+ int cmd_input_fd;
+
+ ARRAY(union doveadm_mail_cmd_module_context *) module_contexts;
+
+ /* if non-zero, exit with this code */
+ int exit_code;
+
+ /* This command is being called by a remote doveadm client. */
+ bool proxying:1;
+ /* We're handling only a single user */
+ bool iterate_single_user:1;
+ /* We're going through all users (not set for wildcard usernames) */
+ bool iterate_all_users:1;
+ /* Add username header to all replies */
+ bool add_username_header:1;
+};
+
+struct doveadm_mail_cmd {
+ struct doveadm_mail_cmd_context *(*alloc)(void);
+ const char *name;
+ const char *usage_args;
+};
+
+extern void (*hook_doveadm_mail_init)(struct doveadm_mail_cmd_context *ctx);
+extern struct doveadm_mail_cmd_module_register doveadm_mail_cmd_module_register;
+extern char doveadm_mail_cmd_hide;
+
+bool doveadm_is_killed(void);
+int doveadm_killed_signo(void);
+
+void doveadm_mail_help(const struct doveadm_mail_cmd *cmd) ATTR_NORETURN;
+void doveadm_mail_help_name(const char *cmd_name) ATTR_NORETURN;
+void doveadm_mail_try_help_name(const char *cmd_name);
+
+void doveadm_mail_init(void);
+void doveadm_mail_init_finish(void);
+void doveadm_mail_deinit(void);
+
+struct doveadm_mail_cmd_context *
+doveadm_mail_cmd_init(const struct doveadm_mail_cmd *cmd,
+ const struct doveadm_settings *set);
+void doveadm_mail_cmd_deinit(struct doveadm_mail_cmd_context *ctx);
+void doveadm_mail_cmd_free(struct doveadm_mail_cmd_context *ctx);
+int doveadm_mail_single_user(struct doveadm_mail_cmd_context *ctx,
+ const char **error_r);
+int doveadm_mail_server_user(struct doveadm_mail_cmd_context *ctx,
+ const struct mail_storage_service_input *input,
+ const char **error_r);
+void doveadm_mail_server_flush(void);
+
+/* Request input stream to be read (from stdin). This must be called from
+ the command's init() function. */
+void doveadm_mail_get_input(struct doveadm_mail_cmd_context *ctx);
+
+struct mailbox *
+doveadm_mailbox_find(struct mail_user *user, const char *mailbox);
+struct mail_search_args *
+doveadm_mail_build_search_args(const char *const args[]);
+void doveadm_mailbox_args_check(const char *const args[]);
+struct mail_search_args *
+doveadm_mail_mailbox_search_args_build(const char *const args[]);
+
+void expunge_search_args_check(struct mail_search_args *args, const char *cmd);
+
+struct doveadm_mail_cmd_context *
+doveadm_mail_cmd_alloc_size(size_t size);
+#define doveadm_mail_cmd_alloc(type) \
+ (type *)doveadm_mail_cmd_alloc_size(sizeof(type))
+
+void doveadm_mail_failed_error(struct doveadm_mail_cmd_context *ctx,
+ enum mail_error error);
+void doveadm_mail_failed_storage(struct doveadm_mail_cmd_context *ctx,
+ struct mail_storage *storage);
+void doveadm_mail_failed_mailbox(struct doveadm_mail_cmd_context *ctx,
+ struct mailbox *box);
+void doveadm_mail_failed_list(struct doveadm_mail_cmd_context *ctx,
+ struct mailbox_list *list);
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_batch;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_set_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_unset_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_get_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_metadata_list_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_status_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_list_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_create_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_delete_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_rename_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_subscribe_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_unsubscribe_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_fetch_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_save_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_index_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_altmove_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_deduplicate_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_expunge_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_flags_add_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_flags_remove_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_flags_replace_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_import_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_search_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_copy_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_move_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_update_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_path_ver2;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_decision;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_remove;
+extern struct doveadm_cmd_ver2 doveadm_cmd_mailbox_cache_purge;
+extern struct doveadm_cmd_ver2 doveadm_cmd_rebuild_attachments;
+
+#define DOVEADM_CMD_MAIL_COMMON \
+DOVEADM_CMD_PARAM('A', "all-users", CMD_PARAM_BOOL, 0) \
+DOVEADM_CMD_PARAM('S', "socket-path", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0) \
+DOVEADM_CMD_PARAM('\0', "trans-flags", CMD_PARAM_INT64, 0) \
+DOVEADM_CMD_PARAM('F', "user-file", CMD_PARAM_ISTREAM, 0)
+
+#define DOVEADM_CMD_MAIL_USAGE_PREFIX \
+ "[-u <user>|-A] [-S <socket_path>] "
+
+#endif
diff --git a/src/doveadm/doveadm-mailbox-list-iter.c b/src/doveadm/doveadm-mailbox-list-iter.c
new file mode 100644
index 0000000..c77398c
--- /dev/null
+++ b/src/doveadm/doveadm-mailbox-list-iter.c
@@ -0,0 +1,195 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "mail-search.h"
+#include "mail-namespace.h"
+#include "mailbox-list.h"
+#include "doveadm-mail.h"
+#include "doveadm-mailbox-list-iter.h"
+
+struct doveadm_mailbox_list_iter {
+ struct mail_user *user;
+ struct doveadm_mail_cmd_context *ctx;
+ struct mail_search_args *search_args;
+ enum mailbox_list_iter_flags iter_flags;
+
+ struct mailbox_list_iterate_context *iter;
+
+ struct mailbox_info info;
+ ARRAY_TYPE(const_string) patterns;
+ unsigned int pattern_idx;
+
+ bool only_selectable;
+};
+
+static bool
+search_args_get_mailbox_patterns(const struct mail_search_arg *args,
+ ARRAY_TYPE(const_string) *patterns,
+ bool *have_guid, bool *have_wildcards)
+{
+ const struct mail_search_arg *subargs;
+
+ for (; args != NULL; args = args->next) {
+ switch (args->type) {
+ case SEARCH_OR:
+ /* we don't currently try to optimize OR. */
+ break;
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ subargs = args->value.subargs;
+ for (; subargs != NULL; subargs = subargs->next) {
+ if (!search_args_get_mailbox_patterns(subargs,
+ patterns, have_guid,
+ have_wildcards))
+ return FALSE;
+ }
+ break;
+ case SEARCH_MAILBOX_GLOB:
+ *have_wildcards = TRUE;
+ /* fall through */
+ case SEARCH_MAILBOX:
+ if (args->match_not) {
+ array_clear(patterns);
+ return FALSE;
+ }
+ array_push_back(patterns, &args->value.str);
+ break;
+ case SEARCH_MAILBOX_GUID:
+ *have_guid = TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static struct doveadm_mailbox_list_iter *
+doveadm_mailbox_list_iter_init_nsmask(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ struct mail_search_args *search_args,
+ enum mailbox_list_iter_flags iter_flags,
+ enum mail_namespace_type ns_mask)
+{
+ static const char *all_pattern = "*";
+ struct doveadm_mailbox_list_iter *iter;
+ bool have_guid = FALSE, have_wildcards = FALSE;
+
+ iter = i_new(struct doveadm_mailbox_list_iter, 1);
+ iter->ctx = ctx;
+ iter->search_args = search_args;
+ iter->user = user;
+ i_array_init(&iter->patterns, 16);
+ (void)search_args_get_mailbox_patterns(search_args->args,
+ &iter->patterns,
+ &have_guid, &have_wildcards);
+
+ if (array_count(&iter->patterns) == 0) {
+ iter_flags |= MAILBOX_LIST_ITER_SKIP_ALIASES;
+ if (have_guid) {
+ ns_mask |= MAIL_NAMESPACE_TYPE_SHARED |
+ MAIL_NAMESPACE_TYPE_PUBLIC;
+ }
+ array_push_back(&iter->patterns, &all_pattern);
+ } else if (have_wildcards) {
+ iter_flags |= MAILBOX_LIST_ITER_STAR_WITHIN_NS;
+ ns_mask |= MAIL_NAMESPACE_TYPE_SHARED |
+ MAIL_NAMESPACE_TYPE_PUBLIC;
+ } else {
+ /* just return the listed mailboxes without actually
+ iterating through. this also allows accessing mailboxes
+ without lookup ACL right */
+ return iter;
+ }
+ array_append_zero(&iter->patterns);
+
+ iter->only_selectable = TRUE;
+ iter->iter_flags = iter_flags;
+ iter->iter = mailbox_list_iter_init_namespaces(user->namespaces,
+ array_front(&iter->patterns),
+ ns_mask, iter_flags);
+ return iter;
+}
+
+struct doveadm_mailbox_list_iter *
+doveadm_mailbox_list_iter_init(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ struct mail_search_args *search_args,
+ enum mailbox_list_iter_flags iter_flags)
+{
+ enum mail_namespace_type ns_mask = MAIL_NAMESPACE_TYPE_PRIVATE;
+
+ return doveadm_mailbox_list_iter_init_nsmask(ctx, user, search_args,
+ iter_flags, ns_mask);
+}
+
+struct doveadm_mailbox_list_iter *
+doveadm_mailbox_list_iter_full_init(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ struct mail_search_args *search_args,
+ enum mailbox_list_iter_flags iter_flags)
+{
+ enum mail_namespace_type ns_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
+ struct doveadm_mailbox_list_iter *iter;
+
+ iter = doveadm_mailbox_list_iter_init_nsmask(ctx, user, search_args,
+ iter_flags, ns_mask);
+ iter->only_selectable = FALSE;
+ return iter;
+}
+
+int doveadm_mailbox_list_iter_deinit(struct doveadm_mailbox_list_iter **_iter)
+{
+ struct doveadm_mailbox_list_iter *iter = *_iter;
+ enum mail_error error;
+ int ret;
+
+ *_iter = NULL;
+
+ if (iter->iter == NULL)
+ ret = 0;
+ else if ((ret = mailbox_list_iter_deinit(&iter->iter)) < 0) {
+ i_error("Listing mailboxes failed: %s",
+ mailbox_list_get_last_internal_error(iter->user->namespaces->list, &error));
+ doveadm_mail_failed_error(iter->ctx, error);
+ }
+ array_free(&iter->patterns);
+ i_free(iter);
+ return ret;
+}
+
+const struct mailbox_info *
+doveadm_mailbox_list_iter_next(struct doveadm_mailbox_list_iter *iter)
+{
+ const struct mailbox_info *info;
+ const char *const *patterns;
+ unsigned int count;
+
+ while (iter->iter == NULL) {
+ patterns = array_get(&iter->patterns, &count);
+ if (iter->pattern_idx == count)
+ return NULL;
+
+ iter->info.vname = patterns[iter->pattern_idx++];
+ iter->info.ns = mail_namespace_find(iter->user->namespaces,
+ iter->info.vname);
+ return &iter->info;
+ }
+
+ while ((info = mailbox_list_iter_next(iter->iter)) != NULL) {
+ char sep = mail_namespace_get_sep(info->ns);
+
+ if ((info->flags & (MAILBOX_NOSELECT |
+ MAILBOX_NONEXISTENT)) != 0) {
+ if (iter->only_selectable)
+ continue;
+ }
+
+ if (mail_search_args_match_mailbox(iter->search_args,
+ info->vname, sep))
+ break;
+ }
+ return info;
+}
diff --git a/src/doveadm/doveadm-mailbox-list-iter.h b/src/doveadm/doveadm-mailbox-list-iter.h
new file mode 100644
index 0000000..9b205e2
--- /dev/null
+++ b/src/doveadm/doveadm-mailbox-list-iter.h
@@ -0,0 +1,25 @@
+#ifndef DOVEADM_MAILBOX_LIST_ITER_H
+#define DOVEADM_MAILBOX_LIST_ITER_H
+
+#include "mailbox-list-iter.h"
+
+struct doveadm_mail_cmd_context;
+
+/* List only selectable mailboxes */
+struct doveadm_mailbox_list_iter *
+doveadm_mailbox_list_iter_init(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ struct mail_search_args *search_args,
+ enum mailbox_list_iter_flags iter_flags);
+/* List all mailboxes */
+struct doveadm_mailbox_list_iter *
+doveadm_mailbox_list_iter_full_init(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user,
+ struct mail_search_args *search_args,
+ enum mailbox_list_iter_flags iter_flags);
+int doveadm_mailbox_list_iter_deinit(struct doveadm_mailbox_list_iter **iter);
+
+const struct mailbox_info *
+doveadm_mailbox_list_iter_next(struct doveadm_mailbox_list_iter *iter);
+
+#endif
diff --git a/src/doveadm/doveadm-master.c b/src/doveadm/doveadm-master.c
new file mode 100644
index 0000000..2f161d7
--- /dev/null
+++ b/src/doveadm/doveadm-master.c
@@ -0,0 +1,297 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "istream.h"
+#include "write-full.h"
+#include "master-service.h"
+#include "sleep.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#define MASTER_PID_FILE_NAME "master.pid"
+
+static bool pid_file_read(const char *path, pid_t *pid_r)
+{
+ char buf[32];
+ int fd;
+ ssize_t ret;
+ bool found;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return FALSE;
+ i_fatal("open(%s) failed: %m", path);
+ }
+
+ ret = read(fd, buf, sizeof(buf)-1);
+ if (ret <= 0) {
+ if (ret == 0)
+ i_error("Empty PID file in %s", path);
+ else
+ i_fatal("read(%s) failed: %m", path);
+ found = FALSE;
+ } else {
+ if (buf[ret-1] == '\n')
+ ret--;
+ buf[ret] = '\0';
+ if (str_to_pid(buf, pid_r) < 0)
+ found = FALSE;
+ else {
+ found = !(*pid_r == getpid() ||
+ (kill(*pid_r, 0) < 0 && errno == ESRCH));
+ }
+ }
+ i_close_fd(&fd);
+ return found;
+}
+
+void doveadm_master_send_signal(int signo)
+{
+ const char *pidfile_path;
+ unsigned int i;
+ pid_t pid;
+
+ pidfile_path = t_strconcat(doveadm_settings->base_dir,
+ "/"MASTER_PID_FILE_NAME, NULL);
+
+ if (!pid_file_read(pidfile_path, &pid))
+ i_fatal("Dovecot is not running (read from %s)", pidfile_path);
+
+ if (kill(pid, signo) < 0)
+ i_fatal("kill(%s, %d) failed: %m", dec2str(pid), signo);
+
+ if (signo == SIGTERM) {
+ /* wait for a while for the process to die */
+ i_sleep_msecs(1);
+ for (i = 0; i < 30; i++) {
+ if (kill(pid, 0) < 0) {
+ if (errno != ESRCH)
+ i_error("kill() failed: %m");
+ break;
+ }
+ i_sleep_msecs(100);
+ }
+ }
+}
+
+static void cmd_stop(struct doveadm_cmd_context *cctx ATTR_UNUSED)
+{
+ doveadm_master_send_signal(SIGTERM);
+}
+
+static void cmd_reload(struct doveadm_cmd_context *cctx ATTR_UNUSED)
+{
+ doveadm_master_send_signal(SIGHUP);
+}
+
+static struct istream *master_service_send_cmd(const char *cmd)
+{
+ struct istream *input;
+ const char *path, *line;
+
+ path = t_strconcat(doveadm_settings->base_dir, "/master", NULL);
+ int fd = net_connect_unix(path);
+ if (fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ net_set_nonblock(fd, FALSE);
+
+ const char *str =
+ t_strdup_printf("VERSION\tmaster-client\t1\t0\n%s\n", cmd);
+ if (write_full(fd, str, strlen(str)) < 0)
+ i_fatal("write(%s) failed: %m", path);
+
+ input = i_stream_create_fd_autoclose(&fd, IO_BLOCK_SIZE);
+ alarm(5);
+ if ((line = i_stream_read_next_line(input)) == NULL)
+ i_fatal("read(%s) failed: %m", path);
+ if (!version_string_verify(line, "master-server", 1)) {
+ i_fatal_status(EX_PROTOCOL,
+ "%s is not a compatible master socket", path);
+ }
+ alarm(0);
+ return input;
+}
+
+static struct istream *
+master_service_send_cmd_with_args(const char *cmd, const char *const *args)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, cmd);
+ if (args != NULL) {
+ for (unsigned int i = 0; args[i] != NULL; i++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, args[i]);
+ }
+ }
+ return master_service_send_cmd(str_c(str));
+}
+
+static void cmd_service_stop(struct doveadm_cmd_context *cctx)
+{
+ const char *line, *const *services;
+
+ if (!doveadm_cmd_param_array(cctx, "service", &services))
+ i_fatal("service parameter missing");
+
+ struct istream *input =
+ master_service_send_cmd_with_args("STOP", services);
+
+ alarm(5);
+ if ((line = i_stream_read_next_line(input)) == NULL) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (line[0] == '-') {
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ i_error("%s", line+1);
+ } else if (line[0] != '+') {
+ i_error("Unexpected input from %s: %s",
+ i_stream_get_name(input), line);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ alarm(0);
+ i_stream_destroy(&input);
+}
+
+static void cmd_service_status(struct doveadm_cmd_context *cctx)
+{
+ const char *line, *const *services;
+ unsigned int fields_count;
+
+ if (!doveadm_cmd_param_array(cctx, "service", &services))
+ services = NULL;
+
+ struct istream *input = master_service_send_cmd("SERVICE-STATUS");
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+ doveadm_print_header_simple("name");
+ doveadm_print_header_simple("process_count");
+ doveadm_print_header_simple("process_avail");
+ doveadm_print_header_simple("process_limit");
+ doveadm_print_header_simple("client_limit");
+ doveadm_print_header_simple("throttle_secs");
+ doveadm_print_header_simple("exit_failure_last");
+ doveadm_print_header_simple("exit_failures_in_sec");
+ doveadm_print_header_simple("last_drop_warning");
+ doveadm_print_header_simple("listen_pending");
+ doveadm_print_header_simple("listening");
+ doveadm_print_header_simple("doveadm_stop");
+ doveadm_print_header_simple("process_total");
+ fields_count = doveadm_print_get_headers_count();
+
+ alarm(5);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (line[0] == '\0')
+ break;
+ T_BEGIN {
+ const char *const *args = t_strsplit_tabescaped(line);
+ if (args[0] != NULL &&
+ (services == NULL ||
+ str_array_find(services, args[0]))) {
+ unsigned int i;
+ for (i = 0; i < fields_count && args[i] != NULL; i++)
+ doveadm_print(args[i]);
+ for (; i < fields_count; i++)
+ doveadm_print("");
+ }
+ } T_END;
+ }
+ if (line == NULL) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ alarm(0);
+ i_stream_destroy(&input);
+}
+
+static void cmd_process_status(struct doveadm_cmd_context *cctx)
+{
+ const char *line, *const *services;
+
+ if (!doveadm_cmd_param_array(cctx, "service", &services))
+ services = NULL;
+
+ struct istream *input =
+ master_service_send_cmd_with_args("PROCESS-STATUS", services);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header_simple("name");
+ doveadm_print_header_simple("pid");
+ doveadm_print_header_simple("available_count");
+ doveadm_print_header_simple("total_count");
+ doveadm_print_header_simple("idle_start");
+ doveadm_print_header_simple("last_status_update");
+ doveadm_print_header_simple("last_kill_sent");
+
+ alarm(5);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (line[0] == '\0')
+ break;
+ T_BEGIN {
+ const char *const *args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) >= 7) {
+ for (unsigned int i = 0; i < 7; i++)
+ doveadm_print(args[i]);
+ }
+ } T_END;
+ }
+ if (line == NULL) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ alarm(0);
+ i_stream_destroy(&input);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_stop_ver2 = {
+ .cmd = cmd_stop,
+ .name = "stop",
+ .usage = "",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_reload_ver2 = {
+ .cmd = cmd_reload,
+ .name = "reload",
+ .usage = "",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_service_stop_ver2 = {
+ .cmd = cmd_service_stop,
+ .name = "service stop",
+ .usage = "<service> [<service> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "service", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_service_status_ver2 = {
+ .cmd = cmd_service_status,
+ .name = "service status",
+ .usage = "[<service> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "service", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_process_status_ver2 = {
+ .cmd = cmd_process_status,
+ .name = "process status",
+ .usage = "[<service> [...]]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "service", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-mutf7.c b/src/doveadm/doveadm-mutf7.c
new file mode 100644
index 0000000..3f788f5
--- /dev/null
+++ b/src/doveadm/doveadm-mutf7.c
@@ -0,0 +1,56 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-utf7.h"
+#include "doveadm.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static void cmd_mailbox_mutf7(struct doveadm_cmd_context *cctx)
+{
+ string_t *str;
+ const char *const *names;
+ bool from_utf8, to_utf8;
+ unsigned int i;
+
+ if (!doveadm_cmd_param_array(cctx, "name", &names))
+ help_ver2(&doveadm_cmd_mailbox_mutf7);
+ if (!doveadm_cmd_param_bool(cctx, "from-utf8", &from_utf8)) {
+ if (!doveadm_cmd_param_bool(cctx, "to-utf8", &to_utf8))
+ from_utf8 = TRUE;
+ else
+ from_utf8 = !to_utf8;
+ }
+
+ str = t_str_new(128);
+ for (i = 0; names[i] != NULL; i++) {
+ str_truncate(str, 0);
+ if (from_utf8) {
+ if (imap_utf8_to_utf7(names[i], str) < 0) {
+ i_error("Mailbox name not valid UTF-8: %s",
+ names[i]);
+ doveadm_exit_code = EX_DATAERR;
+ }
+ } else {
+ if (imap_utf7_to_utf8(names[i], str) < 0) {
+ i_error("Mailbox name not valid mUTF-7: %s",
+ names[i]);
+ doveadm_exit_code = EX_DATAERR;
+ }
+ }
+ printf("%s\n", str_c(str));
+ }
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mailbox_mutf7 = {
+ .name = "mailbox mutf7",
+ .cmd = cmd_mailbox_mutf7,
+ .usage = "[-7|-8] <name> [...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('7', "to-utf8", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('8', "from-utf8", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "name", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-oldstats.c b/src/doveadm/doveadm-oldstats.c
new file mode 100644
index 0000000..4be575e
--- /dev/null
+++ b/src/doveadm/doveadm-oldstats.c
@@ -0,0 +1,604 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "strescape.h"
+#include "write-full.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+
+struct top_line {
+ char *id;
+ /* [headers_count] */
+ const char **prev_values, **cur_values;
+
+ bool flip:1;
+};
+
+struct top_context {
+ const char *path;
+ int fd;
+ struct istream *input;
+ const char *sort_type;
+
+ char **headers;
+ unsigned int headers_count;
+
+ pool_t prev_pool, cur_pool;
+ /* id => struct top_line. */
+ HASH_TABLE(char *, struct top_line *) sessions;
+ ARRAY(struct top_line *) lines;
+ int (*lines_sort)(struct top_line *const *, struct top_line *const *);
+
+ unsigned int last_update_idx, user_idx;
+ unsigned int sort_idx1, sort_idx2;
+
+ bool flip:1;
+};
+
+static struct top_context *sort_ctx = NULL;
+static const char *disk_input_field = "disk_input";
+static const char *disk_output_field = "disk_output";
+
+static char **
+p_read_next_line(pool_t pool, struct istream *input)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(input);
+ if (line == NULL)
+ return NULL;
+
+ return p_strsplit_tabescaped(pool, line);
+}
+
+static const char *const*
+read_next_line(struct istream *input)
+{
+ return (const void *)p_read_next_line(pool_datastack_create(), input);
+}
+
+static void stats_dump(const char *path, const char *cmd)
+{
+ struct istream *input;
+ const char *const *args;
+ unsigned int i;
+ int fd;
+
+ fd = doveadm_connect(path);
+ net_set_nonblock(fd, FALSE);
+ if (write_full(fd, cmd, strlen(cmd)) < 0)
+ i_fatal("write(%s) failed: %m", path);
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+
+ /* read header */
+ args = read_next_line(input);
+ if (args == NULL)
+ i_fatal("read(%s) unexpectedly disconnected", path);
+ if (*args == NULL)
+ i_info("no statistics available");
+ else {
+ for (; *args != NULL; args++)
+ doveadm_print_header_simple(*args);
+
+ /* read lines */
+ do {
+ T_BEGIN {
+ args = read_next_line(input);
+ if (args != NULL && args[0] == NULL)
+ args = NULL;
+ if (args != NULL) {
+ for (i = 0; args[i] != NULL; i++)
+ doveadm_print(args[i]);
+ }
+ } T_END;
+ } while (args != NULL);
+ }
+ if (input->stream_errno != 0)
+ i_fatal("read(%s) failed: %s", path, i_stream_get_error(input));
+ i_stream_destroy(&input);
+}
+
+static void
+doveadm_cmd_stats_dump(struct doveadm_cmd_context* cctx)
+{
+ const char *path, *cmd;
+ const char *args[3] = {0};
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &path))
+ path = t_strconcat(doveadm_settings->base_dir, "/old-stats", NULL);
+
+ if (!doveadm_cmd_param_str(cctx, "type", &args[0])) {
+ i_error("Missing type parameter");
+ doveadm_exit_code = EX_USAGE;
+ return;
+ }
+
+ /* purely optional */
+ if (!doveadm_cmd_param_str(cctx, "filter", &args[1]))
+ args[1] = NULL;
+
+ cmd = t_strdup_printf("EXPORT\t%s\n", t_strarray_join(args, "\t"));
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TAB);
+ stats_dump(path, cmd);
+ return;
+}
+
+static void
+stats_line_set_prev_values(struct top_context *ctx,
+ const struct top_line *old_line,
+ struct top_line *line)
+{
+ const char **values;
+ unsigned int i;
+
+ if (old_line->prev_values == NULL ||
+ strcmp(old_line->cur_values[ctx->last_update_idx],
+ line->cur_values[ctx->last_update_idx]) != 0) {
+ values = old_line->cur_values;
+ } else {
+ values = old_line->prev_values;
+ }
+
+ /* line hasn't been updated, keep old values */
+ line->prev_values =
+ p_new(ctx->cur_pool, const char *, ctx->headers_count);
+ for (i = 0; i < ctx->headers_count; i++)
+ line->prev_values[i] = p_strdup(ctx->cur_pool, values[i]);
+}
+
+static void stats_read(struct top_context *ctx)
+{
+ struct top_line *old_line, *line;
+ unsigned int i;
+ char **args;
+
+ /* read lines */
+ while ((args = p_read_next_line(ctx->cur_pool, ctx->input)) != NULL) {
+ if (args[0] == NULL) {
+ /* end of stats */
+ return;
+ }
+ if (str_array_length((void *)args) != ctx->headers_count)
+ i_fatal("read(%s): invalid stats line", ctx->path);
+
+ line = p_new(ctx->cur_pool, struct top_line, 1);
+ line->id = args[0];
+ line->flip = ctx->flip;
+ line->cur_values = p_new(ctx->cur_pool, const char *, ctx->headers_count);
+ for (i = 0; i < ctx->headers_count; i++)
+ line->cur_values[i] = args[i];
+
+ old_line = hash_table_lookup(ctx->sessions, line->id);
+ if (old_line != NULL) {
+ stats_line_set_prev_values(ctx, old_line, line);
+ array_push_back(&ctx->lines, &line);
+ }
+ hash_table_update(ctx->sessions, line->id, line);
+ }
+
+ if (ctx->input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->path,
+ i_stream_get_error(ctx->input));
+ }
+ i_fatal("read(%s): unexpected EOF", ctx->path);
+}
+
+static void stats_drop_stale(struct top_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ char *id;
+ struct top_line *line;
+
+ iter = hash_table_iterate_init(ctx->sessions);
+ while (hash_table_iterate(iter, ctx->sessions, &id, &line)) {
+ if (line->flip != ctx->flip)
+ hash_table_remove(ctx->sessions, id);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static int get_double(const char *str, double *num_r)
+{
+ char *p;
+
+ *num_r = strtod(str, &p);
+ return *p == '\0' ? 0 : -1;
+}
+
+static double sort_cpu_diff(const struct top_line *line)
+{
+ double prev, cur, diff, prev_time, cur_time, time_multiplier;
+
+ if (get_double(line->prev_values[sort_ctx->last_update_idx], &prev_time) < 0 ||
+ get_double(line->cur_values[sort_ctx->last_update_idx], &cur_time) < 0)
+ i_fatal("sorting: invalid last_update value");
+ time_multiplier = (cur_time - prev_time) * 100;
+
+ if (get_double(line->prev_values[sort_ctx->sort_idx1], &prev) < 0 ||
+ get_double(line->cur_values[sort_ctx->sort_idx1], &cur) < 0)
+ i_fatal("sorting: not a double");
+
+ diff = cur - prev;
+
+ if (sort_ctx->sort_idx2 != 0) {
+ if (get_double(line->prev_values[sort_ctx->sort_idx2], &prev) < 0 ||
+ get_double(line->cur_values[sort_ctx->sort_idx2], &cur) < 0)
+ i_fatal("sorting: not a double");
+ diff += cur - prev;
+ }
+ return diff * time_multiplier;
+}
+
+static int sort_cpu(struct top_line *const *l1, struct top_line *const *l2)
+{
+ double d1, d2;
+
+ d1 = sort_cpu_diff(*l1);
+ d2 = sort_cpu_diff(*l2);
+ if (d1 < d2)
+ return -1;
+ if (d1 > d2)
+ return 1;
+ return strcmp((*l1)->cur_values[sort_ctx->user_idx],
+ (*l2)->cur_values[sort_ctx->user_idx]);
+}
+
+static double sort_num_diff(const struct top_line *line)
+{
+ uint64_t prev, cur, diff;
+
+ if (str_to_uint64(line->prev_values[sort_ctx->sort_idx1], &prev) < 0 ||
+ str_to_uint64(line->cur_values[sort_ctx->sort_idx1], &cur) < 0)
+ i_fatal("sorting: not a number");
+ diff = cur - prev;
+
+ if (sort_ctx->sort_idx2 != 0) {
+ if (str_to_uint64(line->prev_values[sort_ctx->sort_idx2], &prev) < 0 ||
+ str_to_uint64(line->cur_values[sort_ctx->sort_idx2], &cur) < 0)
+ i_fatal("sorting: not a number");
+ diff += cur - prev;
+ }
+ return diff;
+}
+
+static int sort_num(struct top_line *const *l1, struct top_line *const *l2)
+{
+ uint64_t n1, n2;
+
+ n1 = sort_num_diff(*l1);
+ n2 = sort_num_diff(*l2);
+ if (n1 < n2)
+ return -1;
+ if (n1 > n2)
+ return 1;
+ return strcmp((*l1)->cur_values[sort_ctx->user_idx],
+ (*l2)->cur_values[sort_ctx->user_idx]);
+}
+
+static bool
+stats_header_find(struct top_context *ctx, const char *name,
+ unsigned int *idx_r)
+{
+ unsigned int i;
+
+ for (i = 0; ctx->headers[i] != NULL; i++) {
+ if (strcmp(ctx->headers[i], name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void stats_top_get_sorting(struct top_context *ctx)
+{
+ if (stats_header_find(ctx, ctx->sort_type, &ctx->sort_idx1))
+ return;
+
+ if (strcmp(ctx->sort_type, "cpu") == 0) {
+ if (!stats_header_find(ctx, "user_cpu", &ctx->sort_idx1) ||
+ !stats_header_find(ctx, "sys_cpu", &ctx->sort_idx2))
+ i_fatal("cpu sort type is missing fields");
+ return;
+ }
+ if (strcmp(ctx->sort_type, "disk") == 0) {
+ if (!stats_header_find(ctx, disk_input_field, &ctx->sort_idx1) ||
+ !stats_header_find(ctx, disk_output_field, &ctx->sort_idx2))
+ i_fatal("disk sort type is missing fields");
+ return;
+ }
+ i_fatal("unknown sort type: %s", ctx->sort_type);
+}
+
+static bool stats_top_round(struct top_context *ctx)
+{
+#define TOP_CMD "EXPORT\tsession\tconnected\n"
+ const char *const *args;
+ pool_t tmp_pool;
+
+ if (write_full(ctx->fd, TOP_CMD, strlen(TOP_CMD)) < 0)
+ i_fatal("write(%s) failed: %m", ctx->path);
+
+ /* read header */
+ if (ctx->headers != NULL) {
+ args = read_next_line(ctx->input);
+ if (args == NULL)
+ i_fatal("read(%s) unexpectedly disconnected", ctx->path);
+ if (*args == NULL)
+ return TRUE;
+ if (str_array_length(args) != ctx->headers_count)
+ i_fatal("headers changed");
+ } else {
+ ctx->headers = p_read_next_line(default_pool, ctx->input);
+ if (ctx->headers == NULL)
+ i_fatal("read(%s) unexpectedly disconnected", ctx->path);
+ if (*ctx->headers == NULL) {
+ i_free_and_null(ctx->headers);
+ return FALSE;
+ }
+ ctx->headers_count = str_array_length((void *)ctx->headers);
+ if (!stats_header_find(ctx, "last_update", &ctx->last_update_idx))
+ i_fatal("last_update header missing");
+ if (!stats_header_find(ctx, "user", &ctx->user_idx))
+ i_fatal("user header missing");
+ stats_top_get_sorting(ctx);
+ }
+
+ array_clear(&ctx->lines);
+ p_clear(ctx->prev_pool);
+ tmp_pool = ctx->prev_pool;
+ ctx->prev_pool = ctx->cur_pool;
+ ctx->cur_pool = tmp_pool;
+
+ ctx->flip = !ctx->flip;
+ stats_read(ctx);
+ stats_drop_stale(ctx);
+
+ sort_ctx = ctx;
+ array_sort(&ctx->lines, *ctx->lines_sort);
+ sort_ctx = NULL;
+ return TRUE;
+}
+
+static void
+stats_top_output_diff(struct top_context *ctx,
+ const struct top_line *line, unsigned int i)
+{
+ uint64_t prev_num, cur_num;
+ double prev_double, cur_double, prev_time, cur_time;
+ char numstr[MAX_INT_STRLEN];
+
+ if (str_to_uint64(line->prev_values[i], &prev_num) == 0 &&
+ str_to_uint64(line->cur_values[i], &cur_num) == 0) {
+ if (i_snprintf(numstr, sizeof(numstr), "%"PRIu64,
+ (cur_num - prev_num)) < 0)
+ i_unreached();
+ doveadm_print(numstr);
+ } else if (get_double(line->prev_values[i], &prev_double) == 0 &&
+ get_double(line->cur_values[i], &cur_double) == 0 &&
+ get_double(line->prev_values[ctx->last_update_idx], &prev_time) == 0 &&
+ get_double(line->cur_values[ctx->last_update_idx], &cur_time) == 0) {
+ /* %CPU */
+ if (i_snprintf(numstr, sizeof(numstr), "%d",
+ (int)((cur_double - prev_double) *
+ (cur_time - prev_time) * 100)) < 0)
+ i_unreached();
+ doveadm_print(numstr);
+ } else {
+ doveadm_print(line->cur_values[i]);
+ }
+}
+
+static void stats_top_output(struct top_context *ctx)
+{
+ static const char *names[] = {
+ "user", "service", "user_cpu", "sys_cpu",
+ "", ""
+ };
+ struct winsize ws;
+ struct top_line *const *lines;
+ unsigned int i, j, row, maxrow, count, indexes[N_ELEMENTS(names)];
+
+ names[4] = disk_input_field;
+ names[5] = disk_output_field;
+
+ /* ANSI clear screen and move cursor to top of screen */
+ printf("\x1b[2J\x1b[1;1H"); fflush(stdout);
+ doveadm_print_deinit();
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+
+ doveadm_print_header("USER", "USER", DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header_simple("SERVICE");
+ doveadm_print_header_simple("%CPU");
+ doveadm_print_header_simple("%SYS");
+ doveadm_print_header_simple("DISKIN");
+ doveadm_print_header_simple("DISKOUT");
+
+ if (!stats_top_round(ctx)) {
+ /* no connections yet */
+ return;
+ }
+
+ for (i = 0; i < N_ELEMENTS(names); i++) {
+ if (!stats_header_find(ctx, names[i], &indexes[i]))
+ indexes[i] = UINT_MAX;
+ }
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0)
+ ws.ws_row = 24;
+ maxrow = ws.ws_row-1;
+
+ lines = array_get(&ctx->lines, &count);
+ for (i = 0, row = 1; row < maxrow && i < count; i++, row++) {
+ for (j = 0; j < N_ELEMENTS(names); j++) {
+ if (indexes[j] == UINT_MAX)
+ doveadm_print("?");
+ else
+ stats_top_output_diff(ctx, lines[i], indexes[j]);
+ }
+ }
+}
+
+static void stats_top_start(struct top_context *ctx)
+{
+ struct timeout *to;
+
+ stats_top_output(ctx);
+ to = timeout_add(1000, stats_top_output, ctx);
+ io_loop_run(current_ioloop);
+ timeout_remove(&to);
+}
+
+static void stats_top(const char *path, const char *sort_type)
+{
+ struct top_context ctx;
+
+ i_zero(&ctx);
+ ctx.path = path;
+ ctx.fd = doveadm_connect(path);
+ ctx.prev_pool = pool_alloconly_create("stats top", 1024*16);
+ ctx.cur_pool = pool_alloconly_create("stats top", 1024*16);
+ i_array_init(&ctx.lines, 128);
+ hash_table_create(&ctx.sessions, default_pool, 0, str_hash, strcmp);
+ net_set_nonblock(ctx.fd, FALSE);
+
+ ctx.input = i_stream_create_fd(ctx.fd, SIZE_MAX);
+
+ if (strstr(sort_type, "cpu") != NULL)
+ ctx.lines_sort = sort_cpu;
+ else
+ ctx.lines_sort = sort_num;
+ ctx.sort_type = sort_type;
+
+ stats_top_start(&ctx);
+ i_stream_destroy(&ctx.input);
+ hash_table_destroy(&ctx.sessions);
+ array_free(&ctx.lines);
+ pool_unref(&ctx.prev_pool);
+ pool_unref(&ctx.cur_pool);
+ i_close_fd(&ctx.fd);
+}
+
+static void stats_reset(const char *path)
+{
+ const char **ptr ATTR_UNUSED;
+ int fd,ret;
+ string_t *cmd;
+ struct istream *input;
+ const char *line;
+
+ fd = doveadm_connect(path);
+ net_set_nonblock(fd, FALSE);
+ input = i_stream_create_fd(fd, SIZE_MAX);
+
+ cmd = t_str_new(10);
+ str_append(cmd, "RESET");
+/* XXX: Not supported yet.
+ for(ptr = items; *ptr; ptr++)
+ {
+ str_append_c(cmd, '\t');
+ str_append(cmd, *ptr);
+ }
+*/
+ str_append_c(cmd, '\n');
+
+ /* send command */
+ ret = write_full(fd, str_c(cmd), str_len(cmd));
+
+ if (ret < 0) {
+ i_close_fd(&fd);
+ i_error("write(%s) failed: %m", path);
+ return;
+ }
+
+ line = i_stream_read_next_line(input);
+
+ if (line == NULL) {
+ i_error("read(%s) failed: %s", path, i_stream_get_error(input));
+ } else if (!str_begins(line, "OK")) {
+ i_error("%s",line);
+ } else {
+ i_info("Stats reset");
+ }
+
+ i_stream_destroy(&input);
+ i_close_fd(&fd);
+}
+
+static void cmd_stats_top(struct doveadm_cmd_context *cctx)
+{
+ const char *path, *sort_type;
+ bool b;
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &path)) {
+ path = t_strconcat(doveadm_settings->base_dir,
+ "/old-stats", NULL);
+ }
+ if (!doveadm_cmd_param_bool(cctx, "show-disk-io", &b) && b) {
+ disk_input_field = "read_bytes";
+ disk_output_field = "write_bytes";
+ }
+ if (!doveadm_cmd_param_str(cctx, "sort-field", &sort_type))
+ sort_type = "disk";
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ stats_top(path, sort_type);
+}
+
+static void cmd_stats_reset(struct doveadm_cmd_context *cctx)
+{
+ const char *path;
+
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &path)) {
+ path = t_strconcat(doveadm_settings->base_dir,
+ "/old-stats", NULL);
+ }
+
+ stats_reset(path);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_oldstats_dump_ver2 = {
+ .cmd = doveadm_cmd_stats_dump,
+ .name = "oldstats dump",
+ .usage = "[-s <stats socket path>] <type> [<filter>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "type", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "filter", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_oldstats_top_ver2 = {
+ .cmd = cmd_stats_top,
+ .name = "oldstats top",
+ .usage = "[-s <stats socket path>] [-b] [<sort field>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('b', "show-disk-io", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "sort-field", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+
+struct doveadm_cmd_ver2 doveadm_cmd_oldstats_reset_ver2 = {
+ .cmd = cmd_stats_reset,
+ .name = "oldstats reset",
+ .usage = "[-s <stats socket path>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-penalty.c b/src/doveadm/doveadm-penalty.c
new file mode 100644
index 0000000..2f6d2d5
--- /dev/null
+++ b/src/doveadm/doveadm-penalty.c
@@ -0,0 +1,126 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "hash.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <unistd.h>
+
+struct penalty_line {
+ struct ip_addr ip;
+ unsigned int penalty;
+ time_t last_penalty, last_update;
+};
+
+struct penalty_context {
+ const char *anvil_path;
+
+ struct ip_addr net_ip;
+ unsigned int net_bits;
+};
+
+static void penalty_parse_line(const char *line, struct penalty_line *line_r)
+{
+ const char *const *args = t_strsplit_tabescaped(line);
+ const char *ident = args[0];
+ const char *penalty_str = args[1];
+ const char *last_penalty_str = args[2];
+ const char *last_update_str = args[3];
+
+ i_zero(line_r);
+
+ (void)net_addr2ip(ident, &line_r->ip);
+ if (str_to_uint(penalty_str, &line_r->penalty) < 0 ||
+ str_to_time(last_penalty_str, &line_r->last_penalty) < 0 ||
+ str_to_time(last_update_str, &line_r->last_update) < 0)
+ i_fatal("Read invalid penalty line: %s", line);
+}
+
+static void
+penalty_print_line(struct penalty_context *ctx,
+ const struct penalty_line *line)
+{
+ if (ctx->net_bits > 0) {
+ if (!net_is_in_network(&line->ip, &ctx->net_ip, ctx->net_bits))
+ return;
+ }
+
+ doveadm_print(net_ip2addr(&line->ip));
+ doveadm_print(dec2str(line->penalty));
+ doveadm_print(unixdate2str(line->last_penalty));
+ doveadm_print(t_strflocaltime("%H:%M:%S", line->last_update));
+}
+
+static void penalty_lookup(struct penalty_context *ctx)
+{
+#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n"
+#define ANVIL_CMD ANVIL_HANDSHAKE"PENALTY-DUMP\n"
+ struct istream *input;
+ const char *line;
+ int fd;
+
+ fd = doveadm_connect(ctx->anvil_path);
+ net_set_nonblock(fd, FALSE);
+ if (write(fd, ANVIL_CMD, strlen(ANVIL_CMD)) < 0)
+ i_fatal("write(%s) failed: %m", ctx->anvil_path);
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ struct penalty_line penalty_line;
+
+ penalty_parse_line(line, &penalty_line);
+ penalty_print_line(ctx, &penalty_line);
+ } T_END;
+ }
+ if (input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->anvil_path,
+ i_stream_get_error(input));
+ }
+
+ i_stream_destroy(&input);
+}
+
+static void cmd_penalty(struct doveadm_cmd_context *cctx)
+{
+ struct penalty_context ctx;
+ const char *netmask;
+
+ i_zero(&ctx);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &(ctx.anvil_path)))
+ ctx.anvil_path = t_strconcat(doveadm_settings->base_dir, "/anvil", NULL);
+
+ if (doveadm_cmd_param_str(cctx, "netmask", &netmask)) {
+ if (net_parse_range(netmask, &ctx.net_ip, &ctx.net_bits) != 0) {
+ doveadm_exit_code = EX_USAGE;
+ i_error("Invalid netmask '%s' given", netmask);
+ return;
+ }
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header_simple("IP");
+ doveadm_print_header_simple("penalty");
+ doveadm_print_header_simple("last_penalty");
+ doveadm_print_header_simple("last_update");
+
+ penalty_lookup(&ctx);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_penalty_ver2 = {
+ .name = "penalty",
+ .cmd = cmd_penalty,
+ .usage = "[-a <anvil socket path>] [<ip/bits>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a',"socket-path", CMD_PARAM_STR,0)
+DOVEADM_CMD_PARAM('\0',"netmask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-print-flow.c b/src/doveadm/doveadm-print-flow.c
new file mode 100644
index 0000000..7f4d01e
--- /dev/null
+++ b/src/doveadm/doveadm-print-flow.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream.h"
+#include "doveadm-print-private.h"
+
+struct doveadm_print_flow_header {
+ const char *title;
+ enum doveadm_print_header_flags flags;
+};
+
+struct doveadm_print_flow_context {
+ pool_t pool;
+ ARRAY(struct doveadm_print_flow_header) headers;
+ unsigned int header_idx;
+
+ bool streaming:1;
+};
+
+static struct doveadm_print_flow_context *ctx;
+
+static void
+doveadm_print_flow_header(const struct doveadm_print_header *hdr)
+{
+ struct doveadm_print_flow_header *fhdr;
+
+ fhdr = array_append_space(&ctx->headers);
+ fhdr->title = p_strdup(ctx->pool, hdr->title);
+ fhdr->flags = hdr->flags;
+}
+
+static void flow_next_hdr(void)
+{
+ if (++ctx->header_idx < array_count(&ctx->headers))
+ o_stream_nsend(doveadm_print_ostream, " ", 1);
+ else {
+ ctx->header_idx = 0;
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ }
+}
+
+static void doveadm_print_flow_print_header(const struct doveadm_print_flow_header *hdr)
+{
+ if ((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE) == 0) {
+ o_stream_nsend_str(doveadm_print_ostream, hdr->title);
+ o_stream_nsend(doveadm_print_ostream, "=", 1);
+ }
+}
+
+static void doveadm_print_flow_print(const char *value)
+{
+ const struct doveadm_print_flow_header *hdr =
+ array_idx(&ctx->headers, ctx->header_idx);
+
+ doveadm_print_flow_print_header(hdr);
+ o_stream_nsend_str(doveadm_print_ostream, value);
+ flow_next_hdr();
+}
+
+static void
+doveadm_print_flow_print_stream(const unsigned char *value, size_t size)
+{
+ const struct doveadm_print_flow_header *hdr =
+ array_idx(&ctx->headers, ctx->header_idx);
+
+ if (!ctx->streaming) {
+ ctx->streaming = TRUE;
+ doveadm_print_flow_print_header(hdr);
+ }
+ o_stream_nsend(doveadm_print_ostream, value, size);
+ if (size == 0) {
+ flow_next_hdr();
+ ctx->streaming = FALSE;
+ }
+}
+
+static void doveadm_print_flow_init(void)
+{
+ pool_t pool;
+
+ pool = pool_alloconly_create("doveadm print flow", 1024);
+ ctx = p_new(pool, struct doveadm_print_flow_context, 1);
+ ctx->pool = pool;
+ p_array_init(&ctx->headers, pool, 16);
+}
+
+static void doveadm_print_flow_flush(void)
+{
+ if (ctx->header_idx != 0) {
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ ctx->header_idx = 0;
+ }
+}
+
+static void doveadm_print_flow_deinit(void)
+{
+ pool_unref(&ctx->pool);
+ ctx = NULL;
+}
+
+struct doveadm_print_vfuncs doveadm_print_flow_vfuncs = {
+ "flow",
+
+ doveadm_print_flow_init,
+ doveadm_print_flow_deinit,
+ doveadm_print_flow_header,
+ doveadm_print_flow_print,
+ doveadm_print_flow_print_stream,
+ doveadm_print_flow_flush
+};
diff --git a/src/doveadm/doveadm-print-formatted.c b/src/doveadm/doveadm-print-formatted.c
new file mode 100644
index 0000000..d0518b0
--- /dev/null
+++ b/src/doveadm/doveadm-print-formatted.c
@@ -0,0 +1,92 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ostream.h"
+#include "doveadm.h"
+#include "doveadm-server.h"
+#include "doveadm-print.h"
+#include "doveadm-print-private.h"
+#include "client-connection.h"
+#include "var-expand.h"
+
+struct doveadm_print_formatted_context {
+ pool_t pool;
+ const char *format;
+ ARRAY(struct var_expand_table) headers;
+ string_t *buf;
+ string_t *vbuf;
+ unsigned int idx;
+};
+
+static struct doveadm_print_formatted_context ctx;
+
+void doveadm_print_formatted_set_format(const char *format)
+{
+ ctx.format = format;
+}
+
+static void doveadm_print_formatted_init(void)
+{
+ i_zero(&ctx);
+ ctx.pool = pool_alloconly_create("doveadm formatted print", 1024);
+ ctx.buf = str_new(ctx.pool, 256);
+ p_array_init(&ctx.headers, ctx.pool, 8);
+ ctx.idx = 0;
+}
+
+static void
+doveadm_print_formatted_header(const struct doveadm_print_header *hdr)
+{
+ struct var_expand_table entry;
+ i_zero(&entry);
+ entry.key = '\0';
+ entry.long_key = p_strdup(ctx.pool, hdr->key);
+ entry.value = NULL;
+ array_push_back(&ctx.headers, &entry);
+}
+
+
+static void doveadm_print_formatted_flush(void)
+{
+ o_stream_nsend(doveadm_print_ostream, str_data(ctx.buf), str_len(ctx.buf));
+ str_truncate(ctx.buf, 0);
+}
+
+static void doveadm_print_formatted_print(const char *value)
+{
+ if (ctx.format == NULL) {
+ i_fatal("formatted formatter cannot be used without a format.");
+ }
+ const char *error;
+ struct var_expand_table *entry = array_idx_modifiable(&ctx.headers, ctx.idx++);
+ entry->value = value;
+
+ if (ctx.idx >= array_count(&ctx.headers)) {
+ if (var_expand(ctx.buf, ctx.format, array_front(&ctx.headers), &error) <= 0) {
+ i_error("Failed to expand print format '%s': %s",
+ ctx.format, error);
+ }
+ doveadm_print_formatted_flush();
+ ctx.idx = 0;
+ }
+
+}
+
+static void doveadm_print_formatted_deinit(void)
+{
+ pool_unref(&ctx.pool);
+}
+
+struct doveadm_print_vfuncs doveadm_print_formatted_vfuncs = {
+ "formatted",
+
+ doveadm_print_formatted_init,
+ doveadm_print_formatted_deinit,
+ doveadm_print_formatted_header,
+ doveadm_print_formatted_print,
+ NULL,
+ doveadm_print_formatted_flush
+};
+
diff --git a/src/doveadm/doveadm-print-json.c b/src/doveadm/doveadm-print-json.c
new file mode 100644
index 0000000..540ecbb
--- /dev/null
+++ b/src/doveadm/doveadm-print-json.c
@@ -0,0 +1,161 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "json-parser.h"
+#include "doveadm.h"
+#include "doveadm-server.h"
+#include "doveadm-print.h"
+#include "doveadm-print-private.h"
+#include "client-connection.h"
+
+struct doveadm_print_json_context {
+ unsigned int header_idx, header_count;
+ bool first_row;
+ bool in_stream;
+ bool flushed;
+ ARRAY(struct doveadm_print_header) headers;
+ pool_t pool;
+ string_t *str;
+};
+
+static struct doveadm_print_json_context ctx;
+
+static void doveadm_print_json_flush_internal(void);
+
+static void doveadm_print_json_init(void)
+{
+ i_zero(&ctx);
+ ctx.pool = pool_alloconly_create("doveadm json print", 1024);
+ ctx.str = str_new(ctx.pool, 256);
+ p_array_init(&ctx.headers, ctx.pool, 1);
+ ctx.first_row = TRUE;
+ ctx.in_stream = FALSE;
+}
+
+static void
+doveadm_print_json_header(const struct doveadm_print_header *hdr)
+{
+ struct doveadm_print_header *lhdr;
+ lhdr = array_append_space(&ctx.headers);
+ lhdr->key = p_strdup(ctx.pool, hdr->key);
+ lhdr->flags = hdr->flags;
+ ctx.header_count++;
+}
+
+static void
+doveadm_print_json_value_header(const struct doveadm_print_header *hdr)
+{
+ // get header name
+ if (ctx.header_idx == 0) {
+ if (ctx.first_row == TRUE) {
+ ctx.first_row = FALSE;
+ str_append_c(ctx.str, '[');
+ } else {
+ str_append_c(ctx.str, ',');
+ }
+ str_append_c(ctx.str, '{');
+ } else {
+ str_append_c(ctx.str, ',');
+ }
+
+ str_append_c(ctx.str, '"');
+ json_append_escaped(ctx.str, hdr->key);
+ str_append_c(ctx.str, '"');
+ str_append_c(ctx.str, ':');
+}
+
+static void
+doveadm_print_json_value_footer(void) {
+ if (++ctx.header_idx == ctx.header_count) {
+ ctx.header_idx = 0;
+ str_append_c(ctx.str, '}');
+ doveadm_print_json_flush_internal();
+ }
+}
+
+static void doveadm_print_json_print(const char *value)
+{
+ const struct doveadm_print_header *hdr = array_idx(&ctx.headers, ctx.header_idx);
+
+ doveadm_print_json_value_header(hdr);
+
+ if (value == NULL) {
+ str_append(ctx.str, "null");
+ } else if ((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_NUMBER) != 0) {
+ i_assert(str_is_float(value, '\0'));
+ str_append(ctx.str, value);
+ } else {
+ str_append_c(ctx.str, '"');
+ json_append_escaped(ctx.str, value);
+ str_append_c(ctx.str, '"');
+ }
+
+ doveadm_print_json_value_footer();
+}
+
+static void
+doveadm_print_json_print_stream(const unsigned char *value, size_t size)
+{
+ if (!ctx.in_stream) {
+ const struct doveadm_print_header *hdr =
+ array_idx(&ctx.headers, ctx.header_idx);
+ doveadm_print_json_value_header(hdr);
+ i_assert((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_NUMBER) == 0);
+ str_append_c(ctx.str, '"');
+ ctx.in_stream = TRUE;
+ }
+
+ if (size == 0) {
+ str_append_c(ctx.str, '"');
+ doveadm_print_json_value_footer();
+ ctx.in_stream = FALSE;
+ return;
+ }
+
+ json_append_escaped_data(ctx.str, value, size);
+
+ if (str_len(ctx.str) >= IO_BLOCK_SIZE)
+ doveadm_print_json_flush_internal();
+}
+
+static void doveadm_print_json_flush_internal(void)
+{
+ o_stream_nsend(doveadm_print_ostream, str_data(ctx.str), str_len(ctx.str));
+ str_truncate(ctx.str, 0);
+}
+
+static void doveadm_print_json_flush(void)
+{
+ if (ctx.flushed)
+ return;
+ ctx.flushed = TRUE;
+
+ if (ctx.first_row == FALSE)
+ str_append_c(ctx.str,']');
+ else {
+ str_append_c(ctx.str,'[');
+ str_append_c(ctx.str,']');
+ }
+ doveadm_print_json_flush_internal();
+}
+
+static void doveadm_print_json_deinit(void)
+{
+ pool_unref(&ctx.pool);
+}
+
+struct doveadm_print_vfuncs doveadm_print_json_vfuncs = {
+ "json",
+
+ doveadm_print_json_init,
+ doveadm_print_json_deinit,
+ doveadm_print_json_header,
+ doveadm_print_json_print,
+ doveadm_print_json_print_stream,
+ doveadm_print_json_flush
+};
+
diff --git a/src/doveadm/doveadm-print-pager.c b/src/doveadm/doveadm-print-pager.c
new file mode 100644
index 0000000..3e14383
--- /dev/null
+++ b/src/doveadm/doveadm-print-pager.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream.h"
+#include "doveadm-print-private.h"
+
+struct doveadm_print_pager_header {
+ const char *title;
+ enum doveadm_print_header_flags flags;
+};
+
+struct doveadm_print_pager_context {
+ pool_t pool;
+ ARRAY(struct doveadm_print_pager_header) headers;
+ unsigned int header_idx;
+
+ bool streaming:1;
+ bool first_page:1;
+};
+
+static struct doveadm_print_pager_context *ctx;
+
+static void
+doveadm_print_pager_header(const struct doveadm_print_header *hdr)
+{
+ struct doveadm_print_pager_header *fhdr;
+
+ fhdr = array_append_space(&ctx->headers);
+ fhdr->flags = hdr->flags;
+ fhdr->title = p_strdup(ctx->pool, hdr->title);
+}
+
+static void pager_next_hdr(void)
+{
+ if (++ctx->header_idx == array_count(&ctx->headers)) {
+ ctx->header_idx = 0;
+ }
+}
+
+static void doveadm_print_pager_print(const char *value)
+{
+ const struct doveadm_print_pager_header *hdr =
+ array_idx(&ctx->headers, ctx->header_idx);
+
+ if (ctx->header_idx == 0 && !ctx->first_page) {
+ o_stream_nsend(doveadm_print_ostream, "\f\n", 2);
+ }
+ ctx->first_page = FALSE;
+ if ((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE) == 0) {
+ o_stream_nsend_str(doveadm_print_ostream, hdr->title);
+ o_stream_nsend(doveadm_print_ostream, ": ", 2);
+ }
+ o_stream_nsend_str(doveadm_print_ostream, value);
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ pager_next_hdr();
+}
+
+static void
+doveadm_print_pager_print_stream(const unsigned char *value, size_t size)
+{
+ const struct doveadm_print_pager_header *hdr =
+ array_idx(&ctx->headers, ctx->header_idx);
+
+ if (!ctx->streaming) {
+ ctx->streaming = TRUE;
+ if ((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE) == 0) {
+ o_stream_nsend_str(doveadm_print_ostream, hdr->title);
+ o_stream_nsend(doveadm_print_ostream, ":\n", 2);
+ }
+ }
+ o_stream_nsend(doveadm_print_ostream, value, size);
+ if (size == 0) {
+ pager_next_hdr();
+ ctx->streaming = FALSE;
+ }
+}
+
+static void doveadm_print_pager_init(void)
+{
+ pool_t pool;
+
+ pool = pool_alloconly_create("doveadm print pager", 1024);
+ ctx = p_new(pool, struct doveadm_print_pager_context, 1);
+ ctx->pool = pool;
+ ctx->first_page = TRUE;
+ p_array_init(&ctx->headers, pool, 16);
+}
+
+static void doveadm_print_pager_flush(void)
+{
+ if (ctx->header_idx != 0) {
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ ctx->header_idx = 0;
+ }
+}
+
+static void doveadm_print_pager_deinit(void)
+{
+ pool_unref(&ctx->pool);
+ ctx = NULL;
+}
+
+struct doveadm_print_vfuncs doveadm_print_pager_vfuncs = {
+ DOVEADM_PRINT_TYPE_PAGER,
+
+ doveadm_print_pager_init,
+ doveadm_print_pager_deinit,
+ doveadm_print_pager_header,
+ doveadm_print_pager_print,
+ doveadm_print_pager_print_stream,
+ doveadm_print_pager_flush
+};
diff --git a/src/doveadm/doveadm-print-private.h b/src/doveadm/doveadm-print-private.h
new file mode 100644
index 0000000..fe85560
--- /dev/null
+++ b/src/doveadm/doveadm-print-private.h
@@ -0,0 +1,31 @@
+#ifndef DOVEADM_PRINT_PRIVATE_H
+#define DOVEADM_PRINT_PRIVATE_H
+
+#include "doveadm-print.h"
+
+struct doveadm_print_header {
+ const char *key;
+ const char *title;
+ enum doveadm_print_header_flags flags;
+};
+
+struct doveadm_print_vfuncs {
+ const char *name;
+
+ void (*init)(void);
+ void (*deinit)(void);
+
+ void (*header)(const struct doveadm_print_header *hdr);
+ void (*print)(const char *value);
+ void (*print_stream)(const unsigned char *value, size_t size);
+ void (*flush)(void);
+};
+
+extern struct doveadm_print_vfuncs doveadm_print_flow_vfuncs;
+extern struct doveadm_print_vfuncs doveadm_print_tab_vfuncs;
+extern struct doveadm_print_vfuncs doveadm_print_table_vfuncs;
+extern struct doveadm_print_vfuncs doveadm_print_pager_vfuncs;
+extern struct doveadm_print_vfuncs doveadm_print_json_vfuncs;
+extern struct doveadm_print_vfuncs doveadm_print_formatted_vfuncs;
+
+#endif
diff --git a/src/doveadm/doveadm-print-server.c b/src/doveadm/doveadm-print-server.c
new file mode 100644
index 0000000..93c6ed9
--- /dev/null
+++ b/src/doveadm/doveadm-print-server.c
@@ -0,0 +1,119 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "doveadm.h"
+#include "doveadm-print-private.h"
+#include "client-connection.h"
+
+#define DOVEADM_PRINT_FLUSH_TIMEOUT_SECS 60
+
+struct doveadm_print_server_context {
+ unsigned int header_idx, header_count;
+
+ string_t *str;
+};
+
+static struct doveadm_print_server_context ctx;
+
+static void doveadm_print_server_flush(void);
+
+static void doveadm_print_server_init(void)
+{
+ ctx.str = str_new(default_pool, 256);
+}
+
+static void doveadm_print_server_deinit(void)
+{
+ str_free(&ctx.str);
+}
+
+static void
+doveadm_print_server_header(const struct doveadm_print_header *hdr ATTR_UNUSED)
+{
+ /* no need to transfer these. the client should already know what
+ it's getting */
+ ctx.header_count++;
+}
+
+static void doveadm_print_server_print(const char *value)
+{
+ str_append_tabescaped(ctx.str, value);
+ str_append_c(ctx.str, '\t');
+
+ if (++ctx.header_idx == ctx.header_count) {
+ ctx.header_idx = 0;
+ doveadm_print_server_flush();
+ }
+}
+
+static void
+doveadm_print_server_print_stream(const unsigned char *value, size_t size)
+{
+ if (size == 0) {
+ doveadm_print_server_print("");
+ return;
+ }
+ str_append_tabescaped_n(ctx.str, value, size);
+
+ if (str_len(ctx.str) >= IO_BLOCK_SIZE)
+ doveadm_print_server_flush();
+}
+
+static int flush_callback(struct doveadm_print_server_context *ctx ATTR_UNUSED)
+{
+
+ int ret;
+ /* Keep flushing until everything is sent */
+ if ((ret = o_stream_flush(doveadm_print_ostream)) != 0)
+ io_loop_stop(current_ioloop);
+ return ret;
+}
+
+static void handle_flush_timeout(struct doveadm_print_server_context *ctx ATTR_UNUSED)
+{
+ io_loop_stop(current_ioloop);
+ o_stream_close(doveadm_print_ostream);
+ i_error("write(%s) failed: Timed out after %u seconds",
+ o_stream_get_name(doveadm_print_ostream),
+ DOVEADM_PRINT_FLUSH_TIMEOUT_SECS);
+}
+
+static void doveadm_print_server_flush(void)
+{
+ o_stream_nsend(doveadm_print_ostream,
+ str_data(ctx.str), str_len(ctx.str));
+ str_truncate(ctx.str, 0);
+ o_stream_uncork(doveadm_print_ostream);
+
+ if (o_stream_get_buffer_used_size(doveadm_print_ostream) < IO_BLOCK_SIZE ||
+ doveadm_print_ostream->stream_errno != 0)
+ return;
+ /* Wait until buffer is flushed to avoid it growing too large */
+ struct ioloop *prev_loop = current_ioloop;
+ struct ioloop *loop = io_loop_create();
+ /* Ensure we don't get stuck here forever */
+ struct timeout *to =
+ timeout_add(DOVEADM_PRINT_FLUSH_TIMEOUT_SECS*1000, handle_flush_timeout, &ctx);
+ o_stream_switch_ioloop_to(doveadm_print_ostream, loop);
+ o_stream_set_flush_callback(doveadm_print_ostream, flush_callback, &ctx);
+ io_loop_run(loop);
+ timeout_remove(&to);
+ o_stream_unset_flush_callback(doveadm_print_ostream);
+ o_stream_switch_ioloop_to(doveadm_print_ostream, prev_loop);
+ io_loop_destroy(&loop);
+}
+
+struct doveadm_print_vfuncs doveadm_print_server_vfuncs = {
+ DOVEADM_PRINT_TYPE_SERVER,
+
+ doveadm_print_server_init,
+ doveadm_print_server_deinit,
+ doveadm_print_server_header,
+ doveadm_print_server_print,
+ doveadm_print_server_print_stream,
+ doveadm_print_server_flush
+};
diff --git a/src/doveadm/doveadm-print-tab.c b/src/doveadm/doveadm-print-tab.c
new file mode 100644
index 0000000..c5871ec
--- /dev/null
+++ b/src/doveadm/doveadm-print-tab.c
@@ -0,0 +1,76 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream.h"
+#include "doveadm-print-private.h"
+
+struct doveadm_print_tab_context {
+ unsigned int header_idx, header_count;
+
+ bool header_written:1;
+};
+
+static struct doveadm_print_tab_context ctx;
+
+static void doveadm_print_tab_flush_header(void)
+{
+ if (!ctx.header_written) {
+ if (!doveadm_print_hide_titles)
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ ctx.header_written = TRUE;
+ }
+}
+
+static void
+doveadm_print_tab_header(const struct doveadm_print_header *hdr)
+{
+ ctx.header_count++;
+ if (!doveadm_print_hide_titles) {
+ if (ctx.header_count > 1)
+ o_stream_nsend(doveadm_print_ostream, "\t", 1);
+ o_stream_nsend_str(doveadm_print_ostream, hdr->title);
+ }
+}
+
+static void doveadm_print_tab_print(const char *value)
+{
+ doveadm_print_tab_flush_header();
+ if (ctx.header_idx > 0)
+ o_stream_nsend(doveadm_print_ostream, "\t", 1);
+ o_stream_nsend_str(doveadm_print_ostream, value);
+
+ if (++ctx.header_idx == ctx.header_count) {
+ ctx.header_idx = 0;
+ o_stream_nsend(doveadm_print_ostream, "\n", 1);
+ }
+}
+
+static void
+doveadm_print_tab_print_stream(const unsigned char *value, size_t size)
+{
+ if (size == 0) {
+ doveadm_print_tab_print("");
+ return;
+ }
+ doveadm_print_tab_flush_header();
+ if (ctx.header_idx > 0)
+ o_stream_nsend(doveadm_print_ostream, "\t", 1);
+ o_stream_nsend(doveadm_print_ostream, value, size);
+}
+
+static void doveadm_print_tab_flush(void)
+{
+ doveadm_print_tab_flush_header();
+}
+
+struct doveadm_print_vfuncs doveadm_print_tab_vfuncs = {
+ "tab",
+
+ NULL,
+ NULL,
+ doveadm_print_tab_header,
+ doveadm_print_tab_print,
+ doveadm_print_tab_print_stream,
+ doveadm_print_tab_flush
+};
diff --git a/src/doveadm/doveadm-print-table.c b/src/doveadm/doveadm-print-table.c
new file mode 100644
index 0000000..25a4441
--- /dev/null
+++ b/src/doveadm/doveadm-print-table.c
@@ -0,0 +1,268 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "doveadm-print-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+
+#define DEFAULT_COLUMNS 80
+#define MIN_COLUMNS 30
+#define MAX_BUFFER_LINES 100
+
+struct doveadm_print_table_header {
+ const char *key;
+ const char *title;
+ enum doveadm_print_header_flags flags;
+ size_t min_length, max_length, length;
+};
+
+struct doveadm_print_table_context {
+ pool_t pool;
+ ARRAY(struct doveadm_print_table_header) headers;
+ ARRAY_TYPE(const_string) buffered_values;
+ string_t *stream;
+ unsigned int hdr_idx;
+ unsigned int columns;
+
+ bool lengths_set:1;
+};
+
+static struct doveadm_print_table_context *ctx;
+
+static void
+doveadm_print_table_header(const struct doveadm_print_header *hdr)
+{
+ struct doveadm_print_table_header *thdr;
+
+ thdr = array_append_space(&ctx->headers);
+ thdr->key = p_strdup(ctx->pool, hdr->key);
+ thdr->title = p_strdup(ctx->pool, hdr->title);
+ thdr->length = thdr->max_length = thdr->min_length = strlen(hdr->title);
+ thdr->flags = hdr->flags;
+}
+
+static void doveadm_calc_header_length(void)
+{
+ struct doveadm_print_table_header *headers;
+ const char *value, *const *values;
+ unsigned int i, line, hdr_count, value_count, line_count;
+ size_t len, max_length, orig_length, diff;
+
+ ctx->lengths_set = TRUE;
+
+ headers = array_get_modifiable(&ctx->headers, &hdr_count);
+ values = array_get(&ctx->buffered_values, &value_count);
+ i_assert((value_count % hdr_count) == 0);
+ line_count = value_count / hdr_count;
+
+ /* find min and max lengths of fields */
+ for (line = 0; line < line_count; line++) {
+ for (i = 0; i < hdr_count; i++) {
+ value = values[line*hdr_count + i];
+ len = value == NULL ? 0 : uni_utf8_strlen(value);
+ if (headers[i].min_length > len)
+ headers[i].min_length = len;
+ if (headers[i].max_length < len) {
+ headers[i].max_length = len;
+ headers[i].length = len;
+ }
+ }
+ }
+
+ /* +1 for space between fields */
+ max_length = 0;
+ for (i = 0; i < hdr_count; i++)
+ max_length += headers[i].max_length + 1;
+ max_length--;
+
+ while (max_length > ctx->columns) {
+ /* shrink something so we'll fit */
+ orig_length = max_length;
+ for (i = hdr_count - 1;; i--) {
+ diff = headers[i].length - headers[i].min_length;
+ if (max_length - diff <= ctx->columns) {
+ /* we can finish with this */
+ diff = max_length - ctx->columns;
+ headers[i].length -= diff;
+ max_length -= diff;
+ break;
+ }
+ if (diff > 0) {
+ /* take a bit off from it */
+ headers[i].length -= diff == 1 ? 1 : diff/2;
+ }
+
+ if (i == 0)
+ break;
+ }
+ if (max_length == orig_length) {
+ /* can't shrink it any more */
+ break;
+ }
+ }
+ if (max_length < ctx->columns) {
+ for (i = 0; i < hdr_count; i++) {
+ if ((headers[i].flags & DOVEADM_PRINT_HEADER_FLAG_EXPAND) != 0) {
+ i++;
+ break;
+ }
+ }
+ headers[i-1].length += (ctx->columns - max_length) / 2;
+ }
+}
+
+static size_t utf8_correction(const char *str)
+{
+ size_t i, len = 0;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ if ((str[i] & 0xc0) == 0x80)
+ len++;
+ }
+ return len;
+}
+
+static void doveadm_print_next(const char *value)
+{
+ const struct doveadm_print_table_header *hdr;
+ int value_padded_len;
+
+ hdr = array_idx(&ctx->headers, ctx->hdr_idx);
+
+ value_padded_len = hdr->length + utf8_correction(value);
+ if ((hdr->flags & DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY) == 0)
+ printf("%-*s", value_padded_len, value);
+ else
+ printf("%*s", value_padded_len, value);
+
+ if (++ctx->hdr_idx == array_count(&ctx->headers)) {
+ ctx->hdr_idx = 0;
+ printf("\n");
+ } else {
+ printf(" ");
+ }
+}
+
+static void doveadm_print_headers(void)
+{
+ const struct doveadm_print_table_header *headers;
+ unsigned int i, count;
+
+ if (doveadm_print_hide_titles)
+ return;
+
+ headers = array_get(&ctx->headers, &count);
+ /* if all headers are hidden, don't print any of them */
+ for (i = 0; i < count; i++) {
+ if ((headers[i].flags & DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE) == 0)
+ break;
+ }
+ if (i == count)
+ return;
+ for (i = 0; i < count; i++) {
+ if (i > 0) printf(" ");
+
+ if ((headers[i].flags &
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY) == 0) {
+ printf("%-*s", (int)headers[i].length,
+ headers[i].title);
+ } else {
+ printf("%*s", (int)headers[i].length,
+ headers[i].title);
+ }
+ }
+ printf("\n");
+}
+
+static void doveadm_buffer_flush(void)
+{
+ const char *value;
+
+ doveadm_calc_header_length();
+ doveadm_print_headers();
+
+ array_foreach_elem(&ctx->buffered_values, value)
+ doveadm_print_next(value);
+ array_clear(&ctx->buffered_values);
+}
+
+static void doveadm_print_table_print(const char *value)
+{
+ unsigned int line_count;
+
+ if (!ctx->lengths_set) {
+ line_count = array_count(&ctx->buffered_values) /
+ array_count(&ctx->headers);
+ if (line_count < MAX_BUFFER_LINES) {
+ value = p_strdup(ctx->pool, value);
+ array_push_back(&ctx->buffered_values, &value);
+ return;
+ }
+ doveadm_buffer_flush();
+ }
+ doveadm_print_next(value);
+}
+
+static void
+doveadm_print_table_print_stream(const unsigned char *value, size_t size)
+{
+ if (memchr(value, '\n', size) != NULL)
+ i_fatal("table formatter doesn't support multi-line values");
+
+ if (size != 0)
+ str_append_data(ctx->stream, value, size);
+ else {
+ doveadm_print_table_print(str_c(ctx->stream));
+ str_truncate(ctx->stream, 0);
+ }
+}
+
+static void doveadm_print_table_flush(void)
+{
+ if (!ctx->lengths_set && array_count(&ctx->headers) > 0)
+ doveadm_buffer_flush();
+}
+
+static void doveadm_print_table_init(void)
+{
+ pool_t pool;
+ struct winsize ws;
+
+ pool = pool_alloconly_create("doveadm print table", 2048);
+ ctx = p_new(pool, struct doveadm_print_table_context, 1);
+ ctx->pool = pool;
+ ctx->stream = str_new(default_pool, 128);
+ p_array_init(&ctx->headers, pool, 16);
+ i_array_init(&ctx->buffered_values, 64);
+ ctx->columns = DEFAULT_COLUMNS;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
+ ctx->columns = ws.ws_col < MIN_COLUMNS ?
+ MIN_COLUMNS : ws.ws_col;
+ }
+}
+
+static void doveadm_print_table_deinit(void)
+{
+ str_free(&ctx->stream);
+ array_free(&ctx->buffered_values);
+ pool_unref(&ctx->pool);
+ ctx = NULL;
+}
+
+struct doveadm_print_vfuncs doveadm_print_table_vfuncs = {
+ "table",
+
+ doveadm_print_table_init,
+ doveadm_print_table_deinit,
+ doveadm_print_table_header,
+ doveadm_print_table_print,
+ doveadm_print_table_print_stream,
+ doveadm_print_table_flush
+};
diff --git a/src/doveadm/doveadm-print.c b/src/doveadm/doveadm-print.c
new file mode 100644
index 0000000..c1c5fac
--- /dev/null
+++ b/src/doveadm/doveadm-print.c
@@ -0,0 +1,211 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "doveadm-print-private.h"
+
+struct doveadm_print_header_context {
+ const char *key;
+ char *sticky_value;
+ bool sticky;
+};
+
+struct doveadm_print_context {
+ pool_t pool;
+ ARRAY(struct doveadm_print_header_context) headers;
+ const struct doveadm_print_vfuncs *v;
+
+ unsigned int header_idx;
+ bool print_stream_open;
+};
+
+bool doveadm_print_hide_titles = FALSE;
+struct ostream *doveadm_print_ostream = NULL;
+
+static struct doveadm_print_context *ctx;
+
+bool doveadm_print_is_initialized(void)
+{
+ return ctx != NULL;
+}
+
+void doveadm_print_header(const char *key, const char *title,
+ enum doveadm_print_header_flags flags)
+{
+ struct doveadm_print_header hdr;
+ struct doveadm_print_header_context *hdr_ctx;
+
+ i_assert(title != NULL);
+
+ i_zero(&hdr);
+ hdr.key = key;
+ hdr.title = title;
+ hdr.flags = flags;
+ ctx->v->header(&hdr);
+
+ hdr_ctx = array_append_space(&ctx->headers);
+ hdr_ctx->key = p_strdup(ctx->pool, key);
+ hdr_ctx->sticky = (flags & DOVEADM_PRINT_HEADER_FLAG_STICKY) != 0;
+}
+
+void doveadm_print_header_simple(const char *key_title)
+{
+ doveadm_print_header(key_title, key_title, 0);
+}
+
+unsigned int doveadm_print_get_headers_count(void)
+{
+ return array_count(&ctx->headers);
+}
+
+static void doveadm_print_sticky_headers(void)
+{
+ const struct doveadm_print_header_context *headers;
+ unsigned int count;
+
+ headers = array_get(&ctx->headers, &count);
+ i_assert(count > 0);
+ for (;;) {
+ if (ctx->header_idx == count)
+ ctx->header_idx = 0;
+ else if (headers[ctx->header_idx].sticky) {
+ ctx->v->print(headers[ctx->header_idx].sticky_value);
+ ctx->header_idx++;
+ } else {
+ break;
+ }
+ }
+}
+
+void doveadm_print(const char *value)
+{
+ i_assert(!ctx->print_stream_open);
+
+ doveadm_print_sticky_headers();
+ ctx->v->print(value);
+ ctx->header_idx++;
+}
+
+void doveadm_print_num(uintmax_t value)
+{
+ T_BEGIN {
+ doveadm_print(dec2str(value));
+ } T_END;
+}
+
+void doveadm_print_stream(const void *value, size_t size)
+{
+ if (!ctx->print_stream_open) {
+ doveadm_print_sticky_headers();
+ ctx->print_stream_open = TRUE;
+ }
+ ctx->v->print_stream(value, size);
+ if (size == 0) {
+ ctx->header_idx++;
+ ctx->print_stream_open = FALSE;
+ }
+}
+
+int doveadm_print_istream(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ doveadm_print_stream(data, size);
+ i_stream_skip(input, size);
+ }
+ i_assert(ret == -1);
+ doveadm_print_stream("", 0);
+ if (input->stream_errno != 0) {
+ /* caller will log the error */
+ return -1;
+ }
+ return 0;
+}
+
+void doveadm_print_sticky(const char *key, const char *value)
+{
+ struct doveadm_print_header_context *hdr;
+
+ if (ctx == NULL) {
+ /* command doesn't really print anything */
+ return;
+ }
+
+ array_foreach_modifiable(&ctx->headers, hdr) {
+ if (strcmp(hdr->key, key) == 0) {
+ i_free(hdr->sticky_value);
+ hdr->sticky_value = i_strdup(value);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+void doveadm_print_flush(void)
+{
+ if (ctx != NULL && ctx->v->flush != NULL)
+ ctx->v->flush();
+ o_stream_uncork(doveadm_print_ostream);
+ o_stream_cork(doveadm_print_ostream);
+}
+
+void doveadm_print_unstick_headers(void)
+{
+ struct doveadm_print_header_context *hdr;
+
+ if (ctx != NULL) {
+ array_foreach_modifiable(&ctx->headers, hdr)
+ hdr->sticky = FALSE;
+ }
+}
+
+void doveadm_print_init(const char *name)
+{
+ pool_t pool;
+ unsigned int i;
+
+ if (ctx != NULL) {
+ /* already forced the type */
+ return;
+ }
+
+ pool = pool_alloconly_create("doveadm print", 1024);
+ ctx = p_new(pool, struct doveadm_print_context, 1);
+ ctx->pool = pool;
+ p_array_init(&ctx->headers, pool, 16);
+
+ for (i = 0; doveadm_print_vfuncs_all[i] != NULL; i++) {
+ if (strcmp(doveadm_print_vfuncs_all[i]->name, name) == 0) {
+ ctx->v = doveadm_print_vfuncs_all[i];
+ break;
+ }
+ }
+ if (ctx->v == NULL)
+ i_fatal("Unknown print formatter: %s", name);
+ if (ctx->v->init != NULL)
+ ctx->v->init();
+}
+
+void doveadm_print_deinit(void)
+{
+ struct doveadm_print_header_context *hdr;
+
+ if (ctx == NULL)
+ return;
+
+ if (ctx->v->flush != NULL && doveadm_print_ostream != NULL) {
+ ctx->v->flush();
+ o_stream_uncork(doveadm_print_ostream);
+ }
+ if (ctx->v->deinit != NULL)
+ ctx->v->deinit();
+ array_foreach_modifiable(&ctx->headers, hdr)
+ i_free(hdr->sticky_value);
+ pool_unref(&ctx->pool);
+ ctx = NULL;
+}
diff --git a/src/doveadm/doveadm-print.h b/src/doveadm/doveadm-print.h
new file mode 100644
index 0000000..2d610e8
--- /dev/null
+++ b/src/doveadm/doveadm-print.h
@@ -0,0 +1,48 @@
+#ifndef DOVEADM_PRINT_H
+#define DOVEADM_PRINT_H
+
+#define DOVEADM_PRINT_TYPE_TAB "tab"
+#define DOVEADM_PRINT_TYPE_FLOW "flow"
+#define DOVEADM_PRINT_TYPE_PAGER "pager"
+#define DOVEADM_PRINT_TYPE_TABLE "table"
+#define DOVEADM_PRINT_TYPE_SERVER "server"
+#define DOVEADM_PRINT_TYPE_JSON "json"
+#define DOVEADM_PRINT_TYPE_FORMATTED "formatted"
+
+enum doveadm_print_header_flags {
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY = 0x01,
+ DOVEADM_PRINT_HEADER_FLAG_STICKY = 0x02,
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE = 0x04,
+ DOVEADM_PRINT_HEADER_FLAG_EXPAND = 0x08,
+ DOVEADM_PRINT_HEADER_FLAG_NUMBER = 0x10
+};
+
+extern const struct doveadm_print_vfuncs *doveadm_print_vfuncs_all[];
+extern bool doveadm_print_hide_titles;
+/* points to either stdout or to doveadm-server's TCP connection */
+extern struct ostream *doveadm_print_ostream;
+
+bool doveadm_print_is_initialized(void);
+
+void doveadm_print_header(const char *key, const char *title,
+ enum doveadm_print_header_flags flags);
+void doveadm_print_header_simple(const char *key_title);
+unsigned int doveadm_print_get_headers_count(void);
+
+void doveadm_print(const char *value);
+void doveadm_print_num(uintmax_t value);
+/* Stream for same field continues until len=0 */
+void doveadm_print_stream(const void *value, size_t size);
+/* Print the whole input stream. Returns 0 if ok, -1 if stream read() failed.
+ The caller must log the error. */
+int doveadm_print_istream(struct istream *input);
+void doveadm_print_sticky(const char *key, const char *value);
+void doveadm_print_flush(void);
+void doveadm_print_unstick_headers(void);
+
+void doveadm_print_init(const char *name);
+void doveadm_print_deinit(void);
+
+void doveadm_print_formatted_set_format(const char *format);
+
+#endif
diff --git a/src/doveadm/doveadm-proxy.c b/src/doveadm/doveadm-proxy.c
new file mode 100644
index 0000000..04fde52
--- /dev/null
+++ b/src/doveadm/doveadm-proxy.c
@@ -0,0 +1,214 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "strescape.h"
+#include "ipc-client.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+struct proxy_context {
+ struct ipc_client *ipc;
+ const char *username_field;
+ const char *kick_hosts;
+};
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_proxy[];
+
+static void proxy_cmd_help(struct doveadm_cmd_context *cctx) ATTR_NORETURN;
+
+static struct proxy_context *
+cmd_proxy_init(struct doveadm_cmd_context *cctx)
+{
+ struct proxy_context *ctx;
+ const char *socket_path;
+
+ ctx = t_new(struct proxy_context, 1);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &socket_path)) {
+ socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/ipc", NULL);
+ }
+ (void)doveadm_cmd_param_str(cctx, "passdb-field", &ctx->username_field);
+ (void)doveadm_cmd_param_str(cctx, "host", &ctx->kick_hosts);
+ ctx->ipc = ipc_client_init(socket_path);
+ return ctx;
+}
+
+static void cmd_proxy_list_header(const char *const *args)
+{
+ struct {
+ const char *key;
+ const char *title;
+ } header_map[] = {
+ { "service", "proto" },
+ { "src-ip", "src ip" },
+ { "dest-ip", "dest ip" },
+ { "dest-port", "port" },
+ };
+ for (unsigned int i = 0; args[i] != NULL; i++) {
+ const char *arg = args[i];
+
+ if (strcmp(arg, "username") == 0 ||
+ str_begins(arg, "user_")) {
+ doveadm_print_header(arg, arg,
+ DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ continue;
+ }
+ const char *title = arg;
+ for (unsigned int j = 0; j < N_ELEMENTS(header_map); j++) {
+ if (strcmp(header_map[j].key, arg) == 0) {
+ title = header_map[j].title;
+ break;
+ }
+ }
+ doveadm_print_header(arg, title, 0);
+ }
+}
+
+static void cmd_proxy_list_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context)
+{
+ bool *seen_header = context;
+
+ switch (state) {
+ case IPC_CLIENT_CMD_STATE_REPLY: {
+ const char *const *args = t_strsplit_tabescaped(data);
+
+ if (!*seen_header) {
+ cmd_proxy_list_header(args);
+ *seen_header = TRUE;
+ } else {
+ for (; *args != NULL; args++)
+ doveadm_print(*args);
+ }
+ return;
+ }
+ case IPC_CLIENT_CMD_STATE_OK:
+ break;
+ case IPC_CLIENT_CMD_STATE_ERROR:
+ i_error("LIST-FULL failed: %s", data);
+ break;
+ }
+ io_loop_stop(current_ioloop);
+}
+
+static void cmd_proxy_list(struct doveadm_cmd_context *cctx)
+{
+ struct proxy_context *ctx;
+ bool seen_header = FALSE;
+
+ ctx = cmd_proxy_init(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+
+ io_loop_set_running(current_ioloop);
+ ipc_client_cmd(ctx->ipc, "proxy\t*\tLIST-FULL",
+ cmd_proxy_list_callback, &seen_header);
+ if (io_loop_is_running(current_ioloop))
+ io_loop_run(current_ioloop);
+ ipc_client_deinit(&ctx->ipc);
+}
+
+static void cmd_proxy_kick_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context ATTR_UNUSED)
+{
+ switch (state) {
+ case IPC_CLIENT_CMD_STATE_REPLY:
+ return;
+ case IPC_CLIENT_CMD_STATE_OK:
+ if (data[0] == '\0')
+ data = "0";
+ doveadm_print(data);
+ break;
+ case IPC_CLIENT_CMD_STATE_ERROR:
+ i_error("KICK failed: %s", data);
+ doveadm_exit_code = EX_TEMPFAIL;
+ break;
+ }
+ io_loop_stop(current_ioloop);
+}
+
+static void cmd_proxy_kick(struct doveadm_cmd_context *cctx)
+{
+ struct proxy_context *ctx;
+ const char *const *users = NULL;
+ string_t *cmd;
+
+ ctx = cmd_proxy_init(cctx);
+ (void)doveadm_cmd_param_array(cctx, "user", &users);
+ if (users == NULL && ctx->kick_hosts == NULL) {
+ proxy_cmd_help(cctx);
+ return;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{count} connections kicked\n");
+ doveadm_print_header_simple("count");
+
+ cmd = t_str_new(128);
+ str_append(cmd, "proxy\t*\t");
+ if (ctx->kick_hosts != NULL) {
+ str_append(cmd, "KICK-HOST\t");
+ str_append(cmd, ctx->kick_hosts);
+ }
+ else if (ctx->username_field == NULL)
+ str_append(cmd, "KICK");
+ else {
+ str_append(cmd, "KICK-ALT\t");
+ str_append_tabescaped(cmd, ctx->username_field);
+ }
+ if (users != NULL) {
+ for (unsigned int i = 0; users[i] != NULL; i++) {
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, users[i]);
+ }
+ }
+ ipc_client_cmd(ctx->ipc, str_c(cmd), cmd_proxy_kick_callback, NULL);
+ io_loop_run(current_ioloop);
+ ipc_client_deinit(&ctx->ipc);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_proxy[] = {
+{
+ .name = "proxy list",
+ .usage = "[-a <ipc socket path>]",
+ .cmd = cmd_proxy_list,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "proxy kick",
+ .usage = "[-a <ipc socket path>] [-f <passdb field>] [-h <host> [...] | <user> [...]]",
+ .cmd = cmd_proxy_kick,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('f', "passdb-field", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('h', "host", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void proxy_cmd_help(struct doveadm_cmd_context *cctx)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_proxy); i++) {
+ if (doveadm_cmd_proxy[i].cmd == cctx->cmd->cmd)
+ help_ver2(&doveadm_cmd_proxy[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_proxy_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_proxy); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_proxy[i]);
+}
diff --git a/src/doveadm/doveadm-pw.c b/src/doveadm/doveadm-pw.c
new file mode 100644
index 0000000..ed85006
--- /dev/null
+++ b/src/doveadm/doveadm-pw.c
@@ -0,0 +1,135 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "password-scheme.h"
+#include "randgen.h"
+#include "doveadm.h"
+#include "askpass.h"
+#include "module-dir.h"
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define DEFAULT_SCHEME "CRYPT"
+
+static struct module *modules = NULL;
+
+static void cmd_pw(struct doveadm_cmd_context *cctx)
+{
+ const char *hash = NULL;
+ const char *scheme = NULL;
+ const char *plaintext = NULL;
+ const char *test_hash = NULL;
+ bool list_schemes = FALSE, reverse_verify = FALSE;
+ int64_t rounds_int64;
+ struct module_dir_load_settings mod_set;
+ struct password_generate_params gen_params;
+ i_zero(&gen_params);
+
+ password_schemes_init();
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.ignore_dlopen_errors = TRUE;
+ mod_set.debug = doveadm_debug;
+
+ modules = module_dir_load_missing(modules, AUTH_MODULE_DIR, NULL, &mod_set);
+ module_dir_init(modules);
+
+ (void)doveadm_cmd_param_bool(cctx, "list", &list_schemes);
+ (void)doveadm_cmd_param_str(cctx, "plaintext", &plaintext);
+ if (doveadm_cmd_param_int64(cctx, "rounds", &rounds_int64)) {
+ if (rounds_int64 > UINT_MAX)
+ i_fatal("Invalid number of rounds: %"PRId64, rounds_int64);
+ gen_params.rounds = rounds_int64;
+ }
+ (void)doveadm_cmd_param_str(cctx, "scheme", &scheme);
+ if (doveadm_cmd_param_str(cctx, "test-hash", &test_hash))
+ reverse_verify = TRUE;
+ (void)doveadm_cmd_param_str(cctx, "user", &gen_params.user);
+ (void)doveadm_cmd_param_bool(cctx, "reverse-verify", &reverse_verify);
+
+ if (list_schemes) {
+ ARRAY_TYPE(password_scheme_p) arr;
+ const struct password_scheme *const *schemes;
+ unsigned int i, count;
+ t_array_init(&arr, 30);
+ password_schemes_get(&arr);
+ schemes = array_get(&arr, &count);
+ for (i = 0; i < count; i++)
+ printf("%s ", schemes[i]->name);
+ printf("\n");
+ module_dir_unload(&modules);
+ password_schemes_deinit();
+ return;
+ }
+
+ scheme = scheme == NULL ? DEFAULT_SCHEME : t_str_ucase(scheme);
+
+ if (test_hash != NULL && plaintext == NULL)
+ plaintext = t_askpass("Enter password to verify: ");
+ while (plaintext == NULL) {
+ const char *check;
+ static int lives = 3;
+
+ plaintext = t_askpass("Enter new password: ");
+ check = t_askpass("Retype new password: ");
+ if (strcmp(plaintext, check) != 0) {
+ i_error("Passwords don't match!");
+ if (--lives == 0)
+ lib_exit(1);
+ plaintext = NULL;
+ }
+ }
+
+ if (!password_generate_encoded(plaintext, &gen_params, scheme, &hash))
+ i_fatal("Unknown scheme: %s", scheme);
+ if (reverse_verify) {
+ const unsigned char *raw_password;
+ size_t size;
+ const char *error;
+
+ if (test_hash != NULL) {
+ scheme = password_get_scheme(&test_hash);
+ if (scheme == NULL)
+ i_fatal("Missing {scheme} prefix from hash");
+ hash = test_hash;
+ }
+
+ if (password_decode(hash, scheme, &raw_password, &size,
+ &error) <= 0)
+ i_fatal("reverse decode check failed: %s", error);
+
+ if (password_verify(plaintext, &gen_params, scheme,
+ raw_password, size, &error) <= 0) {
+ i_fatal("reverse password verification check failed: %s",
+ error);
+ }
+
+ printf("{%s}%s (verified)\n", scheme, hash);
+ } else {
+ printf("{%s}%s\n", scheme, hash);
+ }
+
+ module_dir_unload(&modules);
+ password_schemes_deinit();
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_pw = {
+ .name = "pw",
+ .cmd = cmd_pw,
+ .usage = "[-l] [-p plaintext] [-r rounds] [-s scheme] [-t hash] [-u user] [-V]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('l', "list", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('p', "plaintext", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('r', "rounds", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('s', "scheme", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('t', "test-hash", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('u', "user", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('V', "reverse-verify", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-replicator.c b/src/doveadm/doveadm-replicator.c
new file mode 100644
index 0000000..57cb972
--- /dev/null
+++ b/src/doveadm/doveadm-replicator.c
@@ -0,0 +1,381 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "strescape.h"
+#include "istream.h"
+#include "write-full.h"
+#include "master-service.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+struct replicator_context {
+ const char *socket_path;
+ const char *priority;
+ const char *user_mask, *username;
+ struct istream *input;
+ bool full_sync;
+};
+
+extern struct doveadm_cmd_ver2 doveadm_cmd_replicator[];
+
+static void replicator_cmd_help(const struct doveadm_cmd_ver2 *cmd) ATTR_NORETURN;
+
+static void
+replicator_send(struct replicator_context *ctx, const char *data)
+{
+ if (write_full(i_stream_get_fd(ctx->input), data, strlen(data)) < 0)
+ i_fatal("write(%s) failed: %m", ctx->socket_path);
+}
+
+static void replicator_connect(struct replicator_context *ctx)
+{
+#define REPLICATOR_HANDSHAKE "VERSION\treplicator-doveadm-client\t1\t0\n"
+ const char *line;
+ int fd;
+
+ fd = doveadm_connect(ctx->socket_path);
+ net_set_nonblock(fd, FALSE);
+
+ ctx->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ replicator_send(ctx, REPLICATOR_HANDSHAKE);
+
+ alarm(5);
+ line = i_stream_read_next_line(ctx->input);
+ alarm(0);
+ if (line == NULL) {
+ if (ctx->input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->socket_path,
+ i_stream_get_error(ctx->input));
+ } else if (ctx->input->eof)
+ i_fatal("%s disconnected", ctx->socket_path);
+ else
+ i_fatal("read(%s) timed out", ctx->socket_path);
+ }
+ if (!version_string_verify(line, "replicator-doveadm-server", 1)) {
+ i_fatal_status(EX_PROTOCOL,
+ "%s not a compatible replicator-doveadm socket",
+ ctx->socket_path);
+ }
+}
+
+static void replicator_disconnect(struct replicator_context *ctx)
+{
+ if (ctx->input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->socket_path,
+ i_stream_get_error(ctx->input));
+ }
+ i_stream_destroy(&ctx->input);
+}
+
+static struct replicator_context *
+cmd_replicator_init(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+
+ ctx = t_new(struct replicator_context, 1);
+ ctx->socket_path = t_strconcat(doveadm_settings->base_dir,
+ "/replicator-doveadm", NULL);
+
+ (void)doveadm_cmd_param_str(cctx, "socket-path", &ctx->socket_path);
+ (void)doveadm_cmd_param_bool(cctx, "full-sync", &ctx->full_sync);
+ (void)doveadm_cmd_param_str(cctx, "priority", &ctx->priority);
+ (void)doveadm_cmd_param_str(cctx, "user-mask", &ctx->user_mask);
+ (void)doveadm_cmd_param_str(cctx, "user", &ctx->username);
+
+ replicator_connect(ctx);
+ return ctx;
+}
+
+static const char *time_formatted_hms(unsigned int secs)
+{
+ return t_strdup_printf("%02d:%02d:%02d", secs/3600,
+ (secs/60)%60, secs%60);
+}
+
+static const char *time_ago(time_t t)
+{
+ int diff = ioloop_time - t;
+
+ if (t == 0)
+ return "-";
+ return time_formatted_hms(diff);
+}
+
+static void cmd_replicator_status_overview(struct replicator_context *ctx)
+{
+ char *line, *value;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("field", "field",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ doveadm_print_header("value", "value",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ replicator_send(ctx, "STATUS\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ value = strchr(line, '\t');
+ if (value != NULL)
+ *value++ = '\0';
+ else
+ value = "";
+ doveadm_print(line);
+ doveadm_print(value);
+ }
+ replicator_disconnect(ctx);
+}
+
+static void cmd_replicator_status(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+ const char *line, *const *args;
+ time_t last_fast, last_full, last_success;
+ unsigned int next_secs;
+
+ ctx = cmd_replicator_init(cctx);
+ if (ctx->user_mask == NULL) {
+ cmd_replicator_status_overview(ctx);
+ return;
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("username", "username",
+ DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header_simple("priority");
+ doveadm_print_header_simple("fast sync");
+ doveadm_print_header_simple("full sync");
+ doveadm_print_header_simple("success sync");
+ doveadm_print_header_simple("failed");
+ doveadm_print_header_simple("next sync secs");
+
+ replicator_send(ctx, t_strdup_printf("STATUS\t%s\n",
+ str_tabescape(ctx->user_mask)));
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) >= 6 &&
+ str_to_time(args[2], &last_fast) == 0 &&
+ str_to_time(args[3], &last_full) == 0 &&
+ str_to_time(args[5], &last_success) == 0 &&
+ str_to_uint(args[6], &next_secs) == 0) {
+ doveadm_print(args[0]);
+ doveadm_print(args[1]);
+ doveadm_print(time_ago(last_fast));
+ doveadm_print(time_ago(last_full));
+ doveadm_print(time_ago(last_success));
+ doveadm_print(args[4][0] == '0' ? "-" : "y");
+ doveadm_print(time_formatted_hms(next_secs));
+ }
+ } T_END;
+ }
+ if (line == NULL) {
+ i_error("Replicator disconnected unexpectedly");
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ replicator_disconnect(ctx);
+}
+
+static void cmd_replicator_dsync_status(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+ const char *line;
+ unsigned int i;
+
+ ctx = cmd_replicator_init(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("username", "username",
+ DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header_simple("type");
+ doveadm_print_header_simple("status");
+
+ replicator_send(ctx, "STATUS-DSYNC\n");
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ const char *const *args = t_strsplit_tabescaped(line);
+
+ for (i = 0; i < 3; i++) {
+ if (args[i] == NULL)
+ break;
+ doveadm_print(args[i]);
+ }
+ for (; i < 3; i++)
+ doveadm_print("");
+ } T_END;
+ }
+ replicator_disconnect(ctx);
+}
+
+static void cmd_replicator_replicate(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+ string_t *str;
+ const char *line;
+
+ ctx = cmd_replicator_init(cctx);
+ if (ctx->user_mask == NULL)
+ replicator_cmd_help(cctx->cmd);
+
+ str = t_str_new(128);
+ str_append(str, "REPLICATE\t");
+ if (ctx->priority == NULL)
+ str_append_tabescaped(str, "low");
+ else
+ str_append_tabescaped(str, ctx->priority);
+ str_append_c(str, '\t');
+ if (ctx->full_sync)
+ str_append_c(str, 'f');
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, ctx->user_mask);
+ str_append_c(str, '\n');
+ replicator_send(ctx, str_c(str));
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ doveadm_print_header("result", "result",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ i_error("Replicator disconnected unexpectedly");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (line[0] != '+') {
+ i_error("Replicator failed: %s", line+1);
+ doveadm_exit_code = EX_USAGE;
+ } else {
+ doveadm_print(t_strdup_printf("%s users updated", line+1));
+ }
+ replicator_disconnect(ctx);
+}
+
+static void cmd_replicator_add(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+ string_t *str;
+ const char *line;
+
+ ctx = cmd_replicator_init(cctx);
+ if (ctx->user_mask == NULL)
+ replicator_cmd_help(cctx->cmd);
+
+ str = t_str_new(128);
+ str_append(str, "ADD\t");
+ str_append_tabescaped(str, ctx->user_mask);
+ str_append_c(str, '\n');
+ replicator_send(ctx, str_c(str));
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ i_error("Replicator disconnected unexpectedly");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (line[0] != '+') {
+ i_error("Replicator failed: %s", line+1);
+ doveadm_exit_code = EX_USAGE;
+ }
+ replicator_disconnect(ctx);
+}
+
+static void cmd_replicator_remove(struct doveadm_cmd_context *cctx)
+{
+ struct replicator_context *ctx;
+ string_t *str;
+ const char *line;
+
+ ctx = cmd_replicator_init(cctx);
+ if (ctx->username == NULL)
+ replicator_cmd_help(cctx->cmd);
+
+ str = t_str_new(128);
+ str_append(str, "REMOVE\t");
+ str_append_tabescaped(str, ctx->username);
+ str_append_c(str, '\n');
+ replicator_send(ctx, str_c(str));
+
+ line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ i_error("Replicator disconnected unexpectedly");
+ doveadm_exit_code = EX_TEMPFAIL;
+ } else if (line[0] != '+') {
+ i_error("Replicator failed: %s", line+1);
+ doveadm_exit_code = EX_USAGE;
+ }
+ replicator_disconnect(ctx);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_replicator[] = {
+{
+ .name = "replicator status",
+ .cmd = cmd_replicator_status,
+ .usage = "[-a <replicator socket path>] [<user mask>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user-mask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "replicator dsync-status",
+ .cmd = cmd_replicator_dsync_status,
+ .usage = "[-a <replicator socket path>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "replicator replicate",
+ .cmd = cmd_replicator_replicate,
+ .usage = "[-a <replicator socket path>] [-f] [-p <priority>] <user mask>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('f', "full-sync", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('p', "priority", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user-mask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "replicator add",
+ .cmd = cmd_replicator_add,
+ .usage = "[-a <replicator socket path>] <user mask>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user-mask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "replicator remove",
+ .cmd = cmd_replicator_remove,
+ .usage = "[-a <replicator socket path>] <username>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "user", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+};
+
+static void replicator_cmd_help(const struct doveadm_cmd_ver2 *cmd)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_replicator); i++) {
+ if (doveadm_cmd_replicator[i].cmd == cmd->cmd)
+ help_ver2(&doveadm_cmd_replicator[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_replicator_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_replicator); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_replicator[i]);
+}
diff --git a/src/doveadm/doveadm-server.h b/src/doveadm/doveadm-server.h
new file mode 100644
index 0000000..e111b4a
--- /dev/null
+++ b/src/doveadm/doveadm-server.h
@@ -0,0 +1,34 @@
+#ifndef DOVEADM_SERVER_H
+#define DOVEADM_SERVER_H
+
+extern struct client_connection *doveadm_client;
+extern struct doveadm_print_vfuncs doveadm_print_server_vfuncs;
+
+enum doveadm_proxy_ssl_flags {
+ /* Use SSL/TLS enabled */
+ PROXY_SSL_FLAG_YES = 0x01,
+ /* Don't do SSL handshake immediately after connected */
+ PROXY_SSL_FLAG_STARTTLS = 0x02,
+ /* Don't require that the received certificate is valid */
+ PROXY_SSL_FLAG_ANY_CERT = 0x04
+};
+
+struct doveadm_server {
+ /* hostname:port or socket name for logging */
+ const char *name;
+ /* hostname without port */
+ const char *hostname;
+ /* host ip to use */
+ struct ip_addr ip;
+ /* port to use */
+ in_port_t port;
+
+ /* ssl related settings */
+ enum doveadm_proxy_ssl_flags ssl_flags;
+ struct ssl_iostream_context *ssl_ctx;
+
+ ARRAY(struct server_connection *) connections;
+ ARRAY_TYPE(string) queue;
+};
+
+#endif
diff --git a/src/doveadm/doveadm-settings.c b/src/doveadm/doveadm-settings.c
new file mode 100644
index 0000000..b6b31cf
--- /dev/null
+++ b/src/doveadm/doveadm-settings.c
@@ -0,0 +1,320 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "iostream-ssl.h"
+#include "doveadm-settings.h"
+
+ARRAY_TYPE(doveadm_setting_root) doveadm_setting_roots;
+bool doveadm_verbose_proctitle;
+
+static pool_t doveadm_settings_pool = NULL;
+
+static bool doveadm_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings doveadm_unix_listeners_array[] = {
+ { "doveadm-server", 0600, "", "" }
+};
+static struct file_listener_settings *doveadm_unix_listeners[] = {
+ &doveadm_unix_listeners_array[0]
+};
+static buffer_t doveadm_unix_listeners_buf = {
+ { { doveadm_unix_listeners, sizeof(doveadm_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings doveadm_service_settings = {
+ .name = "doveadm",
+ .protocol = "",
+ .type = "",
+ .executable = "doveadm-server",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &doveadm_unix_listeners_buf,
+ sizeof(doveadm_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct doveadm_settings)
+
+static const struct setting_define doveadm_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, libexec_dir),
+ DEF(STR, mail_plugins),
+ DEF(STR, mail_plugin_dir),
+ DEF(STR_VARS, mail_temp_dir),
+ DEF(BOOL, auth_debug),
+ DEF(STR, auth_socket_path),
+ DEF(STR, doveadm_socket_path),
+ DEF(UINT, doveadm_worker_count),
+ DEF(IN_PORT, doveadm_port),
+ { .type = SET_ALIAS, .key = "doveadm_proxy_port" },
+ DEF(ENUM, doveadm_ssl),
+ DEF(STR, doveadm_username),
+ DEF(STR, doveadm_password),
+ DEF(STR, doveadm_allowed_commands),
+ DEF(STR, dsync_alt_char),
+ DEF(STR, dsync_remote_cmd),
+ DEF(STR, director_username_hash),
+ DEF(STR, doveadm_api_key),
+ DEF(STR, dsync_features),
+ DEF(UINT, dsync_commit_msgs_interval),
+ DEF(STR, doveadm_http_rawlog_dir),
+ DEF(STR, dsync_hashed_headers),
+
+ { .type = SET_STRLIST, .key = "plugin",
+ .offset = offsetof(struct doveadm_settings, plugin_envs) },
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct doveadm_settings doveadm_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .libexec_dir = PKG_LIBEXECDIR,
+ .mail_plugins = "",
+ .mail_plugin_dir = MODULEDIR,
+ .mail_temp_dir = "/tmp",
+ .auth_debug = FALSE,
+ .auth_socket_path = "auth-userdb",
+ .doveadm_socket_path = "doveadm-server",
+ .doveadm_worker_count = 0,
+ .doveadm_port = 0,
+ .doveadm_ssl = "no:ssl:starttls",
+ .doveadm_username = "doveadm",
+ .doveadm_password = "",
+ .doveadm_allowed_commands = "",
+ .dsync_alt_char = "_",
+ .dsync_remote_cmd = "ssh -l%{login} %{host} doveadm dsync-server -u%u -U",
+ .dsync_features = "",
+ .dsync_hashed_headers = "Date Message-ID",
+ .dsync_commit_msgs_interval = 100,
+ .director_username_hash = "%Lu",
+ .doveadm_api_key = "",
+ .doveadm_http_rawlog_dir = "",
+
+ .plugin_envs = ARRAY_INIT
+};
+
+static const struct setting_parser_info *doveadm_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info doveadm_setting_parser_info = {
+ .module_name = "doveadm",
+ .defines = doveadm_setting_defines,
+ .defaults = &doveadm_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct doveadm_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = doveadm_settings_check,
+ .dependencies = doveadm_setting_dependencies
+};
+
+struct doveadm_settings *doveadm_settings;
+const struct master_service_settings *service_set;
+
+static void
+fix_base_path(struct doveadm_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> */
+struct dsync_feature_list {
+ const char *name;
+ enum dsync_features num;
+};
+
+static const struct dsync_feature_list dsync_feature_list[] = {
+ { "empty-header-workaround", DSYNC_FEATURE_EMPTY_HDR_WORKAROUND },
+ { "no-header-hashes", DSYNC_FEATURE_NO_HEADER_HASHES },
+ { NULL, 0 }
+};
+
+static int
+dsync_settings_parse_features(struct doveadm_settings *set,
+ const char **error_r)
+{
+ enum dsync_features features = 0;
+ const struct dsync_feature_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->dsync_features, " ,");
+ for (; *str != NULL; str++) {
+ list = dsync_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("dsync_features: "
+ "Unknown feature: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_features = features;
+ return 0;
+}
+
+static bool doveadm_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct doveadm_settings *set = _set;
+
+#ifndef CONFIG_BINARY
+ fix_base_path(set, pool, &set->auth_socket_path);
+ fix_base_path(set, pool, &set->doveadm_socket_path);
+#endif
+ if (*set->dsync_hashed_headers == '\0') {
+ *error_r = "dsync_hashed_headers must not be empty";
+ return FALSE;
+ }
+ if (*set->dsync_alt_char == '\0') {
+ *error_r = "dsync_alt_char must not be empty";
+ return FALSE;
+ }
+ if (dsync_settings_parse_features(set, error_r) != 0)
+ return FALSE;
+ return TRUE;
+}
+/* </settings checks> */
+
+const struct master_service_ssl_settings *doveadm_ssl_set = NULL;
+
+void doveadm_get_ssl_settings(struct ssl_iostream_settings *set_r, pool_t pool)
+{
+ master_service_ssl_client_settings_to_iostream_set(doveadm_ssl_set,
+ pool, set_r);
+}
+
+void doveadm_settings_expand(struct doveadm_settings *set, pool_t pool)
+{
+ struct var_expand_table tab[] = { { '\0', NULL, NULL } };
+ const char *error;
+
+ if (settings_var_expand(&doveadm_setting_parser_info, set,
+ pool, tab, &error) <= 0)
+ i_fatal("Failed to expand settings: %s", error);
+}
+
+void doveadm_read_settings(void)
+{
+ static const struct setting_parser_info *default_set_roots[] = {
+ &master_service_ssl_setting_parser_info,
+ &doveadm_setting_parser_info,
+ };
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const struct doveadm_settings *set;
+ struct doveadm_setting_root *root;
+ ARRAY(const struct setting_parser_info *) set_roots;
+ ARRAY_TYPE(const_string) module_names;
+ void **sets;
+ const char *error;
+
+ t_array_init(&set_roots, N_ELEMENTS(default_set_roots) +
+ array_count(&doveadm_setting_roots) + 1);
+ array_append(&set_roots, default_set_roots,
+ N_ELEMENTS(default_set_roots));
+ t_array_init(&module_names, 4);
+ array_foreach_modifiable(&doveadm_setting_roots, root) {
+ array_push_back(&module_names, &root->info->module_name);
+ array_push_back(&set_roots, &root->info);
+ }
+ array_append_zero(&module_names);
+ array_append_zero(&set_roots);
+
+ i_zero(&input);
+ input.roots = array_front(&set_roots);
+ input.module = "doveadm";
+ input.extra_modules = array_front(&module_names);
+ input.service = "doveadm";
+ input.preserve_user = TRUE;
+ input.preserve_home = TRUE;
+ if (master_service_settings_read(master_service, &input,
+ &output, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ doveadm_settings_pool = pool_alloconly_create("doveadm settings", 1024);
+ service_set = master_service_settings_get(master_service);
+ service_set = settings_dup(&master_service_setting_parser_info,
+ service_set, doveadm_settings_pool);
+ doveadm_verbose_proctitle = service_set->verbose_proctitle;
+
+ sets = master_service_settings_get_others(master_service);
+ set = sets[1];
+ doveadm_settings = settings_dup(&doveadm_setting_parser_info, set,
+ doveadm_settings_pool);
+ doveadm_ssl_set = settings_dup(&master_service_ssl_setting_parser_info,
+ master_service_ssl_settings_get(master_service),
+ doveadm_settings_pool);
+ doveadm_settings_expand(doveadm_settings, doveadm_settings_pool);
+ doveadm_settings->parsed_features = set->parsed_features; /* copy this value by hand */
+
+ array_foreach_modifiable(&doveadm_setting_roots, root) {
+ unsigned int idx =
+ array_foreach_idx(&doveadm_setting_roots, root);
+ root->settings = settings_dup(root->info, sets[2+idx],
+ doveadm_settings_pool);
+ }
+}
+
+void doveadm_setting_roots_add(const struct setting_parser_info *info)
+{
+ struct doveadm_setting_root *root;
+
+ root = array_append_space(&doveadm_setting_roots);
+ root->info = info;
+}
+
+void *doveadm_setting_roots_get_settings(const struct setting_parser_info *info)
+{
+ const struct doveadm_setting_root *root;
+
+ array_foreach(&doveadm_setting_roots, root) {
+ if (root->info == info)
+ return root->settings;
+ }
+ i_panic("Failed to find settings for module %s", info->module_name);
+}
+
+void doveadm_settings_init(void)
+{
+ i_array_init(&doveadm_setting_roots, 8);
+}
+
+void doveadm_settings_deinit(void)
+{
+ array_free(&doveadm_setting_roots);
+ pool_unref(&doveadm_settings_pool);
+}
diff --git a/src/doveadm/doveadm-settings.h b/src/doveadm/doveadm-settings.h
new file mode 100644
index 0000000..c718b86
--- /dev/null
+++ b/src/doveadm/doveadm-settings.h
@@ -0,0 +1,66 @@
+#ifndef DOVEADM_SETTINGS_H
+#define DOVEADM_SETTINGS_H
+
+#include "net.h"
+
+struct ssl_iostream_settings;
+
+/* <settings checks> */
+enum dsync_features {
+ DSYNC_FEATURE_EMPTY_HDR_WORKAROUND = 0x1,
+ DSYNC_FEATURE_NO_HEADER_HASHES = 0x2,
+};
+/* </settings checks> */
+
+struct doveadm_settings {
+ const char *base_dir;
+ const char *libexec_dir;
+ const char *mail_plugins;
+ const char *mail_plugin_dir;
+ const char *mail_temp_dir;
+ bool auth_debug;
+ const char *auth_socket_path;
+ const char *doveadm_socket_path;
+ unsigned int doveadm_worker_count;
+ in_port_t doveadm_port;
+ const char *doveadm_ssl;
+ const char *doveadm_username;
+ const char *doveadm_password;
+ const char *doveadm_allowed_commands;
+ const char *dsync_alt_char;
+ const char *dsync_remote_cmd;
+ const char *director_username_hash;
+ const char *doveadm_api_key;
+ const char *dsync_features;
+ const char *dsync_hashed_headers;
+ unsigned int dsync_commit_msgs_interval;
+ const char *doveadm_http_rawlog_dir;
+ enum dsync_features parsed_features;
+ ARRAY(const char *) plugin_envs;
+};
+
+struct doveadm_setting_root {
+ const struct setting_parser_info *info;
+ void *settings;
+};
+ARRAY_DEFINE_TYPE(doveadm_setting_root, struct doveadm_setting_root);
+
+extern const struct setting_parser_info doveadm_setting_parser_info;
+extern struct doveadm_settings *doveadm_settings;
+extern const struct master_service_settings *service_set;
+extern const struct master_service_ssl_settings *doveadm_ssl_set;
+extern ARRAY_TYPE(doveadm_setting_root) doveadm_setting_roots;
+extern bool doveadm_verbose_proctitle;
+
+void doveadm_get_ssl_settings(struct ssl_iostream_settings *set_r, pool_t pool);
+void doveadm_settings_expand(struct doveadm_settings *set, pool_t pool);
+
+void doveadm_setting_roots_add(const struct setting_parser_info *info);
+void *doveadm_setting_roots_get_settings(const struct setting_parser_info *info);
+
+void doveadm_read_settings(void);
+
+void doveadm_settings_init(void);
+void doveadm_settings_deinit(void);
+
+#endif
diff --git a/src/doveadm/doveadm-sis.c b/src/doveadm/doveadm-sis.c
new file mode 100644
index 0000000..85e82e9
--- /dev/null
+++ b/src/doveadm/doveadm-sis.c
@@ -0,0 +1,330 @@
+/* Copyright (c) 2009-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 "read-full.h"
+#include "fs-sis-common.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+/* Files are in <rootdir>/ha/sh/<hash>-<guid>
+ They may be hard linked to hashes/<hash>
+*/
+
+static const char *sis_get_dir(const char *rootdir, const char *hash)
+{
+ if (strlen(hash) < 4 || strchr(hash, '/') != NULL)
+ i_fatal("Invalid hash in filename: %s", hash);
+ return t_strdup_printf("%s/%c%c/%c%c", rootdir,
+ hash[0], hash[1], hash[2], hash[3]);
+}
+
+static int
+file_contents_equal(const char *path1, const char *path2, ino_t *path2_inode_r)
+{
+ struct stat st1, st2;
+ int fd1, fd2, ret = -1;
+
+ *path2_inode_r = 0;
+
+ /* do a byte-by-byte comparison for the files to find out if they're
+ the same or if this is a hash collision */
+ fd1 = open(path1, O_RDONLY);
+ if (fd1 == -1) {
+ if (errno != ENOENT)
+ i_error("open(%s) failed: %m", path1);
+ return -1;
+ }
+ fd2 = open(path2, O_RDONLY);
+ if (fd2 == -1) {
+ if (errno != ENOENT)
+ i_error("open(%s) failed: %m", path2);
+ i_close_fd(&fd1);
+ return -1;
+ }
+
+ if (fstat(fd1, &st1) < 0)
+ i_error("fstat(%s) failed: %m", path1);
+ else if (fstat(fd2, &st2) < 0)
+ i_error("fstat(%s) failed: %m", path1);
+ else if (st1.st_size != st2.st_size)
+ ret = 0;
+ else {
+ /* @UNSAFE: sizes match. compare. */
+ unsigned char buf1[IO_BLOCK_SIZE], buf2[IO_BLOCK_SIZE];
+ ssize_t ret1;
+ int ret2;
+
+ while ((ret1 = read(fd1, buf1, sizeof(buf1))) > 0) {
+ i_assert((size_t)ret1 <= sizeof(buf2));
+ if ((ret2 = read_full(fd2, buf2, ret1)) <= 0) {
+ if (ret2 < 0)
+ i_error("read(%s) failed: %m", path2);
+ else
+ ret = 0;
+ break;
+ }
+ if (memcmp(buf1, buf2, ret1) != 0) {
+ ret = 0;
+ break;
+ }
+ }
+ if (ret1 < 0)
+ i_error("read(%s) failed: %m", path1);
+ else if (ret1 == 0)
+ ret = 1;
+ *path2_inode_r = st2.st_ino;
+ }
+
+ if (close(fd1) < 0)
+ i_error("close(%s) failed: %m", path1);
+ if (close(fd2) < 0)
+ i_error("close(%s) failed: %m", path2);
+
+ return ret;
+}
+
+static int
+hardlink_replace(const char *src, const char *dest, ino_t src_inode)
+{
+ const char *p, *destdir, *tmppath;
+ unsigned char randbuf[8];
+ struct stat st;
+
+ p = strrchr(dest, '/');
+ i_assert(p != NULL);
+ destdir = t_strdup_until(dest, p);
+
+ random_fill(randbuf, sizeof(randbuf));
+ tmppath = t_strdup_printf("%s/temp.%s.%s.%s",
+ destdir, my_hostname, my_pid,
+ binary_to_hex(randbuf, sizeof(randbuf)));
+ if (link(src, tmppath) < 0) {
+ if (errno == EMLINK)
+ return 0;
+ i_error("link(%s, %s) failed: %m", src, tmppath);
+ return -1;
+ }
+ if (stat(tmppath, &st) < 0) {
+ i_error("stat(%s) failed: %m", tmppath);
+ return -1;
+ }
+ if (st.st_ino != src_inode) {
+ i_unlink(tmppath);
+ return 0;
+ }
+ if (rename(tmppath, dest) < 0) {
+ i_error("rename(%s, %s) failed: %m", src, tmppath);
+ i_unlink(tmppath);
+ return -1;
+ }
+ return 1;
+}
+
+static int sis_try_deduplicate(const char *rootdir, const char *fname)
+{
+ const char *p, *hash, *hashdir, *path, *hashes_dir, *hashes_path;
+ struct stat st;
+ ino_t inode;
+ int ret;
+
+ /* fname should be in <hash>-<guid> format */
+ p = strchr(fname, '-');
+ i_assert(p != NULL);
+
+ hash = t_strdup_until(fname, p);
+ hashdir = sis_get_dir(rootdir, hash);
+ path = t_strdup_printf("%s/%s", hashdir, fname);
+
+ hashes_dir = t_strconcat(hashdir, "/", HASH_DIR_NAME, NULL);
+ hashes_path = t_strconcat(hashes_dir, "/", hash, NULL);
+ if (link(path, hashes_path) == 0) {
+ /* first file with this hash. we're done */
+ return 0;
+ }
+ if (errno == ENOENT) {
+ /* either path was already deleted or hashes dir
+ doesn't exist */
+ if (mkdir(hashes_dir, 0700) < 0) {
+ if (errno == EEXIST)
+ return 0;
+ i_error("mkdir(%s) failed: %m", hashes_dir);
+ return -1;
+ }
+ /* try again */
+ if (link(path, hashes_path) == 0 || errno == ENOENT)
+ return 0;
+ }
+ if (errno != EEXIST) {
+ i_error("link(%s, %s) failed: %m", path, hashes_path);
+ return -1;
+ }
+
+ /* need to do a byte-by-byte comparison. but check first if someone
+ else already had deduplicated the file. */
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* just got deleted */
+ return 0;
+ }
+ i_error("stat(%s) failed: %m", path);
+ return -1;
+ }
+ if (st.st_nlink > 1) {
+ /* already deduplicated */
+ return 0;
+ }
+
+ ret = file_contents_equal(path, hashes_path, &inode);
+ if (ret < 0) {
+ if (errno == ENOENT) {
+ /* either path or hashes_path was deleted. */
+ return sis_try_deduplicate(rootdir, fname);
+ }
+ return -1;
+ }
+ if (ret > 0) {
+ /* equal, replace with hard link */
+ ret = hardlink_replace(hashes_path, path, inode);
+ if (ret > 0)
+ return 0;
+ else if (ret < 0)
+ return -1;
+ /* too many hard links or inode changed */
+ }
+
+ /* replace hashes link with this */
+ return hardlink_replace(path, hashes_path, st.st_ino) < 0 ? -1 : 0;
+}
+
+static void cmd_sis_deduplicate(struct doveadm_cmd_context *cctx)
+{
+ const char *rootdir, *queuedir;
+ DIR *dir;
+ struct dirent *d;
+ struct stat st, first_st;
+ string_t *path;
+ size_t dir_len;
+ int ret;
+
+ if (!doveadm_cmd_param_str(cctx, "root-dir", &rootdir) ||
+ !doveadm_cmd_param_str(cctx, "queue-dir", &queuedir))
+ help_ver2(&doveadm_cmd_sis_deduplicate);
+
+ /* go through the filenames in the queue dir and see if
+ we can deduplicate them. */
+ if (stat(rootdir, &st) < 0)
+ i_fatal("stat(%s) failed: %m", rootdir);
+
+ path = t_str_new(256);
+ str_append(path, queuedir);
+ str_append_c(path, '/');
+ dir_len = str_len(path);
+
+ dir = opendir(queuedir);
+ if (dir == NULL)
+ i_fatal("opendir(%s) failed: %m", queuedir);
+
+ first_st.st_size = -1;
+ while ((d = readdir(dir)) != NULL) {
+ if (d->d_name[0] == '.')
+ continue;
+
+ str_truncate(path, dir_len);
+ str_append(path, d->d_name);
+
+ if (first_st.st_size < 0) {
+ if (stat(str_c(path), &first_st) < 0)
+ i_fatal("stat(%s) failed: %m", str_c(path));
+ }
+ if (strchr(d->d_name, '-') == NULL || first_st.st_size != 0) {
+ i_fatal("%s is not a valid sis-queue file, "
+ "is the queue directory correct?",
+ str_c(path));
+ }
+
+ T_BEGIN {
+ ret = sis_try_deduplicate(rootdir, d->d_name);
+ } T_END;
+ if (ret == 0)
+ i_unlink(str_c(path));
+ }
+ if (closedir(dir) < 0)
+ i_error("closedir(%s) failed: %m", queuedir);
+}
+
+static void cmd_sis_find(struct doveadm_cmd_context *cctx)
+{
+ const char *rootdir, *path, *hash;
+ DIR *dir;
+ struct dirent *d;
+ struct stat st;
+ string_t *str;
+ size_t dir_len, hash_len;
+
+ if (!doveadm_cmd_param_str(cctx, "root-dir", &rootdir) ||
+ !doveadm_cmd_param_str(cctx, "hash", &hash) ||
+ strlen(hash) < 4)
+ help_ver2(&doveadm_cmd_sis_find);
+
+ if (stat(rootdir, &st) < 0) {
+ if (errno == ENOENT)
+ i_fatal("Attachment dir doesn't exist: %s", rootdir);
+ i_fatal("stat(%s) failed: %m", rootdir);
+ }
+ hash_len = strlen(hash);
+
+ path = sis_get_dir(rootdir, hash);
+ str = t_str_new(256);
+ str_append(str, path);
+ str_append_c(str, '/');
+ dir_len = str_len(str);
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ if (errno == ENOENT)
+ return;
+ i_fatal("opendir(%s) failed: %m", path);
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ doveadm_print_header("path", "path",
+ DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+ while ((d = readdir(dir)) != NULL) {
+ if (strncmp(d->d_name, hash, hash_len) == 0) {
+ str_truncate(str, dir_len);
+ str_append(str, d->d_name);
+ doveadm_print(str_c(str));
+ }
+ }
+ if (closedir(dir) < 0)
+ i_error("closedir(%s) failed: %m", path);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_sis_deduplicate = {
+ .name = "sis deduplicate",
+ .cmd = cmd_sis_deduplicate,
+ .usage = "<root dir> <queue dir>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "root-dir", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "queue-dir", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+struct doveadm_cmd_ver2 doveadm_cmd_sis_find = {
+ .name = "sis find",
+ .cmd = cmd_sis_find,
+ .usage = "<root dir> <hash>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "root-dir", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "hash", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-stats.c b/src/doveadm/doveadm-stats.c
new file mode 100644
index 0000000..ddb198b
--- /dev/null
+++ b/src/doveadm/doveadm-stats.c
@@ -0,0 +1,343 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "istream.h"
+#include "str.h"
+#include "strescape.h"
+#include "write-full.h"
+#include "master-service.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+#include "stats-settings.h"
+
+#include <math.h>
+
+#define DOVEADM_DUMP_DEFAULT_FIELDS \
+ "count sum min max avg median stddev %95"
+
+#define ADD_NAME_PARAM "name"
+#define ADD_DESCR_PARAM "description"
+#define ADD_FIELDS_PARAM "fields"
+#define ADD_GROUPBY_PARAM "group-by"
+#define ADD_FILTER_PARAM "filter"
+#define ADD_EXPORTER_PARAM "exporter"
+#define ADD_EXPORTERINCL_PARAM "exporter-include"
+
+enum doveadm_dump_field_type {
+ DOVEADM_DUMP_FIELD_TYPE_PASSTHROUGH = 0,
+ DOVEADM_DUMP_FIELD_TYPE_STDDEV,
+};
+
+struct stats_cmd_context {
+ string_t *cmd;
+ struct doveadm_cmd_context *cctx;
+ struct istream *input;
+ const char *path;
+ void *data;
+};
+
+struct dump_data {
+ const char **fields;
+ unsigned int field_count;
+ enum doveadm_dump_field_type *field_types;
+};
+
+struct stats_cmd_vfuncs {
+ int (*build_cmd)(struct stats_cmd_context *ctx, const char **error_r);
+ void (*process_response)(struct stats_cmd_context *ctx);
+};
+
+static int build_stats_dump_cmd(struct stats_cmd_context *ctx, const char **error_r);
+static int build_stats_add_cmd(struct stats_cmd_context *ctx, const char **error_r);
+static int build_stats_remove_cmd(struct stats_cmd_context *ctx, const char **error_r);
+
+static void stats_dump_process_response(struct stats_cmd_context *ctx);
+static void stats_modify_process_response(struct stats_cmd_context *ctx);
+
+static void stats_send_cmd(struct stats_cmd_context *ctx);
+
+static struct stats_cmd_vfuncs dump_vfuncs = {
+ .build_cmd = build_stats_dump_cmd,
+ .process_response = stats_dump_process_response
+};
+
+static struct stats_cmd_vfuncs add_vfuncs = {
+ .build_cmd = build_stats_add_cmd,
+ .process_response = stats_modify_process_response
+};
+
+static struct stats_cmd_vfuncs remove_vfuncs = {
+ .build_cmd = build_stats_remove_cmd,
+ .process_response = stats_modify_process_response
+};
+
+static string_t *init_stats_cmd(void)
+{
+ string_t *cmd = t_str_new(128);
+ str_append(cmd, "VERSION\tstats-reader-client\t2\t0\n");
+ return cmd;
+}
+
+static void stats_exec_cmd(struct doveadm_cmd_context *cctx,
+ struct stats_cmd_vfuncs *vfuncs)
+{
+ struct stats_cmd_context ctx;
+ const char *build_cmd_error;
+ ctx.cctx = cctx;
+ if (vfuncs->build_cmd(&ctx, &build_cmd_error) < 0) {
+ i_error("%s", build_cmd_error);
+ return;
+ }
+ stats_send_cmd(&ctx);
+ vfuncs->process_response(&ctx);
+ i_stream_destroy(&ctx.input);
+}
+
+static void handle_disconnection(struct stats_cmd_context *ctx)
+{
+ i_error("read(%s) failed: %s", ctx->path,
+ i_stream_get_disconnect_reason(ctx->input));
+}
+
+static void stats_send_cmd(struct stats_cmd_context *ctx)
+{
+ int fd;
+ const char *line;
+ if (!doveadm_cmd_param_str(ctx->cctx, "socket-path", &ctx->path))
+ ctx->path = t_strconcat(doveadm_settings->base_dir,
+ "/stats-reader", NULL);
+
+ fd = doveadm_connect(ctx->path);
+ net_set_nonblock(fd, FALSE);
+ if (write_full(fd, str_data(ctx->cmd), str_len(ctx->cmd)) < 0)
+ i_fatal("write(%s) failed %m", ctx->path);
+ ctx->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+
+ if ((line = i_stream_read_next_line(ctx->input)) == NULL)
+ i_fatal("%s: Failed to read VERSION line", ctx->path);
+ else if (!version_string_verify(line, "stats-reader-server", 2)) {
+ i_fatal_status(EX_PROTOCOL,
+ "%s is not a compatible stats-reader socket", ctx->path);
+ }
+}
+
+static void dump_timing(const char *const **args,
+ const enum doveadm_dump_field_type field_types[],
+ unsigned int fields_count)
+{
+ unsigned int i, args_count = str_array_length(*args);
+
+ if (args_count > fields_count)
+ args_count = fields_count;
+ for (i = 0; i < args_count; i++) {
+ const char *value = (*args)[i];
+
+ switch (field_types[i]) {
+ case DOVEADM_DUMP_FIELD_TYPE_PASSTHROUGH:
+ break;
+ case DOVEADM_DUMP_FIELD_TYPE_STDDEV: {
+ double variance = strtod(value, NULL);
+ value = t_strdup_printf("%.02f", sqrt(variance));
+ break;
+ }
+ }
+ doveadm_print(value);
+ }
+ *args += args_count;
+}
+
+static int build_stats_dump_cmd(struct stats_cmd_context *ctx,
+ const char **error_r ATTR_UNUSED)
+{
+ bool reset;
+ struct dump_data *data = t_new(struct dump_data, 1);
+ const char *fields_raw;
+ const char **fields;
+ if (!doveadm_cmd_param_bool(ctx->cctx, "reset", &reset))
+ reset = FALSE;
+ if (!doveadm_cmd_param_str(ctx->cctx, "fields", &fields_raw))
+ fields_raw = DOVEADM_DUMP_DEFAULT_FIELDS;
+
+ fields = t_strsplit_spaces(fields_raw, ", ");
+ data->fields = fields;
+ data->field_count = str_array_length(fields);
+ data->field_types =
+ t_malloc0(sizeof(enum doveadm_dump_field_type) * data->field_count);
+ ctx->data = data;
+ ctx->cmd = init_stats_cmd();
+ str_append(ctx->cmd, reset ? "DUMP-RESET" : "DUMP");
+ unsigned int i;
+ for (i = 0; i < data->field_count; i++) {
+ str_append_c(ctx->cmd, '\t');
+ if (strcmp(fields[i], "stddev") == 0) {
+ data->field_types[i] = DOVEADM_DUMP_FIELD_TYPE_STDDEV;
+ str_append(ctx->cmd, "variance");
+ } else {
+ str_append_tabescaped(ctx->cmd, fields[i]);
+ }
+ }
+ str_append_c(ctx->cmd, '\n');
+ return 0;
+}
+
+static void stats_dump_process_response(struct stats_cmd_context *ctx)
+{
+ unsigned int i;
+ char *line;
+ struct dump_data *data = ctx->data;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TAB);
+ doveadm_print_header_simple("metric_name");
+ doveadm_print_header_simple("field");
+ for (i = 0; i < data->field_count; i++)
+ doveadm_print_header(data->fields[i], data->fields[i],
+ DOVEADM_PRINT_HEADER_FLAG_NUMBER);
+
+ while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+ if (line[0] == '\0')
+ break;
+ T_BEGIN {
+ const char *const *args =
+ t_strsplit_tabescaped_inplace(line);
+
+ const char *metric_name = args[0];
+ doveadm_print(metric_name); args++;
+ doveadm_print("duration");
+ dump_timing(&args, data->field_types, data->field_count);
+ while (*args != NULL) {
+ doveadm_print(metric_name);
+ doveadm_print(*args); args++;
+ dump_timing(&args, data->field_types, data->field_count);
+ }
+ } T_END;
+ }
+ if (line == NULL)
+ handle_disconnection(ctx);
+}
+
+static int build_stats_add_cmd(struct stats_cmd_context *ctx,
+ const char **error_r)
+{
+ unsigned int i;
+ const char *parameter;
+ struct {
+ const char *name;
+ const char *default_val;
+ } params[] = {
+ { ADD_NAME_PARAM, "" },
+ { ADD_DESCR_PARAM, "" },
+ { ADD_FIELDS_PARAM, "" },
+ { ADD_GROUPBY_PARAM, "" },
+ { ADD_FILTER_PARAM, "" },
+ { ADD_EXPORTER_PARAM, "" },
+ /* Default exporter-include is to be modified
+ together with stats-settings */
+ { ADD_EXPORTERINCL_PARAM,
+ STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE },
+ };
+
+ ctx->cmd = init_stats_cmd();
+ str_append(ctx->cmd, "METRICS-ADD");
+
+ for (i = 0; i < N_ELEMENTS(params); i++) {
+ if (!doveadm_cmd_param_str(ctx->cctx, params[i].name, &parameter))
+ parameter = params[i].default_val;
+ if (parameter[0] == '\0' &&
+ (strcmp(params[i].name, "name") == 0 ||
+ strcmp(params[i].name, "filter") == 0)) {
+ *error_r =
+ t_strdup_printf("stats add: missing %s parameter",
+ params[i].name);
+ return -1;
+ }
+ str_append_c(ctx->cmd, '\t');
+ str_append_tabescaped(ctx->cmd, parameter);
+ }
+
+ str_append_c(ctx->cmd, '\n');
+ return 0;
+}
+
+static void stats_modify_process_response(struct stats_cmd_context *ctx)
+{
+ const char *line = i_stream_read_next_line(ctx->input);
+ if (line == NULL) {
+ handle_disconnection(ctx);
+ return;
+ }
+ if (line[0] == '-')
+ i_error("%s", ++line);
+ else if (line[0] != '+')
+ i_error("Invalid response: %s", line);
+}
+
+static int build_stats_remove_cmd(struct stats_cmd_context *ctx,
+ const char **error_r)
+{
+ const char *name;
+
+ ctx->cmd = init_stats_cmd();
+ str_append(ctx->cmd, "METRICS-REMOVE\t");
+
+ if (!doveadm_cmd_param_str(ctx->cctx, "name", &name)) {
+ *error_r = "stats remove: missing name parameter";
+ return -1;
+ }
+ str_append_tabescaped(ctx->cmd, name);
+ str_append_c(ctx->cmd, '\n');
+ return 0;
+}
+
+static void doveadm_cmd_stats_dump(struct doveadm_cmd_context *cctx)
+{
+ stats_exec_cmd(cctx, &dump_vfuncs);
+}
+
+static void doveadm_cmd_stats_add(struct doveadm_cmd_context *cctx)
+{
+ stats_exec_cmd(cctx, &add_vfuncs);
+}
+
+static void doveadm_cmd_stats_remove(struct doveadm_cmd_context *cctx)
+{
+ stats_exec_cmd(cctx, &remove_vfuncs);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_stats_dump_ver2 = {
+ .cmd = doveadm_cmd_stats_dump,
+ .name = "stats dump",
+ .usage = "[-s <stats socket path>] [-r] [-f <fields>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('r', "reset", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('f', "fields", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_stats_add_ver2 = {
+ .cmd = doveadm_cmd_stats_add,
+ .name = "stats add",
+ .usage = "[--"ADD_DESCR_PARAM" <string>] "
+ "[--"ADD_EXPORTER_PARAM" <name> [--"ADD_EXPORTERINCL_PARAM" <fields>]] "
+ "[--"ADD_FIELDS_PARAM" <fields>] "
+ "[--"ADD_GROUPBY_PARAM" <fields>] <name> <filter>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', ADD_NAME_PARAM, CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', ADD_FILTER_PARAM, CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', ADD_EXPORTER_PARAM, CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', ADD_EXPORTERINCL_PARAM, CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', ADD_DESCR_PARAM, CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', ADD_FIELDS_PARAM, CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', ADD_GROUPBY_PARAM, CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_stats_remove_ver2 = {
+ .cmd = doveadm_cmd_stats_remove,
+ .name = "stats remove",
+ .usage = "<name>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "name", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-util.c b/src/doveadm/doveadm-util.c
new file mode 100644
index 0000000..a65ef7f
--- /dev/null
+++ b/src/doveadm/doveadm-util.c
@@ -0,0 +1,221 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "module-dir.h"
+#include "doveadm-settings.h"
+#include "doveadm-mail.h"
+#include "doveadm-util.h"
+
+#include <time.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <ctype.h>
+
+#define DOVEADM_TCP_CONNECT_TIMEOUT_SECS 30
+
+bool doveadm_verbose = FALSE, doveadm_debug = FALSE, doveadm_server = FALSE;
+static struct module *modules = NULL;
+
+void doveadm_load_modules(void)
+{
+ struct module_dir_load_settings mod_set;
+
+ /* some doveadm plugins have dependencies to mail plugins. we can load
+ only those whose dependencies have been loaded earlier, the rest are
+ ignored. */
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = doveadm_debug;
+ mod_set.ignore_dlopen_errors = TRUE;
+
+ modules = module_dir_load_missing(modules, DOVEADM_MODULEDIR,
+ NULL, &mod_set);
+ module_dir_init(modules);
+}
+
+void doveadm_unload_modules(void)
+{
+ module_dir_unload(&modules);
+}
+
+bool doveadm_has_unloaded_plugin(const char *name)
+{
+ struct module *module;
+ DIR *dir;
+ struct dirent *d;
+ const char *plugin_name;
+ size_t name_len = strlen(name);
+ bool found = FALSE;
+
+ /* first check that it's not actually loaded */
+ for (module = modules; module != NULL; module = module->next) {
+ if (strcmp(module_get_plugin_name(module), name) == 0)
+ return FALSE;
+ }
+
+ dir = opendir(DOVEADM_MODULEDIR);
+ if (dir == NULL)
+ return FALSE;
+
+ while ((d = readdir(dir)) != NULL) {
+ plugin_name = module_file_get_name(d->d_name);
+ if (str_begins(plugin_name, "doveadm_"))
+ plugin_name += 8;
+
+ if (strncmp(plugin_name, name, name_len) == 0 &&
+ (plugin_name[name_len] == '\0' ||
+ strcmp(plugin_name + name_len, "_plugin") == 0)) {
+ found = TRUE;
+ break;
+ }
+ }
+ (void)closedir(dir);
+ return found;
+}
+
+const char *unixdate2str(time_t timestamp)
+{
+ return t_strflocaltime("%Y-%m-%d %H:%M:%S", timestamp);
+}
+
+const char *doveadm_plugin_getenv(const char *name)
+{
+ const char *const *envs;
+ unsigned int i, count;
+
+ if (!array_is_created(&doveadm_settings->plugin_envs))
+ return NULL;
+
+ envs = array_get(&doveadm_settings->plugin_envs, &count);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(envs[i], name) == 0)
+ return envs[i+1];
+ }
+ return NULL;
+}
+
+static int
+doveadm_tcp_connect_port(const char *host, in_port_t port)
+{
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int ret, fd;
+
+ alarm(DOVEADM_TCP_CONNECT_TIMEOUT_SECS);
+ ret = net_gethostbyname(host, &ips, &ips_count);
+ if (ret != 0) {
+ i_fatal("Lookup of host %s failed: %s",
+ host, net_gethosterror(ret));
+ }
+ fd = net_connect_ip_blocking(&ips[0], port, NULL);
+ if (fd == -1) {
+ i_fatal("connect(%s:%u) failed: %m",
+ net_ip2addr(&ips[0]), port);
+ }
+ alarm(0);
+ return fd;
+}
+
+int doveadm_tcp_connect(const char *target, in_port_t default_port)
+{
+ const char *host;
+ in_port_t port;
+
+ if (net_str2hostport(target, default_port, &host, &port) < 0) {
+ i_fatal("Port not known for %s. Either set proxy_port "
+ "or use %s:port", target, target);
+ }
+ return doveadm_tcp_connect_port(host, port);
+}
+
+int doveadm_connect_with_default_port(const char *path,
+ in_port_t default_port)
+{
+ int fd;
+
+ /* we'll assume UNIX sockets typically have an absolute path,
+ or at the very least '/' somewhere. */
+ if (strchr(path, '/') == NULL)
+ fd = doveadm_tcp_connect(path, default_port);
+ else {
+ fd = net_connect_unix(path);
+ if (fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ }
+ return fd;
+}
+
+int doveadm_connect(const char *path)
+{
+ return doveadm_connect_with_default_port(path, 0);
+}
+
+int i_strccdascmp(const char *a, const char *b)
+{
+ while(*a != '\0' && *b != '\0') {
+ if ((*a == ' ' || *a == '-') && *a != *b && *b != ' ' && *b != '-') {
+ if (i_toupper(*(a+1)) == *(b)) a++;
+ else break;
+ } else if ((*b == ' ' || *b == '-') && *a != *b && *a != ' ' && *a != '-') {
+ if (*a == i_toupper(*(b+1))) b++;
+ else break;
+ } else if (!((*a == ' ' || *a == '-') &&
+ (*b == ' ' || *b == '-')) &&
+ (*a != *b)) break;
+ a++; b++;
+ }
+ return *a-*b;
+}
+
+char doveadm_log_type_to_char(enum log_type type)
+{
+ switch(type) {
+ case LOG_TYPE_DEBUG:
+ return '\x01';
+ case LOG_TYPE_INFO:
+ return '\x02';
+ case LOG_TYPE_WARNING:
+ return '\x03';
+ case LOG_TYPE_ERROR:
+ return '\x04';
+ case LOG_TYPE_FATAL:
+ return '\x05';
+ case LOG_TYPE_PANIC:
+ return '\x06';
+ default:
+ i_unreached();
+ }
+}
+
+bool doveadm_log_type_from_char(char c, enum log_type *type_r)
+{
+ switch(c) {
+ case '\x01':
+ *type_r = LOG_TYPE_DEBUG;
+ break;
+ case '\x02':
+ *type_r = LOG_TYPE_INFO;
+ break;
+ case '\x03':
+ *type_r = LOG_TYPE_WARNING;
+ break;
+ case '\x04':
+ *type_r = LOG_TYPE_ERROR;
+ break;
+ case '\x05':
+ *type_r = LOG_TYPE_FATAL;
+ break;
+ case '\x06':
+ *type_r = LOG_TYPE_PANIC;
+ break;
+ default:
+ *type_r = LOG_TYPE_WARNING;
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/src/doveadm/doveadm-util.h b/src/doveadm/doveadm-util.h
new file mode 100644
index 0000000..8aefd92
--- /dev/null
+++ b/src/doveadm/doveadm-util.h
@@ -0,0 +1,31 @@
+#ifndef DOVEADM_UTIL_H
+#define DOVEADM_UTIL_H
+
+#include "net.h"
+
+#define DOVEADM_SERVER_PROTOCOL_VERSION_MAJOR 1
+#define DOVEADM_SERVER_PROTOCOL_VERSION_MINOR 2
+#define DOVEADM_SERVER_PROTOCOL_VERSION_LINE "VERSION\tdoveadm-server\t1\t2"
+#define DOVEADM_CLIENT_PROTOCOL_VERSION_LINE "VERSION\tdoveadm-client\t1\t2"
+
+extern bool doveadm_verbose, doveadm_debug, doveadm_server;
+
+const char *unixdate2str(time_t timestamp);
+const char *doveadm_plugin_getenv(const char *name);
+int doveadm_connect(const char *path);
+int doveadm_tcp_connect(const char *target, in_port_t default_port);
+int doveadm_connect_with_default_port(const char *path,
+ in_port_t default_port);
+
+void doveadm_load_modules(void);
+void doveadm_unload_modules(void);
+bool doveadm_has_unloaded_plugin(const char *name);
+
+char doveadm_log_type_to_char(enum log_type type) ATTR_PURE;
+bool doveadm_log_type_from_char(char c, enum log_type *type_r);
+
+/* Similar to strcmp(), except "camel case" == "camel-case" == "camelCase".
+ Otherwise the comparison is case-sensitive. */
+int i_strccdascmp(const char *a, const char *b) ATTR_PURE;
+
+#endif
diff --git a/src/doveadm/doveadm-who.c b/src/doveadm/doveadm-who.c
new file mode 100644
index 0000000..b60e515
--- /dev/null
+++ b/src/doveadm/doveadm-who.c
@@ -0,0 +1,364 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "wildcard-match.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+#include "doveadm-who.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+struct who_user {
+ const char *username;
+ const char *service;
+ ARRAY(struct ip_addr) ips;
+ ARRAY(pid_t) pids;
+ unsigned int connection_count;
+};
+
+static void who_user_ip(const struct who_user *user, struct ip_addr *ip_r)
+{
+ if (array_count(&user->ips) == 0)
+ i_zero(ip_r);
+ else {
+ const struct ip_addr *ip = array_front(&user->ips);
+ *ip_r = *ip;
+ }
+}
+
+static unsigned int who_user_hash(const struct who_user *user)
+{
+ struct ip_addr ip;
+ unsigned int hash = str_hash(user->service);
+
+ if (user->username[0] != '\0')
+ hash += str_hash(user->username);
+ else {
+ who_user_ip(user, &ip);
+ hash += net_ip_hash(&ip);
+ }
+ return hash;
+}
+
+static int who_user_cmp(const struct who_user *user1,
+ const struct who_user *user2)
+{
+ if (strcmp(user1->username, user2->username) != 0)
+ return 1;
+ if (strcmp(user1->service, user2->service) != 0)
+ return 1;
+
+ if (user1->username[0] == '\0') {
+ /* tracking only IP addresses, not usernames */
+ struct ip_addr ip1, ip2;
+
+ who_user_ip(user1, &ip1);
+ who_user_ip(user2, &ip2);
+ return net_ip_cmp(&ip1, &ip2);
+ }
+ return 0;
+}
+
+static bool
+who_user_has_ip(const struct who_user *user, const struct ip_addr *ip)
+{
+ const struct ip_addr *ex_ip;
+
+ array_foreach(&user->ips, ex_ip) {
+ if (net_ip_compare(ex_ip, ip))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int who_parse_line(const char *line, struct who_line *line_r)
+{
+ const char *const *args = t_strsplit_tabescaped(line);
+ const char *ident = args[0];
+ const char *pid_str = args[1];
+ const char *refcount_str = args[2];
+ const char *p, *ip_str;
+
+ i_zero(line_r);
+
+ /* ident = service/ip/username (imap, pop3)
+ or service/username (lmtp) */
+ p = strchr(ident, '/');
+ if (p == NULL)
+ return -1;
+ if (str_to_pid(pid_str, &line_r->pid) < 0)
+ return -1;
+ line_r->service = t_strdup_until(ident, p++);
+ line_r->username = strchr(p, '/');
+ if (line_r->username == NULL) {
+ /* no IP */
+ line_r->username = p;
+ } else {
+ ip_str = t_strdup_until(p, line_r->username++);
+ (void)net_addr2ip(ip_str, &line_r->ip);
+ }
+ if (str_to_uint(refcount_str, &line_r->refcount) < 0)
+ return -1;
+ return 0;
+}
+
+static bool who_user_has_pid(struct who_user *user, pid_t pid)
+{
+ pid_t ex_pid;
+
+ array_foreach_elem(&user->pids, ex_pid) {
+ if (ex_pid == pid)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void who_aggregate_line(struct who_context *ctx,
+ const struct who_line *line)
+{
+ struct who_user *user, lookup_user;
+
+ lookup_user.username = line->username;
+ lookup_user.service = line->service;
+
+ user = hash_table_lookup(ctx->users, &lookup_user);
+ if (user == NULL) {
+ user = p_new(ctx->pool, struct who_user, 1);
+ user->username = p_strdup(ctx->pool, line->username);
+ user->service = p_strdup(ctx->pool, line->service);
+ p_array_init(&user->ips, ctx->pool, 3);
+ p_array_init(&user->pids, ctx->pool, 8);
+ hash_table_insert(ctx->users, user, user);
+ }
+ user->connection_count += line->refcount;
+
+ if (line->ip.family != 0 && !who_user_has_ip(user, &line->ip))
+ array_push_back(&user->ips, &line->ip);
+
+ if (!who_user_has_pid(user, line->pid))
+ array_push_back(&user->pids, &line->pid);
+}
+
+int who_parse_args(struct who_context *ctx, const char *const *masks)
+{
+ struct ip_addr net_ip;
+ unsigned int i, net_bits;
+
+ for (i = 0; masks[i] != NULL; i++) {
+ if (!str_is_numeric(masks[i], '\0') &&
+ net_parse_range(masks[i], &net_ip, &net_bits) == 0) {
+ if (ctx->filter.net_bits != 0) {
+ i_error("Multiple network masks not supported");
+ doveadm_exit_code = EX_USAGE;
+ return -1;
+ }
+ ctx->filter.net_ip = net_ip;
+ ctx->filter.net_bits = net_bits;
+ } else {
+ if (ctx->filter.username != NULL) {
+ i_error("Multiple username masks not supported");
+ doveadm_exit_code = EX_USAGE;
+ return -1;
+ }
+ ctx->filter.username = masks[i];
+ }
+ }
+ return 0;
+}
+
+void who_lookup(struct who_context *ctx, who_callback_t *callback)
+{
+#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n"
+#define ANVIL_CMD ANVIL_HANDSHAKE"CONNECT-DUMP\n"
+ struct istream *input;
+ const char *line;
+ int fd;
+
+ fd = doveadm_connect(ctx->anvil_path);
+ net_set_nonblock(fd, FALSE);
+ if (write(fd, ANVIL_CMD, strlen(ANVIL_CMD)) < 0)
+ i_fatal("write(%s) failed: %m", ctx->anvil_path);
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (*line == '\0')
+ break;
+ T_BEGIN {
+ struct who_line who_line;
+
+ if (who_parse_line(line, &who_line) < 0)
+ i_error("Invalid input: %s", line);
+ else
+ callback(ctx, &who_line);
+ } T_END;
+ }
+ if (input->stream_errno != 0) {
+ i_fatal("read(%s) failed: %s", ctx->anvil_path,
+ i_stream_get_error(input));
+ }
+
+ i_stream_destroy(&input);
+}
+
+static bool who_user_filter_match(const struct who_user *user,
+ const struct who_filter *filter)
+{
+ if (filter->username != NULL) {
+ if (!wildcard_match_icase(user->username, filter->username))
+ return FALSE;
+ }
+ if (filter->net_bits > 0) {
+ const struct ip_addr *ip;
+ bool ret = FALSE;
+
+ array_foreach(&user->ips, ip) {
+ if (net_is_in_network(ip, &filter->net_ip,
+ filter->net_bits)) {
+ ret = TRUE;
+ break;
+ }
+ }
+ if (!ret)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void who_print_user(const struct who_user *user)
+{
+ const struct ip_addr *ip;
+ pid_t pid;
+ string_t *str = t_str_new(256);
+
+ doveadm_print(user->username);
+ doveadm_print(dec2str(user->connection_count));
+ doveadm_print(user->service);
+
+ str_append_c(str, '(');
+ array_foreach_elem(&user->pids, pid)
+ str_printfa(str, "%ld ", (long)pid);
+ if (str_len(str) > 1)
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ')');
+ doveadm_print(str_c(str));
+
+ str_truncate(str, 0);
+ str_append_c(str, '(');
+ array_foreach(&user->ips, ip)
+ str_printfa(str, "%s ", net_ip2addr(ip));
+ if (str_len(str) > 1)
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ')');
+ doveadm_print(str_c(str));
+
+ doveadm_print_flush();
+}
+
+static void who_print(struct who_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ struct who_user *user;
+
+ doveadm_print_header("username", "username", 0);
+ doveadm_print_header("connections", "#",
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY);
+ doveadm_print_header("service", "proto", 0);
+ doveadm_print_header("pids", "(pids)", 0);
+ doveadm_print_header("ips", "(ips)", 0);
+
+ iter = hash_table_iterate_init(ctx->users);
+ while (hash_table_iterate(iter, ctx->users, &user, &user)) {
+ if (who_user_filter_match(user, &ctx->filter)) T_BEGIN {
+ who_print_user(user);
+ } T_END;
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+bool who_line_filter_match(const struct who_line *line,
+ const struct who_filter *filter)
+{
+ if (filter->username != NULL) {
+ if (!wildcard_match_icase(line->username, filter->username))
+ return FALSE;
+ }
+ if (filter->net_bits > 0) {
+ if (!net_is_in_network(&line->ip, &filter->net_ip,
+ filter->net_bits))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void who_print_line(struct who_context *ctx,
+ const struct who_line *line)
+{
+ unsigned int i;
+
+ if (!who_line_filter_match(line, &ctx->filter))
+ return;
+
+ for (i = 0; i < line->refcount; i++) T_BEGIN {
+ doveadm_print(line->username);
+ doveadm_print(line->service);
+ doveadm_print(dec2str(line->pid));
+ doveadm_print(net_ip2addr(&line->ip));
+ } T_END;
+}
+
+static void cmd_who(struct doveadm_cmd_context *cctx)
+{
+ const char *const *masks;
+ struct who_context ctx;
+ bool separate_connections = FALSE;
+
+ i_zero(&ctx);
+ if (!doveadm_cmd_param_str(cctx, "socket-path", &(ctx.anvil_path)))
+ ctx.anvil_path = t_strconcat(doveadm_settings->base_dir, "/anvil", NULL);
+ (void)doveadm_cmd_param_bool(cctx, "separate-connections", &separate_connections);
+
+ ctx.pool = pool_alloconly_create("who users", 10240);
+ hash_table_create(&ctx.users, ctx.pool, 0, who_user_hash, who_user_cmp);
+
+ if (doveadm_cmd_param_array(cctx, "mask", &masks)) {
+ if (who_parse_args(&ctx, masks) != 0) {
+ hash_table_destroy(&ctx.users);
+ pool_unref(&ctx.pool);
+ return;
+ }
+ }
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ if (!separate_connections) {
+ who_lookup(&ctx, who_aggregate_line);
+ who_print(&ctx);
+ } else {
+ doveadm_print_header("username", "username",
+ DOVEADM_PRINT_HEADER_FLAG_EXPAND);
+ doveadm_print_header("service", "proto", 0);
+ doveadm_print_header_simple("pid");
+ doveadm_print_header_simple("ip");
+ who_lookup(&ctx, who_print_line);
+ }
+
+ hash_table_destroy(&ctx.users);
+ pool_unref(&ctx.pool);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_who_ver2 = {
+ .name = "who",
+ .cmd = cmd_who,
+ .usage = "[-a <anvil socket path>] [-1] [<user mask>] [<ip/bits>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('a',"socket-path", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('1',"separate-connections", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0',"mask", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm-who.h b/src/doveadm/doveadm-who.h
new file mode 100644
index 0000000..57c663a
--- /dev/null
+++ b/src/doveadm/doveadm-who.h
@@ -0,0 +1,37 @@
+#ifndef DOVEADM_WHO_H
+#define DOVEADM_WHO_H
+
+struct who_line {
+ const char *username;
+ const char *service;
+ struct ip_addr ip;
+ pid_t pid;
+ unsigned int refcount;
+};
+
+
+struct who_filter {
+ const char *username;
+ struct ip_addr net_ip;
+ unsigned int net_bits;
+};
+
+struct who_context {
+ const char *anvil_path;
+ struct who_filter filter;
+
+ pool_t pool;
+ HASH_TABLE(struct who_user *, struct who_user *) users;
+};
+
+typedef void who_callback_t(struct who_context *ctx,
+ const struct who_line *line);
+
+int who_parse_args(struct who_context *ctx, const char *const *masks);
+
+void who_lookup(struct who_context *ctx, who_callback_t *callback);
+
+bool who_line_filter_match(const struct who_line *line,
+ const struct who_filter *filter);
+
+#endif /* DOVEADM_WHO_H */
diff --git a/src/doveadm/doveadm-zlib.c b/src/doveadm/doveadm-zlib.c
new file mode 100644
index 0000000..f9bb233
--- /dev/null
+++ b/src/doveadm/doveadm-zlib.c
@@ -0,0 +1,297 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-zlib.h"
+#include "ostream-zlib.h"
+#include "module-dir.h"
+#include "master-service.h"
+#include "compression.h"
+#include "doveadm-dump.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static bool test_dump_imapzlib(const char *path)
+{
+ const char *p;
+ char buf[4096];
+ int fd, ret;
+ bool match = FALSE;
+
+ p = strrchr(path, '.');
+ if (p == NULL || (strcmp(p, ".in") != 0 && strcmp(p, ".out") != 0))
+ return FALSE;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return FALSE;
+
+ ret = read(fd, buf, sizeof(buf)-1);
+ if (ret > 0) {
+ buf[ret] = '\0';
+ (void)str_lcase(buf);
+ match = strstr(buf, " ok begin compression.") != NULL ||
+ strstr(buf, " compress deflate") != NULL;
+ }
+ i_close_fd(&fd);
+ return match;
+}
+
+#ifdef HAVE_ZLIB
+static void
+cmd_dump_imapzlib(const char *path, const char *const *args ATTR_UNUSED)
+{
+ struct istream *input, *input2;
+ const unsigned char *data;
+ size_t size;
+ const char *line;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(%s) failed: %m", path);
+ input = i_stream_create_fd_autoclose(&fd, 1024*32);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ /* skip tag */
+ printf("%s\r\n", line);
+ while (*line != ' ' && *line != '\0') line++;
+ if (*line == '\0')
+ continue;
+ line++;
+
+ if (str_begins(line, "OK Begin compression") ||
+ strcasecmp(line, "COMPRESS DEFLATE") == 0)
+ break;
+ }
+
+ input2 = i_stream_create_deflate(input);
+ i_stream_unref(&input);
+
+ while (i_stream_read_more(input2, &data, &size) != -1) {
+ if (fwrite(data, 1, size, stdout) != size)
+ break;
+ i_stream_skip(input2, size);
+ }
+ if (input2->stream_errno != 0)
+ i_error("read(%s) failed: %s", path, i_stream_get_error(input));
+ i_stream_unref(&input2);
+ fflush(stdout);
+}
+
+struct client {
+ int fd;
+ struct io *io_client, *io_server;
+ struct istream *input, *stdin_input;
+ struct ostream *output;
+ const struct compression_handler *handler;
+ char *algorithm;
+ bool compressed;
+ bool compress_waiting;
+};
+
+static bool
+client_input_get_compress_algorithm(struct client *client, const char *line)
+{
+ /* skip tag */
+ while (*line != ' ' && *line != '\0')
+ line++;
+ if (strncasecmp(line, " COMPRESS ", 10) != 0)
+ return FALSE;
+
+ if (compression_lookup_handler(t_str_lcase(line+10),
+ &client->handler) <= 0)
+ i_fatal("Unsupported compression mechanism: %s", line+10);
+ return TRUE;
+}
+
+static bool client_input_uncompressed(struct client *client)
+{
+ const char *line;
+
+ if (client->compress_waiting) {
+ /* just read all the pipelined input for now */
+ (void)i_stream_read(client->stdin_input);
+ return TRUE;
+ }
+
+ while ((line = i_stream_read_next_line(client->stdin_input)) != NULL) {
+ o_stream_nsend_str(client->output, line);
+ o_stream_nsend(client->output, "\n", 1);
+ if (client_input_get_compress_algorithm(client, line))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void client_input(struct client *client)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (!client->compressed &&
+ client_input_uncompressed(client)) {
+ /* stop until server has sent reply to COMPRESS command. */
+ client->compress_waiting = TRUE;
+ return;
+ }
+ if (client->compressed) {
+ if (i_stream_read_more(client->stdin_input, &data, &size) > 0) {
+ o_stream_nsend(client->output, data, size);
+ i_stream_skip(client->stdin_input, size);
+ }
+ if (o_stream_flush(client->output) < 0) {
+ i_fatal("write() failed: %s",
+ o_stream_get_error(client->output));
+ }
+ }
+ if (client->stdin_input->eof) {
+ if (client->stdin_input->stream_errno != 0) {
+ i_fatal("read(stdin) failed: %s",
+ i_stream_get_error(client->stdin_input));
+ }
+ master_service_stop(master_service);
+ }
+}
+
+static bool server_input_is_compress_reply(const char *line)
+{
+ /* skip tag */
+ while (*line != ' ' && *line != '\0')
+ line++;
+ return str_begins(line, " OK Begin compression");
+}
+
+static bool server_input_uncompressed(struct client *client)
+{
+ const char *line;
+
+ while ((line = i_stream_read_next_line(client->input)) != NULL) {
+ if (write(STDOUT_FILENO, line, strlen(line)) < 0)
+ i_fatal("write(stdout) failed: %m");
+ if (write(STDOUT_FILENO, "\n", 1) < 0)
+ i_fatal("write(stdout) failed: %m");
+ if (server_input_is_compress_reply(line))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void server_input(struct client *client)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read(client->input) == -1) {
+ if (client->input->stream_errno != 0) {
+ i_fatal("read(server) failed: %s",
+ i_stream_get_error(client->input));
+ }
+
+ i_info("Server disconnected");
+ master_service_stop(master_service);
+ return;
+ }
+
+ if (!client->compressed && server_input_uncompressed(client)) {
+ /* start compression */
+ struct istream *input;
+ struct ostream *output;
+
+ i_info("<Compression started>");
+ input = client->handler->create_istream(client->input);
+ output = client->handler->create_ostream(client->output, 6);
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+ client->input = input;
+ client->output = output;
+ client->compressed = TRUE;
+ client->compress_waiting = FALSE;
+ i_stream_set_input_pending(client->stdin_input, TRUE);
+ }
+
+ data = i_stream_get_data(client->input, &size);
+ if (write(STDOUT_FILENO, data, size) < 0)
+ i_fatal("write(stdout) failed: %m");
+ i_stream_skip(client->input, size);
+}
+
+static void cmd_zlibconnect(struct doveadm_cmd_context *cctx)
+{
+ struct client client;
+ const char *host;
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int64_t port_int64;
+ in_port_t port = 143;
+ int fd, ret;
+
+ if (!doveadm_cmd_param_str(cctx, "host", &host))
+ help_ver2(&doveadm_cmd_zlibconnect);
+ if (doveadm_cmd_param_int64(cctx, "port", &port_int64)) {
+ if (port_int64 == 0 || port_int64 > 65535)
+ i_fatal("Invalid port: %"PRId64, port_int64);
+ port = (in_port_t)port_int64;
+ }
+
+ ret = net_gethostbyname(host, &ips, &ips_count);
+ if (ret != 0) {
+ i_fatal("Host %s lookup failed: %s", host,
+ net_gethosterror(ret));
+ }
+
+ if ((fd = net_connect_ip(&ips[0], port, NULL)) == -1)
+ i_fatal("connect(%s, %u) failed: %m", host, port);
+
+ i_info("Connected to %s port %u.", net_ip2addr(&ips[0]), port);
+
+ i_zero(&client);
+ client.fd = fd;
+ fd_set_nonblock(STDIN_FILENO, TRUE);
+ client.stdin_input = i_stream_create_fd(STDIN_FILENO, SIZE_MAX);
+ client.input = i_stream_create_fd(fd, SIZE_MAX);
+ client.output = o_stream_create_fd(fd, 0);
+ o_stream_set_no_error_handling(client.output, TRUE);
+ client.io_client = io_add_istream(client.stdin_input, client_input, &client);
+ client.io_server = io_add_istream(client.input, server_input, &client);
+ master_service_run(master_service, NULL);
+ io_remove(&client.io_client);
+ io_remove(&client.io_server);
+ i_stream_unref(&client.stdin_input);
+ i_stream_unref(&client.input);
+ o_stream_unref(&client.output);
+ if (close(fd) < 0)
+ i_fatal("close() failed: %m");
+}
+#else
+static void
+cmd_dump_imapzlib(const char *path ATTR_UNUSED,
+ const char *const *args ATTR_UNUSED)
+{
+ i_fatal("Dovecot compiled without zlib support");
+}
+
+static void cmd_zlibconnect(struct doveadm_cmd_context *cctx ATTR_UNUSED)
+{
+ i_fatal("Dovecot compiled without zlib support");
+}
+#endif
+
+struct doveadm_cmd_dump doveadm_cmd_dump_zlib = {
+ "imapzlib",
+ test_dump_imapzlib,
+ cmd_dump_imapzlib
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_zlibconnect = {
+ .name = "zlibconnect",
+ .cmd = cmd_zlibconnect,
+ .usage = "<host> [<port>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "host", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "port", CMD_PARAM_INT64, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
diff --git a/src/doveadm/doveadm.c b/src/doveadm/doveadm.c
new file mode 100644
index 0000000..f175f2c
--- /dev/null
+++ b/src/doveadm/doveadm.c
@@ -0,0 +1,384 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "sort.h"
+#include "ostream.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "dict.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "settings-parser.h"
+#include "doveadm-print-private.h"
+#include "doveadm-dump.h"
+#include "doveadm-mail.h"
+#include "doveadm-settings.h"
+#include "doveadm-dsync.h"
+#include "doveadm.h"
+
+#include <unistd.h>
+
+const struct doveadm_print_vfuncs *doveadm_print_vfuncs_all[] = {
+ &doveadm_print_flow_vfuncs,
+ &doveadm_print_tab_vfuncs,
+ &doveadm_print_table_vfuncs,
+ &doveadm_print_pager_vfuncs,
+ &doveadm_print_json_vfuncs,
+ &doveadm_print_formatted_vfuncs,
+ NULL
+};
+
+int doveadm_exit_code = 0;
+
+static void failure_exit_callback(int *status)
+{
+ enum fatal_exit_status fatal_status = *status;
+
+ switch (fatal_status) {
+ case FATAL_LOGWRITE:
+ case FATAL_LOGERROR:
+ case FATAL_LOGOPEN:
+ case FATAL_OUTOFMEM:
+ case FATAL_EXEC:
+ case FATAL_DEFAULT:
+ *status = EX_TEMPFAIL;
+ break;
+ }
+}
+
+static void
+doveadm_usage_compress_lines(FILE *out, const char *str, const char *prefix)
+{
+ const char *cmd, *args, *p, *short_name, *sub_name;
+ const char *prev_name = "", *prev_sub_name = "";
+ const char **lines;
+ unsigned int i, count;
+ size_t prefix_len = strlen(prefix);
+
+ /* split lines */
+ lines = (void *)p_strsplit(pool_datastack_create(), str, "\n");
+ for (count = 0; lines[count] != NULL; count++) ;
+
+ /* sort lines */
+ i_qsort(lines, count, sizeof(*lines), i_strcmp_p);
+
+ /* print lines, compress subcommands into a single line */
+ for (i = 0; i < count; i++) {
+ args = strchr(lines[i], '\t');
+ if (args == NULL) {
+ cmd = lines[i];
+ args = "";
+ } else {
+ cmd = t_strdup_until(lines[i], args);
+ args++;
+ }
+ if (*prefix != '\0') {
+ if (strncmp(cmd, prefix, prefix_len) != 0 ||
+ cmd[prefix_len] != ' ')
+ continue;
+ cmd += prefix_len + 1;
+ }
+
+ p = strchr(cmd, ' ');
+ if (p == NULL) {
+ if (*prev_name != '\0') {
+ fprintf(out, "\n");
+ prev_name = "";
+ }
+ fprintf(out, USAGE_CMDNAME_FMT" %s\n", cmd, args);
+ } else {
+ short_name = t_strdup_until(cmd, p);
+ if (strcmp(prev_name, short_name) != 0) {
+ if (*prev_name != '\0')
+ fprintf(out, "\n");
+ fprintf(out, USAGE_CMDNAME_FMT" %s",
+ short_name, t_strcut(p + 1, ' '));
+ prev_name = short_name;
+ prev_sub_name = "";
+ } else {
+ sub_name = t_strcut(p + 1, ' ');
+ if (strcmp(prev_sub_name, sub_name) != 0) {
+ fprintf(out, "|%s", sub_name);
+ prev_sub_name = sub_name;
+ }
+ }
+ }
+ }
+ if (*prev_name != '\0')
+ fprintf(out, "\n");
+}
+
+static void ATTR_NORETURN
+usage_prefix(const char *prefix)
+{
+ const struct doveadm_cmd_ver2 *cmd2;
+ string_t *str = t_str_new(1024);
+
+ fprintf(stderr, "usage: doveadm [-Dv] [-f <formatter>] ");
+ if (*prefix != '\0')
+ fprintf(stderr, "%s ", prefix);
+ fprintf(stderr, "<command> [<args>]\n");
+
+ array_foreach(&doveadm_cmds_ver2, cmd2)
+ str_printfa(str, "%s\t%s\n", cmd2->name, cmd2->usage);
+
+ doveadm_usage_compress_lines(stderr, str_c(str), prefix);
+
+ lib_exit(EX_USAGE);
+}
+
+void usage(void)
+{
+ usage_prefix("");
+}
+
+void help_ver2(const struct doveadm_cmd_ver2 *cmd)
+{
+ fprintf(stderr, "doveadm %s %s\n", cmd->name, cmd->usage);
+ lib_exit(EX_USAGE);
+}
+
+static void cmd_help(struct doveadm_cmd_context *cctx)
+{
+ const char *cmd, *man_argv[3];
+
+ if (!doveadm_cmd_param_str(cctx, "cmd", &cmd))
+ usage_prefix("");
+
+ env_put("MANPATH", MANDIR);
+ man_argv[0] = "man";
+ man_argv[1] = t_strconcat("doveadm-", cmd, NULL);
+ man_argv[2] = NULL;
+ execvp_const(man_argv[0], man_argv);
+}
+
+static struct doveadm_cmd_ver2 doveadm_cmd_help = {
+ .name = "help",
+ .cmd = cmd_help,
+ .usage = "[<cmd>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "cmd", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+static void cmd_config(struct doveadm_cmd_context *cctx)
+{
+ const char *const *args, **argv;
+
+ if (!doveadm_cmd_param_array(cctx, "args", &args))
+ args = NULL;
+
+ env_put(MASTER_CONFIG_FILE_ENV,
+ master_service_get_config_path(master_service));
+
+ unsigned int len = str_array_length(args);
+ argv = t_new(const char *, len + 2);
+ argv[0] = BINDIR"/doveconf";
+ if (len > 0) {
+ i_assert(args != NULL);
+ memcpy(argv+1, args, len * sizeof(args[0]));
+ }
+ execv_const(argv[0], argv);
+}
+
+static struct doveadm_cmd_ver2 doveadm_cmd_config = {
+ .name = "config",
+ .cmd = cmd_config,
+ .usage = "[doveconf parameters]",
+ .flags = CMD_FLAG_NO_OPTIONS,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "args", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+static void cmd_exec(struct doveadm_cmd_context *cctx);
+static struct doveadm_cmd_ver2 doveadm_cmd_exec = {
+ .name = "exec",
+ .cmd = cmd_exec,
+ .usage = "<binary> [binary parameters]",
+ .flags = CMD_FLAG_NO_OPTIONS,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "binary", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "args", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+static void cmd_exec(struct doveadm_cmd_context *cctx)
+{
+ const char *path, *binary, *const *args, **argv;
+
+ if (!doveadm_cmd_param_str(cctx, "binary", &binary))
+ help_ver2(&doveadm_cmd_exec);
+ if (!doveadm_cmd_param_array(cctx, "args", &args))
+ args = NULL;
+
+ path = t_strdup_printf("%s/%s", doveadm_settings->libexec_dir, binary);
+
+ unsigned int len = str_array_length(args);
+ argv = t_new(const char *, len + 2);
+ argv[0] = path;
+ if (len > 0) {
+ i_assert(args != NULL);
+ memcpy(argv+1, args, len * sizeof(args[0]));
+ }
+ execv_const(argv[0], argv);
+}
+
+static bool doveadm_has_subcommands(const char *cmd_name)
+{
+ const struct doveadm_cmd_ver2 *cmd2;
+ size_t len = strlen(cmd_name);
+
+ array_foreach(&doveadm_cmds_ver2, cmd2) {
+ if (strncmp(cmd2->name, cmd_name, len) == 0 &&
+ cmd2->name[len] == ' ')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct doveadm_cmd_ver2 *doveadm_cmdline_commands_ver2[] = {
+ &doveadm_cmd_config,
+ &doveadm_cmd_dump,
+ &doveadm_cmd_exec,
+ &doveadm_cmd_help,
+ &doveadm_cmd_oldstats_top_ver2,
+ &doveadm_cmd_pw,
+ &doveadm_cmd_zlibconnect,
+};
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME;
+ struct doveadm_cmd_context cctx;
+ const char *cmd_name;
+ unsigned int i;
+ bool quick_init = FALSE;
+ int c;
+
+ i_zero(&cctx);
+ cctx.conn_type = DOVEADM_CONNECTION_TYPE_CLI;
+
+ i_set_failure_exit_callback(failure_exit_callback);
+ doveadm_dsync_main(&argc, &argv);
+
+ /* "+" is GNU extension to stop at the first non-option.
+ others just accept -+ option. */
+ master_service = master_service_init("doveadm", service_flags,
+ &argc, &argv, "+Df:hv");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ doveadm_debug = TRUE;
+ doveadm_verbose = TRUE;
+ break;
+ case 'f':
+ doveadm_print_init(optarg);
+ break;
+ case 'h':
+ doveadm_print_hide_titles = TRUE;
+ break;
+ case 'v':
+ doveadm_verbose = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ cmd_name = argv[optind];
+
+ if (cmd_name != NULL && strcmp(cmd_name, "help") == 0 &&
+ argv[optind+1] != NULL) {
+ /* "help cmd" doesn't need any configuration */
+ quick_init = TRUE;
+ }
+ master_service_init_log(master_service);
+
+ doveadm_settings_init();
+ doveadm_cmds_init();
+ for (i = 0; i < N_ELEMENTS(doveadm_cmdline_commands_ver2); i++)
+ doveadm_cmd_register_ver2(doveadm_cmdline_commands_ver2[i]);
+ doveadm_register_auth_commands();
+
+ if (cmd_name != NULL && (quick_init ||
+ strcmp(cmd_name, "config") == 0 ||
+ strcmp(cmd_name, "stop") == 0 ||
+ strcmp(cmd_name, "reload") == 0)) {
+ /* special case commands: even if there is something wrong
+ with the config (e.g. mail_plugins), don't fail these
+ commands */
+ if (strcmp(cmd_name, "help") != 0)
+ doveadm_read_settings();
+ quick_init = TRUE;
+ } else {
+ quick_init = FALSE;
+ doveadm_print_ostream = o_stream_create_fd(STDOUT_FILENO, 0);
+ o_stream_set_no_error_handling(doveadm_print_ostream, TRUE);
+ o_stream_cork(doveadm_print_ostream);
+ doveadm_dump_init();
+ doveadm_mail_init();
+ dict_drivers_register_builtin();
+ doveadm_load_modules();
+
+ /* read settings only after loading doveadm plugins, which
+ may modify what settings are read */
+ doveadm_read_settings();
+ if (doveadm_debug && getenv("LOG_STDERR_TIMESTAMP") == NULL)
+ i_set_failure_timestamp_format(master_service->set->log_timestamp);
+ master_service_init_stats_client(master_service, TRUE);
+ /* Load mail_plugins */
+ doveadm_mail_init_finish();
+ /* kludgy: Load the rest of the doveadm plugins after
+ mail_plugins have been loaded. */
+ doveadm_load_modules();
+
+ if (cmd_name == NULL) {
+ /* show usage after registering all plugins */
+ usage_prefix("");
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ i_getopt_reset();
+
+ master_service_init_finish(master_service);
+ if (!doveadm_debug) {
+ /* disable debugging unless -D is given */
+ i_set_debug_file("/dev/null");
+ }
+
+ /* this has to be done here because proctitle hack can break
+ the env pointer */
+ cctx.username = getenv("USER");
+
+ if (!doveadm_cmd_try_run_ver2(cmd_name, argc, (const char**)argv, &cctx)) {
+ if (doveadm_has_subcommands(cmd_name))
+ usage_prefix(cmd_name);
+ if (doveadm_has_unloaded_plugin(cmd_name)) {
+ i_fatal("Unknown command '%s', but plugin %s exists. "
+ "Try to set mail_plugins=%s",
+ cmd_name, cmd_name, cmd_name);
+ }
+ usage();
+ }
+
+ if (!quick_init) {
+ doveadm_mail_deinit();
+ doveadm_dump_deinit();
+ doveadm_unload_modules();
+ dict_drivers_unregister_builtin();
+ doveadm_print_deinit();
+ o_stream_unref(&doveadm_print_ostream);
+ }
+ doveadm_cmds_deinit();
+ doveadm_settings_deinit();
+ master_service_deinit(&master_service);
+ return doveadm_exit_code;
+}
diff --git a/src/doveadm/doveadm.h b/src/doveadm/doveadm.h
new file mode 100644
index 0000000..d4f74e5
--- /dev/null
+++ b/src/doveadm/doveadm.h
@@ -0,0 +1,34 @@
+#ifndef DOVEADM_H
+#define DOVEADM_H
+
+#include <sysexits.h>
+#include "doveadm-util.h"
+#include "doveadm-settings.h"
+
+#define USAGE_CMDNAME_FMT " %-12s"
+
+#define DOVEADM_EX_NOTFOUND EX_NOHOST
+#define DOVEADM_EX_NOTPOSSIBLE EX_DATAERR
+#define DOVEADM_EX_UNKNOWN -1
+
+#define DOVEADM_EX_NOREPLICATE 1001
+
+enum doveadm_client_type {
+ DOVEADM_CONNECTION_TYPE_CLI = 0,
+ DOVEADM_CONNECTION_TYPE_TCP,
+ DOVEADM_CONNECTION_TYPE_HTTP,
+};
+
+#include "doveadm-cmd.h"
+
+extern bool doveadm_verbose_proctitle;
+extern int doveadm_exit_code;
+
+void usage(void) ATTR_NORETURN;
+void help_ver2(const struct doveadm_cmd_ver2 *cmd) ATTR_NORETURN;
+void doveadm_master_send_signal(int signo);
+
+const char *doveadm_exit_code_to_str(int code);
+int doveadm_str_to_exit_code(const char *reason);
+
+#endif
diff --git a/src/doveadm/dsync/Makefile.am b/src/doveadm/dsync/Makefile.am
new file mode 100644
index 0000000..20cbfc7
--- /dev/null
+++ b/src/doveadm/dsync/Makefile.am
@@ -0,0 +1,76 @@
+pkglib_LTLIBRARIES = libdovecot-dsync.la
+noinst_LTLIBRARIES = libdsync.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-ssl-iostream \
+ -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
+
+libdsync_la_SOURCES = \
+ dsync-brain.c \
+ dsync-brain-mailbox.c \
+ dsync-brain-mailbox-tree.c \
+ dsync-brain-mailbox-tree-sync.c \
+ dsync-brain-mails.c \
+ dsync-deserializer.c \
+ dsync-mail.c \
+ dsync-mailbox.c \
+ dsync-mailbox-import.c \
+ dsync-mailbox-export.c \
+ dsync-mailbox-state.c \
+ dsync-mailbox-tree.c \
+ dsync-mailbox-tree-fill.c \
+ dsync-mailbox-tree-sync.c \
+ dsync-serializer.c \
+ dsync-ibc.c \
+ dsync-ibc-stream.c \
+ dsync-ibc-pipe.c \
+ dsync-transaction-log-scan.c
+
+libdovecot_dsync_la_SOURCES =
+libdovecot_dsync_la_LIBADD = libdsync.la ../../lib-storage/libdovecot-storage.la ../../lib-dovecot/libdovecot.la
+libdovecot_dsync_la_DEPENDENCIES = libdsync.la
+libdovecot_dsync_la_LDFLAGS = -export-dynamic
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ dsync-brain.h \
+ dsync-ibc.h
+
+noinst_HEADERS = \
+ dsync-brain-private.h \
+ dsync-mail.h \
+ dsync-mailbox.h \
+ dsync-mailbox-import.h \
+ dsync-mailbox-export.h \
+ dsync-mailbox-state.h \
+ dsync-mailbox-tree.h \
+ dsync-mailbox-tree-private.h \
+ dsync-serializer.h \
+ dsync-deserializer.h \
+ dsync-ibc-private.h \
+ dsync-transaction-log-scan.h
+
+test_programs = \
+ test-dsync-mailbox-tree-sync
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+
+test_dsync_mailbox_tree_sync_SOURCES = test-dsync-mailbox-tree-sync.c
+test_dsync_mailbox_tree_sync_LDADD = dsync-mailbox-tree-sync.lo dsync-mailbox-tree.lo $(test_libs)
+test_dsync_mailbox_tree_sync_DEPENDENCIES = $(pkglib_LTLIBRARIES) $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/doveadm/dsync/Makefile.in b/src/doveadm/dsync/Makefile.in
new file mode 100644
index 0000000..034c9d4
--- /dev/null
+++ b/src/doveadm/dsync/Makefile.in
@@ -0,0 +1,1019 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/doveadm/dsync
+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-dsync-mailbox-tree-sync$(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_dsync_la_OBJECTS =
+libdovecot_dsync_la_OBJECTS = $(am_libdovecot_dsync_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_dsync_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_dsync_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+libdsync_la_LIBADD =
+am_libdsync_la_OBJECTS = dsync-brain.lo dsync-brain-mailbox.lo \
+ dsync-brain-mailbox-tree.lo dsync-brain-mailbox-tree-sync.lo \
+ dsync-brain-mails.lo dsync-deserializer.lo dsync-mail.lo \
+ dsync-mailbox.lo dsync-mailbox-import.lo \
+ dsync-mailbox-export.lo dsync-mailbox-state.lo \
+ dsync-mailbox-tree.lo dsync-mailbox-tree-fill.lo \
+ dsync-mailbox-tree-sync.lo dsync-serializer.lo dsync-ibc.lo \
+ dsync-ibc-stream.lo dsync-ibc-pipe.lo \
+ dsync-transaction-log-scan.lo
+libdsync_la_OBJECTS = $(am_libdsync_la_OBJECTS)
+am_test_dsync_mailbox_tree_sync_OBJECTS = \
+ test-dsync-mailbox-tree-sync.$(OBJEXT)
+test_dsync_mailbox_tree_sync_OBJECTS = \
+ $(am_test_dsync_mailbox_tree_sync_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)/dsync-brain-mailbox-tree-sync.Plo \
+ ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo \
+ ./$(DEPDIR)/dsync-brain-mailbox.Plo \
+ ./$(DEPDIR)/dsync-brain-mails.Plo ./$(DEPDIR)/dsync-brain.Plo \
+ ./$(DEPDIR)/dsync-deserializer.Plo \
+ ./$(DEPDIR)/dsync-ibc-pipe.Plo \
+ ./$(DEPDIR)/dsync-ibc-stream.Plo ./$(DEPDIR)/dsync-ibc.Plo \
+ ./$(DEPDIR)/dsync-mail.Plo \
+ ./$(DEPDIR)/dsync-mailbox-export.Plo \
+ ./$(DEPDIR)/dsync-mailbox-import.Plo \
+ ./$(DEPDIR)/dsync-mailbox-state.Plo \
+ ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo \
+ ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo \
+ ./$(DEPDIR)/dsync-mailbox-tree.Plo \
+ ./$(DEPDIR)/dsync-mailbox.Plo ./$(DEPDIR)/dsync-serializer.Plo \
+ ./$(DEPDIR)/dsync-transaction-log-scan.Plo \
+ ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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_dsync_la_SOURCES) $(libdsync_la_SOURCES) \
+ $(test_dsync_mailbox_tree_sync_SOURCES)
+DIST_SOURCES = $(libdovecot_dsync_la_SOURCES) $(libdsync_la_SOURCES) \
+ $(test_dsync_mailbox_tree_sync_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-dsync.la
+noinst_LTLIBRARIES = libdsync.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-ssl-iostream \
+ -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
+
+libdsync_la_SOURCES = \
+ dsync-brain.c \
+ dsync-brain-mailbox.c \
+ dsync-brain-mailbox-tree.c \
+ dsync-brain-mailbox-tree-sync.c \
+ dsync-brain-mails.c \
+ dsync-deserializer.c \
+ dsync-mail.c \
+ dsync-mailbox.c \
+ dsync-mailbox-import.c \
+ dsync-mailbox-export.c \
+ dsync-mailbox-state.c \
+ dsync-mailbox-tree.c \
+ dsync-mailbox-tree-fill.c \
+ dsync-mailbox-tree-sync.c \
+ dsync-serializer.c \
+ dsync-ibc.c \
+ dsync-ibc-stream.c \
+ dsync-ibc-pipe.c \
+ dsync-transaction-log-scan.c
+
+libdovecot_dsync_la_SOURCES =
+libdovecot_dsync_la_LIBADD = libdsync.la ../../lib-storage/libdovecot-storage.la ../../lib-dovecot/libdovecot.la
+libdovecot_dsync_la_DEPENDENCIES = libdsync.la
+libdovecot_dsync_la_LDFLAGS = -export-dynamic
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ dsync-brain.h \
+ dsync-ibc.h
+
+noinst_HEADERS = \
+ dsync-brain-private.h \
+ dsync-mail.h \
+ dsync-mailbox.h \
+ dsync-mailbox-import.h \
+ dsync-mailbox-export.h \
+ dsync-mailbox-state.h \
+ dsync-mailbox-tree.h \
+ dsync-mailbox-tree-private.h \
+ dsync-serializer.h \
+ dsync-deserializer.h \
+ dsync-ibc-private.h \
+ dsync-transaction-log-scan.h
+
+test_programs = \
+ test-dsync-mailbox-tree-sync
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+
+test_dsync_mailbox_tree_sync_SOURCES = test-dsync-mailbox-tree-sync.c
+test_dsync_mailbox_tree_sync_LDADD = dsync-mailbox-tree-sync.lo dsync-mailbox-tree.lo $(test_libs)
+test_dsync_mailbox_tree_sync_DEPENDENCIES = $(pkglib_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/doveadm/dsync/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/doveadm/dsync/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-dsync.la: $(libdovecot_dsync_la_OBJECTS) $(libdovecot_dsync_la_DEPENDENCIES) $(EXTRA_libdovecot_dsync_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_dsync_la_LINK) -rpath $(pkglibdir) $(libdovecot_dsync_la_OBJECTS) $(libdovecot_dsync_la_LIBADD) $(LIBS)
+
+libdsync.la: $(libdsync_la_OBJECTS) $(libdsync_la_DEPENDENCIES) $(EXTRA_libdsync_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libdsync_la_OBJECTS) $(libdsync_la_LIBADD) $(LIBS)
+
+test-dsync-mailbox-tree-sync$(EXEEXT): $(test_dsync_mailbox_tree_sync_OBJECTS) $(test_dsync_mailbox_tree_sync_DEPENDENCIES) $(EXTRA_test_dsync_mailbox_tree_sync_DEPENDENCIES)
+ @rm -f test-dsync-mailbox-tree-sync$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dsync_mailbox_tree_sync_OBJECTS) $(test_dsync_mailbox_tree_sync_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox-tree-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox-tree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mails.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-deserializer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc-pipe.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc-stream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-export.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-import.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-state.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree-fill.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-serializer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-transaction-log-scan.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dsync-mailbox-tree-sync.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)/dsync-brain-mailbox-tree-sync.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mailbox.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mails.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain.Plo
+ -rm -f ./$(DEPDIR)/dsync-deserializer.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc-pipe.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc-stream.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc.Plo
+ -rm -f ./$(DEPDIR)/dsync-mail.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-export.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-import.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-state.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox.Plo
+ -rm -f ./$(DEPDIR)/dsync-serializer.Plo
+ -rm -f ./$(DEPDIR)/dsync-transaction-log-scan.Plo
+ -rm -f ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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)/dsync-brain-mailbox-tree-sync.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mailbox.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain-mails.Plo
+ -rm -f ./$(DEPDIR)/dsync-brain.Plo
+ -rm -f ./$(DEPDIR)/dsync-deserializer.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc-pipe.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc-stream.Plo
+ -rm -f ./$(DEPDIR)/dsync-ibc.Plo
+ -rm -f ./$(DEPDIR)/dsync-mail.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-export.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-import.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-state.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/dsync-mailbox.Plo
+ -rm -f ./$(DEPDIR)/dsync-serializer.Plo
+ -rm -f ./$(DEPDIR)/dsync-transaction-log-scan.Plo
+ -rm -f ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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/doveadm/dsync/dsync-brain-mailbox-tree-sync.c b/src/doveadm/dsync/dsync-brain-mailbox-tree-sync.c
new file mode 100644
index 0000000..2030e04
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain-mailbox-tree-sync.c
@@ -0,0 +1,229 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-brain-private.h"
+
+static int
+sync_create_box(struct dsync_brain *brain, struct mailbox *box,
+ const guid_128_t mailbox_guid, uint32_t uid_validity,
+ enum mail_error *error_r)
+{
+ struct mailbox_metadata metadata;
+ struct mailbox_update update;
+ enum mail_error error;
+ const char *errstr;
+ int ret;
+
+ i_zero(&update);
+ memcpy(update.mailbox_guid, mailbox_guid, sizeof(update.mailbox_guid));
+ update.uid_validity = uid_validity;
+
+ if (mailbox_create(box, &update, FALSE) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_EXISTS) {
+ i_error("Can't create mailbox %s: %s",
+ mailbox_get_vname(box), errstr);
+ *error_r = error;
+ return -1;
+ }
+ }
+ if (brain->no_mail_sync) {
+ /* trust that create worked, we can't actually open it
+ and verify. */
+ return 0;
+ }
+ /* sync the mailbox so we can look up its latest status */
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("Can't sync mailbox %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, error_r));
+ return -1;
+ }
+
+ /* verify that the GUID is what we wanted. if it's not, it probably
+ means that the mailbox had already been created. then we'll use the
+ GUID that is higher.
+
+ mismatching UIDVALIDITY is handled later, because we choose it by
+ checking which mailbox has more messages */
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ i_error("Can't get mailbox GUID %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, error_r));
+ return -1;
+ }
+
+ ret = memcmp(mailbox_guid, metadata.guid, sizeof(metadata.guid));
+
+ /* if THEIR guid is bigger than OUR guid, and we are not doing
+ backup in either direction, OR GUID did not match and we are
+ receiving backup, try change the mailbox GUID.
+ */
+
+ if ((ret > 0 && !brain->backup_recv &&
+ !brain->backup_send) || (ret != 0 && brain->backup_recv)) {
+ if (brain->debug) {
+ i_debug("brain %c: Changing mailbox %s GUID %s -> %s",
+ brain->master_brain ? 'M' : 'S',
+ mailbox_get_vname(box),
+ guid_128_to_string(metadata.guid),
+ guid_128_to_string(mailbox_guid));
+ }
+ i_zero(&update);
+ memcpy(update.mailbox_guid, mailbox_guid,
+ sizeof(update.mailbox_guid));
+ if (mailbox_update(box, &update) < 0) {
+ i_error("Can't update mailbox GUID %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, error_r));
+ return -1;
+ }
+ /* verify that the update worked */
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ i_error("Can't get mailbox GUID %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, error_r));
+ return -1;
+ }
+ if (memcmp(mailbox_guid, metadata.guid,
+ sizeof(metadata.guid)) != 0) {
+ i_error("Backend didn't update mailbox %s GUID",
+ mailbox_get_vname(box));
+ *error_r = MAIL_ERROR_TEMP;
+ return -1;
+ }
+ } else if (ret < 0) {
+ if (brain->debug) {
+ i_debug("brain %c: Other brain should change mailbox "
+ "%s GUID %s -> %s",
+ brain->master_brain ? 'M' : 'S',
+ mailbox_get_vname(box),
+ guid_128_to_string(mailbox_guid),
+ guid_128_to_string(metadata.guid));
+ }
+ }
+ return 0;
+}
+
+int dsync_brain_mailbox_tree_sync_change(struct dsync_brain *brain,
+ const struct dsync_mailbox_tree_sync_change *change,
+ enum mail_error *error_r)
+{
+ struct mailbox *box = NULL, *destbox;
+ const char *errstr, *func_name = NULL, *storage_name;
+ enum mail_error error;
+ int ret = -1;
+
+ if (brain->backup_send) {
+ i_assert(brain->no_backup_overwrite);
+ return 0;
+ }
+
+ switch (change->type) {
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX:
+ /* make sure we're deleting the correct mailbox */
+ ret = dsync_brain_mailbox_alloc(brain, change->mailbox_guid,
+ &box, &errstr, error_r);
+ if (ret < 0) {
+ i_error("Mailbox sync: Couldn't allocate mailbox %s GUID %s: %s",
+ change->full_name,
+ guid_128_to_string(change->mailbox_guid), errstr);
+ return -1;
+ }
+ if (ret == 0) {
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Mailbox %s GUID %s deletion conflict: %s",
+ change->full_name,
+ guid_128_to_string(change->mailbox_guid), errstr));
+ return 0;
+ }
+ break;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR:
+ storage_name = mailbox_list_get_storage_name(change->ns->list,
+ change->full_name);
+ if (mailbox_list_delete_dir(change->ns->list, storage_name) == 0)
+ return 0;
+
+ errstr = mailbox_list_get_last_internal_error(change->ns->list, &error);
+ if (error == MAIL_ERROR_NOTFOUND ||
+ error == MAIL_ERROR_EXISTS) {
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Mailbox %s mailbox_list_delete_dir conflict: %s",
+ change->full_name, errstr));
+ return 0;
+ } else {
+ i_error("Mailbox sync: mailbox_list_delete_dir failed: %s",
+ errstr);
+ *error_r = error;
+ return -1;
+ }
+ default:
+ box = mailbox_alloc(change->ns->list, change->full_name, 0);
+ break;
+ }
+ mailbox_skip_create_name_restrictions(box, TRUE);
+ switch (change->type) {
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX:
+ ret = sync_create_box(brain, box, change->mailbox_guid,
+ change->uid_validity, error_r);
+ mailbox_free(&box);
+ return ret;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR:
+ ret = mailbox_create(box, NULL, TRUE);
+ if (ret < 0 &&
+ mailbox_get_last_mail_error(box) == MAIL_ERROR_EXISTS) {
+ /* it doesn't matter if somebody else created this
+ directory or we automatically did while creating its
+ child mailbox. it's there now anyway and we don't
+ gain anything by treating this failure any
+ differently from success. */
+ ret = 0;
+ }
+ func_name = "mailbox_create";
+ break;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX:
+ ret = mailbox_delete(box);
+ func_name = "mailbox_delete";
+ break;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR:
+ i_unreached();
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME:
+ destbox = mailbox_alloc(change->ns->list,
+ change->rename_dest_name, 0);
+ mailbox_skip_create_name_restrictions(destbox, TRUE);
+ ret = mailbox_rename(box, destbox);
+ func_name = "mailbox_rename";
+ mailbox_free(&destbox);
+ break;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE:
+ ret = mailbox_set_subscribed(box, TRUE);
+ func_name = "mailbox_set_subscribed";
+ break;
+ case DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE:
+ ret = mailbox_set_subscribed(box, FALSE);
+ func_name = "mailbox_set_subscribed";
+ break;
+ }
+ if (ret < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_EXISTS ||
+ error == MAIL_ERROR_NOTFOUND) {
+ /* mailbox was already created or was already deleted.
+ let the next sync figure out what to do */
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Mailbox %s %s conflict: %s",
+ mailbox_get_vname(box), func_name, errstr));
+ ret = 0;
+ } else {
+ i_error("Mailbox %s sync: %s failed: %s",
+ mailbox_get_vname(box), func_name, errstr);
+ *error_r = error;
+ }
+ }
+ mailbox_free(&box);
+ return ret;
+}
diff --git a/src/doveadm/dsync/dsync-brain-mailbox-tree.c b/src/doveadm/dsync/dsync-brain-mailbox-tree.c
new file mode 100644
index 0000000..ddf2370
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain-mailbox-tree.c
@@ -0,0 +1,614 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-namespace.h"
+#include "mailbox-list-private.h"
+#include "dsync-ibc.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-brain-private.h"
+
+#include <ctype.h>
+
+static void dsync_brain_check_namespaces(struct dsync_brain *brain)
+{
+ struct mail_namespace *ns, *first_ns = NULL;
+ char sep, escape_char;
+
+ i_assert(brain->hierarchy_sep == '\0');
+ i_assert(brain->escape_char == '\0');
+
+ for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) {
+ if (!dsync_brain_want_namespace(brain, ns))
+ continue;
+
+ sep = mail_namespace_get_sep(ns);
+ escape_char = mailbox_list_get_settings(ns->list)->vname_escape_char;
+ if (first_ns == NULL) {
+ brain->hierarchy_sep = sep;
+ brain->escape_char = escape_char;
+ first_ns = ns;
+ } else if (brain->hierarchy_sep != sep) {
+ i_fatal("Synced namespaces have conflicting separators "
+ "('%c' for prefix=\"%s\", '%c' for prefix=\"%s\")",
+ brain->hierarchy_sep, first_ns->prefix,
+ sep, ns->prefix);
+ } else if (brain->escape_char != escape_char) {
+ i_fatal("Synced namespaces have conflicting escape chars "
+ "('%c' for prefix=\"%s\", '%c' for prefix=\"%s\")",
+ brain->escape_char, first_ns->prefix,
+ escape_char, ns->prefix);
+ }
+ }
+ if (brain->hierarchy_sep != '\0')
+ return;
+
+ i_fatal("All your namespaces have a location setting. "
+ "Only namespaces with empty location settings are converted. "
+ "(One namespace should default to mail_location setting)");
+}
+
+void dsync_brain_mailbox_trees_init(struct dsync_brain *brain)
+{
+ struct mail_namespace *ns;
+
+ dsync_brain_check_namespaces(brain);
+
+ brain->local_mailbox_tree =
+ dsync_mailbox_tree_init(brain->hierarchy_sep,
+ brain->escape_char, brain->alt_char);
+ /* we'll convert remote mailbox names to use our own separator */
+ brain->remote_mailbox_tree =
+ dsync_mailbox_tree_init(brain->hierarchy_sep,
+ brain->escape_char, brain->alt_char);
+
+ /* fill the local mailbox tree */
+ for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) {
+ if (!dsync_brain_want_namespace(brain, ns))
+ continue;
+ if (brain->debug)
+ i_debug("brain %c: Namespace %s has location %s",
+ brain->master_brain ? 'M' : 'S',
+ ns->prefix, ns->set->location);
+ if (dsync_mailbox_tree_fill(brain->local_mailbox_tree, ns,
+ brain->sync_box,
+ brain->sync_box_guid,
+ brain->exclude_mailboxes,
+ brain->alt_char,
+ &brain->mail_error) < 0) {
+ brain->failed = TRUE;
+ break;
+ }
+ }
+
+ brain->local_tree_iter =
+ dsync_mailbox_tree_iter_init(brain->local_mailbox_tree);
+}
+
+void dsync_brain_send_mailbox_tree(struct dsync_brain *brain)
+{
+ struct dsync_mailbox_node *node;
+ enum dsync_ibc_send_ret ret;
+ const char *full_name;
+
+ while (dsync_mailbox_tree_iter_next(brain->local_tree_iter,
+ &full_name, &node)) {
+ if (node->ns == NULL) {
+ /* This node was created when adding a namespace prefix
+ to the tree that has multiple hierarchical names,
+ but the parent names don't belong to any synced
+ namespace. For example when syncing "-n Shared/user/"
+ so "Shared/" is skipped. Or if there is e.g.
+ "Public/files/" namespace prefix, but no "Public/"
+ namespace at all. */
+ continue;
+ }
+
+ T_BEGIN {
+ const char *const *parts;
+
+ if (brain->debug) {
+ i_debug("brain %c: Local mailbox tree: %s %s",
+ brain->master_brain ? 'M' : 'S', full_name,
+ dsync_mailbox_node_to_string(node));
+ }
+
+ parts = dsync_mailbox_name_to_parts(full_name, brain->hierarchy_sep,
+ brain->escape_char);
+ ret = dsync_ibc_send_mailbox_tree_node(brain->ibc,
+ parts, node);
+ } T_END;
+ if (ret == DSYNC_IBC_SEND_RET_FULL)
+ return;
+ }
+ dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter);
+ dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX_TREE);
+
+ brain->state = DSYNC_STATE_SEND_MAILBOX_TREE_DELETES;
+}
+
+void dsync_brain_send_mailbox_tree_deletes(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox_delete *deletes;
+ unsigned int count;
+
+ deletes = dsync_mailbox_tree_get_deletes(brain->local_mailbox_tree,
+ &count);
+ dsync_ibc_send_mailbox_deletes(brain->ibc, deletes, count,
+ brain->hierarchy_sep,
+ brain->escape_char);
+
+ brain->state = DSYNC_STATE_RECV_MAILBOX_TREE;
+}
+
+static bool
+dsync_namespace_match_parts(struct mail_namespace *ns,
+ const char *const *name_parts)
+{
+ const char *part, *prefix = ns->prefix;
+ size_t part_len;
+ char ns_sep = mail_namespace_get_sep(ns);
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ strcmp(name_parts[0], "INBOX") == 0 && name_parts[1] == NULL)
+ return TRUE;
+
+ for (; *name_parts != NULL && *prefix != '\0'; name_parts++) {
+ part = *name_parts;
+ part_len = strlen(part);
+
+ if (!str_begins(prefix, part))
+ return FALSE;
+ if (prefix[part_len] != ns_sep)
+ return FALSE;
+ prefix += part_len + 1;
+ }
+ if (*name_parts != NULL) {
+ /* namespace prefix found with a mailbox */
+ return TRUE;
+ }
+ if (*prefix == '\0') {
+ /* namespace prefix itself matched */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct mail_namespace *
+dsync_find_namespace(struct dsync_brain *brain, const char *const *name_parts)
+{
+ struct mail_namespace *ns, *best_ns = NULL;
+
+ for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) {
+ if (!dsync_brain_want_namespace(brain, ns))
+ continue;
+
+ if (ns->prefix_len == 0) {
+ /* prefix="" is the fallback namespace */
+ if (best_ns == NULL)
+ best_ns = ns;
+ } else if (dsync_namespace_match_parts(ns, name_parts)) {
+ if (best_ns == NULL ||
+ best_ns->prefix_len < ns->prefix_len)
+ best_ns = ns;
+ }
+ }
+ return best_ns;
+}
+
+static bool
+dsync_is_valid_name(struct mail_namespace *ns, const char *vname)
+{
+ struct mailbox *box;
+ bool ret;
+
+ box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY);
+ ret = mailbox_verify_create_name(box) == 0;
+ mailbox_free(&box);
+ return ret;
+}
+
+static bool
+dsync_is_valid_name_until(struct mail_namespace *ns, string_t *vname_full,
+ unsigned int end_pos)
+{
+ const char *vname;
+ if (end_pos == str_len(vname_full))
+ vname = str_c(vname_full);
+ else
+ vname = t_strndup(str_c(vname_full), end_pos);
+ return dsync_is_valid_name(ns, vname);
+}
+
+static bool
+dsync_fix_mailbox_name_until(struct mail_namespace *ns, string_t *vname_full,
+ char alt_char, unsigned int start_pos,
+ unsigned int *_end_pos)
+{
+ unsigned int end_pos = *_end_pos;
+ unsigned int i;
+
+ if (dsync_is_valid_name_until(ns, vname_full, end_pos))
+ return TRUE;
+
+ /* 1) change any real separators to alt separators (this
+ wouldn't be necessary with listescape, but don't bother
+ detecting it) */
+ char list_sep = mailbox_list_get_hierarchy_sep(ns->list);
+ char ns_sep = mail_namespace_get_sep(ns);
+ if (list_sep != ns_sep) {
+ char *v = str_c_modifiable(vname_full);
+ for (i = start_pos; i < end_pos; i++) {
+ if (v[i] == list_sep)
+ v[i] = alt_char;
+ }
+ if (dsync_is_valid_name_until(ns, vname_full, end_pos))
+ return TRUE;
+ }
+
+ /* 2) '/' characters aren't valid without listescape */
+ if (ns_sep != '/' && list_sep != '/') {
+ char *v = str_c_modifiable(vname_full);
+ for (i = start_pos; i < end_pos; i++) {
+ if (v[i] == '/')
+ v[i] = alt_char;
+ }
+ if (dsync_is_valid_name_until(ns, vname_full, end_pos))
+ return TRUE;
+ }
+
+ /* 3) probably some reserved name (e.g. dbox-Mails or ..) */
+ str_insert(vname_full, start_pos, "_"); end_pos++; *_end_pos += 1;
+ if (dsync_is_valid_name_until(ns, vname_full, end_pos))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+dsync_fix_mailbox_name(struct mail_namespace *ns, string_t *vname_str,
+ char alt_char)
+{
+ const char *old_vname;
+ char *vname;
+ char ns_sep = mail_namespace_get_sep(ns);
+ guid_128_t guid;
+ unsigned int i, start_pos;
+
+ vname = str_c_modifiable(vname_str);
+ if (strncmp(vname, ns->prefix, ns->prefix_len) == 0)
+ start_pos = ns->prefix_len;
+ else
+ start_pos = 0;
+
+ /* replace control chars */
+ for (i = start_pos; vname[i] != '\0'; i++) {
+ if ((unsigned char)vname[i] < ' ')
+ vname[i] = alt_char;
+ }
+ /* make it valid UTF8 */
+ if (!uni_utf8_str_is_valid(vname)) {
+ old_vname = t_strdup(vname + start_pos);
+ str_truncate(vname_str, start_pos);
+ if (uni_utf8_get_valid_data((const void *)old_vname,
+ strlen(old_vname), vname_str))
+ i_unreached();
+ vname = str_c_modifiable(vname_str);
+ }
+ if (dsync_is_valid_name(ns, vname))
+ return;
+
+ /* Check/fix each hierarchical name separately */
+ const char *p;
+ do {
+ i_assert(start_pos <= str_len(vname_str));
+ p = strchr(str_c(vname_str) + start_pos, ns_sep);
+ unsigned int end_pos;
+ if (p == NULL)
+ end_pos = str_len(vname_str);
+ else
+ end_pos = p - str_c(vname_str);
+
+ if (!dsync_fix_mailbox_name_until(ns, vname_str, alt_char,
+ start_pos, &end_pos)) {
+ /* Couldn't fix it. Name is too long? Just give up and
+ generate a unique name. */
+ guid_128_generate(guid);
+ str_truncate(vname_str, 0);
+ str_append(vname_str, ns->prefix);
+ str_append(vname_str, guid_128_to_string(guid));
+ i_assert(dsync_is_valid_name(ns, str_c(vname_str)));
+ break;
+ }
+ start_pos = end_pos + 1;
+ } while (p != NULL);
+}
+
+static int
+dsync_get_mailbox_name(struct dsync_brain *brain, const char *const *name_parts,
+ const char **name_r, struct mail_namespace **ns_r)
+{
+ struct mail_namespace *ns;
+ const char *p;
+ string_t *vname;
+ char ns_sep;
+
+ i_assert(*name_parts != NULL);
+
+ ns = dsync_find_namespace(brain, name_parts);
+ if (ns == NULL)
+ return -1;
+ ns_sep = mail_namespace_get_sep(ns);
+
+ /* build the mailbox name */
+ char escape_chars[] = {
+ brain->escape_char,
+ ns_sep,
+ '\0'
+ };
+ struct dsync_mailbox_list *dlist = DSYNC_LIST_CONTEXT(ns->list);
+ if (dlist != NULL && !dlist->have_orig_escape_char) {
+ /* The escape character was added only for dsync internally.
+ Normally there is no escape character configured. Change
+ the mailbox names so that it doesn't rely on it. */
+ escape_chars[0] = '\0';
+ }
+ vname = t_str_new(128);
+ for (; *name_parts != NULL; name_parts++) {
+ if (escape_chars[0] != '\0') {
+ mailbox_list_name_escape(*name_parts, escape_chars,
+ vname);
+ } else {
+ for (p = *name_parts; *p != '\0'; p++) {
+ if (*p != ns_sep)
+ str_append_c(vname, *p);
+ else
+ str_append_c(vname, brain->alt_char);
+ }
+ }
+ str_append_c(vname, ns_sep);
+ }
+ str_truncate(vname, str_len(vname)-1);
+
+ dsync_fix_mailbox_name(ns, vname, brain->alt_char);
+ *name_r = str_c(vname);
+ *ns_r = ns;
+ return 0;
+}
+
+static void dsync_brain_mailbox_trees_sync(struct dsync_brain *brain)
+{
+ struct dsync_mailbox_tree_sync_ctx *ctx;
+ const struct dsync_mailbox_tree_sync_change *change;
+ enum dsync_mailbox_trees_sync_type sync_type;
+ enum dsync_mailbox_trees_sync_flags sync_flags =
+ (brain->debug ? DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG : 0) |
+ (brain->master_brain ? DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN : 0);
+ int ret;
+
+ if (brain->no_backup_overwrite)
+ sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY;
+ else if (brain->backup_send)
+ sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL;
+ else if (brain->backup_recv)
+ sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE;
+ else
+ sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY;
+
+ ctx = dsync_mailbox_trees_sync_init(brain->local_mailbox_tree,
+ brain->remote_mailbox_tree,
+ sync_type, sync_flags);
+ while ((change = dsync_mailbox_trees_sync_next(ctx)) != NULL) {
+ T_BEGIN {
+ ret = dsync_brain_mailbox_tree_sync_change(
+ brain, change, &brain->mail_error);
+ } T_END;
+ if (ret < 0) {
+ brain->failed = TRUE;
+ break;
+ }
+ }
+ if (dsync_mailbox_trees_sync_deinit(&ctx) < 0)
+ brain->failed = TRUE;
+}
+
+static int
+dsync_brain_recv_mailbox_tree_add(struct dsync_brain *brain,
+ const char *const *parts,
+ const struct dsync_mailbox_node *remote_node,
+ const char *sep)
+{
+ struct dsync_mailbox_node *node;
+ struct mail_namespace *ns;
+ const char *name;
+
+ if (dsync_get_mailbox_name(brain, parts, &name, &ns) < 0)
+ return -1;
+ if (brain->debug) {
+ i_debug("brain %c: Remote mailbox tree: %s %s",
+ brain->master_brain ? 'M' : 'S',
+ t_strarray_join(parts, sep),
+ dsync_mailbox_node_to_string(remote_node));
+ }
+ node = dsync_mailbox_tree_get(brain->remote_mailbox_tree, name);
+ node->ns = ns;
+ dsync_mailbox_node_copy_data(node, remote_node);
+ return 0;
+}
+
+bool dsync_brain_recv_mailbox_tree(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox_node *remote_node;
+ struct dsync_mailbox_node *dup_node1, *dup_node2;
+ const char *const *parts;
+ enum dsync_ibc_recv_ret ret;
+ int ret2;
+ char sep[2];
+ bool changed = FALSE;
+
+ sep[0] = brain->hierarchy_sep; sep[1] = '\0';
+ while ((ret = dsync_ibc_recv_mailbox_tree_node(brain->ibc, &parts,
+ &remote_node)) > 0) {
+ T_BEGIN {
+ ret2 = dsync_brain_recv_mailbox_tree_add(
+ brain, parts, remote_node, sep);
+ } T_END;
+ if (ret2 < 0) {
+ i_error("Couldn't find namespace for mailbox %s",
+ t_strarray_join(parts, sep));
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ }
+ if (ret != DSYNC_IBC_RECV_RET_FINISHED)
+ return changed;
+
+ if (dsync_mailbox_tree_build_guid_hash(brain->remote_mailbox_tree,
+ &dup_node1, &dup_node2) < 0) {
+ i_error("Remote sent duplicate mailbox GUID %s for mailboxes %s and %s",
+ guid_128_to_string(dup_node1->mailbox_guid),
+ dsync_mailbox_node_get_full_name(brain->remote_mailbox_tree,
+ dup_node1),
+ dsync_mailbox_node_get_full_name(brain->remote_mailbox_tree,
+ dup_node2));
+ brain->failed = TRUE;
+ }
+
+ brain->state = DSYNC_STATE_RECV_MAILBOX_TREE_DELETES;
+ return TRUE;
+}
+
+static void
+dsync_brain_mailbox_tree_add_delete(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_tree *other_tree,
+ const struct dsync_mailbox_delete *other_del,
+ const struct dsync_mailbox_node **node_r,
+ const char **status_r)
+{
+ const struct dsync_mailbox_node *node;
+ struct dsync_mailbox_node *other_node, *old_node;
+ const char *name;
+
+ /* see if we can find the deletion based on mailbox tree that should
+ still have the mailbox */
+ node = *node_r = dsync_mailbox_tree_find_delete(tree, other_del);
+ if (node == NULL) {
+ *status_r = "not found";
+ return;
+ }
+
+ switch (other_del->type) {
+ case DSYNC_MAILBOX_DELETE_TYPE_MAILBOX:
+ /* mailbox is always deleted */
+ break;
+ case DSYNC_MAILBOX_DELETE_TYPE_DIR:
+ if (other_del->timestamp <= node->last_renamed_or_created) {
+ /* we don't want to delete this directory, we already
+ have a newer timestamp for it */
+ *status_r = "keep directory, we have a newer timestamp";
+ return;
+ }
+ break;
+ case DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE:
+ if (other_del->timestamp <= node->last_subscription_change) {
+ /* we don't want to unsubscribe, since we already have
+ a newer subscription timestamp */
+ *status_r = "keep subscription, we have a newer timestamp";
+ return;
+ }
+ break;
+ }
+
+ /* make a node for it in the other mailbox tree */
+ name = dsync_mailbox_node_get_full_name(tree, node);
+ other_node = dsync_mailbox_tree_get(other_tree, name);
+
+ if (other_node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ (!guid_128_is_empty(other_node->mailbox_guid) ||
+ other_del->type != DSYNC_MAILBOX_DELETE_TYPE_MAILBOX)) {
+ /* other side has already created a new mailbox or
+ directory with this name, we can't delete it */
+ *status_r = "name has already been recreated";
+ return;
+ }
+
+ /* ok, mark the other node deleted */
+ if (other_del->type == DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) {
+ memcpy(other_node->mailbox_guid, node->mailbox_guid,
+ sizeof(other_node->mailbox_guid));
+ }
+ if (other_node->ns != node->ns && other_node->ns != NULL) {
+ /* namespace mismatch for this node. this shouldn't happen
+ normally, but especially during some misconfigurations it's
+ possible that one side has created mailboxes that conflict
+ with another namespace's prefix. since we're here because
+ one of the mailboxes was deleted, we'll just ignore this. */
+ *status_r = "namespace mismatch";
+ return;
+ }
+ other_node->ns = node->ns;
+ if (other_del->type != DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE) {
+ other_node->existence = DSYNC_MAILBOX_NODE_DELETED;
+ *status_r = "marked as deleted";
+ } else {
+ other_node->last_subscription_change = other_del->timestamp;
+ other_node->subscribed = FALSE;
+ *status_r = "marked as unsubscribed";
+ }
+
+ if (dsync_mailbox_tree_guid_hash_add(other_tree, other_node,
+ &old_node) < 0)
+ i_unreached();
+}
+
+bool dsync_brain_recv_mailbox_tree_deletes(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox_node *node;
+ const char *status;
+ const struct dsync_mailbox_delete *deletes;
+ unsigned int i, count;
+ char sep, escape_char;
+
+ if (dsync_ibc_recv_mailbox_deletes(brain->ibc, &deletes, &count,
+ &sep, &escape_char) == 0)
+ return FALSE;
+
+ /* apply remote's mailbox deletions based on our local tree */
+ dsync_mailbox_tree_set_remote_chars(brain->local_mailbox_tree, sep,
+ escape_char);
+ for (i = 0; i < count; i++) {
+ dsync_brain_mailbox_tree_add_delete(brain->local_mailbox_tree,
+ brain->remote_mailbox_tree,
+ &deletes[i], &node, &status);
+ if (brain->debug) {
+ const char *node_name = node == NULL ? "" :
+ dsync_mailbox_node_get_full_name(brain->local_mailbox_tree, node);
+ i_debug("brain %c: Remote mailbox tree deletion: guid=%s type=%s timestamp=%ld name=%s local update=%s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(deletes[i].guid),
+ dsync_mailbox_delete_type_to_string(deletes[i].type),
+ deletes[i].timestamp, node_name, status);
+ }
+ }
+
+ /* apply local mailbox deletions based on remote tree */
+ deletes = dsync_mailbox_tree_get_deletes(brain->local_mailbox_tree,
+ &count);
+ dsync_mailbox_tree_set_remote_chars(brain->remote_mailbox_tree,
+ brain->hierarchy_sep,
+ brain->escape_char);
+ for (i = 0; i < count; i++) {
+ dsync_brain_mailbox_tree_add_delete(brain->remote_mailbox_tree,
+ brain->local_mailbox_tree,
+ &deletes[i], &node, &status);
+ }
+
+ dsync_brain_mailbox_trees_sync(brain);
+ brain->state = brain->master_brain ?
+ DSYNC_STATE_MASTER_SEND_MAILBOX :
+ DSYNC_STATE_SLAVE_RECV_MAILBOX;
+ i_assert(brain->local_tree_iter == NULL);
+ brain->local_tree_iter =
+ dsync_mailbox_tree_iter_init(brain->local_mailbox_tree);
+ return TRUE;
+}
diff --git a/src/doveadm/dsync/dsync-brain-mailbox.c b/src/doveadm/dsync/dsync-brain-mailbox.c
new file mode 100644
index 0000000..7966786
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain-mailbox.c
@@ -0,0 +1,926 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "mail-cache-private.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "dsync-ibc.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-mailbox-import.h"
+#include "dsync-mailbox-export.h"
+#include "dsync-transaction-log-scan.h"
+#include "dsync-brain-private.h"
+
+static int
+ns_mailbox_try_alloc(struct dsync_brain *brain, struct mail_namespace *ns,
+ const guid_128_t guid, struct mailbox **box_r,
+ const char **errstr_r, enum mail_error *error_r)
+{
+ enum mailbox_flags flags = 0;
+ struct mailbox *box;
+ enum mailbox_existence existence;
+ int ret;
+
+ if (brain->backup_send) {
+ /* make sure mailbox isn't modified */
+ flags |= MAILBOX_FLAG_READONLY;
+ }
+
+ box = mailbox_alloc_guid(ns->list, guid, flags);
+ ret = mailbox_exists(box, FALSE, &existence);
+ if (ret < 0) {
+ *errstr_r = mailbox_get_last_internal_error(box, error_r);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (existence != MAILBOX_EXISTENCE_SELECT) {
+ mailbox_free(&box);
+ *errstr_r = existence == MAILBOX_EXISTENCE_NONE ?
+ "Mailbox was already deleted" :
+ "Mailbox is no longer selectable";
+ return 0;
+ }
+ *box_r = box;
+ return 1;
+}
+
+int dsync_brain_mailbox_alloc(struct dsync_brain *brain, const guid_128_t guid,
+ struct mailbox **box_r, const char **errstr_r,
+ enum mail_error *error_r)
+{
+ struct mail_namespace *ns;
+ int ret;
+
+ *box_r = NULL;
+
+ for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) {
+ if (!dsync_brain_want_namespace(brain, ns))
+ continue;
+ if ((ret = ns_mailbox_try_alloc(brain, ns, guid, box_r,
+ errstr_r, error_r)) != 0)
+ return ret;
+ }
+ return 0;
+}
+
+static void
+dsync_mailbox_cache_field_dup(ARRAY_TYPE(mailbox_cache_field) *dest,
+ const ARRAY_TYPE(mailbox_cache_field) *src,
+ pool_t pool)
+{
+ const struct mailbox_cache_field *src_field;
+ struct mailbox_cache_field *dest_field;
+
+ p_array_init(dest, pool, array_count(src));
+ array_foreach(src, src_field) {
+ dest_field = array_append_space(dest);
+ dest_field->name = p_strdup(pool, src_field->name);
+ dest_field->decision = src_field->decision;
+ dest_field->last_used = src_field->last_used;
+ }
+}
+
+
+static const struct dsync_mailbox_state *
+dsync_mailbox_state_find(struct dsync_brain *brain,
+ const guid_128_t mailbox_guid)
+{
+ const uint8_t *guid_p;
+
+ guid_p = mailbox_guid;
+ return hash_table_lookup(brain->mailbox_states, guid_p);
+}
+
+static void
+dsync_mailbox_state_remove(struct dsync_brain *brain,
+ const guid_128_t mailbox_guid)
+{
+ const uint8_t *guid_p;
+
+ guid_p = mailbox_guid;
+ if (hash_table_lookup(brain->mailbox_states, guid_p) != NULL)
+ hash_table_remove(brain->mailbox_states, guid_p);
+}
+
+void dsync_brain_sync_init_box_states(struct dsync_brain *brain)
+{
+ if (brain->backup_send) {
+ /* we have an exporter, but no importer. */
+ brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES;
+ brain->box_recv_state = brain->mail_requests ?
+ DSYNC_BOX_STATE_MAIL_REQUESTS :
+ DSYNC_BOX_STATE_RECV_LAST_COMMON;
+ } else if (brain->backup_recv) {
+ /* we have an importer, but no exporter */
+ brain->box_send_state = brain->mail_requests ?
+ DSYNC_BOX_STATE_MAIL_REQUESTS :
+ DSYNC_BOX_STATE_DONE;
+ brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES;
+ } else {
+ brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES;
+ brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES;
+ }
+}
+
+static void
+dsync_brain_sync_mailbox_init(struct dsync_brain *brain,
+ struct mailbox *box,
+ struct file_lock *lock,
+ const struct dsync_mailbox *local_dsync_box,
+ bool wait_for_remote_box)
+{
+ const struct dsync_mailbox_state *state;
+
+ i_assert(brain->box_importer == NULL);
+ i_assert(brain->box_exporter == NULL);
+ i_assert(box->synced);
+
+ brain->box = box;
+ brain->box_lock = lock;
+ brain->pre_box_state = brain->state;
+ if (wait_for_remote_box) {
+ brain->box_send_state = DSYNC_BOX_STATE_MAILBOX;
+ brain->box_recv_state = DSYNC_BOX_STATE_MAILBOX;
+ } else {
+ dsync_brain_sync_init_box_states(brain);
+ }
+ brain->local_dsync_box = *local_dsync_box;
+ if (brain->dsync_box_pool != NULL)
+ p_clear(brain->dsync_box_pool);
+ else {
+ brain->dsync_box_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"dsync brain box pool", 2048);
+ }
+ dsync_mailbox_cache_field_dup(&brain->local_dsync_box.cache_fields,
+ &local_dsync_box->cache_fields,
+ brain->dsync_box_pool);
+ i_zero(&brain->remote_dsync_box);
+
+ state = dsync_mailbox_state_find(brain, local_dsync_box->mailbox_guid);
+ if (state != NULL)
+ brain->mailbox_state = *state;
+ else {
+ i_zero(&brain->mailbox_state);
+ memcpy(brain->mailbox_state.mailbox_guid,
+ local_dsync_box->mailbox_guid,
+ sizeof(brain->mailbox_state.mailbox_guid));
+ brain->mailbox_state.last_uidvalidity =
+ local_dsync_box->uid_validity;
+ }
+}
+
+static void
+dsync_brain_sync_mailbox_init_remote(struct dsync_brain *brain,
+ const struct dsync_mailbox *remote_dsync_box)
+{
+ enum dsync_mailbox_import_flags import_flags = 0;
+ const struct dsync_mailbox_state *state;
+ uint32_t last_common_uid;
+ uint64_t last_common_modseq, last_common_pvt_modseq;
+
+ i_assert(brain->box_importer == NULL);
+ i_assert(brain->log_scan != NULL);
+
+ i_assert(memcmp(brain->local_dsync_box.mailbox_guid,
+ remote_dsync_box->mailbox_guid,
+ sizeof(remote_dsync_box->mailbox_guid)) == 0);
+
+ brain->remote_dsync_box = *remote_dsync_box;
+ dsync_mailbox_cache_field_dup(&brain->remote_dsync_box.cache_fields,
+ &remote_dsync_box->cache_fields,
+ brain->dsync_box_pool);
+
+ state = dsync_mailbox_state_find(brain, remote_dsync_box->mailbox_guid);
+ if (state != NULL) {
+ last_common_uid = state->last_common_uid;
+ last_common_modseq = state->last_common_modseq;
+ last_common_pvt_modseq = state->last_common_pvt_modseq;
+ } else {
+ last_common_uid = 0;
+ last_common_modseq = 0;
+ last_common_pvt_modseq = 0;
+ }
+
+ if (brain->mail_requests)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS;
+ if (brain->master_brain)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN;
+ if (brain->backup_recv && !brain->no_backup_overwrite)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES;
+ if (brain->debug)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_DEBUG;
+ if (brain->local_dsync_box.have_save_guids &&
+ (remote_dsync_box->have_save_guids ||
+ (brain->backup_recv && remote_dsync_box->have_guids)))
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS;
+ if (brain->local_dsync_box.have_only_guid128 ||
+ remote_dsync_box->have_only_guid128)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128;
+ if (brain->no_notify)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY;
+ if (brain->empty_hdr_workaround)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND;
+ if (brain->no_header_hashes)
+ import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_NO_HEADER_HASHES;
+
+ brain->box_importer = brain->backup_send ? NULL :
+ dsync_mailbox_import_init(brain->box, brain->virtual_all_box,
+ brain->log_scan,
+ last_common_uid, last_common_modseq,
+ last_common_pvt_modseq,
+ remote_dsync_box->uid_next,
+ remote_dsync_box->first_recent_uid,
+ remote_dsync_box->highest_modseq,
+ remote_dsync_box->highest_pvt_modseq,
+ brain->sync_since_timestamp,
+ brain->sync_until_timestamp,
+ brain->sync_max_size,
+ brain->sync_flag,
+ brain->import_commit_msgs_interval,
+ import_flags, brain->hdr_hash_version,
+ brain->hashed_headers);
+}
+
+int dsync_brain_sync_mailbox_open(struct dsync_brain *brain,
+ const struct dsync_mailbox *remote_dsync_box)
+{
+ struct mailbox_status status;
+ enum dsync_mailbox_exporter_flags exporter_flags = 0;
+ uint32_t last_common_uid, highest_wanted_uid;
+ uint64_t last_common_modseq, last_common_pvt_modseq;
+ const char *desync_reason = "";
+ bool pvt_too_old;
+ int ret;
+
+ i_assert(brain->log_scan == NULL);
+ i_assert(brain->box_exporter == NULL);
+
+ last_common_uid = brain->mailbox_state.last_common_uid;
+ last_common_modseq = brain->mailbox_state.last_common_modseq;
+ last_common_pvt_modseq = brain->mailbox_state.last_common_pvt_modseq;
+ highest_wanted_uid = last_common_uid == 0 ?
+ (uint32_t)-1 : last_common_uid;
+ ret = dsync_transaction_log_scan_init(brain->box->view,
+ brain->box->view_pvt,
+ highest_wanted_uid,
+ last_common_modseq,
+ last_common_pvt_modseq,
+ &brain->log_scan, &pvt_too_old);
+ if (ret < 0) {
+ i_error("Failed to read transaction log for mailbox %s",
+ mailbox_get_vname(brain->box));
+ brain->failed = TRUE;
+ return -1;
+ }
+
+ mailbox_get_open_status(brain->box, STATUS_UIDNEXT |
+ STATUS_HIGHESTMODSEQ |
+ STATUS_HIGHESTPVTMODSEQ, &status);
+ if (status.nonpermanent_modseqs)
+ status.highest_modseq = 0;
+ if (ret == 0) {
+ if (pvt_too_old) {
+ desync_reason = t_strdup_printf(
+ "Private modseq %"PRIu64" no longer in transaction log "
+ "(highest=%"PRIu64", last_common_uid=%u, nextuid=%u)",
+ last_common_pvt_modseq,
+ status.highest_pvt_modseq, last_common_uid,
+ status.uidnext);
+ } else {
+ desync_reason = t_strdup_printf(
+ "Modseq %"PRIu64" no longer in transaction log "
+ "(highest=%"PRIu64", last_common_uid=%u, nextuid=%u)",
+ last_common_modseq,
+ status.highest_modseq, last_common_uid,
+ status.uidnext);
+ }
+ }
+
+ if (last_common_uid != 0) {
+ /* if last_common_* is higher than our current ones it means
+ that the incremental sync state is stale and we need to do
+ a full resync */
+ if (status.uidnext < last_common_uid) {
+ desync_reason = t_strdup_printf("uidnext %u < %u",
+ status.uidnext, last_common_uid);
+ ret = 0;
+ } else if (status.highest_modseq < last_common_modseq) {
+ desync_reason = t_strdup_printf("highest_modseq %"PRIu64" < %"PRIu64,
+ status.highest_modseq, last_common_modseq);
+ ret = 0;
+ } else if (status.highest_pvt_modseq < last_common_pvt_modseq) {
+ desync_reason = t_strdup_printf("highest_pvt_modseq %"PRIu64" < %"PRIu64,
+ status.highest_pvt_modseq, last_common_pvt_modseq);
+ ret = 0;
+ }
+ }
+ if (ret == 0) {
+ i_warning("Failed to do incremental sync for mailbox %s, "
+ "retry with a full sync (%s)",
+ mailbox_get_vname(brain->box), desync_reason);
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Incremental sync failed: %s", desync_reason));
+ brain->require_full_resync = TRUE;
+ return 0;
+ }
+
+ if (!brain->mail_requests)
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS;
+ if (remote_dsync_box->have_save_guids &&
+ (brain->local_dsync_box.have_save_guids ||
+ (brain->backup_send && brain->local_dsync_box.have_guids)))
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS;
+ if (brain->no_mail_prefetch)
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL;
+ if (brain->sync_since_timestamp > 0 ||
+ brain->sync_until_timestamp > 0)
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS;
+ if (brain->sync_max_size > 0)
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES;
+ if (remote_dsync_box->messages_count == 0 ||
+ brain->no_header_hashes) {
+ /* remote mailbox is empty - we don't really need to export
+ header hashes since they're not going to match anything
+ anyway. */
+ exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES;
+ }
+
+ brain->box_exporter = brain->backup_recv ? NULL :
+ dsync_mailbox_export_init(brain->box, brain->log_scan,
+ last_common_uid,
+ exporter_flags,
+ brain->hdr_hash_version,
+ brain->hashed_headers);
+ dsync_brain_sync_mailbox_init_remote(brain, remote_dsync_box);
+ return 1;
+}
+
+void dsync_brain_sync_mailbox_deinit(struct dsync_brain *brain)
+{
+ enum mail_error error;
+
+ i_assert(brain->box != NULL);
+
+ array_push_back(&brain->remote_mailbox_states, &brain->mailbox_state);
+ if (brain->box_exporter != NULL) {
+ const char *errstr;
+
+ i_assert(brain->failed || brain->require_full_resync ||
+ brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_CHANGED);
+ if (dsync_mailbox_export_deinit(&brain->box_exporter,
+ &errstr, &error) < 0)
+ i_error("Mailbox export failed: %s", errstr);
+ }
+ if (brain->box_importer != NULL) {
+ uint32_t last_common_uid, last_messages_count;
+ uint64_t last_common_modseq, last_common_pvt_modseq;
+ const char *changes_during_sync;
+ bool require_full_resync;
+
+ i_assert(brain->failed);
+ (void)dsync_mailbox_import_deinit(&brain->box_importer,
+ FALSE,
+ &last_common_uid,
+ &last_common_modseq,
+ &last_common_pvt_modseq,
+ &last_messages_count,
+ &changes_during_sync,
+ &require_full_resync,
+ &brain->mail_error);
+ if (require_full_resync)
+ brain->require_full_resync = TRUE;
+ }
+ if (brain->log_scan != NULL)
+ dsync_transaction_log_scan_deinit(&brain->log_scan);
+ file_lock_free(&brain->box_lock);
+ mailbox_free(&brain->box);
+
+ brain->state = brain->pre_box_state;
+}
+
+static int dsync_box_get(struct mailbox *box, struct dsync_mailbox *dsync_box_r,
+ enum mail_error *error_r)
+{
+ const enum mailbox_status_items status_items =
+ STATUS_UIDVALIDITY | STATUS_UIDNEXT | STATUS_MESSAGES |
+ STATUS_FIRST_RECENT_UID | STATUS_HIGHESTMODSEQ |
+ STATUS_HIGHESTPVTMODSEQ;
+ const enum mailbox_metadata_items metadata_items =
+ MAILBOX_METADATA_CACHE_FIELDS | MAILBOX_METADATA_GUID;
+ struct mailbox_status status;
+ struct mailbox_metadata metadata;
+ const char *errstr;
+ enum mail_error error;
+
+ /* get metadata first, since it may autocreate the mailbox */
+ if (mailbox_get_metadata(box, metadata_items, &metadata) < 0 ||
+ mailbox_get_status(box, status_items, &status) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_NOTFOUND ||
+ error == MAIL_ERROR_NOTPOSSIBLE) {
+ /* Mailbox isn't selectable, try the next one. We
+ should have already caught \Noselect mailboxes, but
+ check them anyway here. The NOTPOSSIBLE check is
+ mainly for invalid mbox files. */
+ return 0;
+ }
+ i_error("Failed to access mailbox %s: %s",
+ mailbox_get_vname(box), errstr);
+ *error_r = error;
+ return -1;
+ }
+ if (status.nonpermanent_modseqs)
+ status.highest_modseq = 0;
+
+ i_assert(status.uidvalidity != 0 || status.messages == 0);
+
+ i_zero(dsync_box_r);
+ memcpy(dsync_box_r->mailbox_guid, metadata.guid,
+ sizeof(dsync_box_r->mailbox_guid));
+ dsync_box_r->uid_validity = status.uidvalidity;
+ dsync_box_r->uid_next = status.uidnext;
+ dsync_box_r->messages_count = status.messages;
+ dsync_box_r->first_recent_uid = status.first_recent_uid;
+ dsync_box_r->highest_modseq = status.highest_modseq;
+ dsync_box_r->highest_pvt_modseq = status.highest_pvt_modseq;
+ dsync_mailbox_cache_field_dup(&dsync_box_r->cache_fields,
+ metadata.cache_fields,
+ pool_datastack_create());
+ dsync_box_r->have_guids = status.have_guids;
+ dsync_box_r->have_save_guids = status.have_save_guids;
+ dsync_box_r->have_only_guid128 = status.have_only_guid128;
+ return 1;
+}
+
+static bool
+dsync_brain_has_mailbox_state_changed(struct dsync_brain *brain,
+ const struct dsync_mailbox *dsync_box)
+{
+ const struct dsync_mailbox_state *state;
+
+ if (brain->sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE)
+ return TRUE;
+
+ state = dsync_mailbox_state_find(brain, dsync_box->mailbox_guid);
+ return state == NULL ||
+ state->last_uidvalidity != dsync_box->uid_validity ||
+ state->last_common_uid+1 != dsync_box->uid_next ||
+ state->last_common_modseq != dsync_box->highest_modseq ||
+ state->last_common_pvt_modseq != dsync_box->highest_pvt_modseq ||
+ state->last_messages_count != dsync_box->messages_count;
+}
+
+static int
+dsync_brain_try_next_mailbox(struct dsync_brain *brain, struct mailbox **box_r,
+ struct file_lock **lock_r,
+ struct dsync_mailbox *dsync_box_r)
+{
+ enum mailbox_flags flags = 0;
+ struct dsync_mailbox dsync_box;
+ struct mailbox *box;
+ struct file_lock *lock = NULL;
+ struct dsync_mailbox_node *node;
+ const char *vname = NULL;
+ enum mail_error error;
+ bool synced = FALSE;
+ int ret;
+
+ *box_r = NULL;
+
+ while (dsync_mailbox_tree_iter_next(brain->local_tree_iter, &vname, &node)) {
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ !guid_128_is_empty(node->mailbox_guid))
+ break;
+ vname = NULL;
+ }
+ if (vname == NULL) {
+ /* no more mailboxes */
+ dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter);
+ return -1;
+ }
+
+ if (brain->backup_send) {
+ /* make sure mailbox isn't modified */
+ flags |= MAILBOX_FLAG_READONLY;
+ }
+ box = mailbox_alloc(node->ns->list, vname, flags);
+ for (;;) {
+ if ((ret = dsync_box_get(box, &dsync_box, &error)) <= 0) {
+ if (ret < 0) {
+ brain->mail_error = error;
+ brain->failed = TRUE;
+ }
+ mailbox_free(&box);
+ file_lock_free(&lock);
+ return ret;
+ }
+
+ /* if mailbox's last_common_* state equals the current state,
+ we can skip the mailbox */
+ if (!dsync_brain_has_mailbox_state_changed(brain, &dsync_box)) {
+ if (brain->debug) {
+ i_debug("brain %c: Skipping mailbox %s with unchanged state "
+ "uidvalidity=%u uidnext=%u highestmodseq=%"PRIu64" highestpvtmodseq=%"PRIu64" messages=%u",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box.mailbox_guid),
+ dsync_box.uid_validity,
+ dsync_box.uid_next,
+ dsync_box.highest_modseq,
+ dsync_box.highest_pvt_modseq,
+ dsync_box.messages_count);
+ }
+ mailbox_free(&box);
+ file_lock_free(&lock);
+ return 0;
+ }
+ if (synced) {
+ /* ok, the mailbox really changed */
+ break;
+ }
+
+ /* mailbox appears to have changed. do a full sync here and get the
+ state again. Lock before syncing. */
+ if (dsync_mailbox_lock(brain, box, &lock) < 0) {
+ brain->failed = TRUE;
+ mailbox_free(&box);
+ return -1;
+ }
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("Can't sync mailbox %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &brain->mail_error));
+ brain->failed = TRUE;
+ mailbox_free(&box);
+ file_lock_free(&lock);
+ return -1;
+ }
+ synced = TRUE;
+ }
+
+ *box_r = box;
+ *lock_r = lock;
+ *dsync_box_r = dsync_box;
+ return 1;
+}
+
+static bool
+dsync_brain_next_mailbox(struct dsync_brain *brain, struct mailbox **box_r,
+ struct file_lock **lock_r,
+ struct dsync_mailbox *dsync_box_r)
+{
+ int ret;
+
+ if (brain->no_mail_sync)
+ return FALSE;
+
+ while ((ret = dsync_brain_try_next_mailbox(brain, box_r, lock_r, dsync_box_r)) == 0)
+ ;
+ return ret > 0;
+}
+
+void dsync_brain_master_send_mailbox(struct dsync_brain *brain)
+{
+ struct dsync_mailbox dsync_box;
+ struct mailbox *box;
+ struct file_lock *lock;
+
+ i_assert(brain->master_brain);
+ i_assert(brain->box == NULL);
+
+ if (!dsync_brain_next_mailbox(brain, &box, &lock, &dsync_box)) {
+ brain->state = DSYNC_STATE_FINISH;
+ dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX);
+ return;
+ }
+
+ /* start exporting this mailbox (wait for remote to start importing) */
+ dsync_ibc_send_mailbox(brain->ibc, &dsync_box);
+ dsync_brain_sync_mailbox_init(brain, box, lock, &dsync_box, TRUE);
+ brain->state = DSYNC_STATE_SYNC_MAILS;
+}
+
+bool dsync_boxes_need_sync(struct dsync_brain *brain,
+ const struct dsync_mailbox *box1,
+ const struct dsync_mailbox *box2,
+ const char **reason_r)
+{
+ if (brain->no_mail_sync)
+ return FALSE;
+ if (brain->sync_type != DSYNC_BRAIN_SYNC_TYPE_CHANGED) {
+ *reason_r = "Full sync";
+ return TRUE;
+ }
+ if (box1->uid_validity != box2->uid_validity)
+ *reason_r = t_strdup_printf("UIDVALIDITY changed: %u -> %u",
+ box1->uid_validity, box2->uid_validity);
+ else if (box1->uid_next != box2->uid_next)
+ *reason_r = t_strdup_printf("UIDNEXT changed: %u -> %u",
+ box1->uid_next, box2->uid_next);
+ else if (box1->messages_count != box2->messages_count)
+ *reason_r = t_strdup_printf("Message count changed: %u -> %u",
+ box1->messages_count, box2->messages_count);
+ else if (box1->highest_modseq != box2->highest_modseq) {
+ *reason_r = t_strdup_printf("HIGHESTMODSEQ changed %"
+ PRIu64" -> %"PRIu64,
+ box1->highest_modseq,
+ box2->highest_modseq);
+ if (box1->highest_modseq == 0 ||
+ box2->highest_modseq == 0) {
+ *reason_r = t_strdup_printf(
+ "%s (Permanent MODSEQs aren't supported)",
+ *reason_r);
+ }
+ } else if (box1->highest_pvt_modseq != box2->highest_pvt_modseq)
+ *reason_r = t_strdup_printf("Private HIGHESTMODSEQ changed %"
+ PRIu64" -> %"PRIu64,
+ box1->highest_pvt_modseq,
+ box2->highest_pvt_modseq);
+ else if (box1->first_recent_uid != box2->first_recent_uid)
+ *reason_r = t_strdup_printf("First RECENT UID changed: %u -> %u",
+ box1->first_recent_uid, box2->first_recent_uid);
+ else
+ return FALSE;
+ return TRUE;
+}
+
+static int
+mailbox_cache_field_name_cmp(const struct mailbox_cache_field *f1,
+ const struct mailbox_cache_field *f2)
+{
+ return strcmp(f1->name, f2->name);
+}
+
+static void
+dsync_cache_fields_update(const struct dsync_mailbox *local_box,
+ const struct dsync_mailbox *remote_box,
+ struct mailbox *box,
+ struct mailbox_update *update)
+{
+ ARRAY_TYPE(mailbox_cache_field) local_sorted, remote_sorted, changes;
+ const struct mailbox_cache_field *local_fields, *remote_fields;
+ unsigned int li, ri, local_count, remote_count;
+ time_t drop_older_timestamp;
+ int ret;
+
+ if (array_count(&remote_box->cache_fields) == 0) {
+ /* remote has no cached fields. there's nothing to update. */
+ return;
+ }
+
+ t_array_init(&local_sorted, array_count(&local_box->cache_fields));
+ t_array_init(&remote_sorted, array_count(&remote_box->cache_fields));
+ array_append_array(&local_sorted, &local_box->cache_fields);
+ array_append_array(&remote_sorted, &remote_box->cache_fields);
+ array_sort(&local_sorted, mailbox_cache_field_name_cmp);
+ array_sort(&remote_sorted, mailbox_cache_field_name_cmp);
+
+ if (array_count(&local_sorted) == 0) {
+ /* local has no cached fields. set them to same as remote. */
+ array_append_zero(&remote_sorted);
+ update->cache_updates = array_front(&remote_sorted);
+ return;
+ }
+
+ /* figure out what to change */
+ local_fields = array_get(&local_sorted, &local_count);
+ remote_fields = array_get(&remote_sorted, &remote_count);
+ t_array_init(&changes, local_count + remote_count);
+ drop_older_timestamp = ioloop_time -
+ box->index->optimization_set.cache.unaccessed_field_drop_secs;
+
+ for (li = ri = 0; li < local_count || ri < remote_count; ) {
+ ret = li == local_count ? 1 :
+ ri == remote_count ? -1 :
+ strcmp(local_fields[li].name, remote_fields[ri].name);
+ if (ret == 0) {
+ /* field exists in both local and remote */
+ const struct mailbox_cache_field *lf = &local_fields[li];
+ const struct mailbox_cache_field *rf = &remote_fields[ri];
+
+ if (lf->last_used > rf->last_used ||
+ (lf->last_used == rf->last_used &&
+ lf->decision > rf->decision)) {
+ /* use local decision and timestamp */
+ } else {
+ array_push_back(&changes, rf);
+ }
+ li++; ri++;
+ } else if (ret < 0) {
+ /* remote field doesn't exist */
+ li++;
+ } else {
+ /* local field doesn't exist */
+ if (remote_fields[ri].last_used < drop_older_timestamp) {
+ /* field hasn't be used for a long time, remote
+ will probably drop this soon as well */
+ } else {
+ array_push_back(&changes, &remote_fields[ri]);
+ }
+ ri++;
+ }
+ }
+ i_assert(li == local_count && ri == remote_count);
+ if (array_count(&changes) > 0) {
+ array_append_zero(&changes);
+ update->cache_updates = array_front(&changes);
+ }
+}
+
+bool dsync_brain_mailbox_update_pre(struct dsync_brain *brain,
+ struct mailbox *box,
+ const struct dsync_mailbox *local_box,
+ const struct dsync_mailbox *remote_box,
+ const char **reason_r)
+{
+ struct mailbox_update update;
+ const struct dsync_mailbox_state *state;
+ bool ret = TRUE;
+
+ *reason_r = NULL;
+ i_zero(&update);
+
+ if (local_box->uid_validity != remote_box->uid_validity) {
+ /* Keep the UIDVALIDITY for the mailbox that has more
+ messages. If they equal, use the higher UIDVALIDITY. */
+ if (remote_box->messages_count > local_box->messages_count ||
+ (remote_box->messages_count == local_box->messages_count &&
+ remote_box->uid_validity > local_box->uid_validity))
+ update.uid_validity = remote_box->uid_validity;
+
+ state = dsync_mailbox_state_find(brain, local_box->mailbox_guid);
+ if (state != NULL && state->last_common_uid > 0) {
+ /* we can't continue syncing this mailbox in this
+ session, because the other side already started
+ sending mailbox changes, but not for all mails. */
+ dsync_mailbox_state_remove(brain, local_box->mailbox_guid);
+ *reason_r = "UIDVALIDITY changed during a stateful sync, need to restart";
+ brain->failed = TRUE;
+ ret = FALSE;
+ }
+ }
+
+ dsync_cache_fields_update(local_box, remote_box, box, &update);
+
+ if (update.uid_validity == 0 &&
+ update.cache_updates == NULL) {
+ /* no changes */
+ return ret;
+ }
+
+ if (mailbox_update(box, &update) < 0) {
+ i_error("Couldn't update mailbox %s metadata: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &brain->mail_error));
+ brain->failed = TRUE;
+ }
+ return ret;
+}
+
+static void
+dsync_brain_slave_send_mailbox_lost(struct dsync_brain *brain,
+ const struct dsync_mailbox *dsync_box,
+ bool ignore)
+{
+ struct dsync_mailbox delete_box;
+
+ if (brain->debug) {
+ i_debug("brain %c: We don't have mailbox %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid));
+ }
+ i_zero(&delete_box);
+ memcpy(delete_box.mailbox_guid, dsync_box->mailbox_guid,
+ sizeof(delete_box.mailbox_guid));
+ t_array_init(&delete_box.cache_fields, 0);
+ if (ignore)
+ delete_box.mailbox_ignore = TRUE;
+ else
+ delete_box.mailbox_lost = TRUE;
+ dsync_ibc_send_mailbox(brain->ibc, &delete_box);
+}
+
+bool dsync_brain_slave_recv_mailbox(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox *dsync_box;
+ struct dsync_mailbox local_dsync_box;
+ struct mailbox *box;
+ struct file_lock *lock;
+ const char *errstr, *resync_reason, *reason;
+ enum mail_error error;
+ int ret;
+ bool resync;
+
+ i_assert(!brain->master_brain);
+ i_assert(brain->box == NULL);
+
+ if ((ret = dsync_ibc_recv_mailbox(brain->ibc, &dsync_box)) == 0)
+ return FALSE;
+ if (ret < 0) {
+ brain->state = DSYNC_STATE_FINISH;
+ return TRUE;
+ }
+
+ if (dsync_brain_mailbox_alloc(brain, dsync_box->mailbox_guid,
+ &box, &errstr, &error) < 0) {
+ i_error("Couldn't allocate mailbox GUID %s: %s",
+ guid_128_to_string(dsync_box->mailbox_guid), errstr);
+ brain->mail_error = error;
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ if (box == NULL) {
+ /* mailbox was probably deleted/renamed during sync */
+ if (brain->backup_send && brain->no_backup_overwrite) {
+ if (brain->debug) {
+ i_debug("brain %c: Ignore nonexistent "
+ "mailbox GUID %s with -1 sync",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid));
+ }
+ dsync_brain_slave_send_mailbox_lost(brain, dsync_box, TRUE);
+ return TRUE;
+ }
+ //FIXME: verify this from log, and if not log an error.
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Mailbox GUID %s was lost",
+ guid_128_to_string(dsync_box->mailbox_guid)));
+ dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE);
+ return TRUE;
+ }
+ /* Lock before syncing */
+ if (dsync_mailbox_lock(brain, box, &lock) < 0) {
+ mailbox_free(&box);
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("Can't sync mailbox %s: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &brain->mail_error));
+ file_lock_free(&lock);
+ mailbox_free(&box);
+ brain->failed = TRUE;
+ return TRUE;
+ }
+
+ if ((ret = dsync_box_get(box, &local_dsync_box, &error)) <= 0) {
+ file_lock_free(&lock);
+ mailbox_free(&box);
+ if (ret < 0) {
+ brain->mail_error = error;
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ /* another process just deleted this mailbox? */
+ if (brain->debug) {
+ i_debug("brain %c: Skipping lost mailbox %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid));
+ }
+ dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE);
+ return TRUE;
+ }
+ i_assert(local_dsync_box.uid_validity != 0);
+ i_assert(memcmp(dsync_box->mailbox_guid, local_dsync_box.mailbox_guid,
+ sizeof(dsync_box->mailbox_guid)) == 0);
+
+ resync = !dsync_brain_mailbox_update_pre(brain, box, &local_dsync_box,
+ dsync_box, &resync_reason);
+
+ if (!dsync_boxes_need_sync(brain, &local_dsync_box, dsync_box, &reason)) {
+ /* no fields appear to have changed, skip this mailbox */
+ if (brain->debug) {
+ i_debug("brain %c: Skipping unchanged mailbox %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid));
+ }
+ dsync_ibc_send_mailbox(brain->ibc, &local_dsync_box);
+ file_lock_free(&lock);
+ mailbox_free(&box);
+ return TRUE;
+ }
+ if (brain->debug) {
+ i_debug("brain %c: Syncing mailbox %s: %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid), reason);
+ }
+
+ /* start export/import */
+ dsync_brain_sync_mailbox_init(brain, box, lock, &local_dsync_box, FALSE);
+ if ((ret = dsync_brain_sync_mailbox_open(brain, dsync_box)) < 0)
+ return TRUE;
+ if (resync)
+ dsync_brain_set_changes_during_sync(brain, resync_reason);
+ if (ret == 0 || resync) {
+ brain->require_full_resync = TRUE;
+ dsync_brain_sync_mailbox_deinit(brain);
+ dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE);
+ return TRUE;
+ }
+
+ dsync_ibc_send_mailbox(brain->ibc, &local_dsync_box);
+ brain->state = DSYNC_STATE_SYNC_MAILS;
+ return TRUE;
+}
diff --git a/src/doveadm/dsync/dsync-brain-mails.c b/src/doveadm/dsync/dsync-brain-mails.c
new file mode 100644
index 0000000..3feea20
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain-mails.c
@@ -0,0 +1,438 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "dsync-ibc.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox-import.h"
+#include "dsync-mailbox-export.h"
+#include "dsync-brain-private.h"
+
+const char *dsync_box_state_names[DSYNC_BOX_STATE_DONE+1] = {
+ "mailbox",
+ "changes",
+ "attributes",
+ "mail_requests",
+ "mails",
+ "recv_last_common",
+ "done"
+};
+
+static bool dsync_brain_master_sync_recv_mailbox(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox *dsync_box;
+ const char *resync_reason, *reason;
+ enum dsync_ibc_recv_ret ret;
+ bool resync;
+
+ i_assert(brain->master_brain);
+
+ if ((ret = dsync_ibc_recv_mailbox(brain->ibc, &dsync_box)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ i_error("Remote sent end-of-list instead of a mailbox");
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ if (memcmp(dsync_box->mailbox_guid, brain->local_dsync_box.mailbox_guid,
+ sizeof(dsync_box->mailbox_guid)) != 0) {
+ i_error("Remote sent mailbox with a wrong GUID");
+ brain->failed = TRUE;
+ return TRUE;
+ }
+
+ if (dsync_box->mailbox_ignore) {
+ /* ignore this box */
+ if (brain->debug)
+ i_debug("brain %c: Ignoring missing remote box GUID %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid));
+ dsync_brain_sync_mailbox_deinit(brain);
+ return TRUE;
+ }
+ if (dsync_box->mailbox_lost) {
+ /* remote lost the mailbox. it's probably already deleted, but
+ verify it on next sync just to be sure */
+ dsync_brain_set_changes_during_sync(brain, t_strdup_printf(
+ "Remote lost mailbox GUID %s (maybe it was just deleted?)",
+ guid_128_to_string(dsync_box->mailbox_guid)));
+ brain->require_full_resync = TRUE;
+ dsync_brain_sync_mailbox_deinit(brain);
+ return TRUE;
+ }
+ resync = !dsync_brain_mailbox_update_pre(brain, brain->box,
+ &brain->local_dsync_box,
+ dsync_box, &resync_reason);
+
+ if (!dsync_boxes_need_sync(brain, &brain->local_dsync_box, dsync_box,
+ &reason)) {
+ /* no fields appear to have changed, skip this mailbox */
+ dsync_brain_sync_mailbox_deinit(brain);
+ return TRUE;
+ }
+ if (brain->debug) {
+ i_debug("brain %c: Syncing mailbox %s: %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(dsync_box->mailbox_guid), reason);
+ }
+ if ((ret = dsync_brain_sync_mailbox_open(brain, dsync_box)) < 0)
+ return TRUE;
+ if (resync)
+ dsync_brain_set_changes_during_sync(brain, resync_reason);
+ if (ret == 0 || resync) {
+ brain->require_full_resync = TRUE;
+ brain->failed = TRUE;
+ dsync_brain_sync_mailbox_deinit(brain);
+ return TRUE;
+ }
+ dsync_brain_sync_init_box_states(brain);
+ return TRUE;
+}
+
+static bool dsync_brain_recv_mailbox_attribute(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox_attribute *attr;
+ struct istream *input;
+ enum dsync_ibc_recv_ret ret;
+
+ if ((ret = dsync_ibc_recv_mailbox_attribute(brain->ibc, &attr)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ brain->box_recv_state = DSYNC_BOX_STATE_CHANGES;
+ return TRUE;
+ }
+ if (dsync_mailbox_import_attribute(brain->box_importer, attr) < 0)
+ brain->failed = TRUE;
+ input = attr->value_stream;
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+static void dsync_brain_send_end_of_list(struct dsync_brain *brain,
+ enum dsync_ibc_eol_type type)
+{
+ i_assert(!brain->failed);
+ dsync_ibc_send_end_of_list(brain->ibc, type);
+}
+
+static int dsync_brain_export_deinit(struct dsync_brain *brain)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ if (dsync_mailbox_export_deinit(&brain->box_exporter,
+ &errstr, &error) < 0) {
+ i_error("Exporting mailbox %s failed: %s",
+ mailbox_get_vname(brain->box), errstr);
+ brain->mail_error = error;
+ brain->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static void dsync_brain_send_mailbox_attribute(struct dsync_brain *brain)
+{
+ const struct dsync_mailbox_attribute *attr;
+ int ret;
+
+ while ((ret = dsync_mailbox_export_next_attr(brain->box_exporter, &attr)) > 0) {
+ if (dsync_ibc_send_mailbox_attribute(brain->ibc, attr) == 0)
+ return;
+ }
+ if (ret < 0) {
+ if (dsync_brain_export_deinit(brain) == 0)
+ i_unreached();
+ return;
+ }
+ dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE);
+ brain->box_send_state = DSYNC_BOX_STATE_CHANGES;
+}
+
+static bool dsync_brain_recv_mail_change(struct dsync_brain *brain)
+{
+ const struct dsync_mail_change *change;
+ enum dsync_ibc_recv_ret ret;
+
+ if ((ret = dsync_ibc_recv_change(brain->ibc, &change)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ if (dsync_mailbox_import_changes_finish(brain->box_importer) < 0)
+ brain->failed = TRUE;
+ if (brain->mail_requests && brain->box_exporter != NULL)
+ brain->box_recv_state = DSYNC_BOX_STATE_MAIL_REQUESTS;
+ else
+ brain->box_recv_state = DSYNC_BOX_STATE_MAILS;
+ return TRUE;
+ }
+ if (dsync_mailbox_import_change(brain->box_importer, change) < 0)
+ brain->failed = TRUE;
+ return TRUE;
+}
+
+static void dsync_brain_send_mail_change(struct dsync_brain *brain)
+{
+ const struct dsync_mail_change *change;
+ int ret;
+
+ while ((ret = dsync_mailbox_export_next(brain->box_exporter, &change)) > 0) {
+ if (dsync_ibc_send_change(brain->ibc, change) == 0)
+ return;
+ }
+ if (ret < 0) {
+ if (dsync_brain_export_deinit(brain) == 0)
+ i_unreached();
+ return;
+ }
+ dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAIL_CHANGES);
+ if (brain->mail_requests && brain->box_importer != NULL)
+ brain->box_send_state = DSYNC_BOX_STATE_MAIL_REQUESTS;
+ else
+ brain->box_send_state = DSYNC_BOX_STATE_MAILS;
+}
+
+static bool dsync_brain_recv_mail_request(struct dsync_brain *brain)
+{
+ const struct dsync_mail_request *request;
+ enum dsync_ibc_recv_ret ret;
+
+ i_assert(brain->mail_requests);
+ i_assert(brain->box_exporter != NULL);
+
+ if ((ret = dsync_ibc_recv_mail_request(brain->ibc, &request)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ brain->box_recv_state = brain->box_importer != NULL ?
+ DSYNC_BOX_STATE_MAILS :
+ DSYNC_BOX_STATE_RECV_LAST_COMMON;
+ return TRUE;
+ }
+ dsync_mailbox_export_want_mail(brain->box_exporter, request);
+ return TRUE;
+}
+
+static bool dsync_brain_send_mail_request(struct dsync_brain *brain)
+{
+ const struct dsync_mail_request *request;
+
+ i_assert(brain->mail_requests);
+
+ while ((request = dsync_mailbox_import_next_request(brain->box_importer)) != NULL) {
+ if (dsync_ibc_send_mail_request(brain->ibc, request) == 0)
+ return TRUE;
+ }
+ if (brain->box_recv_state < DSYNC_BOX_STATE_MAIL_REQUESTS)
+ return FALSE;
+
+ dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAIL_REQUESTS);
+ if (brain->box_exporter != NULL)
+ brain->box_send_state = DSYNC_BOX_STATE_MAILS;
+ else {
+ i_assert(brain->box_recv_state != DSYNC_BOX_STATE_DONE);
+ brain->box_send_state = DSYNC_BOX_STATE_DONE;
+ }
+ return TRUE;
+}
+
+static void dsync_brain_sync_half_finished(struct dsync_brain *brain)
+{
+ struct dsync_mailbox_state state;
+ const char *changes_during_sync;
+ bool require_full_resync;
+
+ if (brain->box_recv_state < DSYNC_BOX_STATE_RECV_LAST_COMMON ||
+ brain->box_send_state < DSYNC_BOX_STATE_RECV_LAST_COMMON)
+ return;
+
+ /* finished with this mailbox */
+ i_zero(&state);
+ memcpy(state.mailbox_guid, brain->local_dsync_box.mailbox_guid,
+ sizeof(state.mailbox_guid));
+ state.last_uidvalidity = brain->local_dsync_box.uid_validity;
+ if (brain->box_importer == NULL) {
+ /* this mailbox didn't exist on remote */
+ state.last_common_uid = brain->local_dsync_box.uid_next-1;
+ state.last_common_modseq =
+ brain->local_dsync_box.highest_modseq;
+ state.last_common_pvt_modseq =
+ brain->local_dsync_box.highest_pvt_modseq;
+ state.last_messages_count =
+ brain->local_dsync_box.messages_count;
+ } else {
+ if (dsync_mailbox_import_deinit(&brain->box_importer,
+ !brain->failed,
+ &state.last_common_uid,
+ &state.last_common_modseq,
+ &state.last_common_pvt_modseq,
+ &state.last_messages_count,
+ &changes_during_sync,
+ &require_full_resync,
+ &brain->mail_error) < 0) {
+ if (require_full_resync) {
+ /* don't treat this as brain failure or the
+ state won't be sent to the other brain.
+ this also means we'll continue syncing the
+ following mailboxes. */
+ brain->require_full_resync = TRUE;
+ } else {
+ brain->failed = TRUE;
+ }
+ }
+ if (changes_during_sync != NULL) {
+ state.changes_during_sync = TRUE;
+ dsync_brain_set_changes_during_sync(brain, changes_during_sync);
+ }
+ }
+ if (brain->require_full_resync) {
+ state.last_uidvalidity = 0;
+ state.changes_during_sync = TRUE;
+ }
+ brain->mailbox_state = state;
+ dsync_ibc_send_mailbox_state(brain->ibc, &state);
+}
+
+static bool dsync_brain_recv_mail(struct dsync_brain *brain)
+{
+ struct dsync_mail *mail;
+ enum dsync_ibc_recv_ret ret;
+
+ if ((ret = dsync_ibc_recv_mail(brain->ibc, &mail)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ brain->box_recv_state = DSYNC_BOX_STATE_RECV_LAST_COMMON;
+ if (brain->box_exporter != NULL &&
+ brain->box_send_state >= DSYNC_BOX_STATE_RECV_LAST_COMMON) {
+ if (dsync_brain_export_deinit(brain) < 0)
+ return TRUE;
+ }
+ dsync_brain_sync_half_finished(brain);
+ return TRUE;
+ }
+ if (brain->debug) {
+ i_debug("brain %c: import mail uid %u guid %s",
+ brain->master_brain ? 'M' : 'S', mail->uid, mail->guid);
+ }
+ if (dsync_mailbox_import_mail(brain->box_importer, mail) < 0)
+ brain->failed = TRUE;
+ i_stream_unref(&mail->input);
+ return TRUE;
+}
+
+static bool dsync_brain_send_mail(struct dsync_brain *brain)
+{
+ const struct dsync_mail *mail;
+
+ if (brain->mail_requests &&
+ brain->box_recv_state < DSYNC_BOX_STATE_MAILS) {
+ /* wait for mail requests to finish. we could already start
+ exporting, but then we're going to do quite a lot of
+ separate searches. especially with pipe backend we'd do
+ a separate search for each mail. */
+ return FALSE;
+ }
+
+ while (dsync_mailbox_export_next_mail(brain->box_exporter, &mail) > 0) {
+ if (dsync_ibc_send_mail(brain->ibc, mail) == 0)
+ return TRUE;
+ }
+
+ if (dsync_brain_export_deinit(brain) < 0)
+ return TRUE;
+
+ brain->box_send_state = DSYNC_BOX_STATE_DONE;
+ dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAILS);
+
+ dsync_brain_sync_half_finished(brain);
+ return TRUE;
+}
+
+static bool dsync_brain_recv_last_common(struct dsync_brain *brain)
+{
+ enum dsync_ibc_recv_ret ret;
+ struct dsync_mailbox_state state;
+
+ if ((ret = dsync_ibc_recv_mailbox_state(brain->ibc, &state)) == 0)
+ return FALSE;
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ i_error("Remote sent end-of-list instead of a mailbox state");
+ brain->failed = TRUE;
+ return TRUE;
+ }
+ i_assert(brain->box_send_state == DSYNC_BOX_STATE_DONE);
+ i_assert(memcmp(state.mailbox_guid, brain->local_dsync_box.mailbox_guid,
+ sizeof(state.mailbox_guid)) == 0);
+
+ /* normally the last_common_* values should be the same in local and
+ remote, but during unexpected changes they may differ. use the
+ values that are lower as the final state. */
+ if (brain->mailbox_state.last_common_uid > state.last_common_uid)
+ brain->mailbox_state.last_common_uid = state.last_common_uid;
+ if (brain->mailbox_state.last_common_modseq > state.last_common_modseq)
+ brain->mailbox_state.last_common_modseq = state.last_common_modseq;
+ if (brain->mailbox_state.last_common_pvt_modseq > state.last_common_pvt_modseq)
+ brain->mailbox_state.last_common_pvt_modseq = state.last_common_pvt_modseq;
+ if (state.changes_during_sync)
+ brain->changes_during_remote_sync = TRUE;
+
+ dsync_brain_sync_mailbox_deinit(brain);
+ return TRUE;
+}
+
+bool dsync_brain_sync_mails(struct dsync_brain *brain)
+{
+ bool changed = FALSE;
+
+ i_assert(brain->box != NULL);
+
+ switch (brain->box_recv_state) {
+ case DSYNC_BOX_STATE_MAILBOX:
+ changed = dsync_brain_master_sync_recv_mailbox(brain);
+ break;
+ case DSYNC_BOX_STATE_ATTRIBUTES:
+ changed = dsync_brain_recv_mailbox_attribute(brain);
+ break;
+ case DSYNC_BOX_STATE_CHANGES:
+ changed = dsync_brain_recv_mail_change(brain);
+ break;
+ case DSYNC_BOX_STATE_MAIL_REQUESTS:
+ changed = dsync_brain_recv_mail_request(brain);
+ break;
+ case DSYNC_BOX_STATE_MAILS:
+ changed = dsync_brain_recv_mail(brain);
+ break;
+ case DSYNC_BOX_STATE_RECV_LAST_COMMON:
+ changed = dsync_brain_recv_last_common(brain);
+ break;
+ case DSYNC_BOX_STATE_DONE:
+ break;
+ }
+
+ if (!dsync_ibc_is_send_queue_full(brain->ibc) && !brain->failed) {
+ switch (brain->box_send_state) {
+ case DSYNC_BOX_STATE_MAILBOX:
+ /* wait for mailbox to be received first */
+ break;
+ case DSYNC_BOX_STATE_ATTRIBUTES:
+ dsync_brain_send_mailbox_attribute(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_BOX_STATE_CHANGES:
+ dsync_brain_send_mail_change(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_BOX_STATE_MAIL_REQUESTS:
+ if (dsync_brain_send_mail_request(brain))
+ changed = TRUE;
+ break;
+ case DSYNC_BOX_STATE_MAILS:
+ if (dsync_brain_send_mail(brain))
+ changed = TRUE;
+ break;
+ case DSYNC_BOX_STATE_RECV_LAST_COMMON:
+ i_unreached();
+ case DSYNC_BOX_STATE_DONE:
+ break;
+ }
+ }
+ return changed;
+}
diff --git a/src/doveadm/dsync/dsync-brain-private.h b/src/doveadm/dsync/dsync-brain-private.h
new file mode 100644
index 0000000..6680a9b
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain-private.h
@@ -0,0 +1,164 @@
+#ifndef DSYNC_BRAIN_PRIVATE_H
+#define DSYNC_BRAIN_PRIVATE_H
+
+#include "hash.h"
+#include "dsync-brain.h"
+#include "dsync-mailbox.h"
+#include "dsync-mailbox-state.h"
+
+#define DSYNC_LOCK_FILENAME ".dovecot-sync.lock"
+#define DSYNC_MAILBOX_LOCK_FILENAME ".dovecot-box-sync.lock"
+#define DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS 30
+
+struct dsync_mailbox_tree_sync_change;
+
+enum dsync_state {
+ DSYNC_STATE_MASTER_RECV_HANDSHAKE,
+ DSYNC_STATE_SLAVE_RECV_HANDSHAKE,
+ /* if sync_type=STATE, the master brain knows the saved "last common
+ mailbox state". this state is sent to the slave. */
+ DSYNC_STATE_MASTER_SEND_LAST_COMMON,
+ DSYNC_STATE_SLAVE_RECV_LAST_COMMON,
+
+ /* both sides send their mailbox trees */
+ DSYNC_STATE_SEND_MAILBOX_TREE,
+ DSYNC_STATE_SEND_MAILBOX_TREE_DELETES,
+ DSYNC_STATE_RECV_MAILBOX_TREE,
+ DSYNC_STATE_RECV_MAILBOX_TREE_DELETES,
+
+ /* master decides in which order mailboxes are synced (it knows the
+ slave's mailboxes by looking at the received mailbox tree) */
+ DSYNC_STATE_MASTER_SEND_MAILBOX,
+ DSYNC_STATE_SLAVE_RECV_MAILBOX,
+ /* once mailbox is selected, the mails inside it are synced.
+ after the mails are synced, another mailbox is synced. */
+ DSYNC_STATE_SYNC_MAILS,
+
+ DSYNC_STATE_FINISH,
+ DSYNC_STATE_DONE
+};
+
+enum dsync_box_state {
+ DSYNC_BOX_STATE_MAILBOX,
+ DSYNC_BOX_STATE_CHANGES,
+ DSYNC_BOX_STATE_ATTRIBUTES,
+ DSYNC_BOX_STATE_MAIL_REQUESTS,
+ DSYNC_BOX_STATE_MAILS,
+ DSYNC_BOX_STATE_RECV_LAST_COMMON,
+ DSYNC_BOX_STATE_DONE
+};
+
+struct dsync_brain {
+ pool_t pool;
+ struct mail_user *user;
+ struct dsync_ibc *ibc;
+ const char *process_title_prefix;
+ ARRAY(struct mail_namespace *) sync_namespaces;
+ const char *sync_box;
+ struct mailbox *virtual_all_box;
+ guid_128_t sync_box_guid;
+ const char *const *exclude_mailboxes;
+ enum dsync_brain_sync_type sync_type;
+ time_t sync_since_timestamp;
+ time_t sync_until_timestamp;
+ uoff_t sync_max_size;
+ const char *sync_flag;
+ char alt_char;
+ unsigned int import_commit_msgs_interval;
+ unsigned int hdr_hash_version;
+
+ unsigned int lock_timeout;
+ int lock_fd;
+ const char *lock_path;
+ struct file_lock *lock;
+
+ char hierarchy_sep, escape_char;
+ struct dsync_mailbox_tree *local_mailbox_tree;
+ struct dsync_mailbox_tree *remote_mailbox_tree;
+ struct dsync_mailbox_tree_iter *local_tree_iter;
+
+ enum dsync_state state, pre_box_state;
+ enum dsync_box_state box_recv_state;
+ enum dsync_box_state box_send_state;
+ unsigned int proctitle_update_counter;
+
+ struct dsync_transaction_log_scan *log_scan;
+ struct dsync_mailbox_importer *box_importer;
+ struct dsync_mailbox_exporter *box_exporter;
+
+ struct mailbox *box;
+ struct file_lock *box_lock;
+ unsigned int mailbox_lock_timeout_secs;
+ struct dsync_mailbox local_dsync_box, remote_dsync_box;
+ pool_t dsync_box_pool;
+ /* list of mailbox states
+ for master brain: given to brain at init and
+ for slave brain: received from DSYNC_STATE_SLAVE_RECV_LAST_COMMON */
+ HASH_TABLE_TYPE(dsync_mailbox_state) mailbox_states;
+ /* DSYNC_STATE_MASTER_SEND_LAST_COMMON: current send position */
+ struct hash_iterate_context *mailbox_states_iter;
+ /* state of the mailbox we're currently syncing, changed at
+ init and deinit */
+ struct dsync_mailbox_state mailbox_state;
+ /* new states for synced mailboxes */
+ ARRAY_TYPE(dsync_mailbox_state) remote_mailbox_states;
+
+ const char *changes_during_sync;
+ enum mail_error mail_error;
+
+ const char *const *hashed_headers;
+
+ bool master_brain:1;
+ bool mail_requests:1;
+ bool backup_send:1;
+ bool backup_recv:1;
+ bool purge:1;
+ bool debug:1;
+ bool sync_visible_namespaces:1;
+ bool no_mail_sync:1;
+ bool no_backup_overwrite:1;
+ bool no_mail_prefetch:1;
+ bool changes_during_remote_sync:1;
+ bool require_full_resync:1;
+ bool verbose_proctitle:1;
+ bool no_notify:1;
+ bool failed:1;
+ bool empty_hdr_workaround:1;
+ bool no_header_hashes:1;
+};
+
+extern const char *dsync_box_state_names[DSYNC_BOX_STATE_DONE+1];
+
+void dsync_brain_mailbox_trees_init(struct dsync_brain *brain);
+void dsync_brain_send_mailbox_tree(struct dsync_brain *brain);
+void dsync_brain_send_mailbox_tree_deletes(struct dsync_brain *brain);
+bool dsync_brain_recv_mailbox_tree(struct dsync_brain *brain);
+bool dsync_brain_recv_mailbox_tree_deletes(struct dsync_brain *brain);
+int dsync_brain_mailbox_tree_sync_change(struct dsync_brain *brain,
+ const struct dsync_mailbox_tree_sync_change *change,
+ enum mail_error *error_r);
+
+void dsync_brain_sync_mailbox_deinit(struct dsync_brain *brain);
+int dsync_brain_mailbox_alloc(struct dsync_brain *brain, const guid_128_t guid,
+ struct mailbox **box_r, const char **errstr_r,
+ enum mail_error *error_r);
+bool dsync_brain_mailbox_update_pre(struct dsync_brain *brain,
+ struct mailbox *box,
+ const struct dsync_mailbox *local_box,
+ const struct dsync_mailbox *remote_box,
+ const char **reason_r);
+bool dsync_boxes_need_sync(struct dsync_brain *brain,
+ const struct dsync_mailbox *box1,
+ const struct dsync_mailbox *box2,
+ const char **reason_r);
+void dsync_brain_sync_init_box_states(struct dsync_brain *brain);
+void dsync_brain_set_changes_during_sync(struct dsync_brain *brain,
+ const char *reason);
+
+void dsync_brain_master_send_mailbox(struct dsync_brain *brain);
+bool dsync_brain_slave_recv_mailbox(struct dsync_brain *brain);
+int dsync_brain_sync_mailbox_open(struct dsync_brain *brain,
+ const struct dsync_mailbox *remote_dsync_box);
+bool dsync_brain_sync_mails(struct dsync_brain *brain);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-brain.c b/src/doveadm/dsync/dsync-brain.c
new file mode 100644
index 0000000..d847169
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain.c
@@ -0,0 +1,902 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "hostpid.h"
+#include "str.h"
+#include "file-create-locked.h"
+#include "process-title.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-namespace.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-ibc.h"
+#include "dsync-brain-private.h"
+#include "dsync-mailbox-import.h"
+#include "dsync-mailbox-export.h"
+
+#include <sys/stat.h>
+
+enum dsync_brain_title {
+ DSYNC_BRAIN_TITLE_NONE = 0,
+ DSYNC_BRAIN_TITLE_LOCKING,
+};
+
+static const char *dsync_state_names[] = {
+ "master_recv_handshake",
+ "slave_recv_handshake",
+ "master_send_last_common",
+ "slave_recv_last_common",
+ "send_mailbox_tree",
+ "send_mailbox_tree_deletes",
+ "recv_mailbox_tree",
+ "recv_mailbox_tree_deletes",
+ "master_send_mailbox",
+ "slave_recv_mailbox",
+ "sync_mails",
+ "finish",
+ "done"
+};
+
+struct dsync_mailbox_list_module dsync_mailbox_list_module =
+ MODULE_CONTEXT_INIT(&mailbox_list_module_register);
+
+static void dsync_brain_mailbox_states_dump(struct dsync_brain *brain);
+
+static const char *
+dsync_brain_get_proctitle_full(struct dsync_brain *brain,
+ enum dsync_brain_title title)
+{
+ string_t *str = t_str_new(128);
+ const char *import_title, *export_title;
+
+ str_append_c(str, '[');
+ if (brain->process_title_prefix != NULL)
+ str_append(str, brain->process_title_prefix);
+ str_append(str, brain->user->username);
+ if (brain->box == NULL) {
+ str_append_c(str, ' ');
+ str_append(str, dsync_state_names[brain->state]);
+ } else {
+ str_append_c(str, ' ');
+ str_append(str, mailbox_get_vname(brain->box));
+ import_title = brain->box_importer == NULL ? "" :
+ dsync_mailbox_import_get_proctitle(brain->box_importer);
+ export_title = brain->box_exporter == NULL ? "" :
+ dsync_mailbox_export_get_proctitle(brain->box_exporter);
+ if (import_title[0] == '\0' && export_title[0] == '\0') {
+ str_printfa(str, " send:%s recv:%s",
+ dsync_box_state_names[brain->box_send_state],
+ dsync_box_state_names[brain->box_recv_state]);
+ } else {
+ if (import_title[0] != '\0') {
+ str_append(str, " import:");
+ str_append(str, import_title);
+ }
+ if (export_title[0] != '\0') {
+ str_append(str, " export:");
+ str_append(str, export_title);
+ }
+ }
+ }
+ switch (title) {
+ case DSYNC_BRAIN_TITLE_NONE:
+ break;
+ case DSYNC_BRAIN_TITLE_LOCKING:
+ str_append(str, " locking "DSYNC_LOCK_FILENAME);
+ break;
+ }
+ str_append_c(str, ']');
+ return str_c(str);
+}
+
+static const char *dsync_brain_get_proctitle(struct dsync_brain *brain)
+{
+ return dsync_brain_get_proctitle_full(brain, DSYNC_BRAIN_TITLE_NONE);
+}
+
+static void dsync_brain_run_io(void *context)
+{
+ struct dsync_brain *brain = context;
+ bool changed, try_pending;
+
+ if (dsync_ibc_has_failed(brain->ibc)) {
+ io_loop_stop(current_ioloop);
+ brain->failed = TRUE;
+ return;
+ }
+
+ try_pending = TRUE;
+ do {
+ if (!dsync_brain_run(brain, &changed)) {
+ io_loop_stop(current_ioloop);
+ break;
+ }
+ if (changed)
+ try_pending = TRUE;
+ else if (try_pending) {
+ if (dsync_ibc_has_pending_data(brain->ibc))
+ changed = TRUE;
+ try_pending = FALSE;
+ }
+ } while (changed);
+}
+
+static struct dsync_brain *
+dsync_brain_common_init(struct mail_user *user, struct dsync_ibc *ibc)
+{
+ struct dsync_brain *brain;
+ const struct master_service_settings *service_set;
+ pool_t pool;
+
+ service_set = master_service_settings_get(master_service);
+ mail_user_ref(user);
+
+ pool = pool_alloconly_create("dsync brain", 10240);
+ brain = p_new(pool, struct dsync_brain, 1);
+ brain->pool = pool;
+ brain->user = user;
+ brain->ibc = ibc;
+ brain->sync_type = DSYNC_BRAIN_SYNC_TYPE_UNKNOWN;
+ brain->lock_fd = -1;
+ brain->verbose_proctitle = service_set->verbose_proctitle;
+ hash_table_create(&brain->mailbox_states, pool, 0,
+ guid_128_hash, guid_128_cmp);
+ p_array_init(&brain->remote_mailbox_states, pool, 64);
+ return brain;
+}
+
+static void
+dsync_brain_set_flags(struct dsync_brain *brain, enum dsync_brain_flags flags)
+{
+ brain->mail_requests =
+ (flags & DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS) != 0;
+ brain->backup_send = (flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0;
+ brain->backup_recv = (flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0;
+ brain->debug = (flags & DSYNC_BRAIN_FLAG_DEBUG) != 0;
+ brain->sync_visible_namespaces =
+ (flags & DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES) != 0;
+ brain->no_mail_sync = (flags & DSYNC_BRAIN_FLAG_NO_MAIL_SYNC) != 0;
+ brain->no_backup_overwrite =
+ (flags & DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE) != 0;
+ brain->no_mail_prefetch =
+ (flags & DSYNC_BRAIN_FLAG_NO_MAIL_PREFETCH) != 0;
+ brain->no_notify = (flags & DSYNC_BRAIN_FLAG_NO_NOTIFY) != 0;
+ brain->empty_hdr_workaround = (flags & DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND) != 0;
+ brain->no_header_hashes = (flags & DSYNC_BRAIN_FLAG_NO_HEADER_HASHES) != 0;
+}
+
+static void
+dsync_brain_open_virtual_all_box(struct dsync_brain *brain,
+ const char *vname)
+{
+ struct mail_namespace *ns;
+
+ ns = mail_namespace_find(brain->user->namespaces, vname);
+ brain->virtual_all_box =
+ mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY);
+}
+
+struct dsync_brain *
+dsync_brain_master_init(struct mail_user *user, struct dsync_ibc *ibc,
+ enum dsync_brain_sync_type sync_type,
+ enum dsync_brain_flags flags,
+ const struct dsync_brain_settings *set)
+{
+ struct dsync_ibc_settings ibc_set;
+ struct dsync_brain *brain;
+ struct mail_namespace *ns;
+ string_t *sync_ns_str = NULL;
+ const char *error;
+
+ i_assert(sync_type != DSYNC_BRAIN_SYNC_TYPE_UNKNOWN);
+ i_assert(sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE ||
+ (set->state != NULL && *set->state != '\0'));
+ i_assert(N_ELEMENTS(dsync_state_names) == DSYNC_STATE_DONE+1);
+
+ brain = dsync_brain_common_init(user, ibc);
+ brain->process_title_prefix =
+ p_strdup(brain->pool, set->process_title_prefix);
+ brain->sync_type = sync_type;
+ if (array_count(&set->sync_namespaces) > 0) {
+ sync_ns_str = t_str_new(128);
+ p_array_init(&brain->sync_namespaces, brain->pool,
+ array_count(&set->sync_namespaces));
+ array_foreach_elem(&set->sync_namespaces, ns) {
+ str_append(sync_ns_str, ns->prefix);
+ str_append_c(sync_ns_str, '\n');
+ array_push_back(&brain->sync_namespaces, &ns);
+ }
+ str_delete(sync_ns_str, str_len(sync_ns_str)-1, 1);
+ }
+ brain->alt_char = set->mailbox_alt_char == '\0' ? '_' :
+ set->mailbox_alt_char;
+ brain->sync_since_timestamp = set->sync_since_timestamp;
+ brain->sync_until_timestamp = set->sync_until_timestamp;
+ brain->sync_max_size = set->sync_max_size;
+ brain->sync_flag = p_strdup(brain->pool, set->sync_flag);
+ brain->sync_box = p_strdup(brain->pool, set->sync_box);
+ brain->exclude_mailboxes = set->exclude_mailboxes == NULL ? NULL :
+ p_strarray_dup(brain->pool, set->exclude_mailboxes);
+ memcpy(brain->sync_box_guid, set->sync_box_guid,
+ sizeof(brain->sync_box_guid));
+ brain->lock_timeout = set->lock_timeout_secs;
+ if (brain->lock_timeout != 0)
+ brain->mailbox_lock_timeout_secs = brain->lock_timeout;
+ else
+ brain->mailbox_lock_timeout_secs =
+ DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS;
+ brain->import_commit_msgs_interval = set->import_commit_msgs_interval;
+ brain->master_brain = TRUE;
+ brain->hashed_headers =
+ (const char*const*)p_strarray_dup(brain->pool, set->hashed_headers);
+ dsync_brain_set_flags(brain, flags);
+
+ if (set->virtual_all_box != NULL)
+ dsync_brain_open_virtual_all_box(brain, set->virtual_all_box);
+
+ if (sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE)
+ ;
+ else if (dsync_mailbox_states_import(brain->mailbox_states, brain->pool,
+ set->state, &error) < 0) {
+ hash_table_clear(brain->mailbox_states, FALSE);
+ i_error("Saved sync state is invalid, "
+ "falling back to full sync: %s", error);
+ brain->sync_type = sync_type = DSYNC_BRAIN_SYNC_TYPE_FULL;
+ } else {
+ if (brain->debug) {
+ i_debug("brain %c: Imported mailbox states:",
+ brain->master_brain ? 'M' : 'S');
+ dsync_brain_mailbox_states_dump(brain);
+ }
+ }
+ dsync_brain_mailbox_trees_init(brain);
+
+ i_zero(&ibc_set);
+ ibc_set.hostname = my_hostdomain();
+ ibc_set.sync_ns_prefixes = sync_ns_str == NULL ?
+ NULL : str_c(sync_ns_str);
+ ibc_set.sync_box = set->sync_box;
+ ibc_set.virtual_all_box = set->virtual_all_box;
+ ibc_set.exclude_mailboxes = set->exclude_mailboxes;
+ ibc_set.sync_since_timestamp = set->sync_since_timestamp;
+ ibc_set.sync_until_timestamp = set->sync_until_timestamp;
+ ibc_set.sync_max_size = set->sync_max_size;
+ ibc_set.sync_flags = set->sync_flag;
+ memcpy(ibc_set.sync_box_guid, set->sync_box_guid,
+ sizeof(ibc_set.sync_box_guid));
+ ibc_set.alt_char = brain->alt_char;
+ ibc_set.sync_type = sync_type;
+ ibc_set.hdr_hash_v2 = TRUE;
+ ibc_set.lock_timeout = set->lock_timeout_secs;
+ ibc_set.import_commit_msgs_interval = set->import_commit_msgs_interval;
+ ibc_set.hashed_headers = set->hashed_headers;
+ /* reverse the backup direction for the slave */
+ ibc_set.brain_flags = flags & ENUM_NEGATE(DSYNC_BRAIN_FLAG_BACKUP_SEND |
+ DSYNC_BRAIN_FLAG_BACKUP_RECV);
+ if ((flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0)
+ ibc_set.brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_RECV;
+ else if ((flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0)
+ ibc_set.brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_SEND;
+ dsync_ibc_send_handshake(ibc, &ibc_set);
+
+ dsync_ibc_set_io_callback(ibc, dsync_brain_run_io, brain);
+ brain->state = DSYNC_STATE_MASTER_RECV_HANDSHAKE;
+
+ if (brain->verbose_proctitle)
+ process_title_set(dsync_brain_get_proctitle(brain));
+ return brain;
+}
+
+struct dsync_brain *
+dsync_brain_slave_init(struct mail_user *user, struct dsync_ibc *ibc,
+ bool local, const char *process_title_prefix,
+ char default_alt_char)
+{
+ struct dsync_ibc_settings ibc_set;
+ struct dsync_brain *brain;
+
+ i_assert(default_alt_char != '\0');
+
+ brain = dsync_brain_common_init(user, ibc);
+ brain->alt_char = default_alt_char;
+ brain->process_title_prefix =
+ p_strdup(brain->pool, process_title_prefix);
+ brain->state = DSYNC_STATE_SLAVE_RECV_HANDSHAKE;
+
+ if (local) {
+ /* both master and slave are running within the same process,
+ update the proctitle only for master. */
+ brain->verbose_proctitle = FALSE;
+ }
+
+ i_zero(&ibc_set);
+ ibc_set.hdr_hash_v2 = TRUE;
+ ibc_set.hostname = my_hostdomain();
+ dsync_ibc_send_handshake(ibc, &ibc_set);
+
+ if (brain->verbose_proctitle)
+ process_title_set(dsync_brain_get_proctitle(brain));
+ dsync_ibc_set_io_callback(ibc, dsync_brain_run_io, brain);
+ return brain;
+}
+
+static void dsync_brain_purge(struct dsync_brain *brain)
+{
+ struct mail_namespace *ns;
+ struct mail_storage *storage;
+
+ for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) {
+ if (!dsync_brain_want_namespace(brain, ns))
+ continue;
+
+ storage = mail_namespace_get_default_storage(ns);
+ if (mail_storage_purge(storage) < 0) {
+ i_error("Purging namespace '%s' failed: %s", ns->prefix,
+ mail_storage_get_last_internal_error(storage, NULL));
+ }
+ }
+}
+
+int dsync_brain_deinit(struct dsync_brain **_brain, enum mail_error *error_r)
+{
+ struct dsync_brain *brain = *_brain;
+ int ret;
+
+ *_brain = NULL;
+
+ if (dsync_ibc_has_timed_out(brain->ibc)) {
+ i_error("Timeout during state=%s%s",
+ dsync_state_names[brain->state],
+ brain->state != DSYNC_STATE_SYNC_MAILS ? "" :
+ t_strdup_printf(" (send=%s recv=%s)",
+ dsync_box_state_names[brain->box_send_state],
+ dsync_box_state_names[brain->box_recv_state]));
+ }
+ if (dsync_ibc_has_failed(brain->ibc) ||
+ brain->state != DSYNC_STATE_DONE)
+ brain->failed = TRUE;
+ dsync_ibc_close_mail_streams(brain->ibc);
+
+ if (brain->purge && !brain->failed)
+ dsync_brain_purge(brain);
+
+ if (brain->box != NULL)
+ dsync_brain_sync_mailbox_deinit(brain);
+ if (brain->virtual_all_box != NULL)
+ mailbox_free(&brain->virtual_all_box);
+ if (brain->local_tree_iter != NULL)
+ dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter);
+ if (brain->local_mailbox_tree != NULL)
+ dsync_mailbox_tree_deinit(&brain->local_mailbox_tree);
+ if (brain->remote_mailbox_tree != NULL)
+ dsync_mailbox_tree_deinit(&brain->remote_mailbox_tree);
+ hash_table_iterate_deinit(&brain->mailbox_states_iter);
+ hash_table_destroy(&brain->mailbox_states);
+
+ pool_unref(&brain->dsync_box_pool);
+
+ if (brain->lock_fd != -1) {
+ /* unlink the lock file before it gets unlocked */
+ i_unlink(brain->lock_path);
+ if (brain->debug) {
+ i_debug("brain %c: Unlocked %s",
+ brain->master_brain ? 'M' : 'S',
+ brain->lock_path);
+ }
+ file_lock_free(&brain->lock);
+ i_close_fd(&brain->lock_fd);
+ }
+
+ ret = brain->failed ? -1 : 0;
+ mail_user_unref(&brain->user);
+
+ *error_r = !brain->failed ? 0 :
+ (brain->mail_error == 0 ? MAIL_ERROR_TEMP : brain->mail_error);
+ pool_unref(&brain->pool);
+ return ret;
+}
+
+static int
+dsync_brain_lock(struct dsync_brain *brain, const char *remote_hostname)
+{
+ const struct file_create_settings lock_set = {
+ .lock_timeout_secs = brain->lock_timeout,
+ .lock_settings = {
+ .lock_method = FILE_LOCK_METHOD_FCNTL,
+ },
+ };
+ const char *home, *error, *local_hostname = my_hostdomain();
+ bool created;
+ int ret;
+
+ if ((ret = strcmp(remote_hostname, local_hostname)) < 0) {
+ /* locking done by remote */
+ if (brain->debug) {
+ i_debug("brain %c: Locking done by remote "
+ "(local hostname=%s, remote hostname=%s)",
+ brain->master_brain ? 'M' : 'S',
+ local_hostname, remote_hostname);
+ }
+ return 0;
+ }
+ if (ret == 0 && !brain->master_brain) {
+ /* running dsync within the same server.
+ locking done by master brain. */
+ if (brain->debug) {
+ i_debug("brain %c: Locking done by local master-brain "
+ "(local hostname=%s, remote hostname=%s)",
+ brain->master_brain ? 'M' : 'S',
+ local_hostname, remote_hostname);
+ }
+ return 0;
+ }
+
+ if ((ret = mail_user_get_home(brain->user, &home)) < 0) {
+ i_error("Couldn't look up user's home dir");
+ return -1;
+ }
+ if (ret == 0) {
+ i_error("User has no home directory");
+ return -1;
+ }
+
+ if (brain->verbose_proctitle)
+ process_title_set(dsync_brain_get_proctitle_full(brain, DSYNC_BRAIN_TITLE_LOCKING));
+ brain->lock_path = p_strconcat(brain->pool, home,
+ "/"DSYNC_LOCK_FILENAME, NULL);
+ brain->lock_fd = file_create_locked(brain->lock_path, &lock_set,
+ &brain->lock, &created, &error);
+ if (brain->lock_fd == -1 && errno == ENOENT) {
+ /* home directory not created */
+ if (mail_user_home_mkdir(brain->user) < 0)
+ return -1;
+ brain->lock_fd = file_create_locked(brain->lock_path, &lock_set,
+ &brain->lock, &created, &error);
+ }
+ if (brain->lock_fd == -1)
+ i_error("Couldn't lock %s: %s", brain->lock_path, error);
+ else if (brain->debug) {
+ i_debug("brain %c: Locking done locally in %s "
+ "(local hostname=%s, remote hostname=%s)",
+ brain->master_brain ? 'M' : 'S',
+ brain->lock_path, local_hostname, remote_hostname);
+ }
+ if (brain->verbose_proctitle)
+ process_title_set(dsync_brain_get_proctitle(brain));
+ return brain->lock_fd == -1 ? -1 : 0;
+}
+
+static void
+dsync_brain_set_hdr_hash_version(struct dsync_brain *brain,
+ const struct dsync_ibc_settings *ibc_set)
+{
+ if (ibc_set->hdr_hash_v3)
+ brain->hdr_hash_version = 3;
+ else if (ibc_set->hdr_hash_v2)
+ brain->hdr_hash_version = 3;
+ else
+ brain->hdr_hash_version = 1;
+}
+
+static bool dsync_brain_master_recv_handshake(struct dsync_brain *brain)
+{
+ const struct dsync_ibc_settings *ibc_set;
+
+ i_assert(brain->master_brain);
+
+ if (dsync_ibc_recv_handshake(brain->ibc, &ibc_set) == 0)
+ return FALSE;
+
+ if (brain->lock_timeout > 0) {
+ if (dsync_brain_lock(brain, ibc_set->hostname) < 0) {
+ brain->failed = TRUE;
+ return FALSE;
+ }
+ }
+ dsync_brain_set_hdr_hash_version(brain, ibc_set);
+
+ brain->state = brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_STATE ?
+ DSYNC_STATE_MASTER_SEND_LAST_COMMON :
+ DSYNC_STATE_SEND_MAILBOX_TREE;
+ return TRUE;
+}
+
+static bool dsync_brain_slave_recv_handshake(struct dsync_brain *brain)
+{
+ const struct dsync_ibc_settings *ibc_set;
+ struct mail_namespace *ns;
+ const char *const *prefixes;
+
+ i_assert(!brain->master_brain);
+
+ if (dsync_ibc_recv_handshake(brain->ibc, &ibc_set) == 0)
+ return FALSE;
+ dsync_brain_set_hdr_hash_version(brain, ibc_set);
+
+ if (ibc_set->lock_timeout > 0) {
+ brain->lock_timeout = ibc_set->lock_timeout;
+ brain->mailbox_lock_timeout_secs = brain->lock_timeout;
+ if (dsync_brain_lock(brain, ibc_set->hostname) < 0) {
+ brain->failed = TRUE;
+ return FALSE;
+ }
+ } else {
+ brain->mailbox_lock_timeout_secs =
+ DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS;
+ }
+
+ if (ibc_set->sync_ns_prefixes != NULL) {
+ p_array_init(&brain->sync_namespaces, brain->pool, 4);
+ prefixes = t_strsplit(ibc_set->sync_ns_prefixes, "\n");
+ if (prefixes[0] == NULL) {
+ /* ugly workaround for strsplit API: there was one
+ prefix="" entry */
+ static const char *empty_prefix[] = { "", NULL };
+ prefixes = empty_prefix;
+ }
+ for (; *prefixes != NULL; prefixes++) {
+ ns = mail_namespace_find(brain->user->namespaces,
+ *prefixes);
+ if (ns == NULL) {
+ i_error("Namespace not found: '%s'", *prefixes);
+ brain->failed = TRUE;
+ return FALSE;
+ }
+ array_push_back(&brain->sync_namespaces, &ns);
+ }
+ }
+ brain->sync_box = p_strdup(brain->pool, ibc_set->sync_box);
+ brain->exclude_mailboxes = ibc_set->exclude_mailboxes == NULL ? NULL :
+ p_strarray_dup(brain->pool, ibc_set->exclude_mailboxes);
+ brain->sync_since_timestamp = ibc_set->sync_since_timestamp;
+ brain->sync_until_timestamp = ibc_set->sync_until_timestamp;
+ brain->sync_max_size = ibc_set->sync_max_size;
+ brain->sync_flag = p_strdup(brain->pool, ibc_set->sync_flags);
+ memcpy(brain->sync_box_guid, ibc_set->sync_box_guid,
+ sizeof(brain->sync_box_guid));
+ if (ibc_set->alt_char != '\0')
+ brain->alt_char = ibc_set->alt_char;
+ i_assert(brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_UNKNOWN);
+ brain->sync_type = ibc_set->sync_type;
+
+ dsync_brain_set_flags(brain, ibc_set->brain_flags);
+ if (ibc_set->hashed_headers != NULL)
+ brain->hashed_headers =
+ p_strarray_dup(brain->pool, (const char*const*)ibc_set->hashed_headers);
+ /* this flag is only set on the remote slave brain */
+ brain->purge = (ibc_set->brain_flags &
+ DSYNC_BRAIN_FLAG_PURGE_REMOTE) != 0;
+
+ if (ibc_set->virtual_all_box != NULL)
+ dsync_brain_open_virtual_all_box(brain, ibc_set->virtual_all_box);
+ dsync_brain_mailbox_trees_init(brain);
+
+ if (brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_STATE)
+ brain->state = DSYNC_STATE_SLAVE_RECV_LAST_COMMON;
+ else
+ brain->state = DSYNC_STATE_SEND_MAILBOX_TREE;
+ return TRUE;
+}
+
+static void dsync_brain_master_send_last_common(struct dsync_brain *brain)
+{
+ struct dsync_mailbox_state *state;
+ uint8_t *guid;
+ enum dsync_ibc_send_ret ret = DSYNC_IBC_SEND_RET_OK;
+
+ i_assert(brain->master_brain);
+
+ if (brain->mailbox_states_iter == NULL) {
+ brain->mailbox_states_iter =
+ hash_table_iterate_init(brain->mailbox_states);
+ }
+
+ for (;;) {
+ if (ret == DSYNC_IBC_SEND_RET_FULL)
+ return;
+ if (!hash_table_iterate(brain->mailbox_states_iter,
+ brain->mailbox_states, &guid, &state))
+ break;
+ ret = dsync_ibc_send_mailbox_state(brain->ibc, state);
+ }
+ hash_table_iterate_deinit(&brain->mailbox_states_iter);
+
+ dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX_STATE);
+ brain->state = DSYNC_STATE_SEND_MAILBOX_TREE;
+}
+
+static void dsync_mailbox_state_add(struct dsync_brain *brain,
+ const struct dsync_mailbox_state *state)
+{
+ struct dsync_mailbox_state *dupstate;
+ uint8_t *guid_p;
+
+ dupstate = p_new(brain->pool, struct dsync_mailbox_state, 1);
+ *dupstate = *state;
+ guid_p = dupstate->mailbox_guid;
+ hash_table_insert(brain->mailbox_states, guid_p, dupstate);
+}
+
+static bool dsync_brain_slave_recv_last_common(struct dsync_brain *brain)
+{
+ struct dsync_mailbox_state state;
+ enum dsync_ibc_recv_ret ret;
+ bool changed = FALSE;
+
+ i_assert(!brain->master_brain);
+
+ while ((ret = dsync_ibc_recv_mailbox_state(brain->ibc, &state)) > 0) {
+ dsync_mailbox_state_add(brain, &state);
+ changed = TRUE;
+ }
+ if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+ brain->state = DSYNC_STATE_SEND_MAILBOX_TREE;
+ changed = TRUE;
+ }
+ return changed;
+}
+
+static bool dsync_brain_finish(struct dsync_brain *brain)
+{
+ const char *error;
+ enum mail_error mail_error;
+ bool require_full_resync;
+ enum dsync_ibc_recv_ret ret;
+
+ if (!brain->master_brain) {
+ dsync_ibc_send_finish(brain->ibc,
+ brain->failed ? "dsync failed" : NULL,
+ brain->mail_error,
+ brain->require_full_resync);
+ brain->state = DSYNC_STATE_DONE;
+ return TRUE;
+ }
+ ret = dsync_ibc_recv_finish(brain->ibc, &error, &mail_error,
+ &require_full_resync);
+ if (ret == DSYNC_IBC_RECV_RET_TRYAGAIN)
+ return FALSE;
+ if (error != NULL) {
+ i_error("Remote dsync failed: %s", error);
+ brain->failed = TRUE;
+ if (mail_error != 0 &&
+ (brain->mail_error == 0 || brain->mail_error == MAIL_ERROR_TEMP))
+ brain->mail_error = mail_error;
+ }
+ if (require_full_resync)
+ brain->require_full_resync = TRUE;
+ brain->state = DSYNC_STATE_DONE;
+ return TRUE;
+}
+
+static bool dsync_brain_run_real(struct dsync_brain *brain, bool *changed_r)
+{
+ enum dsync_state orig_state = brain->state;
+ enum dsync_box_state orig_box_recv_state = brain->box_recv_state;
+ enum dsync_box_state orig_box_send_state = brain->box_send_state;
+ bool changed = FALSE, ret = TRUE;
+
+ if (brain->failed)
+ return FALSE;
+
+ switch (brain->state) {
+ case DSYNC_STATE_MASTER_RECV_HANDSHAKE:
+ changed = dsync_brain_master_recv_handshake(brain);
+ break;
+ case DSYNC_STATE_SLAVE_RECV_HANDSHAKE:
+ changed = dsync_brain_slave_recv_handshake(brain);
+ break;
+ case DSYNC_STATE_MASTER_SEND_LAST_COMMON:
+ dsync_brain_master_send_last_common(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_STATE_SLAVE_RECV_LAST_COMMON:
+ changed = dsync_brain_slave_recv_last_common(brain);
+ break;
+ case DSYNC_STATE_SEND_MAILBOX_TREE:
+ dsync_brain_send_mailbox_tree(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_STATE_RECV_MAILBOX_TREE:
+ changed = dsync_brain_recv_mailbox_tree(brain);
+ break;
+ case DSYNC_STATE_SEND_MAILBOX_TREE_DELETES:
+ dsync_brain_send_mailbox_tree_deletes(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_STATE_RECV_MAILBOX_TREE_DELETES:
+ changed = dsync_brain_recv_mailbox_tree_deletes(brain);
+ break;
+ case DSYNC_STATE_MASTER_SEND_MAILBOX:
+ dsync_brain_master_send_mailbox(brain);
+ changed = TRUE;
+ break;
+ case DSYNC_STATE_SLAVE_RECV_MAILBOX:
+ changed = dsync_brain_slave_recv_mailbox(brain);
+ break;
+ case DSYNC_STATE_SYNC_MAILS:
+ changed = dsync_brain_sync_mails(brain);
+ break;
+ case DSYNC_STATE_FINISH:
+ changed = dsync_brain_finish(brain);
+ break;
+ case DSYNC_STATE_DONE:
+ changed = TRUE;
+ ret = FALSE;
+ break;
+ }
+ if (brain->verbose_proctitle) {
+ if (orig_state != brain->state ||
+ orig_box_recv_state != brain->box_recv_state ||
+ orig_box_send_state != brain->box_send_state ||
+ ++brain->proctitle_update_counter % 100 == 0)
+ process_title_set(dsync_brain_get_proctitle(brain));
+ }
+ *changed_r = changed;
+ return brain->failed ? FALSE : ret;
+}
+
+bool dsync_brain_run(struct dsync_brain *brain, bool *changed_r)
+{
+ bool ret;
+
+ *changed_r = FALSE;
+
+ if (dsync_ibc_has_failed(brain->ibc)) {
+ brain->failed = TRUE;
+ return FALSE;
+ }
+
+ T_BEGIN {
+ ret = dsync_brain_run_real(brain, changed_r);
+ } T_END;
+ return ret;
+}
+
+static void dsync_brain_mailbox_states_dump(struct dsync_brain *brain)
+{
+ struct hash_iterate_context *iter;
+ struct dsync_mailbox_state *state;
+ uint8_t *guid;
+
+ iter = hash_table_iterate_init(brain->mailbox_states);
+ while (hash_table_iterate(iter, brain->mailbox_states, &guid, &state)) {
+ i_debug("brain %c: Mailbox %s state: uidvalidity=%u uid=%u modseq=%"PRIu64" pvt_modseq=%"PRIu64" messages=%u changes_during_sync=%d",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(guid),
+ state->last_uidvalidity,
+ state->last_common_uid,
+ state->last_common_modseq,
+ state->last_common_pvt_modseq,
+ state->last_messages_count,
+ state->changes_during_sync ? 1 : 0);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+void dsync_brain_get_state(struct dsync_brain *brain, string_t *output)
+{
+ struct hash_iterate_context *iter;
+ struct dsync_mailbox_node *node;
+ const struct dsync_mailbox_state *new_state;
+ struct dsync_mailbox_state *state;
+ const uint8_t *guid_p;
+ uint8_t *guid;
+
+ if (brain->require_full_resync)
+ return;
+
+ /* update mailbox states */
+ array_foreach(&brain->remote_mailbox_states, new_state) {
+ guid_p = new_state->mailbox_guid;
+ state = hash_table_lookup(brain->mailbox_states, guid_p);
+ if (state != NULL)
+ *state = *new_state;
+ else
+ dsync_mailbox_state_add(brain, new_state);
+ }
+
+ /* remove nonexistent mailboxes */
+ iter = hash_table_iterate_init(brain->mailbox_states);
+ while (hash_table_iterate(iter, brain->mailbox_states, &guid, &state)) {
+ node = dsync_mailbox_tree_lookup_guid(brain->local_mailbox_tree,
+ guid);
+ if (node == NULL ||
+ node->existence != DSYNC_MAILBOX_NODE_EXISTS) {
+ if (brain->debug) {
+ i_debug("brain %c: Removed state for deleted mailbox %s",
+ brain->master_brain ? 'M' : 'S',
+ guid_128_to_string(guid));
+ }
+ hash_table_remove(brain->mailbox_states, guid);
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (brain->debug) {
+ i_debug("brain %c: Exported mailbox states:",
+ brain->master_brain ? 'M' : 'S');
+ dsync_brain_mailbox_states_dump(brain);
+ }
+ dsync_mailbox_states_export(brain->mailbox_states, output);
+}
+
+enum dsync_brain_sync_type dsync_brain_get_sync_type(struct dsync_brain *brain)
+{
+ return brain->sync_type;
+}
+
+bool dsync_brain_has_failed(struct dsync_brain *brain)
+{
+ return brain->failed;
+}
+
+const char *dsync_brain_get_unexpected_changes_reason(struct dsync_brain *brain,
+ bool *remote_only_r)
+{
+ if (brain->changes_during_sync == NULL &&
+ brain->changes_during_remote_sync) {
+ *remote_only_r = TRUE;
+ return "Remote notified that changes happened during sync";
+ }
+ *remote_only_r = FALSE;
+ return brain->changes_during_sync;
+}
+
+static bool dsync_brain_want_shared_namespace(const struct mail_namespace *ns,
+ const struct mail_namespace *sync_ns)
+{
+ /* Include shared namespaces and all its
+ children in the sync (e.g. "Shared/example.com"
+ will be synced to "Shared/").
+ This also allows "dsync -n Shared/example.com/"
+ with "Shared/example.com/username/" style
+ shared namespace config. */
+ return (ns->type == MAIL_NAMESPACE_TYPE_SHARED) &&
+ (sync_ns->type == MAIL_NAMESPACE_TYPE_SHARED) &&
+ str_begins(ns->prefix, sync_ns->prefix);
+}
+
+bool dsync_brain_want_namespace(struct dsync_brain *brain,
+ struct mail_namespace *ns)
+{
+ struct mail_namespace *sync_ns;
+
+ if (array_is_created(&brain->sync_namespaces)) {
+ array_foreach_elem(&brain->sync_namespaces, sync_ns) {
+ if (ns == sync_ns)
+ return TRUE;
+ if (dsync_brain_want_shared_namespace(ns, sync_ns))
+ return TRUE;
+ }
+ return FALSE;
+ }
+ if (ns->alias_for != NULL) {
+ /* always skip aliases */
+ return FALSE;
+ }
+ if (brain->sync_visible_namespaces) {
+ if ((ns->flags & NAMESPACE_FLAG_HIDDEN) == 0)
+ return TRUE;
+ if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) != 0)
+ return TRUE;
+ return FALSE;
+ } else {
+ return strcmp(ns->unexpanded_set->location,
+ SETTING_STRVAR_UNEXPANDED) == 0;
+ }
+}
+
+void dsync_brain_set_changes_during_sync(struct dsync_brain *brain,
+ const char *reason)
+{
+ if (brain->debug) {
+ i_debug("brain %c: Change during sync: %s",
+ brain->master_brain ? 'M' : 'S', reason);
+ }
+ if (brain->changes_during_sync == NULL)
+ brain->changes_during_sync = p_strdup(brain->pool, reason);
+}
diff --git a/src/doveadm/dsync/dsync-brain.h b/src/doveadm/dsync/dsync-brain.h
new file mode 100644
index 0000000..7677168
--- /dev/null
+++ b/src/doveadm/dsync/dsync-brain.h
@@ -0,0 +1,136 @@
+#ifndef DSYNC_BRAIN_H
+#define DSYNC_BRAIN_H
+
+#include "module-context.h"
+#include "guid.h"
+#include "mail-error.h"
+#include "mailbox-list-private.h"
+
+struct mail_namespace;
+struct mail_user;
+struct dsync_ibc;
+
+enum dsync_brain_flags {
+ DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS = 0x01,
+ DSYNC_BRAIN_FLAG_BACKUP_SEND = 0x02,
+ DSYNC_BRAIN_FLAG_BACKUP_RECV = 0x04,
+ DSYNC_BRAIN_FLAG_DEBUG = 0x08,
+ DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES= 0x10,
+ /* Sync everything but the actual mails (e.g. mailbox creates,
+ deletes) */
+ DSYNC_BRAIN_FLAG_NO_MAIL_SYNC = 0x20,
+ /* Used with BACKUP_SEND/RECV: Don't force the
+ Use the two-way syncing algorithm, but don't actually modify
+ anything locally. (Useful during migration.) */
+ DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE = 0x40,
+ /* Run storage purge on the remote after syncing.
+ Useful with e.g. a nightly doveadm backup. */
+ DSYNC_BRAIN_FLAG_PURGE_REMOTE = 0x80,
+ /* Don't prefetch mail bodies until they're actually needed. This works
+ only with pipe ibc. It's useful if most of the mails can be copied
+ directly within filesystem without having to read them. */
+ DSYNC_BRAIN_FLAG_NO_MAIL_PREFETCH = 0x100,
+ /* Add MAILBOX_TRANSACTION_FLAG_NO_NOTIFY to transactions. */
+ DSYNC_BRAIN_FLAG_NO_NOTIFY = 0x400,
+ /* Workaround missing Date/Message-ID headers */
+ DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND = 0x800,
+ /* If mail GUIDs aren't supported, don't emulate them with header
+ hashes either. This trusts that all the existing mails with
+ identical UIDs have the same email content. This makes it slightly
+ less safe, but can have huge performance improvement with imapc
+ if the remote server doesn't have a fast header cache. */
+ DSYNC_BRAIN_FLAG_NO_HEADER_HASHES = 0x1000,
+};
+
+enum dsync_brain_sync_type {
+ DSYNC_BRAIN_SYNC_TYPE_UNKNOWN,
+ /* Go through all mailboxes to make sure everything is synced */
+ DSYNC_BRAIN_SYNC_TYPE_FULL,
+ /* Go through all mailboxes that have changed (based on UIDVALIDITY,
+ UIDNEXT, HIGHESTMODSEQ). If both sides have had equal amount of
+ changes in some mailbox, it may get incorrectly skipped. */
+ DSYNC_BRAIN_SYNC_TYPE_CHANGED,
+ /* Use saved state to find out what has changed. */
+ DSYNC_BRAIN_SYNC_TYPE_STATE
+};
+
+struct dsync_brain_settings {
+ const char *process_title_prefix;
+ /* Sync only these namespaces */
+ ARRAY(struct mail_namespace *) sync_namespaces;
+ /* Sync only this mailbox name */
+ const char *sync_box;
+ /* Use this virtual \All mailbox to be able to copy mails with the same
+ GUID instead of saving them twice. With most storages this results
+ in less disk space usage. */
+ const char *virtual_all_box;
+ /* Sync only this mailbox GUID */
+ guid_128_t sync_box_guid;
+ /* Exclude these mailboxes from the sync. They can contain '*'
+ wildcards and be \special-use flags. */
+ const char *const *exclude_mailboxes;
+ /* Alternative character to use in mailbox names where the original
+ character cannot be used. */
+ char mailbox_alt_char;
+ /* Sync only mails with received timestamp at least this high. */
+ time_t sync_since_timestamp;
+ /* Sync only mails with received timestamp less or equal than this */
+ time_t sync_until_timestamp;
+ /* Don't sync mails larger than this. */
+ uoff_t sync_max_size;
+ /* Sync only mails which contains / doesn't contain this flag.
+ '-' at the beginning means this flag must not exist. */
+ const char *sync_flag;
+ /* Headers to hash (defaults to Date, Message-ID) */
+ const char *const *hashed_headers;
+
+ /* If non-zero, use dsync lock file for this user */
+ unsigned int lock_timeout_secs;
+ /* If non-zero, importing will attempt to commit transaction after
+ saving this many messages. */
+ unsigned int import_commit_msgs_interval;
+ /* Input state for DSYNC_BRAIN_SYNC_TYPE_STATE */
+ const char *state;
+};
+
+#define DSYNC_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, dsync_mailbox_list_module)
+struct dsync_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+ bool have_orig_escape_char;
+};
+extern MODULE_CONTEXT_DEFINE(dsync_mailbox_list_module,
+ &mailbox_list_module_register);
+
+struct dsync_brain *
+dsync_brain_master_init(struct mail_user *user, struct dsync_ibc *ibc,
+ enum dsync_brain_sync_type sync_type,
+ enum dsync_brain_flags flags,
+ const struct dsync_brain_settings *set);
+struct dsync_brain *
+dsync_brain_slave_init(struct mail_user *user, struct dsync_ibc *ibc,
+ bool local, const char *process_title_prefix,
+ char default_alt_char);
+/* Returns 0 if everything was successful, -1 if syncing failed in some way */
+int dsync_brain_deinit(struct dsync_brain **brain, enum mail_error *error_r);
+
+/* Returns TRUE if brain needs to run more, FALSE if it's finished.
+ changed_r is TRUE if anything happened during this run. */
+bool dsync_brain_run(struct dsync_brain *brain, bool *changed_r);
+/* Returns TRUE if brain has failed, and there's no point in continuing. */
+bool dsync_brain_has_failed(struct dsync_brain *brain);
+/* Returns the current sync state string, which can be given as parameter to
+ dsync_brain_master_init() to quickly sync only the new changes. */
+void dsync_brain_get_state(struct dsync_brain *brain, string_t *output);
+/* Returns the sync type that was used. Mainly useful with slave brain. */
+enum dsync_brain_sync_type dsync_brain_get_sync_type(struct dsync_brain *brain);
+/* If there were any unexpected changes during the sync, return the reason
+ for them. Otherwise return NULL. If remote_only_r=TRUE, this brain itself
+ didn't see any changes, but the remote brain did. */
+const char *dsync_brain_get_unexpected_changes_reason(struct dsync_brain *brain,
+ bool *remote_only_r);
+/* Returns TRUE if we want to sync this namespace. */
+bool dsync_brain_want_namespace(struct dsync_brain *brain,
+ struct mail_namespace *ns);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-deserializer.c b/src/doveadm/dsync/dsync-deserializer.c
new file mode 100644
index 0000000..2a90c47
--- /dev/null
+++ b/src/doveadm/dsync/dsync-deserializer.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "dsync-serializer.h"
+#include "dsync-deserializer.h"
+
+struct dsync_deserializer {
+ pool_t pool;
+ const char *name;
+ const char *const *required_fields;
+ const char *const *keys;
+ unsigned int *required_field_indexes;
+ unsigned int required_field_count;
+};
+
+struct dsync_deserializer_decoder {
+ pool_t pool;
+ struct dsync_deserializer *deserializer;
+ const char *const *values;
+ unsigned int values_count;
+};
+
+static bool field_find(const char *const *names, const char *name,
+ unsigned int *idx_r)
+{
+ unsigned int i;
+
+ for (i = 0; names[i] != NULL; i++) {
+ if (strcmp(names[i], name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+int dsync_deserializer_init(const char *name, const char *const *required_fields,
+ const char *header_line,
+ struct dsync_deserializer **deserializer_r,
+ const char **error_r)
+{
+ struct dsync_deserializer *deserializer;
+ const char **dup_required_fields;
+ unsigned int i, required_count;
+ pool_t pool;
+
+ *deserializer_r = NULL;
+
+ pool = pool_alloconly_create("dsync deserializer", 1024);
+ deserializer = p_new(pool, struct dsync_deserializer, 1);
+ deserializer->pool = pool;
+ deserializer->name = p_strdup(pool, name);
+ deserializer->keys = (void *)p_strsplit_tabescaped(pool, header_line);
+
+ deserializer->required_field_count = required_count =
+ required_fields == NULL ? 0 :
+ str_array_length(required_fields);
+ dup_required_fields = p_new(pool, const char *, required_count + 1);
+ deserializer->required_field_indexes =
+ p_new(pool, unsigned int, required_count + 1);
+ for (i = 0; i < required_count; i++) {
+ dup_required_fields[i] =
+ p_strdup(pool, required_fields[i]);
+ if (!field_find(deserializer->keys, required_fields[i],
+ &deserializer->required_field_indexes[i])) {
+ *error_r = t_strdup_printf(
+ "Header missing required field %s",
+ required_fields[i]);
+ pool_unref(&pool);
+ return -1;
+ }
+ }
+ deserializer->required_fields = dup_required_fields;
+
+ *deserializer_r = deserializer;
+ return 0;
+}
+
+void dsync_deserializer_deinit(struct dsync_deserializer **_deserializer)
+{
+ struct dsync_deserializer *deserializer = *_deserializer;
+
+ *_deserializer = NULL;
+
+ pool_unref(&deserializer->pool);
+}
+
+int dsync_deserializer_decode_begin(struct dsync_deserializer *deserializer,
+ const char *input,
+ struct dsync_deserializer_decoder **decoder_r,
+ const char **error_r)
+{
+ struct dsync_deserializer_decoder *decoder;
+ unsigned int i;
+ char **values;
+ pool_t pool;
+
+ *decoder_r = NULL;
+
+ pool = pool_alloconly_create("dsync deserializer decode", 1024);
+ decoder = p_new(pool, struct dsync_deserializer_decoder, 1);
+ decoder->pool = pool;
+ decoder->deserializer = deserializer;
+ values = p_strsplit_tabescaped(pool, input);
+
+ /* fix NULLs */
+ for (i = 0; values[i] != NULL; i++) {
+ if (values[i][0] == NULL_CHR) {
+ /* NULL? */
+ if (values[i][1] == '\0')
+ values[i] = NULL;
+ else
+ values[i] += 1;
+ }
+ }
+ decoder->values_count = i;
+
+ /* see if all required fields exist */
+ for (i = 0; i < deserializer->required_field_count; i++) {
+ unsigned int ridx = deserializer->required_field_indexes[i];
+
+ if (ridx >= decoder->values_count || values[ridx] == NULL) {
+ *error_r = t_strdup_printf("Missing required field %s",
+ deserializer->required_fields[i]);
+ pool_unref(&pool);
+ return -1;
+ }
+ }
+ decoder->values = (void *)values;
+
+ *decoder_r = decoder;
+ return 0;
+}
+
+static bool
+dsync_deserializer_find_field(struct dsync_deserializer *deserializer,
+ const char *key, unsigned int *idx_r)
+{
+ unsigned int i;
+
+ for (i = 0; deserializer->keys[i] != NULL; i++) {
+ if (strcmp(deserializer->keys[i], key) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool dsync_deserializer_decode_try(struct dsync_deserializer_decoder *decoder,
+ const char *key, const char **value_r)
+{
+ unsigned int idx;
+
+ if (!dsync_deserializer_find_field(decoder->deserializer, key, &idx) ||
+ idx >= decoder->values_count) {
+ *value_r = NULL;
+ return FALSE;
+ } else {
+ *value_r = decoder->values[idx];
+ return *value_r != NULL;
+ }
+}
+
+const char *
+dsync_deserializer_decode_get(struct dsync_deserializer_decoder *decoder,
+ const char *key)
+{
+ const char *value;
+
+ if (!dsync_deserializer_decode_try(decoder, key, &value)) {
+ i_panic("dsync_deserializer_decode_get() "
+ "used for non-required key %s", key);
+ }
+ return value;
+}
+
+const char *
+dsync_deserializer_decoder_get_name(struct dsync_deserializer_decoder *decoder)
+{
+ return decoder->deserializer->name;
+}
+
+void dsync_deserializer_decode_finish(struct dsync_deserializer_decoder **_decoder)
+{
+ struct dsync_deserializer_decoder *decoder = *_decoder;
+
+ *_decoder = NULL;
+
+ pool_unref(&decoder->pool);
+}
diff --git a/src/doveadm/dsync/dsync-deserializer.h b/src/doveadm/dsync/dsync-deserializer.h
new file mode 100644
index 0000000..45b19cc
--- /dev/null
+++ b/src/doveadm/dsync/dsync-deserializer.h
@@ -0,0 +1,27 @@
+#ifndef DSYNC_DESERIALIZER_H
+#define DSYNC_DESERIALIZER_H
+
+struct dsync_deserializer;
+struct dsync_deserializer_decoder;
+
+int dsync_deserializer_init(const char *name, const char *const *required_fields,
+ const char *header_line,
+ struct dsync_deserializer **deserializer_r,
+ const char **error_r);
+void dsync_deserializer_deinit(struct dsync_deserializer **deserializer);
+
+int dsync_deserializer_decode_begin(struct dsync_deserializer *deserializer,
+ const char *input,
+ struct dsync_deserializer_decoder **decoder_r,
+ const char **error_r);
+bool dsync_deserializer_decode_try(struct dsync_deserializer_decoder *decoder,
+ const char *key, const char **value_r);
+/* key must be in required fields. The return value is never NULL. */
+const char *
+dsync_deserializer_decode_get(struct dsync_deserializer_decoder *decoder,
+ const char *key);
+const char *
+dsync_deserializer_decoder_get_name(struct dsync_deserializer_decoder *decoder);
+void dsync_deserializer_decode_finish(struct dsync_deserializer_decoder **decoder);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-ibc-pipe.c b/src/doveadm/dsync/dsync-ibc-pipe.c
new file mode 100644
index 0000000..1b8886e
--- /dev/null
+++ b/src/doveadm/dsync/dsync-ibc-pipe.c
@@ -0,0 +1,599 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox.h"
+#include "dsync-mailbox-state.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-ibc-private.h"
+
+enum item_type {
+ ITEM_END_OF_LIST,
+ ITEM_HANDSHAKE,
+ ITEM_MAILBOX_STATE,
+ ITEM_MAILBOX_TREE_NODE,
+ ITEM_MAILBOX_DELETE,
+ ITEM_MAILBOX,
+ ITEM_MAILBOX_ATTRIBUTE,
+ ITEM_MAIL_CHANGE,
+ ITEM_MAIL_REQUEST,
+ ITEM_MAIL,
+ ITEM_FINISH
+};
+
+struct item {
+ enum item_type type;
+ pool_t pool;
+
+ union {
+ struct dsync_ibc_settings set;
+ struct dsync_mailbox_state state;
+ struct dsync_mailbox_node node;
+ guid_128_t mailbox_guid;
+ struct dsync_mailbox dsync_box;
+ struct dsync_mailbox_attribute attr;
+ struct dsync_mail_change change;
+ struct dsync_mail_request request;
+ struct dsync_mail mail;
+ struct {
+ const struct dsync_mailbox_delete *deletes;
+ unsigned int count;
+ char hierarchy_sep;
+ char escape_char;
+ } mailbox_delete;
+ struct {
+ const char *error;
+ enum mail_error mail_error;
+ bool require_full_resync;
+ } finish;
+ } u;
+};
+
+struct dsync_ibc_pipe {
+ struct dsync_ibc ibc;
+
+ ARRAY(pool_t) pools;
+ ARRAY(struct item) item_queue;
+ struct dsync_ibc_pipe *remote;
+
+ pool_t pop_pool;
+ struct item pop_item;
+};
+
+static pool_t dsync_ibc_pipe_get_pool(struct dsync_ibc_pipe *pipe)
+{
+ pool_t *pools, ret;
+ unsigned int count;
+
+ pools = array_get_modifiable(&pipe->pools, &count);
+ if (count == 0)
+ return pool_alloconly_create(MEMPOOL_GROWING"pipe item pool", 1024);
+
+ ret = pools[count-1];
+ array_delete(&pipe->pools, count-1, 1);
+ p_clear(ret);
+ return ret;
+}
+
+static struct item * ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_pipe_push_item(struct dsync_ibc_pipe *pipe, enum item_type type)
+{
+ struct item *item;
+
+ item = array_append_space(&pipe->item_queue);
+ item->type = type;
+
+ switch (type) {
+ case ITEM_END_OF_LIST:
+ case ITEM_MAILBOX_STATE:
+ case ITEM_MAILBOX_DELETE:
+ break;
+ case ITEM_HANDSHAKE:
+ case ITEM_MAILBOX:
+ case ITEM_MAILBOX_TREE_NODE:
+ case ITEM_MAILBOX_ATTRIBUTE:
+ case ITEM_MAIL_CHANGE:
+ case ITEM_MAIL_REQUEST:
+ case ITEM_MAIL:
+ case ITEM_FINISH:
+ item->pool = dsync_ibc_pipe_get_pool(pipe);
+ break;
+ }
+ return item;
+}
+
+static struct item *
+dsync_ibc_pipe_pop_item(struct dsync_ibc_pipe *pipe, enum item_type type)
+{
+ struct item *item;
+
+ if (array_count(&pipe->item_queue) == 0)
+ return NULL;
+
+ item = array_front_modifiable(&pipe->item_queue);
+ i_assert(item->type == type);
+ pipe->pop_item = *item;
+ array_pop_front(&pipe->item_queue);
+ item = NULL;
+
+ pool_unref(&pipe->pop_pool);
+ pipe->pop_pool = pipe->pop_item.pool;
+ return &pipe->pop_item;
+}
+
+static bool dsync_ibc_pipe_try_pop_eol(struct dsync_ibc_pipe *pipe)
+{
+ const struct item *item;
+
+ if (array_count(&pipe->item_queue) == 0)
+ return FALSE;
+
+ item = array_front(&pipe->item_queue);
+ if (item->type != ITEM_END_OF_LIST)
+ return FALSE;
+
+ array_pop_front(&pipe->item_queue);
+ return TRUE;
+}
+
+static void dsync_ibc_pipe_deinit(struct dsync_ibc *ibc)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+ pool_t pool;
+
+ if (pipe->remote != NULL) {
+ i_assert(pipe->remote->remote == pipe);
+ pipe->remote->remote = NULL;
+ }
+
+ pool_unref(&pipe->pop_pool);
+ array_foreach_modifiable(&pipe->item_queue, item) {
+ pool_unref(&item->pool);
+ }
+ array_foreach_elem(&pipe->pools, pool)
+ pool_unref(&pool);
+ array_free(&pipe->pools);
+ array_free(&pipe->item_queue);
+ i_free(pipe);
+}
+
+static void
+dsync_ibc_pipe_send_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings *set)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_HANDSHAKE);
+ item->u.set = *set;
+ item->u.set.sync_ns_prefixes =
+ p_strdup(item->pool, set->sync_ns_prefixes);
+ item->u.set.sync_box = p_strdup(item->pool, set->sync_box);
+ item->u.set.virtual_all_box = p_strdup(item->pool, set->virtual_all_box);
+ item->u.set.exclude_mailboxes = set->exclude_mailboxes == NULL ? NULL :
+ p_strarray_dup(item->pool, set->exclude_mailboxes);
+ memcpy(item->u.set.sync_box_guid, set->sync_box_guid,
+ sizeof(item->u.set.sync_box_guid));
+ item->u.set.sync_since_timestamp = set->sync_since_timestamp;
+ item->u.set.sync_until_timestamp = set->sync_until_timestamp;
+ item->u.set.sync_max_size = set->sync_max_size;
+ item->u.set.sync_flags = p_strdup(item->pool, set->sync_flags);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings **set_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_HANDSHAKE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *set_r = &item->u.set;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static bool dsync_ibc_pipe_is_send_queue_full(struct dsync_ibc *ibc)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+
+ return array_count(&pipe->remote->item_queue) > 0;
+}
+
+static bool dsync_ibc_pipe_has_pending_data(struct dsync_ibc *ibc)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+
+ return array_count(&pipe->item_queue) > 0;
+}
+
+static void
+dsync_ibc_pipe_send_end_of_list(struct dsync_ibc *ibc,
+ enum dsync_ibc_eol_type type ATTR_UNUSED)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+
+ dsync_ibc_pipe_push_item(pipe->remote, ITEM_END_OF_LIST);
+}
+
+static void
+dsync_ibc_pipe_send_mailbox_state(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_state *state)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_STATE);
+ item->u.state = *state;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox_state(struct dsync_ibc *ibc,
+ struct dsync_mailbox_state *state_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_STATE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *state_r = item->u.state;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const *name,
+ const struct dsync_mailbox_node *node)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_TREE_NODE);
+
+ /* a little bit kludgy way to send it */
+ item->u.node.name = (void *)p_strarray_dup(item->pool, name);
+ dsync_mailbox_node_copy_data(&item->u.node, node);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const **name_r,
+ const struct dsync_mailbox_node **node_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_TREE_NODE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *name_r = (const void *)item->u.node.name;
+ item->u.node.name = NULL;
+
+ *node_r = &item->u.node;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, char hierarchy_sep,
+ char escape_char)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_DELETE);
+
+ /* we'll assume that the deletes are permanent. this works for now.. */
+ /* a little bit kludgy way to send it */
+ item->u.mailbox_delete.deletes = deletes;
+ item->u.mailbox_delete.count = count;
+ item->u.mailbox_delete.hierarchy_sep = hierarchy_sep;
+ item->u.mailbox_delete.escape_char = escape_char;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete **deletes_r,
+ unsigned int *count_r,
+ char *hierarchy_sep_r,
+ char *escape_char_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_DELETE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *deletes_r = item->u.mailbox_delete.deletes;
+ *count_r = item->u.mailbox_delete.count;
+ *hierarchy_sep_r = item->u.mailbox_delete.hierarchy_sep;
+ *escape_char_r = item->u.mailbox_delete.escape_char;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox *dsync_box)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+ const struct mailbox_cache_field *cf;
+ struct mailbox_cache_field *ncf;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX);
+ item->u.dsync_box = *dsync_box;
+ p_array_init(&item->u.dsync_box.cache_fields, item->pool,
+ array_count(&dsync_box->cache_fields));
+ array_foreach(&dsync_box->cache_fields, cf) {
+ ncf = array_append_space(&item->u.dsync_box.cache_fields);
+ ncf->name = p_strdup(item->pool, cf->name);
+ ncf->decision = cf->decision;
+ ncf->last_used = cf->last_used;
+ }
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox **dsync_box_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *dsync_box_r = &item->u.dsync_box;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute *attr)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_ATTRIBUTE);
+ dsync_mailbox_attribute_dup(item->pool, attr, &item->u.attr);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute **attr_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_ATTRIBUTE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *attr_r = &item->u.attr;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change *change)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL_CHANGE);
+ dsync_mail_change_dup(item->pool, change, &item->u.change);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change **change_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL_CHANGE);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *change_r = &item->u.change;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request *request)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL_REQUEST);
+ item->u.request.guid = p_strdup(item->pool, request->guid);
+ item->u.request.uid = request->uid;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request **request_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL_REQUEST);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *request_r = &item->u.request;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL);
+ item->u.mail.guid = p_strdup(item->pool, mail->guid);
+ item->u.mail.uid = mail->uid;
+ item->u.mail.pop3_uidl = p_strdup(item->pool, mail->pop3_uidl);
+ item->u.mail.pop3_order = mail->pop3_order;
+ item->u.mail.received_date = mail->received_date;
+ if (mail->input != NULL) {
+ item->u.mail.input = mail->input;
+ i_stream_ref(mail->input);
+ }
+ item->u.mail.input_mail = mail->input_mail;
+ item->u.mail.input_mail_uid = mail->input_mail_uid;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ if (dsync_ibc_pipe_try_pop_eol(pipe))
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *mail_r = &item->u.mail;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_pipe_send_finish(struct dsync_ibc *ibc, const char *error,
+ enum mail_error mail_error,
+ bool require_full_resync)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_FINISH);
+ item->u.finish.error = p_strdup(item->pool, error);
+ item->u.finish.mail_error = mail_error;
+ item->u.finish.require_full_resync = require_full_resync;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_finish(struct dsync_ibc *ibc, const char **error_r,
+ enum mail_error *mail_error_r,
+ bool *require_full_resync_r)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+ struct item *item;
+
+ item = dsync_ibc_pipe_pop_item(pipe, ITEM_FINISH);
+ if (item == NULL)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ *error_r = item->u.finish.error;
+ *mail_error_r = item->u.finish.mail_error;
+ *require_full_resync_r = item->u.finish.require_full_resync;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void pipe_close_mail_streams(struct dsync_ibc_pipe *pipe)
+{
+ struct item *item;
+
+ if (array_count(&pipe->item_queue) > 0) {
+ item = array_front_modifiable(&pipe->item_queue);
+ if (item->type == ITEM_MAIL &&
+ item->u.mail.input != NULL)
+ i_stream_unref(&item->u.mail.input);
+ }
+}
+
+static void dsync_ibc_pipe_close_mail_streams(struct dsync_ibc *ibc)
+{
+ struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+
+ pipe_close_mail_streams(pipe);
+ pipe_close_mail_streams(pipe->remote);
+}
+
+static const struct dsync_ibc_vfuncs dsync_ibc_pipe_vfuncs = {
+ dsync_ibc_pipe_deinit,
+ dsync_ibc_pipe_send_handshake,
+ dsync_ibc_pipe_recv_handshake,
+ dsync_ibc_pipe_send_end_of_list,
+ dsync_ibc_pipe_send_mailbox_state,
+ dsync_ibc_pipe_recv_mailbox_state,
+ dsync_ibc_pipe_send_mailbox_tree_node,
+ dsync_ibc_pipe_recv_mailbox_tree_node,
+ dsync_ibc_pipe_send_mailbox_deletes,
+ dsync_ibc_pipe_recv_mailbox_deletes,
+ dsync_ibc_pipe_send_mailbox,
+ dsync_ibc_pipe_recv_mailbox,
+ dsync_ibc_pipe_send_mailbox_attribute,
+ dsync_ibc_pipe_recv_mailbox_attribute,
+ dsync_ibc_pipe_send_change,
+ dsync_ibc_pipe_recv_change,
+ dsync_ibc_pipe_send_mail_request,
+ dsync_ibc_pipe_recv_mail_request,
+ dsync_ibc_pipe_send_mail,
+ dsync_ibc_pipe_recv_mail,
+ dsync_ibc_pipe_send_finish,
+ dsync_ibc_pipe_recv_finish,
+ dsync_ibc_pipe_close_mail_streams,
+ dsync_ibc_pipe_is_send_queue_full,
+ dsync_ibc_pipe_has_pending_data
+};
+
+static struct dsync_ibc_pipe *
+dsync_ibc_pipe_alloc(void)
+{
+ struct dsync_ibc_pipe *pipe;
+
+ pipe = i_new(struct dsync_ibc_pipe, 1);
+ pipe->ibc.v = dsync_ibc_pipe_vfuncs;
+ i_array_init(&pipe->pools, 4);
+ i_array_init(&pipe->item_queue, 4);
+ return pipe;
+}
+
+void dsync_ibc_init_pipe(struct dsync_ibc **ibc1_r, struct dsync_ibc **ibc2_r)
+{
+ struct dsync_ibc_pipe *pipe1, *pipe2;
+
+ pipe1 = dsync_ibc_pipe_alloc();
+ pipe2 = dsync_ibc_pipe_alloc();
+ pipe1->remote = pipe2;
+ pipe2->remote = pipe1;
+ *ibc1_r = &pipe1->ibc;
+ *ibc2_r = &pipe2->ibc;
+}
diff --git a/src/doveadm/dsync/dsync-ibc-private.h b/src/doveadm/dsync/dsync-ibc-private.h
new file mode 100644
index 0000000..c055aca
--- /dev/null
+++ b/src/doveadm/dsync/dsync-ibc-private.h
@@ -0,0 +1,96 @@
+#ifndef DSYNC_IBC_PRIVATE_H
+#define DSYNC_IBC_PRIVATE_H
+
+#include "dsync-ibc.h"
+
+struct dsync_ibc_vfuncs {
+ void (*deinit)(struct dsync_ibc *ibc);
+
+ void (*send_handshake)(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings *set);
+ enum dsync_ibc_recv_ret
+ (*recv_handshake)(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings **set_r);
+
+ void (*send_end_of_list)(struct dsync_ibc *ibc,
+ enum dsync_ibc_eol_type type);
+
+ void (*send_mailbox_state)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_state *state);
+ enum dsync_ibc_recv_ret
+ (*recv_mailbox_state)(struct dsync_ibc *ibc,
+ struct dsync_mailbox_state *state_r);
+
+ void (*send_mailbox_tree_node)(struct dsync_ibc *ibc,
+ const char *const *name,
+ const struct dsync_mailbox_node *node);
+ enum dsync_ibc_recv_ret
+ (*recv_mailbox_tree_node)(struct dsync_ibc *ibc,
+ const char *const **name_r,
+ const struct dsync_mailbox_node **node_r);
+
+ void (*send_mailbox_deletes)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, char hierarchy_sep,
+ char escape_char);
+ enum dsync_ibc_recv_ret
+ (*recv_mailbox_deletes)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete **deletes_r,
+ unsigned int *count_r,
+ char *hierarchy_sep_r,
+ char *escape_char_r);
+
+ void (*send_mailbox)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox *dsync_box);
+ enum dsync_ibc_recv_ret
+ (*recv_mailbox)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox **dsync_box_r);
+
+ void (*send_mailbox_attribute)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute *attr);
+ enum dsync_ibc_recv_ret
+ (*recv_mailbox_attribute)(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute **attr_r);
+
+ void (*send_change)(struct dsync_ibc *ibc,
+ const struct dsync_mail_change *change);
+ enum dsync_ibc_recv_ret
+ (*recv_change)(struct dsync_ibc *ibc,
+ const struct dsync_mail_change **change_r);
+
+ void (*send_mail_request)(struct dsync_ibc *ibc,
+ const struct dsync_mail_request *request);
+ enum dsync_ibc_recv_ret
+ (*recv_mail_request)(struct dsync_ibc *ibc,
+ const struct dsync_mail_request **request_r);
+
+ void (*send_mail)(struct dsync_ibc *ibc,
+ const struct dsync_mail *mail);
+ enum dsync_ibc_recv_ret
+ (*recv_mail)(struct dsync_ibc *ibc,
+ struct dsync_mail **mail_r);
+
+ void (*send_finish)(struct dsync_ibc *ibc, const char *error,
+ enum mail_error mail_error,
+ bool require_full_resync);
+ enum dsync_ibc_recv_ret
+ (*recv_finish)(struct dsync_ibc *ibc, const char **error_r,
+ enum mail_error *mail_error_r,
+ bool *require_full_resync_r);
+
+ void (*close_mail_streams)(struct dsync_ibc *ibc);
+ bool (*is_send_queue_full)(struct dsync_ibc *ibc);
+ bool (*has_pending_data)(struct dsync_ibc *ibc);
+};
+
+struct dsync_ibc {
+ struct dsync_ibc_vfuncs v;
+
+ io_callback_t *io_callback;
+ void *io_context;
+
+ bool failed:1;
+ bool timeout:1;
+};
+
+#endif
diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c
new file mode 100644
index 0000000..17115b0
--- /dev/null
+++ b/src/doveadm/dsync/dsync-ibc-stream.c
@@ -0,0 +1,2138 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "safe-mkstemp.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-seekable.h"
+#include "istream-dot.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mail-cache.h"
+#include "mail-storage-private.h"
+#include "dsync-serializer.h"
+#include "dsync-deserializer.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox.h"
+#include "dsync-mailbox-state.h"
+#include "dsync-mailbox-tree.h"
+#include "dsync-ibc-private.h"
+
+
+#define DSYNC_IBC_STREAM_OUTBUF_THROTTLE_SIZE (1024*128)
+
+#define DSYNC_PROTOCOL_VERSION_MAJOR 3
+#define DSYNC_PROTOCOL_VERSION_MINOR 5
+#define DSYNC_HANDSHAKE_VERSION "VERSION\tdsync\t3\t5\n"
+
+#define DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES 1
+#define DSYNC_PROTOCOL_MINOR_HAVE_SAVE_GUID 2
+#define DSYNC_PROTOCOL_MINOR_HAVE_FINISH 3
+#define DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V2 4
+#define DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V3 5
+
+enum item_type {
+ ITEM_NONE,
+ ITEM_DONE,
+
+ ITEM_HANDSHAKE,
+ ITEM_MAILBOX_STATE,
+ ITEM_MAILBOX_TREE_NODE,
+ ITEM_MAILBOX_DELETE,
+ ITEM_MAILBOX,
+
+ ITEM_MAILBOX_ATTRIBUTE,
+ ITEM_MAIL_CHANGE,
+ ITEM_MAIL_REQUEST,
+ ITEM_MAIL,
+ ITEM_FINISH,
+
+ ITEM_MAILBOX_CACHE_FIELD,
+
+ ITEM_END_OF_LIST
+};
+
+#define END_OF_LIST_LINE "."
+static const struct {
+ /* full human readable name of the item */
+ const char *name;
+ /* unique character identifying the item */
+ char chr;
+ const char *required_keys;
+ const char *optional_keys;
+ unsigned int min_minor_version;
+} items[ITEM_END_OF_LIST+1] = {
+ { NULL, '\0', NULL, NULL, 0 },
+ { .name = "done",
+ .chr = 'X',
+ .optional_keys = ""
+ },
+ { .name = "handshake",
+ .chr = 'H',
+ .required_keys = "hostname",
+ .optional_keys = "sync_ns_prefix sync_box sync_box_guid sync_type "
+ "debug sync_visible_namespaces exclude_mailboxes "
+ "send_mail_requests backup_send backup_recv lock_timeout "
+ "no_mail_sync no_backup_overwrite purge_remote "
+ "no_notify sync_since_timestamp sync_max_size sync_flags sync_until_timestamp "
+ "virtual_all_box empty_hdr_workaround import_commit_msgs_interval "
+ "hashed_headers alt_char"
+ },
+ { .name = "mailbox_state",
+ .chr = 'S',
+ .required_keys = "mailbox_guid last_uidvalidity last_common_uid "
+ "last_common_modseq last_common_pvt_modseq",
+ .optional_keys = "last_messages_count changes_during_sync"
+ },
+ { .name = "mailbox_tree_node",
+ .chr = 'N',
+ .required_keys = "name existence",
+ .optional_keys = "mailbox_guid uid_validity uid_next "
+ "last_renamed_or_created subscribed last_subscription_change"
+ },
+ { .name = "mailbox_delete",
+ .chr = 'D',
+ .required_keys = "hierarchy_sep",
+ .optional_keys = "escape_char mailboxes dirs unsubscribes"
+ },
+ { .name = "mailbox",
+ .chr = 'B',
+ .required_keys = "mailbox_guid uid_validity uid_next messages_count "
+ "first_recent_uid highest_modseq highest_pvt_modseq",
+ .optional_keys = "mailbox_lost mailbox_ignore "
+ "cache_fields have_guids have_save_guids have_only_guid128"
+ },
+ { .name = "mailbox_attribute",
+ .chr = 'A',
+ .required_keys = "type key",
+ .optional_keys = "value stream deleted last_change modseq",
+ .min_minor_version = DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES
+ },
+ { .name = "mail_change",
+ .chr = 'C',
+ .required_keys = "type uid",
+ .optional_keys = "guid hdr_hash modseq pvt_modseq "
+ "add_flags remove_flags final_flags "
+ "keywords_reset keyword_changes received_timestamp virtual_size"
+ },
+ { .name = "mail_request",
+ .chr = 'R',
+ .optional_keys = "guid uid"
+ },
+ { .name = "mail",
+ .chr = 'M',
+ .optional_keys = "guid uid pop3_uidl pop3_order received_date saved_date stream"
+ },
+ { .name = "finish",
+ .chr = 'F',
+ .optional_keys = "error mail_error require_full_resync",
+ .min_minor_version = DSYNC_PROTOCOL_MINOR_HAVE_FINISH
+ },
+ { .name = "mailbox_cache_field",
+ .chr = 'c',
+ .required_keys = "name decision",
+ .optional_keys = "last_used"
+ },
+
+ { "end_of_list", '\0', NULL, NULL, 0 }
+};
+
+struct dsync_ibc_stream {
+ struct dsync_ibc ibc;
+
+ char *name, *temp_path_prefix;
+ unsigned int timeout_secs;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+ struct timeout *to;
+
+ unsigned int minor_version;
+ struct dsync_serializer *serializers[ITEM_END_OF_LIST];
+ struct dsync_deserializer *deserializers[ITEM_END_OF_LIST];
+
+ pool_t ret_pool;
+ struct dsync_deserializer_decoder *cur_decoder;
+
+ struct istream *value_output, *value_input;
+ struct dsync_mail *cur_mail;
+ struct dsync_mailbox_attribute *cur_attr;
+ char value_output_last;
+
+ enum item_type last_recv_item, last_sent_item;
+ bool last_recv_item_eol:1;
+ bool last_sent_item_eol:1;
+
+ bool version_received:1;
+ bool handshake_received:1;
+ bool has_pending_data:1;
+ bool finish_received:1;
+ bool done_received:1;
+ bool stopped:1;
+};
+
+static const char *dsync_ibc_stream_get_state(struct dsync_ibc_stream *ibc)
+{
+ if (!ibc->version_received)
+ return "version not received";
+ else if (!ibc->handshake_received)
+ return "handshake not received";
+
+ return t_strdup_printf("last sent=%s%s, last recv=%s%s",
+ items[ibc->last_sent_item].name,
+ ibc->last_sent_item_eol ? " (EOL)" : "",
+ items[ibc->last_recv_item].name,
+ ibc->last_recv_item_eol ? " (EOL)" : "");
+}
+
+static void dsync_ibc_stream_stop(struct dsync_ibc_stream *ibc)
+{
+ ibc->stopped = TRUE;
+ i_stream_close(ibc->input);
+ o_stream_close(ibc->output);
+ io_loop_stop(current_ioloop);
+}
+
+static int dsync_ibc_stream_read_mail_stream(struct dsync_ibc_stream *ibc)
+{
+ do {
+ i_stream_skip(ibc->value_input,
+ i_stream_get_data_size(ibc->value_input));
+ } while (i_stream_read(ibc->value_input) > 0);
+ if (ibc->value_input->eof) {
+ if (ibc->value_input->stream_errno != 0) {
+ i_error("dsync(%s): read(%s) failed: %s (%s)", ibc->name,
+ i_stream_get_name(ibc->value_input),
+ i_stream_get_error(ibc->value_input),
+ dsync_ibc_stream_get_state(ibc));
+ dsync_ibc_stream_stop(ibc);
+ return -1;
+ }
+ /* finished reading the mail stream */
+ i_assert(ibc->value_input->eof);
+ i_stream_seek(ibc->value_input, 0);
+ ibc->has_pending_data = TRUE;
+ ibc->value_input = NULL;
+ return 1;
+ }
+ return 0;
+}
+
+static void dsync_ibc_stream_input(struct dsync_ibc_stream *ibc)
+{
+ timeout_reset(ibc->to);
+ if (ibc->value_input != NULL) {
+ if (dsync_ibc_stream_read_mail_stream(ibc) == 0)
+ return;
+ }
+ o_stream_cork(ibc->output);
+ ibc->ibc.io_callback(ibc->ibc.io_context);
+ o_stream_uncork(ibc->output);
+}
+
+static int dsync_ibc_stream_send_value_stream(struct dsync_ibc_stream *ibc)
+{
+ const unsigned char *data;
+ unsigned char add;
+ size_t i, size;
+ int ret;
+
+ while ((ret = i_stream_read_more(ibc->value_output, &data, &size)) > 0) {
+ add = '\0';
+ for (i = 0; i < size; i++) {
+ if (data[i] == '.' &&
+ ((i == 0 && ibc->value_output_last == '\n') ||
+ (i > 0 && data[i-1] == '\n'))) {
+ /* escape the dot */
+ add = '.';
+ break;
+ }
+ }
+
+ if (i > 0) {
+ o_stream_nsend(ibc->output, data, i);
+ ibc->value_output_last = data[i-1];
+ i_stream_skip(ibc->value_output, i);
+ }
+
+ if (o_stream_get_buffer_used_size(ibc->output) >= 4096) {
+ if ((ret = o_stream_flush(ibc->output)) < 0) {
+ dsync_ibc_stream_stop(ibc);
+ return -1;
+ }
+ if (ret == 0) {
+ /* continue later */
+ o_stream_set_flush_pending(ibc->output, TRUE);
+ return 0;
+ }
+ }
+
+ if (add != '\0') {
+ o_stream_nsend(ibc->output, &add, 1);
+ ibc->value_output_last = add;
+ }
+ }
+ i_assert(ret == -1);
+
+ if (ibc->value_output->stream_errno != 0) {
+ i_error("dsync(%s): read(%s) failed: %s (%s)",
+ ibc->name, i_stream_get_name(ibc->value_output),
+ i_stream_get_error(ibc->value_output),
+ dsync_ibc_stream_get_state(ibc));
+ dsync_ibc_stream_stop(ibc);
+ return -1;
+ }
+
+ /* finished sending the stream. use "CRLF." instead of "LF." just in
+ case we're sending binary data that ends with CR. */
+ o_stream_nsend_str(ibc->output, "\r\n.\r\n");
+ i_stream_unref(&ibc->value_output);
+ return 1;
+}
+
+static int dsync_ibc_stream_output(struct dsync_ibc_stream *ibc)
+{
+ struct ostream *output = ibc->output;
+ int ret;
+
+ if ((ret = o_stream_flush(output)) < 0)
+ ret = 1;
+ else if (ibc->value_output != NULL) {
+ if (dsync_ibc_stream_send_value_stream(ibc) < 0)
+ ret = 1;
+ }
+ timeout_reset(ibc->to);
+
+ if (!dsync_ibc_is_send_queue_full(&ibc->ibc))
+ ibc->ibc.io_callback(ibc->ibc.io_context);
+ return ret;
+}
+
+static void dsync_ibc_stream_timeout(struct dsync_ibc_stream *ibc)
+{
+ i_error("dsync(%s): I/O has stalled, no activity for %u seconds (%s)",
+ ibc->name, ibc->timeout_secs, dsync_ibc_stream_get_state(ibc));
+ ibc->ibc.timeout = TRUE;
+ dsync_ibc_stream_stop(ibc);
+}
+
+static void dsync_ibc_stream_init(struct dsync_ibc_stream *ibc)
+{
+ unsigned int i;
+
+ ibc->io = io_add_istream(ibc->input, dsync_ibc_stream_input, ibc);
+ io_set_pending(ibc->io);
+ o_stream_set_no_error_handling(ibc->output, TRUE);
+ o_stream_set_flush_callback(ibc->output, dsync_ibc_stream_output, ibc);
+ ibc->to = timeout_add(ibc->timeout_secs * 1000,
+ dsync_ibc_stream_timeout, ibc);
+ o_stream_cork(ibc->output);
+ o_stream_nsend_str(ibc->output, DSYNC_HANDSHAKE_VERSION);
+
+ /* initialize serializers and send their headers to remote */
+ for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) T_BEGIN {
+ const char *keys;
+
+ keys = items[i].required_keys == NULL ? items[i].optional_keys :
+ t_strconcat(items[i].required_keys, " ",
+ items[i].optional_keys, NULL);
+ if (keys != NULL) {
+ i_assert(items[i].chr != '\0');
+
+ ibc->serializers[i] =
+ dsync_serializer_init(t_strsplit_spaces(keys, " "));
+ o_stream_nsend(ibc->output, &items[i].chr, 1);
+ o_stream_nsend_str(ibc->output,
+ dsync_serializer_encode_header_line(ibc->serializers[i]));
+ }
+ } T_END;
+ o_stream_nsend_str(ibc->output, ".\n");
+ o_stream_uncork(ibc->output);
+}
+
+static void dsync_ibc_stream_deinit(struct dsync_ibc *_ibc)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ unsigned int i;
+
+ for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) {
+ if (ibc->serializers[i] != NULL)
+ dsync_serializer_deinit(&ibc->serializers[i]);
+ if (ibc->deserializers[i] != NULL)
+ dsync_deserializer_deinit(&ibc->deserializers[i]);
+ }
+ if (ibc->cur_decoder != NULL)
+ dsync_deserializer_decode_finish(&ibc->cur_decoder);
+ if (ibc->value_output != NULL)
+ i_stream_unref(&ibc->value_output);
+ else {
+ /* If the remote has not told us that they are closing we
+ notify remote that we're closing. this is mainly to avoid
+ "read() failed: EOF" errors on failing dsyncs.
+
+ Avoid a race condition:
+ We do not tell the remote we are closing if they have
+ already told us because they close the
+ connection after sending ITEM_DONE and will
+ not be ever receive anything else from us unless
+ it just happens to get combined into the same packet
+ as a previous response and is already in the buffer.
+ */
+ if (!ibc->done_received && !ibc->finish_received) {
+ o_stream_nsend_str(ibc->output,
+ t_strdup_printf("%c\n", items[ITEM_DONE].chr));
+ }
+ (void)o_stream_finish(ibc->output);
+ }
+
+ timeout_remove(&ibc->to);
+ io_remove(&ibc->io);
+ i_stream_destroy(&ibc->input);
+ o_stream_destroy(&ibc->output);
+ pool_unref(&ibc->ret_pool);
+ i_free(ibc->temp_path_prefix);
+ i_free(ibc->name);
+ i_free(ibc);
+}
+
+static int dsync_ibc_stream_next_line(struct dsync_ibc_stream *ibc,
+ const char **line_r)
+{
+ string_t *error;
+ const char *line;
+ ssize_t ret;
+
+ line = i_stream_next_line(ibc->input);
+ if (line != NULL) {
+ *line_r = line;
+ return 1;
+ }
+ /* try reading some */
+ if ((ret = i_stream_read(ibc->input)) == -1) {
+ if (ibc->stopped)
+ return -1;
+ error = t_str_new(128);
+ if (ibc->input->stream_errno != 0) {
+ str_printfa(error, "read(%s) failed: %s", ibc->name,
+ i_stream_get_error(ibc->input));
+ } else {
+ i_assert(ibc->input->eof);
+ str_printfa(error, "read(%s) failed: EOF", ibc->name);
+ }
+ str_printfa(error, " (%s)", dsync_ibc_stream_get_state(ibc));
+ i_error("%s", str_c(error));
+ dsync_ibc_stream_stop(ibc);
+ return -1;
+ }
+ i_assert(ret >= 0);
+ *line_r = i_stream_next_line(ibc->input);
+ if (*line_r == NULL) {
+ ibc->has_pending_data = FALSE;
+ return 0;
+ }
+ ibc->has_pending_data = TRUE;
+ return 1;
+}
+
+static void ATTR_FORMAT(3, 4) ATTR_NULL(2)
+dsync_ibc_input_error(struct dsync_ibc_stream *ibc,
+ struct dsync_deserializer_decoder *decoder,
+ const char *fmt, ...)
+{
+ va_list args;
+ const char *error;
+
+ va_start(args, fmt);
+ error = t_strdup_vprintf(fmt, args);
+ if (decoder == NULL)
+ i_error("dsync(%s): %s", ibc->name, error);
+ else {
+ i_error("dsync(%s): %s: %s", ibc->name,
+ dsync_deserializer_decoder_get_name(decoder), error);
+ }
+ va_end(args);
+
+ dsync_ibc_stream_stop(ibc);
+}
+
+static void
+dsync_ibc_stream_send_string(struct dsync_ibc_stream *ibc,
+ const string_t *str)
+{
+ i_assert(ibc->value_output == NULL);
+ o_stream_nsend(ibc->output, str_data(str), str_len(str));
+}
+
+static int seekable_fd_callback(const char **path_r, void *context)
+{
+ struct dsync_ibc_stream *ibc = context;
+ string_t *path;
+ int fd;
+
+ path = t_str_new(128);
+ str_append(path, ibc->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;
+}
+
+static struct istream *
+dsync_ibc_stream_input_stream(struct dsync_ibc_stream *ibc)
+{
+ struct istream *inputs[2];
+
+ inputs[0] = i_stream_create_dot(ibc->input, FALSE);
+ inputs[1] = NULL;
+ ibc->value_input = i_stream_create_seekable(inputs, MAIL_READ_FULL_BLOCK_SIZE,
+ seekable_fd_callback, ibc);
+ i_stream_unref(&inputs[0]);
+
+ return ibc->value_input;
+}
+
+static int
+dsync_ibc_check_missing_deserializers(struct dsync_ibc_stream *ibc)
+{
+ unsigned int i;
+ int ret = 0;
+
+ for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) {
+ if (ibc->deserializers[i] == NULL &&
+ ibc->minor_version >= items[i].min_minor_version &&
+ (items[i].required_keys != NULL ||
+ items[i].optional_keys != NULL)) {
+ dsync_ibc_input_error(ibc, NULL,
+ "Remote didn't handshake deserializer for %s",
+ items[i].name);
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static bool
+dsync_ibc_stream_handshake(struct dsync_ibc_stream *ibc, const char *line)
+{
+ enum item_type item = ITEM_NONE;
+ const char *const *required_keys, *error;
+ unsigned int i;
+
+ if (ibc->handshake_received)
+ return TRUE;
+
+ if (!ibc->version_received) {
+ if (!version_string_verify_full(line, "dsync",
+ DSYNC_PROTOCOL_VERSION_MAJOR,
+ &ibc->minor_version)) {
+ dsync_ibc_input_error(ibc, NULL,
+ "Remote dsync doesn't use compatible protocol");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ ibc->version_received = TRUE;
+ return FALSE;
+ }
+
+ if (strcmp(line, END_OF_LIST_LINE) == 0) {
+ /* finished handshaking */
+ if (dsync_ibc_check_missing_deserializers(ibc) < 0)
+ return FALSE;
+ ibc->handshake_received = TRUE;
+ ibc->last_recv_item = ITEM_HANDSHAKE;
+ return FALSE;
+ }
+
+ for (i = 1; i < ITEM_END_OF_LIST; i++) {
+ if (items[i].chr == line[0]) {
+ item = i;
+ break;
+ }
+ }
+ if (item == ITEM_NONE) {
+ /* unknown deserializer, ignore */
+ return FALSE;
+ }
+
+ required_keys = items[item].required_keys == NULL ? NULL :
+ t_strsplit(items[item].required_keys, " ");
+ if (dsync_deserializer_init(items[item].name,
+ required_keys, line + 1,
+ &ibc->deserializers[item], &error) < 0) {
+ dsync_ibc_input_error(ibc, NULL,
+ "Remote sent invalid handshake for %s: %s",
+ items[item].name, error);
+ }
+ return FALSE;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_input_next(struct dsync_ibc_stream *ibc, enum item_type item,
+ struct dsync_deserializer_decoder **decoder_r)
+{
+ enum item_type line_item = ITEM_NONE;
+ const char *line, *error;
+ unsigned int i;
+
+ i_assert(ibc->value_input == NULL);
+
+ timeout_reset(ibc->to);
+
+ do {
+ if (dsync_ibc_stream_next_line(ibc, &line) <= 0)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ } while (!dsync_ibc_stream_handshake(ibc, line));
+
+ ibc->last_recv_item = item;
+ ibc->last_recv_item_eol = FALSE;
+
+ if (strcmp(line, END_OF_LIST_LINE) == 0) {
+ /* end of this list */
+ ibc->last_recv_item_eol = TRUE;
+ return DSYNC_IBC_RECV_RET_FINISHED;
+ }
+ if (line[0] == items[ITEM_DONE].chr) {
+ /* remote cleanly closed the connection, possibly because of
+ some failure (which it should have logged). we don't want to
+ log any stream errors anyway after this. */
+ ibc->done_received = TRUE;
+ dsync_ibc_stream_stop(ibc);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+ }
+ for (i = 1; i < ITEM_END_OF_LIST; i++) {
+ if (*line == items[i].chr) {
+ line_item = i;
+ break;
+ }
+ }
+ if (line_item != item) {
+ dsync_ibc_input_error(ibc, NULL,
+ "Received unexpected input %c != %c",
+ *line, items[item].chr);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ if (ibc->cur_decoder != NULL)
+ dsync_deserializer_decode_finish(&ibc->cur_decoder);
+ if (dsync_deserializer_decode_begin(ibc->deserializers[item],
+ line+1, &ibc->cur_decoder,
+ &error) < 0) {
+ dsync_ibc_input_error(ibc, NULL, "Invalid input to %s: %s",
+ items[item].name, error);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ *decoder_r = ibc->cur_decoder;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static struct dsync_serializer_encoder *
+dsync_ibc_send_encode_begin(struct dsync_ibc_stream *ibc, enum item_type item)
+{
+ ibc->last_sent_item = item;
+ ibc->last_sent_item_eol = FALSE;
+ return dsync_serializer_encode_begin(ibc->serializers[item]);
+}
+
+static void
+dsync_ibc_stream_send_handshake(struct dsync_ibc *_ibc,
+ const struct dsync_ibc_settings *set)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+ char sync_type[2];
+
+ str_append_c(str, items[ITEM_HANDSHAKE].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_HANDSHAKE);
+ dsync_serializer_encode_add(encoder, "hostname", set->hostname);
+ if (set->sync_ns_prefixes != NULL) {
+ dsync_serializer_encode_add(encoder, "sync_ns_prefix",
+ set->sync_ns_prefixes);
+ }
+ if (set->sync_box != NULL)
+ dsync_serializer_encode_add(encoder, "sync_box", set->sync_box);
+ if (set->virtual_all_box != NULL) {
+ dsync_serializer_encode_add(encoder, "virtual_all_box",
+ set->virtual_all_box);
+ }
+ if (set->exclude_mailboxes != NULL) {
+ string_t *substr = t_str_new(64);
+ unsigned int i;
+
+ for (i = 0; set->exclude_mailboxes[i] != NULL; i++) {
+ if (i != 0)
+ str_append_c(substr, '\t');
+ str_append_tabescaped(substr, set->exclude_mailboxes[i]);
+ }
+ dsync_serializer_encode_add(encoder, "exclude_mailboxes",
+ str_c(substr));
+ }
+ if (!guid_128_is_empty(set->sync_box_guid)) {
+ dsync_serializer_encode_add(encoder, "sync_box_guid",
+ guid_128_to_string(set->sync_box_guid));
+ }
+
+ sync_type[0] = sync_type[1] = '\0';
+ switch (set->sync_type) {
+ case DSYNC_BRAIN_SYNC_TYPE_UNKNOWN:
+ break;
+ case DSYNC_BRAIN_SYNC_TYPE_FULL:
+ sync_type[0] = 'f';
+ break;
+ case DSYNC_BRAIN_SYNC_TYPE_CHANGED:
+ sync_type[0] = 'c';
+ break;
+ case DSYNC_BRAIN_SYNC_TYPE_STATE:
+ sync_type[0] = 's';
+ break;
+ }
+ if (sync_type[0] != '\0')
+ dsync_serializer_encode_add(encoder, "sync_type", sync_type);
+ if (set->lock_timeout > 0) {
+ dsync_serializer_encode_add(encoder, "lock_timeout",
+ t_strdup_printf("%u", set->lock_timeout));
+ }
+ if (set->import_commit_msgs_interval > 0) {
+ dsync_serializer_encode_add(encoder, "import_commit_msgs_interval",
+ t_strdup_printf("%u", set->import_commit_msgs_interval));
+ }
+ if (set->sync_since_timestamp > 0) {
+ dsync_serializer_encode_add(encoder, "sync_since_timestamp",
+ t_strdup_printf("%ld", (long)set->sync_since_timestamp));
+ }
+ if (set->sync_until_timestamp > 0) {
+ dsync_serializer_encode_add(encoder, "sync_until_timestamp",
+ t_strdup_printf("%ld", (long)set->sync_since_timestamp));
+ }
+ if (set->sync_max_size > 0) {
+ dsync_serializer_encode_add(encoder, "sync_max_size",
+ t_strdup_printf("%"PRIu64, set->sync_max_size));
+ }
+ if (set->sync_flags != NULL) {
+ dsync_serializer_encode_add(encoder, "sync_flags",
+ set->sync_flags);
+ }
+ if (set->alt_char != '\0') {
+ dsync_serializer_encode_add(encoder, "alt_char",
+ t_strdup_printf("%c", set->alt_char));
+ }
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS) != 0)
+ dsync_serializer_encode_add(encoder, "send_mail_requests", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0)
+ dsync_serializer_encode_add(encoder, "backup_send", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0)
+ dsync_serializer_encode_add(encoder, "backup_recv", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_DEBUG) != 0)
+ dsync_serializer_encode_add(encoder, "debug", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES) != 0)
+ dsync_serializer_encode_add(encoder, "sync_visible_namespaces", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_MAIL_SYNC) != 0)
+ dsync_serializer_encode_add(encoder, "no_mail_sync", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE) != 0)
+ dsync_serializer_encode_add(encoder, "no_backup_overwrite", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_PURGE_REMOTE) != 0)
+ dsync_serializer_encode_add(encoder, "purge_remote", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_NOTIFY) != 0)
+ dsync_serializer_encode_add(encoder, "no_notify", "");
+ if ((set->brain_flags & DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND) != 0)
+ dsync_serializer_encode_add(encoder, "empty_hdr_workaround", "");
+ /* this can be NULL in slave */
+ string_t *str2 = t_str_new(32);
+ if (set->hashed_headers != NULL) {
+ for(const char *const *ptr = set->hashed_headers; *ptr != NULL; ptr++) {
+ str_append_tabescaped(str2, *ptr);
+ str_append_c(str2, '\t');
+ }
+ }
+ dsync_serializer_encode_add(encoder, "hashed_headers", str_c(str2));
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_handshake(struct dsync_ibc *_ibc,
+ const struct dsync_ibc_settings **set_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_ibc_settings *set;
+ const char *value;
+ pool_t pool = ibc->ret_pool;
+ enum dsync_ibc_recv_ret ret;
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_HANDSHAKE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK) {
+ if (ret != DSYNC_IBC_RECV_RET_TRYAGAIN) {
+ i_error("dsync(%s): Unexpected input in handshake",
+ ibc->name);
+ dsync_ibc_stream_stop(ibc);
+ }
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ p_clear(pool);
+ set = p_new(pool, struct dsync_ibc_settings, 1);
+
+ value = dsync_deserializer_decode_get(decoder, "hostname");
+ set->hostname = p_strdup(pool, value);
+ /* now that we know the remote's hostname, use it for the
+ stream's name */
+ i_free(ibc->name);
+ ibc->name = i_strdup(set->hostname);
+
+ if (dsync_deserializer_decode_try(decoder, "sync_ns_prefix", &value))
+ set->sync_ns_prefixes = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "sync_box", &value))
+ set->sync_box = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "virtual_all_box", &value))
+ set->virtual_all_box = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "sync_box_guid", &value) &&
+ guid_128_from_string(value, set->sync_box_guid) < 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid sync_box_guid: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "exclude_mailboxes", &value) &&
+ *value != '\0') {
+ char **boxes = p_strsplit_tabescaped(pool, value);
+ set->exclude_mailboxes = (const void *)boxes;
+ }
+ if (dsync_deserializer_decode_try(decoder, "sync_type", &value)) {
+ switch (value[0]) {
+ case 'f':
+ set->sync_type = DSYNC_BRAIN_SYNC_TYPE_FULL;
+ break;
+ case 'c':
+ set->sync_type = DSYNC_BRAIN_SYNC_TYPE_CHANGED;
+ break;
+ case 's':
+ set->sync_type = DSYNC_BRAIN_SYNC_TYPE_STATE;
+ break;
+ default:
+ dsync_ibc_input_error(ibc, decoder,
+ "Unknown sync_type: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "lock_timeout", &value)) {
+ if (str_to_uint(value, &set->lock_timeout) < 0 ||
+ set->lock_timeout == 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid lock_timeout: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "import_commit_msgs_interval", &value)) {
+ if (str_to_uint(value, &set->import_commit_msgs_interval) < 0 ||
+ set->import_commit_msgs_interval == 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid import_commit_msgs_interval: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "sync_since_timestamp", &value)) {
+ if (str_to_time(value, &set->sync_since_timestamp) < 0 ||
+ set->sync_since_timestamp == 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid sync_since_timestamp: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "sync_until_timestamp", &value)) {
+ if (str_to_time(value, &set->sync_until_timestamp) < 0 ||
+ set->sync_until_timestamp == 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid sync_until_timestamp: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "sync_max_size", &value)) {
+ if (str_to_uoff(value, &set->sync_max_size) < 0 ||
+ set->sync_max_size == 0) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid sync_max_size: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "sync_flags", &value))
+ set->sync_flags = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "alt_char", &value))
+ set->alt_char = value[0];
+ if (dsync_deserializer_decode_try(decoder, "send_mail_requests", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS;
+ if (dsync_deserializer_decode_try(decoder, "backup_send", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_SEND;
+ if (dsync_deserializer_decode_try(decoder, "backup_recv", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_RECV;
+ if (dsync_deserializer_decode_try(decoder, "debug", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_DEBUG;
+ if (dsync_deserializer_decode_try(decoder, "sync_visible_namespaces", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES;
+ if (dsync_deserializer_decode_try(decoder, "no_mail_sync", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_NO_MAIL_SYNC;
+ if (dsync_deserializer_decode_try(decoder, "no_backup_overwrite", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE;
+ if (dsync_deserializer_decode_try(decoder, "purge_remote", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_PURGE_REMOTE;
+ if (dsync_deserializer_decode_try(decoder, "no_notify", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_NO_NOTIFY;
+ if (dsync_deserializer_decode_try(decoder, "empty_hdr_workaround", &value))
+ set->brain_flags |= DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND;
+ if (dsync_deserializer_decode_try(decoder, "hashed_headers", &value))
+ set->hashed_headers = (const char*const*)p_strsplit_tabescaped(pool, value);
+ set->hdr_hash_v2 = ibc->minor_version >= DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V2;
+ set->hdr_hash_v3 = ibc->minor_version >= DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V3;
+
+ *set_r = set;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_end_of_list(struct dsync_ibc *_ibc,
+ enum dsync_ibc_eol_type type)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+
+ i_assert(ibc->value_output == NULL);
+
+ switch (type) {
+ case DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE:
+ if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+ return;
+ break;
+ default:
+ break;
+ }
+
+ ibc->last_sent_item_eol = TRUE;
+ o_stream_nsend_str(ibc->output, END_OF_LIST_LINE"\n");
+}
+
+static void
+dsync_ibc_stream_send_mailbox_state(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox_state *state)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+
+ str_append_c(str, items[ITEM_MAILBOX_STATE].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_STATE);
+ dsync_serializer_encode_add(encoder, "mailbox_guid",
+ guid_128_to_string(state->mailbox_guid));
+ dsync_serializer_encode_add(encoder, "last_uidvalidity",
+ dec2str(state->last_uidvalidity));
+ dsync_serializer_encode_add(encoder, "last_common_uid",
+ dec2str(state->last_common_uid));
+ dsync_serializer_encode_add(encoder, "last_common_modseq",
+ dec2str(state->last_common_modseq));
+ dsync_serializer_encode_add(encoder, "last_common_pvt_modseq",
+ dec2str(state->last_common_pvt_modseq));
+ dsync_serializer_encode_add(encoder, "last_messages_count",
+ dec2str(state->last_messages_count));
+ if (state->changes_during_sync)
+ dsync_serializer_encode_add(encoder, "changes_during_sync", "");
+
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox_state(struct dsync_ibc *_ibc,
+ struct dsync_mailbox_state *state_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ i_zero(state_r);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_STATE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ value = dsync_deserializer_decode_get(decoder, "mailbox_guid");
+ if (guid_128_from_string(value, state_r->mailbox_guid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "last_uidvalidity");
+ if (str_to_uint32(value, &state_r->last_uidvalidity) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_uidvalidity");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "last_common_uid");
+ if (str_to_uint32(value, &state_r->last_common_uid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_common_uid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "last_common_modseq");
+ if (str_to_uint64(value, &state_r->last_common_modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_common_modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "last_common_pvt_modseq");
+ if (str_to_uint64(value, &state_r->last_common_pvt_modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_common_pvt_modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "last_messages_count", &value) &&
+ str_to_uint32(value, &state_r->last_messages_count) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_messages_count");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "changes_during_sync", &value))
+ state_r->changes_during_sync = TRUE;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_mailbox_tree_node(struct dsync_ibc *_ibc,
+ const char *const *name,
+ const struct dsync_mailbox_node *node)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str, *namestr;
+
+ i_assert(*name != NULL);
+
+ str = t_str_new(128);
+ str_append_c(str, items[ITEM_MAILBOX_TREE_NODE].chr);
+
+ /* convert all hierarchy separators to tabs. mailbox names really
+ aren't supposed to have any tabs, but escape them anyway if there
+ are. */
+ namestr = t_str_new(128);
+ for (; *name != NULL; name++) {
+ str_append_tabescaped(namestr, *name);
+ str_append_c(namestr, '\t');
+ }
+ str_truncate(namestr, str_len(namestr)-1);
+
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_TREE_NODE);
+ dsync_serializer_encode_add(encoder, "name", str_c(namestr));
+ switch (node->existence) {
+ case DSYNC_MAILBOX_NODE_NONEXISTENT:
+ dsync_serializer_encode_add(encoder, "existence", "n");
+ break;
+ case DSYNC_MAILBOX_NODE_EXISTS:
+ dsync_serializer_encode_add(encoder, "existence", "y");
+ break;
+ case DSYNC_MAILBOX_NODE_DELETED:
+ dsync_serializer_encode_add(encoder, "existence", "d");
+ break;
+ }
+
+ if (!guid_128_is_empty(node->mailbox_guid)) {
+ dsync_serializer_encode_add(encoder, "mailbox_guid",
+ guid_128_to_string(node->mailbox_guid));
+ }
+ if (node->uid_validity != 0) {
+ dsync_serializer_encode_add(encoder, "uid_validity",
+ dec2str(node->uid_validity));
+ }
+ if (node->uid_next != 0) {
+ dsync_serializer_encode_add(encoder, "uid_next",
+ dec2str(node->uid_next));
+ }
+ if (node->last_renamed_or_created != 0) {
+ dsync_serializer_encode_add(encoder, "last_renamed_or_created",
+ dec2str(node->last_renamed_or_created));
+ }
+ if (node->last_subscription_change != 0) {
+ dsync_serializer_encode_add(encoder, "last_subscription_change",
+ dec2str(node->last_subscription_change));
+ }
+ if (node->subscribed)
+ dsync_serializer_encode_add(encoder, "subscribed", "");
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox_tree_node(struct dsync_ibc *_ibc,
+ const char *const **name_r,
+ const struct dsync_mailbox_node **node_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mailbox_node *node;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_TREE_NODE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ p_clear(ibc->ret_pool);
+ node = p_new(ibc->ret_pool, struct dsync_mailbox_node, 1);
+
+ value = dsync_deserializer_decode_get(decoder, "name");
+ if (*value == '\0') {
+ dsync_ibc_input_error(ibc, decoder, "Empty name");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ *name_r = (void *)p_strsplit_tabescaped(ibc->ret_pool, value);
+
+ value = dsync_deserializer_decode_get(decoder, "existence");
+ switch (*value) {
+ case 'n':
+ node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ break;
+ case 'y':
+ node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ break;
+ case 'd':
+ node->existence = DSYNC_MAILBOX_NODE_DELETED;
+ break;
+ }
+
+ if (dsync_deserializer_decode_try(decoder, "mailbox_guid", &value) &&
+ guid_128_from_string(value, node->mailbox_guid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "uid_validity", &value) &&
+ str_to_uint32(value, &node->uid_validity) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid_validity");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "uid_next", &value) &&
+ str_to_uint32(value, &node->uid_next) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid_next");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "last_renamed_or_created", &value) &&
+ str_to_time(value, &node->last_renamed_or_created) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_renamed_or_created");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "last_subscription_change", &value) &&
+ str_to_time(value, &node->last_subscription_change) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_subscription_change");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "subscribed", &value))
+ node->subscribed = TRUE;
+
+ *node_r = node;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_encode_delete(string_t *str,
+ struct dsync_serializer_encoder *encoder,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, const char *key,
+ enum dsync_mailbox_delete_type type)
+{
+ unsigned int i;
+
+ str_truncate(str, 0);
+ for (i = 0; i < count; i++) {
+ if (deletes[i].type == type) {
+ str_append(str, guid_128_to_string(deletes[i].guid));
+ str_printfa(str, " %ld ", (long)deletes[i].timestamp);
+ }
+ }
+ if (str_len(str) > 0) {
+ str_truncate(str, str_len(str)-1);
+ dsync_serializer_encode_add(encoder, key, str_c(str));
+ }
+}
+
+static void
+dsync_ibc_stream_send_mailbox_deletes(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, char hierarchy_sep,
+ char escape_char)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str, *substr;
+ char sep[2];
+
+ str = t_str_new(128);
+ str_append_c(str, items[ITEM_MAILBOX_DELETE].chr);
+
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_DELETE);
+ sep[0] = hierarchy_sep; sep[1] = '\0';
+ dsync_serializer_encode_add(encoder, "hierarchy_sep", sep);
+ sep[0] = escape_char; sep[1] = '\0';
+ dsync_serializer_encode_add(encoder, "escape_char", sep);
+
+ substr = t_str_new(128);
+ dsync_ibc_stream_encode_delete(substr, encoder, deletes, count,
+ "mailboxes",
+ DSYNC_MAILBOX_DELETE_TYPE_MAILBOX);
+ dsync_ibc_stream_encode_delete(substr, encoder, deletes, count,
+ "dirs",
+ DSYNC_MAILBOX_DELETE_TYPE_DIR);
+ dsync_ibc_stream_encode_delete(substr, encoder, deletes, count,
+ "unsubscribes",
+ DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE);
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+ARRAY_DEFINE_TYPE(dsync_mailbox_delete, struct dsync_mailbox_delete);
+static int
+decode_mailbox_deletes(ARRAY_TYPE(dsync_mailbox_delete) *deletes,
+ const char *value, enum dsync_mailbox_delete_type type)
+{
+ struct dsync_mailbox_delete *del;
+ const char *const *tmp;
+ unsigned int i;
+
+ tmp = t_strsplit(value, " ");
+ for (i = 0; tmp[i] != NULL; i += 2) {
+ del = array_append_space(deletes);
+ del->type = type;
+ if (guid_128_from_string(tmp[i], del->guid) < 0)
+ return -1;
+ if (tmp[i+1] == NULL ||
+ str_to_time(tmp[i+1], &del->timestamp) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox_deletes(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox_delete **deletes_r,
+ unsigned int *count_r, char *hierarchy_sep_r,
+ char *escape_char_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ ARRAY_TYPE(dsync_mailbox_delete) deletes;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_DELETE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ p_clear(ibc->ret_pool);
+ p_array_init(&deletes, ibc->ret_pool, 16);
+
+ value = dsync_deserializer_decode_get(decoder, "hierarchy_sep");
+ if (strlen(value) != 1) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid hierarchy_sep");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ *hierarchy_sep_r = value[0];
+
+ if (!dsync_deserializer_decode_try(decoder, "escape_char", &value))
+ *escape_char_r = '\0';
+ else {
+ if (strlen(value) > 1) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid escape_char '%s'", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ *escape_char_r = value[0];
+ }
+
+ if (dsync_deserializer_decode_try(decoder, "mailboxes", &value) &&
+ decode_mailbox_deletes(&deletes, value,
+ DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid mailboxes");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "dirs", &value) &&
+ decode_mailbox_deletes(&deletes, value,
+ DSYNC_MAILBOX_DELETE_TYPE_DIR) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid dirs");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "unsubscribes", &value) &&
+ decode_mailbox_deletes(&deletes, value,
+ DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid dirs");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ *deletes_r = array_get(&deletes, count_r);
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static const char *
+get_cache_fields(struct dsync_ibc_stream *ibc,
+ const struct dsync_mailbox *dsync_box)
+{
+ struct dsync_serializer_encoder *encoder;
+ string_t *str;
+ const struct mailbox_cache_field *cache_fields;
+ unsigned int i, count;
+ char decision[3];
+
+ cache_fields = array_get(&dsync_box->cache_fields, &count);
+ if (count == 0)
+ return "";
+
+ str = t_str_new(128);
+ for (i = 0; i < count; i++) {
+ const struct mailbox_cache_field *field = &cache_fields[i];
+
+ encoder = dsync_serializer_encode_begin(ibc->serializers[ITEM_MAILBOX_CACHE_FIELD]);
+ dsync_serializer_encode_add(encoder, "name", field->name);
+
+ memset(decision, 0, sizeof(decision));
+ switch (field->decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) {
+ case MAIL_CACHE_DECISION_NO:
+ decision[0] = 'n';
+ break;
+ case MAIL_CACHE_DECISION_TEMP:
+ decision[0] = 't';
+ break;
+ case MAIL_CACHE_DECISION_YES:
+ decision[0] = 'y';
+ break;
+ }
+ i_assert(decision[0] != '\0');
+ if ((field->decision & MAIL_CACHE_DECISION_FORCED) != 0)
+ decision[1] = 'F';
+ dsync_serializer_encode_add(encoder, "decision", decision);
+ if (field->last_used != 0) {
+ dsync_serializer_encode_add(encoder, "last_used",
+ dec2str(field->last_used));
+ }
+ dsync_serializer_encode_finish(&encoder, str);
+ }
+ if (i > 0) {
+ /* remove the trailing LF */
+ str_truncate(str, str_len(str)-1);
+ }
+ return str_c(str);
+}
+
+static void
+dsync_ibc_stream_send_mailbox(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox *dsync_box)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+ const char *value;
+
+ str_append_c(str, items[ITEM_MAILBOX].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX);
+ dsync_serializer_encode_add(encoder, "mailbox_guid",
+ guid_128_to_string(dsync_box->mailbox_guid));
+
+ if (dsync_box->mailbox_lost)
+ dsync_serializer_encode_add(encoder, "mailbox_lost", "");
+ if (dsync_box->mailbox_ignore)
+ dsync_serializer_encode_add(encoder, "mailbox_ignore", "");
+ if (dsync_box->have_guids)
+ dsync_serializer_encode_add(encoder, "have_guids", "");
+ if (dsync_box->have_save_guids)
+ dsync_serializer_encode_add(encoder, "have_save_guids", "");
+ if (dsync_box->have_only_guid128)
+ dsync_serializer_encode_add(encoder, "have_only_guid128", "");
+ dsync_serializer_encode_add(encoder, "uid_validity",
+ dec2str(dsync_box->uid_validity));
+ dsync_serializer_encode_add(encoder, "uid_next",
+ dec2str(dsync_box->uid_next));
+ dsync_serializer_encode_add(encoder, "messages_count",
+ dec2str(dsync_box->messages_count));
+ dsync_serializer_encode_add(encoder, "first_recent_uid",
+ dec2str(dsync_box->first_recent_uid));
+ dsync_serializer_encode_add(encoder, "highest_modseq",
+ dec2str(dsync_box->highest_modseq));
+ dsync_serializer_encode_add(encoder, "highest_pvt_modseq",
+ dec2str(dsync_box->highest_pvt_modseq));
+
+ value = get_cache_fields(ibc, dsync_box);
+ if (value != NULL)
+ dsync_serializer_encode_add(encoder, "cache_fields", value);
+
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static int
+parse_cache_field(struct dsync_ibc_stream *ibc, struct dsync_mailbox *box,
+ const char *value)
+{
+ struct dsync_deserializer_decoder *decoder;
+ struct mailbox_cache_field field;
+ const char *error;
+ int ret = 0;
+
+ if (dsync_deserializer_decode_begin(ibc->deserializers[ITEM_MAILBOX_CACHE_FIELD],
+ value, &decoder, &error) < 0) {
+ dsync_ibc_input_error(ibc, NULL,
+ "cache_field: Invalid input: %s", error);
+ return -1;
+ }
+
+ i_zero(&field);
+ value = dsync_deserializer_decode_get(decoder, "name");
+ field.name = p_strdup(ibc->ret_pool, value);
+
+ value = dsync_deserializer_decode_get(decoder, "decision");
+ switch (*value) {
+ case 'n':
+ field.decision = MAIL_CACHE_DECISION_NO;
+ break;
+ case 't':
+ field.decision = MAIL_CACHE_DECISION_TEMP;
+ break;
+ case 'y':
+ field.decision = MAIL_CACHE_DECISION_YES;
+ break;
+ default:
+ dsync_ibc_input_error(ibc, decoder, "Invalid decision: %s",
+ value);
+ ret = -1;
+ break;
+ }
+ if (value[1] == 'F')
+ field.decision |= MAIL_CACHE_DECISION_FORCED;
+
+ if (dsync_deserializer_decode_try(decoder, "last_used", &value) &&
+ str_to_time(value, &field.last_used) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_used");
+ ret = -1;
+ }
+ array_push_back(&box->cache_fields, &field);
+
+ dsync_deserializer_decode_finish(&decoder);
+ return ret;
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox **dsync_box_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ pool_t pool = ibc->ret_pool;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mailbox *box;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ p_clear(pool);
+ box = p_new(pool, struct dsync_mailbox, 1);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ value = dsync_deserializer_decode_get(decoder, "mailbox_guid");
+ if (guid_128_from_string(value, box->mailbox_guid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ if (dsync_deserializer_decode_try(decoder, "mailbox_lost", &value))
+ box->mailbox_lost = TRUE;
+ if (dsync_deserializer_decode_try(decoder, "mailbox_ignore", &value))
+ box->mailbox_ignore = TRUE;
+ if (dsync_deserializer_decode_try(decoder, "have_guids", &value))
+ box->have_guids = TRUE;
+ if (dsync_deserializer_decode_try(decoder, "have_save_guids", &value) ||
+ (box->have_guids && ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_SAVE_GUID))
+ box->have_save_guids = TRUE;
+ if (dsync_deserializer_decode_try(decoder, "have_only_guid128", &value))
+ box->have_only_guid128 = TRUE;
+ value = dsync_deserializer_decode_get(decoder, "uid_validity");
+ if (str_to_uint32(value, &box->uid_validity) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid_validity");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "uid_next");
+ if (str_to_uint32(value, &box->uid_next) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid_next");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "messages_count");
+ if (str_to_uint32(value, &box->messages_count) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid messages_count");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "first_recent_uid");
+ if (str_to_uint32(value, &box->first_recent_uid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid first_recent_uid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "highest_modseq");
+ if (str_to_uint64(value, &box->highest_modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid highest_modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ value = dsync_deserializer_decode_get(decoder, "highest_pvt_modseq");
+ if (str_to_uint64(value, &box->highest_pvt_modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid highest_pvt_modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ p_array_init(&box->cache_fields, pool, 32);
+ if (dsync_deserializer_decode_try(decoder, "cache_fields", &value)) {
+ const char *const *fields = t_strsplit(value, "\n");
+ for (; *fields != NULL; fields++) {
+ if (parse_cache_field(ibc, box, *fields) < 0)
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ }
+
+ *dsync_box_r = box;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox_attribute *attr)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+ char type[2];
+
+ if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+ return;
+
+ str_append_c(str, items[ITEM_MAILBOX_ATTRIBUTE].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_ATTRIBUTE);
+
+ type[0] = type[1] = '\0';
+ switch (attr->type) {
+ case MAIL_ATTRIBUTE_TYPE_PRIVATE:
+ type[0] = 'p';
+ break;
+ case MAIL_ATTRIBUTE_TYPE_SHARED:
+ type[0] = 's';
+ break;
+ }
+ i_assert(type[0] != '\0');
+ dsync_serializer_encode_add(encoder, "type", type);
+ dsync_serializer_encode_add(encoder, "key", attr->key);
+ if (attr->value != NULL)
+ dsync_serializer_encode_add(encoder, "value", attr->value);
+ else if (attr->value_stream != NULL)
+ dsync_serializer_encode_add(encoder, "stream", "");
+
+ if (attr->deleted)
+ dsync_serializer_encode_add(encoder, "deleted", "");
+ if (attr->last_change != 0) {
+ dsync_serializer_encode_add(encoder, "last_change",
+ dec2str(attr->last_change));
+ }
+ if (attr->modseq != 0) {
+ dsync_serializer_encode_add(encoder, "modseq",
+ dec2str(attr->modseq));
+ }
+
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+
+ if (attr->value_stream != NULL) {
+ ibc->value_output_last = '\0';
+ ibc->value_output = attr->value_stream;
+ i_stream_ref(ibc->value_output);
+ (void)dsync_ibc_stream_send_value_stream(ibc);
+ }
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox_attribute(struct dsync_ibc *_ibc,
+ const struct dsync_mailbox_attribute **attr_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ pool_t pool = ibc->ret_pool;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mailbox_attribute *attr;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+ return DSYNC_IBC_RECV_RET_FINISHED;
+
+ if (ibc->value_input != NULL) {
+ /* wait until the mail's stream has been read */
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ if (ibc->cur_attr != NULL) {
+ /* finished reading the stream, return the mail now */
+ *attr_r = ibc->cur_attr;
+ ibc->cur_attr = NULL;
+ return DSYNC_IBC_RECV_RET_OK;
+ }
+
+ p_clear(pool);
+ attr = p_new(pool, struct dsync_mailbox_attribute, 1);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_ATTRIBUTE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ value = dsync_deserializer_decode_get(decoder, "type");
+ switch (*value) {
+ case 'p':
+ attr->type = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ break;
+ case 's':
+ attr->type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ break;
+ default:
+ dsync_ibc_input_error(ibc, decoder, "Invalid type: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ value = dsync_deserializer_decode_get(decoder, "key");
+ attr->key = p_strdup(pool, value);
+
+ if (dsync_deserializer_decode_try(decoder, "deleted", &value))
+ attr->deleted = TRUE;
+ if (dsync_deserializer_decode_try(decoder, "last_change", &value) &&
+ str_to_time(value, &attr->last_change) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid last_change");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "modseq", &value) &&
+ str_to_uint64(value, &attr->modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ /* NOTE: stream reading must be the last here, because reading a large
+ stream will be finished later by return TRYAGAIN. We need to
+ deserialize all the other fields before that or they'll get lost. */
+ if (dsync_deserializer_decode_try(decoder, "stream", &value)) {
+ attr->value_stream = dsync_ibc_stream_input_stream(ibc);
+ if (dsync_ibc_stream_read_mail_stream(ibc) <= 0) {
+ ibc->cur_attr = attr;
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ /* already finished reading the stream */
+ i_assert(ibc->value_input == NULL);
+ } else if (dsync_deserializer_decode_try(decoder, "value", &value))
+ attr->value = p_strdup(pool, value);
+
+ *attr_r = attr;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_change(struct dsync_ibc *_ibc,
+ const struct dsync_mail_change *change)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+ char type[2];
+
+ str_append_c(str, items[ITEM_MAIL_CHANGE].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL_CHANGE);
+
+ type[0] = type[1] = '\0';
+ switch (change->type) {
+ case DSYNC_MAIL_CHANGE_TYPE_SAVE:
+ type[0] = 's';
+ break;
+ case DSYNC_MAIL_CHANGE_TYPE_EXPUNGE:
+ type[0] = 'e';
+ break;
+ case DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE:
+ type[0] = 'f';
+ break;
+ }
+ i_assert(type[0] != '\0');
+ dsync_serializer_encode_add(encoder, "type", type);
+ dsync_serializer_encode_add(encoder, "uid", dec2str(change->uid));
+ if (change->guid != NULL)
+ dsync_serializer_encode_add(encoder, "guid", change->guid);
+ if (change->hdr_hash != NULL) {
+ dsync_serializer_encode_add(encoder, "hdr_hash",
+ change->hdr_hash);
+ }
+ if (change->modseq != 0) {
+ dsync_serializer_encode_add(encoder, "modseq",
+ dec2str(change->modseq));
+ }
+ if (change->pvt_modseq != 0) {
+ dsync_serializer_encode_add(encoder, "pvt_modseq",
+ dec2str(change->pvt_modseq));
+ }
+ if (change->add_flags != 0) {
+ dsync_serializer_encode_add(encoder, "add_flags",
+ t_strdup_printf("%x", change->add_flags));
+ }
+ if (change->remove_flags != 0) {
+ dsync_serializer_encode_add(encoder, "remove_flags",
+ t_strdup_printf("%x", change->remove_flags));
+ }
+ if (change->final_flags != 0) {
+ dsync_serializer_encode_add(encoder, "final_flags",
+ t_strdup_printf("%x", change->final_flags));
+ }
+ if (change->keywords_reset)
+ dsync_serializer_encode_add(encoder, "keywords_reset", "");
+
+ if (array_is_created(&change->keyword_changes) &&
+ array_count(&change->keyword_changes) > 0) {
+ string_t *kw_str = t_str_new(128);
+ const char *const *changes;
+ unsigned int i, count;
+
+ changes = array_get(&change->keyword_changes, &count);
+ str_append_tabescaped(kw_str, changes[0]);
+ for (i = 1; i < count; i++) {
+ str_append_c(kw_str, '\t');
+ str_append_tabescaped(kw_str, changes[i]);
+ }
+ dsync_serializer_encode_add(encoder, "keyword_changes",
+ str_c(kw_str));
+ }
+ if (change->received_timestamp > 0) {
+ dsync_serializer_encode_add(encoder, "received_timestamp",
+ t_strdup_printf("%"PRIxTIME_T, change->received_timestamp));
+ }
+ if (change->virtual_size > 0) {
+ dsync_serializer_encode_add(encoder, "virtual_size",
+ t_strdup_printf("%llx", (unsigned long long)change->virtual_size));
+ }
+
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_change(struct dsync_ibc *_ibc,
+ const struct dsync_mail_change **change_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ pool_t pool = ibc->ret_pool;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mail_change *change;
+ const char *value;
+ unsigned int uintval;
+ unsigned long long ullongval;
+ enum dsync_ibc_recv_ret ret;
+
+ p_clear(pool);
+ change = p_new(pool, struct dsync_mail_change, 1);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL_CHANGE, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ value = dsync_deserializer_decode_get(decoder, "type");
+ switch (*value) {
+ case 's':
+ change->type = DSYNC_MAIL_CHANGE_TYPE_SAVE;
+ break;
+ case 'e':
+ change->type = DSYNC_MAIL_CHANGE_TYPE_EXPUNGE;
+ break;
+ case 'f':
+ change->type = DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE;
+ break;
+ default:
+ dsync_ibc_input_error(ibc, decoder, "Invalid type: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ value = dsync_deserializer_decode_get(decoder, "uid");
+ if (str_to_uint32(value, &change->uid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ if (dsync_deserializer_decode_try(decoder, "guid", &value))
+ change->guid = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "hdr_hash", &value))
+ change->hdr_hash = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "modseq", &value) &&
+ str_to_uint64(value, &change->modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "pvt_modseq", &value) &&
+ str_to_uint64(value, &change->pvt_modseq) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid pvt_modseq");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ if (dsync_deserializer_decode_try(decoder, "add_flags", &value)) {
+ if (str_to_uint_hex(value, &uintval) < 0 ||
+ uintval > (uint8_t)-1) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid add_flags: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ change->add_flags = uintval;
+ }
+ if (dsync_deserializer_decode_try(decoder, "remove_flags", &value)) {
+ if (str_to_uint_hex(value, &uintval) < 0 ||
+ uintval > (uint8_t)-1) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid remove_flags: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ change->remove_flags = uintval;
+ }
+ if (dsync_deserializer_decode_try(decoder, "final_flags", &value)) {
+ if (str_to_uint_hex(value, &uintval) < 0 ||
+ uintval > (uint8_t)-1) {
+ dsync_ibc_input_error(ibc, decoder,
+ "Invalid final_flags: %s", value);
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ change->final_flags = uintval;
+ }
+ if (dsync_deserializer_decode_try(decoder, "keywords_reset", &value))
+ change->keywords_reset = TRUE;
+
+ if (dsync_deserializer_decode_try(decoder, "keyword_changes", &value) &&
+ *value != '\0') {
+ const char *const *changes = t_strsplit_tabescaped(value);
+ unsigned int i, count = str_array_length(changes);
+
+ p_array_init(&change->keyword_changes, pool, count);
+ for (i = 0; i < count; i++) {
+ value = p_strdup(pool, changes[i]);
+ array_push_back(&change->keyword_changes, &value);
+ }
+ }
+ if (dsync_deserializer_decode_try(decoder, "received_timestamp", &value)) {
+ if (str_to_ullong_hex(value, &ullongval) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid received_timestamp");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ change->received_timestamp = ullongval;
+ }
+ if (dsync_deserializer_decode_try(decoder, "virtual_size", &value)) {
+ if (str_to_ullong_hex(value, &ullongval) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid virtual_size");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ change->virtual_size = ullongval;
+ }
+
+ *change_r = change;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_mail_request(struct dsync_ibc *_ibc,
+ const struct dsync_mail_request *request)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+
+ str_append_c(str, items[ITEM_MAIL_REQUEST].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL_REQUEST);
+ if (request->guid != NULL)
+ dsync_serializer_encode_add(encoder, "guid", request->guid);
+ if (request->uid != 0) {
+ dsync_serializer_encode_add(encoder, "uid",
+ dec2str(request->uid));
+ }
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mail_request(struct dsync_ibc *_ibc,
+ const struct dsync_mail_request **request_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mail_request *request;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ p_clear(ibc->ret_pool);
+ request = p_new(ibc->ret_pool, struct dsync_mail_request, 1);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL_REQUEST, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ if (dsync_deserializer_decode_try(decoder, "guid", &value))
+ request->guid = p_strdup(ibc->ret_pool, value);
+ if (dsync_deserializer_decode_try(decoder, "uid", &value) &&
+ str_to_uint32(value, &request->uid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+
+ *request_r = request;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_mail(struct dsync_ibc *_ibc,
+ const struct dsync_mail *mail)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+
+ i_assert(!mail->minimal_fields);
+ i_assert(ibc->value_output == NULL);
+
+ str_append_c(str, items[ITEM_MAIL].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL);
+ if (mail->guid != NULL)
+ dsync_serializer_encode_add(encoder, "guid", mail->guid);
+ if (mail->uid != 0)
+ dsync_serializer_encode_add(encoder, "uid", dec2str(mail->uid));
+ if (mail->pop3_uidl != NULL) {
+ dsync_serializer_encode_add(encoder, "pop3_uidl",
+ mail->pop3_uidl);
+ }
+ if (mail->pop3_order > 0) {
+ dsync_serializer_encode_add(encoder, "pop3_order",
+ dec2str(mail->pop3_order));
+ }
+ if (mail->received_date > 0) {
+ dsync_serializer_encode_add(encoder, "received_date",
+ dec2str(mail->received_date));
+ }
+ if (mail->saved_date != 0) {
+ dsync_serializer_encode_add(encoder, "saved_date",
+ dec2str(mail->saved_date));
+ }
+ if (mail->input != NULL)
+ dsync_serializer_encode_add(encoder, "stream", "");
+
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+
+ if (mail->input != NULL) {
+ ibc->value_output_last = '\0';
+ ibc->value_output = mail->input;
+ i_stream_ref(ibc->value_output);
+ (void)dsync_ibc_stream_send_value_stream(ibc);
+ }
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mail(struct dsync_ibc *_ibc, struct dsync_mail **mail_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ pool_t pool = ibc->ret_pool;
+ struct dsync_deserializer_decoder *decoder;
+ struct dsync_mail *mail;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+
+ if (ibc->value_input != NULL) {
+ /* wait until the mail's stream has been read */
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (ibc->cur_mail != NULL) {
+ /* finished reading the stream, return the mail now */
+ *mail_r = ibc->cur_mail;
+ ibc->cur_mail = NULL;
+ return DSYNC_IBC_RECV_RET_OK;
+ }
+
+ p_clear(pool);
+ mail = p_new(pool, struct dsync_mail, 1);
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ if (dsync_deserializer_decode_try(decoder, "guid", &value))
+ mail->guid = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "uid", &value) &&
+ str_to_uint32(value, &mail->uid) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid uid");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "pop3_uidl", &value))
+ mail->pop3_uidl = p_strdup(pool, value);
+ if (dsync_deserializer_decode_try(decoder, "pop3_order", &value) &&
+ str_to_uint32(value, &mail->pop3_order) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid pop3_order");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "received_date", &value) &&
+ str_to_time(value, &mail->received_date) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid received_date");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "saved_date", &value) &&
+ str_to_time(value, &mail->saved_date) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid saved_date");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "stream", &value)) {
+ mail->input = dsync_ibc_stream_input_stream(ibc);
+ if (dsync_ibc_stream_read_mail_stream(ibc) <= 0) {
+ ibc->cur_mail = mail;
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ /* already finished reading the stream */
+ i_assert(ibc->value_input == NULL);
+ }
+
+ *mail_r = mail;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
+dsync_ibc_stream_send_finish(struct dsync_ibc *_ibc, const char *error,
+ enum mail_error mail_error,
+ bool require_full_resync)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_serializer_encoder *encoder;
+ string_t *str = t_str_new(128);
+
+ str_append_c(str, items[ITEM_FINISH].chr);
+ encoder = dsync_ibc_send_encode_begin(ibc, ITEM_FINISH);
+ if (error != NULL)
+ dsync_serializer_encode_add(encoder, "error", error);
+ if (mail_error != 0) {
+ dsync_serializer_encode_add(encoder, "mail_error",
+ dec2str(mail_error));
+ }
+ if (require_full_resync)
+ dsync_serializer_encode_add(encoder, "require_full_resync", "");
+ dsync_serializer_encode_finish(&encoder, str);
+ dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_finish(struct dsync_ibc *_ibc, const char **error_r,
+ enum mail_error *mail_error_r,
+ bool *require_full_resync_r)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ struct dsync_deserializer_decoder *decoder;
+ const char *value;
+ enum dsync_ibc_recv_ret ret;
+ int i = 0;
+
+ *error_r = NULL;
+ *mail_error_r = 0;
+ *require_full_resync_r = FALSE;
+
+ p_clear(ibc->ret_pool);
+
+ if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_FINISH)
+ return DSYNC_IBC_RECV_RET_OK;
+
+ ret = dsync_ibc_stream_input_next(ibc, ITEM_FINISH, &decoder);
+ if (ret != DSYNC_IBC_RECV_RET_OK)
+ return ret;
+
+ if (dsync_deserializer_decode_try(decoder, "error", &value))
+ *error_r = p_strdup(ibc->ret_pool, value);
+ if (dsync_deserializer_decode_try(decoder, "mail_error", &value) &&
+ str_to_int(value, &i) < 0) {
+ dsync_ibc_input_error(ibc, decoder, "Invalid mail_error");
+ return DSYNC_IBC_RECV_RET_TRYAGAIN;
+ }
+ if (dsync_deserializer_decode_try(decoder, "require_full_resync", &value))
+ *require_full_resync_r = TRUE;
+ *mail_error_r = i;
+
+ ibc->finish_received = TRUE;
+ return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void dsync_ibc_stream_close_mail_streams(struct dsync_ibc *_ibc)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+
+ if (ibc->value_output != NULL) {
+ i_stream_unref(&ibc->value_output);
+ dsync_ibc_stream_stop(ibc);
+ }
+}
+
+static bool dsync_ibc_stream_is_send_queue_full(struct dsync_ibc *_ibc)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+ size_t bytes;
+
+ if (ibc->value_output != NULL)
+ return TRUE;
+
+ bytes = o_stream_get_buffer_used_size(ibc->output);
+ if (bytes < DSYNC_IBC_STREAM_OUTBUF_THROTTLE_SIZE)
+ return FALSE;
+
+ o_stream_set_flush_pending(ibc->output, TRUE);
+ return TRUE;
+}
+
+static bool dsync_ibc_stream_has_pending_data(struct dsync_ibc *_ibc)
+{
+ struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+
+ return ibc->has_pending_data;
+}
+
+static const struct dsync_ibc_vfuncs dsync_ibc_stream_vfuncs = {
+ dsync_ibc_stream_deinit,
+ dsync_ibc_stream_send_handshake,
+ dsync_ibc_stream_recv_handshake,
+ dsync_ibc_stream_send_end_of_list,
+ dsync_ibc_stream_send_mailbox_state,
+ dsync_ibc_stream_recv_mailbox_state,
+ dsync_ibc_stream_send_mailbox_tree_node,
+ dsync_ibc_stream_recv_mailbox_tree_node,
+ dsync_ibc_stream_send_mailbox_deletes,
+ dsync_ibc_stream_recv_mailbox_deletes,
+ dsync_ibc_stream_send_mailbox,
+ dsync_ibc_stream_recv_mailbox,
+ dsync_ibc_stream_send_mailbox_attribute,
+ dsync_ibc_stream_recv_mailbox_attribute,
+ dsync_ibc_stream_send_change,
+ dsync_ibc_stream_recv_change,
+ dsync_ibc_stream_send_mail_request,
+ dsync_ibc_stream_recv_mail_request,
+ dsync_ibc_stream_send_mail,
+ dsync_ibc_stream_recv_mail,
+ dsync_ibc_stream_send_finish,
+ dsync_ibc_stream_recv_finish,
+ dsync_ibc_stream_close_mail_streams,
+ dsync_ibc_stream_is_send_queue_full,
+ dsync_ibc_stream_has_pending_data
+};
+
+struct dsync_ibc *
+dsync_ibc_init_stream(struct istream *input, struct ostream *output,
+ const char *name, const char *temp_path_prefix,
+ unsigned int timeout_secs)
+{
+ struct dsync_ibc_stream *ibc;
+
+ ibc = i_new(struct dsync_ibc_stream, 1);
+ ibc->ibc.v = dsync_ibc_stream_vfuncs;
+ ibc->input = input;
+ ibc->output = output;
+ i_stream_ref(ibc->input);
+ o_stream_ref(ibc->output);
+ ibc->name = i_strdup(name);
+ ibc->temp_path_prefix = i_strdup(temp_path_prefix);
+ ibc->timeout_secs = timeout_secs;
+ ibc->ret_pool = pool_alloconly_create("ibc stream data", 2048);
+ dsync_ibc_stream_init(ibc);
+ return &ibc->ibc;
+}
diff --git a/src/doveadm/dsync/dsync-ibc.c b/src/doveadm/dsync/dsync-ibc.c
new file mode 100644
index 0000000..ede7b83
--- /dev/null
+++ b/src/doveadm/dsync/dsync-ibc.c
@@ -0,0 +1,239 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dsync-mail.h"
+#include "dsync-ibc-private.h"
+
+void dsync_ibc_deinit(struct dsync_ibc **_ibc)
+{
+ struct dsync_ibc *ibc = *_ibc;
+
+ *_ibc = NULL;
+ ibc->v.deinit(ibc);
+}
+
+void dsync_ibc_set_io_callback(struct dsync_ibc *ibc,
+ io_callback_t *callback, void *context)
+{
+ ibc->io_callback = callback;
+ ibc->io_context = context;
+}
+
+void dsync_ibc_send_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings *set)
+{
+ ibc->v.send_handshake(ibc, set);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings **set_r)
+{
+ return ibc->v.recv_handshake(ibc, set_r);
+}
+
+static enum dsync_ibc_send_ret
+dsync_ibc_send_ret(struct dsync_ibc *ibc)
+{
+ return ibc->v.is_send_queue_full(ibc) ?
+ DSYNC_IBC_SEND_RET_FULL :
+ DSYNC_IBC_SEND_RET_OK;
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_end_of_list(struct dsync_ibc *ibc, enum dsync_ibc_eol_type type)
+{
+ ibc->v.send_end_of_list(ibc, type);
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mailbox_state(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_state *state)
+{
+ T_BEGIN {
+ ibc->v.send_mailbox_state(ibc, state);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_state(struct dsync_ibc *ibc,
+ struct dsync_mailbox_state *state_r)
+{
+ return ibc->v.recv_mailbox_state(ibc, state_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const *name,
+ const struct dsync_mailbox_node *node)
+{
+ i_assert(*name != NULL);
+
+ T_BEGIN {
+ ibc->v.send_mailbox_tree_node(ibc, name, node);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const **name_r,
+ const struct dsync_mailbox_node **node_r)
+{
+ return ibc->v.recv_mailbox_tree_node(ibc, name_r, node_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, char hierarchy_sep,
+ char escape_char)
+{
+ T_BEGIN {
+ ibc->v.send_mailbox_deletes(ibc, deletes, count,
+ hierarchy_sep, escape_char);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete **deletes_r,
+ unsigned int *count_r, char *hierarchy_sep_r,
+ char *escape_char_r)
+{
+ return ibc->v.recv_mailbox_deletes(ibc, deletes_r, count_r,
+ hierarchy_sep_r, escape_char_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox *dsync_box)
+{
+ T_BEGIN {
+ ibc->v.send_mailbox(ibc, dsync_box);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox **dsync_box_r)
+{
+ return ibc->v.recv_mailbox(ibc, dsync_box_r);
+}
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute *attr)
+{
+ T_BEGIN {
+ ibc->v.send_mailbox_attribute(ibc, attr);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute **attr_r)
+{
+ return ibc->v.recv_mailbox_attribute(ibc, attr_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change *change)
+{
+ i_assert(change->uid > 0);
+
+ T_BEGIN {
+ ibc->v.send_change(ibc, change);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change **change_r)
+{
+ return ibc->v.recv_change(ibc, change_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request *request)
+{
+ i_assert(request->guid != NULL || request->uid != 0);
+
+ T_BEGIN {
+ ibc->v.send_mail_request(ibc, request);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request **request_r)
+{
+ return ibc->v.recv_mail_request(ibc, request_r);
+}
+
+enum dsync_ibc_send_ret
+dsync_ibc_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail)
+{
+ i_assert(*mail->guid != '\0' || mail->uid != 0);
+
+ T_BEGIN {
+ ibc->v.send_mail(ibc, mail);
+ } T_END;
+ return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r)
+{
+ return ibc->v.recv_mail(ibc, mail_r);
+}
+
+void dsync_ibc_send_finish(struct dsync_ibc *ibc, const char *error,
+ enum mail_error mail_error,
+ bool require_full_resync)
+{
+ ibc->v.send_finish(ibc, error, mail_error, require_full_resync);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_finish(struct dsync_ibc *ibc, const char **error_r,
+ enum mail_error *mail_error_r,
+ bool *require_full_resync_r)
+{
+ return ibc->v.recv_finish(ibc, error_r, mail_error_r,
+ require_full_resync_r);
+}
+
+void dsync_ibc_close_mail_streams(struct dsync_ibc *ibc)
+{
+ ibc->v.close_mail_streams(ibc);
+}
+
+bool dsync_ibc_has_failed(struct dsync_ibc *ibc)
+{
+ return ibc->failed;
+}
+
+bool dsync_ibc_has_timed_out(struct dsync_ibc *ibc)
+{
+ return ibc->timeout;
+}
+
+bool dsync_ibc_is_send_queue_full(struct dsync_ibc *ibc)
+{
+ return ibc->v.is_send_queue_full(ibc);
+}
+
+bool dsync_ibc_has_pending_data(struct dsync_ibc *ibc)
+{
+ return ibc->v.has_pending_data(ibc);
+}
diff --git a/src/doveadm/dsync/dsync-ibc.h b/src/doveadm/dsync/dsync-ibc.h
new file mode 100644
index 0000000..dc85560
--- /dev/null
+++ b/src/doveadm/dsync/dsync-ibc.h
@@ -0,0 +1,178 @@
+#ifndef DSYNC_IBC_H
+#define DSYNC_IBC_H
+
+/* dsync inter-brain communicator */
+
+#include "ioloop.h"
+#include "guid.h"
+#include "mail-error.h"
+#include "dsync-brain.h"
+
+struct dsync_mailbox;
+struct dsync_mailbox_state;
+struct dsync_mailbox_node;
+struct dsync_mailbox_delete;
+struct dsync_mailbox_attribute;
+struct dsync_mail;
+struct dsync_mail_change;
+struct dsync_mail_request;
+
+enum dsync_ibc_send_ret {
+ DSYNC_IBC_SEND_RET_OK = 1,
+ /* send queue is full, stop sending more */
+ DSYNC_IBC_SEND_RET_FULL = 0
+};
+
+enum dsync_ibc_recv_ret {
+ DSYNC_IBC_RECV_RET_FINISHED = -1,
+ /* try again / error (the error handling delayed until io callback) */
+ DSYNC_IBC_RECV_RET_TRYAGAIN = 0,
+ DSYNC_IBC_RECV_RET_OK = 1
+};
+
+enum dsync_ibc_eol_type {
+ DSYNC_IBC_EOL_MAILBOX_STATE,
+ DSYNC_IBC_EOL_MAILBOX_TREE,
+ DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE,
+ DSYNC_IBC_EOL_MAILBOX,
+ DSYNC_IBC_EOL_MAIL_CHANGES,
+ DSYNC_IBC_EOL_MAIL_REQUESTS,
+ DSYNC_IBC_EOL_MAILS
+};
+
+struct dsync_ibc_settings {
+ /* Server hostname. Used for determining which server does the
+ locking. */
+ const char *hostname;
+ /* if non-NULL, sync only these namespaces (LF-separated) */
+ const char *sync_ns_prefixes;
+ /* if non-NULL, sync only this mailbox name */
+ const char *sync_box;
+ /* if non-NULL, use this mailbox for finding messages with GUIDs and
+ copying them instead of saving them again. */
+ const char *virtual_all_box;
+ /* if non-empty, sync only this mailbox GUID */
+ guid_128_t sync_box_guid;
+ /* Exclude these mailboxes from the sync. They can contain '*'
+ wildcards and be \special-use flags. */
+ const char *const *exclude_mailboxes;
+ /* Sync only mails with received timestamp at least this high. */
+ time_t sync_since_timestamp;
+ /* Sync only mails with received timestamp less or equal than this */
+ time_t sync_until_timestamp;
+ /* Don't sync mails larger than this. */
+ uoff_t sync_max_size;
+ /* Sync only mails with specified flags. */
+ const char *sync_flags;
+ /* Hashed headers */
+ const char *const *hashed_headers;
+
+ char alt_char;
+ enum dsync_brain_sync_type sync_type;
+ enum dsync_brain_flags brain_flags;
+ bool hdr_hash_v2;
+ bool hdr_hash_v3;
+ unsigned int lock_timeout;
+ unsigned int import_commit_msgs_interval;
+};
+
+void dsync_ibc_init_pipe(struct dsync_ibc **ibc1_r,
+ struct dsync_ibc **ibc2_r);
+struct dsync_ibc *
+dsync_ibc_init_stream(struct istream *input, struct ostream *output,
+ const char *name, const char *temp_path_prefix,
+ unsigned int timeout_secs);
+void dsync_ibc_deinit(struct dsync_ibc **ibc);
+
+/* I/O callback is called whenever new data is available. It's also called on
+ errors, so check first the error status. */
+void dsync_ibc_set_io_callback(struct dsync_ibc *ibc,
+ io_callback_t *callback, void *context);
+
+void dsync_ibc_send_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings *set);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_handshake(struct dsync_ibc *ibc,
+ const struct dsync_ibc_settings **set_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_end_of_list(struct dsync_ibc *ibc, enum dsync_ibc_eol_type type);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_state(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_state *state);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_state(struct dsync_ibc *ibc,
+ struct dsync_mailbox_state *state_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const *name,
+ const struct dsync_mailbox_node *node);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_tree_node(struct dsync_ibc *ibc,
+ const char *const **name_r,
+ const struct dsync_mailbox_node **node_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete *deletes,
+ unsigned int count, char hierarchy_sep,
+ char escape_char);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_deletes(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_delete **deletes_r,
+ unsigned int *count_r, char *hierarchy_sep_r,
+ char *escape_char_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox *dsync_box);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox(struct dsync_ibc *ibc,
+ const struct dsync_mailbox **dsync_box_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute *attr);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc,
+ const struct dsync_mailbox_attribute **attr_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change *change);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_change(struct dsync_ibc *ibc,
+ const struct dsync_mail_change **change_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request *request);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mail_request(struct dsync_ibc *ibc,
+ const struct dsync_mail_request **request_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r);
+
+void dsync_ibc_send_finish(struct dsync_ibc *ibc, const char *error,
+ enum mail_error mail_error,
+ bool require_full_resync);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_finish(struct dsync_ibc *ibc, const char **error_r,
+ enum mail_error *mail_error_r,
+ bool *require_full_resync_r);
+
+/* Close any mail input streams that are kept open. This needs to be called
+ before the mail is attempted to be freed (usually on error conditions). */
+void dsync_ibc_close_mail_streams(struct dsync_ibc *ibc);
+
+bool dsync_ibc_has_failed(struct dsync_ibc *ibc);
+bool dsync_ibc_has_timed_out(struct dsync_ibc *ibc);
+bool dsync_ibc_is_send_queue_full(struct dsync_ibc *ibc);
+bool dsync_ibc_has_pending_data(struct dsync_ibc *ibc);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mail.c b/src/doveadm/dsync/dsync-mail.c
new file mode 100644
index 0000000..b82818f
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mail.c
@@ -0,0 +1,156 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hex-binary.h"
+#include "md5.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "message-header-hash.h"
+#include "message-size.h"
+#include "mail-storage.h"
+#include "dsync-mail.h"
+
+struct mailbox_header_lookup_ctx *
+dsync_mail_get_hash_headers(struct mailbox *box, const char *const *hashed_headers)
+{
+ return mailbox_header_lookup_init(box, hashed_headers);
+}
+
+int dsync_mail_get_hdr_hash(struct mail *mail, unsigned int version,
+ const char *const *hashed_headers, const char **hdr_hash_r)
+{
+ struct istream *hdr_input, *input;
+ struct mailbox_header_lookup_ctx *hdr_ctx;
+ struct message_header_hash_context hash_ctx;
+ struct md5_context md5_ctx;
+ unsigned char md5_result[MD5_RESULTLEN];
+ const unsigned char *data;
+ size_t size;
+ ssize_t sret;
+ int ret = 0;
+
+ hdr_ctx = mailbox_header_lookup_init(mail->box, hashed_headers);
+ ret = mail_get_header_stream(mail, hdr_ctx, &hdr_input);
+ mailbox_header_lookup_unref(&hdr_ctx);
+ if (ret < 0)
+ return -1;
+
+ input = i_stream_create_lf(hdr_input);
+
+ md5_init(&md5_ctx);
+ i_zero(&hash_ctx);
+ while ((sret = i_stream_read_more(input, &data, &size)) > 0) {
+ message_header_hash_more(&hash_ctx, &hash_method_md5, &md5_ctx,
+ version, data, size);
+ i_stream_skip(input, size);
+ }
+ i_assert(sret == -1);
+ if (input->stream_errno != 0)
+ ret = -1;
+ i_stream_unref(&input);
+
+ md5_final(&md5_ctx, md5_result);
+ *hdr_hash_r = binary_to_hex(md5_result, sizeof(md5_result));
+ return ret;
+}
+
+int dsync_mail_fill(struct mail *mail, bool minimal_fill,
+ struct dsync_mail *dmail_r, const char **error_field_r)
+{
+ const char *guid;
+
+ i_zero(dmail_r);
+
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) {
+ *error_field_r = "GUID";
+ return -1;
+ }
+ dmail_r->guid = guid;
+ dmail_r->uid = mail->uid;
+
+ dmail_r->input_mail = mail;
+ dmail_r->input_mail_uid = mail->uid;
+
+ if (mail_get_save_date(mail, &dmail_r->saved_date) < 0) {
+ *error_field_r = "saved-date";
+ return -1;
+ }
+ if (!minimal_fill)
+ return dsync_mail_fill_nonminimal(mail, dmail_r, error_field_r);
+ dmail_r->minimal_fields = TRUE;
+ return 0;
+}
+
+int dsync_mail_fill_nonminimal(struct mail *mail, struct dsync_mail *dmail_r,
+ const char **error_field_r)
+{
+ const char *str;
+
+ if (mail_get_stream(mail, NULL, NULL, &dmail_r->input) < 0) {
+ *error_field_r = "body";
+ return -1;
+ }
+
+ if (mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &dmail_r->pop3_uidl) < 0) {
+ *error_field_r = "pop3-uidl";
+ return -1;
+ }
+ if (mail_get_special(mail, MAIL_FETCH_POP3_ORDER, &str) < 0) {
+ *error_field_r = "pop3-order";
+ return -1;
+ }
+ if (*str != '\0') {
+ if (str_to_uint32(str, &dmail_r->pop3_order) < 0)
+ i_unreached();
+ }
+ if (mail_get_received_date(mail, &dmail_r->received_date) < 0) {
+ *error_field_r = "received-date";
+ return -1;
+ }
+ return 0;
+}
+
+static void
+const_string_array_dup(pool_t pool, const ARRAY_TYPE(const_string) *src,
+ ARRAY_TYPE(const_string) *dest)
+{
+ const char *const *strings, *str;
+ unsigned int i, count;
+
+ if (!array_is_created(src))
+ return;
+
+ strings = array_get(src, &count);
+ if (count == 0)
+ return;
+
+ p_array_init(dest, pool, count);
+ for (i = 0; i < count; i++) {
+ str = p_strdup(pool, strings[i]);
+ array_push_back(dest, &str);
+ }
+}
+
+void dsync_mail_change_dup(pool_t pool, const struct dsync_mail_change *src,
+ struct dsync_mail_change *dest_r)
+{
+ dest_r->type = src->type;
+ dest_r->uid = src->uid;
+ if (src->guid != NULL) {
+ dest_r->guid = *src->guid == '\0' ? "" :
+ p_strdup(pool, src->guid);
+ }
+ dest_r->hdr_hash = p_strdup(pool, src->hdr_hash);
+ dest_r->modseq = src->modseq;
+ dest_r->pvt_modseq = src->pvt_modseq;
+
+ dest_r->add_flags = src->add_flags;
+ dest_r->remove_flags = src->remove_flags;
+ dest_r->final_flags = src->final_flags;
+ dest_r->keywords_reset = src->keywords_reset;
+ const_string_array_dup(pool, &src->keyword_changes,
+ &dest_r->keyword_changes);
+ dest_r->received_timestamp = src->received_timestamp;
+ dest_r->virtual_size = src->virtual_size;
+}
diff --git a/src/doveadm/dsync/dsync-mail.h b/src/doveadm/dsync/dsync-mail.h
new file mode 100644
index 0000000..fc00059
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mail.h
@@ -0,0 +1,108 @@
+#ifndef DSYNC_MAIL_H
+#define DSYNC_MAIL_H
+
+#include "mail-types.h"
+
+struct md5_context;
+struct mail;
+struct mailbox;
+
+struct dsync_mail {
+ /* either GUID="" or uid=0 */
+ const char *guid;
+ uint32_t uid;
+ time_t saved_date;
+
+ /* If non-NULL, we're syncing within the dsync process using ibc-pipe.
+ This mail can be used to mailbox_copy() the mail. */
+ struct mail *input_mail;
+ /* Verify that this equals to input_mail->uid */
+ uint32_t input_mail_uid;
+
+ /* TRUE if the following fields aren't set, because minimal_fill=TRUE
+ parameter was used. */
+ bool minimal_fields;
+
+ const char *pop3_uidl;
+ uint32_t pop3_order;
+ time_t received_date;
+ /* Input stream containing the message text, or NULL if all instances
+ of the message were already expunged from this mailbox. */
+ struct istream *input;
+};
+
+struct dsync_mail_request {
+ /* either GUID=NULL or uid=0 */
+ const char *guid;
+ uint32_t uid;
+};
+
+enum dsync_mail_change_type {
+ DSYNC_MAIL_CHANGE_TYPE_SAVE,
+ DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
+ DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE
+};
+
+#define KEYWORD_CHANGE_ADD '+'
+#define KEYWORD_CHANGE_REMOVE '-'
+#define KEYWORD_CHANGE_FINAL '='
+#define KEYWORD_CHANGE_ADD_AND_FINAL '&'
+
+struct dsync_mail_change {
+ enum dsync_mail_change_type type;
+
+ uint32_t uid;
+ /* Message's GUID:
+ - for expunges either 128bit hex or NULL if unknown
+ - "" if backend doesn't support GUIDs */
+ const char *guid;
+ /* If GUID is "", this contains hash of the message header,
+ otherwise NULL */
+ const char *hdr_hash;
+
+ /* Message's current modseq (saves, flag changes) */
+ uint64_t modseq;
+ /* Message's current private modseq (for private flags in
+ shared mailboxes, otherwise 0) */
+ uint64_t pvt_modseq;
+
+ /* List of flag/keyword changes: (saves, flag changes) */
+
+ /* Flags added/removed since last sync, and final flags containing
+ flags that exist now but haven't changed */
+ uint8_t add_flags, remove_flags, final_flags;
+ uint8_t add_pvt_flags, remove_pvt_flags;
+ /* Remove all keywords before applying changes. This is used only with
+ old transaction logs, new ones never reset keywords (just explicitly
+ remove unwanted keywords) */
+ bool keywords_reset;
+ /* +add, -remove, =final, &add_and_final. */
+ ARRAY_TYPE(const_string) keyword_changes;
+
+ /* Received timestamp for saves, if brain.sync_since/until_timestamp is set */
+ time_t received_timestamp;
+ /* Mail's size for saves if brain.sync_max_size is set,
+ UOFF_T_MAX otherwise. */
+ uoff_t virtual_size;
+};
+
+struct mailbox_header_lookup_ctx *
+dsync_mail_get_hash_headers(struct mailbox *box, const char *const *hashed_headers);
+
+int dsync_mail_get_hdr_hash(struct mail *mail, unsigned int version,
+ const char *const *hashed_headers, const char **hdr_hash_r);
+static inline bool dsync_mail_hdr_hash_is_empty(const char *hdr_hash)
+{
+ /* md5(\n) */
+ return strcmp(hdr_hash, "68b329da9893e34099c7d8ad5cb9c940") == 0;
+}
+
+int dsync_mail_fill(struct mail *mail, bool minimal_fill,
+ struct dsync_mail *dmail_r, const char **error_field_r);
+int dsync_mail_fill_nonminimal(struct mail *mail, struct dsync_mail *dmail_r,
+ const char **error_field_r);
+
+void dsync_mail_change_dup(pool_t pool, const struct dsync_mail_change *src,
+ struct dsync_mail_change *dest_r);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox-export.c b/src/doveadm/dsync/dsync-mailbox-export.c
new file mode 100644
index 0000000..a58fbec
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-export.c
@@ -0,0 +1,961 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "istream.h"
+#include "mail-index-modseq.h"
+#include "mail-storage-private.h"
+#include "mail-search-build.h"
+#include "dsync-transaction-log-scan.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox.h"
+#include "dsync-mailbox-export.h"
+
+struct dsync_mail_guid_instances {
+ ARRAY_TYPE(seq_range) seqs;
+ bool requested;
+ bool searched;
+};
+
+struct dsync_mailbox_exporter {
+ pool_t pool;
+ struct mailbox *box;
+ struct dsync_transaction_log_scan *log_scan;
+ uint32_t last_common_uid;
+
+ struct mailbox_header_lookup_ctx *wanted_headers;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+ unsigned int search_pos, search_count;
+ unsigned int hdr_hash_version;
+
+ const char *const *hashed_headers;
+
+ /* GUID => instances */
+ HASH_TABLE(char *, struct dsync_mail_guid_instances *) export_guids;
+ ARRAY_TYPE(seq_range) requested_uids;
+ ARRAY_TYPE(seq_range) search_uids;
+
+ ARRAY_TYPE(seq_range) expunged_seqs;
+ ARRAY_TYPE(const_string) expunged_guids;
+ unsigned int expunged_guid_idx;
+
+ /* uint32_t UID => struct dsync_mail_change */
+ HASH_TABLE(void *, struct dsync_mail_change *) changes;
+ /* changes sorted by UID */
+ ARRAY(struct dsync_mail_change *) sorted_changes;
+ unsigned int change_idx;
+ uint32_t highest_changed_uid;
+
+ struct mailbox_attribute_iter *attr_iter;
+ struct hash_iterate_context *attr_change_iter;
+ enum mail_attribute_type attr_type;
+ struct dsync_mailbox_attribute attr;
+
+ struct dsync_mail_change change;
+ struct dsync_mail dsync_mail;
+
+ const char *error;
+ enum mail_error mail_error;
+
+ bool body_search_initialized:1;
+ bool auto_export_mails:1;
+ bool mails_have_guids:1;
+ bool minimal_dmail_fill:1;
+ bool return_all_mails:1;
+ bool export_received_timestamps:1;
+ bool export_virtual_sizes:1;
+ bool no_hdr_hashes:1;
+};
+
+static int dsync_mail_error(struct dsync_mailbox_exporter *exporter,
+ struct mail *mail, const char *field)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_internal_error(exporter->box, &error);
+ if (error == MAIL_ERROR_EXPUNGED)
+ return 0;
+
+ exporter->mail_error = error;
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Can't lookup %s for UID=%u: %s",
+ field, mail->uid, errstr);
+ return -1;
+}
+
+static bool
+final_keyword_check(struct dsync_mail_change *change, const char *name,
+ char *type_r)
+{
+ const char *const *changes;
+ unsigned int i, count;
+
+ *type_r = KEYWORD_CHANGE_FINAL;
+
+ changes = array_get(&change->keyword_changes, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(changes[i]+1, name) != 0)
+ continue;
+
+ switch (changes[i][0]) {
+ case KEYWORD_CHANGE_ADD:
+ /* replace with ADD_AND_FINAL */
+ array_delete(&change->keyword_changes, i, 1);
+ *type_r = KEYWORD_CHANGE_ADD_AND_FINAL;
+ return FALSE;
+ case KEYWORD_CHANGE_REMOVE:
+ /* a final keyword is marked as removed.
+ this shouldn't normally happen. */
+ array_delete(&change->keyword_changes, i, 1);
+ return FALSE;
+ case KEYWORD_CHANGE_ADD_AND_FINAL:
+ case KEYWORD_CHANGE_FINAL:
+ /* no change */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+search_update_flag_changes(struct dsync_mailbox_exporter *exporter,
+ struct mail *mail, struct dsync_mail_change *change)
+{
+ const char *const *keywords;
+ unsigned int i;
+ char type;
+
+ i_assert((change->add_flags & change->remove_flags) == 0);
+
+ change->modseq = mail_get_modseq(mail);
+ change->pvt_modseq = mail_get_pvt_modseq(mail);
+ change->final_flags = mail_get_flags(mail);
+
+ keywords = mail_get_keywords(mail);
+ if (!array_is_created(&change->keyword_changes) &&
+ keywords[0] != NULL) {
+ p_array_init(&change->keyword_changes, exporter->pool,
+ str_array_length(keywords));
+ }
+ for (i = 0; keywords[i] != NULL; i++) {
+ /* add the final keyword if it's not already there
+ as +keyword */
+ if (!final_keyword_check(change, keywords[i], &type)) {
+ const char *keyword_change =
+ p_strdup_printf(exporter->pool, "%c%s",
+ type, keywords[i]);
+ array_push_back(&change->keyword_changes,
+ &keyword_change);
+ }
+ }
+}
+
+static int
+exporter_get_guids(struct dsync_mailbox_exporter *exporter,
+ struct mail *mail, const char **guid_r,
+ const char **hdr_hash_r)
+{
+ *guid_r = "";
+ *hdr_hash_r = NULL;
+
+ /* always try to get GUID, even if we're also getting header hash */
+ if (mail_get_special(mail, MAIL_FETCH_GUID, guid_r) < 0)
+ return dsync_mail_error(exporter, mail, "GUID");
+
+ if (!exporter->mails_have_guids) {
+ /* get header hash also */
+ if (exporter->no_hdr_hashes) {
+ *hdr_hash_r = "";
+ return 1;
+ }
+ if (dsync_mail_get_hdr_hash(mail, exporter->hdr_hash_version,
+ exporter->hashed_headers, hdr_hash_r) < 0)
+ return dsync_mail_error(exporter, mail, "hdr-stream");
+ return 1;
+ } else if (**guid_r == '\0') {
+ exporter->mail_error = MAIL_ERROR_TEMP;
+ exporter->error = "Backend doesn't support GUIDs, "
+ "sync with header hashes instead";
+ return -1;
+ } else {
+ /* GUIDs are required, we don't need header hash */
+ return 1;
+ }
+}
+
+static int
+search_update_flag_change_guid(struct dsync_mailbox_exporter *exporter,
+ struct mail *mail)
+{
+ struct dsync_mail_change *change, *log_change;
+ const char *guid, *hdr_hash;
+ int ret;
+
+ change = hash_table_lookup(exporter->changes, POINTER_CAST(mail->uid));
+ if (change != NULL) {
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE);
+ } else {
+ i_assert(exporter->return_all_mails);
+
+ change = p_new(exporter->pool, struct dsync_mail_change, 1);
+ change->uid = mail->uid;
+ change->type = DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE;
+ hash_table_insert(exporter->changes,
+ POINTER_CAST(mail->uid), change);
+ }
+
+ if ((ret = exporter_get_guids(exporter, mail, &guid, &hdr_hash)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* the message was expunged during export */
+ i_zero(change);
+ change->type = DSYNC_MAIL_CHANGE_TYPE_EXPUNGE;
+ change->uid = mail->uid;
+
+ /* find its GUID from log if possible */
+ log_change = dsync_transaction_log_scan_find_new_expunge(
+ exporter->log_scan, mail->uid);
+ if (log_change != NULL)
+ change->guid = log_change->guid;
+ } else {
+ change->guid = *guid == '\0' ? "" :
+ p_strdup(exporter->pool, guid);
+ change->hdr_hash = p_strdup(exporter->pool, hdr_hash);
+ search_update_flag_changes(exporter, mail, change);
+ }
+ return 0;
+}
+
+static struct dsync_mail_change *
+export_save_change_get(struct dsync_mailbox_exporter *exporter, uint32_t uid)
+{
+ struct dsync_mail_change *change;
+
+ change = hash_table_lookup(exporter->changes, POINTER_CAST(uid));
+ if (change == NULL) {
+ change = p_new(exporter->pool, struct dsync_mail_change, 1);
+ change->uid = uid;
+ hash_table_insert(exporter->changes, POINTER_CAST(uid), change);
+ } else {
+ /* move flag changes into a save. this happens only when
+ last_common_uid isn't known */
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE);
+ i_assert(exporter->last_common_uid == 0);
+ }
+
+ change->type = DSYNC_MAIL_CHANGE_TYPE_SAVE;
+ return change;
+}
+
+static void
+export_add_mail_instance(struct dsync_mailbox_exporter *exporter,
+ struct dsync_mail_change *change, uint32_t seq)
+{
+ struct dsync_mail_guid_instances *instances;
+
+ if (exporter->auto_export_mails && !exporter->mails_have_guids) {
+ /* GUIDs not supported, mail is requested by UIDs */
+ seq_range_array_add(&exporter->requested_uids, change->uid);
+ return;
+ }
+ if (*change->guid == '\0') {
+ /* mail UIDs are manually requested */
+ i_assert(!exporter->mails_have_guids);
+ return;
+ }
+
+ instances = hash_table_lookup(exporter->export_guids, change->guid);
+ if (instances == NULL) {
+ instances = p_new(exporter->pool,
+ struct dsync_mail_guid_instances, 1);
+ p_array_init(&instances->seqs, exporter->pool, 2);
+ hash_table_insert(exporter->export_guids,
+ p_strdup(exporter->pool, change->guid),
+ instances);
+ if (exporter->auto_export_mails)
+ instances->requested = TRUE;
+ }
+ seq_range_array_add(&instances->seqs, seq);
+}
+
+static int
+search_add_save(struct dsync_mailbox_exporter *exporter, struct mail *mail)
+{
+ struct dsync_mail_change *change;
+ const char *guid, *hdr_hash;
+ enum mail_fetch_field wanted_fields = MAIL_FETCH_GUID;
+ time_t received_timestamp = 0;
+ uoff_t virtual_size = UOFF_T_MAX;
+ int ret;
+
+ /* update wanted fields in case we didn't already set them for the
+ search */
+ if (exporter->export_received_timestamps)
+ wanted_fields |= MAIL_FETCH_RECEIVED_DATE;
+ if (exporter->export_virtual_sizes)
+ wanted_fields |= MAIL_FETCH_VIRTUAL_SIZE;
+ mail_add_temp_wanted_fields(mail, wanted_fields,
+ exporter->wanted_headers);
+
+ /* If message is already expunged here, just skip it */
+ if ((ret = exporter_get_guids(exporter, mail, &guid, &hdr_hash)) <= 0)
+ return ret;
+
+ if (exporter->export_received_timestamps) {
+ if (mail_get_received_date(mail, &received_timestamp) < 0)
+ return dsync_mail_error(exporter, mail, "received-time");
+ if (received_timestamp == 0) {
+ /* don't allow timestamps to be zero. we want to have
+ asserts verify that the timestamp is set properly. */
+ received_timestamp = 1;
+ }
+ }
+ if (exporter->export_virtual_sizes) {
+ if (mail_get_virtual_size(mail, &virtual_size) < 0)
+ return dsync_mail_error(exporter, mail, "virtual-size");
+ i_assert(virtual_size != UOFF_T_MAX);
+ }
+
+ change = export_save_change_get(exporter, mail->uid);
+ change->guid = *guid == '\0' ? "" :
+ p_strdup(exporter->pool, guid);
+ change->hdr_hash = p_strdup(exporter->pool, hdr_hash);
+ change->received_timestamp = received_timestamp;
+ change->virtual_size = virtual_size;
+ search_update_flag_changes(exporter, mail, change);
+
+ export_add_mail_instance(exporter, change, mail->seq);
+ return 1;
+}
+
+static void
+dsync_mailbox_export_add_flagchange_uids(struct dsync_mailbox_exporter *exporter,
+ ARRAY_TYPE(seq_range) *uids)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct dsync_mail_change *change;
+
+ iter = hash_table_iterate_init(exporter->changes);
+ while (hash_table_iterate(iter, exporter->changes, &key, &change)) {
+ if (change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE)
+ seq_range_array_add(uids, change->uid);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static void
+dsync_mailbox_export_drop_expunged_flag_changes(struct dsync_mailbox_exporter *exporter)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct dsync_mail_change *change;
+
+ /* any flag changes for UIDs above last_common_uid weren't found by
+ mailbox search, which means they were already expunged. for some
+ reason the log scanner found flag changes for the message, but not
+ the expunge. just remove these. */
+ iter = hash_table_iterate_init(exporter->changes);
+ while (hash_table_iterate(iter, exporter->changes, &key, &change)) {
+ if (change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE &&
+ change->uid > exporter->last_common_uid)
+ hash_table_remove(exporter->changes, key);
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static void
+dsync_mailbox_export_search(struct dsync_mailbox_exporter *exporter)
+{
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+ struct mail *mail;
+ enum mail_fetch_field wanted_fields = 0;
+ struct mailbox_header_lookup_ctx *wanted_headers = NULL;
+ int ret = 0;
+
+ search_args = mail_search_build_init();
+ sarg = mail_search_build_add(search_args, SEARCH_UIDSET);
+ p_array_init(&sarg->value.seqset, search_args->pool, 1);
+
+ if (exporter->return_all_mails || exporter->last_common_uid == 0) {
+ /* we want to know about all mails */
+ seq_range_array_add_range(&sarg->value.seqset, 1, (uint32_t)-1);
+ } else {
+ /* lookup GUIDs for messages with flag changes */
+ dsync_mailbox_export_add_flagchange_uids(exporter,
+ &sarg->value.seqset);
+ /* lookup new messages */
+ seq_range_array_add_range(&sarg->value.seqset,
+ exporter->last_common_uid + 1,
+ (uint32_t)-1);
+ }
+
+ if (exporter->last_common_uid == 0) {
+ /* we're syncing all mails, so we can request the wanted
+ fields for all the mails */
+ wanted_fields = MAIL_FETCH_GUID;
+ wanted_headers = exporter->wanted_headers;
+ }
+
+ exporter->trans = mailbox_transaction_begin(exporter->box,
+ MAILBOX_TRANSACTION_FLAG_SYNC,
+ __func__);
+ search_ctx = mailbox_search_init(exporter->trans, search_args, NULL,
+ wanted_fields, wanted_headers);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ T_BEGIN {
+ if (mail->uid <= exporter->last_common_uid)
+ ret = search_update_flag_change_guid(exporter, mail);
+ else
+ ret = search_add_save(exporter, mail);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ i_assert(ret >= 0 || exporter->error != NULL);
+
+ dsync_mailbox_export_drop_expunged_flag_changes(exporter);
+
+ if (mailbox_search_deinit(&search_ctx) < 0 &&
+ exporter->error == NULL) {
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Mail search failed: %s",
+ mailbox_get_last_internal_error(exporter->box,
+ &exporter->mail_error));
+ }
+}
+
+static int dsync_mail_change_p_uid_cmp(struct dsync_mail_change *const *c1,
+ struct dsync_mail_change *const *c2)
+{
+ if ((*c1)->uid < (*c2)->uid)
+ return -1;
+ if ((*c1)->uid > (*c2)->uid)
+ return 1;
+ return 0;
+}
+
+static void
+dsync_mailbox_export_sort_changes(struct dsync_mailbox_exporter *exporter)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct dsync_mail_change *change;
+
+ p_array_init(&exporter->sorted_changes, exporter->pool,
+ hash_table_count(exporter->changes));
+
+ iter = hash_table_iterate_init(exporter->changes);
+ while (hash_table_iterate(iter, exporter->changes, &key, &change))
+ array_push_back(&exporter->sorted_changes, &change);
+ hash_table_iterate_deinit(&iter);
+ array_sort(&exporter->sorted_changes, dsync_mail_change_p_uid_cmp);
+}
+
+static void
+dsync_mailbox_export_attr_init(struct dsync_mailbox_exporter *exporter,
+ enum mail_attribute_type type)
+{
+ exporter->attr_iter =
+ mailbox_attribute_iter_init(exporter->box, type, "");
+ exporter->attr_type = type;
+}
+
+static void
+dsync_mailbox_export_log_scan(struct dsync_mailbox_exporter *exporter,
+ struct dsync_transaction_log_scan *log_scan)
+{
+ HASH_TABLE_TYPE(dsync_uid_mail_change) log_changes;
+ struct hash_iterate_context *iter;
+ void *key;
+ struct dsync_mail_change *change, *dup_change;
+
+ log_changes = dsync_transaction_log_scan_get_hash(log_scan);
+ if (dsync_transaction_log_scan_has_all_changes(log_scan)) {
+ /* we tried to access too old/invalid modseqs. to make sure
+ no changes get lost, we need to send all of the messages */
+ exporter->return_all_mails = TRUE;
+ }
+
+ /* clone the hash table, since we're changing it. */
+ hash_table_create_direct(&exporter->changes, exporter->pool,
+ hash_table_count(log_changes));
+ iter = hash_table_iterate_init(log_changes);
+ while (hash_table_iterate(iter, log_changes, &key, &change)) {
+ dup_change = p_new(exporter->pool, struct dsync_mail_change, 1);
+ *dup_change = *change;
+ hash_table_insert(exporter->changes, key, dup_change);
+ if (exporter->highest_changed_uid < change->uid)
+ exporter->highest_changed_uid = change->uid;
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+struct dsync_mailbox_exporter *
+dsync_mailbox_export_init(struct mailbox *box,
+ struct dsync_transaction_log_scan *log_scan,
+ uint32_t last_common_uid,
+ enum dsync_mailbox_exporter_flags flags,
+ unsigned int hdr_hash_version,
+ const char *const *hashed_headers)
+{
+ struct dsync_mailbox_exporter *exporter;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox export",
+ 4096);
+ exporter = p_new(pool, struct dsync_mailbox_exporter, 1);
+ exporter->pool = pool;
+ exporter->box = box;
+ exporter->log_scan = log_scan;
+ exporter->last_common_uid = last_common_uid;
+ exporter->auto_export_mails =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS) != 0;
+ exporter->mails_have_guids =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS) != 0;
+ exporter->minimal_dmail_fill =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL) != 0;
+ exporter->export_received_timestamps =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS) != 0;
+ exporter->export_virtual_sizes =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES) != 0;
+ exporter->hdr_hash_version = hdr_hash_version;
+ exporter->no_hdr_hashes =
+ (flags & DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES) != 0;
+ exporter->hashed_headers = hashed_headers;
+
+ p_array_init(&exporter->requested_uids, pool, 16);
+ p_array_init(&exporter->search_uids, pool, 16);
+ hash_table_create(&exporter->export_guids, pool, 0, str_hash, strcmp);
+ p_array_init(&exporter->expunged_seqs, pool, 16);
+ p_array_init(&exporter->expunged_guids, pool, 16);
+
+ if (!exporter->mails_have_guids && !exporter->no_hdr_hashes)
+ exporter->wanted_headers =
+ dsync_mail_get_hash_headers(box, exporter->hashed_headers);
+
+ /* first scan transaction log and save any expunges and flag changes */
+ dsync_mailbox_export_log_scan(exporter, log_scan);
+ /* get saves and also find GUIDs for flag changes */
+ dsync_mailbox_export_search(exporter);
+ /* get the changes sorted by UID */
+ dsync_mailbox_export_sort_changes(exporter);
+
+ dsync_mailbox_export_attr_init(exporter, MAIL_ATTRIBUTE_TYPE_PRIVATE);
+ return exporter;
+}
+
+static int
+dsync_mailbox_export_iter_next_nonexistent_attr(struct dsync_mailbox_exporter *exporter)
+{
+ struct dsync_mailbox_attribute *attr;
+ struct mail_attribute_value value;
+
+ while (hash_table_iterate(exporter->attr_change_iter,
+ dsync_transaction_log_scan_get_attr_hash(exporter->log_scan),
+ &attr, &attr)) {
+ if (attr->exported || !attr->deleted)
+ continue;
+
+ /* lookup the value mainly to get its last_change value. */
+ if (mailbox_attribute_get_stream(exporter->box, attr->type,
+ attr->key, &value) < 0) {
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Mailbox attribute %s lookup failed: %s", attr->key,
+ mailbox_get_last_internal_error(exporter->box,
+ &exporter->mail_error));
+ break;
+ }
+ if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0) {
+ i_stream_unref(&value.value_stream);
+ continue;
+ }
+
+ attr->last_change = value.last_change;
+ if (value.value != NULL || value.value_stream != NULL) {
+ attr->value = p_strdup(exporter->pool, value.value);
+ attr->value_stream = value.value_stream;
+ attr->deleted = FALSE;
+ }
+ attr->exported = TRUE;
+ exporter->attr = *attr;
+ return 1;
+ }
+ hash_table_iterate_deinit(&exporter->attr_change_iter);
+ return 0;
+}
+
+static int
+dsync_mailbox_export_iter_next_attr(struct dsync_mailbox_exporter *exporter)
+{
+ HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
+ struct dsync_mailbox_attribute lookup_attr, *attr;
+ struct dsync_mailbox_attribute *attr_change;
+ const char *key;
+ struct mail_attribute_value value;
+ bool export_all_attrs;
+
+ export_all_attrs = exporter->return_all_mails ||
+ exporter->last_common_uid == 0;
+ attr_changes = dsync_transaction_log_scan_get_attr_hash(exporter->log_scan);
+ lookup_attr.type = exporter->attr_type;
+
+ /* note that the order of processing may be important for some
+ attributes. for example sieve can't set a script active until it's
+ first been created */
+ while ((key = mailbox_attribute_iter_next(exporter->attr_iter)) != NULL) {
+ lookup_attr.key = key;
+ attr_change = hash_table_lookup(attr_changes, &lookup_attr);
+ if (attr_change == NULL && !export_all_attrs)
+ continue;
+
+ if (mailbox_attribute_get_stream(exporter->box,
+ exporter->attr_type, key,
+ &value) < 0) {
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Mailbox attribute %s lookup failed: %s", key,
+ mailbox_get_last_internal_error(exporter->box,
+ &exporter->mail_error));
+ return -1;
+ }
+ if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0) {
+ /* readonly attributes can't be changed,
+ no point in exporting them */
+ if (value.value_stream != NULL)
+ i_stream_unref(&value.value_stream);
+ continue;
+ }
+ if (value.value == NULL && value.value_stream == NULL &&
+ (attr_change == NULL || !attr_change->deleted)) {
+ /* the attribute was just deleted?
+ skip for this sync. */
+ continue;
+ }
+ if (attr_change != NULL && attr_change->exported) {
+ /* duplicate attribute returned.
+ shouldn't normally happen, but don't crash. */
+ i_warning("Ignoring duplicate attributes '%s'", key);
+ continue;
+ }
+
+ attr = &exporter->attr;
+ i_zero(attr);
+ attr->type = exporter->attr_type;
+ attr->value = p_strdup(exporter->pool, value.value);
+ attr->value_stream = value.value_stream;
+ attr->last_change = value.last_change;
+ if (attr_change != NULL) {
+ attr_change->exported = TRUE;
+ attr->key = attr_change->key;
+ attr->deleted = attr_change->deleted &&
+ !DSYNC_ATTR_HAS_VALUE(attr);
+ attr->modseq = attr_change->modseq;
+ } else {
+ attr->key = p_strdup(exporter->pool, key);
+ }
+ return 1;
+ }
+ if (mailbox_attribute_iter_deinit(&exporter->attr_iter) < 0) {
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Mailbox attribute iteration failed: %s",
+ mailbox_get_last_internal_error(exporter->box,
+ &exporter->mail_error));
+ return -1;
+ }
+ if (exporter->attr_type == MAIL_ATTRIBUTE_TYPE_PRIVATE) {
+ /* export shared attributes */
+ dsync_mailbox_export_attr_init(exporter,
+ MAIL_ATTRIBUTE_TYPE_SHARED);
+ return dsync_mailbox_export_iter_next_attr(exporter);
+ }
+ exporter->attr_change_iter = hash_table_iterate_init(attr_changes);
+ return dsync_mailbox_export_iter_next_nonexistent_attr(exporter);
+}
+
+int dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mailbox_attribute **attr_r)
+{
+ int ret;
+
+ if (exporter->error != NULL)
+ return -1;
+
+ i_stream_unref(&exporter->attr.value_stream);
+
+ if (exporter->attr_iter != NULL) {
+ ret = dsync_mailbox_export_iter_next_attr(exporter);
+ } else {
+ ret = dsync_mailbox_export_iter_next_nonexistent_attr(exporter);
+ }
+ if (ret > 0)
+ *attr_r = &exporter->attr;
+ return ret;
+}
+
+int dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail_change **change_r)
+{
+ struct dsync_mail_change *const *changes;
+ unsigned int count;
+
+ if (exporter->error != NULL)
+ return -1;
+
+ changes = array_get(&exporter->sorted_changes, &count);
+ if (exporter->change_idx == count)
+ return 0;
+ *change_r = changes[exporter->change_idx++];
+ return 1;
+}
+
+static int
+dsync_mailbox_export_body_search_init(struct dsync_mailbox_exporter *exporter)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+ struct hash_iterate_context *iter;
+ const struct seq_range *uids;
+ char *guid;
+ const char *const_guid;
+ enum mail_fetch_field wanted_fields;
+ struct dsync_mail_guid_instances *instances;
+ const struct seq_range *range;
+ unsigned int i, count;
+ uint32_t seq, seq1, seq2;
+
+ i_assert(exporter->search_ctx == NULL);
+
+ search_args = mail_search_build_init();
+ sarg = mail_search_build_add(search_args, SEARCH_SEQSET);
+ p_array_init(&sarg->value.seqset, search_args->pool, 128);
+
+ /* get a list of messages we want to fetch. if there are more than one
+ instance for a GUID, use the first one. */
+ iter = hash_table_iterate_init(exporter->export_guids);
+ while (hash_table_iterate(iter, exporter->export_guids,
+ &guid, &instances)) {
+ if (!instances->requested ||
+ array_count(&instances->seqs) == 0)
+ continue;
+
+ uids = array_front(&instances->seqs);
+ seq = uids[0].seq1;
+ if (!instances->searched) {
+ instances->searched = TRUE;
+ seq_range_array_add(&sarg->value.seqset, seq);
+ } else if (seq_range_exists(&exporter->expunged_seqs, seq)) {
+ /* we're on a second round, refetching expunged
+ messages */
+ seq_range_array_remove(&instances->seqs, seq);
+ seq_range_array_remove(&exporter->expunged_seqs, seq);
+ if (array_count(&instances->seqs) == 0) {
+ /* no instances left */
+ const_guid = guid;
+ array_push_back(&exporter->expunged_guids,
+ &const_guid);
+ continue;
+ }
+ uids = array_front(&instances->seqs);
+ seq = uids[0].seq1;
+ seq_range_array_add(&sarg->value.seqset, seq);
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ /* add requested UIDs */
+ range = array_get(&exporter->requested_uids, &count);
+ for (i = 0; i < count; i++) {
+ mailbox_get_seq_range(exporter->box,
+ range[i].seq1, range[i].seq2,
+ &seq1, &seq2);
+ seq_range_array_add_range(&sarg->value.seqset,
+ seq1, seq2);
+ }
+ array_clear(&exporter->search_uids);
+ array_append_array(&exporter->search_uids, &exporter->requested_uids);
+ array_clear(&exporter->requested_uids);
+
+ wanted_fields = MAIL_FETCH_GUID | MAIL_FETCH_SAVE_DATE;
+ if (!exporter->minimal_dmail_fill) {
+ wanted_fields |= MAIL_FETCH_RECEIVED_DATE |
+ MAIL_FETCH_UIDL_BACKEND | MAIL_FETCH_POP3_ORDER |
+ MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY;
+ }
+ exporter->search_count += seq_range_count(&sarg->value.seqset);
+ exporter->search_ctx =
+ mailbox_search_init(exporter->trans, search_args, NULL,
+ wanted_fields, NULL);
+ mail_search_args_unref(&search_args);
+ return array_count(&sarg->value.seqset) > 0 ? 1 : 0;
+}
+
+static void
+dsync_mailbox_export_body_search_deinit(struct dsync_mailbox_exporter *exporter)
+{
+ if (exporter->search_ctx == NULL)
+ return;
+
+ if (mailbox_search_deinit(&exporter->search_ctx) < 0 &&
+ exporter->error == NULL) {
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Mail search failed: %s",
+ mailbox_get_last_internal_error(exporter->box,
+ &exporter->mail_error));
+ }
+}
+
+static int dsync_mailbox_export_mail(struct dsync_mailbox_exporter *exporter,
+ struct mail *mail)
+{
+ struct dsync_mail_guid_instances *instances;
+ const char *error_field;
+
+ if (dsync_mail_fill(mail, exporter->minimal_dmail_fill,
+ &exporter->dsync_mail, &error_field) < 0)
+ return dsync_mail_error(exporter, mail, error_field);
+
+ instances = *exporter->dsync_mail.guid == '\0' ? NULL :
+ hash_table_lookup(exporter->export_guids,
+ exporter->dsync_mail.guid);
+ if (instances != NULL) {
+ /* GUID found */
+ } else if (exporter->dsync_mail.uid != 0) {
+ /* mail requested by UID */
+ } else {
+ exporter->mail_error = MAIL_ERROR_TEMP;
+ exporter->error = p_strdup_printf(exporter->pool,
+ "GUID unexpectedly changed for UID=%u GUID=%s",
+ mail->uid, exporter->dsync_mail.guid);
+ return -1;
+ }
+
+ if (!seq_range_exists(&exporter->search_uids, mail->uid))
+ exporter->dsync_mail.uid = 0;
+ else
+ exporter->dsync_mail.guid = "";
+
+ /* this message was successfully returned, don't try retrying it */
+ if (instances != NULL)
+ array_clear(&instances->seqs);
+ return 1;
+}
+
+void dsync_mailbox_export_want_mail(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail_request *request)
+{
+ struct dsync_mail_guid_instances *instances;
+
+ i_assert(!exporter->auto_export_mails);
+
+ if (request->guid == NULL) {
+ i_assert(request->uid > 0);
+ seq_range_array_add(&exporter->requested_uids, request->uid);
+ return;
+ }
+
+ instances = hash_table_lookup(exporter->export_guids, request->guid);
+ if (instances == NULL) {
+ exporter->mail_error = MAIL_ERROR_TEMP;
+ exporter->error = p_strdup_printf(exporter->pool,
+ "Remote requested unexpected GUID %s", request->guid);
+ return;
+ }
+ instances->requested = TRUE;
+}
+
+int dsync_mailbox_export_next_mail(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail **mail_r)
+{
+ struct mail *mail;
+ const char *const *guids;
+ unsigned int count;
+ int ret;
+
+ if (exporter->error != NULL)
+ return -1;
+ if (!exporter->body_search_initialized) {
+ exporter->body_search_initialized = TRUE;
+ if (dsync_mailbox_export_body_search_init(exporter) < 0) {
+ i_assert(exporter->error != NULL);
+ return -1;
+ }
+ }
+
+ while (mailbox_search_next(exporter->search_ctx, &mail)) {
+ exporter->search_pos++;
+ if ((ret = dsync_mailbox_export_mail(exporter, mail)) > 0) {
+ *mail_r = &exporter->dsync_mail;
+ return 1;
+ }
+ if (ret < 0) {
+ i_assert(exporter->error != NULL);
+ return -1;
+ }
+ /* the message was expunged. if the GUID has another instance,
+ try sending it later. */
+ seq_range_array_add(&exporter->expunged_seqs, mail->seq);
+ }
+ /* if some instances of messages were expunged, retry fetching them
+ with other instances */
+ dsync_mailbox_export_body_search_deinit(exporter);
+ if ((ret = dsync_mailbox_export_body_search_init(exporter)) < 0) {
+ i_assert(exporter->error != NULL);
+ return -1;
+ }
+ if (ret > 0) {
+ /* not finished yet */
+ return dsync_mailbox_export_next_mail(exporter, mail_r);
+ }
+
+ /* finished with messages. if there are any expunged messages,
+ return them */
+ guids = array_get(&exporter->expunged_guids, &count);
+ if (exporter->expunged_guid_idx < count) {
+ i_zero(&exporter->dsync_mail);
+ exporter->dsync_mail.guid =
+ guids[exporter->expunged_guid_idx++];
+ *mail_r = &exporter->dsync_mail;
+ return 1;
+ }
+ return 0;
+}
+
+int dsync_mailbox_export_deinit(struct dsync_mailbox_exporter **_exporter,
+ const char **errstr_r, enum mail_error *error_r)
+{
+ struct dsync_mailbox_exporter *exporter = *_exporter;
+
+ *_exporter = NULL;
+
+ if (exporter->attr_iter != NULL)
+ (void)mailbox_attribute_iter_deinit(&exporter->attr_iter);
+ dsync_mailbox_export_body_search_deinit(exporter);
+ (void)mailbox_transaction_commit(&exporter->trans);
+ mailbox_header_lookup_unref(&exporter->wanted_headers);
+
+ i_stream_unref(&exporter->attr.value_stream);
+ hash_table_destroy(&exporter->export_guids);
+ hash_table_destroy(&exporter->changes);
+
+ i_assert((exporter->error != NULL) == (exporter->mail_error != 0));
+
+ *error_r = exporter->mail_error;
+ *errstr_r = t_strdup(exporter->error);
+ pool_unref(&exporter->pool);
+ return *errstr_r != NULL ? -1 : 0;
+}
+
+const char *dsync_mailbox_export_get_proctitle(struct dsync_mailbox_exporter *exporter)
+{
+ if (exporter->search_ctx == NULL)
+ return "";
+ return t_strdup_printf("%u/%u", exporter->search_pos,
+ exporter->search_count);
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-export.h b/src/doveadm/dsync/dsync-mailbox-export.h
new file mode 100644
index 0000000..67e3216
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-export.h
@@ -0,0 +1,37 @@
+#ifndef DSYNC_MAILBOX_EXPORT_H
+#define DSYNC_MAILBOX_EXPORT_H
+
+enum dsync_mailbox_exporter_flags {
+ DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS = 0x01,
+ DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS = 0x02,
+ DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL = 0x04,
+ DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS = 0x08,
+ DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES = 0x20,
+ DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES = 0x40,
+};
+
+struct dsync_mailbox_exporter *
+dsync_mailbox_export_init(struct mailbox *box,
+ struct dsync_transaction_log_scan *log_scan,
+ uint32_t last_common_uid,
+ enum dsync_mailbox_exporter_flags flags,
+ unsigned int hdr_hash_version,
+ const char *const *hashed_headers);
+/* Returns 1 if attribute was returned, 0 if no more attributes, -1 on error */
+int dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mailbox_attribute **attr_r);
+/* Returns 1 if change was returned, 0 if no more changes, -1 on error */
+int dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail_change **change_r);
+
+void dsync_mailbox_export_want_mail(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail_request *request);
+/* Returns 1 if mail was returned, 0 if no more mails, -1 on error */
+int dsync_mailbox_export_next_mail(struct dsync_mailbox_exporter *exporter,
+ const struct dsync_mail **mail_r);
+int dsync_mailbox_export_deinit(struct dsync_mailbox_exporter **exporter,
+ const char **errstr_r, enum mail_error *error_r);
+
+const char *dsync_mailbox_export_get_proctitle(struct dsync_mailbox_exporter *exporter);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox-import.c b/src/doveadm/dsync/dsync-mailbox-import.c
new file mode 100644
index 0000000..08f2f26
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-import.c
@@ -0,0 +1,3018 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "istream.h"
+#include "seq-range-array.h"
+#include "imap-util.h"
+#include "mail-storage-private.h"
+#include "mail-search-build.h"
+#include "dsync-transaction-log-scan.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox.h"
+#include "dsync-mailbox-import.h"
+
+struct importer_mail {
+ const char *guid;
+ uint32_t uid;
+};
+
+struct importer_new_mail {
+ /* linked list of mails for this GUID */
+ struct importer_new_mail *next;
+ /* if non-NULL, this mail exists in both local and remote. this link
+ points to the other side. */
+ struct importer_new_mail *link;
+
+ const char *guid;
+ struct dsync_mail_change *change;
+
+ /* the final UID for the message */
+ uint32_t final_uid;
+ /* the original local UID, or 0 if exists only remotely */
+ uint32_t local_uid;
+ /* the original remote UID, or 0 if exists only remotely */
+ uint32_t remote_uid;
+ /* UID for the mail in the virtual \All mailbox */
+ uint32_t virtual_all_uid;
+
+ bool uid_in_local:1;
+ bool uid_is_usable:1;
+ bool skip:1;
+ bool expunged:1;
+ bool copy_failed:1;
+ bool saved:1;
+};
+
+/* for quickly testing that two-way sync doesn't actually do any unexpected
+ modifications. */
+#define IMPORTER_DEBUG_CHANGE(importer) /*i_assert(!importer->master_brain)*/
+
+HASH_TABLE_DEFINE_TYPE(guid_new_mail, const char *, struct importer_new_mail *);
+HASH_TABLE_DEFINE_TYPE(uid_new_mail, void *, struct importer_new_mail *);
+
+struct dsync_mailbox_importer {
+ pool_t pool;
+ struct mailbox *box;
+ uint32_t last_common_uid;
+ uint64_t last_common_modseq, last_common_pvt_modseq;
+ uint32_t remote_uid_next;
+ uint32_t remote_first_recent_uid;
+ uint64_t remote_highest_modseq, remote_highest_pvt_modseq;
+ time_t sync_since_timestamp;
+ time_t sync_until_timestamp;
+ uoff_t sync_max_size;
+ enum mailbox_transaction_flags transaction_flags;
+ unsigned int hdr_hash_version;
+ unsigned int commit_msgs_interval;
+
+ const char *const *hashed_headers;
+
+ enum mail_flags sync_flag;
+ const char *sync_keyword;
+ bool sync_flag_dontwant;
+
+ struct mailbox_transaction_context *trans, *ext_trans;
+ struct mail_search_context *search_ctx;
+ struct mail *mail, *ext_mail;
+
+ struct mailbox *virtual_all_box;
+ struct mailbox_transaction_context *virtual_trans;
+ struct mail *virtual_mail;
+
+ struct mail *cur_mail;
+ const char *cur_guid;
+ const char *cur_hdr_hash;
+
+ /* UID => struct dsync_mail_change */
+ HASH_TABLE_TYPE(dsync_uid_mail_change) local_changes;
+ HASH_TABLE_TYPE(dsync_attr_change) local_attr_changes;
+
+ ARRAY_TYPE(seq_range) maybe_expunge_uids;
+ ARRAY(struct dsync_mail_change *) maybe_saves;
+
+ /* GUID => struct importer_new_mail */
+ HASH_TABLE_TYPE(guid_new_mail) import_guids;
+ /* UID => struct importer_new_mail */
+ HASH_TABLE_TYPE(uid_new_mail) import_uids;
+
+ ARRAY(struct importer_new_mail *) newmails;
+ ARRAY_TYPE(uint32_t) wanted_uids;
+ ARRAY_TYPE(uint32_t) saved_uids;
+ uint32_t highest_wanted_uid;
+
+ ARRAY(struct dsync_mail_request) mail_requests;
+ unsigned int mail_request_idx;
+
+ uint32_t prev_uid, next_local_seq, local_uid_next;
+ uint64_t local_initial_highestmodseq, local_initial_highestpvtmodseq;
+ unsigned int import_pos, import_count;
+ unsigned int first_unsaved_idx, saves_since_commit;
+
+ enum mail_error mail_error;
+
+ bool failed:1;
+ bool require_full_resync:1;
+ bool debug:1;
+ bool stateful_import:1;
+ bool last_common_uid_found:1;
+ bool cur_uid_has_change:1;
+ bool cur_mail_skip:1;
+ bool local_expunged_guids_set:1;
+ bool new_uids_assigned:1;
+ bool want_mail_requests:1;
+ bool master_brain:1;
+ bool revert_local_changes:1;
+ bool mails_have_guids:1;
+ bool mails_use_guid128:1;
+ bool delete_mailbox:1;
+ bool empty_hdr_workaround:1;
+ bool no_header_hashes:1;
+};
+
+static const char *dsync_mail_change_type_names[] = {
+ "save", "expunge", "flag-change"
+};
+
+static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail,
+ struct importer_new_mail *all_newmails,
+ bool remote_mail);
+static int dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer,
+ bool final);
+
+static void ATTR_FORMAT(2, 3)
+imp_debug(struct dsync_mailbox_importer *importer, const char *fmt, ...)
+{
+ va_list args;
+
+ if (importer->debug) T_BEGIN {
+ va_start(args, fmt);
+ i_debug("brain %c: Import %s: %s",
+ importer->master_brain ? 'M' : 'S',
+ mailbox_get_vname(importer->box),
+ t_strdup_vprintf(fmt, args));
+ va_end(args);
+ } T_END;
+}
+
+static void
+dsync_import_unexpected_state(struct dsync_mailbox_importer *importer,
+ const char *error)
+{
+ if (!importer->stateful_import) {
+ i_error("Mailbox %s: %s", mailbox_get_vname(importer->box),
+ error);
+ } else {
+ i_warning("Mailbox %s doesn't match previous state: %s "
+ "(dsync must be run again without the state)",
+ mailbox_get_vname(importer->box), error);
+ }
+ importer->require_full_resync = TRUE;
+}
+
+static void
+dsync_mailbox_import_search_init(struct dsync_mailbox_importer *importer)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+
+ search_args = mail_search_build_init();
+ sarg = mail_search_build_add(search_args, SEARCH_UIDSET);
+ p_array_init(&sarg->value.seqset, search_args->pool, 128);
+ seq_range_array_add_range(&sarg->value.seqset,
+ importer->last_common_uid+1, (uint32_t)-1);
+
+ importer->search_ctx =
+ mailbox_search_init(importer->trans, search_args, NULL,
+ 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ if (mailbox_search_next(importer->search_ctx, &importer->cur_mail))
+ importer->next_local_seq = importer->cur_mail->seq;
+ /* this flag causes cur_guid to be looked up later */
+ importer->cur_mail_skip = TRUE;
+}
+
+static void
+dsync_mailbox_import_transaction_begin(struct dsync_mailbox_importer *importer)
+{
+ const enum mailbox_transaction_flags ext_trans_flags =
+ importer->transaction_flags |
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS;
+
+ importer->trans = mailbox_transaction_begin(importer->box,
+ importer->transaction_flags,
+ "dsync import");
+ importer->ext_trans = mailbox_transaction_begin(importer->box,
+ ext_trans_flags,
+ "dsync ext import");
+ importer->mail = mail_alloc(importer->trans, 0, NULL);
+ importer->ext_mail = mail_alloc(importer->ext_trans, 0, NULL);
+}
+
+struct dsync_mailbox_importer *
+dsync_mailbox_import_init(struct mailbox *box,
+ struct mailbox *virtual_all_box,
+ struct dsync_transaction_log_scan *log_scan,
+ uint32_t last_common_uid,
+ uint64_t last_common_modseq,
+ uint64_t last_common_pvt_modseq,
+ uint32_t remote_uid_next,
+ uint32_t remote_first_recent_uid,
+ uint64_t remote_highest_modseq,
+ uint64_t remote_highest_pvt_modseq,
+ time_t sync_since_timestamp,
+ time_t sync_until_timestamp,
+ uoff_t sync_max_size,
+ const char *sync_flag,
+ unsigned int commit_msgs_interval,
+ enum dsync_mailbox_import_flags flags,
+ unsigned int hdr_hash_version,
+ const char *const *hashed_headers)
+{
+ struct dsync_mailbox_importer *importer;
+ struct mailbox_status status;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox importer",
+ 10240);
+ importer = p_new(pool, struct dsync_mailbox_importer, 1);
+ importer->pool = pool;
+ importer->box = box;
+ importer->virtual_all_box = virtual_all_box;
+ importer->last_common_uid = last_common_uid;
+ importer->last_common_modseq = last_common_modseq;
+ importer->last_common_pvt_modseq = last_common_pvt_modseq;
+ importer->last_common_uid_found =
+ last_common_uid != 0 || last_common_modseq != 0;
+ importer->remote_uid_next = remote_uid_next;
+ importer->remote_first_recent_uid = remote_first_recent_uid;
+ importer->remote_highest_modseq = remote_highest_modseq;
+ importer->remote_highest_pvt_modseq = remote_highest_pvt_modseq;
+ importer->sync_since_timestamp = sync_since_timestamp;
+ importer->sync_until_timestamp = sync_until_timestamp;
+ importer->sync_max_size = sync_max_size;
+ importer->stateful_import = importer->last_common_uid_found;
+ importer->hashed_headers = hashed_headers;
+
+ if (sync_flag != NULL) {
+ if (sync_flag[0] == '-') {
+ importer->sync_flag_dontwant = TRUE;
+ sync_flag++;
+ }
+ if (sync_flag[0] == '\\')
+ importer->sync_flag = imap_parse_system_flag(sync_flag);
+ else
+ importer->sync_keyword = p_strdup(pool, sync_flag);
+ }
+ importer->commit_msgs_interval = commit_msgs_interval;
+ importer->transaction_flags = MAILBOX_TRANSACTION_FLAG_SYNC;
+ if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY) != 0)
+ importer->transaction_flags |= MAILBOX_TRANSACTION_FLAG_NO_NOTIFY;
+
+ hash_table_create(&importer->import_guids, pool, 0, str_hash, strcmp);
+ hash_table_create_direct(&importer->import_uids, pool, 0);
+ i_array_init(&importer->maybe_expunge_uids, 16);
+ i_array_init(&importer->maybe_saves, 128);
+ i_array_init(&importer->newmails, 128);
+ i_array_init(&importer->wanted_uids, 128);
+ i_array_init(&importer->saved_uids, 128);
+
+ dsync_mailbox_import_transaction_begin(importer);
+
+ if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS) != 0) {
+ i_array_init(&importer->mail_requests, 128);
+ importer->want_mail_requests = TRUE;
+ }
+ importer->master_brain =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN) != 0;
+ importer->revert_local_changes =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES) != 0;
+ importer->debug = (flags & DSYNC_MAILBOX_IMPORT_FLAG_DEBUG) != 0;
+ importer->mails_have_guids =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS) != 0;
+ importer->mails_use_guid128 =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128) != 0;
+ importer->hdr_hash_version = hdr_hash_version;
+ importer->empty_hdr_workaround =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND) != 0;
+ importer->no_header_hashes =
+ (flags & DSYNC_MAILBOX_IMPORT_FLAG_NO_HEADER_HASHES) != 0;
+ mailbox_get_open_status(importer->box, STATUS_UIDNEXT |
+ STATUS_HIGHESTMODSEQ | STATUS_HIGHESTPVTMODSEQ,
+ &status);
+ if (status.nonpermanent_modseqs)
+ status.highest_modseq = 0;
+ importer->local_uid_next = status.uidnext;
+ importer->local_initial_highestmodseq = status.highest_modseq;
+ importer->local_initial_highestpvtmodseq = status.highest_pvt_modseq;
+ dsync_mailbox_import_search_init(importer);
+
+ if (!importer->stateful_import)
+ ;
+ else if (importer->local_uid_next <= last_common_uid) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "local UIDNEXT %u <= last common UID %u",
+ importer->local_uid_next, last_common_uid));
+ } else if (importer->local_initial_highestmodseq < last_common_modseq) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64,
+ importer->local_initial_highestmodseq,
+ last_common_modseq));
+ } else if (importer->local_initial_highestpvtmodseq < last_common_pvt_modseq) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64,
+ importer->local_initial_highestpvtmodseq,
+ last_common_pvt_modseq));
+ }
+
+ importer->local_changes = dsync_transaction_log_scan_get_hash(log_scan);
+ importer->local_attr_changes = dsync_transaction_log_scan_get_attr_hash(log_scan);
+ return importer;
+}
+
+static int
+dsync_mailbox_import_lookup_attr(struct dsync_mailbox_importer *importer,
+ enum mail_attribute_type type, const char *key,
+ struct dsync_mailbox_attribute **attr_r)
+{
+ struct dsync_mailbox_attribute lookup_attr, *attr;
+ const struct dsync_mailbox_attribute *attr_change;
+ struct mail_attribute_value value;
+
+ *attr_r = NULL;
+
+ if (mailbox_attribute_get_stream(importer->box, type, key, &value) < 0) {
+ i_error("Mailbox %s: Failed to get attribute %s: %s",
+ mailbox_get_vname(importer->box), key,
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ return -1;
+ }
+
+ lookup_attr.type = type;
+ lookup_attr.key = key;
+
+ attr_change = hash_table_lookup(importer->local_attr_changes,
+ &lookup_attr);
+ if (attr_change == NULL &&
+ value.value == NULL && value.value_stream == NULL) {
+ /* we have no knowledge of this attribute */
+ return 0;
+ }
+ attr = t_new(struct dsync_mailbox_attribute, 1);
+ attr->type = type;
+ attr->key = key;
+ attr->value = value.value;
+ attr->value_stream = value.value_stream;
+ attr->last_change = value.last_change;
+ if (attr_change != NULL) {
+ attr->deleted = attr_change->deleted &&
+ !DSYNC_ATTR_HAS_VALUE(attr);
+ attr->modseq = attr_change->modseq;
+ }
+ *attr_r = attr;
+ return 0;
+}
+
+static int
+dsync_istreams_cmp(struct istream *input1, struct istream *input2, int *cmp_r)
+{
+ const unsigned char *data1, *data2;
+ size_t size1, size2, size;
+
+ *cmp_r = -1; /* quiet gcc */
+
+ for (;;) {
+ (void)i_stream_read_more(input1, &data1, &size1);
+ (void)i_stream_read_more(input2, &data2, &size2);
+
+ if (size1 == 0 || size2 == 0)
+ break;
+ size = I_MIN(size1, size2);
+ *cmp_r = memcmp(data1, data2, size);
+ if (*cmp_r != 0)
+ return 0;
+ i_stream_skip(input1, size);
+ i_stream_skip(input2, size);
+ }
+ if (input1->stream_errno != 0) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input1),
+ i_stream_get_error(input1));
+ return -1;
+ }
+ if (input2->stream_errno != 0) {
+ i_error("read(%s) failed: %s", i_stream_get_name(input2),
+ i_stream_get_error(input2));
+ return -1;
+ }
+ if (size1 == 0 && size2 == 0)
+ *cmp_r = 0;
+ else
+ *cmp_r = size1 == 0 ? -1 : 1;
+ return 0;
+}
+
+static int
+dsync_attributes_cmp_values(const struct dsync_mailbox_attribute *attr1,
+ const struct dsync_mailbox_attribute *attr2,
+ int *cmp_r)
+{
+ struct istream *input1, *input2;
+ int ret;
+
+ i_assert(attr1->value_stream != NULL || attr1->value != NULL);
+ i_assert(attr2->value_stream != NULL || attr2->value != NULL);
+
+ if (attr1->value != NULL && attr2->value != NULL) {
+ *cmp_r = strcmp(attr1->value, attr2->value);
+ return 0;
+ }
+ /* at least one of them is a stream. make both of them streams. */
+ input1 = attr1->value_stream != NULL ? attr1->value_stream :
+ i_stream_create_from_data(attr1->value, strlen(attr1->value));
+ input2 = attr2->value_stream != NULL ? attr2->value_stream :
+ i_stream_create_from_data(attr2->value, strlen(attr2->value));
+ i_stream_seek(input1, 0);
+ i_stream_seek(input2, 0);
+ ret = dsync_istreams_cmp(input1, input2, cmp_r);
+ if (attr1->value_stream == NULL)
+ i_stream_unref(&input1);
+ if (attr2->value_stream == NULL)
+ i_stream_unref(&input2);
+ return ret;
+}
+
+static int
+dsync_attributes_cmp(const struct dsync_mailbox_attribute *attr,
+ const struct dsync_mailbox_attribute *local_attr,
+ int *cmp_r)
+{
+ if (DSYNC_ATTR_HAS_VALUE(attr) &&
+ !DSYNC_ATTR_HAS_VALUE(local_attr)) {
+ /* remote has a value and local doesn't -> use it */
+ *cmp_r = 1;
+ return 0;
+ } else if (!DSYNC_ATTR_HAS_VALUE(attr) &&
+ DSYNC_ATTR_HAS_VALUE(local_attr)) {
+ /* remote doesn't have a value, bt local does -> skip */
+ *cmp_r = -1;
+ return 0;
+ }
+
+ return dsync_attributes_cmp_values(attr, local_attr, cmp_r);
+}
+
+static int
+dsync_mailbox_import_attribute_real(struct dsync_mailbox_importer *importer,
+ const struct dsync_mailbox_attribute *attr,
+ const struct dsync_mailbox_attribute *local_attr,
+ const char **result_r)
+{
+ struct mail_attribute_value value;
+ int cmp;
+ bool ignore = FALSE;
+
+ i_assert(DSYNC_ATTR_HAS_VALUE(attr) || attr->deleted);
+
+ if (attr->deleted &&
+ (local_attr == NULL || !DSYNC_ATTR_HAS_VALUE(local_attr))) {
+ /* attribute doesn't exist on either side -> ignore */
+ *result_r = "Nonexistent in both sides";
+ return 0;
+ }
+ if (local_attr == NULL) {
+ /* we haven't seen this locally -> use whatever remote has */
+ *result_r = "Nonexistent locally";
+ } else if (local_attr->modseq <= importer->last_common_modseq &&
+ attr->modseq > importer->last_common_modseq &&
+ importer->last_common_modseq > 0) {
+ /* we're doing incremental syncing, and we can see that the
+ attribute was changed remotely, but not locally -> use it */
+ *result_r = "Changed remotely";
+ } else if (local_attr->modseq > importer->last_common_modseq &&
+ attr->modseq <= importer->last_common_modseq &&
+ importer->last_common_modseq > 0) {
+ /* we're doing incremental syncing, and we can see that the
+ attribute was changed locally, but not remotely -> ignore */
+ *result_r = "Changed locally";
+ ignore = TRUE;
+ } else if (attr->last_change > local_attr->last_change) {
+ /* remote has a newer timestamp -> use it */
+ *result_r = "Remote has newer timestamp";
+ } else if (attr->last_change < local_attr->last_change) {
+ /* remote has an older timestamp -> ignore */
+ *result_r = "Local has newer timestamp";
+ ignore = TRUE;
+ } else {
+ /* the timestamps are the same. now we're down to guessing
+ the right answer, unless the values are actually equal,
+ so check that first. next try to use modseqs, but if even
+ they are the same, fallback to just picking one based on the
+ value. */
+ if (dsync_attributes_cmp(attr, local_attr, &cmp) < 0) {
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ return -1;
+ }
+ if (cmp == 0) {
+ /* identical scripts */
+ *result_r = "Unchanged value";
+ return 0;
+ }
+
+ if (attr->modseq > local_attr->modseq) {
+ /* remote has a higher modseq -> use it */
+ *result_r = "Remote has newer modseq";
+ } else if (attr->modseq < local_attr->modseq) {
+ /* remote has an older modseq -> ignore */
+ *result_r = "Local has newer modseq";
+ ignore = TRUE;
+ } else if (cmp < 0) {
+ ignore = TRUE;
+ *result_r = "Value changed, but unknown which is newer - picking local";
+ } else {
+ *result_r = "Value changed, but unknown which is newer - picking remote";
+ }
+ }
+ if (ignore)
+ return 0;
+
+ i_zero(&value);
+ value.value = attr->value;
+ value.value_stream = attr->value_stream;
+ value.last_change = attr->last_change;
+ if (mailbox_attribute_set(importer->trans, attr->type,
+ attr->key, &value) < 0) {
+ i_error("Mailbox %s: Failed to set attribute %s: %s",
+ mailbox_get_vname(importer->box), attr->key,
+ mailbox_get_last_internal_error(importer->box, NULL));
+ /* the attributes aren't vital, don't fail everything just
+ because of them. */
+ }
+ return 0;
+}
+
+int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer,
+ const struct dsync_mailbox_attribute *attr)
+{
+ struct dsync_mailbox_attribute *local_attr;
+ const char *result = "";
+ int ret;
+
+ if (dsync_mailbox_import_lookup_attr(importer, attr->type,
+ attr->key, &local_attr) < 0)
+ ret = -1;
+ else {
+ ret = dsync_mailbox_import_attribute_real(importer, attr,
+ local_attr, &result);
+ if (local_attr != NULL && local_attr->value_stream != NULL)
+ i_stream_unref(&local_attr->value_stream);
+ }
+ imp_debug(importer, "Import attribute %s: %s", attr->key,
+ ret < 0 ? "failed" : result);
+ return ret;
+}
+
+static void dsync_mail_error(struct dsync_mailbox_importer *importer,
+ struct mail *mail, const char *field)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_internal_error(importer->box, &error);
+ if (error == MAIL_ERROR_EXPUNGED)
+ return;
+
+ i_error("Mailbox %s: Can't lookup %s for UID=%u: %s",
+ mailbox_get_vname(mail->box), field, mail->uid, errstr);
+ importer->mail_error = error;
+ importer->failed = TRUE;
+}
+
+static bool
+dsync_mail_change_guid_equals(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change,
+ const char *guid, const char **cmp_guid_r)
+{
+ guid_128_t guid_128, change_guid_128;
+
+ if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ if (guid_128_from_string(change->guid, change_guid_128) < 0)
+ i_unreached();
+ } else if (importer->mails_use_guid128) {
+ mail_generate_guid_128_hash(change->guid, change_guid_128);
+ } else {
+ if (cmp_guid_r != NULL)
+ *cmp_guid_r = change->guid;
+ return strcmp(change->guid, guid) == 0;
+ }
+
+ mail_generate_guid_128_hash(guid, guid_128);
+ if (memcmp(change_guid_128, guid_128, GUID_128_SIZE) != 0) {
+ if (cmp_guid_r != NULL) {
+ *cmp_guid_r = t_strdup_printf("%s(guid128, orig=%s)",
+ binary_to_hex(change_guid_128, sizeof(change_guid_128)),
+ change->guid);
+ }
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+importer_try_next_mail(struct dsync_mailbox_importer *importer,
+ uint32_t wanted_uid)
+{
+ struct mail_private *pmail;
+ const char *hdr_hash;
+
+ if (importer->cur_mail == NULL) {
+ /* end of search */
+ return -1;
+ }
+ while (importer->cur_mail->seq < importer->next_local_seq ||
+ importer->cur_mail->uid < wanted_uid) {
+ if (!importer->cur_uid_has_change &&
+ !importer->last_common_uid_found) {
+ /* this message exists locally, but remote didn't send
+ expunge-change for it. if the message's
+ uid <= last-common-uid, it should be deleted */
+ seq_range_array_add(&importer->maybe_expunge_uids,
+ importer->cur_mail->uid);
+ }
+
+ importer->cur_mail_skip = FALSE;
+ if (!mailbox_search_next(importer->search_ctx,
+ &importer->cur_mail)) {
+ importer->cur_mail = NULL;
+ importer->cur_guid = NULL;
+ importer->cur_hdr_hash = NULL;
+ return -1;
+ }
+ importer->cur_uid_has_change = FALSE;
+ }
+ importer->cur_uid_has_change = importer->cur_mail->uid == wanted_uid;
+ if (importer->mails_have_guids) {
+ if (mail_get_special(importer->cur_mail, MAIL_FETCH_GUID,
+ &importer->cur_guid) < 0) {
+ dsync_mail_error(importer, importer->cur_mail, "GUID");
+ return 0;
+ }
+ } else {
+ if (dsync_mail_get_hdr_hash(importer->cur_mail,
+ importer->hdr_hash_version,
+ importer->hashed_headers,
+ &hdr_hash) < 0) {
+ dsync_mail_error(importer, importer->cur_mail,
+ "header hash");
+ return 0;
+ }
+ pmail = (struct mail_private *)importer->cur_mail;
+ importer->cur_hdr_hash = p_strdup(pmail->pool, hdr_hash);
+ importer->cur_guid = "";
+ }
+ /* make sure next_local_seq gets updated in case we came here
+ because of min_uid */
+ importer->next_local_seq = importer->cur_mail->seq;
+ return 1;
+}
+
+static bool
+importer_next_mail(struct dsync_mailbox_importer *importer, uint32_t wanted_uid)
+{
+ int ret;
+
+ for (;;) {
+ T_BEGIN {
+ ret = importer_try_next_mail(importer, wanted_uid);
+ } T_END;
+ if (ret != 0 || importer->failed)
+ break;
+ importer->next_local_seq = importer->cur_mail->seq + 1;
+ }
+ return ret > 0;
+}
+
+static int
+importer_mail_cmp(const struct importer_mail *m1,
+ const struct importer_mail *m2)
+{
+ int ret;
+
+ if (m1->guid == NULL)
+ return 1;
+ if (m2->guid == NULL)
+ return -1;
+
+ ret = strcmp(m1->guid, m2->guid);
+ if (ret != 0)
+ return ret;
+
+ if (m1->uid < m2->uid)
+ return -1;
+ if (m1->uid > m2->uid)
+ return 1;
+ return 0;
+}
+
+static void newmail_link(struct dsync_mailbox_importer *importer,
+ struct importer_new_mail *newmail, uint32_t remote_uid)
+{
+ struct importer_new_mail *first_mail, **last, *mail, *link = NULL;
+
+ if (*newmail->guid != '\0') {
+ first_mail = hash_table_lookup(importer->import_guids,
+ newmail->guid);
+ if (first_mail == NULL) {
+ /* first mail for this GUID */
+ hash_table_insert(importer->import_guids,
+ newmail->guid, newmail);
+ return;
+ }
+ } else {
+ if (remote_uid == 0) {
+ /* mail exists locally. we don't want to request
+ it, and we'll assume it has no duplicate
+ instances. */
+ return;
+ }
+ first_mail = hash_table_lookup(importer->import_uids,
+ POINTER_CAST(remote_uid));
+ if (first_mail == NULL) {
+ /* first mail for this UID */
+ hash_table_insert(importer->import_uids,
+ POINTER_CAST(remote_uid), newmail);
+ return;
+ }
+ }
+ /* 1) add the newmail to the end of the linked list
+ 2) find our link
+
+ FIXME: this loop is slow if the same GUID has a ton of instances.
+ Could it be improved in some way? */
+ last = &first_mail->next;
+ for (mail = first_mail; mail != NULL; mail = mail->next) {
+ if (mail->final_uid == newmail->final_uid)
+ mail->uid_is_usable = TRUE;
+ if (link == NULL && mail->link == NULL &&
+ mail->uid_in_local != newmail->uid_in_local)
+ link = mail;
+ last = &mail->next;
+ }
+ *last = newmail;
+ if (link != NULL && newmail->link == NULL) {
+ link->link = newmail;
+ newmail->link = link;
+ }
+}
+
+static void
+dsync_mailbox_revert_existing_uid(struct dsync_mailbox_importer *importer,
+ uint32_t uid, const char *reason)
+{
+ i_assert(importer->revert_local_changes);
+
+ /* UID either already exists or UIDNEXT is too high. we can't set the
+ wanted UID, so we'll need to delete the whole mailbox and resync */
+ i_warning("Deleting mailbox '%s': UID=%u already exists locally for a different mail: %s",
+ mailbox_get_vname(importer->box), uid, reason);
+ importer->delete_mailbox = TRUE;
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+}
+
+static bool dsync_mailbox_try_save_cur(struct dsync_mailbox_importer *importer,
+ struct dsync_mail_change *save_change)
+{
+ struct importer_mail m1, m2;
+ struct importer_new_mail *newmail;
+ int diff;
+ bool remote_saved;
+
+ i_zero(&m1);
+ if (importer->cur_mail != NULL) {
+ m1.guid = importer->mails_have_guids ?
+ importer->cur_guid : importer->cur_hdr_hash;
+ m1.uid = importer->cur_mail->uid;
+ }
+ i_zero(&m2);
+ if (save_change != NULL) {
+ m2.guid = importer->mails_have_guids ?
+ save_change->guid : save_change->hdr_hash;
+ m2.uid = save_change->uid;
+ i_assert(save_change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE);
+ }
+
+ if (importer->empty_hdr_workaround && !importer->mails_have_guids &&
+ importer->cur_mail != NULL && save_change != NULL &&
+ (dsync_mail_hdr_hash_is_empty(m1.guid) ||
+ dsync_mail_hdr_hash_is_empty(m2.guid))) {
+ /* one of the headers is empty. assume it's broken and that
+ the header matches what we have currently. */
+ diff = 0;
+ } else if (importer->no_header_hashes && m1.uid == m2.uid &&
+ !importer->mails_have_guids &&
+ importer->cur_mail != NULL && save_change != NULL &&
+ (m1.guid[0] == '\0' || m2.guid[0] == '\0')) {
+ /* Header hashes aren't known. Assume that the mails match if
+ their UIDs match. */
+ diff = 0;
+ } else {
+ diff = importer_mail_cmp(&m1, &m2);
+ }
+ if (diff < 0) {
+ /* add a record for local mail */
+ i_assert(importer->cur_mail != NULL);
+ if (importer->revert_local_changes) {
+ if (save_change == NULL &&
+ importer->cur_mail->uid >= importer->remote_uid_next) {
+ dsync_mailbox_revert_existing_uid(importer, importer->cur_mail->uid,
+ t_strdup_printf("higher than remote's UIDs (remote UIDNEXT=%u)", importer->remote_uid_next));
+ return TRUE;
+ }
+ mail_expunge(importer->cur_mail);
+ importer->cur_mail_skip = TRUE;
+ importer->next_local_seq++;
+ return FALSE;
+ }
+ newmail = p_new(importer->pool, struct importer_new_mail, 1);
+ newmail->guid = p_strdup(importer->pool, importer->cur_guid);
+ newmail->final_uid = importer->cur_mail->uid;
+ newmail->local_uid = importer->cur_mail->uid;
+ newmail->uid_in_local = TRUE;
+ newmail->uid_is_usable =
+ newmail->final_uid >= importer->remote_uid_next;
+ remote_saved = FALSE;
+ } else if (diff > 0) {
+ i_assert(save_change != NULL);
+ newmail = p_new(importer->pool, struct importer_new_mail, 1);
+ newmail->guid = save_change->guid;
+ newmail->final_uid = save_change->uid;
+ newmail->remote_uid = save_change->uid;
+ newmail->uid_in_local = FALSE;
+ newmail->uid_is_usable =
+ newmail->final_uid >= importer->local_uid_next;
+ if (!newmail->uid_is_usable && importer->revert_local_changes) {
+ dsync_mailbox_revert_existing_uid(importer, newmail->final_uid,
+ t_strdup_printf("UID >= local UIDNEXT=%u", importer->local_uid_next));
+ return TRUE;
+ }
+ remote_saved = TRUE;
+ } else {
+ /* identical */
+ i_assert(importer->cur_mail != NULL);
+ i_assert(save_change != NULL);
+ newmail = p_new(importer->pool, struct importer_new_mail, 1);
+ newmail->guid = save_change->guid;
+ newmail->final_uid = importer->cur_mail->uid;
+ newmail->local_uid = importer->cur_mail->uid;
+ newmail->remote_uid = save_change->uid;
+ newmail->uid_in_local = TRUE;
+ newmail->uid_is_usable = TRUE;
+ newmail->link = newmail;
+ remote_saved = TRUE;
+ }
+
+ if (newmail->uid_in_local) {
+ importer->cur_mail_skip = TRUE;
+ importer->next_local_seq++;
+ }
+ /* NOTE: assumes save_change is allocated from importer pool */
+ newmail->change = save_change;
+
+ array_push_back(&importer->newmails, &newmail);
+ if (newmail->uid_in_local)
+ newmail_link(importer, newmail, 0);
+ else {
+ i_assert(save_change != NULL);
+ newmail_link(importer, newmail, save_change->uid);
+ }
+ return remote_saved;
+}
+
+static bool ATTR_NULL(2)
+dsync_mailbox_try_save(struct dsync_mailbox_importer *importer,
+ struct dsync_mail_change *save_change)
+{
+ if (importer->cur_mail_skip) {
+ if (!importer_next_mail(importer, 0) && save_change == NULL)
+ return FALSE;
+ }
+ return dsync_mailbox_try_save_cur(importer, save_change);
+}
+
+static void dsync_mailbox_save(struct dsync_mailbox_importer *importer,
+ struct dsync_mail_change *save_change)
+{
+ while (!dsync_mailbox_try_save(importer, save_change)) ;
+}
+
+static bool
+dsync_import_set_mail(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ const char *guid, *cmp_guid;
+
+ if (!mail_set_uid(importer->mail, change->uid))
+ return FALSE;
+ if (change->guid == NULL) {
+ /* GUID is unknown */
+ return TRUE;
+ }
+ if (*change->guid == '\0') {
+ /* backend doesn't support GUIDs. if hdr_hash is set, we could
+ verify it, but since this message really is supposed to
+ match, it's probably too much trouble. */
+ return TRUE;
+ }
+
+ /* verify that GUID matches, just in case */
+ if (mail_get_special(importer->mail, MAIL_FETCH_GUID, &guid) < 0) {
+ dsync_mail_error(importer, importer->mail, "GUID");
+ return FALSE;
+ }
+ if (!dsync_mail_change_guid_equals(importer, change, guid, &cmp_guid)) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "Unexpected GUID mismatch for UID=%u: %s != %s",
+ change->uid, guid, cmp_guid));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool dsync_check_cur_guid(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ const char *cmp_guid;
+
+ if (change->guid == NULL || change->guid[0] == '\0' ||
+ importer->cur_guid[0] == '\0')
+ return TRUE;
+ if (!dsync_mail_change_guid_equals(importer, change,
+ importer->cur_guid, &cmp_guid)) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "Unexpected GUID mismatch (2) for UID=%u: %s != %s",
+ change->uid, importer->cur_guid, cmp_guid));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+merge_flags(uint32_t local_final, uint32_t local_add, uint32_t local_remove,
+ uint32_t remote_final, uint32_t remote_add, uint32_t remote_remove,
+ uint32_t pvt_mask, bool prefer_remote, bool prefer_pvt_remote,
+ uint32_t *change_add_r, uint32_t *change_remove_r,
+ bool *remote_changed, bool *remote_pvt_changed)
+{
+ uint32_t combined_add, combined_remove, conflict_flags;
+ uint32_t local_wanted, remote_wanted, conflict_pvt_flags;
+
+ /* resolve conflicts */
+ conflict_flags = local_add & remote_remove;
+ if (conflict_flags != 0) {
+ conflict_pvt_flags = conflict_flags & pvt_mask;
+ conflict_flags &= ~pvt_mask;
+ if (prefer_remote)
+ local_add &= ~conflict_flags;
+ else
+ remote_remove &= ~conflict_flags;
+ if (prefer_pvt_remote)
+ local_add &= ~conflict_pvt_flags;
+ else
+ remote_remove &= ~conflict_pvt_flags;
+ }
+ conflict_flags = local_remove & remote_add;
+ if (conflict_flags != 0) {
+ conflict_pvt_flags = conflict_flags & pvt_mask;
+ conflict_flags &= ~pvt_mask;
+ if (prefer_remote)
+ local_remove &= ~conflict_flags;
+ else
+ remote_add &= ~conflict_flags;
+ if (prefer_pvt_remote)
+ local_remove &= ~conflict_pvt_flags;
+ else
+ remote_add &= ~conflict_pvt_flags;
+ }
+
+ combined_add = local_add|remote_add;
+ combined_remove = local_remove|remote_remove;
+ i_assert((combined_add & combined_remove) == 0);
+
+ /* don't change flags that are currently identical in both sides */
+ conflict_flags = local_final ^ remote_final;
+ combined_add &= conflict_flags;
+ combined_remove &= conflict_flags;
+
+ /* see if there are conflicting final flags */
+ local_wanted = (local_final|combined_add) & ~combined_remove;
+ remote_wanted = (remote_final|combined_add) & ~combined_remove;
+
+ conflict_flags = local_wanted ^ remote_wanted;
+ if (conflict_flags != 0) {
+ if (prefer_remote && prefer_pvt_remote)
+ local_wanted = remote_wanted;
+ else if (prefer_remote && !prefer_pvt_remote) {
+ local_wanted = (local_wanted & pvt_mask) |
+ (remote_wanted & ~pvt_mask);
+ } else if (!prefer_remote && prefer_pvt_remote) {
+ local_wanted = (local_wanted & ~pvt_mask) |
+ (remote_wanted & pvt_mask);
+ }
+ }
+
+ *change_add_r = local_wanted & ~local_final;
+ *change_remove_r = local_final & ~local_wanted;
+ if ((local_wanted & ~pvt_mask) != (remote_final & ~pvt_mask))
+ *remote_changed = TRUE;
+ if ((local_wanted & pvt_mask) != (remote_final & pvt_mask))
+ *remote_pvt_changed = TRUE;
+}
+
+static bool
+keyword_find(ARRAY_TYPE(const_string) *keywords, const char *name,
+ unsigned int *idx_r)
+{
+ const char *const *names;
+ unsigned int i, count;
+
+ names = array_get(keywords, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(names[i], name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void keywords_append(ARRAY_TYPE(const_string) *dest,
+ const ARRAY_TYPE(const_string) *keywords,
+ uint32_t bits, unsigned int start_idx)
+{
+ const char *name;
+ unsigned int i;
+
+ for (i = 0; i < 32; i++) {
+ if ((bits & (1U << i)) == 0)
+ continue;
+
+ name = array_idx_elem(keywords, start_idx+i);
+ array_push_back(dest, &name);
+ }
+}
+
+static void
+merge_keywords(struct mail *mail, const ARRAY_TYPE(const_string) *local_changes,
+ const ARRAY_TYPE(const_string) *remote_changes,
+ bool prefer_remote,
+ bool *remote_changed, bool *remote_pvt_changed)
+{
+ /* local_changes and remote_changes are assumed to have no
+ duplicates names */
+ uint32_t *local_add, *local_remove, *local_final;
+ uint32_t *remote_add, *remote_remove, *remote_final;
+ uint32_t *change_add, *change_remove;
+ ARRAY_TYPE(const_string) all_keywords, add_keywords, remove_keywords;
+ const char *const *changes, *name, *const *local_keywords;
+ struct mail_keywords *kw;
+ unsigned int i, count, name_idx, array_size;
+
+ local_keywords = mail_get_keywords(mail);
+
+ /* we'll assign a common index for each keyword name and place
+ the changes to separate bit arrays. */
+ if (array_is_created(remote_changes))
+ changes = array_get(remote_changes, &count);
+ else {
+ changes = NULL;
+ count = 0;
+ }
+
+ array_size = str_array_length(local_keywords) + count;
+ if (array_is_created(local_changes))
+ array_size += array_count(local_changes);
+ if (array_size == 0) {
+ /* this message has no keywords */
+ return;
+ }
+ t_array_init(&all_keywords, array_size);
+ t_array_init(&add_keywords, array_size);
+ t_array_init(&remove_keywords, array_size);
+
+ /* @UNSAFE: create large enough arrays to fit all keyword indexes. */
+ array_size = (array_size+31)/32;
+ local_add = t_new(uint32_t, array_size);
+ local_remove = t_new(uint32_t, array_size);
+ local_final = t_new(uint32_t, array_size);
+ remote_add = t_new(uint32_t, array_size);
+ remote_remove = t_new(uint32_t, array_size);
+ remote_final = t_new(uint32_t, array_size);
+ change_add = t_new(uint32_t, array_size);
+ change_remove = t_new(uint32_t, array_size);
+
+ /* get remote changes */
+ for (i = 0; i < count; i++) {
+ name = changes[i]+1;
+ name_idx = array_count(&all_keywords);
+ array_push_back(&all_keywords, &name);
+
+ switch (changes[i][0]) {
+ case KEYWORD_CHANGE_ADD:
+ remote_add[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ case KEYWORD_CHANGE_REMOVE:
+ remote_remove[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ case KEYWORD_CHANGE_FINAL:
+ remote_final[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ case KEYWORD_CHANGE_ADD_AND_FINAL:
+ remote_add[name_idx/32] |= 1U << (name_idx%32);
+ remote_final[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ }
+ }
+
+ /* get local changes. use existing indexes for names when they exist. */
+ if (array_is_created(local_changes))
+ changes = array_get(local_changes, &count);
+ else {
+ changes = NULL;
+ count = 0;
+ }
+ for (i = 0; i < count; i++) {
+ name = changes[i]+1;
+ if (!keyword_find(&all_keywords, name, &name_idx)) {
+ name_idx = array_count(&all_keywords);
+ array_push_back(&all_keywords, &name);
+ }
+
+ switch (changes[i][0]) {
+ case KEYWORD_CHANGE_ADD:
+ case KEYWORD_CHANGE_ADD_AND_FINAL:
+ local_add[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ case KEYWORD_CHANGE_REMOVE:
+ local_remove[name_idx/32] |= 1U << (name_idx%32);
+ break;
+ case KEYWORD_CHANGE_FINAL:
+ break;
+ }
+ }
+ for (i = 0; local_keywords[i] != NULL; i++) {
+ name = local_keywords[i];
+ if (!keyword_find(&all_keywords, name, &name_idx)) {
+ name_idx = array_count(&all_keywords);
+ array_push_back(&all_keywords, &name);
+ }
+ local_final[name_idx/32] |= 1U << (name_idx%32);
+ }
+ i_assert(array_count(&all_keywords) <= array_size*32);
+ array_size = (array_count(&all_keywords)+31) / 32;
+
+ /* merge keywords */
+ for (i = 0; i < array_size; i++) {
+ merge_flags(local_final[i], local_add[i], local_remove[i],
+ remote_final[i], remote_add[i], remote_remove[i],
+ 0, prefer_remote, prefer_remote,
+ &change_add[i], &change_remove[i],
+ remote_changed, remote_pvt_changed);
+ if (change_add[i] != 0) {
+ keywords_append(&add_keywords, &all_keywords,
+ change_add[i], i*32);
+ }
+ if (change_remove[i] != 0) {
+ keywords_append(&remove_keywords, &all_keywords,
+ change_remove[i], i*32);
+ }
+ }
+
+ /* apply changes */
+ if (array_count(&add_keywords) > 0) {
+ array_append_zero(&add_keywords);
+ kw = mailbox_keywords_create_valid(mail->box,
+ array_front(&add_keywords));
+ mail_update_keywords(mail, MODIFY_ADD, kw);
+ mailbox_keywords_unref(&kw);
+ }
+ if (array_count(&remove_keywords) > 0) {
+ array_append_zero(&remove_keywords);
+ kw = mailbox_keywords_create_valid(mail->box,
+ array_front(&remove_keywords));
+ mail_update_keywords(mail, MODIFY_REMOVE, kw);
+ mailbox_keywords_unref(&kw);
+ }
+}
+
+static void
+dsync_mailbox_import_replace_flags(struct mail *mail,
+ const struct dsync_mail_change *change)
+{
+ ARRAY_TYPE(const_string) keywords;
+ struct mail_keywords *kw;
+ const char *const *changes, *name;
+ unsigned int i, count;
+
+ if (array_is_created(&change->keyword_changes))
+ changes = array_get(&change->keyword_changes, &count);
+ else {
+ changes = NULL;
+ count = 0;
+ }
+ t_array_init(&keywords, count+1);
+ for (i = 0; i < count; i++) {
+ switch (changes[i][0]) {
+ case KEYWORD_CHANGE_ADD:
+ case KEYWORD_CHANGE_FINAL:
+ case KEYWORD_CHANGE_ADD_AND_FINAL:
+ name = changes[i]+1;
+ array_push_back(&keywords, &name);
+ break;
+ case KEYWORD_CHANGE_REMOVE:
+ break;
+ }
+ }
+ array_append_zero(&keywords);
+
+ kw = mailbox_keywords_create_valid(mail->box, array_front(&keywords));
+ mail_update_keywords(mail, MODIFY_REPLACE, kw);
+ mailbox_keywords_unref(&kw);
+
+ mail_update_flags(mail, MODIFY_REPLACE,
+ change->add_flags | change->final_flags);
+ if (mail_get_modseq(mail) < change->modseq)
+ mail_update_modseq(mail, change->modseq);
+ if (mail_get_pvt_modseq(mail) < change->pvt_modseq)
+ mail_update_pvt_modseq(mail, change->pvt_modseq);
+}
+
+static void
+dsync_mailbox_import_flag_change(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ const struct dsync_mail_change *local_change;
+ enum mail_flags local_add, local_remove;
+ uint32_t change_add, change_remove;
+ uint64_t new_modseq;
+ ARRAY_TYPE(const_string) local_keyword_changes = ARRAY_INIT;
+ struct mail *mail;
+ bool prefer_remote, prefer_pvt_remote;
+ bool remote_changed = FALSE, remote_pvt_changed = FALSE;
+
+ i_assert((change->add_flags & change->remove_flags) == 0);
+
+ if (importer->cur_mail != NULL &&
+ importer->cur_mail->uid == change->uid) {
+ if (!dsync_check_cur_guid(importer, change))
+ return;
+ mail = importer->cur_mail;
+ } else {
+ if (!dsync_import_set_mail(importer, change))
+ return;
+ mail = importer->mail;
+ }
+
+ if (importer->revert_local_changes) {
+ /* dsync backup: just make the local look like remote. */
+ dsync_mailbox_import_replace_flags(mail, change);
+ return;
+ }
+
+ local_change = hash_table_lookup(importer->local_changes,
+ POINTER_CAST(change->uid));
+ if (local_change == NULL) {
+ local_add = local_remove = 0;
+ } else {
+ local_add = local_change->add_flags;
+ local_remove = local_change->remove_flags;
+ local_keyword_changes = local_change->keyword_changes;
+ }
+
+ if (mail_get_modseq(mail) < change->modseq)
+ prefer_remote = TRUE;
+ else if (mail_get_modseq(mail) > change->modseq)
+ prefer_remote = FALSE;
+ else {
+ /* identical modseq, we'll just have to pick one.
+ Note that both brains need to pick the same one, otherwise
+ they become unsynced. */
+ prefer_remote = !importer->master_brain;
+ }
+ if (mail_get_pvt_modseq(mail) < change->pvt_modseq)
+ prefer_pvt_remote = TRUE;
+ else if (mail_get_pvt_modseq(mail) > change->pvt_modseq)
+ prefer_pvt_remote = FALSE;
+ else
+ prefer_pvt_remote = !importer->master_brain;
+
+ /* merge flags */
+ merge_flags(mail_get_flags(mail), local_add, local_remove,
+ change->final_flags, change->add_flags, change->remove_flags,
+ mailbox_get_private_flags_mask(mail->box),
+ prefer_remote, prefer_pvt_remote,
+ &change_add, &change_remove,
+ &remote_changed, &remote_pvt_changed);
+
+ if (change_add != 0)
+ mail_update_flags(mail, MODIFY_ADD, change_add);
+ if (change_remove != 0)
+ mail_update_flags(mail, MODIFY_REMOVE, change_remove);
+
+ /* merge keywords */
+ merge_keywords(mail, &local_keyword_changes, &change->keyword_changes,
+ prefer_remote, &remote_changed, &remote_pvt_changed);
+
+ /* update modseqs. try to anticipate when we have to increase modseq
+ to get it closer to what remote has (although we can't guess it
+ exactly correctly) */
+ new_modseq = change->modseq;
+ if (remote_changed && new_modseq <= importer->remote_highest_modseq)
+ new_modseq = importer->remote_highest_modseq+1;
+ if (mail_get_modseq(mail) < new_modseq)
+ mail_update_modseq(mail, new_modseq);
+
+ new_modseq = change->pvt_modseq;
+ if (remote_pvt_changed && new_modseq <= importer->remote_highest_pvt_modseq)
+ new_modseq = importer->remote_highest_pvt_modseq+1;
+ if (mail_get_pvt_modseq(mail) < new_modseq)
+ mail_update_pvt_modseq(mail, new_modseq);
+}
+
+static bool
+dsync_mail_change_have_keyword(const struct dsync_mail_change *change,
+ const char *keyword)
+{
+ const char *str;
+
+ if (!array_is_created(&change->keyword_changes))
+ return FALSE;
+
+ array_foreach_elem(&change->keyword_changes, str) {
+ switch (str[0]) {
+ case KEYWORD_CHANGE_FINAL:
+ case KEYWORD_CHANGE_ADD_AND_FINAL:
+ if (strcasecmp(str+1, keyword) == 0)
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+dsync_mailbox_import_want_change(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change,
+ const char **result_r)
+{
+ if (importer->sync_since_timestamp > 0) {
+ i_assert(change->received_timestamp > 0);
+ if (change->received_timestamp < importer->sync_since_timestamp) {
+ /* mail has too old timestamp - skip it */
+ *result_r = "Ignoring missing local mail with too old timestamp";
+ return FALSE;
+ }
+ }
+ if (importer->sync_until_timestamp > 0) {
+ i_assert(change->received_timestamp > 0);
+ if (change->received_timestamp > importer->sync_until_timestamp) {
+ /* mail has too new timestamp - skip it */
+ *result_r = "Ignoring missing local mail with too new timestamp";
+ return FALSE;
+ }
+ }
+ if (importer->sync_max_size > 0) {
+ i_assert(change->virtual_size != UOFF_T_MAX);
+ if (change->virtual_size > importer->sync_max_size) {
+ /* mail is too large - skip it */
+ *result_r = "Ignoring missing local mail with too large size";
+ return FALSE;
+ }
+ }
+ if (importer->sync_flag != 0) {
+ bool have_flag = (change->final_flags & importer->sync_flag) != 0;
+
+ if (have_flag && importer->sync_flag_dontwant) {
+ *result_r = "Ignoring missing local mail that doesn't have wanted flags";
+ return FALSE;
+ }
+ if (!have_flag && !importer->sync_flag_dontwant) {
+ *result_r = "Ignoring missing local mail that has unwanted flags";
+ return FALSE;
+ }
+ }
+ if (importer->sync_keyword != NULL) {
+ bool have_kw = dsync_mail_change_have_keyword(change, importer->sync_keyword);
+
+ if (have_kw && importer->sync_flag_dontwant) {
+ *result_r = "Ignoring missing local mail that doesn't have wanted keywords";
+ return FALSE;
+ }
+ if (!have_kw && !importer->sync_flag_dontwant) {
+ *result_r = "Ignoring missing local mail that has unwanted keywords";
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void
+dsync_mailbox_import_save(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ struct dsync_mail_change *save;
+ const char *result;
+
+ i_assert(change->guid != NULL);
+
+ if (change->uid == importer->last_common_uid) {
+ /* we've already verified that the GUID matches.
+ apply flag changes if there are any. */
+ i_assert(!importer->last_common_uid_found);
+ dsync_mailbox_import_flag_change(importer, change);
+ return;
+ }
+ if (!dsync_mailbox_import_want_change(importer, change, &result))
+ return;
+
+ save = p_new(importer->pool, struct dsync_mail_change, 1);
+ dsync_mail_change_dup(importer->pool, change, save);
+
+ if (importer->last_common_uid_found) {
+ /* this is a new mail. its UID may or may not conflict with
+ an existing local mail, we'll figure it out later. */
+ i_assert(change->uid > importer->last_common_uid);
+ dsync_mailbox_save(importer, save);
+ } else {
+ /* the local mail is expunged. we'll decide later if we want
+ to save this mail locally or expunge it form remote. */
+ i_assert(change->uid > importer->last_common_uid);
+ i_assert(importer->cur_mail == NULL ||
+ change->uid < importer->cur_mail->uid);
+ array_push_back(&importer->maybe_saves, &save);
+ }
+}
+
+static void
+dsync_mailbox_import_expunge(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+
+ if (importer->last_common_uid_found) {
+ /* expunge the message, unless its GUID unexpectedly doesn't
+ match */
+ i_assert(change->uid <= importer->last_common_uid);
+ if (dsync_import_set_mail(importer, change))
+ mail_expunge(importer->mail);
+ } else if (importer->cur_mail == NULL ||
+ change->uid < importer->cur_mail->uid) {
+ /* already expunged locally, we can ignore this.
+ uid=last_common_uid if we managed to verify from
+ transaction log that the GUIDs match */
+ i_assert(change->uid >= importer->last_common_uid);
+ } else if (change->uid == importer->last_common_uid) {
+ /* already verified that the GUID matches */
+ i_assert(importer->cur_mail->uid == change->uid);
+ if (dsync_check_cur_guid(importer, change))
+ mail_expunge(importer->cur_mail);
+ } else {
+ /* we don't know yet if we should expunge this
+ message or not. queue it until we do. */
+ i_assert(change->uid > importer->last_common_uid);
+ seq_range_array_add(&importer->maybe_expunge_uids, change->uid);
+ }
+}
+
+static void
+dsync_mailbox_rewind_search(struct dsync_mailbox_importer *importer)
+{
+ /* If there are local mails after last_common_uid which we skipped
+ while trying to match the next message, we need to now go back */
+ if (importer->cur_mail != NULL &&
+ importer->cur_mail->uid <= importer->last_common_uid+1)
+ return;
+
+ importer->cur_mail = NULL;
+ importer->cur_guid = NULL;
+ importer->cur_hdr_hash = NULL;
+ importer->next_local_seq = 0;
+
+ (void)mailbox_search_deinit(&importer->search_ctx);
+ dsync_mailbox_import_search_init(importer);
+}
+
+static void
+dsync_mailbox_common_uid_found(struct dsync_mailbox_importer *importer)
+{
+ struct dsync_mail_change *const *saves;
+ struct seq_range_iter iter;
+ unsigned int n, i, count;
+ uint32_t uid;
+
+ if (importer->debug) T_BEGIN {
+ string_t *expunges = t_str_new(64);
+
+ imap_write_seq_range(expunges, &importer->maybe_expunge_uids);
+ imp_debug(importer, "Last common UID=%u. Delayed expunges=%s",
+ importer->last_common_uid, str_c(expunges));
+ } T_END;
+
+ importer->last_common_uid_found = TRUE;
+ dsync_mailbox_rewind_search(importer);
+
+ /* expunge the messages whose expunge-decision we delayed previously */
+ seq_range_array_iter_init(&iter, &importer->maybe_expunge_uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ if (uid > importer->last_common_uid) {
+ /* we expunge messages only up to last_common_uid,
+ ignore the rest */
+ break;
+ }
+
+ if (mail_set_uid(importer->mail, uid))
+ mail_expunge(importer->mail);
+ }
+
+ /* handle pending saves */
+ saves = array_get(&importer->maybe_saves, &count);
+ for (i = 0; i < count; i++) {
+ if (saves[i]->uid > importer->last_common_uid) {
+ imp_debug(importer, "Delayed save UID=%u: Save",
+ saves[i]->uid);
+ dsync_mailbox_save(importer, saves[i]);
+ } else {
+ imp_debug(importer, "Delayed save UID=%u: Ignore",
+ saves[i]->uid);
+ }
+ }
+}
+
+static int
+dsync_mailbox_import_match_msg(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change,
+ const char **result_r)
+{
+ const char *hdr_hash, *cmp_guid;
+
+ if (*change->guid != '\0' && *importer->cur_guid != '\0') {
+ /* we have GUIDs, verify them */
+ if (dsync_mail_change_guid_equals(importer, change,
+ importer->cur_guid, &cmp_guid)) {
+ *result_r = "GUIDs match";
+ return 1;
+ } else {
+ *result_r = t_strdup_printf("GUIDs don't match (%s vs %s)",
+ importer->cur_guid, cmp_guid);
+ return 0;
+ }
+ }
+
+ /* verify hdr_hash if it exists */
+ if (change->hdr_hash == NULL) {
+ i_assert(*importer->cur_guid == '\0');
+ if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ /* the message was already expunged, so we don't know
+ its header. return "unknown". */
+ *result_r = "Unknown match for expunge";
+ return -1;
+ }
+ i_error("Mailbox %s: GUIDs not supported, "
+ "sync with header hashes instead",
+ mailbox_get_vname(importer->box));
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ *result_r = "Error, invalid parameters";
+ return -1;
+ }
+
+ if (dsync_mail_get_hdr_hash(importer->cur_mail,
+ importer->hdr_hash_version,
+ importer->hashed_headers, &hdr_hash) < 0) {
+ dsync_mail_error(importer, importer->cur_mail, "hdr-stream");
+ *result_r = "Error fetching header stream";
+ return -1;
+ }
+ if (importer->empty_hdr_workaround &&
+ (dsync_mail_hdr_hash_is_empty(change->hdr_hash) ||
+ dsync_mail_hdr_hash_is_empty(hdr_hash))) {
+ *result_r = "Empty headers found with workaround enabled - assuming a match";
+ return 1;
+ } else if (importer->no_header_hashes &&
+ (change->hdr_hash[0] == '\0' || hdr_hash[0] == '\0')) {
+ *result_r = "Header hashing disabled - assuming a match";
+ return 1;
+ } else if (strcmp(change->hdr_hash, hdr_hash) == 0) {
+ *result_r = "Headers hashes match";
+ return 1;
+ } else {
+ *result_r = t_strdup_printf("Headers hashes don't match (%s vs %s)",
+ change->hdr_hash, hdr_hash);
+ return 0;
+ }
+}
+
+static bool
+dsync_mailbox_find_common_expunged_uid(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change,
+ const char **result_r)
+{
+ const struct dsync_mail_change *local_change;
+
+ if (*change->guid == '\0') {
+ /* remote doesn't support GUIDs, can't verify expunge */
+ *result_r = "GUIDs not supported, can't verify expunge";
+ return FALSE;
+ }
+
+ /* local message is expunged. see if we can find its GUID from
+ transaction log and check if the GUIDs match. The GUID in
+ log is a 128bit GUID, so we may need to convert the remote's
+ GUID string to 128bit GUID first. */
+ local_change = hash_table_lookup(importer->local_changes,
+ POINTER_CAST(change->uid));
+ if (local_change == NULL || local_change->guid == NULL) {
+ *result_r = "Expunged local mail's GUID not found";
+ return FALSE;
+ }
+
+ i_assert(local_change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE);
+ if (dsync_mail_change_guid_equals(importer, local_change,
+ change->guid, NULL)) {
+ importer->last_common_uid = change->uid;
+ *result_r = "Expunged local mail's GUID matches remote";
+ } else if (change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ dsync_mailbox_common_uid_found(importer);
+ *result_r = "Expunged local mail's GUID doesn't match remote GUID";
+ } else {
+ /* GUID mismatch for two expunged mails. dsync can't update
+ GUIDs for already expunged messages, so we can't immediately
+ determine that the rest of the messages are a mismatch. so
+ for now we'll just skip over this pair. */
+ *result_r = "Expunged mails' GUIDs don't match - delaying decision";
+ /* NOTE: the return value here doesn't matter, because the only
+ caller that checks for it never reaches this code path */
+ }
+ return TRUE;
+}
+
+static void
+dsync_mailbox_revert_missing(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ i_assert(importer->revert_local_changes);
+
+ /* mail exists on remote, but not locally. we'll need to
+ insert this mail back, which means deleting the whole
+ mailbox and resyncing. */
+ i_warning("Deleting mailbox '%s': UID=%u GUID=%s is missing locally",
+ mailbox_get_vname(importer->box),
+ change->uid, change->guid);
+ importer->delete_mailbox = TRUE;
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+}
+
+static void
+dsync_mailbox_find_common_uid(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change,
+ const char **result_r)
+{
+ int ret;
+
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE ||
+ ((change->received_timestamp > 0 ||
+ (importer->sync_since_timestamp == 0 &&
+ importer->sync_until_timestamp == 0)) &&
+ (change->virtual_size != UOFF_T_MAX || importer->sync_max_size == 0)));
+
+ /* try to find the matching local mail */
+ if (!importer_next_mail(importer, change->uid)) {
+ /* no more local mails. we can still try to match
+ expunged mails though. */
+ if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ /* mail doesn't exist remotely either, don't bother
+ looking it up locally. */
+ *result_r = "Expunged mail not found locally";
+ return;
+ }
+ i_assert(change->guid != NULL);
+ if (!dsync_mailbox_import_want_change(importer, change, result_r))
+ ;
+ else if (importer->local_uid_next <= change->uid) {
+ dsync_mailbox_common_uid_found(importer);
+ *result_r = "Mail's UID is above local UIDNEXT";
+ } else if (importer->revert_local_changes) {
+ dsync_mailbox_revert_missing(importer, change);
+ *result_r = "Reverting local change by deleting mailbox";
+ } else if (!dsync_mailbox_find_common_expunged_uid(importer, change, result_r)) {
+ /* it's unknown if this mail existed locally and was
+ expunged. since we don't want to lose any mails,
+ assume that we need to preserve the mail. use the
+ last message with a matching GUID as the last common
+ UID. */
+ dsync_mailbox_common_uid_found(importer);
+ }
+ *result_r = t_strdup_printf("%s - No more local mails found", *result_r);
+ return;
+ }
+
+ if (change->guid == NULL) {
+ /* we can't know if this UID matches */
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE);
+ *result_r = "Expunged mail has no GUID, can't verify it";
+ return;
+ }
+ if (importer->cur_mail->uid == change->uid) {
+ /* we have a matching local UID. check GUID to see if it's
+ really the same mail or not */
+ if ((ret = dsync_mailbox_import_match_msg(importer, change, result_r)) < 0) {
+ /* unknown */
+ return;
+ }
+ if (ret > 0) {
+ importer->last_common_uid = change->uid;
+ imp_debug(importer, "Last UID matched - "
+ "last_common_uid=%u",
+ importer->last_common_uid);
+ } else if (!importer->revert_local_changes) {
+ /* mismatch - found the first non-common UID */
+ imp_debug(importer, "Last UID mismatch - "
+ "last_common_uid=%u",
+ importer->last_common_uid);
+ dsync_mailbox_common_uid_found(importer);
+ } else {
+ /* mismatch and we want to revert local changes -
+ need to delete the mailbox. */
+ imp_debug(importer, "Last UID %u mismatch - "
+ "revert local changes", change->uid);
+ dsync_mailbox_revert_existing_uid(importer, change->uid, *result_r);
+ }
+ return;
+ }
+ /* mail exists remotely, but doesn't exist locally. */
+ if (!dsync_mailbox_import_want_change(importer, change, result_r))
+ return;
+ if (importer->revert_local_changes &&
+ change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ dsync_mailbox_revert_missing(importer, change);
+ *result_r = "Reverting local change by deleting mailbox";
+ } else {
+ (void)dsync_mailbox_find_common_expunged_uid(importer, change, result_r);
+ }
+ *result_r = t_strdup_printf("%s (next local mail UID=%u)",
+ *result_r, importer->cur_mail == NULL ? 0 : importer->cur_mail->uid);
+}
+
+int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change)
+{
+ const char *result;
+
+ i_assert(!importer->new_uids_assigned);
+ i_assert(importer->prev_uid < change->uid);
+
+ importer->prev_uid = change->uid;
+
+ if (importer->failed)
+ return -1;
+ if (importer->require_full_resync)
+ return 0;
+
+ if (!importer->last_common_uid_found) {
+ result = NULL;
+ dsync_mailbox_find_common_uid(importer, change, &result);
+ i_assert(result != NULL);
+ } else {
+ result = "New mail";
+ }
+
+ imp_debug(importer, "Import change type=%s GUID=%s UID=%u hdr_hash=%s result=%s",
+ dsync_mail_change_type_names[change->type],
+ change->guid != NULL ? change->guid : "<unknown>", change->uid,
+ change->hdr_hash != NULL ? change->hdr_hash : "", result);
+
+ if (importer->failed)
+ return -1;
+ if (importer->require_full_resync)
+ return 0;
+
+ if (importer->last_common_uid_found) {
+ /* a) uid <= last_common_uid for flag changes and expunges.
+ this happens only when last_common_uid was originally given
+ as parameter to importer.
+
+ when we're finding the last_common_uid ourself,
+ uid>last_common_uid always in here, because
+ last_common_uid_found=TRUE only after we find the first
+ mismatch.
+
+ b) uid > last_common_uid for i) new messages, ii) expunges
+ that were sent "just in case" */
+ if (change->uid <= importer->last_common_uid) {
+ i_assert(change->type != DSYNC_MAIL_CHANGE_TYPE_SAVE);
+ } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ /* ignore */
+ return 0;
+ } else {
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_SAVE);
+ }
+ } else {
+ /* a) uid < last_common_uid can never happen */
+ i_assert(change->uid >= importer->last_common_uid);
+ /* b) uid = last_common_uid if we've verified that the
+ messages' GUIDs match so far.
+
+ c) uid > last_common_uid: i) TYPE_EXPUNGE change has
+ GUID=NULL, so we couldn't verify yet if it matches our
+ local message, ii) local message is expunged and we couldn't
+ find its GUID */
+ if (change->uid > importer->last_common_uid) {
+ i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE ||
+ importer->cur_mail == NULL ||
+ change->uid < importer->cur_mail->uid);
+ }
+ }
+
+ switch (change->type) {
+ case DSYNC_MAIL_CHANGE_TYPE_SAVE:
+ dsync_mailbox_import_save(importer, change);
+ break;
+ case DSYNC_MAIL_CHANGE_TYPE_EXPUNGE:
+ dsync_mailbox_import_expunge(importer, change);
+ break;
+ case DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE:
+ i_assert(importer->last_common_uid_found);
+ dsync_mailbox_import_flag_change(importer, change);
+ break;
+ }
+ return importer->failed ? -1 : 0;
+}
+
+static int
+importer_new_mail_final_uid_cmp(struct importer_new_mail *const *newmail1,
+ struct importer_new_mail *const *newmail2)
+{
+ if ((*newmail1)->final_uid < (*newmail2)->final_uid)
+ return -1;
+ if ((*newmail1)->final_uid > (*newmail2)->final_uid)
+ return 1;
+ return 0;
+}
+
+static void
+dsync_mailbox_import_assign_new_uids(struct dsync_mailbox_importer *importer)
+{
+ struct importer_new_mail *newmail;
+ uint32_t common_uid_next, new_uid;
+
+ common_uid_next = I_MAX(importer->local_uid_next,
+ importer->remote_uid_next);
+ array_foreach_elem(&importer->newmails, newmail) {
+ if (newmail->skip) {
+ /* already assigned */
+ i_assert(newmail->final_uid != 0);
+ continue;
+ }
+
+ /* figure out what UID to use for the mail */
+ if (newmail->uid_is_usable) {
+ /* keep the UID */
+ new_uid = newmail->final_uid;
+ } else if (newmail->link != NULL &&
+ newmail->link->uid_is_usable) {
+ /* we can use the linked message's UID and expunge
+ this mail */
+ new_uid = newmail->link->final_uid;
+ } else {
+ i_assert(!importer->revert_local_changes);
+ new_uid = common_uid_next++;
+ imp_debug(importer, "UID %u isn't usable, assigning new UID %u",
+ newmail->final_uid, new_uid);
+ }
+
+ newmail->final_uid = new_uid;
+ if (newmail->link != NULL && newmail->link != newmail) {
+ /* skip processing the linked mail */
+ newmail->link->skip = TRUE;
+ }
+ }
+ importer->last_common_uid = common_uid_next-1;
+ importer->new_uids_assigned = TRUE;
+ /* Sort the newmails by their final_uid. This is used for tracking
+ whether an intermediate commit is allowed. */
+ array_sort(&importer->newmails, importer_new_mail_final_uid_cmp);
+}
+
+static int
+dsync_mailbox_import_local_uid(struct dsync_mailbox_importer *importer,
+ struct mail *mail, uint32_t uid, const char *guid,
+ struct dsync_mail *dmail_r)
+{
+ const char *error_field, *errstr;
+ enum mail_error error;
+
+ if (!mail_set_uid(mail, uid))
+ return 0;
+
+ /* NOTE: Errors are logged, but they don't cause the entire import
+ to fail. */
+ if (dsync_mail_fill(mail, TRUE, dmail_r, &error_field) < 0) {
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ if (error == MAIL_ERROR_EXPUNGED)
+ return 0;
+
+ i_error("Mailbox %s: Can't lookup %s for UID=%u: %s",
+ mailbox_get_vname(importer->box),
+ error_field, uid, errstr);
+ return -1;
+ }
+ if (*guid != '\0' && strcmp(guid, dmail_r->guid) != 0) {
+ dsync_import_unexpected_state(importer, t_strdup_printf(
+ "Unexpected GUID mismatch (3) for UID=%u: %s != %s",
+ uid, dmail_r->guid, guid));
+ return -1;
+ }
+ return 1;
+}
+
+static void
+dsync_mailbox_import_saved_uid(struct dsync_mailbox_importer *importer,
+ uint32_t uid)
+{
+ i_assert(importer->search_ctx == NULL);
+
+ if (importer->highest_wanted_uid < uid)
+ importer->highest_wanted_uid = uid;
+ array_push_back(&importer->wanted_uids, &uid);
+}
+
+static void
+dsync_mailbox_import_update_first_saved(struct dsync_mailbox_importer *importer)
+{
+ struct importer_new_mail *const *newmails;
+ unsigned int count;
+
+ newmails = array_get(&importer->newmails, &count);
+ while (importer->first_unsaved_idx < count) {
+ if (!newmails[importer->first_unsaved_idx]->saved)
+ break;
+ importer->first_unsaved_idx++;
+ }
+}
+
+static void
+dsync_mailbox_import_saved_newmail(struct dsync_mailbox_importer *importer,
+ struct importer_new_mail *newmail)
+{
+ dsync_mailbox_import_saved_uid(importer, newmail->final_uid);
+ newmail->saved = TRUE;
+
+ dsync_mailbox_import_update_first_saved(importer);
+ importer->saves_since_commit++;
+ /* we can commit only if all the upcoming mails will have UIDs that
+ are larger than we're committing.
+
+ Note that if any existing UIDs have been changed, the new UID is
+ usually higher than anything that is being saved so we can't do
+ an intermediate commit. It's too much extra work to try to handle
+ that situation. So here this never happens, because then
+ array_count(wanted_uids) is always higher than first_unsaved_idx. */
+ if (importer->saves_since_commit >= importer->commit_msgs_interval &&
+ importer->first_unsaved_idx == array_count(&importer->wanted_uids)) {
+ if (dsync_mailbox_import_commit(importer, FALSE) < 0)
+ importer->failed = TRUE;
+ importer->saves_since_commit = 0;
+ }
+}
+
+static bool
+dsync_msg_change_uid(struct dsync_mailbox_importer *importer,
+ uint32_t old_uid, uint32_t new_uid)
+{
+ struct mail_save_context *save_ctx;
+
+ IMPORTER_DEBUG_CHANGE(importer);
+
+ if (!mail_set_uid(importer->mail, old_uid))
+ return FALSE;
+
+ save_ctx = mailbox_save_alloc(importer->ext_trans);
+ mailbox_save_copy_flags(save_ctx, importer->mail);
+ mailbox_save_set_uid(save_ctx, new_uid);
+ if (mailbox_move(&save_ctx, importer->mail) < 0)
+ return FALSE;
+ dsync_mailbox_import_saved_uid(importer, new_uid);
+ return TRUE;
+}
+
+static bool
+dsync_mailbox_import_change_uid(struct dsync_mailbox_importer *importer,
+ ARRAY_TYPE(seq_range) *unwanted_uids,
+ uint32_t wanted_uid)
+{
+ const struct seq_range *range;
+ unsigned int count, n;
+ struct seq_range_iter iter;
+ uint32_t uid;
+
+ /* optimize by first trying to use the latest UID */
+ range = array_get(unwanted_uids, &count);
+ if (count == 0)
+ return FALSE;
+ if (dsync_msg_change_uid(importer, range[count-1].seq2, wanted_uid)) {
+ seq_range_array_remove(unwanted_uids, range[count-1].seq2);
+ return TRUE;
+ }
+ if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED)
+ seq_range_array_remove(unwanted_uids, range[count-1].seq2);
+
+ /* now try to use any of them by iterating through them. (would be
+ easier&faster to just iterate backwards, but probably too much
+ trouble to add such API) */
+ n = 0; seq_range_array_iter_init(&iter, unwanted_uids);
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ if (dsync_msg_change_uid(importer, uid, wanted_uid)) {
+ seq_range_array_remove(unwanted_uids, uid);
+ return TRUE;
+ }
+ if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED)
+ seq_range_array_remove(unwanted_uids, uid);
+ }
+ return FALSE;
+}
+
+static bool
+dsync_mailbox_import_try_local(struct dsync_mailbox_importer *importer,
+ struct importer_new_mail *all_newmails,
+ ARRAY_TYPE(seq_range) *local_uids,
+ ARRAY_TYPE(seq_range) *wanted_uids)
+{
+ ARRAY_TYPE(seq_range) assigned_uids, unwanted_uids;
+ struct seq_range_iter local_iter, wanted_iter;
+ unsigned int local_n, wanted_n;
+ uint32_t local_uid, wanted_uid;
+ struct importer_new_mail *mail;
+ struct dsync_mail dmail;
+
+ if (array_count(local_uids) == 0)
+ return FALSE;
+
+ local_n = wanted_n = 0;
+ seq_range_array_iter_init(&local_iter, local_uids);
+ seq_range_array_iter_init(&wanted_iter, wanted_uids);
+
+ /* wanted_uids contains UIDs that need to exist at the end. those that
+ don't already exist in local_uids have a higher UID than any
+ existing local UID */
+ t_array_init(&assigned_uids, array_count(wanted_uids));
+ t_array_init(&unwanted_uids, 8);
+ while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) {
+ if (seq_range_array_iter_nth(&wanted_iter, wanted_n,
+ &wanted_uid)) {
+ if (local_uid == wanted_uid) {
+ /* we have exactly the UID we want. keep it. */
+ seq_range_array_add(&assigned_uids, wanted_uid);
+ wanted_n++;
+ continue;
+ }
+ i_assert(local_uid < wanted_uid);
+ }
+ /* we no longer want this local UID. */
+ seq_range_array_add(&unwanted_uids, local_uid);
+ }
+
+ /* reuse as many existing messages as possible by changing their UIDs */
+ while (seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) {
+ if (!dsync_mailbox_import_change_uid(importer, &unwanted_uids,
+ wanted_uid))
+ break;
+ seq_range_array_add(&assigned_uids, wanted_uid);
+ wanted_n++;
+ }
+
+ /* expunge all unwanted messages */
+ local_n = 0; seq_range_array_iter_init(&local_iter, &unwanted_uids);
+ while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) {
+ IMPORTER_DEBUG_CHANGE(importer);
+ if (mail_set_uid(importer->mail, local_uid))
+ mail_expunge(importer->mail);
+ }
+
+ /* mark mails whose UIDs we got to be skipped over later */
+ for (mail = all_newmails; mail != NULL; mail = mail->next) {
+ if (!mail->skip &&
+ seq_range_exists(&assigned_uids, mail->final_uid))
+ mail->skip = TRUE;
+ }
+
+ if (!seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) {
+ /* we've assigned all wanted UIDs */
+ return TRUE;
+ }
+
+ /* try to find one existing message that we can use to copy to the
+ other instances */
+ local_n = 0; seq_range_array_iter_init(&local_iter, local_uids);
+ while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) {
+ if (dsync_mailbox_import_local_uid(importer, importer->mail,
+ local_uid, all_newmails->guid,
+ &dmail) > 0) {
+ if (dsync_mailbox_save_newmails(importer, &dmail,
+ all_newmails, FALSE))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+dsync_mailbox_import_try_virtual_all(struct dsync_mailbox_importer *importer,
+ struct importer_new_mail *all_newmails)
+{
+ struct dsync_mail dmail;
+
+ if (all_newmails->virtual_all_uid == 0)
+ return FALSE;
+
+ if (dsync_mailbox_import_local_uid(importer, importer->virtual_mail,
+ all_newmails->virtual_all_uid,
+ all_newmails->guid, &dmail) > 0) {
+ if (dsync_mailbox_save_newmails(importer, &dmail,
+ all_newmails, FALSE))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+dsync_mailbox_import_handle_mail(struct dsync_mailbox_importer *importer,
+ struct importer_new_mail *all_newmails)
+{
+ ARRAY_TYPE(seq_range) local_uids, wanted_uids;
+ struct dsync_mail_request *request;
+ struct importer_new_mail *mail;
+ const char *request_guid = NULL;
+ uint32_t request_uid = 0;
+
+ i_assert(all_newmails != NULL);
+
+ /* get the list of the current local UIDs and the wanted UIDs.
+ find the first remote instance that we can request in case there are
+ no local instances */
+ t_array_init(&local_uids, 8);
+ t_array_init(&wanted_uids, 8);
+ for (mail = all_newmails; mail != NULL; mail = mail->next) {
+ if (mail->uid_in_local)
+ seq_range_array_add(&local_uids, mail->local_uid);
+ else if (request_guid == NULL) {
+ if (*mail->guid != '\0')
+ request_guid = mail->guid;
+ request_uid = mail->remote_uid;
+ i_assert(request_uid != 0);
+ }
+ if (!mail->skip)
+ seq_range_array_add(&wanted_uids, mail->final_uid);
+ }
+ i_assert(array_count(&wanted_uids) > 0);
+
+ if (!dsync_mailbox_import_try_local(importer, all_newmails,
+ &local_uids, &wanted_uids) &&
+ !dsync_mailbox_import_try_virtual_all(importer, all_newmails)) {
+ /* no local instance. request from remote */
+ IMPORTER_DEBUG_CHANGE(importer);
+ if (importer->want_mail_requests) {
+ request = array_append_space(&importer->mail_requests);
+ request->guid = request_guid;
+ request->uid = request_uid;
+ }
+ return FALSE;
+ }
+ /* successfully handled all the mails locally */
+ importer->import_pos++;
+ return TRUE;
+}
+
+static void
+dsync_mailbox_import_find_virtual_uids(struct dsync_mailbox_importer *importer)
+{
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct importer_new_mail *newmail;
+ struct mail *mail;
+ const char *guid;
+
+ if (mailbox_sync(importer->virtual_all_box, 0) < 0) {
+ i_error("Couldn't sync \\All mailbox '%s': %s",
+ mailbox_get_vname(importer->virtual_all_box),
+ mailbox_get_last_internal_error(importer->virtual_all_box, NULL));
+ return;
+ }
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+
+ importer->virtual_trans =
+ mailbox_transaction_begin(importer->virtual_all_box,
+ importer->transaction_flags,
+ __func__);
+ search_ctx = mailbox_search_init(importer->virtual_trans, search_args,
+ NULL, MAIL_FETCH_GUID, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) {
+ /* ignore errors */
+ continue;
+ }
+ newmail = hash_table_lookup(importer->import_guids, guid);
+ if (newmail != NULL && newmail->virtual_all_uid == 0)
+ newmail->virtual_all_uid = mail->uid;
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0) {
+ i_error("Couldn't search \\All mailbox '%s': %s",
+ mailbox_get_vname(importer->virtual_all_box),
+ mailbox_get_last_internal_error(importer->virtual_all_box, NULL));
+ }
+
+ importer->virtual_mail = mail_alloc(importer->virtual_trans, 0, NULL);
+}
+
+static void
+dsync_mailbox_import_handle_local_mails(struct dsync_mailbox_importer *importer)
+{
+ struct hash_iterate_context *iter;
+ const char *key;
+ void *key2;
+ struct importer_new_mail *mail;
+
+ if (importer->virtual_all_box != NULL &&
+ hash_table_count(importer->import_guids) > 0) {
+ /* find UIDs in \All mailbox for all wanted GUIDs. */
+ dsync_mailbox_import_find_virtual_uids(importer);
+ }
+
+ iter = hash_table_iterate_init(importer->import_guids);
+ while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) {
+ T_BEGIN {
+ if (dsync_mailbox_import_handle_mail(importer, mail))
+ hash_table_remove(importer->import_guids, key);
+ } T_END;
+ }
+ hash_table_iterate_deinit(&iter);
+
+ iter = hash_table_iterate_init(importer->import_uids);
+ while (hash_table_iterate(iter, importer->import_uids, &key2, &mail)) {
+ T_BEGIN {
+ if (dsync_mailbox_import_handle_mail(importer, mail))
+ hash_table_remove(importer->import_uids, key2);
+ } T_END;
+ }
+ hash_table_iterate_deinit(&iter);
+ if (!importer->mails_have_guids) {
+ array_foreach_elem(&importer->newmails, mail) {
+ if (mail->uid_in_local)
+ (void)dsync_mailbox_import_handle_mail(importer, mail);
+ }
+ }
+}
+
+int dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer)
+{
+ i_assert(!importer->new_uids_assigned);
+
+ if (!importer->last_common_uid_found) {
+ /* handle pending expunges and flag updates */
+ dsync_mailbox_common_uid_found(importer);
+ }
+ /* skip common local mails */
+ (void)importer_next_mail(importer, importer->last_common_uid+1);
+ /* if there are any local mails left, add them to newmails list */
+ while (importer->cur_mail != NULL && !importer->failed)
+ (void)dsync_mailbox_try_save(importer, NULL);
+
+ if (importer->search_ctx != NULL) {
+ if (mailbox_search_deinit(&importer->search_ctx) < 0) {
+ i_error("Mailbox %s: Search failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ }
+ }
+ importer->import_count = hash_table_count(importer->import_guids) +
+ hash_table_count(importer->import_uids);
+
+ dsync_mailbox_import_assign_new_uids(importer);
+ /* save mails from local sources where possible,
+ request the rest from remote */
+ if (!importer->failed)
+ dsync_mailbox_import_handle_local_mails(importer);
+ return importer->failed ? -1 : 0;
+}
+
+const struct dsync_mail_request *
+dsync_mailbox_import_next_request(struct dsync_mailbox_importer *importer)
+{
+ const struct dsync_mail_request *requests;
+ unsigned int count;
+
+ requests = array_get(&importer->mail_requests, &count);
+ if (importer->mail_request_idx == count)
+ return NULL;
+ return &requests[importer->mail_request_idx++];
+}
+
+static const char *const *
+dsync_mailbox_get_final_keywords(const struct dsync_mail_change *change)
+{
+ ARRAY_TYPE(const_string) keywords;
+ const char *const *changes;
+ unsigned int i, count;
+
+ if (!array_is_created(&change->keyword_changes))
+ return NULL;
+
+ changes = array_get(&change->keyword_changes, &count);
+ t_array_init(&keywords, count);
+ for (i = 0; i < count; i++) {
+ if (changes[i][0] == KEYWORD_CHANGE_ADD ||
+ changes[i][0] == KEYWORD_CHANGE_FINAL ||
+ changes[i][0] == KEYWORD_CHANGE_ADD_AND_FINAL) {
+ const char *name = changes[i]+1;
+
+ array_push_back(&keywords, &name);
+ }
+ }
+ if (array_count(&keywords) == 0)
+ return NULL;
+
+ array_append_zero(&keywords);
+ return array_front(&keywords);
+}
+
+static void
+dsync_mailbox_save_set_metadata(struct dsync_mailbox_importer *importer,
+ struct mail_save_context *save_ctx,
+ const struct dsync_mail_change *change)
+{
+ const char *const *keyword_names;
+ struct mail_keywords *keywords;
+
+ keyword_names = dsync_mailbox_get_final_keywords(change);
+ keywords = keyword_names == NULL ? NULL :
+ mailbox_keywords_create_valid(importer->box,
+ keyword_names);
+ mailbox_save_set_flags(save_ctx, change->final_flags, keywords);
+ if (keywords != NULL)
+ mailbox_keywords_unref(&keywords);
+
+ if (change->modseq > 1) {
+ (void)mailbox_enable(importer->box, MAILBOX_FEATURE_CONDSTORE);
+ mailbox_save_set_min_modseq(save_ctx, change->modseq);
+ }
+ /* FIXME: if there already are private flags, they get lost because
+ saving can't handle updating private index. they get added on the
+ next sync though. if this is fixed here, set min_pvt_modseq also. */
+}
+
+static int
+dsync_msg_try_copy(struct dsync_mailbox_importer *importer,
+ struct mail_save_context **save_ctx_p,
+ struct importer_new_mail **all_newmails_forcopy)
+{
+ struct importer_new_mail *inst;
+
+ for (inst = *all_newmails_forcopy; inst != NULL; inst = inst->next) {
+ if (inst->uid_in_local && !inst->copy_failed &&
+ mail_set_uid(importer->mail, inst->local_uid)) {
+ if (mailbox_copy(save_ctx_p, importer->mail) < 0) {
+ enum mail_error error;
+ const char *errstr;
+
+ errstr = mailbox_get_last_internal_error(importer->box, &error);
+ if (error != MAIL_ERROR_EXPUNGED) {
+ i_warning("Failed to copy mail from UID=%u: "
+ "%s - falling back to other means",
+ inst->local_uid, errstr);
+ }
+ inst->copy_failed = TRUE;
+ return -1;
+ }
+ *all_newmails_forcopy = inst;
+ return 1;
+ }
+ }
+ *all_newmails_forcopy = NULL;
+ return 0;
+}
+
+static void
+dsync_mailbox_save_set_nonminimal(struct mail_save_context *save_ctx,
+ const struct dsync_mail *mail)
+{
+ if (mail->pop3_uidl != NULL && *mail->pop3_uidl != '\0')
+ mailbox_save_set_pop3_uidl(save_ctx, mail->pop3_uidl);
+ if (mail->pop3_order > 0)
+ mailbox_save_set_pop3_order(save_ctx, mail->pop3_order);
+ mailbox_save_set_received_date(save_ctx, mail->received_date, 0);
+}
+
+static struct mail_save_context *
+dsync_mailbox_save_init(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail,
+ struct importer_new_mail *newmail)
+{
+ struct mail_save_context *save_ctx;
+
+ save_ctx = mailbox_save_alloc(importer->ext_trans);
+ mailbox_save_set_uid(save_ctx, newmail->final_uid);
+ if (*mail->guid != '\0')
+ mailbox_save_set_guid(save_ctx, mail->guid);
+ if (mail->saved_date != 0)
+ mailbox_save_set_save_date(save_ctx, mail->saved_date);
+ dsync_mailbox_save_set_metadata(importer, save_ctx, newmail->change);
+
+ if (!mail->minimal_fields)
+ dsync_mailbox_save_set_nonminimal(save_ctx, mail);
+ return save_ctx;
+}
+
+static bool
+dsync_mailbox_save_body(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail,
+ struct importer_new_mail *newmail,
+ struct importer_new_mail **all_newmails_forcopy,
+ bool remote_mail)
+{
+ struct mail_save_context *save_ctx;
+ struct istream *input;
+ ssize_t ret;
+ bool save_failed = FALSE;
+
+ /* try to save the mail by copying an existing mail */
+ save_ctx = dsync_mailbox_save_init(importer, mail, newmail);
+ if ((ret = dsync_msg_try_copy(importer, &save_ctx, all_newmails_forcopy)) < 0) {
+ if (save_ctx == NULL)
+ save_ctx = dsync_mailbox_save_init(importer, mail, newmail);
+ }
+ if (ret <= 0 && mail->input_mail != NULL) {
+ /* copy using the source mail */
+ i_assert(mail->input_mail->uid == mail->input_mail_uid);
+ if (mailbox_copy(&save_ctx, mail->input_mail) == 0)
+ ret = 1;
+ else {
+ enum mail_error error;
+ const char *errstr;
+
+ errstr = mailbox_get_last_internal_error(importer->box, &error);
+ if (error != MAIL_ERROR_EXPUNGED) {
+ i_warning("Failed to copy source UID=%u mail: "
+ "%s - falling back to regular saving",
+ mail->input_mail->uid, errstr);
+ }
+ ret = -1;
+ save_ctx = dsync_mailbox_save_init(importer, mail, newmail);
+ }
+
+ }
+ if (ret > 0) {
+ i_assert(save_ctx == NULL);
+ dsync_mailbox_import_saved_newmail(importer, newmail);
+ return TRUE;
+ }
+ /* fallback to saving from remote stream */
+ if (!remote_mail) {
+ /* the mail isn't remote yet. we were just trying to copy a
+ local mail to avoid downloading the remote mail. */
+ mailbox_save_cancel(&save_ctx);
+ return FALSE;
+ }
+ if (mail->minimal_fields) {
+ struct dsync_mail mail2;
+ const char *error_field;
+
+ i_assert(mail->input_mail != NULL);
+
+ if (dsync_mail_fill_nonminimal(mail->input_mail, &mail2,
+ &error_field) < 0) {
+ i_error("Mailbox %s: Failed to read mail %s uid=%u: %s",
+ mailbox_get_vname(importer->box),
+ error_field, mail->uid,
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ mailbox_save_cancel(&save_ctx);
+ return TRUE;
+ }
+ dsync_mailbox_save_set_nonminimal(save_ctx, &mail2);
+ input = mail2.input;
+ } else {
+ input = mail->input;
+ }
+
+ if (input == NULL) {
+ /* it was just expunged in remote, skip it */
+ mailbox_save_cancel(&save_ctx);
+ return TRUE;
+ }
+
+ i_stream_seek(input, 0);
+ if (mailbox_save_begin(&save_ctx, input) < 0) {
+ i_error("Mailbox %s: Saving failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ return TRUE;
+ }
+ while ((ret = i_stream_read(input)) > 0 || ret == -2) {
+ if (mailbox_save_continue(save_ctx) < 0) {
+ save_failed = TRUE;
+ ret = -1;
+ break;
+ }
+ }
+ i_assert(ret == -1);
+
+ if (input->stream_errno != 0) {
+ i_error("Mailbox %s: read(msg input) failed: %s",
+ mailbox_get_vname(importer->box),
+ i_stream_get_error(input));
+ mailbox_save_cancel(&save_ctx);
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ } else if (save_failed) {
+ i_error("Mailbox %s: Saving failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ mailbox_save_cancel(&save_ctx);
+ importer->failed = TRUE;
+ } else {
+ i_assert(input->eof);
+ if (mailbox_save_finish(&save_ctx) < 0) {
+ i_error("Mailbox %s: Saving failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ } else {
+ dsync_mailbox_import_saved_newmail(importer, newmail);
+ }
+ }
+ return TRUE;
+}
+
+static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail,
+ struct importer_new_mail *all_newmails,
+ bool remote_mail)
+{
+ struct importer_new_mail *newmail, *all_newmails_forcopy;
+ bool ret = TRUE;
+
+ /* if all_newmails list is large, avoid scanning through the
+ uninteresting ones for each newmail */
+ all_newmails_forcopy = all_newmails;
+
+ /* save all instances of the message */
+ for (newmail = all_newmails; newmail != NULL && ret; newmail = newmail->next) {
+ if (!newmail->skip) T_BEGIN {
+ if (!dsync_mailbox_save_body(importer, mail, newmail,
+ &all_newmails_forcopy,
+ remote_mail))
+ ret = FALSE;
+ } T_END;
+ }
+ return ret;
+}
+
+int dsync_mailbox_import_mail(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail)
+{
+ struct importer_new_mail *all_newmails;
+
+ i_assert(mail->input == NULL || mail->input->seekable);
+ i_assert(importer->new_uids_assigned);
+
+ if (importer->failed)
+ return -1;
+ if (importer->require_full_resync)
+ return 0;
+
+ imp_debug(importer, "Import mail body for GUID=%s UID=%u",
+ mail->guid, mail->uid);
+
+ all_newmails = *mail->guid != '\0' ?
+ hash_table_lookup(importer->import_guids, mail->guid) :
+ hash_table_lookup(importer->import_uids, POINTER_CAST(mail->uid));
+ if (all_newmails == NULL) {
+ if (importer->want_mail_requests) {
+ i_error("Mailbox %s: Remote sent unwanted message body for "
+ "GUID=%s UID=%u",
+ mailbox_get_vname(importer->box),
+ mail->guid, mail->uid);
+ } else {
+ imp_debug(importer, "Skip unwanted mail body for "
+ "GUID=%s UID=%u", mail->guid, mail->uid);
+ }
+ return 0;
+ }
+ if (*mail->guid != '\0')
+ hash_table_remove(importer->import_guids, mail->guid);
+ else {
+ hash_table_remove(importer->import_uids,
+ POINTER_CAST(mail->uid));
+ }
+ importer->import_pos++;
+ if (!dsync_mailbox_save_newmails(importer, mail, all_newmails, TRUE))
+ i_unreached();
+ return importer->failed ? -1 : 0;
+}
+
+static int
+reassign_uids_in_seq_range(struct dsync_mailbox_importer *importer,
+ const ARRAY_TYPE(seq_range) *unwanted_uids)
+{
+ struct mailbox *box = importer->box;
+ const enum mailbox_transaction_flags trans_flags =
+ importer->transaction_flags |
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_arg *arg;
+ struct mail_search_context *search_ctx;
+ struct mail_save_context *save_ctx;
+ struct mail *mail;
+ unsigned int renumber_count = 0;
+ int ret = 1;
+
+ if (array_count(unwanted_uids) == 0)
+ return 1;
+
+ if (importer->debug) T_BEGIN {
+ string_t *str = t_str_new(256);
+ imap_write_seq_range(str, unwanted_uids);
+ imp_debug(importer, "Reassign UIDs: %s", str_c(str));
+ } T_END;
+
+ search_args = mail_search_build_init();
+ arg = mail_search_build_add(search_args, SEARCH_UIDSET);
+ p_array_init(&arg->value.seqset, search_args->pool,
+ array_count(unwanted_uids));
+ array_append_array(&arg->value.seqset, unwanted_uids);
+
+ trans = mailbox_transaction_begin(box, trans_flags, __func__);
+ search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ save_ctx = mailbox_save_alloc(trans);
+ mailbox_save_copy_flags(save_ctx, mail);
+ if (mailbox_move(&save_ctx, mail) < 0) {
+ i_error("Mailbox %s: Couldn't move mail within mailbox: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &importer->mail_error));
+ ret = -1;
+ } else if (ret > 0) {
+ ret = 0;
+ }
+ renumber_count++;
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0) {
+ i_error("Mailbox %s: mail search failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &importer->mail_error));
+ ret = -1;
+ }
+
+ if (mailbox_transaction_commit(&trans) < 0) {
+ i_error("Mailbox %s: UID reassign commit failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &importer->mail_error));
+ ret = -1;
+ }
+ if (ret == 0) {
+ imp_debug(importer, "Mailbox %s: Change during sync: "
+ "Renumbered %u of %u unwanted UIDs",
+ mailbox_get_vname(box),
+ renumber_count, array_count(unwanted_uids));
+ }
+ return ret;
+}
+
+static int
+reassign_unwanted_uids(struct dsync_mailbox_importer *importer,
+ const char **changes_during_sync_r)
+{
+ ARRAY_TYPE(seq_range) unwanted_uids;
+ const uint32_t *wanted_uids, *saved_uids;
+ uint32_t highest_seen_uid;
+ unsigned int i, wanted_count, saved_count;
+ int ret = 0;
+
+ wanted_uids = array_get(&importer->wanted_uids, &wanted_count);
+ saved_uids = array_get(&importer->saved_uids, &saved_count);
+ i_assert(wanted_count == saved_count);
+ if (wanted_count == 0)
+ return 0;
+ /* wanted_uids contains the UIDs we tried to save mails with.
+ if nothing changed during dsync, we should have the expected UIDs
+ (saved_uids) and all is well.
+
+ if any new messages got inserted during dsync, we'll need to fix up
+ the UIDs and let the next dsync fix up the other side. for example:
+
+ remote uids = 5,7,9 = wanted_uids
+ remote uidnext = 12
+ locally added new uid=5 ->
+ saved_uids = 10,7,9
+
+ we'll now need to reassign UIDs 5 and 10. to be fully future-proof
+ we'll reassign all UIDs between [original local uidnext .. highest
+ UID we think we know] that aren't in saved_uids. */
+
+ /* create uidset for the list of UIDs we don't want to exist */
+ t_array_init(&unwanted_uids, 8);
+ highest_seen_uid = I_MAX(importer->remote_uid_next-1,
+ importer->highest_wanted_uid);
+ i_assert(importer->local_uid_next <= highest_seen_uid);
+ seq_range_array_add_range(&unwanted_uids,
+ importer->local_uid_next, highest_seen_uid);
+ for (i = 0; i < wanted_count; i++) {
+ i_assert(i < wanted_count);
+ if (saved_uids[i] == wanted_uids[i])
+ seq_range_array_remove(&unwanted_uids, saved_uids[i]);
+ }
+
+ ret = reassign_uids_in_seq_range(importer, &unwanted_uids);
+ if (ret == 0) {
+ *changes_during_sync_r = t_strdup_printf(
+ "%u UIDs changed due to UID conflicts",
+ seq_range_count(&unwanted_uids));
+ /* conflicting changes during sync, revert our last-common-uid
+ back to a safe value. */
+ importer->last_common_uid = importer->local_uid_next - 1;
+ }
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer, bool final)
+{
+ struct mail_transaction_commit_changes changes;
+ struct seq_range_iter iter;
+ uint32_t uid;
+ unsigned int n;
+ int ret = importer->failed ? -1 : 0;
+
+ mail_free(&importer->mail);
+ mail_free(&importer->ext_mail);
+
+ /* commit saves */
+ if (mailbox_transaction_commit_get_changes(&importer->ext_trans,
+ &changes) < 0) {
+ i_error("Mailbox %s: Save commit failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box, &importer->mail_error));
+ /* removed wanted_uids that weren't actually saved */
+ array_delete(&importer->wanted_uids,
+ array_count(&importer->saved_uids),
+ array_count(&importer->wanted_uids) -
+ array_count(&importer->saved_uids));
+ mailbox_transaction_rollback(&importer->trans);
+ ret = -1;
+ } else {
+ /* remember the UIDs that were successfully saved */
+ if (importer->debug) T_BEGIN {
+ string_t *str = t_str_new(256);
+ imap_write_seq_range(str, &changes.saved_uids);
+ imp_debug(importer, "Saved UIDs: %s", str_c(str));
+ } T_END;
+ seq_range_array_iter_init(&iter, &changes.saved_uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid))
+ array_push_back(&importer->saved_uids, &uid);
+ pool_unref(&changes.pool);
+
+ /* commit flag changes and expunges */
+ if (mailbox_transaction_commit(&importer->trans) < 0) {
+ i_error("Mailbox %s: Commit failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ ret = -1;
+ }
+ }
+
+ if (!final)
+ dsync_mailbox_import_transaction_begin(importer);
+ return ret;
+}
+
+static int dsync_mailbox_import_finish(struct dsync_mailbox_importer *importer,
+ const char **changes_during_sync_r)
+{
+ struct mailbox_update update;
+ int ret;
+
+ ret = dsync_mailbox_import_commit(importer, TRUE);
+
+ if (ret == 0) {
+ /* update mailbox metadata if we successfully saved
+ everything. */
+ i_zero(&update);
+ update.min_next_uid = importer->remote_uid_next;
+ update.min_first_recent_uid =
+ I_MIN(importer->last_common_uid+1,
+ importer->remote_first_recent_uid);
+ update.min_highest_modseq = importer->remote_highest_modseq;
+ update.min_highest_pvt_modseq = importer->remote_highest_pvt_modseq;
+
+ imp_debug(importer, "Finish update: min_next_uid=%u "
+ "min_first_recent_uid=%u min_highest_modseq=%"PRIu64" "
+ "min_highest_pvt_modseq=%"PRIu64,
+ update.min_next_uid, update.min_first_recent_uid,
+ update.min_highest_modseq,
+ update.min_highest_pvt_modseq);
+
+ if (mailbox_update(importer->box, &update) < 0) {
+ i_error("Mailbox %s: Update failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ ret = -1;
+ }
+ }
+
+ /* sync mailbox to finish flag changes and expunges. */
+ if (mailbox_sync(importer->box, 0) < 0) {
+ i_error("Mailbox %s: Sync failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ ret = -1;
+ }
+ if (ret == 0) {
+ /* give new UIDs to messages that got saved with unwanted UIDs.
+ do it only if the whole transaction succeeded. */
+ if (reassign_unwanted_uids(importer, changes_during_sync_r) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+static void
+dsync_mailbox_import_check_missing_guid_imports(struct dsync_mailbox_importer *importer)
+{
+ struct hash_iterate_context *iter;
+ const char *key;
+ struct importer_new_mail *mail;
+
+ iter = hash_table_iterate_init(importer->import_guids);
+ while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) {
+ for (; mail != NULL; mail = mail->next) {
+ if (mail->skip)
+ continue;
+
+ i_error("Mailbox %s: Remote didn't send mail GUID=%s (UID=%u)",
+ mailbox_get_vname(importer->box),
+ mail->guid, mail->remote_uid);
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static void
+dsync_mailbox_import_check_missing_uid_imports(struct dsync_mailbox_importer *importer)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct importer_new_mail *mail;
+
+ iter = hash_table_iterate_init(importer->import_uids);
+ while (hash_table_iterate(iter, importer->import_uids, &key, &mail)) {
+ for (; mail != NULL; mail = mail->next) {
+ if (mail->skip)
+ continue;
+
+ i_error("Mailbox %s: Remote didn't send mail UID=%u",
+ mailbox_get_vname(importer->box),
+ mail->remote_uid);
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+int dsync_mailbox_import_deinit(struct dsync_mailbox_importer **_importer,
+ bool success,
+ uint32_t *last_common_uid_r,
+ uint64_t *last_common_modseq_r,
+ uint64_t *last_common_pvt_modseq_r,
+ uint32_t *last_messages_count_r,
+ const char **changes_during_sync_r,
+ bool *require_full_resync_r,
+ enum mail_error *error_r)
+{
+ struct dsync_mailbox_importer *importer = *_importer;
+ struct mailbox_status status;
+ int ret;
+
+ *_importer = NULL;
+ *changes_during_sync_r = NULL;
+ *require_full_resync_r = importer->require_full_resync;
+
+ if ((!success || importer->require_full_resync) && !importer->failed) {
+ importer->mail_error = MAIL_ERROR_TEMP;
+ importer->failed = TRUE;
+ }
+
+ if (!importer->new_uids_assigned && !importer->failed)
+ dsync_mailbox_import_assign_new_uids(importer);
+
+ if (!importer->failed) {
+ dsync_mailbox_import_check_missing_guid_imports(importer);
+ dsync_mailbox_import_check_missing_uid_imports(importer);
+ }
+
+ if (importer->search_ctx != NULL) {
+ if (mailbox_search_deinit(&importer->search_ctx) < 0) {
+ i_error("Mailbox %s: Search failed: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ }
+ }
+ if (dsync_mailbox_import_finish(importer, changes_during_sync_r) < 0)
+ importer->failed = TRUE;
+
+ if (importer->virtual_mail != NULL)
+ mail_free(&importer->virtual_mail);
+ if (importer->virtual_trans != NULL)
+ (void)mailbox_transaction_commit(&importer->virtual_trans);
+
+ hash_table_destroy(&importer->import_guids);
+ hash_table_destroy(&importer->import_uids);
+ array_free(&importer->maybe_expunge_uids);
+ array_free(&importer->maybe_saves);
+ array_free(&importer->wanted_uids);
+ array_free(&importer->saved_uids);
+ array_free(&importer->newmails);
+ if (array_is_created(&importer->mail_requests))
+ array_free(&importer->mail_requests);
+
+ *last_common_uid_r = importer->last_common_uid;
+ if (*changes_during_sync_r == NULL) {
+ *last_common_modseq_r = importer->remote_highest_modseq;
+ *last_common_pvt_modseq_r = importer->remote_highest_pvt_modseq;
+ } else {
+ /* local changes occurred during dsync. we exported changes up
+ to local_initial_highestmodseq, so all of the changes have
+ happened after it. we want the next run to see those changes,
+ so return it as the last common modseq */
+ *last_common_modseq_r = importer->local_initial_highestmodseq;
+ *last_common_pvt_modseq_r = importer->local_initial_highestpvtmodseq;
+ }
+ if (importer->delete_mailbox) {
+ if (mailbox_delete(importer->box) < 0) {
+ i_error("Couldn't delete mailbox %s: %s",
+ mailbox_get_vname(importer->box),
+ mailbox_get_last_internal_error(importer->box,
+ &importer->mail_error));
+ importer->failed = TRUE;
+ }
+ *last_messages_count_r = 0;
+ } else {
+ mailbox_get_open_status(importer->box, STATUS_MESSAGES, &status);
+ *last_messages_count_r = status.messages;
+ }
+
+ i_assert(importer->failed == (importer->mail_error != 0));
+ ret = importer->failed ? -1 : 0;
+ *error_r = importer->mail_error;
+ pool_unref(&importer->pool);
+ return ret;
+}
+
+const char *dsync_mailbox_import_get_proctitle(struct dsync_mailbox_importer *importer)
+{
+ if (importer->search_ctx != NULL)
+ return "";
+ return t_strdup_printf("%u/%u", importer->import_pos,
+ importer->import_count);
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-import.h b/src/doveadm/dsync/dsync-mailbox-import.h
new file mode 100644
index 0000000..c860e06
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-import.h
@@ -0,0 +1,64 @@
+#ifndef DSYNC_MAILBOX_IMPORT_H
+#define DSYNC_MAILBOX_IMPORT_H
+
+#include "mail-error.h"
+
+enum dsync_mailbox_import_flags {
+ DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN = 0x01,
+ DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS = 0x02,
+ DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES = 0x04,
+ DSYNC_MAILBOX_IMPORT_FLAG_DEBUG = 0x08,
+ DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS = 0x10,
+ DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128 = 0x20,
+ DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY = 0x40,
+ DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND = 0x100,
+ DSYNC_MAILBOX_IMPORT_FLAG_NO_HEADER_HASHES = 0x200,
+};
+
+struct mailbox;
+struct dsync_mailbox_attribute;
+struct dsync_mail;
+struct dsync_mail_change;
+struct dsync_transaction_log_scan;
+
+struct dsync_mailbox_importer *
+dsync_mailbox_import_init(struct mailbox *box,
+ struct mailbox *virtual_all_box,
+ struct dsync_transaction_log_scan *log_scan,
+ uint32_t last_common_uid,
+ uint64_t last_common_modseq,
+ uint64_t last_common_pvt_modseq,
+ uint32_t remote_uid_next,
+ uint32_t remote_first_recent_uid,
+ uint64_t remote_highest_modseq,
+ uint64_t remote_highest_pvt_modseq,
+ time_t sync_since_timestamp,
+ time_t sync_until_timestamp,
+ uoff_t sync_max_size,
+ const char *sync_flag,
+ unsigned int commit_msgs_interval,
+ enum dsync_mailbox_import_flags flags,
+ unsigned int hdr_hash_version,
+ const char *const *hashed_headers);
+int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer,
+ const struct dsync_mailbox_attribute *attr);
+int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail_change *change);
+int dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer);
+const struct dsync_mail_request *
+dsync_mailbox_import_next_request(struct dsync_mailbox_importer *importer);
+int dsync_mailbox_import_mail(struct dsync_mailbox_importer *importer,
+ const struct dsync_mail *mail);
+int dsync_mailbox_import_deinit(struct dsync_mailbox_importer **importer,
+ bool success,
+ uint32_t *last_common_uid_r,
+ uint64_t *last_common_modseq_r,
+ uint64_t *last_common_pvt_modseq_r,
+ uint32_t *last_messages_count_r,
+ const char **changes_during_sync_r,
+ bool *require_full_resync_r,
+ enum mail_error *error_r);
+
+const char *dsync_mailbox_import_get_proctitle(struct dsync_mailbox_importer *importer);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox-state.c b/src/doveadm/dsync/dsync-mailbox-state.c
new file mode 100644
index 0000000..cca24d4
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-state.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "base64.h"
+#include "crc32.h"
+#include "hash.h"
+#include "dsync-mailbox-state.h"
+
+#define DSYNC_STATE_MAJOR_VERSION 1
+#define DSYNC_STATE_MINOR_VERSION 0
+
+#define V0_MAILBOX_SIZE (GUID_128_SIZE + 4 + 4 + 8 + 8)
+#define MAILBOX_SIZE (GUID_128_SIZE + 4 + 4 + 8 + 8 + 4)
+
+static void put_uint32(buffer_t *output, uint32_t num)
+{
+ uint8_t tmp[sizeof(uint32_t)];
+
+ cpu32_to_le_unaligned(num, tmp);
+
+ buffer_append(output, tmp, sizeof(tmp));
+}
+
+void dsync_mailbox_states_export(const HASH_TABLE_TYPE(dsync_mailbox_state) states,
+ string_t *output)
+{
+ struct hash_iterate_context *iter;
+ struct dsync_mailbox_state *state;
+ uint8_t *guid;
+ buffer_t *buf = t_buffer_create(128);
+ uint32_t crc = 0;
+
+ buffer_append_c(buf, DSYNC_STATE_MAJOR_VERSION);
+ buffer_append_c(buf, DSYNC_STATE_MINOR_VERSION);
+ buffer_append_c(buf, '\0');
+ buffer_append_c(buf, '\0');
+
+ iter = hash_table_iterate_init(states);
+ while (hash_table_iterate(iter, states, &guid, &state)) {
+ buffer_append(buf, state->mailbox_guid,
+ sizeof(state->mailbox_guid));
+ put_uint32(buf, state->last_uidvalidity);
+ put_uint32(buf, state->last_common_uid);
+ put_uint32(buf, state->last_common_modseq & 0xffffffffU);
+ put_uint32(buf, state->last_common_modseq >> 32);
+ put_uint32(buf, state->last_common_pvt_modseq & 0xffffffffU);
+ put_uint32(buf, state->last_common_pvt_modseq >> 32);
+ put_uint32(buf, state->last_messages_count); /* v1 */
+ if (buf->used % 3 == 0) {
+ crc = crc32_data_more(crc, buf->data, buf->used);
+ base64_encode(buf->data, buf->used, output);
+ buffer_set_used_size(buf, 0);
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ crc = crc32_data_more(crc, buf->data, buf->used);
+ put_uint32(buf, crc);
+ base64_encode(buf->data, buf->used, output);
+}
+
+static int dsync_mailbox_states_retry_import_v0(const buffer_t *buf)
+{
+ const unsigned char *data = buf->data;
+
+ /* v0 had no version header and no last_messages_count */
+
+ if ((buf->used-4) % V0_MAILBOX_SIZE != 0 ||
+ le32_to_cpu_unaligned(data + buf->used-4) != crc32_data(data, buf->used-4))
+ return -1;
+ /* looks like valid v0 format, silently treat it as empty state */
+ return 0;
+}
+
+int dsync_mailbox_states_import(HASH_TABLE_TYPE(dsync_mailbox_state) states,
+ pool_t pool, const char *input,
+ const char **error_r)
+{
+ struct dsync_mailbox_state *state;
+ buffer_t *buf;
+ uint8_t *guid_p;
+ const unsigned char *data;
+ unsigned int i, count;
+
+ buf = t_buffer_create(strlen(input));
+ if (base64_decode(input, strlen(input), NULL, buf) < 0) {
+ *error_r = "Invalid base64 data";
+ return -1;
+ }
+ /* v1: 4 byte header, mailboxes[], CRC32 */
+ data = buf->data;
+
+ if (buf->used == 4 && le32_to_cpu_unaligned(data) == 0) {
+ /* v0: Empty state */
+ return 0;
+ }
+ if (buf->used < 8) {
+ *error_r = "Input too small";
+ return -1;
+ }
+
+ if ((buf->used-8) % MAILBOX_SIZE != 0) {
+ *error_r = "Invalid input size";
+ return dsync_mailbox_states_retry_import_v0(buf);
+ }
+
+ if (le32_to_cpu_unaligned(data + buf->used-4) != crc32_data(data, buf->used-4)) {
+ *error_r = "CRC32 mismatch";
+ return dsync_mailbox_states_retry_import_v0(buf);
+ }
+ data += 4;
+ count = (buf->used-8) / MAILBOX_SIZE;
+
+ for (i = 0; i < count; i++, data += MAILBOX_SIZE) {
+ state = p_new(pool, struct dsync_mailbox_state, 1);
+ memcpy(state->mailbox_guid, data, GUID_128_SIZE);
+ state->last_uidvalidity = le32_to_cpu_unaligned(data + GUID_128_SIZE);
+ state->last_common_uid = le32_to_cpu_unaligned(data + GUID_128_SIZE + 4);
+ state->last_common_modseq = le64_to_cpu_unaligned(data + GUID_128_SIZE + 8);
+ state->last_common_pvt_modseq = le64_to_cpu_unaligned(data + GUID_128_SIZE + 16);
+ state->last_messages_count = le32_to_cpu_unaligned(data + GUID_128_SIZE + 24);
+ guid_p = state->mailbox_guid;
+ hash_table_insert(states, guid_p, state);
+ }
+ return 0;
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-state.h b/src/doveadm/dsync/dsync-mailbox-state.h
new file mode 100644
index 0000000..a01f531
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-state.h
@@ -0,0 +1,24 @@
+#ifndef DSYNC_MAILBOX_STATE_H
+#define DSYNC_MAILBOX_STATE_H
+
+#include "guid.h"
+
+struct dsync_mailbox_state {
+ guid_128_t mailbox_guid;
+ uint32_t last_uidvalidity;
+ uint32_t last_common_uid;
+ uint64_t last_common_modseq;
+ uint64_t last_common_pvt_modseq;
+ uint32_t last_messages_count;
+ bool changes_during_sync;
+};
+ARRAY_DEFINE_TYPE(dsync_mailbox_state, struct dsync_mailbox_state);
+HASH_TABLE_DEFINE_TYPE(dsync_mailbox_state, uint8_t *, struct dsync_mailbox_state *);
+
+void dsync_mailbox_states_export(const HASH_TABLE_TYPE(dsync_mailbox_state) states,
+ string_t *output);
+int dsync_mailbox_states_import(HASH_TABLE_TYPE(dsync_mailbox_state) states,
+ pool_t pool, const char *input,
+ const char **error_r);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox-tree-fill.c b/src/doveadm/dsync/dsync-mailbox-tree-fill.c
new file mode 100644
index 0000000..b255122
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-tree-fill.c
@@ -0,0 +1,463 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "guid.h"
+#include "str.h"
+#include "wildcard-match.h"
+#include "mailbox-log.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mailbox-list-iter.h"
+#include "dsync-brain.h"
+#include "dsync-mailbox-tree-private.h"
+
+static const char *
+dsync_mailbox_tree_name_unescape(struct mail_namespace *ns,
+ const char *old_vname, char alt_char)
+{
+ const char ns_sep = mail_namespace_get_sep(ns);
+ const char escape_char =
+ mailbox_list_get_settings(ns->list)->vname_escape_char;
+ const char *const *old_vname_parts =
+ dsync_mailbox_name_to_parts(old_vname, ns_sep, escape_char);
+
+ string_t *new_vname = t_str_new(128);
+ for (; *old_vname_parts != NULL; old_vname_parts++) {
+ for (const char *p = *old_vname_parts; *p != '\0'; p++) {
+ if (*p != ns_sep)
+ str_append_c(new_vname, *p);
+ else
+ str_append_c(new_vname, alt_char);
+ }
+ str_append_c(new_vname, ns_sep);
+ }
+ str_truncate(new_vname, str_len(new_vname)-1);
+ return str_c(new_vname);
+};
+
+static int
+dsync_mailbox_tree_add_node(struct dsync_mailbox_tree *tree,
+ const struct mailbox_info *info,
+ char alt_char,
+ struct dsync_mailbox_node **node_r)
+{
+ struct dsync_mailbox_node *node;
+ const char *vname = info->vname;
+
+ struct dsync_mailbox_list *dlist = DSYNC_LIST_CONTEXT(info->ns->list);
+ if (dlist != NULL && !dlist->have_orig_escape_char) {
+ /* The escape character was added only for dsync internally.
+ Normally there is no escape character configured. Change
+ the mailbox names so that it doesn't rely on it. */
+ vname = dsync_mailbox_tree_name_unescape(info->ns, vname, alt_char);
+ }
+
+ node = dsync_mailbox_tree_get(tree, vname);
+ if (node->ns == info->ns)
+ ;
+ else if (node->ns == NULL) {
+ i_assert(tree->root.ns == NULL);
+ node->ns = info->ns;
+ } else {
+ i_error("Mailbox '%s' exists in two namespaces: '%s' and '%s'",
+ info->vname, node->ns->prefix, info->ns->prefix);
+ return -1;
+ }
+ *node_r = node;
+ return 0;
+}
+
+static int
+dsync_mailbox_tree_add_exists_node(struct dsync_mailbox_tree *tree,
+ const struct mailbox_info *info,
+ struct dsync_mailbox_node **node_r,
+ char alt_char,
+ enum mail_error *error_r)
+{
+ if (dsync_mailbox_tree_add_node(tree, info, alt_char, node_r) < 0) {
+ *error_r = MAIL_ERROR_TEMP;
+ return -1;
+ }
+ (*node_r)->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ return 0;
+}
+
+static int
+dsync_mailbox_tree_get_selectable(struct mailbox *box,
+ struct mailbox_metadata *metadata_r,
+ struct mailbox_status *status_r)
+{
+ /* try the fast path */
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, metadata_r) < 0)
+ return -1;
+ if (mailbox_get_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT, status_r) < 0)
+ return -1;
+
+ i_assert(!guid_128_is_empty(metadata_r->guid));
+ if (status_r->uidvalidity != 0)
+ return 0;
+
+ /* no UIDVALIDITY assigned yet. syncing a mailbox should add it. */
+ if (mailbox_sync(box, 0) < 0)
+ return -1;
+ if (mailbox_get_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT, status_r) < 0)
+ return -1;
+ i_assert(status_r->uidvalidity != 0);
+ return 0;
+}
+
+static int dsync_mailbox_tree_add(struct dsync_mailbox_tree *tree,
+ const struct mailbox_info *info,
+ const guid_128_t box_guid,
+ char alt_char,
+ enum mail_error *error_r)
+{
+ struct dsync_mailbox_node *node;
+ struct mailbox *box;
+ enum mailbox_existence existence;
+ struct mailbox_metadata metadata;
+ struct mailbox_status status;
+ const char *errstr;
+ enum mail_error error;
+ int ret = 0;
+
+ if ((info->flags & MAILBOX_NONEXISTENT) != 0)
+ return 0;
+ if ((info->flags & MAILBOX_NOSELECT) != 0) {
+ return !guid_128_is_empty(box_guid) ? 0 :
+ dsync_mailbox_tree_add_exists_node(
+ tree, info, &node, alt_char, error_r);
+ }
+
+ /* get GUID and UIDVALIDITY for selectable mailbox */
+ box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY);
+ ret = mailbox_exists(box, FALSE, &existence);
+ if (ret == 0 && existence != MAILBOX_EXISTENCE_SELECT) {
+ /* autocreated mailbox doesn't exist yet */
+ mailbox_free(&box);
+ if (existence == MAILBOX_EXISTENCE_NOSELECT) {
+ return !guid_128_is_empty(box_guid) ? 0 :
+ dsync_mailbox_tree_add_exists_node(
+ tree, info, &node, alt_char, error_r);
+ } else {
+ return 0;
+ }
+ }
+ if (ret == 0)
+ ret = dsync_mailbox_tree_get_selectable(box, &metadata, &status);
+ if (ret < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ ret = 0;
+ switch (error) {
+ case MAIL_ERROR_NOTFOUND:
+ /* mailbox was just deleted? */
+ break;
+ case MAIL_ERROR_NOTPOSSIBLE:
+ /* invalid mbox files? ignore */
+ break;
+ default:
+ i_error("Failed to access mailbox %s: %s",
+ info->vname, errstr);
+ *error_r = error;
+ ret = -1;
+ }
+ mailbox_free(&box);
+ return ret;
+ }
+ mailbox_free(&box);
+
+ if (!guid_128_is_empty(box_guid) &&
+ !guid_128_equals(box_guid, metadata.guid)) {
+ /* unwanted mailbox */
+ return 0;
+ }
+ if (dsync_mailbox_tree_add_exists_node(
+ tree, info, &node, alt_char, error_r) < 0)
+ return -1;
+ memcpy(node->mailbox_guid, metadata.guid,
+ sizeof(node->mailbox_guid));
+ node->uid_validity = status.uidvalidity;
+ node->uid_next = status.uidnext;
+ return 0;
+}
+
+static struct dsync_mailbox_node *
+dsync_mailbox_tree_find_sha(struct dsync_mailbox_tree *tree,
+ struct mail_namespace *ns, const guid_128_t sha128)
+{
+ struct dsync_mailbox_node *node;
+
+ if (!hash_table_is_created(tree->name128_hash))
+ dsync_mailbox_tree_build_name128_hash(tree);
+
+ node = hash_table_lookup(tree->name128_hash, sha128);
+ return node == NULL || node->ns != ns ? NULL : node;
+}
+
+static int
+dsync_mailbox_tree_add_change_timestamps(struct dsync_mailbox_tree *tree,
+ struct mail_namespace *ns)
+{
+ struct dsync_mailbox_node *node;
+ struct dsync_mailbox_delete *del;
+ struct mailbox_log *log;
+ struct mailbox_log_iter *iter;
+ const struct mailbox_log_record *rec;
+ const uint8_t *guid_p;
+ time_t timestamp;
+
+ log = mailbox_list_get_changelog(ns->list);
+ if (log == NULL)
+ return 0;
+
+ iter = mailbox_log_iter_init(log);
+ while ((rec = mailbox_log_iter_next(iter)) != NULL) {
+ /* For DELETE_MAILBOX the record_guid is the mailbox GUID.
+ Otherwise it's 128bit SHA1 of the mailbox vname. */
+ node = rec->type == MAILBOX_LOG_RECORD_DELETE_MAILBOX ? NULL :
+ dsync_mailbox_tree_find_sha(tree, ns, rec->mailbox_guid);
+
+ timestamp = mailbox_log_record_get_timestamp(rec);
+ switch (rec->type) {
+ case MAILBOX_LOG_RECORD_DELETE_MAILBOX:
+ guid_p = rec->mailbox_guid;
+ if (hash_table_lookup(tree->guid_hash, guid_p) != NULL) {
+ /* mailbox still exists. maybe it was restored
+ from backup or something. */
+ break;
+ }
+ del = array_append_space(&tree->deletes);
+ del->type = DSYNC_MAILBOX_DELETE_TYPE_MAILBOX;
+ del->timestamp = timestamp;
+ memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid));
+ break;
+ case MAILBOX_LOG_RECORD_DELETE_DIR:
+ if (node != NULL &&
+ node->existence == DSYNC_MAILBOX_NODE_EXISTS) {
+ /* directory exists again, skip it */
+ break;
+ }
+ /* we don't know what directory name was deleted,
+ just its hash. if the name still exists on the other
+ dsync side, it can match this deletion to the
+ name. */
+ del = array_append_space(&tree->deletes);
+ del->type = DSYNC_MAILBOX_DELETE_TYPE_DIR;
+ del->timestamp = timestamp;
+ memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid));
+ break;
+ case MAILBOX_LOG_RECORD_CREATE_DIR:
+ if (node == NULL) {
+ /* directory has been deleted again, skip it */
+ break;
+ }
+ /* notify the remote that we want to keep this
+ directory created (unless remote has a newer delete
+ timestamp) */
+ node->last_renamed_or_created = timestamp;
+ break;
+ case MAILBOX_LOG_RECORD_RENAME:
+ if (node != NULL)
+ node->last_renamed_or_created = timestamp;
+ break;
+ case MAILBOX_LOG_RECORD_SUBSCRIBE:
+ if (node != NULL)
+ node->last_subscription_change = timestamp;
+ break;
+ case MAILBOX_LOG_RECORD_UNSUBSCRIBE:
+ if (node != NULL) {
+ node->last_subscription_change = timestamp;
+ break;
+ }
+ /* The mailbox is already deleted, but it may still
+ exist on the other side (even the subscription
+ alone). */
+ del = array_append_space(&tree->deletes);
+ del->type = DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE;
+ del->timestamp = timestamp;
+ memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid));
+ break;
+ }
+ }
+ if (mailbox_log_iter_deinit(&iter) < 0) {
+ i_error("Mailbox log iteration for namespace '%s' failed",
+ ns->prefix);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+dsync_mailbox_tree_fix_guid_duplicate(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ struct mailbox *box;
+ struct mailbox_update update;
+ struct dsync_mailbox_node *change_node;
+ const char *change_vname;
+ int ret = 0;
+
+ i_zero(&update);
+ guid_128_generate(update.mailbox_guid);
+
+ /* just in case the duplication exists in both sides,
+ make them choose the same node */
+ if (strcmp(dsync_mailbox_node_get_full_name(tree, node1),
+ dsync_mailbox_node_get_full_name(tree, node2)) <= 0)
+ change_node = node1;
+ else
+ change_node = node2;
+
+ change_vname = dsync_mailbox_node_get_full_name(tree, change_node);
+ i_error("Duplicate mailbox GUID %s for mailboxes %s and %s - "
+ "giving a new GUID %s to %s",
+ guid_128_to_string(node1->mailbox_guid),
+ dsync_mailbox_node_get_full_name(tree, node1),
+ dsync_mailbox_node_get_full_name(tree, node2),
+ guid_128_to_string(update.mailbox_guid), change_vname);
+
+ i_assert(node1->ns != NULL && node2->ns != NULL);
+ box = mailbox_alloc(change_node->ns->list, change_vname, 0);
+ if (mailbox_update(box, &update) < 0) {
+ i_error("Couldn't update mailbox %s GUID: %s",
+ change_vname, mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else {
+ memcpy(change_node->mailbox_guid, update.mailbox_guid,
+ sizeof(change_node->mailbox_guid));
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static bool
+dsync_mailbox_info_is_wanted(const struct mailbox_info *info,
+ const char *box_name,
+ const char *const *exclude_mailboxes)
+{
+ const char *const *info_specialuses;
+ unsigned int i;
+
+ if (exclude_mailboxes == NULL &&
+ (box_name == NULL || box_name[0] != '\\'))
+ return TRUE;
+
+ info_specialuses = info->special_use == NULL ? NULL :
+ t_strsplit(info->special_use, " ");
+ /* include */
+ if (box_name != NULL && box_name[0] == '\\') {
+ if (info_specialuses == NULL ||
+ !str_array_icase_find(info_specialuses, box_name))
+ return FALSE;
+ }
+ /* exclude */
+ if (exclude_mailboxes == NULL)
+ return TRUE;
+ for (i = 0; exclude_mailboxes[i] != NULL; i++) {
+ const char *exclude = exclude_mailboxes[i];
+
+ if (exclude[0] == '\\') {
+ /* special-use */
+ if (info_specialuses != NULL &&
+ str_array_icase_find(info_specialuses, exclude))
+ return FALSE;
+ } else {
+ /* mailbox with wildcards */
+ if (wildcard_match(info->vname, exclude))
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+int dsync_mailbox_tree_fill(struct dsync_mailbox_tree *tree,
+ struct mail_namespace *ns, const char *box_name,
+ const guid_128_t box_guid,
+ const char *const *exclude_mailboxes,
+ char alt_char,
+ enum mail_error *error_r)
+{
+ const enum mailbox_list_iter_flags list_flags =
+ /* FIXME: we'll skip symlinks, because we can't handle them
+ currently. in future we could detect them and create them
+ by creating the symlink. */
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES;
+ const enum mailbox_list_iter_flags subs_list_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct mailbox_list_iterate_context *iter;
+ struct dsync_mailbox_node *node, *dup_node1, *dup_node2;
+ const struct mailbox_info *info;
+ const char *list_pattern =
+ box_name != NULL && box_name[0] != '\\' ? box_name : "*";
+ int ret = 0;
+
+ i_assert(mail_namespace_get_sep(ns) == tree->sep);
+
+ /* assign namespace to its root, so it gets copied to children */
+ if (ns->prefix_len > 0) {
+ const char *vname = t_strndup(ns->prefix, ns->prefix_len-1);
+ node = dsync_mailbox_tree_get(tree, vname);
+ node->ns = ns;
+
+ struct mailbox_info ns_info = {
+ .vname = vname,
+ .ns = ns,
+ };
+ if (dsync_mailbox_tree_add(
+ tree, &ns_info, box_guid, alt_char, error_r) < 0)
+ return -1;
+ } else {
+ tree->root.ns = ns;
+ }
+
+ /* first add all of the existing mailboxes */
+ iter = mailbox_list_iter_init(ns->list, list_pattern, list_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (dsync_mailbox_info_is_wanted(info, box_name,
+ exclude_mailboxes)) {
+ if (dsync_mailbox_tree_add(
+ tree, info, box_guid, alt_char, error_r) < 0)
+ ret = -1;
+ }
+ } T_END;
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ i_error("Mailbox listing for namespace '%s' failed: %s",
+ ns->prefix, mailbox_list_get_last_internal_error(ns->list, error_r));
+ ret = -1;
+ }
+
+ /* add subscriptions */
+ iter = mailbox_list_iter_init(ns->list, list_pattern, subs_list_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if (dsync_mailbox_tree_add_node(tree, info, alt_char, &node) == 0)
+ node->subscribed = TRUE;
+ else {
+ *error_r = MAIL_ERROR_TEMP;
+ ret = -1;
+ }
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ i_error("Mailbox listing for namespace '%s' failed: %s",
+ ns->prefix, mailbox_list_get_last_internal_error(ns->list, error_r));
+ ret = -1;
+ }
+ if (ret < 0)
+ return -1;
+
+ while (dsync_mailbox_tree_build_guid_hash(tree, &dup_node1,
+ &dup_node2) < 0) {
+ if (dsync_mailbox_tree_fix_guid_duplicate(tree, dup_node1, dup_node2) < 0)
+ return -1;
+ }
+
+ /* add timestamps */
+ if (dsync_mailbox_tree_add_change_timestamps(tree, ns) < 0)
+ return -1;
+ return 0;
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-tree-private.h b/src/doveadm/dsync/dsync-mailbox-tree-private.h
new file mode 100644
index 0000000..0614151
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-tree-private.h
@@ -0,0 +1,38 @@
+#ifndef DSYNC_MAILBOX_TREE_PRIVATE_H
+#define DSYNC_MAILBOX_TREE_PRIVATE_H
+
+#include "dsync-mailbox-tree.h"
+
+struct dsync_mailbox_tree {
+ pool_t pool;
+ char sep, sep_str[2], remote_sep, alt_char;
+ char escape_char, remote_escape_char;
+ /* root node isn't part of the real mailbox tree. its name is "" and
+ it has no siblings */
+ struct dsync_mailbox_node root;
+
+ unsigned int iter_count;
+
+ ARRAY(struct dsync_mailbox_delete) deletes;
+
+ /* guid_128_t => struct dsync_mailbox_node */
+ HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) name128_hash;
+ HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) name128_remotesep_hash;
+ HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) guid_hash;
+};
+
+void dsync_mailbox_tree_build_name128_hash(struct dsync_mailbox_tree *tree);
+
+int dsync_mailbox_node_name_cmp(struct dsync_mailbox_node *const *n1,
+ struct dsync_mailbox_node *const *n2);
+
+void dsync_mailbox_tree_node_attach(struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *parent);
+void dsync_mailbox_tree_node_detach(struct dsync_mailbox_node *node);
+
+struct dsync_mailbox_tree *
+dsync_mailbox_tree_dup(const struct dsync_mailbox_tree *src);
+bool dsync_mailbox_trees_equal(struct dsync_mailbox_tree *tree1,
+ struct dsync_mailbox_tree *tree2);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox-tree-sync.c b/src/doveadm/dsync/dsync-mailbox-tree-sync.c
new file mode 100644
index 0000000..e4a50ae
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-tree-sync.c
@@ -0,0 +1,1473 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "md5.h"
+#include "hex-binary.h"
+#include "aqueue.h"
+#include "hash.h"
+#include "dsync-brain-private.h"
+#include "dsync-mailbox-tree-private.h"
+
+#define TEMP_MAX_NAME_LEN 100
+#define TEMP_SUFFIX_MAX_LEN (sizeof("temp-")-1 + 8)
+#define TEMP_SUFFIX_FORMAT "temp-%x"
+
+struct dsync_mailbox_tree_bfs_iter {
+ struct dsync_mailbox_tree *tree;
+
+ ARRAY(struct dsync_mailbox_node *) queue_arr;
+ struct aqueue *queue;
+ struct dsync_mailbox_node *cur;
+};
+
+struct dsync_mailbox_tree_sync_ctx {
+ pool_t pool;
+ struct dsync_mailbox_tree *local_tree, *remote_tree;
+ enum dsync_mailbox_trees_sync_type sync_type;
+ enum dsync_mailbox_trees_sync_flags sync_flags;
+ unsigned int combined_mailboxes_count;
+
+ ARRAY(struct dsync_mailbox_tree_sync_change) changes;
+ unsigned int change_idx;
+ bool failed;
+};
+
+static struct dsync_mailbox_tree_bfs_iter *
+dsync_mailbox_tree_bfs_iter_init(struct dsync_mailbox_tree *tree)
+{
+ struct dsync_mailbox_tree_bfs_iter *iter;
+
+ iter = i_new(struct dsync_mailbox_tree_bfs_iter, 1);
+ iter->tree = tree;
+ i_array_init(&iter->queue_arr, 32);
+ iter->queue = aqueue_init(&iter->queue_arr.arr);
+ iter->cur = tree->root.first_child;
+ return iter;
+}
+
+static bool
+dsync_mailbox_tree_bfs_iter_next(struct dsync_mailbox_tree_bfs_iter *iter,
+ struct dsync_mailbox_node **node_r)
+{
+ if (iter->cur == NULL) {
+ if (aqueue_count(iter->queue) == 0)
+ return FALSE;
+ iter->cur = array_idx_elem(&iter->queue_arr,
+ aqueue_idx(iter->queue, 0));
+ aqueue_delete_tail(iter->queue);
+ }
+ *node_r = iter->cur;
+
+ if (iter->cur->first_child != NULL)
+ aqueue_append(iter->queue, &iter->cur->first_child);
+ iter->cur = iter->cur->next;
+ return TRUE;
+}
+
+static void
+dsync_mailbox_tree_bfs_iter_deinit(struct dsync_mailbox_tree_bfs_iter **_iter)
+{
+ struct dsync_mailbox_tree_bfs_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ aqueue_deinit(&iter->queue);
+ array_free(&iter->queue_arr);
+ i_free(iter);
+}
+
+static void
+sync_add_dir_change(struct dsync_mailbox_tree_sync_ctx *ctx,
+ const struct dsync_mailbox_node *node,
+ enum dsync_mailbox_tree_sync_type type)
+{
+ struct dsync_mailbox_tree_sync_change *change;
+ const char *name;
+
+ i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL);
+
+ name = dsync_mailbox_node_get_full_name(ctx->local_tree, node);
+
+ change = array_append_space(&ctx->changes);
+ change->type = type;
+ change->ns = node->ns;
+ change->full_name = p_strdup(ctx->pool, name);
+}
+
+static void
+sync_add_create_change(struct dsync_mailbox_tree_sync_ctx *ctx,
+ const struct dsync_mailbox_node *node, const char *name)
+{
+ struct dsync_mailbox_tree_sync_change *change;
+
+ i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL);
+
+ change = array_append_space(&ctx->changes);
+ change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX;
+ change->ns = node->ns;
+ change->full_name = p_strdup(ctx->pool, name);
+ memcpy(change->mailbox_guid, node->mailbox_guid,
+ sizeof(change->mailbox_guid));
+ change->uid_validity = node->uid_validity;
+}
+
+static void sort_siblings(ARRAY_TYPE(dsync_mailbox_node) *siblings)
+{
+ struct dsync_mailbox_node *const *nodes;
+ unsigned int i, count;
+
+ array_sort(siblings, dsync_mailbox_node_name_cmp);
+
+ nodes = array_get(siblings, &count);
+ if (count == 0)
+ return;
+
+ nodes[0]->parent->first_child = nodes[0];
+ for (i = 1; i < count; i++)
+ nodes[i-1]->next = nodes[i];
+ nodes[count-1]->next = NULL;
+}
+
+static void
+sync_set_node_deleted(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node)
+{
+ const uint8_t *guid_p;
+
+ /* for the rest of this sync assume that the mailbox has
+ already been deleted */
+ guid_p = node->mailbox_guid;
+ hash_table_remove(tree->guid_hash, guid_p);
+
+ node->existence = DSYNC_MAILBOX_NODE_DELETED;
+ memset(node->mailbox_guid, 0, sizeof(node->mailbox_guid));
+ node->uid_validity = 0;
+}
+
+static void
+sync_delete_mailbox_node(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node, const char *reason)
+{
+ struct dsync_mailbox_tree_sync_change *change;
+ const char *name;
+
+ if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0 &&
+ tree == ctx->local_tree) {
+ i_debug("brain %c: Deleting mailbox '%s' (GUID %s): %s",
+ (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S',
+ dsync_mailbox_node_get_full_name(tree, node),
+ guid_128_to_string(node->mailbox_guid), reason);
+ }
+
+ if (tree == ctx->local_tree &&
+ node->existence != DSYNC_MAILBOX_NODE_DELETED) {
+ /* delete this mailbox locally */
+ i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL);
+ change = array_append_space(&ctx->changes);
+ change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX;
+ change->ns = node->ns;
+ name = dsync_mailbox_node_get_full_name(tree, node);
+ change->full_name = p_strdup(ctx->pool, name);
+ memcpy(change->mailbox_guid, node->mailbox_guid,
+ sizeof(change->mailbox_guid));
+ }
+ sync_set_node_deleted(tree, node);
+}
+
+static void
+sync_delete_mailbox(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node, const char *reason)
+{
+ struct dsync_mailbox_tree *other_tree;
+ struct dsync_mailbox_node *other_node;
+ const uint8_t *guid_p;
+
+ other_tree = tree == ctx->local_tree ?
+ ctx->remote_tree : ctx->local_tree;
+ guid_p = node->mailbox_guid;
+ other_node = hash_table_lookup(other_tree->guid_hash, guid_p);
+ if (other_node == NULL) {
+ /* doesn't exist / already deleted */
+ } else {
+ sync_delete_mailbox_node(ctx, other_tree, other_node, reason);
+ }
+ sync_delete_mailbox_node(ctx, tree, node, reason);
+}
+
+static void
+sync_tree_sort_and_delete_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ bool twoway_sync)
+{
+ struct dsync_mailbox_tree_bfs_iter *iter;
+ struct dsync_mailbox_node *node, *parent = NULL;
+ ARRAY_TYPE(dsync_mailbox_node) siblings;
+
+ t_array_init(&siblings, 64);
+
+ iter = dsync_mailbox_tree_bfs_iter_init(tree);
+ while (dsync_mailbox_tree_bfs_iter_next(iter, &node)) {
+ if (node->parent != parent) {
+ sort_siblings(&siblings);
+ array_clear(&siblings);
+ parent = node->parent;
+ }
+ if (node->existence == DSYNC_MAILBOX_NODE_DELETED &&
+ !dsync_mailbox_node_is_dir(node)) {
+ if (twoway_sync) {
+ /* this mailbox was deleted. delete it from the
+ other side as well */
+ sync_delete_mailbox(ctx, tree, node,
+ "Mailbox has been deleted");
+ } else {
+ /* treat the node as if it didn't exist. it'll
+ get either recreated or deleted later. in
+ any case this function must handle all
+ existence=DELETED mailbox nodes by changing
+ them into directories (setting GUID=0) or
+ we'll assert-crash later */
+ sync_set_node_deleted(tree, node);
+ }
+ }
+ ctx->combined_mailboxes_count++;
+ array_push_back(&siblings, &node);
+ }
+ sort_siblings(&siblings);
+ dsync_mailbox_tree_bfs_iter_deinit(&iter);
+}
+
+static bool node_names_equal(const struct dsync_mailbox_node *n1,
+ const struct dsync_mailbox_node *n2)
+{
+ while (n1 != NULL && n2 != NULL) {
+ if (strcmp(n1->name, n2->name) != 0)
+ return FALSE;
+ n1 = n1->parent;
+ n2 = n2->parent;
+ }
+ return n1 == NULL && n2 == NULL;
+}
+
+static void
+dsync_mailbox_tree_node_attach_sorted(struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *parent)
+{
+ struct dsync_mailbox_node **p;
+
+ node->parent = parent;
+ for (p = &parent->first_child; *p != NULL; p = &(*p)->next) {
+ if (dsync_mailbox_node_name_cmp(p, &node) > 0)
+ break;
+ }
+ node->next = *p;
+ *p = node;
+}
+
+static void
+dsync_mailbox_tree_node_move_sorted(struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *parent)
+{
+ /* detach from old parent */
+ dsync_mailbox_tree_node_detach(node);
+ /* attach to new parent */
+ dsync_mailbox_tree_node_attach_sorted(node, parent);
+}
+
+static struct dsync_mailbox_node *
+sorted_tree_get(struct dsync_mailbox_tree *tree, const char *name)
+{
+ struct dsync_mailbox_node *node, *parent, *ret;
+
+ node = ret = dsync_mailbox_tree_get(tree, name);
+ while (node->parent != NULL &&
+ node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT) {
+ parent = node->parent;
+ dsync_mailbox_tree_node_detach(node);
+ dsync_mailbox_tree_node_attach_sorted(node, parent);
+ node = parent;
+ }
+ return ret;
+}
+
+static struct dsync_mailbox_node *
+sync_node_new(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node **pos,
+ struct dsync_mailbox_node *parent,
+ const struct dsync_mailbox_node *src)
+{
+ struct dsync_mailbox_node *node;
+
+ node = p_new(tree->pool, struct dsync_mailbox_node, 1);
+ node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ node->name = p_strdup(tree->pool, src->name);
+ node->sync_temporary_name = src->sync_temporary_name;
+ node->ns = src->ns;
+ node->parent = parent;
+ node->next = *pos;
+ *pos = node;
+ return node;
+}
+
+static struct dsync_mailbox_node *
+sorted_tree_get_by_node_name(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_tree *other_tree,
+ struct dsync_mailbox_node *other_node)
+{
+ const char *parent_name;
+
+ if (other_node == &other_tree->root)
+ return &tree->root;
+
+ parent_name = dsync_mailbox_node_get_full_name(other_tree, other_node);
+ return sorted_tree_get(tree, parent_name);
+}
+
+static bool node_has_child(struct dsync_mailbox_node *parent, const char *name)
+{
+ struct dsync_mailbox_node *node;
+
+ for (node = parent->first_child; node != NULL; node = node->next) {
+ if (strcmp(node->name, name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+node_has_existent_children(struct dsync_mailbox_node *node, bool dirs_ok)
+{
+ for (node = node->first_child; node != NULL; node = node->next) {
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ (dirs_ok || !dsync_mailbox_node_is_dir(node)))
+ return TRUE;
+ if (node_has_existent_children(node, dirs_ok))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool node_is_existent(struct dsync_mailbox_node *node)
+{
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS)
+ return TRUE;
+ return node_has_existent_children(node, TRUE);
+}
+
+static bool sync_node_is_namespace_prefix(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node)
+{
+ const char *full_name;
+ size_t prefix_len = node->ns == NULL ? 0 : node->ns->prefix_len;
+
+ if (strcmp(node->name, "INBOX") == 0 && node->parent == &tree->root)
+ return TRUE;
+
+ if (prefix_len == 0)
+ return FALSE;
+
+ full_name = dsync_mailbox_node_get_full_name(tree, node);
+ if (node->ns->prefix[prefix_len-1] == mail_namespace_get_sep(node->ns))
+ prefix_len--;
+ return strncmp(full_name, node->ns->prefix, prefix_len) == 0 &&
+ full_name[prefix_len] == '\0';
+}
+
+static void
+sync_rename_node_to_temp(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *new_parent,
+ const char **reason_r)
+{
+ struct dsync_mailbox_tree_sync_change *change;
+ const char *old_name, *new_name, *p;
+ char name[TEMP_MAX_NAME_LEN+1];
+ buffer_t buf;
+ size_t prefix_len, max_prefix_len;
+ unsigned int counter = 1;
+
+ i_assert(!sync_node_is_namespace_prefix(tree, node));
+
+ buffer_create_from_data(&buf, name, sizeof(name));
+ max_prefix_len = TEMP_MAX_NAME_LEN - TEMP_SUFFIX_MAX_LEN - 1;
+ if (node->sync_temporary_name) {
+ /* the source name was also a temporary name. drop the
+ -<suffix> from it */
+ p = strrchr(node->name, '-');
+ i_assert(p != NULL);
+ if (max_prefix_len > (size_t)(p - node->name))
+ max_prefix_len = p - node->name;
+ }
+ str_append_max(&buf, node->name, max_prefix_len);
+ str_append_c(&buf, '-');
+ prefix_len = buf.used;
+
+ do {
+ str_truncate(&buf, prefix_len);
+ str_printfa(&buf, TEMP_SUFFIX_FORMAT, counter++);
+ /* the generated name is quite unlikely to exist,
+ but check anyway.. */
+ } while (node_has_child(node->parent, str_c(&buf)));
+
+ old_name = tree != ctx->local_tree ? NULL :
+ dsync_mailbox_node_get_full_name(tree, node);
+
+ *reason_r = t_strdup_printf("Renamed '%s' to '%s'", node->name, str_c(&buf));
+ node->name = p_strdup(tree->pool, str_c(&buf));
+ node->sync_temporary_name = TRUE;
+ node->last_renamed_or_created = 0;
+ dsync_mailbox_tree_node_move_sorted(node, new_parent);
+
+ if (tree == ctx->local_tree && node_is_existent(node)) {
+ /* we're modifying a local tree. remember this change. */
+ new_name = dsync_mailbox_node_get_full_name(tree, node);
+
+ i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL);
+ i_assert(strcmp(old_name, "INBOX") != 0);
+ change = array_append_space(&ctx->changes);
+ change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME;
+ change->ns = node->ns;
+ change->full_name = p_strdup(ctx->pool, old_name);
+ change->rename_dest_name = p_strdup(ctx->pool, new_name);
+ }
+}
+
+static bool node_has_parent(struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *parent)
+{
+ for (; node != NULL; node = node->parent) {
+ if (node == parent)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+sync_rename_node(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *temp_node,
+ struct dsync_mailbox_node *node,
+ const struct dsync_mailbox_node *other_node,
+ const char **reason_r)
+{
+ struct dsync_mailbox_tree_sync_change *change;
+ struct dsync_mailbox_tree *other_tree;
+ struct dsync_mailbox_node *parent;
+ const char *name, *other_name;
+
+ i_assert(node != NULL);
+ i_assert(other_node != NULL);
+
+ /* move/rename node in the tree, so that its position/name is identical
+ to other_node (in other_tree). temp_node's name is changed to
+ temporary name (i.e. it assumes that node's name becomes temp_node's
+ original name) */
+ other_tree = tree == ctx->local_tree ?
+ ctx->remote_tree : ctx->local_tree;
+
+ parent = sorted_tree_get_by_node_name(tree, other_tree,
+ other_node->parent);
+ if (node_has_parent(parent, node)) {
+ /* don't introduce a loop. temporarily rename node
+ under root. */
+ sync_rename_node_to_temp(ctx, tree, node, &tree->root, reason_r);
+ *reason_r = t_strconcat(*reason_r, " (Don't introduce loop)", NULL);
+ return;
+ }
+ sync_rename_node_to_temp(ctx, tree, temp_node, temp_node->parent, reason_r);
+
+ /* get the old name before it's modified */
+ name = dsync_mailbox_node_get_full_name(tree, node);
+
+ /* set the new name */
+ *reason_r = t_strdup_printf("%s + Renamed '%s' to '%s'",
+ *reason_r, name, other_node->name);
+ node->name = p_strdup(tree->pool, other_node->name);
+ node->sync_temporary_name = other_node->sync_temporary_name;
+ node->last_renamed_or_created = other_node->last_renamed_or_created;
+ /* change node's parent if necessary. in any case detach+reattach it
+ sorted, because the nodes must be sorted by name, and the node's
+ name (or its parent) changed. */
+ dsync_mailbox_tree_node_move_sorted(node, parent);
+
+ if (tree == ctx->local_tree && node_is_existent(node)) {
+ /* we're modifying a local tree. remember this change. */
+ other_name = dsync_mailbox_node_get_full_name(other_tree, other_node);
+
+ i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL);
+ i_assert(strcmp(name, "INBOX") != 0);
+ change = array_append_space(&ctx->changes);
+ change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME;
+ change->ns = node->ns;
+ change->full_name = p_strdup(ctx->pool, name);
+ change->rename_dest_name = p_strdup(ctx->pool, other_name);
+ }
+}
+
+static int node_mailbox_guids_cmp(struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ int ret;
+
+ while (node1 != NULL && node2 != NULL) {
+ if (node1->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ node2->existence != DSYNC_MAILBOX_NODE_EXISTS)
+ return -1;
+ if (node2->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ node1->existence != DSYNC_MAILBOX_NODE_EXISTS)
+ return 1;
+
+ ret = memcmp(node1->mailbox_guid, node2->mailbox_guid,
+ sizeof(node1->mailbox_guid));
+ if (ret != 0)
+ return ret;
+
+ ret = node_mailbox_guids_cmp(node1->first_child,
+ node2->first_child);
+ if (ret != 0)
+ return ret;
+ node1 = node1->next;
+ node2 = node2->next;
+ }
+ if (node1 == NULL && node2 == NULL)
+ return 0;
+ return node1 != NULL ? -1 : 1;
+}
+
+static int node_mailbox_names_cmp(struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ int ret;
+
+ while (node1 != NULL && node2 != NULL) {
+ ret = strcmp(node1->name, node2->name);
+ if (ret != 0)
+ return ret;
+
+ ret = node_mailbox_names_cmp(node1->first_child,
+ node2->first_child);
+ if (ret != 0)
+ return ret;
+ node1 = node1->next;
+ node2 = node2->next;
+ }
+ if (node1 == NULL && node2 == NULL)
+ return 0;
+ return node1 != NULL ? -1 : 1;
+}
+
+static int node_mailbox_trees_cmp(struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ int ret;
+
+ ret = node_mailbox_guids_cmp(node1, node2);
+ if (ret == 0) {
+ /* only a directory name changed and all the timestamps
+ are equal. just pick the alphabetically smaller. */
+ ret = node_mailbox_names_cmp(node1, node2);
+ }
+ i_assert(ret != 0);
+ return ret;
+}
+
+static time_t nodes_get_timestamp(struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ time_t ts = 0;
+
+ /* avoid using temporary names in case all the timestamps are 0 */
+ if (node1 != NULL && !node1->sync_temporary_name)
+ ts = node1->last_renamed_or_created + 1;
+ if (node2 != NULL && !node2->sync_temporary_name &&
+ ts <= node2->last_renamed_or_created)
+ ts = node2->last_renamed_or_created + 1;
+ return ts;
+}
+
+static bool sync_node_is_namespace_root(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node)
+{
+ if (node == NULL)
+ return FALSE;
+ if (node == &tree->root)
+ return TRUE;
+ return sync_node_is_namespace_prefix(tree, node);
+}
+
+static bool ATTR_NULL(3, 4)
+sync_rename_lower_ts(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_node1,
+ struct dsync_mailbox_node *remote_node1,
+ struct dsync_mailbox_node *local_node2,
+ struct dsync_mailbox_node *remote_node2,
+ const char **reason_r)
+{
+ time_t local_ts, remote_ts;
+
+ /* We're scanning the tree at the position of local_node1
+ and remote_node2. They have identical names. We also know that
+ local_node1&remote_node1 and local_node2&remote_node2 are "the same"
+ either because their GUIDs or (in case of one being a directory)
+ their childrens' GUIDs match. We don't know where local_node2 or
+ remote_node1 are located in the mailbox tree, or if they exist
+ at all. Note that node1 and node2 may be the same node pointers. */
+ i_assert(strcmp(local_node1->name, remote_node2->name) == 0);
+
+ if (sync_node_is_namespace_root(ctx->remote_tree, remote_node1) ||
+ sync_node_is_namespace_root(ctx->remote_tree, remote_node2) ||
+ sync_node_is_namespace_root(ctx->local_tree, local_node1) ||
+ sync_node_is_namespace_root(ctx->local_tree, local_node2)) {
+ local_node1->sync_delayed_guid_change = TRUE;
+ remote_node2->sync_delayed_guid_change = TRUE;
+ *reason_r = "Can't rename namespace prefixes - will be merged later";
+ return FALSE;
+ }
+
+ local_ts = nodes_get_timestamp(local_node1, local_node2);
+ remote_ts = nodes_get_timestamp(remote_node1, remote_node2);
+
+ if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL)
+ local_ts = remote_ts+1;
+ else if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE)
+ remote_ts = local_ts+1;
+
+ /* The algorithm must be deterministic regardless of the sync direction,
+ so in case the timestamps are equal we need to resort to looking at
+ the other data. We'll start by looking at the nodes' mailbox GUIDs,
+ but if both of them don't exist continue looking into their
+ children. */
+ if (local_ts > remote_ts ||
+ (local_ts == remote_ts &&
+ node_mailbox_trees_cmp(local_node1, remote_node2) < 0)) {
+ /* local nodes have a higher timestamp. we only want to do
+ renames where the destination parent is the current node's
+ (local_node1/remote_node2) parent. */
+
+ /* Numbers are GUIDs, letters are mailbox names:
+
+ local 1A <-name conflict-> remote 2A
+ local 2B <- potentially -> remote 1[BC]
+
+ Here we want to preserve the local 1A & 2B names: */
+ if (local_node2 == NULL) {
+ /* local : 1A
+ remote: 1B, 2A -> 2A-temp, 1A */
+ sync_rename_node(ctx, ctx->remote_tree, remote_node2,
+ remote_node1, local_node1, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(local: local_node2=NULL)", NULL);
+ return TRUE;
+ } else if (remote_node1 == remote_node2) {
+ /* FIXME: this fixes an infinite loop when in
+ rename2 test, think it through why :) */
+ *reason_r = "local: remote_node1=remote_node2";
+ } else if (remote_node1 != NULL) {
+ /* a) local_node1->parent == local_node2->parent
+
+ local : 1A, 2B
+ remote: 1B, 2A -> 2A-temp, 1A(, 2B)
+ remote: 1C, 2A -> 2B, 1A
+ remote: 1C, 2A, 3B -> 2A-temp, 1A(, 3B-temp, 2B)
+
+ b) local_node1->parent != local_node2->parent
+
+ local : 1X/A, 2Y/B
+ remote: 1Y/B, 2X/A -> 2X/A-temp, 1X/A(, 2Y/B)
+ remote: 1Z/C, 2X/A -> 2X/A-temp, 1X/A
+ remote: 1Z/C, 2X/A, 3Y/B -> 2X/A-temp, 1X/A
+
+ We can handle all of these more easily by simply
+ always renaming 2 to a temporary name and handling
+ it when we reach B handling. */
+ sync_rename_node(ctx, ctx->remote_tree, remote_node2,
+ remote_node1, local_node1, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(local: remote_node1=NULL)", NULL);
+ return TRUE;
+ } else if (node_has_parent(local_node1, local_node2) &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) {
+ /* node2 is a parent of node1, but it should be
+ vice versa */
+ sync_rename_node_to_temp(ctx, ctx->local_tree,
+ local_node1, local_node2->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(local: node2 parent of node1)", NULL);
+ return TRUE;
+ } else if (node_has_parent(local_node2, local_node1) &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) {
+ /* node1 is a parent of node2, but it should be
+ vice versa */
+ sync_rename_node_to_temp(ctx, ctx->local_tree,
+ local_node2, local_node1->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(local: node1 parent of node2)", NULL);
+ return TRUE;
+ } else if (local_node1->existence == DSYNC_MAILBOX_NODE_EXISTS) {
+ sync_rename_node_to_temp(ctx, ctx->remote_tree,
+ remote_node2, remote_node2->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(local: local_node1 exists)", NULL);
+ return TRUE;
+ } else {
+ /* local : 1A, 2B
+ remote: 2A -> (2B)
+ remote: 2A, 3B -> (3B-temp, 2B) */
+ *reason_r = "local: unchanged";
+ }
+ } else {
+ /* remote nodes have a higher timestamp */
+ if (remote_node1 == NULL) {
+ sync_rename_node(ctx, ctx->local_tree, local_node1,
+ local_node2, remote_node2, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(remote: remote_node1=NULL)", NULL);
+ return TRUE;
+ } else if (local_node2 == local_node1) {
+ *reason_r = "remote: remote_node2=remote_node1";
+ } else if (local_node2 != NULL) {
+ sync_rename_node(ctx, ctx->local_tree, local_node1,
+ local_node2, remote_node2, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(remote: local_node2=NULL)", NULL);
+ return TRUE;
+ } else if (node_has_parent(remote_node1, remote_node2) &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) {
+ sync_rename_node_to_temp(ctx, ctx->remote_tree,
+ remote_node1, remote_node2->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(remote: node2 parent of node1)", NULL);
+ return TRUE;
+ } else if (node_has_parent(remote_node2, remote_node1) &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) {
+ sync_rename_node_to_temp(ctx, ctx->remote_tree,
+ remote_node2, remote_node1->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(remote: node1 parent of node2)", NULL);
+ return TRUE;
+ } else if (remote_node2->existence == DSYNC_MAILBOX_NODE_EXISTS) {
+ sync_rename_node_to_temp(ctx, ctx->local_tree,
+ local_node1, local_node1->parent, reason_r);
+ *reason_r = t_strconcat(*reason_r, "(remote: remote_node2 exists)", NULL);
+ return TRUE;
+ } else {
+ *reason_r = "remote: unchanged";
+ }
+ }
+ return FALSE;
+}
+
+static bool sync_rename_conflict(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_node1,
+ struct dsync_mailbox_node *remote_node2,
+ const char **reason_r)
+{
+ struct dsync_mailbox_node *local_node2, *remote_node1;
+ const uint8_t *guid_p;
+ bool ret;
+
+ guid_p = local_node1->mailbox_guid;
+ remote_node1 = hash_table_lookup(ctx->remote_tree->guid_hash, guid_p);
+ guid_p = remote_node2->mailbox_guid;
+ local_node2 = hash_table_lookup(ctx->local_tree->guid_hash, guid_p);
+
+ if ((remote_node1 != NULL && remote_node1->existence == DSYNC_MAILBOX_NODE_EXISTS) ||
+ (local_node2 != NULL && local_node2->existence == DSYNC_MAILBOX_NODE_EXISTS)) {
+ /* conflicting name, rename the one with lower timestamp */
+ ret = sync_rename_lower_ts(ctx, local_node1, remote_node1,
+ local_node2, remote_node2, reason_r);
+ *reason_r = t_strconcat("conflicting name: ", *reason_r, NULL);
+ return ret;
+ } else if (dsync_mailbox_node_is_dir(local_node1) ||
+ dsync_mailbox_node_is_dir(remote_node2)) {
+ /* one of the nodes is a directory, and the other is a mailbox
+ that doesn't exist on the other side. there is no conflict,
+ we'll just need to create the mailbox later. */
+ *reason_r = "mailbox not selectable yet";
+ return FALSE;
+ } else {
+ /* both nodes are mailboxes that don't exist on the other side.
+ we'll merge these mailboxes together later and change their
+ GUIDs and UIDVALIDITYs to be the same */
+ local_node1->sync_delayed_guid_change = TRUE;
+ remote_node2->sync_delayed_guid_change = TRUE;
+ *reason_r = "GUIDs conflict - will be merged later";
+ return FALSE;
+ }
+}
+
+static struct dsync_mailbox_node *
+sync_find_branch(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_tree *other_tree,
+ struct dsync_mailbox_node *dir_node)
+{
+ struct dsync_mailbox_node *node, *other_node;
+ const uint8_t *guid_p;
+
+ for (node = dir_node->first_child; node != NULL; node = node->next) {
+ if (dsync_mailbox_node_is_dir(node)) {
+ other_node = sync_find_branch(tree, other_tree, node);
+ if (other_node != NULL)
+ return other_node;
+ } else {
+ guid_p = node->mailbox_guid;
+ other_node = hash_table_lookup(other_tree->guid_hash,
+ guid_p);
+ if (other_node != NULL)
+ return other_node->parent;
+ }
+ }
+ return NULL;
+}
+
+static bool sync_rename_directory(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_node1,
+ struct dsync_mailbox_node *remote_node2,
+ const char **reason_r)
+{
+ struct dsync_mailbox_node *remote_node1, *local_node2;
+
+ /* see if we can find matching mailbox branches based on the nodes'
+ child mailboxes (with GUIDs). we can then rename the entire branch.
+ don't try to do this for namespace prefixes though. */
+ remote_node1 = sync_find_branch(ctx->local_tree,
+ ctx->remote_tree, local_node1);
+ local_node2 = sync_find_branch(ctx->remote_tree, ctx->local_tree,
+ remote_node2);
+ if (remote_node1 == NULL || local_node2 == NULL) {
+ *reason_r = "Directory rename branch not found";
+ return FALSE;
+ }
+ if (node_names_equal(remote_node1, local_node2)) {
+ *reason_r = "Directory name paths are equal";
+ return FALSE;
+ }
+
+ return sync_rename_lower_ts(ctx, local_node1, remote_node1,
+ local_node2, remote_node2, reason_r);
+}
+
+static bool sync_rename_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_parent,
+ struct dsync_mailbox_node *remote_parent)
+{
+ struct dsync_mailbox_node **local_nodep = &local_parent->first_child;
+ struct dsync_mailbox_node **remote_nodep = &remote_parent->first_child;
+ struct dsync_mailbox_node *local_node, *remote_node;
+ const char *reason;
+ string_t *debug = NULL;
+ bool changed;
+
+ if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0)
+ debug = t_str_new(128);
+
+ /* the nodes are sorted by their names. */
+ while (*local_nodep != NULL || *remote_nodep != NULL) {
+ local_node = *local_nodep;
+ remote_node = *remote_nodep;
+
+ if (local_node == NULL ||
+ (remote_node != NULL &&
+ strcmp(local_node->name, remote_node->name) > 0)) {
+ /* add a missing local node */
+ local_node = sync_node_new(ctx->local_tree, local_nodep,
+ local_parent, remote_node);
+ }
+ if (remote_node == NULL ||
+ strcmp(remote_node->name, local_node->name) > 0) {
+ /* add a missing remote node */
+ remote_node = sync_node_new(ctx->remote_tree, remote_nodep,
+ remote_parent, local_node);
+ }
+ i_assert(strcmp(local_node->name, remote_node->name) == 0);
+ if (debug != NULL) {
+ str_truncate(debug, 0);
+ str_append(debug, "Mailbox ");
+ dsync_mailbox_node_append_full_name(debug, ctx->local_tree, local_node);
+ str_printfa(debug, ": local=%s/%ld/%d, remote=%s/%ld/%d",
+ guid_128_to_string(local_node->mailbox_guid),
+ (long)local_node->last_renamed_or_created,
+ local_node->existence,
+ guid_128_to_string(remote_node->mailbox_guid),
+ (long)remote_node->last_renamed_or_created,
+ remote_node->existence);
+ }
+
+ if (dsync_mailbox_node_is_dir(local_node) &&
+ dsync_mailbox_node_is_dir(remote_node)) {
+ /* both nodes are directories (or other side is
+ nonexistent). see if we can match them by their
+ child mailboxes */
+ changed = sync_rename_directory(ctx, local_node, remote_node, &reason);
+ } else if (dsync_mailbox_node_guids_equal(local_node,
+ remote_node)) {
+ /* mailboxes are equal, no need to rename */
+ reason = "Mailboxes are equal";
+ changed = FALSE;
+ } else {
+ /* mailbox naming conflict */
+ changed = sync_rename_conflict(ctx, local_node,
+ remote_node, &reason);
+ }
+ /* handle children, if there are any */
+ if (debug != NULL) {
+ i_debug("brain %c: %s: %s",
+ (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S',
+ str_c(debug), reason);
+ }
+
+ if (!changed) T_BEGIN {
+ changed = sync_rename_mailboxes(ctx, local_node,
+ remote_node);
+ } T_END;
+ if (changed)
+ return TRUE;
+
+ local_nodep = &local_node->next;
+ remote_nodep = &remote_node->next;
+ }
+ return FALSE;
+}
+
+static bool mailbox_node_hash_first_child(struct dsync_mailbox_node *node,
+ struct md5_context *md5)
+{
+ for (node = node->first_child; node != NULL; node = node->next) {
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS) {
+ md5_update(md5, node->mailbox_guid,
+ sizeof(node->mailbox_guid));
+ md5_update(md5, node->name, strlen(node->name));
+ return TRUE;
+ }
+ if (node->first_child != NULL) {
+ if (mailbox_node_hash_first_child(node, md5))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static const char *
+mailbox_node_generate_suffix(struct dsync_mailbox_node *node)
+{
+ struct md5_context md5;
+ unsigned char digest[MD5_RESULTLEN];
+
+ if (!dsync_mailbox_node_is_dir(node))
+ return guid_128_to_string(node->mailbox_guid);
+
+ md5_init(&md5);
+ if (!mailbox_node_hash_first_child(node, &md5))
+ i_unreached(); /* we would have deleted it */
+ md5_final(&md5, digest);
+ return binary_to_hex(digest, sizeof(digest));
+}
+
+static void suffix_inc(string_t *str)
+{
+ char *data;
+ size_t i;
+
+ data = str_c_modifiable(str) + str_len(str)-1;
+ for (i = str_len(str); i > 0; i--, data--) {
+ if ((*data >= '0' && *data < '9') ||
+ (*data >= 'a' && *data < 'f')) {
+ *data += 1;
+ return;
+ } else if (*data == '9') {
+ *data = 'a';
+ return;
+ } else if (*data != 'f') {
+ i_unreached();
+ }
+ }
+ i_unreached();
+}
+
+static void
+sync_rename_temp_mailbox_node(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node,
+ const char **reason_r)
+{
+ const char *p, *new_suffix;
+ string_t *str = t_str_new(256);
+ size_t max_prefix_len;
+
+ /* The name is currently <oldname>-<temp>. Both sides need to
+ use equivalent names, so we'll replace the <temp> if possible
+ with a) mailbox GUID, b) sha1 of childrens' (GUID|name)s. In the
+ very unlikely case of such name already existing, just increase
+ the last letters until it's not found. */
+ new_suffix = mailbox_node_generate_suffix(node);
+
+ p = strrchr(node->name, '-');
+ i_assert(p != NULL);
+ p++;
+ max_prefix_len = TEMP_MAX_NAME_LEN - strlen(new_suffix) - 1;
+ if (max_prefix_len > (size_t)(p-node->name))
+ max_prefix_len = p-node->name;
+ str_append_max(str, node->name, max_prefix_len);
+ str_append(str, new_suffix);
+ while (node_has_child(node->parent, str_c(str)))
+ suffix_inc(str);
+
+ *reason_r = t_strdup_printf("Renamed '%s' to '%s'",
+ dsync_mailbox_node_get_full_name(tree, node), str_c(str));
+ node->name = p_strdup(tree->pool, str_c(str));
+
+ dsync_mailbox_tree_node_move_sorted(node, node->parent);
+ node->sync_temporary_name = FALSE;
+}
+
+static void
+sync_rename_delete_node_dirs(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node)
+{
+ struct dsync_mailbox_node *child;
+
+ for (child = node->first_child; child != NULL; child = child->next)
+ sync_rename_delete_node_dirs(ctx, tree, child);
+
+ if (tree == ctx->local_tree &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL &&
+ node->existence != DSYNC_MAILBOX_NODE_NONEXISTENT) {
+ sync_add_dir_change(ctx, node,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR);
+ }
+ node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ node->sync_temporary_name = FALSE;
+}
+
+static bool
+sync_rename_temp_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node, bool *renames_r)
+{
+ const char *reason;
+
+ for (; node != NULL; node = node->next) {
+ while (sync_rename_temp_mailboxes(ctx, tree, node->first_child, renames_r)) ;
+
+ if (!node->sync_temporary_name) {
+ } else if (dsync_mailbox_node_is_dir(node) &&
+ (node->first_child == NULL ||
+ !node_has_existent_children(node, FALSE))) {
+ /* we can just delete this directory and
+ any child directories it may have */
+ if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0) {
+ i_debug("brain %c: %s mailbox %s: Delete directory-only tree",
+ (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S',
+ tree == ctx->local_tree ? "local" : "remote",
+ dsync_mailbox_node_get_full_name(tree, node));
+ }
+ sync_rename_delete_node_dirs(ctx, tree, node);
+ } else {
+ T_BEGIN {
+ *renames_r = TRUE;
+ sync_rename_temp_mailbox_node(tree, node, &reason);
+ if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0) {
+ i_debug("brain %c: %s mailbox %s: %s",
+ (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S',
+ tree == ctx->local_tree ? "local" : "remote",
+ dsync_mailbox_node_get_full_name(tree, node), reason);
+ }
+ } T_END;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static int
+dsync_mailbox_tree_handle_renames(struct dsync_mailbox_tree_sync_ctx *ctx,
+ bool *renames_r)
+{
+ unsigned int max_renames, count = 0;
+ bool changed;
+
+ *renames_r = FALSE;
+
+ max_renames = ctx->combined_mailboxes_count * 3;
+ do {
+ T_BEGIN {
+ changed = sync_rename_mailboxes(ctx, &ctx->local_tree->root,
+ &ctx->remote_tree->root);
+ } T_END;
+ if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0 &&
+ changed) {
+ i_debug("brain %c: -- Mailbox renamed, restart sync --",
+ (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S');
+ }
+ } while (changed && ++count <= max_renames);
+
+ if (changed) {
+ i_error("BUG: Mailbox renaming algorithm got into a potentially infinite loop, aborting");
+ return -1;
+ }
+
+ while (sync_rename_temp_mailboxes(ctx, ctx->local_tree, &ctx->local_tree->root, renames_r)) ;
+ while (sync_rename_temp_mailboxes(ctx, ctx->remote_tree, &ctx->remote_tree->root, renames_r)) ;
+ return 0;
+}
+
+static bool sync_is_wrong_mailbox(struct dsync_mailbox_node *node,
+ const struct dsync_mailbox_node *wanted_node,
+ const char **reason_r)
+{
+ if (wanted_node->existence != DSYNC_MAILBOX_NODE_EXISTS) {
+ *reason_r = wanted_node->existence == DSYNC_MAILBOX_NODE_DELETED ?
+ "Mailbox has been deleted" : "Mailbox doesn't exist";
+ return TRUE;
+ }
+ if (node->uid_validity != wanted_node->uid_validity) {
+ *reason_r = t_strdup_printf("UIDVALIDITY changed (%u -> %u)",
+ wanted_node->uid_validity,
+ node->uid_validity);
+ return TRUE;
+ }
+ if (node->uid_next > wanted_node->uid_next) {
+ /* we can't lower the UIDNEXT */
+ *reason_r = t_strdup_printf("UIDNEXT is too high (%u > %u)",
+ node->uid_next,
+ wanted_node->uid_next);
+ return TRUE;
+ }
+ if (memcmp(node->mailbox_guid, wanted_node->mailbox_guid,
+ sizeof(node->mailbox_guid)) != 0) {
+ *reason_r = "GUID changed";
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+sync_delete_wrong_mailboxes_branch(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_tree *wanted_tree,
+ struct dsync_mailbox_node *node,
+ const struct dsync_mailbox_node *wanted_node)
+{
+ const char *reason;
+ int ret;
+
+ while (node != NULL && wanted_node != NULL) {
+ if (node->first_child != NULL) {
+ sync_delete_wrong_mailboxes_branch(ctx,
+ tree, wanted_tree,
+ node->first_child, wanted_node->first_child);
+ }
+ ret = strcmp(node->name, wanted_node->name);
+ if (ret < 0) {
+ /* node shouldn't exist */
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ !dsync_mailbox_node_is_dir(node)) {
+ sync_delete_mailbox_node(ctx, tree, node,
+ "Mailbox doesn't exist");
+ }
+ node = node->next;
+ } else if (ret > 0) {
+ /* wanted_node doesn't exist. it's created later. */
+ wanted_node = wanted_node->next;
+ } else T_BEGIN {
+ if (sync_is_wrong_mailbox(node, wanted_node, &reason) &&
+ node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ !dsync_mailbox_node_is_dir(node))
+ sync_delete_mailbox_node(ctx, tree, node, reason);
+ node = node->next;
+ wanted_node = wanted_node->next;
+ } T_END;
+ }
+ for (; node != NULL; node = node->next) {
+ /* node and its children shouldn't exist */
+ if (node->first_child != NULL) {
+ sync_delete_wrong_mailboxes_branch(ctx,
+ tree, wanted_tree,
+ node->first_child, NULL);
+ }
+ if (node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ !dsync_mailbox_node_is_dir(node))
+ sync_delete_mailbox_node(ctx, tree, node, "Mailbox doesn't exist");
+ }
+}
+
+static void
+sync_delete_wrong_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_tree *wanted_tree)
+{
+ sync_delete_wrong_mailboxes_branch(ctx, tree, wanted_tree,
+ tree->root.first_child,
+ wanted_tree->root.first_child);
+}
+
+static void sync_create_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_tree *tree)
+{
+ struct dsync_mailbox_tree *other_tree;
+ struct dsync_mailbox_tree_iter *iter;
+ struct dsync_mailbox_node *node, *other_node;
+ const char *name;
+ const uint8_t *guid_p;
+
+ other_tree = tree == ctx->local_tree ?
+ ctx->remote_tree : ctx->local_tree;
+
+ iter = dsync_mailbox_tree_iter_init(tree);
+ while (dsync_mailbox_tree_iter_next(iter, &name, &node)) {
+ /* make sure the renaming handled everything */
+ i_assert(!node->sync_temporary_name);
+ if (dsync_mailbox_node_is_dir(node))
+ continue;
+
+ i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS);
+
+ guid_p = node->mailbox_guid;
+ other_node = hash_table_lookup(other_tree->guid_hash, guid_p);
+ if (other_node == NULL)
+ other_node = sorted_tree_get(other_tree, name);
+ if (dsync_mailbox_node_is_dir(other_node)) {
+ /* create a missing mailbox */
+ other_node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ other_node->ns = node->ns;
+ other_node->uid_validity = node->uid_validity;
+ memcpy(other_node->mailbox_guid, node->mailbox_guid,
+ sizeof(other_node->mailbox_guid));
+ if (other_tree == ctx->local_tree)
+ sync_add_create_change(ctx, other_node, name);
+ } else if (!guid_128_equals(node->mailbox_guid,
+ other_node->mailbox_guid)) {
+ /* mailbox with same name exists both locally and
+ remotely, but they have different GUIDs and neither
+ side has the other's GUID. typically this means that
+ both sides had autocreated some mailboxes (e.g.
+ INBOX). we'll just change the GUID for one of
+ them. */
+ i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS);
+ i_assert(node->ns == other_node->ns);
+
+ if (other_tree == ctx->local_tree)
+ sync_add_create_change(ctx, node, name);
+ } else {
+ /* existing mailbox. mismatching UIDVALIDITY is handled
+ later while syncing the mailbox. */
+ i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS);
+ i_assert(node->ns == other_node->ns);
+ }
+ }
+ dsync_mailbox_tree_iter_deinit(&iter);
+}
+
+static void
+sync_subscription(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_node,
+ struct dsync_mailbox_node *remote_node)
+{
+ bool use_local;
+
+ if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL)
+ use_local = TRUE;
+ else if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE)
+ use_local = FALSE;
+ else if (local_node->last_subscription_change > remote_node->last_subscription_change)
+ use_local = TRUE;
+ else if (local_node->last_subscription_change < remote_node->last_subscription_change)
+ use_local = FALSE;
+ else {
+ /* local and remote have equal timestamps. prefer to subscribe
+ rather than unsubscribe. */
+ use_local = local_node->subscribed;
+ }
+ if (use_local) {
+ /* use local subscription state */
+ remote_node->subscribed = local_node->subscribed;
+ } else {
+ /* use remote subscription state */
+ local_node->subscribed = remote_node->subscribed;
+ sync_add_dir_change(ctx, local_node, local_node->subscribed ?
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE :
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE);
+ }
+}
+
+static void sync_mailbox_child_dirs(struct dsync_mailbox_tree_sync_ctx *ctx,
+ struct dsync_mailbox_node *local_parent,
+ struct dsync_mailbox_node *remote_parent)
+{
+ struct dsync_mailbox_node **local_nodep = &local_parent->first_child;
+ struct dsync_mailbox_node **remote_nodep = &remote_parent->first_child;
+ struct dsync_mailbox_node *local_node, *remote_node;
+ int ret;
+
+ /* NOTE: the nodes are always sorted. renaming created all of the
+ interesting nodes, but it may have left some extra nonexistent nodes
+ lying around, which we will delete. */
+ while (*local_nodep != NULL && *remote_nodep != NULL) {
+ local_node = *local_nodep;
+ remote_node = *remote_nodep;
+
+ ret = strcmp(local_node->name, remote_node->name);
+ if (ret < 0) {
+ i_assert(!node_is_existent(local_node));
+ *local_nodep = local_node->next;
+ continue;
+ }
+ if (ret > 0) {
+ i_assert(!node_is_existent(remote_node));
+ *remote_nodep = remote_node->next;
+ continue;
+ }
+
+ if (local_node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ remote_node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) {
+ /* create to remote */
+ remote_node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ }
+ if (remote_node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ local_node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) {
+ /* create to local */
+ local_node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ sync_add_dir_change(ctx, local_node,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR);
+ }
+
+ /* create/delete child dirs */
+ sync_mailbox_child_dirs(ctx, local_node, remote_node);
+
+ if (local_node->subscribed != remote_node->subscribed)
+ sync_subscription(ctx, local_node, remote_node);
+
+ if (local_node->existence == DSYNC_MAILBOX_NODE_DELETED &&
+ !node_has_existent_children(local_node, TRUE) &&
+ remote_node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) {
+ /* delete from remote */
+ i_assert(!node_has_existent_children(remote_node, TRUE));
+ remote_node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ }
+ if (remote_node->existence == DSYNC_MAILBOX_NODE_DELETED &&
+ !node_has_existent_children(remote_node, TRUE) &&
+ local_node->existence == DSYNC_MAILBOX_NODE_EXISTS &&
+ ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) {
+ /* delete from local */
+ i_assert(!node_has_existent_children(local_node, TRUE));
+ local_node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ sync_add_dir_change(ctx, local_node,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR);
+ }
+
+ local_nodep = &local_node->next;
+ remote_nodep = &remote_node->next;
+ }
+ while (*local_nodep != NULL) {
+ i_assert(!node_is_existent(*local_nodep));
+ *local_nodep = (*local_nodep)->next;
+ }
+ while (*remote_nodep != NULL) {
+ i_assert(!node_is_existent(*remote_nodep));
+ *remote_nodep = (*remote_nodep)->next;
+ }
+}
+
+static void sync_mailbox_dirs(struct dsync_mailbox_tree_sync_ctx *ctx)
+{
+ sync_mailbox_child_dirs(ctx, &ctx->local_tree->root,
+ &ctx->remote_tree->root);
+}
+
+static void
+dsync_mailbox_tree_update_child_timestamps(struct dsync_mailbox_node *node,
+ time_t parent_timestamp)
+{
+ time_t ts;
+
+ if (node->last_renamed_or_created < parent_timestamp)
+ node->last_renamed_or_created = parent_timestamp;
+ ts = node->last_renamed_or_created;
+
+ for (node = node->first_child; node != NULL; node = node->next)
+ dsync_mailbox_tree_update_child_timestamps(node, ts);
+}
+
+struct dsync_mailbox_tree_sync_ctx *
+dsync_mailbox_trees_sync_init(struct dsync_mailbox_tree *local_tree,
+ struct dsync_mailbox_tree *remote_tree,
+ enum dsync_mailbox_trees_sync_type sync_type,
+ enum dsync_mailbox_trees_sync_flags sync_flags)
+{
+ struct dsync_mailbox_tree_sync_ctx *ctx;
+ unsigned int rename_counter = 0;
+ bool renames;
+ pool_t pool;
+
+ i_assert(hash_table_is_created(local_tree->guid_hash));
+ i_assert(hash_table_is_created(remote_tree->guid_hash));
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox trees sync",
+ 1024*64);
+ ctx = p_new(pool, struct dsync_mailbox_tree_sync_ctx, 1);
+ ctx->pool = pool;
+ ctx->local_tree = local_tree;
+ ctx->remote_tree = remote_tree;
+ ctx->sync_type = sync_type;
+ ctx->sync_flags = sync_flags;
+ i_array_init(&ctx->changes, 128);
+
+again:
+ renames = FALSE;
+ ctx->combined_mailboxes_count = 0;
+ sync_tree_sort_and_delete_mailboxes(ctx, remote_tree,
+ sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY);
+ sync_tree_sort_and_delete_mailboxes(ctx, local_tree,
+ sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY);
+
+ dsync_mailbox_tree_update_child_timestamps(&local_tree->root, 0);
+ dsync_mailbox_tree_update_child_timestamps(&remote_tree->root, 0);
+ if ((sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_NO_RENAMES) == 0) {
+ if (dsync_mailbox_tree_handle_renames(ctx, &renames) < 0) {
+ ctx->failed = TRUE;
+ return ctx;
+ }
+ }
+
+ /* if we're not doing a two-way sync, delete now any mailboxes, which
+ a) shouldn't exist, b) doesn't have a matching GUID/UIDVALIDITY,
+ c) has a too high UIDNEXT */
+ if (sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL)
+ sync_delete_wrong_mailboxes(ctx, remote_tree, local_tree);
+ else if (sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE)
+ sync_delete_wrong_mailboxes(ctx, local_tree, remote_tree);
+
+ if (sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL)
+ sync_create_mailboxes(ctx, remote_tree);
+ if (sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE)
+ sync_create_mailboxes(ctx, local_tree);
+ if (renames && rename_counter++ <= ctx->combined_mailboxes_count*3) {
+ /* this rename algorithm is just horrible. we're retrying this
+ because the final sync_rename_temp_mailbox_node() calls
+ give different names to local & remote mailbox trees.
+ something's not right here, but this looping is better than
+ a crash in sync_mailbox_dirs() due to trees not matching. */
+ goto again;
+ }
+ sync_mailbox_dirs(ctx);
+ return ctx;
+}
+
+const struct dsync_mailbox_tree_sync_change *
+dsync_mailbox_trees_sync_next(struct dsync_mailbox_tree_sync_ctx *ctx)
+{
+ if (ctx->change_idx == array_count(&ctx->changes))
+ return NULL;
+ return array_idx(&ctx->changes, ctx->change_idx++);
+}
+
+int dsync_mailbox_trees_sync_deinit(struct dsync_mailbox_tree_sync_ctx **_ctx)
+{
+ struct dsync_mailbox_tree_sync_ctx *ctx = *_ctx;
+ int ret = ctx->failed ? -1 : 0;
+
+ *_ctx = NULL;
+
+ array_free(&ctx->changes);
+ pool_unref(&ctx->pool);
+ return ret;
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-tree.c b/src/doveadm/dsync/dsync-mailbox-tree.c
new file mode 100644
index 0000000..e2a86a3
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-tree.c
@@ -0,0 +1,571 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "sort.h"
+#include "mailbox-list-private.h"
+#include "dsync-mailbox-tree-private.h"
+
+
+struct dsync_mailbox_tree_iter {
+ struct dsync_mailbox_tree *tree;
+
+ struct dsync_mailbox_node *cur;
+ string_t *name;
+};
+
+struct dsync_mailbox_tree *
+dsync_mailbox_tree_init(char sep, char escape_char, char alt_char)
+{
+ struct dsync_mailbox_tree *tree;
+ pool_t pool;
+
+ i_assert(sep != '\0');
+ i_assert(alt_char != '\0');
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox tree", 4096);
+ tree = p_new(pool, struct dsync_mailbox_tree, 1);
+ tree->pool = pool;
+ tree->sep = tree->sep_str[0] = sep;
+ tree->escape_char = escape_char;
+ tree->alt_char = alt_char;
+ tree->root.name = "";
+ i_array_init(&tree->deletes, 32);
+ return tree;
+}
+
+void dsync_mailbox_tree_deinit(struct dsync_mailbox_tree **_tree)
+{
+ struct dsync_mailbox_tree *tree = *_tree;
+
+ *_tree = NULL;
+ hash_table_destroy(&tree->name128_hash);
+ hash_table_destroy(&tree->name128_remotesep_hash);
+ hash_table_destroy(&tree->guid_hash);
+ array_free(&tree->deletes);
+ pool_unref(&tree->pool);
+}
+
+static struct dsync_mailbox_node *
+dsync_mailbox_node_find(struct dsync_mailbox_node *nodes, const char *name)
+{
+ for (; nodes != NULL; nodes = nodes->next) {
+ if (strcmp(name, nodes->name) == 0)
+ return nodes;
+ }
+ return NULL;
+}
+
+struct dsync_mailbox_node *
+dsync_mailbox_tree_lookup(struct dsync_mailbox_tree *tree,
+ const char *full_name)
+{
+ struct dsync_mailbox_node *node = &tree->root;
+
+ T_BEGIN {
+ const char *const *path;
+
+ path = t_strsplit(full_name, tree->sep_str);
+ for (; *path != NULL && node != NULL; path++)
+ node = dsync_mailbox_node_find(node->first_child, *path);
+ } T_END;
+ return node;
+}
+
+void dsync_mailbox_tree_node_attach(struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node *parent)
+{
+ node->parent = parent;
+ node->next = parent->first_child;
+ parent->first_child = node;
+}
+
+void dsync_mailbox_tree_node_detach(struct dsync_mailbox_node *node)
+{
+ struct dsync_mailbox_node **p;
+
+ for (p = &node->parent->first_child;; p = &(*p)->next) {
+ if (*p == node) {
+ *p = node->next;
+ break;
+ }
+ }
+ node->parent = NULL;
+}
+
+struct dsync_mailbox_node *
+dsync_mailbox_tree_get(struct dsync_mailbox_tree *tree, const char *full_name)
+{
+ struct dsync_mailbox_node *parent = NULL, *node = &tree->root;
+
+ i_assert(tree->iter_count == 0);
+
+ T_BEGIN {
+ const char *const *path;
+
+ /* find the existing part */
+ path = t_strsplit(full_name, tree->sep_str);
+ for (; *path != NULL; path++) {
+ parent = node;
+ node = dsync_mailbox_node_find(node->first_child, *path);
+ if (node == NULL)
+ break;
+ }
+ /* create the rest */
+ for (; *path != NULL; path++) {
+ node = p_new(tree->pool, struct dsync_mailbox_node, 1);
+ node->name = p_strdup(tree->pool, *path);
+ node->ns = parent->ns;
+ dsync_mailbox_tree_node_attach(node, parent);
+ parent = node;
+ }
+ } T_END;
+ return node;
+}
+
+static void
+node_get_full_name_recurse(const struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_node *node, string_t *str)
+{
+ if (node->parent != &tree->root)
+ node_get_full_name_recurse(tree, node->parent, str);
+
+ str_append(str, node->name);
+ str_append_c(str, tree->sep);
+}
+
+const char *dsync_mailbox_node_get_full_name(const struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_node *node)
+{
+ string_t *str = t_str_new(128);
+ dsync_mailbox_node_append_full_name(str, tree, node);
+ return str_c(str);
+}
+
+void dsync_mailbox_node_append_full_name(string_t *str,
+ const struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_node *node)
+{
+ i_assert(node->parent != NULL);
+
+ node_get_full_name_recurse(tree, node, str);
+ /* remove the trailing separator */
+ str_truncate(str, str_len(str)-1);
+}
+
+void dsync_mailbox_node_copy_data(struct dsync_mailbox_node *dest,
+ const struct dsync_mailbox_node *src)
+{
+ memcpy(dest->mailbox_guid, src->mailbox_guid,
+ sizeof(dest->mailbox_guid));
+ dest->uid_validity = src->uid_validity;
+ dest->uid_next = src->uid_next;
+ dest->existence = src->existence;
+ dest->last_renamed_or_created = src->last_renamed_or_created;
+ dest->subscribed = src->subscribed;
+ dest->last_subscription_change = src->last_subscription_change;
+}
+
+struct dsync_mailbox_tree_iter *
+dsync_mailbox_tree_iter_init(struct dsync_mailbox_tree *tree)
+{
+ struct dsync_mailbox_tree_iter *iter;
+
+ iter = i_new(struct dsync_mailbox_tree_iter, 1);
+ iter->tree = tree;
+ iter->name = str_new(default_pool, 128);
+ iter->cur = &tree->root;
+
+ tree->iter_count++;
+ return iter;
+}
+
+static size_t node_get_full_name_length(struct dsync_mailbox_node *node)
+{
+ if (node->parent->parent == NULL)
+ return strlen(node->name);
+ else {
+ return strlen(node->name) + 1 +
+ node_get_full_name_length(node->parent);
+ }
+}
+
+bool dsync_mailbox_tree_iter_next(struct dsync_mailbox_tree_iter *iter,
+ const char **full_name_r,
+ struct dsync_mailbox_node **node_r)
+{
+ size_t len;
+
+ if (iter->cur->first_child != NULL)
+ iter->cur = iter->cur->first_child;
+ else {
+ while (iter->cur->next == NULL) {
+ if (iter->cur == &iter->tree->root)
+ return FALSE;
+ iter->cur = iter->cur->parent;
+ }
+ iter->cur = iter->cur->next;
+ len = iter->cur->parent == &iter->tree->root ? 0 :
+ node_get_full_name_length(iter->cur->parent);
+ str_truncate(iter->name, len);
+ }
+ if (str_len(iter->name) > 0)
+ str_append_c(iter->name, iter->tree->sep);
+ str_append(iter->name, iter->cur->name);
+ *full_name_r = str_c(iter->name);
+ *node_r = iter->cur;
+ return TRUE;
+}
+
+void dsync_mailbox_tree_iter_deinit(struct dsync_mailbox_tree_iter **_iter)
+{
+ struct dsync_mailbox_tree_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ i_assert(iter->tree->iter_count > 0);
+ iter->tree->iter_count--;
+
+ str_free(&iter->name);
+ i_free(iter);
+}
+
+void dsync_mailbox_tree_build_name128_hash(struct dsync_mailbox_tree *tree)
+{
+ struct dsync_mailbox_tree_iter *iter;
+ struct dsync_mailbox_node *node;
+ const char *name;
+ guid_128_t *sha128;
+ uint8_t *guid_p;
+
+ i_assert(!hash_table_is_created(tree->name128_hash));
+
+ hash_table_create(&tree->name128_hash,
+ tree->pool, 0, guid_128_hash, guid_128_cmp);
+ iter = dsync_mailbox_tree_iter_init(tree);
+ while (dsync_mailbox_tree_iter_next(iter, &name, &node)) {
+ sha128 = p_new(tree->pool, guid_128_t, 1);
+ mailbox_name_get_sha128(name, *sha128);
+ guid_p = *sha128;
+ hash_table_insert(tree->name128_hash, guid_p, node);
+ }
+ dsync_mailbox_tree_iter_deinit(&iter);
+}
+
+static const char *
+convert_name_to_remote_sep(struct dsync_mailbox_tree *tree, const char *name)
+{
+ string_t *str = t_str_new(128);
+
+ char remote_escape_chars[3] = {
+ tree->remote_escape_char,
+ tree->remote_sep,
+ '\0'
+ };
+
+ for (;;) {
+ const char *end = strchr(name, tree->sep);
+ const char *name_part = end == NULL ? name :
+ t_strdup_until(name, end++);
+ name = end;
+
+ if (tree->escape_char != '\0')
+ mailbox_list_name_unescape(&name_part, tree->escape_char);
+ if (remote_escape_chars[0] != '\0') {
+ /* The local name can be fully escaped to remote
+ name and back. */
+ mailbox_list_name_escape(name_part, remote_escape_chars,
+ str);
+ } else {
+ /* There is no remote escape char, so for conflicting
+ separator use the alt_char. */
+ for (; *name_part != '\0'; name_part++) {
+ if (*name_part == tree->remote_sep)
+ str_append_c(str, tree->alt_char);
+ else
+ str_append_c(str, *name_part);
+ }
+ }
+ if (end == NULL)
+ break;
+ str_append_c(str, tree->remote_sep);
+ }
+ return str_c(str);
+}
+
+static void
+dsync_mailbox_tree_build_name128_remotesep_hash(struct dsync_mailbox_tree *tree)
+{
+ struct dsync_mailbox_tree_iter *iter;
+ struct dsync_mailbox_node *node;
+ const char *name;
+ guid_128_t *sha128;
+ uint8_t *guid_p;
+
+ i_assert(tree->sep != tree->remote_sep);
+ i_assert(!hash_table_is_created(tree->name128_remotesep_hash));
+
+ hash_table_create(&tree->name128_remotesep_hash, tree->pool, 0,
+ guid_128_hash, guid_128_cmp);
+ iter = dsync_mailbox_tree_iter_init(tree);
+ while (dsync_mailbox_tree_iter_next(iter, &name, &node)) {
+ sha128 = p_new(tree->pool, guid_128_t, 1);
+ T_BEGIN {
+ const char *remote_name =
+ convert_name_to_remote_sep(tree, name);
+ mailbox_name_get_sha128(remote_name, *sha128);
+ } T_END;
+ guid_p = *sha128;
+ hash_table_insert(tree->name128_remotesep_hash, guid_p, node);
+ }
+ dsync_mailbox_tree_iter_deinit(&iter);
+}
+
+int dsync_mailbox_tree_guid_hash_add(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node **old_node_r)
+{
+ struct dsync_mailbox_node *old_node;
+ uint8_t *guid = node->mailbox_guid;
+
+ if (guid_128_is_empty(node->mailbox_guid))
+ return 0;
+
+ *old_node_r = old_node = hash_table_lookup(tree->guid_hash, guid);
+ if (old_node == NULL)
+ hash_table_insert(tree->guid_hash, guid, node);
+ else if (old_node != node)
+ return -1;
+ return 0;
+}
+
+int dsync_mailbox_tree_build_guid_hash(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node **dup_node1_r,
+ struct dsync_mailbox_node **dup_node2_r)
+{
+ struct dsync_mailbox_tree_iter *iter;
+ struct dsync_mailbox_node *node, *old_node;
+ const char *name;
+ int ret = 0;
+
+ if (!hash_table_is_created(tree->guid_hash)) {
+ hash_table_create(&tree->guid_hash, tree->pool, 0,
+ guid_128_hash, guid_128_cmp);
+ }
+ iter = dsync_mailbox_tree_iter_init(tree);
+ while (dsync_mailbox_tree_iter_next(iter, &name, &node)) {
+ if (dsync_mailbox_tree_guid_hash_add(tree, node, &old_node) < 0) {
+ *dup_node1_r = node;
+ *dup_node2_r = old_node;
+ ret = -1;
+ }
+ }
+ dsync_mailbox_tree_iter_deinit(&iter);
+ return ret;
+}
+
+struct dsync_mailbox_node *
+dsync_mailbox_tree_lookup_guid(struct dsync_mailbox_tree *tree,
+ const guid_128_t guid)
+{
+ const uint8_t *guid_p = guid;
+
+ return hash_table_lookup(tree->guid_hash, guid_p);
+}
+
+const struct dsync_mailbox_delete *
+dsync_mailbox_tree_get_deletes(struct dsync_mailbox_tree *tree,
+ unsigned int *count_r)
+{
+ return array_get(&tree->deletes, count_r);
+}
+
+struct dsync_mailbox_node *
+dsync_mailbox_tree_find_delete(struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_delete *del)
+{
+ const uint8_t *guid_p = del->guid;
+
+ i_assert(hash_table_is_created(tree->guid_hash));
+ i_assert(tree->remote_sep != '\0');
+
+ if (del->type == DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) {
+ /* find node by GUID */
+ return hash_table_lookup(tree->guid_hash, guid_p);
+ }
+
+ /* find node by name. this is a bit tricky, since the hierarchy
+ separator may differ from ours. */
+ if (tree->sep == tree->remote_sep) {
+ if (!hash_table_is_created(tree->name128_hash))
+ dsync_mailbox_tree_build_name128_hash(tree);
+ return hash_table_lookup(tree->name128_hash, guid_p);
+ } else {
+ if (!hash_table_is_created(tree->name128_remotesep_hash))
+ dsync_mailbox_tree_build_name128_remotesep_hash(tree);
+ return hash_table_lookup(tree->name128_remotesep_hash, guid_p);
+ }
+}
+
+void dsync_mailbox_tree_set_remote_chars(struct dsync_mailbox_tree *tree,
+ char remote_sep, char escape_char)
+{
+ i_assert(tree->remote_sep == '\0');
+ i_assert(tree->remote_escape_char == '\0');
+
+ tree->remote_sep = remote_sep;
+ tree->remote_escape_char = escape_char;
+}
+
+static void
+dsync_mailbox_tree_dup_nodes(struct dsync_mailbox_tree *dest_tree,
+ const struct dsync_mailbox_node *src,
+ string_t *path)
+{
+ size_t prefix_len = str_len(path);
+ struct dsync_mailbox_node *node;
+
+ if (prefix_len > 0) {
+ str_append_c(path, dest_tree->sep);
+ prefix_len++;
+ }
+ for (; src != NULL; src = src->next) {
+ str_truncate(path, prefix_len);
+ str_append(path, src->name);
+ node = dsync_mailbox_tree_get(dest_tree, str_c(path));
+
+ node->ns = src->ns;
+ memcpy(node->mailbox_guid, src->mailbox_guid,
+ sizeof(node->mailbox_guid));
+ node->uid_validity = src->uid_validity;
+ node->uid_next = src->uid_next;
+ node->existence = src->existence;
+ node->last_renamed_or_created = src->last_renamed_or_created;
+ node->subscribed = src->subscribed;
+ node->last_subscription_change = src->last_subscription_change;
+
+ if (src->first_child != NULL) {
+ dsync_mailbox_tree_dup_nodes(dest_tree,
+ src->first_child, path);
+ }
+ }
+}
+
+struct dsync_mailbox_tree *
+dsync_mailbox_tree_dup(const struct dsync_mailbox_tree *src)
+{
+ struct dsync_mailbox_tree *dest;
+ string_t *str = t_str_new(128);
+
+ dest = dsync_mailbox_tree_init(src->sep, src->escape_char,
+ src->alt_char);
+ dsync_mailbox_tree_dup_nodes(dest, &src->root, str);
+ return dest;
+}
+
+int dsync_mailbox_node_name_cmp(struct dsync_mailbox_node *const *n1,
+ struct dsync_mailbox_node *const *n2)
+{
+ return strcmp((*n1)->name, (*n2)->name);
+}
+
+static bool
+dsync_mailbox_nodes_equal(const struct dsync_mailbox_node *node1,
+ const struct dsync_mailbox_node *node2)
+{
+ return strcmp(node1->name, node2->name) == 0 &&
+ node1->ns == node2->ns &&
+ memcmp(node1->mailbox_guid, node2->mailbox_guid,
+ sizeof(node1->mailbox_guid)) == 0 &&
+ node1->uid_validity == node2->uid_validity &&
+ node1->existence == node2->existence &&
+ node1->subscribed == node2->subscribed;
+}
+
+static bool
+dsync_mailbox_branches_equal(struct dsync_mailbox_node *node1,
+ struct dsync_mailbox_node *node2)
+{
+ /* this function is used only for unit tests, so performance doesn't
+ really matter */
+ struct dsync_mailbox_node *n, **snodes1, **snodes2;
+ unsigned int i, count;
+
+ for (n = node1, count = 0; n != NULL; n = n->next) count++;
+ for (n = node2, i = 0; n != NULL; n = n->next) i++;
+ if (i != count)
+ return FALSE;
+ if (count == 0)
+ return TRUE;
+
+ /* sort the trees by name */
+ snodes1 = t_new(struct dsync_mailbox_node *, count);
+ snodes2 = t_new(struct dsync_mailbox_node *, count);
+ for (n = node1, i = 0; n != NULL; n = n->next, i++)
+ snodes1[i] = n;
+ for (n = node2, i = 0; n != NULL; n = n->next, i++)
+ snodes2[i] = n;
+ i_qsort(snodes1, count, sizeof(*snodes1), dsync_mailbox_node_name_cmp);
+ i_qsort(snodes2, count, sizeof(*snodes2), dsync_mailbox_node_name_cmp);
+
+ for (i = 0; i < count; i++) {
+ if (!dsync_mailbox_nodes_equal(snodes1[i], snodes2[i]))
+ return FALSE;
+ if (!dsync_mailbox_branches_equal(snodes1[i]->first_child,
+ snodes2[i]->first_child))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool dsync_mailbox_trees_equal(struct dsync_mailbox_tree *tree1,
+ struct dsync_mailbox_tree *tree2)
+{
+ bool ret;
+
+ T_BEGIN {
+ ret = dsync_mailbox_branches_equal(&tree1->root, &tree2->root);
+ } T_END;
+ return ret;
+}
+
+const char *dsync_mailbox_node_to_string(const struct dsync_mailbox_node *node)
+{
+ return t_strdup_printf("guid=%s uid_validity=%u uid_next=%u subs=%s last_change=%ld last_subs=%ld",
+ guid_128_to_string(node->mailbox_guid),
+ node->uid_validity, node->uid_next,
+ node->subscribed ? "yes" : "no",
+ (long)node->last_renamed_or_created,
+ (long)node->last_subscription_change);
+}
+
+const char *
+dsync_mailbox_delete_type_to_string(enum dsync_mailbox_delete_type type)
+{
+ switch (type) {
+ case DSYNC_MAILBOX_DELETE_TYPE_MAILBOX:
+ return "mailbox";
+ case DSYNC_MAILBOX_DELETE_TYPE_DIR:
+ return "dir";
+ case DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE:
+ return "unsubscribe";
+ }
+ i_unreached();
+}
+
+const char *const *
+dsync_mailbox_name_to_parts(const char *name, char hierarchy_sep,
+ char escape_char)
+{
+ const char sep[] = { hierarchy_sep, '\0' };
+ char **parts = p_strsplit(unsafe_data_stack_pool, name, sep);
+ if (escape_char != '\0') {
+ for (unsigned int i = 0; parts[i] != NULL; i++) {
+ mailbox_list_name_unescape((const char **)&parts[i],
+ escape_char);
+ }
+ }
+ return (const char *const *)parts;
+}
diff --git a/src/doveadm/dsync/dsync-mailbox-tree.h b/src/doveadm/dsync/dsync-mailbox-tree.h
new file mode 100644
index 0000000..3542ef4
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox-tree.h
@@ -0,0 +1,211 @@
+#ifndef DSYNC_MAILBOX_TREE_H
+#define DSYNC_MAILBOX_TREE_H
+
+#include "guid.h"
+#include "mail-error.h"
+
+struct mail_namespace;
+struct dsync_brain;
+
+enum dsync_mailbox_trees_sync_type {
+ /* two-way sync for both mailboxes */
+ DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY,
+ /* make remote tree look exactly like the local tree */
+ DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL,
+ /* make local tree look exactly like the remote tree */
+ DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE
+};
+
+enum dsync_mailbox_trees_sync_flags {
+ /* Enable debugging */
+ DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG = 0x01,
+ /* Show ourself as "master brain" in the debug output */
+ DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN = 0x02,
+ /* Disable mailbox renaming logic. This is just a kludge that should
+ be removed once the renaming logic has no more bugs.. */
+ DSYNC_MAILBOX_TREES_SYNC_FLAG_NO_RENAMES = 0x04
+};
+
+enum dsync_mailbox_node_existence {
+ /* this is just a filler node for children or for
+ subscription deletion */
+ DSYNC_MAILBOX_NODE_NONEXISTENT = 0,
+ /* if mailbox GUID is set, the mailbox exists.
+ otherwise the directory exists. */
+ DSYNC_MAILBOX_NODE_EXISTS,
+ /* if mailbox GUID is set, the mailbox has been deleted.
+ otherwise the directory has been deleted. */
+ DSYNC_MAILBOX_NODE_DELETED
+};
+
+struct dsync_mailbox_node {
+ struct dsync_mailbox_node *parent, *next, *first_child;
+
+ /* namespace where this node belongs to */
+ struct mail_namespace *ns;
+ /* this node's name (not including parents) */
+ const char *name;
+ /* mailbox GUID, or full of zeros if this is about a directory name */
+ guid_128_t mailbox_guid;
+ /* mailbox's UIDVALIDITY/UIDNEXT (may be 0 if not assigned yet) */
+ uint32_t uid_validity, uid_next;
+
+ /* existence of this mailbox/directory.
+ doesn't affect subscription state. */
+ enum dsync_mailbox_node_existence existence;
+ /* last time the mailbox/directory was created/renamed,
+ 0 if not known */
+ time_t last_renamed_or_created;
+
+ /* last time the subscription state was changed, 0 if not known */
+ time_t last_subscription_change;
+ /* is this mailbox or directory subscribed? */
+ bool subscribed:1;
+
+ /* Internal syncing flags: */
+ bool sync_delayed_guid_change:1;
+ bool sync_temporary_name:1;
+};
+ARRAY_DEFINE_TYPE(dsync_mailbox_node, struct dsync_mailbox_node *);
+
+#define dsync_mailbox_node_guids_equal(node1, node2) \
+ (memcmp((node1)->mailbox_guid, (node2)->mailbox_guid, \
+ sizeof(guid_128_t)) == 0)
+
+#define dsync_mailbox_node_is_dir(node) \
+ guid_128_is_empty((node)->mailbox_guid)
+
+enum dsync_mailbox_delete_type {
+ /* Delete mailbox by given GUID */
+ DSYNC_MAILBOX_DELETE_TYPE_MAILBOX = 1,
+ /* Delete mailbox directory by given SHA1 name */
+ DSYNC_MAILBOX_DELETE_TYPE_DIR,
+ /* Unsubscribe mailbox by given SHA1 name */
+ DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE,
+};
+
+struct dsync_mailbox_delete {
+ enum dsync_mailbox_delete_type type;
+ guid_128_t guid;
+ time_t timestamp;
+};
+
+enum dsync_mailbox_tree_sync_type {
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR,
+ /* Rename given mailbox name and its children */
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE,
+ DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE
+};
+
+struct dsync_mailbox_tree_sync_change {
+ enum dsync_mailbox_tree_sync_type type;
+
+ /* for all types: */
+ struct mail_namespace *ns;
+ const char *full_name;
+
+ /* for create_box and delete_box: */
+ guid_128_t mailbox_guid;
+ /* for create_box: */
+ uint32_t uid_validity;
+ /* for rename: */
+ const char *rename_dest_name;
+};
+
+struct dsync_mailbox_tree *
+dsync_mailbox_tree_init(char sep, char escape_char, char alt_char);
+void dsync_mailbox_tree_deinit(struct dsync_mailbox_tree **tree);
+
+/* Lookup a mailbox node by name. Returns NULL if not known. */
+struct dsync_mailbox_node *
+dsync_mailbox_tree_lookup(struct dsync_mailbox_tree *tree,
+ const char *full_name);
+/* Lookup a mailbox node by GUID. Returns NULL if not known.
+ The mailbox GUID hash must have been build before calling this. */
+struct dsync_mailbox_node *
+dsync_mailbox_tree_lookup_guid(struct dsync_mailbox_tree *tree,
+ const guid_128_t guid);
+/* Lookup or create a mailbox node by name. */
+struct dsync_mailbox_node *
+dsync_mailbox_tree_get(struct dsync_mailbox_tree *tree, const char *full_name);
+
+/* Returns full name for the given mailbox node. */
+const char *dsync_mailbox_node_get_full_name(const struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_node *node);
+void dsync_mailbox_node_append_full_name(string_t *str,
+ const struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_node *node);
+
+/* Copy everything from src to dest, except name and hierarchy pointers */
+void dsync_mailbox_node_copy_data(struct dsync_mailbox_node *dest,
+ const struct dsync_mailbox_node *src);
+
+/* Split mailbox name into its hierarchical parts. The mailbox name is
+ unescaped if the escape_char is not '\0'. */
+const char *const *
+dsync_mailbox_name_to_parts(const char *name, char hierarchy_sep,
+ char escape_char);
+/* Add nodes to tree from the given namespace. If box_name or box_guid is
+ non-NULL, add only that mailbox to the tree. */
+int dsync_mailbox_tree_fill(struct dsync_mailbox_tree *tree,
+ struct mail_namespace *ns, const char *box_name,
+ const guid_128_t box_guid,
+ const char *const *exclude_mailboxes,
+ char alt_char,
+ enum mail_error *error_r);
+
+/* Return all known deleted mailboxes and directories. */
+const struct dsync_mailbox_delete *
+dsync_mailbox_tree_get_deletes(struct dsync_mailbox_tree *tree,
+ unsigned int *count_r);
+/* Return mailbox node for a given delete record, or NULL if it doesn't exist.
+ The delete record is intended to come from another tree, possibly with
+ a different hierarchy separator. dsync_mailbox_tree_build_guid_hash() must
+ have been called before this. */
+struct dsync_mailbox_node *
+dsync_mailbox_tree_find_delete(struct dsync_mailbox_tree *tree,
+ const struct dsync_mailbox_delete *del);
+/* Build GUID lookup hash, if it's not already built. Returns 0 if ok, -1 if
+ there are duplicate GUIDs. The nodes with the duplicate GUIDs are
+ returned. */
+int dsync_mailbox_tree_build_guid_hash(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node **dup_node1_r,
+ struct dsync_mailbox_node **dup_node2_r);
+/* Manually add a new node to hash. */
+int dsync_mailbox_tree_guid_hash_add(struct dsync_mailbox_tree *tree,
+ struct dsync_mailbox_node *node,
+ struct dsync_mailbox_node **old_node_r);
+/* Set remote separator used for directory deletions in
+ dsync_mailbox_tree_find_delete() */
+void dsync_mailbox_tree_set_remote_chars(struct dsync_mailbox_tree *tree,
+ char remote_sep,
+ char remote_escape_char);
+
+/* Iterate through all nodes in a tree (depth-first) */
+struct dsync_mailbox_tree_iter *
+dsync_mailbox_tree_iter_init(struct dsync_mailbox_tree *tree);
+bool dsync_mailbox_tree_iter_next(struct dsync_mailbox_tree_iter *iter,
+ const char **full_name_r,
+ struct dsync_mailbox_node **node_r);
+void dsync_mailbox_tree_iter_deinit(struct dsync_mailbox_tree_iter **iter);
+
+/* Sync local and remote trees so at the end they're exactly the same.
+ Return changes done to local tree. */
+struct dsync_mailbox_tree_sync_ctx *
+dsync_mailbox_trees_sync_init(struct dsync_mailbox_tree *local_tree,
+ struct dsync_mailbox_tree *remote_tree,
+ enum dsync_mailbox_trees_sync_type sync_type,
+ enum dsync_mailbox_trees_sync_flags sync_flags);
+const struct dsync_mailbox_tree_sync_change *
+dsync_mailbox_trees_sync_next(struct dsync_mailbox_tree_sync_ctx *ctx);
+int dsync_mailbox_trees_sync_deinit(struct dsync_mailbox_tree_sync_ctx **ctx);
+
+const char *dsync_mailbox_node_to_string(const struct dsync_mailbox_node *node);
+const char *
+dsync_mailbox_delete_type_to_string(enum dsync_mailbox_delete_type type);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-mailbox.c b/src/doveadm/dsync/dsync-mailbox.c
new file mode 100644
index 0000000..d6d06fd
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "mail-storage-private.h"
+#include "dsync-brain-private.h"
+#include "dsync-mailbox.h"
+
+void dsync_mailbox_attribute_dup(pool_t pool,
+ const struct dsync_mailbox_attribute *src,
+ struct dsync_mailbox_attribute *dest_r)
+{
+ dest_r->type = src->type;
+ dest_r->key = p_strdup(pool, src->key);
+ dest_r->value = p_strdup(pool, src->value);
+ if (src->value_stream != NULL) {
+ dest_r->value_stream = src->value_stream;
+ i_stream_ref(dest_r->value_stream);
+ }
+
+ dest_r->deleted = src->deleted;
+ dest_r->last_change = src->last_change;
+ dest_r->modseq = src->modseq;
+}
+
+int dsync_mailbox_lock(struct dsync_brain *brain, struct mailbox *box,
+ struct file_lock **lock_r)
+{
+ const char *path, *error;
+ int ret;
+
+ /* Make sure the mailbox is open - locking requires it */
+ if (mailbox_open(box) < 0) {
+ i_error("Can't open mailbox %s: %s", mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &brain->mail_error));
+ return -1;
+ }
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path);
+ if (ret < 0) {
+ i_error("Can't get mailbox %s path: %s", mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, &brain->mail_error));
+ return -1;
+ }
+ if (ret == 0) {
+ /* No index files - don't do any locking. In theory we still
+ could, but this lock is mainly meant to prevent replication
+ problems, and replication wouldn't work without indexes. */
+ *lock_r = NULL;
+ return 0;
+ }
+
+ if (mailbox_lock_file_create(box, DSYNC_MAILBOX_LOCK_FILENAME,
+ brain->mailbox_lock_timeout_secs,
+ lock_r, &error) <= 0) {
+ i_error("Failed to lock mailbox %s for dsyncing: %s",
+ box->vname, error);
+ return -1;
+ }
+ return 0;
+}
diff --git a/src/doveadm/dsync/dsync-mailbox.h b/src/doveadm/dsync/dsync-mailbox.h
new file mode 100644
index 0000000..7e81c0c
--- /dev/null
+++ b/src/doveadm/dsync/dsync-mailbox.h
@@ -0,0 +1,44 @@
+#ifndef DSYNC_MAILBOX_H
+#define DSYNC_MAILBOX_H
+
+#include "mail-storage.h"
+
+struct dsync_brain;
+
+/* Mailbox that is going to be synced. Its name was already sent in the
+ mailbox tree. */
+struct dsync_mailbox {
+ guid_128_t mailbox_guid;
+ bool mailbox_lost;
+ bool mailbox_ignore;
+ bool have_guids, have_save_guids, have_only_guid128;
+
+ uint32_t uid_validity, uid_next, messages_count, first_recent_uid;
+ uint64_t highest_modseq, highest_pvt_modseq;
+ ARRAY_TYPE(mailbox_cache_field) cache_fields;
+};
+
+struct dsync_mailbox_attribute {
+ enum mail_attribute_type type;
+ const char *key;
+ /* if both values are NULL = not looked up yet / deleted */
+ const char *value;
+ struct istream *value_stream;
+
+ time_t last_change; /* 0 = unknown */
+ uint64_t modseq; /* 0 = unknown */
+
+ bool deleted; /* attribute is known to have been deleted */
+ bool exported; /* internally used by exporting */
+};
+#define DSYNC_ATTR_HAS_VALUE(attr) \
+ ((attr)->value != NULL || (attr)->value_stream != NULL)
+
+void dsync_mailbox_attribute_dup(pool_t pool,
+ const struct dsync_mailbox_attribute *src,
+ struct dsync_mailbox_attribute *dest_r);
+
+int dsync_mailbox_lock(struct dsync_brain *brain, struct mailbox *box,
+ struct file_lock **lock_r);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-serializer.c b/src/doveadm/dsync/dsync-serializer.c
new file mode 100644
index 0000000..7b369b8
--- /dev/null
+++ b/src/doveadm/dsync/dsync-serializer.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "dsync-serializer.h"
+
+struct dsync_serializer {
+ pool_t pool;
+ const char *const *keys;
+ unsigned int keys_count;
+};
+
+struct dsync_serializer_encoder {
+ pool_t pool;
+ struct dsync_serializer *serializer;
+ ARRAY_TYPE(const_string) values;
+};
+
+struct dsync_serializer *dsync_serializer_init(const char *const keys[])
+{
+ struct dsync_serializer *serializer;
+ pool_t pool;
+ const char **dup_keys;
+ unsigned int i, count;
+
+ pool = pool_alloconly_create("dsync serializer", 512);
+ serializer = p_new(pool, struct dsync_serializer, 1);
+ serializer->pool = pool;
+
+ count = str_array_length(keys);
+ dup_keys = p_new(pool, const char *, count + 1);
+ for (i = 0; i < count; i++)
+ dup_keys[i] = p_strdup(pool, keys[i]);
+ serializer->keys = dup_keys;
+ serializer->keys_count = count;
+ return serializer;
+}
+
+void dsync_serializer_deinit(struct dsync_serializer **_serializer)
+{
+ struct dsync_serializer *serializer = *_serializer;
+
+ *_serializer = NULL;
+
+ pool_unref(&serializer->pool);
+}
+
+const char *
+dsync_serializer_encode_header_line(struct dsync_serializer *serializer)
+{
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ for (i = 0; serializer->keys[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, serializer->keys[i]);
+ }
+ str_append_c(str, '\n');
+ return str_c(str);
+}
+
+struct dsync_serializer_encoder *
+dsync_serializer_encode_begin(struct dsync_serializer *serializer)
+{
+ struct dsync_serializer_encoder *encoder;
+ pool_t pool = pool_alloconly_create("dsync serializer encode", 1024);
+
+ encoder = p_new(pool, struct dsync_serializer_encoder, 1);
+ encoder->pool = pool;
+ encoder->serializer = serializer;
+ p_array_init(&encoder->values, pool, serializer->keys_count);
+ return encoder;
+}
+
+void dsync_serializer_encode_add(struct dsync_serializer_encoder *encoder,
+ const char *key, const char *value)
+{
+ unsigned int i;
+
+ for (i = 0; encoder->serializer->keys[i] != NULL; i++) {
+ if (strcmp(encoder->serializer->keys[i], key) == 0) {
+ value = p_strdup(encoder->pool, value);
+ array_idx_set(&encoder->values, i, &value);
+ return;
+ }
+ }
+ i_panic("Unknown key: %s", key);
+}
+
+void dsync_serializer_encode_finish(struct dsync_serializer_encoder **_encoder,
+ string_t *output)
+{
+ struct dsync_serializer_encoder *encoder = *_encoder;
+ const char *const *values;
+ unsigned int i, count;
+
+ *_encoder = NULL;
+
+ values = array_get(&encoder->values, &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(output, '\t');
+ if (values[i] == NULL)
+ str_append_c(output, NULL_CHR);
+ else {
+ if (values[i][0] == NULL_CHR)
+ str_append_c(output, NULL_CHR);
+ str_append_tabescaped(output, values[i]);
+ }
+ }
+ str_append_c(output, '\n');
+
+ pool_unref(&encoder->pool);
+}
diff --git a/src/doveadm/dsync/dsync-serializer.h b/src/doveadm/dsync/dsync-serializer.h
new file mode 100644
index 0000000..1ed6c31
--- /dev/null
+++ b/src/doveadm/dsync/dsync-serializer.h
@@ -0,0 +1,18 @@
+#ifndef DSYNC_SERIALIZER_H
+#define DSYNC_SERIALIZER_H
+
+#define NULL_CHR '\002'
+
+struct dsync_serializer *dsync_serializer_init(const char *const keys[]);
+void dsync_serializer_deinit(struct dsync_serializer **serializer);
+
+const char *
+dsync_serializer_encode_header_line(struct dsync_serializer *serializer);
+struct dsync_serializer_encoder *
+dsync_serializer_encode_begin(struct dsync_serializer *serializer);
+void dsync_serializer_encode_add(struct dsync_serializer_encoder *encoder,
+ const char *key, const char *value);
+void dsync_serializer_encode_finish(struct dsync_serializer_encoder **encoder,
+ string_t *output);
+
+#endif
diff --git a/src/doveadm/dsync/dsync-transaction-log-scan.c b/src/doveadm/dsync/dsync-transaction-log-scan.c
new file mode 100644
index 0000000..dd2834a
--- /dev/null
+++ b/src/doveadm/dsync/dsync-transaction-log-scan.c
@@ -0,0 +1,608 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "mail-index-modseq.h"
+#include "mail-storage-private.h"
+#include "dsync-mail.h"
+#include "dsync-mailbox.h"
+#include "dsync-transaction-log-scan.h"
+
+struct dsync_transaction_log_scan {
+ pool_t pool;
+ HASH_TABLE_TYPE(dsync_uid_mail_change) changes;
+ HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
+ struct mail_index_view *view;
+ uint32_t highest_wanted_uid;
+
+ uint32_t last_log_seq;
+ uoff_t last_log_offset;
+
+ bool returned_all_changes;
+};
+
+static bool ATTR_NOWARN_UNUSED_RESULT
+export_change_get(struct dsync_transaction_log_scan *ctx, uint32_t uid,
+ enum dsync_mail_change_type type,
+ struct dsync_mail_change **change_r)
+{
+ struct dsync_mail_change *change;
+ const char *orig_guid;
+
+ i_assert(uid > 0);
+ i_assert(type != DSYNC_MAIL_CHANGE_TYPE_SAVE);
+
+ *change_r = NULL;
+
+ if (uid > ctx->highest_wanted_uid)
+ return FALSE;
+
+ change = hash_table_lookup(ctx->changes, POINTER_CAST(uid));
+ if (change == NULL) {
+ /* first change for this UID */
+ change = p_new(ctx->pool, struct dsync_mail_change, 1);
+ change->uid = uid;
+ change->type = type;
+ hash_table_insert(ctx->changes, POINTER_CAST(uid), change);
+ } else if (type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ /* expunge overrides flag changes */
+ orig_guid = change->guid;
+ i_zero(change);
+ change->type = type;
+ change->uid = uid;
+ change->guid = orig_guid;
+ } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) {
+ /* already expunged, this change doesn't matter */
+ return FALSE;
+ } else {
+ /* another flag update */
+ }
+ *change_r = change;
+ return TRUE;
+}
+
+static void
+log_add_expunge(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr)
+{
+ const struct mail_transaction_expunge *rec = data, *end;
+ struct dsync_mail_change *change;
+ uint32_t uid;
+
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* this is simply a request for expunge */
+ return;
+ }
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ for (uid = rec->uid1; uid <= rec->uid2; uid++) {
+ export_change_get(ctx, uid,
+ DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
+ &change);
+ }
+ }
+}
+
+static bool
+log_add_expunge_uid(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr, uint32_t uid)
+{
+ const struct mail_transaction_expunge *rec = data, *end;
+ struct dsync_mail_change *change;
+
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* this is simply a request for expunge */
+ return FALSE;
+ }
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ if (uid >= rec->uid1 && uid <= rec->uid2) {
+ export_change_get(ctx, uid,
+ DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
+ &change);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+log_add_expunge_guid(struct dsync_transaction_log_scan *ctx,
+ struct mail_index_view *view, const void *data,
+ const struct mail_transaction_header *hdr)
+{
+ const struct mail_transaction_expunge_guid *rec = data, *end;
+ struct dsync_mail_change *change;
+ uint32_t seq;
+ bool external;
+
+ external = (hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ if (!external && mail_index_lookup_seq(view, rec->uid, &seq)) {
+ /* expunge request that hasn't been actually done yet.
+ we check non-external ones because they might have
+ the GUID while external ones don't. */
+ continue;
+ }
+ if (export_change_get(ctx, rec->uid,
+ DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
+ &change) &&
+ !guid_128_is_empty(rec->guid_128)) T_BEGIN {
+ change->guid = p_strdup(ctx->pool,
+ guid_128_to_string(rec->guid_128));
+ } T_END;
+ }
+}
+
+static bool
+log_add_expunge_guid_uid(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr, uint32_t uid)
+{
+ const struct mail_transaction_expunge_guid *rec = data, *end;
+ struct dsync_mail_change *change;
+
+ /* we're assuming UID is already known to be expunged */
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ if (rec->uid != uid)
+ continue;
+
+ if (!export_change_get(ctx, rec->uid,
+ DSYNC_MAIL_CHANGE_TYPE_EXPUNGE,
+ &change))
+ i_unreached();
+ if (!guid_128_is_empty(rec->guid_128)) T_BEGIN {
+ change->guid = p_strdup(ctx->pool,
+ guid_128_to_string(rec->guid_128));
+ } T_END;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+log_add_flag_update(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr)
+{
+ const struct mail_transaction_flag_update *rec = data, *end;
+ struct dsync_mail_change *change;
+ uint32_t uid;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ for (uid = rec->uid1; uid <= rec->uid2; uid++) {
+ if (export_change_get(ctx, uid,
+ DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
+ &change)) {
+ change->add_flags |= rec->add_flags;
+ change->remove_flags &= ENUM_NEGATE(rec->add_flags);
+ change->remove_flags |= rec->remove_flags;
+ change->add_flags &= ENUM_NEGATE(rec->remove_flags);
+ }
+ }
+ }
+}
+
+static void
+log_add_keyword_reset(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr)
+{
+ const struct mail_transaction_keyword_reset *rec = data, *end;
+ struct dsync_mail_change *change;
+ uint32_t uid;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ for (uid = rec->uid1; uid <= rec->uid2; uid++) {
+ if (!export_change_get(ctx, uid,
+ DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
+ &change))
+ continue;
+
+ change->keywords_reset = TRUE;
+ if (array_is_created(&change->keyword_changes))
+ array_clear(&change->keyword_changes);
+ }
+ }
+}
+
+static void
+keywords_change_remove(struct dsync_mail_change *change, const char *name)
+{
+ const char *const *changes;
+ unsigned int i, count;
+
+ changes = array_get(&change->keyword_changes, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(changes[i]+1, name) == 0) {
+ array_delete(&change->keyword_changes, i, 1);
+ break;
+ }
+ }
+}
+
+static void
+log_add_keyword_update(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr)
+{
+ const struct mail_transaction_keyword_update *rec = data;
+ struct dsync_mail_change *change;
+ const char *kw_name, *change_str;
+ const uint32_t *uids, *end;
+ unsigned int uids_offset;
+ uint32_t uid;
+
+ uids_offset = sizeof(*rec) + rec->name_size;
+ if ((uids_offset % 4) != 0)
+ uids_offset += 4 - (uids_offset % 4);
+
+ kw_name = t_strndup((const void *)(rec+1), rec->name_size);
+ switch (rec->modify_type) {
+ case MODIFY_ADD:
+ change_str = p_strdup_printf(ctx->pool, "%c%s",
+ KEYWORD_CHANGE_ADD, kw_name);
+ break;
+ case MODIFY_REMOVE:
+ change_str = p_strdup_printf(ctx->pool, "%c%s",
+ KEYWORD_CHANGE_REMOVE, kw_name);
+ break;
+ default:
+ i_unreached();
+ }
+
+ uids = CONST_PTR_OFFSET(rec, uids_offset);
+ end = CONST_PTR_OFFSET(rec, hdr->size);
+
+ for (; uids < end; uids += 2) {
+ for (uid = uids[0]; uid <= uids[1]; uid++) {
+ if (!export_change_get(ctx, uid,
+ DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
+ &change))
+ continue;
+ if (!array_is_created(&change->keyword_changes)) {
+ p_array_init(&change->keyword_changes,
+ ctx->pool, 4);
+ } else {
+ keywords_change_remove(change, kw_name);
+ }
+ array_push_back(&change->keyword_changes, &change_str);
+ }
+ }
+}
+
+static void
+log_add_modseq_update(struct dsync_transaction_log_scan *ctx, const void *data,
+ const struct mail_transaction_header *hdr, bool pvt_scan)
+{
+ const struct mail_transaction_modseq_update *rec = data, *end;
+ struct dsync_mail_change *change;
+ uint64_t modseq;
+
+ /* update message's modseq, possibly by creating an empty flag change */
+ end = CONST_PTR_OFFSET(rec, hdr->size);
+ for (; rec != end; rec++) {
+ if (rec->uid == 0) {
+ /* highestmodseq update */
+ continue;
+ }
+
+ if (!export_change_get(ctx, rec->uid,
+ DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE,
+ &change))
+ continue;
+
+ modseq = rec->modseq_low32 |
+ ((uint64_t)rec->modseq_high32 << 32);
+ if (!pvt_scan) {
+ if (change->modseq < modseq)
+ change->modseq = modseq;
+ } else {
+ if (change->pvt_modseq < modseq)
+ change->pvt_modseq = modseq;
+ }
+ }
+}
+
+static void
+log_add_attribute_update_key(struct dsync_transaction_log_scan *ctx,
+ const char *attr_change, uint64_t modseq)
+{
+ struct dsync_mailbox_attribute lookup_attr, *attr;
+
+ i_assert(strlen(attr_change) > 2); /* checked by lib-index */
+
+ lookup_attr.type = attr_change[1] == 'p' ?
+ MAIL_ATTRIBUTE_TYPE_PRIVATE : MAIL_ATTRIBUTE_TYPE_SHARED;
+ lookup_attr.key = attr_change+2;
+
+ attr = hash_table_lookup(ctx->attr_changes, &lookup_attr);
+ if (attr == NULL) {
+ attr = p_new(ctx->pool, struct dsync_mailbox_attribute, 1);
+ attr->type = lookup_attr.type;
+ attr->key = p_strdup(ctx->pool, lookup_attr.key);
+ hash_table_insert(ctx->attr_changes, attr, attr);
+ }
+ attr->deleted = attr_change[0] == '-';
+ attr->modseq = modseq;
+}
+
+static void
+log_add_attribute_update(struct dsync_transaction_log_scan *ctx,
+ const void *data,
+ const struct mail_transaction_header *hdr,
+ uint64_t modseq)
+{
+ const char *attr_changes = data;
+ unsigned int i;
+
+ for (i = 0; i < hdr->size && attr_changes[i] != '\0'; ) {
+ log_add_attribute_update_key(ctx, attr_changes+i, modseq);
+ i += strlen(attr_changes+i) + 1;
+ }
+}
+
+static int
+dsync_log_set(struct dsync_transaction_log_scan *ctx,
+ struct mail_index_view *view, bool pvt_scan,
+ struct mail_transaction_log_view *log_view, uint64_t modseq)
+{
+ uint32_t log_seq, end_seq;
+ uoff_t log_offset, end_offset;
+ const char *reason;
+ bool reset;
+ int ret;
+
+ end_seq = view->log_file_head_seq;
+ end_offset = view->log_file_head_offset;
+
+ if (modseq != 0 &&
+ mail_index_modseq_get_next_log_offset(view, modseq,
+ &log_seq, &log_offset)) {
+ /* scan the view only up to end of the current view.
+ if there are more changes, we don't care about them until
+ the next sync. */
+ ret = mail_transaction_log_view_set(log_view,
+ log_seq, log_offset,
+ end_seq, end_offset,
+ &reset, &reason);
+ if (ret != 0)
+ return ret;
+ }
+
+ /* return everything we've got (until the end of the view) */
+ if (!pvt_scan)
+ ctx->returned_all_changes = TRUE;
+ if (mail_transaction_log_view_set_all(log_view) < 0)
+ return -1;
+
+ mail_transaction_log_view_get_prev_pos(log_view, &log_seq, &log_offset);
+ if (log_seq > end_seq ||
+ (log_seq == end_seq && log_offset > end_offset)) {
+ end_seq = log_seq;
+ end_offset = log_offset;
+ }
+ ret = mail_transaction_log_view_set(log_view,
+ log_seq, log_offset,
+ end_seq, end_offset,
+ &reset, &reason);
+ if (ret == 0) {
+ /* we shouldn't get here. _view_set_all() already
+ reserved all the log files, the _view_set() only
+ removed unwanted ones. */
+ i_error("%s: Couldn't set transaction log view (seq %u..%u): %s",
+ view->index->filepath, log_seq, end_seq, reason);
+ ret = -1;
+ }
+ if (ret < 0)
+ return -1;
+ if (modseq != 0) {
+ /* we didn't see all the changes that we wanted to */
+ return 0;
+ }
+ return 1;
+}
+
+static int
+dsync_log_scan(struct dsync_transaction_log_scan *ctx,
+ struct mail_index_view *view, uint64_t modseq, bool pvt_scan)
+{
+ struct mail_transaction_log_view *log_view;
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ uint32_t file_seq, max_seq;
+ uoff_t file_offset, max_offset;
+ uint64_t cur_modseq;
+ int ret;
+
+ log_view = mail_transaction_log_view_open(view->index->log);
+ if ((ret = dsync_log_set(ctx, view, pvt_scan, log_view, modseq)) < 0) {
+ mail_transaction_log_view_close(&log_view);
+ return -1;
+ }
+
+ /* read the log only up to current position in view */
+ max_seq = view->log_file_expunge_seq;
+ max_offset = view->log_file_expunge_offset;
+
+ mail_transaction_log_view_get_prev_pos(log_view, &file_seq,
+ &file_offset);
+
+ while (mail_transaction_log_view_next(log_view, &hdr, &data) > 0) {
+ mail_transaction_log_view_get_prev_pos(log_view, &file_seq,
+ &file_offset);
+ if (file_offset >= max_offset && file_seq == max_seq)
+ break;
+
+ if ((hdr->type & MAIL_TRANSACTION_SYNC) != 0) {
+ /* ignore changes done by dsync, unless we can get
+ expunged message's GUID from it */
+ if ((hdr->type & MAIL_TRANSACTION_TYPE_MASK) !=
+ MAIL_TRANSACTION_EXPUNGE_GUID)
+ continue;
+ }
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ if (!pvt_scan)
+ log_add_expunge(ctx, data, hdr);
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ if (!pvt_scan)
+ log_add_expunge_guid(ctx, view, data, hdr);
+ break;
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ log_add_flag_update(ctx, data, hdr);
+ break;
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ log_add_keyword_reset(ctx, data, hdr);
+ break;
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ T_BEGIN {
+ log_add_keyword_update(ctx, data, hdr);
+ } T_END;
+ break;
+ case MAIL_TRANSACTION_MODSEQ_UPDATE:
+ log_add_modseq_update(ctx, data, hdr, pvt_scan);
+ break;
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ cur_modseq = mail_transaction_log_view_get_prev_modseq(log_view);
+ log_add_attribute_update(ctx, data, hdr, cur_modseq);
+ break;
+ }
+ }
+
+ if (!pvt_scan) {
+ ctx->last_log_seq = file_seq;
+ ctx->last_log_offset = file_offset;
+ }
+ mail_transaction_log_view_close(&log_view);
+ return ret;
+}
+
+static int
+dsync_mailbox_attribute_cmp(const struct dsync_mailbox_attribute *attr1,
+ const struct dsync_mailbox_attribute *attr2)
+{
+ if (attr1->type < attr2->type)
+ return -1;
+ if (attr1->type > attr2->type)
+ return 1;
+ return strcmp(attr1->key, attr2->key);
+}
+
+static unsigned int
+dsync_mailbox_attribute_hash(const struct dsync_mailbox_attribute *attr)
+{
+ return str_hash(attr->key) ^ attr->type;
+}
+
+int dsync_transaction_log_scan_init(struct mail_index_view *view,
+ struct mail_index_view *pvt_view,
+ uint32_t highest_wanted_uid,
+ uint64_t modseq, uint64_t pvt_modseq,
+ struct dsync_transaction_log_scan **scan_r,
+ bool *pvt_too_old_r)
+{
+ struct dsync_transaction_log_scan *ctx;
+ pool_t pool;
+ int ret, ret2;
+
+ *pvt_too_old_r = FALSE;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"dsync transaction log scan",
+ 10240);
+ ctx = p_new(pool, struct dsync_transaction_log_scan, 1);
+ ctx->pool = pool;
+ hash_table_create_direct(&ctx->changes, pool, 0);
+ hash_table_create(&ctx->attr_changes, pool, 0,
+ dsync_mailbox_attribute_hash,
+ dsync_mailbox_attribute_cmp);
+ ctx->view = view;
+ ctx->highest_wanted_uid = highest_wanted_uid;
+
+ if ((ret = dsync_log_scan(ctx, view, modseq, FALSE)) < 0)
+ return -1;
+ if (pvt_view != NULL) {
+ if ((ret2 = dsync_log_scan(ctx, pvt_view, pvt_modseq, TRUE)) < 0)
+ return -1;
+ if (ret2 == 0) {
+ ret = 0;
+ *pvt_too_old_r = TRUE;
+ }
+ }
+
+ *scan_r = ctx;
+ return ret;
+}
+
+HASH_TABLE_TYPE(dsync_uid_mail_change)
+dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan)
+{
+ return scan->changes;
+}
+
+HASH_TABLE_TYPE(dsync_attr_change)
+dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan)
+{
+ return scan->attr_changes;
+}
+
+bool
+dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan)
+{
+ return scan->returned_all_changes;
+}
+
+struct dsync_mail_change *
+dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan *scan,
+ uint32_t uid)
+{
+ struct mail_transaction_log_view *log_view;
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ const char *reason;
+ bool reset, found = FALSE;
+
+ i_assert(uid > 0);
+
+ if (scan->highest_wanted_uid < uid)
+ scan->highest_wanted_uid = uid;
+
+ log_view = mail_transaction_log_view_open(scan->view->index->log);
+ if (mail_transaction_log_view_set(log_view,
+ scan->last_log_seq,
+ scan->last_log_offset,
+ (uint32_t)-1, UOFF_T_MAX,
+ &reset, &reason) > 0) {
+ while (!found &&
+ mail_transaction_log_view_next(log_view, &hdr, &data) > 0) {
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ if (log_add_expunge_uid(scan, data, hdr, uid))
+ found = TRUE;
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ if (log_add_expunge_guid_uid(scan, data, hdr, uid))
+ found = TRUE;
+ break;
+ }
+ }
+ }
+ mail_transaction_log_view_close(&log_view);
+
+ return !found ? NULL :
+ hash_table_lookup(scan->changes, POINTER_CAST(uid));
+}
+
+void dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan **_scan)
+{
+ struct dsync_transaction_log_scan *scan = *_scan;
+
+ *_scan = NULL;
+
+ hash_table_destroy(&scan->changes);
+ hash_table_destroy(&scan->attr_changes);
+ pool_unref(&scan->pool);
+}
diff --git a/src/doveadm/dsync/dsync-transaction-log-scan.h b/src/doveadm/dsync/dsync-transaction-log-scan.h
new file mode 100644
index 0000000..458b775
--- /dev/null
+++ b/src/doveadm/dsync/dsync-transaction-log-scan.h
@@ -0,0 +1,32 @@
+#ifndef DSYNC_TRANSACTION_LOG_SCAN_H
+#define DSYNC_TRANSACTION_LOG_SCAN_H
+
+HASH_TABLE_DEFINE_TYPE(dsync_uid_mail_change,
+ void *, struct dsync_mail_change *);
+HASH_TABLE_DEFINE_TYPE(dsync_attr_change,
+ struct dsync_mailbox_attribute *,
+ struct dsync_mailbox_attribute *);
+
+struct mail_index_view;
+struct dsync_transaction_log_scan;
+
+int dsync_transaction_log_scan_init(struct mail_index_view *view,
+ struct mail_index_view *pvt_view,
+ uint32_t highest_wanted_uid,
+ uint64_t modseq, uint64_t pvt_modseq,
+ struct dsync_transaction_log_scan **scan_r,
+ bool *pvt_too_old_r);
+HASH_TABLE_TYPE(dsync_uid_mail_change)
+dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan);
+HASH_TABLE_TYPE(dsync_attr_change)
+dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan);
+/* Returns TRUE if the entire transaction log was scanned */
+bool dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan);
+/* If the given UID has been expunged after the initial log scan, create/update
+ a change record for it and return it. */
+struct dsync_mail_change *
+dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan *scan,
+ uint32_t uid);
+void dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan **scan);
+
+#endif
diff --git a/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c b/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c
new file mode 100644
index 0000000..b068137
--- /dev/null
+++ b/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c
@@ -0,0 +1,781 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sha1.h"
+#include "str.h"
+#include "mailbox-list-private.h"
+#include "dsync-mailbox-tree-private.h"
+#include "test-common.h"
+
+#include <stdio.h>
+
+#define MAX_DEPTH 4
+#define TEST_NAMESPACE_NAME "INBOX"
+
+static struct mail_namespace inbox_namespace = {
+ .prefix = TEST_NAMESPACE_NAME"/",
+ .prefix_len = sizeof(TEST_NAMESPACE_NAME)-1 + 1
+};
+
+char mail_namespace_get_sep(struct mail_namespace *ns ATTR_UNUSED)
+{
+ return '/';
+}
+
+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)));
+}
+
+void mailbox_list_name_unescape(const char **name ATTR_UNUSED,
+ char escape_char ATTR_UNUSED)
+{
+}
+
+void mailbox_list_name_escape(const char *name,
+ const char *escape_chars ATTR_UNUSED,
+ string_t *dest)
+{
+ str_append(dest, name);
+}
+
+static struct dsync_mailbox_node *
+node_create(struct dsync_mailbox_tree *tree, unsigned int counter,
+ const char *name, unsigned int last_renamed_or_created)
+{
+ struct dsync_mailbox_node *node;
+
+ node = dsync_mailbox_tree_get(tree, name);
+ memcpy(node->mailbox_guid, &counter, sizeof(counter));
+ node->uid_validity = counter;
+ node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ node->last_renamed_or_created = last_renamed_or_created;
+ return node;
+}
+
+static struct dsync_mailbox_node *
+random_node_create(struct dsync_mailbox_tree *tree, unsigned int counter,
+ const char *name)
+{
+ return node_create(tree, counter, name, i_rand_limit(10));
+}
+
+static void nodes_create(struct dsync_mailbox_tree *tree, unsigned int *counter,
+ const char *const *names)
+{
+ for (; *names != NULL; names++) {
+ *counter += 1;
+ node_create(tree, *counter, *names, 0);
+ }
+}
+
+static void nodes_delete(struct dsync_mailbox_tree *tree, unsigned int *counter,
+ const char *const *names)
+{
+ struct dsync_mailbox_node *node;
+
+ for (; *names != NULL; names++) {
+ *counter += 1;
+ node = node_create(tree, *counter, *names, 0);
+ node->existence = DSYNC_MAILBOX_NODE_DELETED;
+ }
+}
+
+static void
+create_random_nodes(struct dsync_mailbox_tree *tree, const char *parent_name,
+ unsigned int depth, unsigned int *counter)
+{
+ unsigned int parent_len, i, nodes_count = i_rand_minmax(1, 3);
+ string_t *str;
+
+ if (depth == MAX_DEPTH)
+ return;
+
+ str = t_str_new(32);
+ if (*parent_name != '\0')
+ str_printfa(str, "%s/", parent_name);
+ parent_len = str_len(str);
+
+ for (i = 0; i < nodes_count; i++) {
+ *counter += 1;
+ str_truncate(str, parent_len);
+ str_printfa(str, "%u.%u", depth, i);
+ random_node_create(tree, *counter, str_c(str));
+ create_random_nodes(tree, str_c(str), depth+1, counter);
+ }
+}
+
+static struct dsync_mailbox_tree *create_random_tree(void)
+{
+ struct dsync_mailbox_tree *tree;
+ unsigned int counter = 0;
+
+ tree = dsync_mailbox_tree_init('/', '\0', '_');
+ create_random_nodes(tree, "", 0, &counter);
+ return tree;
+}
+
+static void test_tree_nodes_fixup(struct dsync_mailbox_node **pos,
+ unsigned int *newguid_counter)
+{
+ struct dsync_mailbox_node *node;
+
+ for (node = *pos; node != NULL; node = node->next) {
+ if (node->sync_delayed_guid_change) {
+ /* the real code will pick one of the GUIDs.
+ we don't really care which one gets picked, so we'll
+ just change them to the same new one */
+ memcpy(node->mailbox_guid, newguid_counter,
+ sizeof(*newguid_counter));
+ node->uid_validity = *newguid_counter;
+ *newguid_counter += 1;
+ }
+ if (node->existence == DSYNC_MAILBOX_NODE_DELETED)
+ node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT;
+ test_tree_nodes_fixup(&node->first_child, newguid_counter);
+ if (node->existence != DSYNC_MAILBOX_NODE_EXISTS &&
+ node->first_child == NULL) {
+ /* nonexistent node, drop it */
+ *pos = node->next;
+ } else {
+ pos = &node->next;
+ }
+ }
+}
+
+static void test_tree_fixup(struct dsync_mailbox_tree *tree)
+{
+ unsigned int newguid_counter = INT_MAX;
+
+ test_tree_nodes_fixup(&tree->root.first_child, &newguid_counter);
+}
+
+static void nodes_dump(const struct dsync_mailbox_node *node, unsigned int depth)
+{
+ unsigned int i;
+
+ for (; node != NULL; node = node->next) {
+ for (i = 0; i < depth; i++) printf(" ");
+ printf("%-*s guid:%.5s uidv:%u %d%d %ld\n", 40-depth, node->name,
+ guid_128_to_string(node->mailbox_guid), node->uid_validity,
+ node->existence, node->subscribed ? 1 : 0,
+ (long)node->last_renamed_or_created);
+ nodes_dump(node->first_child, depth+1);
+ }
+}
+
+static void trees_dump(struct dsync_mailbox_tree *tree1,
+ struct dsync_mailbox_tree *tree2)
+{
+ printf("tree1:\n");
+ nodes_dump(tree1->root.first_child, 1);
+ printf("tree2:\n");
+ nodes_dump(tree2->root.first_child, 1);
+}
+
+static void test_trees_nofree(struct dsync_mailbox_tree *tree1,
+ struct dsync_mailbox_tree **_tree2)
+{
+ struct dsync_mailbox_tree *tree2 = *_tree2;
+ struct dsync_mailbox_tree *orig_tree1, *orig_tree2;
+ struct dsync_mailbox_tree_sync_ctx *ctx;
+ struct dsync_mailbox_node *dup_node1, *dup_node2;
+
+ orig_tree1 = dsync_mailbox_tree_dup(tree1);
+ orig_tree2 = dsync_mailbox_tree_dup(tree2);
+
+ /* test tree1 -> tree2 */
+ dsync_mailbox_tree_build_guid_hash(tree1, &dup_node1, &dup_node2);
+ dsync_mailbox_tree_build_guid_hash(tree2, &dup_node1, &dup_node2);
+ ctx = dsync_mailbox_trees_sync_init(tree1, tree2,
+ DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY,
+ DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG);
+ while (dsync_mailbox_trees_sync_next(ctx) != NULL) {
+ }
+ dsync_mailbox_trees_sync_deinit(&ctx);
+ test_tree_fixup(tree1);
+ test_tree_fixup(tree2);
+ if (!dsync_mailbox_trees_equal(tree1, tree2)) {
+ test_assert(FALSE);
+ trees_dump(tree1, tree2);
+ }
+
+ /* test tree2 -> tree1 */
+ dsync_mailbox_tree_build_guid_hash(orig_tree1, &dup_node1, &dup_node2);
+ dsync_mailbox_tree_build_guid_hash(orig_tree2, &dup_node1, &dup_node2);
+ ctx = dsync_mailbox_trees_sync_init(orig_tree2, orig_tree1,
+ DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY, 0);
+ while (dsync_mailbox_trees_sync_next(ctx) != NULL) {
+ }
+ dsync_mailbox_trees_sync_deinit(&ctx);
+ test_tree_fixup(orig_tree1);
+ test_tree_fixup(orig_tree2);
+ if (!dsync_mailbox_trees_equal(orig_tree1, orig_tree2)) {
+ test_assert(FALSE);
+ trees_dump(orig_tree1, orig_tree2);
+ }
+
+ /* make sure both directions produced equal trees */
+ if (!dsync_mailbox_trees_equal(tree1, orig_tree1)) {
+ test_assert(FALSE);
+ trees_dump(tree1, orig_tree1);
+ }
+
+ dsync_mailbox_tree_deinit(_tree2);
+ dsync_mailbox_tree_deinit(&orig_tree1);
+ dsync_mailbox_tree_deinit(&orig_tree2);
+}
+
+static void
+test_tree_nodes_add_namespace(struct dsync_mailbox_node *node,
+ struct mail_namespace *ns)
+{
+ for (; node != NULL; node = node->next) {
+ node->ns = ns;
+ test_tree_nodes_add_namespace(node->first_child, ns);
+ }
+}
+
+static void
+test_tree_add_namespace(struct dsync_mailbox_tree *tree,
+ struct mail_namespace *ns)
+{
+ struct dsync_mailbox_node *node, *n;
+
+ node = dsync_mailbox_tree_get(tree, TEST_NAMESPACE_NAME);
+ node->existence = DSYNC_MAILBOX_NODE_EXISTS;
+ i_assert(tree->root.first_child == node);
+ i_assert(node->first_child == NULL);
+ node->first_child = node->next;
+ for (n = node->first_child; n != NULL; n = n->next)
+ n->parent = node;
+ node->next = NULL;
+
+ test_tree_nodes_add_namespace(&tree->root, ns);
+}
+
+static void test_trees(struct dsync_mailbox_tree *tree1,
+ struct dsync_mailbox_tree *tree2)
+{
+ struct dsync_mailbox_tree *tree1_dup, *tree2_dup;
+
+ tree1_dup = dsync_mailbox_tree_dup(tree1);
+ tree2_dup = dsync_mailbox_tree_dup(tree2);
+
+ /* test without namespace prefix */
+ test_trees_nofree(tree1, &tree2);
+ dsync_mailbox_tree_deinit(&tree1);
+
+ /* test with namespace prefix */
+ test_tree_add_namespace(tree1_dup, &inbox_namespace);
+ test_tree_add_namespace(tree2_dup, &inbox_namespace);
+ test_trees_nofree(tree1_dup, &tree2_dup);
+ dsync_mailbox_tree_deinit(&tree1_dup);
+}
+
+static void test_dsync_mailbox_tree_sync_creates(void)
+{
+ static const char *common_nodes[] = { "foo", "foo/bar", NULL };
+ static const char *create1_nodes[] = { "bar", "foo/baz", NULL };
+ static const char *create2_nodes[] = { "foo/xyz", "foo/bar/3", NULL };
+ struct dsync_mailbox_tree *tree1, *tree2;
+ unsigned int counter = 0;
+
+ test_begin("dsync mailbox tree sync creates");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ nodes_create(tree1, &counter, common_nodes);
+ tree2 = dsync_mailbox_tree_dup(tree1);
+ nodes_create(tree1, &counter, create1_nodes);
+ nodes_create(tree2, &counter, create2_nodes);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_deletes(void)
+{
+ static const char *common_nodes[] = { "1", "2", "3", "2/s1", "2/s2", "x/y", NULL };
+ static const char *delete1_nodes[] = { "1", "2", NULL };
+ static const char *delete2_nodes[] = { "2/s1", "x/y", NULL };
+ struct dsync_mailbox_tree *tree1, *tree2;
+ unsigned int counter = 0;
+
+ test_begin("dsync mailbox tree sync deletes");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ nodes_create(tree1, &counter, common_nodes);
+ tree2 = dsync_mailbox_tree_dup(tree1);
+ nodes_delete(tree1, &counter, delete1_nodes);
+ nodes_delete(tree2, &counter, delete2_nodes);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames1(void)
+{
+ static const char *common_nodes[] = { "1", "2", "3", "2/s1", "2/s2", "x/y", "3/s3", NULL };
+ struct dsync_mailbox_tree *tree1, *tree2;
+ struct dsync_mailbox_node *node;
+ unsigned int counter = 0;
+
+ test_begin("dsync mailbox tree sync renames 1");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ nodes_create(tree1, &counter, common_nodes);
+ tree2 = dsync_mailbox_tree_dup(tree1);
+
+ node = dsync_mailbox_tree_get(tree1, "1");
+ node->name = "a";
+ node->last_renamed_or_created = 1000;
+ node = dsync_mailbox_tree_get(tree2, "2");
+ node->name = "b";
+ node->last_renamed_or_created = 1000;
+
+ node = dsync_mailbox_tree_get(tree1, "3/s3");
+ node->name = "z";
+ node->last_renamed_or_created = 1000;
+ dsync_mailbox_tree_node_detach(node);
+ dsync_mailbox_tree_node_attach(node, &tree1->root);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames2(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 2");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1", 1);
+ node_create(tree1, 2, "0/1/2", 3);
+
+ node_create(tree2, 1, "0", 0);
+ node_create(tree2, 2, "0/1/2", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames3(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 3");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/2", 1);
+ node_create(tree1, 2, "0/3", 1);
+
+ node_create(tree2, 1, "0/4/5", 0);
+ node_create(tree2, 2, "1", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames4(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 4");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/b", 0);
+ node_create(tree1, 2, "c", 2);
+
+ node_create(tree2, 2, "0/a", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames5(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 5");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "b", 0);
+ node_create(tree1, 2, "c", 2);
+
+ node_create(tree2, 2, "0/a", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames6(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 6");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1", 0);
+ node_create(tree1, 2, "0/2", 1);
+
+ node_create(tree2, 1, "0", 1);
+ node_create(tree2, 2, "0/3", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames7(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 7");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/2", 0);
+ node_create(tree2, 1, "1/2", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames8(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 8");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1", 0);
+ node_create(tree1, 2, "0/2", 1);
+
+ node_create(tree2, 1, "0", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames9(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 9");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1/2", 0);
+ node_create(tree1, 2, "0/3", 1);
+
+ node_create(tree2, 1, "0", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames10(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 10");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1", 0);
+ node_create(tree1, 3, "0/2/3", 0);
+
+ node_create(tree2, 1, "0", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames11(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 11");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/1", 2);
+ node_create(tree1, 0, "0/1/2", 0);
+
+ node_create(tree2, 1, "0", 1);
+ node_create(tree2, 0, "0/1/2", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames12(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 12");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/2", 0);
+ node_create(tree1, 2, "1", 0);
+ node_create(tree1, 3, "1/4", 0);
+ node_create(tree1, 4, "1/4/5", 1);
+
+ node_create(tree2, 1, "1", 2);
+ node_create(tree2, 2, "1/4", 3);
+ node_create(tree2, 3, "1/4/6", 4);
+ node_create(tree2, 4, "1/3", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames13(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 13");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 4, "0.0/1.0/2.1", 0);
+ node_create(tree1, 5, "0.1", 2);
+ node_create(tree1, 6, "0.1/1.0", 2);
+ node_create(tree1, 7, "0.1/1.0/2.0", 8);
+
+ node_create(tree2, 5, "0.1/1.0", 5);
+ node_create(tree2, 6, "0.1/1.0/2.0", 8);
+ node_create(tree2, 7, "0.1/1.1", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames14(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 14");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "1", 0);
+ node_create(tree1, 2, "1/2", 0);
+ node_create(tree1, 3, "1/2/4", 1);
+
+ node_create(tree2, 1, "1/2", 3);
+ node_create(tree2, 2, "1/2/5", 4);
+ node_create(tree2, 3, "1/2/4", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames15(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 15");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "1", 0);
+ node_create(tree2, 2, "1", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames16(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 16");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "1/2", 4);
+ node_create(tree1, 2, "1", 2);
+
+ node_create(tree2, 1, "2", 1);
+ node_create(tree2, 2, "1/2", 3);
+ node_create(tree2, 3, "1", 5);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames17(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 17");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "1", 1);
+
+ node_create(tree2, 1, "1/2", 0);
+ node_create(tree2, 2, "1", 2);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames18(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 18");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 2, "a", 5);
+ node_create(tree1, 4, "a/c", 2);
+ node_create(tree1, 5, "b", 6);
+
+ node_create(tree2, 1, "a", 7);
+ node_create(tree2, 2, "b", 3);
+ node_create(tree2, 3, "b/c", 4);
+ node_create(tree2, 4, "d", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames19(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 19");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "0/2/1", 1);
+ node_create(tree1, 2, "0/4", 3);
+ node_create(tree1, 3, "0/2", 2);
+
+ node_create(tree2, 1, "1", 0);
+ node_create(tree2, 2, "1/3", 4);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames20(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 20");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "1", 0);
+ node_create(tree1, 2, "0", 0);
+ node_create(tree1, 3, "0/2", 0);
+ /* rename 0 -> 1/0 */
+ node_create(tree2, 1, "1", 0);
+ node_create(tree2, 2, "1/0", 1);
+ node_create(tree2, 3, "1/0/2", 0);
+
+ test_trees_nofree(tree1, &tree2);
+ test_assert(tree1->root.first_child != NULL &&
+ tree1->root.first_child->next == NULL);
+ dsync_mailbox_tree_deinit(&tree1);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_renames21(void)
+{
+#if 0
+ /* FIXME: we can't currently test this without crashing */
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 21");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 1, "INBOX", 0);
+ node_create(tree1, 2, "foo", 0);
+ /* swap INBOX and foo - the INBOX name is important since it's
+ treated specially */
+ node_create(tree2, 1, "foo", 0);
+ node_create(tree2, 2, "INBOX", 1);
+
+ test_trees(tree1, tree2);
+ test_end();
+#endif
+}
+
+static void test_dsync_mailbox_tree_sync_renames22(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync renames 22");
+ tree1 = dsync_mailbox_tree_init('/', '\0', '_');
+ tree2 = dsync_mailbox_tree_init('/', '\0', '_');
+
+ node_create(tree1, 3, "p/a", 0);
+ node_create(tree1, 0, "p/2", 0);
+ node_create(tree1, 5, "p/2/h", 0);
+
+ node_create(tree2, 4, "p/1/z", 0);
+ node_create(tree2, 1, "p/2", 0);
+ node_create(tree2, 2, "p/2/a", 0);
+ node_create(tree2, 5, "p/2/y", 0);
+ node_create(tree2, 3, "p/3", 0);
+
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+static void test_dsync_mailbox_tree_sync_random(void)
+{
+ struct dsync_mailbox_tree *tree1, *tree2;
+
+ test_begin("dsync mailbox tree sync random");
+ tree1 = create_random_tree();
+ tree2 = create_random_tree();
+ test_trees(tree1, tree2);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_dsync_mailbox_tree_sync_creates,
+ test_dsync_mailbox_tree_sync_deletes,
+ test_dsync_mailbox_tree_sync_renames1,
+ test_dsync_mailbox_tree_sync_renames2,
+ test_dsync_mailbox_tree_sync_renames3,
+ test_dsync_mailbox_tree_sync_renames4,
+ test_dsync_mailbox_tree_sync_renames5,
+ test_dsync_mailbox_tree_sync_renames6,
+ test_dsync_mailbox_tree_sync_renames7,
+ test_dsync_mailbox_tree_sync_renames8,
+ test_dsync_mailbox_tree_sync_renames9,
+ test_dsync_mailbox_tree_sync_renames10,
+ test_dsync_mailbox_tree_sync_renames11,
+ test_dsync_mailbox_tree_sync_renames12,
+ test_dsync_mailbox_tree_sync_renames13,
+ test_dsync_mailbox_tree_sync_renames14,
+ test_dsync_mailbox_tree_sync_renames15,
+ test_dsync_mailbox_tree_sync_renames16,
+ test_dsync_mailbox_tree_sync_renames17,
+ test_dsync_mailbox_tree_sync_renames18,
+ test_dsync_mailbox_tree_sync_renames19,
+ test_dsync_mailbox_tree_sync_renames20,
+ test_dsync_mailbox_tree_sync_renames21,
+ test_dsync_mailbox_tree_sync_renames22,
+ test_dsync_mailbox_tree_sync_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/doveadm/main.c b/src/doveadm/main.c
new file mode 100644
index 0000000..9635b88
--- /dev/null
+++ b/src/doveadm/main.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "process-title.h"
+#include "master-service.h"
+#include "settings-parser.h"
+#include "dict.h"
+#include "doveadm.h"
+#include "client-connection.h"
+#include "client-connection-private.h"
+#include "doveadm-settings.h"
+#include "doveadm-dump.h"
+#include "doveadm-mail.h"
+#include "doveadm-print-private.h"
+#include "doveadm-server.h"
+#include "ostream.h"
+
+const struct doveadm_print_vfuncs *doveadm_print_vfuncs_all[] = {
+ &doveadm_print_server_vfuncs,
+ &doveadm_print_json_vfuncs,
+ NULL
+};
+
+struct client_connection *doveadm_client;
+int doveadm_exit_code = 0;
+
+static void doveadm_die(void)
+{
+ /* do nothing. doveadm connections should be over soon. */
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (doveadm_client != NULL) {
+ i_error("doveadm server can handle only a single client");
+ return;
+ }
+
+ master_service_client_connection_accept(conn);
+ if (strcmp(conn->name, "http") == 0) {
+ doveadm_client = client_connection_http_create(conn->fd, conn->ssl);
+ } else {
+ doveadm_client = client_connection_tcp_create(conn->fd, conn->listen_fd,
+ conn->ssl);
+ }
+}
+
+void help_ver2(const struct doveadm_cmd_ver2 *cmd)
+{
+ i_fatal("Client sent invalid command. Usage: %s %s",
+ cmd->name, cmd->usage);
+}
+
+static void main_preinit(void)
+{
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_init(void)
+{
+ doveadm_server = TRUE;
+
+ doveadm_settings_init();
+ doveadm_cmds_init();
+ doveadm_register_auth_server_commands();
+ doveadm_dump_init();
+ doveadm_mail_init();
+ dict_drivers_register_builtin();
+ doveadm_load_modules();
+ /* read settings only after loading doveadm plugins, which
+ may modify what settings are read */
+ doveadm_read_settings();
+ /* Load mail_plugins */
+ doveadm_mail_init_finish();
+ /* kludgy: Load the rest of the doveadm plugins after
+ mail_plugins have been loaded. */
+ doveadm_load_modules();
+
+ doveadm_server_init();
+ if (doveadm_verbose_proctitle)
+ process_title_set("[idling]");
+}
+
+static void main_deinit(void)
+{
+ doveadm_server_deinit();
+ doveadm_mail_deinit();
+ doveadm_dump_deinit();
+ doveadm_unload_modules();
+ dict_drivers_unregister_builtin();
+ doveadm_print_deinit();
+ doveadm_cmds_deinit();
+ doveadm_settings_deinit();
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN |
+ MASTER_SERVICE_FLAG_HAVE_STARTTLS;
+ int c;
+
+ master_service = master_service_init("doveadm", service_flags,
+ &argc, &argv, "D");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ doveadm_debug = TRUE;
+ doveadm_verbose = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ master_service_init_log(master_service);
+ main_preinit();
+ master_service_set_die_callback(master_service, doveadm_die);
+
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/doveadm/server-connection.c b/src/doveadm/server-connection.c
new file mode 100644
index 0000000..a0e6500
--- /dev/null
+++ b/src/doveadm/server-connection.c
@@ -0,0 +1,691 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "istream-multiplex.h"
+#include "ostream.h"
+#include "ostream-dot.h"
+#include "str.h"
+#include "strescape.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "settings-parser.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+#include "doveadm-util.h"
+#include "doveadm-server.h"
+#include "doveadm-settings.h"
+#include "server-connection.h"
+
+#include <sysexits.h>
+#include <unistd.h>
+
+#define DOVEADM_LOG_CHANNEL_ID 'L'
+
+#define MAX_INBUF_SIZE (1024*32)
+
+enum server_reply_state {
+ SERVER_REPLY_STATE_DONE = 0,
+ SERVER_REPLY_STATE_PRINT,
+ SERVER_REPLY_STATE_RET
+};
+
+struct server_connection {
+ struct doveadm_server *server;
+
+ pool_t pool;
+ struct doveadm_settings *set;
+
+ int fd;
+ unsigned int minor;
+
+ struct io *io;
+ struct io *io_log;
+ struct istream *input;
+ struct istream *log_input;
+ struct ostream *output;
+ struct ssl_iostream *ssl_iostream;
+
+ struct istream *cmd_input;
+ struct ostream *cmd_output;
+ const char *delayed_cmd;
+ server_cmd_callback_t *callback;
+ void *context;
+
+ enum server_reply_state state;
+
+ bool version_received:1;
+ bool authenticate_sent:1;
+ bool authenticated:1;
+ bool streaming:1;
+ bool ssl_done:1;
+};
+
+static struct server_connection *printing_conn = NULL;
+static ARRAY(struct doveadm_server *) print_pending_servers = ARRAY_INIT;
+
+static void server_connection_input(struct server_connection *conn);
+static bool server_connection_input_one(struct server_connection *conn);
+static int server_connection_init_ssl(struct server_connection *conn,
+ const char **error_r);
+
+static void server_set_print_pending(struct doveadm_server *server)
+{
+ struct doveadm_server *pending_server;
+
+ if (!array_is_created(&print_pending_servers))
+ i_array_init(&print_pending_servers, 16);
+ array_foreach_elem(&print_pending_servers, pending_server) {
+ if (pending_server == server)
+ return;
+ }
+ array_push_back(&print_pending_servers, &server);
+}
+
+static void server_print_connection_released(struct doveadm_server *server)
+{
+ struct server_connection *const *conns;
+ unsigned int i, count;
+
+ conns = array_get(&server->connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i]->io != NULL)
+ continue;
+
+ conns[i]->io = io_add(conns[i]->fd, IO_READ,
+ server_connection_input, conns[i]);
+ io_set_pending(conns[i]->io);
+ }
+}
+
+static void print_connection_released(void)
+{
+ struct doveadm_server *server;
+
+ printing_conn = NULL;
+ if (!array_is_created(&print_pending_servers))
+ return;
+
+ array_foreach_elem(&print_pending_servers, server)
+ server_print_connection_released(server);
+ array_free(&print_pending_servers);
+}
+
+static int server_connection_send_cmd_input_more(struct server_connection *conn)
+{
+ enum ostream_send_istream_result res;
+ int ret = -1;
+
+ /* ostream-dot writes only up to max buffer size, so keep it non-zero */
+ o_stream_set_max_buffer_size(conn->cmd_output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(conn->cmd_output, conn->cmd_input);
+ o_stream_set_max_buffer_size(conn->cmd_output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(conn->cmd_input),
+ i_stream_get_error(conn->cmd_input));
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_error("write(%s) failed: %s",
+ o_stream_get_name(conn->cmd_output),
+ o_stream_get_error(conn->cmd_output));
+ break;
+ }
+ if (res == OSTREAM_SEND_ISTREAM_RESULT_FINISHED) {
+ if ((ret = o_stream_finish(conn->cmd_output)) == 0)
+ return 0;
+ else if (ret < 0) {
+ i_error("write(%s) failed: %s",
+ o_stream_get_name(conn->cmd_output),
+ o_stream_get_error(conn->cmd_output));
+ }
+ }
+
+ i_stream_destroy(&conn->cmd_input);
+ o_stream_destroy(&conn->cmd_output);
+ return ret;
+}
+
+static void server_connection_send_cmd_input(struct server_connection *conn)
+{
+ if (conn->cmd_input == NULL)
+ return;
+
+ conn->cmd_output = o_stream_create_dot(conn->output, TRUE);
+ (void)server_connection_send_cmd_input_more(conn);
+}
+
+static int server_connection_output(struct server_connection *conn)
+{
+ int ret;
+
+ ret = o_stream_flush(conn->output);
+ if (ret > 0 && conn->cmd_input != NULL && conn->delayed_cmd == NULL)
+ ret = server_connection_send_cmd_input_more(conn);
+ if (ret < 0)
+ server_connection_destroy(&conn);
+ return ret;
+}
+
+static void
+server_connection_callback(struct server_connection *conn,
+ int exit_code, const char *error)
+{
+ server_cmd_callback_t *callback = conn->callback;
+
+ conn->callback = NULL;
+ callback(exit_code, error, conn->context);
+}
+
+static void stream_data(string_t *str, const unsigned char *data, size_t size)
+{
+ str_truncate(str, 0);
+ str_append_tabunescaped(str, data, size);
+ doveadm_print_stream(str->data, str->used);
+}
+
+static void server_flush_field(struct server_connection *conn, string_t *str,
+ const unsigned char *data, size_t size)
+{
+ if (conn->streaming) {
+ conn->streaming = FALSE;
+ if (size > 0)
+ stream_data(str, data, size);
+ doveadm_print_stream("", 0);
+ } else {
+ str_truncate(str, 0);
+ str_append_tabunescaped(str, data, size);
+ doveadm_print(str_c(str));
+ }
+}
+
+static void
+server_handle_input(struct server_connection *conn,
+ const unsigned char *data, size_t size)
+{
+ string_t *str;
+ size_t i, start;
+
+ if (printing_conn == conn) {
+ /* continue printing */
+ } else if (printing_conn == NULL) {
+ printing_conn = conn;
+ } else {
+ /* someone else is printing. don't continue until it
+ goes away */
+ server_set_print_pending(conn->server);
+ io_remove(&conn->io);
+ return;
+ }
+
+ if (data[size-1] == '\001') {
+ /* last character is an escape */
+ size--;
+ }
+
+ str = t_str_new(128);
+ for (i = start = 0; i < size; i++) {
+ if (data[i] == '\n') {
+ if (i != start) {
+ i_error("doveadm server sent broken print input");
+ server_connection_destroy(&conn);
+ return;
+ }
+ conn->state = SERVER_REPLY_STATE_RET;
+ i_stream_skip(conn->input, i + 1);
+
+ print_connection_released();
+ return;
+ }
+ if (data[i] == '\t') {
+ server_flush_field(conn, str, data + start, i - start);
+ start = i + 1;
+ }
+ }
+ if (start != size) {
+ conn->streaming = TRUE;
+ stream_data(str, data + start, size - start);
+ }
+ i_stream_skip(conn->input, size);
+}
+
+static void server_connection_authenticated(struct server_connection *conn)
+{
+ conn->authenticated = TRUE;
+ if (conn->delayed_cmd != NULL) {
+ o_stream_nsend_str(conn->output, conn->delayed_cmd);
+ conn->delayed_cmd = NULL;
+ server_connection_send_cmd_input(conn);
+ }
+}
+
+static int
+server_connection_authenticate(struct server_connection *conn)
+{
+ string_t *plain = t_str_new(128);
+ string_t *cmd = t_str_new(128);
+
+ if (*conn->set->doveadm_password == '\0') {
+ i_error("doveadm_password not set, "
+ "can't authenticate to remote server");
+ return -1;
+ }
+
+ str_append_c(plain, '\0');
+ str_append(plain, conn->set->doveadm_username);
+ str_append_c(plain, '\0');
+ str_append(plain, conn->set->doveadm_password);
+
+ str_append(cmd, "PLAIN\t");
+ base64_encode(plain->data, plain->used, cmd);
+ str_append_c(cmd, '\n');
+
+ o_stream_nsend(conn->output, cmd->data, cmd->used);
+ conn->authenticate_sent = TRUE;
+ return 0;
+}
+
+static void server_log_disconnect_error(struct server_connection *conn)
+{
+ const char *error;
+
+ error = conn->ssl_iostream == NULL ? NULL :
+ ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (error == NULL) {
+ error = conn->input->stream_errno == 0 ? "EOF" :
+ strerror(conn->input->stream_errno);
+ }
+ i_error("doveadm server disconnected before handshake: %s", error);
+}
+
+static void server_connection_print_log(struct server_connection *conn)
+{
+ const char *line;
+ struct failure_context ctx;
+ i_zero(&ctx);
+
+ while((line = i_stream_read_next_line(conn->log_input))!=NULL) {
+ /* skip empty lines */
+ if (*line == '\0') continue;
+
+ if (!doveadm_log_type_from_char(line[0], &ctx.type))
+ i_warning("Doveadm server sent invalid log type 0x%02x",
+ line[0]);
+ line++;
+ i_log_type(&ctx, "remote(%s): %s", conn->server->name, line);
+ }
+}
+
+static void server_connection_start_multiplex(struct server_connection *conn)
+{
+ struct istream *is = conn->input;
+ conn->input = i_stream_create_multiplex(is, MAX_INBUF_SIZE);
+ i_stream_unref(&is);
+ io_remove(&conn->io);
+ conn->io = io_add_istream(conn->input, server_connection_input, conn);
+ conn->log_input = i_stream_multiplex_add_channel(conn->input, DOVEADM_LOG_CHANNEL_ID);
+ conn->io_log = io_add_istream(conn->log_input, server_connection_print_log, conn);
+ i_stream_set_return_partial_line(conn->log_input, TRUE);
+}
+
+static void server_connection_input(struct server_connection *conn)
+{
+ const char *line;
+ const char *error;
+
+ if (i_stream_read(conn->input) < 0) {
+ /* disconnected */
+ server_log_disconnect_error(conn);
+ server_connection_destroy(&conn);
+ return;
+ }
+
+ while (!conn->authenticated) {
+ if ((line = i_stream_next_line(conn->input)) == NULL) {
+ if (conn->input->eof) {
+ /* we'll also get here if the line is too long */
+ server_log_disconnect_error(conn);
+ server_connection_destroy(&conn);
+ }
+ return;
+ }
+ /* Allow VERSION before or after the "+" or "-" line,
+ because v2.2.33 sent the version after and newer
+ versions send before. */
+ if (!conn->version_received &&
+ str_begins(line, "VERSION\t")) {
+ if (!version_string_verify_full(line, "doveadm-client",
+ DOVEADM_SERVER_PROTOCOL_VERSION_MAJOR,
+ &conn->minor)) {
+ i_error("doveadm server not compatible with this client"
+ "(mixed old and new binaries?)");
+ server_connection_destroy(&conn);
+ return;
+ }
+ conn->version_received = TRUE;
+ } else if (strcmp(line, "+") == 0) {
+ if (conn->minor > 0)
+ server_connection_start_multiplex(conn);
+ server_connection_authenticated(conn);
+ } else if (strcmp(line, "-") == 0) {
+ if (conn->authenticate_sent) {
+ i_error("doveadm authentication failed (%s)",
+ line+1);
+ server_connection_destroy(&conn);
+ return;
+ }
+ if (!conn->ssl_done &&
+ (conn->server->ssl_flags & PROXY_SSL_FLAG_STARTTLS) != 0) {
+ io_remove(&conn->io);
+ if (conn->minor < 2) {
+ i_error("doveadm STARTTLS failed: Server does not support it");
+ server_connection_destroy(&conn);
+ return;
+ }
+ /* send STARTTLS */
+ o_stream_nsend_str(conn->output, "STARTTLS\n");
+ if (server_connection_init_ssl(conn, &error) < 0) {
+ i_error("doveadm STARTTLS failed: %s", error);
+ server_connection_destroy(&conn);
+ return;
+ }
+ conn->ssl_done = TRUE;
+ conn->io = io_add_istream(conn->input, server_connection_input, conn);
+ }
+ if (server_connection_authenticate(conn) < 0) {
+ server_connection_destroy(&conn);
+ return;
+ }
+ } else {
+ i_error("doveadm server sent invalid handshake: %s",
+ line);
+ server_connection_destroy(&conn);
+ return;
+ }
+ }
+
+ while (server_connection_input_one(conn)) ;
+}
+
+static bool server_connection_input_one(struct server_connection *conn)
+{
+ const unsigned char *data;
+ size_t size;
+ const char *line;
+ int exit_code;
+
+ /* check logs - NOTE: must be before i_stream_get_data() since checking
+ for logs may add data to our channel. */
+ if (conn->log_input != NULL)
+ (void)server_connection_print_log(conn);
+
+ data = i_stream_get_data(conn->input, &size);
+ if (size == 0)
+ return FALSE;
+
+ switch (conn->state) {
+ case SERVER_REPLY_STATE_DONE:
+ i_error("doveadm server sent unexpected input");
+ server_connection_destroy(&conn);
+ return FALSE;
+ case SERVER_REPLY_STATE_PRINT:
+ server_handle_input(conn, data, size);
+ if (conn->state != SERVER_REPLY_STATE_RET)
+ return FALSE;
+ /* fall through */
+ case SERVER_REPLY_STATE_RET:
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return FALSE;
+ if (line[0] == '+')
+ server_connection_callback(conn, 0, "");
+ else if (line[0] == '-') {
+ line++;
+ exit_code = doveadm_str_to_exit_code(line);
+ if (exit_code == DOVEADM_EX_UNKNOWN &&
+ str_to_int(line, &exit_code) < 0) {
+ /* old doveadm-server */
+ exit_code = EX_TEMPFAIL;
+ }
+ server_connection_callback(conn, exit_code, line);
+ } else {
+ i_error("doveadm server sent broken input "
+ "(expected cmd reply): %s", line);
+ server_connection_destroy(&conn);
+ return FALSE;
+ }
+ if (conn->callback == NULL) {
+ /* we're finished, close the connection */
+ server_connection_destroy(&conn);
+ return FALSE;
+ }
+ return TRUE;
+ }
+ i_unreached();
+}
+
+static int server_connection_read_settings(struct server_connection *conn,
+ const char **error_r)
+{
+ const struct setting_parser_info *set_roots[] = {
+ &doveadm_setting_parser_info,
+ NULL
+ };
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const char *error;
+ in_port_t port;
+ void *set;
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.service = "doveadm";
+
+ (void)net_getsockname(conn->fd, &input.local_ip, &port);
+ (void)net_getpeername(conn->fd, &input.remote_ip, &port);
+
+ if (master_service_settings_read(master_service, &input,
+ &output, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Error reading configuration: %s", error);
+ return -1;
+ }
+ set = master_service_settings_get_others(master_service)[0];
+ conn->set = settings_dup(&doveadm_setting_parser_info, set, conn->pool);
+ return 0;
+}
+
+static int server_connection_init_ssl(struct server_connection *conn,
+ const char **error_r)
+{
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ if (conn->server->ssl_flags == 0)
+ return 0;
+
+ doveadm_get_ssl_settings(&ssl_set, pool_datastack_create());
+
+ if ((conn->server->ssl_flags & PROXY_SSL_FLAG_ANY_CERT) != 0)
+ ssl_set.allow_invalid_cert = TRUE;
+ if (ssl_set.allow_invalid_cert)
+ ssl_set.verbose_invalid_cert = TRUE;
+
+ if (conn->server->ssl_ctx == NULL &&
+ ssl_iostream_client_context_cache_get(&ssl_set,
+ &conn->server->ssl_ctx,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL client: %s", error);
+ return -1;
+ }
+
+ if (io_stream_create_ssl_client(conn->server->ssl_ctx,
+ conn->server->hostname, &ssl_set,
+ &conn->input, &conn->output,
+ &conn->ssl_iostream, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL client: %s", error);
+ return -1;
+ }
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ *error_r = t_strdup_printf(
+ "SSL handshake failed: %s",
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+ return 0;
+}
+
+int server_connection_create(struct doveadm_server *server,
+ struct server_connection **conn_r,
+ const char **error_r)
+{
+ const char *target;
+ struct server_connection *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("doveadm server connection", 1024*16);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+ conn->server = server;
+ if (server->ip.family != 0) {
+ (void)net_ipport2str(&server->ip, server->port, &target);
+ } else {
+ target = server->name;
+ }
+ conn->fd = doveadm_connect_with_default_port(target,
+ doveadm_settings->doveadm_port);
+ net_set_nonblock(conn->fd, TRUE);
+ conn->input = i_stream_create_fd(conn->fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_flush_callback(conn->output, server_connection_output, conn);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ i_stream_set_name(conn->input, server->name);
+ o_stream_set_name(conn->output, server->name);
+
+ array_push_back(&conn->server->connections, &conn);
+
+ if (server_connection_read_settings(conn, error_r) < 0 ||
+ ((server->ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0 &&
+ server_connection_init_ssl(conn, error_r) < 0)) {
+ server_connection_destroy(&conn);
+ return -1;
+ }
+ conn->io = io_add_istream(conn->input, server_connection_input, conn);
+
+ conn->state = SERVER_REPLY_STATE_DONE;
+ o_stream_nsend_str(conn->output, DOVEADM_SERVER_PROTOCOL_VERSION_LINE"\n");
+
+ *conn_r = conn;
+ return 0;
+}
+
+void server_connection_destroy(struct server_connection **_conn)
+{
+ struct server_connection *conn = *_conn;
+ struct server_connection *const *conns;
+ const char *error;
+ unsigned int i, count;
+
+ *_conn = NULL;
+
+ conns = array_get(&conn->server->connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i] == conn) {
+ array_delete(&conn->server->connections, i, 1);
+ break;
+ }
+ }
+
+ if (conn->callback != NULL) {
+ error = conn->ssl_iostream == NULL ? NULL :
+ ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (error == NULL) {
+ error = conn->input->stream_errno == 0 ? "EOF" :
+ strerror(conn->input->stream_errno);
+ }
+ server_connection_callback(conn, SERVER_EXIT_CODE_DISCONNECTED,
+ error);
+ }
+ if (printing_conn == conn)
+ print_connection_released();
+
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ i_stream_destroy(&conn->cmd_input);
+ /* close cmd_output after its parent, so the "." isn't sent */
+ o_stream_destroy(&conn->cmd_output);
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ io_remove(&conn->io_log);
+ /* make sure all logs got consumed */
+ if (conn->log_input != NULL)
+ server_connection_print_log(conn);
+ i_stream_unref(&conn->log_input);
+ io_remove(&conn->io);
+ i_close_fd(&conn->fd);
+ pool_unref(&conn->pool);
+}
+
+struct doveadm_server *
+server_connection_get_server(struct server_connection *conn)
+{
+ return conn->server;
+}
+
+void server_connection_cmd(struct server_connection *conn, const char *line,
+ struct istream *cmd_input,
+ server_cmd_callback_t *callback, void *context)
+{
+ i_assert(conn->delayed_cmd == NULL);
+
+ conn->state = SERVER_REPLY_STATE_PRINT;
+ if (cmd_input != NULL) {
+ i_assert(conn->cmd_input == NULL);
+ i_stream_ref(cmd_input);
+ conn->cmd_input = cmd_input;
+ }
+ if (!conn->authenticated)
+ conn->delayed_cmd = p_strdup(conn->pool, line);
+ else {
+ o_stream_nsend_str(conn->output, line);
+ server_connection_send_cmd_input(conn);
+ }
+ conn->callback = callback;
+ conn->context = context;
+}
+
+bool server_connection_is_idle(struct server_connection *conn)
+{
+ return conn->callback == NULL;
+}
+
+void server_connection_extract(struct server_connection *conn,
+ struct istream **istream_r,
+ struct ostream **ostream_r,
+ struct ssl_iostream **ssl_iostream_r)
+{
+ *istream_r = conn->input;
+ *ostream_r = conn->output;
+ *ssl_iostream_r = conn->ssl_iostream;
+
+ conn->input = NULL;
+ conn->output = NULL;
+ conn->ssl_iostream = NULL;
+ io_remove(&conn->io);
+ conn->fd = -1;
+}
diff --git a/src/doveadm/server-connection.h b/src/doveadm/server-connection.h
new file mode 100644
index 0000000..1db30b9
--- /dev/null
+++ b/src/doveadm/server-connection.h
@@ -0,0 +1,35 @@
+#ifndef SERVER_CONNECTION_H
+#define SERVER_CONNECTION_H
+
+#define SERVER_EXIT_CODE_DISCONNECTED 1000
+
+struct doveadm_server;
+struct server_connection;
+struct ssl_iostream;
+
+typedef void server_cmd_callback_t(int exit_code, const char *error,
+ void *context);
+
+int server_connection_create(struct doveadm_server *server,
+ struct server_connection **conn_r,
+ const char **error_r);
+void server_connection_destroy(struct server_connection **conn);
+
+/* Return the server given to create() */
+struct doveadm_server *
+server_connection_get_server(struct server_connection *conn);
+
+void server_connection_cmd(struct server_connection *conn, const char *line,
+ struct istream *cmd_input,
+ server_cmd_callback_t *callback, void *context);
+/* Returns TRUE if no command is being processed */
+bool server_connection_is_idle(struct server_connection *conn);
+
+/* Extract iostreams from connection. Afterwards the server_connection simply
+ waits for itself to be destroyed. */
+void server_connection_extract(struct server_connection *conn,
+ struct istream **istream_r,
+ struct ostream **ostream_r,
+ struct ssl_iostream **ssl_iostream_r);
+
+#endif
diff --git a/src/doveadm/test-doveadm-util.c b/src/doveadm/test-doveadm-util.c
new file mode 100644
index 0000000..aec1b39
--- /dev/null
+++ b/src/doveadm/test-doveadm-util.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "doveadm-settings.h"
+#include "doveadm-util.h"
+
+struct doveadm_settings *doveadm_settings; /* just to avoid linker error */
+
+static void test_i_strccdascmp(void)
+{
+ test_begin("i_strccdascmp()");
+
+ test_assert(i_strccdascmp("", "")==0);
+ test_assert(i_strccdascmp("", "-")!=0);
+ test_assert(i_strccdascmp("-", "")!=0);
+ test_assert(i_strccdascmp("-", "-")==0);
+ test_assert(i_strccdascmp("-\0baz", "-\0bar")==0);
+ test_assert(i_strccdascmp("", "a")!=0);
+ test_assert(i_strccdascmp("a", "")!=0);
+ test_assert(i_strccdascmp("a", "a")==0);
+ test_assert(i_strccdascmp("a-", "a-")==0);
+ test_assert(i_strccdascmp("a-a", "a-a")==0);
+ test_assert(i_strccdascmp("ca", "ba")!=0);
+
+ test_assert(i_strccdascmp("camel case", "camel case")==0);
+ test_assert(i_strccdascmp("camel case", "camel-case")==0);
+ test_assert(i_strccdascmp("camel case", "camelCase")==0);
+
+ test_assert(i_strccdascmp("camel case", "camel-case")==0);
+ test_assert(i_strccdascmp("camel-case", "camel-case")==0);
+ test_assert(i_strccdascmp("camelCase", "camel-case")==0);
+
+ test_assert(i_strccdascmp("camel case", "camel Case")==-i_strccdascmp("camel Case", "camel case"));
+ test_assert(i_strccdascmp("camel-case", "camel Case")==-i_strccdascmp("camel Case", "camel-case"));
+ test_assert(i_strccdascmp("camel dase", "camel case")==-i_strccdascmp("camel case", "camel dase"));
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_i_strccdascmp,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/imap-hibernate/Makefile.am b/src/imap-hibernate/Makefile.am
new file mode 100644
index 0000000..3641c90
--- /dev/null
+++ b/src/imap-hibernate/Makefile.am
@@ -0,0 +1,27 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = imap-hibernate
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-imap \
+ $(BINARY_CFLAGS)
+
+imap_hibernate_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+imap_hibernate_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+imap_hibernate_SOURCES = \
+ imap-client.c \
+ imap-hibernate-client.c \
+ imap-hibernate-settings.c \
+ imap-master-connection.c \
+ main.c
+
+noinst_HEADERS = \
+ imap-client.h \
+ imap-hibernate-client.h \
+ imap-master-connection.h
diff --git a/src/imap-hibernate/Makefile.in b/src/imap-hibernate/Makefile.in
new file mode 100644
index 0000000..920a0b7
--- /dev/null
+++ b/src/imap-hibernate/Makefile.in
@@ -0,0 +1,828 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = imap-hibernate$(EXEEXT)
+subdir = src/imap-hibernate
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_imap_hibernate_OBJECTS = imap-client.$(OBJEXT) \
+ imap-hibernate-client.$(OBJEXT) \
+ imap-hibernate-settings.$(OBJEXT) \
+ imap-master-connection.$(OBJEXT) main.$(OBJEXT)
+imap_hibernate_OBJECTS = $(am_imap_hibernate_OBJECTS)
+am__DEPENDENCIES_1 =
+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-client.Po \
+ ./$(DEPDIR)/imap-hibernate-client.Po \
+ ./$(DEPDIR)/imap-hibernate-settings.Po \
+ ./$(DEPDIR)/imap-master-connection.Po ./$(DEPDIR)/main.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 = $(imap_hibernate_SOURCES)
+DIST_SOURCES = $(imap_hibernate_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-imap \
+ $(BINARY_CFLAGS)
+
+imap_hibernate_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+imap_hibernate_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+imap_hibernate_SOURCES = \
+ imap-client.c \
+ imap-hibernate-client.c \
+ imap-hibernate-settings.c \
+ imap-master-connection.c \
+ main.c
+
+noinst_HEADERS = \
+ imap-client.h \
+ imap-hibernate-client.h \
+ imap-master-connection.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/imap-hibernate/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/imap-hibernate/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+imap-hibernate$(EXEEXT): $(imap_hibernate_OBJECTS) $(imap_hibernate_DEPENDENCIES) $(EXTRA_imap_hibernate_DEPENDENCIES)
+ @rm -f imap-hibernate$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(imap_hibernate_OBJECTS) $(imap_hibernate_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-hibernate-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-hibernate-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-master-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-client.Po
+ -rm -f ./$(DEPDIR)/imap-hibernate-client.Po
+ -rm -f ./$(DEPDIR)/imap-hibernate-settings.Po
+ -rm -f ./$(DEPDIR)/imap-master-connection.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+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-client.Po
+ -rm -f ./$(DEPDIR)/imap-hibernate-client.Po
+ -rm -f ./$(DEPDIR)/imap-hibernate-settings.Po
+ -rm -f ./$(DEPDIR)/imap-master-connection.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c
new file mode 100644
index 0000000..fa5edde
--- /dev/null
+++ b/src/imap-hibernate/imap-client.c
@@ -0,0 +1,810 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "fdpass.h"
+#include "hostpid.h"
+#include "connection.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "priorityq.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "imap-keepalive.h"
+#include "imap-master-connection.h"
+#include "imap-client.h"
+
+#include <unistd.h>
+
+#define IMAP_MASTER_SOCKET_NAME "imap-master"
+
+/* we only need enough for "DONE\r\n<tag> IDLE\r\n" */
+#define IMAP_MAX_INBUF 12 + 1 + 128 /* DONE\r\nIDLE\r\n + ' ' + <tag> */
+#define IMAP_MAX_OUTBUF 1024
+
+/* If client has sent input and we can't recreate imap process in this
+ many seconds, disconnect the client. */
+#define IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS 10
+/* If there's a change notification and we can't recreate imap process in this
+ many seconds, disconnect the client. */
+#define IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS (60*5)
+
+/* How often to try to unhibernate clients. */
+#define IMAP_UNHIBERNATE_RETRY_MSECS 100
+
+#define IMAP_CLIENT_BUFFER_FULL_ERROR "Client output buffer is full"
+#define IMAP_CLIENT_UNHIBERNATE_ERROR "Failed to unhibernate client"
+
+enum imap_client_input_state {
+ IMAP_CLIENT_INPUT_STATE_UNKNOWN,
+ IMAP_CLIENT_INPUT_STATE_BAD,
+ IMAP_CLIENT_INPUT_STATE_DONE_LF,
+ IMAP_CLIENT_INPUT_STATE_DONE_CRLF,
+ IMAP_CLIENT_INPUT_STATE_DONEIDLE
+};
+
+struct imap_client_notify {
+ int fd;
+ struct io *io;
+};
+
+struct imap_client {
+ struct priorityq_item item;
+
+ struct imap_client *prev, *next;
+ pool_t pool;
+ struct event *event;
+ struct imap_client_state state;
+ ARRAY(struct imap_client_notify) notifys;
+
+ time_t move_back_start;
+ struct timeout *to_move_back;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_keepalive;
+ struct imap_master_connection *master_conn;
+ struct ioloop_context *ioloop_ctx;
+ const char *log_prefix;
+ unsigned int next_read_threshold;
+ bool bad_done, idle_done;
+ bool unhibernate_queued;
+ bool input_pending;
+ bool shutdown_fd_on_destroy;
+};
+
+static struct imap_client *imap_clients;
+static struct priorityq *unhibernate_queue;
+static struct timeout *to_unhibernate;
+static const char imap_still_here_text[] = "* OK Still here\r\n";
+
+static struct event_category event_category_imap = {
+ .name = "imap",
+};
+static struct event_category event_category_imap_hibernate = {
+ .name = "imap-hibernate",
+ .parent = &event_category_imap,
+};
+
+static void imap_client_stop(struct imap_client *client);
+void imap_client_destroy(struct imap_client **_client, const char *reason);
+static void imap_client_add_idle_keepalive_timeout(struct imap_client *client);
+static void imap_clients_unhibernate(void *context);
+static void imap_client_stop_notify_listening(struct imap_client *client);
+
+static void imap_client_disconnected(struct imap_client **_client)
+{
+ struct imap_client *client = *_client;
+ const char *reason;
+
+ reason = io_stream_get_disconnect_reason(client->input, client->output);
+ imap_client_destroy(_client, reason);
+}
+
+static void
+imap_client_unhibernate_failed(struct imap_client **_client, const char *error)
+{
+ struct imap_client *client = *_client;
+ struct timeval created;
+ event_get_create_time(client->event, &created);
+
+ struct event_passthrough *e =
+ event_create_passthrough(client->event)->
+ set_name("imap_client_unhibernated")->
+ add_int("hibernation_usecs",
+ timeval_diff_usecs(&ioloop_timeval, &created))->
+ add_str("error", error);
+ e_error(e->event(), IMAP_CLIENT_UNHIBERNATE_ERROR": %s", error);
+ imap_client_destroy(_client, IMAP_CLIENT_UNHIBERNATE_ERROR);
+}
+
+static void
+imap_client_parse_userdb_fields(struct imap_client *client,
+ const char **auth_user_r)
+{
+ const char *const *field;
+ unsigned int i;
+
+ *auth_user_r = NULL;
+
+ if (client->state.userdb_fields == NULL)
+ return;
+
+ field = t_strsplit_tabescaped(client->state.userdb_fields);
+ for (i = 0; field[i] != NULL; i++) {
+ if (str_begins(field[i], "auth_user="))
+ *auth_user_r = field[i] + 10;
+ }
+}
+
+static void
+imap_client_move_back_send_callback(void *context, struct ostream *output)
+{
+ struct imap_client *client = context;
+ const struct imap_client_state *state = &client->state;
+ string_t *str = t_str_new(256);
+ struct timeval created;
+ const unsigned char *input_data;
+ size_t input_size;
+ ssize_t ret;
+
+ str_append_tabescaped(str, state->username);
+ event_get_create_time(client->event, &created);
+ str_printfa(str, "\thibernation_started=%"PRIdTIME_T".%06u",
+ created.tv_sec, (unsigned int)created.tv_usec);
+
+ if (state->session_id != NULL) {
+ str_append(str, "\tsession=");
+ str_append_tabescaped(str, state->session_id);
+ }
+ if (state->session_created != 0) {
+ str_printfa(str, "\tsession_created=%s",
+ dec2str(state->session_created));
+ }
+ if (state->tag != NULL)
+ str_printfa(str, "\ttag=%s", client->state.tag);
+ if (state->local_ip.family != 0)
+ str_printfa(str, "\tlip=%s", net_ip2addr(&state->local_ip));
+ if (state->local_port != 0)
+ str_printfa(str, "\tlport=%u", state->local_port);
+ if (state->remote_ip.family != 0)
+ str_printfa(str, "\trip=%s", net_ip2addr(&state->remote_ip));
+ if (state->remote_port != 0)
+ str_printfa(str, "\trport=%u", state->remote_port);
+ if (state->userdb_fields != NULL) {
+ str_append(str, "\tuserdb_fields=");
+ str_append_tabescaped(str, state->userdb_fields);
+ }
+ if (major(state->peer_dev) != 0 || minor(state->peer_dev) != 0) {
+ str_printfa(str, "\tpeer_dev_major=%lu\tpeer_dev_minor=%lu",
+ (unsigned long)major(state->peer_dev),
+ (unsigned long)minor(state->peer_dev));
+ }
+ if (state->peer_ino != 0)
+ str_printfa(str, "\tpeer_ino=%llu", (unsigned long long)state->peer_ino);
+ if (state->state_size > 0) {
+ str_append(str, "\tstate=");
+ base64_encode(state->state, state->state_size, str);
+ }
+ input_data = i_stream_get_data(client->input, &input_size);
+ if (input_size > 0) {
+ str_append(str, "\tclient_input=");
+ base64_encode(input_data, input_size, str);
+ }
+ i_assert(o_stream_get_buffer_used_size(client->output) == 0);
+ if (client->idle_done) {
+ if (client->bad_done)
+ str_append(str, "\tbad-done");
+ } else if (client->state.idle_cmd) {
+ /* IDLE continues after sending changes */
+ str_append(str, "\tidle-continue");
+ }
+ str_append_c(str, '\n');
+
+ /* send the fd first */
+ ret = fd_send(o_stream_get_fd(output), client->fd, str_data(str), 1);
+ if (ret < 0) {
+ const char *error = t_strdup_printf(
+ "fd_send(%s) failed: %m", o_stream_get_name(output));
+ imap_client_unhibernate_failed(&client, error);
+ return;
+ }
+ /* If unhibernation fails after this, shutdown() the fd to make sure
+ the imap process won't later on finish unhibernation after all and
+ cause confusion. */
+ client->shutdown_fd_on_destroy = TRUE;
+ i_assert(ret > 0);
+ o_stream_nsend(output, str_data(str) + 1, str_len(str) - 1);
+}
+
+static void
+imap_client_move_back_read_callback(void *context, const char *line)
+{
+ struct imap_client *client = context;
+
+ if (line[0] != '+') {
+ /* failed - FIXME: retry later? */
+ imap_client_unhibernate_failed(&client, line+1);
+ } else {
+ client->shutdown_fd_on_destroy = FALSE;
+ imap_client_destroy(&client, NULL);
+ }
+}
+
+static bool imap_move_has_reached_timeout(struct imap_client *client)
+{
+ int max_secs = client->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS;
+ return client->move_back_start != 0 &&
+ ioloop_time - client->move_back_start > max_secs;
+}
+
+static bool imap_client_try_move_back(struct imap_client *client)
+{
+ const struct master_service_settings *master_set;
+ const char *path, *error;
+ int ret;
+
+ if (o_stream_get_buffer_used_size(client->output) > 0) {
+ /* there is data buffered, so we have to disconnect you */
+ imap_client_destroy(&client, IMAP_CLIENT_BUFFER_FULL_ERROR);
+ return TRUE;
+ }
+
+ master_set = master_service_settings_get(master_service);
+ path = t_strconcat(master_set->base_dir,
+ "/"IMAP_MASTER_SOCKET_NAME, NULL);
+ ret = imap_master_connection_init(path,
+ imap_client_move_back_send_callback,
+ imap_client_move_back_read_callback,
+ client, &client->master_conn, &error);
+ if (ret > 0) {
+ /* success */
+ imap_client_stop(client);
+ return TRUE;
+ } else if (ret < 0 || imap_move_has_reached_timeout(client)) {
+ /* failed to connect to the imap-master socket */
+ imap_client_unhibernate_failed(&client, error);
+ return TRUE;
+ }
+
+ e_debug(event_create_passthrough(client->event)->
+ set_name("imap_client_unhibernate_retried")->
+ add_str("error", error)->event(),
+ "Unhibernation failed: %s - retrying", error);
+ /* Stop listening for client's IOs while waiting for the next
+ reconnection attempt. However if we got here because of an external
+ notification keep waiting to see if client sends any IO, since that
+ will cause the unhibernation to be aborted earlier. */
+ if (client->input_pending)
+ io_remove(&client->io);
+ imap_client_stop_notify_listening(client);
+ return FALSE;
+}
+
+static void imap_client_move_back(struct imap_client *client)
+{
+ if (imap_client_try_move_back(client))
+ return;
+
+ /* imap-master socket is busy. retry in a while. */
+ if (client->move_back_start == 0)
+ client->move_back_start = ioloop_time;
+ if (!client->unhibernate_queued) {
+ client->unhibernate_queued = TRUE;
+ priorityq_add(unhibernate_queue, &client->item);
+ }
+ if (to_unhibernate == NULL) {
+ to_unhibernate = timeout_add_short(IMAP_UNHIBERNATE_RETRY_MSECS,
+ imap_clients_unhibernate, NULL);
+ }
+}
+
+static enum imap_client_input_state
+imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r)
+{
+ const unsigned char *tag_start, *tag_end;
+
+ enum imap_client_input_state state = IMAP_CLIENT_INPUT_STATE_DONE_LF;
+
+ /* skip over DONE[\r]\n */
+ if (i_memcasecmp(data, "DONE", I_MIN(size, 4)) != 0)
+ return IMAP_CLIENT_INPUT_STATE_BAD;
+ if (size <= 4)
+ return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+ data += 4; size -= 4;
+
+ if (data[0] == '\r') {
+ state = IMAP_CLIENT_INPUT_STATE_DONE_CRLF;
+ data++; size--;
+ }
+ if (size == 0)
+ return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+ if (data[0] != '\n')
+ return IMAP_CLIENT_INPUT_STATE_BAD;
+ data++; size--;
+ if (size == 0)
+ return state;
+
+ tag_start = data;
+
+ /* skip over tag */
+ while(data[0] != ' ' &&
+ data[0] != '\r' &&
+ data[0] != '\t' ) { data++; size--; }
+
+ tag_end = data;
+
+ if (size == 0)
+ return state;
+ if (data[0] != ' ')
+ return IMAP_CLIENT_INPUT_STATE_BAD;
+ data++; size--;
+
+ /* skip over IDLE[\r]\n - checking this assumes that the DONE and IDLE
+ are sent in the same IP packet, otherwise we'll unnecessarily
+ recreate the imap process and immediately resume IDLE there. if this
+ becomes an issue we could add a small delay to the imap process
+ creation and wait for the IDLE command during it. */
+ if (size <= 4 || i_memcasecmp(data, "IDLE", 4) != 0)
+ return state;
+ data += 4; size -= 4;
+
+ if (data[0] == '\r') {
+ data++; size--;
+ }
+ if (size == 1 && data[0] == '\n') {
+ *tag_r = t_strdup_until(tag_start, tag_end);
+ return IMAP_CLIENT_INPUT_STATE_DONEIDLE;
+ }
+ return state;
+}
+
+static void imap_client_input_idle_cmd(struct imap_client *client)
+{
+ char *old_tag;
+ const char *new_tag;
+ const char *output;
+ const unsigned char *data;
+ size_t size;
+ bool done = TRUE;
+ int ret;
+
+ /* we should read either DONE or disconnection. also handle if client
+ sends DONE\nIDLE simply to recreate the IDLE. */
+ ret = i_stream_read_bytes(client->input, &data, &size,
+ client->next_read_threshold + 1);
+ if (size == 0) {
+ if (ret < 0)
+ imap_client_disconnected(&client);
+ return;
+ }
+ client->next_read_threshold = 0;
+ switch (imap_client_input_parse(data, size, &new_tag)) {
+ case IMAP_CLIENT_INPUT_STATE_UNKNOWN:
+ /* we haven't received a full DONE[\r]\n yet - wait */
+ client->next_read_threshold = size;
+ return;
+ case IMAP_CLIENT_INPUT_STATE_BAD:
+ /* invalid input - return this to the imap process */
+ client->bad_done = TRUE;
+ break;
+ case IMAP_CLIENT_INPUT_STATE_DONE_LF:
+ i_stream_skip(client->input, 4+1);
+ break;
+ case IMAP_CLIENT_INPUT_STATE_DONE_CRLF:
+ i_stream_skip(client->input, 4+2);
+ break;
+ case IMAP_CLIENT_INPUT_STATE_DONEIDLE:
+ /* we received DONE+IDLE, so the client simply wanted to notify
+ us that it's still there. continue hibernation. */
+ old_tag = client->state.tag;
+ client->state.tag = i_strdup(new_tag);
+ output = t_strdup_printf("%s OK Idle completed.\r\n+ idling\r\n", old_tag);
+ i_free(old_tag);
+ ret = o_stream_flush(client->output);
+ if (ret > 0)
+ ret = o_stream_send_str(client->output, output);
+ if (ret < 0) {
+ imap_client_disconnected(&client);
+ return;
+ }
+ if ((size_t)ret != strlen(output)) {
+ /* disconnect */
+ imap_client_destroy(&client, IMAP_CLIENT_BUFFER_FULL_ERROR);
+ return;
+ } else {
+ done = FALSE;
+ i_stream_skip(client->input, size);
+ }
+ break;
+ }
+
+ if (done) {
+ client->idle_done = TRUE;
+ client->input_pending = TRUE;
+ imap_client_move_back(client);
+ } else
+ imap_client_add_idle_keepalive_timeout(client);
+}
+
+static void imap_client_input_nonidle(struct imap_client *client)
+{
+ if (i_stream_read(client->input) < 0)
+ imap_client_disconnected(&client);
+ else {
+ client->input_pending = TRUE;
+ imap_client_move_back(client);
+ }
+}
+
+static void imap_client_input_notify(struct imap_client *client)
+{
+ imap_client_move_back(client);
+}
+
+static void keepalive_timeout(struct imap_client *client)
+{
+ ssize_t ret;
+
+ /* do not send this if there is data buffered */
+ if ((ret = o_stream_flush(client->output)) < 0) {
+ imap_client_disconnected(&client);
+ return;
+ } else if (ret == 0)
+ return;
+
+ ret = o_stream_send_str(client->output, imap_still_here_text);
+ if (ret < 0) {
+ imap_client_disconnected(&client);
+ return;
+ }
+ /* ostream buffer size is definitely large enough for this text */
+ i_assert((size_t)ret == strlen(imap_still_here_text));
+ imap_client_add_idle_keepalive_timeout(client);
+}
+
+static void imap_client_add_idle_keepalive_timeout(struct imap_client *client)
+{
+ unsigned int interval = client->state.imap_idle_notify_interval;
+
+ if (interval == 0)
+ return;
+
+ interval = imap_keepalive_interval_msecs(client->state.username,
+ &client->state.remote_ip,
+ interval);
+
+ timeout_remove(&client->to_keepalive);
+ client->to_keepalive = timeout_add(interval, keepalive_timeout, client);
+}
+
+static const struct var_expand_table *
+imap_client_get_var_expand_table(struct imap_client *client)
+{
+ const char *username = t_strcut(client->state.username, '@');
+ const char *domain = i_strchr_to_next(client->state.username, '@');
+ const char *local_ip = client->state.local_ip.family == 0 ? NULL :
+ net_ip2addr(&client->state.local_ip);
+ const char *remote_ip = client->state.remote_ip.family == 0 ? NULL :
+ net_ip2addr(&client->state.remote_ip);
+
+ const char *auth_user, *auth_username, *auth_domain;
+ imap_client_parse_userdb_fields(client, &auth_user);
+ if (auth_user == NULL) {
+ auth_user = client->state.username;
+ auth_username = username;
+ auth_domain = domain;
+ } else {
+ auth_username = t_strcut(auth_user, '@');
+ auth_domain = i_strchr_to_next(auth_user, '@');
+ }
+
+ const struct var_expand_table stack_tab[] = {
+ { 'u', client->state.username, "user" },
+ { 'n', username, "username" },
+ { 'd', domain, "domain" },
+ { 's', "imap-hibernate", "service" },
+ { 'h', NULL /* we shouldn't need this */, "home" },
+ { 'l', local_ip, "lip" },
+ { 'r', remote_ip, "rip" },
+ { 'p', my_pid, "pid" },
+ { 'i', dec2str(client->state.uid), "uid" },
+ { '\0', dec2str(client->state.gid), "gid" },
+ { '\0', client->state.session_id, "session" },
+ { '\0', auth_user, "auth_user" },
+ { '\0', auth_username, "auth_username" },
+ { '\0', auth_domain, "auth_domain" },
+ /* aliases: */
+ { '\0', local_ip, "local_ip" },
+ { '\0', remote_ip, "remote_ip" },
+ /* NOTE: keep this synced with lib-storage's
+ mail_user_var_expand_table() */
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+ return tab;
+}
+
+static int
+imap_client_var_expand_func_userdb(const char *data, void *context,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ const char *const *fields = context;
+ const char *field_name = t_strdup_printf("%s=",t_strcut(data, ':'));
+ const char *default_value = i_strchr_to_next(data, ':');
+ const char *value = NULL;
+
+ for(;*fields != NULL; fields++) {
+ if (str_begins(*fields, field_name)) {
+ value = *fields+strlen(field_name);
+ break;
+ }
+ }
+
+ *value_r = value != NULL ? value : default_value;
+
+ return 1;
+}
+
+static void imap_client_io_activate_user(struct imap_client *client)
+{
+ i_set_failure_prefix("%s", client->log_prefix);
+}
+
+static void imap_client_io_deactivate_user(struct imap_client *client ATTR_UNUSED)
+{
+ i_set_failure_prefix("imap-hibernate: ");
+}
+
+static const char *imap_client_get_anvil_userip_ident(struct imap_client_state *state)
+{
+ if (state->remote_ip.family == 0)
+ return NULL;
+ return t_strconcat(net_ip2addr(&state->remote_ip), "/",
+ str_tabescape(state->username), NULL);
+}
+
+struct imap_client *
+imap_client_create(int fd, const struct imap_client_state *state)
+{
+ const struct var_expand_func_table funcs[] = {
+ { "userdb", imap_client_var_expand_func_userdb },
+ { NULL, NULL }
+ };
+ struct imap_client *client;
+ pool_t pool = pool_alloconly_create("imap client", 256);
+ void *statebuf;
+ const char *ident, *error;
+
+ i_assert(state->username != NULL);
+ i_assert(state->mail_log_prefix != NULL);
+
+ fd_set_nonblock(fd, TRUE); /* it should already be, but be sure */
+
+ client = p_new(pool, struct imap_client, 1);
+ client->pool = pool;
+ client->fd = fd;
+ client->input = i_stream_create_fd(fd, IMAP_MAX_INBUF);
+ client->output = o_stream_create_fd(fd, IMAP_MAX_OUTBUF);
+ client->state = *state;
+ client->state.username = p_strdup(pool, state->username);
+ client->state.session_id = p_strdup(pool, state->session_id);
+ client->state.userdb_fields = p_strdup(pool, state->userdb_fields);
+ client->state.stats = p_strdup(pool, state->stats);
+
+ client->event = event_create(NULL);
+ event_add_category(client->event, &event_category_imap_hibernate);
+ event_add_str(client->event, "user", state->username);
+ event_add_str(client->event, "session", state->session_id);
+ if (state->mailbox_vname != NULL)
+ event_add_str(client->event, "mailbox", state->mailbox_vname);
+ if (state->local_ip.family != 0)
+ event_add_str(client->event, "local_ip",
+ net_ip2addr(&state->local_ip));
+ if (state->local_port != 0)
+ event_add_int(client->event, "local_port", state->local_port);
+ if (state->remote_ip.family != 0)
+ event_add_str(client->event, "remote_ip",
+ net_ip2addr(&state->remote_ip));
+ if (state->remote_port != 0)
+ event_add_int(client->event, "remote_port", state->remote_port);
+
+ if (state->state_size > 0) {
+ client->state.state = statebuf = p_malloc(pool, state->state_size);
+ memcpy(statebuf, state->state, state->state_size);
+ client->state.state_size = state->state_size;
+ }
+ T_BEGIN {
+ string_t *str;
+ char **fields = p_strsplit_tabescaped(unsafe_data_stack_pool,
+ client->state.userdb_fields);
+ str = t_str_new(256);
+ if (var_expand_with_funcs(str, state->mail_log_prefix,
+ imap_client_get_var_expand_table(client),
+ funcs, fields, &error) <= 0) {
+ e_error(client->event,
+ "Failed to expand mail_log_prefix=%s: %s",
+ state->mail_log_prefix, error);
+ }
+ client->log_prefix = p_strdup(pool, str_c(str));
+ } T_END;
+
+ ident = imap_client_get_anvil_userip_ident(&client->state);
+ if (ident != NULL) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\timap/", ident, "\n", NULL));
+ client->state.anvil_sent = TRUE;
+ }
+
+ p_array_init(&client->notifys, pool, 2);
+ DLLIST_PREPEND(&imap_clients, client);
+ return client;
+}
+
+static void imap_client_stop_notify_listening(struct imap_client *client)
+{
+ struct imap_client_notify *notify;
+
+ array_foreach_modifiable(&client->notifys, notify) {
+ io_remove(&notify->io);
+ i_close_fd(&notify->fd);
+ }
+}
+
+static void imap_client_stop(struct imap_client *client)
+{
+ if (client->unhibernate_queued) {
+ priorityq_remove(unhibernate_queue, &client->item);
+ client->unhibernate_queued = FALSE;
+ }
+ io_remove(&client->io);
+ timeout_remove(&client->to_keepalive);
+ imap_client_stop_notify_listening(client);
+}
+
+void imap_client_destroy(struct imap_client **_client, const char *reason)
+{
+ struct imap_client *client = *_client;
+
+ *_client = NULL;
+
+ if (reason != NULL) {
+ /* the client input/output bytes don't count the DONE+IDLE by
+ imap-hibernate, but that shouldn't matter much. */
+ e_info(client->event, "Disconnected: %s %s",
+ reason, client->state.stats);
+ }
+
+ if (client->state.anvil_sent) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\timap/",
+ imap_client_get_anvil_userip_ident(&client->state),
+ "\n", NULL));
+ }
+
+ if (client->master_conn != NULL)
+ imap_master_connection_free(&client->master_conn);
+ if (client->ioloop_ctx != NULL) {
+ io_loop_context_remove_callbacks(client->ioloop_ctx,
+ imap_client_io_activate_user,
+ imap_client_io_deactivate_user, client);
+ imap_client_io_deactivate_user(client);
+ io_loop_context_unref(&client->ioloop_ctx);
+ }
+
+ if (client->state.tag != NULL)
+ i_free(client->state.tag);
+
+ if (client->shutdown_fd_on_destroy) {
+ if (shutdown(client->fd, SHUT_RDWR) < 0)
+ e_error(client->event, "shutdown() failed: %m");
+ }
+
+ DLLIST_REMOVE(&imap_clients, client);
+ imap_client_stop(client);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ i_close_fd(&client->fd);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void imap_client_add_notify_fd(struct imap_client *client, int fd)
+{
+ struct imap_client_notify *notify;
+
+ notify = array_append_space(&client->notifys);
+ notify->fd = fd;
+}
+
+void imap_client_create_finish(struct imap_client *client)
+{
+ struct imap_client_notify *notify;
+
+ client->ioloop_ctx = io_loop_context_new(current_ioloop);
+ io_loop_context_add_callbacks(client->ioloop_ctx,
+ imap_client_io_activate_user,
+ imap_client_io_deactivate_user, client);
+ io_loop_context_switch(client->ioloop_ctx);
+
+ if (client->state.idle_cmd) {
+ client->io = io_add(client->fd, IO_READ,
+ imap_client_input_idle_cmd, client);
+ } else {
+ client->io = io_add(client->fd, IO_READ,
+ imap_client_input_nonidle, client);
+ }
+ imap_client_add_idle_keepalive_timeout(client);
+
+ array_foreach_modifiable(&client->notifys, notify) {
+ notify->io = io_add(notify->fd, IO_READ,
+ imap_client_input_notify, client);
+ }
+}
+
+static int client_unhibernate_cmp(const void *p1, const void *p2)
+{
+ const struct imap_client *c1 = p1, *c2 = p2;
+ time_t t1, t2;
+
+ t1 = c1->move_back_start +
+ (c1->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS);
+ t2 = c2->move_back_start +
+ (c2->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS);
+ if (t1 < t2)
+ return -1;
+ if (t1 > t2)
+ return 1;
+ return 0;
+}
+
+static void imap_clients_unhibernate(void *context ATTR_UNUSED)
+{
+ struct priorityq_item *item;
+
+ while ((item = priorityq_peek(unhibernate_queue)) != NULL) {
+ struct imap_client *client = (struct imap_client *)item;
+
+ if (!imap_client_try_move_back(client))
+ return;
+ }
+ timeout_remove(&to_unhibernate);
+}
+
+void imap_clients_init(void)
+{
+ unhibernate_queue = priorityq_init(client_unhibernate_cmp, 64);
+}
+
+void imap_clients_deinit(void)
+{
+ while (imap_clients != NULL) {
+ struct imap_client *client = imap_clients;
+
+ imap_client_io_activate_user(client);
+ imap_client_destroy(&client, "Shutting down");
+ }
+ timeout_remove(&to_unhibernate);
+ priorityq_deinit(&unhibernate_queue);
+}
diff --git a/src/imap-hibernate/imap-client.h b/src/imap-hibernate/imap-client.h
new file mode 100644
index 0000000..b6bb0e6
--- /dev/null
+++ b/src/imap-hibernate/imap-client.h
@@ -0,0 +1,40 @@
+#ifndef IMAP_CLIENT_H
+#define IMAP_CLIENT_H
+
+#include "net.h"
+
+struct imap_client_state {
+ /* required: */
+ const char *username, *mail_log_prefix;
+ /* optional: */
+ const char *session_id, *mailbox_vname, *userdb_fields, *stats;
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+ time_t session_created;
+
+ uid_t uid;
+ gid_t gid;
+
+ dev_t peer_dev;
+ ino_t peer_ino;
+
+ char *tag;
+ const unsigned char *state;
+ size_t state_size;
+
+ unsigned int imap_idle_notify_interval;
+ bool idle_cmd;
+ bool have_notify_fd;
+ bool anvil_sent;
+};
+
+struct imap_client *
+imap_client_create(int fd, const struct imap_client_state *state);
+void imap_client_add_notify_fd(struct imap_client *client, int fd);
+void imap_client_create_finish(struct imap_client *client);
+void imap_client_destroy(struct imap_client **_client, const char *reason);
+
+void imap_clients_init(void);
+void imap_clients_deinit(void);
+
+#endif
diff --git a/src/imap-hibernate/imap-hibernate-client.c b/src/imap-hibernate/imap-hibernate-client.c
new file mode 100644
index 0000000..4065adc
--- /dev/null
+++ b/src/imap-hibernate/imap-hibernate-client.c
@@ -0,0 +1,304 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "connection.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "ostream.h"
+#include "buffer.h"
+#include "base64.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "imap-client.h"
+#include "imap-hibernate-client.h"
+
+struct imap_hibernate_client {
+ struct connection conn;
+ struct imap_client *imap_client;
+ bool imap_client_created;
+ bool finished;
+ bool debug;
+};
+
+struct imap_hibernate_input {
+ /* input we've already read from the IMAP client. */
+ buffer_t *client_input;
+ /* IMAP connection state */
+ buffer_t *state;
+};
+
+static struct connection_list *hibernate_clients = NULL;
+
+static void imap_hibernate_client_destroy(struct connection *conn)
+{
+ struct imap_hibernate_client *client = (struct imap_hibernate_client *)conn;
+
+ if (!client->imap_client_created)
+ master_service_client_connection_destroyed(master_service);
+ else if (client->finished)
+ imap_client_create_finish(client->imap_client);
+ connection_deinit(conn);
+ i_free(conn);
+}
+
+static int
+imap_hibernate_client_parse_input(const char *const *args, pool_t pool,
+ struct imap_client_state *state_r,
+ const char **error_r)
+{
+ const char *key, *value;
+ unsigned int peer_dev_major = 0, peer_dev_minor = 0;
+
+ i_zero(state_r);
+ if (args[0] == NULL) {
+ *error_r = "Missing username in input";
+ return -1;
+ }
+ state_r->username = args[0]; args++;
+ if (args[0] == NULL) {
+ *error_r = "Missing mail_log_prefix in input";
+ return -1;
+ }
+ state_r->mail_log_prefix = args[0]; args++;
+
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value != NULL)
+ key = t_strdup_until(*args, value++);
+ else {
+ key = *args;
+ value = "";
+ }
+ if (strcmp(key, "lip") == 0) {
+ if (net_addr2ip(value, &state_r->local_ip) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid lip value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "lport") == 0) {
+ if (net_str2port(value, &state_r->local_port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid lport value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "rip") == 0) {
+ if (net_addr2ip(value, &state_r->remote_ip) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid rip value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "rport") == 0) {
+ if (net_str2port(value, &state_r->remote_port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid rport value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_dev_major") == 0) {
+ if (str_to_uint(value, &peer_dev_major) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_dev_major value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_dev_minor") == 0) {
+ if (str_to_uint(value, &peer_dev_minor) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_dev_minor value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_ino") == 0) {
+ if (str_to_ino(value, &state_r->peer_ino) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_ino value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "uid") == 0) {
+ if (str_to_uid(value, &state_r->uid) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid uid value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "gid") == 0) {
+ if (str_to_gid(value, &state_r->gid) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid gid value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "stats") == 0) {
+ state_r->stats = value;
+ } else if (strcmp(key, "idle-cmd") == 0) {
+ state_r->idle_cmd = TRUE;
+ } else if (strcmp(key, "session") == 0) {
+ state_r->session_id = value;
+ } else if (strcmp(key, "mailbox") == 0) {
+ state_r->mailbox_vname = value;
+ } else if (strcmp(key, "session_created") == 0) {
+ if (str_to_time(value, &state_r->session_created) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid session_created value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "userdb_fields") == 0) {
+ state_r->userdb_fields = value;
+ } else if (strcmp(key, "notify_fd") == 0) {
+ state_r->have_notify_fd = TRUE;
+ } else if (strcmp(key, "idle_notify_interval") == 0) {
+ if (str_to_uint(value, &state_r->imap_idle_notify_interval) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid idle_notify_interval value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "tag") == 0) {
+ state_r->tag = i_strdup(value);
+ } else if (strcmp(key, "state") == 0) {
+ buffer_t *state_buf;
+
+ state_buf = buffer_create_dynamic(pool, 1024);
+ if (base64_decode(value, strlen(value), NULL,
+ state_buf) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid state base64 value: %s", value);
+ return -1;
+ }
+ state_r->state = state_buf->data;
+ state_r->state_size = state_buf->used;
+ }
+ }
+ if (state_r->tag == NULL) {
+ *error_r = "Missing tag";
+ return -1;
+ }
+ if (peer_dev_major != 0 || peer_dev_minor != 0)
+ state_r->peer_dev = makedev(peer_dev_major, peer_dev_minor);
+ return 0;
+}
+
+static int
+imap_hibernate_client_input_args(struct connection *conn,
+ const char *const *args, int fd, pool_t pool)
+{
+ struct imap_hibernate_client *client =
+ (struct imap_hibernate_client *)conn;
+ struct imap_client_state state;
+ const char *error;
+
+ if (imap_hibernate_client_parse_input(args, pool, &state, &error) < 0) {
+ e_error(conn->event, "Failed to parse client input: %s", error);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "-Failed to parse client input: %s\n", error));
+ return -1;
+ }
+ client->imap_client = imap_client_create(fd, &state);
+ /* the transferred imap client fd is now counted as the client. */
+ client->imap_client_created = TRUE;
+ return state.have_notify_fd ? 0 : 1;
+}
+
+static int
+imap_hibernate_client_input_line(struct connection *conn, const char *line)
+{
+ struct imap_hibernate_client *client =
+ (struct imap_hibernate_client *)conn;
+ int fd = -1, ret;
+
+ if (!conn->version_received) {
+ if (connection_handshake_args_default(
+ conn, t_strsplit_tabescaped(line)) < 0)
+ return -1;
+ conn->version_received = TRUE;
+ return 1;
+ }
+ if (client->finished) {
+ e_error(conn->event, "Received unexpected line: %s", line);
+ return -1;
+ }
+
+ if (client->imap_client == NULL) {
+ char *const *args;
+ pool_t pool;
+
+ fd = i_stream_unix_get_read_fd(conn->input);
+ if (fd == -1) {
+ e_error(conn->event, "IMAP client fd not received");
+ return -1;
+ }
+
+ pool = pool_alloconly_create("client cmd", 1024);
+ args = p_strsplit_tabescaped(pool, line);
+ ret = imap_hibernate_client_input_args(conn, (const void *)args,
+ fd, pool);
+ if (ret >= 0 && client->debug)
+ e_debug(conn->event, "Create client with input: %s", line);
+ pool_unref(&pool);
+ } else {
+ fd = i_stream_unix_get_read_fd(conn->input);
+ if (fd == -1) {
+ e_error(conn->event,
+ "IMAP notify fd not received (input: %s)", line);
+ ret = -1;
+ } else if (line[0] != '\0') {
+ e_error(conn->event,
+ "Expected empty notify fd line from client, but got: %s", line);
+ o_stream_nsend_str(conn->output,
+ "Expected empty notify fd line");
+ ret = -1;
+ } else {
+ imap_client_add_notify_fd(client->imap_client, fd);
+ ret = 1;
+ }
+ }
+
+ if (ret < 0) {
+ if (client->imap_client != NULL)
+ imap_client_destroy(&client->imap_client, NULL);
+ i_close_fd(&fd);
+ return -1;
+ } else if (ret == 0) {
+ /* still need to read another fd */
+ i_stream_unix_set_read_fd(conn->input);
+ } else {
+ /* finished - wait for disconnection from imap before finishing.
+ this way the old imap process will have time to destroy
+ itself before we have a chance to create another one. */
+ client->finished = TRUE;
+ }
+ o_stream_nsend_str(conn->output, "+\n");
+ return 1;
+}
+
+void imap_hibernate_client_create(int fd, bool debug)
+{
+ struct imap_hibernate_client *client;
+
+ client = i_new(struct imap_hibernate_client, 1);
+ client->debug = debug;
+ client->conn.unix_socket = TRUE;
+ connection_init_server(hibernate_clients, &client->conn,
+ "imap-hibernate", fd, fd);
+ i_stream_unix_set_read_fd(client->conn.input);
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "imap-hibernate",
+ .service_name_out = "imap-hibernate",
+ .major_version = 1,
+ .minor_version = 0,
+
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = imap_hibernate_client_destroy,
+ .input_line = imap_hibernate_client_input_line
+};
+
+void imap_hibernate_clients_init(void)
+{
+ hibernate_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_hibernate_clients_deinit(void)
+{
+ connection_list_deinit(&hibernate_clients);
+}
diff --git a/src/imap-hibernate/imap-hibernate-client.h b/src/imap-hibernate/imap-hibernate-client.h
new file mode 100644
index 0000000..c7bec73
--- /dev/null
+++ b/src/imap-hibernate/imap-hibernate-client.h
@@ -0,0 +1,9 @@
+#ifndef IMAP_HIBERNATE_CLIENT_H
+#define IMAP_HIBERNATE_CLIENT_H
+
+void imap_hibernate_client_create(int fd, bool debug);
+
+void imap_hibernate_clients_init(void);
+void imap_hibernate_clients_deinit(void);
+
+#endif
diff --git a/src/imap-hibernate/imap-hibernate-settings.c b/src/imap-hibernate/imap-hibernate-settings.c
new file mode 100644
index 0000000..d7618c0
--- /dev/null
+++ b/src/imap-hibernate/imap-hibernate-settings.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+/* <settings checks> */
+static struct file_listener_settings imap_hibernate_unix_listeners_array[] = {
+ { "imap-hibernate", 0660, "", "$default_internal_group" }
+};
+static struct file_listener_settings *imap_hibernate_unix_listeners[] = {
+ &imap_hibernate_unix_listeners_array[0]
+};
+static buffer_t imap_hibernate_unix_listeners_buf = {
+ { { imap_hibernate_unix_listeners, sizeof(imap_hibernate_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_hibernate_service_settings = {
+ .name = "imap-hibernate",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-hibernate",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_hibernate_unix_listeners_buf,
+ sizeof(imap_hibernate_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
diff --git a/src/imap-hibernate/imap-master-connection.c b/src/imap-hibernate/imap-master-connection.c
new file mode 100644
index 0000000..71fa4dd
--- /dev/null
+++ b/src/imap-hibernate/imap-master-connection.c
@@ -0,0 +1,140 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "connection.h"
+#include "imap-master-connection.h"
+
+#define IMAP_MASTER_CONNECTION_TIMEOUT_MSECS 30000
+
+struct imap_master_connection {
+ struct connection conn;
+ struct timeout *to;
+
+ imap_master_connection_send_callback_t *send_callback;
+ imap_master_connection_read_callback_t *read_callback;
+ void *context;
+};
+
+static struct connection_list *master_clients;
+
+static void imap_master_connection_timeout(struct imap_master_connection *conn)
+{
+ e_error(conn->conn.event,
+ "Timeout communicating with %s (version %sreceived)",
+ conn->conn.name, conn->conn.version_received ? "" : "not ");
+ imap_master_connection_deinit(&conn);
+}
+
+int imap_master_connection_init(const char *path,
+ imap_master_connection_send_callback_t *send_callback,
+ imap_master_connection_read_callback_t *read_callback,
+ void *context,
+ struct imap_master_connection **conn_r,
+ const char **error_r)
+{
+ struct imap_master_connection *conn;
+
+ conn = i_new(struct imap_master_connection, 1);
+ conn->send_callback = send_callback;
+ conn->read_callback = read_callback;
+ conn->context = context;
+ connection_init_client_unix(master_clients, &conn->conn, path);
+ if (connection_client_connect(&conn->conn) < 0) {
+ int ret = errno == EAGAIN ? 0 : -1;
+
+ *error_r = t_strdup_printf(
+ "net_connect_unix(%s) failed: %m", path);
+ connection_deinit(&conn->conn);
+ i_free(conn);
+ return ret;
+ }
+ conn->to = timeout_add(IMAP_MASTER_CONNECTION_TIMEOUT_MSECS,
+ imap_master_connection_timeout, conn);
+ *conn_r = conn;
+ return 1;
+}
+
+static void
+imap_master_read_callback(struct imap_master_connection **_conn,
+ const char *line)
+{
+ struct imap_master_connection *conn = *_conn;
+ imap_master_connection_read_callback_t *read_callback =
+ conn->read_callback;
+
+ *_conn = NULL;
+ conn->read_callback = NULL;
+ read_callback(conn->context, line);
+ /* connection is destroyed now */
+}
+
+void imap_master_connection_deinit(struct imap_master_connection **_conn)
+{
+ imap_master_read_callback(_conn, t_strdup_printf(
+ "-%s", connection_disconnect_reason(&(*_conn)->conn)));
+}
+
+void imap_master_connection_free(struct imap_master_connection **_conn)
+{
+ struct imap_master_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ timeout_remove(&conn->to);
+ connection_deinit(&conn->conn);
+ i_free(conn);
+}
+
+static void imap_master_client_destroy(struct connection *_conn)
+{
+ struct imap_master_connection *conn =
+ (struct imap_master_connection *)_conn;
+
+ imap_master_connection_deinit(&conn);
+}
+
+static int
+imap_master_client_input_line(struct connection *_conn, const char *line)
+{
+ struct imap_master_connection *conn =
+ (struct imap_master_connection *)_conn;
+
+ if (!_conn->version_received) {
+ if (connection_input_line_default(_conn, line) < 0)
+ return -1;
+
+ conn->send_callback(conn->context, _conn->output);
+ return 1;
+ } else {
+ imap_master_read_callback(&conn, line);
+ /* we're finished now with this connection - disconnect it */
+ return -1;
+ }
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "imap-master",
+ .service_name_out = "imap-master",
+ .major_version = 1,
+ .minor_version = 0,
+
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = imap_master_client_destroy,
+ .input_line = imap_master_client_input_line
+};
+
+void imap_master_connections_init(void)
+{
+ master_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_master_connections_deinit(void)
+{
+ connection_list_deinit(&master_clients);
+}
diff --git a/src/imap-hibernate/imap-master-connection.h b/src/imap-hibernate/imap-master-connection.h
new file mode 100644
index 0000000..0ff2f3c
--- /dev/null
+++ b/src/imap-hibernate/imap-master-connection.h
@@ -0,0 +1,24 @@
+#ifndef IMAP_MASTER_CONNECTION_H
+#define IMAP_MASTER_CONNECTION_H
+
+struct imap_master_connection;
+
+typedef void
+imap_master_connection_send_callback_t(void *context, struct ostream *output);
+typedef void
+imap_master_connection_read_callback_t(void *context, const char *reply);
+
+/* Returns 1 = success, 0 = retry later, -1 = error */
+int imap_master_connection_init(const char *path,
+ imap_master_connection_send_callback_t *send_callback,
+ imap_master_connection_read_callback_t *read_callback,
+ void *context,
+ struct imap_master_connection **conn_r,
+ const char **error_r);
+void imap_master_connection_deinit(struct imap_master_connection **conn);
+void imap_master_connection_free(struct imap_master_connection **conn);
+
+void imap_master_connections_init(void);
+void imap_master_connections_deinit(void);
+
+#endif
diff --git a/src/imap-hibernate/main.c b/src/imap-hibernate/main.c
new file mode 100644
index 0000000..dce14e0
--- /dev/null
+++ b/src/imap-hibernate/main.c
@@ -0,0 +1,57 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "imap-client.h"
+#include "imap-hibernate-client.h"
+#include "imap-master-connection.h"
+
+static bool debug = FALSE;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ imap_hibernate_client_create(conn->fd, debug);
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+ const char *error;
+ int c;
+
+ master_service = master_service_init("imap-hibernate", service_flags,
+ &argc, &argv, "D");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ if (master_service_settings_read_simple(master_service, NULL, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ master_service_init_log(master_service);
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ imap_clients_init();
+ imap_master_connections_init();
+ imap_hibernate_clients_init();
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ imap_master_connections_deinit();
+ imap_hibernate_clients_deinit();
+ imap_clients_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/imap-login/Makefile.am b/src/imap-login/Makefile.am
new file mode 100644
index 0000000..904f530
--- /dev/null
+++ b/src/imap-login/Makefile.am
@@ -0,0 +1,41 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = imap-login
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/login-common \
+ $(BINARY_CFLAGS)
+
+imap_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+imap_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+imap_login_SOURCES = \
+ imap-login-client.c \
+ client-authenticate.c \
+ imap-login-cmd-id.c \
+ imap-login-commands.c \
+ imap-login-settings.c \
+ imap-proxy.c
+
+noinst_HEADERS = \
+ client-authenticate.h \
+ imap-proxy.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ imap-login-client.h \
+ imap-login-commands.h \
+ imap-login-settings.h
diff --git a/src/imap-login/Makefile.in b/src/imap-login/Makefile.in
new file mode 100644
index 0000000..bbf5afa
--- /dev/null
+++ b/src/imap-login/Makefile.in
@@ -0,0 +1,897 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = imap-login$(EXEEXT)
+subdir = src/imap-login
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_imap_login_OBJECTS = imap-login-client.$(OBJEXT) \
+ client-authenticate.$(OBJEXT) imap-login-cmd-id.$(OBJEXT) \
+ imap-login-commands.$(OBJEXT) imap-login-settings.$(OBJEXT) \
+ imap-proxy.$(OBJEXT)
+imap_login_OBJECTS = $(am_imap_login_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/client-authenticate.Po \
+ ./$(DEPDIR)/imap-login-client.Po \
+ ./$(DEPDIR)/imap-login-cmd-id.Po \
+ ./$(DEPDIR)/imap-login-commands.Po \
+ ./$(DEPDIR)/imap-login-settings.Po ./$(DEPDIR)/imap-proxy.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 = $(imap_login_SOURCES)
+DIST_SOURCES = $(imap_login_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; }; \
+ }
+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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/login-common \
+ $(BINARY_CFLAGS)
+
+imap_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+imap_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+imap_login_SOURCES = \
+ imap-login-client.c \
+ client-authenticate.c \
+ imap-login-cmd-id.c \
+ imap-login-commands.c \
+ imap-login-settings.c \
+ imap-proxy.c
+
+noinst_HEADERS = \
+ client-authenticate.h \
+ imap-proxy.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ imap-login-client.h \
+ imap-login-commands.h \
+ imap-login-settings.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/imap-login/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/imap-login/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+imap-login$(EXEEXT): $(imap_login_OBJECTS) $(imap_login_DEPENDENCIES) $(EXTRA_imap_login_DEPENDENCIES)
+ @rm -f imap-login$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(imap_login_OBJECTS) $(imap_login_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-authenticate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-login-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-login-cmd-id.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-login-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-login-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-proxy.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/imap-login-client.Po
+ -rm -f ./$(DEPDIR)/imap-login-cmd-id.Po
+ -rm -f ./$(DEPDIR)/imap-login-commands.Po
+ -rm -f ./$(DEPDIR)/imap-login-settings.Po
+ -rm -f ./$(DEPDIR)/imap-proxy.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-pkglibexecPROGRAMS
+
+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)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/imap-login-client.Po
+ -rm -f ./$(DEPDIR)/imap-login-cmd-id.Po
+ -rm -f ./$(DEPDIR)/imap-login-commands.Po
+ -rm -f ./$(DEPDIR)/imap-login-settings.Po
+ -rm -f ./$(DEPDIR)/imap-proxy.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/imap-login/client-authenticate.c b/src/imap-login/client-authenticate.c
new file mode 100644
index 0000000..c8c3db2
--- /dev/null
+++ b/src/imap-login/client-authenticate.c
@@ -0,0 +1,213 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "net.h"
+#include "imap-resp-code.h"
+#include "imap-parser.h"
+#include "imap-url.h"
+#include "auth-client.h"
+#include "imap-login-client.h"
+#include "client-authenticate.h"
+#include "imap-proxy.h"
+
+
+void client_authenticate_get_capabilities(struct client *client, string_t *str)
+{
+ const struct auth_mech_desc *mech;
+ unsigned int i, count;
+
+ mech = sasl_server_get_advertised_mechs(client, &count);
+ for (i = 0; i < count; i++) {
+ str_append_c(str, ' ');
+ str_append(str, "AUTH=");
+ str_append(str, mech[i].name);
+ }
+}
+
+void imap_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply,
+ const char *text)
+{
+ struct imap_url url;
+ string_t *referral;
+
+ switch (result) {
+ case CLIENT_AUTH_RESULT_SUCCESS:
+ /* nothing to be done for IMAP */
+ break;
+ case CLIENT_AUTH_RESULT_REFERRAL_SUCCESS:
+ case CLIENT_AUTH_RESULT_REFERRAL_NOLOGIN:
+ /* IMAP referral
+
+ [nologin] referral host=.. [port=..] [destuser=..]
+ [reason=..]
+
+ NO [REFERRAL imap://destuser;AUTH=..@host:port/] Can't login.
+ OK [...] Logged in, but you should use this server instead.
+ .. [REFERRAL ..] (Reason from auth server)
+ */
+ referral = t_str_new(128);
+
+ i_zero(&url);
+ url.userid = reply->destuser;
+ url.auth_type = client->auth_mech_name;
+ url.host.name = reply->host;
+ if (reply->port != 143)
+ url.port = reply->port;
+ str_append(referral, "REFERRAL ");
+ str_append(referral, imap_url_create(&url));
+
+ if (result == CLIENT_AUTH_RESULT_REFERRAL_SUCCESS) {
+ client_send_reply_code(client, IMAP_CMD_REPLY_OK,
+ str_c(referral), text);
+ } else {
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ str_c(referral), text);
+ }
+ break;
+ case CLIENT_AUTH_RESULT_INVALID_BASE64:
+ case CLIENT_AUTH_RESULT_ABORTED:
+ client_send_reply(client, IMAP_CMD_REPLY_BAD, text);
+ break;
+ case CLIENT_AUTH_RESULT_AUTHFAILED_REASON:
+ case CLIENT_AUTH_RESULT_MECH_INVALID:
+ if (text[0] == '[')
+ client_send_reply(client, IMAP_CMD_REPLY_NO, text);
+ else {
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ "ALERT", text);
+ }
+ break;
+ case CLIENT_AUTH_RESULT_AUTHZFAILED:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_AUTHZFAILED, text);
+ break;
+ case CLIENT_AUTH_RESULT_TEMPFAIL:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_UNAVAILABLE, text);
+ break;
+ case CLIENT_AUTH_RESULT_SSL_REQUIRED:
+ case CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_PRIVACYREQUIRED, text);
+ break;
+ case CLIENT_AUTH_RESULT_PASS_EXPIRED:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_EXPIRED, text);
+ break;
+ case CLIENT_AUTH_RESULT_LOGIN_DISABLED:
+ case CLIENT_AUTH_RESULT_ANONYMOUS_DENIED:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_CONTACTADMIN, text);
+ break;
+ case CLIENT_AUTH_RESULT_AUTHFAILED:
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_AUTHFAILED, text);
+ break;
+ }
+}
+
+static int
+imap_client_auth_begin(struct imap_client *imap_client, const char *mech_name,
+ const char *init_resp)
+{
+ char *prefix;
+
+ prefix = i_strdup_printf("%d%s",
+ imap_client->client_ignores_capability_resp_code ? 1 : 0,
+ imap_client->cmd_tag);
+
+ i_free(imap_client->common.master_data_prefix);
+ imap_client->common.master_data_prefix = (void *)prefix;
+ imap_client->common.master_data_prefix_len = strlen(prefix)+1;
+
+ if (*init_resp == '\0')
+ init_resp = NULL;
+ else if (strcmp(init_resp, "=") == 0)
+ init_resp = "";
+ return client_auth_begin(&imap_client->common, mech_name, init_resp);
+}
+
+int cmd_authenticate(struct imap_client *imap_client, bool *parsed_r)
+{
+ /* NOTE: This command's input is handled specially because the
+ SASL-IR can be large. */
+ struct client *client = &imap_client->common;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ *parsed_r = FALSE;
+
+ /* <auth mechanism name> [<initial SASL response>] */
+ if (!imap_client->auth_mech_name_parsed) {
+ data = i_stream_get_data(client->input, &size);
+ for (i = 0; i < size; i++) {
+ if (data[i] == ' ' ||
+ data[i] == '\r' || data[i] == '\n')
+ break;
+ }
+ if (i == size)
+ return 0;
+ if (i == 0) {
+ /* empty mechanism name */
+ imap_client->skip_line = TRUE;
+ return -1;
+ }
+ i_free(client->auth_mech_name);
+ client->auth_mech_name = i_strndup(data, i);
+ imap_client->auth_mech_name_parsed = TRUE;
+ if (data[i] == ' ')
+ i++;
+ i_stream_skip(client->input, i);
+ }
+
+ /* get SASL-IR, if any */
+ if ((ret = client_auth_read_line(client)) <= 0)
+ return ret;
+
+ *parsed_r = TRUE;
+ imap_client->auth_mech_name_parsed = FALSE;
+ return imap_client_auth_begin(imap_client,
+ t_strdup(client->auth_mech_name),
+ t_strdup(str_c(client->auth_response)));
+}
+
+int cmd_login(struct imap_client *imap_client, const struct imap_arg *args)
+{
+ struct client *client = &imap_client->common;
+ const char *user, *pass;
+ string_t *plain_login, *base64;
+
+ /* two arguments: username and password */
+ if (!imap_arg_get_astring(&args[0], &user) ||
+ !imap_arg_get_astring(&args[1], &pass) ||
+ !IMAP_ARG_IS_EOL(&args[2]))
+ return -1;
+
+ if (!client_check_plaintext_auth(client, TRUE)) {
+ if (client->virtual_user == NULL)
+ client->virtual_user = i_strdup(user);
+ return 1;
+ }
+
+ /* authorization ID \0 authentication ID \0 pass */
+ plain_login = t_buffer_create(64);
+ buffer_append_c(plain_login, '\0');
+ buffer_append(plain_login, user, strlen(user));
+ buffer_append_c(plain_login, '\0');
+ buffer_append(plain_login, pass, strlen(pass));
+
+ base64 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(plain_login->used));
+ base64_encode(plain_login->data, plain_login->used, base64);
+ return imap_client_auth_begin(imap_client, "PLAIN", str_c(base64));
+}
diff --git a/src/imap-login/client-authenticate.h b/src/imap-login/client-authenticate.h
new file mode 100644
index 0000000..68e05ef
--- /dev/null
+++ b/src/imap-login/client-authenticate.h
@@ -0,0 +1,16 @@
+#ifndef CLIENT_AUTHENTICATE_H
+#define CLIENT_AUTHENTICATE_H
+
+struct imap_arg;
+
+void client_authenticate_get_capabilities(struct client *client, string_t *str);
+
+void imap_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply,
+ const char *text);
+
+int cmd_login(struct imap_client *client, const struct imap_arg *args);
+int cmd_authenticate(struct imap_client *imap_client, bool *parsed_r);
+
+#endif
diff --git a/src/imap-login/imap-login-client.c b/src/imap-login/imap-login-client.c
new file mode 100644
index 0000000..29ade52
--- /dev/null
+++ b/src/imap-login/imap-login-client.c
@@ -0,0 +1,571 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "imap-parser.h"
+#include "imap-id.h"
+#include "imap-resp-code.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "master-auth.h"
+#include "imap-login-client.h"
+#include "client-authenticate.h"
+#include "auth-client.h"
+#include "imap-proxy.h"
+#include "imap-quote.h"
+#include "imap-login-commands.h"
+#include "imap-login-settings.h"
+
+#if LOGIN_MAX_INBUF_SIZE < 1024+2
+# error LOGIN_MAX_INBUF_SIZE too short to fit all ID command parameters
+#endif
+
+/* Disconnect client when it sends too many bad commands */
+#define CLIENT_MAX_BAD_COMMANDS 3
+
+/* Skip incoming data until newline is found,
+ returns TRUE if newline was found. */
+bool client_skip_line(struct imap_client *client)
+{
+ const unsigned char *data;
+ size_t i, data_size;
+
+ data = i_stream_get_data(client->common.input, &data_size);
+
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\n') {
+ i_stream_skip(client->common.input, i+1);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+bool client_handle_parser_error(struct imap_client *client,
+ struct imap_parser *parser)
+{
+ const char *msg;
+ enum imap_parser_error parse_error;
+
+ msg = imap_parser_get_error(parser, &parse_error);
+ switch (parse_error) {
+ case IMAP_PARSE_ERROR_NONE:
+ i_unreached();
+ case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
+ client_send_reply(&client->common,
+ IMAP_CMD_REPLY_BYE, msg);
+ client_destroy(&client->common, msg);
+ return FALSE;
+ default:
+ break;
+ }
+
+ client_send_reply(&client->common, IMAP_CMD_REPLY_BAD, msg);
+ client->cmd_finished = TRUE;
+ client->skip_line = TRUE;
+ return TRUE;
+}
+
+static bool is_login_cmd_disabled(struct client *client)
+{
+ if (client->secured) {
+ if (sasl_server_find_available_mech(client, "PLAIN") == NULL) {
+ /* no PLAIN authentication, can't use LOGIN command */
+ return TRUE;
+ }
+ return FALSE;
+ }
+ if (client->set->disable_plaintext_auth)
+ return TRUE;
+ if (strcmp(client->ssl_set->ssl, "required") == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static const char *get_capability(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+ string_t *cap_str = t_str_new(256);
+ bool explicit_capability = FALSE;
+
+ if (*imap_client->set->imap_capability == '\0')
+ str_append(cap_str, CAPABILITY_BANNER_STRING);
+ else if (*imap_client->set->imap_capability != '+') {
+ explicit_capability = TRUE;
+ str_append(cap_str, imap_client->set->imap_capability);
+ } else {
+ str_append(cap_str, CAPABILITY_BANNER_STRING);
+ str_append_c(cap_str, ' ');
+ str_append(cap_str, imap_client->set->imap_capability + 1);
+ }
+
+ if (!explicit_capability) {
+ if (imap_client->set->imap_literal_minus)
+ str_append(cap_str, " LITERAL-");
+ else
+ str_append(cap_str, " LITERAL+");
+ }
+
+ if (client_is_tls_enabled(client) && !client->tls)
+ str_append(cap_str, " STARTTLS");
+ if (is_login_cmd_disabled(client))
+ str_append(cap_str, " LOGINDISABLED");
+
+ client_authenticate_get_capabilities(client, cap_str);
+ return str_c(cap_str);
+}
+
+static int cmd_capability(struct imap_client *imap_client,
+ const struct imap_arg *args ATTR_UNUSED)
+{
+ struct client *client = &imap_client->common;
+
+ /* Client is required to send CAPABILITY after STARTTLS, so the
+ capability resp-code workaround checks only pre-STARTTLS
+ CAPABILITY commands. */
+ if (!client->starttls)
+ imap_client->client_ignores_capability_resp_code = TRUE;
+ client_send_raw(client, t_strconcat(
+ "* CAPABILITY ", get_capability(client), "\r\n", NULL));
+ client_send_reply(client, IMAP_CMD_REPLY_OK,
+ "Pre-login capabilities listed, post-login capabilities have more.");
+ return 1;
+}
+
+static int cmd_starttls(struct imap_client *client,
+ const struct imap_arg *args ATTR_UNUSED)
+{
+ client_cmd_starttls(&client->common);
+ return 1;
+}
+
+static void
+imap_client_notify_starttls(struct client *client,
+ bool success, const char *text)
+{
+ if (success)
+ client_send_reply(client, IMAP_CMD_REPLY_OK, text);
+ else
+ client_send_reply(client, IMAP_CMD_REPLY_BAD, text);
+}
+
+static int cmd_noop(struct imap_client *client,
+ const struct imap_arg *args ATTR_UNUSED)
+{
+ client_send_reply(&client->common, IMAP_CMD_REPLY_OK,
+ "NOOP completed.");
+ return 1;
+}
+
+static int cmd_logout(struct imap_client *client,
+ const struct imap_arg *args ATTR_UNUSED)
+{
+ client_send_reply(&client->common, IMAP_CMD_REPLY_BYE, "Logging out");
+ client_send_reply(&client->common, IMAP_CMD_REPLY_OK,
+ "Logout completed.");
+ client_destroy(&client->common, CLIENT_UNAUTHENTICATED_LOGOUT_MSG);
+ return 1;
+}
+
+static int cmd_enable(struct imap_client *client,
+ const struct imap_arg *args ATTR_UNUSED)
+{
+ client_send_raw(&client->common, "* ENABLED\r\n");
+ client_send_reply(&client->common, IMAP_CMD_REPLY_OK,
+ "ENABLE ignored in non-authenticated state.");
+ return 1;
+}
+
+static int client_command_execute(struct imap_client *client, const char *cmd,
+ const struct imap_arg *args)
+{
+ struct imap_login_command *login_cmd;
+
+ login_cmd = imap_login_command_lookup(cmd);
+ if (login_cmd == NULL)
+ return -2;
+ return login_cmd->func(client, args);
+}
+
+static bool client_invalid_command(struct imap_client *client)
+{
+ if (client->cmd_tag == NULL || *client->cmd_tag == '\0')
+ client->cmd_tag = "*";
+ if (++client->common.bad_counter >= CLIENT_MAX_BAD_COMMANDS) {
+ client_send_reply(&client->common, IMAP_CMD_REPLY_BYE,
+ "Too many invalid IMAP commands.");
+ client_destroy(&client->common, "Too many invalid commands");
+ return FALSE;
+ }
+ client_send_reply(&client->common, IMAP_CMD_REPLY_BAD,
+ "Error in IMAP command received by server.");
+ return TRUE;
+}
+
+static int client_parse_command(struct imap_client *client,
+ const struct imap_arg **args_r)
+{
+ switch (imap_parser_read_args(client->parser, 0, 0, args_r)) {
+ case -1:
+ /* error */
+ if (!client_handle_parser_error(client, client->parser)) {
+ /* client destroyed */
+ return 0;
+ }
+ return -1;
+ case -2:
+ /* not enough data */
+ return 0;
+ default:
+ /* we read the entire line - skip over the CRLF */
+ if (!client_skip_line(client))
+ i_unreached();
+ return 1;
+ }
+}
+
+static bool client_handle_input(struct imap_client *client)
+{
+ const char *tag, *name;
+ int ret;
+
+ i_assert(!client->common.authenticating);
+
+ if (client->cmd_finished) {
+ /* clear the previous command from memory. don't do this
+ immediately after handling command since we need the
+ cmd_tag to stay some time after authentication commands. */
+ client->cmd_tag = NULL;
+ client->cmd_name = NULL;
+ imap_parser_reset(client->parser);
+
+ /* remove \r\n */
+ if (client->skip_line) {
+ if (!client_skip_line(client))
+ return FALSE;
+ client->skip_line = FALSE;
+ }
+
+ client->cmd_finished = FALSE;
+ }
+
+ if (client->cmd_tag == NULL) {
+ ret = imap_parser_read_tag(client->parser, &tag);
+ if (ret == 0)
+ return FALSE; /* need more data */
+ if (ret < 0 || strlen(tag) > IMAP_TAG_MAX_LEN) {
+ /* the tag is invalid, don't allow it and don't
+ send it back. this attempts to prevent any
+ potentially dangerous replies in case someone tries
+ to access us using HTTP protocol. */
+ client->skip_line = TRUE;
+ client->cmd_finished = TRUE;
+ if (!client_invalid_command(client))
+ return FALSE;
+ return client_handle_input(client);
+ }
+ client->cmd_tag = tag;
+ }
+
+ if (client->cmd_name == NULL) {
+ ret = imap_parser_read_command_name(client->parser, &name);
+ if (ret == 0)
+ return FALSE; /* need more data */
+ if (ret < 0) {
+ client->skip_line = TRUE;
+ client->cmd_finished = TRUE;
+ if (!client_invalid_command(client))
+ return FALSE;
+ return client_handle_input(client);
+ }
+ client->cmd_name = name;
+ }
+ return client->common.v.input_next_cmd(&client->common);
+}
+
+static bool imap_client_input_next_cmd(struct client *_client)
+{
+ struct imap_client *client = (struct imap_client *)_client;
+ const struct imap_arg *args;
+ bool parsed;
+ int ret;
+
+ if (strcasecmp(client->cmd_name, "AUTHENTICATE") == 0) {
+ /* SASL-IR may need more space than input buffer's size,
+ so we'll handle it as a special case. */
+ ret = cmd_authenticate(client, &parsed);
+ if (ret == 0 && !parsed)
+ return FALSE;
+ } else if (strcasecmp(client->cmd_name, "ID") == 0) {
+ /* ID extensions allows max. 30 parameters,
+ each max. 1024 bytes long. that brings us over the input
+ buffer's size, so handle the parameters one at a time */
+ ret = cmd_id(client);
+ if (ret == 0)
+ return FALSE;
+ if (ret < 0)
+ ret = 1; /* don't send the error reply again */
+ } else {
+ ret = client_parse_command(client, &args);
+ if (ret < 0)
+ return TRUE;
+ if (ret == 0)
+ return FALSE;
+ ret = *client->cmd_tag == '\0' ? -1 :
+ client_command_execute(client, client->cmd_name, args);
+ }
+
+ client->cmd_finished = TRUE;
+ if (ret == -2 && strcasecmp(client->cmd_tag, "LOGIN") == 0) {
+ client_send_reply(&client->common, IMAP_CMD_REPLY_BAD,
+ "First parameter in line is IMAP's command tag, "
+ "not the command name. Add that before the command, "
+ "like: a login user pass");
+ } else if (ret < 0) {
+ if (!client_invalid_command(client))
+ return FALSE;
+ }
+
+ return ret != 0 && !client->common.destroyed;
+}
+
+static void imap_client_input(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ if (!client_read(client))
+ return;
+
+ client_ref(client);
+ o_stream_cork(imap_client->common.output);
+ for (;;) {
+ if (!auth_client_is_connected(auth_client)) {
+ /* we're not currently connected to auth process -
+ don't allow any commands */
+ client_notify_status(client, FALSE,
+ AUTH_SERVER_WAITING_MSG);
+ timeout_remove(&client->to_auth_waiting);
+
+ client->input_blocked = TRUE;
+ break;
+ } else {
+ if (!client_handle_input(imap_client))
+ break;
+ }
+ }
+ o_stream_uncork(imap_client->common.output);
+ client_unref(&client);
+}
+
+static struct client *imap_client_alloc(pool_t pool)
+{
+ struct imap_client *imap_client;
+
+ imap_client = p_new(pool, struct imap_client, 1);
+ return &imap_client->common;
+}
+
+static void imap_client_create(struct client *client, void **other_sets)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ imap_client->set = other_sets[0];
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+ IMAP_LOGIN_MAX_LINE_LENGTH);
+ if (imap_client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(imap_client->parser);
+ client->io = io_add_istream(client->input, client_input, client);
+}
+
+static void imap_client_destroy(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ i_free_and_null(imap_client->proxy_backend_capability);
+ imap_parser_unref(&imap_client->parser);
+}
+
+static void imap_client_notify_auth_ready(struct client *client)
+{
+ string_t *greet;
+
+ greet = t_str_new(128);
+ str_append(greet, "* OK ");
+ str_printfa(greet, "[CAPABILITY %s] ", get_capability(client));
+ str_append(greet, client->set->login_greeting);
+ str_append(greet, "\r\n");
+
+ client_send_raw(client, str_c(greet));
+
+ client->banner_sent = TRUE;
+}
+
+static void imap_client_starttls(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ imap_parser_unref(&imap_client->parser);
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+ IMAP_LOGIN_MAX_LINE_LENGTH);
+
+ /* CRLF is lost from buffer when streams are reopened. */
+ imap_client->skip_line = FALSE;
+}
+
+static void ATTR_NULL(3)
+client_send_reply_raw(struct client *client,
+ const char *prefix, const char *resp_code,
+ const char *text, bool tagged)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ T_BEGIN {
+ string_t *line = t_str_new(256);
+
+ if (tagged)
+ str_append(line, imap_client->cmd_tag);
+ else
+ str_append_c(line, '*');
+ str_append_c(line, ' ');
+ str_append(line, prefix);
+ str_append_c(line, ' ');
+ if (resp_code != NULL)
+ str_printfa(line, "[%s] ", resp_code);
+ str_append(line, text);
+ str_append(line, "\r\n");
+
+ client_send_raw_data(client, str_data(line), str_len(line));
+ } T_END;
+}
+
+void client_send_reply_code(struct client *client, enum imap_cmd_reply reply,
+ const char *resp_code, const char *text)
+{
+ const char *prefix = "NO";
+ bool tagged = TRUE;
+
+ switch (reply) {
+ case IMAP_CMD_REPLY_OK:
+ prefix = "OK";
+ break;
+ case IMAP_CMD_REPLY_NO:
+ break;
+ case IMAP_CMD_REPLY_BAD:
+ prefix = "BAD";
+ break;
+ case IMAP_CMD_REPLY_BYE:
+ prefix = "BYE";
+ tagged = FALSE;
+ break;
+ }
+ client_send_reply_raw(client, prefix, resp_code, text, tagged);
+}
+
+void client_send_reply(struct client *client, enum imap_cmd_reply reply,
+ const char *text)
+{
+ client_send_reply_code(client, reply, NULL, text);
+}
+
+static void
+imap_client_notify_status(struct client *client, bool bad, const char *text)
+{
+ if (bad)
+ client_send_reply_raw(client, "BAD", "ALERT", text, FALSE);
+ else
+ client_send_reply_raw(client, "OK", NULL, text, FALSE);
+}
+
+static void
+imap_client_notify_disconnect(struct client *client,
+ enum client_disconnect_reason reason,
+ const char *text)
+{
+ if (reason == CLIENT_DISCONNECT_INTERNAL_ERROR) {
+ client_send_reply_code(client, IMAP_CMD_REPLY_BYE,
+ IMAP_RESP_CODE_UNAVAILABLE, text);
+ } else {
+ client_send_reply_code(client, IMAP_CMD_REPLY_BYE, NULL, text);
+ }
+}
+
+static void imap_login_preinit(void)
+{
+ login_set_roots = imap_login_setting_roots;
+}
+
+static const struct imap_login_command imap_login_commands[] = {
+ { "LOGIN", cmd_login },
+ { "CAPABILITY", cmd_capability },
+ { "STARTTLS", cmd_starttls },
+ { "NOOP", cmd_noop },
+ { "LOGOUT", cmd_logout },
+ { "ENABLE", cmd_enable }
+};
+
+static void imap_login_init(void)
+{
+ imap_login_commands_init();
+ imap_login_commands_register(imap_login_commands,
+ N_ELEMENTS(imap_login_commands));
+}
+
+static void imap_login_deinit(void)
+{
+ clients_destroy_all();
+ imap_login_commands_deinit();
+}
+
+static struct client_vfuncs imap_client_vfuncs = {
+ .alloc = imap_client_alloc,
+ .create = imap_client_create,
+ .destroy = imap_client_destroy,
+ .notify_auth_ready = imap_client_notify_auth_ready,
+ .notify_disconnect = imap_client_notify_disconnect,
+ .notify_status = imap_client_notify_status,
+ .notify_starttls = imap_client_notify_starttls,
+ .starttls = imap_client_starttls,
+ .input = imap_client_input,
+ .auth_result = imap_client_auth_result,
+ .proxy_reset = imap_proxy_reset,
+ .proxy_parse_line = imap_proxy_parse_line,
+ .proxy_failed = imap_proxy_failed,
+ .proxy_get_state = imap_proxy_get_state,
+ .send_raw_data = client_common_send_raw_data,
+ .input_next_cmd = imap_client_input_next_cmd,
+ .free = client_common_default_free,
+};
+
+static struct login_binary imap_login_binary = {
+ .protocol = "imap",
+ .process_name = "imap-login",
+ .default_port = 143,
+ .default_ssl_port = 993,
+
+ .event_category = {
+ .name = "imap",
+ },
+
+ .client_vfuncs = &imap_client_vfuncs,
+ .preinit = imap_login_preinit,
+ .init = imap_login_init,
+ .deinit = imap_login_deinit,
+
+ .sasl_support_final_reply = FALSE,
+ .anonymous_login_acceptable = TRUE,
+};
+
+int main(int argc, char *argv[])
+{
+ return login_binary_run(&imap_login_binary, argc, argv);
+}
diff --git a/src/imap-login/imap-login-client.h b/src/imap-login/imap-login-client.h
new file mode 100644
index 0000000..002829b
--- /dev/null
+++ b/src/imap-login/imap-login-client.h
@@ -0,0 +1,97 @@
+#ifndef IMAP_LOGIN_CLIENT_H
+#define IMAP_LOGIN_CLIENT_H
+
+#include "net.h"
+#include "imap-id.h"
+#include "client-common.h"
+
+/* Master prefix is: <1|0><imap tag><NUL> */
+#define IMAP_TAG_MAX_LEN (LOGIN_MAX_MASTER_PREFIX_LEN-2)
+
+/* maximum length for IMAP command line. */
+#define IMAP_LOGIN_MAX_LINE_LENGTH 8192
+
+enum imap_client_id_state {
+ IMAP_CLIENT_ID_STATE_LIST = 0,
+ IMAP_CLIENT_ID_STATE_KEY,
+ IMAP_CLIENT_ID_STATE_VALUE
+};
+
+/* Multiple commands can be sent pipelined, so the sent_state is a bitmask */
+enum imap_proxy_sent_state {
+ IMAP_PROXY_SENT_STATE_ID = 0x01,
+ IMAP_PROXY_SENT_STATE_STARTTLS = 0x02,
+ IMAP_PROXY_SENT_STATE_CAPABILITY = 0x04,
+ IMAP_PROXY_SENT_STATE_AUTHENTICATE = 0x08,
+ IMAP_PROXY_SENT_STATE_AUTH_CONTINUE = 0x10,
+ IMAP_PROXY_SENT_STATE_LOGIN = 0x20,
+
+ IMAP_PROXY_SENT_STATE_COUNT = 6
+};
+
+enum imap_proxy_rcvd_state {
+ IMAP_PROXY_RCVD_STATE_NONE,
+ IMAP_PROXY_RCVD_STATE_BANNER,
+ IMAP_PROXY_RCVD_STATE_ID,
+ IMAP_PROXY_RCVD_STATE_STARTTLS,
+ IMAP_PROXY_RCVD_STATE_CAPABILITY,
+ IMAP_PROXY_RCVD_STATE_AUTH_CONTINUE,
+ IMAP_PROXY_RCVD_STATE_LOGIN,
+
+ IMAP_PROXY_RCVD_STATE_COUNT
+};
+
+struct imap_client_cmd_id {
+ struct imap_parser *parser;
+
+ enum imap_client_id_state state;
+ char key[IMAP_ID_KEY_MAX_LEN+1];
+
+ char **log_keys;
+ string_t *log_reply;
+};
+
+struct imap_client {
+ struct client common;
+
+ const struct imap_login_settings *set;
+ struct imap_parser *parser;
+ char *proxy_backend_capability;
+
+ const char *cmd_tag, *cmd_name;
+ struct imap_client_cmd_id *cmd_id;
+
+ enum imap_proxy_sent_state proxy_sent_state;
+ enum imap_proxy_rcvd_state proxy_rcvd_state;
+
+ bool cmd_finished:1;
+ bool proxy_sasl_ir:1;
+ bool proxy_logindisabled:1;
+ bool proxy_seen_banner:1;
+ bool skip_line:1;
+ bool id_logged:1;
+ bool proxy_capability_request_sent:1;
+ bool client_ignores_capability_resp_code:1;
+ bool auth_mech_name_parsed:1;
+};
+
+bool client_skip_line(struct imap_client *client);
+
+enum imap_cmd_reply {
+ IMAP_CMD_REPLY_OK,
+ IMAP_CMD_REPLY_NO,
+ IMAP_CMD_REPLY_BAD,
+ IMAP_CMD_REPLY_BYE
+};
+
+void client_send_reply(struct client *client,
+ enum imap_cmd_reply reply, const char *text);
+void client_send_reply_code(struct client *client,
+ enum imap_cmd_reply reply, const char *resp_code,
+ const char *text) ATTR_NULL(3);
+bool client_handle_parser_error(struct imap_client *client,
+ struct imap_parser *parser);
+
+int cmd_id(struct imap_client *client);
+
+#endif
diff --git a/src/imap-login/imap-login-cmd-id.c b/src/imap-login/imap-login-cmd-id.c
new file mode 100644
index 0000000..a1c6294
--- /dev/null
+++ b/src/imap-login/imap-login-cmd-id.c
@@ -0,0 +1,281 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "str.h"
+#include "imap-parser.h"
+#include "imap-quote.h"
+#include "imap-login-settings.h"
+#include "imap-login-client.h"
+
+struct imap_id_param_handler {
+ const char *key;
+ bool key_is_prefix;
+
+ void (*callback)(struct imap_client *client,
+ const char *key, const char *value);
+};
+
+static void
+cmd_id_x_originating_ip(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ (void)net_addr2ip(value, &client->common.ip);
+}
+
+static void
+cmd_id_x_originating_port(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ (void)net_str2port(value, &client->common.remote_port);
+}
+
+static void
+cmd_id_x_connected_ip(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ (void)net_addr2ip(value, &client->common.local_ip);
+}
+
+static void
+cmd_id_x_connected_port(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ (void)net_str2port(value, &client->common.local_port);
+}
+
+static void
+cmd_id_x_proxy_ttl(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ if (str_to_uint(value, &client->common.proxy_ttl) < 0) {
+ /* nothing */
+ }
+}
+
+static void
+cmd_id_x_session_id(struct imap_client *client,
+ const char *key ATTR_UNUSED, const char *value)
+{
+ if (strlen(value) <= LOGIN_MAX_SESSION_ID_LEN) {
+ client->common.session_id =
+ p_strdup(client->common.pool, value);
+ }
+}
+
+static void
+cmd_id_x_forward_(struct imap_client *client,
+ const char *key, const char *value)
+{
+ i_assert(strncasecmp(key, "x-forward-", 10) == 0);
+ client_add_forward_field(&client->common, key+10, value);
+}
+
+static const struct imap_id_param_handler imap_login_id_params[] = {
+ { "x-originating-ip", FALSE, cmd_id_x_originating_ip },
+ { "x-originating-port", FALSE, cmd_id_x_originating_port },
+ { "x-connected-ip", FALSE, cmd_id_x_connected_ip },
+ { "x-connected-port", FALSE, cmd_id_x_connected_port },
+ { "x-proxy-ttl", FALSE, cmd_id_x_proxy_ttl },
+ { "x-session-id", FALSE, cmd_id_x_session_id },
+ { "x-session-ext-id", FALSE, cmd_id_x_session_id },
+ { "x-forward-", TRUE, cmd_id_x_forward_ },
+
+ { NULL, FALSE, NULL }
+};
+
+static const struct imap_id_param_handler *
+imap_id_param_handler_find(const char *key)
+{
+ for (unsigned int i = 0; imap_login_id_params[i].key != NULL; i++) {
+ unsigned int prefix_len = strlen(imap_login_id_params[i].key);
+
+ if (strncasecmp(imap_login_id_params[i].key, key, prefix_len) == 0 &&
+ (key[prefix_len] == '\0' ||
+ imap_login_id_params[i].key_is_prefix))
+ return &imap_login_id_params[i];
+ }
+ return NULL;
+}
+
+static bool
+client_try_update_info(struct imap_client *client,
+ const char *key, const char *value)
+{
+ const struct imap_id_param_handler *handler;
+
+ handler = imap_id_param_handler_find(key);
+ if (handler == NULL)
+ return FALSE;
+
+ /* do not try to process NIL values as client-info,
+ but store them for non-reserved keys */
+ if (client->common.trusted && !client->id_logged && value != NULL)
+ handler->callback(client, key, value);
+ return TRUE;
+}
+
+static void cmd_id_handle_keyvalue(struct imap_client *client,
+ const char *key, const char *value)
+{
+ bool client_id_str;
+ /* length of key + length of value (NIL for NULL) and two set of
+ quotes and space */
+ size_t kvlen = strlen(key) + 2 + 1 +
+ (value == NULL ? 3 : strlen(value)) + 2;
+
+ client_id_str = !client_try_update_info(client, key, value);
+
+ if (client->set->imap_id_retain && client_id_str &&
+ (client->common.client_id == NULL ||
+ str_len(client->common.client_id) + kvlen < LOGIN_MAX_CLIENT_ID_LEN)) {
+ if (client->common.client_id == NULL) {
+ client->common.client_id = str_new(client->common.preproxy_pool, 64);
+ } else {
+ str_append_c(client->common.client_id, ' ');
+ }
+ imap_append_quoted(client->common.client_id, key);
+ str_append_c(client->common.client_id, ' ');
+ if (value == NULL)
+ str_append(client->common.client_id, "NIL");
+ else
+ imap_append_quoted(client->common.client_id, value);
+ }
+
+ if (client->cmd_id->log_reply != NULL &&
+ (client->cmd_id->log_keys == NULL ||
+ str_array_icase_find((void *)client->cmd_id->log_keys, key)))
+ imap_id_log_reply_append(client->cmd_id->log_reply, key, value);
+}
+
+static int cmd_id_handle_args(struct imap_client *client,
+ const struct imap_arg *arg)
+{
+ struct imap_client_cmd_id *id = client->cmd_id;
+ const char *key, *value;
+
+ switch (id->state) {
+ case IMAP_CLIENT_ID_STATE_LIST:
+ if (arg->type == IMAP_ARG_NIL)
+ return 1;
+ if (arg->type != IMAP_ARG_LIST)
+ return -1;
+ if (client->set->imap_id_log[0] == '\0') {
+ /* no ID logging */
+ } else if (client->id_logged) {
+ /* already logged the ID reply */
+ } else {
+ id->log_reply = str_new(default_pool, 64);
+ if (strcmp(client->set->imap_id_log, "*") == 0) {
+ /* log all keys */
+ } else {
+ /* log only specified keys */
+ id->log_keys = p_strsplit_spaces(default_pool,
+ client->set->imap_id_log, " ");
+ }
+ }
+ id->state = IMAP_CLIENT_ID_STATE_KEY;
+ break;
+ case IMAP_CLIENT_ID_STATE_KEY:
+ if (!imap_arg_get_string(arg, &key))
+ return -1;
+ if (i_strocpy(id->key, key, sizeof(id->key)) < 0)
+ return -1;
+ id->state = IMAP_CLIENT_ID_STATE_VALUE;
+ break;
+ case IMAP_CLIENT_ID_STATE_VALUE:
+ if (!imap_arg_get_nstring(arg, &value))
+ return -1;
+ cmd_id_handle_keyvalue(client, id->key, value);
+ id->state = IMAP_CLIENT_ID_STATE_KEY;
+ break;
+ }
+ return 0;
+}
+
+static void cmd_id_finish(struct imap_client *client)
+{
+ /* finished handling the parameters */
+ if (!client->id_logged) {
+ client->id_logged = TRUE;
+
+ if (client->cmd_id->log_reply != NULL) {
+ e_info(client->common.event, "ID sent: %s",
+ str_c(client->cmd_id->log_reply));
+ }
+ }
+
+ client_send_raw(&client->common,
+ t_strdup_printf("* ID %s\r\n",
+ imap_id_reply_generate(client->set->imap_id_send)));
+ client_send_reply(&client->common, IMAP_CMD_REPLY_OK, "ID completed.");
+}
+
+static void cmd_id_free(struct imap_client *client)
+{
+ struct imap_client_cmd_id *id = client->cmd_id;
+
+ str_free(&id->log_reply);
+ if (id->log_keys != NULL)
+ p_strsplit_free(default_pool, id->log_keys);
+ imap_parser_unref(&id->parser);
+
+ i_free_and_null(client->cmd_id);
+ client->skip_line = TRUE;
+}
+
+int cmd_id(struct imap_client *client)
+{
+ struct imap_client_cmd_id *id;
+ enum imap_parser_flags parser_flags;
+ const struct imap_arg *args;
+ int ret;
+
+ if (client->common.client_id != NULL)
+ str_truncate(client->common.client_id, 0);
+
+ if (client->cmd_id == NULL) {
+ client->cmd_id = id = i_new(struct imap_client_cmd_id, 1);
+ id->parser = imap_parser_create(client->common.input,
+ client->common.output,
+ IMAP_LOGIN_MAX_LINE_LENGTH);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(id->parser);
+ parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST;
+ } else {
+ id = client->cmd_id;
+ parser_flags = IMAP_PARSE_FLAG_INSIDE_LIST;
+ }
+
+ while ((ret = imap_parser_read_args(id->parser, 1, parser_flags, &args)) > 0) {
+ i_assert(ret == 1);
+
+ if ((ret = cmd_id_handle_args(client, args)) < 0) {
+ client_send_reply(&client->common,
+ IMAP_CMD_REPLY_BAD,
+ "Invalid ID parameters");
+ cmd_id_free(client);
+ return -1;
+ }
+ if (ret > 0) {
+ /* NIL parameter */
+ ret = 0;
+ break;
+ }
+ imap_parser_reset(id->parser);
+ parser_flags = IMAP_PARSE_FLAG_INSIDE_LIST;
+ }
+ if (ret == 0) {
+ /* finished the line */
+ cmd_id_finish(client);
+ cmd_id_free(client);
+ return 1;
+ } else if (ret == -1) {
+ if (!client_handle_parser_error(client, id->parser))
+ return 0;
+ cmd_id_free(client);
+ return -1;
+ } else {
+ i_assert(ret == -2);
+ return 0;
+ }
+}
diff --git a/src/imap-login/imap-login-commands.c b/src/imap-login/imap-login-commands.c
new file mode 100644
index 0000000..8d4878d
--- /dev/null
+++ b/src/imap-login/imap-login-commands.c
@@ -0,0 +1,70 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "array.h"
+#include "imap-login-commands.h"
+
+static ARRAY(struct imap_login_command *) imap_login_commands;
+static pool_t imap_login_commands_pool;
+
+struct imap_login_command *imap_login_command_lookup(const char *name)
+{
+ struct imap_login_command *cmd;
+
+ array_foreach_elem(&imap_login_commands, cmd) {
+ if (strcasecmp(cmd->name, name) == 0)
+ return cmd;
+ }
+ return NULL;
+}
+
+void imap_login_commands_register(const struct imap_login_command *commands,
+ unsigned int count)
+{
+ struct imap_login_command *cmd;
+ unsigned int i;
+
+ for (i = 0; i < count; i++) {
+ cmd = p_new(imap_login_commands_pool, struct imap_login_command, 1);
+ cmd->name = p_strdup(imap_login_commands_pool, commands[i].name);
+ cmd->func = commands[i].func;
+ array_push_back(&imap_login_commands, &cmd);
+ }
+}
+
+static void
+imap_login_command_unregister(const struct imap_login_command *unreg_cmd)
+{
+ struct imap_login_command *const *cmdp;
+
+ array_foreach(&imap_login_commands, cmdp) {
+ if ((*cmdp)->func == unreg_cmd->func &&
+ strcmp((*cmdp)->name, unreg_cmd->name) == 0) {
+ array_delete(&imap_login_commands,
+ array_foreach_idx(&imap_login_commands, cmdp), 1);
+ return;
+ }
+ }
+ i_panic("imap_login_command_unregister: Command '%s' not found", unreg_cmd->name);
+}
+
+void imap_login_commands_unregister(const struct imap_login_command *commands,
+ unsigned int count)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ imap_login_command_unregister(&commands[i]);
+}
+
+void imap_login_commands_init(void)
+{
+ imap_login_commands_pool =
+ pool_alloconly_create("imap login commands", 128);
+ p_array_init(&imap_login_commands, imap_login_commands_pool, 8);
+}
+
+void imap_login_commands_deinit(void)
+{
+ pool_unref(&imap_login_commands_pool);
+}
diff --git a/src/imap-login/imap-login-commands.h b/src/imap-login/imap-login-commands.h
new file mode 100644
index 0000000..d9ecd1c
--- /dev/null
+++ b/src/imap-login/imap-login-commands.h
@@ -0,0 +1,25 @@
+#ifndef IMAP_LOGIN_COMMANDS_H
+#define IMAP_LOGIN_COMMANDS_H
+
+struct imap_arg;
+struct imap_client;
+
+typedef int imap_login_command_t(struct imap_client *client,
+ const struct imap_arg *args);
+
+struct imap_login_command {
+ const char *name;
+ imap_login_command_t *func;
+};
+
+struct imap_login_command *imap_login_command_lookup(const char *name);
+
+void imap_login_commands_register(const struct imap_login_command *commands,
+ unsigned int count);
+void imap_login_commands_unregister(const struct imap_login_command *commands,
+ unsigned int count);
+
+void imap_login_commands_init(void);
+void imap_login_commands_deinit(void);
+
+#endif
diff --git a/src/imap-login/imap-login-settings.c b/src/imap-login/imap-login-settings.c
new file mode 100644
index 0000000..74ab65c
--- /dev/null
+++ b/src/imap-login/imap-login-settings.c
@@ -0,0 +1,95 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "login-settings.h"
+#include "imap-login-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct inet_listener_settings imap_login_inet_listeners_array[] = {
+ { .name = "imap", .address = "", .port = 143 },
+ { .name = "imaps", .address = "", .port = 993, .ssl = TRUE }
+};
+static struct inet_listener_settings *imap_login_inet_listeners[] = {
+ &imap_login_inet_listeners_array[0],
+ &imap_login_inet_listeners_array[1]
+};
+static buffer_t imap_login_inet_listeners_buf = {
+ { { imap_login_inet_listeners, sizeof(imap_login_inet_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_login_service_settings = {
+ .name = "imap-login",
+ .protocol = "imap",
+ .type = "login",
+ .executable = "imap-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &imap_login_inet_listeners_buf,
+ sizeof(imap_login_inet_listeners[0]) } }
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_login_settings)
+
+static const struct setting_define imap_login_setting_defines[] = {
+ DEF(STR, imap_capability),
+ DEF(STR, imap_id_send),
+ DEF(STR, imap_id_log),
+ DEF(BOOL, imap_literal_minus),
+ DEF(BOOL, imap_id_retain),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct imap_login_settings imap_login_default_settings = {
+ .imap_capability = "",
+ .imap_id_send = "name *",
+ .imap_id_log = "",
+ .imap_literal_minus = FALSE,
+ .imap_id_retain = FALSE,
+};
+
+static const struct setting_parser_info *imap_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+
+static const struct setting_parser_info imap_login_setting_parser_info = {
+ .module_name = "imap-login",
+ .defines = imap_login_setting_defines,
+ .defaults = &imap_login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_login_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = imap_login_setting_dependencies
+};
+
+const struct setting_parser_info *imap_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &imap_login_setting_parser_info,
+ NULL
+};
diff --git a/src/imap-login/imap-login-settings.h b/src/imap-login/imap-login-settings.h
new file mode 100644
index 0000000..51ce1a8
--- /dev/null
+++ b/src/imap-login/imap-login-settings.h
@@ -0,0 +1,14 @@
+#ifndef IMAP_LOGIN_SETTINGS_H
+#define IMAP_LOGIN_SETTINGS_H
+
+struct imap_login_settings {
+ const char *imap_capability;
+ const char *imap_id_send;
+ const char *imap_id_log;
+ bool imap_literal_minus;
+ bool imap_id_retain;
+};
+
+extern const struct setting_parser_info *imap_login_setting_roots[];
+
+#endif
diff --git a/src/imap-login/imap-proxy.c b/src/imap-login/imap-proxy.c
new file mode 100644
index 0000000..aa5a5aa
--- /dev/null
+++ b/src/imap-login/imap-proxy.c
@@ -0,0 +1,530 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "safe-memset.h"
+#include "dsasl-client.h"
+#include "imap-login-client.h"
+#include "client-authenticate.h"
+#include "imap-resp-code.h"
+#include "imap-quote.h"
+#include "imap-proxy.h"
+
+static const char *imap_proxy_sent_state_names[IMAP_PROXY_SENT_STATE_COUNT] = {
+ "id", "starttls", "capability",
+ "authenticate", "auth-continue", "login"
+};
+static const char *imap_proxy_rcvd_state_names[IMAP_PROXY_RCVD_STATE_COUNT] = {
+ "none", "banner", "id", "starttls", "capability",
+ "auth-continue", "login"
+};
+
+static void proxy_write_id(struct imap_client *client, string_t *str)
+{
+ i_assert(client->common.proxy_ttl > 1);
+
+ str_append(str, "I ID (");
+ if (client->common.client_id != NULL &&
+ str_len(client->common.client_id) > 0) {
+ str_append_str(str, client->common.client_id);
+ str_append_c(str, ' ');
+ }
+ str_printfa(str, "\"x-session-id\" \"%s\" "
+ "\"x-originating-ip\" \"%s\" "
+ "\"x-originating-port\" \"%u\" "
+ "\"x-connected-ip\" \"%s\" "
+ "\"x-connected-port\" \"%u\" "
+ "\"x-proxy-ttl\" \"%u\"",
+ client_get_session_id(&client->common),
+ net_ip2addr(&client->common.ip),
+ client->common.remote_port,
+ net_ip2addr(&client->common.local_ip),
+ client->common.local_port,
+ client->common.proxy_ttl - 1);
+
+ /* append any forward_ variables to request */
+ for(const char *const *ptr = client->common.auth_passdb_args; *ptr != NULL; ptr++) {
+ if (strncasecmp(*ptr, "forward_", 8) == 0) {
+ const char *key = t_strconcat("x-forward-",
+ t_strcut((*ptr)+8, '='),
+ NULL);
+ const char *val = i_strchr_to_next(*ptr, '=');
+ str_append_c(str, ' ');
+ imap_append_string(str, key);
+ str_append_c(str, ' ');
+ imap_append_nstring(str, val);
+ }
+ }
+
+ str_append(str, ")\r\n");
+}
+
+static int proxy_write_starttls(struct imap_client *client, string_t *str)
+{
+ enum login_proxy_ssl_flags ssl_flags = login_proxy_get_ssl_flags(client->common.login_proxy);
+ if ((ssl_flags & PROXY_SSL_FLAG_STARTTLS) != 0) {
+ if (client->proxy_backend_capability != NULL &&
+ !str_array_icase_find(t_strsplit(client->proxy_backend_capability, " "), "STARTTLS")) {
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ "STARTTLS not supported");
+ return -1;
+ }
+ str_append(str, "S STARTTLS\r\n");
+ client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_STARTTLS;
+ return 1;
+ }
+ return 0;
+}
+
+static int proxy_write_login(struct imap_client *client, string_t *str)
+{
+ struct dsasl_client_settings sasl_set;
+ const unsigned char *output;
+ size_t len;
+ const char *mech_name, *error;
+
+ /* Send CAPABILITY command if we don't know the capabilities yet.
+ Also as kind of a Dovecot-backend workaround if the client insisted
+ on sending CAPABILITY command (even though our banner already sent
+ it), send the (unnecessary) CAPABILITY command to backend as well
+ to avoid sending the CAPABILITY reply twice (untagged and OK resp
+ code). */
+ if (!client->proxy_capability_request_sent &&
+ (client->proxy_backend_capability == NULL ||
+ client->client_ignores_capability_resp_code)) {
+ client->proxy_capability_request_sent = TRUE;
+ client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_CAPABILITY;
+ str_append(str, "C CAPABILITY\r\n");
+ if (client->common.proxy_nopipelining) {
+ /* authenticate only after receiving C OK reply. */
+ return 0;
+ }
+ }
+
+ if (client->common.proxy_mech == NULL) {
+ /* logging in normally - use LOGIN command */
+ if (client->proxy_logindisabled &&
+ login_proxy_get_ssl_flags(client->common.login_proxy) == 0) {
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ "LOGINDISABLED advertised, but SSL/TLS not enabled");
+ return -1;
+ }
+ str_append(str, "L LOGIN ");
+ imap_append_string(str, client->common.proxy_user);
+ str_append_c(str, ' ');
+ imap_append_string(str, client->common.proxy_password);
+ str_append(str, "\r\n");
+
+ client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_LOGIN;
+ return 0;
+ }
+
+ i_assert(client->common.proxy_sasl_client == NULL);
+ i_zero(&sasl_set);
+ sasl_set.authid = client->common.proxy_master_user != NULL ?
+ client->common.proxy_master_user : client->common.proxy_user;
+ sasl_set.authzid = client->common.proxy_user;
+ sasl_set.password = client->common.proxy_password;
+ client->common.proxy_sasl_client =
+ dsasl_client_new(client->common.proxy_mech, &sasl_set);
+ mech_name = dsasl_client_mech_get_name(client->common.proxy_mech);
+
+ str_append(str, "L AUTHENTICATE ");
+ str_append(str, mech_name);
+ if (client->proxy_sasl_ir) {
+ if (dsasl_client_output(client->common.proxy_sasl_client,
+ &output, &len, &error) < 0) {
+ const char *reason = t_strdup_printf(
+ "SASL mechanism %s init failed: %s",
+ mech_name, error);
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ return -1;
+ }
+ str_append_c(str, ' ');
+ if (len == 0)
+ str_append_c(str, '=');
+ else
+ base64_encode(output, len, str);
+ }
+ str_append(str, "\r\n");
+ client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_AUTHENTICATE;
+ return 0;
+}
+
+static int proxy_input_banner(struct imap_client *client,
+ struct ostream *output, const char *line)
+{
+ const char *const *capabilities = NULL;
+ string_t *str;
+ int ret;
+
+ if (!str_begins(line, "* OK ")) {
+ const char *reason = t_strdup_printf("Invalid banner: %s",
+ str_sanitize(line, 160));
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+
+ str = t_str_new(128);
+ if (str_begins(line + 5, "[CAPABILITY ")) {
+ capabilities = t_strsplit(t_strcut(line + 5 + 12, ']'), " ");
+ if (str_array_icase_find(capabilities, "SASL-IR"))
+ client->proxy_sasl_ir = TRUE;
+ if (str_array_icase_find(capabilities, "LOGINDISABLED"))
+ client->proxy_logindisabled = TRUE;
+ i_free(client->proxy_backend_capability);
+ client->proxy_backend_capability =
+ i_strdup(t_strcut(line + 5 + 12, ']'));
+ if (str_array_icase_find(capabilities, "ID") &&
+ !client->common.proxy_not_trusted) {
+ client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_ID;
+ proxy_write_id(client, str);
+ if (client->common.proxy_nopipelining) {
+ /* write login or starttls after I OK */
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+ }
+ }
+ }
+
+ if ((ret = proxy_write_starttls(client, str)) < 0) {
+ return -1;
+ } else if (ret == 0) {
+ if (proxy_write_login(client, str) < 0)
+ return -1;
+ }
+
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+}
+
+static void
+client_send_login_reply(struct imap_client *client, string_t *str,
+ const char *line)
+{
+ const char *capability;
+ bool tagged_capability;
+
+ capability = client->proxy_backend_capability;
+ tagged_capability = strncasecmp(line, "[CAPABILITY ", 12) == 0;
+ if (tagged_capability)
+ capability = t_strcut(line + 12, ']');
+
+ if (client->client_ignores_capability_resp_code && capability != NULL) {
+ /* client has used CAPABILITY command, so it didn't understand
+ the capabilities in the banner. send the backend's untagged
+ CAPABILITY reply and hope that the client understands it */
+ str_printfa(str, "* CAPABILITY %s\r\n", capability);
+ }
+ str_append(str, client->cmd_tag);
+ str_append(str, " OK ");
+ if (!client->client_ignores_capability_resp_code &&
+ !tagged_capability && capability != NULL) {
+ str_printfa(str, "[CAPABILITY %s] ", capability);
+ if (*line == '[') {
+ /* we need to send the capability.
+ skip over this resp-code */
+ while (*line != ']' && *line != '\0')
+ line++;
+ if (*line == ' ') line++;
+ }
+ }
+ str_append(str, line);
+ str_append(str, "\r\n");
+}
+
+static bool auth_resp_code_is_tempfail(const char *resp_code)
+{
+ /* Dovecot uses [UNAVAILABLE] for failures that can be retried.
+ Non-retriable failures are [SERVERBUG]. */
+ return strcasecmp(resp_code, IMAP_RESP_CODE_UNAVAILABLE) == 0;
+}
+
+int imap_proxy_parse_line(struct client *client, const char *line)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+ struct ostream *output;
+ string_t *str;
+ const unsigned char *data;
+ size_t data_len;
+ const char *error;
+ int ret;
+
+ i_assert(!client->destroyed);
+
+ output = login_proxy_get_ostream(client->login_proxy);
+ if (!imap_client->proxy_seen_banner) {
+ /* this is a banner */
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_BANNER;
+ imap_client->proxy_seen_banner = TRUE;
+ if (proxy_input_banner(imap_client, output, line) < 0)
+ return -1;
+ return 0;
+ } else if (*line == '+') {
+ /* AUTHENTICATE started. finish it. */
+ if (client->proxy_sasl_client == NULL) {
+ /* used literals with LOGIN command, just ignore. */
+ return 0;
+ }
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_AUTHENTICATE);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_AUTH_CONTINUE;
+
+ str = t_str_new(128);
+ if (line[1] != ' ' ||
+ base64_decode(line+2, strlen(line+2), NULL, str) < 0) {
+ const char *reason = t_strdup_printf(
+ "Invalid base64 data in AUTHENTICATE response");
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ ret = dsasl_client_input(client->proxy_sasl_client,
+ str_data(str), str_len(str), &error);
+ if (ret == 0) {
+ ret = dsasl_client_output(client->proxy_sasl_client,
+ &data, &data_len, &error);
+ }
+ if (ret < 0) {
+ const char *reason = t_strdup_printf(
+ "Invalid authentication data: %s", error);
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ i_assert(ret == 0);
+
+ str_truncate(str, 0);
+ base64_encode(data, data_len, str);
+ str_append(str, "\r\n");
+
+ imap_client->proxy_sent_state |= IMAP_PROXY_SENT_STATE_AUTH_CONTINUE;
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+ } else if (str_begins(line, "S ")) {
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_STARTTLS);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_STARTTLS;
+
+ if (!str_begins(line, "S OK ")) {
+ /* STARTTLS failed */
+ const char *reason = t_strdup_printf(
+ "STARTTLS failed: %s",
+ str_sanitize(line + 5, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE, reason);
+ return -1;
+ }
+ /* STARTTLS successful, begin TLS negotiation. */
+ if (login_proxy_starttls(client->login_proxy) < 0)
+ return -1;
+ /* i/ostreams changed. */
+ output = login_proxy_get_ostream(client->login_proxy);
+ str = t_str_new(128);
+ if (proxy_write_login(imap_client, str) < 0)
+ return -1;
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 1;
+ } else if (str_begins(line, "L OK ")) {
+ /* Login successful. Send this line to client. */
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_LOGIN);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_LOGIN;
+ str = t_str_new(128);
+ client_send_login_reply(imap_client, str, line + 5);
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+
+ client_proxy_finish_destroy_client(client);
+ return 1;
+ } else if (str_begins(line, "L ")) {
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_LOGIN);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_LOGIN;
+
+ line += 2;
+ const char *log_line = line;
+ if (strncasecmp(log_line, "NO ", 3) == 0)
+ log_line += 3;
+ enum login_proxy_failure_type failure_type =
+ LOGIN_PROXY_FAILURE_TYPE_AUTH;
+#define STR_NO_IMAP_RESP_CODE_AUTHFAILED "NO ["IMAP_RESP_CODE_AUTHFAILED"]"
+ if (str_begins(line, STR_NO_IMAP_RESP_CODE_AUTHFAILED)) {
+ /* the remote sent a generic "authentication failed"
+ error. replace it with our one, so that in case
+ the remote is sending a different error message
+ an attacker can't find out what users exist in
+ the system. */
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_AUTHFAILED,
+ AUTH_FAILED_MSG);
+ } else if (str_begins(line, "NO [")) {
+ /* remote sent some other resp-code. forward it. */
+ const char *resp_code = t_strcut(line + 4, ']');
+ if (auth_resp_code_is_tempfail(resp_code))
+ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL;
+ else {
+ client_send_raw(client, t_strconcat(
+ imap_client->cmd_tag, " ", line, "\r\n", NULL));
+ }
+ } else {
+ /* there was no [resp-code], so remote isn't Dovecot
+ v1.2+. we could either forward the line as-is and
+ leak information about what users exist in this
+ system, or we could hide other errors than password
+ failures. since other errors are pretty rare,
+ it's safer to just hide them. they're still
+ available in logs though. */
+ client_send_reply_code(client, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_AUTHFAILED,
+ AUTH_FAILED_MSG);
+ }
+
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ failure_type, log_line);
+ return -1;
+ } else if (strncasecmp(line, "* CAPABILITY ", 13) == 0) {
+ i_free(imap_client->proxy_backend_capability);
+ imap_client->proxy_backend_capability = i_strdup(line + 13);
+ return 0;
+ } else if (str_begins(line, "C ")) {
+ /* Reply to CAPABILITY command we sent */
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_CAPABILITY);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_CAPABILITY;
+ if (str_begins(line, "C OK ") &&
+ HAS_NO_BITS(imap_client->proxy_sent_state,
+ IMAP_PROXY_SENT_STATE_AUTHENTICATE |
+ IMAP_PROXY_SENT_STATE_LOGIN)) {
+ /* pipelining was disabled, send the login now. */
+ str = t_str_new(128);
+ if (proxy_write_login(imap_client, str) < 0)
+ return -1;
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 1;
+ }
+ return 0;
+ } else if (strncasecmp(line, "I ", 2) == 0) {
+ /* Reply to ID command we sent, ignore it unless
+ pipelining is disabled, in which case send
+ either STARTTLS or login */
+ imap_client->proxy_sent_state &= ENUM_NEGATE(IMAP_PROXY_SENT_STATE_ID);
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_ID;
+
+ if (client->proxy_nopipelining) {
+ str = t_str_new(128);
+ if ((ret = proxy_write_starttls(imap_client, str)) < 0) {
+ return -1;
+ } else if (ret == 0) {
+ if (proxy_write_login(imap_client, str) < 0)
+ return -1;
+ }
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 1;
+ }
+ return 0;
+ } else if (strncasecmp(line, "* ID ", 5) == 0) {
+ /* Reply to ID command we sent, ignore it */
+ return 0;
+ } else if (str_begins(line, "* BYE ")) {
+ /* Login unexpectedly failed (due to some internal error).
+ Don't forward the BYE to the client, since we're not going
+ to disconnect it. It could be a possibility to convert these
+ to NO replies, but they're likely not going to provide
+ anything useful. */
+ return 0;
+ } else if (str_begins(line, "* ")) {
+ /* untagged reply. just forward it. */
+ client_send_raw(client, t_strconcat(line, "\r\n", NULL));
+ return 0;
+ } else {
+ /* tagged reply, shouldn't happen. */
+ e_error(login_proxy_get_event(client->login_proxy),
+ "Unexpected input, ignoring: %s",
+ str_sanitize(line, 160));
+ return 0;
+ }
+}
+
+void imap_proxy_reset(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+ imap_client->proxy_sasl_ir = FALSE;
+ imap_client->proxy_logindisabled = FALSE;
+ imap_client->proxy_seen_banner = FALSE;
+ imap_client->proxy_capability_request_sent = FALSE;
+ imap_client->proxy_sent_state = 0;
+ imap_client->proxy_rcvd_state = IMAP_PROXY_RCVD_STATE_NONE;
+}
+
+static void
+imap_proxy_send_failure_reply(struct imap_client *imap_client,
+ enum login_proxy_failure_type type,
+ const char *reason)
+{
+ switch (type) {
+ case LOGIN_PROXY_FAILURE_TYPE_CONNECT:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE:
+ case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+ client_send_reply_code(&imap_client->common, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_UNAVAILABLE,
+ LOGIN_PROXY_FAILURE_MSG);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+ client_send_reply_code(&imap_client->common, IMAP_CMD_REPLY_NO,
+ IMAP_RESP_CODE_SERVERBUG,
+ LOGIN_PROXY_FAILURE_MSG);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL:
+ client_send_raw(&imap_client->common, t_strconcat(
+ imap_client->cmd_tag, " NO ", reason, "\r\n", NULL));
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH:
+ /* reply was already sent */
+ break;
+ }
+}
+
+void imap_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting)
+{
+ struct imap_client *imap_client =
+ container_of(client, struct imap_client, common);
+
+ if (!reconnecting)
+ imap_proxy_send_failure_reply(imap_client, type, reason);
+ client_common_proxy_failed(client, type, reason, reconnecting);
+}
+
+const char *imap_proxy_get_state(struct client *client)
+{
+ struct imap_client *imap_client = (struct imap_client *)client;
+ string_t *str = t_str_new(128);
+
+ for (unsigned int i = 0; i < IMAP_PROXY_SENT_STATE_COUNT; i++) {
+ if ((imap_client->proxy_sent_state & (1 << i)) != 0) {
+ if (str_len(str) > 0)
+ str_append_c(str, '+');
+ str_append(str, imap_proxy_sent_state_names[i]);
+ }
+ }
+ str_append_c(str, '/');
+ str_append(str, imap_proxy_rcvd_state_names[imap_client->proxy_rcvd_state]);
+ return str_c(str);
+}
diff --git a/src/imap-login/imap-proxy.h b/src/imap-login/imap-proxy.h
new file mode 100644
index 0000000..71c2f64
--- /dev/null
+++ b/src/imap-login/imap-proxy.h
@@ -0,0 +1,12 @@
+#ifndef IMAP_PROXY_H
+#define IMAP_PROXY_H
+
+void imap_proxy_reset(struct client *client);
+int imap_proxy_parse_line(struct client *client, const char *line);
+
+void imap_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting);
+const char *imap_proxy_get_state(struct client *client);
+
+#endif
diff --git a/src/imap-urlauth/Makefile.am b/src/imap-urlauth/Makefile.am
new file mode 100644
index 0000000..c9a37e6
--- /dev/null
+++ b/src/imap-urlauth/Makefile.am
@@ -0,0 +1,85 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+# Refer to comment in imap-urlauth.c for info on what these binaries are for.
+pkglibexec_PROGRAMS = imap-urlauth-login imap-urlauth imap-urlauth-worker
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+# imap-urlauth-login
+
+imap_urlauth_login_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/login-common
+
+imap_urlauth_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+imap_urlauth_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+imap_urlauth_login_SOURCES = \
+ imap-urlauth-login.c \
+ imap-urlauth-login-settings.c
+
+# imap-urlauth
+
+imap_urlauth_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-dict \
+ -DPKG_RUNDIR=\""$(rundir)"\"
+
+imap_urlauth_LDFLAGS = -export-dynamic
+
+imap_urlauth_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+imap_urlauth_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+imap_urlauth_SOURCES = \
+ imap-urlauth.c \
+ imap-urlauth-client.c \
+ imap-urlauth-settings.c
+
+# imap-urlauth-worker
+
+imap_urlauth_worker_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/imap \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/login-common
+
+imap_urlauth_worker_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+urlauth_libs = \
+ $(top_builddir)/src/lib-imap-urlauth/libimap-urlauth.la
+
+imap_urlauth_worker_LDADD = $(urlauth_libs) $(LIBDOVECOT_STORAGE) $(LIBDOVECOT)
+imap_urlauth_worker_DEPENDENCIES = $(urlauth_libs) $(LIBDOVECOT_STORAGE_DEPS) $(LIBDOVECOT_DEPS)
+
+imap_urlauth_worker_SOURCES = \
+ imap-urlauth-worker.c \
+ imap-urlauth-worker-settings.c
+
+noinst_HEADERS = \
+ imap-urlauth-client.h \
+ imap-urlauth-common.h \
+ imap-urlauth-settings.h \
+ imap-urlauth-login-settings.h \
+ imap-urlauth-worker-settings.h
+
diff --git a/src/imap-urlauth/Makefile.in b/src/imap-urlauth/Makefile.in
new file mode 100644
index 0000000..9c1689f
--- /dev/null
+++ b/src/imap-urlauth/Makefile.in
@@ -0,0 +1,1014 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = imap-urlauth-login$(EXEEXT) \
+ imap-urlauth$(EXEEXT) imap-urlauth-worker$(EXEEXT)
+subdir = src/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 $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_imap_urlauth_OBJECTS = imap_urlauth-imap-urlauth.$(OBJEXT) \
+ imap_urlauth-imap-urlauth-client.$(OBJEXT) \
+ imap_urlauth-imap-urlauth-settings.$(OBJEXT)
+imap_urlauth_OBJECTS = $(am_imap_urlauth_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+imap_urlauth_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(imap_urlauth_LDFLAGS) $(LDFLAGS) -o $@
+am_imap_urlauth_login_OBJECTS = \
+ imap_urlauth_login-imap-urlauth-login.$(OBJEXT) \
+ imap_urlauth_login-imap-urlauth-login-settings.$(OBJEXT)
+imap_urlauth_login_OBJECTS = $(am_imap_urlauth_login_OBJECTS)
+am_imap_urlauth_worker_OBJECTS = \
+ imap_urlauth_worker-imap-urlauth-worker.$(OBJEXT) \
+ imap_urlauth_worker-imap-urlauth-worker-settings.$(OBJEXT)
+imap_urlauth_worker_OBJECTS = $(am_imap_urlauth_worker_OBJECTS)
+imap_urlauth_worker_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(imap_urlauth_worker_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)/imap_urlauth-imap-urlauth-client.Po \
+ ./$(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po \
+ ./$(DEPDIR)/imap_urlauth-imap-urlauth.Po \
+ ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po \
+ ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po \
+ ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po \
+ ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.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 = $(imap_urlauth_SOURCES) $(imap_urlauth_login_SOURCES) \
+ $(imap_urlauth_worker_SOURCES)
+DIST_SOURCES = $(imap_urlauth_SOURCES) $(imap_urlauth_login_SOURCES) \
+ $(imap_urlauth_worker_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+
+# imap-urlauth-login
+imap_urlauth_login_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/login-common
+
+imap_urlauth_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+imap_urlauth_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+imap_urlauth_login_SOURCES = \
+ imap-urlauth-login.c \
+ imap-urlauth-login-settings.c
+
+
+# imap-urlauth
+imap_urlauth_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-dict \
+ -DPKG_RUNDIR=\""$(rundir)"\"
+
+imap_urlauth_LDFLAGS = -export-dynamic
+imap_urlauth_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+imap_urlauth_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+imap_urlauth_SOURCES = \
+ imap-urlauth.c \
+ imap-urlauth-client.c \
+ imap-urlauth-settings.c
+
+
+# imap-urlauth-worker
+imap_urlauth_worker_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/imap \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/login-common
+
+imap_urlauth_worker_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+urlauth_libs = \
+ $(top_builddir)/src/lib-imap-urlauth/libimap-urlauth.la
+
+imap_urlauth_worker_LDADD = $(urlauth_libs) $(LIBDOVECOT_STORAGE) $(LIBDOVECOT)
+imap_urlauth_worker_DEPENDENCIES = $(urlauth_libs) $(LIBDOVECOT_STORAGE_DEPS) $(LIBDOVECOT_DEPS)
+imap_urlauth_worker_SOURCES = \
+ imap-urlauth-worker.c \
+ imap-urlauth-worker-settings.c
+
+noinst_HEADERS = \
+ imap-urlauth-client.h \
+ imap-urlauth-common.h \
+ imap-urlauth-settings.h \
+ imap-urlauth-login-settings.h \
+ imap-urlauth-worker-settings.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/imap-urlauth/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+imap-urlauth$(EXEEXT): $(imap_urlauth_OBJECTS) $(imap_urlauth_DEPENDENCIES) $(EXTRA_imap_urlauth_DEPENDENCIES)
+ @rm -f imap-urlauth$(EXEEXT)
+ $(AM_V_CCLD)$(imap_urlauth_LINK) $(imap_urlauth_OBJECTS) $(imap_urlauth_LDADD) $(LIBS)
+
+imap-urlauth-login$(EXEEXT): $(imap_urlauth_login_OBJECTS) $(imap_urlauth_login_DEPENDENCIES) $(EXTRA_imap_urlauth_login_DEPENDENCIES)
+ @rm -f imap-urlauth-login$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(imap_urlauth_login_OBJECTS) $(imap_urlauth_login_LDADD) $(LIBS)
+
+imap-urlauth-worker$(EXEEXT): $(imap_urlauth_worker_OBJECTS) $(imap_urlauth_worker_DEPENDENCIES) $(EXTRA_imap_urlauth_worker_DEPENDENCIES)
+ @rm -f imap-urlauth-worker$(EXEEXT)
+ $(AM_V_CCLD)$(imap_urlauth_worker_LINK) $(imap_urlauth_worker_OBJECTS) $(imap_urlauth_worker_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth-imap-urlauth-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth-imap-urlauth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.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 $@ $<
+
+imap_urlauth-imap-urlauth.o: imap-urlauth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth.o -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth.Tpo -c -o imap_urlauth-imap-urlauth.o `test -f 'imap-urlauth.c' || echo '$(srcdir)/'`imap-urlauth.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth.c' object='imap_urlauth-imap-urlauth.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth.o `test -f 'imap-urlauth.c' || echo '$(srcdir)/'`imap-urlauth.c
+
+imap_urlauth-imap-urlauth.obj: imap-urlauth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth.obj -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth.Tpo -c -o imap_urlauth-imap-urlauth.obj `if test -f 'imap-urlauth.c'; then $(CYGPATH_W) 'imap-urlauth.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth.c' object='imap_urlauth-imap-urlauth.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth.obj `if test -f 'imap-urlauth.c'; then $(CYGPATH_W) 'imap-urlauth.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth.c'; fi`
+
+imap_urlauth-imap-urlauth-client.o: imap-urlauth-client.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth-client.o -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth-client.Tpo -c -o imap_urlauth-imap-urlauth-client.o `test -f 'imap-urlauth-client.c' || echo '$(srcdir)/'`imap-urlauth-client.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth-client.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth-client.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-client.c' object='imap_urlauth-imap-urlauth-client.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth-client.o `test -f 'imap-urlauth-client.c' || echo '$(srcdir)/'`imap-urlauth-client.c
+
+imap_urlauth-imap-urlauth-client.obj: imap-urlauth-client.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth-client.obj -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth-client.Tpo -c -o imap_urlauth-imap-urlauth-client.obj `if test -f 'imap-urlauth-client.c'; then $(CYGPATH_W) 'imap-urlauth-client.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-client.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth-client.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth-client.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-client.c' object='imap_urlauth-imap-urlauth-client.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth-client.obj `if test -f 'imap-urlauth-client.c'; then $(CYGPATH_W) 'imap-urlauth-client.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-client.c'; fi`
+
+imap_urlauth-imap-urlauth-settings.o: imap-urlauth-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth-settings.o -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Tpo -c -o imap_urlauth-imap-urlauth-settings.o `test -f 'imap-urlauth-settings.c' || echo '$(srcdir)/'`imap-urlauth-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-settings.c' object='imap_urlauth-imap-urlauth-settings.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth-settings.o `test -f 'imap-urlauth-settings.c' || echo '$(srcdir)/'`imap-urlauth-settings.c
+
+imap_urlauth-imap-urlauth-settings.obj: imap-urlauth-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth-imap-urlauth-settings.obj -MD -MP -MF $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Tpo -c -o imap_urlauth-imap-urlauth-settings.obj `if test -f 'imap-urlauth-settings.c'; then $(CYGPATH_W) 'imap-urlauth-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-settings.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Tpo $(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-settings.c' object='imap_urlauth-imap-urlauth-settings.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) $(imap_urlauth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth-imap-urlauth-settings.obj `if test -f 'imap-urlauth-settings.c'; then $(CYGPATH_W) 'imap-urlauth-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-settings.c'; fi`
+
+imap_urlauth_login-imap-urlauth-login.o: imap-urlauth-login.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_login-imap-urlauth-login.o -MD -MP -MF $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Tpo -c -o imap_urlauth_login-imap-urlauth-login.o `test -f 'imap-urlauth-login.c' || echo '$(srcdir)/'`imap-urlauth-login.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Tpo $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-login.c' object='imap_urlauth_login-imap-urlauth-login.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) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_login-imap-urlauth-login.o `test -f 'imap-urlauth-login.c' || echo '$(srcdir)/'`imap-urlauth-login.c
+
+imap_urlauth_login-imap-urlauth-login.obj: imap-urlauth-login.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_login-imap-urlauth-login.obj -MD -MP -MF $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Tpo -c -o imap_urlauth_login-imap-urlauth-login.obj `if test -f 'imap-urlauth-login.c'; then $(CYGPATH_W) 'imap-urlauth-login.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-login.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Tpo $(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-login.c' object='imap_urlauth_login-imap-urlauth-login.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) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_login-imap-urlauth-login.obj `if test -f 'imap-urlauth-login.c'; then $(CYGPATH_W) 'imap-urlauth-login.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-login.c'; fi`
+
+imap_urlauth_login-imap-urlauth-login-settings.o: imap-urlauth-login-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_login-imap-urlauth-login-settings.o -MD -MP -MF $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Tpo -c -o imap_urlauth_login-imap-urlauth-login-settings.o `test -f 'imap-urlauth-login-settings.c' || echo '$(srcdir)/'`imap-urlauth-login-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Tpo $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-login-settings.c' object='imap_urlauth_login-imap-urlauth-login-settings.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) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_login-imap-urlauth-login-settings.o `test -f 'imap-urlauth-login-settings.c' || echo '$(srcdir)/'`imap-urlauth-login-settings.c
+
+imap_urlauth_login-imap-urlauth-login-settings.obj: imap-urlauth-login-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_login-imap-urlauth-login-settings.obj -MD -MP -MF $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Tpo -c -o imap_urlauth_login-imap-urlauth-login-settings.obj `if test -f 'imap-urlauth-login-settings.c'; then $(CYGPATH_W) 'imap-urlauth-login-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-login-settings.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Tpo $(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-login-settings.c' object='imap_urlauth_login-imap-urlauth-login-settings.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) $(imap_urlauth_login_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_login-imap-urlauth-login-settings.obj `if test -f 'imap-urlauth-login-settings.c'; then $(CYGPATH_W) 'imap-urlauth-login-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-login-settings.c'; fi`
+
+imap_urlauth_worker-imap-urlauth-worker.o: imap-urlauth-worker.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_worker-imap-urlauth-worker.o -MD -MP -MF $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Tpo -c -o imap_urlauth_worker-imap-urlauth-worker.o `test -f 'imap-urlauth-worker.c' || echo '$(srcdir)/'`imap-urlauth-worker.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Tpo $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-worker.c' object='imap_urlauth_worker-imap-urlauth-worker.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) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_worker-imap-urlauth-worker.o `test -f 'imap-urlauth-worker.c' || echo '$(srcdir)/'`imap-urlauth-worker.c
+
+imap_urlauth_worker-imap-urlauth-worker.obj: imap-urlauth-worker.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_worker-imap-urlauth-worker.obj -MD -MP -MF $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Tpo -c -o imap_urlauth_worker-imap-urlauth-worker.obj `if test -f 'imap-urlauth-worker.c'; then $(CYGPATH_W) 'imap-urlauth-worker.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-worker.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Tpo $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-worker.c' object='imap_urlauth_worker-imap-urlauth-worker.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) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_worker-imap-urlauth-worker.obj `if test -f 'imap-urlauth-worker.c'; then $(CYGPATH_W) 'imap-urlauth-worker.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-worker.c'; fi`
+
+imap_urlauth_worker-imap-urlauth-worker-settings.o: imap-urlauth-worker-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_worker-imap-urlauth-worker-settings.o -MD -MP -MF $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Tpo -c -o imap_urlauth_worker-imap-urlauth-worker-settings.o `test -f 'imap-urlauth-worker-settings.c' || echo '$(srcdir)/'`imap-urlauth-worker-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Tpo $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-worker-settings.c' object='imap_urlauth_worker-imap-urlauth-worker-settings.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) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_worker-imap-urlauth-worker-settings.o `test -f 'imap-urlauth-worker-settings.c' || echo '$(srcdir)/'`imap-urlauth-worker-settings.c
+
+imap_urlauth_worker-imap-urlauth-worker-settings.obj: imap-urlauth-worker-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imap_urlauth_worker-imap-urlauth-worker-settings.obj -MD -MP -MF $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Tpo -c -o imap_urlauth_worker-imap-urlauth-worker-settings.obj `if test -f 'imap-urlauth-worker-settings.c'; then $(CYGPATH_W) 'imap-urlauth-worker-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-worker-settings.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Tpo $(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imap-urlauth-worker-settings.c' object='imap_urlauth_worker-imap-urlauth-worker-settings.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) $(imap_urlauth_worker_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imap_urlauth_worker-imap-urlauth-worker-settings.obj `if test -f 'imap-urlauth-worker-settings.c'; then $(CYGPATH_W) 'imap-urlauth-worker-settings.c'; else $(CYGPATH_W) '$(srcdir)/imap-urlauth-worker-settings.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap_urlauth-imap-urlauth-client.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth-imap-urlauth.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.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-pkglibexecPROGRAMS
+
+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-imap-urlauth-client.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth-imap-urlauth-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth-imap-urlauth.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_login-imap-urlauth-login.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker-settings.Po
+ -rm -f ./$(DEPDIR)/imap_urlauth_worker-imap-urlauth-worker.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/imap-urlauth/imap-urlauth-client.c b/src/imap-urlauth/imap-urlauth-client.c
new file mode 100644
index 0000000..73f5b18
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-client.c
@@ -0,0 +1,380 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-urlauth-common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "fdpass.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "eacces-error.h"
+#include "llist.h"
+#include "hostpid.h"
+#include "execv-const.h"
+#include "env-util.h"
+#include "var-expand.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-interface.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+
+#define IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION 1
+#define IMAP_URLAUTH_PROTOCOL_MINOR_VERSION 0
+
+#define IMAP_URLAUTH_WORKER_SOCKET "imap-urlauth-worker"
+
+/* max. length of input lines (URLs) */
+#define MAX_INBUF_SIZE 2048
+
+/* Disconnect client after idling this many milliseconds */
+#define CLIENT_IDLE_TIMEOUT_MSECS (10*60*1000)
+
+#define USER_EXECUTABLE "imap-urlauth-worker"
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+static struct event_category event_category_urlauth = {
+ .name = "imap-urlauth",
+};
+
+struct client *imap_urlauth_clients;
+unsigned int imap_urlauth_client_count;
+
+static int client_worker_connect(struct client *client);
+static void client_worker_disconnect(struct client *client);
+static void client_worker_input(struct client *client);
+
+int client_create(const char *service, const char *username,
+ int fd_in, int fd_out, const struct imap_urlauth_settings *set,
+ struct client **client_r)
+{
+ struct client *client;
+ const char *app;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd_in, TRUE);
+ net_set_nonblock(fd_out, TRUE);
+
+ client = i_new(struct client, 1);
+ client->fd_in = fd_in;
+ client->fd_out = fd_out;
+ client->fd_ctrl = -1;
+ client->set = set;
+
+ client->event = event_create(NULL);
+ event_set_forced_debug(client->event, set->mail_debug);
+ event_add_category(client->event, &event_category_urlauth);
+ event_set_append_log_prefix(client->event, t_strdup_printf(
+ "user %s: ", username));
+
+ if (client_worker_connect(client) < 0) {
+ event_unref(&client->event);
+ i_free(client);
+ return -1;
+ }
+
+ /* determine user's special privileges */
+ i_array_init(&client->access_apps, 4);
+ if (username != NULL) {
+ if (set->imap_urlauth_submit_user != NULL &&
+ strcmp(set->imap_urlauth_submit_user, username) == 0) {
+ e_debug(client->event, "User has URLAUTH submit access");
+ app = "submit+";
+ array_push_back(&client->access_apps, &app);
+ }
+ if (set->imap_urlauth_stream_user != NULL &&
+ strcmp(set->imap_urlauth_stream_user, username) == 0) {
+ e_debug(client->event, "User has URLAUTH stream access");
+ app = "stream";
+ array_push_back(&client->access_apps, &app);
+ }
+ }
+
+ client->username = i_strdup(username);
+ client->service = i_strdup(service);
+
+ client->output = o_stream_create_fd(fd_out, SIZE_MAX);
+
+ imap_urlauth_client_count++;
+ DLLIST_PREPEND(&imap_urlauth_clients, client);
+
+ imap_urlauth_refresh_proctitle();
+ *client_r = client;
+ return 0;
+}
+
+void client_send_line(struct client *client, const char *fmt, ...)
+{
+ va_list va;
+ ssize_t ret;
+
+ if (client->output->closed)
+ return;
+
+ va_start(va, fmt);
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ str_vprintfa(str, fmt, va);
+ str_append(str, "\n");
+
+ ret = o_stream_send(client->output,
+ str_data(str), str_len(str));
+ i_assert(ret < 0 || (size_t)ret == str_len(str));
+ } T_END;
+
+ va_end(va);
+}
+
+static int client_worker_connect(struct client *client)
+{
+ static const char handshake[] = "VERSION\timap-urlauth-worker\t2\t0\n";
+ const char *socket_path;
+ ssize_t ret;
+ unsigned char data;
+
+ socket_path = t_strconcat(client->set->base_dir,
+ "/"IMAP_URLAUTH_WORKER_SOCKET, NULL);
+
+ e_debug(client->event, "Connecting to worker socket %s", socket_path);
+
+ client->fd_ctrl = net_connect_unix_with_retries(socket_path, 1000);
+ if (client->fd_ctrl < 0) {
+ if (errno == EACCES) {
+ e_error(client->event, "imap-urlauth-client: %s",
+ eacces_error_get("net_connect_unix",
+ socket_path));
+ } else {
+ e_error(client->event, "imap-urlauth-client: "
+ "net_connect_unix(%s) failed: %m",
+ socket_path);
+ }
+ return -1;
+ }
+
+ /* transfer one or two fds */
+ data = (client->fd_in == client->fd_out ? '0' : '1');
+ ret = fd_send(client->fd_ctrl, client->fd_in, &data, sizeof(data));
+ if (ret > 0 && client->fd_in != client->fd_out) {
+ data = '0';
+ ret = fd_send(client->fd_ctrl, client->fd_out,
+ &data, sizeof(data));
+ }
+
+ if (ret <= 0) {
+ if (ret < 0) {
+ e_error(client->event, "fd_send(%s, %d) failed: %m",
+ socket_path, client->fd_ctrl);
+ } else {
+ e_error(client->event, "fd_send(%s, %d) failed to send byte",
+ socket_path, client->fd_ctrl);
+ }
+ client_worker_disconnect(client);
+ return -1;
+ }
+
+ client->ctrl_output = o_stream_create_fd(client->fd_ctrl, SIZE_MAX);
+
+ /* send protocol version handshake */
+ if (o_stream_send_str(client->ctrl_output, handshake) < 0) {
+ e_error(client->event,
+ "Error sending handshake to imap-urlauth worker: %m");
+ client_worker_disconnect(client);
+ return -1;
+ }
+
+ client->ctrl_input =
+ i_stream_create_fd(client->fd_ctrl, MAX_INBUF_SIZE);
+ client->ctrl_io =
+ io_add(client->fd_ctrl, IO_READ, client_worker_input, client);
+ return 0;
+}
+
+void client_worker_disconnect(struct client *client)
+{
+ client->worker_state = IMAP_URLAUTH_WORKER_STATE_INACTIVE;
+
+ io_remove(&client->ctrl_io);
+ o_stream_destroy(&client->ctrl_output);
+ i_stream_destroy(&client->ctrl_input);
+ if (client->fd_ctrl >= 0) {
+ net_disconnect(client->fd_ctrl);
+ client->fd_ctrl = -1;
+ }
+}
+
+static int
+client_worker_input_line(struct client *client, const char *response)
+{
+ const char *const *apps;
+ unsigned int count, i;
+ bool restart;
+ string_t *str;
+ int ret;
+
+ switch (client->worker_state) {
+ case IMAP_URLAUTH_WORKER_STATE_INACTIVE:
+ if (strcasecmp(response, "OK") != 0) {
+ client_disconnect(client, "Worker handshake failed");
+ return -1;
+ }
+ client->worker_state = IMAP_URLAUTH_WORKER_STATE_CONNECTED;
+
+ str = t_str_new(256);
+ str_append(str, "ACCESS\t");
+ if (client->username != NULL)
+ str_append_tabescaped(str, client->username);
+ str_append(str, "\t");
+ str_append_tabescaped(str, client->service);
+ if (client->set->mail_debug)
+ str_append(str, "\tdebug");
+ if (array_count(&client->access_apps) > 0) {
+ str_append(str, "\tapps=");
+ apps = array_get(&client->access_apps, &count);
+ str_append(str, apps[0]);
+ for (i = 1; i < count; i++) {
+ str_append_c(str, ',');
+ str_append_tabescaped(str, apps[i]);
+ }
+ }
+ str_append(str, "\n");
+
+ ret = o_stream_send(client->ctrl_output,
+ str_data(str), str_len(str));
+ i_assert(ret < 0 || (size_t)ret == str_len(str));
+ if (ret < 0) {
+ client_disconnect(client,
+ "Failed to send ACCESS control command to worker");
+ return -1;
+ }
+ break;
+
+ case IMAP_URLAUTH_WORKER_STATE_CONNECTED:
+ if (strcasecmp(response, "OK") != 0) {
+ client_disconnect(client,
+ "Failed to negotiate access parameters");
+ return -1;
+ }
+ client->worker_state = IMAP_URLAUTH_WORKER_STATE_ACTIVE;
+ break;
+
+ case IMAP_URLAUTH_WORKER_STATE_ACTIVE:
+ restart = TRUE;
+ if (strcasecmp(response, "DISCONNECTED") == 0) {
+ /* worker detected client disconnect */
+ restart = FALSE;
+ } else if (strcasecmp(response, "FINISHED") != 0) {
+ /* unknown response */
+ client_disconnect(client,
+ "Worker finished with unknown response");
+ return -1;
+ }
+
+ e_debug(client->event, "Worker finished successfully");
+
+ if (restart) {
+ /* connect to new worker for accessing different user */
+ client_worker_disconnect(client);
+ if (client_worker_connect(client) < 0) {
+ client_disconnect(client,
+ "Failed to connect to new worker");
+ return -1;
+ }
+
+ /* indicate success of "END" command */
+ client_send_line(client, "OK");
+ } else {
+ client_disconnect(client, "Client disconnected");
+ }
+ return -1;
+ default:
+ i_unreached();
+ }
+ return 0;
+}
+
+void client_worker_input(struct client *client)
+{
+ struct istream *input = client->ctrl_input;
+ const char *line;
+
+ if (input->closed) {
+ /* disconnected */
+ client_disconnect(client, "Worker disconnected unexpectedly");
+ return;
+ }
+
+ switch (i_stream_read(input)) {
+ case -1:
+ /* disconnected */
+ client_disconnect(client, "Worker disconnected unexpectedly");
+ return;
+ case -2:
+ /* input buffer full */
+ client_disconnect(client, "Worker sent too large input");
+ return;
+ }
+
+ while ((line = i_stream_next_line(input)) != NULL) {
+ if (client_worker_input_line(client, line) < 0)
+ return;
+ }
+}
+
+void client_destroy(struct client *client, const char *reason)
+{
+ i_assert(reason != NULL || client->disconnected);
+
+ if (!client->disconnected)
+ e_info(client->event, "Disconnected: %s", reason);
+
+ imap_urlauth_client_count--;
+ DLLIST_REMOVE(&imap_urlauth_clients, client);
+
+ timeout_remove(&client->to_idle);
+
+ client_worker_disconnect(client);
+
+ o_stream_destroy(&client->output);
+
+ fd_close_maybe_stdio(&client->fd_in, &client->fd_out);
+
+ event_unref(&client->event);
+
+ i_free(client->username);
+ i_free(client->service);
+ array_free(&client->access_apps);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+ imap_urlauth_refresh_proctitle();
+}
+
+static void client_destroy_timeout(struct client *client)
+{
+ client_destroy(client, NULL);
+}
+
+void client_disconnect(struct client *client, const char *reason)
+{
+ if (client->disconnected)
+ return;
+
+ client->disconnected = TRUE;
+ e_info(client->event, "Disconnected: %s", reason);
+
+ client->to_idle = timeout_add(0, client_destroy_timeout, client);
+}
+
+void clients_destroy_all(void)
+{
+ while (imap_urlauth_clients != NULL)
+ client_destroy(imap_urlauth_clients, "Server shutting down.");
+}
diff --git a/src/imap-urlauth/imap-urlauth-client.h b/src/imap-urlauth/imap-urlauth-client.h
new file mode 100644
index 0000000..ada8407
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-client.h
@@ -0,0 +1,49 @@
+#ifndef IMAP_URLAUTH_CLIENT_H
+#define IMAP_URLAUTH_CLIENT_H
+
+struct client;
+struct mail_storage;
+
+enum imap_urlauth_worker_state {
+ IMAP_URLAUTH_WORKER_STATE_INACTIVE = 0,
+ IMAP_URLAUTH_WORKER_STATE_CONNECTED,
+ IMAP_URLAUTH_WORKER_STATE_ACTIVE,
+};
+
+struct client {
+ struct client *prev, *next;
+
+ int fd_in, fd_out, fd_ctrl;
+ struct io *ctrl_io;
+ struct ostream *output, *ctrl_output;
+ struct istream *ctrl_input;
+ struct timeout *to_idle;
+ struct event *event;
+
+ char *username, *service;
+ ARRAY_TYPE(const_string) access_apps;
+
+ /* settings: */
+ const struct imap_urlauth_settings *set;
+
+ enum imap_urlauth_worker_state worker_state;
+
+ bool disconnected:1;
+};
+
+extern struct client *imap_urlauth_clients;
+extern unsigned int imap_urlauth_client_count;
+
+int client_create(const char *service, const char *username,
+ int fd_in, int fd_out, const struct imap_urlauth_settings *set,
+ struct client **client_r);
+void client_destroy(struct client *client, const char *reason);
+
+void client_send_line(struct client *client, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+
+void client_disconnect(struct client *client, const char *reason);
+
+void clients_destroy_all(void);
+
+#endif
diff --git a/src/imap-urlauth/imap-urlauth-common.h b/src/imap-urlauth/imap-urlauth-common.h
new file mode 100644
index 0000000..ac6d06f
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-common.h
@@ -0,0 +1,13 @@
+#ifndef IMAP_URLAUTH_COMMON_H
+#define IMAP_URLAUTH_COMMON_H
+
+#include "lib.h"
+#include "imap-urlauth-client.h"
+#include "imap-urlauth-settings.h"
+
+extern bool verbose_proctitle;
+extern struct mail_storage_service_ctx *storage_service;
+
+void imap_urlauth_refresh_proctitle(void);
+
+#endif
diff --git a/src/imap-urlauth/imap-urlauth-login-settings.c b/src/imap-urlauth/imap-urlauth-login-settings.c
new file mode 100644
index 0000000..3dc41f6
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-login-settings.c
@@ -0,0 +1,75 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "login-settings.h"
+#include "imap-urlauth-login-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings
+imap_urlauth_login_unix_listeners_array[] = {
+ { "imap-urlauth", 0666, "", "" }
+};
+static struct file_listener_settings *imap_urlauth_login_unix_listeners[] = {
+ &imap_urlauth_login_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_login_unix_listeners_buf = {
+ { { imap_urlauth_login_unix_listeners,
+ sizeof(imap_urlauth_login_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_urlauth_login_service_settings = {
+ .name = "imap-urlauth-login",
+ .protocol = "imap",
+ .type = "login",
+ .executable = "imap-urlauth-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "token-login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_login_unix_listeners_buf,
+ sizeof(imap_urlauth_login_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+static const struct setting_define imap_urlauth_login_setting_defines[] = {
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct setting_parser_info *imap_urlauth_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info imap_urlauth_login_setting_parser_info = {
+ .module_name = "imap-urlauth-login",
+ .defines = imap_urlauth_login_setting_defines,
+
+ .type_offset = SIZE_MAX,
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_login_setting_dependencies
+};
+
+const struct setting_parser_info *imap_urlauth_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &imap_urlauth_login_setting_parser_info,
+ NULL
+};
diff --git a/src/imap-urlauth/imap-urlauth-login-settings.h b/src/imap-urlauth/imap-urlauth-login-settings.h
new file mode 100644
index 0000000..775638a
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-login-settings.h
@@ -0,0 +1,6 @@
+#ifndef IMAP_URLAUTH_LOGIN_SETTINGS_H
+#define IMAP_URLAUTH_LOGIN_SETTINGS_H
+
+extern const struct setting_parser_info *imap_urlauth_login_setting_roots[];
+
+#endif
diff --git a/src/imap-urlauth/imap-urlauth-login.c b/src/imap-urlauth/imap-urlauth-login.c
new file mode 100644
index 0000000..e917a5b
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-login.c
@@ -0,0 +1,199 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "str.h"
+#include "strescape.h"
+#include "base64.h"
+#include "net.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "auth-client.h"
+#include "client-common.h"
+#include "imap-urlauth-login-settings.h"
+
+#define IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION 2
+#define IMAP_URLAUTH_PROTOCOL_MINOR_VERSION 0
+
+struct imap_urlauth_client {
+ struct client common;
+
+ const struct imap_urlauth_login_settings *set;
+
+ bool version_received:1;
+};
+
+static void
+imap_urlauth_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply ATTR_UNUSED,
+ const char *text ATTR_UNUSED)
+{
+ if (result != CLIENT_AUTH_RESULT_SUCCESS) {
+ /* failed or otherwise invalid status */
+ client_send_raw(client, "FAILED\n");
+ client_destroy(client, "Authentication failed");
+ } else {
+ /* authentication succeeded */
+ }
+}
+
+static void imap_urlauth_client_handle_input(struct client *client)
+{
+#define AUTH_ARG_COUNT 6
+ struct imap_urlauth_client *uauth_client =
+ (struct imap_urlauth_client *)client;
+ struct net_unix_cred cred;
+ const char *line;
+ const char *const *args;
+ pid_t pid;
+
+ if (!uauth_client->version_received) {
+ if ((line = i_stream_next_line(client->input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "imap-urlauth",
+ IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION)) {
+ e_error(client->event,
+ "IMAP URLAUTH client not compatible with this server "
+ "(mixed old and new binaries?) %s", line);
+ client_destroy(client, "Version mismatch");
+ return;
+ }
+ uauth_client->version_received = TRUE;
+ }
+
+ if ((line = i_stream_next_line(client->input)) == NULL)
+ return;
+
+ /* read authentication info from input;
+ "AUTH"\t<service>\t<session-pid>\t<auth-username>\t<session_id>\t<token> */
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < AUTH_ARG_COUNT ||
+ strcmp(args[0], "AUTH") != 0 || str_to_pid(args[2], &pid) < 0) {
+ e_error(client->event,
+ "IMAP URLAUTH client sent unexpected AUTH input: %s", line);
+ client_destroy(client, "Unexpected input");
+ return;
+ }
+
+ /* only imap and submission have direct access to urlauth service */
+ if (strcmp(args[1], "imap") != 0 && strcmp(args[1], "submission") != 0) {
+ e_error(client->event,
+ "IMAP URLAUTH accessed from inappropriate service: %s", args[1]);
+ client_destroy(client, "Unexpected input");
+ return;
+ }
+
+ /* verify session pid if possible */
+ if (net_getunixcred(client->fd, &cred) == 0 &&
+ cred.pid != (pid_t)-1 && pid != cred.pid) {
+ e_error(client->event,
+ "IMAP URLAUTH client sent invalid session pid %ld in AUTH request: "
+ "it did not match peer credentials (pid=%ld, uid=%ld)",
+ (long)pid, (long)cred.pid, (long)cred.uid);
+ client_destroy(client, "Invalid AUTH request");
+ return;
+ }
+
+ T_BEGIN {
+ string_t *auth_data = t_str_new(128);
+ string_t *init_resp;
+ unsigned int i;
+
+ str_append(auth_data, args[1]);
+ for (i = 2; i < AUTH_ARG_COUNT; i++) {
+ str_append_c(auth_data, '\0');
+ str_append(auth_data, args[i]);
+ }
+ init_resp = t_str_new(256);
+ base64_encode(str_data(auth_data),
+ str_len(auth_data), init_resp);
+
+ (void)client_auth_begin_private(client, "DOVECOT-TOKEN",
+ str_c(init_resp));
+ } T_END;
+}
+
+static void imap_urlauth_client_input(struct client *client)
+{
+ if (!client_read(client))
+ return;
+
+ client_ref(client);
+ o_stream_cork(client->output);
+ if (!auth_client_is_connected(auth_client)) {
+ /* we're not currently connected to auth process -
+ don't allow any commands */
+ timeout_remove(&client->to_auth_waiting);
+ client->input_blocked = TRUE;
+ } else {
+ imap_urlauth_client_handle_input(client);
+ }
+ o_stream_uncork(client->output);
+ client_unref(&client);
+}
+
+static struct client *imap_urlauth_client_alloc(pool_t pool)
+{
+ struct imap_urlauth_client *uauth_client;
+
+ uauth_client = p_new(pool, struct imap_urlauth_client, 1);
+ return &uauth_client->common;
+}
+
+static void imap_urlauth_client_create
+(struct client *client, void **other_sets)
+{
+ struct imap_urlauth_client *uauth_client =
+ (struct imap_urlauth_client *)client;
+
+ uauth_client->set = other_sets[0];
+ client->io = io_add_istream(client->input, client_input, client);
+}
+
+static void imap_urlauth_login_preinit(void)
+{
+ login_set_roots = imap_urlauth_login_setting_roots;
+}
+
+static void imap_urlauth_login_init(void)
+{
+}
+
+static void imap_urlauth_login_deinit(void)
+{
+ clients_destroy_all();
+}
+
+static struct client_vfuncs imap_urlauth_vfuncs = {
+ .alloc = imap_urlauth_client_alloc,
+ .create = imap_urlauth_client_create,
+ .input = imap_urlauth_client_input,
+ .auth_result = imap_urlauth_client_auth_result,
+ .send_raw_data = client_common_send_raw_data,
+ .free = client_common_default_free,
+};
+
+static struct login_binary imap_urlauth_login_binary = {
+ .protocol = "imap-urlauth",
+ .process_name = "imap-urlauth-login",
+ .default_login_socket = LOGIN_TOKEN_DEFAULT_SOCKET,
+
+ .event_category = {
+ .name = "imap",
+ },
+
+ .client_vfuncs = &imap_urlauth_vfuncs,
+ .preinit = imap_urlauth_login_preinit,
+ .init = imap_urlauth_login_init,
+ .deinit = imap_urlauth_login_deinit,
+
+ .anonymous_login_acceptable = TRUE,
+};
+
+int main(int argc, char *argv[])
+{
+ return login_binary_run(&imap_urlauth_login_binary, argc, argv);
+}
diff --git a/src/imap-urlauth/imap-urlauth-settings.c b/src/imap-urlauth/imap-urlauth-settings.c
new file mode 100644
index 0000000..73c3220
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-settings.c
@@ -0,0 +1,94 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "imap-urlauth-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+/* <settings checks> */
+static struct file_listener_settings imap_urlauth_unix_listeners_array[] = {
+ { "token-login/imap-urlauth", 0666, "", "" }
+};
+static struct file_listener_settings *imap_urlauth_unix_listeners[] = {
+ &imap_urlauth_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_unix_listeners_buf = {
+ { { imap_urlauth_unix_listeners, sizeof(imap_urlauth_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_urlauth_service_settings = {
+ .name = "imap-urlauth",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-urlauth",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_unix_listeners_buf,
+ sizeof(imap_urlauth_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_urlauth_settings)
+
+static const struct setting_define imap_urlauth_setting_defines[] = {
+ DEF(STR, base_dir),
+
+ DEF(BOOL, mail_debug),
+
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, imap_urlauth_logout_format),
+ DEF(STR, imap_urlauth_submit_user),
+ DEF(STR, imap_urlauth_stream_user),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct imap_urlauth_settings imap_urlauth_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .mail_debug = FALSE,
+
+ .verbose_proctitle = FALSE,
+
+ .imap_urlauth_logout_format = "in=%i out=%o",
+ .imap_urlauth_submit_user = NULL,
+ .imap_urlauth_stream_user = NULL
+};
+
+static const struct setting_parser_info *imap_urlauth_setting_dependencies[] = {
+ NULL
+};
+
+const struct setting_parser_info imap_urlauth_setting_parser_info = {
+ .module_name = "imap-urlauth",
+ .defines = imap_urlauth_setting_defines,
+ .defaults = &imap_urlauth_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_urlauth_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_setting_dependencies
+};
diff --git a/src/imap-urlauth/imap-urlauth-settings.h b/src/imap-urlauth/imap-urlauth-settings.h
new file mode 100644
index 0000000..c04080e
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-settings.h
@@ -0,0 +1,24 @@
+#ifndef IMAP_URLAUTH_SETTINGS_H
+#define IMAP_URLAUTH_SETTINGS_H
+
+struct mail_user_settings;
+
+struct imap_urlauth_settings {
+ const char *base_dir;
+
+ bool mail_debug;
+
+ bool verbose_proctitle;
+
+ /* imap_urlauth: */
+ const char *imap_urlauth_logout_format;
+
+ const char *imap_urlauth_submit_user;
+ const char *imap_urlauth_stream_user;
+};
+
+extern const struct imap_urlauth_settings imap_urlauth_default_settings;
+
+extern const struct setting_parser_info imap_urlauth_setting_parser_info;
+
+#endif
diff --git a/src/imap-urlauth/imap-urlauth-worker-settings.c b/src/imap-urlauth/imap-urlauth-worker-settings.c
new file mode 100644
index 0000000..a459aed
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-worker-settings.c
@@ -0,0 +1,88 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "imap-urlauth-worker-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+/* <settings checks> */
+static struct file_listener_settings imap_urlauth_worker_unix_listeners_array[] = {
+ { "imap-urlauth-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *imap_urlauth_worker_unix_listeners[] = {
+ &imap_urlauth_worker_unix_listeners_array[0]
+};
+static buffer_t imap_urlauth_worker_unix_listeners_buf = {
+ { { imap_urlauth_worker_unix_listeners,
+ sizeof(imap_urlauth_worker_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_urlauth_worker_service_settings = {
+ .name = "imap-urlauth-worker",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap-urlauth-worker",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_urlauth_worker_unix_listeners_buf,
+ sizeof(imap_urlauth_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_urlauth_worker_settings)
+
+static const struct setting_define imap_urlauth_worker_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct imap_urlauth_worker_settings imap_urlauth_worker_default_settings = {
+ .verbose_proctitle = FALSE,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143
+};
+
+static const struct setting_parser_info *imap_urlauth_worker_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info imap_urlauth_worker_setting_parser_info = {
+ .module_name = "imap-urlauth-worker",
+ .defines = imap_urlauth_worker_setting_defines,
+ .defaults = &imap_urlauth_worker_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_urlauth_worker_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = imap_urlauth_worker_setting_dependencies
+};
diff --git a/src/imap-urlauth/imap-urlauth-worker-settings.h b/src/imap-urlauth/imap-urlauth-worker-settings.h
new file mode 100644
index 0000000..28154e8
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-worker-settings.h
@@ -0,0 +1,18 @@
+#ifndef IMAP_URLAUTH_SETTINGS_H
+#define IMAP_URLAUTH_SETTINGS_H
+
+struct mail_user_settings;
+
+struct imap_urlauth_worker_settings {
+ bool verbose_proctitle;
+
+ /* imap_urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+};
+
+extern const struct imap_urlauth_worker_settings imap_urlauth_worker_default_settings;
+
+extern const struct setting_parser_info imap_urlauth_worker_setting_parser_info;
+
+#endif
diff --git a/src/imap-urlauth/imap-urlauth-worker.c b/src/imap-urlauth/imap-urlauth-worker.c
new file mode 100644
index 0000000..4dc413a
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth-worker.c
@@ -0,0 +1,1033 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "fdpass.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "llist.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "process-title.h"
+#include "randgen.h"
+#include "restrict-access.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-interface.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "imap-url.h"
+#include "imap-msgpart-url.h"
+#include "imap-urlauth.h"
+#include "imap-urlauth-fetch.h"
+#include "imap-urlauth-worker-settings.h"
+
+#include <unistd.h>
+#include <sysexits.h>
+
+#define MAX_CTRL_HANDSHAKE 255
+
+/* max. length of input lines (URLs) */
+#define MAX_INBUF_SIZE 2048
+
+/* Disconnect client after idling this many milliseconds */
+#define CLIENT_IDLE_TIMEOUT_MSECS (10*60*1000)
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+#define IMAP_URLAUTH_WORKER_PROTOCOL_MAJOR_VERSION 2
+#define IMAP_URLAUTH_WORKER_PROTOCOL_MINOR_VERSION 0
+
+struct client {
+ struct client *prev, *next;
+
+ int fd_in, fd_out, fd_ctrl;
+
+ struct io *io, *ctrl_io;
+ struct istream *input, *ctrl_input;
+ struct ostream *output, *ctrl_output;
+ struct timeout *to_idle;
+
+ char *access_user, *access_service;
+ ARRAY_TYPE(string) access_apps;
+
+ struct mail_storage_service_user *service_user;
+ struct mail_user *mail_user;
+
+ struct imap_urlauth_context *urlauth_ctx;
+
+ struct imap_msgpart_url *url;
+ struct istream *msg_part_input;
+ uoff_t msg_part_size;
+
+ /* settings: */
+ const struct imap_urlauth_worker_settings *set;
+ const struct mail_storage_settings *mail_set;
+
+ bool debug:1;
+ bool finished:1;
+ bool waiting_input:1;
+ bool version_received:1;
+ bool access_received:1;
+ bool access_anonymous:1;
+};
+
+static bool verbose_proctitle = FALSE;
+static struct mail_storage_service_ctx *storage_service;
+
+struct client *imap_urlauth_worker_clients;
+unsigned int imap_urlauth_worker_client_count;
+
+static void client_destroy(struct client *client);
+static void client_abort(struct client *client, const char *reason);
+static int client_run_url(struct client *client);
+static void client_input(struct client *client);
+static bool client_handle_input(struct client *client);
+static int client_output(struct client *client);
+
+static void client_ctrl_input(struct client *client);
+
+static void imap_urlauth_worker_refresh_proctitle(void)
+{
+ struct client *client = imap_urlauth_worker_clients;
+ string_t *title;
+
+ if (!verbose_proctitle)
+ return;
+
+ title = t_str_new(128);
+ str_append_c(title, '[');
+ switch (imap_urlauth_worker_client_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ if (client->mail_user == NULL)
+ str_append(title, client->access_user);
+ else {
+ str_append(title, client->access_user);
+ str_append(title, "->");
+ str_append(title, client->mail_user->username);
+ }
+ break;
+ default:
+ str_printfa(title, "%u connections",
+ imap_urlauth_worker_client_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void client_idle_timeout(struct client *client)
+{
+ if (client->url != NULL) {
+ client_abort(client,
+ "Session closed for inactivity in reading our output");
+ } else {
+ client_destroy(client);
+ }
+}
+
+static struct client *client_create(int fd)
+{
+ struct client *client;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd, TRUE);
+
+ client = i_new(struct client, 1);
+ i_array_init(&client->access_apps, 16);
+ client->fd_in = -1;
+ client->fd_out = -1;
+ client->fd_ctrl = fd;
+ client->access_anonymous = TRUE; /* default until overridden */
+
+ client->ctrl_io = io_add(fd, IO_READ, client_ctrl_input, client);
+ client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS,
+ client_idle_timeout, client);
+
+ imap_urlauth_worker_client_count++;
+ DLLIST_PREPEND(&imap_urlauth_worker_clients, client);
+
+ imap_urlauth_worker_refresh_proctitle();
+ return client;
+}
+
+static struct client *
+client_create_standalone(const char *access_user,
+ const char *const *access_applications,
+ int fd_in, int fd_out, bool debug)
+{
+ struct client *client;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd_in, TRUE);
+ net_set_nonblock(fd_out, TRUE);
+
+ client = i_new(struct client, 1);
+ i_array_init(&client->access_apps, 16);
+ client->fd_in = fd_in;
+ client->fd_out = fd_out;
+ client->fd_ctrl = -1;
+
+ if (access_user != NULL && *access_user != '\0')
+ client->access_user = i_strdup(access_user);
+ else {
+ client->access_user = i_strdup("anonymous");
+ client->access_anonymous = TRUE;
+ }
+ if (access_applications != NULL) {
+ const char *const *apps = access_applications;
+ for (; *apps != NULL; apps++) {
+ char *app = i_strdup(*apps);
+ array_push_back(&client->access_apps, &app);
+ }
+ }
+ client->debug = debug;
+
+ client->input = i_stream_create_fd(fd_in, MAX_INBUF_SIZE);
+ client->output = o_stream_create_fd(fd_out, SIZE_MAX);
+ client->io = io_add(fd_in, IO_READ, client_input, client);
+ client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS,
+ client_idle_timeout, client);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ o_stream_set_flush_callback(client->output, client_output, client);
+
+ imap_urlauth_worker_client_count++;
+ DLLIST_PREPEND(&imap_urlauth_worker_clients, client);
+
+ i_set_failure_prefix("imap-urlauth[%s](%s): ",
+ my_pid, client->access_user);
+ return client;
+}
+
+static void client_abort(struct client *client, const char *reason)
+{
+ i_error("%s", reason);
+ client_destroy(client);
+}
+
+static void client_destroy(struct client *client)
+{
+ char *app;
+
+ i_set_failure_prefix("imap-urlauth[%s](%s): ",
+ my_pid, client->access_user);
+
+ if (client->url != NULL) {
+ /* deinitialize url */
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+ (void)client_run_url(client);
+ i_assert(client->url == NULL);
+ }
+
+ imap_urlauth_worker_client_count--;
+ DLLIST_REMOVE(&imap_urlauth_worker_clients, client);
+
+ if (client->urlauth_ctx != NULL)
+ imap_urlauth_deinit(&client->urlauth_ctx);
+
+ if (client->mail_user != NULL)
+ mail_user_deinit(&client->mail_user);
+
+ io_remove(&client->io);
+ io_remove(&client->ctrl_io);
+ timeout_remove(&client->to_idle);
+
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+
+ i_stream_destroy(&client->ctrl_input);
+ o_stream_destroy(&client->ctrl_output);
+
+ fd_close_maybe_stdio(&client->fd_in, &client->fd_out);
+ if (client->fd_ctrl >= 0)
+ net_disconnect(client->fd_ctrl);
+
+ if (client->service_user != NULL)
+ mail_storage_service_user_unref(&client->service_user);
+ i_free(client->access_user);
+ i_free(client->access_service);
+ array_foreach_elem(&client->access_apps, app)
+ i_free(app);
+ array_free(&client->access_apps);
+ i_free(client);
+
+ imap_urlauth_worker_refresh_proctitle();
+ master_service_client_connection_destroyed(master_service);
+}
+
+static int client_run_url(struct client *client)
+{
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret = 0;
+
+ while (i_stream_read_more(client->msg_part_input, &data, &size) > 0) {
+ if ((ret = o_stream_send(client->output, data, size)) < 0)
+ break;
+ i_stream_skip(client->msg_part_input, ret);
+
+ if (o_stream_get_buffer_used_size(client->output) >= 4096) {
+ if ((ret = o_stream_flush(client->output)) < 0)
+ break;
+ if (ret == 0)
+ return 0;
+ }
+ }
+
+ if (client->output->closed || ret < 0) {
+ imap_msgpart_url_free(&client->url);
+ return -1;
+ }
+
+ if (client->msg_part_input->eof) {
+ o_stream_nsend(client->output, "\n", 1);
+ imap_msgpart_url_free(&client->url);
+ return 1;
+ }
+ return 0;
+}
+
+static void clients_destroy_all(void)
+{
+ while (imap_urlauth_worker_clients != NULL)
+ client_destroy(imap_urlauth_worker_clients);
+}
+
+static void ATTR_FORMAT(2, 3)
+client_send_line(struct client *client, const char *fmt, ...)
+{
+ va_list va;
+
+ if (client->output->closed)
+ return;
+
+ va_start(va, fmt);
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ str_vprintfa(str, fmt, va);
+ str_append(str, "\n");
+
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ } T_END;
+
+ va_end(va);
+}
+
+static int
+client_fetch_urlpart(struct client *client, const char *url,
+ enum imap_urlauth_fetch_flags url_flags,
+ const char **bpstruct_r, bool *binary_with_nuls_r,
+ const char **errormsg_r)
+{
+ const char *error;
+ struct imap_msgpart_open_result mpresult;
+ enum mail_error error_code;
+ int ret;
+
+ *bpstruct_r = NULL;
+ *errormsg_r = NULL;
+ *binary_with_nuls_r = FALSE;
+
+ ret = imap_urlauth_fetch(client->urlauth_ctx, url,
+ &client->url, &error_code, &error);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ error = t_strdup_printf("Failed to fetch URLAUTH \"%s\": %s",
+ url, error);
+ if (client->debug)
+ i_debug("%s", error);
+ /* don't leak info about existence/accessibility
+ of mailboxes */
+ if (error_code == MAIL_ERROR_PARAMS)
+ *errormsg_r = error;
+ return 0;
+ }
+
+ if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)
+ imap_msgpart_url_set_decode_to_binary(client->url);
+ if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0) {
+ ret = imap_msgpart_url_get_bodypartstructure(client->url,
+ bpstruct_r, &error);
+ if (ret <= 0) {
+ *errormsg_r = t_strdup_printf(
+ "Failed to read URLAUTH \"%s\": %s", url, error);
+ if (client->debug)
+ i_debug("%s", *errormsg_r);
+ return ret;
+ }
+ }
+
+ /* if requested, read the message part the URL points to */
+ if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 ||
+ (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) {
+ ret = imap_msgpart_url_read_part(client->url, &mpresult, &error);
+ if (ret <= 0) {
+ *errormsg_r = t_strdup_printf(
+ "Failed to read URLAUTH \"%s\": %s", url, error);
+ if (client->debug)
+ i_debug("%s", *errormsg_r);
+ return ret;
+ }
+ client->msg_part_size = mpresult.size;
+ client->msg_part_input = mpresult.input;
+ *binary_with_nuls_r = mpresult.binary_decoded_input_has_nuls;
+ }
+ return 1;
+}
+
+static int client_fetch_url(struct client *client, const char *url,
+ enum imap_urlauth_fetch_flags url_flags)
+{
+ string_t *response;
+ const char *bpstruct, *errormsg;
+ bool binary_with_nuls;
+ int ret;
+
+ i_assert(client->url == NULL);
+
+ client->msg_part_size = 0;
+ client->msg_part_input = NULL;
+
+ if (client->debug)
+ i_debug("Fetching URLAUTH %s", url);
+
+ /* fetch URL */
+ ret = client_fetch_urlpart(client, url, url_flags, &bpstruct,
+ &binary_with_nuls, &errormsg);
+ if (ret <= 0) {
+ /* fetch failed */
+ if (client->url != NULL)
+ imap_msgpart_url_free(&client->url);
+ /* don't send error details to anonymous users: just to be sure
+ that no information about the target user account is unduly
+ leaked. */
+ if (client->access_anonymous || errormsg == NULL)
+ client_send_line(client, "NO");
+ else {
+ client_send_line(client, "NO\terror=%s",
+ str_tabescape(errormsg));
+ }
+ if (ret < 0) {
+ /* fetch failed badly */
+ client_abort(client, "Session aborted: Fatal failure while fetching URL");
+ }
+ return 0;
+ }
+
+ response = t_str_new(256);
+ str_append(response, "OK");
+ if (binary_with_nuls)
+ str_append(response, "\thasnuls");
+ if (bpstruct != NULL) {
+ str_append(response, "\tbpstruct=");
+ str_append(response, str_tabescape(bpstruct));
+ if (client->debug) {
+ i_debug("Fetched URLAUTH yielded BODYPARTSTRUCTURE (%s)",
+ bpstruct);
+ }
+ }
+
+ /* return content */
+ o_stream_cork(client->output);
+ if (client->msg_part_size == 0 || client->msg_part_input == NULL) {
+ /* empty */
+ str_append(response, "\t0");
+ client_send_line(client, "%s", str_c(response));
+
+ imap_msgpart_url_free(&client->url);
+ client->url = NULL;
+ if (client->debug)
+ i_debug("Fetched URLAUTH yielded empty result");
+ } else {
+
+ /* actual content */
+ str_printfa(response, "\t%"PRIuUOFF_T, client->msg_part_size);
+ client_send_line(client, "%s", str_c(response));
+
+ if (client->debug) {
+ i_debug("Fetched URLAUTH yielded %"PRIuUOFF_T" bytes "
+ "of %smessage data", client->msg_part_size,
+ (binary_with_nuls ? "binary " : ""));
+ }
+ if (client_run_url(client) < 0) {
+ client_abort(client,
+ "Session aborted: Fatal failure while transferring URL");
+ return 0;
+ }
+ }
+
+ if (client->url != NULL) {
+ /* URL not finished */
+ o_stream_set_flush_pending(client->output, TRUE);
+ client->waiting_input = TRUE;
+ }
+ o_stream_uncork(client->output);
+ return client->url != NULL ? 0 : 1;
+}
+
+static int
+client_handle_command(struct client *client, const char *cmd,
+ const char *const *args, const char **error_r)
+{
+ int ret;
+
+ *error_r = NULL;
+
+ /* "URL"["\tbody"]["\tbinary"]["\tbpstruct"]"\t"<url>:
+ fetch URL (meta)data */
+ if (strcmp(cmd, "URL") == 0) {
+ enum imap_urlauth_fetch_flags url_flags = 0;
+ const char *url;
+
+ if (*args == NULL) {
+ *error_r = "URL: Missing URL parameter";
+ return -1;
+ }
+
+ url = *args;
+
+ args++;
+ while (*args != NULL) {
+ if (strcasecmp(*args, "body") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY;
+ else if (strcasecmp(*args, "binary") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BINARY;
+ else if (strcasecmp(*args, "bpstruct") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE;
+
+ args++;
+ }
+
+ if (url_flags == 0)
+ url_flags = IMAP_URLAUTH_FETCH_FLAG_BODY;
+
+ T_BEGIN {
+ ret = client_fetch_url(client, url, url_flags);
+ } T_END;
+ return ret;
+ }
+
+ /* "END": unselect current user (closes worker) */
+ if (strcmp(cmd, "END") == 0) {
+ if (args[0] != NULL) {
+ *error_r = "END: Invalid number of parameters";
+ return -1;
+ }
+
+ client->finished = TRUE;
+ if (client->ctrl_output != NULL)
+ o_stream_nsend_str(client->ctrl_output, "FINISHED\n");
+ client_destroy(client);
+ return 0;
+ }
+
+ *error_r = t_strconcat("Unknown or inappropriate command: ", cmd, NULL);
+ return -1;
+}
+
+static int
+client_handle_user_command(struct client *client, const char *cmd,
+ const char *const *args, const char **error_r)
+{
+ struct mail_storage_service_input input;
+ struct imap_urlauth_worker_settings *set;
+ struct mail_storage_service_user *user;
+ struct imap_urlauth_config config;
+ struct mail_user *mail_user;
+ const char *error;
+ unsigned int count;
+ int ret;
+
+ /* "USER\t"<username> */
+ *error_r = NULL;
+
+ /* check command syntax */
+ if (strcmp(cmd, "USER") != 0) {
+ *error_r = t_strconcat("Unknown or inappropriate command: ",
+ cmd, NULL);
+ return -1;
+ }
+
+ if (args[0] == NULL || args[1] != NULL) {
+ *error_r = "USER: Invalid number of parameters";
+ return -1;
+ }
+
+ /* lookup user */
+ i_zero(&input);
+ input.module = "imap-urlauth-worker";
+ input.service = "imap-urlauth-worker";
+ input.username = args[0];
+
+ if (client->debug)
+ i_debug("Looking up user %s", input.username);
+
+ ret = mail_storage_service_lookup_next(storage_service, &input,
+ &user, &mail_user, &error);
+ if (ret < 0) {
+ i_error("Failed to lookup user %s: %s", input.username, error);
+ client_abort(client, "Session aborted: Failed to lookup user");
+ return 0;
+ } else if (ret == 0) {
+ if (client->debug)
+ i_debug("User %s doesn't exist", input.username);
+
+ client_send_line(client, "NO");
+ return 1;
+ }
+
+ client->debug = mail_user->mail_debug =
+ client->debug || mail_user->mail_debug;
+
+ /* drop privileges */
+ restrict_access_allow_coredumps(TRUE);
+
+ set = mail_storage_service_user_get_set(user)[1];
+ if (settings_var_expand(&imap_urlauth_worker_setting_parser_info, set,
+ mail_user->pool,
+ mail_user_var_expand_table(mail_user),
+ &error) <= 0) {
+ client_send_line(client, "NO");
+ client_abort(client, t_strdup_printf(
+ "Session aborted: Failed to expand settings: %s", error));
+ return 0;
+ }
+
+ if (set->verbose_proctitle) {
+ verbose_proctitle = TRUE;
+ imap_urlauth_worker_refresh_proctitle();
+ }
+
+ client->service_user = user;
+ client->mail_user = mail_user;
+ client->set = set;
+
+ if (client->debug) {
+ i_debug("Found user account `%s' on behalf of user `%s'",
+ mail_user->username, client->access_user);
+ }
+
+ /* initialize urlauth context */
+ if (*set->imap_urlauth_host == '\0') {
+ i_error("imap_urlauth_host setting is not configured for user %s",
+ mail_user->username);
+ client_send_line(client, "NO");
+ client_abort(client, "Session aborted: URLAUTH not configured");
+ return 0;
+ }
+
+ i_zero(&config);
+ config.url_host = set->imap_urlauth_host;
+ config.url_port = set->imap_urlauth_port;
+ config.access_user = client->access_user;
+ config.access_service = client->access_service;
+ config.access_anonymous = client->access_anonymous;
+ config.access_applications =
+ (const void *)array_get(&client->access_apps, &count);
+
+ client->urlauth_ctx = imap_urlauth_init(client->mail_user, &config);
+ if (client->debug) {
+ i_debug("Providing access to user account `%s' on behalf of user `%s' "
+ "using service `%s'", mail_user->username, client->access_user,
+ client->access_service);
+ }
+
+ i_set_failure_prefix("imap-urlauth[%s](%s->%s): ",
+ my_pid, client->access_user, mail_user->username);
+
+ client_send_line(client, "OK");
+ return 1;
+}
+
+static bool client_handle_input(struct client *client)
+{
+ const char *line, *cmd, *error;
+ int ret;
+
+ if (client->url != NULL) {
+ /* we're still processing a URL. wait until it's
+ finished. */
+ io_remove(&client->io);
+ client->io = NULL;
+ client->waiting_input = TRUE;
+ return TRUE;
+ }
+
+ if (client->io == NULL) {
+ client->io = io_add(client->fd_in, IO_READ,
+ client_input, client);
+ }
+ client->waiting_input = FALSE;
+ timeout_reset(client->to_idle);
+
+ switch (i_stream_read(client->input)) {
+ case -1:
+ /* disconnected */
+ if (client->ctrl_output != NULL)
+ o_stream_nsend_str(client->ctrl_output, "DISCONNECTED\n");
+ client_destroy(client);
+ return FALSE;
+ case -2:
+ /* line too long, kill it */
+ client_abort(client, "Session aborted: Input line too long");
+ return FALSE;
+ }
+
+ while ((line = i_stream_next_line(client->input)) != NULL) {
+ const char *const *args = t_strsplit_tabescaped(line);
+
+ if (args[0] == NULL)
+ continue;
+ cmd = args[0]; args++;
+
+ if (client->mail_user == NULL)
+ ret = client_handle_user_command(client, cmd, args, &error);
+ else
+ ret = client_handle_command(client, cmd, args, &error);
+
+ if (ret <= 0) {
+ if (ret == 0)
+ break;
+ i_error("Client input error: %s", error);
+ client_abort(client, "Session aborted: Unexpected input");
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void client_input(struct client *client)
+{
+ (void)client_handle_input(client);
+}
+
+static int client_output(struct client *client)
+{
+ if (o_stream_flush(client->output) < 0) {
+ if (client->ctrl_output != NULL)
+ o_stream_nsend_str(client->ctrl_output, "DISCONNECTED\n");
+ client_destroy(client);
+ return 1;
+ }
+ timeout_reset(client->to_idle);
+
+ if (client->url != NULL) {
+ if (client_run_url(client) < 0) {
+ client_destroy(client);
+ return 1;
+ }
+
+ if (client->url == NULL && client->waiting_input) {
+ if (!client_handle_input(client)) {
+ /* client got destroyed */
+ return 1;
+ }
+ }
+ }
+
+ if (client->url != NULL) {
+ /* url not finished yet */
+ return 0;
+ } else if (client->io == NULL) {
+ /* data still in output buffer, get back here to add IO */
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+static int
+client_ctrl_read_fds(struct client *client)
+{
+ unsigned char data = 0;
+ ssize_t ret = 1;
+
+ if (client->fd_in == -1) {
+ ret = fd_read(client->fd_ctrl, &data,
+ sizeof(data), &client->fd_in);
+ if (ret > 0 && data == '0')
+ client->fd_out = client->fd_in;
+ }
+ if (ret > 0 && client->fd_out == -1) {
+ ret = fd_read(client->fd_ctrl, &data,
+ sizeof(data), &client->fd_out);
+ }
+
+ if (ret == 0) {
+ /* unexpectedly disconnected */
+ client_destroy(client);
+ return 0;
+ } else if (ret < 0) {
+ if (errno == EAGAIN)
+ return 0;
+ i_error("fd_read() failed: %m");
+ return -1;
+ } else if (data != '0') {
+ i_error("fd_read() returned invalid byte 0x%2x", data);
+ return -1;
+ }
+
+ if (client->fd_in == -1 || client->fd_out == -1) {
+ i_error("Handshake is missing a file descriptor");
+ return -1;
+ }
+
+ client->ctrl_input =
+ i_stream_create_fd(client->fd_ctrl, MAX_INBUF_SIZE);
+ client->ctrl_output = o_stream_create_fd(client->fd_ctrl, SIZE_MAX);
+ o_stream_set_no_error_handling(client->ctrl_output, TRUE);
+ return 1;
+}
+
+static void client_ctrl_input(struct client *client)
+{
+ const char *const *args;
+ const char *line;
+ int ret;
+
+ timeout_reset(client->to_idle);
+
+ if (client->fd_in == -1 || client->fd_out == -1) {
+ if ((ret = client_ctrl_read_fds(client)) <= 0) {
+ if (ret < 0)
+ client_abort(client, "FD Transfer failed");
+ return;
+ }
+ }
+
+ switch (i_stream_read(client->ctrl_input)) {
+ case -1:
+ /* disconnected */
+ client_destroy(client);
+ return;
+ case -2:
+ /* line too long, kill it */
+ client_abort(client,
+ "Control session aborted: Input line too long");
+ return;
+ }
+
+ if (!client->version_received) {
+ if ((line = i_stream_next_line(client->ctrl_input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "imap-urlauth-worker",
+ IMAP_URLAUTH_WORKER_PROTOCOL_MAJOR_VERSION)) {
+ i_error("imap-urlauth-worker client not compatible with this server "
+ "(mixed old and new binaries?) %s", line);
+ client_abort(client, "Control session aborted: Version mismatch");
+ return;
+ }
+
+ client->version_received = TRUE;
+ if (o_stream_send_str(client->ctrl_output, "OK\n") < 0) {
+ client_destroy(client);
+ return;
+ }
+ }
+
+ if (client->access_received) {
+ client_abort(client, "Control session aborted: Unexpected input");
+ return;
+ }
+
+ if ((line = i_stream_next_line(client->ctrl_input)) == NULL)
+ return;
+
+ args = t_strsplit_tabescaped(line);
+ if (*args == NULL || strcmp(*args, "ACCESS") != 0) {
+ i_error("Invalid control command: %s", str_sanitize(line, 80));
+ client_abort(client, "Control session aborted: Invalid command");
+ return;
+ }
+ args++;
+ if (args[0] == NULL || args[1] == NULL) {
+ i_error("Invalid ACCESS command: %s", str_sanitize(line, 80));
+ client_abort(client, "Control session aborted: Invalid command");
+ return;
+ }
+
+ i_assert(client->access_user == NULL);
+ i_assert(client->access_service == NULL);
+ if (**args != '\0') {
+ client->access_user = i_strdup(*args);
+ client->access_anonymous = FALSE;
+ } else {
+ client->access_user = i_strdup("anonymous");
+ client->access_anonymous = TRUE;
+ }
+ args++;
+ client->access_service = i_strdup(*args);
+
+ i_set_failure_prefix("imap-urlauth[%s](%s): ",
+ my_pid, client->access_user);
+
+ args++;
+ while (*args != NULL) {
+ /* debug */
+ if (strcasecmp(*args, "debug") == 0) {
+ client->debug = TRUE;
+ /* apps=<access-application>[,<access-application,...] */
+ } else if (strncasecmp(*args, "apps=", 5) == 0 &&
+ (*args)[5] != '\0') {
+ const char *const *apps = t_strsplit(*args+5, ",");
+
+ while (*apps != NULL) {
+ char *app = i_strdup(*apps);
+
+ array_push_back(&client->access_apps, &app);
+ if (client->debug) {
+ i_debug("User %s has URLAUTH %s access",
+ client->access_user, app);
+ }
+ apps++;
+ }
+ } else {
+ i_error("Invalid ACCESS parameter: %s", str_sanitize(*args, 80));
+ client_abort(client, "Control session aborted: Invalid command");
+ return;
+ }
+ args++;
+ }
+
+ client->access_received = TRUE;
+
+ if (o_stream_send_str(client->ctrl_output, "OK\n") < 0) {
+ client_destroy(client);
+ return;
+ }
+
+ client->input = i_stream_create_fd(client->fd_in, MAX_INBUF_SIZE);
+ client->output = o_stream_create_fd(client->fd_out, SIZE_MAX);
+ client->io = io_add(client->fd_in, IO_READ, client_input, client);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ o_stream_set_flush_callback(client->output, client_output, client);
+
+ if (client->debug) {
+ i_debug("Worker activated for access by user `%s' using service `%s'",
+ client->access_user, client->access_service);
+ }
+}
+
+static void imap_urlauth_worker_die(void)
+{
+ /* do nothing */
+}
+
+static void main_stdio_run(const char *access_user,
+ const char *const *access_applications)
+{
+ bool debug;
+
+ debug = getenv("DEBUG") != NULL;
+ access_user = access_user != NULL ? access_user : getenv("USER");
+ if (access_user == NULL && IS_STANDALONE())
+ access_user = getlogin();
+ if (access_user == NULL)
+ i_fatal("USER environment missing");
+
+ (void)client_create_standalone(access_user, access_applications,
+ STDIN_FILENO, STDOUT_FILENO, debug);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ (void)client_create(conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &imap_urlauth_worker_setting_parser_info,
+ NULL
+ };
+ enum master_service_flags service_flags = 0;
+ enum mail_storage_service_flags storage_service_flags =
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
+ ARRAY_TYPE (const_string) access_apps;
+ const char *access_user = NULL;
+ int c;
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ }
+
+ master_service = master_service_init("imap-urlauth-worker", service_flags,
+ &argc, &argv, "a:");
+
+ t_array_init(&access_apps, 4);
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a': {
+ const char *app = t_strdup(optarg);
+
+ array_push_back(&access_apps, &app);
+ break;
+ }
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ if ( optind < argc ) {
+ access_user = argv[optind++];
+ }
+
+ if (optind != argc) {
+ i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]);
+ }
+
+ master_service_init_log_with_pid(master_service);
+ master_service_set_die_callback(master_service, imap_urlauth_worker_die);
+
+ storage_service =
+ mail_storage_service_init(master_service,
+ set_roots, storage_service_flags);
+ master_service_init_finish(master_service);
+
+ /* fake that we're running, so we know if client was destroyed
+ while handling its initial input */
+ io_loop_set_running(current_ioloop);
+
+ if (IS_STANDALONE()) {
+ T_BEGIN {
+ if (array_count(&access_apps) > 0) {
+ (void)array_append_space(&access_apps);
+ main_stdio_run(access_user,
+ array_front(&access_apps));
+ } else {
+ main_stdio_run(access_user, NULL);
+ }
+ } T_END;
+ } else {
+ io_loop_set_running(current_ioloop);
+ }
+
+ if (io_loop_is_running(current_ioloop))
+ master_service_run(master_service, client_connected);
+ clients_destroy_all();
+
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/imap-urlauth/imap-urlauth.c b/src/imap-urlauth/imap-urlauth.c
new file mode 100644
index 0000000..ec9ffc5
--- /dev/null
+++ b/src/imap-urlauth/imap-urlauth.c
@@ -0,0 +1,290 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+/*
+The imap-urlauth service provides URLAUTH access between different accounts. If
+user A has an URLAUTH that references a mail from user B, it makes a connection
+to the imap-urlauth service to access user B's mail store to retrieve the
+mail.
+
+The authentication and authorization of the URLAUTH is performed within
+this service. Because access to the mailbox and the associated mailbox keys is
+necessary to retrieve the message and for verification of the URLAUTH, the
+urlauth services need root privileges. To mitigate security concerns, the
+retrieval and verification of the URLs is performed in a worker service that
+drops root privileges and acts as target user B.
+
+The imap-urlauth service thus consists of three separate stages:
+
+- imap-urlauth-login:
+ This is the login service which operates identical to imap-login and
+ pop3-login equivalents, except for the fact that only token authentication is
+ allowed. It verifies that the connecting client is an IMAP service acting on
+ behaf of an authenticated user.
+
+- imap-urlauth:
+ Once the client is authenticated, the connection gets passed to the
+ imap-urlauth service (as implemented here). The goal of this stage is
+ to prevent the need for re-authenticating to the imap-urlauth service when
+ the clients wants to switch to a different target user. It normally runs as
+ $default_internal_user and starts workers to perform the actual work. To start
+ a worker, the imap-urlauth service establishes a control connection to the
+ imap-urlauth-worker service. In the handshake phase of the control protocol,
+ the connection of the client is passed to the worker. Once the worker
+ finishes, a new worker is started and the client connection is transfered to
+ it, unless the client is disconnected.
+
+- imap-urlauth-worker:
+ The worker handles the URLAUTH requests from the client, so this is where the
+ mail store of the target user is accessed. The worker starts as root. In the
+ protocol interaction the client first indicates what the target user is.
+ The worker then performs a userdb lookup and drops privileges. The client can
+ then submit URLAUTH requests, which are limited to that user. Once the client
+ wants to access a different user, the worker terminates and the imap-urlauth
+ service starts a new worker for the next target user.
+*/
+
+#include "imap-urlauth-common.h"
+#include "lib-signals.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "path-util.h"
+#include "base64.h"
+#include "str.h"
+#include "process-title.h"
+#include "auth-master.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-login.h"
+#include "master-interface.h"
+#include "var-expand.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+bool verbose_proctitle = FALSE;
+static struct master_login *master_login = NULL;
+
+static const struct imap_urlauth_settings *imap_urlauth_settings;
+
+void imap_urlauth_refresh_proctitle(void)
+{
+ struct client *client;
+ string_t *title = t_str_new(128);
+
+ if (!verbose_proctitle)
+ return;
+
+ str_append_c(title, '[');
+ switch (imap_urlauth_client_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ client = imap_urlauth_clients;
+ str_append(title, client->username);
+ break;
+ default:
+ str_printfa(title, "%u connections", imap_urlauth_client_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void imap_urlauth_die(void)
+{
+ /* do nothing. imap_urlauth connections typically die pretty quick anyway. */
+}
+
+static int
+client_create_from_input(const char *service, const char *username,
+ int fd_in, int fd_out)
+{
+ struct client *client;
+
+ if (client_create(service, username, fd_in, fd_out,
+ imap_urlauth_settings, &client) < 0)
+ return -1;
+
+ if (!IS_STANDALONE())
+ client_send_line(client, "OK");
+ return 0;
+}
+
+static void main_stdio_run(const char *username)
+{
+ username = username != NULL ? username : getenv("USER");
+ if (username == NULL && IS_STANDALONE())
+ username = getlogin();
+ if (username == NULL)
+ i_fatal("USER environment missing");
+
+ (void)client_create_from_input("", username, STDIN_FILENO, STDOUT_FILENO);
+}
+
+static void
+login_client_connected(const struct master_login_client *client,
+ const char *username, const char *const *extra_fields)
+{
+ const char *msg = "NO\n";
+ struct auth_user_reply reply;
+ struct net_unix_cred cred;
+ const char *const *fields;
+ const char *service = NULL;
+ unsigned int count, i;
+
+ auth_user_fields_parse(extra_fields, pool_datastack_create(), &reply);
+
+ /* check peer credentials if possible */
+ if (reply.uid != (uid_t)-1 && net_getunixcred(client->fd, &cred) == 0 &&
+ reply.uid != cred.uid) {
+ i_error("Peer's credentials (uid=%ld) do not match "
+ "the user that logged in (uid=%ld).",
+ (long)cred.uid, (long)reply.uid);
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+ net_disconnect(client->fd);
+ return;
+ }
+
+ fields = array_get(&reply.extra_fields, &count);
+ for (i = 0; i < count; i++) {
+ if (str_begins(fields[i], "client_service=")) {
+ service = fields[i] + 15;
+ break;
+ }
+ }
+
+ if (service == NULL) {
+ i_error("Auth did not yield required client_service field (BUG).");
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+ net_disconnect(client->fd);
+ return;
+ }
+
+ if (reply.anonymous)
+ username = NULL;
+
+ if (client_create_from_input(service, username, client->fd, client->fd) < 0)
+ net_disconnect(client->fd);
+}
+
+static void login_client_failed(const struct master_login_client *client,
+ const char *errormsg ATTR_UNUSED)
+{
+ const char *msg = "NO\n";
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ /* when running standalone, we shouldn't even get here */
+ i_assert(master_login != NULL);
+
+ master_service_client_connection_accept(conn);
+ master_login_add(master_login, conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &imap_urlauth_setting_parser_info,
+ NULL
+ };
+ struct master_login_settings login_set;
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ void **sets;
+ enum master_service_flags service_flags = 0;
+ const char *error = NULL, *username = NULL;
+ const char *auth_socket_path = "auth-master";
+ int c;
+
+ i_zero(&login_set);
+ login_set.postlogin_timeout_secs = MASTER_POSTLOGIN_TIMEOUT_DEFAULT;
+
+ if (IS_STANDALONE() && getuid() == 0 &&
+ net_getpeername(1, NULL, NULL) == 0) {
+ printf("NO imap_urlauth binary must not be started from "
+ "inetd, use imap-urlauth-login instead.\n");
+ return 1;
+ }
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ }
+
+ master_service = master_service_init("imap-urlauth", service_flags,
+ &argc, &argv, "a:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a':
+ auth_socket_path = optarg;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ master_service_init_log(master_service);
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.module = "imap-urlauth";
+ input.service = "imap-urlauth";
+ if (master_service_settings_read(master_service, &input, &output,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ sets = master_service_settings_get_others(master_service);
+ imap_urlauth_settings = sets[0];
+
+ if (imap_urlauth_settings->verbose_proctitle)
+ verbose_proctitle = TRUE;
+
+ if (t_abspath(auth_socket_path, &login_set.auth_socket_path, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", auth_socket_path, error);
+ }
+ login_set.callback = login_client_connected;
+ login_set.failure_callback = login_client_failed;
+ login_set.update_proctitle = verbose_proctitle &&
+ master_service_get_client_limit(master_service) == 1;
+
+ master_service_init_finish(master_service);
+ master_service_set_die_callback(master_service, imap_urlauth_die);
+
+ /* fake that we're running, so we know if client was destroyed
+ while handling its initial input */
+ io_loop_set_running(current_ioloop);
+
+ if (IS_STANDALONE()) {
+ T_BEGIN {
+ main_stdio_run(username);
+ } T_END;
+ } else {
+ master_login = master_login_init(master_service, &login_set);
+ io_loop_set_running(current_ioloop);
+ }
+
+ if (io_loop_is_running(current_ioloop))
+ master_service_run(master_service, client_connected);
+ clients_destroy_all();
+
+ if (master_login != NULL)
+ master_login_deinit(&master_login);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/imap/Makefile.am b/src/imap/Makefile.am
new file mode 100644
index 0000000..0a45fd3
--- /dev/null
+++ b/src/imap/Makefile.am
@@ -0,0 +1,130 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = imap
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ $(BINARY_CFLAGS)
+
+imap_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+imap_LDADD = \
+ ../lib-imap-urlauth/libimap-urlauth.la \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+imap_DEPENDENCIES = \
+ ../lib-imap-urlauth/libimap-urlauth.la \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+cmds = \
+ cmd-append.c \
+ cmd-capability.c \
+ cmd-cancelupdate.c \
+ cmd-check.c \
+ cmd-close.c \
+ cmd-copy.c \
+ cmd-create.c \
+ cmd-delete.c \
+ cmd-enable.c \
+ cmd-examine.c \
+ cmd-expunge.c \
+ cmd-fetch.c \
+ cmd-genurlauth.c \
+ cmd-getmetadata.c \
+ cmd-id.c \
+ cmd-idle.c \
+ cmd-list.c \
+ cmd-logout.c \
+ cmd-lsub.c \
+ cmd-namespace.c \
+ cmd-noop.c \
+ cmd-notify.c \
+ cmd-rename.c \
+ cmd-resetkey.c \
+ cmd-search.c \
+ cmd-select.c \
+ cmd-setmetadata.c \
+ cmd-sort.c \
+ cmd-status.c \
+ cmd-store.c \
+ cmd-subscribe.c \
+ cmd-thread.c \
+ cmd-unselect.c \
+ cmd-unsubscribe.c \
+ cmd-urlfetch.c \
+ cmd-x-cancel.c \
+ cmd-x-state.c
+
+common_sources = \
+ $(cmds) \
+ imap-client.c \
+ imap-client-hibernate.c \
+ imap-commands.c \
+ imap-commands-util.c \
+ imap-expunge.c \
+ imap-feature.c \
+ imap-fetch.c \
+ imap-fetch-body.c \
+ imap-list.c \
+ imap-master-client.c \
+ imap-notify.c \
+ imap-search.c \
+ imap-search-args.c \
+ imap-settings.c \
+ imap-status.c \
+ imap-state.c \
+ imap-sync.c \
+ mail-storage-callbacks.c
+
+imap_SOURCES = \
+ $(common_sources) \
+ main.c
+
+headers = \
+ imap-client.h \
+ imap-commands.h \
+ imap-commands-util.h \
+ imap-common.h \
+ imap-expunge.h \
+ imap-feature.h \
+ imap-fetch.h \
+ imap-list.h \
+ imap-master-client.h \
+ imap-notify.h \
+ imap-search.h \
+ imap-search-args.h \
+ imap-settings.h \
+ imap-status.h \
+ imap-state.h \
+ imap-sync.h \
+ imap-sync-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-imap-client-hibernate
+noinst_PROGRAMS = $(test_programs)
+
+test_imap_client_hibernate_SOURCES = \
+ test-imap-client-hibernate.c $(common_sources)
+test_imap_client_hibernate_LDADD = $(imap_LDADD)
+test_imap_client_hibernate_DEPENDENCIES = $(imap_DEPENDENCIES)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/imap/Makefile.in b/src/imap/Makefile.in
new file mode 100644
index 0000000..f6211e2
--- /dev/null
+++ b/src/imap/Makefile.in
@@ -0,0 +1,1213 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = imap$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/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-client-hibernate$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am__objects_1 = cmd-append.$(OBJEXT) cmd-capability.$(OBJEXT) \
+ cmd-cancelupdate.$(OBJEXT) cmd-check.$(OBJEXT) \
+ cmd-close.$(OBJEXT) cmd-copy.$(OBJEXT) cmd-create.$(OBJEXT) \
+ cmd-delete.$(OBJEXT) cmd-enable.$(OBJEXT) \
+ cmd-examine.$(OBJEXT) cmd-expunge.$(OBJEXT) \
+ cmd-fetch.$(OBJEXT) cmd-genurlauth.$(OBJEXT) \
+ cmd-getmetadata.$(OBJEXT) cmd-id.$(OBJEXT) cmd-idle.$(OBJEXT) \
+ cmd-list.$(OBJEXT) cmd-logout.$(OBJEXT) cmd-lsub.$(OBJEXT) \
+ cmd-namespace.$(OBJEXT) cmd-noop.$(OBJEXT) \
+ cmd-notify.$(OBJEXT) cmd-rename.$(OBJEXT) \
+ cmd-resetkey.$(OBJEXT) cmd-search.$(OBJEXT) \
+ cmd-select.$(OBJEXT) cmd-setmetadata.$(OBJEXT) \
+ cmd-sort.$(OBJEXT) cmd-status.$(OBJEXT) cmd-store.$(OBJEXT) \
+ cmd-subscribe.$(OBJEXT) cmd-thread.$(OBJEXT) \
+ cmd-unselect.$(OBJEXT) cmd-unsubscribe.$(OBJEXT) \
+ cmd-urlfetch.$(OBJEXT) cmd-x-cancel.$(OBJEXT) \
+ cmd-x-state.$(OBJEXT)
+am__objects_2 = $(am__objects_1) imap-client.$(OBJEXT) \
+ imap-client-hibernate.$(OBJEXT) imap-commands.$(OBJEXT) \
+ imap-commands-util.$(OBJEXT) imap-expunge.$(OBJEXT) \
+ imap-feature.$(OBJEXT) imap-fetch.$(OBJEXT) \
+ imap-fetch-body.$(OBJEXT) imap-list.$(OBJEXT) \
+ imap-master-client.$(OBJEXT) imap-notify.$(OBJEXT) \
+ imap-search.$(OBJEXT) imap-search-args.$(OBJEXT) \
+ imap-settings.$(OBJEXT) imap-status.$(OBJEXT) \
+ imap-state.$(OBJEXT) imap-sync.$(OBJEXT) \
+ mail-storage-callbacks.$(OBJEXT)
+am_imap_OBJECTS = $(am__objects_2) main.$(OBJEXT)
+imap_OBJECTS = $(am_imap_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+imap_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(imap_LDFLAGS) $(LDFLAGS) -o $@
+am_test_imap_client_hibernate_OBJECTS = \
+ test-imap-client-hibernate.$(OBJEXT) $(am__objects_2)
+test_imap_client_hibernate_OBJECTS = \
+ $(am_test_imap_client_hibernate_OBJECTS)
+am__DEPENDENCIES_2 = ../lib-imap-urlauth/libimap-urlauth.la \
+ $(am__DEPENDENCIES_1) $(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)/cmd-append.Po \
+ ./$(DEPDIR)/cmd-cancelupdate.Po ./$(DEPDIR)/cmd-capability.Po \
+ ./$(DEPDIR)/cmd-check.Po ./$(DEPDIR)/cmd-close.Po \
+ ./$(DEPDIR)/cmd-copy.Po ./$(DEPDIR)/cmd-create.Po \
+ ./$(DEPDIR)/cmd-delete.Po ./$(DEPDIR)/cmd-enable.Po \
+ ./$(DEPDIR)/cmd-examine.Po ./$(DEPDIR)/cmd-expunge.Po \
+ ./$(DEPDIR)/cmd-fetch.Po ./$(DEPDIR)/cmd-genurlauth.Po \
+ ./$(DEPDIR)/cmd-getmetadata.Po ./$(DEPDIR)/cmd-id.Po \
+ ./$(DEPDIR)/cmd-idle.Po ./$(DEPDIR)/cmd-list.Po \
+ ./$(DEPDIR)/cmd-logout.Po ./$(DEPDIR)/cmd-lsub.Po \
+ ./$(DEPDIR)/cmd-namespace.Po ./$(DEPDIR)/cmd-noop.Po \
+ ./$(DEPDIR)/cmd-notify.Po ./$(DEPDIR)/cmd-rename.Po \
+ ./$(DEPDIR)/cmd-resetkey.Po ./$(DEPDIR)/cmd-search.Po \
+ ./$(DEPDIR)/cmd-select.Po ./$(DEPDIR)/cmd-setmetadata.Po \
+ ./$(DEPDIR)/cmd-sort.Po ./$(DEPDIR)/cmd-status.Po \
+ ./$(DEPDIR)/cmd-store.Po ./$(DEPDIR)/cmd-subscribe.Po \
+ ./$(DEPDIR)/cmd-thread.Po ./$(DEPDIR)/cmd-unselect.Po \
+ ./$(DEPDIR)/cmd-unsubscribe.Po ./$(DEPDIR)/cmd-urlfetch.Po \
+ ./$(DEPDIR)/cmd-x-cancel.Po ./$(DEPDIR)/cmd-x-state.Po \
+ ./$(DEPDIR)/imap-client-hibernate.Po \
+ ./$(DEPDIR)/imap-client.Po ./$(DEPDIR)/imap-commands-util.Po \
+ ./$(DEPDIR)/imap-commands.Po ./$(DEPDIR)/imap-expunge.Po \
+ ./$(DEPDIR)/imap-feature.Po ./$(DEPDIR)/imap-fetch-body.Po \
+ ./$(DEPDIR)/imap-fetch.Po ./$(DEPDIR)/imap-list.Po \
+ ./$(DEPDIR)/imap-master-client.Po ./$(DEPDIR)/imap-notify.Po \
+ ./$(DEPDIR)/imap-search-args.Po ./$(DEPDIR)/imap-search.Po \
+ ./$(DEPDIR)/imap-settings.Po ./$(DEPDIR)/imap-state.Po \
+ ./$(DEPDIR)/imap-status.Po ./$(DEPDIR)/imap-sync.Po \
+ ./$(DEPDIR)/mail-storage-callbacks.Po ./$(DEPDIR)/main.Po \
+ ./$(DEPDIR)/test-imap-client-hibernate.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 = $(imap_SOURCES) $(test_imap_client_hibernate_SOURCES)
+DIST_SOURCES = $(imap_SOURCES) $(test_imap_client_hibernate_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; }; \
+ }
+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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ $(BINARY_CFLAGS)
+
+imap_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+imap_LDADD = \
+ ../lib-imap-urlauth/libimap-urlauth.la \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+imap_DEPENDENCIES = \
+ ../lib-imap-urlauth/libimap-urlauth.la \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+cmds = \
+ cmd-append.c \
+ cmd-capability.c \
+ cmd-cancelupdate.c \
+ cmd-check.c \
+ cmd-close.c \
+ cmd-copy.c \
+ cmd-create.c \
+ cmd-delete.c \
+ cmd-enable.c \
+ cmd-examine.c \
+ cmd-expunge.c \
+ cmd-fetch.c \
+ cmd-genurlauth.c \
+ cmd-getmetadata.c \
+ cmd-id.c \
+ cmd-idle.c \
+ cmd-list.c \
+ cmd-logout.c \
+ cmd-lsub.c \
+ cmd-namespace.c \
+ cmd-noop.c \
+ cmd-notify.c \
+ cmd-rename.c \
+ cmd-resetkey.c \
+ cmd-search.c \
+ cmd-select.c \
+ cmd-setmetadata.c \
+ cmd-sort.c \
+ cmd-status.c \
+ cmd-store.c \
+ cmd-subscribe.c \
+ cmd-thread.c \
+ cmd-unselect.c \
+ cmd-unsubscribe.c \
+ cmd-urlfetch.c \
+ cmd-x-cancel.c \
+ cmd-x-state.c
+
+common_sources = \
+ $(cmds) \
+ imap-client.c \
+ imap-client-hibernate.c \
+ imap-commands.c \
+ imap-commands-util.c \
+ imap-expunge.c \
+ imap-feature.c \
+ imap-fetch.c \
+ imap-fetch-body.c \
+ imap-list.c \
+ imap-master-client.c \
+ imap-notify.c \
+ imap-search.c \
+ imap-search-args.c \
+ imap-settings.c \
+ imap-status.c \
+ imap-state.c \
+ imap-sync.c \
+ mail-storage-callbacks.c
+
+imap_SOURCES = \
+ $(common_sources) \
+ main.c
+
+headers = \
+ imap-client.h \
+ imap-commands.h \
+ imap-commands-util.h \
+ imap-common.h \
+ imap-expunge.h \
+ imap-feature.h \
+ imap-fetch.h \
+ imap-list.h \
+ imap-master-client.h \
+ imap-notify.h \
+ imap-search.h \
+ imap-search-args.h \
+ imap-settings.h \
+ imap-status.h \
+ imap-state.h \
+ imap-sync.h \
+ imap-sync-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-imap-client-hibernate
+
+test_imap_client_hibernate_SOURCES = \
+ test-imap-client-hibernate.c $(common_sources)
+
+test_imap_client_hibernate_LDADD = $(imap_LDADD)
+test_imap_client_hibernate_DEPENDENCIES = $(imap_DEPENDENCIES)
+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/imap/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+imap$(EXEEXT): $(imap_OBJECTS) $(imap_DEPENDENCIES) $(EXTRA_imap_DEPENDENCIES)
+ @rm -f imap$(EXEEXT)
+ $(AM_V_CCLD)$(imap_LINK) $(imap_OBJECTS) $(imap_LDADD) $(LIBS)
+
+test-imap-client-hibernate$(EXEEXT): $(test_imap_client_hibernate_OBJECTS) $(test_imap_client_hibernate_DEPENDENCIES) $(EXTRA_test_imap_client_hibernate_DEPENDENCIES)
+ @rm -f test-imap-client-hibernate$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_client_hibernate_OBJECTS) $(test_imap_client_hibernate_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-append.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-cancelupdate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-capability.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-check.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-close.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-copy.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-create.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-delete.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-enable.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-examine.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-expunge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-fetch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-genurlauth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-getmetadata.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-id.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-idle.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-list.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-logout.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-lsub.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-namespace.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-noop.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-notify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-rename.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-resetkey.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-search.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-select.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-setmetadata.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-sort.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-store.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-subscribe.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-thread.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-unselect.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-unsubscribe.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-urlfetch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-x-cancel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-x-state.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-client-hibernate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-commands-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-expunge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-feature.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-fetch-body.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-fetch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-list.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-master-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-notify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-search-args.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-search.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-state.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-sync.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-callbacks.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-client-hibernate.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/cmd-append.Po
+ -rm -f ./$(DEPDIR)/cmd-cancelupdate.Po
+ -rm -f ./$(DEPDIR)/cmd-capability.Po
+ -rm -f ./$(DEPDIR)/cmd-check.Po
+ -rm -f ./$(DEPDIR)/cmd-close.Po
+ -rm -f ./$(DEPDIR)/cmd-copy.Po
+ -rm -f ./$(DEPDIR)/cmd-create.Po
+ -rm -f ./$(DEPDIR)/cmd-delete.Po
+ -rm -f ./$(DEPDIR)/cmd-enable.Po
+ -rm -f ./$(DEPDIR)/cmd-examine.Po
+ -rm -f ./$(DEPDIR)/cmd-expunge.Po
+ -rm -f ./$(DEPDIR)/cmd-fetch.Po
+ -rm -f ./$(DEPDIR)/cmd-genurlauth.Po
+ -rm -f ./$(DEPDIR)/cmd-getmetadata.Po
+ -rm -f ./$(DEPDIR)/cmd-id.Po
+ -rm -f ./$(DEPDIR)/cmd-idle.Po
+ -rm -f ./$(DEPDIR)/cmd-list.Po
+ -rm -f ./$(DEPDIR)/cmd-logout.Po
+ -rm -f ./$(DEPDIR)/cmd-lsub.Po
+ -rm -f ./$(DEPDIR)/cmd-namespace.Po
+ -rm -f ./$(DEPDIR)/cmd-noop.Po
+ -rm -f ./$(DEPDIR)/cmd-notify.Po
+ -rm -f ./$(DEPDIR)/cmd-rename.Po
+ -rm -f ./$(DEPDIR)/cmd-resetkey.Po
+ -rm -f ./$(DEPDIR)/cmd-search.Po
+ -rm -f ./$(DEPDIR)/cmd-select.Po
+ -rm -f ./$(DEPDIR)/cmd-setmetadata.Po
+ -rm -f ./$(DEPDIR)/cmd-sort.Po
+ -rm -f ./$(DEPDIR)/cmd-status.Po
+ -rm -f ./$(DEPDIR)/cmd-store.Po
+ -rm -f ./$(DEPDIR)/cmd-subscribe.Po
+ -rm -f ./$(DEPDIR)/cmd-thread.Po
+ -rm -f ./$(DEPDIR)/cmd-unselect.Po
+ -rm -f ./$(DEPDIR)/cmd-unsubscribe.Po
+ -rm -f ./$(DEPDIR)/cmd-urlfetch.Po
+ -rm -f ./$(DEPDIR)/cmd-x-cancel.Po
+ -rm -f ./$(DEPDIR)/cmd-x-state.Po
+ -rm -f ./$(DEPDIR)/imap-client-hibernate.Po
+ -rm -f ./$(DEPDIR)/imap-client.Po
+ -rm -f ./$(DEPDIR)/imap-commands-util.Po
+ -rm -f ./$(DEPDIR)/imap-commands.Po
+ -rm -f ./$(DEPDIR)/imap-expunge.Po
+ -rm -f ./$(DEPDIR)/imap-feature.Po
+ -rm -f ./$(DEPDIR)/imap-fetch-body.Po
+ -rm -f ./$(DEPDIR)/imap-fetch.Po
+ -rm -f ./$(DEPDIR)/imap-list.Po
+ -rm -f ./$(DEPDIR)/imap-master-client.Po
+ -rm -f ./$(DEPDIR)/imap-notify.Po
+ -rm -f ./$(DEPDIR)/imap-search-args.Po
+ -rm -f ./$(DEPDIR)/imap-search.Po
+ -rm -f ./$(DEPDIR)/imap-settings.Po
+ -rm -f ./$(DEPDIR)/imap-state.Po
+ -rm -f ./$(DEPDIR)/imap-status.Po
+ -rm -f ./$(DEPDIR)/imap-sync.Po
+ -rm -f ./$(DEPDIR)/mail-storage-callbacks.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/test-imap-client-hibernate.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-pkglibexecPROGRAMS
+
+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)/cmd-append.Po
+ -rm -f ./$(DEPDIR)/cmd-cancelupdate.Po
+ -rm -f ./$(DEPDIR)/cmd-capability.Po
+ -rm -f ./$(DEPDIR)/cmd-check.Po
+ -rm -f ./$(DEPDIR)/cmd-close.Po
+ -rm -f ./$(DEPDIR)/cmd-copy.Po
+ -rm -f ./$(DEPDIR)/cmd-create.Po
+ -rm -f ./$(DEPDIR)/cmd-delete.Po
+ -rm -f ./$(DEPDIR)/cmd-enable.Po
+ -rm -f ./$(DEPDIR)/cmd-examine.Po
+ -rm -f ./$(DEPDIR)/cmd-expunge.Po
+ -rm -f ./$(DEPDIR)/cmd-fetch.Po
+ -rm -f ./$(DEPDIR)/cmd-genurlauth.Po
+ -rm -f ./$(DEPDIR)/cmd-getmetadata.Po
+ -rm -f ./$(DEPDIR)/cmd-id.Po
+ -rm -f ./$(DEPDIR)/cmd-idle.Po
+ -rm -f ./$(DEPDIR)/cmd-list.Po
+ -rm -f ./$(DEPDIR)/cmd-logout.Po
+ -rm -f ./$(DEPDIR)/cmd-lsub.Po
+ -rm -f ./$(DEPDIR)/cmd-namespace.Po
+ -rm -f ./$(DEPDIR)/cmd-noop.Po
+ -rm -f ./$(DEPDIR)/cmd-notify.Po
+ -rm -f ./$(DEPDIR)/cmd-rename.Po
+ -rm -f ./$(DEPDIR)/cmd-resetkey.Po
+ -rm -f ./$(DEPDIR)/cmd-search.Po
+ -rm -f ./$(DEPDIR)/cmd-select.Po
+ -rm -f ./$(DEPDIR)/cmd-setmetadata.Po
+ -rm -f ./$(DEPDIR)/cmd-sort.Po
+ -rm -f ./$(DEPDIR)/cmd-status.Po
+ -rm -f ./$(DEPDIR)/cmd-store.Po
+ -rm -f ./$(DEPDIR)/cmd-subscribe.Po
+ -rm -f ./$(DEPDIR)/cmd-thread.Po
+ -rm -f ./$(DEPDIR)/cmd-unselect.Po
+ -rm -f ./$(DEPDIR)/cmd-unsubscribe.Po
+ -rm -f ./$(DEPDIR)/cmd-urlfetch.Po
+ -rm -f ./$(DEPDIR)/cmd-x-cancel.Po
+ -rm -f ./$(DEPDIR)/cmd-x-state.Po
+ -rm -f ./$(DEPDIR)/imap-client-hibernate.Po
+ -rm -f ./$(DEPDIR)/imap-client.Po
+ -rm -f ./$(DEPDIR)/imap-commands-util.Po
+ -rm -f ./$(DEPDIR)/imap-commands.Po
+ -rm -f ./$(DEPDIR)/imap-expunge.Po
+ -rm -f ./$(DEPDIR)/imap-feature.Po
+ -rm -f ./$(DEPDIR)/imap-fetch-body.Po
+ -rm -f ./$(DEPDIR)/imap-fetch.Po
+ -rm -f ./$(DEPDIR)/imap-list.Po
+ -rm -f ./$(DEPDIR)/imap-master-client.Po
+ -rm -f ./$(DEPDIR)/imap-notify.Po
+ -rm -f ./$(DEPDIR)/imap-search-args.Po
+ -rm -f ./$(DEPDIR)/imap-search.Po
+ -rm -f ./$(DEPDIR)/imap-settings.Po
+ -rm -f ./$(DEPDIR)/imap-state.Po
+ -rm -f ./$(DEPDIR)/imap-status.Po
+ -rm -f ./$(DEPDIR)/imap-sync.Po
+ -rm -f ./$(DEPDIR)/mail-storage-callbacks.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/test-imap-client-hibernate.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-pkglibexecPROGRAMS
+
+.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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS
+
+.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/imap/cmd-append.c b/src/imap/cmd-append.c
new file mode 100644
index 0000000..0d0c1c3
--- /dev/null
+++ b/src/imap/cmd-append.c
@@ -0,0 +1,957 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "str.h"
+#include "imap-resp-code.h"
+#include "istream-binary-converter.h"
+#include "mail-storage-private.h"
+#include "imap-parser.h"
+#include "imap-date.h"
+#include "imap-util.h"
+#include "imap-commands.h"
+#include "imap-msgpart-url.h"
+
+#include <sys/time.h>
+
+/* Don't allow internaldates to be too far in the future. At least with Maildir
+ they can cause problems with incremental backups since internaldate is
+ stored in file's mtime. But perhaps there are also some other reasons why
+ it might not be wanted. */
+#define INTERNALDATE_MAX_FUTURE_SECS (2*3600)
+
+struct cmd_append_context {
+ struct client *client;
+ struct client_command_context *cmd;
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ time_t started;
+
+ struct istream_chain *catchain;
+ uoff_t cat_msg_size;
+
+ struct istream *input;
+ struct istream *litinput;
+ uoff_t literal_size;
+
+ struct imap_parser *save_parser;
+ struct mail_save_context *save_ctx;
+ unsigned int count;
+
+ bool message_input:1;
+ bool binary_input:1;
+ bool catenate:1;
+ bool cmd_args_set:1;
+ bool failed:1;
+};
+
+static void cmd_append_finish(struct cmd_append_context *ctx);
+static bool cmd_append_continue_message(struct client_command_context *cmd);
+static bool cmd_append_parse_new_msg(struct client_command_context *cmd);
+
+static const char *
+get_disconnect_reason(struct cmd_append_context *ctx, uoff_t lit_offset)
+{
+ string_t *str = t_str_new(128);
+ unsigned int secs = ioloop_time - ctx->started;
+
+ str_printfa(str, "%s (While APPENDing: %u msgs, %u secs",
+ i_stream_get_disconnect_reason(ctx->input),
+ ctx->count, secs);
+ if (ctx->literal_size > 0) {
+ str_printfa(str, ", %"PRIuUOFF_T"/%"PRIuUOFF_T" bytes",
+ lit_offset, ctx->literal_size);
+ }
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void client_input_append(struct client_command_context *cmd)
+{
+ struct cmd_append_context *ctx = cmd->context;
+ struct client *client = cmd->client;
+ const char *reason;
+ bool finished;
+ uoff_t lit_offset;
+
+ i_assert(!client->destroyed);
+
+ client->last_input = ioloop_time;
+ timeout_reset(client->to_idle);
+
+ switch (i_stream_read(client->input)) {
+ case -1:
+ /* disconnected */
+ lit_offset = ctx->litinput == NULL ? 0 :
+ ctx->litinput->v_offset;
+ reason = get_disconnect_reason(ctx, lit_offset);
+ cmd_append_finish(cmd->context);
+ /* Reset command so that client_destroy() doesn't try to call
+ cmd_append_continue_message() anymore. */
+ client_command_free(&cmd);
+ client_destroy(client, reason);
+ return;
+ case -2:
+ if (ctx->message_input) {
+ /* message data, this is handled internally by
+ mailbox_save_continue() */
+ break;
+ }
+ cmd_append_finish(cmd->context);
+
+ /* parameter word is longer than max. input buffer size.
+ this is most likely an error, so skip the new data
+ until newline is found. */
+ client->input_skip_line = TRUE;
+
+ if (!ctx->failed)
+ client_send_command_error(cmd, "Too long argument.");
+ cmd->param_error = TRUE;
+ client_command_free(&cmd);
+ return;
+ }
+
+ o_stream_cork(client->output);
+ finished = command_exec(cmd);
+ if (!finished)
+ (void)client_handle_unfinished_cmd(cmd);
+ else
+ client_command_free(&cmd);
+ cmd_sync_delayed(client);
+ o_stream_uncork(client->output);
+
+ client_continue_pending_input(client);
+}
+
+static void cmd_append_finish(struct cmd_append_context *ctx)
+{
+ if (ctx->save_parser != NULL)
+ imap_parser_unref(&ctx->save_parser);
+
+ i_assert(ctx->client->input_lock == ctx->cmd);
+
+ io_remove(&ctx->client->io);
+ /* we must put back the original flush callback before beginning to
+ sync (the command is still unfinished at that point) */
+ o_stream_set_flush_callback(ctx->client->output,
+ client_output, ctx->client);
+
+ i_stream_unref(&ctx->litinput);
+ i_stream_unref(&ctx->input);
+ if (ctx->save_ctx != NULL)
+ mailbox_save_cancel(&ctx->save_ctx);
+ if (ctx->t != NULL)
+ mailbox_transaction_rollback(&ctx->t);
+ if (ctx->box != ctx->cmd->client->mailbox && ctx->box != NULL)
+ mailbox_free(&ctx->box);
+}
+
+static bool cmd_append_send_literal_continue(struct cmd_append_context *ctx)
+{
+ if (ctx->failed) {
+ /* tagline was already sent, we can abort here */
+ return FALSE;
+ }
+
+ o_stream_nsend(ctx->client->output, "+ OK\r\n", 6);
+ o_stream_uncork(ctx->client->output);
+ o_stream_cork(ctx->client->output);
+ return TRUE;
+}
+
+static int
+cmd_append_catenate_mpurl(struct client_command_context *cmd,
+ const char *caturl, struct imap_msgpart_url *mpurl)
+{
+ struct cmd_append_context *ctx = cmd->context;
+ struct imap_msgpart_open_result mpresult;
+ uoff_t newsize;
+ const char *client_error;
+ int ret;
+
+ /* catenate URL */
+ ret = imap_msgpart_url_read_part(mpurl, &mpresult, &client_error);
+ if (ret < 0) {
+ client_send_box_error(cmd, ctx->box);
+ return -1;
+ }
+ if (ret == 0) {
+ /* invalid url, abort */
+ client_send_tagline(cmd,
+ t_strdup_printf("NO [BADURL %s] %s.",
+ caturl, client_error));
+ return -1;
+ }
+ if (mpresult.size == 0) {
+ /* empty input */
+ return 0;
+ }
+
+ newsize = ctx->cat_msg_size + mpresult.size;
+ if (newsize < ctx->cat_msg_size) {
+ client_send_tagline(cmd,
+ "NO [TOOBIG] Composed message grows too big.");
+ return -1;
+ }
+
+ ctx->cat_msg_size = newsize;
+ /* add this input stream to chain */
+ i_stream_chain_append(ctx->catchain, mpresult.input);
+ /* save by reading the chain stream */
+ do {
+ ret = i_stream_read(mpresult.input);
+ i_assert(ret != 0); /* we can handle only blocking input here */
+ } while (mailbox_save_continue(ctx->save_ctx) == 0 && ret != -1);
+
+ if (mpresult.input->stream_errno != 0) {
+ mailbox_set_critical(ctx->box,
+ "read(%s) failed: %s (for CATENATE URL %s)",
+ i_stream_get_name(mpresult.input),
+ i_stream_get_error(mpresult.input), caturl);
+ client_send_box_error(cmd, ctx->box);
+ ret = -1;
+ } else if (!mpresult.input->eof) {
+ /* save failed */
+ client_send_box_error(cmd, ctx->box);
+ ret = -1;
+ } else {
+ /* all the input must be consumed, so istream-chain's read()
+ unreferences the stream and we can free its parent mail */
+ i_assert(!i_stream_have_bytes_left(mpresult.input));
+ ret = 0;
+ }
+ return ret;
+}
+
+static int
+cmd_append_catenate_url(struct client_command_context *cmd, const char *caturl)
+{
+ struct cmd_append_context *ctx = cmd->context;
+ struct imap_msgpart_url *mpurl;
+ const char *client_error;
+ int ret;
+
+ if (ctx->failed)
+ return -1;
+
+ ret = imap_msgpart_url_parse(cmd->client->user, cmd->client->mailbox,
+ caturl, &mpurl, &client_error);
+ if (ret < 0) {
+ client_send_box_error(cmd, ctx->box);
+ return -1;
+ }
+ if (ret == 0) {
+ /* invalid url, abort */
+ client_send_tagline(cmd,
+ t_strdup_printf("NO [BADURL %s] %s.",
+ caturl, client_error));
+ return -1;
+ }
+ ret = cmd_append_catenate_mpurl(cmd, caturl, mpurl);
+ imap_msgpart_url_free(&mpurl);
+ return ret;
+}
+
+static void cmd_append_catenate_text(struct client_command_context *cmd)
+{
+ struct cmd_append_context *ctx = cmd->context;
+
+ if (ctx->literal_size > UOFF_T_MAX - ctx->cat_msg_size &&
+ !ctx->failed) {
+ client_send_tagline(cmd,
+ "NO [TOOBIG] Composed message grows too big.");
+ ctx->failed = TRUE;
+ }
+
+ /* save the mail */
+ ctx->cat_msg_size += ctx->literal_size;
+ if (ctx->literal_size == 0) {
+ /* zero length literal. RFC doesn't explicitly specify
+ what should be done with this, so we'll simply
+ handle it by skipping the empty text part. */
+ ctx->litinput = i_stream_create_from_data("", 0);
+ ctx->litinput->eof = TRUE;
+ } else {
+ ctx->litinput = i_stream_create_limit(cmd->client->input,
+ ctx->literal_size);
+ i_stream_chain_append(ctx->catchain, ctx->litinput);
+ }
+}
+
+static int
+cmd_append_catenate(struct client_command_context *cmd,
+ const struct imap_arg *args, bool *nonsync_r)
+{
+ struct cmd_append_context *ctx = cmd->context;
+ const char *catpart;
+
+ *nonsync_r = FALSE;
+
+ /* Handle URLs until a TEXT literal is encountered */
+ while (imap_arg_get_atom(args, &catpart)) {
+ const char *caturl;
+
+ if (strcasecmp(catpart, "URL") == 0 ) {
+ /* URL <url> */
+ args++;
+ if (!imap_arg_get_astring(args, &caturl))
+ break;
+ if (cmd_append_catenate_url(cmd, caturl) < 0) {
+ /* delay failure until we can stop
+ parsing input */
+ ctx->failed = TRUE;
+ }
+ } else if (strcasecmp(catpart, "TEXT") == 0) {
+ /* TEXT <literal> */
+ args++;
+ if (!imap_arg_get_literal_size(args, &ctx->literal_size))
+ break;
+ if (args->literal8 && !ctx->binary_input &&
+ !ctx->failed) {
+ client_send_tagline(cmd,
+ "NO ["IMAP_RESP_CODE_UNKNOWN_CTE"] "
+ "Binary input allowed only when the first part is binary.");
+ ctx->failed = TRUE;
+ }
+ *nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC;
+ cmd_append_catenate_text(cmd);
+ return 1;
+ } else {
+ break;
+ }
+ args++;
+ }
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ /* ")" */
+ return 0;
+ }
+ if (!ctx->failed)
+ client_send_command_error(cmd, "Invalid arguments.");
+ return -1;
+}
+
+static void cmd_append_finish_catenate(struct client_command_context *cmd)
+{
+ struct cmd_append_context *ctx = cmd->context;
+
+ i_stream_chain_append_eof(ctx->catchain);
+ i_stream_unref(&ctx->input);
+ ctx->catenate = FALSE;
+ ctx->catchain = NULL;
+
+ if (ctx->failed) {
+ /* APPEND has already failed */
+ if (ctx->save_ctx != NULL)
+ mailbox_save_cancel(&ctx->save_ctx);
+ } else {
+ if (mailbox_save_finish(&ctx->save_ctx) < 0) {
+ client_send_box_error(cmd, ctx->box);
+ ctx->failed = TRUE;
+ }
+ }
+}
+
+static bool catenate_args_can_stop(struct cmd_append_context *ctx,
+ const struct imap_arg *args)
+{
+ /* eat away literal_sizes from URLs */
+ while (args->type != IMAP_ARG_EOL) {
+ if (imap_arg_atom_equals(args, "TEXT"))
+ return TRUE;
+ if (!imap_arg_atom_equals(args, "URL")) {
+ /* error - handle it later */
+ return TRUE;
+ }
+ args++;
+ if (args->type == IMAP_ARG_LITERAL_SIZE ||
+ args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC) {
+ if (args->type == IMAP_ARG_LITERAL_SIZE) {
+ if (!cmd_append_send_literal_continue(ctx))
+ return TRUE;
+ }
+ imap_parser_read_last_literal(ctx->save_parser);
+ return FALSE;
+ }
+ args++;
+ }
+ return TRUE;
+}
+
+static bool cmd_append_continue_catenate(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_append_context *ctx = cmd->context;
+ const struct imap_arg *args;
+ const char *client_error;
+ enum imap_parser_error parse_error;
+ bool nonsync = FALSE;
+ int ret;
+
+ if (cmd->cancel) {
+ /* cancel the command immediately (disconnection) */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ /* we're parsing inside CATENATE (..) list after handling a TEXT part.
+ it's fine that this would need to fully fit into input buffer
+ (although clients attempting to DoS could simply insert an extra
+ {1+} between the URLs) */
+ do {
+ ret = imap_parser_read_args(ctx->save_parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_SIZE |
+ IMAP_PARSE_FLAG_LITERAL8 |
+ IMAP_PARSE_FLAG_INSIDE_LIST, &args);
+ } while (ret > 0 && !catenate_args_can_stop(ctx, args));
+ if (ret == -1) {
+ client_error = imap_parser_get_error(ctx->save_parser,
+ &parse_error);
+ switch (parse_error) {
+ case IMAP_PARSE_ERROR_NONE:
+ i_unreached();
+ case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
+ client_send_line(client, t_strconcat("* BYE ",
+ (client->set->imap_literal_minus ? "[TOOBIG] " : ""),
+ client_error, NULL));
+ client_disconnect(client, client_error);
+ break;
+ default:
+ if (!ctx->failed)
+ client_send_command_error(cmd, client_error);
+ }
+ client->input_skip_line = TRUE;
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ if (ret < 0) {
+ /* need more data */
+ return FALSE;
+ }
+
+ if ((ret = cmd_append_catenate(cmd, args, &nonsync)) < 0) {
+ /* invalid parameters, abort immediately */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ if (ret == 0) {
+ /* ")" */
+ cmd_append_finish_catenate(cmd);
+
+ /* last catenate part */
+ imap_parser_reset(ctx->save_parser);
+ cmd->func = cmd_append_parse_new_msg;
+ return cmd_append_parse_new_msg(cmd);
+ }
+
+ /* TEXT <literal> */
+
+ if (!nonsync) {
+ if (!cmd_append_send_literal_continue(ctx)) {
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ }
+
+ i_assert(ctx->litinput != NULL);
+ ctx->message_input = TRUE;
+ cmd->func = cmd_append_continue_message;
+ return cmd_append_continue_message(cmd);
+}
+
+static int
+cmd_append_handle_args(struct client_command_context *cmd,
+ const struct imap_arg *args, bool *nonsync_r)
+{
+ struct client *client = cmd->client;
+ struct cmd_append_context *ctx = cmd->context;
+ const struct imap_arg *flags_list;
+ const struct imap_arg *cat_list = NULL;
+ enum mail_flags flags;
+ const char *const *keywords_list;
+ struct mail_keywords *keywords;
+ struct istream *input;
+ const char *internal_date_str;
+ time_t internal_date;
+ int ret, timezone_offset;
+ bool valid;
+
+ if (!ctx->cmd_args_set) {
+ ctx->cmd_args_set = TRUE;
+ client_args_finished(cmd, args);
+ }
+
+ /* [<flags>] */
+ if (!imap_arg_get_list(args, &flags_list))
+ flags_list = NULL;
+ else
+ args++;
+
+ /* [<internal date>] */
+ if (args->type != IMAP_ARG_STRING)
+ internal_date_str = NULL;
+ else {
+ internal_date_str = imap_arg_as_astring(args);
+ args++;
+ }
+
+ /* <message literal> | CATENATE (..) */
+ valid = FALSE;
+ *nonsync_r = FALSE;
+ ctx->catenate = FALSE;
+ if (imap_arg_get_literal_size(args, &ctx->literal_size)) {
+ *nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC;
+ ctx->binary_input = args->literal8;
+ valid = TRUE;
+ } else if (!imap_arg_atom_equals(args, "CATENATE")) {
+ /* invalid */
+ } else if (!imap_arg_get_list(++args, &cat_list)) {
+ /* invalid */
+ } else {
+ valid = TRUE;
+ ctx->catenate = TRUE;
+ /* We'll do BINARY conversion only if the CATENATE's first
+ part is a literal8. If it doesn't and a literal8 is seen
+ later we'll abort the append with UNKNOWN-CTE. */
+ ctx->binary_input = imap_arg_atom_equals(&cat_list[0], "TEXT") &&
+ cat_list[1].literal8;
+
+ }
+ if (!IMAP_ARG_IS_EOL(&args[1]))
+ valid = FALSE;
+ if (!valid) {
+ client->input_skip_line = TRUE;
+ if (!ctx->failed)
+ client_send_command_error(cmd, "Invalid arguments.");
+ return -1;
+ }
+
+ if (flags_list == NULL || ctx->failed) {
+ flags = 0;
+ keywords = NULL;
+ } else {
+ if (!client_parse_mail_flags(cmd, flags_list,
+ &flags, &keywords_list))
+ return -1;
+ if (keywords_list == NULL)
+ keywords = NULL;
+ else if (mailbox_keywords_create(ctx->box, keywords_list,
+ &keywords) < 0) {
+ /* invalid keywords - delay failure */
+ client_send_box_error(cmd, ctx->box);
+ ctx->failed = TRUE;
+ keywords = NULL;
+ }
+ }
+
+ if (internal_date_str == NULL || ctx->failed) {
+ /* no time given, default to now. */
+ internal_date = (time_t)-1;
+ timezone_offset = 0;
+ } else if (!imap_parse_datetime(internal_date_str,
+ &internal_date, &timezone_offset)) {
+ client_send_command_error(cmd, "Invalid internal date.");
+ if (keywords != NULL)
+ mailbox_keywords_unref(&keywords);
+ return -1;
+ }
+
+ if (internal_date != (time_t)-1 &&
+ internal_date > ioloop_time + INTERNALDATE_MAX_FUTURE_SECS) {
+ /* the client specified a time in the future, set it to now. */
+ internal_date = (time_t)-1;
+ timezone_offset = 0;
+ }
+
+ if (cat_list != NULL) {
+ ctx->cat_msg_size = 0;
+ ctx->input = i_stream_create_chain(&ctx->catchain,
+ IO_BLOCK_SIZE);
+ } else {
+ if (ctx->literal_size == 0) {
+ /* no message data, abort */
+ if (!ctx->failed) {
+ client_send_tagline(cmd,
+ "NO Can't save a zero byte message.");
+ ctx->failed = TRUE;
+ }
+ if (!*nonsync_r) {
+ if (keywords != NULL)
+ mailbox_keywords_unref(&keywords);
+ return -1;
+ }
+ /* {0+} used. although there isn't any point in using
+ MULTIAPPEND here and adding more messages, it is
+ technically valid so we'll continue parsing.. */
+ }
+ ctx->litinput = i_stream_create_limit(client->input, ctx->literal_size);
+ ctx->input = ctx->litinput;
+ i_stream_ref(ctx->input);
+ }
+ if (ctx->binary_input) {
+ input = i_stream_create_binary_converter(ctx->input);
+ i_stream_unref(&ctx->input);
+ ctx->input = input;
+ }
+
+ if (!ctx->failed) {
+ /* save the mail */
+ ctx->save_ctx = mailbox_save_alloc(ctx->t);
+ mailbox_save_set_flags(ctx->save_ctx, flags, keywords);
+ mailbox_save_set_received_date(ctx->save_ctx,
+ internal_date, timezone_offset);
+ if (mailbox_save_begin(&ctx->save_ctx, ctx->input) < 0) {
+ /* save initialization failed */
+ client_send_box_error(cmd, ctx->box);
+ ctx->failed = TRUE;
+ }
+ }
+ if (keywords != NULL)
+ mailbox_keywords_unref(&keywords);
+ ctx->count++;
+
+ if (cat_list == NULL) {
+ /* normal APPEND */
+ return 1;
+ } else if (cat_list->type == IMAP_ARG_EOL) {
+ /* zero parts */
+ if (!ctx->failed)
+ client_send_command_error(cmd, "Empty CATENATE list.");
+ client->input_skip_line = TRUE;
+ return -1;
+ } else if ((ret = cmd_append_catenate(cmd, cat_list, nonsync_r)) < 0) {
+ /* invalid parameters, abort immediately */
+ return -1;
+ } else if (ret == 0) {
+ /* CATENATE consisted only of URLs */
+ return 0;
+ } else {
+ /* TEXT part found from CATENATE */
+ return 1;
+ }
+}
+
+static bool cmd_append_finish_parsing(struct client_command_context *cmd)
+{
+ struct cmd_append_context *ctx = cmd->context;
+ enum mailbox_sync_flags sync_flags;
+ enum imap_sync_flags imap_flags;
+ struct mail_transaction_commit_changes changes;
+ unsigned int save_count;
+ string_t *msg;
+ int ret;
+
+ /* eat away the trailing CRLF */
+ cmd->client->input_skip_line = TRUE;
+
+ if (ctx->failed) {
+ /* we failed earlier, error message is sent */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ if (ctx->count == 0) {
+ client_send_command_error(cmd, "Missing message size.");
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ ret = mailbox_transaction_commit_get_changes(&ctx->t, &changes);
+ if (ret < 0) {
+ client_send_box_error(cmd, ctx->box);
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ msg = t_str_new(256);
+ save_count = seq_range_count(&changes.saved_uids);
+ if (save_count == 0 || changes.no_read_perm) {
+ /* not supported by backend (virtual) */
+ str_append(msg, "OK Append completed.");
+ } else {
+ i_assert(ctx->count == save_count);
+ str_printfa(msg, "OK [APPENDUID %u ",
+ changes.uid_validity);
+ imap_write_seq_range(msg, &changes.saved_uids);
+ str_append(msg, "] Append completed.");
+ }
+ ctx->client->append_count += save_count;
+ pool_unref(&changes.pool);
+
+ if (ctx->box == cmd->client->mailbox) {
+ sync_flags = 0;
+ imap_flags = IMAP_SYNC_FLAG_SAFE;
+ } else {
+ sync_flags = MAILBOX_SYNC_FLAG_FAST;
+ imap_flags = 0;
+ }
+
+ cmd_append_finish(ctx);
+ return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg));
+}
+
+static bool cmd_append_args_can_stop(struct cmd_append_context *ctx,
+ const struct imap_arg *args,
+ bool *last_literal_r)
+{
+ const struct imap_arg *cat_list;
+
+ *last_literal_r = FALSE;
+ if (args->type == IMAP_ARG_EOL)
+ return TRUE;
+
+ /* [(flags)] ["internal date"] <message literal> | CATENATE (..) */
+ if (args->type == IMAP_ARG_LIST)
+ args++;
+ if (args->type == IMAP_ARG_STRING)
+ args++;
+
+ if (args->type == IMAP_ARG_LITERAL_SIZE ||
+ args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC)
+ return TRUE;
+ if (imap_arg_atom_equals(args, "CATENATE") &&
+ imap_arg_get_list(&args[1], &cat_list)) {
+ if (catenate_args_can_stop(ctx, cat_list))
+ return TRUE;
+ *last_literal_r = TRUE;
+ }
+ return FALSE;
+}
+
+static bool cmd_append_parse_new_msg(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_append_context *ctx = cmd->context;
+ const struct imap_arg *args;
+ const char *client_error;
+ enum imap_parser_error parse_error;
+ unsigned int arg_min_count;
+ bool nonsync, last_literal;
+ int ret;
+
+ /* this function gets called 1) after parsing APPEND <mailbox> and
+ 2) with MULTIAPPEND extension after already saving one or more
+ mails. */
+ if (cmd->cancel) {
+ /* cancel the command immediately (disconnection) */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ /* if error occurs, the CRLF is already read. */
+ client->input_skip_line = FALSE;
+
+ /* parse the entire line up to the first message literal, or in case
+ the input buffer is full of MULTIAPPEND CATENATE URLs, parse at
+ least until the beginning of the next message */
+ arg_min_count = 0; last_literal = FALSE;
+ do {
+ if (!last_literal)
+ arg_min_count++;
+ else {
+ /* we only read the literal size. now we read the
+ literal itself. */
+ }
+ ret = imap_parser_read_args(ctx->save_parser, arg_min_count,
+ IMAP_PARSE_FLAG_LITERAL_SIZE |
+ IMAP_PARSE_FLAG_LITERAL8, &args);
+ } while (ret >= (int)arg_min_count &&
+ !cmd_append_args_can_stop(ctx, args, &last_literal));
+ if (ret == -1) {
+ if (!ctx->failed) {
+ client_error = imap_parser_get_error(ctx->save_parser, &parse_error);
+ switch (parse_error) {
+ case IMAP_PARSE_ERROR_NONE:
+ i_unreached();
+ case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
+ client_send_line(client, t_strconcat("* BYE ",
+ (client->set->imap_literal_minus ? "[TOOBIG] " : ""),
+ client_error, NULL));
+ client_disconnect(client, client_error);
+ break;
+ default:
+ client_send_command_error(cmd, client_error);
+ }
+ }
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ if (ret < 0) {
+ /* need more data */
+ return FALSE;
+ }
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ /* last message */
+ return cmd_append_finish_parsing(cmd);
+ }
+
+ ret = cmd_append_handle_args(cmd, args, &nonsync);
+ if (ret < 0) {
+ /* invalid parameters, abort immediately */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ if (ret == 0) {
+ /* CATENATE contained only URLs. Finish it and see if there
+ are more messages. */
+ cmd_append_finish_catenate(cmd);
+ imap_parser_reset(ctx->save_parser);
+ return cmd_append_parse_new_msg(cmd);
+ }
+
+ if (!nonsync) {
+ if (!cmd_append_send_literal_continue(ctx)) {
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+ }
+
+ i_assert(ctx->litinput != NULL);
+ ctx->message_input = TRUE;
+ cmd->func = cmd_append_continue_message;
+ return cmd_append_continue_message(cmd);
+}
+
+static bool cmd_append_continue_message(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_append_context *ctx = cmd->context;
+ int ret = 0;
+
+ if (cmd->cancel) {
+ /* cancel the command immediately (disconnection) */
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ if (ctx->save_ctx != NULL) {
+ while (ctx->litinput->v_offset != ctx->literal_size) {
+ ret = i_stream_read(ctx->litinput);
+ if (mailbox_save_continue(ctx->save_ctx) < 0) {
+ /* we still have to finish reading the message
+ from client */
+ mailbox_save_cancel(&ctx->save_ctx);
+ break;
+ }
+ if (ret == -1 || ret == 0)
+ break;
+ }
+ }
+
+ if (ctx->save_ctx == NULL) {
+ /* saving has already failed, we're just eating away the
+ literal */
+ (void)i_stream_read(ctx->litinput);
+ i_stream_skip(ctx->litinput,
+ i_stream_get_data_size(ctx->litinput));
+ }
+
+ if (ctx->litinput->eof || client->input->closed) {
+ uoff_t lit_offset = ctx->litinput->v_offset;
+
+ /* finished - do one more read, to make sure istream-chain
+ unreferences its stream, which is needed for litinput's
+ unreferencing to seek the client->input to correct
+ position. the seek is needed to avoid trying to seek
+ backwards in the ctx->input's parent stream. */
+ i_stream_seek(ctx->input, ctx->input->v_offset);
+ (void)i_stream_read(ctx->input);
+ i_stream_unref(&ctx->litinput);
+
+ if (ctx->failed) {
+ if (ctx->save_ctx != NULL)
+ mailbox_save_cancel(&ctx->save_ctx);
+ } else if (ctx->save_ctx == NULL) {
+ /* failed above */
+ client_send_box_error(cmd, ctx->box);
+ ctx->failed = TRUE;
+ } else if (lit_offset != ctx->literal_size) {
+ /* client disconnected before it finished sending the
+ whole message. */
+ ctx->failed = TRUE;
+ mailbox_save_cancel(&ctx->save_ctx);
+ client_disconnect(client,
+ get_disconnect_reason(ctx, lit_offset));
+ } else if (ctx->catenate) {
+ /* CATENATE isn't finished yet */
+ } else if (mailbox_save_finish(&ctx->save_ctx) < 0) {
+ client_send_box_error(cmd, ctx->box);
+ ctx->failed = TRUE;
+ }
+
+ if (client->input->closed) {
+ cmd_append_finish(ctx);
+ return TRUE;
+ }
+
+ /* prepare for the next message (or its part with catenate) */
+ ctx->message_input = FALSE;
+ imap_parser_reset(ctx->save_parser);
+
+ if (ctx->catenate) {
+ cmd->func = cmd_append_continue_catenate;
+ return cmd_append_continue_catenate(cmd);
+ }
+
+ i_stream_unref(&ctx->input);
+ cmd->func = cmd_append_parse_new_msg;
+ return cmd_append_parse_new_msg(cmd);
+ }
+ return FALSE;
+}
+
+bool cmd_append(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_append_context *ctx;
+ const char *mailbox;
+
+ if (client->syncing) {
+ /* if transaction is created while its view is synced,
+ appends aren't allowed for it. */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY;
+ return FALSE;
+ }
+
+ /* <mailbox> */
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+
+ /* we keep the input locked all the time */
+ client->input_lock = cmd;
+
+ ctx = p_new(cmd->pool, struct cmd_append_context, 1);
+ ctx->cmd = cmd;
+ ctx->client = client;
+ ctx->started = ioloop_time;
+ if (client_open_save_dest_box(cmd, mailbox, &ctx->box) < 0)
+ ctx->failed = TRUE;
+ else {
+ event_add_str(cmd->global_event, "mailbox",
+ mailbox_get_vname(ctx->box));
+ ctx->t = mailbox_transaction_begin(ctx->box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS,
+ imap_client_command_get_reason(cmd));
+ }
+
+ io_remove(&client->io);
+ client->io = io_add_istream(client->input, client_input_append, cmd);
+ /* append is special because we're only waiting on client input, not
+ client output, so disable the standard output handler until we're
+ finished */
+ o_stream_unset_flush_callback(client->output);
+
+ ctx->save_parser = imap_parser_create(client->input, client->output,
+ client->set->imap_max_line_length);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(ctx->save_parser);
+
+ cmd->func = cmd_append_parse_new_msg;
+ cmd->context = ctx;
+ return cmd_append_parse_new_msg(cmd);
+}
diff --git a/src/imap/cmd-cancelupdate.c b/src/imap/cmd-cancelupdate.c
new file mode 100644
index 0000000..8676567
--- /dev/null
+++ b/src/imap/cmd-cancelupdate.c
@@ -0,0 +1,45 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-search.h"
+#include "imap-commands.h"
+
+static bool client_search_update_cancel(struct client *client, const char *tag)
+{
+ struct imap_search_update *update;
+ unsigned int idx;
+
+ update = client_search_update_lookup(client, tag, &idx);
+ if (update == NULL)
+ return FALSE;
+
+ imap_search_update_free(update);
+ array_delete(&client->search_updates, idx, 1);
+ return TRUE;
+}
+
+bool cmd_cancelupdate(struct client_command_context *cmd)
+{
+ const struct imap_arg *args;
+ const char *tag;
+ unsigned int i;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ for (i = 0; args[i].type == IMAP_ARG_STRING; i++) ;
+ if (!IMAP_ARG_IS_EOL(&args[i]) || i == 0) {
+ client_send_command_error(cmd, "Invalid parameters.");
+ return TRUE;
+ }
+
+ while (imap_arg_get_quoted(args, &tag)) {
+ if (!client_search_update_cancel(cmd->client, tag)) {
+ client_send_tagline(cmd, "NO Unknown tag.");
+ return TRUE;
+ }
+ args++;
+ }
+ client_send_tagline(cmd, "OK Updates cancelled.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-capability.c b/src/imap/cmd-capability.c
new file mode 100644
index 0000000..3433c89
--- /dev/null
+++ b/src/imap/cmd-capability.c
@@ -0,0 +1,14 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+#include "str.h"
+
+bool cmd_capability(struct client_command_context *cmd)
+{
+ client_send_line(cmd->client, t_strconcat(
+ "* CAPABILITY ", str_c(cmd->client->capability_string), NULL));
+
+ client_send_tagline(cmd, "OK Capability completed.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-check.c b/src/imap/cmd-check.c
new file mode 100644
index 0000000..a456f7c
--- /dev/null
+++ b/src/imap/cmd-check.c
@@ -0,0 +1,14 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_check(struct client_command_context *cmd)
+{
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FULL_READ |
+ MAILBOX_SYNC_FLAG_FULL_WRITE, IMAP_SYNC_FLAG_SAFE,
+ "OK Check completed.");
+}
diff --git a/src/imap/cmd-close.c b/src/imap/cmd-close.c
new file mode 100644
index 0000000..1f4dfeb
--- /dev/null
+++ b/src/imap/cmd-close.c
@@ -0,0 +1,36 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+#include "imap-expunge.h"
+
+bool cmd_close(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct mailbox *mailbox = client->mailbox;
+ struct mail_storage *storage;
+ const char *errstr, *tagged_reply = "OK Close completed.";
+ enum mail_error error = MAIL_ERROR_NONE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ i_assert(client->mailbox_change_lock == NULL);
+
+ storage = mailbox_get_storage(mailbox);
+ if (imap_expunge(mailbox, NULL, &client->expunged_count) < 0) {
+ errstr = mailbox_get_last_error(mailbox, &error);
+ if (error != MAIL_ERROR_PERM)
+ client_send_untagged_storage_error(client, storage);
+ else {
+ tagged_reply = t_strdup_printf(
+ "OK Closed without expunging: %s", errstr);
+ }
+ }
+ if (mailbox_sync(mailbox, 0) < 0)
+ client_send_untagged_storage_error(client, storage);
+
+ imap_client_close_mailbox(client);
+ client_send_tagline(cmd, tagged_reply);
+ return TRUE;
+}
diff --git a/src/imap/cmd-copy.c b/src/imap/cmd-copy.c
new file mode 100644
index 0000000..1ce018b
--- /dev/null
+++ b/src/imap/cmd-copy.c
@@ -0,0 +1,407 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "imap-resp-code.h"
+#include "imap-util.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+
+#include <time.h>
+
+#define COPY_CHECK_INTERVAL 100
+#define MOVE_COMMIT_INTERVAL 1000
+
+struct cmd_copy_context {
+ struct client_command_context *cmd;
+ struct mailbox *srcbox;
+ struct mailbox *destbox;
+ bool move;
+
+ unsigned int copy_count;
+
+ uint32_t uid_validity;
+ ARRAY_TYPE(seq_range) src_uids;
+ ARRAY_TYPE(seq_range) saved_uids;
+ bool hide_saved_uids;
+
+ const char *error_string;
+ enum mail_error mail_error;
+};
+
+static int client_send_sendalive_if_needed(struct client *client)
+{
+ time_t now, last_io;
+ int ret = 0;
+
+ if (o_stream_get_buffer_used_size(client->output) != 0)
+ return 0;
+
+ now = time(NULL);
+ last_io = I_MAX(client->last_input, client->last_output);
+ if (now - last_io > MAIL_STORAGE_STAYALIVE_SECS) {
+ o_stream_nsend_str(client->output, "* OK Hang in there..\r\n");
+ /* make sure it doesn't get stuck on the corked stream */
+ if (o_stream_uncork_flush(client->output) < 0)
+ ret = -1;
+ o_stream_cork(client->output);
+ client->last_output = now;
+ }
+ return ret;
+}
+
+static void copy_update_trashed(struct client *client, struct mailbox *box,
+ unsigned int count)
+{
+ const struct mailbox_settings *set;
+
+ set = mailbox_settings_find(mailbox_get_namespace(box),
+ mailbox_get_vname(box));
+ if (set != NULL && set->special_use[0] != '\0' &&
+ str_array_icase_find(t_strsplit_spaces(set->special_use, " "),
+ "\\Trash"))
+ client->trashed_count += count;
+}
+
+static bool client_is_disconnected(struct client *client)
+{
+ if (client->fd_in == STDIN_FILENO) {
+ /* Skip this check for stdio clients. It's often used in
+ testing where the test expects that all commands will be
+ run even though stdin already has reached EOF. */
+ return FALSE;
+ }
+ ssize_t bytes = i_stream_read(client->input);
+ if (bytes == -1)
+ return TRUE;
+ if (bytes != 0)
+ i_stream_set_input_pending(client->input, TRUE);
+ return FALSE;
+}
+
+static int fetch_and_copy(struct cmd_copy_context *copy_ctx,
+ const struct mail_search_args *uid_search_args)
+{
+ struct client *client = copy_ctx->cmd->client;
+ struct mailbox_transaction_context *t, *src_trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mail_save_context *save_ctx;
+ struct mail *mail;
+ const char *cmd_reason;
+ struct mail_transaction_commit_changes changes;
+ ARRAY_TYPE(seq_range) src_uids;
+ int ret;
+
+ /* convert uidset to seqset */
+ search_args = mail_search_args_dup(uid_search_args);
+ mail_search_args_init(search_args, copy_ctx->srcbox, TRUE, NULL);
+ /* make sure the number of messages didn't already change */
+ i_assert(uid_search_args->args->type == SEARCH_UIDSET);
+ i_assert(search_args->args->type == SEARCH_SEQSET ||
+ (search_args->args->type == SEARCH_ALL &&
+ search_args->args->match_not));
+ if (search_args->args->type != SEARCH_SEQSET ||
+ seq_range_count(&search_args->args->value.seqset) !=
+ seq_range_count(&uid_search_args->args->value.seqset)) {
+ mail_search_args_unref(&search_args);
+ return 0;
+ }
+
+ i_assert(o_stream_is_corked(client->output) ||
+ client->output->stream_errno != 0);
+
+ cmd_reason = imap_client_command_get_reason(copy_ctx->cmd);
+ t = mailbox_transaction_begin(copy_ctx->destbox,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS,
+ cmd_reason);
+ /* Refresh source index so expunged mails will be noticed */
+ src_trans = mailbox_transaction_begin(copy_ctx->srcbox,
+ MAILBOX_TRANSACTION_FLAG_REFRESH,
+ cmd_reason);
+ search_ctx = mailbox_search_init(src_trans, search_args,
+ NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ t_array_init(&src_uids, 64);
+ ret = 1;
+ while (mailbox_search_next(search_ctx, &mail) && ret > 0) {
+ if (mail->expunged) {
+ ret = 0;
+ break;
+ }
+
+ if ((++copy_ctx->copy_count % COPY_CHECK_INTERVAL) == 0) {
+ /* If we're COPYing (not MOVEing), check if client has
+ already disconnected. If yes, abort the COPY to
+ avoid client duplicating the COPY again later.
+ We can detect this as long as the client doesn't
+ fill the input buffer full. */
+ if (client_send_sendalive_if_needed(client) < 0 ||
+ (!copy_ctx->move &&
+ client_is_disconnected(client))) {
+ /* Client disconnected. Use the same failure
+ code path as if some messages were
+ expunged. */
+ ret = 0;
+ break;
+ }
+ }
+
+ save_ctx = mailbox_save_alloc(t);
+ mailbox_save_copy_flags(save_ctx, mail);
+
+ if (copy_ctx->move) {
+ if (mailbox_move(&save_ctx, mail) < 0)
+ ret = -1;
+ } else {
+ if (mailbox_copy(&save_ctx, mail) < 0)
+ ret = -1;
+ }
+ if (ret < 0 && mail->expunged)
+ ret = 0;
+
+ if (ret > 0)
+ seq_range_array_add(&src_uids, mail->uid);
+ }
+
+ if (ret < 0) {
+ copy_ctx->error_string =
+ mailbox_get_last_error(copy_ctx->destbox, &copy_ctx->mail_error);
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0 && ret >= 0) {
+ copy_ctx->error_string =
+ mailbox_get_last_error(copy_ctx->srcbox, &copy_ctx->mail_error);
+ ret = -1;
+ }
+
+ /* Do a final check before committing COPY to see if the client has
+ already disconnected. */
+ if (!copy_ctx->move && client_is_disconnected(client))
+ ret = 0;
+
+ if (ret <= 0)
+ mailbox_transaction_rollback(&t);
+ else if (mailbox_transaction_commit_get_changes(&t, &changes) < 0) {
+ if (mailbox_get_last_mail_error(copy_ctx->destbox) == MAIL_ERROR_EXPUNGED) {
+ /* storage backend didn't notice the expunge until
+ at commit time. */
+ ret = 0;
+ } else {
+ ret = -1;
+ copy_ctx->error_string =
+ mailbox_get_last_error(copy_ctx->destbox, &copy_ctx->mail_error);
+ }
+ } else {
+ if (changes.no_read_perm)
+ copy_ctx->hide_saved_uids = TRUE;
+
+ if (seq_range_count(&changes.saved_uids) == 0) {
+ /* storage doesn't support returning UIDs */
+ copy_ctx->hide_saved_uids = TRUE;
+ }
+
+ if (copy_ctx->uid_validity == 0)
+ copy_ctx->uid_validity = changes.uid_validity;
+ else if (copy_ctx->uid_validity != changes.uid_validity) {
+ /* UIDVALIDITY unexpectedly changed */
+ copy_ctx->hide_saved_uids = TRUE;
+ }
+ seq_range_array_merge(&copy_ctx->src_uids, &src_uids);
+ seq_range_array_merge(&copy_ctx->saved_uids, &changes.saved_uids);
+
+ i_assert(copy_ctx->copy_count == seq_range_count(&copy_ctx->saved_uids) ||
+ copy_ctx->hide_saved_uids);
+ copy_update_trashed(client, copy_ctx->destbox, copy_ctx->copy_count);
+ pool_unref(&changes.pool);
+ }
+
+ if (!copy_ctx->move ||
+ copy_ctx->srcbox == copy_ctx->destbox) {
+ /* copying or moving within the same mailbox
+ succeeded or failed */
+ if (mailbox_transaction_commit(&src_trans) < 0 && ret >= 0) {
+ copy_ctx->error_string =
+ mailbox_get_last_error(copy_ctx->srcbox, &copy_ctx->mail_error);
+ ret = -1;
+ }
+ } else if (ret <= 0) {
+ /* move failed, don't expunge anything */
+ mailbox_transaction_rollback(&src_trans);
+ } else {
+ /* move succeeded */
+ if (mailbox_transaction_commit(&src_trans) < 0 ||
+ mailbox_sync(copy_ctx->srcbox,
+ MAILBOX_SYNC_FLAG_EXPUNGE) < 0) {
+ copy_ctx->error_string =
+ mailbox_get_last_error(copy_ctx->srcbox, &copy_ctx->mail_error);
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static void cmd_move_send_untagged(struct cmd_copy_context *copy_ctx,
+ string_t *msg, string_t *src_uidset)
+{
+ if (array_count(&copy_ctx->saved_uids) == 0)
+ return;
+ str_printfa(msg, "* OK [COPYUID %u %s ",
+ copy_ctx->uid_validity, str_c(src_uidset));
+ imap_write_seq_range(msg, &copy_ctx->saved_uids);
+ str_append(msg, "] Moved UIDs.");
+ client_send_line(copy_ctx->cmd->client, str_c(msg));
+}
+
+static bool cmd_copy_full(struct client_command_context *cmd, bool move)
+{
+ struct client *client = cmd->client;
+ struct mailbox *destbox;
+ struct mail_search_args *search_args;
+ struct imap_search_seqset_iter *seqset_iter = NULL;
+ const char *messageset, *mailbox;
+ enum mailbox_sync_flags sync_flags = 0;
+ enum imap_sync_flags imap_flags = 0;
+ struct cmd_copy_context copy_ctx;
+ string_t *msg, *src_uidset;
+ int ret;
+
+ /* <message set> <mailbox> */
+ if (!client_read_string_args(cmd, 2, &messageset, &mailbox))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ /* First convert the message set to sequences. This way nonexistent
+ UIDs are dropped. */
+ ret = imap_search_get_seqset(cmd, messageset, cmd->uid, &search_args);
+ if (ret <= 0)
+ return ret < 0;
+ if (search_args->args->type == SEARCH_ALL) {
+ i_assert(search_args->args->match_not);
+ mail_search_args_unref(&search_args);
+ return cmd_sync(cmd, sync_flags, imap_flags,
+ "OK No messages found.");
+ }
+ /* Convert seqset to uidset. This is required for MOVE to work
+ correctly, since it opens another view for the source mailbox
+ that can have different sequences. */
+ imap_search_anyset_to_uidset(cmd, search_args);
+
+ if (client_open_save_dest_box(cmd, mailbox, &destbox) < 0) {
+ mail_search_args_unref(&search_args);
+ return TRUE;
+ }
+
+ i_zero(&copy_ctx);
+ copy_ctx.cmd = cmd;
+ copy_ctx.destbox = destbox;
+ if (destbox == client->mailbox || !move)
+ copy_ctx.srcbox = client->mailbox;
+ else {
+ copy_ctx.srcbox = mailbox_alloc(mailbox_get_namespace(client->mailbox)->list,
+ mailbox_get_vname(client->mailbox), 0);
+ if (mailbox_sync(copy_ctx.srcbox, 0) < 0) {
+ mail_search_args_unref(&search_args);
+ client_send_box_error(cmd, copy_ctx.srcbox);
+ mailbox_free(&copy_ctx.srcbox);
+ mailbox_free(&destbox);
+ return TRUE;
+ }
+ }
+ copy_ctx.move = move;
+ i_array_init(&copy_ctx.src_uids, 8);
+ i_array_init(&copy_ctx.saved_uids, 8);
+
+ if (move) {
+ /* When moving mails, perform the work in batches of
+ MOVE_COMMIT_INTERVAL. Each such batch has its own
+ transaction and search query. */
+ seqset_iter = imap_search_seqset_iter_init(search_args,
+ client->messages_count, MOVE_COMMIT_INTERVAL);
+ }
+ do {
+ T_BEGIN {
+ ret = fetch_and_copy(&copy_ctx, search_args);
+ } T_END;
+ if (ret <= 0) {
+ /* failed */
+ break;
+ }
+ } while (seqset_iter != NULL &&
+ imap_search_seqset_iter_next(seqset_iter));
+ imap_search_seqset_iter_deinit(&seqset_iter);
+ mail_search_args_unref(&search_args);
+
+ src_uidset = t_str_new(256);
+ imap_write_seq_range(src_uidset, &copy_ctx.src_uids);
+
+ msg = t_str_new(256);
+ if (ret <= 0) {
+ if (move && array_count(&copy_ctx.src_uids) > 0) {
+ /* some of the messages were successfully moved */
+ cmd_move_send_untagged(&copy_ctx, msg, src_uidset);
+ }
+ } else if (copy_ctx.copy_count == 0) {
+ str_append(msg, "OK No messages found.");
+ } else if (seq_range_count(&copy_ctx.saved_uids) == 0 ||
+ copy_ctx.hide_saved_uids) {
+ /* not supported by backend (virtual) or no read permissions
+ for mailbox */
+ str_append(msg, move ? "OK Move completed." :
+ "OK Copy completed.");
+ } else if (move) {
+ cmd_move_send_untagged(&copy_ctx, msg, src_uidset);
+ str_truncate(msg, 0);
+ str_append(msg, "OK Move completed.");
+ } else {
+ str_printfa(msg, "OK [COPYUID %u %s ", copy_ctx.uid_validity,
+ str_c(src_uidset));
+ imap_write_seq_range(msg, &copy_ctx.saved_uids);
+ str_append(msg, "] Copy completed.");
+ }
+
+ array_free(&copy_ctx.src_uids);
+ array_free(&copy_ctx.saved_uids);
+
+ if (destbox != client->mailbox) {
+ if (move)
+ sync_flags |= MAILBOX_SYNC_FLAG_EXPUNGE;
+ else
+ sync_flags |= MAILBOX_SYNC_FLAG_FAST;
+ imap_flags |= IMAP_SYNC_FLAG_SAFE;
+ mailbox_free(&destbox);
+ } else if (move) {
+ sync_flags |= MAILBOX_SYNC_FLAG_EXPUNGE;
+ imap_flags |= IMAP_SYNC_FLAG_SAFE;
+ }
+ if (copy_ctx.srcbox != client->mailbox)
+ mailbox_free(&copy_ctx.srcbox);
+
+ if (ret > 0)
+ return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg));
+ else if (ret == 0) {
+ /* some messages were expunged, sync them */
+ return cmd_sync(cmd, 0, 0,
+ "NO ["IMAP_RESP_CODE_EXPUNGEISSUED"] "
+ "Some of the requested messages no longer exist.");
+ } else {
+ client_send_error(cmd, copy_ctx.error_string,
+ copy_ctx.mail_error);
+ return TRUE;
+ }
+}
+
+bool cmd_copy(struct client_command_context *cmd)
+{
+ return cmd_copy_full(cmd, FALSE);
+}
+
+bool cmd_move(struct client_command_context *cmd)
+{
+ return cmd_copy_full(cmd, TRUE);
+}
diff --git a/src/imap/cmd-create.c b/src/imap/cmd-create.c
new file mode 100644
index 0000000..2459173
--- /dev/null
+++ b/src/imap/cmd-create.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-resp-code.h"
+#include "mail-namespace.h"
+#include "imap-commands.h"
+
+bool cmd_create(struct client_command_context *cmd)
+{
+ struct mail_namespace *ns;
+ const char *mailbox, *orig_mailbox;
+ struct mailbox *box;
+ bool directory;
+ size_t len;
+
+ /* <mailbox> */
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+
+ orig_mailbox = mailbox;
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ len = strlen(orig_mailbox);
+ if (len == 0 || orig_mailbox[len-1] != mail_namespace_get_sep(ns))
+ directory = FALSE;
+ else {
+ /* name ends with hierarchy separator - client is just
+ informing us that it wants to create children under this
+ mailbox. */
+ directory = TRUE;
+
+ /* drop separator from mailbox. it's already dropped when
+ WORKAROUND_TB_EXTRA_MAILBOX_SEP is enabled */
+ if (len == strlen(mailbox))
+ mailbox = t_strndup(mailbox, len-1);
+ }
+
+ box = mailbox_alloc(ns->list, mailbox, 0);
+ event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box));
+ if (mailbox_create(box, NULL, directory) < 0)
+ client_send_box_error(cmd, box);
+ else
+ client_send_tagline(cmd, "OK Create completed.");
+ mailbox_free(&box);
+ return TRUE;
+}
diff --git a/src/imap/cmd-delete.c b/src/imap/cmd-delete.c
new file mode 100644
index 0000000..e095321
--- /dev/null
+++ b/src/imap/cmd-delete.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_delete(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *name, *client_error;
+ enum mail_error error;
+ bool disconnect = FALSE;
+
+ /* <mailbox> */
+ if (!client_read_string_args(cmd, 1, &name))
+ return FALSE;
+
+ ns = client_find_namespace(cmd, &name);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, name, 0);
+ event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box));
+ if (mailbox_is_any_inbox(box)) {
+ /* IMAP protocol allows this, but I think it's safer to
+ not allow it. */
+ mailbox_free(&box);
+ client_send_tagline(cmd, "NO INBOX can't be deleted.");
+ return TRUE;
+ }
+ if (client->mailbox != NULL &&
+ mailbox_backends_equal(box, client->mailbox)) {
+ /* deleting selected mailbox. close it first */
+ client_search_updates_free(client);
+ mailbox_free(&client->mailbox);
+ disconnect = TRUE;
+ }
+
+ if (mailbox_delete(box) == 0)
+ client_send_tagline(cmd, "OK Delete completed.");
+ else {
+ client_error = mailbox_get_last_error(box, &error);
+ if (error != MAIL_ERROR_EXISTS)
+ client_send_box_error(cmd, box);
+ else {
+ /* mailbox has children */
+ client_send_tagline(cmd, t_strdup_printf("NO %s",
+ client_error));
+ }
+ }
+ mailbox_free(&box);
+
+ if (disconnect) {
+ client_disconnect_with_error(cmd->client,
+ "Selected mailbox was deleted, have to disconnect.");
+ }
+ return TRUE;
+}
diff --git a/src/imap/cmd-enable.c b/src/imap/cmd-enable.c
new file mode 100644
index 0000000..904d6db
--- /dev/null
+++ b/src/imap/cmd-enable.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-feature.h"
+
+bool cmd_enable(struct client_command_context *cmd)
+{
+ const struct imap_arg *args;
+ const char *str;
+ string_t *reply;
+ unsigned int feature_idx;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ reply = t_str_new(64);
+ str_append(reply, "* ENABLED");
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ if (imap_feature_lookup(str, &feature_idx)) {
+ client_enable(cmd->client, feature_idx);
+ str_append_c(reply, ' ');
+ str_append(reply, t_str_ucase(str));
+ }
+ }
+ if (str_len(reply) > 9)
+ client_send_line(cmd->client, str_c(reply));
+ client_send_tagline(cmd, "OK Enabled.");
+ return TRUE;
+}
+
diff --git a/src/imap/cmd-examine.c b/src/imap/cmd-examine.c
new file mode 100644
index 0000000..4000fa0
--- /dev/null
+++ b/src/imap/cmd-examine.c
@@ -0,0 +1,9 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_examine(struct client_command_context *cmd)
+{
+ return cmd_select_full(cmd, TRUE);
+}
diff --git a/src/imap/cmd-expunge.c b/src/imap/cmd-expunge.c
new file mode 100644
index 0000000..7388937
--- /dev/null
+++ b/src/imap/cmd-expunge.c
@@ -0,0 +1,68 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "imap-expunge.h"
+
+static bool ATTR_NULL(2)
+cmd_expunge_finish(struct client_command_context *cmd,
+ struct mail_search_args *search_args)
+{
+ struct client *client = cmd->client;
+ const char *errstr;
+ enum mail_error error = MAIL_ERROR_NONE;
+ int ret;
+
+ ret = imap_expunge(client->mailbox, search_args == NULL ? NULL :
+ search_args->args, &client->expunged_count);
+ if (search_args != NULL)
+ mail_search_args_unref(&search_args);
+ if (ret < 0) {
+ errstr = mailbox_get_last_error(client->mailbox, &error);
+ if (error != MAIL_ERROR_PERM) {
+ client_send_box_error(cmd, client->mailbox);
+ return TRUE;
+ } else {
+ return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE,
+ t_strdup_printf("OK Expunge ignored: %s.",
+ errstr));
+ }
+ }
+
+ client->sync_seen_deletes = FALSE;
+ return cmd_sync(cmd, MAILBOX_SYNC_FLAG_EXPUNGE,
+ IMAP_SYNC_FLAG_SAFE, "OK Expunge completed.");
+}
+
+bool cmd_uid_expunge(struct client_command_context *cmd)
+{
+ const struct imap_arg *args;
+ struct mail_search_args *search_args;
+ const char *uidset;
+ int ret;
+
+ if (!client_read_args(cmd, 1, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ if (!imap_arg_get_astring(&args[0], &uidset)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ ret = imap_search_get_seqset(cmd, uidset, TRUE, &search_args);
+ if (ret <= 0)
+ return ret < 0;
+ return cmd_expunge_finish(cmd, search_args);
+}
+
+bool cmd_expunge(struct client_command_context *cmd)
+{
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ return cmd_expunge_finish(cmd, NULL);
+}
diff --git a/src/imap/cmd-fetch.c b/src/imap/cmd-fetch.c
new file mode 100644
index 0000000..35e85d1
--- /dev/null
+++ b/src/imap/cmd-fetch.c
@@ -0,0 +1,391 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ostream.h"
+#include "imap-resp-code.h"
+#include "imap-commands.h"
+#include "imap-fetch.h"
+#include "imap-search-args.h"
+#include "mail-search.h"
+
+
+static const char *all_macro[] = {
+ "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", NULL
+};
+static const char *fast_macro[] = {
+ "FLAGS", "INTERNALDATE", "RFC822.SIZE", NULL
+};
+static const char *full_macro[] = {
+ "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY", NULL
+};
+
+static bool
+imap_fetch_cmd_init_handler(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd,
+ const char *name, const struct imap_arg **args)
+{
+ struct imap_fetch_init_context init_ctx;
+
+ i_zero(&init_ctx);
+ init_ctx.fetch_ctx = ctx;
+ init_ctx.pool = ctx->ctx_pool;
+ init_ctx.name = name;
+ init_ctx.args = *args;
+
+ if (!imap_fetch_init_handler(&init_ctx)) {
+ i_assert(init_ctx.error != NULL);
+ client_send_command_error(cmd, init_ctx.error);
+ return FALSE;
+ }
+ *args = init_ctx.args;
+ return TRUE;
+}
+
+static bool
+fetch_parse_args(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd,
+ const struct imap_arg *arg, const struct imap_arg **next_arg_r)
+{
+ const char *str, *const *macro;
+
+ if (cmd->uid) {
+ if (!imap_fetch_cmd_init_handler(ctx, cmd, "UID", &arg))
+ return FALSE;
+ }
+ if (imap_arg_get_atom(arg, &str)) {
+ str = t_str_ucase(str);
+ arg++;
+
+ /* handle macros first */
+ if (strcmp(str, "ALL") == 0)
+ macro = all_macro;
+ else if (strcmp(str, "FAST") == 0)
+ macro = fast_macro;
+ else if (strcmp(str, "FULL") == 0)
+ macro = full_macro;
+ else {
+ macro = NULL;
+ if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg))
+ return FALSE;
+ }
+ if (macro != NULL) {
+ while (*macro != NULL) {
+ if (!imap_fetch_cmd_init_handler(ctx, cmd, *macro, &arg))
+ return FALSE;
+ macro++;
+ }
+ }
+ *next_arg_r = arg;
+ } else {
+ *next_arg_r = arg + 1;
+ arg = imap_arg_as_list(arg);
+ if (IMAP_ARG_IS_EOL(arg)) {
+ client_send_command_error(cmd,
+ "FETCH list is empty.");
+ return FALSE;
+ }
+ while (imap_arg_get_atom(arg, &str)) {
+ str = t_str_ucase(str);
+ arg++;
+ if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg))
+ return FALSE;
+ }
+ if (!IMAP_ARG_IS_EOL(arg)) {
+ client_send_command_error(cmd,
+ "FETCH list contains non-atoms.");
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+fetch_parse_modifier(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd,
+ struct mail_search_args *search_args,
+ const char *name, const struct imap_arg **args,
+ bool *send_vanished)
+{
+ const char *str;
+ uint64_t modseq;
+
+ if (strcmp(name, "CHANGEDSINCE") == 0) {
+ if (cmd->client->nonpermanent_modseqs) {
+ client_send_command_error(cmd,
+ "FETCH CHANGEDSINCE can't be used with non-permanent modseqs");
+ return FALSE;
+ }
+ if (!imap_arg_get_atom(*args, &str) ||
+ str_to_uint64(str, &modseq) < 0) {
+ client_send_command_error(cmd,
+ "Invalid CHANGEDSINCE modseq.");
+ return FALSE;
+ }
+ *args += 1;
+ imap_search_add_changed_since(search_args, modseq);
+ imap_fetch_init_nofail_handler(ctx, imap_fetch_modseq_init);
+ return TRUE;
+ }
+ if (strcmp(name, "VANISHED") == 0 && cmd->uid) {
+ if (!client_has_enabled(ctx->client, imap_feature_qresync)) {
+ client_send_command_error(cmd, "QRESYNC not enabled");
+ return FALSE;
+ }
+ *send_vanished = TRUE;
+ return TRUE;
+ }
+
+ client_send_command_error(cmd, "Unknown FETCH modifier");
+ return FALSE;
+}
+
+static bool
+fetch_parse_modifiers(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd,
+ struct mail_search_args *search_args,
+ const struct imap_arg *args, bool *send_vanished_r)
+{
+ const char *name;
+
+ *send_vanished_r = FALSE;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &name)) {
+ client_send_command_error(cmd,
+ "FETCH modifiers contain non-atoms.");
+ return FALSE;
+ }
+ args++;
+ if (!fetch_parse_modifier(ctx, cmd, search_args,
+ t_str_ucase(name),
+ &args, send_vanished_r))
+ return FALSE;
+ }
+ if (*send_vanished_r &&
+ (search_args->args->next == NULL ||
+ search_args->args->next->type != SEARCH_MODSEQ)) {
+ client_send_command_error(cmd,
+ "VANISHED used without CHANGEDSINCE");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool cmd_fetch_finished(struct client_command_context *cmd ATTR_UNUSED)
+{
+ return TRUE;
+}
+
+static bool imap_fetch_is_failed_retry(struct imap_fetch_context *ctx)
+{
+ if (!array_is_created(&ctx->client->fetch_failed_uids) ||
+ !array_is_created(&ctx->fetch_failed_uids))
+ return FALSE;
+ return seq_range_array_have_common(&ctx->client->fetch_failed_uids,
+ &ctx->fetch_failed_uids);
+
+}
+
+static void imap_fetch_add_failed_uids(struct imap_fetch_context *ctx)
+{
+ if (!array_is_created(&ctx->fetch_failed_uids))
+ return;
+ if (!array_is_created(&ctx->client->fetch_failed_uids)) {
+ p_array_init(&ctx->client->fetch_failed_uids, ctx->client->pool,
+ array_count(&ctx->fetch_failed_uids));
+ }
+ seq_range_array_merge(&ctx->client->fetch_failed_uids,
+ &ctx->fetch_failed_uids);
+}
+
+static bool cmd_fetch_finish(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd)
+{
+ static const char *ok_message = "OK Fetch completed.";
+ const char *tagged_reply = ok_message;
+ enum mail_error error;
+ bool seen_flags_changed = ctx->state.seen_flags_changed;
+
+ if (ctx->state.skipped_expunged_msgs) {
+ tagged_reply = "OK ["IMAP_RESP_CODE_EXPUNGEISSUED"] "
+ "Some messages were already expunged.";
+ }
+
+ if (imap_fetch_end(ctx) < 0) {
+ const char *client_error;
+
+ if (cmd->client->output->closed) {
+ /* If we're canceling we need to finish this command
+ or we'll assert crash. But normally we want to
+ return FALSE so that the disconnect message logs
+ about this fetch command and that these latest
+ output bytes are included in it (which wouldn't
+ happen if we called client_disconnect() here
+ directly). */
+ cmd->func = cmd_fetch_finished;
+ imap_fetch_free(&ctx);
+ return cmd->cancel;
+ }
+
+ if (ctx->error == MAIL_ERROR_NONE)
+ client_error = mailbox_get_last_error(cmd->client->mailbox, &error);
+ else {
+ client_error = ctx->errstr;
+ error = ctx->error;
+ }
+ if (error == MAIL_ERROR_CONVERSION) {
+ /* BINARY found unsupported Content-Transfer-Encoding */
+ tagged_reply = t_strdup_printf(
+ "NO ["IMAP_RESP_CODE_UNKNOWN_CTE"] %s", client_error);
+ } else if (error == MAIL_ERROR_INVALIDDATA) {
+ /* Content was invalid */
+ tagged_reply = t_strdup_printf(
+ "NO ["IMAP_RESP_CODE_PARSE"] %s", client_error);
+ } else if (cmd->client->set->parsed_fetch_failure != IMAP_CLIENT_FETCH_FAILURE_NO_AFTER ||
+ imap_fetch_is_failed_retry(ctx)) {
+ /* By default we never want to reply NO to FETCH
+ requests, because many IMAP clients become confused
+ about what they should on NO. A disconnection causes
+ less confusion. */
+ const char *internal_error =
+ mailbox_get_last_internal_error(cmd->client->mailbox, NULL);
+ client_send_line(cmd->client, t_strconcat(
+ "* BYE FETCH failed: ", client_error, NULL));
+ client_disconnect(cmd->client, t_strconcat(
+ "FETCH failed: ", internal_error, NULL));
+ imap_fetch_free(&ctx);
+ return TRUE;
+ } else {
+ /* Use a tagged NO to FETCH failure, but only if client
+ hasn't repeated the FETCH to the same email (so that
+ we avoid infinitely retries from client.) */
+ imap_fetch_add_failed_uids(ctx);
+ tagged_reply = t_strdup_printf(
+ "NO ["IMAP_RESP_CODE_SERVERBUG"] %s", client_error);
+ }
+ }
+ imap_fetch_free(&ctx);
+
+ return cmd_sync(cmd,
+ (seen_flags_changed ? 0 : MAILBOX_SYNC_FLAG_FAST) |
+ (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), 0,
+ tagged_reply);
+}
+
+static bool cmd_fetch_continue(struct client_command_context *cmd)
+{
+ struct imap_fetch_context *ctx = cmd->context;
+
+ if (imap_fetch_more(ctx, cmd) == 0) {
+ /* unfinished */
+ return FALSE;
+ }
+ return cmd_fetch_finish(ctx, cmd);
+}
+
+static void cmd_fetch_set_reason_codes(struct client_command_context *cmd,
+ struct imap_fetch_context *ctx)
+{
+ /* Fetching body or header always causes the message to be opened.
+ Use them as the primary reason. */
+ if ((ctx->fetch_data & MAIL_FETCH_STREAM_BODY) != 0) {
+ event_strlist_append(cmd->global_event, EVENT_REASON_CODE,
+ "imap:fetch_body");
+ return;
+ }
+ if ((ctx->fetch_data & MAIL_FETCH_STREAM_HEADER) != 0) {
+ event_strlist_append(cmd->global_event, EVENT_REASON_CODE,
+ "imap:fetch_header");
+ return;
+ }
+
+ /* The rest of these can come from cache. Since especially with
+ prefetching we can't really know which one of them specifically
+ triggered opening the mail, just use all of them as the reasons. */
+ if (ctx->fetch_header_fields ||
+ HAS_ANY_BITS(ctx->fetch_data,
+ MAIL_FETCH_IMAP_ENVELOPE |
+ MAIL_FETCH_DATE)) {
+ event_strlist_append(cmd->global_event, EVENT_REASON_CODE,
+ "imap:fetch_header_fields");
+ }
+ if (HAS_ANY_BITS(ctx->fetch_data,
+ MAIL_FETCH_IMAP_BODY |
+ MAIL_FETCH_IMAP_BODYSTRUCTURE)) {
+ event_strlist_append(cmd->global_event, EVENT_REASON_CODE,
+ "imap:fetch_bodystructure");
+ }
+ if (HAS_ANY_BITS(ctx->fetch_data,
+ MAIL_FETCH_PHYSICAL_SIZE |
+ MAIL_FETCH_VIRTUAL_SIZE)) {
+ event_strlist_append(cmd->global_event, EVENT_REASON_CODE,
+ "imap:fetch_size");
+ }
+}
+
+bool cmd_fetch(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct imap_fetch_context *ctx;
+ const struct imap_arg *args, *next_arg, *list_arg;
+ struct mail_search_args *search_args;
+ struct imap_fetch_qresync_args qresync_args;
+ const char *messageset;
+ bool send_vanished = FALSE;
+ int ret;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ /* <messageset> <field(s)> [(modifiers)] */
+ if (!imap_arg_get_atom(&args[0], &messageset) ||
+ (args[1].type != IMAP_ARG_LIST && args[1].type != IMAP_ARG_ATOM) ||
+ (!IMAP_ARG_IS_EOL(&args[2]) && args[2].type != IMAP_ARG_LIST)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ /* UID FETCH VANISHED needs the uidset, so convert it to
+ sequence set later */
+ ret = imap_search_get_anyset(cmd, messageset, cmd->uid, &search_args);
+ if (ret <= 0)
+ return ret < 0;
+
+ ctx = imap_fetch_alloc(client, cmd->pool,
+ imap_client_command_get_reason(cmd));
+
+ if (!fetch_parse_args(ctx, cmd, &args[1], &next_arg) ||
+ (imap_arg_get_list(next_arg, &list_arg) &&
+ !fetch_parse_modifiers(ctx, cmd, search_args, list_arg,
+ &send_vanished))) {
+ imap_fetch_free(&ctx);
+ mail_search_args_unref(&search_args);
+ return TRUE;
+ }
+
+ if (send_vanished) {
+ i_zero(&qresync_args);
+ if (imap_fetch_send_vanished(client, client->mailbox,
+ search_args, &qresync_args) < 0) {
+ mail_search_args_unref(&search_args);
+ return cmd_fetch_finish(ctx, cmd);
+ }
+ }
+
+ cmd_fetch_set_reason_codes(cmd, ctx);
+ imap_fetch_begin(ctx, client->mailbox, search_args);
+ mail_search_args_unref(&search_args);
+
+ if (imap_fetch_more(ctx, cmd) == 0) {
+ /* unfinished */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+
+ cmd->func = cmd_fetch_continue;
+ cmd->context = ctx;
+ return FALSE;
+ }
+ return cmd_fetch_finish(ctx, cmd);
+}
diff --git a/src/imap/cmd-genurlauth.c b/src/imap/cmd-genurlauth.c
new file mode 100644
index 0000000..2cdbdd1
--- /dev/null
+++ b/src/imap/cmd-genurlauth.c
@@ -0,0 +1,54 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "imap-quote.h"
+#include "imap-urlauth.h"
+
+bool cmd_genurlauth(struct client_command_context *cmd)
+{
+ const struct imap_arg *args;
+ string_t *response;
+ int ret;
+
+ if (cmd->client->urlauth_ctx == NULL) {
+ client_send_command_error(cmd, "URLAUTH disabled.");
+ return TRUE;
+ }
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ response = t_str_new(1024);
+ str_append(response, "* GENURLAUTH");
+ for (;;) {
+ const char *url_rump, *mechanism, *url, *client_error;
+
+ if (IMAP_ARG_IS_EOL(args))
+ break;
+ if (!imap_arg_get_astring(args++, &url_rump) ||
+ !imap_arg_get_atom(args++, &mechanism)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return FALSE;
+ }
+
+ ret = imap_urlauth_generate(cmd->client->urlauth_ctx,
+ mechanism, url_rump, &url,
+ &client_error);
+ if (ret <= 0) {
+ if (ret < 0)
+ client_send_internal_error(cmd);
+ else
+ client_send_command_error(cmd, client_error);
+ return TRUE;
+ }
+
+ str_append_c(response, ' ');
+ imap_append_astring(response, url);
+ }
+
+ client_send_line(cmd->client, str_c(response));
+ client_send_tagline(cmd, "OK GENURLAUTH completed.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-getmetadata.c b/src/imap/cmd-getmetadata.c
new file mode 100644
index 0000000..ec23135
--- /dev/null
+++ b/src/imap/cmd-getmetadata.c
@@ -0,0 +1,559 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "mail-storage-private.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-sized.h"
+#include "ostream.h"
+#include "mailbox-list-iter.h"
+#include "imap-utf7.h"
+#include "imap-quote.h"
+#include "imap-metadata.h"
+
+struct imap_getmetadata_context {
+ struct client_command_context *cmd;
+
+ struct mailbox *box;
+ struct imap_metadata_transaction *trans;
+ struct mailbox_list_iterate_context *list_iter;
+
+ ARRAY_TYPE(const_string) entries;
+ uint32_t maxsize;
+ uoff_t largest_seen_size;
+ unsigned int depth;
+
+ struct istream *cur_stream;
+
+ struct imap_metadata_iter *iter;
+ string_t *iter_entry_prefix;
+
+ string_t *delayed_errors;
+ string_t *last_error_str;
+ enum mail_error last_error;
+
+ unsigned int entry_idx;
+ bool first_entry_sent;
+};
+
+static bool
+cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx);
+
+static bool
+cmd_getmetadata_parse_options(struct imap_getmetadata_context *ctx,
+ const struct imap_arg *options)
+{
+ const char *value;
+
+ while (!IMAP_ARG_IS_EOL(options)) {
+ if (imap_arg_atom_equals(options, "MAXSIZE")) {
+ options++;
+ if (!imap_arg_get_atom(options, &value) ||
+ str_to_uint32(value, &ctx->maxsize) < 0) {
+ client_send_command_error(ctx->cmd,
+ "Invalid value for MAXSIZE option");
+ return FALSE;
+ }
+ } else if (imap_arg_atom_equals(options, "DEPTH")) {
+ options++;
+ if (!imap_arg_get_atom(options, &value)) {
+ client_send_command_error(ctx->cmd,
+ "Invalid value for DEPTH option");
+ return FALSE;
+ }
+ if (strcmp(value, "0") == 0)
+ ctx->depth = 0;
+ else if (strcmp(value, "1") == 0)
+ ctx->depth = 1;
+ else if (strcmp(value, "infinity") == 0)
+ ctx->depth = UINT_MAX;
+ else {
+ client_send_command_error(ctx->cmd,
+ "Invalid value for DEPTH option");
+ return FALSE;
+ }
+ } else {
+ client_send_command_error(ctx->cmd, "Unknown option");
+ return FALSE;
+ }
+ options++;
+ }
+ return TRUE;
+}
+
+static bool
+imap_metadata_parse_entry_names(struct imap_getmetadata_context *ctx,
+ const struct imap_arg *entries)
+{
+ const char *value, *client_error;
+
+ p_array_init(&ctx->entries, ctx->cmd->pool, 4);
+ for (; !IMAP_ARG_IS_EOL(entries); entries++) {
+ if (!imap_arg_get_astring(entries, &value)) {
+ client_send_command_error(ctx->cmd, "Entry isn't astring");
+ return FALSE;
+ }
+ if (!imap_metadata_verify_entry_name(value, &client_error)) {
+ client_send_command_error(ctx->cmd, client_error);
+ return FALSE;
+ }
+
+ /* names are case-insensitive so we'll always lowercase them */
+ value = p_strdup(ctx->cmd->pool, t_str_lcase(value));
+ array_push_back(&ctx->entries, &value);
+ }
+ return TRUE;
+}
+
+static string_t *
+metadata_add_entry(struct imap_getmetadata_context *ctx, const char *entry)
+{
+ string_t *str;
+
+ str = t_str_new(64);
+ if (!ctx->first_entry_sent) {
+ string_t *mailbox_mutf7 = t_str_new(64);
+
+ ctx->first_entry_sent = TRUE;
+ str_append(str, "* METADATA ");
+ if (ctx->box == NULL) {
+ /* server metadata reply */
+ str_append(str, "\"\"");
+ } else {
+ if (imap_utf8_to_utf7(mailbox_get_vname(ctx->box), mailbox_mutf7) < 0)
+ i_unreached();
+ imap_append_astring(str, str_c(mailbox_mutf7));
+ }
+ str_append(str, " (");
+
+ /* nothing can be sent until untagged METADATA is finished */
+ ctx->cmd->client->output_cmd_lock = ctx->cmd;
+ } else {
+ str_append_c(str, ' ');
+ }
+ imap_append_astring(str, entry);
+ return str;
+}
+
+static void
+cmd_getmetadata_send_nil_reply(struct imap_getmetadata_context *ctx,
+ const char *entry)
+{
+ string_t *str;
+
+ /* client requested a specific entry that didn't exist.
+ we must return it as NIL. */
+ str = metadata_add_entry(ctx, entry);
+ str_append(str, " NIL");
+ o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str));
+}
+
+static void
+cmd_getmetadata_handle_error_str(struct imap_getmetadata_context *ctx,
+ const char *error_string,
+ enum mail_error error)
+{
+ if (str_len(ctx->last_error_str) > 0) {
+ str_append(ctx->delayed_errors, "* NO ");
+ str_append_str(ctx->delayed_errors, ctx->last_error_str);
+ str_append(ctx->delayed_errors, "\r\n");
+ str_truncate(ctx->last_error_str, 0);
+ }
+ str_append(ctx->last_error_str, error_string);
+ ctx->last_error = error;
+}
+
+static bool
+cmd_getmetadata_handle_error(struct imap_getmetadata_context *ctx)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ error_string = imap_metadata_transaction_get_last_error(ctx->trans, &error);
+ if (error == MAIL_ERROR_NOTPOSSIBLE && ctx->depth > 0) {
+ /* Using DEPTH to iterate children with imap_metadata=no.
+ Don't return an error, since some of the entries could be
+ returned successfully. */
+ return FALSE;
+ }
+
+ cmd_getmetadata_handle_error_str(ctx, error_string, error);
+ return TRUE;
+}
+
+static void cmd_getmetadata_send_entry(struct imap_getmetadata_context *ctx,
+ const char *entry, bool require_reply)
+{
+ struct client *client = ctx->cmd->client;
+ struct mail_attribute_value value;
+ uoff_t value_len;
+ string_t *str;
+
+ if (imap_metadata_get_stream(ctx->trans, entry, &value) < 0) {
+ if (cmd_getmetadata_handle_error(ctx))
+ return;
+ }
+
+ if (value.value != NULL)
+ value_len = strlen(value.value);
+ else if (value.value_stream != NULL) {
+ if (i_stream_get_size(value.value_stream, TRUE, &value_len) < 0) {
+ e_error(client->event,
+ "GETMETADATA %s: i_stream_get_size(%s) failed: %s", entry,
+ i_stream_get_name(value.value_stream),
+ i_stream_get_error(value.value_stream));
+ i_stream_unref(&value.value_stream);
+ cmd_getmetadata_handle_error_str(ctx,
+ MAIL_ERRSTR_CRITICAL_MSG, MAIL_ERROR_TEMP);
+ return;
+ }
+ } else {
+ /* skip nonexistent entries */
+ if (require_reply)
+ cmd_getmetadata_send_nil_reply(ctx, entry);
+ return;
+ }
+
+ if (value_len > ctx->maxsize) {
+ /* value length is larger than specified MAXSIZE,
+ skip this entry */
+ if (ctx->largest_seen_size < value_len)
+ ctx->largest_seen_size = value_len;
+ i_stream_unref(&value.value_stream);
+ return;
+ }
+
+ str = metadata_add_entry(ctx, entry);
+ if (value.value != NULL) {
+ str_printfa(str, " {%"PRIuUOFF_T"}\r\n%s", value_len, value.value);
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ } else {
+ str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", value_len);
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+
+ ctx->cur_stream = i_stream_create_sized(value.value_stream, value_len);
+ i_stream_unref(&value.value_stream);
+ }
+}
+
+static bool
+cmd_getmetadata_stream_continue(struct imap_getmetadata_context *ctx)
+{
+ enum ostream_send_istream_result res;
+
+ o_stream_set_max_buffer_size(ctx->cmd->client->output, 0);
+ res = o_stream_send_istream(ctx->cmd->client->output, ctx->cur_stream);
+ o_stream_set_max_buffer_size(ctx->cmd->client->output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ return TRUE;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return FALSE;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ e_error(ctx->cmd->client->event, "read(%s) failed: %s",
+ i_stream_get_name(ctx->cur_stream),
+ i_stream_get_error(ctx->cur_stream));
+ client_disconnect(ctx->cmd->client,
+ "Internal GETMETADATA failure");
+ return TRUE;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* client disconnected */
+ return TRUE;
+ }
+ i_unreached();
+}
+
+static int
+cmd_getmetadata_send_entry_tree(struct imap_getmetadata_context *ctx,
+ const char *entry)
+{
+ struct client *client = ctx->cmd->client;
+
+ if (o_stream_get_buffer_used_size(client->output) >=
+ CLIENT_OUTPUT_OPTIMAL_SIZE) {
+ if (o_stream_flush(client->output) <= 0) {
+ o_stream_set_flush_pending(client->output, TRUE);
+ return 0;
+ }
+ }
+
+ if (ctx->iter != NULL) {
+ const char *subentry;
+
+ /* DEPTH iteration */
+ do {
+ subentry = imap_metadata_iter_next(ctx->iter);
+ if (subentry == NULL) {
+ /* iteration finished, get to the next entry */
+ if (imap_metadata_iter_deinit(&ctx->iter) < 0) {
+ if (!cmd_getmetadata_handle_error(ctx))
+ i_unreached();
+ }
+ return -1;
+ }
+ } while (ctx->depth == 1 && strchr(subentry, '/') != NULL);
+ entry = t_strconcat(str_c(ctx->iter_entry_prefix), subentry, NULL);
+ }
+ /* send NIL only on depth 0 query */
+ cmd_getmetadata_send_entry(ctx, entry, ctx->depth == 0);
+
+ if (ctx->cur_stream != NULL) {
+ if (!cmd_getmetadata_stream_continue(ctx))
+ return 0;
+ i_stream_unref(&ctx->cur_stream);
+ }
+
+ if (ctx->iter != NULL) {
+ /* already iterating the entry */
+ return 1;
+ } else if (ctx->depth == 0) {
+ /* no iteration for the entry */
+ return -1;
+ } else {
+ /* we just sent the entry root. iterate its children. */
+ str_truncate(ctx->iter_entry_prefix, 0);
+ str_append(ctx->iter_entry_prefix, entry);
+ str_append_c(ctx->iter_entry_prefix, '/');
+
+ ctx->iter = imap_metadata_iter_init(ctx->trans, entry);
+ return 1;
+ }
+}
+
+static void cmd_getmetadata_iter_deinit(struct imap_getmetadata_context *ctx)
+{
+ if (ctx->iter != NULL)
+ (void)imap_metadata_iter_deinit(&ctx->iter);
+ if (ctx->trans != NULL)
+ (void)imap_metadata_transaction_commit(&ctx->trans, NULL, NULL);
+ if (ctx->box != NULL)
+ mailbox_free(&ctx->box);
+ ctx->first_entry_sent = FALSE;
+ ctx->entry_idx = 0;
+}
+
+static void cmd_getmetadata_deinit(struct imap_getmetadata_context *ctx)
+{
+ struct client_command_context *cmd = ctx->cmd;
+
+ cmd_getmetadata_iter_deinit(ctx);
+ cmd->client->output_cmd_lock = NULL;
+
+ if (ctx->list_iter != NULL &&
+ mailbox_list_iter_deinit(&ctx->list_iter) < 0)
+ client_send_list_error(cmd, cmd->client->user->namespaces->list);
+ else if (ctx->last_error != 0) {
+ i_assert(str_len(ctx->last_error_str) > 0);
+ const char *tagline =
+ imap_get_error_string(cmd, str_c(ctx->last_error_str),
+ ctx->last_error);
+ client_send_tagline(cmd, tagline);
+ } else if (ctx->largest_seen_size != 0) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "OK [METADATA LONGENTRIES %"PRIuUOFF_T"] "
+ "Getmetadata completed.", ctx->largest_seen_size));
+ } else {
+ client_send_tagline(cmd, "OK Getmetadata completed.");
+ }
+}
+
+static bool cmd_getmetadata_continue(struct client_command_context *cmd)
+{
+ struct imap_getmetadata_context *ctx = cmd->context;
+ const char *const *entries;
+ unsigned int count;
+ int ret;
+
+ if (cmd->cancel) {
+ cmd_getmetadata_deinit(ctx);
+ return TRUE;
+ }
+
+ if (ctx->cur_stream != NULL) {
+ if (!cmd_getmetadata_stream_continue(ctx))
+ return FALSE;
+ i_stream_unref(&ctx->cur_stream);
+ }
+
+ entries = array_get(&ctx->entries, &count);
+ for (; ctx->entry_idx < count; ctx->entry_idx++) {
+ do {
+ T_BEGIN {
+ ret = cmd_getmetadata_send_entry_tree(ctx, entries[ctx->entry_idx]);
+ } T_END;
+ if (ret == 0)
+ return FALSE;
+ } while (ret > 0);
+ }
+ if (ctx->first_entry_sent)
+ o_stream_nsend_str(cmd->client->output, ")\r\n");
+
+ if (str_len(ctx->delayed_errors) > 0) {
+ o_stream_nsend(cmd->client->output,
+ str_data(ctx->delayed_errors),
+ str_len(ctx->delayed_errors));
+ str_truncate(ctx->delayed_errors, 0);
+ }
+
+ cmd_getmetadata_iter_deinit(ctx);
+ if (ctx->list_iter != NULL)
+ return cmd_getmetadata_mailbox_iter_next(ctx);
+ cmd_getmetadata_deinit(ctx);
+ return TRUE;
+}
+
+static bool
+cmd_getmetadata_start(struct imap_getmetadata_context *ctx)
+{
+ struct client_command_context *cmd = ctx->cmd;
+
+ if (ctx->depth > 0)
+ ctx->iter_entry_prefix = str_new(cmd->pool, 128);
+ imap_metadata_transaction_validated_only(ctx->trans,
+ !cmd->client->set->imap_metadata);
+
+ if (!cmd_getmetadata_continue(cmd)) {
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ cmd->func = cmd_getmetadata_continue;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+cmd_getmetadata_server(struct imap_getmetadata_context *ctx)
+{
+ ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user);
+ return cmd_getmetadata_start(ctx);
+}
+
+static int
+cmd_getmetadata_try_mailbox(struct imap_getmetadata_context *ctx,
+ struct mail_namespace *ns, const char *mailbox)
+{
+ ctx->box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY);
+ event_add_str(ctx->cmd->global_event, "mailbox",
+ mailbox_get_vname(ctx->box));
+
+ enum mailbox_existence existence;
+ if (mailbox_exists(ctx->box, TRUE, &existence) < 0) {
+ return -1;
+ } else if (existence == MAILBOX_EXISTENCE_NONE) {
+ const char *err = t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND,
+ mailbox_get_vname(ctx->box));
+ mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND, err);
+ return -1;
+ }
+
+ ctx->trans = imap_metadata_transaction_begin(ctx->box);
+ return cmd_getmetadata_start(ctx) ? 1 : 0;
+}
+
+static bool
+cmd_getmetadata_mailbox(struct imap_getmetadata_context *ctx,
+ struct mail_namespace *ns, const char *mailbox)
+{
+ int ret;
+
+ ret = cmd_getmetadata_try_mailbox(ctx, ns, mailbox);
+ if (ret < 0) {
+ client_send_box_error(ctx->cmd, ctx->box);
+ mailbox_free(&ctx->box);
+ }
+ return ret != 0;
+}
+
+static bool
+cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx)
+{
+ const struct mailbox_info *info;
+ int ret;
+
+ while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) {
+ if ((info->flags & (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0)
+ continue;
+ ret = cmd_getmetadata_try_mailbox(ctx, info->ns, info->vname);
+ if (ret > 0) {
+ /* we'll already recursively went through
+ all the mailboxes (FIXME: ugly and potentially
+ stack consuming) */
+ return TRUE;
+ } else if (ret == 0) {
+ /* need to send more data later */
+ return FALSE;
+ }
+ T_BEGIN {
+ client_send_line(ctx->cmd->client, t_strdup_printf(
+ "* NO Failed to open mailbox %s: %s",
+ info->vname, mailbox_get_last_error(ctx->box, NULL)));
+ } T_END;
+ mailbox_free(&ctx->box);
+ }
+ cmd_getmetadata_deinit(ctx);
+ return TRUE;
+}
+
+bool cmd_getmetadata(struct client_command_context *cmd)
+{
+ struct imap_getmetadata_context *ctx;
+ struct mail_namespace *ns;
+ const struct imap_arg *args, *options, *entries;
+ const char *mailbox, *entry_name;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ ctx = p_new(cmd->pool, struct imap_getmetadata_context, 1);
+ ctx->cmd = cmd;
+ ctx->maxsize = (uint32_t)-1;
+ ctx->cmd->context = ctx;
+ ctx->delayed_errors = str_new(cmd->pool, 128);
+ ctx->last_error_str = str_new(cmd->pool, 128);
+
+ if (imap_arg_get_list(&args[0], &options)) {
+ if (!cmd_getmetadata_parse_options(ctx, options))
+ return TRUE;
+ args++;
+ }
+ if (!imap_arg_get_astring(&args[0], &mailbox)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ if (!imap_arg_get_list(&args[1], &entries)) {
+ if (!imap_arg_get_astring(&args[1], &entry_name) ||
+ !IMAP_ARG_IS_EOL(&args[2])) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ entries = args+1;
+ }
+ if (!imap_metadata_parse_entry_names(ctx, entries))
+ return TRUE;
+
+ if (mailbox[0] == '\0') {
+ /* server attribute */
+ return cmd_getmetadata_server(ctx);
+ } else if (strchr(mailbox, '*') == NULL &&
+ strchr(mailbox, '%') == NULL) {
+ /* mailbox attribute */
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+ return cmd_getmetadata_mailbox(ctx, ns, mailbox);
+ } else {
+ /* wildcards in mailbox name. this isn't supported by RFC 5464,
+ but it was in the earlier drafts and is already used by
+ some software (Horde). */
+ const char *patterns[2];
+ patterns[0] = mailbox; patterns[1] = NULL;
+
+ ctx->list_iter =
+ mailbox_list_iter_init_namespaces(
+ cmd->client->user->namespaces,
+ patterns, MAIL_NAMESPACE_TYPE_MASK_ALL, 0);
+ return cmd_getmetadata_mailbox_iter_next(ctx);
+ }
+}
diff --git a/src/imap/cmd-id.c b/src/imap/cmd-id.c
new file mode 100644
index 0000000..98355d4
--- /dev/null
+++ b/src/imap/cmd-id.c
@@ -0,0 +1,27 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-id.h"
+
+bool cmd_id(struct client_command_context *cmd)
+{
+ const struct imap_settings *set = cmd->client->set;
+ const struct imap_arg *args;
+ const char *value;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!cmd->client->id_logged) {
+ cmd->client->id_logged = TRUE;
+ value = imap_id_args_get_log_reply(args, set->imap_id_log);
+ if (value != NULL)
+ e_info(cmd->client->event, "ID sent: %s", value);
+ }
+
+ client_send_line(cmd->client, t_strdup_printf(
+ "* ID %s", imap_id_reply_generate(set->imap_id_send)));
+ client_send_tagline(cmd, "OK ID completed.");
+ return TRUE;
+}
+
diff --git a/src/imap/cmd-idle.c b/src/imap/cmd-idle.c
new file mode 100644
index 0000000..2b31dc7
--- /dev/null
+++ b/src/imap/cmd-idle.c
@@ -0,0 +1,308 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "istream.h"
+#include "ostream.h"
+#include "crc32.h"
+#include "mail-storage-settings.h"
+#include "imap-commands.h"
+#include "imap-keepalive.h"
+#include "imap-sync.h"
+
+struct cmd_idle_context {
+ struct client *client;
+ struct client_command_context *cmd;
+
+ struct imap_sync_context *sync_ctx;
+ struct timeout *keepalive_to, *to_hibernate;
+
+ bool manual_cork:1;
+ bool sync_pending:1;
+};
+
+static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx);
+static bool cmd_idle_continue(struct client_command_context *cmd);
+
+static void
+idle_finish(struct cmd_idle_context *ctx, bool done_ok, bool free_cmd)
+{
+ struct client *client = ctx->client;
+
+ timeout_remove(&ctx->keepalive_to);
+ timeout_remove(&ctx->to_hibernate);
+
+ if (ctx->sync_ctx != NULL) {
+ /* we're here only in connection failure cases */
+ (void)imap_sync_deinit(ctx->sync_ctx, ctx->cmd);
+ }
+
+ o_stream_cork(client->output);
+ io_remove(&client->io);
+
+ if (client->mailbox != NULL)
+ mailbox_notify_changes_stop(client->mailbox);
+
+ if (done_ok)
+ client_send_tagline(ctx->cmd, "OK Idle completed.");
+ else
+ client_send_tagline(ctx->cmd, "BAD Expected DONE.");
+
+ o_stream_uncork(client->output);
+ if (free_cmd)
+ client_command_free(&ctx->cmd);
+}
+
+static bool
+idle_client_handle_input(struct cmd_idle_context *ctx, bool free_cmd)
+{
+ const char *line;
+
+ while ((line = i_stream_next_line(ctx->client->input)) != NULL) {
+ if (ctx->client->input_skip_line)
+ ctx->client->input_skip_line = FALSE;
+ else {
+ idle_finish(ctx, strcasecmp(line, "DONE") == 0,
+ free_cmd);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool idle_client_input_more(struct cmd_idle_context *ctx)
+{
+ struct client *client = ctx->client;
+
+ client->last_input = ioloop_time;
+ timeout_reset(client->to_idle);
+
+ switch (i_stream_read(client->input)) {
+ case -1:
+ /* disconnected */
+ client_disconnect(client, NULL);
+ return TRUE;
+ case -2:
+ client->input_skip_line = TRUE;
+ idle_finish(ctx, FALSE, TRUE);
+ return TRUE;
+ }
+
+ if (ctx->sync_ctx != NULL) {
+ /* we're still sending output to client. wait until it's all
+ sent so we don't lose any changes. */
+ io_remove(&client->io);
+ return FALSE;
+ }
+
+ return idle_client_handle_input(ctx, TRUE);
+}
+
+static void idle_client_input(struct cmd_idle_context *ctx)
+{
+ struct client *client = ctx->client;
+
+ if (idle_client_input_more(ctx))
+ client_continue_pending_input(client);
+}
+
+static void keepalive_timeout(struct cmd_idle_context *ctx)
+{
+ if (ctx->client->output_cmd_lock != NULL) {
+ /* it's busy sending output */
+ return;
+ }
+
+ if (o_stream_get_buffer_used_size(ctx->client->output) == 0) {
+ /* Sending this keeps NATs/stateful firewalls alive.
+ Sending this also catches dead connections. Don't send
+ anything if there is already data waiting in output
+ buffer. */
+ o_stream_cork(ctx->client->output);
+ client_send_line(ctx->client, "* OK Still here");
+ o_stream_uncork(ctx->client->output);
+ }
+ /* Make sure idling connections don't get disconnected. There are
+ several clients that really want to IDLE forever and there's not
+ much harm in letting them do so. */
+ timeout_reset(ctx->client->to_idle);
+ /* recalculate time for the next keepalive timeout */
+ idle_add_keepalive_timeout(ctx);
+}
+
+static bool idle_sync_now(struct mailbox *box, struct cmd_idle_context *ctx)
+{
+ i_assert(ctx->sync_ctx == NULL);
+
+ /* hibernation can't happen while sync is running.
+ the timeout is added back afterwards. */
+ timeout_remove(&ctx->to_hibernate);
+
+ ctx->sync_pending = FALSE;
+ ctx->sync_ctx = imap_sync_init(ctx->client, box, 0, 0);
+ return cmd_idle_continue(ctx->cmd);
+}
+
+static void idle_callback(struct mailbox *box, struct cmd_idle_context *ctx)
+{
+ struct client *client = ctx->client;
+
+ if (ctx->sync_ctx != NULL)
+ ctx->sync_pending = TRUE;
+ else {
+ ctx->manual_cork = TRUE;
+ (void)idle_sync_now(box, ctx);
+ if (client->disconnected)
+ client_destroy(client, NULL);
+ }
+}
+
+static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx)
+{
+ struct client *client = ctx->client;
+ unsigned int interval = client->set->imap_idle_notify_interval;
+
+ if (interval == 0)
+ return;
+
+ interval = imap_keepalive_interval_msecs(client->user->username,
+ client->user->conn.remote_ip,
+ interval);
+
+ timeout_remove(&ctx->keepalive_to);
+ ctx->keepalive_to = timeout_add(interval, keepalive_timeout, ctx);
+}
+
+static void idle_hibernate_timeout(struct cmd_idle_context *ctx)
+{
+ struct client *client = ctx->client;
+ const char *reason;
+
+ i_assert(ctx->sync_ctx == NULL);
+ i_assert(!ctx->sync_pending);
+
+ if (imap_client_hibernate(&client, &reason)) {
+ /* client may be destroyed now */
+ } else {
+ /* failed - don't bother retrying */
+ timeout_remove(&ctx->to_hibernate);
+ }
+}
+
+static void idle_add_hibernate_timeout(struct cmd_idle_context *ctx)
+{
+ unsigned int secs = ctx->client->set->imap_hibernate_timeout;
+
+ i_assert(ctx->to_hibernate == NULL);
+
+ if (secs == 0)
+ return;
+
+ ctx->to_hibernate =
+ timeout_add(secs * 1000, idle_hibernate_timeout, ctx);
+}
+
+static bool cmd_idle_continue(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_idle_context *ctx = cmd->context;
+ uoff_t orig_offset = client->output->offset;
+
+ if (cmd->cancel) {
+ idle_finish(ctx, FALSE, FALSE);
+ return TRUE;
+ }
+
+ if (ctx->to_hibernate != NULL)
+ timeout_reset(ctx->to_hibernate);
+
+ if (ctx->manual_cork) {
+ /* we're coming from idle_callback instead of a normal
+ I/O handler, so we'll have to do corking manually */
+ o_stream_cork(client->output);
+ }
+
+ if (ctx->sync_ctx != NULL) {
+ if (imap_sync_more(ctx->sync_ctx) == 0) {
+ /* unfinished */
+ if (ctx->manual_cork) {
+ ctx->manual_cork = FALSE;
+ o_stream_uncork(client->output);
+ }
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ return FALSE;
+ }
+
+ if (imap_sync_deinit(ctx->sync_ctx, ctx->cmd) < 0) {
+ client_send_untagged_storage_error(client,
+ mailbox_get_storage(client->mailbox));
+ mailbox_notify_changes_stop(client->mailbox);
+ }
+ ctx->sync_ctx = NULL;
+ }
+ if (client->output->offset != orig_offset &&
+ ctx->keepalive_to != NULL)
+ idle_add_keepalive_timeout(ctx);
+
+ if (ctx->sync_pending) {
+ /* more changes occurred while we were sending changes to
+ client.
+
+ NOTE: this recurses back to this function,
+ so we return here instead of doing everything twice. */
+ return idle_sync_now(client->mailbox, ctx);
+ }
+ if (ctx->to_hibernate == NULL)
+ idle_add_hibernate_timeout(ctx);
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;
+
+ if (ctx->manual_cork) {
+ ctx->manual_cork = FALSE;
+ o_stream_uncork(client->output);
+ }
+
+ if (client->output->closed) {
+ idle_finish(ctx, FALSE, FALSE);
+ return TRUE;
+ }
+ if (client->io == NULL) {
+ /* input is pending. add the io back and mark the input as
+ pending. we can't safely read more input immediately here. */
+ client->io = io_add_istream(client->input,
+ idle_client_input, ctx);
+ i_stream_set_input_pending(client->input, TRUE);
+ }
+ return FALSE;
+}
+
+bool cmd_idle(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_idle_context *ctx;
+
+ ctx = p_new(cmd->pool, struct cmd_idle_context, 1);
+ ctx->cmd = cmd;
+ ctx->client = client;
+ idle_add_keepalive_timeout(ctx);
+ idle_add_hibernate_timeout(ctx);
+
+ if (client->mailbox != NULL)
+ mailbox_notify_changes(client->mailbox, idle_callback, ctx);
+ if (!client->state_import_idle_continue)
+ client_send_line(client, "+ idling");
+ else {
+ /* continuing an IDLE after hibernation */
+ client->state_import_idle_continue = FALSE;
+ }
+
+ io_remove(&client->io);
+ client->io = io_add_istream(client->input, idle_client_input, ctx);
+
+ cmd->func = cmd_idle_continue;
+ cmd->context = ctx;
+
+ /* check immediately if there are changes. if they came before we
+ added mailbox-notifier, we wouldn't see them otherwise. */
+ if (client->mailbox != NULL)
+ idle_sync_now(client->mailbox, ctx);
+ return idle_client_handle_input(ctx, FALSE);
+}
diff --git a/src/imap/cmd-list.c b/src/imap/cmd-list.c
new file mode 100644
index 0000000..dd6c00e
--- /dev/null
+++ b/src/imap/cmd-list.c
@@ -0,0 +1,484 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "mailbox-list-iter.h"
+#include "imap-utf7.h"
+#include "imap-quote.h"
+#include "imap-match.h"
+#include "imap-status.h"
+#include "imap-commands.h"
+#include "imap-list.h"
+
+struct cmd_list_context {
+ struct client_command_context *cmd;
+ struct mail_user *user;
+
+ enum mailbox_list_iter_flags list_flags;
+ struct imap_status_items status_items;
+
+ struct mailbox_list_iterate_context *list_iter;
+
+ bool lsub:1;
+ bool lsub_no_unsubscribed:1;
+ bool used_listext:1;
+ bool used_status:1;
+};
+
+static void
+mailbox_flags2str(struct cmd_list_context *ctx, string_t *str,
+ const char *special_use, enum mailbox_info_flags flags)
+{
+ size_t orig_len = str_len(str);
+
+ if ((flags & MAILBOX_NONEXISTENT) != 0 && !ctx->used_listext) {
+ flags |= MAILBOX_NOSELECT;
+ flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ }
+
+ if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0)
+ flags &= ENUM_NEGATE(MAILBOX_CHILDREN | MAILBOX_NOCHILDREN);
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (flags & MAILBOX_SUBSCRIBED) == 0 && !ctx->used_listext) {
+ /* LSUB uses \Noselect for this */
+ flags |= MAILBOX_NOSELECT;
+ } else if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) == 0)
+ flags &= ENUM_NEGATE(MAILBOX_SUBSCRIBED);
+ imap_mailbox_flags2str(str, flags);
+
+ if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SPECIALUSE) != 0 &&
+ special_use != NULL) {
+ if (str_len(str) != orig_len)
+ str_append_c(str, ' ');
+ str_append(str, special_use);
+ }
+}
+
+static void
+mailbox_childinfo2str(struct cmd_list_context *ctx, string_t *str,
+ enum mailbox_info_flags flags)
+{
+ if (!ctx->used_listext)
+ return;
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0)
+ str_append(str, " (CHILDINFO (\"SUBSCRIBED\"))");
+ if ((flags & MAILBOX_CHILD_SPECIALUSE) != 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0)
+ str_append(str, " (CHILDINFO (\"SPECIAL-USE\"))");
+}
+
+static bool
+parse_select_flags(struct cmd_list_context *ctx, const struct imap_arg *args)
+{
+ enum mailbox_list_iter_flags list_flags = 0;
+ const char *str;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(ctx->cmd,
+ "List options contains non-atoms.");
+ return FALSE;
+ }
+
+ if (strcasecmp(str, "SUBSCRIBED") == 0) {
+ list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED;
+ } else if (strcasecmp(str, "RECURSIVEMATCH") == 0)
+ list_flags |= MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH;
+ else if (strcasecmp(str, "SPECIAL-USE") == 0) {
+ list_flags |= MAILBOX_LIST_ITER_SELECT_SPECIALUSE |
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ } else if (strcasecmp(str, "REMOTE") == 0) {
+ /* not supported, ignore */
+ } else {
+ /* skip also optional list value */
+ client_send_command_error(ctx->cmd,
+ "Unknown select options");
+ return FALSE;
+ }
+ args++;
+ }
+
+ if ((list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
+ (list_flags & (MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_SELECT_SPECIALUSE)) == 0) {
+ client_send_command_error(ctx->cmd,
+ "RECURSIVEMATCH must not be the only selection.");
+ return FALSE;
+ }
+
+ ctx->list_flags = list_flags;
+ return TRUE;
+}
+
+static bool
+parse_return_flags(struct cmd_list_context *ctx, const struct imap_arg *args)
+{
+ enum mailbox_list_iter_flags list_flags = 0;
+ const struct imap_arg *list_args;
+ const char *str;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(ctx->cmd,
+ "List options contains non-atoms.");
+ return FALSE;
+ }
+
+ if (strcasecmp(str, "SUBSCRIBED") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_SUBSCRIBED;
+ else if (strcasecmp(str, "CHILDREN") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN;
+ else if (strcasecmp(str, "SPECIAL-USE") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ else if (strcasecmp(str, "STATUS") == 0 &&
+ imap_arg_get_list(&args[1], &list_args)) {
+ if (imap_status_parse_items(ctx->cmd, list_args,
+ &ctx->status_items) < 0)
+ return FALSE;
+ ctx->used_status = TRUE;
+ args++;
+ } else {
+ /* skip also optional list value */
+ client_send_command_error(ctx->cmd,
+ "Unknown return options");
+ return FALSE;
+ }
+ args++;
+ }
+
+ ctx->list_flags |= list_flags;
+ return TRUE;
+}
+
+static const char *ns_prefix_mutf7(struct mail_namespace *ns)
+{
+ string_t *str;
+
+ if (*ns->prefix == '\0')
+ return "";
+
+ str = t_str_new(64);
+ if (imap_utf8_to_utf7(ns->prefix, str) < 0)
+ i_panic("Namespace prefix not UTF-8: %s", ns->prefix);
+ return str_c(str);
+}
+
+static void list_reply_append_ns_sep_param(string_t *str, char sep)
+{
+ str_append_c(str, '"');
+ if (sep == '\\')
+ str_append(str, "\\\\");
+ else
+ str_append_c(str, sep);
+ str_append_c(str, '"');
+}
+
+static void
+list_send_status(struct cmd_list_context *ctx, const char *name,
+ const char *mutf7_name, enum mailbox_info_flags flags)
+{
+ struct imap_status_result result;
+ struct mail_namespace *ns;
+
+ if ((flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0) {
+ /* doesn't exist, don't even try to get STATUS */
+ return;
+ }
+ if ((flags & MAILBOX_SUBSCRIBED) == 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* listing subscriptions, but only child is subscribed */
+ return;
+ }
+
+ /* if we're listing subscriptions and there are subscriptions=no
+ namespaces, ctx->ns may not point to correct one */
+ ns = mail_namespace_find(ctx->user->namespaces, name);
+ if (imap_status_get(ctx->cmd, ns, name,
+ &ctx->status_items, &result) < 0) {
+ client_send_line(ctx->cmd->client,
+ t_strconcat("* ", result.errstr, NULL));
+ return;
+ }
+
+ imap_status_send(ctx->cmd->client, mutf7_name,
+ &ctx->status_items, &result);
+}
+
+static bool cmd_list_continue(struct client_command_context *cmd)
+{
+ struct cmd_list_context *ctx = cmd->context;
+ const struct mailbox_info *info;
+ enum mailbox_info_flags flags;
+ string_t *str, *mutf7_name;
+ const char *name;
+ int ret = 0;
+
+ if (cmd->cancel) {
+ if (ctx->list_iter != NULL)
+ (void)mailbox_list_iter_deinit(&ctx->list_iter);
+ return TRUE;
+ }
+ str = t_str_new(256);
+ mutf7_name = t_str_new(128);
+ while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) {
+ name = info->vname;
+ flags = info->flags;
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (flags & MAILBOX_SUBSCRIBED) == 0 &&
+ ctx->lsub_no_unsubscribed) {
+ /* mask doesn't end with %. we don't want to show
+ any extra mailboxes. */
+ continue;
+ }
+
+ str_truncate(mutf7_name, 0);
+ if (imap_utf8_to_utf7(name, mutf7_name) < 0)
+ i_panic("LIST: Mailbox name not UTF-8: %s", name);
+
+ str_truncate(str, 0);
+ str_printfa(str, "* %s (", ctx->lsub ? "LSUB" : "LIST");
+ mailbox_flags2str(ctx, str, info->special_use, flags);
+ str_append(str, ") ");
+ list_reply_append_ns_sep_param(str,
+ mail_namespace_get_sep(info->ns));
+ str_append_c(str, ' ');
+ imap_append_astring(str, str_c(mutf7_name));
+ mailbox_childinfo2str(ctx, str, flags);
+
+ ret = client_send_line_next(ctx->cmd->client, str_c(str));
+ if (ctx->used_status) T_BEGIN {
+ list_send_status(ctx, name, str_c(mutf7_name), flags);
+ } T_END;
+ if (ret == 0) {
+ /* buffer is full, continue later */
+ return FALSE;
+ }
+ }
+
+ if (mailbox_list_iter_deinit(&ctx->list_iter) < 0) {
+ client_send_list_error(cmd, ctx->user->namespaces->list);
+ return TRUE;
+ }
+ client_send_tagline(cmd, !ctx->lsub ?
+ "OK List completed." :
+ "OK Lsub completed.");
+ return TRUE;
+}
+
+static const char *const *
+list_get_ref_patterns(struct cmd_list_context *ctx, const char *ref,
+ const char *const *patterns)
+{
+ struct mail_namespace *ns;
+ const char *const *pat, *pattern;
+ ARRAY(const char *) full_patterns;
+
+ if (*ref == '\0')
+ return patterns;
+
+ ns = mail_namespace_find(ctx->user->namespaces, ref);
+
+ t_array_init(&full_patterns, 16);
+ for (pat = patterns; *pat != NULL; pat++) {
+ pattern = mailbox_list_join_refpattern(ns->list, ref, *pat);
+ array_push_back(&full_patterns, &pattern);
+ }
+ array_append_zero(&full_patterns); /* NULL-terminate */
+ return array_front(&full_patterns);
+}
+
+static void cmd_list_init(struct cmd_list_context *ctx,
+ const char *const *patterns)
+{
+ enum mail_namespace_type type_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
+
+ ctx->list_iter =
+ mailbox_list_iter_init_namespaces(ctx->user->namespaces,
+ patterns, type_mask,
+ ctx->list_flags);
+}
+
+static void cmd_list_ref_root(struct client *client, const char *ref)
+{
+ struct mail_namespace *ns;
+ const char *ns_prefix;
+ char ns_sep;
+ string_t *str;
+
+ /* Special request to return the hierarchy delimiter and mailbox root
+ name. If namespace has a prefix, it's returned as the mailbox root.
+ Otherwise we'll emulate UW-IMAP behavior. */
+ ns = mail_namespace_find_visible(client->user->namespaces, ref);
+ if (ns != NULL) {
+ ns_prefix = ns_prefix_mutf7(ns);
+ ns_sep = mail_namespace_get_sep(ns);
+ } else {
+ ns_prefix = "";
+ ns_sep = mail_namespaces_get_root_sep(client->user->namespaces);
+ }
+
+ str = t_str_new(64);
+ str_append(str, "* LIST (\\Noselect) \"");
+ if (ns_sep == '\\' || ns_sep == '"')
+ str_append_c(str, '\\');
+ str_printfa(str, "%c\" ", ns_sep);
+ if (*ns_prefix != '\0') {
+ /* non-hidden namespace, use it as the root name */
+ imap_append_astring(str, ns_prefix);
+ } else {
+ /* Hidden namespace or empty namespace prefix. We could just
+ return an empty root name, but it's safer to emulate what
+ UW-IMAP does. With full filesystem access this might even
+ matter (root of "~user/mail/" is "~user/", not "") */
+ const char *p = strchr(ref, ns_sep);
+
+ if (p == NULL)
+ str_append(str, "\"\"");
+ else
+ imap_append_astring(str, t_strdup_until(ref, p + 1));
+ }
+ client_send_line(client, str_c(str));
+}
+
+bool cmd_list_full(struct client_command_context *cmd, bool lsub)
+{
+ struct client *client = cmd->client;
+ const struct imap_arg *args, *list_args;
+ unsigned int arg_count;
+ struct cmd_list_context *ctx;
+ ARRAY(const char *) patterns = ARRAY_INIT;
+ const char *ref, *pattern, *const *patterns_strarr;
+ string_t *str;
+
+ /* [(<selection options>)] <reference> <pattern>|(<pattern list>)
+ [RETURN (<return options>)] */
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ ctx = p_new(cmd->pool, struct cmd_list_context, 1);
+ ctx->cmd = cmd;
+ ctx->lsub = lsub;
+ ctx->user = client->user;
+
+ cmd->context = ctx;
+
+ if (!lsub && imap_arg_get_list(&args[0], &list_args)) {
+ /* LIST-EXTENDED selection options */
+ ctx->used_listext = TRUE;
+ if (!parse_select_flags(ctx, list_args))
+ return TRUE;
+ args++;
+ }
+
+ if (!imap_arg_get_astring(&args[0], &ref)) {
+ client_send_command_error(cmd, "Invalid reference.");
+ return TRUE;
+ }
+ str = t_str_new(64);
+ if (imap_utf7_to_utf8(ref, str) == 0)
+ ref = p_strdup(cmd->pool, str_c(str));
+ str_truncate(str, 0);
+
+ if (imap_arg_get_list_full(&args[1], &list_args, &arg_count)) {
+ ctx->used_listext = TRUE;
+ /* convert pattern list to string array */
+ p_array_init(&patterns, cmd->pool, arg_count);
+ for (; !IMAP_ARG_IS_EOL(list_args); list_args++) {
+ if (!imap_arg_get_astring(list_args, &pattern)) {
+ client_send_command_error(cmd,
+ "Invalid pattern list.");
+ return TRUE;
+ }
+ if (imap_utf7_to_utf8(pattern, str) == 0)
+ pattern = p_strdup(cmd->pool, str_c(str));
+ array_push_back(&patterns, &pattern);
+ str_truncate(str, 0);
+ }
+ args += 2;
+ } else {
+ if (!imap_arg_get_astring(&args[1], &pattern)) {
+ client_send_command_error(cmd, "Invalid pattern.");
+ return TRUE;
+ }
+ if (imap_utf7_to_utf8(pattern, str) == 0)
+ pattern = p_strdup(cmd->pool, str_c(str));
+
+ p_array_init(&patterns, cmd->pool, 1);
+ array_push_back(&patterns, &pattern);
+ args += 2;
+
+ if (lsub) {
+ size_t len = strlen(pattern);
+ ctx->lsub_no_unsubscribed = len == 0 ||
+ pattern[len-1] != '%';
+ }
+ }
+
+ if (imap_arg_atom_equals(&args[0], "RETURN") &&
+ imap_arg_get_list(&args[1], &list_args)) {
+ /* LIST-EXTENDED return options */
+ ctx->used_listext = TRUE;
+ if (!parse_return_flags(ctx, list_args))
+ return TRUE;
+ args += 2;
+ }
+
+ if (lsub) {
+ /* LSUB - we don't care about flags except if
+ tb-lsub-flags workaround is explicitly set */
+ ctx->list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH;
+ /* Return SPECIAL-USE flags for LSUB anyway. Outlook 2013
+ does this and since it's not expensive for us to return
+ them, it's not worth the trouble of adding an explicit
+ workaround setting. */
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ if ((cmd->client->set->parsed_workarounds &
+ WORKAROUND_TB_LSUB_FLAGS) == 0)
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ } else if (!ctx->used_listext) {
+ /* non-extended LIST: use default flags */
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN |
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ }
+
+ if (!IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd, "Extra arguments.");
+ return TRUE;
+ }
+
+ array_append_zero(&patterns); /* NULL-terminate */
+ patterns_strarr = array_front(&patterns);
+ if (!ctx->used_listext && !lsub && *patterns_strarr[0] == '\0') {
+ /* Only LIST ref "" gets us here */
+ cmd_list_ref_root(client, ref);
+ client_send_tagline(cmd, "OK List completed.");
+ } else {
+ patterns_strarr =
+ list_get_ref_patterns(ctx, ref, patterns_strarr);
+ cmd_list_init(ctx, patterns_strarr);
+
+ if (!cmd_list_continue(cmd)) {
+ /* unfinished */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ cmd->func = cmd_list_continue;
+ return FALSE;
+ }
+
+ cmd->context = NULL;
+ return TRUE;
+ }
+ return TRUE;
+}
+
+bool cmd_list(struct client_command_context *cmd)
+{
+ return cmd_list_full(cmd, FALSE);
+}
diff --git a/src/imap/cmd-logout.c b/src/imap/cmd-logout.c
new file mode 100644
index 0000000..fbdd416
--- /dev/null
+++ b/src/imap/cmd-logout.c
@@ -0,0 +1,24 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ostream.h"
+#include "imap-commands.h"
+
+bool cmd_logout(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+
+ client->logged_out = TRUE;
+ client_send_line(client, "* BYE Logging out");
+
+ if (client->mailbox != NULL) {
+ /* this could be done at client_disconnect() as well,
+ but eg. mbox rewrite takes a while so the waiting is
+ better to happen before "OK" message. */
+ imap_client_close_mailbox(client);
+ }
+
+ client_send_tagline(cmd, "OK Logout completed.");
+ client_disconnect(client, "Logged out");
+ return TRUE;
+}
diff --git a/src/imap/cmd-lsub.c b/src/imap/cmd-lsub.c
new file mode 100644
index 0000000..55d9b68
--- /dev/null
+++ b/src/imap/cmd-lsub.c
@@ -0,0 +1,9 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_lsub(struct client_command_context *cmd)
+{
+ return cmd_list_full(cmd, TRUE);
+}
diff --git a/src/imap/cmd-namespace.c b/src/imap/cmd-namespace.c
new file mode 100644
index 0000000..23d2286
--- /dev/null
+++ b/src/imap/cmd-namespace.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-utf7.h"
+#include "imap-quote.h"
+#include "imap-commands.h"
+#include "mail-namespace.h"
+
+struct namespace_order {
+ int secondary_order;
+ struct mail_namespace *ns;
+};
+
+static int namespace_order_cmp(const struct namespace_order *no1,
+ const struct namespace_order *no2)
+{
+ if (no1->ns->set->order < no2->ns->set->order)
+ return -1;
+ if (no1->ns->set->order > no2->ns->set->order)
+ return 1;
+
+ if (no1->secondary_order < no2->secondary_order)
+ return -1;
+ if (no1->secondary_order > no2->secondary_order)
+ return 1;
+ return 0;
+}
+
+static void list_namespaces(struct mail_namespace *ns,
+ enum mail_namespace_type type, string_t *str)
+{
+ ARRAY(struct namespace_order) ns_order;
+ struct namespace_order *no;
+ unsigned int count = 0;
+ string_t *mutf7_prefix;
+ char ns_sep;
+
+ t_array_init(&ns_order, 4);
+
+ while (ns != NULL) {
+ if (ns->type == type &&
+ (ns->flags & NAMESPACE_FLAG_HIDDEN) == 0) {
+ no = array_append_space(&ns_order);
+ no->ns = ns;
+ no->secondary_order = ++count;
+ }
+ ns = ns->next;
+ }
+
+ if (array_count(&ns_order) == 0) {
+ str_append(str, "NIL");
+ return;
+ }
+ array_sort(&ns_order, namespace_order_cmp);
+
+ mutf7_prefix = t_str_new(64);
+ str_append_c(str, '(');
+ array_foreach_modifiable(&ns_order, no) {
+ ns_sep = mail_namespace_get_sep(no->ns);
+ str_append_c(str, '(');
+
+ str_truncate(mutf7_prefix, 0);
+ if (imap_utf8_to_utf7(no->ns->prefix, mutf7_prefix) < 0) {
+ i_panic("LIST: Namespace prefix not UTF-8: %s",
+ no->ns->prefix);
+ }
+
+ imap_append_string(str, str_c(mutf7_prefix));
+ str_append(str, " \"");
+ if (ns_sep == '\\')
+ str_append_c(str, '\\');
+ str_append_c(str, ns_sep);
+ str_append(str, "\")");
+ }
+ str_append_c(str, ')');
+}
+
+bool cmd_namespace(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ string_t *str;
+
+ str = t_str_new(256);
+ str_append(str, "* NAMESPACE ");
+
+ list_namespaces(client->user->namespaces,
+ MAIL_NAMESPACE_TYPE_PRIVATE, str);
+ str_append_c(str, ' ');
+ list_namespaces(client->user->namespaces,
+ MAIL_NAMESPACE_TYPE_SHARED, str);
+ str_append_c(str, ' ');
+ list_namespaces(client->user->namespaces,
+ MAIL_NAMESPACE_TYPE_PUBLIC, str);
+
+ client_send_line(client, str_c(str));
+ client_send_tagline(cmd, "OK Namespace completed.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-noop.c b/src/imap/cmd-noop.c
new file mode 100644
index 0000000..3fd2444
--- /dev/null
+++ b/src/imap/cmd-noop.c
@@ -0,0 +1,15 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-notify.h"
+#include "imap-commands.h"
+
+bool cmd_noop(struct client_command_context *cmd)
+{
+ if (cmd->client->notify_ctx != NULL) {
+ /* flush any delayed notifications now. this is mainly useful
+ for testing. */
+ imap_notify_flush(cmd->client->notify_ctx);
+ }
+ return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, "OK NOOP completed.");
+}
diff --git a/src/imap/cmd-notify.c b/src/imap/cmd-notify.c
new file mode 100644
index 0000000..e043e97
--- /dev/null
+++ b/src/imap/cmd-notify.c
@@ -0,0 +1,597 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "mailbox-list-iter.h"
+#include "imap-quote.h"
+#include "imap-commands.h"
+#include "imap-fetch.h"
+#include "imap-list.h"
+#include "imap-status.h"
+#include "imap-notify.h"
+
+#define IMAP_NOTIFY_MAX_NAMES_PER_NS 100
+
+static const char *imap_notify_event_names[] = {
+ "MessageNew", "MessageExpunge", "FlagChange", "AnnotationChange",
+ "MailboxName", "SubscriptionChange", "MailboxMetadataChange",
+ "ServerMetadataChange"
+};
+
+static int
+cmd_notify_parse_event(const struct imap_arg *arg,
+ enum imap_notify_event *event_r)
+{
+ const char *str;
+ unsigned int i;
+
+ if (!imap_arg_get_atom(arg, &str))
+ return -1;
+
+ for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) {
+ if (strcasecmp(str, imap_notify_event_names[i]) == 0) {
+ *event_r = (enum imap_notify_event)(1 << i);
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static int
+cmd_notify_parse_fetch(struct imap_notify_context *ctx,
+ const struct imap_arg *list)
+{
+ if (list->type == IMAP_ARG_EOL)
+ return -1; /* at least one attribute must be set */
+ return imap_fetch_att_list_parse(ctx->client, ctx->pool, list,
+ &ctx->fetch_ctx, &ctx->error);
+}
+
+static int
+cmd_notify_set_selected(struct imap_notify_context *ctx,
+ const struct imap_arg *events)
+{
+#define EV_NEW_OR_EXPUNGE \
+ (IMAP_NOTIFY_EVENT_MESSAGE_NEW | IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE)
+ const struct imap_arg *list, *fetch_att_list;
+ const char *str;
+ enum imap_notify_event event;
+
+ if (imap_arg_get_atom(events, &str) &&
+ strcasecmp(str, "NONE") == 0) {
+ /* no events for selected mailbox. this is also the default
+ when NOTIFY command doesn't specify it explicitly */
+ if (events[1].type != IMAP_ARG_EOL)
+ return -1; /* no extra parameters */
+ return 0;
+ }
+
+ if (!imap_arg_get_list(events, &list))
+ return -1;
+ if (events[1].type != IMAP_ARG_EOL)
+ return -1; /* no extra parameters */
+ if (list->type == IMAP_ARG_EOL)
+ return -1; /* at least one event */
+
+ for (; list->type != IMAP_ARG_EOL; list++) {
+ if (cmd_notify_parse_event(list, &event) < 0)
+ return -1;
+ ctx->selected_events |= event;
+ ctx->global_used_events |= event;
+
+ if (event == IMAP_NOTIFY_EVENT_MESSAGE_NEW &&
+ imap_arg_get_list(&list[1], &fetch_att_list)) {
+ /* MessageNew: list of fetch-att */
+ if (cmd_notify_parse_fetch(ctx, fetch_att_list) < 0)
+ return -1;
+ list++;
+ }
+ }
+
+ /* if MessageNew or MessageExpunge is specified, both of them must */
+ if ((ctx->selected_events & EV_NEW_OR_EXPUNGE) != 0 &&
+ (ctx->selected_events & EV_NEW_OR_EXPUNGE) != EV_NEW_OR_EXPUNGE) {
+ ctx->error = "MessageNew and MessageExpunge must be together";
+ return -1;
+ }
+
+ /* if FlagChange or AnnotationChange is specified,
+ MessageNew and MessageExpunge must also be specified */
+ if ((ctx->selected_events &
+ (IMAP_NOTIFY_EVENT_FLAG_CHANGE |
+ IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0 &&
+ (ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) == 0) {
+ ctx->error = "FlagChange requires MessageNew and MessageExpunge";
+ return -1;
+ }
+ return 0;
+}
+
+static struct imap_notify_namespace *
+imap_notify_namespace_get(struct imap_notify_context *ctx,
+ struct mail_namespace *ns)
+{
+ struct imap_notify_namespace *notify_ns;
+
+ array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+ if (notify_ns->ns == ns)
+ return notify_ns;
+ }
+ notify_ns = array_append_space(&ctx->namespaces);
+ notify_ns->ctx = ctx;
+ notify_ns->ns = ns;
+ p_array_init(&notify_ns->mailboxes, ctx->pool, 4);
+ return notify_ns;
+}
+
+static struct imap_notify_mailboxes *
+imap_notify_mailboxes_get(struct imap_notify_namespace *notify_ns,
+ enum imap_notify_type type,
+ enum imap_notify_event events)
+{
+ struct imap_notify_mailboxes *notify_boxes;
+
+ array_foreach_modifiable(&notify_ns->mailboxes, notify_boxes) {
+ if (notify_boxes->type == type &&
+ notify_boxes->events == events)
+ return notify_boxes;
+ }
+ notify_boxes = array_append_space(&notify_ns->mailboxes);
+ notify_boxes->type = type;
+ notify_boxes->events = events;
+ p_array_init(&notify_boxes->names, notify_ns->ctx->pool, 4);
+ return notify_boxes;
+}
+
+static void
+cmd_notify_add_mailbox(struct imap_notify_context *ctx,
+ struct mail_namespace *ns, const char *name,
+ enum imap_notify_type type,
+ enum imap_notify_event events)
+{
+ struct imap_notify_namespace *notify_ns;
+ struct imap_notify_mailboxes *notify_boxes;
+ const char *const *names;
+ unsigned int i, count;
+ size_t cur_len, name_len = strlen(name);
+ char ns_sep = mail_namespace_get_sep(ns);
+
+ if (mail_namespace_is_removable(ns)) {
+ /* exclude removable namespaces */
+ return;
+ }
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ !str_begins(name, "INBOX") &&
+ strncasecmp(name, "INBOX", 5) == 0 &&
+ (name[5] == '\0' || name[5] == ns_sep)) {
+ /* we'll do only case-sensitive comparisons later,
+ so sanitize INBOX to be uppercase */
+ name = t_strconcat("INBOX", name + 5, NULL);
+ }
+
+ notify_ns = imap_notify_namespace_get(ctx, ns);
+ notify_boxes = imap_notify_mailboxes_get(notify_ns, type, events);
+
+ names = array_get(&notify_boxes->names, &count);
+ for (i = 0; i < count; ) {
+ if (strcmp(names[i], name) == 0) {
+ /* exact duplicate, already added */
+ return;
+ }
+ if (type != IMAP_NOTIFY_TYPE_SUBTREE)
+ i++;
+ else {
+ /* see if one is a subtree of the other */
+ cur_len = strlen(names[i]);
+ if (str_begins(name, names[i]) &&
+ names[i][cur_len] == ns_sep) {
+ /* already matched in this subtree */
+ return;
+ }
+ if (strncmp(names[i], name, name_len) == 0 &&
+ names[i][name_len] == ns_sep) {
+ /* we're adding a parent, remove the child */
+ array_delete(&notify_boxes->names, i, 1);
+ names = array_get(&notify_boxes->names, &count);
+ } else {
+ i++;
+ }
+ }
+ }
+ name = p_strdup(ctx->pool, name);
+ array_push_back(&notify_boxes->names, &name);
+
+ ctx->global_max_mailbox_names =
+ I_MAX(ctx->global_max_mailbox_names,
+ array_count(&notify_boxes->names));
+}
+
+static void cmd_notify_add_personal(struct imap_notify_context *ctx,
+ enum imap_notify_event events)
+{
+ struct mail_namespace *ns;
+
+ for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) {
+ cmd_notify_add_mailbox(ctx, ns, "",
+ IMAP_NOTIFY_TYPE_SUBTREE, events);
+ }
+ }
+}
+
+static int
+imap_notify_refresh_subscriptions(struct client_command_context *cmd,
+ struct imap_notify_context *ctx)
+{
+ struct mailbox_list_iterate_context *iter;
+ struct mail_namespace *ns;
+
+ if (!ctx->have_subscriptions)
+ return 0;
+
+ /* make sure subscriptions are refreshed at least once */
+ for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
+ iter = mailbox_list_iter_init(ns->list, "*", MAILBOX_LIST_ITER_SELECT_SUBSCRIBED);
+ (void)mailbox_list_iter_next(iter);
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ client_send_list_error(cmd, ns->list);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void cmd_notify_add_subscribed(struct imap_notify_context *ctx,
+ enum imap_notify_event events)
+{
+ struct mail_namespace *ns;
+
+ ctx->have_subscriptions = TRUE;
+ for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
+ cmd_notify_add_mailbox(ctx, ns, "",
+ IMAP_NOTIFY_TYPE_SUBSCRIBED, events);
+ }
+}
+
+static void
+cmd_notify_add_mailbox_namespaces(struct imap_notify_context *ctx,
+ const char *name,
+ enum imap_notify_type type,
+ enum imap_notify_event events)
+{
+ struct mail_namespace *ns;
+
+ ns = mail_namespace_find(ctx->client->user->namespaces, name);
+ cmd_notify_add_mailbox(ctx, ns, name, type, events);
+}
+
+static int
+cmd_notify_add_mailboxes(struct imap_notify_context *ctx,
+ const struct imap_arg *arg,
+ enum imap_notify_type type,
+ enum imap_notify_event events)
+{
+ const struct imap_arg *list;
+ const char *name;
+
+ if (imap_arg_get_astring(arg, &name)) {
+ cmd_notify_add_mailbox_namespaces(ctx, name, type, events);
+ return 0;
+ }
+ if (!imap_arg_get_list(arg, &list))
+ return -1;
+
+ for (; list->type != IMAP_ARG_EOL; list++) {
+ if (!imap_arg_get_astring(list, &name))
+ return -1;
+
+ cmd_notify_add_mailbox_namespaces(ctx, name, type, events);
+ }
+ return 0;
+}
+
+static int
+cmd_notify_set(struct imap_notify_context *ctx, const struct imap_arg *args)
+{
+ const struct imap_arg *event_group, *mailboxes, *list;
+ const char *str, *filter_mailboxes;
+ enum imap_notify_event event, event_mask;
+
+ if (imap_arg_get_atom(args, &str) &&
+ strcasecmp(str, "STATUS") == 0) {
+ /* send STATUS replies for all matched mailboxes before
+ NOTIFY's OK reply */
+ ctx->send_immediate_status = TRUE;
+ args++;
+ }
+ for (; args->type != IMAP_ARG_EOL; args++) {
+ if (!imap_arg_get_list(args, &event_group))
+ return -1;
+
+ /* filter-mailboxes */
+ if (!imap_arg_get_atom(event_group, &filter_mailboxes))
+ return -1;
+ event_group++;
+
+ if (strcasecmp(filter_mailboxes, "selected") == 0 ||
+ strcasecmp(filter_mailboxes, "selected-delayed") == 0) {
+ /* setting events for selected mailbox.
+ handle specially. */
+ if (ctx->selected_set) {
+ ctx->error = "Duplicate selected filter";
+ return -1;
+ }
+ ctx->selected_set = TRUE;
+ if (strcasecmp(filter_mailboxes, "selected") == 0)
+ ctx->selected_immediate_expunges = TRUE;
+ if (cmd_notify_set_selected(ctx, event_group) < 0)
+ return -1;
+ continue;
+ }
+
+ if (strcasecmp(filter_mailboxes, "subtree") == 0 ||
+ strcasecmp(filter_mailboxes, "mailboxes") == 0) {
+ if (event_group->type == IMAP_ARG_EOL)
+ return -1;
+ mailboxes = event_group++;
+ /* check that the mailboxes parameter is valid */
+ if (IMAP_ARG_IS_ASTRING(mailboxes))
+ ;
+ else if (!imap_arg_get_list(mailboxes, &list))
+ return -1;
+ else if (list->type == IMAP_ARG_EOL) {
+ /* should have at least one mailbox */
+ return -1;
+ }
+ } else {
+ mailboxes = NULL;
+ }
+
+ /* parse events */
+ if (imap_arg_get_atom(event_group, &str) &&
+ strcasecmp(str, "NONE") == 0) {
+ /* NONE is the default, ignore this */
+ continue;
+ }
+ if (!imap_arg_get_list(event_group, &list) ||
+ list[0].type == IMAP_ARG_EOL)
+ return -1;
+
+ event_mask = 0;
+ for (; list->type != IMAP_ARG_EOL; list++) {
+ if (cmd_notify_parse_event(list, &event) < 0)
+ return -1;
+ event_mask |= event;
+ ctx->global_used_events |= event;
+ }
+
+ /* we can't currently know inboxes, so treat it the
+ same as personal */
+ if (strcasecmp(filter_mailboxes, "inboxes") == 0 ||
+ strcasecmp(filter_mailboxes, "personal") == 0)
+ cmd_notify_add_personal(ctx, event_mask);
+ else if (strcasecmp(filter_mailboxes, "subscribed") == 0)
+ cmd_notify_add_subscribed(ctx, event_mask);
+ else if (strcasecmp(filter_mailboxes, "subtree") == 0) {
+ if (cmd_notify_add_mailboxes(ctx, mailboxes,
+ IMAP_NOTIFY_TYPE_SUBTREE,
+ event_mask) < 0)
+ return -1;
+ } else if (strcasecmp(filter_mailboxes, "mailboxes") == 0) {
+ if (cmd_notify_add_mailboxes(ctx, mailboxes,
+ IMAP_NOTIFY_TYPE_MAILBOX,
+ event_mask) < 0)
+ return -1;
+ } else {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+imap_notify_box_list_noperm(struct client *client, struct mailbox *box)
+{
+ string_t *str = t_str_new(128);
+ char ns_sep = mail_namespace_get_sep(mailbox_get_namespace(box));
+ enum mailbox_info_flags mailbox_flags;
+
+ if (mailbox_list_mailbox(mailbox_get_namespace(box)->list,
+ mailbox_get_name(box), &mailbox_flags) < 0)
+ mailbox_flags = 0;
+
+ str_append(str, "* LIST (");
+ if (imap_mailbox_flags2str(str, mailbox_flags))
+ str_append_c(str, ' ');
+ str_append(str, "\\NoAccess) \"");
+ if (ns_sep == '\\')
+ str_append_c(str, '\\');
+ str_append_c(str, ns_sep);
+ str_append(str, "\" ");
+
+ imap_append_astring(str, mailbox_get_vname(box));
+ client_send_line(client, str_c(str));
+}
+
+static void
+imap_notify_box_send_status(struct client_command_context *cmd,
+ struct imap_notify_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct mailbox *box;
+ struct imap_status_items items;
+ struct imap_status_result result;
+
+ if ((info->flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0)
+ return;
+
+ /* don't send STATUS to selected mailbox */
+ if (cmd->client->mailbox != NULL &&
+ mailbox_equals(cmd->client->mailbox, info->ns, info->vname))
+ return;
+
+ i_zero(&items);
+ i_zero(&result);
+
+ items.flags = IMAP_STATUS_ITEM_UIDVALIDITY | IMAP_STATUS_ITEM_UIDNEXT |
+ IMAP_STATUS_ITEM_MESSAGES | IMAP_STATUS_ITEM_UNSEEN;
+ if ((ctx->global_used_events & (IMAP_NOTIFY_EVENT_FLAG_CHANGE |
+ IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0)
+ items.flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ;
+
+ box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY);
+ (void)mailbox_enable(box, client_enabled_mailbox_features(ctx->client));
+
+ if (imap_status_get(cmd, info->ns, info->vname, &items, &result) < 0) {
+ if (result.error == MAIL_ERROR_PERM)
+ imap_notify_box_list_noperm(ctx->client, box);
+ else if (result.error != MAIL_ERROR_NOTFOUND) {
+ client_send_line(ctx->client,
+ t_strconcat("* ", result.errstr, NULL));
+ }
+ } else {
+ imap_status_send(ctx->client, info->vname, &items, &result);
+ }
+ mailbox_free(&box);
+}
+
+static bool imap_notify_ns_want_status(struct imap_notify_namespace *notify_ns)
+{
+ const struct imap_notify_mailboxes *notify_boxes;
+
+ array_foreach(&notify_ns->mailboxes, notify_boxes) {
+ if ((notify_boxes->events &
+ (IMAP_NOTIFY_EVENT_MESSAGE_NEW |
+ IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE |
+ IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE |
+ IMAP_NOTIFY_EVENT_FLAG_CHANGE)) != 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+imap_notify_ns_send_status(struct client_command_context *cmd,
+ struct imap_notify_context *ctx,
+ struct imap_notify_namespace *notify_ns)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct imap_notify_mailboxes *notify_boxes;
+ const struct mailbox_info *info;
+
+ if (!imap_notify_ns_want_status(notify_ns))
+ return;
+
+ /* set _RETURN_SUBSCRIBED flag just in case IMAP_NOTIFY_TYPE_SUBSCRIBED
+ is used, which requires refreshing subscriptions */
+ iter = mailbox_list_iter_init(notify_ns->ns->list, "*",
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ array_foreach(&notify_ns->mailboxes, notify_boxes) {
+ if (imap_notify_match_mailbox(notify_ns, notify_boxes,
+ info->vname)) {
+ imap_notify_box_send_status(cmd, ctx, info);
+ break;
+ }
+ }
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ client_send_line(notify_ns->ctx->client,
+ "* NO Mailbox listing failed");
+ }
+}
+
+static void cmd_notify_send_status(struct client_command_context *cmd,
+ struct imap_notify_context *ctx)
+{
+ struct imap_notify_namespace *notify_ns;
+
+ array_foreach_modifiable(&ctx->namespaces, notify_ns)
+ imap_notify_ns_send_status(cmd, ctx, notify_ns);
+}
+
+bool cmd_notify(struct client_command_context *cmd)
+{
+ struct imap_notify_context *ctx;
+ const struct imap_arg *args;
+ const char *str;
+ int ret = 0;
+ pool_t pool;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ pool = pool_alloconly_create("imap notify context", 1024);
+ ctx = p_new(pool, struct imap_notify_context, 1);
+ ctx->pool = pool;
+ ctx->client = cmd->client;
+ p_array_init(&ctx->namespaces, pool, 4);
+
+ if (!imap_arg_get_atom(&args[0], &str))
+ ret = -1;
+ else if (strcasecmp(str, "NONE") == 0)
+ ;
+ else if (strcasecmp(str, "SET") == 0)
+ ret = cmd_notify_set(ctx, args+1);
+ else
+ ret = -1;
+
+ if (ret < 0) {
+ client_send_command_error(cmd, ctx->error != NULL ? ctx->error :
+ "Invalid arguments.");
+ pool_unref(&pool);
+ return TRUE;
+ }
+
+ if ((ctx->global_used_events & UNSUPPORTED_EVENTS) != 0) {
+ string_t *client_error = t_str_new(128);
+ unsigned int i;
+
+ str_append(client_error, "NO [BADEVENT");
+ for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) {
+ if ((ctx->global_used_events & (1 << i)) != 0 &&
+ ((1 << i) & UNSUPPORTED_EVENTS) != 0) {
+ str_append_c(client_error, ' ');
+ str_append(client_error, imap_notify_event_names[i]);
+ }
+ }
+ str_append(client_error, "] Unsupported NOTIFY events.");
+ client_send_tagline(cmd, str_c(client_error));
+ pool_unref(&pool);
+ return TRUE;
+ }
+
+ if (array_count(&ctx->namespaces) == 0) {
+ /* selected mailbox only */
+ } else if (ctx->global_max_mailbox_names > IMAP_NOTIFY_MAX_NAMES_PER_NS) {
+ client_send_tagline(cmd,
+ "NO [NOTIFICATIONOVERFLOW] Too many mailbox names");
+ pool_unref(&pool);
+ return TRUE;
+ } else if (imap_notify_refresh_subscriptions(cmd, ctx) < 0) {
+ /* tagline already sent */
+ pool_unref(&pool);
+ return TRUE;
+ } else if (imap_notify_begin(ctx) < 0) {
+ client_send_tagline(cmd,
+ "NO [NOTIFICATIONOVERFLOW] NOTIFY not supported for these mailboxes.");
+ pool_unref(&pool);
+ return TRUE;
+ }
+ if (cmd->client->notify_ctx != NULL)
+ imap_notify_deinit(&cmd->client->notify_ctx);
+
+ if (ctx->send_immediate_status)
+ cmd_notify_send_status(cmd, ctx);
+ cmd->client->notify_immediate_expunges =
+ ctx->selected_immediate_expunges;
+ cmd->client->notify_count_changes =
+ (ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0;
+ cmd->client->notify_flag_changes =
+ (ctx->selected_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0;
+
+ cmd->client->notify_ctx = ctx;
+ return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, "OK NOTIFY completed.");
+}
diff --git a/src/imap/cmd-rename.c b/src/imap/cmd-rename.c
new file mode 100644
index 0000000..45b44fd
--- /dev/null
+++ b/src/imap/cmd-rename.c
@@ -0,0 +1,51 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "mail-namespace.h"
+#include "imap-commands.h"
+
+bool cmd_rename(struct client_command_context *cmd)
+{
+ struct mail_namespace *old_ns, *new_ns;
+ struct mailbox *old_box, *new_box;
+ const char *oldname, *newname;
+ size_t oldlen;
+
+ /* <old name> <new name> */
+ if (!client_read_string_args(cmd, 2, &oldname, &newname))
+ return FALSE;
+
+ old_ns = client_find_namespace(cmd, &oldname);
+ if (old_ns == NULL)
+ return TRUE;
+ new_ns = client_find_namespace(cmd, &newname);
+ if (new_ns == NULL)
+ return TRUE;
+
+ if (old_ns == new_ns) {
+ /* disallow box -> box/child, because it may break clients and
+ there's really no point in doing it anyway. */
+ old_ns = mailbox_list_get_namespace(old_ns->list);
+ oldlen = strlen(oldname);
+ if (str_begins(newname, oldname) &&
+ newname[oldlen] == mail_namespace_get_sep(old_ns)) {
+ client_send_tagline(cmd,
+ "NO Can't rename mailbox under its own child.");
+ return TRUE;
+ }
+ }
+
+ old_box = mailbox_alloc(old_ns->list, oldname, 0);
+ new_box = mailbox_alloc(new_ns->list, newname, 0);
+ event_add_str(cmd->global_event, "old_mailbox",
+ mailbox_get_vname(old_box));
+ event_add_str(cmd->global_event, "new_mailbox",
+ mailbox_get_vname(new_box));
+ if (mailbox_rename(old_box, new_box) < 0)
+ client_send_box_error(cmd, old_box);
+ else
+ client_send_tagline(cmd, "OK Rename completed.");
+ mailbox_free(&old_box);
+ mailbox_free(&new_box);
+ return TRUE;
+}
diff --git a/src/imap/cmd-resetkey.c b/src/imap/cmd-resetkey.c
new file mode 100644
index 0000000..3475994
--- /dev/null
+++ b/src/imap/cmd-resetkey.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-resp-code.h"
+#include "imap-commands.h"
+#include "imap-urlauth.h"
+
+static bool cmd_resetkey_all(struct client_command_context *cmd)
+{
+ if (imap_urlauth_reset_all_keys(cmd->client->urlauth_ctx) < 0) {
+ client_send_internal_error(cmd);
+ return TRUE;
+ }
+
+ client_send_tagline(cmd, "OK All keys removed.");
+ return TRUE;
+}
+
+static bool
+cmd_resetkey_mailbox(struct client_command_context *cmd,
+ const char *mailbox, const struct imap_arg *mech_args)
+{
+ struct mail_namespace *ns;
+ enum mailbox_flags flags = MAILBOX_FLAG_READONLY;
+ struct mailbox *box;
+
+ /* check mechanism arguments (we support only INTERNAL mechanism) */
+ while (!IMAP_ARG_IS_EOL(mech_args)) {
+ const char *mechanism;
+
+ if (imap_arg_get_astring(mech_args, &mechanism)) {
+ if (strcasecmp(mechanism, "INTERNAL") != 0) {
+ client_send_tagline(cmd,
+ "NO Unsupported URLAUTH mechanism.");
+ return TRUE;
+ }
+ } else {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ mech_args++;
+ }
+
+ /* find mailbox namespace */
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ /* open mailbox */
+ box = mailbox_alloc(ns->list, mailbox, flags);
+ event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box));
+ if (mailbox_open(box) < 0) {
+ client_send_box_error(cmd, box);
+ mailbox_free(&box);
+ return TRUE;
+ }
+
+ /* check urlauth environment and reset requested key */
+ if (imap_urlauth_reset_mailbox_key(cmd->client->urlauth_ctx, box) < 0) {
+ client_send_internal_error(cmd);
+ mailbox_free(&box);
+ return TRUE;
+ }
+
+ /* confirm success */
+ /* FIXME: RFC Says: `Any current IMAP session logged in as the user
+ that has the mailbox selected will receive an untagged OK response
+ with the URLMECH status response code'. We currently don't do that
+ at all. We could probably do it by communicating via mailbox list
+ index. */
+ client_send_tagline(cmd, "OK [URLMECH INTERNAL] Key removed.");
+ mailbox_free(&box);
+ return TRUE;
+}
+
+bool cmd_resetkey(struct client_command_context *cmd)
+{
+ const struct imap_arg *args;
+ const char *mailbox;
+
+ if (cmd->client->urlauth_ctx == NULL) {
+ client_send_command_error(cmd, "URLAUTH disabled.");
+ return TRUE;
+ }
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (IMAP_ARG_IS_EOL(&args[0]))
+ return cmd_resetkey_all(cmd);
+ else if (imap_arg_get_astring(&args[0], &mailbox))
+ return cmd_resetkey_mailbox(cmd, mailbox, &args[1]);
+
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-search.c b/src/imap/cmd-search.c
new file mode 100644
index 0000000..52be6f6
--- /dev/null
+++ b/src/imap/cmd-search.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-search-args.h"
+#include "imap-search.h"
+
+bool cmd_search(struct client_command_context *cmd)
+{
+ struct imap_search_context *ctx;
+ struct mail_search_args *sargs;
+ const struct imap_arg *args;
+ const char *charset;
+ int ret;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ ctx = p_new(cmd->pool, struct imap_search_context, 1);
+ ctx->cmd = cmd;
+
+ if ((ret = cmd_search_parse_return_if_found(ctx, &args)) <= 0) {
+ /* error / waiting for unambiguity */
+ return ret < 0;
+ }
+
+ if (imap_arg_atom_equals(args, "CHARSET")) {
+ /* CHARSET specified */
+ if (!imap_arg_get_astring(&args[1], &charset)) {
+ client_send_command_error(cmd,
+ "Invalid charset argument.");
+ imap_search_context_free(ctx);
+ return TRUE;
+ }
+ args += 2;
+ } else {
+ charset = "UTF-8";
+ }
+
+ ret = imap_search_args_build(cmd, args, charset, &sargs);
+ if (ret <= 0) {
+ imap_search_context_free(ctx);
+ return ret < 0;
+ }
+
+ return imap_search_start(ctx, sargs, NULL);
+}
diff --git a/src/imap/cmd-select.c b/src/imap/cmd-select.c
new file mode 100644
index 0000000..c9af870
--- /dev/null
+++ b/src/imap/cmd-select.c
@@ -0,0 +1,426 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "seq-range-array.h"
+#include "time-util.h"
+#include "imap-commands.h"
+#include "mail-search-build.h"
+#include "imap-search-args.h"
+#include "imap-seqset.h"
+#include "imap-fetch.h"
+#include "imap-sync.h"
+
+
+struct imap_select_context {
+ struct client_command_context *cmd;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+
+ struct imap_fetch_context *fetch_ctx;
+
+ uint32_t qresync_uid_validity;
+ uint64_t qresync_modseq;
+ ARRAY_TYPE(seq_range) qresync_known_uids;
+ ARRAY_TYPE(uint32_t) qresync_sample_seqset;
+ ARRAY_TYPE(uint32_t) qresync_sample_uidset;
+
+ bool condstore:1;
+};
+
+static int select_qresync_get_uids(struct imap_select_context *ctx,
+ const ARRAY_TYPE(seq_range) *seqset,
+ const ARRAY_TYPE(seq_range) *uidset)
+{
+ const struct seq_range *uid_range;
+ struct seq_range_iter seq_iter;
+ unsigned int i, uid_count, diff, n = 0;
+ uint32_t seq;
+
+ /* change all n:m ranges to n,m and store the results */
+ uid_range = array_get(uidset, &uid_count);
+
+ seq_range_array_iter_init(&seq_iter, seqset);
+ i_array_init(&ctx->qresync_sample_uidset, uid_count);
+ i_array_init(&ctx->qresync_sample_seqset, uid_count);
+ for (i = 0; i < uid_count; i++) {
+ if (!seq_range_array_iter_nth(&seq_iter, n++, &seq))
+ return -1;
+ array_push_back(&ctx->qresync_sample_uidset,
+ &uid_range[i].seq1);
+ array_push_back(&ctx->qresync_sample_seqset, &seq);
+
+ diff = uid_range[i].seq2 - uid_range[i].seq1;
+ if (diff > 0) {
+ n += diff - 1;
+ if (!seq_range_array_iter_nth(&seq_iter, n++, &seq))
+ return -1;
+
+ array_push_back(&ctx->qresync_sample_uidset,
+ &uid_range[i].seq2);
+ array_push_back(&ctx->qresync_sample_seqset, &seq);
+ }
+ }
+ if (seq_range_array_iter_nth(&seq_iter, n, &seq))
+ return -1;
+ return 0;
+}
+
+static bool
+select_parse_qresync_known_set(struct imap_select_context *ctx,
+ const struct imap_arg *args,
+ const char **error_r)
+{
+ ARRAY_TYPE(seq_range) seqset, uidset;
+ const char *str;
+
+ t_array_init(&seqset, 32);
+ if (!imap_arg_get_atom(args, &str) ||
+ imap_seq_set_nostar_parse(str, &seqset) < 0) {
+ *error_r = "Invalid QRESYNC known-sequence-set";
+ return FALSE;
+ }
+ args++;
+
+ t_array_init(&uidset, 32);
+ if (!imap_arg_get_atom(args, &str) ||
+ imap_seq_set_nostar_parse(str, &uidset) < 0) {
+ *error_r = "Invalid QRESYNC known-uid-set";
+ return FALSE;
+ }
+ args++;
+
+ if (select_qresync_get_uids(ctx, &seqset, &uidset) < 0) {
+ *error_r = "Invalid QRESYNC sets";
+ return FALSE;
+ }
+ if (!IMAP_ARG_IS_EOL(args)) {
+ *error_r = "Too many parameters to QRESYNC known set";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+select_parse_qresync(struct imap_select_context *ctx,
+ const struct imap_arg *args, const char **error_r)
+{
+ const struct imap_arg *list_args;
+ const char *str;
+ unsigned int count;
+
+ if (!client_has_enabled(ctx->cmd->client, imap_feature_qresync)) {
+ *error_r = "QRESYNC not enabled";
+ return FALSE;
+ }
+ if (!imap_arg_get_list_full(args, &args, &count)) {
+ *error_r = "QRESYNC parameters missing";
+ return FALSE;
+ }
+
+ if (!imap_arg_get_atom(&args[0], &str) ||
+ str_to_uint32(str, &ctx->qresync_uid_validity) < 0 ||
+ !imap_arg_get_atom(&args[1], &str) ||
+ str_to_uint64(str, &ctx->qresync_modseq) < 0) {
+ *error_r = "Invalid QRESYNC parameters";
+ return FALSE;
+ }
+ args += 2;
+
+ i_array_init(&ctx->qresync_known_uids, 64);
+ if (imap_arg_get_atom(args, &str)) {
+ if (imap_seq_set_nostar_parse(str, &ctx->qresync_known_uids) < 0) {
+ *error_r = "Invalid QRESYNC known-uids";
+ return FALSE;
+ }
+ args++;
+ } else {
+ seq_range_array_add_range(&ctx->qresync_known_uids,
+ 1, (uint32_t)-1);
+ }
+ if (imap_arg_get_list(args, &list_args)) {
+ if (!select_parse_qresync_known_set(ctx, list_args, error_r))
+ return FALSE;
+ args++;
+ }
+ if (!IMAP_ARG_IS_EOL(args)) {
+ *error_r = "Invalid QRESYNC parameters";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+select_parse_options(struct imap_select_context *ctx,
+ const struct imap_arg *args, const char **error_r)
+{
+ const char *name;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &name)) {
+ *error_r = "SELECT options contain non-atoms.";
+ return FALSE;
+ }
+ name = t_str_ucase(name);
+ args++;
+
+ if (strcmp(name, "CONDSTORE") == 0)
+ ctx->condstore = TRUE;
+ else if (strcmp(name, "QRESYNC") == 0) {
+ if (!select_parse_qresync(ctx, args, error_r))
+ return FALSE;
+ args++;
+ } else {
+ *error_r = "Unknown FETCH modifier";
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void select_context_free(struct imap_select_context *ctx)
+{
+ if (array_is_created(&ctx->qresync_known_uids))
+ array_free(&ctx->qresync_known_uids);
+ if (array_is_created(&ctx->qresync_sample_seqset))
+ array_free(&ctx->qresync_sample_seqset);
+ if (array_is_created(&ctx->qresync_sample_uidset))
+ array_free(&ctx->qresync_sample_uidset);
+}
+
+static void cmd_select_finish(struct imap_select_context *ctx, int ret)
+{
+ const char *resp_code;
+
+ if (ret < 0) {
+ if (ctx->box != NULL)
+ mailbox_free(&ctx->box);
+ ctx->cmd->client->mailbox = NULL;
+ } else {
+ resp_code = mailbox_is_readonly(ctx->box) ?
+ "READ-ONLY" : "READ-WRITE";
+ client_send_tagline(ctx->cmd, t_strdup_printf(
+ "OK [%s] %s completed", resp_code,
+ ctx->cmd->client->mailbox_examined ? "Examine" : "Select"));
+ }
+ select_context_free(ctx);
+}
+
+static bool cmd_select_continue(struct client_command_context *cmd)
+{
+ struct imap_select_context *ctx = cmd->context;
+ int ret;
+
+ if (imap_fetch_more(ctx->fetch_ctx, cmd) == 0) {
+ /* unfinished */
+ return FALSE;
+ }
+
+ ret = imap_fetch_end(ctx->fetch_ctx);
+ if (ret < 0)
+ client_send_box_error(ctx->cmd, ctx->box);
+ imap_fetch_free(&ctx->fetch_ctx);
+ cmd_select_finish(ctx, ret);
+ return TRUE;
+}
+
+static int select_qresync(struct imap_select_context *ctx)
+{
+ struct imap_fetch_context *fetch_ctx;
+ struct mail_search_args *search_args;
+ struct imap_fetch_qresync_args qresync_args;
+ int ret;
+
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_args->args->type = SEARCH_UIDSET;
+ search_args->args->value.seqset = ctx->qresync_known_uids;
+ imap_search_add_changed_since(search_args, ctx->qresync_modseq);
+
+ i_zero(&qresync_args);
+ qresync_args.qresync_sample_seqset = &ctx->qresync_sample_seqset;
+ qresync_args.qresync_sample_uidset = &ctx->qresync_sample_uidset;
+
+ if (imap_fetch_send_vanished(ctx->cmd->client, ctx->box,
+ search_args, &qresync_args) < 0) {
+ mail_search_args_unref(&search_args);
+ return -1;
+ }
+
+ fetch_ctx = imap_fetch_alloc(ctx->cmd->client, ctx->cmd->pool,
+ t_strdup_printf("%s %s", ctx->cmd->name, ctx->cmd->args));
+
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init);
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init);
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init);
+
+ imap_fetch_begin(fetch_ctx, ctx->box, search_args);
+ mail_search_args_unref(&search_args);
+
+ if (imap_fetch_more(fetch_ctx, ctx->cmd) == 0) {
+ /* unfinished */
+ ctx->fetch_ctx = fetch_ctx;
+ ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+
+ ctx->cmd->func = cmd_select_continue;
+ ctx->cmd->context = ctx;
+ return 0;
+ }
+ ret = imap_fetch_end(fetch_ctx);
+ imap_fetch_free(&fetch_ctx);
+ return ret < 0 ? -1 : 1;
+}
+
+static int
+select_open(struct imap_select_context *ctx, const char *mailbox, bool readonly)
+{
+ struct client *client = ctx->cmd->client;
+ struct mailbox_status status;
+ enum mailbox_flags flags = 0;
+ int ret = 0;
+
+ if (readonly)
+ flags |= MAILBOX_FLAG_READONLY;
+ else
+ flags |= MAILBOX_FLAG_DROP_RECENT;
+ ctx->box = mailbox_alloc(ctx->ns->list, mailbox, flags);
+ event_add_str(ctx->cmd->global_event, "mailbox",
+ mailbox_get_vname(ctx->box));
+ if (mailbox_open(ctx->box) < 0) {
+ client_send_box_error(ctx->cmd, ctx->box);
+ mailbox_free(&ctx->box);
+ return -1;
+ }
+
+ ret = mailbox_enable(ctx->box, client_enabled_mailbox_features(client));
+ if (ret < 0 ||
+ mailbox_sync(ctx->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ client_send_box_error(ctx->cmd, ctx->box);
+ return -1;
+ }
+ mailbox_get_open_status(ctx->box, STATUS_MESSAGES | STATUS_RECENT |
+ STATUS_FIRST_UNSEEN_SEQ | STATUS_UIDVALIDITY |
+ STATUS_UIDNEXT | STATUS_KEYWORDS |
+ STATUS_HIGHESTMODSEQ, &status);
+
+ client->mailbox = ctx->box;
+ client->mailbox_examined = readonly;
+ client->messages_count = status.messages;
+ client->recent_count = status.recent;
+ client->uidvalidity = status.uidvalidity;
+ client->notify_uidnext = status.uidnext;
+
+ client_update_mailbox_flags(client, status.keywords);
+ client_send_mailbox_flags(client, TRUE);
+
+ client_send_line(client,
+ t_strdup_printf("* %u EXISTS", status.messages));
+ client_send_line(client,
+ t_strdup_printf("* %u RECENT", status.recent));
+
+ if (status.first_unseen_seq != 0) {
+ client_send_line(client,
+ t_strdup_printf("* OK [UNSEEN %u] First unseen.",
+ status.first_unseen_seq));
+ }
+
+ client_send_line(client,
+ t_strdup_printf("* OK [UIDVALIDITY %u] UIDs valid",
+ status.uidvalidity));
+
+ client_send_line(client,
+ t_strdup_printf("* OK [UIDNEXT %u] Predicted next UID",
+ status.uidnext));
+
+ client->nonpermanent_modseqs = status.nonpermanent_modseqs;
+ if (status.nonpermanent_modseqs) {
+ client_send_line(client,
+ "* OK [NOMODSEQ] No permanent modsequences");
+ } else if (!status.no_modseq_tracking) {
+ client_send_line(client,
+ t_strdup_printf("* OK [HIGHESTMODSEQ %"PRIu64"] Highest",
+ status.highest_modseq));
+ client->sync_last_full_modseq = status.highest_modseq;
+ }
+
+ if (ctx->qresync_uid_validity == status.uidvalidity &&
+ status.uidvalidity != 0 && !client->nonpermanent_modseqs) {
+ if ((ret = select_qresync(ctx)) < 0) {
+ client_send_box_error(ctx->cmd, ctx->box);
+ return -1;
+ }
+ } else {
+ ret = 1;
+ }
+ return ret;
+}
+
+static void close_selected_mailbox(struct client *client)
+{
+ if (client->mailbox == NULL)
+ return;
+
+ imap_client_close_mailbox(client);
+ /* CLOSED response is required by QRESYNC */
+ client_send_line(client, "* OK [CLOSED] Previous mailbox closed.");
+}
+
+bool cmd_select_full(struct client_command_context *cmd, bool readonly)
+{
+ struct client *client = cmd->client;
+ struct imap_select_context *ctx;
+ const struct imap_arg *args, *list_args;
+ const char *mailbox, *client_error;
+ int ret;
+
+ /* <mailbox> [(optional parameters)] */
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!imap_arg_get_astring(args, &mailbox)) {
+ close_selected_mailbox(client);
+ client_send_command_error(cmd, "Invalid arguments.");
+ return FALSE;
+ }
+
+ ctx = p_new(cmd->pool, struct imap_select_context, 1);
+ ctx->cmd = cmd;
+ ctx->ns = client_find_namespace_full(cmd->client, &mailbox, &client_error);
+ if (ctx->ns == NULL) {
+ /* send * OK [CLOSED] before the tagged reply */
+ close_selected_mailbox(client);
+ client_send_tagline(cmd, client_error);
+ return TRUE;
+ }
+
+ if (imap_arg_get_list(&args[1], &list_args)) {
+ if (!select_parse_options(ctx, list_args, &client_error)) {
+ select_context_free(ctx);
+ /* send * OK [CLOSED] before the tagged reply */
+ close_selected_mailbox(client);
+ client_send_command_error(ctx->cmd, client_error);
+ return TRUE;
+ }
+ }
+
+ i_assert(client->mailbox_change_lock == NULL);
+ client->mailbox_change_lock = cmd;
+
+ close_selected_mailbox(client);
+
+ if (ctx->condstore) {
+ /* Enable while no mailbox is opened to avoid sending
+ HIGHESTMODSEQ for previously opened mailbox */
+ client_enable(client, imap_feature_condstore);
+ }
+
+ ret = select_open(ctx, mailbox, readonly);
+ if (ret == 0)
+ return FALSE;
+ cmd_select_finish(ctx, ret);
+ return TRUE;
+}
+
+bool cmd_select(struct client_command_context *cmd)
+{
+ return cmd_select_full(cmd, FALSE);
+}
diff --git a/src/imap/cmd-setmetadata.c b/src/imap/cmd-setmetadata.c
new file mode 100644
index 0000000..247afce
--- /dev/null
+++ b/src/imap/cmd-setmetadata.c
@@ -0,0 +1,378 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+#include "str.h"
+#include "imap-metadata.h"
+#include "mail-storage-private.h"
+
+#define METADATA_MAX_INMEM_SIZE (1024*128)
+
+struct imap_setmetadata_context {
+ struct client_command_context *cmd;
+ struct imap_parser *parser;
+
+ struct mailbox *box;
+ struct imap_metadata_transaction *trans;
+
+ char *entry_name;
+ uoff_t entry_value_len;
+ struct istream *input;
+ bool failed;
+ bool cmd_error_sent;
+ bool storage_failure;
+};
+
+static void cmd_setmetadata_deinit(struct imap_setmetadata_context *ctx)
+{
+ o_stream_set_flush_callback(ctx->cmd->client->output,
+ client_output, ctx->cmd->client);
+
+ ctx->cmd->client->input_lock = NULL;
+ imap_parser_unref(&ctx->parser);
+ if (ctx->trans != NULL)
+ imap_metadata_transaction_rollback(&ctx->trans);
+ if (ctx->box != NULL && ctx->box != ctx->cmd->client->mailbox)
+ mailbox_free(&ctx->box);
+ i_free(ctx->entry_name);
+}
+
+static int
+cmd_setmetadata_parse_entryvalue(struct imap_setmetadata_context *ctx,
+ const char **entry_r,
+ const struct imap_arg **value_r)
+{
+ const struct imap_arg *args;
+ const char *name, *client_error;
+ enum imap_parser_error parse_error;
+ int ret;
+
+ /* parse the entry name */
+ ret = imap_parser_read_args(ctx->parser, 1,
+ IMAP_PARSE_FLAG_INSIDE_LIST, &args);
+ if (ret >= 0) {
+ if (ret == 0) {
+ /* ')' found */
+ *entry_r = NULL;
+ return 1;
+ }
+ if (!imap_arg_get_astring(args, &name)) {
+ client_send_command_error(ctx->cmd,
+ "Entry name isn't astring");
+ return -1;
+ }
+
+ ret = imap_parser_read_args(ctx->parser, 2,
+ IMAP_PARSE_FLAG_INSIDE_LIST |
+ IMAP_PARSE_FLAG_LITERAL_SIZE |
+ IMAP_PARSE_FLAG_LITERAL8, &args);
+ }
+ if (ret < 0) {
+ if (ret == -2)
+ return 0;
+ client_error = imap_parser_get_error(ctx->parser, &parse_error);
+ switch (parse_error) {
+ case IMAP_PARSE_ERROR_NONE:
+ i_unreached();
+ case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
+ client_disconnect_with_error(ctx->cmd->client,
+ client_error);
+ break;
+ default:
+ client_send_command_error(ctx->cmd, client_error);
+ break;
+ }
+ return -1;
+ }
+ if (args[1].type == IMAP_ARG_EOL) {
+ client_send_command_error(ctx->cmd, "Entry value missing");
+ return -1;
+ }
+ if (args[1].type == IMAP_ARG_LIST) {
+ client_send_command_error(ctx->cmd, "Entry value can't be a list");
+ return -1;
+ }
+ if (!ctx->cmd_error_sent &&
+ !imap_metadata_verify_entry_name(name, &client_error)) {
+ client_send_command_error(ctx->cmd, client_error);
+ ctx->cmd_error_sent = TRUE;
+ }
+ if (ctx->cmd_error_sent) {
+ ctx->cmd->param_error = FALSE;
+ ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;
+
+ ctx->failed = TRUE;
+ if (args[1].type == IMAP_ARG_LITERAL_SIZE) {
+ /* client won't see "+ OK", so we can abort
+ immediately */
+ ctx->cmd->client->input_skip_line = FALSE;
+ return -1;
+ }
+ }
+
+ /* entry names are case-insensitive. handle this by using only
+ lowercase names. */
+ *entry_r = t_str_lcase(name);
+ *value_r = &args[1];
+ return 1;
+}
+
+static int
+cmd_setmetadata_entry_read_stream(struct imap_setmetadata_context *ctx)
+{
+ const unsigned char *data;
+ size_t size;
+ struct mail_attribute_value value;
+ int ret;
+
+ while ((ret = i_stream_read_more(ctx->input, &data, &size)) > 0)
+ i_stream_skip(ctx->input, size);
+ if (ret == 0)
+ return 0;
+
+ if (ctx->input->v_offset != ctx->entry_value_len) {
+ /* client disconnected */
+ i_assert(ctx->input->eof);
+ return -1;
+ }
+
+ /* finished reading the value */
+ i_stream_seek(ctx->input, 0);
+
+ if (ctx->failed) {
+ i_stream_unref(&ctx->input);
+ return 1;
+ }
+
+ i_zero(&value);
+ value.value_stream = ctx->input;
+ if (imap_metadata_set(ctx->trans, ctx->entry_name, &value) < 0) {
+ /* delay reporting the failure so we'll finish
+ reading the command input */
+ ctx->storage_failure = TRUE;
+ ctx->failed = TRUE;
+ }
+ i_stream_unref(&ctx->input);
+ return 1;
+}
+
+static int
+cmd_setmetadata_entry(struct imap_setmetadata_context *ctx,
+ const char *entry_name,
+ const struct imap_arg *entry_value)
+{
+ struct istream *inputs[2];
+ struct mail_attribute_value value;
+ string_t *path;
+ int ret;
+
+ switch (entry_value->type) {
+ case IMAP_ARG_NIL:
+ case IMAP_ARG_ATOM:
+ case IMAP_ARG_STRING:
+ /* we have the value already */
+ if (ctx->failed)
+ return 1;
+ i_zero(&value);
+ /* NOTE: The RFC doesn't allow atoms as value, but since
+ Dovecot has traditionally supported it this is kept for
+ backwards compatibility just in case some client is
+ using it. */
+ if (entry_value->type == IMAP_ARG_NIL)
+ ;
+ else if (!imap_arg_get_atom(entry_value, &value.value))
+ value.value = imap_arg_as_nstring(entry_value);
+ ret = imap_metadata_set(ctx->trans, entry_name, &value);
+ if (ret < 0) {
+ /* delay reporting the failure so we'll finish
+ reading the command input */
+ ctx->storage_failure = TRUE;
+ ctx->failed = TRUE;
+ }
+ return 1;
+ case IMAP_ARG_LITERAL_SIZE:
+ o_stream_nsend(ctx->cmd->client->output, "+ OK\r\n", 6);
+ o_stream_uncork(ctx->cmd->client->output);
+ o_stream_cork(ctx->cmd->client->output);
+ /* fall through */
+ case IMAP_ARG_LITERAL_SIZE_NONSYNC:
+ i_free(ctx->entry_name);
+ ctx->entry_name = i_strdup(entry_name);
+ ctx->entry_value_len = imap_arg_as_literal_size(entry_value);
+
+ inputs[0] = i_stream_create_limit(ctx->cmd->client->input,
+ ctx->entry_value_len);
+ inputs[1] = NULL;
+
+ path = t_str_new(128);
+ mail_user_set_get_temp_prefix(path, ctx->cmd->client->user->set);
+ ctx->input = i_stream_create_seekable_path(inputs,
+ METADATA_MAX_INMEM_SIZE, str_c(path));
+ i_stream_set_name(ctx->input, i_stream_get_name(inputs[0]));
+ i_stream_unref(&inputs[0]);
+ return cmd_setmetadata_entry_read_stream(ctx);
+ case IMAP_ARG_LITERAL:
+ case IMAP_ARG_LIST:
+ case IMAP_ARG_EOL:
+ break;
+ }
+ i_unreached();
+}
+
+static bool cmd_setmetadata_continue(struct client_command_context *cmd)
+{
+ struct imap_setmetadata_context *ctx = cmd->context;
+ const char *entry, *client_error;
+ enum mail_error error;
+ const struct imap_arg *value;
+ int ret;
+
+ if (cmd->cancel) {
+ cmd_setmetadata_deinit(ctx);
+ return TRUE;
+ }
+
+ if (ctx->input != NULL) {
+ if ((ret = cmd_setmetadata_entry_read_stream(ctx)) == 0)
+ return FALSE;
+ if (ret < 0) {
+ cmd_setmetadata_deinit(ctx);
+ return TRUE;
+ }
+ }
+
+ while ((ret = cmd_setmetadata_parse_entryvalue(ctx, &entry, &value)) > 0 &&
+ entry != NULL) {
+ ret = ctx->failed ? 1 :
+ cmd_setmetadata_entry(ctx, entry, value);
+ imap_parser_reset(ctx->parser);
+ if (ret <= 0)
+ break;
+ }
+ if (ret == 0)
+ return 0;
+
+ if (ret < 0 || ctx->cmd_error_sent) {
+ /* already sent the error to client */ ;
+ } else if (ctx->storage_failure) {
+ if (ctx->box == NULL)
+ client_disconnect_if_inconsistent(cmd->client);
+ client_error = imap_metadata_transaction_get_last_error
+ (ctx->trans, &error);
+ client_send_tagline(cmd,
+ imap_get_error_string(cmd, client_error, error));
+ } else if (imap_metadata_transaction_commit(&ctx->trans,
+ &error, &client_error) < 0) {
+ if (ctx->box == NULL)
+ client_disconnect_if_inconsistent(cmd->client);
+ client_send_tagline(cmd,
+ imap_get_error_string(cmd, client_error, error));
+ } else {
+ client_send_tagline(cmd, "OK Setmetadata completed.");
+ }
+ cmd_setmetadata_deinit(ctx);
+ return TRUE;
+}
+
+static bool
+cmd_setmetadata_start(struct imap_setmetadata_context *ctx)
+{
+ struct client_command_context *cmd = ctx->cmd;
+ struct client *client = cmd->client;
+
+ imap_metadata_transaction_validated_only(ctx->trans,
+ !cmd->client->set->imap_metadata);
+ /* we support large literals, so read the values from client
+ asynchronously the same way as APPEND does. */
+ client->input_lock = cmd;
+ ctx->parser = imap_parser_create(client->input, client->output,
+ client->set->imap_max_line_length);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(ctx->parser);
+ o_stream_unset_flush_callback(client->output);
+
+ cmd->func = cmd_setmetadata_continue;
+ cmd->context = ctx;
+ return cmd_setmetadata_continue(cmd);
+}
+
+static bool
+cmd_setmetadata_server(struct imap_setmetadata_context *ctx)
+{
+ ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user);
+ return cmd_setmetadata_start(ctx);
+}
+
+static bool
+cmd_setmetadata_mailbox(struct imap_setmetadata_context *ctx,
+ const char *mailbox)
+{
+ struct client_command_context *cmd = ctx->cmd;
+ struct client *client = cmd->client;
+ struct mail_namespace *ns;
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ if (client->mailbox != NULL && !client->mailbox_examined &&
+ mailbox_equals(client->mailbox, ns, mailbox))
+ ctx->box = client->mailbox;
+ else {
+ ctx->box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_ATTRIBUTE_SESSION);
+ enum mailbox_existence existence;
+ if (mailbox_exists(ctx->box, TRUE, &existence) < 0) {
+ client_send_box_error(cmd, ctx->box);
+ mailbox_free(&ctx->box);
+ return TRUE;
+ } else if (existence == MAILBOX_EXISTENCE_NONE) {
+ const char *err = t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND,
+ mailbox_get_vname(ctx->box));
+ mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND, err);
+ client_send_box_error(cmd, ctx->box);
+ mailbox_free(&ctx->box);
+ return TRUE;
+ }
+ }
+ event_add_str(ctx->cmd->global_event, "mailbox",
+ mailbox_get_vname(ctx->box));
+ ctx->trans = imap_metadata_transaction_begin(ctx->box);
+ return cmd_setmetadata_start(ctx);
+}
+
+bool cmd_setmetadata(struct client_command_context *cmd)
+{
+ struct imap_setmetadata_context *ctx;
+ const struct imap_arg *args;
+ const char *mailbox;
+ int ret;
+
+ ret = imap_parser_read_args(cmd->parser, 2,
+ IMAP_PARSE_FLAG_STOP_AT_LIST, &args);
+ if (ret == -1) {
+ client_send_command_error(cmd, NULL);
+ return TRUE;
+ }
+ if (ret == -2)
+ return FALSE;
+ if (!imap_arg_get_astring(&args[0], &mailbox) ||
+ args[1].type != IMAP_ARG_LIST) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ ctx = p_new(cmd->pool, struct imap_setmetadata_context, 1);
+ ctx->cmd = cmd;
+ ctx->cmd->context = ctx;
+
+ if (mailbox[0] == '\0') {
+ /* server attribute */
+ return cmd_setmetadata_server(ctx);
+ }
+
+ return cmd_setmetadata_mailbox(ctx, mailbox);
+}
diff --git a/src/imap/cmd-sort.c b/src/imap/cmd-sort.c
new file mode 100644
index 0000000..6515a67
--- /dev/null
+++ b/src/imap/cmd-sort.c
@@ -0,0 +1,142 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "buffer.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "imap-search.h"
+
+struct sort_name {
+ enum mail_sort_type type;
+ const char *name;
+};
+
+static struct sort_name sort_names[] = {
+ { MAIL_SORT_ARRIVAL, "arrival" },
+ { MAIL_SORT_CC, "cc" },
+ { MAIL_SORT_DATE, "date" },
+ { MAIL_SORT_FROM, "from" },
+ { MAIL_SORT_SIZE, "size" },
+ { MAIL_SORT_SUBJECT, "subject" },
+ { MAIL_SORT_TO, "to" },
+ { MAIL_SORT_RELEVANCY, "x-score" }, /* FIXME: obsolete */
+ { MAIL_SORT_RELEVANCY, "relevancy" },
+ { MAIL_SORT_DISPLAYFROM, "displayfrom" },
+ { MAIL_SORT_DISPLAYTO, "displayto" },
+
+ { MAIL_SORT_END, NULL }
+};
+
+static int
+get_sort_program(struct client_command_context *cmd,
+ const struct imap_arg *args,
+ enum mail_sort_type program[MAX_SORT_PROGRAM_SIZE])
+{
+ enum mail_sort_type mask = 0;
+ const char *arg;
+ unsigned int i, pos;
+ bool reverse, last_reverse;
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ /* empty list */
+ client_send_command_error(cmd, "Empty sort program.");
+ return -1;
+ }
+
+ pos = 0; reverse = last_reverse = FALSE;
+ for (; imap_arg_get_astring(args, &arg); args++) {
+ last_reverse = strcasecmp(arg, "reverse") == 0;
+ if (last_reverse) {
+ reverse = !reverse;
+ continue;
+ }
+
+ for (i = 0; sort_names[i].type != MAIL_SORT_END; i++) {
+ if (strcasecmp(arg, sort_names[i].name) == 0)
+ break;
+ }
+
+ if (sort_names[i].type == MAIL_SORT_END) {
+ client_send_command_error(cmd, t_strconcat(
+ "Unknown sort argument: ", arg, NULL));
+ return -1;
+ }
+
+ if ((mask & sort_names[i].type) != 0)
+ continue;
+ mask |= sort_names[i].type;
+
+ /* @UNSAFE: mask check should prevent us from ever
+ overflowing */
+ i_assert(pos < MAX_SORT_PROGRAM_SIZE-1);
+ program[pos++] = sort_names[i].type |
+ (reverse ? MAIL_SORT_FLAG_REVERSE : 0);
+ reverse = FALSE;
+ }
+ if (last_reverse) {
+ client_send_command_error(cmd, "Sort list ends with REVERSE.");
+ return -1;
+ }
+ program[pos] = MAIL_SORT_END;
+
+ if (!IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd,
+ "Invalid sort list argument.");
+ return -1;
+ }
+
+ return 0;
+}
+
+bool cmd_sort(struct client_command_context *cmd)
+{
+ struct imap_search_context *ctx;
+ struct mail_search_args *sargs;
+ enum mail_sort_type sort_program[MAX_SORT_PROGRAM_SIZE];
+ const struct imap_arg *args, *list_args;
+ const char *charset;
+ int ret;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ ctx = p_new(cmd->pool, struct imap_search_context, 1);
+ ctx->cmd = cmd;
+
+ if ((ret = cmd_search_parse_return_if_found(ctx, &args)) <= 0) {
+ /* error / waiting for unambiguity */
+ return ret < 0;
+ }
+
+ /* sort program */
+ if (!imap_arg_get_list(args, &list_args)) {
+ client_send_command_error(cmd, "Invalid sort argument.");
+ imap_search_context_free(ctx);
+ return TRUE;
+ }
+
+ if (get_sort_program(cmd, list_args, sort_program) < 0) {
+ imap_search_context_free(ctx);
+ return TRUE;
+ }
+ args++;
+
+ /* charset */
+ if (!imap_arg_get_astring(args, &charset)) {
+ client_send_command_error(cmd, "Invalid charset argument.");
+ imap_search_context_free(ctx);
+ return TRUE;
+ }
+ args++;
+
+ ret = imap_search_args_build(cmd, args, charset, &sargs);
+ if (ret <= 0) {
+ imap_search_context_free(ctx);
+ return ret < 0;
+ }
+
+ return imap_search_start(ctx, sargs, sort_program);
+}
diff --git a/src/imap/cmd-status.c b/src/imap/cmd-status.c
new file mode 100644
index 0000000..69c0a51
--- /dev/null
+++ b/src/imap/cmd-status.c
@@ -0,0 +1,54 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-resp-code.h"
+#include "imap-commands.h"
+#include "imap-sync.h"
+#include "imap-status.h"
+
+bool cmd_status(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ const struct imap_arg *args, *list_args;
+ struct imap_status_items items;
+ struct imap_status_result result;
+ struct mail_namespace *ns;
+ const char *mailbox, *orig_mailbox;
+ bool selected_mailbox;
+
+ /* <mailbox> <status items> */
+ if (!client_read_args(cmd, 2, 0, &args))
+ return FALSE;
+
+ if (!imap_arg_get_astring(&args[0], &mailbox) ||
+ !imap_arg_get_list(&args[1], &list_args)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ /* get the items client wants */
+ if (imap_status_parse_items(cmd, list_args, &items) < 0)
+ return TRUE;
+
+ orig_mailbox = mailbox;
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ event_add_str(cmd->global_event, "mailbox", mailbox);
+ selected_mailbox = client->mailbox != NULL &&
+ mailbox_equals(client->mailbox, ns, mailbox);
+ if (imap_status_get(cmd, ns, mailbox, &items, &result) < 0) {
+ client_send_tagline(cmd, result.errstr);
+ return TRUE;
+ }
+
+ imap_status_send(client, orig_mailbox, &items, &result);
+ if (!selected_mailbox)
+ client_send_tagline(cmd, "OK Status completed.");
+ else {
+ client_send_tagline(cmd, "OK ["IMAP_RESP_CODE_CLIENTBUG"] "
+ "Status on selected mailbox completed.");
+ }
+ return TRUE;
+}
diff --git a/src/imap/cmd-store.c b/src/imap/cmd-store.c
new file mode 100644
index 0000000..126c042
--- /dev/null
+++ b/src/imap/cmd-store.c
@@ -0,0 +1,261 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "seq-range-array.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "imap-util.h"
+
+
+struct imap_store_context {
+ struct client_command_context *cmd;
+ uint64_t max_modseq;
+
+ enum mail_flags flags;
+ struct mail_keywords *keywords;
+
+ enum modify_type modify_type;
+ bool silent;
+};
+
+static bool
+get_modify_type(struct imap_store_context *ctx, const char *type)
+{
+ if (*type == '+') {
+ ctx->modify_type = MODIFY_ADD;
+ type++;
+ } else if (*type == '-') {
+ ctx->modify_type = MODIFY_REMOVE;
+ type++;
+ } else {
+ ctx->modify_type = MODIFY_REPLACE;
+ }
+
+ if (strncasecmp(type, "FLAGS", 5) != 0)
+ return FALSE;
+
+ ctx->silent = strcasecmp(type+5, ".SILENT") == 0;
+ if (!ctx->silent && type[5] != '\0')
+ return FALSE;
+ return TRUE;
+}
+
+static bool
+store_parse_modifiers(struct imap_store_context *ctx,
+ const struct imap_arg *args)
+{
+ const char *name, *value;
+
+ for (; !IMAP_ARG_IS_EOL(args); args += 2) {
+ if (!imap_arg_get_atom(&args[0], &name) ||
+ !imap_arg_get_atom(&args[1], &value)) {
+ client_send_command_error(ctx->cmd,
+ "Invalid STORE modifiers.");
+ return FALSE;
+ }
+
+ if (strcasecmp(name, "UNCHANGEDSINCE") == 0) {
+ if (ctx->cmd->client->nonpermanent_modseqs) {
+ client_send_command_error(ctx->cmd,
+ "STORE UNCHANGEDSINCE can't be used with non-permanent modseqs");
+ return FALSE;
+ }
+ if (str_to_uint64(value, &ctx->max_modseq) < 0) {
+ client_send_command_error(ctx->cmd,
+ "Invalid modseq");
+ return FALSE;
+ }
+ client_enable(ctx->cmd->client, imap_feature_condstore);
+ } else {
+ client_send_command_error(ctx->cmd,
+ "Unknown STORE modifier");
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+store_parse_args(struct imap_store_context *ctx, const struct imap_arg *args)
+{
+ struct client_command_context *cmd = ctx->cmd;
+ const struct imap_arg *list_args;
+ const char *type;
+ const char *const *keywords_list = NULL;
+
+ ctx->max_modseq = (uint64_t)-1;
+ if (imap_arg_get_list(args, &list_args)) {
+ if (!store_parse_modifiers(ctx, list_args))
+ return FALSE;
+ args++;
+ }
+
+ if (!imap_arg_get_astring(args, &type) ||
+ !get_modify_type(ctx, type)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return FALSE;
+ }
+ args++;
+
+ if (imap_arg_get_list(args, &list_args)) {
+ if (!client_parse_mail_flags(cmd, list_args,
+ &ctx->flags, &keywords_list))
+ return FALSE;
+ } else {
+ if (!client_parse_mail_flags(cmd, args,
+ &ctx->flags, &keywords_list))
+ return FALSE;
+ }
+
+ if (keywords_list != NULL || ctx->modify_type == MODIFY_REPLACE) {
+ if (mailbox_keywords_create(cmd->client->mailbox, keywords_list,
+ &ctx->keywords) < 0) {
+ /* invalid keywords */
+ client_send_box_error(cmd, cmd->client->mailbox);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+bool cmd_store(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ const struct imap_arg *args;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ struct imap_store_context ctx;
+ ARRAY_TYPE(seq_range) modified_set, uids;
+ enum mailbox_transaction_flags flags = 0;
+ enum imap_sync_flags imap_sync_flags = 0;
+ const char *set, *reply, *tagged_reply;
+ string_t *str;
+ int ret;
+ bool update_deletes;
+ unsigned int deleted_count;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ if (!imap_arg_get_atom(args, &set)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ ret = imap_search_get_seqset(cmd, set, cmd->uid, &search_args);
+ if (ret <= 0)
+ return ret < 0;
+
+ i_zero(&ctx);
+ ctx.cmd = cmd;
+ if (!store_parse_args(&ctx, ++args)) {
+ mail_search_args_unref(&search_args);
+ return TRUE;
+ }
+
+ if (client->mailbox_examined) {
+ mail_search_args_unref(&search_args);
+ if (ctx.max_modseq < (uint64_t)-1)
+ reply = "NO CONDSTORE failed: Mailbox is read-only.";
+ else
+ reply = "OK Store ignored with read-only mailbox.";
+ return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FAST |
+ (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES),
+ 0, reply);
+ }
+
+ if (ctx.silent)
+ flags |= MAILBOX_TRANSACTION_FLAG_HIDE;
+ if (ctx.max_modseq < (uint64_t)-1) {
+ /* update modseqs so we can check them early */
+ flags |= MAILBOX_TRANSACTION_FLAG_REFRESH;
+ }
+
+ t = mailbox_transaction_begin(client->mailbox, flags,
+ imap_client_command_get_reason(cmd));
+
+ search_ctx = mailbox_search_init(t, search_args, NULL,
+ MAIL_FETCH_FLAGS, NULL);
+ mail_search_args_unref(&search_args);
+
+ i_array_init(&modified_set, 64);
+ if (ctx.max_modseq < (uint64_t)-1) {
+ /* STORE UNCHANGEDSINCE is being used */
+ mailbox_transaction_set_max_modseq(t, ctx.max_modseq,
+ &modified_set);
+ }
+
+ update_deletes = (ctx.flags & MAIL_DELETED) != 0 &&
+ ctx.modify_type != MODIFY_REMOVE;
+ deleted_count = 0;
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (ctx.max_modseq < (uint64_t)-1) {
+ /* check early so there's less work for transaction
+ commit if something has to be cancelled */
+ if (mail_get_modseq(mail) > ctx.max_modseq) {
+ seq_range_array_add(&modified_set, mail->seq);
+ continue;
+ }
+ }
+ if (update_deletes) {
+ if ((mail_get_flags(mail) & MAIL_DELETED) == 0)
+ deleted_count++;
+ }
+ if (ctx.modify_type == MODIFY_REPLACE || ctx.flags != 0)
+ mail_update_flags(mail, ctx.modify_type, ctx.flags);
+ if (ctx.modify_type == MODIFY_REPLACE || ctx.keywords != NULL) {
+ mail_update_keywords(mail, ctx.modify_type,
+ ctx.keywords);
+ }
+ }
+
+ if (ctx.keywords != NULL)
+ mailbox_keywords_unref(&ctx.keywords);
+
+ ret = mailbox_search_deinit(&search_ctx);
+ if (ret < 0)
+ mailbox_transaction_rollback(&t);
+ else
+ ret = mailbox_transaction_commit(&t);
+ if (ret < 0) {
+ array_free(&modified_set);
+ client_send_box_error(cmd, client->mailbox);
+ return TRUE;
+ }
+ client->deleted_count += deleted_count;
+
+ if (array_count(&modified_set) == 0)
+ tagged_reply = "OK Store completed.";
+ else {
+ if (cmd->uid) {
+ i_array_init(&uids, array_count(&modified_set)*2);
+ mailbox_get_uid_range(client->mailbox, &modified_set,
+ &uids);
+ array_free(&modified_set);
+ modified_set = uids;
+ }
+ str = str_new(cmd->pool, 256);
+ str_append(str, "OK [MODIFIED ");
+ imap_write_seq_range(str, &modified_set);
+ str_append(str, "] Conditional store failed.");
+ tagged_reply = str_c(str);
+ }
+ array_free(&modified_set);
+
+ /* With UID STORE we have to return UID for the flags as well.
+ Unfortunately we don't have the ability to separate those
+ flag changes that were caused by UID STORE and those that
+ came externally, so we'll just send the UID for all flag
+ changes that we see. */
+ if (cmd->uid && (!ctx.silent ||
+ client_has_enabled(client, imap_feature_condstore)))
+ imap_sync_flags |= IMAP_SYNC_FLAG_SEND_UID;
+
+ return cmd_sync(cmd, (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES),
+ imap_sync_flags, tagged_reply);
+}
diff --git a/src/imap/cmd-subscribe.c b/src/imap/cmd-subscribe.c
new file mode 100644
index 0000000..03c2684
--- /dev/null
+++ b/src/imap/cmd-subscribe.c
@@ -0,0 +1,87 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-utf7.h"
+#include "imap-commands.h"
+#include "mail-namespace.h"
+
+static bool
+subscribe_is_valid_name(struct client_command_context *cmd, struct mailbox *box)
+{
+ enum mailbox_existence existence;
+
+ if (mailbox_exists(box, TRUE, &existence) < 0) {
+ client_send_box_error(cmd, box);
+ return FALSE;
+ }
+ if (existence == MAILBOX_EXISTENCE_NONE) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO "MAIL_ERRSTR_MAILBOX_NOT_FOUND,
+ mailbox_get_vname(box)));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool str_ends_with_char(const char *str, char c)
+{
+ size_t len = strlen(str);
+
+ return len > 0 && str[len-1] == c;
+}
+
+bool cmd_subscribe_full(struct client_command_context *cmd, bool subscribe)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box, *box2;
+ const char *mailbox, *orig_mailbox;
+ bool unsubscribed_mailbox2;
+ char sep;
+
+ /* <mailbox> */
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, mailbox, 0);
+ event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box));
+ if (subscribe) {
+ if (!subscribe_is_valid_name(cmd, box)) {
+ mailbox_free(&box);
+ return TRUE;
+ }
+ }
+
+ sep = mail_namespace_get_sep(ns);
+ unsubscribed_mailbox2 = FALSE;
+ if (!subscribe &&
+ str_ends_with_char(orig_mailbox, sep) &&
+ !str_ends_with_char(mailbox, sep)) {
+ /* try to unsubscribe both "box" and "box/" */
+ const char *name2 = t_strdup_printf("%s%c", mailbox, sep);
+ box2 = mailbox_alloc(ns->list, name2, 0);
+ if (mailbox_set_subscribed(box2, FALSE) == 0)
+ unsubscribed_mailbox2 = TRUE;
+ mailbox_free(&box2);
+ }
+
+ if (mailbox_set_subscribed(box, subscribe) < 0 &&
+ !unsubscribed_mailbox2) {
+ client_send_box_error(cmd, box);
+ } else {
+ client_send_tagline(cmd, subscribe ?
+ "OK Subscribe completed." :
+ "OK Unsubscribe completed.");
+ }
+ mailbox_free(&box);
+ return TRUE;
+}
+
+bool cmd_subscribe(struct client_command_context *cmd)
+{
+ return cmd_subscribe_full(cmd, TRUE);
+}
diff --git a/src/imap/cmd-thread.c b/src/imap/cmd-thread.c
new file mode 100644
index 0000000..64388d5
--- /dev/null
+++ b/src/imap/cmd-thread.c
@@ -0,0 +1,294 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "ostream.h"
+#include "imap-base-subject.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "mail-thread.h"
+
+static int imap_thread_write(struct mail_thread_iterate_context *iter,
+ string_t *str, bool root)
+{
+ const struct mail_thread_child_node *node;
+ struct mail_thread_iterate_context *child_iter;
+ unsigned int count;
+ int ret = 0;
+
+ count = mail_thread_iterate_count(iter);
+ if (count == 0)
+ return 0;
+
+ if (count == 1 && !root) {
+ /* only one child - special case to avoid extra parenthesis */
+ node = mail_thread_iterate_next(iter, &child_iter);
+ str_printfa(str, "%u", node->uid);
+ if (child_iter != NULL) {
+ str_append_c(str, ' ');
+ T_BEGIN {
+ ret = imap_thread_write(child_iter, str, FALSE);
+ } T_END;
+ if (mail_thread_iterate_deinit(&child_iter) < 0)
+ return -1;
+ }
+ return ret;
+ }
+
+ while ((node = mail_thread_iterate_next(iter, &child_iter)) != NULL) {
+ if (child_iter == NULL) {
+ /* no children */
+ str_printfa(str, "(%u)", node->uid);
+ } else {
+ /* node with children */
+ str_append_c(str, '(');
+ if (node->uid != 0)
+ str_printfa(str, "%u ", node->uid);
+ T_BEGIN {
+ ret = imap_thread_write(child_iter, str, FALSE);
+ } T_END;
+ if (mail_thread_iterate_deinit(&child_iter) < 0 ||
+ ret < 0)
+ return -1;
+ str_append_c(str, ')');
+ }
+ }
+ return 0;
+}
+
+static int
+imap_thread_write_reply(struct mail_thread_context *ctx, string_t *str,
+ enum mail_thread_type thread_type, bool write_seqs)
+{
+ struct mail_thread_iterate_context *iter;
+ int ret;
+
+ iter = mail_thread_iterate_init(ctx, thread_type, write_seqs);
+ str_append(str, "* THREAD ");
+ T_BEGIN {
+ ret = imap_thread_write(iter, str, TRUE);
+ } T_END;
+ if (mail_thread_iterate_deinit(&iter) < 0)
+ ret = -1;
+
+ str_append(str, "\r\n");
+ return ret;
+}
+
+static int imap_thread(struct client_command_context *cmd,
+ struct mail_search_args *search_args,
+ enum mail_thread_type thread_type)
+{
+ struct mail_thread_context *ctx;
+ string_t *str;
+ int ret;
+
+ i_assert(thread_type == MAIL_THREAD_REFERENCES ||
+ thread_type == MAIL_THREAD_REFS);
+
+ str = str_new(default_pool, 1024);
+ ret = mail_thread_init(cmd->client->mailbox,
+ search_args, &ctx);
+ if (ret == 0) {
+ ret = imap_thread_write_reply(ctx, str, thread_type,
+ !cmd->uid);
+ mail_thread_deinit(&ctx);
+ }
+
+ if (ret == 0)
+ o_stream_nsend(cmd->client->output, str_data(str), str_len(str));
+ str_free(&str);
+ return ret;
+}
+
+struct orderedsubject_thread {
+ time_t timestamp;
+ ARRAY_TYPE(uint32_t) msgs;
+};
+
+static int orderedsubject_thread_cmp(const struct orderedsubject_thread *t1,
+ const struct orderedsubject_thread *t2)
+{
+ const uint32_t *m1, *m2;
+
+ if (t1->timestamp < t2->timestamp)
+ return -1;
+ if (t1->timestamp > t2->timestamp)
+ return 1;
+
+ m1 = array_front(&t1->msgs);
+ m2 = array_front(&t2->msgs);
+ if (*m1 < *m2)
+ return -1;
+ if (*m1 > *m2)
+ return 1;
+ i_unreached();
+}
+
+static void
+imap_orderedsubject_thread_write(struct ostream *output, string_t *reply,
+ const struct orderedsubject_thread *thread)
+{
+ const uint32_t *msgs;
+ unsigned int i, count;
+
+ if (str_len(reply) > 128-10) {
+ o_stream_nsend(output, str_data(reply), str_len(reply));
+ str_truncate(reply, 0);
+ }
+
+ msgs = array_get(&thread->msgs, &count);
+ switch (count) {
+ case 1:
+ str_printfa(reply, "(%u)", msgs[0]);
+ break;
+ case 2:
+ str_printfa(reply, "(%u %u)", msgs[0], msgs[1]);
+ break;
+ default:
+ /* (1 (2)(3)) */
+ str_printfa(reply, "(%u ", msgs[0]);
+ for (i = 1; i < count; i++) {
+ if (str_len(reply) > 128-10) {
+ o_stream_nsend(output, str_data(reply),
+ str_len(reply));
+ str_truncate(reply, 0);
+ }
+ str_printfa(reply, "(%u)", msgs[i]);
+ }
+ str_append_c(reply, ')');
+ }
+}
+
+static int imap_thread_orderedsubject(struct client_command_context *cmd,
+ struct mail_search_args *search_args)
+{
+ static const enum mail_sort_type sort_program[] = {
+ MAIL_SORT_SUBJECT,
+ MAIL_SORT_DATE,
+ 0
+ };
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ string_t *prev_subject, *reply;
+ const char *subject, *base_subject;
+ pool_t pool;
+ ARRAY(struct orderedsubject_thread) threads;
+ const struct orderedsubject_thread *thread;
+ struct orderedsubject_thread *cur_thread = NULL;
+ uint32_t num;
+ bool reply_or_fw;
+ int ret, tz;
+
+ prev_subject = str_new(default_pool, 128);
+
+ /* first read all of the threads into memory */
+ pool = pool_alloconly_create("orderedsubject thread", 1024);
+ i_array_init(&threads, 128);
+ trans = mailbox_transaction_begin(cmd->client->mailbox, 0,
+ imap_client_command_get_reason(cmd));
+ search_ctx = mailbox_search_init(trans, search_args, sort_program,
+ 0, NULL);
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (mail_get_first_header(mail, "Subject", &subject) <= 0)
+ subject = "";
+ T_BEGIN {
+ base_subject = imap_get_base_subject_cased(
+ pool_datastack_create(), subject,
+ &reply_or_fw);
+ if (strcmp(str_c(prev_subject), base_subject) != 0) {
+ /* thread changed */
+ cur_thread = NULL;
+ }
+ str_truncate(prev_subject, 0);
+ str_append(prev_subject, base_subject);
+ } T_END;
+
+ if (cur_thread == NULL) {
+ /* starting a new thread. get the first message's
+ date */
+ cur_thread = array_append_space(&threads);
+ if (mail_get_date(mail, &cur_thread->timestamp,
+ &tz) == 0 &&
+ cur_thread->timestamp == 0) {
+ (void)mail_get_received_date(mail,
+ &cur_thread->timestamp);
+ }
+ p_array_init(&cur_thread->msgs, pool, 4);
+ }
+ num = cmd->uid ? mail->uid : mail->seq;
+ array_push_back(&cur_thread->msgs, &num);
+ }
+ str_free(&prev_subject);
+ ret = mailbox_search_deinit(&search_ctx);
+ (void)mailbox_transaction_commit(&trans);
+ if (ret < 0) {
+ array_free(&threads);
+ pool_unref(&pool);
+ return -1;
+ }
+
+ /* sort the threads by their first message's timestamp */
+ array_sort(&threads, orderedsubject_thread_cmp);
+
+ /* write the threads to client */
+ reply = t_str_new(128);
+ str_append(reply, "* THREAD ");
+ array_foreach(&threads, thread) {
+ imap_orderedsubject_thread_write(cmd->client->output,
+ reply, thread);
+ }
+ str_append(reply, "\r\n");
+ o_stream_nsend(cmd->client->output, str_data(reply), str_len(reply));
+
+ array_free(&threads);
+ pool_unref(&pool);
+ return 0;
+}
+
+bool cmd_thread(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ enum mail_thread_type thread_type;
+ struct mail_search_args *sargs;
+ const struct imap_arg *args;
+ const char *charset, *str;
+ int ret;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ if (!imap_arg_get_astring(&args[0], &str) ||
+ !imap_arg_get_astring(&args[1], &charset)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ args += 2;
+
+ if (!mail_thread_type_parse(str, &thread_type)) {
+ client_send_command_error(cmd, "Unknown thread algorithm.");
+ return TRUE;
+ }
+
+ ret = imap_search_args_build(cmd, args, charset, &sargs);
+ if (ret <= 0)
+ return ret < 0;
+
+ if (thread_type != MAIL_THREAD_ORDEREDSUBJECT)
+ ret = imap_thread(cmd, sargs, thread_type);
+ else
+ ret = imap_thread_orderedsubject(cmd, sargs);
+ mail_search_args_unref(&sargs);
+ if (ret < 0) {
+ client_send_box_error(cmd, client->mailbox);
+ return TRUE;
+ }
+
+ return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FAST |
+ (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES),
+ 0, "OK Thread completed.");
+}
diff --git a/src/imap/cmd-unselect.c b/src/imap/cmd-unselect.c
new file mode 100644
index 0000000..7f9f547
--- /dev/null
+++ b/src/imap/cmd-unselect.c
@@ -0,0 +1,18 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_unselect(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+
+ if (!client_verify_open_mailbox(cmd))
+ return TRUE;
+
+ i_assert(client->mailbox_change_lock == NULL);
+
+ imap_client_close_mailbox(client);
+ client_send_tagline(cmd, "OK Unselect completed.");
+ return TRUE;
+}
diff --git a/src/imap/cmd-unsubscribe.c b/src/imap/cmd-unsubscribe.c
new file mode 100644
index 0000000..1d23624
--- /dev/null
+++ b/src/imap/cmd-unsubscribe.c
@@ -0,0 +1,9 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_unsubscribe(struct client_command_context *cmd)
+{
+ return cmd_subscribe_full(cmd, FALSE);
+}
diff --git a/src/imap/cmd-urlfetch.c b/src/imap/cmd-urlfetch.c
new file mode 100644
index 0000000..6ea5ad5
--- /dev/null
+++ b/src/imap/cmd-urlfetch.c
@@ -0,0 +1,410 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "strfuncs.h"
+#include "str.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "istream-sized.h"
+#include "ostream.h"
+#include "imap-url.h"
+#include "imap-quote.h"
+#include "imap-common.h"
+#include "imap-commands.h"
+#include "imap-urlauth.h"
+#include "imap-urlauth-fetch.h"
+
+struct cmd_urlfetch_context {
+ struct imap_urlauth_fetch *ufetch;
+ struct istream *input;
+
+ bool failed:1;
+ bool finished:1;
+ bool extended:1;
+ bool in_io_handler:1;
+};
+
+struct cmd_urlfetch_url {
+ const char *url;
+
+ enum imap_urlauth_fetch_flags flags;
+};
+
+static void cmd_urlfetch_finish(struct client_command_context *cmd)
+{
+ struct cmd_urlfetch_context *ctx =
+ (struct cmd_urlfetch_context *)cmd->context;
+
+ if (ctx->finished)
+ return;
+ ctx->finished = TRUE;
+
+ i_stream_unref(&ctx->input);
+ if (ctx->ufetch != NULL)
+ imap_urlauth_fetch_deinit(&ctx->ufetch);
+
+ if (ctx->failed) {
+ if (cmd->client->output_cmd_lock == cmd) {
+ /* failed in the middle of a literal.
+ we need to disconnect. */
+ cmd->client->output_cmd_lock = NULL;
+ client_disconnect(cmd->client, "URLFETCH failed");
+ } else {
+ client_send_internal_error(cmd);
+ }
+ return;
+ }
+
+ client_send_tagline(cmd, "OK URLFETCH completed.");
+}
+
+static bool cmd_urlfetch_cancel(struct client_command_context *cmd)
+{
+ struct cmd_urlfetch_context *ctx =
+ (struct cmd_urlfetch_context *)cmd->context;
+
+ if (!cmd->cancel)
+ return FALSE;
+
+ if (ctx->ufetch != NULL) {
+ e_debug(cmd->client->event,
+ "URLFETCH: Canceling command; "
+ "aborting URLAUTH fetch requests prematurely");
+ imap_urlauth_fetch_deinit(&ctx->ufetch);
+ }
+ return TRUE;
+}
+
+static int cmd_urlfetch_transfer_literal(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_urlfetch_context *ctx =
+ (struct cmd_urlfetch_context *)cmd->context;
+ enum ostream_send_istream_result res;
+ int ret;
+
+ /* are we in the middle of an urlfetch literal? */
+ if (ctx->input == NULL)
+ return 1;
+
+ /* flush output to client if buffer is filled above optimum */
+ if (o_stream_get_buffer_used_size(client->output) >=
+ CLIENT_OUTPUT_OPTIMAL_SIZE) {
+ if ((ret = o_stream_flush(client->output)) <= 0)
+ return ret;
+ }
+
+ /* transfer literal to client */
+ o_stream_set_max_buffer_size(client->output, 0);
+ res = o_stream_send_istream(client->output, ctx->input);
+ o_stream_set_max_buffer_size(client->output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ i_stream_unref(&ctx->input);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ e_error(client->event, "read(%s) failed: %s (URLFETCH)",
+ i_stream_get_name(ctx->input),
+ i_stream_get_error(ctx->input));
+ client_disconnect(client, "URLFETCH failed");
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* client disconnected */
+ return -1;
+ }
+ i_unreached();
+}
+
+static bool cmd_urlfetch_continue(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_urlfetch_context *ctx =
+ (struct cmd_urlfetch_context *)cmd->context;
+ bool urls_pending;
+ int ret = 1;
+
+ if (cmd->cancel)
+ return cmd_urlfetch_cancel(cmd);
+
+ i_assert(client->output_cmd_lock == NULL ||
+ client->output_cmd_lock == cmd);
+
+ /* finish a pending literal transfer */
+ ret = cmd_urlfetch_transfer_literal(cmd);
+ if (ret < 0) {
+ ctx->failed = TRUE;
+ cmd_urlfetch_finish(cmd);
+ return TRUE;
+ }
+ if (ret == 0) {
+ /* not finished; apparently output blocked again */
+ return FALSE;
+ }
+
+ if (ctx->extended)
+ client_send_line(client, ")");
+ else
+ client_send_line(client, "");
+ client->output_cmd_lock = NULL;
+
+ ctx->in_io_handler = TRUE;
+ urls_pending = imap_urlauth_fetch_continue(ctx->ufetch);
+ ctx->in_io_handler = FALSE;
+
+ if (urls_pending) {
+ /* waiting for imap urlauth service */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
+ cmd->func = cmd_urlfetch_cancel;
+
+ /* retrieve next url */
+ return FALSE;
+ }
+
+ /* finished */
+ cmd_urlfetch_finish(cmd);
+ return TRUE;
+}
+
+static int cmd_urlfetch_url_success(struct client_command_context *cmd,
+ struct imap_urlauth_fetch_reply *reply)
+{
+ struct cmd_urlfetch_context *ctx = cmd->context;
+ string_t *response = t_str_new(256);
+ int ret;
+
+ str_append(response, "* URLFETCH ");
+ imap_append_astring(response, reply->url);
+
+ if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_EXTENDED) == 0) {
+ /* simple */
+ ctx->extended = FALSE;
+
+ str_printfa(response, " {%"PRIuUOFF_T"}", reply->size);
+ client_send_line(cmd->client, str_c(response));
+ i_assert(reply->size == 0 || reply->input != NULL);
+ } else {
+ bool metadata = FALSE;
+
+ /* extended */
+ ctx->extended = TRUE;
+
+ str_append(response, " (");
+ if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0 &&
+ reply->bodypartstruct != NULL) {
+ str_append(response, "BODYPARTSTRUCTURE (");
+ str_append(response, reply->bodypartstruct);
+ str_append_c(response, ')');
+ metadata = TRUE;
+ }
+ if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 ||
+ (reply->flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) {
+ if (metadata)
+ str_append_c(response, ' ');
+ if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0) {
+ str_append(response, "BODY ");
+ } else {
+ str_append(response, "BINARY ");
+ if (reply->binary_has_nuls)
+ str_append_c(response, '~');
+ }
+ str_printfa(response, "{%"PRIuUOFF_T"}", reply->size);
+ i_assert(reply->size == 0 || reply->input != NULL);
+ } else {
+ str_append_c(response, ')');
+ ctx->extended = FALSE;
+ }
+
+ client_send_line(cmd->client, str_c(response));
+ }
+
+ if (reply->input != NULL) {
+ ctx->input = i_stream_create_sized(reply->input, reply->size);
+
+ ret = cmd_urlfetch_transfer_literal(cmd);
+ if (ret < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ if (ret == 0) {
+ /* not finished; apparently output blocked */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ cmd->func = cmd_urlfetch_continue;
+ cmd->client->output_cmd_lock = cmd;
+ return 0;
+ }
+
+ if (ctx->extended)
+ client_send_line(cmd->client, ")");
+ else
+ client_send_line(cmd->client, "");
+ }
+ return 1;
+}
+
+static int
+cmd_urlfetch_url_callback(struct imap_urlauth_fetch_reply *reply,
+ bool last, void *context)
+{
+ struct client_command_context *cmd = context;
+ struct client *client = cmd->client;
+ struct cmd_urlfetch_context *ctx = cmd->context;
+ bool in_io_handler = ctx->in_io_handler;
+ int ret;
+
+ if (!in_io_handler)
+ o_stream_cork(client->output);
+ if (reply == NULL) {
+ /* fatal failure */
+ ctx->failed = TRUE;
+ ret = -1;
+ } else if (reply->succeeded) {
+ /* URL fetch succeeded */
+ ret = cmd_urlfetch_url_success(cmd, reply);
+ } else {
+ /* URL fetch failed */
+ string_t *response = t_str_new(128);
+
+ str_append(response, "* URLFETCH ");
+ imap_append_astring(response, reply->url);
+ str_append(response, " NIL");
+ client_send_line(client, str_c(response));
+ if (reply->error != NULL) {
+ client_send_line(client, t_strdup_printf(
+ "* NO %s.", reply->error));
+ }
+ ret = 1;
+ }
+
+ if ((last && cmd->state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL) ||
+ ret < 0) {
+ cmd_urlfetch_finish(cmd);
+ client_command_free(&cmd);
+ }
+ if (!in_io_handler)
+ o_stream_uncork(client->output);
+ return ret;
+}
+
+static int
+cmd_urlfetch_parse_arg(struct client_command_context *cmd,
+ const struct imap_arg *arg,
+ struct cmd_urlfetch_url *ufurl_r)
+{
+ enum imap_urlauth_fetch_flags url_flags = 0;
+ const struct imap_arg *params;
+ const char *url_text;
+
+ if (imap_arg_get_list(arg, &params))
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_EXTENDED;
+ else
+ params = arg;
+
+ /* read url */
+ if (!imap_arg_get_astring(params++, &url_text)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return -1;
+ }
+ ufurl_r->url = t_strdup(url_text);
+ if (url_flags == 0)
+ return 0;
+
+ while (!IMAP_ARG_IS_EOL(params)) {
+ const char *fetch_param;
+
+ if (!imap_arg_get_atom(params++, &fetch_param)) {
+ client_send_command_error(cmd,
+ "Invalid URL fetch parameter.");
+ return -1;
+ }
+
+ if (strcasecmp(fetch_param, "BODY") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY;
+ else if (strcasecmp(fetch_param, "BINARY") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BINARY;
+ else if (strcasecmp(fetch_param, "BODYPARTSTRUCTURE") == 0)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE;
+ else {
+ client_send_command_error(cmd,
+ t_strdup_printf("Unknown URL fetch parameter: %s",
+ fetch_param));
+ return -1;
+ }
+ }
+
+ if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 &&
+ (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) {
+ client_send_command_error(cmd,
+ "URL cannot have both BODY and BINARY fetch parameters.");
+ return -1;
+ }
+
+ if (url_flags == IMAP_URLAUTH_FETCH_FLAG_EXTENDED)
+ url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY;
+ ufurl_r->flags = url_flags;
+ return 0;
+}
+
+bool cmd_urlfetch(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct cmd_urlfetch_context *ctx;
+ ARRAY(struct cmd_urlfetch_url) urls;
+ const struct cmd_urlfetch_url *url;
+ const struct imap_arg *args;
+ struct cmd_urlfetch_url *ufurl;
+
+ if (client->urlauth_ctx == NULL) {
+ client_send_command_error(cmd, "URLAUTH disabled.");
+ return TRUE;
+ }
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ t_array_init(&urls, 32);
+
+ /* parse url arguments and group them per userid */
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ ufurl = array_append_space(&urls);
+ if (cmd_urlfetch_parse_arg(cmd, args, ufurl) < 0)
+ return TRUE;
+ }
+ cmd->context = ctx = p_new(cmd->pool, struct cmd_urlfetch_context, 1);
+ cmd->func = cmd_urlfetch_cancel;
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;
+
+ ctx->ufetch = imap_urlauth_fetch_init(client->urlauth_ctx,
+ cmd_urlfetch_url_callback, cmd);
+
+ ctx->in_io_handler = TRUE;
+ array_foreach(&urls, url) {
+ if (imap_urlauth_fetch_url(ctx->ufetch, url->url, url->flags) < 0) {
+ /* fatal error */
+ ctx->failed = TRUE;
+ break;
+ }
+ }
+ ctx->in_io_handler = FALSE;
+
+ if ((ctx->failed || !imap_urlauth_fetch_is_pending(ctx->ufetch))
+ && cmd->client->output_cmd_lock != cmd) {
+ /* finished */
+ cmd_urlfetch_finish(cmd);
+ return TRUE;
+ }
+
+ if (cmd->client->output_cmd_lock != cmd)
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
+ return FALSE;
+}
diff --git a/src/imap/cmd-x-cancel.c b/src/imap/cmd-x-cancel.c
new file mode 100644
index 0000000..3f49451
--- /dev/null
+++ b/src/imap/cmd-x-cancel.c
@@ -0,0 +1,28 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-commands.h"
+
+bool cmd_x_cancel(struct client_command_context *cmd)
+{
+ struct client_command_context *cancel_cmd;
+ const char *tag;
+
+ /* <tag> */
+ if (!client_read_string_args(cmd, 1, &tag))
+ return FALSE;
+
+ cancel_cmd = cmd->client->command_queue;
+ for (; cancel_cmd != NULL; cancel_cmd = cancel_cmd->next) {
+ if (cancel_cmd->tag != NULL && cancel_cmd != cmd &&
+ strcmp(cancel_cmd->tag, tag) == 0) {
+ client_command_cancel(&cancel_cmd);
+ client_send_tagline(cmd, "OK Command cancelled.");
+ return TRUE;
+ }
+ }
+
+ client_send_tagline(cmd, "NO Command tag not found.");
+ return TRUE;
+}
+
diff --git a/src/imap/cmd-x-state.c b/src/imap/cmd-x-state.c
new file mode 100644
index 0000000..8809998
--- /dev/null
+++ b/src/imap/cmd-x-state.c
@@ -0,0 +1,68 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "base64.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "imap-state.h"
+
+bool cmd_x_state(struct client_command_context *cmd)
+{
+ /* FIXME: state importing can cause unnecessarily large memory usage
+ by specifying an old modseq, because the EXPUNGE/FETCH replies
+ aren't currently sent asynchronously. so this command is disabled
+ for now. */
+#if 0
+ const struct imap_arg *args;
+ const char *str, *error;
+ buffer_t *state, *state_encoded;
+ int ret;
+
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ state = buffer_create_dynamic(cmd->pool, 256);
+ if (imap_arg_get_astring(&args[0], &str)) {
+ if (cmd->client->mailbox != NULL) {
+ client_send_tagline(cmd,
+ "BAD Can't be used in SELECTED state");
+ return TRUE;
+ }
+ if (base64_decode(str, strlen(str), NULL, state) < 0)
+ ret = 0;
+ else {
+ ret = imap_state_import_external(cmd->client,
+ state->data, state->used, &error);
+ }
+ if (ret < 0) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO Failed to restore state: %s", error));
+ } else if (ret == 0) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "BAD Broken state: %s", error));
+ } else {
+ client_send_tagline(cmd, "OK State imported.");
+ }
+ return TRUE;
+ } else if (args[0].type == IMAP_ARG_EOL) {
+ if (!imap_state_export_external(cmd->client, state, &error)) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO Can't save state: %s", error));
+ return TRUE;
+ }
+ state_encoded = buffer_create_dynamic(cmd->pool,
+ MAX_BASE64_ENCODED_SIZE(state->used)+10);
+ str_append(state_encoded, "* STATE ");
+ base64_encode(state->data, state->used, state_encoded);
+ client_send_line(cmd->client, str_c(state_encoded));
+ client_send_tagline(cmd, "OK State exported.");
+ return TRUE;
+ } else {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+#else
+ client_send_command_error(cmd, "Command is disabled for now.");
+ return TRUE;
+#endif
+}
diff --git a/src/imap/imap-client-hibernate.c b/src/imap/imap-client-hibernate.c
new file mode 100644
index 0000000..b29f500
--- /dev/null
+++ b/src/imap/imap-client-hibernate.c
@@ -0,0 +1,294 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "fdpass.h"
+#include "net.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mailbox-watch.h"
+#include "imap-state.h"
+#include "imap-client.h"
+
+#include <sys/stat.h>
+
+#define IMAP_HIBERNATE_SOCKET_NAME "imap-hibernate"
+#define IMAP_HIBERNATE_SEND_TIMEOUT_SECS 10
+#define IMAP_HIBERNATE_HANDSHAKE "VERSION\timap-hibernate\t1\t0\n"
+
+static int
+imap_hibernate_handshake(int fd, const char *path, const char **error_r)
+{
+ char buf[1024];
+ ssize_t ret;
+
+ if (write_full(fd, IMAP_HIBERNATE_HANDSHAKE,
+ strlen(IMAP_HIBERNATE_HANDSHAKE)) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %m", path);
+ return -1;
+ } else if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %m", path);
+ return -1;
+ } else if (ret > 0 && buf[ret-1] == '\n') {
+ buf[ret-1] = '\0';
+ if (version_string_verify(buf, "imap-hibernate", 1))
+ return 0;
+ } else {
+ buf[ret] = '\0';
+ }
+ *error_r = t_strdup_printf("%s sent invalid VERSION handshake: %s",
+ path, buf);
+ return -1;
+}
+
+static void imap_hibernate_write_cmd(struct client *client, string_t *cmd,
+ const buffer_t *state, int fd_notify)
+{
+ struct mail_user *user = client->user;
+ struct stat peer_st;
+ const char *tag;
+
+ tag = client->command_queue == NULL ? NULL : client->command_queue->tag;
+
+ str_append_tabescaped(cmd, user->username);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, user->set->mail_log_prefix);
+ str_printfa(cmd, "\tidle_notify_interval=%u",
+ client->set->imap_idle_notify_interval);
+ if (fstat(client->fd_in, &peer_st) == 0) {
+ str_printfa(cmd, "\tpeer_dev_major=%lu\tpeer_dev_minor=%lu\tpeer_ino=%llu",
+ (unsigned long)major(peer_st.st_dev),
+ (unsigned long)minor(peer_st.st_dev),
+ (unsigned long long)peer_st.st_ino);
+ }
+
+ str_append(cmd, "\tsession=");
+ str_append_tabescaped(cmd, user->session_id);
+ if (user->session_create_time != 0) {
+ str_printfa(cmd, "\tsession_created=%s",
+ dec2str(user->session_create_time));
+ }
+ if (user->conn.local_ip != NULL)
+ str_printfa(cmd, "\tlip=%s", net_ip2addr(user->conn.local_ip));
+ if (user->conn.local_port != 0)
+ str_printfa(cmd, "\tlport=%u", user->conn.local_port);
+ if (user->conn.remote_ip != NULL)
+ str_printfa(cmd, "\trip=%s", net_ip2addr(user->conn.remote_ip));
+ if (user->conn.remote_port != 0)
+ str_printfa(cmd, "\trport=%u", user->conn.remote_port);
+ if (client->userdb_fields != NULL) {
+ string_t *userdb_fields = t_str_new(256);
+ unsigned int i;
+
+ for (i = 0; client->userdb_fields[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(userdb_fields, '\t');
+ str_append_tabescaped(userdb_fields, client->userdb_fields[i]);
+ }
+ str_append(cmd, "\tuserdb_fields=");
+ str_append_tabescaped(cmd, str_c(userdb_fields));
+ }
+ if (user->uid != (uid_t)-1)
+ str_printfa(cmd, "\tuid=%s", dec2str(user->uid));
+ if (user->gid != (gid_t)-1)
+ str_printfa(cmd, "\tgid=%s", dec2str(user->gid));
+ if (client->mailbox != NULL) {
+ str_append(cmd, "\tmailbox=");
+ str_append_tabescaped(cmd, mailbox_get_vname(client->mailbox));
+ }
+ if (tag != NULL) {
+ str_append(cmd, "\ttag=");
+ str_append_tabescaped(cmd, tag);
+ }
+ str_append(cmd, "\tstats=");
+ str_append_tabescaped(cmd, client_stats(client));
+ if (client->command_queue != NULL &&
+ strcasecmp(client->command_queue->name, "IDLE") == 0)
+ str_append(cmd, "\tidle-cmd");
+ if (fd_notify != -1)
+ str_append(cmd, "\tnotify_fd");
+ str_append(cmd, "\tstate=");
+ base64_encode(state->data, state->used, cmd);
+ str_append_c(cmd, '\n');
+}
+
+static int
+imap_hibernate_process_send_cmd(int fd_socket, const char *path,
+ const string_t *cmd, int fd_client,
+ const char **error_r)
+{
+ ssize_t ret;
+
+ i_assert(fd_socket != -1);
+ i_assert(str_len(cmd) > 1);
+
+ if (imap_hibernate_handshake(fd_socket, path, error_r) < 0)
+ return -1;
+ if ((ret = fd_send(fd_socket, fd_client, str_data(cmd), 1)) < 0) {
+ *error_r = t_strdup_printf("fd_send(%s) failed: %m", path);
+ return -1;
+ }
+ i_assert(ret == 1);
+ if (write_full(fd_socket, str_data(cmd)+1, str_len(cmd)-1) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imap_hibernate_process_read(int fd, const char *path, const char **error_r)
+{
+ char buf[1024];
+ ssize_t ret;
+
+ if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %m", path);
+ return -1;
+ } else if (ret == 0) {
+ *error_r = t_strdup_printf("%s disconnected", path);
+ return -1;
+ } else if (buf[0] != '+') {
+ buf[ret] = '\0';
+ *error_r = t_strdup_printf("%s returned failure: %s", path,
+ ret > 0 && buf[0] == '-' ? buf+1 : buf);
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+static int
+imap_hibernate_process_send(struct client *client, const buffer_t *state,
+ int fd_notify, int *fd_r, const char **error_r)
+{
+ string_t *cmd = t_str_new(512);
+ const char *path;
+ ssize_t ret = 0;
+ int fd;
+
+ i_assert(state->used > 0);
+
+ *fd_r = -1;
+
+ path = t_strconcat(client->user->set->base_dir,
+ "/"IMAP_HIBERNATE_SOCKET_NAME, NULL);
+ fd = net_connect_unix_with_retries(path, 1000);
+ if (fd == -1) {
+ *error_r = t_strdup_printf(
+ "net_connect_unix(%s) failed: %m", path);
+ return -1;
+ }
+ net_set_nonblock(fd, FALSE);
+
+ imap_hibernate_write_cmd(client, cmd, state, fd_notify);
+
+ alarm(IMAP_HIBERNATE_SEND_TIMEOUT_SECS);
+ if (imap_hibernate_process_send_cmd(fd, path, cmd, client->fd_in, error_r) < 0 ||
+ imap_hibernate_process_read(fd, path, error_r) < 0)
+ ret = -1;
+ else if (fd_notify != -1) {
+ if ((ret = fd_send(fd, fd_notify, "\n", 1)) < 0)
+ *error_r = t_strdup_printf("fd_send(%s) failed: %m", path);
+ else
+ ret = imap_hibernate_process_read(fd, path, error_r);
+ }
+ alarm(0);
+ if (ret < 0) {
+ net_disconnect(fd);
+ return -1;
+ }
+ *fd_r = fd;
+ return 0;
+}
+
+bool imap_client_hibernate(struct client **_client, const char **reason_r)
+{
+ struct client *client = *_client;
+ buffer_t *state;
+ const char *error;
+ int ret, fd_notify = -1, fd_hibernate = -1;
+
+ *reason_r = NULL;
+
+ if (client->fd_in != client->fd_out) {
+ /* we won't try to hibernate stdio clients */
+ *reason_r = "stdio clients can't be hibernated";
+ return FALSE;
+ }
+ if (o_stream_get_buffer_used_size(client->output) > 0) {
+ /* wait until we've sent the pending output to client */
+ *reason_r = "output pending to client";
+ return FALSE;
+ }
+
+ struct event_passthrough *e =
+ event_create_passthrough(client->event)->
+ set_name("imap_client_hibernated");
+ if (client->mailbox != NULL)
+ e->add_str("mailbox", mailbox_get_vname(client->mailbox));
+
+ state = buffer_create_dynamic(default_pool, 1024);
+ ret = imap_state_export_internal(client, state, &error);
+ if (ret < 0) {
+ e->add_str("error", error);
+ e_error(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't export state: %s (mailbox=%s)", error,
+ client->mailbox == NULL ? "" :
+ mailbox_get_vname(client->mailbox));
+ *reason_r = error;
+ } else if (ret == 0) {
+ e->add_str("error", error);
+ e_debug(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't export state: %s (mailbox=%s)", error,
+ client->mailbox == NULL ? "" :
+ mailbox_get_vname(client->mailbox));
+ *reason_r = error;
+ }
+ if (ret > 0 && client->mailbox != NULL) {
+ fd_notify = mailbox_watch_extract_notify_fd(client->mailbox,
+ &error);
+ if (fd_notify == -1) {
+ e->add_str("error", error);
+ e_debug(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't extract notifications fd: %s",
+ error);
+ *reason_r = error;
+ ret = -1;
+ }
+ }
+ if (ret > 0) {
+ if (imap_hibernate_process_send(client, state, fd_notify,
+ &fd_hibernate, &error) < 0) {
+ e->add_str("error", error);
+ e_error(e->event(),
+ "Couldn't hibernate imap client: %s", error);
+ *reason_r = error;
+ ret = -1;
+ }
+ }
+ i_close_fd(&fd_notify);
+ if (ret > 0) {
+ /* hide the disconnect log message, because the client didn't
+ actually log out */
+ e_debug(e->event(),
+ "Successfully hibernated imap client in mailbox %s",
+ client->mailbox == NULL ? "<none>" :
+ mailbox_get_vname(client->mailbox));
+ client->disconnected = TRUE;
+ client->hibernated = TRUE;
+ client_destroy(client, NULL);
+ *_client = NULL;
+ }
+ /* notify imap-hibernate that we're done by closing the connection.
+ do this only after client is destroyed. this way imap-hibernate
+ won't try to launch another imap process too early and cause
+ problems (like sending duplicate session ID to stats process) */
+ if (fd_hibernate != -1)
+ net_disconnect(fd_hibernate);
+ buffer_free(&state);
+ return ret > 0;
+}
diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c
new file mode 100644
index 0000000..f3c1643
--- /dev/null
+++ b/src/imap/imap-client.c
@@ -0,0 +1,1681 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ioloop.h"
+#include "llist.h"
+#include "str.h"
+#include "hostpid.h"
+#include "net.h"
+#include "iostream.h"
+#include "iostream-rawlog.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "master-service.h"
+#include "imap-resp-code.h"
+#include "imap-util.h"
+#include "imap-urlauth.h"
+#include "mail-error.h"
+#include "mail-namespace.h"
+#include "mail-storage-service.h"
+#include "mail-autoexpunge.h"
+#include "imap-state.h"
+#include "imap-search.h"
+#include "imap-notify.h"
+#include "imap-commands.h"
+#include "imap-feature.h"
+
+#include <unistd.h>
+
+/* If the last command took longer than this to run, log statistics on
+ where the time was spent. */
+#define IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS 1000
+
+extern struct mail_storage_callbacks mail_storage_callbacks;
+extern struct imap_client_vfuncs imap_client_vfuncs;
+
+struct imap_module_register imap_module_register = { 0 };
+
+struct client *imap_clients = NULL;
+unsigned int imap_client_count = 0;
+
+unsigned int imap_feature_condstore = UINT_MAX;
+unsigned int imap_feature_qresync = UINT_MAX;
+
+static const char *client_command_state_names[CLIENT_COMMAND_STATE_DONE+1] = {
+ "wait-input",
+ "wait-output",
+ "wait-external",
+ "wait-unambiguity",
+ "wait-sync",
+ "done"
+};
+
+static void client_idle_timeout(struct client *client)
+{
+ if (client->output_cmd_lock == NULL)
+ client_send_line(client, "* BYE Disconnected for inactivity.");
+ client_destroy(client, t_strdup_printf(
+ "Inactivity - no input for %"PRIdTIME_T" secs",
+ ioloop_time - client->last_input));
+}
+
+static void client_init_urlauth(struct client *client)
+{
+ struct imap_urlauth_config config;
+
+ i_zero(&config);
+ config.url_host = client->set->imap_urlauth_host;
+ config.url_port = client->set->imap_urlauth_port;
+ config.socket_path = t_strconcat(client->user->set->base_dir,
+ "/"IMAP_URLAUTH_SOCKET_NAME, NULL);
+ config.session_id = client->user->session_id;
+ config.access_user = client->user->username;
+ config.access_service = "imap";
+ config.access_anonymous = client->user->anonymous;
+
+ client->urlauth_ctx = imap_urlauth_init(client->user, &config);
+}
+
+static bool user_has_special_use_mailboxes(struct mail_user *user)
+{
+ struct mail_namespace_settings *ns_set;
+
+ /*
+ * We have to iterate over namespace and mailbox *settings* since
+ * the namespaces haven't been set up yet. The namespaces haven't
+ * been set up so that we don't hold up the OK response to LOGIN
+ * when using slow lib-storage backends.
+ */
+
+ /* no namespaces => no special use flags */
+ if (!array_is_created(&user->set->namespaces))
+ return FALSE;
+
+ array_foreach_elem(&user->set->namespaces, ns_set) {
+ struct mailbox_settings *box_set;
+
+ /* no mailboxes => no special use flags */
+ if (!array_is_created(&ns_set->mailboxes))
+ continue;
+
+ array_foreach_elem(&ns_set->mailboxes, box_set) {
+ if (box_set->special_use != NULL)
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+struct client *client_create(int fd_in, int fd_out, bool unhibernated,
+ struct event *event, struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct imap_settings *set,
+ const struct smtp_submit_settings *smtp_set)
+{
+ const struct mail_storage_settings *mail_set;
+ struct client *client;
+ const char *ident;
+ pool_t pool;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd_in, TRUE);
+ net_set_nonblock(fd_out, TRUE);
+
+ pool = pool_alloconly_create("imap client", 2048);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+ client->v = imap_client_vfuncs;
+ client->event = event;
+ event_ref(client->event);
+ client->unhibernated = unhibernated;
+ client->set = set;
+ client->smtp_set = smtp_set;
+ client->service_user = service_user;
+ client->fd_in = fd_in;
+ client->fd_out = fd_out;
+ client->input = i_stream_create_fd(fd_in,
+ set->imap_max_line_length);
+ client->output = o_stream_create_fd(fd_out, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ i_stream_set_name(client->input, "<imap client>");
+ o_stream_set_name(client->output, "<imap client>");
+
+ o_stream_set_flush_callback(client->output, client_output, client);
+
+ p_array_init(&client->module_contexts, client->pool, 5);
+ client->last_input = ioloop_time;
+ client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS,
+ client_idle_timeout, client);
+
+ client->command_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"client command", 1024*2);
+ client->user = user;
+ client->notify_count_changes = TRUE;
+ client->notify_flag_changes = TRUE;
+ p_array_init(&client->enabled_features, client->pool, 8);
+
+ client->capability_string =
+ str_new(client->pool, sizeof(CAPABILITY_STRING)+64);
+
+ if (*set->imap_capability == '\0')
+ str_append(client->capability_string, CAPABILITY_STRING);
+ else if (*set->imap_capability != '+') {
+ str_append(client->capability_string, set->imap_capability);
+ } else {
+ str_append(client->capability_string, CAPABILITY_STRING);
+ str_append_c(client->capability_string, ' ');
+ str_append(client->capability_string, set->imap_capability + 1);
+ }
+ if (client->set->imap_literal_minus)
+ client_add_capability(client, "LITERAL-");
+ else
+ client_add_capability(client, "LITERAL+");
+ if (user->fuzzy_search) {
+ /* Enable FUZZY capability only when it actually has
+ a chance of working */
+ client_add_capability(client, "SEARCH=FUZZY");
+ }
+
+ mail_set = mail_user_set_get_storage_set(user);
+ if (mail_set->mailbox_list_index) {
+ /* NOTIFY is enabled only when mailbox list indexes are
+ enabled, although even that doesn't necessarily guarantee
+ it always */
+ client_add_capability(client, "NOTIFY");
+ }
+
+ if (*set->imap_urlauth_host != '\0' &&
+ *mail_set->mail_attribute_dict != '\0') {
+ /* Enable URLAUTH capability only when dict is
+ configured correctly */
+ client_init_urlauth(client);
+ client_add_capability(client, "URLAUTH");
+ client_add_capability(client, "URLAUTH=BINARY");
+ }
+ if (set->imap_metadata && *mail_set->mail_attribute_dict != '\0')
+ client_add_capability(client, "METADATA");
+ if (user_has_special_use_mailboxes(user)) {
+ /* Advertise SPECIAL-USE only if there are actually some
+ SPECIAL-USE flags in mailbox configuration. */
+ client_add_capability(client, "SPECIAL-USE");
+ }
+
+ ident = mail_user_get_anvil_userip_ident(client->user);
+ if (ident != NULL) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\timap/", ident, "\n", NULL));
+ client->anvil_sent = TRUE;
+ }
+
+ imap_client_count++;
+ DLLIST_PREPEND(&imap_clients, client);
+ if (hook_client_created != NULL)
+ hook_client_created(&client);
+
+ imap_refresh_proctitle();
+ return client;
+}
+
+void client_create_finish_io(struct client *client)
+{
+ if (client->set->rawlog_dir[0] != '\0') {
+ (void)iostream_rawlog_create(client->set->rawlog_dir,
+ &client->input, &client->output);
+ }
+ client->io = io_add_istream(client->input, client_input, client);
+}
+
+int client_create_finish(struct client *client, const char **error_r)
+{
+ if (mail_namespaces_init(client->user, error_r) < 0)
+ return -1;
+ mail_namespaces_set_storage_callbacks(client->user->namespaces,
+ &mail_storage_callbacks, client);
+ client->v.init(client);
+ return 0;
+}
+
+void client_add_istream_prefix(struct client *client,
+ const unsigned char *data, size_t size)
+{
+ i_assert(client->io == NULL);
+
+ struct istream *inputs[] = {
+ i_stream_create_copy_from_data(data, size),
+ client->input,
+ NULL
+ };
+ client->input = i_stream_create_concat(inputs);
+ i_stream_copy_fd(client->input, inputs[1]);
+ i_stream_unref(&inputs[0]);
+ i_stream_unref(&inputs[1]);
+
+ i_stream_set_input_pending(client->input, TRUE);
+}
+
+static void client_default_init(struct client *client ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+void client_command_cancel(struct client_command_context **_cmd)
+{
+ struct client_command_context *cmd = *_cmd;
+ bool cmd_ret;
+
+ switch (cmd->state) {
+ case CLIENT_COMMAND_STATE_WAIT_INPUT:
+ /* a bit kludgy check: cancel command only if it has context
+ set. currently only append command matches this check. all
+ other commands haven't even started the processing yet. */
+ if (cmd->context == NULL)
+ break;
+ /* fall through */
+ case CLIENT_COMMAND_STATE_WAIT_EXTERNAL:
+ case CLIENT_COMMAND_STATE_WAIT_OUTPUT:
+ cmd->cancel = TRUE;
+ break;
+ case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY:
+ case CLIENT_COMMAND_STATE_WAIT_SYNC:
+ /* commands haven't started yet */
+ break;
+ case CLIENT_COMMAND_STATE_DONE:
+ i_unreached();
+ }
+
+ cmd_ret = !cmd->cancel || cmd->func == NULL ? TRUE :
+ command_exec(cmd);
+ if (!cmd_ret) {
+ if (cmd->client->output->closed)
+ i_panic("command didn't cancel itself: %s", cmd->name);
+ } else {
+ client_command_free(*_cmd != NULL ? _cmd : &cmd);
+ }
+}
+
+const char *client_stats(struct client *client)
+{
+ const struct var_expand_table logout_tab[] = {
+ { 'i', dec2str(i_stream_get_absolute_offset(client->input)), "input" },
+ { 'o', dec2str(client->output->offset), "output" },
+ { '\0', client->user->session_id, "session" },
+ { '\0', dec2str(client->fetch_hdr_count), "fetch_hdr_count" },
+ { '\0', dec2str(client->fetch_hdr_bytes), "fetch_hdr_bytes" },
+ { '\0', dec2str(client->fetch_body_count), "fetch_body_count" },
+ { '\0', dec2str(client->fetch_body_bytes), "fetch_body_bytes" },
+ { '\0', dec2str(client->deleted_count), "deleted" },
+ { '\0', dec2str(client->expunged_count), "expunged" },
+ { '\0', dec2str(client->trashed_count), "trashed" },
+ { '\0', dec2str(client->autoexpunged_count), "autoexpunged" },
+ { '\0', dec2str(client->append_count), "appended" },
+ { '\0', NULL, NULL }
+ };
+ const struct var_expand_table *user_tab =
+ mail_user_var_expand_table(client->user);
+ const struct var_expand_table *tab =
+ t_var_expand_merge_tables(logout_tab, user_tab);
+ string_t *str;
+ const char *error;
+
+ str = t_str_new(128);
+ if (var_expand_with_funcs(str, client->set->imap_logout_format,
+ tab, mail_user_var_expand_func_table,
+ client->user, &error) < 0) {
+ e_error(client->event,
+ "Failed to expand imap_logout_format=%s: %s",
+ client->set->imap_logout_format, error);
+ }
+ return str_c(str);
+}
+
+void client_destroy(struct client *client, const char *reason)
+{
+ client->v.destroy(client, reason);
+}
+
+static void
+client_command_stats_append(string_t *str,
+ const struct client_command_stats *stats,
+ const char *wait_condition,
+ size_t buffered_size)
+{
+ uint64_t ioloop_wait_usecs;
+ unsigned int msecs_in_ioloop;
+
+ ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop);
+ msecs_in_ioloop = (ioloop_wait_usecs -
+ stats->start_ioloop_wait_usecs + 999) / 1000;
+ str_printfa(str, "running for %d.%03d + waiting ",
+ (int)((stats->running_usecs+999)/1000 / 1000),
+ (int)((stats->running_usecs+999)/1000 % 1000));
+ if (wait_condition[0] != '\0')
+ str_printfa(str, "%s ", wait_condition);
+ str_printfa(str, "for %d.%03d secs",
+ msecs_in_ioloop / 1000, msecs_in_ioloop % 1000);
+ if (stats->lock_wait_usecs > 0) {
+ int lock_wait_msecs = (stats->lock_wait_usecs+999)/1000;
+ str_printfa(str, ", %d.%03d in locks",
+ lock_wait_msecs/1000, lock_wait_msecs%1000);
+ }
+ str_printfa(str, ", %"PRIu64" B in + %"PRIu64,
+ stats->bytes_in, stats->bytes_out);
+ if (buffered_size > 0)
+ str_printfa(str, "+%zu", buffered_size);
+ str_append(str, " B out");
+}
+
+static const char *client_get_last_command_status(struct client *client)
+{
+ if (client->logged_out)
+ return "";
+ if (client->last_cmd_name == NULL) {
+ if (client->unhibernated)
+ return " (No commands sent after unhibernation)";
+ else
+ return " (No commands sent)";
+ }
+
+ /* client disconnected without sending LOGOUT. if the last command
+ took over 1 second to run, log it. */
+ const struct client_command_stats *stats = &client->last_cmd_stats;
+
+ string_t *str = t_str_new(128);
+ int last_run_secs = timeval_diff_msecs(&ioloop_timeval,
+ &stats->last_run_timeval);
+ str_printfa(str, " (%s finished %d.%03d secs ago",
+ client->last_cmd_name, last_run_secs/1000,
+ last_run_secs%1000);
+
+ if (timeval_diff_msecs(&stats->last_run_timeval, &stats->start_time) >=
+ IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS) {
+ str_append(str, " - ");
+ client_command_stats_append(str, stats, "", 0);
+ }
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static const char *client_get_commands_status(struct client *client)
+{
+ struct client_command_context *cmd, *last_cmd = NULL;
+ struct client_command_stats all_stats;
+ string_t *str;
+ enum io_condition cond;
+ const char *cond_str;
+
+ if (client->command_queue == NULL)
+ return client_get_last_command_status(client);
+
+ i_zero(&all_stats);
+ str = t_str_new(128);
+ str_append(str, " (");
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ if (cmd->name == NULL) {
+ /* (parts of a) tag were received, but not yet
+ the command name */
+ continue;
+ }
+ str_append(str, cmd->name);
+ if (cmd->next != NULL)
+ str_append_c(str, ',');
+ all_stats.running_usecs += cmd->stats.running_usecs;
+ all_stats.lock_wait_usecs += cmd->stats.lock_wait_usecs;
+ all_stats.bytes_in += cmd->stats.bytes_in;
+ all_stats.bytes_out += cmd->stats.bytes_out;
+ last_cmd = cmd;
+ }
+ if (last_cmd == NULL)
+ return client_get_last_command_status(client);
+
+ cond = io_loop_find_fd_conditions(current_ioloop, client->fd_out);
+ if ((cond & (IO_READ | IO_WRITE)) == (IO_READ | IO_WRITE))
+ cond_str = "input/output";
+ else if ((cond & IO_READ) != 0)
+ cond_str = "input";
+ else if ((cond & IO_WRITE) != 0)
+ cond_str = "output";
+ else
+ cond_str = "nothing";
+
+ all_stats.start_ioloop_wait_usecs =
+ last_cmd->stats.start_ioloop_wait_usecs;
+ str_append_c(str, ' ');
+ client_command_stats_append(str, &all_stats, cond_str,
+ o_stream_get_buffer_used_size(client->output));
+ str_printfa(str, ", state=%s)",
+ client_command_state_names[last_cmd->state]);
+ return str_c(str);
+}
+
+static void client_log_disconnect(struct client *client, const char *reason)
+{
+ e_info(client->event, "Disconnected: %s %s", reason, client_stats(client));
+}
+
+static void client_default_destroy(struct client *client, const char *reason)
+{
+ struct client_command_context *cmd;
+
+ i_assert(!client->destroyed);
+ client->destroyed = TRUE;
+ client->disconnected = TRUE;
+
+ if (client->disconnect_reason != NULL)
+ reason = client->disconnect_reason;
+ if (reason == NULL)
+ reason = t_strconcat(
+ io_stream_get_disconnect_reason(client->input,
+ client->output),
+ client_get_commands_status(client), NULL);
+
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+
+ /* finish off all the queued commands. */
+ if (client->output_cmd_lock != NULL)
+ client_command_cancel(&client->output_cmd_lock);
+ while (client->command_queue != NULL) {
+ cmd = client->command_queue;
+ client_command_cancel(&cmd);
+ }
+ /* handle the input_lock command last. it might have been waiting on
+ other queued commands (although we probably should just drop the
+ command at that point since it hasn't started running. but this may
+ change in future). */
+ if (client->input_lock != NULL)
+ client_command_cancel(&client->input_lock);
+
+ if (client->notify_ctx != NULL)
+ imap_notify_deinit(&client->notify_ctx);
+ if (client->urlauth_ctx != NULL)
+ imap_urlauth_deinit(&client->urlauth_ctx);
+ /* Keep mailbox closing close to last, so anything that could
+ potentially have transactions open will close them first. */
+ if (client->mailbox != NULL)
+ imap_client_close_mailbox(client);
+ if (client->anvil_sent) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\timap/",
+ mail_user_get_anvil_userip_ident(client->user),
+ "\n", NULL));
+ }
+
+ if (client->free_parser != NULL)
+ imap_parser_unref(&client->free_parser);
+ io_remove(&client->io);
+ timeout_remove(&client->to_idle_output);
+ timeout_remove(&client->to_idle);
+
+ /* i/ostreams are already closed at this stage, so fd can be closed */
+ fd_close_maybe_stdio(&client->fd_in, &client->fd_out);
+
+ /* Autoexpunging might run for a long time. Disconnect the client
+ before it starts, and refresh proctitle so it's clear that it's
+ doing autoexpunging. We've also sent DISCONNECT to anvil already,
+ because this is background work and shouldn't really be counted
+ as an active IMAP session for the user.
+
+ Don't autoexpunge if the client is hibernated - it shouldn't be any
+ different from the non-hibernating IDLE case. For frequent
+ hibernations it could also be doing unnecessarily much work. */
+ imap_refresh_proctitle();
+ if (!client->hibernated) {
+ client->autoexpunged_count = mail_user_autoexpunge(client->user);
+ client_log_disconnect(client, reason);
+ }
+ mail_user_deinit(&client->user);
+
+ /* free the i/ostreams after mail_user_unref(), which could trigger
+ mail_storage_callbacks notifications that write to the ostream. */
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+
+ if (array_is_created(&client->search_saved_uidset))
+ array_free(&client->search_saved_uidset);
+ if (array_is_created(&client->search_updates))
+ array_free(&client->search_updates);
+ pool_unref(&client->command_pool);
+ mail_storage_service_user_unref(&client->service_user);
+
+ imap_client_count--;
+ DLLIST_REMOVE(&imap_clients, client);
+
+ event_unref(&client->event);
+ i_free(client->last_cmd_name);
+ pool_unref(&client->pool);
+
+ master_service_client_connection_destroyed(master_service);
+ imap_refresh_proctitle();
+}
+
+static void client_destroy_timeout(struct client *client)
+{
+ client_destroy(client, NULL);
+}
+
+void client_disconnect(struct client *client, const char *reason)
+{
+ if (client->disconnected)
+ return;
+
+ client->disconnected = TRUE;
+ client->disconnect_reason = p_strdup(client->pool, reason);
+ /* Finish the ostream. With IMAP COMPRESS this sends the EOF marker. */
+ (void)o_stream_finish(client->output);
+ o_stream_uncork(client->output);
+
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+
+ timeout_remove(&client->to_idle);
+ client->to_idle = timeout_add(0, client_destroy_timeout, client);
+}
+
+void client_disconnect_with_error(struct client *client,
+ const char *client_error)
+{
+ client_send_line(client, t_strconcat("* BYE ", client_error, NULL));
+ client_disconnect(client, client_error);
+}
+
+void client_add_capability(struct client *client, const char *capability)
+{
+ /* require a single capability at a time (feels cleaner) */
+ i_assert(strchr(capability, ' ') == NULL);
+
+ if (client->set->imap_capability[0] != '\0' &&
+ client->set->imap_capability[0] != '+') {
+ /* explicit capability - don't change it */
+ return;
+ }
+ str_append_c(client->capability_string, ' ');
+ str_append(client->capability_string, capability);
+}
+
+void client_send_line(struct client *client, const char *data)
+{
+ (void)client_send_line_next(client, data);
+}
+
+int client_send_line_next(struct client *client, const char *data)
+{
+ struct const_iovec iov[2];
+
+ if (client->output->closed)
+ return -1;
+
+ iov[0].iov_base = data;
+ iov[0].iov_len = strlen(data);
+ iov[1].iov_base = "\r\n";
+ iov[1].iov_len = 2;
+
+ if (o_stream_sendv(client->output, iov, 2) < 0)
+ return -1;
+ client->last_output = ioloop_time;
+
+ if (o_stream_get_buffer_used_size(client->output) >=
+ CLIENT_OUTPUT_OPTIMAL_SIZE) {
+ /* buffer full, try flushing */
+ return o_stream_flush(client->output);
+ }
+ return 1;
+}
+
+static void
+client_cmd_append_timing_stats(struct client_command_context *cmd,
+ string_t *str)
+{
+ unsigned int msecs_in_cmd, msecs_in_ioloop;
+ uint64_t ioloop_wait_usecs;
+ unsigned int msecs_since_cmd;
+
+ if (cmd->stats.start_time.tv_sec == 0)
+ return;
+ command_stats_flush(cmd);
+
+ ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop);
+ msecs_in_cmd = (cmd->stats.running_usecs + 999) / 1000;
+ msecs_in_ioloop = (ioloop_wait_usecs -
+ cmd->stats.start_ioloop_wait_usecs + 999) / 1000;
+ msecs_since_cmd = timeval_diff_msecs(&ioloop_timeval,
+ &cmd->stats.last_run_timeval);
+
+ if (str_data(str)[str_len(str)-1] == '.')
+ str_truncate(str, str_len(str)-1);
+ str_printfa(str, " (%d.%03d + %d.%03d ",
+ msecs_in_cmd / 1000, msecs_in_cmd % 1000,
+ msecs_in_ioloop / 1000, msecs_in_ioloop % 1000);
+ if (msecs_since_cmd > 0) {
+ str_printfa(str, "+ %d.%03d ",
+ msecs_since_cmd / 1000, msecs_since_cmd % 1000);
+ }
+ str_append(str, "secs).");
+}
+
+void client_send_tagline(struct client_command_context *cmd, const char *data)
+{
+ cmd->client->v.send_tagline(cmd, data);
+}
+
+static void
+client_default_send_tagline(struct client_command_context *cmd, const char *data)
+{
+ struct client *client = cmd->client;
+ const char *tag = cmd->tag;
+
+ if (client->output->closed || cmd->cancel)
+ return;
+
+ i_assert(!cmd->tagline_sent);
+ cmd->tagline_sent = TRUE;
+ cmd->tagline_reply = p_strdup(cmd->pool, data);
+
+ if (tag == NULL || *tag == '\0')
+ tag = "*";
+
+ T_BEGIN {
+ string_t *str = t_str_new(256);
+ str_printfa(str, "%s %s", tag, data);
+ client_cmd_append_timing_stats(cmd, str);
+ str_append(str, "\r\n");
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ } T_END;
+
+ client->last_output = ioloop_time;
+}
+
+static int
+client_default_sync_notify_more(struct imap_sync_context *ctx ATTR_UNUSED)
+{
+ return 1;
+}
+
+void client_send_command_error(struct client_command_context *cmd,
+ const char *client_error)
+{
+ struct client *client = cmd->client;
+ const char *error, *cmd_name;
+ enum imap_parser_error parse_error;
+
+ if (client_error == NULL) {
+ client_error = imap_parser_get_error(cmd->parser, &parse_error);
+ switch (parse_error) {
+ case IMAP_PARSE_ERROR_NONE:
+ i_unreached();
+ case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
+ client_disconnect_with_error(client, client_error);
+ return;
+ default:
+ break;
+ }
+ }
+
+ if (cmd->tag == NULL)
+ error = t_strconcat("BAD Error in IMAP tag: ", client_error, NULL);
+ else if (cmd->name == NULL)
+ error = t_strconcat("BAD Error in IMAP command: ", client_error, NULL);
+ else {
+ cmd_name = t_str_ucase(cmd->name);
+ error = t_strconcat("BAD Error in IMAP command ",
+ cmd_name, ": ", client_error, NULL);
+ }
+
+ client_send_tagline(cmd, error);
+
+ if (++client->bad_counter >= CLIENT_MAX_BAD_COMMANDS) {
+ client_disconnect_with_error(client,
+ "Too many invalid IMAP commands.");
+ }
+
+ cmd->param_error = TRUE;
+ /* client_read_args() failures rely on this being set, so that the
+ command processing is stopped even while command function returns
+ FALSE. */
+ cmd->state = CLIENT_COMMAND_STATE_DONE;
+}
+
+void client_send_internal_error(struct client_command_context *cmd)
+{
+ client_send_tagline(cmd,
+ t_strflocaltime("NO "MAIL_ERRSTR_CRITICAL_MSG_STAMP, ioloop_time));
+}
+
+bool client_read_args(struct client_command_context *cmd, unsigned int count,
+ unsigned int flags, const struct imap_arg **args_r)
+{
+ int ret;
+
+ i_assert(count <= INT_MAX);
+
+ ret = imap_parser_read_args(cmd->parser, count, flags, args_r);
+ if (ret >= (int)count) {
+ /* all parameters read successfully */
+ i_assert(cmd->client->input_lock == NULL ||
+ cmd->client->input_lock == cmd);
+
+ client_args_finished(cmd, *args_r);
+ cmd->client->input_lock = NULL;
+ return TRUE;
+ } else if (ret == -2) {
+ /* need more data */
+ if (cmd->client->input->closed) {
+ /* disconnected */
+ cmd->state = CLIENT_COMMAND_STATE_DONE;
+ }
+ return FALSE;
+ } else {
+ /* error, or missing arguments */
+ client_send_command_error(cmd, ret < 0 ? NULL :
+ "Missing arguments");
+ return FALSE;
+ }
+}
+
+bool client_read_string_args(struct client_command_context *cmd,
+ unsigned int count, ...)
+{
+ const struct imap_arg *imap_args;
+ va_list va;
+ const char *str;
+ unsigned int i;
+
+ if (!client_read_args(cmd, count, 0, &imap_args))
+ return FALSE;
+
+ va_start(va, count);
+ for (i = 0; i < count; i++) {
+ const char **ret = va_arg(va, const char **);
+
+ if (IMAP_ARG_IS_EOL(&imap_args[i])) {
+ client_send_command_error(cmd, "Missing arguments.");
+ break;
+ }
+
+ if (!imap_arg_get_astring(&imap_args[i], &str)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ break;
+ }
+
+ if (ret != NULL)
+ *ret = str;
+ }
+ va_end(va);
+
+ return i == count;
+}
+
+void client_args_finished(struct client_command_context *cmd,
+ const struct imap_arg *args)
+{
+ string_t *str = t_str_new(256);
+
+ if (cmd->args != NULL && cmd->args[0] != '\0') {
+ str_append(str, cmd->args);
+ str_append_c(str, ' ');
+ }
+ imap_write_args(str, args);
+ cmd->args = p_strdup(cmd->pool, str_c(str));
+ event_add_str(cmd->event, "cmd_args", cmd->args);
+
+ str_truncate(str, 0);
+ if (cmd->human_args != NULL && cmd->human_args[0] != '\0') {
+ str_append(str, cmd->human_args);
+ str_append_c(str, ' ');
+ }
+ imap_write_args_for_human(str, args);
+ cmd->human_args = p_strdup(cmd->pool, str_c(str));
+ event_add_str(cmd->event, "cmd_human_args", cmd->human_args);
+}
+
+static struct client_command_context *
+client_command_find_with_flags(struct client_command_context *new_cmd,
+ enum command_flags flags,
+ enum client_command_state max_state)
+{
+ struct client_command_context *cmd;
+
+ cmd = new_cmd->client->command_queue;
+ for (; cmd != NULL; cmd = cmd->next) {
+ /* The tagline_sent check is a bit kludgy here. Plugins may
+ hook into sync_notify_more() and send the tagline before
+ finishing the command. During this stage the state was been
+ dropped from _WAIT_SYNC to _WAIT_OUTPUT, so the <= max_state
+ check doesn't work correctly here. (Perhaps we should add
+ a new _WAIT_SYNC_OUTPUT?) */
+ if (cmd->state <= max_state && !cmd->tagline_sent &&
+ cmd != new_cmd && (cmd->cmd_flags & flags) != 0)
+ return cmd;
+ }
+ return NULL;
+}
+
+static bool client_command_is_ambiguous(struct client_command_context *cmd)
+{
+ enum command_flags flags;
+ enum client_command_state max_state =
+ CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY;
+ bool broken_client = FALSE;
+
+ if ((cmd->cmd_flags & COMMAND_FLAG_REQUIRES_SYNC) != 0 &&
+ !imap_sync_is_allowed(cmd->client))
+ return TRUE;
+
+ if (cmd->search_save_result_used) {
+ /* if there are pending commands that update the search
+ save result, wait */
+ struct client_command_context *old_cmd = cmd->next;
+
+ for (; old_cmd != NULL; old_cmd = old_cmd->next) {
+ if (old_cmd->search_save_result)
+ return TRUE;
+ }
+ }
+
+ if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_MAILBOX) ==
+ COMMAND_FLAG_BREAKS_MAILBOX) {
+ /* there must be no other command running that uses the
+ selected mailbox */
+ flags = COMMAND_FLAG_USES_MAILBOX;
+ max_state = CLIENT_COMMAND_STATE_DONE;
+ } else if ((cmd->cmd_flags & COMMAND_FLAG_USES_SEQS) != 0) {
+ /* no existing command must be breaking sequences */
+ flags = COMMAND_FLAG_BREAKS_SEQS;
+ broken_client = TRUE;
+ } else if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_SEQS) != 0) {
+ /* if existing command uses sequences, we'll have to block */
+ flags = COMMAND_FLAG_USES_SEQS;
+ } else {
+ return FALSE;
+ }
+
+ if (client_command_find_with_flags(cmd, flags, max_state) == NULL) {
+ if (cmd->client->syncing) {
+ /* don't do anything until syncing is finished */
+ return TRUE;
+ }
+ if (cmd->client->mailbox_change_lock != NULL &&
+ cmd->client->mailbox_change_lock != cmd) {
+ /* don't do anything until mailbox is fully
+ opened/closed */
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ if (broken_client) {
+ client_send_line(cmd->client,
+ "* BAD ["IMAP_RESP_CODE_CLIENTBUG"] "
+ "Command pipelining results in ambiguity.");
+ }
+
+ return TRUE;
+}
+
+struct client_command_context *client_command_alloc(struct client *client)
+{
+ struct client_command_context *cmd;
+
+ cmd = p_new(client->command_pool, struct client_command_context, 1);
+ cmd->client = client;
+ cmd->pool = client->command_pool;
+ cmd->global_event = event_create(client->event);
+ cmd->event = event_create(cmd->global_event);
+ cmd->stats.start_time = ioloop_timeval;
+ cmd->stats.last_run_timeval = ioloop_timeval;
+ cmd->stats.start_ioloop_wait_usecs =
+ io_loop_get_wait_usecs(current_ioloop);
+ p_array_init(&cmd->module_contexts, cmd->pool, 5);
+
+ DLLIST_PREPEND(&client->command_queue, cmd);
+ client->command_queue_size++;
+
+ imap_client_notify_command_allocated(client);
+ return cmd;
+}
+
+void client_command_init_finished(struct client_command_context *cmd)
+{
+ event_add_str(cmd->event, "cmd_tag", cmd->tag);
+ /* use "unknown" until we checked that the command name is known/valid */
+ event_add_str(cmd->event, "cmd_name", "unknown");
+ /* the actual command name received from client - as-is */
+ event_add_str(cmd->event, "cmd_input_name", cmd->name);
+}
+
+static struct client_command_context *
+client_command_new(struct client *client)
+{
+ struct client_command_context *cmd;
+
+ cmd = client_command_alloc(client);
+ if (client->free_parser != NULL) {
+ cmd->parser = client->free_parser;
+ client->free_parser = NULL;
+ } else {
+ cmd->parser =
+ imap_parser_create(client->input, client->output,
+ client->set->imap_max_line_length);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(cmd->parser);
+ }
+ return cmd;
+}
+
+void client_add_missing_io(struct client *client)
+{
+ if (client->io == NULL && !client->disconnected)
+ client->io = io_add_istream(client->input, client_input, client);
+}
+
+void client_command_free(struct client_command_context **_cmd)
+{
+ struct client_command_context *cmd = *_cmd;
+ struct client *client = cmd->client;
+ enum client_command_state state = cmd->state;
+
+ *_cmd = NULL;
+
+ i_assert(!cmd->executing);
+ i_assert(client->output_cmd_lock == NULL);
+
+ /* reset input idle time because command output might have taken a
+ long time and we don't want to disconnect client immediately then */
+ client->last_input = ioloop_time;
+ timeout_reset(client->to_idle);
+
+ if (cmd->cancel) {
+ cmd->cancel = FALSE;
+ client_send_tagline(cmd, "NO Command cancelled.");
+ }
+
+ i_free(client->last_cmd_name);
+ client->last_cmd_name = i_strdup(cmd->name);
+ client->last_cmd_stats = cmd->stats;
+
+ if (!cmd->param_error)
+ client->bad_counter = 0;
+
+ if (client->input_lock == cmd)
+ client->input_lock = NULL;
+ if (client->mailbox_change_lock == cmd)
+ client->mailbox_change_lock = NULL;
+
+ event_set_name(cmd->event, "imap_command_finished");
+ if (cmd->tagline_reply != NULL) {
+ event_add_str(cmd->event, "tagged_reply_state",
+ t_strcut(cmd->tagline_reply, ' '));
+ event_add_str(cmd->event, "tagged_reply", cmd->tagline_reply);
+ }
+ event_add_timeval(cmd->event, "last_run_time",
+ &cmd->stats.last_run_timeval);
+ event_add_int(cmd->event, "running_usecs", cmd->stats.running_usecs);
+ event_add_int(cmd->event, "lock_wait_usecs", cmd->stats.lock_wait_usecs);
+ event_add_int(cmd->event, "bytes_in", cmd->stats.bytes_in);
+ event_add_int(cmd->event, "bytes_out", cmd->stats.bytes_out);
+
+ e_debug(cmd->event, "Command finished: %s %s", cmd->name,
+ cmd->human_args != NULL ? cmd->human_args : "");
+ event_unref(&cmd->event);
+ event_unref(&cmd->global_event);
+
+ if (cmd->parser != NULL) {
+ if (client->free_parser == NULL) {
+ imap_parser_reset(cmd->parser);
+ client->free_parser = cmd->parser;
+ } else {
+ imap_parser_unref(&cmd->parser);
+ }
+ }
+
+ client->command_queue_size--;
+ DLLIST_REMOVE(&client->command_queue, cmd);
+ cmd = NULL;
+
+ if (client->command_queue == NULL) {
+ /* no commands left in the queue, we can clear the pool */
+ p_clear(client->command_pool);
+ timeout_remove(&client->to_idle_output);
+ }
+ imap_client_notify_command_freed(client);
+ imap_refresh_proctitle();
+
+ /* if command finished from external event, check input for more
+ unhandled commands since we may not be executing from client_input
+ or client_output. */
+ if (state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL &&
+ !client->disconnected) {
+ client_add_missing_io(client);
+ io_set_pending(client->io);
+ }
+}
+
+static void client_check_command_hangs(struct client *client)
+{
+ struct client_command_context *cmd;
+ unsigned int unfinished_count = 0;
+ bool have_wait_unfinished = FALSE;
+
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ switch (cmd->state) {
+ case CLIENT_COMMAND_STATE_WAIT_INPUT:
+ /* We need to be reading input for this command.
+ However, if there is already an output lock for
+ another command we'll wait for it to finish first.
+ This is needed because if there are any literals
+ we'd need to send "+ OK" responses. */
+ i_assert(client->io != NULL ||
+ (client->output_cmd_lock != NULL &&
+ client->output_cmd_lock != client->input_lock));
+ unfinished_count++;
+ break;
+ case CLIENT_COMMAND_STATE_WAIT_OUTPUT:
+ i_assert((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) != 0);
+ unfinished_count++;
+ break;
+ case CLIENT_COMMAND_STATE_WAIT_EXTERNAL:
+ unfinished_count++;
+ break;
+ case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY:
+ have_wait_unfinished = TRUE;
+ break;
+ case CLIENT_COMMAND_STATE_WAIT_SYNC:
+ if ((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) == 0)
+ have_wait_unfinished = TRUE;
+ else {
+ /* we have an output callback, which will be
+ called soon and it'll run cmd_sync_delayed().
+ FIXME: is this actually wanted? */
+ }
+ break;
+ case CLIENT_COMMAND_STATE_DONE:
+ i_unreached();
+ }
+ }
+ i_assert(!have_wait_unfinished || unfinished_count > 0);
+}
+
+static bool client_remove_pending_unambiguity(struct client *client)
+{
+ if (client->input_lock != NULL) {
+ /* there's a command that has locked the input */
+ struct client_command_context *cmd = client->input_lock;
+
+ if (cmd->state != CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY)
+ return TRUE;
+
+ /* the command is waiting for existing ambiguity causing
+ commands to finish. */
+ if (client_command_is_ambiguous(cmd)) {
+ /* we could be waiting for existing sync to finish */
+ if (!cmd_sync_delayed(client))
+ return FALSE;
+ if (client_command_is_ambiguous(cmd))
+ return FALSE;
+ }
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;
+ }
+ return TRUE;
+}
+
+void client_continue_pending_input(struct client *client)
+{
+ i_assert(!client->handling_input);
+
+ if (client->disconnected) {
+ client_destroy(client, NULL);
+ return;
+ }
+
+ /* this function is called at the end of I/O callbacks (and only there).
+ fix up the command states and verify that they're correct. */
+ while (client_remove_pending_unambiguity(client)) {
+ client_add_missing_io(client);
+
+ /* if there's unread data in buffer, handle it. */
+ if (i_stream_get_data_size(client->input) == 0 ||
+ client->disconnected)
+ break;
+
+ struct ostream *output = client->output;
+ o_stream_ref(output);
+ o_stream_cork(output);
+ bool ret = client_handle_input(client);
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+ if (!ret)
+ break;
+ }
+ if (client->input->closed || client->output->closed)
+ client_destroy(client, NULL);
+ else
+ client_check_command_hangs(client);
+}
+
+/* Skip incoming data until newline is found,
+ returns TRUE if newline was found. */
+static bool client_skip_line(struct client *client)
+{
+ const unsigned char *data;
+ size_t i, data_size;
+
+ data = i_stream_get_data(client->input, &data_size);
+
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\n') {
+ client->input_skip_line = FALSE;
+ i++;
+ break;
+ }
+ }
+
+ i_stream_skip(client->input, i);
+ return !client->input_skip_line;
+}
+
+static void client_idle_output_timeout(struct client *client)
+{
+ client_destroy(client, t_strdup_printf(
+ "Client has not read server output for for %"PRIdTIME_T" secs",
+ ioloop_time - client->last_output));
+}
+
+bool client_handle_unfinished_cmd(struct client_command_context *cmd)
+{
+ if (cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT) {
+ /* need more input */
+ return FALSE;
+ }
+ if (cmd->state != CLIENT_COMMAND_STATE_WAIT_OUTPUT) {
+ /* waiting for something */
+ if (cmd->state == CLIENT_COMMAND_STATE_WAIT_SYNC) {
+ /* this is mainly for APPEND. */
+ client_add_missing_io(cmd->client);
+ }
+ return TRUE;
+ }
+
+ /* output is blocking, we can execute more commands */
+ o_stream_set_flush_pending(cmd->client->output, TRUE);
+ if (cmd->client->to_idle_output == NULL) {
+ /* disconnect sooner if client isn't reading our output */
+ cmd->client->to_idle_output =
+ timeout_add(CLIENT_OUTPUT_TIMEOUT_MSECS,
+ client_idle_output_timeout, cmd->client);
+ }
+ return TRUE;
+}
+
+static void
+client_command_failed_early(struct client_command_context **_cmd,
+ const char *error)
+{
+ struct client_command_context *cmd = *_cmd;
+
+ /* ignore the rest of this line */
+ cmd->client->input_skip_line = TRUE;
+
+ io_loop_time_refresh();
+ command_stats_start(cmd);
+ client_send_command_error(cmd, error);
+ cmd->param_error = TRUE;
+ client_command_free(_cmd);
+}
+
+static bool client_command_input(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct command *command;
+ const char *tag, *name;
+ int ret;
+
+ if (cmd->func != NULL) {
+ /* command is being executed - continue it */
+ if (command_exec(cmd)) {
+ /* command execution was finished */
+ client_command_free(&cmd);
+ client_add_missing_io(client);
+ return TRUE;
+ }
+
+ return client_handle_unfinished_cmd(cmd);
+ }
+
+ if (cmd->tag == NULL) {
+ ret = imap_parser_read_tag(cmd->parser, &tag);
+ if (ret == 0)
+ return FALSE; /* need more data */
+ if (ret < 0) {
+ client_command_failed_early(&cmd, "Invalid tag.");
+ return TRUE;
+ }
+ cmd->tag = p_strdup(cmd->pool, tag);
+ }
+
+ if (cmd->name == NULL) {
+ ret = imap_parser_read_command_name(cmd->parser, &name);
+ if (ret == 0)
+ return FALSE; /* need more data */
+ if (ret < 0) {
+ client_command_failed_early(&cmd, "Invalid command name.");
+ return TRUE;
+ }
+
+ /* UID commands are a special case. better to handle them
+ here. */
+ if (!cmd->uid && strcasecmp(name, "UID") == 0) {
+ cmd->uid = TRUE;
+ return client_command_input(cmd);
+ }
+ cmd->name = !cmd->uid ? p_strdup(cmd->pool, name) :
+ p_strconcat(cmd->pool, "UID ", name, NULL);
+ client_command_init_finished(cmd);
+ imap_refresh_proctitle();
+ }
+
+ client->input_skip_line = TRUE;
+
+ if (cmd->name[0] == '\0') {
+ /* command not given - cmd->func is already NULL. */
+ } else if ((command = command_find(cmd->name)) != NULL) {
+ cmd->func = command->func;
+ cmd->cmd_flags = command->flags;
+ /* valid command - overwrite the "unknown" string set earlier */
+ event_add_str(cmd->global_event, "cmd_name", command->name);
+ event_strlist_append(cmd->global_event, "reason_code",
+ event_reason_code_prefix("imap", "cmd_", command->name));
+ event_add_str(cmd->event, "cmd_name", command->name);
+ if (client_command_is_ambiguous(cmd)) {
+ /* do nothing until existing commands are finished */
+ i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT);
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY;
+ io_remove(&client->io);
+ return FALSE;
+ }
+ }
+
+ if (cmd->func == NULL) {
+ /* unknown command */
+ client_command_failed_early(&cmd, "Unknown command.");
+ return TRUE;
+ } else {
+ i_assert(!client->disconnected);
+
+ return client_command_input(cmd);
+ }
+}
+
+static bool client_handle_next_command(struct client *client, bool *remove_io_r)
+{
+ *remove_io_r = FALSE;
+
+ if (client->input_lock != NULL) {
+ if (client->input_lock->state ==
+ CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY ||
+ /* we can't send literal "+ OK" replies if output is
+ locked by another command. */
+ (client->output_cmd_lock != NULL &&
+ client->output_cmd_lock != client->input_lock)) {
+ *remove_io_r = TRUE;
+ return FALSE;
+ }
+ return client_command_input(client->input_lock);
+ }
+
+ if (client->input_skip_line) {
+ /* first eat the previous command line */
+ if (!client_skip_line(client))
+ return FALSE;
+ client->input_skip_line = FALSE;
+ }
+
+ /* don't bother creating a new client command before there's at least
+ some input */
+ if (i_stream_get_data_size(client->input) == 0)
+ return FALSE;
+
+ /* beginning a new command */
+ if (client->command_queue_size >= CLIENT_COMMAND_QUEUE_MAX_SIZE ||
+ client->output_cmd_lock != NULL) {
+ /* wait for some of the commands to finish */
+ *remove_io_r = TRUE;
+ return FALSE;
+ }
+
+ client->input_lock = client_command_new(client);
+ return client_command_input(client->input_lock);
+}
+
+bool client_handle_input(struct client *client)
+{
+ bool ret, remove_io, handled_commands = FALSE;
+
+ i_assert(o_stream_is_corked(client->output) ||
+ client->output->stream_errno != 0);
+ i_assert(!client->disconnected);
+
+ client->handling_input = TRUE;
+ do {
+ T_BEGIN {
+ ret = client_handle_next_command(client, &remove_io);
+ } T_END;
+ if (ret)
+ handled_commands = TRUE;
+ } while (ret && !client->disconnected && client->io != NULL);
+ client->handling_input = FALSE;
+
+ if (remove_io)
+ io_remove(&client->io);
+ else
+ client_add_missing_io(client);
+ if (!handled_commands)
+ return FALSE;
+
+ if (client->input_lock == NULL) {
+ /* finished handling all commands. sync them all at once now. */
+ cmd_sync_delayed(client);
+ } else if (client->input_lock->state == CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY) {
+ /* the command may be waiting for previous command to sync. */
+ cmd_sync_delayed(client);
+ }
+ return TRUE;
+}
+
+void client_input(struct client *client)
+{
+ struct client_command_context *cmd;
+ struct ostream *output = client->output;
+ ssize_t bytes;
+
+ i_assert(client->io != NULL);
+
+ client->last_input = ioloop_time;
+ timeout_reset(client->to_idle);
+
+ bytes = i_stream_read(client->input);
+ if (bytes == -1) {
+ /* disconnected */
+ client_destroy(client, NULL);
+ return;
+ }
+
+ o_stream_ref(output);
+ o_stream_cork(output);
+ if (!client_handle_input(client) && bytes == -2) {
+ /* parameter word is longer than max. input buffer size.
+ this is most likely an error, so skip the new data
+ until newline is found. */
+ client->input_skip_line = TRUE;
+
+ cmd = client->input_lock != NULL ? client->input_lock :
+ client_command_new(client);
+ cmd->param_error = TRUE;
+ client_send_command_error(cmd, "Too long argument.");
+ client_command_free(&cmd);
+ }
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+ imap_refresh_proctitle();
+
+ client_continue_pending_input(client);
+}
+
+static void client_output_cmd(struct client_command_context *cmd)
+{
+ bool finished;
+
+ /* continue processing command */
+ finished = command_exec(cmd);
+
+ if (!finished)
+ (void)client_handle_unfinished_cmd(cmd);
+ else {
+ /* command execution was finished */
+ client_command_free(&cmd);
+ }
+}
+
+static void client_output_commands(struct client *client)
+{
+ struct client_command_context *cmd;
+
+ /* mark all commands non-executed */
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next)
+ cmd->temp_executed = FALSE;
+
+ if (client->output_cmd_lock != NULL) {
+ client->output_cmd_lock->temp_executed = TRUE;
+ client_output_cmd(client->output_cmd_lock);
+ }
+ while (client->output_cmd_lock == NULL) {
+ /* go through the entire commands list every time in case
+ multiple commands were freed. temp_executed keeps track of
+ which messages we've called so far */
+ cmd = client->command_queue;
+ for (; cmd != NULL; cmd = cmd->next) {
+ if (!cmd->temp_executed &&
+ cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT) {
+ cmd->temp_executed = TRUE;
+ client_output_cmd(cmd);
+ break;
+ }
+ }
+ if (cmd == NULL) {
+ /* all commands executed */
+ break;
+ }
+ }
+}
+
+int client_output(struct client *client)
+{
+ int ret;
+
+ i_assert(!client->destroyed);
+
+ client->last_output = ioloop_time;
+ timeout_reset(client->to_idle);
+ if (client->to_idle_output != NULL)
+ timeout_reset(client->to_idle_output);
+
+ if ((ret = o_stream_flush(client->output)) < 0) {
+ client_destroy(client, NULL);
+ return 1;
+ }
+
+ client_output_commands(client);
+ (void)cmd_sync_delayed(client);
+
+ imap_refresh_proctitle_delayed();
+ if (client->output->closed)
+ client_destroy(client, NULL);
+ else {
+ /* corking is added automatically by ostream-file. we need to
+ uncork here before client_check_command_hangs() is called,
+ because otherwise it can assert-crash due to ioloop not
+ having IO_WRITE callback set for the ostream. */
+ o_stream_uncork(client->output);
+ client_continue_pending_input(client);
+ }
+ return ret;
+}
+
+bool client_handle_search_save_ambiguity(struct client_command_context *cmd)
+{
+ struct client_command_context *old_cmd = cmd->next;
+
+ /* search only commands that were added before this command
+ (commands are prepended to the queue, so they're after ourself) */
+ for (; old_cmd != NULL; old_cmd = old_cmd->next) {
+ if (old_cmd->search_save_result)
+ break;
+ }
+ if (old_cmd == NULL)
+ return FALSE;
+
+ /* ambiguity, wait until it's over */
+ i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT);
+ cmd->client->input_lock = cmd;
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY;
+ cmd->search_save_result_used = TRUE;
+ io_remove(&cmd->client->io);
+ return TRUE;
+}
+
+void client_enable(struct client *client, unsigned int feature_idx)
+{
+ if (client_has_enabled(client, feature_idx))
+ return;
+
+ const struct imap_feature *feat = imap_feature_idx(feature_idx);
+ feat->callback(client);
+ /* set after the callback, so the callback can see what features were
+ previously set */
+ bool value = TRUE;
+ array_idx_set(&client->enabled_features, feature_idx, &value);
+}
+
+bool client_has_enabled(struct client *client, unsigned int feature_idx)
+{
+ if (feature_idx >= array_count(&client->enabled_features))
+ return FALSE;
+ const bool *featurep =
+ array_idx(&client->enabled_features, feature_idx);
+ return *featurep;
+}
+
+static void imap_client_enable_condstore(struct client *client)
+{
+ struct mailbox_status status;
+ int ret;
+
+ if (client->mailbox == NULL)
+ return;
+
+ if ((client_enabled_mailbox_features(client) & MAILBOX_FEATURE_CONDSTORE) != 0)
+ return;
+
+ ret = mailbox_enable(client->mailbox, MAILBOX_FEATURE_CONDSTORE);
+ if (ret == 0) {
+ /* CONDSTORE being enabled while mailbox is selected.
+ Notify client of the latest HIGHESTMODSEQ. */
+ ret = mailbox_get_status(client->mailbox,
+ STATUS_HIGHESTMODSEQ, &status);
+ if (ret == 0) {
+ client_send_line(client, t_strdup_printf(
+ "* OK [HIGHESTMODSEQ %"PRIu64"] Highest",
+ status.highest_modseq));
+ }
+ }
+ if (ret < 0) {
+ client_send_untagged_storage_error(client,
+ mailbox_get_storage(client->mailbox));
+ }
+}
+
+static void imap_client_enable_qresync(struct client *client)
+{
+ /* enable also CONDSTORE */
+ client_enable(client, imap_feature_condstore);
+}
+
+enum mailbox_feature client_enabled_mailbox_features(struct client *client)
+{
+ enum mailbox_feature mailbox_features = 0;
+ const struct imap_feature *feature;
+ const bool *client_enabled;
+ unsigned int count;
+
+ client_enabled = array_get(&client->enabled_features, &count);
+ for (unsigned int idx = 0; idx < count; idx++) {
+ if (client_enabled[idx]) {
+ feature = imap_feature_idx(idx);
+ mailbox_features |= feature->mailbox_features;
+ }
+ }
+ return mailbox_features;
+}
+
+const char *const *client_enabled_features(struct client *client)
+{
+ ARRAY_TYPE(const_string) feature_strings;
+ const struct imap_feature *feature;
+ const bool *client_enabled;
+ unsigned int count;
+
+ t_array_init(&feature_strings, 8);
+ client_enabled = array_get(&client->enabled_features, &count);
+ for (unsigned int idx = 0; idx < count; idx++) {
+ if (client_enabled[idx]) {
+ feature = imap_feature_idx(idx);
+ array_push_back(&feature_strings, &feature->feature);
+ }
+ }
+ array_append_zero(&feature_strings);
+ return array_front(&feature_strings);
+}
+
+struct imap_search_update *
+client_search_update_lookup(struct client *client, const char *tag,
+ unsigned int *idx_r)
+{
+ struct imap_search_update *updates;
+ unsigned int i, count;
+
+ if (!array_is_created(&client->search_updates))
+ return NULL;
+
+ updates = array_get_modifiable(&client->search_updates, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(updates[i].tag, tag) == 0) {
+ *idx_r = i;
+ return &updates[i];
+ }
+ }
+ return NULL;
+}
+
+void client_search_updates_free(struct client *client)
+{
+ struct imap_search_update *update;
+
+ if (!array_is_created(&client->search_updates))
+ return;
+
+ array_foreach_modifiable(&client->search_updates, update)
+ imap_search_update_free(update);
+ array_clear(&client->search_updates);
+}
+
+void clients_init(void)
+{
+ imap_feature_condstore =
+ imap_feature_register("CONDSTORE", MAILBOX_FEATURE_CONDSTORE,
+ imap_client_enable_condstore);
+ imap_feature_qresync =
+ imap_feature_register("QRESYNC", MAILBOX_FEATURE_CONDSTORE,
+ imap_client_enable_qresync);
+}
+
+void clients_destroy_all(void)
+{
+ while (imap_clients != NULL) {
+ mail_storage_service_io_activate_user(imap_clients->service_user);
+ client_send_line(imap_clients, "* BYE Server shutting down.");
+ client_destroy(imap_clients, "Server shutting down.");
+ }
+}
+
+struct imap_client_vfuncs imap_client_vfuncs = {
+ .init = client_default_init,
+ .destroy = client_default_destroy,
+
+ .send_tagline = client_default_send_tagline,
+ .sync_notify_more = client_default_sync_notify_more,
+
+ .state_export = imap_state_export_base,
+ .state_import = imap_state_import_base,
+};
diff --git a/src/imap/imap-client.h b/src/imap/imap-client.h
new file mode 100644
index 0000000..48b0bc5
--- /dev/null
+++ b/src/imap/imap-client.h
@@ -0,0 +1,362 @@
+#ifndef IMAP_CLIENT_H
+#define IMAP_CLIENT_H
+
+#include "imap-commands.h"
+#include "message-size.h"
+
+#define CLIENT_COMMAND_QUEUE_MAX_SIZE 4
+/* Maximum number of CONTEXT=SEARCH UPDATEs. Clients probably won't need more
+ than a few, so this is mainly to avoid more or less accidental pointless
+ resource usage. */
+#define CLIENT_MAX_SEARCH_UPDATES 10
+
+struct client;
+struct mail_storage;
+struct mail_storage_service_ctx;
+struct lda_settings;
+struct imap_parser;
+struct imap_arg;
+struct imap_urlauth_context;
+
+struct mailbox_keywords {
+ /* All keyword names. The array itself exists in mail_index.
+ Keywords are currently only appended, they're never removed. */
+ const ARRAY_TYPE(keywords) *names;
+ /* Number of keywords announced to client via FLAGS/PERMANENTFLAGS.
+ This relies on keywords not being removed while mailbox is
+ selected. */
+ unsigned int announce_count;
+};
+
+struct imap_search_update {
+ char *tag;
+ struct mail_search_result *result;
+ bool return_uids;
+
+ pool_t fetch_pool;
+ struct imap_fetch_context *fetch_ctx;
+};
+
+enum client_command_state {
+ /* Waiting for more input */
+ CLIENT_COMMAND_STATE_WAIT_INPUT,
+ /* Waiting to be able to send more output */
+ CLIENT_COMMAND_STATE_WAIT_OUTPUT,
+ /* Waiting for external interaction */
+ CLIENT_COMMAND_STATE_WAIT_EXTERNAL,
+ /* Wait for other commands to finish execution */
+ CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY,
+ /* Waiting for other commands to finish so we can sync */
+ CLIENT_COMMAND_STATE_WAIT_SYNC,
+ /* Command is finished */
+ CLIENT_COMMAND_STATE_DONE
+};
+
+struct client_command_stats {
+ /* time when command handling was started - typically this is after
+ reading all the parameters. */
+ struct timeval start_time;
+ /* time when command handling was last finished. this is before
+ mailbox syncing is done. */
+ struct timeval last_run_timeval;
+ /* io_loop_get_wait_usecs()'s value when the command was started */
+ uint64_t start_ioloop_wait_usecs;
+ /* how many usecs this command itself has spent running */
+ uint64_t running_usecs;
+ /* how many usecs this command itself has spent waiting for locks */
+ uint64_t lock_wait_usecs;
+ /* how many bytes of client input/output command has used */
+ uint64_t bytes_in, bytes_out;
+};
+
+struct client_command_stats_start {
+ struct timeval timeval;
+ uint64_t lock_wait_usecs;
+ uint64_t bytes_in, bytes_out;
+};
+
+struct client_command_context {
+ struct client_command_context *prev, *next;
+ struct client *client;
+ struct event *event;
+ /* global_event is pushed to the global event stack while the command
+ is running. It has only the minimal fields that are actually wanted
+ to be in all the events while it's being run. */
+ struct event *global_event;
+
+ pool_t pool;
+ /* IMAP command tag */
+ const char *tag;
+ /* Name of this command */
+ const char *name;
+ /* Parameters for this command. These are generated from parsed IMAP
+ arguments, so they may not be exactly the same as how client sent
+ them. */
+ const char *args;
+ /* Parameters for this command generated with
+ imap_write_args_for_human(), so it's suitable for logging. */
+ const char *human_args;
+ enum command_flags cmd_flags;
+ const char *tagline_reply;
+
+ command_func_t *func;
+ void *context;
+
+ /* Module-specific contexts. */
+ ARRAY(union imap_module_context *) module_contexts;
+
+ struct imap_parser *parser;
+ enum client_command_state state;
+ struct client_command_stats stats;
+ struct client_command_stats_start stats_start;
+
+ struct imap_client_sync_context *sync;
+
+ bool uid:1; /* used UID command */
+ bool cancel:1; /* command is wanted to be cancelled */
+ bool param_error:1;
+ bool search_save_result:1; /* search result is being updated */
+ bool search_save_result_used:1; /* command uses search save */
+ bool temp_executed:1; /* temporary execution state tracking */
+ bool tagline_sent:1;
+ bool executing:1;
+};
+
+struct imap_client_vfuncs {
+ /* Perform client initialization. This is called when client creation is
+ finished completely. Particulary, at this point the namespaces are
+ fully initialized, which is not the case for the client create hook.
+ */
+ void (*init)(struct client *client);
+ /* Destroy the client.*/
+ void (*destroy)(struct client *client, const char *reason);
+
+ /* Send a tagged response line. */
+ void (*send_tagline)(struct client_command_context *cmd,
+ const char *data);
+ /* Run "mailbox syncing". This can send any unsolicited untagged
+ replies. Returns 1 = done, 0 = wait for more space in output buffer,
+ -1 = failed. */
+ int (*sync_notify_more)(struct imap_sync_context *ctx);
+
+ /* Export client state into buffer. Returns 1 if ok, 0 if some state
+ couldn't be preserved, -1 if temporary internal error occurred. */
+ int (*state_export)(struct client *client, bool internal,
+ buffer_t *dest, const char **error_r);
+ /* Import a single block of client state from the given data. Returns
+ number of bytes successfully imported from the block, or 0 if state
+ is corrupted or contains unknown data (e.g. some plugin is no longer
+ loaded), -1 if temporary internal error occurred. */
+ ssize_t (*state_import)(struct client *client, bool internal,
+ const unsigned char *data, size_t size,
+ const char **error_r);
+};
+
+struct client {
+ struct client *prev, *next;
+
+ struct imap_client_vfuncs v;
+ struct event *event;
+ const char *const *userdb_fields; /* for internal session saving/restoring */
+
+ int fd_in, fd_out;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_idle, *to_idle_output, *to_delayed_input;
+
+ pool_t pool;
+ struct mail_storage_service_user *service_user;
+ const struct imap_settings *set;
+ const struct smtp_submit_settings *smtp_set;
+ string_t *capability_string;
+ const char *disconnect_reason;
+
+ struct mail_user *user;
+ struct mailbox *mailbox;
+ struct mailbox_keywords keywords;
+ unsigned int sync_counter;
+ uint32_t messages_count, recent_count, uidvalidity;
+ ARRAY(bool) enabled_features;
+
+ time_t last_input, last_output;
+ unsigned int bad_counter;
+
+ /* one parser is kept here to be used for new commands */
+ struct imap_parser *free_parser;
+ /* command_pool is cleared when the command queue gets empty */
+ pool_t command_pool;
+ /* New commands are always prepended to the queue */
+ struct client_command_context *command_queue;
+ unsigned int command_queue_size;
+
+ char *last_cmd_name;
+ struct client_command_stats last_cmd_stats;
+
+ uint64_t sync_last_full_modseq;
+ uint64_t highest_fetch_modseq;
+ ARRAY_TYPE(seq_range) fetch_failed_uids;
+
+ /* For imap_logout_format statistics: */
+ unsigned int fetch_hdr_count, fetch_body_count;
+ uint64_t fetch_hdr_bytes, fetch_body_bytes;
+ unsigned int deleted_count, expunged_count, trashed_count;
+ unsigned int autoexpunged_count, append_count;
+
+ /* SEARCHRES extension: Last saved SEARCH result */
+ ARRAY_TYPE(seq_range) search_saved_uidset;
+ /* SEARCH=CONTEXT extension: Searches that get updated */
+ ARRAY(struct imap_search_update) search_updates;
+ /* NOTIFY extension */
+ struct imap_notify_context *notify_ctx;
+ uint32_t notify_uidnext;
+
+ /* client input/output is locked by this command */
+ struct client_command_context *input_lock;
+ struct client_command_context *output_cmd_lock;
+ /* command changing the mailbox */
+ struct client_command_context *mailbox_change_lock;
+
+ /* IMAP URLAUTH context (RFC4467) */
+ struct imap_urlauth_context *urlauth_ctx;
+
+ /* Module-specific contexts. */
+ ARRAY(union imap_module_context *) module_contexts;
+
+ /* syncing marks this TRUE when it sees \Deleted flags. this is by
+ EXPUNGE for Outlook-workaround. */
+ bool sync_seen_deletes:1;
+ bool logged_out:1;
+ bool disconnected:1;
+ bool hibernated:1;
+ bool unhibernated:1; /* client was created by unhibernation */
+ bool destroyed:1;
+ bool handling_input:1;
+ bool syncing:1;
+ bool id_logged:1;
+ bool mailbox_examined:1;
+ bool anvil_sent:1;
+ bool tls_compression:1;
+ bool input_skip_line:1; /* skip all the data until we've
+ found a new line */
+ bool modseqs_sent_since_sync:1;
+ bool notify_immediate_expunges:1;
+ bool notify_count_changes:1;
+ bool notify_flag_changes:1;
+ bool nonpermanent_modseqs:1;
+ bool state_import_bad_idle_done:1;
+ bool state_import_idle_continue:1;
+};
+
+struct imap_module_register {
+ unsigned int id;
+};
+
+union imap_module_context {
+ struct imap_client_vfuncs super;
+ struct imap_module_register *reg;
+};
+extern struct imap_module_register imap_module_register;
+
+extern struct client *imap_clients;
+extern unsigned int imap_client_count;
+
+extern unsigned int imap_feature_condstore;
+extern unsigned int imap_feature_qresync;
+
+/* Create new client with specified input/output handles. socket specifies
+ if the handle is a socket. */
+struct client *client_create(int fd_in, int fd_out, bool unhibernated,
+ struct event *event, struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct imap_settings *set,
+ const struct smtp_submit_settings *smtp_set);
+void client_create_finish_io(struct client *client);
+/* Finish creating the client. Returns 0 if ok, -1 if there's an error. */
+int client_create_finish(struct client *client, const char **error_r);
+void client_add_istream_prefix(struct client *client,
+ const unsigned char *data, size_t size);
+void client_destroy(struct client *client, const char *reason) ATTR_NULL(2);
+
+/* Disconnect client connection */
+void client_disconnect(struct client *client, const char *reason);
+void client_disconnect_with_error(struct client *client,
+ const char *client_error);
+
+/* Add the given capability to the CAPABILITY reply. If imap_capability setting
+ has an explicit capability, nothing is changed. */
+void client_add_capability(struct client *client, const char *capability);
+
+/* Send a line of data to client. */
+void client_send_line(struct client *client, const char *data);
+/* Send a line of data to client. Returns 1 if ok, 0 if buffer is getting full,
+ -1 if error. This should be used when you're (potentially) sending a lot of
+ lines to client. */
+int client_send_line_next(struct client *client, const char *data);
+/* Send line of data to client, prefixed with client->tag. You need to prefix
+ the data with "OK ", "NO " or "BAD ". */
+void client_send_tagline(struct client_command_context *cmd, const char *data);
+
+/* Send a BAD command reply to client via client_send_tagline(). If there have
+ been too many command errors, the client is disconnected. client_error may
+ be NULL, in which case the error is looked up from imap_parser. */
+void client_send_command_error(struct client_command_context *cmd,
+ const char *client_error);
+
+/* Send a NO command reply with the default internal error message to client
+ via client_send_tagline(). */
+void client_send_internal_error(struct client_command_context *cmd);
+
+/* Read a number of arguments. Returns TRUE if everything was read or
+ FALSE if either needs more data or error occurred. */
+bool client_read_args(struct client_command_context *cmd, unsigned int count,
+ unsigned int flags, const struct imap_arg **args_r);
+/* Reads a number of string arguments. ... is a list of pointers where to
+ store the arguments. */
+bool client_read_string_args(struct client_command_context *cmd,
+ unsigned int count, ...);
+void client_args_finished(struct client_command_context *cmd,
+ const struct imap_arg *args);
+
+/* SEARCHRES extension: Call if $ is being used/updated, returns TRUE if we
+ have to wait for an existing SEARCH SAVE to finish. */
+bool client_handle_search_save_ambiguity(struct client_command_context *cmd);
+
+void client_enable(struct client *client, unsigned int feature_idx);
+/* Returns TRUE if the given feature is enabled */
+bool client_has_enabled(struct client *client, unsigned int feature_idx);
+/* Returns mailbox features that are currently enabled. */
+enum mailbox_feature client_enabled_mailbox_features(struct client *client);
+/* Returns all enabled features as strings. */
+const char *const *client_enabled_features(struct client *client);
+
+/* Send client processing to imap-idle process. If successful, returns TRUE
+ and destroys the client. If hibernation failed, the exact reason is
+ returned (mainly for unit tests). */
+bool imap_client_hibernate(struct client **client, const char **reason_r);
+
+struct imap_search_update *
+client_search_update_lookup(struct client *client, const char *tag,
+ unsigned int *idx_r);
+void client_search_updates_free(struct client *client);
+
+struct client_command_context *client_command_alloc(struct client *client);
+void client_command_init_finished(struct client_command_context *cmd);
+void client_command_cancel(struct client_command_context **cmd);
+void client_command_free(struct client_command_context **cmd);
+
+bool client_handle_unfinished_cmd(struct client_command_context *cmd);
+/* Handle any pending command input. This must be run at the end of all
+ I/O callbacks after they've (potentially) finished some commands. */
+void client_continue_pending_input(struct client *client);
+void client_add_missing_io(struct client *client);
+const char *client_stats(struct client *client);
+
+void client_input(struct client *client);
+bool client_handle_input(struct client *client);
+int client_output(struct client *client);
+
+void clients_init(void);
+void clients_destroy_all(void);
+
+#endif
diff --git a/src/imap/imap-commands-util.c b/src/imap/imap-commands-util.c
new file mode 100644
index 0000000..2d454a7
--- /dev/null
+++ b/src/imap/imap-commands-util.c
@@ -0,0 +1,402 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "imap-resp-code.h"
+#include "imap-parser.h"
+#include "imap-sync.h"
+#include "imap-utf7.h"
+#include "imap-util.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "imap-commands-util.h"
+
+struct mail_namespace *
+client_find_namespace_full(struct client *client,
+ const char **mailbox, const char **client_error_r)
+{
+ struct mail_namespace *namespaces = client->user->namespaces;
+ struct mail_namespace *ns;
+ string_t *utf8_name;
+
+ utf8_name = t_str_new(64);
+ if (imap_utf7_to_utf8(*mailbox, utf8_name) < 0) {
+ *client_error_r = "NO Mailbox name is not valid mUTF-7";
+ return NULL;
+ }
+
+ ns = mail_namespace_find(namespaces, str_c(utf8_name));
+ if ((ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0 &&
+ ns->prefix_len == 0) {
+ /* this matched only the autocreated prefix="" namespace.
+ give a nice human-readable error message */
+ *client_error_r = t_strdup_printf(
+ "NO Client tried to access nonexistent namespace. "
+ "(Mailbox name should probably be prefixed with: %s)",
+ mail_namespace_find_inbox(namespaces)->prefix);
+ return NULL;
+ }
+
+ if ((client->set->parsed_workarounds &
+ WORKAROUND_TB_EXTRA_MAILBOX_SEP) != 0 &&
+ str_len(utf8_name) > 0 &&
+ str_c(utf8_name)[str_len(utf8_name)-1] == mail_namespace_get_sep(ns)) {
+ /* drop the extra trailing hierarchy separator */
+ str_truncate(utf8_name, str_len(utf8_name)-1);
+ }
+
+ *mailbox = str_c(utf8_name);
+ return ns;
+}
+
+struct mail_namespace *
+client_find_namespace(struct client_command_context *cmd, const char **mailbox)
+{
+ struct mail_namespace *ns;
+ const char *client_error;
+
+ ns = client_find_namespace_full(cmd->client, mailbox, &client_error);
+ if (ns == NULL)
+ client_send_tagline(cmd, client_error);
+ return ns;
+}
+
+bool client_verify_open_mailbox(struct client_command_context *cmd)
+{
+ if (cmd->client->mailbox != NULL) {
+ event_add_str(cmd->global_event, "mailbox",
+ mailbox_get_vname(cmd->client->mailbox));
+ return TRUE;
+ } else {
+ client_send_tagline(cmd, "BAD No mailbox selected.");
+ return FALSE;
+ }
+}
+
+void imap_client_close_mailbox(struct client *client)
+{
+ struct mailbox *box;
+
+ i_assert(client->mailbox != NULL);
+
+ if (array_is_created(&client->fetch_failed_uids))
+ array_clear(&client->fetch_failed_uids);
+ client_search_updates_free(client);
+ array_free(&client->search_saved_uidset);
+
+ box = client->mailbox;
+ client->mailbox = NULL;
+
+ mailbox_free(&box);
+ client_update_mailbox_flags(client, NULL);
+}
+
+int client_open_save_dest_box(struct client_command_context *cmd,
+ const char *name, struct mailbox **destbox_r)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *error_string;
+ enum mail_error error;
+
+ ns = client_find_namespace(cmd, &name);
+ if (ns == NULL)
+ return -1;
+
+ if (cmd->client->mailbox != NULL &&
+ mailbox_equals(cmd->client->mailbox, ns, name)) {
+ *destbox_r = cmd->client->mailbox;
+ return 0;
+ }
+ box = mailbox_alloc(ns->list, name, MAILBOX_FLAG_SAVEONLY);
+ if (mailbox_open(box) < 0) {
+ error_string = mailbox_get_last_error(box, &error);
+ if (error == MAIL_ERROR_NOTFOUND) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO [TRYCREATE] %s", error_string));
+ } else {
+ client_send_box_error(cmd, box);
+ }
+ mailbox_free(&box);
+ return -1;
+ }
+ if (mailbox_enable(box, client_enabled_mailbox_features(cmd->client)) < 0) {
+ client_send_box_error(cmd, box);
+ mailbox_free(&box);
+ return -1;
+ }
+ *destbox_r = box;
+ return 0;
+}
+
+const char *imap_client_command_get_reason(struct client_command_context *cmd)
+{
+ return cmd->args[0] == '\0' ? cmd->name :
+ t_strdup_printf("%s %s", cmd->name, cmd->human_args);
+}
+
+const char *
+imap_get_error_string(struct client_command_context *cmd,
+ const char *error_string, enum mail_error error)
+{
+ const char *resp_code = NULL;
+
+ switch (error) {
+ case MAIL_ERROR_NONE:
+ break;
+ case MAIL_ERROR_TEMP:
+ case MAIL_ERROR_LOOKUP_ABORTED: /* BUG: shouldn't be visible here */
+ resp_code = IMAP_RESP_CODE_SERVERBUG;
+ break;
+ case MAIL_ERROR_UNAVAILABLE:
+ resp_code = IMAP_RESP_CODE_UNAVAILABLE;
+ break;
+ case MAIL_ERROR_NOTPOSSIBLE:
+ case MAIL_ERROR_PARAMS:
+ resp_code = IMAP_RESP_CODE_CANNOT;
+ break;
+ case MAIL_ERROR_PERM:
+ resp_code = IMAP_RESP_CODE_NOPERM;
+ break;
+ case MAIL_ERROR_NOQUOTA:
+ resp_code = IMAP_RESP_CODE_OVERQUOTA;
+ break;
+ case MAIL_ERROR_NOTFOUND:
+ if ((cmd->cmd_flags & COMMAND_FLAG_USE_NONEXISTENT) != 0)
+ resp_code = IMAP_RESP_CODE_NONEXISTENT;
+ break;
+ case MAIL_ERROR_EXISTS:
+ resp_code = IMAP_RESP_CODE_ALREADYEXISTS;
+ break;
+ case MAIL_ERROR_EXPUNGED:
+ resp_code = IMAP_RESP_CODE_EXPUNGEISSUED;
+ break;
+ case MAIL_ERROR_INUSE:
+ resp_code = IMAP_RESP_CODE_INUSE;
+ break;
+ case MAIL_ERROR_CONVERSION:
+ case MAIL_ERROR_INVALIDDATA:
+ break;
+ case MAIL_ERROR_LIMIT:
+ resp_code = IMAP_RESP_CODE_LIMIT;
+ break;
+ }
+ if (resp_code == NULL || *error_string == '[')
+ return t_strconcat("NO ", error_string, NULL);
+ else
+ return t_strdup_printf("NO [%s] %s", resp_code, error_string);
+}
+
+void client_send_error(struct client_command_context *cmd,
+ const char *error_string, enum mail_error error)
+{
+ client_send_tagline(cmd, imap_get_error_string(cmd, error_string,
+ error));
+ client_disconnect_if_inconsistent(cmd->client);
+}
+
+void client_send_list_error(struct client_command_context *cmd,
+ struct mailbox_list *list)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ error_string = mailbox_list_get_last_error(list, &error);
+ client_send_tagline(cmd, imap_get_error_string(cmd, error_string,
+ error));
+}
+
+void client_disconnect_if_inconsistent(struct client *client)
+{
+ if (client->mailbox != NULL &&
+ mailbox_is_inconsistent(client->mailbox)) {
+ /* we can't do forced CLOSE, so have to disconnect */
+ client_disconnect_with_error(client,
+ "IMAP session state is inconsistent, please relogin.");
+ }
+}
+
+void client_send_box_error(struct client_command_context *cmd,
+ struct mailbox *box)
+{
+ client_send_storage_error(cmd, mailbox_get_storage(box));
+}
+
+void client_send_storage_error(struct client_command_context *cmd,
+ struct mail_storage *storage)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ error_string = mail_storage_get_last_error(storage, &error);
+ client_send_error(cmd, error_string, error);
+}
+
+void client_send_untagged_storage_error(struct client *client,
+ struct mail_storage *storage)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ error_string = mail_storage_get_last_error(storage, &error);
+ client_send_line(client, t_strconcat("* NO ", error_string, NULL));
+
+ client_disconnect_if_inconsistent(client);
+}
+
+bool client_parse_mail_flags(struct client_command_context *cmd,
+ const struct imap_arg *args,
+ enum mail_flags *flags_r,
+ const char *const **keywords_r)
+{
+ const char *atom;
+ enum mail_flags flag;
+ ARRAY(const char *) keywords;
+
+ *flags_r = 0;
+ *keywords_r = NULL;
+ p_array_init(&keywords, cmd->pool, 16);
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &atom)) {
+ client_send_command_error(cmd,
+ "Flags list contains non-atoms.");
+ return FALSE;
+ }
+
+ if (*atom == '\\') {
+ /* system flag */
+ atom = t_str_ucase(atom);
+ flag = imap_parse_system_flag(atom);
+ if (flag != 0 && flag != MAIL_RECENT)
+ *flags_r |= flag;
+ else {
+ client_send_command_error(cmd, t_strconcat(
+ "Invalid system flag ", atom, NULL));
+ return FALSE;
+ }
+ } else {
+ /* keyword validity checks are done by lib-storage */
+ array_push_back(&keywords, &atom);
+ }
+
+ args++;
+ }
+
+ if (array_count(&keywords) == 0)
+ *keywords_r = NULL;
+ else {
+ array_append_zero(&keywords); /* NULL-terminate */
+ *keywords_r = array_front(&keywords);
+ }
+ return TRUE;
+}
+
+void client_send_mailbox_flags(struct client *client, bool selecting)
+{
+ struct mailbox_status status;
+ unsigned int count = array_count(client->keywords.names);
+ const char *const *keywords;
+ string_t *str;
+
+ if (!selecting && count == client->keywords.announce_count) {
+ /* no changes to keywords and we're not selecting a mailbox */
+ return;
+ }
+
+ client->keywords.announce_count = count;
+ mailbox_get_open_status(client->mailbox, STATUS_PERMANENT_FLAGS,
+ &status);
+
+ keywords = count == 0 ? NULL :
+ array_front(client->keywords.names);
+ str = t_str_new(128);
+ str_append(str, "* FLAGS (");
+ imap_write_flags(str, status.flags, keywords);
+ str_append_c(str, ')');
+ client_send_line(client, str_c(str));
+
+ if (!status.permanent_keywords)
+ keywords = NULL;
+
+ str_truncate(str, 0);
+ str_append(str, "* OK [PERMANENTFLAGS (");
+ imap_write_flags(str, status.permanent_flags, keywords);
+ if (status.allow_new_keywords) {
+ if (status.permanent_flags != 0 || keywords != NULL)
+ str_append_c(str, ' ');
+ str_append(str, "\\*");
+ }
+ str_append(str, ")] ");
+
+ if (mailbox_is_readonly(client->mailbox))
+ str_append(str, "Read-only mailbox.");
+ else
+ str_append(str, "Flags permitted.");
+ client_send_line(client, str_c(str));
+}
+
+void client_update_mailbox_flags(struct client *client,
+ const ARRAY_TYPE(keywords) *keywords)
+{
+ client->keywords.names = keywords;
+ client->keywords.announce_count = 0;
+}
+
+const char *const *
+client_get_keyword_names(struct client *client, ARRAY_TYPE(keywords) *dest,
+ const ARRAY_TYPE(keyword_indexes) *src)
+{
+ unsigned int kw_index;
+ const char *const *all_names;
+ unsigned int all_count;
+
+ client_send_mailbox_flags(client, FALSE);
+
+ /* convert indexes to names */
+ all_names = array_get(client->keywords.names, &all_count);
+ array_clear(dest);
+ array_foreach_elem(src, kw_index) {
+ i_assert(kw_index < all_count);
+ array_push_back(dest, &all_names[kw_index]);
+ }
+
+ array_append_zero(dest);
+ return array_front(dest);
+}
+
+void msgset_generator_init(struct msgset_generator_context *ctx, string_t *str)
+{
+ i_zero(ctx);
+ ctx->str = str;
+ ctx->last_uid = (uint32_t)-1;
+}
+
+void msgset_generator_next(struct msgset_generator_context *ctx, uint32_t uid)
+{
+ i_assert(uid > 0);
+
+ if (uid-1 != ctx->last_uid) {
+ if (ctx->first_uid == 0)
+ ;
+ else if (ctx->first_uid == ctx->last_uid)
+ str_printfa(ctx->str, "%u,", ctx->first_uid);
+ else {
+ str_printfa(ctx->str, "%u:%u,",
+ ctx->first_uid, ctx->last_uid);
+ }
+ ctx->first_uid = uid;
+ }
+ ctx->last_uid = uid;
+}
+
+void msgset_generator_finish(struct msgset_generator_context *ctx)
+{
+ if (ctx->first_uid == ctx->last_uid)
+ str_printfa(ctx->str, "%u", ctx->first_uid);
+ else
+ str_printfa(ctx->str, "%u:%u", ctx->first_uid, ctx->last_uid);
+}
diff --git a/src/imap/imap-commands-util.h b/src/imap/imap-commands-util.h
new file mode 100644
index 0000000..997ed02
--- /dev/null
+++ b/src/imap/imap-commands-util.h
@@ -0,0 +1,81 @@
+#ifndef IMAP_COMMANDS_UTIL_H
+#define IMAP_COMMANDS_UTIL_H
+
+struct msgset_generator_context {
+ string_t *str;
+ uint32_t first_uid, last_uid;
+};
+
+struct mail_full_flags;
+struct mailbox_keywords;
+
+/* Finds namespace for given mailbox from namespaces. If namespace isn't found
+ or mailbox name is invalid, sends a tagged NO reply to client. */
+struct mail_namespace *
+client_find_namespace(struct client_command_context *cmd, const char **mailbox);
+struct mail_namespace *
+client_find_namespace_full(struct client *client,
+ const char **mailbox, const char **client_error_r);
+
+/* Returns TRUE if mailbox is selected. If not, sends "No mailbox selected"
+ error message to client. */
+bool client_verify_open_mailbox(struct client_command_context *cmd);
+/* Close the selected mailbox. */
+void imap_client_close_mailbox(struct client *client);
+
+/* Open APPEND/COPY destination mailbox. */
+int client_open_save_dest_box(struct client_command_context *cmd,
+ const char *name, struct mailbox **destbox_r);
+
+/* Returns string based in IMAP command name and parameters. */
+const char *imap_client_command_get_reason(struct client_command_context *cmd);
+/* Set transaction's reason to the IMAP command name and parameters. */
+void imap_transaction_set_cmd_reason(struct mailbox_transaction_context *trans,
+ struct client_command_context *cmd);
+const char *
+imap_get_error_string(struct client_command_context *cmd,
+ const char *error_string, enum mail_error error);
+
+void client_disconnect_if_inconsistent(struct client *client);
+
+/* Send an explicit error message to client. */
+void client_send_error(struct client_command_context *cmd,
+ const char *error_string, enum mail_error error);
+/* Send last mailbox list error message to client. */
+void client_send_list_error(struct client_command_context *cmd,
+ struct mailbox_list *list);
+/* Send last mail storage error message to client. */
+void client_send_storage_error(struct client_command_context *cmd,
+ struct mail_storage *storage);
+/* Send last mailbox's storage error message to client. */
+void client_send_box_error(struct client_command_context *cmd,
+ struct mailbox *box);
+
+/* Send untagged error message to client. */
+void client_send_untagged_storage_error(struct client *client,
+ struct mail_storage *storage);
+
+/* Parse flags. Returns TRUE if successful, if not sends an error message to
+ client. */
+bool client_parse_mail_flags(struct client_command_context *cmd,
+ const struct imap_arg *args,
+ enum mail_flags *flags_r,
+ const char *const **keywords_r);
+
+/* Send FLAGS + PERMANENTFLAGS to client if they have changed,
+ or if selecting=TRUE. */
+void client_send_mailbox_flags(struct client *client, bool selecting);
+/* Update client->keywords array. Use keywords=NULL when unselecting. */
+void client_update_mailbox_flags(struct client *client,
+ const ARRAY_TYPE(keywords) *keywords)
+ ATTR_NULL(2);
+/* Convert keyword indexes to keyword names in selected mailbox. */
+const char *const *
+client_get_keyword_names(struct client *client, ARRAY_TYPE(keywords) *dest,
+ const ARRAY_TYPE(keyword_indexes) *src);
+
+void msgset_generator_init(struct msgset_generator_context *ctx, string_t *str);
+void msgset_generator_next(struct msgset_generator_context *ctx, uint32_t uid);
+void msgset_generator_finish(struct msgset_generator_context *ctx);
+
+#endif
diff --git a/src/imap/imap-commands.c b/src/imap/imap-commands.c
new file mode 100644
index 0000000..b78d0a1
--- /dev/null
+++ b/src/imap/imap-commands.c
@@ -0,0 +1,247 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "imap-commands.h"
+
+
+struct command_hook {
+ command_hook_callback_t *pre;
+ command_hook_callback_t *post;
+};
+
+static const struct command imap4rev1_commands[] = {
+ { "CAPABILITY", cmd_capability, 0 },
+ { "LOGOUT", cmd_logout, COMMAND_FLAG_BREAKS_MAILBOX },
+ { "NOOP", cmd_noop, COMMAND_FLAG_BREAKS_SEQS },
+
+ { "APPEND", cmd_append, COMMAND_FLAG_BREAKS_SEQS |
+ /* finish syncing and sending
+ all tagged commands before
+ we wait for APPEND input */
+ COMMAND_FLAG_BREAKS_MAILBOX },
+ { "EXAMINE", cmd_examine, COMMAND_FLAG_BREAKS_MAILBOX },
+ { "CREATE", cmd_create, 0 },
+ { "DELETE", cmd_delete, COMMAND_FLAG_BREAKS_MAILBOX |
+ COMMAND_FLAG_USE_NONEXISTENT },
+ { "RENAME", cmd_rename, COMMAND_FLAG_USE_NONEXISTENT },
+ { "LIST", cmd_list, 0 },
+ { "LSUB", cmd_lsub, 0 },
+ { "SELECT", cmd_select, COMMAND_FLAG_BREAKS_MAILBOX },
+ { "STATUS", cmd_status, 0 },
+ { "SUBSCRIBE", cmd_subscribe, 0 },
+ { "UNSUBSCRIBE", cmd_unsubscribe, COMMAND_FLAG_USE_NONEXISTENT },
+
+ { "CHECK", cmd_check, COMMAND_FLAG_BREAKS_SEQS },
+ { "CLOSE", cmd_close, COMMAND_FLAG_BREAKS_MAILBOX },
+ { "COPY", cmd_copy, COMMAND_FLAG_USES_SEQS |
+ COMMAND_FLAG_BREAKS_SEQS },
+ { "EXPUNGE", cmd_expunge, COMMAND_FLAG_BREAKS_SEQS },
+ { "FETCH", cmd_fetch, COMMAND_FLAG_USES_SEQS },
+ { "SEARCH", cmd_search, COMMAND_FLAG_USES_SEQS },
+ { "STORE", cmd_store, COMMAND_FLAG_USES_SEQS },
+ { "UID COPY", cmd_copy, COMMAND_FLAG_BREAKS_SEQS },
+ { "UID FETCH", cmd_fetch, COMMAND_FLAG_BREAKS_SEQS },
+ { "UID SEARCH", cmd_search, COMMAND_FLAG_BREAKS_SEQS },
+ { "UID STORE", cmd_store, COMMAND_FLAG_BREAKS_SEQS }
+};
+#define IMAP4REV1_COMMANDS_COUNT N_ELEMENTS(imap4rev1_commands)
+
+static const struct command imap_ext_commands[] = {
+ /* IMAP extensions: */
+ { "CANCELUPDATE", cmd_cancelupdate,0 },
+ { "ENABLE", cmd_enable, 0 },
+ { "ID", cmd_id, 0 },
+ { "IDLE", cmd_idle, COMMAND_FLAG_BREAKS_SEQS |
+ COMMAND_FLAG_REQUIRES_SYNC |
+ /* finish syncing and sending
+ all tagged commands before
+ IDLE is started */
+ COMMAND_FLAG_BREAKS_MAILBOX },
+ { "GETMETADATA", cmd_getmetadata, 0 },
+ { "SETMETADATA", cmd_setmetadata, 0 },
+ { "NAMESPACE", cmd_namespace, 0 },
+ { "NOTIFY", cmd_notify, COMMAND_FLAG_BREAKS_SEQS },
+ { "SORT", cmd_sort, COMMAND_FLAG_USES_SEQS },
+ { "THREAD", cmd_thread, COMMAND_FLAG_USES_SEQS },
+ { "UID EXPUNGE", cmd_uid_expunge, COMMAND_FLAG_BREAKS_SEQS },
+ { "MOVE", cmd_move, COMMAND_FLAG_USES_SEQS |
+ COMMAND_FLAG_BREAKS_SEQS },
+ { "UID MOVE", cmd_move, COMMAND_FLAG_BREAKS_SEQS },
+ { "UID SORT", cmd_sort, COMMAND_FLAG_BREAKS_SEQS },
+ { "UID THREAD", cmd_thread, COMMAND_FLAG_BREAKS_SEQS },
+ { "UNSELECT", cmd_unselect, COMMAND_FLAG_BREAKS_MAILBOX },
+ { "X-CANCEL", cmd_x_cancel, 0 },
+ { "X-STATE", cmd_x_state, COMMAND_FLAG_REQUIRES_SYNC },
+ { "XLIST", cmd_list, 0 },
+ /* IMAP URLAUTH (RFC4467): */
+ { "GENURLAUTH", cmd_genurlauth, 0 },
+ { "RESETKEY", cmd_resetkey, 0 },
+ { "URLFETCH", cmd_urlfetch, 0 }
+};
+#define IMAP_EXT_COMMANDS_COUNT N_ELEMENTS(imap_ext_commands)
+
+ARRAY_TYPE(command) imap_commands;
+static bool commands_unsorted;
+static ARRAY(struct command_hook) command_hooks;
+
+void command_register(const char *name, command_func_t *func,
+ enum command_flags flags)
+{
+ struct command cmd;
+
+ i_zero(&cmd);
+ cmd.name = name;
+ cmd.func = func;
+ cmd.flags = flags;
+ array_push_back(&imap_commands, &cmd);
+
+ commands_unsorted = TRUE;
+}
+
+void command_unregister(const char *name)
+{
+ const struct command *cmd;
+ unsigned int i, count;
+
+ cmd = array_get(&imap_commands, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(cmd[i].name, name) == 0) {
+ array_delete(&imap_commands, i, 1);
+ return;
+ }
+ }
+
+ i_error("Trying to unregister unknown command '%s'", name);
+}
+
+void command_register_array(const struct command *cmdarr, unsigned int count)
+{
+ commands_unsorted = TRUE;
+ array_append(&imap_commands, cmdarr, count);
+}
+
+void command_unregister_array(const struct command *cmdarr, unsigned int count)
+{
+ while (count > 0) {
+ command_unregister(cmdarr->name);
+ count--; cmdarr++;
+ }
+}
+
+void command_hook_register(command_hook_callback_t *pre,
+ command_hook_callback_t *post)
+{
+ struct command_hook hook;
+
+ hook.pre = pre;
+ hook.post = post;
+ array_push_back(&command_hooks, &hook);
+}
+
+void command_hook_unregister(command_hook_callback_t *pre,
+ command_hook_callback_t *post)
+{
+ const struct command_hook *hooks;
+ unsigned int i, count;
+
+ hooks = array_get(&command_hooks, &count);
+ for (i = 0; i < count; i++) {
+ if (hooks[i].pre == pre && hooks[i].post == post) {
+ array_delete(&command_hooks, i, 1);
+ return;
+ }
+ }
+ i_panic("command_hook_unregister(): hook not registered");
+}
+
+void command_stats_start(struct client_command_context *cmd)
+{
+ cmd->stats_start.timeval = ioloop_timeval;
+ cmd->stats_start.lock_wait_usecs = file_lock_wait_get_total_usecs();
+ cmd->stats_start.bytes_in = i_stream_get_absolute_offset(cmd->client->input);
+ cmd->stats_start.bytes_out = cmd->client->output->offset;
+}
+
+void command_stats_flush(struct client_command_context *cmd)
+{
+ io_loop_time_refresh();
+ cmd->stats.running_usecs +=
+ timeval_diff_usecs(&ioloop_timeval, &cmd->stats_start.timeval);
+ cmd->stats.lock_wait_usecs +=
+ file_lock_wait_get_total_usecs() -
+ cmd->stats_start.lock_wait_usecs;
+ cmd->stats.bytes_in += i_stream_get_absolute_offset(cmd->client->input) -
+ cmd->stats_start.bytes_in;
+ cmd->stats.bytes_out += cmd->client->output->offset -
+ cmd->stats_start.bytes_out;
+ /* allow flushing multiple times */
+ command_stats_start(cmd);
+}
+
+bool command_exec(struct client_command_context *cmd)
+{
+ const struct command_hook *hook;
+ bool finished;
+
+ i_assert(!cmd->executing);
+
+ io_loop_time_refresh();
+ command_stats_start(cmd);
+
+ event_push_global(cmd->global_event);
+ cmd->executing = TRUE;
+ array_foreach(&command_hooks, hook)
+ hook->pre(cmd);
+ finished = cmd->func(cmd);
+ array_foreach(&command_hooks, hook)
+ hook->post(cmd);
+ cmd->executing = FALSE;
+ event_pop_global(cmd->global_event);
+ if (cmd->state == CLIENT_COMMAND_STATE_DONE)
+ finished = TRUE;
+
+ command_stats_flush(cmd);
+ return finished;
+}
+
+static int command_cmp(const struct command *c1, const struct command *c2)
+{
+ return strcasecmp(c1->name, c2->name);
+}
+
+static int command_bsearch(const char *name, const struct command *cmd)
+{
+ return strcasecmp(name, cmd->name);
+}
+
+struct command *command_find(const char *name)
+{
+ if (commands_unsorted) {
+ array_sort(&imap_commands, command_cmp);
+ commands_unsorted = FALSE;
+ }
+
+ return array_bsearch(&imap_commands, name, command_bsearch);
+}
+
+void commands_init(void)
+{
+ i_array_init(&imap_commands, 64);
+ i_array_init(&command_hooks, 4);
+ commands_unsorted = FALSE;
+
+ command_register_array(imap4rev1_commands, IMAP4REV1_COMMANDS_COUNT);
+ command_register_array(imap_ext_commands, IMAP_EXT_COMMANDS_COUNT);
+}
+
+void commands_deinit(void)
+{
+ array_free(&imap_commands);
+ array_free(&command_hooks);
+}
diff --git a/src/imap/imap-commands.h b/src/imap/imap-commands.h
new file mode 100644
index 0000000..651a6df
--- /dev/null
+++ b/src/imap/imap-commands.h
@@ -0,0 +1,137 @@
+#ifndef IMAP_COMMANDS_H
+#define IMAP_COMMANDS_H
+
+struct client_command_context;
+
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "imap-parser.h"
+#include "imap-sync.h"
+#include "imap-commands-util.h"
+
+typedef bool command_func_t(struct client_command_context *cmd);
+typedef void command_hook_callback_t(struct client_command_context *ctx);
+
+enum command_flags {
+ /* Command uses sequences as its input parameters */
+ COMMAND_FLAG_USES_SEQS = 0x01,
+ /* Command may reply with EXPUNGE, causing sequences to break */
+ COMMAND_FLAG_BREAKS_SEQS = 0x02,
+ /* Command changes the mailbox */
+ COMMAND_FLAG_BREAKS_MAILBOX = 0x04 | COMMAND_FLAG_BREAKS_SEQS,
+
+ /* Command uses selected mailbox */
+ COMMAND_FLAG_USES_MAILBOX = COMMAND_FLAG_BREAKS_MAILBOX |
+ COMMAND_FLAG_USES_SEQS,
+
+ /* Command requires mailbox syncing before it can do its job. */
+ COMMAND_FLAG_REQUIRES_SYNC = 0x08,
+ /* Command allows replying with [NONEXISTENT] imap resp code.
+ Dovecot internally returns it for all kinds of commands,
+ but unfortunately RFC 5530 specifies it only for "delete something"
+ operations. */
+ COMMAND_FLAG_USE_NONEXISTENT = 0x10
+};
+
+struct command {
+ const char *name;
+ command_func_t *func;
+
+ enum command_flags flags;
+};
+ARRAY_DEFINE_TYPE(command, struct command);
+
+extern ARRAY_TYPE(command) imap_commands;
+
+/* Register command. Given name parameter must be permanently stored until
+ command is unregistered. */
+void command_register(const char *name, command_func_t *func,
+ enum command_flags flags);
+void command_unregister(const char *name);
+
+/* Register array of commands. */
+void command_register_array(const struct command *cmdarr, unsigned int count);
+void command_unregister_array(const struct command *cmdarr, unsigned int count);
+
+/* Register hook callbacks that are called before and after all commands */
+void command_hook_register(command_hook_callback_t *pre,
+ command_hook_callback_t *post);
+void command_hook_unregister(command_hook_callback_t *pre,
+ command_hook_callback_t *post);
+/* Execute command and hooks */
+bool command_exec(struct client_command_context *cmd);
+/* Starts counting command statistics. */
+void command_stats_start(struct client_command_context *cmd);
+/* Finish counting command statistics. This is called automatically when
+ command_exec() returns, but it should be called explicitly if the stats are
+ needed during command_exec(). */
+void command_stats_flush(struct client_command_context *cmd);
+
+struct command *command_find(const char *name);
+
+void commands_init(void);
+void commands_deinit(void);
+
+/* IMAP4rev1 commands: */
+
+/* Non-Authenticated State */
+bool cmd_logout(struct client_command_context *cmd);
+
+bool cmd_capability(struct client_command_context *cmd);
+bool cmd_noop(struct client_command_context *cmd);
+
+/* Authenticated State */
+bool cmd_select(struct client_command_context *cmd);
+bool cmd_examine(struct client_command_context *cmd);
+
+bool cmd_create(struct client_command_context *cmd);
+bool cmd_delete(struct client_command_context *cmd);
+bool cmd_rename(struct client_command_context *cmd);
+
+bool cmd_subscribe(struct client_command_context *cmd);
+bool cmd_unsubscribe(struct client_command_context *cmd);
+
+bool cmd_list(struct client_command_context *cmd);
+bool cmd_lsub(struct client_command_context *cmd);
+
+bool cmd_status(struct client_command_context *cmd);
+bool cmd_append(struct client_command_context *cmd);
+
+/* Selected state */
+bool cmd_check(struct client_command_context *cmd);
+bool cmd_close(struct client_command_context *cmd);
+bool cmd_expunge(struct client_command_context *cmd);
+bool cmd_search(struct client_command_context *cmd);
+bool cmd_fetch(struct client_command_context *cmd);
+bool cmd_store(struct client_command_context *cmd);
+bool cmd_copy(struct client_command_context *cmd);
+bool cmd_uid(struct client_command_context *cmd);
+
+/* IMAP extensions: */
+bool cmd_cancelupdate(struct client_command_context *cmd);
+bool cmd_enable(struct client_command_context *cmd);
+bool cmd_id(struct client_command_context *cmd);
+bool cmd_idle(struct client_command_context *cmd);
+bool cmd_namespace(struct client_command_context *cmd);
+bool cmd_getmetadata(struct client_command_context *cmd);
+bool cmd_setmetadata(struct client_command_context *cmd);
+bool cmd_notify(struct client_command_context *cmd);
+bool cmd_sort(struct client_command_context *cmd);
+bool cmd_thread(struct client_command_context *cmd);
+bool cmd_uid_expunge(struct client_command_context *cmd);
+bool cmd_move(struct client_command_context *cmd);
+bool cmd_unselect(struct client_command_context *cmd);
+bool cmd_x_cancel(struct client_command_context *cmd);
+bool cmd_x_state(struct client_command_context *cmd);
+
+/* IMAP URLAUTH (RFC4467): */
+bool cmd_genurlauth(struct client_command_context *cmd);
+bool cmd_resetkey(struct client_command_context *cmd);
+bool cmd_urlfetch(struct client_command_context *cmd);
+
+/* private: */
+bool cmd_list_full(struct client_command_context *cmd, bool lsub);
+bool cmd_select_full(struct client_command_context *cmd, bool readonly);
+bool cmd_subscribe_full(struct client_command_context *cmd, bool subscribe);
+
+#endif
diff --git a/src/imap/imap-common.h b/src/imap/imap-common.h
new file mode 100644
index 0000000..ec23879
--- /dev/null
+++ b/src/imap/imap-common.h
@@ -0,0 +1,40 @@
+#ifndef IMAP_COMMON_H
+#define IMAP_COMMON_H
+
+/* Disconnect client after idling this many milliseconds */
+#define CLIENT_IDLE_TIMEOUT_MSECS (60*30*1000)
+
+/* If we can't send anything to client for this long, disconnect the client */
+#define CLIENT_OUTPUT_TIMEOUT_MSECS (5*60*1000)
+
+/* Stop buffering more data into output stream after this many bytes */
+#define CLIENT_OUTPUT_OPTIMAL_SIZE 2048
+
+/* Disconnect client when it sends too many bad commands in a row */
+#define CLIENT_MAX_BAD_COMMANDS 20
+
+#include "lib.h"
+#include "imap-client.h"
+#include "imap-settings.h"
+
+struct mail_storage_service_input;
+
+typedef void imap_client_created_func_t(struct client **client);
+
+extern imap_client_created_func_t *hook_client_created;
+extern bool imap_debug;
+extern struct event_category event_category_imap;
+
+/* Sets the hook_client_created and returns the previous hook,
+ which the new_hook should call if it's non-NULL. */
+imap_client_created_func_t * ATTR_NOWARN_UNUSED_RESULT
+imap_client_created_hook_set(imap_client_created_func_t *new_hook);
+
+void imap_refresh_proctitle(void);
+void imap_refresh_proctitle_delayed(void);
+
+int client_create_from_input(const struct mail_storage_service_input *input,
+ int fd_in, int fd_out, bool unhibernated,
+ struct client **client_r, const char **error_r);
+
+#endif
diff --git a/src/imap/imap-expunge.c b/src/imap/imap-expunge.c
new file mode 100644
index 0000000..ff1a213
--- /dev/null
+++ b/src/imap/imap-expunge.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "mail-storage.h"
+#include "mail-search-build.h"
+#include "imap-search-args.h"
+#include "imap-expunge.h"
+
+#define IMAP_EXPUNGE_BATCH_SIZE 1000
+
+/* get a seqset of all the mails with \Deleted */
+static int imap_expunge_get_seqset(struct mailbox *box,
+ struct mail_search_arg *next_search_arg,
+ ARRAY_TYPE(seq_range) *seqset)
+{
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ int ret;
+
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_args->args->type = SEARCH_FLAGS;
+ search_args->args->value.flags = MAIL_DELETED;
+ search_args->args->next = next_search_arg;
+
+ /* Refresh the flags so we'll expunge all messages marked as \Deleted
+ by any session. */
+ t = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_REFRESH,
+ "EXPUNGE");
+ ctx = mailbox_search_init(t, search_args, NULL, 0, NULL);
+
+ /* collect the seqs into a seqset */
+ while (mailbox_search_next(ctx, &mail))
+ seq_range_array_add(seqset, mail->seq);
+
+ ret = mailbox_search_deinit(&ctx);
+ /* commit in case a plugin made changes - failures should not abort the expunge */
+ (void) mailbox_transaction_commit(&t);
+ mail_search_args_unref(&search_args);
+
+ if (ret < 0)
+ array_free(seqset);
+
+ return ret;
+}
+
+int imap_expunge(struct mailbox *box, struct mail_search_arg *next_search_arg,
+ unsigned int *expunged_count)
+{
+ struct imap_search_seqset_iter *seqset_iter;
+ struct mail_search_args *search_args;
+ struct mailbox_status status;
+ bool expunges = FALSE;
+ int ret;
+
+ if (mailbox_is_readonly(box)) {
+ /* silently ignore */
+ return 0;
+ }
+
+ mailbox_get_open_status(box, STATUS_MESSAGES, &status);
+
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_args->args->type = SEARCH_SEQSET;
+ p_array_init(&search_args->args->value.seqset, search_args->pool, 16);
+
+ if (imap_expunge_get_seqset(box, next_search_arg,
+ &search_args->args->value.seqset) < 0) {
+ mail_search_args_unref(&search_args);
+ return -1;
+ }
+
+ seqset_iter = imap_search_seqset_iter_init(search_args, status.messages,
+ IMAP_EXPUNGE_BATCH_SIZE);
+
+ do {
+ struct mailbox_transaction_context *t;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+
+ t = mailbox_transaction_begin(box, 0, "EXPUNGE");
+ ctx = mailbox_search_init(t, search_args, NULL, 0, NULL);
+
+ while (mailbox_search_next(ctx, &mail)) {
+ *expunged_count += 1;
+ mail_expunge(mail);
+ expunges = TRUE;
+ }
+
+ ret = mailbox_search_deinit(&ctx);
+ if (ret < 0) {
+ mailbox_transaction_rollback(&t);
+ break;
+ } else {
+ ret = mailbox_transaction_commit(&t);
+ if (ret < 0)
+ break;
+ }
+ } while (imap_search_seqset_iter_next(seqset_iter));
+
+ imap_search_seqset_iter_deinit(&seqset_iter);
+ mail_search_args_unref(&search_args);
+
+ if (ret < 0)
+ return ret;
+
+ return expunges ? 1 : 0;
+}
diff --git a/src/imap/imap-expunge.h b/src/imap/imap-expunge.h
new file mode 100644
index 0000000..e5b3825
--- /dev/null
+++ b/src/imap/imap-expunge.h
@@ -0,0 +1,10 @@
+#ifndef IMAP_EXPUNGE_H
+#define IMAP_EXPUNGE_H
+
+struct mail_search_arg;
+
+int imap_expunge(struct mailbox *box, struct mail_search_arg *next_search_arg,
+ unsigned int *expunged_count)
+ ATTR_NULL(2);
+
+#endif
diff --git a/src/imap/imap-feature.c b/src/imap/imap-feature.c
new file mode 100644
index 0000000..d8d52b8
--- /dev/null
+++ b/src/imap/imap-feature.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "imap-feature.h"
+
+static ARRAY_TYPE(imap_feature) feature_register = ARRAY_INIT;
+
+bool imap_feature_lookup(const char *name, unsigned int *feature_idx_r)
+{
+ for (unsigned int idx = 0; idx < array_count(&feature_register); idx++) {
+ const struct imap_feature *feat =
+ array_idx(&feature_register, idx);
+ if (strcasecmp(name, feat->feature) == 0) {
+ *feature_idx_r = idx;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+const struct imap_feature *imap_feature_idx(unsigned int feature_idx)
+{
+ return array_idx(&feature_register, feature_idx);
+}
+
+unsigned int
+imap_feature_register(const char *feature, enum mailbox_feature mailbox_features,
+ imap_client_enable_callback_t *callback)
+{
+ struct imap_feature *feat = array_append_space(&feature_register);
+ feat->feature = feature;
+ feat->mailbox_features = mailbox_features;
+ feat->callback = callback;
+ return array_count(&feature_register)-1;
+}
+
+void imap_features_init(void)
+{
+ i_assert(!array_is_created(&feature_register));
+ i_array_init(&feature_register, 8);
+}
+
+void imap_features_deinit(void)
+{
+ array_free(&feature_register);
+}
diff --git a/src/imap/imap-feature.h b/src/imap/imap-feature.h
new file mode 100644
index 0000000..a96aa6e
--- /dev/null
+++ b/src/imap/imap-feature.h
@@ -0,0 +1,24 @@
+#ifndef IMAP_FEATURE_H
+#define IMAP_FEATURE_H
+
+typedef void imap_client_enable_callback_t(struct client *);
+
+struct imap_feature {
+ const char *feature;
+ enum mailbox_feature mailbox_features;
+ imap_client_enable_callback_t *callback;
+ bool enabled;
+};
+ARRAY_DEFINE_TYPE(imap_feature, struct imap_feature);
+
+bool imap_feature_lookup(const char *name, unsigned int *feature_idx_r);
+const struct imap_feature *imap_feature_idx(unsigned int feature_idx);
+
+unsigned int
+imap_feature_register(const char *feature, enum mailbox_feature mailbox_features,
+ imap_client_enable_callback_t *callback);
+
+void imap_features_init(void);
+void imap_features_deinit(void);
+
+#endif
diff --git a/src/imap/imap-fetch-body.c b/src/imap/imap-fetch-body.c
new file mode 100644
index 0000000..b467334
--- /dev/null
+++ b/src/imap/imap-fetch-body.c
@@ -0,0 +1,722 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "buffer.h"
+#include "str.h"
+#include "strescape.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-header-filter.h"
+#include "message-parser.h"
+#include "mail-storage-private.h"
+#include "imap-quote.h"
+#include "imap-parser.h"
+#include "imap-msgpart.h"
+#include "imap-fetch.h"
+
+#include <ctype.h>
+#include <unistd.h>
+
+struct imap_fetch_body_data {
+ const char *section; /* NOTE: always uppercased */
+ struct imap_msgpart *msgpart;
+
+ bool partial:1;
+ bool binary:1;
+ bool binary_size:1;
+};
+
+struct imap_fetch_preview_data {
+ /* If TRUE, lazy modifier is specified. */
+ bool lazy:1;
+ /* Uses the pre-RFC 8970 standard (requires algorithm to be returned
+ * as part of FETCH response). */
+ bool old_standard:1;
+ /* If TRUE, uses "PREVIEW" command; if FALSE, uses older "SNIPPET"
+ * command. */
+ bool preview_cmd:1;
+};
+
+static void fetch_read_error(struct imap_fetch_context *ctx,
+ const char **disconnect_reason_r)
+{
+ struct imap_fetch_state *state = &ctx->state;
+
+ if (state->cur_input->stream_errno == ENOENT) {
+ if (state->cur_mail->expunged) {
+ *disconnect_reason_r = "Mail expunged while it was being FETCHed";
+ return;
+ }
+ }
+ mail_set_critical(state->cur_mail,
+ "read(%s) failed: %s (FETCH %s)",
+ i_stream_get_name(state->cur_input),
+ i_stream_get_error(state->cur_input),
+ state->cur_human_name);
+ *disconnect_reason_r = "FETCH read() failed";
+}
+
+static const char *get_body_name(const struct imap_fetch_body_data *body)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ if (body->binary_size)
+ str_append(str, "BINARY.SIZE");
+ else if (body->binary)
+ str_append(str, "BINARY");
+ else
+ str_append(str, "BODY");
+ str_printfa(str, "[%s]", body->section);
+ if (body->partial) {
+ str_printfa(str, "<%"PRIuUOFF_T">",
+ imap_msgpart_get_partial_offset(body->msgpart));
+ }
+ return str_c(str);
+}
+
+static string_t *get_prefix(struct imap_fetch_state *state,
+ const struct imap_fetch_body_data *body,
+ uoff_t size, bool has_nuls)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ if (state->cur_first)
+ state->cur_first = FALSE;
+ else
+ str_append_c(str, ' ');
+
+ str_append(str, get_body_name(body));
+
+ if (size == UOFF_T_MAX)
+ str_append(str, " NIL");
+ else if (has_nuls && body->binary)
+ str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", size);
+ else
+ str_printfa(str, " {%"PRIuUOFF_T"}\r\n", size);
+ return str;
+}
+
+static int fetch_stream_continue(struct imap_fetch_context *ctx)
+{
+ struct imap_fetch_state *state = &ctx->state;
+ const char *disconnect_reason;
+ uoff_t orig_input_offset = state->cur_input->v_offset;
+ enum ostream_send_istream_result res;
+
+ o_stream_set_max_buffer_size(ctx->client->output, 0);
+ res = o_stream_send_istream(ctx->client->output, state->cur_input);
+ o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX);
+
+ if (ctx->state.cur_stats_sizep != NULL) {
+ *ctx->state.cur_stats_sizep +=
+ state->cur_input->v_offset - orig_input_offset;
+ }
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ if (state->cur_input->v_offset != state->cur_size) {
+ /* Input stream gave less data than expected */
+ mail_set_cache_corrupted(state->cur_mail,
+ state->cur_size_field, t_strdup_printf(
+ "read(%s): FETCH %s got too little data: "
+ "%"PRIuUOFF_T" vs %"PRIuUOFF_T,
+ i_stream_get_name(state->cur_input),
+ state->cur_human_name,
+ state->cur_input->v_offset, state->cur_size));
+ client_disconnect(ctx->client, "FETCH failed");
+ return -1;
+ }
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ fetch_read_error(ctx, &disconnect_reason);
+ client_disconnect(ctx->client, disconnect_reason);
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* client disconnected */
+ return -1;
+ }
+ i_unreached();
+}
+
+static const char *
+get_body_human_name(pool_t pool, struct imap_fetch_body_data *body)
+{
+ string_t *str;
+ uoff_t partial_offset, partial_size;
+
+ str = t_str_new(64);
+ if (body->binary)
+ str_append(str, "BINARY[");
+ else
+ str_append(str, "BODY[");
+ str_append(str, body->section);
+ str_append_c(str, ']');
+
+ partial_offset = imap_msgpart_get_partial_offset(body->msgpart);
+ partial_size = imap_msgpart_get_partial_size(body->msgpart);
+ if (partial_offset != 0 || partial_size != UOFF_T_MAX) {
+ str_printfa(str, "<%"PRIuUOFF_T, partial_offset);
+ if (partial_size != UOFF_T_MAX)
+ str_printfa(str, ".%"PRIuUOFF_T, partial_size);
+ str_append_c(str, '>');
+ }
+ return p_strdup(pool, str_c(str));
+}
+
+static void fetch_state_update_stats(struct imap_fetch_context *ctx,
+ const struct imap_msgpart *msgpart)
+{
+ if (!imap_msgpart_contains_body(msgpart)) {
+ ctx->client->fetch_hdr_count++;
+ ctx->state.cur_stats_sizep = &ctx->client->fetch_hdr_bytes;
+ } else {
+ ctx->client->fetch_body_count++;
+ ctx->state.cur_stats_sizep = &ctx->client->fetch_body_bytes;
+ }
+}
+
+static int fetch_body_msgpart(struct imap_fetch_context *ctx, struct mail *mail,
+ struct imap_fetch_body_data *body)
+{
+ struct imap_msgpart_open_result result;
+ string_t *str;
+
+ if (mail == NULL) {
+ imap_msgpart_free(&body->msgpart);
+ return 1;
+ }
+
+ if (imap_msgpart_open(mail, body->msgpart, &result) < 0)
+ return -1;
+ i_assert(result.input->v_offset == 0);
+ ctx->state.cur_input = result.input;
+ ctx->state.cur_size = result.size;
+ ctx->state.cur_size_field = result.size_field;
+ ctx->state.cur_human_name = get_body_human_name(ctx->ctx_pool, body);
+
+ fetch_state_update_stats(ctx, body->msgpart);
+ str = get_prefix(&ctx->state, body, ctx->state.cur_size,
+ result.binary_decoded_input_has_nuls);
+ o_stream_nsend(ctx->client->output, str_data(str), str_len(str));
+
+ ctx->state.cont_handler = fetch_stream_continue;
+ return ctx->state.cont_handler(ctx);
+}
+
+static int fetch_binary_size(struct imap_fetch_context *ctx, struct mail *mail,
+ struct imap_fetch_body_data *body)
+{
+ string_t *str;
+ uoff_t size;
+
+ if (mail == NULL) {
+ imap_msgpart_free(&body->msgpart);
+ return 1;
+ }
+
+ if (imap_msgpart_size(mail, body->msgpart, &size) < 0)
+ return -1;
+
+ str = t_str_new(128);
+ if (ctx->state.cur_first)
+ ctx->state.cur_first = FALSE;
+ else
+ str_append_c(str, ' ');
+ str_printfa(str, "%s %"PRIuUOFF_T, get_body_name(body), size);
+
+ if (o_stream_send(ctx->client->output, str_data(str), str_len(str)) < 0)
+ return -1;
+ return 1;
+}
+
+/* Parse next digits in string into integer. Returns -1 if the integer
+ becomes too big and wraps. */
+static int read_uoff_t(const char **p, uoff_t *value)
+{
+ return str_parse_uoff(*p, value, p);
+}
+
+static int
+body_header_fields_parse(struct imap_fetch_init_context *ctx,
+ struct imap_fetch_body_data *body, const char *prefix,
+ const struct imap_arg *args, unsigned int args_count)
+{
+ string_t *str;
+ const char *value;
+ size_t i;
+
+ ctx->fetch_ctx->fetch_header_fields = TRUE;
+
+ str = str_new(ctx->pool, 128);
+ str_append(str, prefix);
+ str_append(str, " (");
+
+ for (i = 0; i < args_count; i++) {
+ if (!imap_arg_get_astring(&args[i], &value)) {
+ ctx->error = "Invalid BODY[..] parameter: "
+ "Header list contains non-strings";
+ return -1;
+ }
+ value = t_str_ucase(value);
+
+ if (i != 0)
+ str_append_c(str, ' ');
+
+ if (args[i].type == IMAP_ARG_ATOM)
+ str_append(str, value);
+ else
+ imap_append_quoted(str, value);
+ }
+ str_append_c(str, ')');
+ body->section = str_c(str);
+ return 0;
+}
+
+static int body_parse_partial(struct imap_fetch_body_data *body,
+ const char *p, const char **error_r)
+{
+ uoff_t offset, size = UOFF_T_MAX;
+
+ if (*p == '\0')
+ return 0;
+ /* <start.end> */
+ if (*p != '<') {
+ *error_r = "Unexpected data after ']'";
+ return -1;
+ }
+
+ p++;
+ body->partial = TRUE;
+
+ if (read_uoff_t(&p, &offset) < 0 || offset > OFF_T_MAX) {
+ /* wrapped */
+ *error_r = "Too big partial start";
+ return -1;
+ }
+
+ if (*p == '.') {
+ p++;
+ if (read_uoff_t(&p, &size) < 0 || size > OFF_T_MAX) {
+ /* wrapped */
+ *error_r = "Too big partial end";
+ return -1;
+ }
+ }
+
+ if (*p != '>') {
+ *error_r = "Missing '>' in partial";
+ return -1;
+ }
+ if (p[1] != '\0') {
+ *error_r = "Unexpected data after '>'";
+ return -1;
+ }
+ imap_msgpart_set_partial(body->msgpart, offset, size);
+ return 0;
+}
+
+bool imap_fetch_body_section_init(struct imap_fetch_init_context *ctx)
+{
+ struct imap_fetch_body_data *body;
+ const struct imap_arg *list_args;
+ unsigned int list_count;
+ const char *str, *p, *error;
+
+ i_assert(str_begins(ctx->name, "BODY"));
+ p = ctx->name + 4;
+
+ body = p_new(ctx->pool, struct imap_fetch_body_data, 1);
+
+ if (str_begins(p, ".PEEK"))
+ p += 5;
+ else
+ ctx->fetch_ctx->flags_update_seen = TRUE;
+ if (*p != '[') {
+ ctx->error = "Invalid BODY[..] parameter: Missing '['";
+ return FALSE;
+ }
+
+ if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) {
+ /* BODY[HEADER.FIELDS.. (headers list)] */
+ if (!imap_arg_get_atom(&ctx->args[1], &str) ||
+ str[0] != ']') {
+ ctx->error = "Invalid BODY[..] parameter: Missing ']'";
+ return FALSE;
+ }
+ if (body_header_fields_parse(ctx, body, p+1,
+ list_args, list_count) < 0)
+ return FALSE;
+ p = str+1;
+ ctx->args += 2;
+ } else {
+ /* no headers list */
+ body->section = p+1;
+ p = strchr(body->section, ']');
+ if (p == NULL) {
+ ctx->error = "Invalid BODY[..] parameter: Missing ']'";
+ return FALSE;
+ }
+ body->section = p_strdup_until(ctx->pool, body->section, p);
+ p++;
+ }
+ if (imap_msgpart_parse(body->section, &body->msgpart) < 0) {
+ ctx->error = "Invalid BODY[..] section";
+ return FALSE;
+ }
+ ctx->fetch_ctx->fetch_data |=
+ imap_msgpart_get_fetch_data(body->msgpart);
+ imap_msgpart_get_wanted_headers(body->msgpart, &ctx->fetch_ctx->all_headers);
+
+ if (body_parse_partial(body, p, &error) < 0) {
+ ctx->error = p_strdup_printf(ctx->pool,
+ "Invalid BODY[..] parameter: %s", error);
+ return FALSE;
+ }
+
+ /* update the section name for the imap_fetch_add_handler() */
+ ctx->name = p_strdup(ctx->pool, get_body_name(body));
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT,
+ "NIL", fetch_body_msgpart, body);
+ return TRUE;
+}
+
+bool imap_fetch_binary_init(struct imap_fetch_init_context *ctx)
+{
+ struct imap_fetch_body_data *body;
+ const struct imap_arg *list_args;
+ unsigned int list_count;
+ const char *str, *p, *error;
+
+ i_assert(str_begins(ctx->name, "BINARY"));
+ p = ctx->name + 6;
+
+ body = p_new(ctx->pool, struct imap_fetch_body_data, 1);
+ body->binary = TRUE;
+
+ if (str_begins(p, ".SIZE")) {
+ /* fetch decoded size of the section */
+ p += 5;
+ body->binary_size = TRUE;
+ } else if (str_begins(p, ".PEEK")) {
+ p += 5;
+ } else {
+ ctx->fetch_ctx->flags_update_seen = TRUE;
+ }
+ if (*p != '[') {
+ ctx->error = "Invalid BINARY[..] parameter: Missing '['";
+ return FALSE;
+ }
+
+ if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) {
+ /* BINARY[HEADER.FIELDS.. (headers list)] */
+ if (!imap_arg_get_atom(&ctx->args[1], &str) ||
+ str[0] != ']') {
+ ctx->error = "Invalid BINARY[..] parameter: Missing ']'";
+ return FALSE;
+ }
+ if (body_header_fields_parse(ctx, body, p+1,
+ list_args, list_count) < 0)
+ return FALSE;
+ p = str+1;
+ ctx->args += 2;
+ } else {
+ /* no headers list */
+ body->section = p+1;
+ p = strchr(body->section, ']');
+ if (p == NULL) {
+ ctx->error = "Invalid BINARY[..] parameter: Missing ']'";
+ return FALSE;
+ }
+ body->section = p_strdup_until(ctx->pool, body->section, p);
+ p++;
+ }
+ if (imap_msgpart_parse(body->section, &body->msgpart) < 0) {
+ ctx->error = "Invalid BINARY[..] section";
+ return FALSE;
+ }
+ imap_msgpart_set_decode_to_binary(body->msgpart);
+ ctx->fetch_ctx->fetch_data |=
+ imap_msgpart_get_fetch_data(body->msgpart);
+
+ if (!body->binary_size) {
+ if (body_parse_partial(body, p, &error) < 0) {
+ ctx->error = p_strdup_printf(ctx->pool,
+ "Invalid BINARY[..] parameter: %s", error);
+ return FALSE;
+ }
+ }
+
+ /* update the section name for the imap_fetch_add_handler() */
+ ctx->name = p_strdup(ctx->pool, get_body_name(body));
+ if (body->binary_size) {
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT,
+ "0", fetch_binary_size, body);
+ } else {
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT,
+ "NIL", fetch_body_msgpart, body);
+ }
+ return TRUE;
+}
+
+static int ATTR_NULL(3)
+fetch_rfc822_size(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ uoff_t size;
+
+ if (mail_get_virtual_size(mail, &size) < 0)
+ return -1;
+
+ str_printfa(ctx->state.cur_str, "RFC822.SIZE %"PRIuUOFF_T" ", size);
+ return 1;
+}
+
+static int
+fetch_and_free_msgpart(struct imap_fetch_context *ctx,
+ struct mail *mail, struct imap_msgpart **_msgpart)
+{
+ struct imap_msgpart_open_result result;
+ int ret;
+
+ ret = imap_msgpart_open(mail, *_msgpart, &result);
+ imap_msgpart_free(_msgpart);
+ if (ret < 0)
+ return -1;
+ i_assert(result.input->v_offset == 0);
+ ctx->state.cur_input = result.input;
+ ctx->state.cur_size = result.size;
+ ctx->state.cur_size_field = result.size_field;
+ ctx->state.cont_handler = fetch_stream_continue;
+ return 0;
+}
+
+static int ATTR_NULL(3)
+fetch_rfc822(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ struct imap_msgpart *msgpart;
+ const char *str;
+
+ msgpart = imap_msgpart_full();
+ fetch_state_update_stats(ctx, msgpart);
+ if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0)
+ return -1;
+
+ str = t_strdup_printf(" RFC822 {%"PRIuUOFF_T"}\r\n",
+ ctx->state.cur_size);
+ if (ctx->state.cur_first) {
+ str++; ctx->state.cur_first = FALSE;
+ }
+ o_stream_nsend_str(ctx->client->output, str);
+
+ ctx->state.cur_human_name = "RFC822";
+ return ctx->state.cont_handler(ctx);
+}
+
+static int ATTR_NULL(3)
+fetch_rfc822_header(struct imap_fetch_context *ctx,
+ struct mail *mail, void *context ATTR_UNUSED)
+{
+ struct imap_msgpart *msgpart;
+ const char *str;
+
+ msgpart = imap_msgpart_header();
+ fetch_state_update_stats(ctx, msgpart);
+ if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0)
+ return -1;
+
+ str = t_strdup_printf(" RFC822.HEADER {%"PRIuUOFF_T"}\r\n",
+ ctx->state.cur_size);
+ if (ctx->state.cur_first) {
+ str++; ctx->state.cur_first = FALSE;
+ }
+ o_stream_nsend_str(ctx->client->output, str);
+
+ ctx->state.cur_human_name = "RFC822.HEADER";
+ return ctx->state.cont_handler(ctx);
+}
+
+static int ATTR_NULL(3)
+fetch_rfc822_text(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ struct imap_msgpart *msgpart;
+ const char *str;
+
+ msgpart = imap_msgpart_body();
+ fetch_state_update_stats(ctx, msgpart);
+ if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0)
+ return -1;
+
+ str = t_strdup_printf(" RFC822.TEXT {%"PRIuUOFF_T"}\r\n",
+ ctx->state.cur_size);
+ if (ctx->state.cur_first) {
+ str++; ctx->state.cur_first = FALSE;
+ }
+ o_stream_nsend_str(ctx->client->output, str);
+
+ ctx->state.cur_human_name = "RFC822.TEXT";
+ return ctx->state.cont_handler(ctx);
+}
+
+bool imap_fetch_rfc822_init(struct imap_fetch_init_context *ctx)
+{
+ const char *name = ctx->name;
+
+ if (name[6] == '\0') {
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY;
+ ctx->fetch_ctx->flags_update_seen = TRUE;
+ imap_fetch_add_handler(ctx, 0, "NIL",
+ fetch_rfc822, NULL);
+ return TRUE;
+ }
+
+ if (strcmp(name+6, ".SIZE") == 0) {
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_VIRTUAL_SIZE;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "0", fetch_rfc822_size, NULL);
+ return TRUE;
+ }
+ if (strcmp(name+6, ".HEADER") == 0) {
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_HEADER;
+ imap_fetch_add_handler(ctx, 0, "NIL",
+ fetch_rfc822_header, NULL);
+ return TRUE;
+ }
+ if (strcmp(name+6, ".TEXT") == 0) {
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_BODY;
+ ctx->fetch_ctx->flags_update_seen = TRUE;
+ imap_fetch_add_handler(ctx, 0, "NIL",
+ fetch_rfc822_text, NULL);
+ return TRUE;
+ }
+
+ ctx->error = t_strconcat("Unknown parameter ", name, NULL);
+ return FALSE;
+}
+
+static int ATTR_NULL(3)
+fetch_snippet(struct imap_fetch_context *ctx, struct mail *mail,
+ struct imap_fetch_preview_data *preview)
+{
+ enum mail_lookup_abort temp_lookup_abort = preview->lazy ? MAIL_LOOKUP_ABORT_NOT_IN_CACHE_START_CACHING : mail->lookup_abort;
+ enum mail_lookup_abort orig_lookup_abort = mail->lookup_abort;
+ const char *resp, *snippet;
+ int ret;
+
+ mail->lookup_abort = temp_lookup_abort;
+ ret = mail_get_special(mail, MAIL_FETCH_BODY_SNIPPET, &snippet);
+ mail->lookup_abort = orig_lookup_abort;
+
+ resp = preview->preview_cmd ? "PREVIEW" : "SNIPPET";
+
+ if (ret == 0) {
+ /* got it => nothing to do */
+ snippet++; /* skip over snippet version byte */
+ } else if (mailbox_get_last_mail_error(mail->box) != MAIL_ERROR_LOOKUP_ABORTED) {
+ /* actual error => bail */
+ return -1;
+ } else {
+ /*
+ * Two ways to get here:
+ * - not in cache && lazy => give up
+ * - not in cache && !lazy => someone higher up set
+ * MAIL_LOOKUP_ABORT_NOT_IN_CACHE and so even though we got
+ * a non-lazy request we failed the cache lookup.
+ *
+ * This is not an error, but since the scenario is
+ * sufficiently convoluted this else branch serves to
+ * document it.
+ *
+ * This path will return NIL as the preview response.
+ */
+ }
+
+ str_append(ctx->state.cur_str, resp);
+ if (preview->old_standard)
+ str_append(ctx->state.cur_str, " (FUZZY ");
+ else
+ str_append(ctx->state.cur_str, " ");
+ if (ret == 0)
+ imap_append_string(ctx->state.cur_str, snippet);
+ else
+ str_append(ctx->state.cur_str, "NIL");
+ if (preview->old_standard)
+ str_append(ctx->state.cur_str, ")");
+ str_append_c(ctx->state.cur_str, ' ');
+
+ return 1;
+}
+
+static bool fetch_preview_init(struct imap_fetch_init_context *ctx,
+ bool preview_cmd)
+{
+ const struct imap_arg *list_args;
+ unsigned int list_count;
+ struct imap_fetch_preview_data *preview;
+
+ preview = p_new(ctx->pool, struct imap_fetch_preview_data, 1);
+ preview->preview_cmd = preview_cmd;
+ /* We can tell we are using the "Old" (pre-RFC 8970) standard
+ * if:
+ * 1) SNIPPET command is used
+ * 2) FUZZY algorithm is explicitly requested
+ * The one usage we cannot catch is if PREVIEW command is used, and
+ * no algorithm is specified - the pre-RFC standard requires that the
+ * algorithm must be output as part of the response; the RFC standard
+ * does not output the algorithm. There is unfortunately nothing we
+ * can do about this, so we need to send the standards compliant
+ * way. */
+ preview->old_standard = !preview_cmd;
+
+ if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) {
+ unsigned int i;
+
+ for (i = 0; i < list_count; i++) {
+ const char *str;
+
+ if (!imap_arg_get_atom(&list_args[i], &str)) {
+ ctx->error = "Invalid PREVIEW modifier";
+ return FALSE;
+ }
+
+ if (strcasecmp(str, "LAZY") == 0) {
+ preview->lazy = TRUE;
+ } else if (strcasecmp(str, "LAZY=FUZZY") == 0) {
+ preview->lazy = TRUE;
+ preview->old_standard = TRUE;
+ } else if (strcasecmp(str, "FUZZY") == 0) {
+ preview->old_standard = TRUE;
+ } else {
+ ctx->error = t_strdup_printf("'%s' is not a "
+ "supported PREVIEW modifier",
+ str);
+ return FALSE;
+ }
+ }
+
+ ctx->args += list_count;
+ }
+
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_BODY_SNIPPET;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "NIL", fetch_snippet, preview);
+ return TRUE;
+}
+
+bool imap_fetch_preview_init(struct imap_fetch_init_context *ctx)
+{
+ return fetch_preview_init(ctx, TRUE);
+}
+
+bool imap_fetch_snippet_init(struct imap_fetch_init_context *ctx)
+{
+ return fetch_preview_init(ctx, FALSE);
+}
diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c
new file mode 100644
index 0000000..db811f7
--- /dev/null
+++ b/src/imap/imap-fetch.c
@@ -0,0 +1,1040 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "message-size.h"
+#include "imap-date.h"
+#include "imap-utf7.h"
+#include "mail-search-build.h"
+#include "imap-commands.h"
+#include "imap-quote.h"
+#include "imap-fetch.h"
+#include "imap-util.h"
+
+#include <ctype.h>
+
+#define BODY_NIL_REPLY \
+ "\"text\" \"plain\" NIL NIL NIL \"7bit\" 0 0"
+#define ENVELOPE_NIL_REPLY \
+ "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)"
+
+static ARRAY(struct imap_fetch_handler) fetch_handlers;
+
+static int imap_fetch_handler_cmp(const struct imap_fetch_handler *h1,
+ const struct imap_fetch_handler *h2)
+{
+ return strcmp(h1->name, h2->name);
+}
+
+void imap_fetch_handlers_register(const struct imap_fetch_handler *handlers,
+ size_t count)
+{
+ array_append(&fetch_handlers, handlers, count);
+ array_sort(&fetch_handlers, imap_fetch_handler_cmp);
+}
+
+void imap_fetch_handler_unregister(const char *name)
+{
+ const struct imap_fetch_handler *handler, *first_handler;
+
+ first_handler = array_front(&fetch_handlers);
+ handler = imap_fetch_handler_lookup(name);
+ i_assert(handler != NULL);
+ array_delete(&fetch_handlers, handler - first_handler, 1);
+}
+
+static int
+imap_fetch_handler_bsearch(const char *name, const struct imap_fetch_handler *h)
+{
+ return strcmp(name, h->name);
+}
+
+const struct imap_fetch_handler *imap_fetch_handler_lookup(const char *name)
+{
+ return array_bsearch(&fetch_handlers, name, imap_fetch_handler_bsearch);
+}
+
+bool imap_fetch_init_handler(struct imap_fetch_init_context *init_ctx)
+{
+ const struct imap_fetch_handler *handler;
+ const char *lookup_name, *p;
+
+ for (p = init_ctx->name; i_isalnum(*p) || *p == '-'; p++) ;
+ lookup_name = t_strdup_until(init_ctx->name, p);
+
+ handler = array_bsearch(&fetch_handlers, lookup_name,
+ imap_fetch_handler_bsearch);
+ if (handler == NULL) {
+ init_ctx->error = t_strdup_printf("Unknown parameter: %s",
+ init_ctx->name);
+ return FALSE;
+ }
+ if (!handler->init(init_ctx)) {
+ i_assert(init_ctx->error != NULL);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void imap_fetch_init_nofail_handler(struct imap_fetch_context *ctx,
+ bool (*init)(struct imap_fetch_init_context *))
+{
+ struct imap_fetch_init_context init_ctx;
+
+ i_zero(&init_ctx);
+ init_ctx.fetch_ctx = ctx;
+ init_ctx.pool = ctx->ctx_pool;
+
+ if (!init(&init_ctx))
+ i_unreached();
+}
+
+int imap_fetch_att_list_parse(struct client *client, pool_t pool,
+ const struct imap_arg *list,
+ struct imap_fetch_context **fetch_ctx_r,
+ const char **client_error_r)
+{
+ struct imap_fetch_init_context init_ctx;
+ const char *str;
+
+ i_zero(&init_ctx);
+ init_ctx.fetch_ctx = imap_fetch_alloc(client, pool, "NOTIFY");
+ init_ctx.pool = pool;
+ init_ctx.args = list;
+
+ while (imap_arg_get_atom(init_ctx.args, &str)) {
+ init_ctx.name = t_str_ucase(str);
+ init_ctx.args++;
+ if (!imap_fetch_init_handler(&init_ctx)) {
+ *client_error_r = t_strconcat("Invalid fetch-att list: ",
+ init_ctx.error, NULL);
+ imap_fetch_free(&init_ctx.fetch_ctx);
+ return -1;
+ }
+ }
+ if (!IMAP_ARG_IS_EOL(init_ctx.args)) {
+ *client_error_r = "fetch-att list contains non-atoms.";
+ imap_fetch_free(&init_ctx.fetch_ctx);
+ return -1;
+ }
+ *fetch_ctx_r = init_ctx.fetch_ctx;
+ return 0;
+}
+
+struct imap_fetch_context *
+imap_fetch_alloc(struct client *client, pool_t pool, const char *reason)
+{
+ struct imap_fetch_context *ctx;
+
+ ctx = p_new(pool, struct imap_fetch_context, 1);
+ ctx->client = client;
+ ctx->ctx_pool = pool;
+ ctx->reason = p_strdup(pool, reason);
+ pool_ref(pool);
+
+ p_array_init(&ctx->all_headers, pool, 64);
+ p_array_init(&ctx->handlers, pool, 16);
+ p_array_init(&ctx->tmp_keywords, pool,
+ client->keywords.announce_count + 8);
+ return ctx;
+}
+
+#undef imap_fetch_add_handler
+void imap_fetch_add_handler(struct imap_fetch_init_context *ctx,
+ enum imap_fetch_handler_flags flags,
+ const char *nil_reply,
+ imap_fetch_handler_t *handler, void *context)
+{
+ /* partially because of broken clients, but also partially because
+ it potentially can make client implementations faster, we have a
+ buffered parameter which basically means that the handler promises
+ to write the output in fetch_ctx->state.cur_str. The cur_str is then
+ sent to client before calling any non-buffered handlers.
+
+ We try to keep the handler registration order the same as the
+ client requested them. This is especially useful to get UID
+ returned first, which some clients rely on..
+ */
+ const struct imap_fetch_context_handler *ctx_handler;
+ struct imap_fetch_context_handler h;
+
+ if (context == NULL) {
+ /* don't allow duplicate handlers */
+ array_foreach(&ctx->fetch_ctx->handlers, ctx_handler) {
+ if (ctx_handler->handler == handler &&
+ ctx_handler->context == NULL)
+ return;
+ }
+ }
+
+ i_zero(&h);
+ h.handler = handler;
+ h.context = context;
+ h.buffered = (flags & IMAP_FETCH_HANDLER_FLAG_BUFFERED) != 0;
+ h.want_deinit = (flags & IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT) != 0;
+ h.name = p_strdup(ctx->pool, ctx->name);
+ h.nil_reply = p_strdup(ctx->pool, nil_reply);
+
+ if (!h.buffered)
+ array_push_back(&ctx->fetch_ctx->handlers, &h);
+ else {
+ array_insert(&ctx->fetch_ctx->handlers,
+ ctx->fetch_ctx->buffered_handlers_count, &h, 1);
+ ctx->fetch_ctx->buffered_handlers_count++;
+ }
+}
+
+static void
+expunges_drop_known(struct mailbox *box,
+ const struct imap_fetch_qresync_args *qresync_args,
+ struct mailbox_transaction_context *trans,
+ ARRAY_TYPE(seq_range) *expunged_uids)
+{
+ struct mailbox_status status;
+ struct mail *mail;
+ const uint32_t *seqs, *uids;
+ unsigned int i, count;
+
+ seqs = array_get(qresync_args->qresync_sample_seqset, &count);
+ uids = array_front(qresync_args->qresync_sample_uidset);
+ i_assert(array_count(qresync_args->qresync_sample_uidset) == count);
+ i_assert(count > 0);
+
+ mailbox_get_open_status(box, STATUS_MESSAGES, &status);
+ mail = mail_alloc(trans, 0, NULL);
+
+ /* FIXME: we could do removals from the middle as well */
+ for (i = 0; i < count && seqs[i] <= status.messages; i++) {
+ mail_set_seq(mail, seqs[i]);
+ if (uids[i] != mail->uid)
+ break;
+ }
+ if (i > 0)
+ seq_range_array_remove_range(expunged_uids, 1, uids[i-1]);
+ mail_free(&mail);
+}
+
+static int
+get_expunges_fallback(struct mailbox *box,
+ const struct imap_fetch_qresync_args *qresync_args,
+ const ARRAY_TYPE(seq_range) *uid_filter_arr,
+ ARRAY_TYPE(seq_range) *expunged_uids)
+{
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ const struct seq_range *uid_filter;
+ struct mailbox_status status;
+ unsigned int i, count;
+ uint32_t next_uid;
+ int ret = 0;
+
+ uid_filter = array_get(uid_filter_arr, &count);
+ i_assert(count > 0);
+ i = 0;
+ next_uid = uid_filter[0].seq1;
+
+ /* search UIDs only in given range */
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_args->args->type = SEARCH_UIDSET;
+ p_array_init(&search_args->args->value.seqset, search_args->pool, count);
+ array_append_array(&search_args->args->value.seqset, uid_filter_arr);
+
+ trans = mailbox_transaction_begin(box, 0, "FETCH send VANISHED");
+ search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (mail->uid == next_uid) {
+ if (next_uid < uid_filter[i].seq2)
+ next_uid++;
+ else if (++i < count)
+ next_uid = uid_filter[i].seq1;
+ else
+ break;
+ } else {
+ /* next_uid .. mail->uid-1 are expunged */
+ i_assert(mail->uid > next_uid);
+ while (mail->uid > uid_filter[i].seq2) {
+ seq_range_array_add_range(expunged_uids,
+ next_uid,
+ uid_filter[i].seq2);
+ i++;
+ i_assert(i < count);
+ next_uid = uid_filter[i].seq1;
+ }
+ if (next_uid != mail->uid) {
+ seq_range_array_add_range(expunged_uids,
+ next_uid,
+ mail->uid - 1);
+ }
+ if (uid_filter[i].seq2 != mail->uid)
+ next_uid = mail->uid + 1;
+ else if (++i < count)
+ next_uid = uid_filter[i].seq1;
+ else
+ break;
+ }
+ }
+ if (i < count) {
+ i_assert(next_uid <= uid_filter[i].seq2);
+ seq_range_array_add_range(expunged_uids, next_uid,
+ uid_filter[i].seq2);
+ i++;
+ }
+ for (; i < count; i++) {
+ seq_range_array_add_range(expunged_uids, uid_filter[i].seq1,
+ uid_filter[i].seq2);
+ }
+
+ mailbox_get_open_status(box, STATUS_UIDNEXT, &status);
+ seq_range_array_remove_range(expunged_uids, status.uidnext,
+ (uint32_t)-1);
+
+ if (mailbox_search_deinit(&search_ctx) < 0)
+ ret = -1;
+
+ if (ret == 0 && qresync_args->qresync_sample_seqset != NULL &&
+ array_is_created(qresync_args->qresync_sample_seqset))
+ expunges_drop_known(box, qresync_args, trans, expunged_uids);
+
+ (void)mailbox_transaction_commit(&trans);
+ return ret;
+}
+
+int imap_fetch_send_vanished(struct client *client, struct mailbox *box,
+ const struct mail_search_args *search_args,
+ const struct imap_fetch_qresync_args *qresync_args)
+{
+ const struct mail_search_arg *uidarg = search_args->args;
+ const struct mail_search_arg *modseqarg = uidarg->next;
+ const ARRAY_TYPE(seq_range) *uid_filter;
+ uint64_t modseq;
+ ARRAY_TYPE(seq_range) expunged_uids_range;
+ string_t *str;
+ int ret = 0;
+
+ i_assert(uidarg->type == SEARCH_UIDSET);
+ i_assert(modseqarg->type == SEARCH_MODSEQ);
+
+ uid_filter = &uidarg->value.seqset;
+ modseq = modseqarg->value.modseq->modseq - 1;
+
+ i_array_init(&expunged_uids_range, array_count(uid_filter));
+ if (!mailbox_get_expunged_uids(box, modseq, uid_filter, &expunged_uids_range)) {
+ /* return all expunged UIDs */
+ if (get_expunges_fallback(box, qresync_args, uid_filter,
+ &expunged_uids_range) < 0) {
+ array_clear(&expunged_uids_range);
+ ret = -1;
+ }
+ }
+ if (array_count(&expunged_uids_range) > 0) {
+ str = str_new(default_pool, 128);
+ str_append(str, "* VANISHED (EARLIER) ");
+ imap_write_seq_range(str, &expunged_uids_range);
+ str_append(str, "\r\n");
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ str_free(&str);
+ }
+ array_free(&expunged_uids_range);
+ return ret;
+}
+
+static void imap_fetch_init(struct imap_fetch_context *ctx)
+{
+ if (ctx->initialized)
+ return;
+ ctx->initialized = TRUE;
+
+ if (ctx->flags_update_seen && !ctx->flags_have_handler) {
+ ctx->flags_show_only_seen_changes = TRUE;
+ imap_fetch_init_nofail_handler(ctx, imap_fetch_flags_init);
+ }
+ if ((ctx->fetch_data &
+ (MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY)) != 0)
+ ctx->fetch_data |= MAIL_FETCH_NUL_STATE;
+}
+
+void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box,
+ struct mail_search_args *search_args)
+{
+ enum mailbox_transaction_flags trans_flags =
+ MAILBOX_TRANSACTION_FLAG_REFRESH;
+ struct mailbox_header_lookup_ctx *wanted_headers = NULL;
+ const char *const *headers;
+
+ i_assert(!ctx->state.fetching);
+
+ imap_fetch_init(ctx);
+ i_zero(&ctx->state);
+
+ if (array_count(&ctx->all_headers) > 0 &&
+ ((ctx->fetch_data & (MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY)) == 0)) {
+ array_append_zero(&ctx->all_headers);
+
+ headers = array_front(&ctx->all_headers);
+ wanted_headers = mailbox_header_lookup_init(box, headers);
+ array_pop_back(&ctx->all_headers);
+ }
+
+ if (ctx->flags_update_seen) {
+ /* Hide the implicit \Seen flag addition. Otherwise a separate
+ untagged FETCH FLAGS (\Seen) would be sent on top of the
+ one FLAGS (\Seen) already added in the main FETCH reply.
+
+ We don't set this always, because some plugins might want
+ to do their own flag changes which we don't want hidden.
+ (Of course this isn't perfect since if implicit \Seen flags
+ are added, other flag changes are also hidden.) */
+ trans_flags |= MAILBOX_TRANSACTION_FLAG_HIDE;
+ }
+ ctx->state.trans = mailbox_transaction_begin(box, trans_flags,
+ ctx->reason);
+
+ mail_search_args_init(search_args, box, TRUE,
+ &ctx->client->search_saved_uidset);
+ ctx->state.search_ctx =
+ mailbox_search_init(ctx->state.trans, search_args, NULL,
+ ctx->fetch_data, wanted_headers);
+ ctx->state.cur_str = str_new(default_pool, 8192);
+ ctx->state.fetching = TRUE;
+
+ mailbox_header_lookup_unref(&wanted_headers);
+}
+
+static int imap_fetch_flush_buffer(struct imap_fetch_context *ctx)
+{
+ const unsigned char *data;
+ size_t len;
+
+ data = str_data(ctx->state.cur_str);
+ len = str_len(ctx->state.cur_str);
+
+ if (len == 0)
+ return 0;
+
+ /* there's an extra space at the end if we added any fetch items
+ to buffer */
+ if (data[len-1] == ' ') {
+ len--;
+ ctx->state.cur_first = FALSE;
+ }
+ ctx->state.line_partial = TRUE;
+
+ if (o_stream_send(ctx->client->output, data, len) < 0)
+ return -1;
+
+ str_truncate(ctx->state.cur_str, 0);
+ return 0;
+}
+
+static int imap_fetch_send_nil_reply(struct imap_fetch_context *ctx)
+{
+ const struct imap_fetch_context_handler *handler;
+
+ if (!ctx->state.cur_first)
+ str_append_c(ctx->state.cur_str, ' ');
+
+ handler = array_idx(&ctx->handlers, ctx->state.cur_handler);
+ str_printfa(ctx->state.cur_str, "%s %s ",
+ handler->name, handler->nil_reply);
+
+ if (!handler->buffered) {
+ if (imap_fetch_flush_buffer(ctx) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void imap_fetch_fix_empty_reply(struct imap_fetch_context *ctx)
+{
+ if (ctx->state.line_partial && ctx->state.cur_first) {
+ /* we've flushed an empty "FETCH (" reply so
+ far. we can't take it back, but RFC 3501
+ doesn't allow returning empty "FETCH ()"
+ either, so just add the current message's
+ UID there. */
+ str_printfa(ctx->state.cur_str, "UID %u ",
+ ctx->state.cur_mail->uid);
+ }
+}
+
+static bool imap_fetch_cur_failed(struct imap_fetch_context *ctx)
+{
+ ctx->failures = TRUE;
+ if (ctx->client->set->parsed_fetch_failure ==
+ IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY)
+ return FALSE;
+
+ if (!array_is_created(&ctx->fetch_failed_uids))
+ p_array_init(&ctx->fetch_failed_uids, ctx->ctx_pool, 8);
+ seq_range_array_add(&ctx->fetch_failed_uids, ctx->state.cur_mail->uid);
+
+ if (ctx->error == MAIL_ERROR_NONE) {
+ /* preserve the first error, since it may change in storage. */
+ ctx->errstr = p_strdup(ctx->ctx_pool,
+ mailbox_get_last_error(ctx->state.cur_mail->box, &ctx->error));
+ }
+ return TRUE;
+}
+
+static int imap_fetch_more_int(struct imap_fetch_context *ctx, bool cancel)
+{
+ struct imap_fetch_state *state = &ctx->state;
+ struct client *client = ctx->client;
+ const struct imap_fetch_context_handler *handlers;
+ unsigned int count;
+ int ret;
+
+ if (state->cont_handler != NULL) {
+ ret = state->cont_handler(ctx);
+ if (ret == 0)
+ return 0;
+
+ if (ret < 0) {
+ if (client->output->closed)
+ return -1;
+
+ if (state->cur_mail->expunged) {
+ /* not an error, just lost it. */
+ state->skipped_expunged_msgs = TRUE;
+ if (imap_fetch_send_nil_reply(ctx) < 0)
+ return -1;
+ } else {
+ if (!imap_fetch_cur_failed(ctx))
+ return -1;
+ }
+ }
+
+ state->cont_handler = NULL;
+ state->cur_handler++;
+ if (state->cur_input != NULL)
+ i_stream_unref(&state->cur_input);
+ }
+
+ handlers = array_get(&ctx->handlers, &count);
+ for (;;) {
+ if (o_stream_get_buffer_used_size(client->output) >=
+ CLIENT_OUTPUT_OPTIMAL_SIZE) {
+ ret = o_stream_flush(client->output);
+ if (ret <= 0)
+ return ret;
+ }
+
+ if (state->cur_mail == NULL) {
+ if (cancel)
+ return 1;
+
+ if (!mailbox_search_next(state->search_ctx,
+ &state->cur_mail))
+ break;
+
+ str_printfa(state->cur_str, "* %u FETCH (",
+ state->cur_mail->seq);
+ ctx->fetched_mails_count++;
+ state->cur_first = TRUE;
+ state->cur_str_prefix_size = str_len(state->cur_str);
+ i_assert(!state->line_partial);
+ }
+
+ for (; state->cur_handler < count; state->cur_handler++) {
+ if (str_len(state->cur_str) > 0 &&
+ !handlers[state->cur_handler].buffered) {
+ /* first non-buffered handler.
+ flush the buffer. */
+ if (imap_fetch_flush_buffer(ctx) < 0)
+ return -1;
+ }
+
+ i_assert(state->cur_input == NULL);
+ T_BEGIN {
+ const struct imap_fetch_context_handler *h =
+ &handlers[state->cur_handler];
+
+ ret = h->handler(ctx, state->cur_mail,
+ h->context);
+ } T_END;
+
+ if (ret == 0)
+ return 0;
+
+ if (ret < 0) {
+ if (state->cur_mail->expunged) {
+ /* not an error, just lost it. */
+ state->skipped_expunged_msgs = TRUE;
+ if (imap_fetch_send_nil_reply(ctx) < 0)
+ return -1;
+ } else {
+ i_assert(ret < 0 ||
+ state->cont_handler != NULL);
+ if (!imap_fetch_cur_failed(ctx))
+ return -1;
+ }
+ }
+
+ state->cont_handler = NULL;
+ if (state->cur_input != NULL)
+ i_stream_unref(&state->cur_input);
+ }
+
+ imap_fetch_fix_empty_reply(ctx);
+ if (str_len(state->cur_str) > 0 &&
+ (state->line_partial ||
+ str_len(state->cur_str) != state->cur_str_prefix_size)) {
+ /* no non-buffered handlers */
+ if (imap_fetch_flush_buffer(ctx) < 0)
+ return -1;
+ }
+
+ if (state->line_partial)
+ o_stream_nsend(client->output, ")\r\n", 3);
+ client->last_output = ioloop_time;
+
+ state->cur_mail = NULL;
+ state->cur_handler = 0;
+ state->line_partial = FALSE;
+ }
+
+ return ctx->failures ? -1 : 1;
+}
+
+int imap_fetch_more(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd)
+{
+ int ret;
+
+ i_assert(ctx->client->output_cmd_lock == NULL ||
+ ctx->client->output_cmd_lock == cmd);
+
+ ret = imap_fetch_more_int(ctx, cmd->cancel);
+ if (ret < 0)
+ ctx->state.failed = TRUE;
+ if (ctx->state.line_partial) {
+ /* nothing can be sent until FETCH is finished */
+ ctx->client->output_cmd_lock = cmd;
+ }
+ if (cmd->cancel && ctx->client->output_cmd_lock != NULL) {
+ /* canceling didn't really work. we must not output
+ anything anymore. */
+ if (!ctx->client->destroyed)
+ client_disconnect(ctx->client, "Failed to cancel FETCH");
+ ctx->client->output_cmd_lock = NULL;
+ }
+ return ret;
+}
+
+int imap_fetch_more_no_lock_update(struct imap_fetch_context *ctx)
+{
+ int ret;
+
+ ret = imap_fetch_more_int(ctx, FALSE);
+ if (ret < 0) {
+ ctx->state.failed = TRUE;
+ if (ctx->state.line_partial) {
+ /* we can't send any more replies to client, because
+ the FETCH reply wasn't fully sent. */
+ client_disconnect(ctx->client,
+ "NOTIFY failed in the middle of FETCH reply");
+ }
+ }
+ return ret;
+}
+
+int imap_fetch_end(struct imap_fetch_context *ctx)
+{
+ struct imap_fetch_state *state = &ctx->state;
+
+ if (ctx->state.fetching) {
+ ctx->state.fetching = FALSE;
+ if (state->line_partial) {
+ imap_fetch_fix_empty_reply(ctx);
+ if (imap_fetch_flush_buffer(ctx) < 0)
+ state->failed = TRUE;
+ if (o_stream_send(ctx->client->output, ")\r\n", 3) < 0)
+ state->failed = TRUE;
+ }
+ }
+ ctx->client->output_cmd_lock = NULL;
+
+ str_free(&state->cur_str);
+
+ i_stream_unref(&state->cur_input);
+
+ if (state->search_ctx != NULL) {
+ if (mailbox_search_deinit(&state->search_ctx) < 0)
+ state->failed = TRUE;
+ }
+
+ if (state->trans != NULL) {
+ /* even if something failed, we want to commit changes to
+ cache, as well as possible \Seen flag changes for FETCH
+ replies we returned so far. */
+ if (mailbox_transaction_commit(&state->trans) < 0)
+ state->failed = TRUE;
+ }
+ return state->failed ? -1 : 0;
+}
+
+void imap_fetch_free(struct imap_fetch_context **_ctx)
+{
+ struct imap_fetch_context *ctx = *_ctx;
+ const struct imap_fetch_context_handler *handler;
+
+ *_ctx = NULL;
+
+ (void)imap_fetch_end(ctx);
+
+ array_foreach(&ctx->handlers, handler) {
+ if (handler->want_deinit)
+ handler->handler(ctx, NULL, handler->context);
+ }
+ pool_unref(&ctx->ctx_pool);
+}
+
+static int fetch_body(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ const char *body;
+
+ if (mail_get_special(mail, MAIL_FETCH_IMAP_BODY, &body) < 0)
+ return -1;
+
+ if (ctx->state.cur_first)
+ ctx->state.cur_first = FALSE;
+ else {
+ if (o_stream_send(ctx->client->output, " ", 1) < 0)
+ return -1;
+ }
+
+ if (o_stream_send(ctx->client->output, "BODY (", 6) < 0 ||
+ o_stream_send_str(ctx->client->output, body) < 0 ||
+ o_stream_send(ctx->client->output, ")", 1) < 0)
+ return -1;
+ return 1;
+}
+
+static bool fetch_body_init(struct imap_fetch_init_context *ctx)
+{
+ if (ctx->name[4] == '\0') {
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_BODY;
+ imap_fetch_add_handler(ctx, 0, "("BODY_NIL_REPLY")",
+ fetch_body, NULL);
+ return TRUE;
+ }
+ return imap_fetch_body_section_init(ctx);
+}
+
+static int fetch_bodystructure(struct imap_fetch_context *ctx,
+ struct mail *mail, void *context ATTR_UNUSED)
+{
+ const char *bodystructure;
+
+ if (mail_get_special(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE,
+ &bodystructure) < 0)
+ return -1;
+
+ if (ctx->state.cur_first)
+ ctx->state.cur_first = FALSE;
+ else {
+ if (o_stream_send(ctx->client->output, " ", 1) < 0)
+ return -1;
+ }
+
+ if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0 ||
+ o_stream_send_str(ctx->client->output, bodystructure) < 0 ||
+ o_stream_send(ctx->client->output, ")", 1) < 0)
+ return -1;
+
+ return 1;
+}
+
+static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_BODYSTRUCTURE;
+ imap_fetch_add_handler(ctx, 0, "("BODY_NIL_REPLY" NIL NIL NIL NIL)",
+ fetch_bodystructure, NULL);
+ return TRUE;
+}
+
+static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ const char *envelope;
+
+ if (mail_get_special(mail, MAIL_FETCH_IMAP_ENVELOPE, &envelope) < 0)
+ return -1;
+
+ if (ctx->state.cur_first)
+ ctx->state.cur_first = FALSE;
+ else {
+ if (o_stream_send(ctx->client->output, " ", 1) < 0)
+ return -1;
+ }
+
+ if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0 ||
+ o_stream_send_str(ctx->client->output, envelope) < 0 ||
+ o_stream_send(ctx->client->output, ")", 1) < 0)
+ return -1;
+ return 1;
+}
+
+static bool fetch_envelope_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_ENVELOPE;
+ imap_fetch_add_handler(ctx, 0, ENVELOPE_NIL_REPLY,
+ fetch_envelope, NULL);
+ return TRUE;
+}
+
+static int fetch_flags(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ enum mail_flags flags;
+ const char *const *keywords;
+
+ flags = mail_get_flags(mail);
+ if (ctx->flags_update_seen && (flags & MAIL_SEEN) == 0 &&
+ !mailbox_is_readonly(mail->box)) {
+ /* Add \Seen flag */
+ ctx->state.seen_flags_changed = TRUE;
+ flags |= MAIL_SEEN;
+ mail_update_flags(mail, MODIFY_ADD, MAIL_SEEN);
+ } else if (ctx->flags_show_only_seen_changes) {
+ return 1;
+ }
+
+ keywords = client_get_keyword_names(ctx->client, &ctx->tmp_keywords,
+ mail_get_keyword_indexes(mail));
+
+ str_append(ctx->state.cur_str, "FLAGS (");
+ imap_write_flags(ctx->state.cur_str, flags, keywords);
+ str_append(ctx->state.cur_str, ") ");
+ return 1;
+}
+
+bool imap_fetch_flags_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->flags_have_handler = TRUE;
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_FLAGS;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "()", fetch_flags, NULL);
+ return TRUE;
+}
+
+static int fetch_internaldate(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ time_t date;
+
+ if (mail_get_received_date(mail, &date) < 0)
+ return -1;
+
+ str_printfa(ctx->state.cur_str, "INTERNALDATE \"%s\" ",
+ imap_to_datetime(date));
+ return 1;
+}
+
+static bool fetch_internaldate_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_RECEIVED_DATE;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "\"01-Jan-1970 00:00:00 +0000\"",
+ fetch_internaldate, NULL);
+ return TRUE;
+}
+
+static int fetch_modseq(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ uint64_t modseq;
+
+ modseq = mail_get_modseq(mail);
+ if (ctx->client->highest_fetch_modseq < modseq)
+ ctx->client->highest_fetch_modseq = modseq;
+ str_printfa(ctx->state.cur_str, "MODSEQ (%"PRIu64") ", modseq);
+ return 1;
+}
+
+bool imap_fetch_modseq_init(struct imap_fetch_init_context *ctx)
+{
+ if (ctx->fetch_ctx->client->nonpermanent_modseqs) {
+ ctx->error = "FETCH MODSEQ can't be used with non-permanent modseqs";
+ return FALSE;
+ }
+ client_enable(ctx->fetch_ctx->client, imap_feature_condstore);
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ NULL, fetch_modseq, NULL);
+ return TRUE;
+}
+
+static int fetch_uid(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ str_printfa(ctx->state.cur_str, "UID %u ", mail->uid);
+ return 1;
+}
+
+bool imap_fetch_uid_init(struct imap_fetch_init_context *ctx)
+{
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ NULL, fetch_uid, NULL);
+ return TRUE;
+}
+
+static int imap_fetch_savedate(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ time_t date;
+ int ret;
+
+ ret = mail_get_save_date(mail, &date);
+ if (ret < 0)
+ return -1;
+
+ if (ret == 0)
+ str_append(ctx->state.cur_str, "SAVEDATE NIL ");
+ else {
+ str_printfa(ctx->state.cur_str, "SAVEDATE \"%s\" ",
+ imap_to_datetime(date));
+ }
+ return 1;
+}
+
+static bool imap_fetch_savedate_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_SAVE_DATE;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "NIL", imap_fetch_savedate, NULL);
+ return TRUE;
+}
+
+static int fetch_guid(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ const char *value;
+
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &value) < 0)
+ return -1;
+
+ str_append(ctx->state.cur_str, "X-GUID ");
+ imap_append_astring(ctx->state.cur_str, value);
+ str_append_c(ctx->state.cur_str, ' ');
+ return 1;
+}
+
+static bool fetch_guid_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_GUID;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "", fetch_guid, NULL);
+ return TRUE;
+}
+
+static int fetch_x_mailbox(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ const char *name;
+ string_t *mutf7_name;
+
+ if (mail_get_special(mail, MAIL_FETCH_MAILBOX_NAME, &name) < 0) {
+ /* This can happen with virtual mailbox if the backend mail
+ is expunged. */
+ return -1;
+ }
+
+ mutf7_name = t_str_new(strlen(name)*2);
+ if (imap_utf8_to_utf7(name, mutf7_name) < 0)
+ i_panic("FETCH: Mailbox name not UTF-8: %s", name);
+
+ str_append(ctx->state.cur_str, "X-MAILBOX ");
+ imap_append_astring(ctx->state.cur_str, str_c(mutf7_name));
+ str_append_c(ctx->state.cur_str, ' ');
+ return 1;
+}
+
+static bool fetch_x_mailbox_init(struct imap_fetch_init_context *ctx)
+{
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ NULL, fetch_x_mailbox, NULL);
+ return TRUE;
+}
+
+static int fetch_x_real_uid(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ struct mail *real_mail;
+
+ if (mail_get_backend_mail(mail, &real_mail) < 0)
+ return -1;
+ str_printfa(ctx->state.cur_str, "X-REAL-UID %u ", real_mail->uid);
+ return 1;
+}
+
+static bool fetch_x_real_uid_init(struct imap_fetch_init_context *ctx)
+{
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ NULL, fetch_x_real_uid, NULL);
+ return TRUE;
+}
+
+static int fetch_x_savedate(struct imap_fetch_context *ctx, struct mail *mail,
+ void *context ATTR_UNUSED)
+{
+ time_t date;
+
+ if (mail_get_save_date(mail, &date) < 0)
+ return -1;
+
+ str_printfa(ctx->state.cur_str, "X-SAVEDATE \"%s\" ",
+ imap_to_datetime(date));
+ return 1;
+}
+
+static bool fetch_x_savedate_init(struct imap_fetch_init_context *ctx)
+{
+ ctx->fetch_ctx->fetch_data |= MAIL_FETCH_SAVE_DATE;
+ imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED,
+ "\"01-Jan-1970 00:00:00 +0000\"",
+ fetch_x_savedate, NULL);
+ return TRUE;
+}
+
+static const struct imap_fetch_handler
+imap_fetch_default_handlers[] = {
+ { "BINARY", imap_fetch_binary_init },
+ { "BODY", fetch_body_init },
+ { "BODYSTRUCTURE", fetch_bodystructure_init },
+ { "ENVELOPE", fetch_envelope_init },
+ { "FLAGS", imap_fetch_flags_init },
+ { "INTERNALDATE", fetch_internaldate_init },
+ { "MODSEQ", imap_fetch_modseq_init },
+ { "PREVIEW", imap_fetch_preview_init },
+ { "RFC822", imap_fetch_rfc822_init },
+ { "SNIPPET", imap_fetch_snippet_init },
+ { "UID", imap_fetch_uid_init },
+ { "SAVEDATE", imap_fetch_savedate_init },
+ { "X-GUID", fetch_guid_init },
+ { "X-MAILBOX", fetch_x_mailbox_init },
+ { "X-REAL-UID", fetch_x_real_uid_init },
+ { "X-SAVEDATE", fetch_x_savedate_init }
+};
+
+void imap_fetch_handlers_init(void)
+{
+ i_array_init(&fetch_handlers, 32);
+ imap_fetch_handlers_register(imap_fetch_default_handlers,
+ N_ELEMENTS(imap_fetch_default_handlers));
+}
+
+void imap_fetch_handlers_deinit(void)
+{
+ array_free(&fetch_handlers);
+}
diff --git a/src/imap/imap-fetch.h b/src/imap/imap-fetch.h
new file mode 100644
index 0000000..ba8cfc8
--- /dev/null
+++ b/src/imap/imap-fetch.h
@@ -0,0 +1,166 @@
+#ifndef IMAP_FETCH_H
+#define IMAP_FETCH_H
+
+struct imap_fetch_context;
+
+enum imap_fetch_handler_flags {
+ IMAP_FETCH_HANDLER_FLAG_BUFFERED = 0x01,
+ IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT = 0x02
+};
+
+/* Returns 1 = ok, 0 = client output buffer full, call again, -1 = error.
+ mail = NULL for deinit. */
+typedef int imap_fetch_handler_t(struct imap_fetch_context *ctx,
+ struct mail *mail, void *context);
+
+struct imap_fetch_init_context {
+ struct imap_fetch_context *fetch_ctx;
+ pool_t pool;
+
+ const char *name;
+ const struct imap_arg *args;
+
+ const char *error;
+};
+
+struct imap_fetch_handler {
+ const char *name;
+
+ /* Returns FALSE and sets ctx->error if arg is invalid */
+ bool (*init)(struct imap_fetch_init_context *ctx);
+};
+
+struct imap_fetch_context_handler {
+ imap_fetch_handler_t *handler;
+ void *context;
+
+ const char *name;
+ const char *nil_reply;
+
+ bool buffered:1;
+ bool want_deinit:1;
+};
+
+struct imap_fetch_qresync_args {
+ const ARRAY_TYPE(uint32_t) *qresync_sample_seqset;
+ const ARRAY_TYPE(uint32_t) *qresync_sample_uidset;
+};
+
+struct imap_fetch_state {
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+
+ struct mail *cur_mail;
+ unsigned int cur_handler;
+ const char *cur_human_name;
+ uoff_t cur_size;
+ enum mail_fetch_field cur_size_field;
+ string_t *cur_str;
+ size_t cur_str_prefix_size;
+ struct istream *cur_input;
+ bool skip_cr;
+ int (*cont_handler)(struct imap_fetch_context *ctx);
+ uint64_t *cur_stats_sizep;
+
+ bool fetching:1;
+ bool seen_flags_changed:1;
+ /* TRUE if the first FETCH parameter result hasn't yet been sent to
+ the IMAP client. Note that this doesn't affect buffered content in
+ cur_str until it gets flushed out. */
+ bool cur_first:1;
+ /* TRUE if the cur_str prefix has been flushed. More data may still
+ be added to it. */
+ bool line_partial:1;
+ bool skipped_expunged_msgs:1;
+ bool failed:1;
+};
+
+struct imap_fetch_context {
+ struct client *client;
+ pool_t ctx_pool;
+ const char *reason;
+
+ enum mail_fetch_field fetch_data;
+ ARRAY_TYPE(const_string) all_headers;
+
+ ARRAY(struct imap_fetch_context_handler) handlers;
+ unsigned int buffered_handlers_count;
+
+ ARRAY_TYPE(keywords) tmp_keywords;
+
+ struct imap_fetch_state state;
+ ARRAY_TYPE(seq_range) fetch_failed_uids;
+ unsigned int fetched_mails_count;
+
+ enum mail_error error;
+ const char *errstr;
+
+ bool initialized:1;
+ bool failures:1;
+ bool flags_have_handler:1;
+ bool flags_update_seen:1;
+ bool flags_show_only_seen_changes:1;
+ /* HEADER.FIELDS or HEADER.FIELDS.NOT is fetched */
+ bool fetch_header_fields:1;
+};
+
+void imap_fetch_handlers_register(const struct imap_fetch_handler *handlers,
+ size_t count);
+void imap_fetch_handler_unregister(const char *name);
+
+void imap_fetch_add_handler(struct imap_fetch_init_context *ctx,
+ enum imap_fetch_handler_flags flags,
+ const char *nil_reply,
+ imap_fetch_handler_t *handler, void *context)
+ ATTR_NULL(3, 5);
+#define imap_fetch_add_handler(ctx, flags, nil_reply, handler, context) \
+ imap_fetch_add_handler(ctx, flags, nil_reply - \
+ CALLBACK_TYPECHECK(handler, int (*)( \
+ struct imap_fetch_context *, struct mail *, \
+ typeof(context))), \
+ (imap_fetch_handler_t *)handler, context)
+
+int imap_fetch_att_list_parse(struct client *client, pool_t pool,
+ const struct imap_arg *list,
+ struct imap_fetch_context **fetch_ctx_r,
+ const char **client_error_r);
+
+struct imap_fetch_context *
+imap_fetch_alloc(struct client *client, pool_t pool, const char *reason);
+void imap_fetch_free(struct imap_fetch_context **ctx);
+bool imap_fetch_init_handler(struct imap_fetch_init_context *init_ctx);
+void imap_fetch_init_nofail_handler(struct imap_fetch_context *ctx,
+ bool (*init)(struct imap_fetch_init_context *));
+const struct imap_fetch_handler *imap_fetch_handler_lookup(const char *name);
+
+void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box,
+ struct mail_search_args *search_args);
+int imap_fetch_send_vanished(struct client *client, struct mailbox *box,
+ const struct mail_search_args *search_args,
+ const struct imap_fetch_qresync_args *qresync_args);
+/* Returns 1 if finished, 0 if more data is needed, -1 if error.
+ When 0 is returned, line_partial=TRUE if literal is open and must be
+ finished before anything else to client. */
+int imap_fetch_more(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd);
+/* Like imap_fetch_more(), but don't check/update output_lock.
+ The caller must handle this itself. */
+int imap_fetch_more_no_lock_update(struct imap_fetch_context *ctx);
+int imap_fetch_end(struct imap_fetch_context *ctx);
+int imap_fetch_more(struct imap_fetch_context *ctx,
+ struct client_command_context *cmd);
+
+bool imap_fetch_flags_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_modseq_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_uid_init(struct imap_fetch_init_context *ctx);
+
+bool imap_fetch_body_section_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_rfc822_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_binary_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_preview_init(struct imap_fetch_init_context *ctx);
+bool imap_fetch_snippet_init(struct imap_fetch_init_context *ctx);
+
+void imap_fetch_handlers_init(void);
+void imap_fetch_handlers_deinit(void);
+
+#endif
diff --git a/src/imap/imap-list.c b/src/imap/imap-list.c
new file mode 100644
index 0000000..38a81ba
--- /dev/null
+++ b/src/imap/imap-list.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-list.h"
+
+bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags)
+{
+ size_t orig_len = str_len(str);
+
+ if ((flags & MAILBOX_SUBSCRIBED) != 0)
+ str_append(str, "\\Subscribed ");
+
+ if ((flags & MAILBOX_NOSELECT) != 0)
+ str_append(str, "\\Noselect ");
+ if ((flags & MAILBOX_NONEXISTENT) != 0)
+ str_append(str, "\\NonExistent ");
+
+ if ((flags & MAILBOX_CHILDREN) != 0)
+ str_append(str, "\\HasChildren ");
+ else if ((flags & MAILBOX_NOINFERIORS) != 0)
+ str_append(str, "\\NoInferiors ");
+ else if ((flags & MAILBOX_NOCHILDREN) != 0)
+ str_append(str, "\\HasNoChildren ");
+
+ if ((flags & MAILBOX_MARKED) != 0)
+ str_append(str, "\\Marked ");
+ if ((flags & MAILBOX_UNMARKED) != 0)
+ str_append(str, "\\UnMarked ");
+
+ if (str_len(str) == orig_len)
+ return FALSE;
+ str_truncate(str, str_len(str)-1);
+ return TRUE;
+}
diff --git a/src/imap/imap-list.h b/src/imap/imap-list.h
new file mode 100644
index 0000000..6a0e076
--- /dev/null
+++ b/src/imap/imap-list.h
@@ -0,0 +1,7 @@
+#ifndef IMAP_LIST_H
+#define IMAP_LIST_H
+
+/* Returns TRUE if anything was added to the string. */
+bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags);
+
+#endif
diff --git a/src/imap/imap-master-client.c b/src/imap/imap-master-client.c
new file mode 100644
index 0000000..84814f0
--- /dev/null
+++ b/src/imap/imap-master-client.c
@@ -0,0 +1,448 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "connection.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "ostream.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "time-util.h"
+#include "process-title.h"
+#include "master-service.h"
+#include "mail-storage-service.h"
+#include "imap-client.h"
+#include "imap-state.h"
+#include "imap-master-client.h"
+
+struct imap_master_client {
+ struct connection conn;
+ bool imap_client_created;
+};
+
+struct imap_master_input {
+ /* input we've already read from the IMAP client. */
+ buffer_t *client_input;
+ /* output that imap-hibernate was supposed to send to IMAP client,
+ but couldn't send it yet. */
+ buffer_t *client_output;
+ /* IMAP connection state */
+ buffer_t *state;
+ /* command tag */
+ const char *tag;
+ /* Timestamp when hibernation started */
+ struct timeval hibernation_start_time;
+
+ dev_t peer_dev;
+ ino_t peer_ino;
+
+ bool state_import_bad_idle_done;
+ bool state_import_idle_continue;
+};
+
+static struct connection_list *master_clients = NULL;
+
+static void imap_master_client_destroy(struct connection *conn)
+{
+ struct imap_master_client *client = (struct imap_master_client *)conn;
+
+ if (!client->imap_client_created)
+ master_service_client_connection_destroyed(master_service);
+ connection_deinit(conn);
+ i_free(conn);
+}
+
+static int
+imap_master_client_parse_input(const char *const *args, pool_t pool,
+ struct mail_storage_service_input *input_r,
+ struct imap_master_input *master_input_r,
+ const char **error_r)
+{
+ const char *key, *value;
+ unsigned int peer_dev_major = 0, peer_dev_minor = 0;
+
+ i_zero(input_r);
+ i_zero(master_input_r);
+ master_input_r->client_input = buffer_create_dynamic(pool, 64);
+ master_input_r->client_output = buffer_create_dynamic(pool, 16);
+ master_input_r->state = buffer_create_dynamic(pool, 512);
+
+ input_r->module = input_r->service = "imap";
+ /* we never want to do userdb lookup again when restoring the client.
+ we have the userdb_fields cached already. */
+ input_r->flags_override_remove = MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+
+ if (args[0] == NULL) {
+ *error_r = "Missing username in input";
+ return -1;
+ }
+ input_r->username = args[0];
+
+ for (args++; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value != NULL)
+ key = t_strdup_until(*args, value++);
+ else {
+ key = *args;
+ value = "";
+ }
+ if (strcmp(key, "lip") == 0) {
+ if (net_addr2ip(value, &input_r->local_ip) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid lip value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "lport") == 0) {
+ if (net_str2port(value, &input_r->local_port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid lport value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "rip") == 0) {
+ if (net_addr2ip(value, &input_r->remote_ip) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid rip value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "rport") == 0) {
+ if (net_str2port(value, &input_r->remote_port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid rport value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_dev_major") == 0) {
+ if (str_to_uint(value, &peer_dev_major) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_dev_major value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_dev_minor") == 0) {
+ if (str_to_uint(value, &peer_dev_minor) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_dev_minor value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "peer_ino") == 0) {
+ if (str_to_ino(value, &master_input_r->peer_ino) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid peer_ino value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "session") == 0) {
+ input_r->session_id = value;
+ } else if (strcmp(key, "session_created") == 0) {
+ if (str_to_time(value, &input_r->session_create_time) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid session_created value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "hibernation_started") == 0) {
+ if (str_to_timeval(value, &master_input_r->hibernation_start_time) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid hibernation_started value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "userdb_fields") == 0) {
+ input_r->userdb_fields =
+ t_strsplit_tabescaped(value);
+ } else if (strcmp(key, "client_input") == 0) {
+ if (base64_decode(value, strlen(value), NULL,
+ master_input_r->client_input) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid client_input base64 value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "client_output") == 0) {
+ if (base64_decode(value, strlen(value), NULL,
+ master_input_r->client_output) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid client_output base64 value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "state") == 0) {
+ if (base64_decode(value, strlen(value), NULL,
+ master_input_r->state) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid state base64 value: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "tag") == 0) {
+ master_input_r->tag = t_strdup(value);
+ } else if (strcmp(key, "bad-done") == 0) {
+ master_input_r->state_import_bad_idle_done = TRUE;
+ } else if (strcmp(key, "idle-continue") == 0) {
+ master_input_r->state_import_idle_continue = TRUE;
+ }
+ }
+ if (peer_dev_major != 0 || peer_dev_minor != 0) {
+ master_input_r->peer_dev =
+ makedev(peer_dev_major, peer_dev_minor);
+ }
+ return 0;
+}
+
+static int imap_master_client_verify(const struct imap_master_input *master_input,
+ int fd_client, const char **error_r)
+{
+ struct stat peer_st;
+
+ if (master_input->peer_ino == 0)
+ return 0;
+
+ /* make sure we have the right fd */
+ if (fstat(fd_client, &peer_st) < 0) {
+ *error_r = t_strdup_printf("fstat(peer) failed: %m");
+ return -1;
+ }
+ if (peer_st.st_ino != master_input->peer_ino ||
+ !CMP_DEV_T(peer_st.st_dev, master_input->peer_dev)) {
+ *error_r = t_strdup_printf(
+ "BUG: Expected peer device=%lu,%lu inode=%s doesn't match "
+ "client fd's actual device=%lu,%lu inode=%s",
+ (unsigned long)major(peer_st.st_dev),
+ (unsigned long)minor(peer_st.st_dev), dec2str(peer_st.st_ino),
+ (unsigned long)major(master_input->peer_dev),
+ (unsigned long)minor(master_input->peer_dev),
+ dec2str(master_input->peer_ino));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imap_master_client_input_args(struct connection *conn, const char *const *args,
+ int fd_client, pool_t pool)
+{
+ struct imap_master_client *client = (struct imap_master_client *)conn;
+ struct client *imap_client;
+ struct mail_storage_service_input input;
+ struct imap_master_input master_input;
+ const char *error = NULL, *reason;
+ int ret;
+
+ if (imap_master_client_parse_input(args, pool, &input, &master_input,
+ &error) < 0) {
+ e_error(conn->event, "imap-master: Failed to parse client input: %s", error);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "-Failed to parse client input: %s\n", error));
+ i_close_fd(&fd_client);
+ return -1;
+ }
+ if (imap_master_client_verify(&master_input, fd_client, &error) < 0) {
+ e_error(conn->event, "imap-master: Failed to verify client input: %s", error);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "-Failed to verify client input: %s\n", error));
+ i_close_fd(&fd_client);
+ return -1;
+ }
+ process_title_set("[unhibernating]");
+
+ /* NOTE: before client_create_from_input() on failures we need to close
+ fd_client, but afterward it gets closed by client_destroy() */
+ ret = client_create_from_input(&input, fd_client, fd_client,
+ TRUE, &imap_client, &error);
+ if (ret < 0) {
+ e_error(conn->event,
+ "imap-master(%s): Failed to create client: %s",
+ input.username, error);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "-Failed to create client: %s\n", error));
+ i_close_fd(&fd_client);
+ return -1;
+ }
+ client->imap_client_created = TRUE;
+
+ long long hibernation_usecs =
+ timeval_diff_usecs(&ioloop_timeval,
+ &master_input.hibernation_start_time);
+ struct event *event = event_create(imap_client->event);
+ event_set_name(event, "imap_client_unhibernated");
+ event_add_int(event, "hibernation_usecs", hibernation_usecs);
+ imap_client->state_import_bad_idle_done =
+ master_input.state_import_bad_idle_done;
+ imap_client->state_import_idle_continue =
+ master_input.state_import_idle_continue;
+ if (imap_client->state_import_bad_idle_done) {
+ reason = "IDLE was stopped with BAD command";
+ event_add_str(event, "reason", "idle_bad_reply");
+ } else if (imap_client->state_import_idle_continue) {
+ reason = "mailbox changes need to be sent";
+ event_add_str(event, "reason", "mailbox_changes");
+ } else {
+ reason = "IDLE was stopped with DONE";
+ event_add_str(event, "reason", "idle_done");
+ }
+
+ /* Send a success notification before we start anything that lasts
+ potentially a long time. imap-hibernate process is waiting for us
+ to answer. Even if we fail later, we log the error anyway. From now
+ on it's our responsibility to also log the imap_client_unhibernated
+ event. */
+ o_stream_nsend_str(conn->output, "+\n");
+ (void)o_stream_flush(conn->output);
+
+ if (master_input.client_input->used > 0) {
+ client_add_istream_prefix(imap_client,
+ master_input.client_input->data,
+ master_input.client_input->used);
+ }
+
+ client_create_finish_io(imap_client);
+ if (client_create_finish(imap_client, &error) < 0) {
+ event_add_str(event, "error", error);
+ e_error(event, "imap-master: %s", error);
+ event_unref(&event);
+ client_destroy(imap_client, error);
+ return -1;
+ }
+ /* log prefix is set at this point, so we don't need to add the
+ username anymore to the log messages */
+
+ o_stream_nsend(imap_client->output,
+ master_input.client_output->data,
+ master_input.client_output->used);
+
+ struct event_reason *event_reason =
+ event_reason_begin("imap:unhibernate");
+ ret = imap_state_import_internal(imap_client, master_input.state->data,
+ master_input.state->used, &error);
+ event_reason_end(&event_reason);
+
+ if (ret <= 0) {
+ error = t_strdup_printf("Failed to import client state: %s", error);
+ event_add_str(event, "error", error);
+ e_error(event, "imap-master: %s", error);
+ event_unref(&event);
+ client_destroy(imap_client, "Client state initialization failed");
+ return -1;
+ }
+ if (imap_client->mailbox != NULL) {
+ /* Would be nice to set this earlier, but the previous errors
+ happen rarely enough that it shouldn't really matter. */
+ event_add_str(event, "mailbox",
+ mailbox_get_vname(imap_client->mailbox));
+ }
+
+ if (master_input.tag != NULL)
+ imap_state_import_idle_cmd_tag(imap_client, master_input.tag);
+
+ e_debug(event, "imap-master: Unhibernated because %s "
+ "(hibernated for %llu.%06llu secs)", reason,
+ hibernation_usecs/1000000, hibernation_usecs%1000000);
+ event_unref(&event);
+
+ /* make sure all pending input gets handled */
+ if (master_input.client_input->used > 0) {
+ e_debug(imap_client->event,
+ "imap-master: Pending client input: '%s'",
+ str_sanitize(str_c(master_input.client_input), 128));
+ io_set_pending(imap_client->io);
+ }
+
+ imap_refresh_proctitle();
+ /* we'll always disconnect the client afterwards */
+ return -1;
+}
+
+static int
+imap_master_client_input_line(struct connection *conn, const char *line)
+{
+ char *const *args;
+ pool_t pool;
+ int fd_client, ret;
+
+ if (!conn->version_received) {
+ if (connection_handshake_args_default(conn, t_strsplit_tabescaped(line)) < 0)
+ return -1;
+ conn->version_received = TRUE;
+ return 1;
+ }
+
+ fd_client = i_stream_unix_get_read_fd(conn->input);
+ if (fd_client == -1) {
+ e_error(conn->event, "imap-master: IMAP client fd not received");
+ return -1;
+ }
+
+ if (imap_debug)
+ e_debug(conn->event, "imap-master: Client input: %s", line);
+
+ pool = pool_alloconly_create("imap master client cmd", 1024);
+ args = p_strsplit_tabescaped(pool, line);
+ ret = imap_master_client_input_args(conn, (const void *)args,
+ fd_client, pool);
+ pool_unref(&pool);
+ return ret;
+}
+
+static void imap_master_client_idle_timeout(struct connection *conn)
+{
+ e_error(conn->event, "imap-master: Client didn't send any input for %"
+ PRIdTIME_T" seconds - disconnecting",
+ ioloop_time - conn->last_input_tv.tv_sec);
+
+ conn->disconnect_reason = CONNECTION_DISCONNECT_IDLE_TIMEOUT;
+ conn->v.destroy(conn);
+}
+
+void imap_master_client_create(int fd)
+{
+ struct imap_master_client *client;
+
+ client = i_new(struct imap_master_client, 1);
+ client->conn.unix_socket = TRUE;
+ connection_init_server(master_clients, &client->conn,
+ "imap-master", fd, fd);
+
+ /* read the first file descriptor that we can */
+ i_stream_unix_set_read_fd(client->conn.input);
+
+ imap_refresh_proctitle();
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "imap-master",
+ .service_name_out = "imap-master",
+ .major_version = 1,
+ .minor_version = 0,
+
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+
+ /* less than imap-hibernate's IMAP_MASTER_CONNECTION_TIMEOUT_MSECS */
+ .input_idle_timeout_secs = 25,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = imap_master_client_destroy,
+ .input_line = imap_master_client_input_line,
+ .idle_timeout = imap_master_client_idle_timeout,
+};
+
+bool imap_master_clients_refresh_proctitle(void)
+{
+ switch (master_clients->connections_count) {
+ case 0:
+ return FALSE;
+ case 1:
+ process_title_set("[waiting on unhibernate client]");
+ return TRUE;
+ default:
+ process_title_set(t_strdup_printf("[unhibernating %u clients]",
+ master_clients->connections_count));
+ return TRUE;
+ }
+}
+
+void imap_master_clients_init(void)
+{
+ master_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_master_clients_deinit(void)
+{
+ connection_list_deinit(&master_clients);
+}
diff --git a/src/imap/imap-master-client.h b/src/imap/imap-master-client.h
new file mode 100644
index 0000000..ef66ab9
--- /dev/null
+++ b/src/imap/imap-master-client.h
@@ -0,0 +1,10 @@
+#ifndef IMAP_MASTER_CLIENT_H
+#define IMAP_MASTER_CLIENT_H
+
+void imap_master_client_create(int fd);
+bool imap_master_clients_refresh_proctitle(void);
+
+void imap_master_clients_init(void);
+void imap_master_clients_deinit(void);
+
+#endif
diff --git a/src/imap/imap-notify.c b/src/imap/imap-notify.c
new file mode 100644
index 0000000..fa2c9ef
--- /dev/null
+++ b/src/imap/imap-notify.c
@@ -0,0 +1,523 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "ostream.h"
+#include "imap-quote.h"
+#include "mailbox-list-iter.h"
+#include "mailbox-list-notify.h"
+#include "mail-search.h"
+#include "mail-search-build.h"
+#include "imap-commands.h"
+#include "imap-fetch.h"
+#include "imap-list.h"
+#include "imap-status.h"
+#include "imap-notify.h"
+
+#define IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS 1000
+
+static int imap_notify_list(struct imap_notify_namespace *notify_ns,
+ const struct mailbox_list_notify_rec *rec,
+ enum mailbox_info_flags flags)
+{
+ string_t *str = t_str_new(128);
+ char ns_sep = mail_namespace_get_sep(notify_ns->ns);
+
+ str_append(str, "* LIST (");
+ imap_mailbox_flags2str(str, flags);
+ str_append(str, ") \"");
+ if (ns_sep == '\\')
+ str_append_c(str, '\\');
+ str_append_c(str, ns_sep);
+ str_append(str, "\" ");
+
+ imap_append_astring(str, rec->vname);
+ if (rec->old_vname != NULL) {
+ str_append(str, " (\"OLDNAME\" (");
+ imap_append_astring(str, rec->old_vname);
+ str_append(str, "))");
+ }
+ return client_send_line_next(notify_ns->ctx->client, str_c(str));
+}
+
+static int imap_notify_status(struct imap_notify_namespace *notify_ns,
+ const struct mailbox_list_notify_rec *rec)
+{
+ struct client *client = notify_ns->ctx->client;
+ struct mailbox *box;
+ struct imap_status_items items;
+ struct imap_status_result result;
+ enum mail_error error;
+ int ret = 1;
+
+ i_zero(&items);
+ if (client_has_enabled(client, imap_feature_condstore))
+ items.flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ;
+
+ box = mailbox_alloc(notify_ns->ns->list, rec->vname, 0);
+ if ((rec->events & MAILBOX_LIST_NOTIFY_UIDVALIDITY) != 0) {
+ items.flags |= IMAP_STATUS_ITEM_UIDVALIDITY |
+ IMAP_STATUS_ITEM_UIDNEXT | IMAP_STATUS_ITEM_MESSAGES |
+ IMAP_STATUS_ITEM_UNSEEN;
+ }
+ if ((rec->events & (MAILBOX_LIST_NOTIFY_APPENDS |
+ MAILBOX_LIST_NOTIFY_EXPUNGES)) != 0)
+ items.flags |= IMAP_STATUS_ITEM_UIDNEXT |
+ IMAP_STATUS_ITEM_MESSAGES | IMAP_STATUS_ITEM_UNSEEN;
+ if ((rec->events & MAILBOX_LIST_NOTIFY_SEEN_CHANGES) != 0)
+ items.flags |= IMAP_STATUS_ITEM_UNSEEN;
+ if ((rec->events & MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES) != 0) {
+ /* if HIGHESTMODSEQ isn't being sent, don't send anything */
+ }
+ if (imap_status_items_is_empty(&items)) {
+ /* don't send anything */
+ } else if (imap_status_get_result(client, box, &items, &result) < 0) {
+ /* hide permission errors from client. we don't want to leak
+ information about existence of mailboxes where user doesn't
+ have access to */
+ (void)mailbox_get_last_error(box, &error);
+ if (error != MAIL_ERROR_PERM)
+ ret = -1;
+ } else {
+ ret = imap_status_send(client, rec->vname, &items, &result);
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int
+imap_notify_next(struct imap_notify_namespace *notify_ns,
+ const struct mailbox_list_notify_rec *rec)
+{
+ enum mailbox_info_flags mailbox_flags;
+ int ret;
+
+ if ((rec->events & MAILBOX_LIST_NOTIFY_CREATE) != 0) {
+ if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+ &mailbox_flags) < 0)
+ mailbox_flags = 0;
+ if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) <= 0)
+ return ret;
+ }
+ if ((rec->events & MAILBOX_LIST_NOTIFY_DELETE) != 0) {
+ if ((ret = imap_notify_list(notify_ns, rec, MAILBOX_NONEXISTENT)) < 0)
+ return ret;
+ }
+ if ((rec->events & MAILBOX_LIST_NOTIFY_RENAME) != 0) {
+ if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+ &mailbox_flags) < 0)
+ mailbox_flags = 0;
+ if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) < 0)
+ return ret;
+ }
+ if ((rec->events & MAILBOX_LIST_NOTIFY_SUBSCRIBE) != 0) {
+ if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+ &mailbox_flags) < 0)
+ mailbox_flags = 0;
+ if ((ret = imap_notify_list(notify_ns, rec,
+ mailbox_flags | MAILBOX_SUBSCRIBED)) < 0)
+ return ret;
+ }
+ if ((rec->events & MAILBOX_LIST_NOTIFY_UNSUBSCRIBE) != 0) {
+ if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+ &mailbox_flags) < 0)
+ mailbox_flags = 0;
+ if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) < 0)
+ return ret;
+ }
+ if ((rec->events & (MAILBOX_LIST_NOTIFY_UIDVALIDITY |
+ MAILBOX_LIST_NOTIFY_APPENDS |
+ MAILBOX_LIST_NOTIFY_EXPUNGES |
+ MAILBOX_LIST_NOTIFY_SEEN_CHANGES |
+ MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES)) != 0) {
+ if ((ret = imap_notify_status(notify_ns, rec)) < 0)
+ return ret;
+ }
+ return 1;
+}
+
+static bool
+imap_notify_match_event(struct imap_notify_namespace *notify_ns,
+ const struct imap_notify_mailboxes *notify_boxes,
+ const struct mailbox_list_notify_rec *rec)
+{
+ enum imap_notify_event wanted_events = notify_boxes->events;
+ struct mailbox *box;
+
+ /* check for mailbox list events first */
+ if ((wanted_events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) != 0) {
+ if ((rec->events & (MAILBOX_LIST_NOTIFY_CREATE |
+ MAILBOX_LIST_NOTIFY_DELETE |
+ MAILBOX_LIST_NOTIFY_RENAME)) != 0)
+ return TRUE;
+ }
+ if ((wanted_events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) != 0) {
+ if ((rec->events & (MAILBOX_LIST_NOTIFY_SUBSCRIBE |
+ MAILBOX_LIST_NOTIFY_UNSUBSCRIBE)) != 0)
+ return TRUE;
+ }
+
+ /* if this is an event for the selected mailbox, ignore it */
+ box = notify_ns->ctx->client->mailbox;
+ if (box != NULL && mailbox_equals(box, notify_ns->ns, rec->vname))
+ return FALSE;
+
+ if ((wanted_events & (IMAP_NOTIFY_EVENT_MESSAGE_NEW |
+ IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE |
+ IMAP_NOTIFY_EVENT_FLAG_CHANGE)) != 0) {
+ if ((rec->events & MAILBOX_LIST_NOTIFY_UIDVALIDITY) != 0)
+ return TRUE;
+ }
+ if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0) {
+ if ((rec->events & MAILBOX_LIST_NOTIFY_APPENDS) != 0)
+ return TRUE;
+ }
+ if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) != 0) {
+ if ((rec->events & MAILBOX_LIST_NOTIFY_EXPUNGES) != 0)
+ return TRUE;
+ }
+ if ((wanted_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0) {
+ if ((rec->events & (MAILBOX_LIST_NOTIFY_SEEN_CHANGES |
+ MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES)) != 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns,
+ const struct imap_notify_mailboxes *notify_boxes,
+ const char *vname)
+{
+ struct mailbox *box;
+ const char *name;
+ size_t name_len;
+ char ns_sep;
+ bool ret;
+
+ switch (notify_boxes->type) {
+ case IMAP_NOTIFY_TYPE_SUBSCRIBED:
+ box = mailbox_alloc(notify_ns->ns->list, vname, 0);
+ ret = mailbox_is_subscribed(box);
+ mailbox_free(&box);
+ return ret;
+ case IMAP_NOTIFY_TYPE_SUBTREE:
+ ns_sep = mail_namespace_get_sep(notify_ns->ns);
+ array_foreach_elem(&notify_boxes->names, name) {
+ name_len = strlen(name);
+ if (name_len == 0) {
+ /* everything under root. NOTIFY spec itself
+ doesn't define this, but we use it for
+ implementing "personal" */
+ return TRUE;
+ }
+ if (str_begins(vname, name) &&
+ (vname[name_len] == '\0' ||
+ vname[name_len] == ns_sep))
+ return TRUE;
+ }
+ break;
+ case IMAP_NOTIFY_TYPE_MAILBOX:
+ array_foreach_elem(&notify_boxes->names, name) {
+ if (strcmp(name, vname) == 0)
+ return TRUE;
+ }
+ break;
+ }
+ return FALSE;
+}
+
+static bool
+imap_notify_match(struct imap_notify_namespace *notify_ns,
+ const struct mailbox_list_notify_rec *rec)
+{
+ const struct imap_notify_mailboxes *notify_boxes;
+
+ array_foreach(&notify_ns->mailboxes, notify_boxes) {
+ if (imap_notify_match_event(notify_ns, notify_boxes, rec) &&
+ imap_notify_match_mailbox(notify_ns, notify_boxes, rec->vname))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int imap_client_notify_ns(struct imap_notify_namespace *notify_ns)
+{
+ const struct mailbox_list_notify_rec *rec;
+ int ret, ret2 = 1;
+
+ if (notify_ns->notify == NULL)
+ return 0; /* notifications not supported in this namespace */
+
+ while ((ret = mailbox_list_notify_next(notify_ns->notify, &rec)) > 0) {
+ if (imap_notify_match(notify_ns, rec)) T_BEGIN {
+ ret2 = imap_notify_next(notify_ns, rec);
+ } T_END;
+ if (ret2 <= 0)
+ break;
+ }
+ if (ret < 0) {
+ /* failed to get some notifications */
+ return -1;
+ }
+ return ret2;
+}
+
+static int
+imap_client_notify_selected(struct client *client)
+{
+ struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx;
+ int ret;
+
+ if (!fetch_ctx->state.fetching)
+ return 1;
+
+ if ((ret = imap_fetch_more_no_lock_update(fetch_ctx)) == 0)
+ return 0;
+ /* finished the FETCH */
+ if (imap_fetch_end(fetch_ctx) < 0)
+ return -1;
+ return ret;
+}
+
+static int imap_client_notify_more(struct client *client)
+{
+ struct imap_notify_namespace *notify_ns;
+ int ret = 1;
+
+ struct event_reason *reason = event_reason_begin("imap:notify_update");
+
+ /* send notifications for selected mailbox first. note that it may
+ leave the client's output stream in the middle of a FETCH reply. */
+ if (client->notify_ctx->fetch_ctx != NULL) {
+ if ((ret = imap_client_notify_selected(client)) < 0) {
+ client->notify_ctx->fetch_ctx->state.failed = FALSE;
+ ret = -1;
+ }
+ }
+
+ /* send notifications for non-selected mailboxes */
+ array_foreach_modifiable(&client->notify_ctx->namespaces, notify_ns) {
+ if (ret == 0)
+ break;
+ if (imap_client_notify_ns(notify_ns) < 0)
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ client_send_line(client,
+ "* NO NOTIFY error, some events may have got lost");
+ }
+ event_reason_end(&reason);
+ return ret;
+}
+
+int imap_client_notify_newmails(struct client *client)
+{
+ struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx;
+ struct mailbox_status status;
+ struct mail_search_args *search_args;
+ struct mail_search_arg *arg;
+
+ i_assert(client->mailbox != NULL);
+
+ if (fetch_ctx == NULL) {
+ /* FETCH notifications not enabled in this session */
+ return 1;
+ }
+ if (client->notify_ctx->notifying)
+ return imap_client_notify_more(client);
+ client->notify_ctx->notifying = TRUE;
+
+ i_assert(!fetch_ctx->state.fetching);
+
+ mailbox_get_open_status(client->mailbox, STATUS_UIDNEXT, &status);
+
+ search_args = mail_search_build_init();
+ arg = mail_search_build_add(search_args, SEARCH_UIDSET);
+ p_array_init(&arg->value.seqset, search_args->pool, 1);
+ seq_range_array_add_range(&arg->value.seqset,
+ client->notify_uidnext, status.uidnext-1);
+ client->notify_uidnext = status.uidnext;
+
+ imap_fetch_begin(fetch_ctx, client->mailbox, search_args);
+ mail_search_args_unref(&search_args);
+
+ return imap_client_notify_more(client);
+}
+
+void imap_client_notify_finished(struct client *client)
+{
+ if (client->notify_ctx != NULL)
+ client->notify_ctx->notifying = FALSE;
+}
+
+static void notify_callback(struct imap_notify_namespace *notify_ns)
+{
+ struct event_reason *reason = event_reason_begin("imap:notify_update");
+ o_stream_cork(notify_ns->ctx->client->output);
+ imap_client_notify_ns(notify_ns);
+ o_stream_uncork(notify_ns->ctx->client->output);
+ event_reason_end(&reason);
+}
+
+static enum mailbox_list_notify_event
+imap_events_to_notify(enum imap_notify_event events)
+{
+ enum mailbox_list_notify_event ret = 0;
+
+ if ((events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0) {
+ ret |= MAILBOX_LIST_NOTIFY_APPENDS |
+ MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+ }
+ if ((events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) != 0) {
+ ret |= MAILBOX_LIST_NOTIFY_EXPUNGES |
+ MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+ }
+ if ((events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0) {
+ ret |= MAILBOX_LIST_NOTIFY_SEEN_CHANGES |
+ MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES |
+ MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+ }
+ if ((events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) != 0) {
+ ret |= MAILBOX_LIST_NOTIFY_CREATE |
+ MAILBOX_LIST_NOTIFY_DELETE |
+ MAILBOX_LIST_NOTIFY_RENAME;
+ }
+ if ((events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) != 0) {
+ ret |= MAILBOX_LIST_NOTIFY_SUBSCRIBE |
+ MAILBOX_LIST_NOTIFY_UNSUBSCRIBE;
+ }
+ return ret;
+}
+
+static void imap_notify_callback(struct mailbox *box, struct client *client)
+{
+ struct client_command_context *cmd;
+ enum mailbox_sync_flags sync_flags = 0;
+
+ i_assert(client->command_queue_size == 0);
+ i_assert(box == client->mailbox);
+
+ /* create a fake command to handle this */
+ cmd = client_command_alloc(client);
+ cmd->tag = "*";
+ cmd->name = "NOTIFY-CALLBACK";
+ client_command_init_finished(cmd);
+
+ if (!client->notify_ctx->selected_immediate_expunges)
+ sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+ if (cmd_sync(cmd, sync_flags, 0, NULL))
+ i_unreached();
+ (void)cmd_sync_delayed(client);
+}
+
+static void imap_notify_watch_selected_mailbox(struct client *client)
+{
+ i_assert(client->command_queue_size == 0);
+
+ if (client->mailbox == NULL) {
+ /* mailbox not selected */
+ return;
+ }
+ if (client->notify_ctx == NULL || !client->notify_ctx->selected_set) {
+ /* client doesn't want selected mailbox notifications */
+ return;
+
+ }
+ mailbox_notify_changes(client->mailbox, imap_notify_callback, client);
+ client->notify_ctx->watching_mailbox = TRUE;
+}
+
+static void imap_notify_watch_timeout(struct client *client)
+{
+ timeout_remove(&client->notify_ctx->to_watch);
+ imap_notify_watch_selected_mailbox(client);
+}
+
+void imap_client_notify_command_freed(struct client *client)
+{
+ struct imap_notify_context *ctx = client->notify_ctx;
+
+ if (ctx == NULL)
+ return;
+
+ if (client->command_queue_size > 0) {
+ /* don't add it until all commands are finished */
+ i_assert(ctx->to_watch == NULL);
+ return;
+ }
+
+ /* add mailbox watch back after a small delay. if another command
+ is started this timeout is aborted. */
+ ctx->to_watch = timeout_add(IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS,
+ imap_notify_watch_timeout, client);
+}
+
+void imap_client_notify_command_allocated(struct client *client)
+{
+ struct imap_notify_context *ctx = client->notify_ctx;
+
+ if (ctx == NULL)
+ return;
+
+ /* remove mailbox watcher before starting any commands */
+ if (ctx->watching_mailbox) {
+ mailbox_notify_changes_stop(client->mailbox);
+ ctx->watching_mailbox = FALSE;
+ }
+ timeout_remove(&ctx->to_watch);
+}
+
+int imap_notify_begin(struct imap_notify_context *ctx)
+{
+ struct imap_notify_namespace *notify_ns;
+ const struct imap_notify_mailboxes *notify_boxes;
+ enum mailbox_list_notify_event notify_events;
+ int ret = -1;
+
+ array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+ notify_events = 0;
+ array_foreach(&notify_ns->mailboxes, notify_boxes) {
+ notify_events |=
+ imap_events_to_notify(notify_boxes->events);
+ }
+ if (mailbox_list_notify_init(notify_ns->ns->list, notify_events,
+ &notify_ns->notify) < 0) {
+ /* notifications not supported */
+ } else {
+ ret = 0;
+ mailbox_list_notify_wait(notify_ns->notify,
+ notify_callback, notify_ns);
+ }
+ }
+ /* enable NOTIFY as long as even one namespace supports it,
+ ignore the rest */
+ return ret;
+}
+
+void imap_notify_deinit(struct imap_notify_context **_ctx)
+{
+ struct imap_notify_context *ctx = *_ctx;
+ struct imap_notify_namespace *notify_ns;
+
+ *_ctx = NULL;
+
+ array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+ if (notify_ns->notify != NULL)
+ mailbox_list_notify_deinit(&notify_ns->notify);
+ }
+ timeout_remove(&ctx->to_watch);
+ if (ctx->fetch_ctx != NULL)
+ imap_fetch_free(&ctx->fetch_ctx);
+ pool_unref(&ctx->pool);
+}
+
+void imap_notify_flush(struct imap_notify_context *ctx)
+{
+ struct imap_notify_namespace *notify_ns;
+
+ array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+ if (notify_ns->notify != NULL)
+ mailbox_list_notify_flush(notify_ns->notify);
+ }
+}
diff --git a/src/imap/imap-notify.h b/src/imap/imap-notify.h
new file mode 100644
index 0000000..936d622
--- /dev/null
+++ b/src/imap/imap-notify.h
@@ -0,0 +1,75 @@
+#ifndef IMAP_NOTIFY_H
+#define IMAP_NOTIFY_H
+
+enum imap_notify_type {
+ IMAP_NOTIFY_TYPE_SUBSCRIBED,
+ IMAP_NOTIFY_TYPE_SUBTREE,
+ IMAP_NOTIFY_TYPE_MAILBOX
+};
+
+enum imap_notify_event {
+ IMAP_NOTIFY_EVENT_MESSAGE_NEW = 0x01,
+ IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE = 0x02,
+ IMAP_NOTIFY_EVENT_FLAG_CHANGE = 0x04,
+ IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE = 0x08,
+ IMAP_NOTIFY_EVENT_MAILBOX_NAME = 0x10,
+ IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE = 0x20,
+ IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE = 0x40,
+ IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE = 0x80
+};
+#define UNSUPPORTED_EVENTS \
+ (IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE | \
+ IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE | \
+ IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE)
+
+struct imap_notify_mailboxes {
+ enum imap_notify_event events;
+ enum imap_notify_type type;
+ ARRAY_TYPE(const_string) names;
+};
+
+struct imap_notify_namespace {
+ struct imap_notify_context *ctx;
+ struct mail_namespace *ns;
+
+ struct mailbox_list_notify *notify;
+ ARRAY(struct imap_notify_mailboxes) mailboxes;
+};
+
+struct imap_notify_context {
+ pool_t pool;
+ struct client *client;
+ const char *error;
+
+ ARRAY(struct imap_notify_namespace) namespaces;
+ enum imap_notify_event selected_events;
+ enum imap_notify_event global_used_events;
+ unsigned int global_max_mailbox_names;
+
+ struct imap_fetch_context *fetch_ctx;
+ struct timeout *to_watch;
+
+ bool have_subscriptions:1;
+ bool selected_set:1;
+ bool selected_immediate_expunges:1;
+ bool send_immediate_status:1;
+ bool watching_mailbox:1;
+ bool notifying:1;
+};
+
+bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns,
+ const struct imap_notify_mailboxes *notify_boxes,
+ const char *vname);
+
+int imap_client_notify_newmails(struct client *client);
+void imap_client_notify_finished(struct client *client);
+
+void imap_client_notify_command_allocated(struct client *client);
+void imap_client_notify_command_freed(struct client *client);
+
+int imap_notify_begin(struct imap_notify_context *ctx);
+void imap_notify_deinit(struct imap_notify_context **ctx);
+
+void imap_notify_flush(struct imap_notify_context *ctx);
+
+#endif
diff --git a/src/imap/imap-search-args.c b/src/imap/imap-search-args.c
new file mode 100644
index 0000000..67e8679
--- /dev/null
+++ b/src/imap/imap-search-args.c
@@ -0,0 +1,353 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "mail-storage.h"
+#include "mail-search-parser.h"
+#include "mail-search-build.h"
+#include "imap-search-args.h"
+#include "imap-parser.h"
+#include "imap-seqset.h"
+
+
+struct search_build_data {
+ pool_t pool;
+ struct mailbox *box;
+ const char *error;
+};
+
+static bool search_args_have_searchres(struct mail_search_arg *sargs)
+{
+ for (; sargs != NULL; sargs = sargs->next) {
+ switch (sargs->type) {
+ case SEARCH_UIDSET:
+ if (strcmp(sargs->value.str, "$") == 0)
+ return TRUE;
+ break;
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ if (search_args_have_searchres(sargs->value.subargs))
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return FALSE;
+}
+
+static void imap_search_saved_uidset_clear(struct client_command_context *cmd)
+{
+ if (array_is_created(&cmd->client->search_saved_uidset))
+ array_clear(&cmd->client->search_saved_uidset);
+ else
+ i_array_init(&cmd->client->search_saved_uidset, 128);
+}
+
+int imap_search_args_build(struct client_command_context *cmd,
+ const struct imap_arg *args, const char *charset,
+ struct mail_search_args **search_args_r)
+{
+ struct mail_search_parser *parser;
+ struct mail_search_args *sargs;
+ const char *client_error;
+ int ret;
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd, "Missing search parameters");
+ return -1;
+ }
+
+ parser = mail_search_parser_init_imap(args);
+ ret = mail_search_build(mail_search_register_get_imap(),
+ parser, &charset, &sargs, &client_error);
+ mail_search_parser_deinit(&parser);
+ if (ret < 0) {
+ if (charset == NULL) {
+ if (cmd->search_save_result)
+ imap_search_saved_uidset_clear(cmd);
+ client_send_tagline(cmd, t_strconcat(
+ "NO [BADCHARSET] ", client_error, NULL));
+ } else {
+ client_send_command_error(cmd, client_error);
+ }
+ return -1;
+ }
+
+ if (search_args_have_searchres(sargs->args)) {
+ if (client_handle_search_save_ambiguity(cmd))
+ return 0;
+ }
+
+ mail_search_args_init(sargs, cmd->client->mailbox, TRUE,
+ &cmd->client->search_saved_uidset);
+ if (cmd->search_save_result) {
+ /* clear the SAVE resultset only after potentially using $
+ in the search args themselves */
+ imap_search_saved_uidset_clear(cmd);
+ }
+ *search_args_r = sargs;
+ return 1;
+}
+
+static bool
+msgset_is_valid(ARRAY_TYPE(seq_range) *seqset, uint32_t messages_count)
+{
+ const struct seq_range *range;
+ unsigned int count;
+
+ /* when there are no messages, all messagesets are invalid.
+ if there's at least one message:
+ - * gives seq1 = seq2 = (uint32_t)-1
+ - n:* should work if n <= messages_count
+ - n:m or m should work if m <= messages_count
+ */
+ range = array_get(seqset, &count);
+ if (count == 0 || messages_count == 0)
+ return FALSE;
+
+ if (range[count-1].seq2 == (uint32_t)-1) {
+ if (range[count-1].seq1 > messages_count &&
+ range[count-1].seq1 != (uint32_t)-1)
+ return FALSE;
+ } else {
+ if (range[count-1].seq2 > messages_count)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int imap_search_get_msgset_arg(struct client_command_context *cmd,
+ const char *messageset,
+ struct mail_search_args **args_r,
+ const char **error_r)
+{
+ struct mail_search_args *args;
+
+ args = mail_search_build_init();
+ args->args = p_new(args->pool, struct mail_search_arg, 1);
+ args->args->type = SEARCH_SEQSET;
+ p_array_init(&args->args->value.seqset, args->pool, 16);
+ if (imap_seq_set_parse(messageset, &args->args->value.seqset) < 0 ||
+ !msgset_is_valid(&args->args->value.seqset,
+ cmd->client->messages_count)) {
+ *error_r = "Invalid messageset";
+ mail_search_args_unref(&args);
+ return -1;
+ }
+ *args_r = args;
+ return 0;
+}
+
+static int
+imap_search_get_uidset_arg(const char *uidset, struct mail_search_args **args_r,
+ const char **error_r)
+{
+ struct mail_search_args *args;
+
+ args = mail_search_build_init();
+ args->args = p_new(args->pool, struct mail_search_arg, 1);
+ args->args->type = SEARCH_UIDSET;
+ p_array_init(&args->args->value.seqset, args->pool, 16);
+ if (imap_seq_set_parse(uidset, &args->args->value.seqset) < 0) {
+ *error_r = "Invalid uidset";
+ mail_search_args_unref(&args);
+ return -1;
+ }
+
+ *args_r = args;
+ return 0;
+}
+
+int imap_search_get_seqset(struct client_command_context *cmd,
+ const char *set, bool uid,
+ struct mail_search_args **search_args_r)
+{
+ int ret;
+
+ ret = imap_search_get_anyset(cmd, set, uid, search_args_r);
+ if (ret > 0) {
+ mail_search_args_init(*search_args_r,
+ cmd->client->mailbox, TRUE,
+ &cmd->client->search_saved_uidset);
+ }
+ return ret;
+}
+
+void imap_search_anyset_to_uidset(struct client_command_context *cmd,
+ struct mail_search_args *args)
+{
+ struct mail_search_arg *arg = args->args;
+
+ i_assert(arg->next == NULL);
+ switch (arg->type) {
+ case SEARCH_ALL:
+ if (arg->match_not)
+ break;
+ t_array_init(&arg->value.seqset, 1);
+ seq_range_array_add_range(&arg->value.seqset, 1, (uint32_t)-1);
+ /* fall through */
+ case SEARCH_SEQSET: {
+ ARRAY_TYPE(seq_range) seqs = arg->value.seqset;
+
+ arg->type = SEARCH_UIDSET;
+ p_array_init(&arg->value.seqset, args->pool, 16);
+ mailbox_get_uid_range(cmd->client->mailbox, &seqs,
+ &arg->value.seqset);
+ break;
+ }
+ case SEARCH_UIDSET:
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static int imap_search_get_searchres(struct client_command_context *cmd,
+ struct mail_search_args **search_args_r)
+{
+ struct mail_search_args *search_args;
+
+ if (client_handle_search_save_ambiguity(cmd))
+ return 0;
+
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ if (array_is_created(&cmd->client->search_saved_uidset)) {
+ search_args->args->type = SEARCH_UIDSET;
+ p_array_init(&search_args->args->value.seqset,
+ search_args->pool,
+ array_count(&cmd->client->search_saved_uidset));
+ array_append_array(&search_args->args->value.seqset,
+ &cmd->client->search_saved_uidset);
+ } else {
+ /* $ not set yet, match nothing */
+ search_args->args->type = SEARCH_ALL;
+ search_args->args->match_not = TRUE;
+ }
+ *search_args_r = search_args;
+ return 1;
+}
+
+int imap_search_get_anyset(struct client_command_context *cmd,
+ const char *set, bool uid,
+ struct mail_search_args **search_args_r)
+{
+ const char *client_error = NULL;
+ int ret;
+
+ if (strcmp(set, "$") == 0) {
+ /* SEARCHRES extension: replace $ with the last saved
+ search result */
+ return imap_search_get_searchres(cmd, search_args_r);
+ }
+ if (!uid) {
+ ret = imap_search_get_msgset_arg(cmd, set, search_args_r,
+ &client_error);
+ } else {
+ ret = imap_search_get_uidset_arg(set, search_args_r,
+ &client_error);
+ }
+ if (ret < 0) {
+ client_send_command_error(cmd, client_error);
+ return -1;
+ }
+ return 1;
+}
+
+void imap_search_add_changed_since(struct mail_search_args *search_args,
+ uint64_t modseq)
+{
+ struct mail_search_arg *search_arg;
+
+ search_arg = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_arg->type = SEARCH_MODSEQ;
+ search_arg->value.modseq =
+ p_new(search_args->pool, struct mail_search_modseq, 1);
+ search_arg->value.modseq->modseq = modseq + 1;
+
+ search_arg->next = search_args->args->next;
+ search_args->args->next = search_arg;
+}
+
+struct imap_search_seqset_iter {
+ struct mail_search_args *search_args;
+ ARRAY_TYPE(seq_range) seqset_left;
+ unsigned int batch_size;
+};
+
+static void imap_search_seqset_next_batch(struct imap_search_seqset_iter *iter)
+{
+ array_clear(&iter->search_args->args->value.seqset);
+ seq_range_array_merge_n(&iter->search_args->args->value.seqset,
+ &iter->seqset_left, iter->batch_size);
+}
+
+struct imap_search_seqset_iter *
+imap_search_seqset_iter_init(struct mail_search_args *search_args,
+ uint32_t messages_count, unsigned int batch_size)
+{
+ struct imap_search_seqset_iter *iter;
+
+ i_assert(search_args->args->next == NULL);
+
+ iter = i_new(struct imap_search_seqset_iter, 1);
+ iter->search_args = search_args;
+ iter->batch_size = batch_size;
+ mail_search_args_ref(iter->search_args);
+
+ switch (search_args->args->type) {
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ break;
+ case SEARCH_ALL:
+ if (search_args->args->match_not) {
+ /* $ used before search result was saved */
+ return iter;
+ }
+ /* 1:* - convert to seqset */
+ search_args->args->type = SEARCH_SEQSET;
+ p_array_init(&search_args->args->value.seqset,
+ search_args->pool, 1);
+ seq_range_array_add_range(&search_args->args->value.seqset,
+ 1, messages_count);
+ break;
+ default:
+ i_panic("Unexpected search_args type %d",
+ search_args->args->type);
+ }
+
+ i_assert(search_args->args->type == SEARCH_SEQSET ||
+ search_args->args->type == SEARCH_UIDSET);
+
+ i_array_init(&iter->seqset_left,
+ array_count(&search_args->args->value.seqset));
+ array_append_array(&iter->seqset_left, &search_args->args->value.seqset);
+ imap_search_seqset_next_batch(iter);
+ return iter;
+}
+
+void imap_search_seqset_iter_deinit(struct imap_search_seqset_iter **_iter)
+{
+ struct imap_search_seqset_iter *iter = *_iter;
+
+ if (iter == NULL)
+ return;
+
+ mail_search_args_unref(&iter->search_args);
+ array_free(&iter->seqset_left);
+ i_free(iter);
+}
+
+bool imap_search_seqset_iter_next(struct imap_search_seqset_iter *iter)
+{
+ if (!array_is_created(&iter->seqset_left) ||
+ array_count(&iter->seqset_left) == 0)
+ return FALSE;
+
+ /* remove the last batch of searched mails from seqset_left */
+ if (seq_range_array_remove_seq_range(&iter->seqset_left,
+ &iter->search_args->args->value.seqset) == 0)
+ i_unreached();
+ imap_search_seqset_next_batch(iter);
+ return array_count(&iter->search_args->args->value.seqset) > 0;
+}
diff --git a/src/imap/imap-search-args.h b/src/imap/imap-search-args.h
new file mode 100644
index 0000000..4bf6c4c
--- /dev/null
+++ b/src/imap/imap-search-args.h
@@ -0,0 +1,47 @@
+#ifndef IMAP_SEARCH_ARGS_H
+#define IMAP_SEARCH_ARGS_H
+
+#include "mail-search.h"
+
+struct imap_arg;
+struct mailbox;
+struct client_command_context;
+
+/* Builds search arguments based on IMAP arguments. Returns -1 if search
+ arguments are invalid, 0 if we have to wait for unambiguity,
+ 1 if we can continue. */
+int imap_search_args_build(struct client_command_context *cmd,
+ const struct imap_arg *args, const char *charset,
+ struct mail_search_args **search_args_r);
+
+/* Returns -1 if set is invalid, 0 if we have to wait for unambiguity,
+ 1 if we were successful. search_args_r is set to contain either a seqset
+ or uidset. */
+int imap_search_get_anyset(struct client_command_context *cmd,
+ const char *set, bool uid,
+ struct mail_search_args **search_args_r);
+/* Like imap_search_get_anyset(), but always returns a seqset. */
+int imap_search_get_seqset(struct client_command_context *cmd,
+ const char *set, bool uid,
+ struct mail_search_args **search_args_r);
+/* Convert search args returned by imap_search_get_anyset() to SEARCH_UIDSET. */
+void imap_search_anyset_to_uidset(struct client_command_context *cmd,
+ struct mail_search_args *args);
+
+void imap_search_add_changed_since(struct mail_search_args *search_args,
+ uint64_t modseq);
+
+/* Iterate search_args in batches of messages. The search_args itself is
+ modified each time imap_search_seqset_iter_next() is called. Note that
+ search_args is expected to come from imap_search_get_anyset(), so it should
+ have a single parameter containing SEARCH_ALL, SEARCH_SEQSET or
+ SEARCH_UIDSET. */
+struct imap_search_seqset_iter *
+imap_search_seqset_iter_init(struct mail_search_args *search_args,
+ uint32_t messages_count, unsigned int batch_size);
+/* Iterate the next batch. Returns TRUE if the batch was updated, FALSE if
+ all the batches have been iterated. */
+bool imap_search_seqset_iter_next(struct imap_search_seqset_iter *iter);
+void imap_search_seqset_iter_deinit(struct imap_search_seqset_iter **iter);
+
+#endif
diff --git a/src/imap/imap-search.c b/src/imap/imap-search.c
new file mode 100644
index 0000000..8b5d660
--- /dev/null
+++ b/src/imap/imap-search.c
@@ -0,0 +1,612 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ostream.h"
+#include "str.h"
+#include "seq-range-array.h"
+#include "time-util.h"
+#include "imap-resp-code.h"
+#include "imap-quote.h"
+#include "imap-seqset.h"
+#include "imap-util.h"
+#include "mail-search-build.h"
+#include "imap-fetch.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "imap-search.h"
+
+
+static int imap_search_deinit(struct imap_search_context *ctx);
+
+static int
+imap_partial_range_parse(struct imap_search_context *ctx, const char *str)
+{
+ ctx->partial1 = 0;
+ ctx->partial2 = 0;
+ for (; *str >= '0' && *str <= '9'; str++)
+ ctx->partial1 = ctx->partial1 * 10 + *str-'0';
+ if (*str != ':' || ctx->partial1 == 0)
+ return -1;
+ for (str++; *str >= '0' && *str <= '9'; str++)
+ ctx->partial2 = ctx->partial2 * 10 + *str-'0';
+ if (*str != '\0' || ctx->partial2 == 0)
+ return -1;
+
+ if (ctx->partial1 > ctx->partial2) {
+ uint32_t temp = ctx->partial2;
+ ctx->partial2 = ctx->partial1;
+ ctx->partial1 = temp;
+ }
+
+ return 0;
+}
+
+static bool
+search_parse_fetch_att(struct imap_search_context *ctx,
+ const struct imap_arg *update_args)
+{
+ const char *client_error;
+
+ ctx->fetch_pool = pool_alloconly_create("search update fetch", 512);
+ if (imap_fetch_att_list_parse(ctx->cmd->client, ctx->fetch_pool,
+ update_args, &ctx->fetch_ctx,
+ &client_error) < 0) {
+ client_send_command_error(ctx->cmd, t_strconcat(
+ "SEARCH UPDATE fetch-att: ", client_error, NULL));
+ pool_unref(&ctx->fetch_pool);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+search_parse_return_options(struct imap_search_context *ctx,
+ const struct imap_arg *args)
+{
+ struct client_command_context *cmd = ctx->cmd;
+ const struct imap_arg *update_args;
+ const char *name, *str;
+ unsigned int idx;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &name)) {
+ client_send_command_error(cmd,
+ "SEARCH return options contain non-atoms.");
+ return FALSE;
+ }
+ name = t_str_ucase(name);
+ args++;
+ if (strcmp(name, "MIN") == 0)
+ ctx->return_options |= SEARCH_RETURN_MIN;
+ else if (strcmp(name, "MAX") == 0)
+ ctx->return_options |= SEARCH_RETURN_MAX;
+ else if (strcmp(name, "ALL") == 0)
+ ctx->return_options |= SEARCH_RETURN_ALL;
+ else if (strcmp(name, "COUNT") == 0)
+ ctx->return_options |= SEARCH_RETURN_COUNT;
+ else if (strcmp(name, "SAVE") == 0)
+ ctx->return_options |= SEARCH_RETURN_SAVE;
+ else if (strcmp(name, "CONTEXT") == 0) {
+ /* no-op */
+ } else if (strcmp(name, "UPDATE") == 0) {
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0) {
+ client_send_command_error(cmd,
+ "SEARCH return options have duplicate UPDATE.");
+ return FALSE;
+ }
+ ctx->return_options |= SEARCH_RETURN_UPDATE;
+ if (imap_arg_get_list(args, &update_args)) {
+ if (!search_parse_fetch_att(ctx, update_args))
+ return FALSE;
+ args++;
+ }
+ } else if (strcmp(name, "RELEVANCY") == 0)
+ ctx->return_options |= SEARCH_RETURN_RELEVANCY;
+ else if (strcmp(name, "PARTIAL") == 0) {
+ if (ctx->partial1 != 0) {
+ client_send_command_error(cmd,
+ "PARTIAL can be used only once.");
+ return FALSE;
+ }
+ ctx->return_options |= SEARCH_RETURN_PARTIAL;
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(cmd,
+ "PARTIAL range missing.");
+ return FALSE;
+ }
+ if (imap_partial_range_parse(ctx, str) < 0) {
+ client_send_command_error(cmd,
+ "PARTIAL range broken.");
+ return FALSE;
+ }
+ args++;
+ } else {
+ client_send_command_error(cmd,
+ "Unknown SEARCH return option");
+ return FALSE;
+ }
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0 &&
+ client_search_update_lookup(cmd->client, cmd->tag, &idx) != NULL) {
+ client_send_command_error(cmd, "Duplicate search update tag");
+ return FALSE;
+ }
+ if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0 &&
+ (ctx->return_options & SEARCH_RETURN_ALL) != 0) {
+ client_send_command_error(cmd, "PARTIAL conflicts with ALL");
+ return FALSE;
+ }
+
+ if (ctx->return_options == 0)
+ ctx->return_options = SEARCH_RETURN_ALL;
+ ctx->return_options |= SEARCH_RETURN_ESEARCH;
+ return TRUE;
+}
+
+static void imap_search_args_check(struct imap_search_context *ctx,
+ const struct mail_search_arg *sargs)
+{
+ for (; sargs != NULL; sargs = sargs->next) {
+ switch (sargs->type) {
+ case SEARCH_SEQSET:
+ ctx->have_seqsets = TRUE;
+ break;
+ case SEARCH_MODSEQ:
+ ctx->have_modseqs = TRUE;
+ break;
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ imap_search_args_check(ctx, sargs->value.subargs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+static void imap_search_result_save(struct imap_search_context *ctx)
+{
+ struct client *client = ctx->cmd->client;
+ struct mail_search_result *result;
+ struct imap_search_update *update;
+
+ if (!array_is_created(&client->search_updates))
+ i_array_init(&client->search_updates, 32);
+ else if (array_count(&client->search_updates) >=
+ CLIENT_MAX_SEARCH_UPDATES) {
+ /* too many updates */
+ string_t *str = t_str_new(256);
+ str_append(str, "* NO [NOUPDATE ");
+ imap_append_quoted(str, ctx->cmd->tag);
+ str_append_c(str, ']');
+ client_send_line(client, str_c(str));
+ ctx->return_options &= ENUM_NEGATE(SEARCH_RETURN_UPDATE);
+ imap_search_context_free(ctx);
+ return;
+ }
+ result = mailbox_search_result_save(ctx->search_ctx,
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC);
+
+ update = array_append_space(&client->search_updates);
+ update->tag = i_strdup(ctx->cmd->tag);
+ update->result = result;
+ update->return_uids = ctx->cmd->uid;
+ update->fetch_pool = ctx->fetch_pool;
+ update->fetch_ctx = ctx->fetch_ctx;
+ ctx->fetch_pool = NULL;
+ ctx->fetch_ctx = NULL;
+}
+
+static void imap_search_send_result_standard(struct imap_search_context *ctx)
+{
+ const struct seq_range *range;
+ string_t *str;
+ uint32_t seq;
+
+ str = t_str_new(1024);
+ str_append(str, ctx->sorting ? "* SORT" : "* SEARCH");
+ array_foreach(&ctx->result, range) {
+ for (seq = range->seq1; seq <= range->seq2; seq++)
+ str_printfa(str, " %u", seq);
+ if (str_len(str) >= 1024-32) {
+ o_stream_nsend(ctx->cmd->client->output,
+ str_data(str), str_len(str));
+ str_truncate(str, 0);
+ }
+ }
+
+ if (ctx->highest_seen_modseq != 0) {
+ str_printfa(str, " (MODSEQ %"PRIu64")",
+ ctx->highest_seen_modseq);
+ }
+ str_append(str, "\r\n");
+ o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str));
+}
+
+static void
+imap_search_send_partial(struct imap_search_context *ctx, string_t *str)
+{
+ str_printfa(str, " PARTIAL (%u:%u ", ctx->partial1, ctx->partial2);
+ if (array_count(&ctx->result) == 0) {
+ /* no results (in range) */
+ str_append(str, "NIL");
+ } else {
+ imap_write_seq_range(str, &ctx->result);
+ }
+ str_append_c(str, ')');
+}
+
+static void
+imap_search_send_relevancy(struct imap_search_context *ctx, string_t *dest)
+{
+ const float *scores;
+ unsigned int i, count;
+ float diff, imap_score;
+
+ scores = array_get(&ctx->relevancy_scores, &count);
+ if (count == 0)
+ return;
+
+ /* we'll need to convert float scores to numbers 1..100
+ FIXME: would be a good idea to try to detect non-linear score
+ mappings and convert them better.. */
+ diff = ctx->max_relevancy - ctx->min_relevancy;
+ if (diff == 0)
+ diff = 1.0;
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(dest, ' ');
+ imap_score = (scores[i] - ctx->min_relevancy) / diff * 100.0;
+ if (imap_score < 1)
+ str_append(dest, "1");
+ else
+ str_printfa(dest, "%u", (unsigned int)imap_score);
+ }
+}
+
+static void imap_search_send_result(struct imap_search_context *ctx)
+{
+ struct client *client = ctx->cmd->client;
+ string_t *str;
+
+ if ((ctx->return_options & SEARCH_RETURN_ESEARCH) == 0) {
+ imap_search_send_result_standard(ctx);
+ return;
+ }
+
+ if (ctx->return_options ==
+ (SEARCH_RETURN_ESEARCH | SEARCH_RETURN_SAVE)) {
+ /* we only wanted to save the result, don't return
+ ESEARCH result. */
+ return;
+ }
+
+ str = str_new(default_pool, 1024);
+ str_append(str, "* ESEARCH (TAG ");
+ imap_append_string(str, ctx->cmd->tag);
+ str_append_c(str, ')');
+
+ if (ctx->cmd->uid)
+ str_append(str, " UID");
+
+ if ((ctx->return_options & SEARCH_RETURN_MIN) != 0 && ctx->min_id != 0)
+ str_printfa(str, " MIN %u", ctx->min_id);
+ if ((ctx->return_options & SEARCH_RETURN_MAX) != 0 &&
+ ctx->max_seq != 0) {
+ uint32_t id = ctx->cmd->uid ? ctx->max_uid : ctx->max_seq;
+ str_printfa(str, " MAX %u", id);
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_ALL) != 0 &&
+ array_count(&ctx->result) > 0) {
+ str_append(str, " ALL ");
+ imap_write_seq_range(str, &ctx->result);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) {
+ str_append(str, " RELEVANCY (");
+ imap_search_send_relevancy(ctx, str);
+ str_append_c(str, ')');
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0)
+ imap_search_send_partial(ctx, str);
+
+ if ((ctx->return_options & SEARCH_RETURN_COUNT) != 0)
+ str_printfa(str, " COUNT %u", ctx->result_count);
+ if (ctx->highest_seen_modseq != 0) {
+ str_printfa(str, " MODSEQ %"PRIu64,
+ ctx->highest_seen_modseq);
+ }
+ str_append(str, "\r\n");
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ str_free(&str);
+}
+
+static void
+search_update_mail(struct imap_search_context *ctx, struct mail *mail)
+{
+ uint64_t modseq;
+
+ if (ctx->max_update_seq == mail->seq) {
+ /* MIN already handled this mail */
+ return;
+ }
+ ctx->max_update_seq = mail->seq;
+
+ if ((ctx->return_options & SEARCH_RETURN_MODSEQ) != 0) {
+ modseq = mail_get_modseq(mail);
+ if (ctx->highest_seen_modseq < modseq)
+ ctx->highest_seen_modseq = modseq;
+ }
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) {
+ seq_range_array_add(&ctx->cmd->client->search_saved_uidset,
+ mail->uid);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) {
+ const char *str;
+ float score;
+
+ if (mail_get_special(mail, MAIL_FETCH_SEARCH_RELEVANCY, &str) < 0)
+ score = 0;
+ else
+ score = strtod(str, NULL);
+ array_push_back(&ctx->relevancy_scores, &score);
+ if (ctx->min_relevancy > score)
+ ctx->min_relevancy = score;
+ if (ctx->max_relevancy < score)
+ ctx->max_relevancy = score;
+ }
+}
+
+static void search_add_result_id(struct imap_search_context *ctx, uint32_t id)
+{
+ struct seq_range *range;
+ unsigned int count;
+
+ /* only append the data. this is especially important when we're
+ returning a sort result. */
+ range = array_get_modifiable(&ctx->result, &count);
+ if (count > 0 && id == range[count-1].seq2 + 1) {
+ range[count-1].seq2++;
+ } else {
+ range = array_append_space(&ctx->result);
+ range->seq1 = range->seq2 = id;
+ }
+}
+
+static bool cmd_search_more(struct client_command_context *cmd)
+{
+ struct imap_search_context *ctx = cmd->context;
+ enum search_return_options opts = ctx->return_options;
+ struct mail *mail;
+ enum mailbox_sync_flags sync_flags;
+ uint32_t id;
+ const char *ok_reply;
+ bool tryagain, lost_data;
+
+ if (cmd->cancel) {
+ (void)imap_search_deinit(ctx);
+ return TRUE;
+ }
+
+ while (mailbox_search_next_nonblock(ctx->search_ctx,
+ &mail, &tryagain)) {
+ id = cmd->uid ? mail->uid : mail->seq;
+ ctx->result_count++;
+
+ ctx->max_seq = mail->seq;
+ ctx->max_uid = mail->uid;
+ if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN) && ctx->min_id == 0) {
+ /* MIN not set yet */
+ ctx->min_id = id;
+ search_update_mail(ctx, mail);
+ }
+ if (HAS_ANY_BITS(opts, SEARCH_RETURN_ALL)) {
+ /* ALL and PARTIAL are mutually exclusive */
+ i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_PARTIAL));
+ search_add_result_id(ctx, id);
+ } else if ((opts & SEARCH_RETURN_PARTIAL) != 0) {
+ /* only update if it's within range */
+ i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_ALL));
+ if (ctx->partial1 <= ctx->result_count &&
+ ctx->partial2 >= ctx->result_count)
+ search_add_result_id(ctx, id);
+ else if (HAS_ALL_BITS(opts, SEARCH_RETURN_COUNT |
+ SEARCH_RETURN_SAVE)) {
+ /* (SAVE COUNT PARTIAL n:m) must include all
+ results in SAVE, but not include mails
+ outside the PARTIAL range in MODSEQ or
+ RELEVANCY */
+ seq_range_array_add(&cmd->client->search_saved_uidset,
+ mail->uid);
+ continue;
+ } else {
+ continue;
+ }
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_COUNT)) {
+ /* with COUNT don't add it to results, but handle
+ SAVE and MODSEQ */
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN |
+ SEARCH_RETURN_MAX)) {
+ /* MIN and/or MAX only requested, but we don't know if
+ this is MAX until the search is finished. */
+ continue;
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_SAVE)) {
+ /* Only SAVE used */
+ }
+ search_update_mail(ctx, mail);
+ }
+ if (tryagain)
+ return FALSE;
+
+ if ((opts & SEARCH_RETURN_MAX) != 0 && ctx->max_seq != 0 &&
+ ctx->max_update_seq != ctx->max_seq &&
+ HAS_ANY_BITS(opts, SEARCH_RETURN_MODSEQ |
+ SEARCH_RETURN_SAVE | SEARCH_RETURN_RELEVANCY)) {
+ /* finish handling MAX */
+ mail = mail_alloc(ctx->trans, 0, NULL);
+ mail_set_seq(mail, ctx->max_seq);
+ search_update_mail(ctx, mail);
+ mail_free(&mail);
+ }
+
+ lost_data = mailbox_search_seen_lost_data(ctx->search_ctx);
+ if (imap_search_deinit(ctx) < 0) {
+ client_send_box_error(cmd, cmd->client->mailbox);
+ return TRUE;
+ }
+
+ sync_flags = MAILBOX_SYNC_FLAG_FAST;
+ if (!cmd->uid || ctx->have_seqsets)
+ sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+ ok_reply = t_strdup_printf("OK %s%s completed",
+ lost_data ? "["IMAP_RESP_CODE_EXPUNGEISSUED"] " : "",
+ !ctx->sorting ? "Search" : "Sort");
+ return cmd_sync(cmd, sync_flags, 0, ok_reply);
+}
+
+static void cmd_search_more_callback(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ bool finished;
+
+ o_stream_cork(client->output);
+ finished = command_exec(cmd);
+ o_stream_uncork(client->output);
+
+ if (!finished)
+ (void)client_handle_unfinished_cmd(cmd);
+ else
+ client_command_free(&cmd);
+ cmd_sync_delayed(client);
+
+ client_continue_pending_input(client);
+}
+
+int cmd_search_parse_return_if_found(struct imap_search_context *ctx,
+ const struct imap_arg **_args)
+{
+ const struct imap_arg *list_args, *args = *_args;
+ struct client_command_context *cmd = ctx->cmd;
+
+ if (!imap_arg_atom_equals(&args[0], "RETURN") ||
+ !imap_arg_get_list(&args[1], &list_args)) {
+ ctx->return_options = SEARCH_RETURN_ALL;
+ return 1;
+ }
+
+ if (!search_parse_return_options(ctx, list_args)) {
+ imap_search_context_free(ctx);
+ return -1;
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) {
+ /* wait if there is another SEARCH SAVE command running. */
+ if (client_handle_search_save_ambiguity(cmd)) {
+ imap_search_context_free(ctx);
+ return 0;
+ }
+
+ cmd->search_save_result = TRUE;
+ }
+
+ *_args = args + 2;
+ return 1;
+}
+
+bool imap_search_start(struct imap_search_context *ctx,
+ struct mail_search_args *sargs,
+ const enum mail_sort_type *sort_program)
+{
+ struct client_command_context *cmd = ctx->cmd;
+
+ imap_search_args_check(ctx, sargs->args);
+
+ if (ctx->have_modseqs) {
+ ctx->return_options |= SEARCH_RETURN_MODSEQ;
+ client_enable(cmd->client, imap_feature_condstore);
+ }
+
+ ctx->box = cmd->client->mailbox;
+ ctx->trans = mailbox_transaction_begin(ctx->box, 0,
+ imap_client_command_get_reason(cmd));
+ ctx->sargs = sargs;
+ ctx->search_ctx =
+ mailbox_search_init(ctx->trans, sargs, sort_program, 0, NULL);
+ ctx->sorting = sort_program != NULL;
+ i_array_init(&ctx->result, 128);
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0)
+ imap_search_result_save(ctx);
+ else {
+ i_assert(ctx->fetch_ctx == NULL);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0)
+ i_array_init(&ctx->relevancy_scores, 128);
+
+ cmd->func = cmd_search_more;
+ cmd->context = ctx;
+
+ if (cmd_search_more(cmd))
+ return TRUE;
+
+ /* we may have moved onto syncing by now */
+ if (cmd->func == cmd_search_more) {
+ ctx->to = timeout_add(0, cmd_search_more_callback, cmd);
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
+ }
+ return FALSE;
+}
+
+static int imap_search_deinit(struct imap_search_context *ctx)
+{
+ int ret = 0;
+
+ if (mailbox_search_deinit(&ctx->search_ctx) < 0)
+ ret = -1;
+
+ /* Send the result also after failing. It might have something useful,
+ even though it didn't fully succeed. The client should be able to
+ realize that there was some failure because NO is returned. */
+ if (!ctx->cmd->cancel &&
+ (ret == 0 || array_count(&ctx->result) > 0))
+ imap_search_send_result(ctx);
+
+ if (ret < 0 || ctx->cmd->cancel) {
+ /* search failed */
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0)
+ array_clear(&ctx->cmd->client->search_saved_uidset);
+ }
+
+ (void)mailbox_transaction_commit(&ctx->trans);
+
+ timeout_remove(&ctx->to);
+ if (array_is_created(&ctx->relevancy_scores))
+ array_free(&ctx->relevancy_scores);
+ array_free(&ctx->result);
+ mail_search_args_deinit(ctx->sargs);
+ mail_search_args_unref(&ctx->sargs);
+ imap_search_context_free(ctx);
+
+ ctx->cmd->context = NULL;
+ return ret;
+}
+
+void imap_search_context_free(struct imap_search_context *ctx)
+{
+ if (ctx->fetch_ctx != NULL) {
+ imap_fetch_free(&ctx->fetch_ctx);
+ pool_unref(&ctx->fetch_pool);
+ }
+}
+
+void imap_search_update_free(struct imap_search_update *update)
+{
+ if (update->fetch_ctx != NULL) {
+ imap_fetch_free(&update->fetch_ctx);
+ pool_unref(&update->fetch_pool);
+ }
+ mailbox_search_result_free(&update->result);
+ i_free(update->tag);
+}
diff --git a/src/imap/imap-search.h b/src/imap/imap-search.h
new file mode 100644
index 0000000..f665375
--- /dev/null
+++ b/src/imap/imap-search.h
@@ -0,0 +1,61 @@
+#ifndef IMAP_SEARCH_H
+#define IMAP_SEARCH_H
+
+#include <sys/time.h>
+
+enum search_return_options {
+ SEARCH_RETURN_ESEARCH = 0x0001,
+ SEARCH_RETURN_MIN = 0x0002,
+ SEARCH_RETURN_MAX = 0x0004,
+ SEARCH_RETURN_ALL = 0x0008,
+ SEARCH_RETURN_COUNT = 0x0010,
+ SEARCH_RETURN_MODSEQ = 0x0020,
+ SEARCH_RETURN_SAVE = 0x0040,
+ SEARCH_RETURN_UPDATE = 0x0080,
+ SEARCH_RETURN_PARTIAL = 0x0100,
+ SEARCH_RETURN_RELEVANCY = 0x0200
+/* Options that don't return any seq/uid results, and also don't affect
+ SEARCHRES $ when combined with MIN/MAX. */
+#define SEARCH_RETURN_NORESULTS \
+ (SEARCH_RETURN_ESEARCH | SEARCH_RETURN_MODSEQ | SEARCH_RETURN_SAVE | \
+ SEARCH_RETURN_UPDATE | SEARCH_RETURN_RELEVANCY)
+};
+
+struct imap_search_context {
+ struct client_command_context *cmd;
+ struct mailbox *box;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+
+ pool_t fetch_pool;
+ struct imap_fetch_context *fetch_ctx;
+
+ struct mail_search_args *sargs;
+ enum search_return_options return_options;
+ uint32_t partial1, partial2;
+
+ struct timeout *to;
+ ARRAY_TYPE(seq_range) result;
+ unsigned int result_count;
+ uint32_t min_id, max_update_seq, max_seq, max_uid;
+
+ ARRAY(float) relevancy_scores;
+ float min_relevancy, max_relevancy;
+
+ uint64_t highest_seen_modseq;
+
+ bool have_seqsets:1;
+ bool have_modseqs:1;
+ bool sorting:1;
+};
+
+int cmd_search_parse_return_if_found(struct imap_search_context *ctx,
+ const struct imap_arg **args);
+void imap_search_context_free(struct imap_search_context *ctx);
+
+bool imap_search_start(struct imap_search_context *ctx,
+ struct mail_search_args *sargs,
+ const enum mail_sort_type *sort_program) ATTR_NULL(3);
+void imap_search_update_free(struct imap_search_update *update);
+
+#endif
diff --git a/src/imap/imap-settings.c b/src/imap/imap-settings.c
new file mode 100644
index 0000000..2acf9dd
--- /dev/null
+++ b/src/imap/imap-settings.c
@@ -0,0 +1,197 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hostpid.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "smtp-submit-settings.h"
+#include "imap-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+static bool imap_settings_verify(void *_set, pool_t pool,
+ const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings imap_unix_listeners_array[] = {
+ { "login/imap", 0666, "", "" },
+ { "imap-master", 0600, "", "" }
+};
+static struct file_listener_settings *imap_unix_listeners[] = {
+ &imap_unix_listeners_array[0],
+ &imap_unix_listeners_array[1]
+};
+static buffer_t imap_unix_listeners_buf = {
+ { { imap_unix_listeners, sizeof(imap_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings imap_service_settings = {
+ .name = "imap",
+ .protocol = "imap",
+ .type = "",
+ .executable = "imap",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &imap_unix_listeners_buf,
+ sizeof(imap_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct imap_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define imap_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(SIZE, imap_max_line_length),
+ DEF(TIME, imap_idle_notify_interval),
+ DEF(STR, imap_capability),
+ DEF(STR, imap_client_workarounds),
+ DEF(STR, imap_logout_format),
+ DEF(STR, imap_id_send),
+ DEF(STR, imap_id_log),
+ DEF(ENUM, imap_fetch_failure),
+ DEF(BOOL, imap_metadata),
+ DEF(BOOL, imap_literal_minus),
+ DEF(TIME, imap_hibernate_timeout),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct imap_settings imap_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ /* RFC-2683 recommends at least 8000 bytes. Some clients however don't
+ break large message sets to multiple commands, so we're pretty
+ liberal by default. */
+ .imap_max_line_length = 64*1024,
+ .imap_idle_notify_interval = 2*60,
+ .imap_capability = "",
+ .imap_client_workarounds = "",
+ .imap_logout_format = "in=%i out=%o deleted=%{deleted} "
+ "expunged=%{expunged} trashed=%{trashed} "
+ "hdr_count=%{fetch_hdr_count} hdr_bytes=%{fetch_hdr_bytes} "
+ "body_count=%{fetch_body_count} body_bytes=%{fetch_body_bytes}",
+ .imap_id_send = "name *",
+ .imap_id_log = "",
+ .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after",
+ .imap_metadata = FALSE,
+ .imap_literal_minus = FALSE,
+ .imap_hibernate_timeout = 0,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143
+};
+
+static const struct setting_parser_info *imap_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ &smtp_submit_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info imap_setting_parser_info = {
+ .module_name = "imap",
+ .defines = imap_setting_defines,
+ .defaults = &imap_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imap_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = imap_settings_verify,
+ .dependencies = imap_setting_dependencies
+};
+
+/* <settings checks> */
+struct imap_client_workaround_list {
+ const char *name;
+ enum imap_client_workarounds num;
+};
+
+static const struct imap_client_workaround_list imap_client_workaround_list[] = {
+ { "delay-newmail", WORKAROUND_DELAY_NEWMAIL },
+ { "tb-extra-mailbox-sep", WORKAROUND_TB_EXTRA_MAILBOX_SEP },
+ { "tb-lsub-flags", WORKAROUND_TB_LSUB_FLAGS },
+ { NULL, 0 }
+};
+
+static int
+imap_settings_parse_workarounds(struct imap_settings *set,
+ const char **error_r)
+{
+ enum imap_client_workarounds client_workarounds = 0;
+ const struct imap_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->imap_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = imap_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("imap_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+
+static bool
+imap_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct imap_settings *set = _set;
+
+ if (imap_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+ if (strcmp(set->imap_fetch_failure, "disconnect-immediately") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY;
+ else if (strcmp(set->imap_fetch_failure, "disconnect-after") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER;
+ else if (strcmp(set->imap_fetch_failure, "no-after") == 0)
+ set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_NO_AFTER;
+ else {
+ *error_r = t_strdup_printf("Unknown imap_fetch_failure: %s",
+ set->imap_fetch_failure);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/imap/imap-settings.h b/src/imap/imap-settings.h
new file mode 100644
index 0000000..f4ff7bc
--- /dev/null
+++ b/src/imap/imap-settings.h
@@ -0,0 +1,49 @@
+#ifndef IMAP_SETTINGS_H
+#define IMAP_SETTINGS_H
+
+#include "net.h"
+
+struct mail_user_settings;
+
+/* <settings checks> */
+enum imap_client_workarounds {
+ WORKAROUND_DELAY_NEWMAIL = 0x01,
+ WORKAROUND_TB_EXTRA_MAILBOX_SEP = 0x08,
+ WORKAROUND_TB_LSUB_FLAGS = 0x10
+};
+
+enum imap_client_fetch_failure {
+ IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY,
+ IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER,
+ IMAP_CLIENT_FETCH_FAILURE_NO_AFTER,
+};
+/* </settings checks> */
+
+struct imap_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ /* imap: */
+ uoff_t imap_max_line_length;
+ unsigned int imap_idle_notify_interval;
+ const char *imap_capability;
+ const char *imap_client_workarounds;
+ const char *imap_logout_format;
+ const char *imap_id_send;
+ const char *imap_id_log;
+ const char *imap_fetch_failure;
+ bool imap_metadata;
+ bool imap_literal_minus;
+ unsigned int imap_hibernate_timeout;
+
+ /* imap urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+
+ enum imap_client_workarounds parsed_workarounds;
+ enum imap_client_fetch_failure parsed_fetch_failure;
+};
+
+extern const struct setting_parser_info imap_setting_parser_info;
+
+#endif
diff --git a/src/imap/imap-state.c b/src/imap/imap-state.c
new file mode 100644
index 0000000..2b064ec
--- /dev/null
+++ b/src/imap/imap-state.c
@@ -0,0 +1,897 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "crc32.h"
+#include "numpack.h"
+#include "net.h"
+#include "ostream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "imap-util.h"
+#include "mail-search-build.h"
+#include "mail-storage-private.h"
+#include "mailbox-recent-flags.h"
+#include "imap-client.h"
+#include "imap-feature.h"
+#include "imap-fetch.h"
+#include "imap-search-args.h"
+#include "imap-state.h"
+
+enum imap_state_type_public {
+ IMAP_STATE_TYPE_MAILBOX = 'B',
+ IMAP_STATE_TYPE_ENABLED_FEATURE = 'F',
+ IMAP_STATE_TYPE_SEARCHRES = '1',
+};
+
+enum imap_state_type_internal {
+ IMAP_STATE_TYPE_ID_LOGGED = 'I',
+ IMAP_STATE_TYPE_TLS_COMPRESSION = 'C',
+};
+
+struct mailbox_import_state {
+ const char *vname;
+ guid_128_t mailbox_guid;
+ bool examined;
+ uint32_t keywords_count, keywords_crc32, uids_crc32;
+ uint32_t uidvalidity, uidnext, messages;
+ uint64_t highest_modseq;
+ ARRAY_TYPE(seq_range) recent_uids;
+};
+
+static void
+export_seq_range(buffer_t *dest, const ARRAY_TYPE(seq_range) *range)
+{
+ const struct seq_range *uids;
+ unsigned int i, count;
+ uint32_t next_uid;
+
+ uids = array_get(range, &count);
+ numpack_encode(dest, count);
+ next_uid = 1;
+ for (i = 0; i < count; i++) {
+ i_assert(uids[i].seq1 >= next_uid);
+ if (uids[i].seq1 == uids[i].seq2) {
+ numpack_encode(dest, (uids[i].seq1 - next_uid) << 1);
+ } else {
+ numpack_encode(dest, 1 | ((uids[i].seq1 - next_uid) << 1));
+ numpack_encode(dest, uids[i].seq2 - uids[i].seq1 - 1);
+ }
+ next_uid = uids[i].seq2 + 1;
+ }
+}
+
+static int
+import_seq_range(const unsigned char **data, const unsigned char *end,
+ ARRAY_TYPE(seq_range) *range)
+{
+ uint32_t i, count, next_uid, num, uid1, uid2;
+
+ if (numpack_decode32(data, end, &count) < 0)
+ return -1;
+ next_uid = 1;
+
+ for (i = 0; i < count; i++) {
+ if (numpack_decode32(data, end, &num) < 0)
+ return -1;
+ uid1 = next_uid + (num >> 1);
+ if ((num & 1) == 0) {
+ uid2 = uid1;
+ seq_range_array_add(range, uid1);
+ } else {
+ if (numpack_decode32(data, end, &num) < 0)
+ return -1;
+ uid2 = uid1 + num + 1;
+ seq_range_array_add_range(range, uid1, uid2);
+ }
+ next_uid = uid2 + 1;
+ }
+ return 0;
+}
+
+int imap_state_export_internal(struct client *client, buffer_t *dest,
+ const char **error_r)
+{
+ /* the only IMAP command we allow running is IDLE or X-STATE */
+ if (client->command_queue_size > 1) {
+ *error_r = "Multiple commands in progress";
+ return 0;
+ }
+ if (client->command_queue == NULL ||
+ strcasecmp(client->command_queue->name, "IDLE") != 0) {
+ /* this would require saving the seq <-> uid mapping
+ and restore it on import. quite a lot of trouble if
+ messages have been expunged in the mean time. */
+ *error_r = "Non-IDLE connections not supported currently";
+ return 0;
+ }
+ return client->v.state_export(client, TRUE, dest, error_r);
+}
+
+int imap_state_export_external(struct client *client, buffer_t *dest,
+ const char **error_r)
+{
+ if (client->command_queue_size > 1) {
+ *error_r = "Multiple commands in progress";
+ return 0;
+ }
+
+ i_assert(client->command_queue_size == 1);
+ i_assert(strcmp(client->command_queue->name, "X-STATE") == 0);
+ return client->v.state_export(client, FALSE, dest, error_r);
+}
+
+static int
+imap_state_import(struct client *client, bool internal,
+ const unsigned char *data, size_t size, const char **error_r)
+{
+ ssize_t ret;
+
+ while (size > 0) {
+ ret = client->v.state_import(client, internal,
+ data, size, error_r);
+ if (ret <= 0) {
+ i_assert(*error_r != NULL);
+ return ret < 0 ? -1 : 0;
+ }
+ i_assert((size_t)ret <= size);
+ data += ret;
+ size -= ret;
+ }
+ return 1;
+}
+
+int imap_state_import_internal(struct client *client,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ return imap_state_import(client, TRUE, data, size, error_r);
+}
+
+int imap_state_import_external(struct client *client,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ return imap_state_import(client, FALSE, data, size, error_r);
+}
+
+static int
+imap_state_export_mailbox_mails(buffer_t *dest, struct mailbox *box,
+ const char **error_r)
+{
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ ARRAY_TYPE(seq_range) recent_uids;
+ uint32_t crc = 0;
+ int ret = 1;
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+
+ trans = mailbox_transaction_begin(box, 0,
+ "unhibernate imap_state_export_mailbox_mails");
+ search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ t_array_init(&recent_uids, 8);
+ while (mailbox_search_next(search_ctx, &mail)) {
+ crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid));
+ if ((mail_get_flags(mail) & MAIL_RECENT) != 0)
+ seq_range_array_add(&recent_uids, mail->uid);
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0) {
+ *error_r = mailbox_get_last_internal_error(box, NULL);
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&trans);
+
+ numpack_encode(dest, crc);
+ export_seq_range(dest, &recent_uids);
+ return ret;
+}
+
+static uint32_t
+mailbox_status_keywords_crc32(const struct mailbox_status *status)
+{
+ const char *str;
+ uint32_t crc = 0;
+
+ array_foreach_elem(status->keywords, str)
+ crc = crc32_str(str);
+ return crc;
+}
+
+static int
+imap_state_export_mailbox(buffer_t *dest, struct client *client,
+ struct mailbox *box, const char **error_r)
+{
+ struct mailbox_status status;
+ struct mailbox_metadata metadata;
+ const char *vname = mailbox_get_vname(box);
+ enum mail_error mail_error;
+
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT |
+ STATUS_MESSAGES | STATUS_HIGHESTMODSEQ |
+ STATUS_KEYWORDS,
+ &status);
+ if (status.nonpermanent_modseqs) {
+ *error_r = "Nonpermanent modseqs";
+ return 0;
+ }
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ *error_r = mailbox_get_last_internal_error(box, &mail_error);
+ /* if the selected mailbox can't have a GUID, fail silently */
+ return mail_error == MAIL_ERROR_NOTPOSSIBLE ? 0 : -1;
+ }
+
+ buffer_append_c(dest, IMAP_STATE_TYPE_MAILBOX);
+ buffer_append(dest, vname, strlen(vname)+1);
+ buffer_append(dest, metadata.guid, sizeof(metadata.guid));
+
+ buffer_append_c(dest, client->mailbox_examined ? 1 : 0);
+ numpack_encode(dest, status.uidvalidity);
+ numpack_encode(dest, status.uidnext);
+ numpack_encode(dest, status.messages);
+ if (client_has_enabled(client, imap_feature_qresync) &&
+ !client->nonpermanent_modseqs)
+ numpack_encode(dest, client->sync_last_full_modseq);
+ else
+ numpack_encode(dest, status.highest_modseq);
+
+ /* keywords count + CRC32 should be enough to figure out if it
+ needs to be resent */
+ numpack_encode(dest, array_count(status.keywords));
+ numpack_encode(dest, mailbox_status_keywords_crc32(&status));
+
+ /* we're now basically done, but just in case there's a bug add a
+ checksum of the currently existing UIDs and verify it when
+ importing. this also writes the list of recent UIDs. */
+ return imap_state_export_mailbox_mails(dest, box, error_r);
+}
+
+int imap_state_export_base(struct client *client, bool internal,
+ buffer_t *dest, const char **error_r)
+{
+ int ret;
+
+ str_append(dest, "base\n");
+ if (array_is_created(&client->search_updates) &&
+ array_count(&client->search_updates) > 0) {
+ /* these could be tricky */
+ *error_r = "CONTEXT=SEARCH updates not supported currently";
+ return 0;
+ }
+ if (client->notify_ctx != NULL) {
+ /* FIXME: this really should be supported. also IDLE wouldn't
+ be needed if NOTIFY allows sending EXPUNGEs to selected
+ mailbox. */
+ *error_r = "NOTIFY not supported currently";
+ return 0;
+ }
+
+ if (client->mailbox != NULL) {
+ ret = imap_state_export_mailbox(dest, client,
+ client->mailbox, error_r);
+ if (ret <= 0)
+ return ret;
+ }
+
+ /* IMAP features */
+ const char *const *features = client_enabled_features(client);
+ if (features != NULL) {
+ for (unsigned int i = 0; features[i] != NULL; i++) {
+ buffer_append_c(dest, IMAP_STATE_TYPE_ENABLED_FEATURE);
+ buffer_append(dest, features[i], strlen(features[i])+1);
+ }
+ }
+ if (internal) {
+ if (client->id_logged)
+ buffer_append_c(dest, IMAP_STATE_TYPE_ID_LOGGED);
+ if (client->tls_compression)
+ buffer_append_c(dest, IMAP_STATE_TYPE_TLS_COMPRESSION);
+ }
+
+ /* IMAP SEARCHRES extension */
+ if (array_is_created(&client->search_saved_uidset) &&
+ array_count(&client->search_saved_uidset) > 0) {
+ buffer_append_c(dest, IMAP_STATE_TYPE_SEARCHRES);
+ export_seq_range(dest, &client->search_saved_uidset);
+ }
+ return 1;
+}
+
+static int
+import_string(const unsigned char **data, const unsigned char *end,
+ const char **str_r)
+{
+ const unsigned char *p;
+
+ p = memchr(*data, '\0', end - *data);
+ if (p == NULL)
+ return -1;
+ *str_r = (const void *)*data;
+ *data = p + 1;
+ return 0;
+}
+
+static int
+import_send_expunges(struct client *client,
+ const struct mailbox_import_state *state,
+ unsigned int *expunge_count_r,
+ const char **error_r)
+{
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ uint32_t crc = 0, seq, expunged_uid;
+ ARRAY_TYPE(seq_range) uids_filter, expunged_uids;
+ ARRAY_TYPE(uint32_t) expunged_seqs;
+ struct seq_range_iter iter;
+ const uint32_t *seqs;
+ unsigned int i, expunge_count, n = 0;
+ string_t *str;
+ int ret = 0;
+
+ *expunge_count_r = 0;
+
+ if (state->messages == 0) {
+ /* the mailbox was empty originally - there couldn't be any
+ pending expunges. */
+ return 0;
+ }
+ if (state->uidnext <= 1) {
+ *error_r = "Invalid UIDNEXT";
+ return -1;
+ }
+
+ /* get all the message UIDs expunged since the last known modseq */
+ t_array_init(&uids_filter, 1);
+ t_array_init(&expunged_uids, 128);
+ seq_range_array_add_range(&uids_filter, 1, state->uidnext-1);
+ if (!mailbox_get_expunged_uids(client->mailbox, state->highest_modseq,
+ &uids_filter, &expunged_uids)) {
+ *error_r = t_strdup_printf(
+ "Couldn't get recently expunged UIDs "
+ "(uidnext=%u highest_modseq=%"PRIu64")",
+ state->uidnext, state->highest_modseq);
+ return -1;
+ }
+ seq_range_array_iter_init(&iter, &expunged_uids);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+
+ trans = mailbox_transaction_begin(client->mailbox, 0,
+ "unhibernate import_send_expunges");
+ search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ /* find sequence numbers for the expunged UIDs */
+ t_array_init(&expunged_seqs, array_count(&expunged_uids)+1); seq = 0;
+ while (mailbox_search_next(search_ctx, &mail)) {
+ while (seq_range_array_iter_nth(&iter, n, &expunged_uid) &&
+ expunged_uid < mail->uid && seq < state->messages) {
+ seq++; n++;
+ array_push_back(&expunged_seqs, &seq);
+ crc = crc32_data_more(crc, &expunged_uid,
+ sizeof(expunged_uid));
+ }
+ if (seq == state->messages)
+ break;
+ crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid));
+ if (++seq == state->messages)
+ break;
+ }
+ while (seq_range_array_iter_nth(&iter, n, &expunged_uid) &&
+ seq < state->messages) {
+ seq++; n++;
+ array_push_back(&expunged_seqs, &seq);
+ crc = crc32_data_more(crc, &expunged_uid,
+ sizeof(expunged_uid));
+ }
+
+ if (mailbox_search_deinit(&search_ctx) < 0) {
+ *error_r = mailbox_get_last_internal_error(client->mailbox, NULL);
+ ret = -1;
+ } else if (seq != state->messages) {
+ *error_r = t_strdup_printf("Message count mismatch after "
+ "handling expunges (%u != %u)",
+ seq, state->messages);
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&trans);
+ if (ret < 0)
+ return -1;
+
+ seqs = array_get(&expunged_seqs, &expunge_count);
+ if (client->messages_count + expunge_count < state->messages) {
+ *error_r = t_strdup_printf("Message count too low after "
+ "handling expunges (%u < %u)",
+ client->messages_count + expunge_count,
+ state->messages);
+ return -1;
+ }
+ if (crc != state->uids_crc32) {
+ *error_r = t_strdup_printf("Message UIDs CRC32 mismatch (%u != %u)",
+ crc, state->uids_crc32);
+ return -1;
+ }
+
+ if (!client_has_enabled(client, imap_feature_qresync)) {
+ str = t_str_new(32);
+ for (i = expunge_count; i > 0; i--) {
+ str_truncate(str, 0);
+ str_printfa(str, "* %u EXPUNGE", seqs[i-1]);
+ client_send_line(client, str_c(str));
+ }
+ } else {
+ str = str_new(default_pool, 128);
+ str_append(str, "* VANISHED ");
+ imap_write_seq_range(str, &expunged_uids);
+ str_append(str, "\r\n");
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ str_free(&str);
+ }
+ *expunge_count_r = expunge_count;
+ return 0;
+}
+
+static int
+import_send_flag_changes(struct client *client,
+ const struct mailbox_import_state *state,
+ unsigned int *flag_change_count_r)
+{
+ struct imap_fetch_context *fetch_ctx;
+ struct mail_search_args *search_args;
+ ARRAY_TYPE(seq_range) old_uids;
+ pool_t pool;
+ int ret;
+
+ *flag_change_count_r = 0;
+ if (state->messages == 0)
+ return 0;
+
+ t_array_init(&old_uids, 1);
+ seq_range_array_add_range(&old_uids, 1, state->uidnext-1);
+
+ search_args = mail_search_build_init();
+ search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+ search_args->args->type = SEARCH_UIDSET;
+ search_args->args->value.seqset = old_uids;
+ imap_search_add_changed_since(search_args, state->highest_modseq);
+
+ pool = pool_alloconly_create("imap state flag changes", 1024);
+ fetch_ctx = imap_fetch_alloc(client, pool, "unhibernate");
+ pool_unref(&pool);
+
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init);
+ if (client_has_enabled(client, imap_feature_qresync)) {
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init);
+ imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init);
+ }
+
+ imap_fetch_begin(fetch_ctx, client->mailbox, search_args);
+ mail_search_args_unref(&search_args);
+
+ /* FIXME: ideally do this asynchronously.. */
+ while (imap_fetch_more_no_lock_update(fetch_ctx) == 0) ;
+
+ ret = imap_fetch_end(fetch_ctx);
+ *flag_change_count_r = fetch_ctx->fetched_mails_count;
+ imap_fetch_free(&fetch_ctx);
+ return ret;
+}
+
+static ssize_t
+import_state_mailbox_struct(const unsigned char *data, size_t size,
+ struct mailbox_import_state *state_r,
+ const char **error_r)
+{
+ const unsigned char *p = data, *end = data + size;
+
+ i_zero(state_r);
+ t_array_init(&state_r->recent_uids, 8);
+
+ /* vname */
+ if (import_string(&p, end, &state_r->vname) < 0) {
+ *error_r = "Mailbox state truncated at name";
+ return 0;
+ }
+
+ /* GUID */
+ if (end-p < (int)sizeof(state_r->mailbox_guid)) {
+ *error_r = "Mailbox state truncated at GUID";
+ return 0;
+ }
+ memcpy(state_r->mailbox_guid, p, sizeof(state_r->mailbox_guid));
+ p += sizeof(state_r->mailbox_guid);
+
+ if (guid_128_is_empty(state_r->mailbox_guid)) {
+ *error_r = "Empty GUID";
+ return 0;
+ }
+
+ /* EXAMINEd vs SELECTed */
+ if (p == end) {
+ *error_r = "Mailbox state truncated at examined-flag";
+ return 0;
+ }
+ state_r->examined = p[0] != 0;
+ p++;
+
+ /* mailbox state */
+ if (numpack_decode32(&p, end, &state_r->uidvalidity) < 0 ||
+ numpack_decode32(&p, end, &state_r->uidnext) < 0 ||
+ numpack_decode32(&p, end, &state_r->messages) < 0 ||
+ numpack_decode(&p, end, &state_r->highest_modseq) < 0 ||
+ numpack_decode32(&p, end, &state_r->keywords_count) < 0 ||
+ numpack_decode32(&p, end, &state_r->keywords_crc32) < 0 ||
+ numpack_decode32(&p, end, &state_r->uids_crc32) < 0 ||
+ import_seq_range(&p, end, &state_r->recent_uids) < 0) {
+ *error_r = "Mailbox state truncated";
+ return 0;
+ }
+ if (state_r->uidvalidity == 0) {
+ *error_r = "Empty UIDVALIDITY";
+ return 0;
+ }
+ if (state_r->uidnext == 0) {
+ *error_r = "Empty UIDNEXT";
+ return 0;
+ }
+ return p - data;
+}
+
+static int
+import_state_mailbox_open(struct client *client,
+ const struct mailbox_import_state *state,
+ const char **error_r)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+ struct mailbox_status status;
+ const struct seq_range *range;
+ enum mailbox_flags flags = 0;
+ unsigned int expunge_count, new_mails_count = 0, flag_change_count = 0;
+ uint32_t uid;
+ int ret = 0;
+
+ ns = mail_namespace_find(client->user->namespaces, state->vname);
+ if (ns == NULL) {
+ *error_r = "Namespace not found for mailbox";
+ return -1;
+ }
+
+ if (state->examined)
+ flags |= MAILBOX_FLAG_READONLY;
+ else
+ flags |= MAILBOX_FLAG_DROP_RECENT;
+ box = mailbox_alloc(ns->list, state->vname, flags);
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("Couldn't open mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ return -1;
+ }
+
+ ret = mailbox_enable(box, client_enabled_mailbox_features(client));
+ if (ret < 0 || mailbox_sync(box, 0) < 0) {
+ *error_r = t_strdup_printf("Couldn't sync mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ return -1;
+ }
+ /* verify that this still looks like the same mailbox */
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ *error_r = mailbox_get_last_internal_error(box, NULL);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (!guid_128_equals(metadata.guid, state->mailbox_guid)) {
+ *error_r = t_strdup_printf("Mailbox GUID has changed %s->%s",
+ guid_128_to_string(state->mailbox_guid),
+ guid_128_to_string(metadata.guid));
+ mailbox_free(&box);
+ return -1;
+ }
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT |
+ STATUS_HIGHESTMODSEQ | STATUS_RECENT |
+ STATUS_KEYWORDS, &status);
+ if (status.uidvalidity != state->uidvalidity) {
+ *error_r = t_strdup_printf("Mailbox UIDVALIDITY has changed %u->%u",
+ state->uidvalidity, status.uidvalidity);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (status.uidnext < state->uidnext) {
+ *error_r = t_strdup_printf("Mailbox UIDNEXT shrank %u -> %u",
+ state->uidnext, status.uidnext);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (status.highest_modseq < state->highest_modseq) {
+ *error_r = t_strdup_printf("Mailbox HIGHESTMODSEQ shrank %"PRIu64" -> %"PRIu64,
+ state->highest_modseq,
+ status.highest_modseq);
+ mailbox_free(&box);
+ return -1;
+ }
+
+ client->mailbox = box;
+ client->mailbox_examined = state->examined;
+ client->messages_count = status.messages;
+ client->uidvalidity = status.uidvalidity;
+ client->notify_uidnext = status.uidnext;
+
+ if (import_send_expunges(client, state, &expunge_count, error_r) < 0)
+ return -1;
+ i_assert(expunge_count <= state->messages);
+ if (state->messages - expunge_count > client->messages_count) {
+ *error_r = t_strdup_printf("Mailbox message count shrank %u -> %u",
+ client->messages_count,
+ state->messages - expunge_count);
+ return -1;
+ }
+
+ client_update_mailbox_flags(client, status.keywords);
+ array_foreach(&state->recent_uids, range) {
+ for (uid = range->seq1; uid <= range->seq2; uid++) {
+ uint32_t seq;
+
+ if (mail_index_lookup_seq(box->view, uid, &seq))
+ mailbox_recent_flags_set_uid_forced(box, uid);
+ }
+ }
+ client->recent_count = mailbox_recent_flags_count(box);
+
+ if (state->messages - expunge_count < client->messages_count) {
+ /* new messages arrived */
+ new_mails_count = client->messages_count -
+ (state->messages - expunge_count);
+ client_send_line(client,
+ t_strdup_printf("* %u EXISTS", client->messages_count));
+ client_send_line(client,
+ t_strdup_printf("* %u RECENT", client->recent_count));
+ }
+
+ if (array_count(status.keywords) == state->keywords_count &&
+ mailbox_status_keywords_crc32(&status) == state->keywords_crc32) {
+ /* no changes to keywords */
+ client->keywords.announce_count = state->keywords_count;
+ } else {
+ client_send_mailbox_flags(client, TRUE);
+ }
+ if (import_send_flag_changes(client, state, &flag_change_count) < 0) {
+ *error_r = "Couldn't send flag changes";
+ return -1;
+ }
+ if (client_has_enabled(client, imap_feature_qresync) &&
+ !client->nonpermanent_modseqs &&
+ status.highest_modseq != state->highest_modseq) {
+ client_send_line(client, t_strdup_printf(
+ "* OK [HIGHESTMODSEQ %"PRIu64"] Highest",
+ status.highest_modseq));
+ client->sync_last_full_modseq = status.highest_modseq;
+ }
+ e_debug(client->event,
+ "Unhibernation sync: %u expunges, %u new messages, %u flag changes, %"PRIu64" modseq changes",
+ expunge_count, new_mails_count, flag_change_count,
+ status.highest_modseq - state->highest_modseq);
+ return 0;
+}
+
+static ssize_t
+import_state_mailbox(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r)
+{
+ struct mailbox_import_state state;
+ ssize_t ret;
+
+ if (client->mailbox != NULL) {
+ *error_r = "Duplicate mailbox state";
+ return 0;
+ }
+
+ ret = import_state_mailbox_struct(data, size, &state, error_r);
+ if (ret <= 0) {
+ i_assert(*error_r != NULL);
+ return ret;
+ }
+ if (import_state_mailbox_open(client, &state, error_r) < 0) {
+ *error_r = t_strdup_printf("Mailbox %s: %s", state.vname, *error_r);
+ return -1;
+ }
+ return ret;
+}
+
+static ssize_t
+import_state_enabled_feature(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r)
+{
+ const unsigned char *p = data, *end = data + size;
+ const char *name;
+ unsigned int feature_idx;
+
+ if (import_string(&p, end, &name) < 0) {
+ *error_r = "Mailbox state truncated at name";
+ return 0;
+ }
+ if (!imap_feature_lookup(name, &feature_idx)) {
+ *error_r = t_strdup_printf("Unknown feature '%s'", name);
+ return 0;
+ }
+ client_enable(client, feature_idx);
+ return p - data;
+}
+
+static ssize_t
+import_state_searchres(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r)
+{
+ const unsigned char *p = data;
+
+ i_array_init(&client->search_saved_uidset, 128);
+ if (import_seq_range(&p, data+size, &client->search_saved_uidset) < 0) {
+ *error_r = "Invalid SEARCHRES seq-range";
+ return 0;
+ }
+ return p - data;
+}
+
+static ssize_t
+import_state_id_logged(struct client *client,
+ const unsigned char *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ client->id_logged = TRUE;
+ return 0;
+}
+
+static ssize_t
+import_state_tls_compression(struct client *client,
+ const unsigned char *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ client->tls_compression = TRUE;
+ return 0;
+}
+
+void imap_state_import_idle_cmd_tag(struct client *client, const char *tag)
+{
+ if (client->state_import_idle_continue) {
+ /* IDLE command continues */
+ struct client_command_context *cmd;
+ struct command *command;
+
+ cmd = client_command_alloc(client);
+ cmd->tag = p_strdup(cmd->pool, tag);
+ cmd->name = "IDLE";
+
+ command = command_find("IDLE");
+ i_assert(command != NULL);
+ cmd->func = command->func;
+ cmd->cmd_flags = command->flags;
+ client_command_init_finished(cmd);
+
+ if (command_exec(cmd)) {
+ /* IDLE terminated because of an external change, but
+ DONE was already buffered */
+ client_command_free(&cmd);
+ client_add_missing_io(client);
+ } else {
+ i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT ||
+ cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT);
+ }
+ } else {
+ /* we're finishing IDLE command */
+ client_send_line(client, t_strdup_printf(
+ "%s %s Idle completed.", tag,
+ client->state_import_bad_idle_done ? "BAD" : "OK"));
+ }
+}
+
+static struct {
+ enum imap_state_type_public type;
+ ssize_t (*import)(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r);
+} imap_states_public[] = {
+ { IMAP_STATE_TYPE_MAILBOX, import_state_mailbox },
+ { IMAP_STATE_TYPE_ENABLED_FEATURE, import_state_enabled_feature },
+ { IMAP_STATE_TYPE_SEARCHRES, import_state_searchres }
+};
+
+static struct {
+ enum imap_state_type_internal type;
+ ssize_t (*import)(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r);
+} imap_states_internal[] = {
+ { IMAP_STATE_TYPE_ID_LOGGED, import_state_id_logged },
+ { IMAP_STATE_TYPE_TLS_COMPRESSION, import_state_tls_compression }
+};
+
+static ssize_t
+imap_state_try_import_public(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r)
+{
+ unsigned int i;
+ ssize_t ret;
+
+ i_assert(size > 0);
+
+ for (i = 0; i < N_ELEMENTS(imap_states_public); i++) {
+ if (imap_states_public[i].type == data[0]) {
+ ret = imap_states_public[i].
+ import(client, data+1, size-1, error_r);
+ return ret < 0 ? -1 : ret+1;
+ }
+ }
+ return -2;
+}
+
+static ssize_t
+imap_state_try_import_internal(struct client *client, const unsigned char *data,
+ size_t size, const char **error_r)
+{
+ unsigned int i;
+ ssize_t ret;
+
+ i_assert(size > 0);
+
+ for (i = 0; i < N_ELEMENTS(imap_states_internal); i++) {
+ if (imap_states_internal[i].type == data[0]) {
+ ret = imap_states_internal[i].
+ import(client, data+1, size-1, error_r);
+ return ret < 0 ? -1 : ret+1;
+ }
+ }
+ return -2;
+}
+
+ssize_t imap_state_import_base(struct client *client, bool internal,
+ const unsigned char *data, size_t size,
+ const char **error_r)
+{
+ const unsigned char *p;
+ ssize_t ret;
+ size_t pos;
+
+ i_assert(client->mailbox == NULL);
+
+ *error_r = NULL;
+
+ if (size < 5 || memcmp(data, "base\n", 5) != 0) {
+ p = memchr(data, '\n', size);
+ if (p == NULL)
+ p = data + I_MIN(size, 20);
+ *error_r = t_strdup_printf("Unknown state block '%s'",
+ str_sanitize(t_strdup_until(data, p), 20));
+ return 0;
+ }
+
+ pos = 5;
+ while (pos < size) {
+ ret = imap_state_try_import_public(client, data+pos,
+ size-pos, error_r);
+ if (ret == -2 && internal) {
+ ret = imap_state_try_import_internal(client, data+pos,
+ size-pos, error_r);
+ }
+ if (ret < 0 || *error_r != NULL) {
+ if (ret == -2) {
+ *error_r = t_strdup_printf("Unknown type '%c'",
+ data[pos]);
+ }
+ i_assert(*error_r != NULL);
+ return ret < 0 ? -1 : 0;
+ }
+ i_assert(size - pos >= (size_t)ret);
+ pos += ret;
+ }
+ return pos;
+}
diff --git a/src/imap/imap-state.h b/src/imap/imap-state.h
new file mode 100644
index 0000000..ee00376
--- /dev/null
+++ b/src/imap/imap-state.h
@@ -0,0 +1,30 @@
+#ifndef IMAP_STATE_H
+#define IMAP_STATE_H
+
+/* Export the IMAP client state to the given buffer. Returns 1 if ok,
+ 0 if state couldn't be exported, -1 if temporary internal error error. */
+int imap_state_export_internal(struct client *client, buffer_t *dest,
+ const char **error_r);
+int imap_state_export_external(struct client *client, buffer_t *dest,
+ const char **error_r);
+
+/* Returns 1 if ok, 0 if state was corrupted, -1 if other error. Internal state
+ comes from another Dovecot component, which can override IP addresses,
+ session IDs, etc. */
+int imap_state_import_internal(struct client *client,
+ const unsigned char *data, size_t size,
+ const char **error_r);
+int imap_state_import_external(struct client *client,
+ const unsigned char *data, size_t size,
+ const char **error_r);
+
+/* INTERNAL API: Note that the "internal" flag specifies whether we're doing
+ the import/export from/to another Dovecot component or an untrusted
+ IMAP client. */
+int imap_state_export_base(struct client *client, bool internal,
+ buffer_t *dest, const char **error_r);
+ssize_t imap_state_import_base(struct client *client, bool internal,
+ const unsigned char *data, size_t size,
+ const char **error_r);
+void imap_state_import_idle_cmd_tag(struct client *client, const char *tag);
+#endif
diff --git a/src/imap/imap-status.c b/src/imap/imap-status.c
new file mode 100644
index 0000000..341e537
--- /dev/null
+++ b/src/imap/imap-status.c
@@ -0,0 +1,172 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "imap-quote.h"
+#include "imap-status.h"
+
+int imap_status_parse_items(struct client_command_context *cmd,
+ const struct imap_arg *args,
+ struct imap_status_items *items_r)
+{
+ enum imap_status_item_flags flags = 0;
+ const char *item;
+
+ if (IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd, "Empty status list.");
+ return -1;
+ }
+
+ i_zero(items_r);
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ if (!imap_arg_get_atom(args, &item)) {
+ /* list may contain only atoms */
+ client_send_command_error(cmd,
+ "Status list contains non-atoms.");
+ return -1;
+ }
+
+ item = t_str_ucase(item);
+ if (strcmp(item, "MESSAGES") == 0)
+ flags |= IMAP_STATUS_ITEM_MESSAGES;
+ else if (strcmp(item, "RECENT") == 0)
+ flags |= IMAP_STATUS_ITEM_RECENT;
+ else if (strcmp(item, "UIDNEXT") == 0)
+ flags |= IMAP_STATUS_ITEM_UIDNEXT;
+ else if (strcmp(item, "UIDVALIDITY") == 0)
+ flags |= IMAP_STATUS_ITEM_UIDVALIDITY;
+ else if (strcmp(item, "UNSEEN") == 0)
+ flags |= IMAP_STATUS_ITEM_UNSEEN;
+ else if (strcmp(item, "HIGHESTMODSEQ") == 0)
+ flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ;
+ else if (strcmp(item, "SIZE") == 0)
+ flags |= IMAP_STATUS_ITEM_SIZE;
+ else if (strcmp(item, "X-SIZE") == 0)
+ flags |= IMAP_STATUS_ITEM_X_SIZE;
+ else if (strcmp(item, "X-GUID") == 0)
+ flags |= IMAP_STATUS_ITEM_X_GUID;
+ else {
+ client_send_command_error(cmd, t_strconcat(
+ "Invalid status item ", item, NULL));
+ return -1;
+ }
+ }
+
+ items_r->flags = flags;
+ return 0;
+}
+
+int imap_status_get_result(struct client *client, struct mailbox *box,
+ const struct imap_status_items *items,
+ struct imap_status_result *result_r)
+{
+ enum mailbox_status_items status = 0;
+ enum mailbox_metadata_items metadata = 0;
+ int ret;
+
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_MESSAGES))
+ status |= STATUS_MESSAGES;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_RECENT))
+ status |= STATUS_RECENT;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDNEXT))
+ status |= STATUS_UIDNEXT;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDVALIDITY))
+ status |= STATUS_UIDVALIDITY;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UNSEEN))
+ status |= STATUS_UNSEEN;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_HIGHESTMODSEQ)) {
+ client_enable(client, imap_feature_condstore);
+ status |= STATUS_HIGHESTMODSEQ;
+ }
+ if (HAS_ANY_BITS(items->flags, IMAP_STATUS_ITEM_SIZE |
+ IMAP_STATUS_ITEM_X_SIZE))
+ metadata |= MAILBOX_METADATA_VIRTUAL_SIZE;
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_GUID))
+ metadata |= MAILBOX_METADATA_GUID;
+
+ ret = mailbox_get_status(box, status, &result_r->status);
+ if (metadata != 0 && ret == 0)
+ ret = mailbox_get_metadata(box, metadata, &result_r->metadata);
+
+ return ret;
+}
+
+int imap_status_get(struct client_command_context *cmd,
+ struct mail_namespace *ns, const char *mailbox,
+ const struct imap_status_items *items,
+ struct imap_status_result *result_r)
+{
+ struct client *client = cmd->client;
+ struct mailbox *box;
+ const char *errstr;
+ int ret = 0;
+
+ if (client->mailbox != NULL &&
+ mailbox_equals(client->mailbox, ns, mailbox)) {
+ /* this mailbox is selected */
+ box = client->mailbox;
+ } else {
+ /* open the mailbox */
+ box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY);
+ (void)mailbox_enable(box, client_enabled_mailbox_features(client));
+ }
+
+ ret = imap_status_get_result(client, box, items, result_r);
+ if (ret < 0) {
+ errstr = mailbox_get_last_error(box, &result_r->error);
+ result_r->errstr = imap_get_error_string(cmd, errstr,
+ result_r->error);
+ }
+ if (box != client->mailbox)
+ mailbox_free(&box);
+ return ret;
+}
+
+int imap_status_send(struct client *client, const char *mailbox_mutf7,
+ const struct imap_status_items *items,
+ const struct imap_status_result *result)
+{
+ const struct mailbox_status *status = &result->status;
+ string_t *str;
+ size_t prefix_len;
+
+ str = t_str_new(128);
+ str_append(str, "* STATUS ");
+ imap_append_astring(str, mailbox_mutf7);
+ str_append(str, " (");
+
+ prefix_len = str_len(str);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_MESSAGES))
+ str_printfa(str, "MESSAGES %u ", status->messages);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_RECENT))
+ str_printfa(str, "RECENT %u ", status->recent);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDNEXT))
+ str_printfa(str, "UIDNEXT %u ", status->uidnext);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDVALIDITY))
+ str_printfa(str, "UIDVALIDITY %u ", status->uidvalidity);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UNSEEN))
+ str_printfa(str, "UNSEEN %u ", status->unseen);
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_HIGHESTMODSEQ)) {
+ str_printfa(str, "HIGHESTMODSEQ %"PRIu64" ",
+ status->highest_modseq);
+ }
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_SIZE)) {
+ str_printfa(str, "SIZE %"PRIu64" ",
+ result->metadata.virtual_size);
+ }
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_SIZE)) {
+ str_printfa(str, "X-SIZE %"PRIu64" ",
+ result->metadata.virtual_size);
+ }
+ if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_GUID)) {
+ str_printfa(str, "X-GUID %s ",
+ guid_128_to_string(result->metadata.guid));
+ }
+
+ if (str_len(str) != prefix_len)
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ')');
+
+ return client_send_line_next(client, str_c(str));
+}
diff --git a/src/imap/imap-status.h b/src/imap/imap-status.h
new file mode 100644
index 0000000..58bd581
--- /dev/null
+++ b/src/imap/imap-status.h
@@ -0,0 +1,51 @@
+#ifndef IMAP_STATUS_H
+#define IMAP_STATUS_H
+
+enum imap_status_item_flags {
+ IMAP_STATUS_ITEM_MESSAGES = BIT(0),
+ IMAP_STATUS_ITEM_RECENT = BIT(1),
+ IMAP_STATUS_ITEM_UIDNEXT = BIT(2),
+ IMAP_STATUS_ITEM_UIDVALIDITY = BIT(3),
+ IMAP_STATUS_ITEM_UNSEEN = BIT(4),
+ IMAP_STATUS_ITEM_HIGHESTMODSEQ = BIT(5),
+ IMAP_STATUS_ITEM_SIZE = BIT(6),
+
+ IMAP_STATUS_ITEM_X_SIZE = BIT(16), /* to be deprecated */
+ IMAP_STATUS_ITEM_X_GUID = BIT(17),
+};
+
+struct imap_status_items {
+ enum imap_status_item_flags flags;
+};
+
+struct imap_status_result {
+ struct mailbox_status status;
+ struct mailbox_metadata metadata;
+ enum mail_error error;
+ const char *errstr;
+};
+
+static inline bool
+imap_status_items_is_empty(const struct imap_status_items *items)
+{
+ return (items->flags == 0);
+}
+
+int imap_status_parse_items(struct client_command_context *cmd,
+ const struct imap_arg *args,
+ struct imap_status_items *items_r);
+
+int imap_status_get_result(struct client *client, struct mailbox *box,
+ const struct imap_status_items *items,
+ struct imap_status_result *result_r);
+int imap_status_get(struct client_command_context *cmd,
+ struct mail_namespace *ns, const char *mailbox,
+ const struct imap_status_items *items,
+ struct imap_status_result *result_r);
+
+int imap_status_send(struct client *client, const char *mailbox_mutf7,
+ const struct imap_status_items *items,
+ const struct imap_status_result *result)
+ ATTR_NOWARN_UNUSED_RESULT;
+
+#endif
diff --git a/src/imap/imap-sync-private.h b/src/imap/imap-sync-private.h
new file mode 100644
index 0000000..b0b1715
--- /dev/null
+++ b/src/imap/imap-sync-private.h
@@ -0,0 +1,47 @@
+#ifndef IMAP_SYNC_PRIVATE_H
+#define IMAP_SYNC_PRIVATE_H
+
+#include "imap-sync.h"
+
+struct imap_client_sync_context {
+ /* if multiple commands are in progress, we may need to wait for them
+ to finish before syncing mailbox. */
+ unsigned int counter;
+ enum mailbox_sync_flags flags;
+ enum imap_sync_flags imap_flags;
+ const char *tagline;
+};
+
+struct imap_sync_context {
+ struct client *client;
+ struct mailbox *box;
+ enum imap_sync_flags imap_flags;
+
+ struct mailbox_transaction_context *t;
+ struct mailbox_sync_context *sync_ctx;
+ struct mail *mail;
+
+ struct mailbox_status status;
+ struct mailbox_sync_status sync_status;
+
+ struct mailbox_sync_rec sync_rec;
+ ARRAY_TYPE(keywords) tmp_keywords;
+ ARRAY_TYPE(seq_range) expunges;
+ uint32_t seq;
+
+ ARRAY_TYPE(seq_range) search_adds, search_removes;
+ unsigned int search_update_idx;
+
+ unsigned int messages_count;
+
+ /* Module-specific contexts. */
+ ARRAY(union imap_module_context *) module_contexts;
+
+ bool failed:1;
+ bool finished:1;
+ bool no_newmail:1;
+ bool have_new_mails:1;
+ bool search_update_notifying:1;
+};
+
+#endif
diff --git a/src/imap/imap-sync.c b/src/imap/imap-sync.c
new file mode 100644
index 0000000..fc49ae7
--- /dev/null
+++ b/src/imap/imap-sync.c
@@ -0,0 +1,841 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "ostream.h"
+#include "mail-user.h"
+#include "mail-storage.h"
+#include "mail-search-build.h"
+#include "imap-quote.h"
+#include "imap-util.h"
+#include "imap-fetch.h"
+#include "imap-notify.h"
+#include "imap-commands.h"
+#include "imap-sync-private.h"
+
+static void uids_to_seqs(struct mailbox *box, ARRAY_TYPE(seq_range) *uids)
+{
+ T_BEGIN {
+ ARRAY_TYPE(seq_range) seqs;
+ const struct seq_range *range;
+ uint32_t seq1, seq2;
+
+ t_array_init(&seqs, array_count(uids));
+ array_foreach(uids, range) {
+ mailbox_get_seq_range(box, range->seq1, range->seq2,
+ &seq1, &seq2);
+ /* since we have to notify about expunged messages,
+ we expect that all the referenced UIDs exist */
+ i_assert(seq1 != 0);
+ i_assert(seq2 - seq1 == range->seq2 - range->seq1);
+
+ seq_range_array_add_range(&seqs, seq1, seq2);
+ }
+ /* replace uids with seqs */
+ array_clear(uids);
+ array_append_array(uids, &seqs);
+
+ } T_END;
+}
+
+static int search_update_fetch_more(const struct imap_search_update *update)
+{
+ int ret;
+
+ if ((ret = imap_fetch_more_no_lock_update(update->fetch_ctx)) == 0)
+ return 0;
+ /* finished the FETCH */
+ if (imap_fetch_end(update->fetch_ctx) < 0)
+ return -1;
+ return ret;
+}
+
+static int
+imap_sync_send_fetch_to_search_update(struct imap_sync_context *ctx,
+ const struct imap_search_update *update)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *arg;
+ ARRAY_TYPE(seq_range) seqs;
+
+ if (ctx->search_update_notifying)
+ return search_update_fetch_more(update);
+
+ i_assert(!update->fetch_ctx->state.fetching);
+
+ if (array_count(&ctx->search_adds) == 0 || !ctx->have_new_mails)
+ return 1;
+
+ search_args = mail_search_build_init();
+ arg = mail_search_build_add(search_args, SEARCH_UIDSET);
+ p_array_init(&arg->value.seqset, search_args->pool, 1);
+
+ /* find the newly appended messages: ctx->messages_count is the message
+ count before new messages found by sync, client->messages_count is
+ the number of messages after. */
+ t_array_init(&seqs, 1);
+ seq_range_array_add_range(&seqs, ctx->messages_count+1,
+ ctx->client->messages_count);
+ mailbox_get_uid_range(ctx->client->mailbox, &seqs, &arg->value.seqset);
+ /* remove messages not in the search_adds list */
+ seq_range_array_intersect(&arg->value.seqset, &ctx->search_adds);
+
+ imap_fetch_begin(update->fetch_ctx, ctx->client->mailbox, search_args);
+ mail_search_args_unref(&search_args);
+ return search_update_fetch_more(update);
+}
+
+static int
+imap_sync_send_search_update(struct imap_sync_context *ctx,
+ const struct imap_search_update *update,
+ bool removes_only)
+{
+ string_t *cmd;
+ int ret = 1;
+
+ if (!ctx->search_update_notifying) {
+ mailbox_search_result_sync(update->result, &ctx->search_removes,
+ &ctx->search_adds);
+ }
+ if (array_count(&ctx->search_adds) == 0 &&
+ array_count(&ctx->search_removes) == 0)
+ return 1;
+
+ i_assert(array_count(&ctx->search_adds) == 0 || !removes_only);
+ if (update->fetch_ctx != NULL) {
+ ret = imap_sync_send_fetch_to_search_update(ctx, update);
+ if (ret == 0) {
+ ctx->search_update_notifying = TRUE;
+ return 0;
+ }
+ }
+ ctx->search_update_notifying = FALSE;
+
+ cmd = t_str_new(256);
+ str_append(cmd, "* ESEARCH (TAG ");
+ imap_append_string(cmd, update->tag);
+ str_append_c(cmd, ')');
+ if (update->return_uids)
+ str_append(cmd, " UID");
+ else {
+ /* convert to sequences */
+ uids_to_seqs(ctx->client->mailbox, &ctx->search_removes);
+ uids_to_seqs(ctx->client->mailbox, &ctx->search_adds);
+ }
+
+ if (array_count(&ctx->search_removes) != 0) {
+ str_printfa(cmd, " REMOVEFROM (0 ");
+ imap_write_seq_range(cmd, &ctx->search_removes);
+ str_append_c(cmd, ')');
+ }
+ if (array_count(&ctx->search_adds) != 0) {
+ str_printfa(cmd, " ADDTO (0 ");
+ imap_write_seq_range(cmd, &ctx->search_adds);
+ str_append_c(cmd, ')');
+ }
+ str_append(cmd, "\r\n");
+ o_stream_nsend(ctx->client->output, str_data(cmd), str_len(cmd));
+ return ret;
+}
+
+static int
+imap_sync_send_search_updates(struct imap_sync_context *ctx, bool removes_only)
+{
+ const struct imap_search_update *updates;
+ unsigned int i, count;
+ int ret = 1;
+
+ if (!array_is_created(&ctx->client->search_updates))
+ return 1;
+
+ if (!array_is_created(&ctx->search_removes)) {
+ i_array_init(&ctx->search_removes, 64);
+ i_array_init(&ctx->search_adds, 128);
+ }
+
+ updates = array_get(&ctx->client->search_updates, &count);
+ for (i = ctx->search_update_idx; i < count; i++) {
+ T_BEGIN {
+ ret = imap_sync_send_search_update(ctx, &updates[i],
+ removes_only);
+ } T_END;
+ if (ret <= 0)
+ break;
+ }
+ ctx->search_update_idx = i;
+ return ret;
+}
+
+struct imap_sync_context *
+imap_sync_init(struct client *client, struct mailbox *box,
+ enum imap_sync_flags imap_flags, enum mailbox_sync_flags flags)
+{
+ struct imap_sync_context *ctx;
+
+ i_assert(client->mailbox == box);
+
+ if (client->notify_immediate_expunges) {
+ /* NOTIFY enabled without SELECTED-DELAYED */
+ flags &= ENUM_NEGATE(MAILBOX_SYNC_FLAG_NO_EXPUNGES);
+ }
+
+ ctx = i_new(struct imap_sync_context, 1);
+ ctx->client = client;
+ ctx->box = box;
+ ctx->imap_flags = imap_flags;
+ i_array_init(&ctx->module_contexts, 5);
+
+ /* make sure user can't DoS the system by causing Dovecot to create
+ tons of useless namespaces. */
+ mail_user_drop_useless_namespaces(client->user);
+
+ ctx->sync_ctx = mailbox_sync_init(box, flags);
+ ctx->t = mailbox_transaction_begin(box, 0, "Mailbox sync");
+ ctx->mail = mail_alloc(ctx->t, MAIL_FETCH_FLAGS, NULL);
+ ctx->messages_count = client->messages_count;
+ i_array_init(&ctx->tmp_keywords, client->keywords.announce_count + 8);
+
+ if (client_has_enabled(client, imap_feature_qresync)) {
+ i_array_init(&ctx->expunges, 128);
+ /* always send UIDs in FETCH replies */
+ ctx->imap_flags |= IMAP_SYNC_FLAG_SEND_UID;
+ }
+
+ client_send_mailbox_flags(client, FALSE);
+ /* send search updates the first time after sync is initialized.
+ it now contains expunged messages that must be sent before
+ EXPUNGE replies. */
+ if (imap_sync_send_search_updates(ctx, TRUE) == 0)
+ i_unreached();
+ ctx->search_update_idx = 0;
+ return ctx;
+}
+
+static void
+imap_sync_send_highestmodseq(struct imap_sync_context *ctx,
+ struct client_command_context *sync_cmd)
+{
+ struct client *client = ctx->client;
+ uint64_t send_modseq = 0;
+
+ if (ctx->sync_status.sync_delayed_expunges &&
+ client->highest_fetch_modseq > client->sync_last_full_modseq) {
+ /* if client updates highest-modseq using returned MODSEQs
+ it loses expunges. try to avoid this by sending it a lower
+ pre-expunge HIGHESTMODSEQ reply. */
+ send_modseq = client->sync_last_full_modseq;
+ } else if (!ctx->sync_status.sync_delayed_expunges &&
+ ctx->status.highest_modseq > client->sync_last_full_modseq &&
+ ctx->status.highest_modseq > client->highest_fetch_modseq) {
+ /* we've probably sent some VANISHED or EXISTS replies which
+ increased the highest-modseq. notify the client about
+ this. */
+ send_modseq = ctx->status.highest_modseq;
+ }
+
+ if (send_modseq == 0) {
+ /* no sending */
+ } else if (sync_cmd->sync != NULL && /* IDLE doesn't have ->sync */
+ sync_cmd->sync->tagline != NULL && /* NOTIFY doesn't have tagline */
+ str_begins(sync_cmd->sync->tagline, "OK ") &&
+ sync_cmd->sync->tagline[3] != '[') {
+ /* modify the tagged reply directly */
+ sync_cmd->sync->tagline = p_strdup_printf(sync_cmd->pool,
+ "OK [HIGHESTMODSEQ %"PRIu64"] %s",
+ send_modseq, sync_cmd->sync->tagline + 3);
+ } else {
+ /* send an untagged OK reply */
+ client_send_line(client, t_strdup_printf(
+ "* OK [HIGHESTMODSEQ %"PRIu64"] Highest",
+ send_modseq));
+ }
+
+ if (!ctx->sync_status.sync_delayed_expunges) {
+ /* no delayed expunges, remember this for future */
+ client->sync_last_full_modseq = ctx->status.highest_modseq;
+ }
+ client->highest_fetch_modseq = 0;
+}
+
+static int imap_sync_finish(struct imap_sync_context *ctx, bool aborting)
+{
+ struct client *client = ctx->client;
+ int ret = ctx->failed ? -1 : 0;
+
+ if (ctx->finished)
+ return ret;
+ ctx->finished = TRUE;
+
+ mail_free(&ctx->mail);
+ /* the transaction is used only for fetching modseqs/flags.
+ it can't really fail.. */
+ (void)mailbox_transaction_commit(&ctx->t);
+
+ if (array_is_created(&ctx->expunges))
+ array_free(&ctx->expunges);
+
+ if (mailbox_sync_deinit(&ctx->sync_ctx, &ctx->sync_status) < 0 ||
+ ctx->failed) {
+ ctx->failed = TRUE;
+ ret = -1;
+ }
+ mailbox_get_open_status(ctx->box, STATUS_UIDVALIDITY |
+ STATUS_MESSAGES | STATUS_RECENT |
+ STATUS_HIGHESTMODSEQ, &ctx->status);
+
+ if (ctx->status.uidvalidity != client->uidvalidity) {
+ /* most clients would get confused by this. disconnect them. */
+ client_disconnect_with_error(client,
+ "Mailbox UIDVALIDITY changed");
+ }
+ if (mailbox_is_inconsistent(ctx->box)) {
+ client_disconnect_with_error(client,
+ "IMAP session state is inconsistent, please relogin.");
+ /* we can't trust status information anymore, so don't try to
+ sync message counts. */
+ return -1;
+ }
+ if (!ctx->no_newmail && !aborting) {
+ if (ctx->status.messages < ctx->messages_count)
+ i_panic("Message count decreased");
+ if (ctx->status.messages != ctx->messages_count &&
+ client->notify_count_changes) {
+ client_send_line(client,
+ t_strdup_printf("* %u EXISTS", ctx->status.messages));
+ ctx->have_new_mails = TRUE;
+ }
+ if (ctx->status.recent != client->recent_count &&
+ client->notify_count_changes) {
+ client_send_line(client,
+ t_strdup_printf("* %u RECENT", ctx->status.recent));
+ }
+ client->messages_count = ctx->status.messages;
+ client->recent_count = ctx->status.recent;
+ }
+ return ret;
+}
+
+static int imap_sync_notify_more(struct imap_sync_context *ctx)
+{
+ int ret = 1;
+
+ if (ctx->have_new_mails && ctx->client->notify_ctx != NULL) {
+ /* send FETCH replies for the new mails */
+ if ((ret = imap_client_notify_newmails(ctx->client)) == 0)
+ return 0;
+ if (ret < 0)
+ ctx->failed = TRUE;
+ }
+
+ /* send search updates the second time after syncing in done.
+ now it contains added/removed messages. */
+ if ((ret = imap_sync_send_search_updates(ctx, FALSE)) < 0)
+ ctx->failed = TRUE;
+
+ if (ret > 0)
+ ret = ctx->client->v.sync_notify_more(ctx);
+ return ret;
+}
+
+int imap_sync_deinit(struct imap_sync_context *ctx,
+ struct client_command_context *sync_cmd)
+{
+ int ret;
+
+ ret = imap_sync_finish(ctx, TRUE);
+ imap_client_notify_finished(ctx->client);
+
+ if (client_has_enabled(ctx->client, imap_feature_qresync) &&
+ !ctx->client->nonpermanent_modseqs)
+ imap_sync_send_highestmodseq(ctx, sync_cmd);
+
+ if (array_is_created(&ctx->search_removes)) {
+ array_free(&ctx->search_removes);
+ array_free(&ctx->search_adds);
+ }
+
+ array_free(&ctx->tmp_keywords);
+ array_free(&ctx->module_contexts);
+ i_free(ctx);
+ return ret;
+}
+
+static void imap_sync_add_modseq(struct imap_sync_context *ctx, string_t *str)
+{
+ uint64_t modseq;
+
+ modseq = mail_get_modseq(ctx->mail);
+ if (ctx->client->highest_fetch_modseq < modseq)
+ ctx->client->highest_fetch_modseq = modseq;
+ str_printfa(str, "MODSEQ (%"PRIu64")", modseq);
+}
+
+static int imap_sync_send_flags(struct imap_sync_context *ctx, string_t *str)
+{
+ enum mail_flags flags;
+ const char *const *keywords;
+
+ mail_set_seq(ctx->mail, ctx->seq);
+ flags = mail_get_flags(ctx->mail);
+ keywords = client_get_keyword_names(ctx->client, &ctx->tmp_keywords,
+ mail_get_keyword_indexes(ctx->mail));
+
+ if ((flags & MAIL_DELETED) != 0)
+ ctx->client->sync_seen_deletes = TRUE;
+
+ str_truncate(str, 0);
+ str_printfa(str, "* %u FETCH (", ctx->seq);
+ if ((ctx->imap_flags & IMAP_SYNC_FLAG_SEND_UID) != 0)
+ str_printfa(str, "UID %u ", ctx->mail->uid);
+ if (client_has_enabled(ctx->client, imap_feature_condstore) &&
+ !ctx->client->nonpermanent_modseqs) {
+ imap_sync_add_modseq(ctx, str);
+ str_append_c(str, ' ');
+ }
+ str_append(str, "FLAGS (");
+ imap_write_flags(str, flags, keywords);
+ str_append(str, "))");
+ return client_send_line_next(ctx->client, str_c(str));
+}
+
+static int imap_sync_send_modseq(struct imap_sync_context *ctx, string_t *str)
+{
+ mail_set_seq(ctx->mail, ctx->seq);
+
+ str_truncate(str, 0);
+ str_printfa(str, "* %u FETCH (", ctx->seq);
+ if ((ctx->imap_flags & IMAP_SYNC_FLAG_SEND_UID) != 0)
+ str_printfa(str, "UID %u ", ctx->mail->uid);
+ imap_sync_add_modseq(ctx, str);
+ str_append_c(str, ')');
+ return client_send_line_next(ctx->client, str_c(str));
+}
+
+static void imap_sync_vanished(struct imap_sync_context *ctx)
+{
+ const struct seq_range *seqs;
+ unsigned int i, count;
+ string_t *line;
+ uint32_t seq, prev_uid, start_uid;
+ bool comma = FALSE;
+
+ /* Convert expunge sequences to UIDs and send them in VANISHED line. */
+ seqs = array_get(&ctx->expunges, &count);
+ if (count == 0)
+ return;
+
+ line = t_str_new(256);
+ str_append(line, "* VANISHED ");
+ for (i = 0; i < count; i++) {
+ start_uid = 0; prev_uid = 0;
+ for (seq = seqs[i].seq1; seq <= seqs[i].seq2; seq++) {
+ mail_set_seq(ctx->mail, seq);
+ if (prev_uid == 0 || prev_uid + 1 != ctx->mail->uid) {
+ if (start_uid != 0) {
+ if (!comma)
+ comma = TRUE;
+ else
+ str_append_c(line, ',');
+ str_printfa(line, "%u", start_uid);
+ if (start_uid != prev_uid) {
+ str_printfa(line, ":%u",
+ prev_uid);
+ }
+ }
+ start_uid = ctx->mail->uid;
+ }
+ prev_uid = ctx->mail->uid;
+ }
+ if (!comma)
+ comma = TRUE;
+ else
+ str_append_c(line, ',');
+ str_printfa(line, "%u", start_uid);
+ if (start_uid != prev_uid)
+ str_printfa(line, ":%u", prev_uid);
+ }
+ str_append(line, "\r\n");
+ o_stream_nsend(ctx->client->output, str_data(line), str_len(line));
+}
+
+static int imap_sync_send_expunges(struct imap_sync_context *ctx, string_t *str)
+{
+ int ret = 1;
+
+ if (!ctx->client->notify_count_changes) {
+ /* NOTIFY: MessageEvent not specified for selected mailbox */
+ return 1;
+ }
+
+ if (array_is_created(&ctx->expunges)) {
+ /* Use a single VANISHED line */
+ seq_range_array_add_range(&ctx->expunges,
+ ctx->sync_rec.seq1,
+ ctx->sync_rec.seq2);
+ return 1;
+ }
+ if (ctx->seq == 0)
+ ctx->seq = ctx->sync_rec.seq2;
+ for (; ctx->seq >= ctx->sync_rec.seq1; ctx->seq--) {
+ if (ret == 0) {
+ /* buffer full, continue later */
+ return 0;
+ }
+
+ str_truncate(str, 0);
+ str_printfa(str, "* %u EXPUNGE", ctx->seq);
+ ret = client_send_line_next(ctx->client, str_c(str));
+ }
+ return 1;
+}
+
+int imap_sync_more(struct imap_sync_context *ctx)
+{
+ string_t *str;
+ int ret = 1;
+
+ if (ctx->finished)
+ return imap_sync_notify_more(ctx);
+
+ /* finish syncing even when client has disconnected. otherwise our
+ internal state (ctx->messages_count) can get messed up and unless
+ we immediately stop handling all commands and syncs we could end up
+ assert-crashing. */
+ str = t_str_new(256);
+ for (;;) {
+ if (ctx->seq == 0) {
+ /* get next one */
+ if (!mailbox_sync_next(ctx->sync_ctx, &ctx->sync_rec)) {
+ /* finished */
+ ret = 1;
+ break;
+ }
+ }
+
+ if (ctx->sync_rec.seq2 > ctx->messages_count) {
+ /* don't send change notifications of messages we
+ haven't even announced to client yet */
+ if (ctx->sync_rec.seq1 > ctx->messages_count) {
+ ctx->seq = 0;
+ continue;
+ }
+ ctx->sync_rec.seq2 = ctx->messages_count;
+ }
+
+ /* EXPUNGEs must come last */
+ i_assert(!array_is_created(&ctx->expunges) ||
+ array_count(&ctx->expunges) == 0 ||
+ ctx->sync_rec.type == MAILBOX_SYNC_TYPE_EXPUNGE);
+ switch (ctx->sync_rec.type) {
+ case MAILBOX_SYNC_TYPE_FLAGS:
+ if (!ctx->client->notify_flag_changes) {
+ /* NOTIFY: FlagChange not specified for
+ selected mailbox */
+ break;
+ }
+ if (ctx->seq == 0)
+ ctx->seq = ctx->sync_rec.seq1;
+
+ ret = 1;
+ for (; ctx->seq <= ctx->sync_rec.seq2; ctx->seq++) {
+ if (ret == 0)
+ break;
+
+ ret = imap_sync_send_flags(ctx, str);
+ }
+ break;
+ case MAILBOX_SYNC_TYPE_EXPUNGE:
+ ret = imap_sync_send_expunges(ctx, str);
+ if (ret > 0) {
+ /* update only after we're finished, so that
+ the seq2 > messages_count check above
+ doesn't break */
+ ctx->messages_count -=
+ ctx->sync_rec.seq2 -
+ ctx->sync_rec.seq1 + 1;
+ }
+ break;
+ case MAILBOX_SYNC_TYPE_MODSEQ:
+ if (!client_has_enabled(ctx->client, imap_feature_condstore))
+ break;
+ if (!ctx->client->notify_flag_changes) {
+ /* NOTIFY: FlagChange not specified for
+ selected mailbox. The RFC doesn't explicitly
+ specify MODSEQ changes, but they're close
+ enough to flag changes. */
+ break;
+ }
+
+ if (ctx->seq == 0)
+ ctx->seq = ctx->sync_rec.seq1;
+
+ ret = 1;
+ for (; ctx->seq <= ctx->sync_rec.seq2; ctx->seq++) {
+ if (ret == 0)
+ break;
+
+ ret = imap_sync_send_modseq(ctx, str);
+ }
+ break;
+ }
+ if (ret == 0) {
+ /* buffer full */
+ break;
+ }
+
+ ctx->seq = 0;
+ }
+ if (ret > 0) {
+ if (array_is_created(&ctx->expunges))
+ imap_sync_vanished(ctx);
+ if (imap_sync_finish(ctx, FALSE) < 0)
+ return -1;
+ return imap_sync_more(ctx);
+ }
+ return ret;
+}
+
+bool imap_sync_is_allowed(struct client *client)
+{
+ if (client->syncing)
+ return FALSE;
+
+ if (client->mailbox != NULL &&
+ mailbox_transaction_get_count(client->mailbox) > 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+static bool cmd_finish_sync(struct client_command_context *cmd)
+{
+ if (cmd->sync->tagline != NULL)
+ client_send_tagline(cmd, cmd->sync->tagline);
+ return TRUE;
+}
+
+static bool cmd_sync_continue(struct client_command_context *sync_cmd)
+{
+ struct client_command_context *cmd, *prev;
+ struct client *client = sync_cmd->client;
+ struct imap_sync_context *ctx = sync_cmd->context;
+ int ret;
+
+ i_assert(ctx->client == client);
+
+ if ((ret = imap_sync_more(ctx)) == 0)
+ return FALSE;
+ if (ret < 0)
+ ctx->failed = TRUE;
+
+ client->syncing = FALSE;
+ if (imap_sync_deinit(ctx, sync_cmd) < 0) {
+ client_send_untagged_storage_error(client,
+ mailbox_get_storage(client->mailbox));
+ }
+ sync_cmd->context = NULL;
+
+ /* Finish all commands that waited for this sync. Go through the queue
+ backwards, so that tagged replies are sent in the same order as
+ they were received. This fixes problems with clients that rely on
+ this (Apple Mail 3.2) */
+ for (cmd = client->command_queue; cmd->next != NULL; cmd = cmd->next) ;
+ for (; cmd != NULL; cmd = prev) {
+ prev = cmd->prev;
+
+ if (cmd->state == CLIENT_COMMAND_STATE_WAIT_SYNC &&
+ cmd != sync_cmd &&
+ cmd->sync->counter+1 == client->sync_counter) {
+ cmd_finish_sync(cmd);
+ client_command_free(&cmd);
+ }
+ }
+ cmd_finish_sync(sync_cmd);
+ return TRUE;
+}
+
+static void get_common_sync_flags(struct client *client,
+ enum mailbox_sync_flags *flags_r,
+ enum imap_sync_flags *imap_flags_r)
+{
+ struct client_command_context *cmd;
+ unsigned int count = 0, fast_count = 0, noexpunges_count = 0;
+
+ *flags_r = 0;
+ *imap_flags_r = 0;
+
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ if (cmd->sync != NULL &&
+ cmd->sync->counter == client->sync_counter) {
+ if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_FAST) != 0)
+ fast_count++;
+ if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) != 0)
+ noexpunges_count++;
+ *flags_r |= cmd->sync->flags;
+ *imap_flags_r |= cmd->sync->imap_flags;
+ count++;
+ }
+ }
+ i_assert(noexpunges_count == 0 || noexpunges_count == count);
+ if (fast_count != count)
+ *flags_r &= ENUM_NEGATE(MAILBOX_SYNC_FLAG_FAST);
+
+ i_assert((*flags_r & MAILBOX_SYNC_FLAG_FIX_INCONSISTENT) == 0);
+}
+
+static bool cmd_sync_client(struct client_command_context *sync_cmd)
+{
+ struct client *client = sync_cmd->client;
+ struct imap_sync_context *ctx;
+ enum mailbox_sync_flags flags;
+ enum imap_sync_flags imap_flags;
+ bool no_newmail;
+
+ /* there may be multiple commands waiting. use their combined flags */
+ get_common_sync_flags(client, &flags, &imap_flags);
+ client->sync_counter++;
+
+ no_newmail = (client->set->parsed_workarounds & WORKAROUND_DELAY_NEWMAIL) != 0 &&
+ client->notify_ctx == NULL && /* always disabled with NOTIFY */
+ (imap_flags & IMAP_SYNC_FLAG_SAFE) == 0;
+ if (no_newmail) {
+ /* expunges might break the client just as badly as new mail
+ notifications. */
+ flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+ }
+
+ client->syncing = TRUE;
+
+ ctx = imap_sync_init(client, client->mailbox, imap_flags, flags);
+ ctx->no_newmail = no_newmail;
+
+ /* handle the syncing using sync_cmd. it doesn't actually matter which
+ one of the pending commands it is. */
+ sync_cmd->func = cmd_sync_continue;
+ sync_cmd->context = ctx;
+ sync_cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ if (!cmd_sync_continue(sync_cmd)) {
+ o_stream_set_flush_pending(client->output, TRUE);
+ return FALSE;
+ }
+
+ client_command_free(&sync_cmd);
+ cmd_sync_delayed(client);
+ return TRUE;
+}
+
+bool cmd_sync(struct client_command_context *cmd, enum mailbox_sync_flags flags,
+ enum imap_sync_flags imap_flags, const char *tagline)
+{
+ struct client *client = cmd->client;
+
+ i_assert(client->output_cmd_lock == NULL);
+
+ if (cmd->cancel)
+ return TRUE;
+
+ cmd->stats.last_run_timeval = ioloop_timeval;
+ if (client->mailbox == NULL) {
+ /* no mailbox selected, no point in delaying the sync */
+ if (tagline != NULL)
+ client_send_tagline(cmd, tagline);
+ return TRUE;
+ }
+ cmd->tagline_reply = p_strdup(cmd->pool, tagline);
+
+ cmd->sync = p_new(cmd->pool, struct imap_client_sync_context, 1);
+ cmd->sync->counter = client->sync_counter;
+ cmd->sync->flags = flags;
+ cmd->sync->imap_flags = imap_flags;
+ cmd->sync->tagline = cmd->tagline_reply;
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_SYNC;
+
+ cmd->func = NULL;
+ cmd->context = NULL;
+
+ if (client->input_lock == cmd)
+ client->input_lock = NULL;
+ return FALSE;
+}
+
+static bool cmd_sync_drop_fast(struct client *client)
+{
+ struct client_command_context *cmd, *prev;
+ bool ret = FALSE;
+
+ if (client->command_queue == NULL)
+ return FALSE;
+
+ for (cmd = client->command_queue; cmd->next != NULL; cmd = cmd->next) ;
+ for (; cmd != NULL; cmd = prev) {
+ prev = cmd->next;
+
+ if (cmd->state != CLIENT_COMMAND_STATE_WAIT_SYNC)
+ continue;
+
+ i_assert(cmd->sync != NULL);
+ if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_FAST) != 0) {
+ cmd_finish_sync(cmd);
+ client_command_free(&cmd);
+ ret = TRUE;
+ }
+ }
+ return ret;
+}
+
+static bool cmd_sync_delayed_real(struct client *client)
+{
+ struct client_command_context *cmd, *first_expunge, *first_nonexpunge;
+
+ if (client->output_cmd_lock != NULL) {
+ /* wait until we can send output to client */
+ return FALSE;
+ }
+
+ if (!imap_sync_is_allowed(client)) {
+ /* wait until mailbox can be synced */
+ return cmd_sync_drop_fast(client);
+ }
+
+ /* separate syncs that can send expunges from those that can't */
+ first_expunge = first_nonexpunge = NULL;
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ if (cmd->sync != NULL &&
+ cmd->sync->counter == client->sync_counter) {
+ if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) != 0) {
+ if (first_nonexpunge == NULL)
+ first_nonexpunge = cmd;
+ } else {
+ if (first_expunge == NULL)
+ first_expunge = cmd;
+ }
+ }
+ }
+ if (first_expunge != NULL && first_nonexpunge != NULL) {
+ /* sync expunges after nonexpunges */
+ for (cmd = first_expunge; cmd != NULL; cmd = cmd->next) {
+ if (cmd->sync != NULL &&
+ cmd->sync->counter == client->sync_counter &&
+ (cmd->sync->flags &
+ MAILBOX_SYNC_FLAG_NO_EXPUNGES) == 0)
+ cmd->sync->counter++;
+ }
+ first_expunge = NULL;
+ }
+ cmd = first_nonexpunge != NULL ? first_nonexpunge : first_expunge;
+
+ if (cmd == NULL)
+ return cmd_sync_drop_fast(client);
+ i_assert(client->mailbox != NULL);
+ return cmd_sync_client(cmd);
+}
+
+bool cmd_sync_delayed(struct client *client)
+{
+ bool ret;
+
+ T_BEGIN {
+ ret = cmd_sync_delayed_real(client);
+ } T_END;
+ return ret;
+}
diff --git a/src/imap/imap-sync.h b/src/imap/imap-sync.h
new file mode 100644
index 0000000..a1c5198
--- /dev/null
+++ b/src/imap/imap-sync.h
@@ -0,0 +1,25 @@
+#ifndef IMAP_SYNC_H
+#define IMAP_SYNC_H
+
+enum imap_sync_flags {
+ IMAP_SYNC_FLAG_SEND_UID = 0x01,
+ IMAP_SYNC_FLAG_SAFE = 0x02
+};
+
+struct client;
+
+struct imap_sync_context *
+imap_sync_init(struct client *client, struct mailbox *box,
+ enum imap_sync_flags imap_flags, enum mailbox_sync_flags flags);
+int imap_sync_deinit(struct imap_sync_context *ctx,
+ struct client_command_context *sync_cmd);
+int imap_sync_more(struct imap_sync_context *ctx);
+
+/* Returns TRUE if syncing would be allowed currently. */
+bool imap_sync_is_allowed(struct client *client);
+
+bool cmd_sync(struct client_command_context *cmd, enum mailbox_sync_flags flags,
+ enum imap_sync_flags imap_flags, const char *tagline);
+bool cmd_sync_delayed(struct client *client) ATTR_NOWARN_UNUSED_RESULT;
+
+#endif
diff --git a/src/imap/mail-storage-callbacks.c b/src/imap/mail-storage-callbacks.c
new file mode 100644
index 0000000..16c2d51
--- /dev/null
+++ b/src/imap/mail-storage-callbacks.c
@@ -0,0 +1,45 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ostream.h"
+#include "mail-storage.h"
+#include "imap-commands-util.h"
+
+static void notify_ok(struct mailbox *mailbox ATTR_UNUSED,
+ const char *text, void *context)
+{
+ struct client *client = context;
+
+ if (o_stream_get_buffer_used_size(client->output) != 0)
+ return;
+
+ T_BEGIN {
+ const char *str;
+
+ str = t_strconcat("* OK ", text, "\r\n", NULL);
+ o_stream_nsend_str(client->output, str);
+ (void)o_stream_flush(client->output);
+ } T_END;
+}
+
+static void notify_no(struct mailbox *mailbox ATTR_UNUSED,
+ const char *text, void *context)
+{
+ struct client *client = context;
+
+ if (o_stream_get_buffer_used_size(client->output) != 0)
+ return;
+
+ T_BEGIN {
+ const char *str;
+
+ str = t_strconcat("* NO ", text, "\r\n", NULL);
+ o_stream_nsend_str(client->output, str);
+ (void)o_stream_flush(client->output);
+ } T_END;
+}
+
+struct mail_storage_callbacks mail_storage_callbacks = {
+ notify_ok,
+ notify_no
+};
diff --git a/src/imap/main.c b/src/imap/main.c
new file mode 100644
index 0000000..cca5075
--- /dev/null
+++ b/src/imap/main.c
@@ -0,0 +1,591 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "path-util.h"
+#include "str.h"
+#include "base64.h"
+#include "process-title.h"
+#include "randgen.h"
+#include "restrict-access.h"
+#include "write-full.h"
+#include "settings-parser.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-login.h"
+#include "mail-user.h"
+#include "mail-storage-service.h"
+#include "smtp-submit-settings.h"
+#include "imap-master-client.h"
+#include "imap-resp-code.h"
+#include "imap-commands.h"
+#include "imap-feature.h"
+#include "imap-fetch.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+#define IMAP_DIE_IDLE_SECS 10
+
+static bool verbose_proctitle = FALSE;
+static struct mail_storage_service_ctx *storage_service;
+static struct master_login *master_login = NULL;
+static struct timeout *to_proctitle;
+
+imap_client_created_func_t *hook_client_created = NULL;
+bool imap_debug = FALSE;
+
+struct event_category event_category_imap = {
+ .name = "imap",
+};
+
+imap_client_created_func_t *
+imap_client_created_hook_set(imap_client_created_func_t *new_hook)
+{
+ imap_client_created_func_t *old_hook = hook_client_created;
+
+ hook_client_created = new_hook;
+ return old_hook;
+}
+
+static void imap_refresh_proctitle_callback(void *context ATTR_UNUSED)
+{
+ timeout_remove(&to_proctitle);
+ imap_refresh_proctitle();
+}
+
+void imap_refresh_proctitle_delayed(void)
+{
+ if (to_proctitle == NULL)
+ to_proctitle = timeout_add_short(0,
+ imap_refresh_proctitle_callback, NULL);
+}
+
+void imap_refresh_proctitle(void)
+{
+#define IMAP_PROCTITLE_PREFERRED_LEN 80
+ struct client *client;
+ struct client_command_context *cmd;
+ bool wait_output;
+
+ if (!verbose_proctitle)
+ return;
+ if (imap_client_count == 0) {
+ if (imap_master_clients_refresh_proctitle())
+ return;
+ }
+
+ string_t *title = t_str_new(128);
+ str_append_c(title, '[');
+ switch (imap_client_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ client = imap_clients;
+ str_append(title, client->user->username);
+ if (client->user->conn.remote_ip != NULL) {
+ str_append_c(title, ' ');
+ str_append(title,
+ net_ip2addr(client->user->conn.remote_ip));
+ }
+ wait_output = FALSE;
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ if (cmd->name == NULL)
+ continue;
+
+ if (str_len(title) < IMAP_PROCTITLE_PREFERRED_LEN) {
+ str_append_c(title, ' ');
+ str_append(title, cmd->name);
+ }
+ if (cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT)
+ wait_output = TRUE;
+ }
+ if (wait_output) {
+ str_printfa(title, " - %zu bytes waiting",
+ o_stream_get_buffer_used_size(client->output));
+ if (o_stream_is_corked(client->output))
+ str_append(title, " corked");
+ }
+ if (client->destroyed)
+ str_append(title, " (deinit)");
+ break;
+ default:
+ str_printfa(title, "%u connections", imap_client_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void client_kill_idle(struct client *client)
+{
+ if (client->output_cmd_lock != NULL)
+ return;
+
+ mail_storage_service_io_activate_user(client->service_user);
+ client_send_line(client, "* BYE Server shutting down.");
+ client_destroy(client, "Server shutting down.");
+}
+
+static void imap_die(void)
+{
+ struct client *client, *next;
+ time_t last_io, now = time(NULL);
+ time_t stop_timestamp = now - IMAP_DIE_IDLE_SECS;
+ unsigned int stop_msecs;
+
+ for (client = imap_clients; client != NULL; client = next) {
+ next = client->next;
+
+ last_io = I_MAX(client->last_input, client->last_output);
+ if (last_io <= stop_timestamp)
+ client_kill_idle(client);
+ else {
+ timeout_remove(&client->to_idle);
+ stop_msecs = (last_io - stop_timestamp) * 1000;
+ client->to_idle = timeout_add(stop_msecs,
+ client_kill_idle, client);
+ }
+ }
+}
+
+struct imap_login_request {
+ const char *tag;
+
+ const unsigned char *input;
+ size_t input_size;
+ bool send_untagged_capability;
+};
+
+static void
+client_parse_imap_login_request(const unsigned char *data, size_t len,
+ struct imap_login_request *input_r)
+{
+ size_t taglen;
+
+ i_zero(input_r);
+ if (len == 0)
+ return;
+
+ if (data[0] == '1')
+ input_r->send_untagged_capability = TRUE;
+ data++; len--;
+
+ input_r->tag = t_strndup(data, len);
+ taglen = strlen(input_r->tag) + 1;
+
+ if (len > taglen) {
+ input_r->input = data + taglen;
+ input_r->input_size = len - taglen;
+ }
+}
+
+static void
+client_send_login_reply(struct ostream *output, const char *capability_string,
+ const char *preauth_username,
+ const struct imap_login_request *request)
+{
+ string_t *reply = t_str_new(256);
+
+ /* cork/uncork around the OK reply to minimize latency */
+ o_stream_cork(output);
+ if (request->tag == NULL) {
+ str_printfa(reply, "* PREAUTH [CAPABILITY %s] Logged in as %s\r\n",
+ capability_string, preauth_username);
+ } else if (capability_string == NULL) {
+ /* Client initialization failed. There's no need to send
+ capabilities. Just send the tagged OK so the client knows
+ the login itself succeeded, followed by a BYE. */
+ str_printfa(reply, "%s OK Logged in, but initialization failed.\r\n",
+ request->tag);
+ str_append(reply, "* BYE "MAIL_ERRSTR_CRITICAL_MSG"\r\n");
+ } else if (request->send_untagged_capability) {
+ /* client doesn't seem to understand tagged capabilities. send
+ untagged instead and hope that it works. */
+ str_printfa(reply, "* CAPABILITY %s\r\n", capability_string);
+ str_printfa(reply, "%s OK Logged in\r\n", request->tag);
+ } else {
+ str_printfa(reply, "%s OK [CAPABILITY %s] Logged in\r\n",
+ request->tag, capability_string);
+ }
+ o_stream_nsend(output, str_data(reply), str_len(reply));
+ if (o_stream_uncork_flush(output) < 0 &&
+ output->stream_errno != EPIPE &&
+ output->stream_errno != ECONNRESET)
+ i_error("write(client) failed: %s", o_stream_get_error(output));
+}
+
+static void
+client_add_input_finalize(struct client *client)
+{
+ struct ostream *output;
+
+ /* try to condense any responses into as few packets as possible */
+ output = client->output;
+ o_stream_ref(output);
+ o_stream_cork(output);
+ (void)client_handle_input(client);
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+
+ /* we could have already handled LOGOUT, or we might need to continue
+ pending ambiguous commands. */
+ client_continue_pending_input(client);
+}
+
+int client_create_from_input(const struct mail_storage_service_input *input,
+ int fd_in, int fd_out, bool unhibernated,
+ struct client **client_r, const char **error_r)
+{
+ struct mail_storage_service_input service_input;
+ struct mail_storage_service_user *user;
+ struct mail_user *mail_user;
+ struct client *client;
+ struct imap_settings *imap_set;
+ struct smtp_submit_settings *smtp_set;
+ struct event *event;
+ const char *errstr;
+
+ event = event_create(NULL);
+ event_add_category(event, &event_category_imap);
+ event_add_fields(event, (const struct event_add_field []){
+ { .key = "user", .value = input->username },
+ { .key = NULL }
+ });
+ if (input->local_ip.family != 0)
+ event_add_str(event, "local_ip", net_ip2addr(&input->local_ip));
+ if (input->local_port != 0)
+ event_add_int(event, "local_port", input->local_port);
+ if (input->remote_ip.family != 0)
+ event_add_str(event, "remote_ip", net_ip2addr(&input->remote_ip));
+ if (input->remote_port != 0)
+ event_add_int(event, "remote_port", input->remote_port);
+
+ service_input = *input;
+ service_input.event_parent = event;
+ if (mail_storage_service_lookup_next(storage_service, &service_input,
+ &user, &mail_user, error_r) <= 0) {
+ event_unref(&event);
+ return -1;
+ }
+ /* Add the session only after creating the user, because
+ input->session_id may be NULL */
+ event_add_str(event, "session", mail_user->session_id);
+
+ restrict_access_allow_coredumps(TRUE);
+
+ smtp_set = mail_storage_service_user_get_set(user)[1];
+ imap_set = mail_storage_service_user_get_set(user)[2];
+ if (imap_set->verbose_proctitle)
+ verbose_proctitle = TRUE;
+
+ if (settings_var_expand(&smtp_submit_setting_parser_info, smtp_set,
+ mail_user->pool, mail_user_var_expand_table(mail_user),
+ &errstr) <= 0 ||
+ settings_var_expand(&imap_setting_parser_info, imap_set,
+ mail_user->pool, mail_user_var_expand_table(mail_user),
+ &errstr) <= 0) {
+ *error_r = t_strdup_printf("Failed to expand settings: %s", errstr);
+ mail_user_deinit(&mail_user);
+ mail_storage_service_user_unref(&user);
+ event_unref(&event);
+ return -1;
+ }
+
+ client = client_create(fd_in, fd_out, unhibernated,
+ event, mail_user, user, imap_set, smtp_set);
+ client->userdb_fields = input->userdb_fields == NULL ? NULL :
+ p_strarray_dup(client->pool, input->userdb_fields);
+ event_unref(&event);
+ *client_r = client;
+ return 0;
+}
+
+static void main_stdio_run(const char *username)
+{
+ struct client *client;
+ struct mail_storage_service_input input;
+ struct imap_login_request request;
+ const char *value, *error, *input_base64;
+
+ i_zero(&input);
+ input.module = input.service = "imap";
+ input.username = username != NULL ? username : getenv("USER");
+ if (input.username == NULL && IS_STANDALONE())
+ input.username = getlogin();
+ if (input.username == NULL)
+ i_fatal("USER environment missing");
+ if ((value = getenv("IP")) != NULL)
+ (void)net_addr2ip(value, &input.remote_ip);
+ if ((value = getenv("LOCAL_IP")) != NULL)
+ (void)net_addr2ip(value, &input.local_ip);
+
+ if (client_create_from_input(&input, STDIN_FILENO, STDOUT_FILENO,
+ FALSE, &client, &error) < 0)
+ i_fatal("%s", error);
+
+ input_base64 = getenv("CLIENT_INPUT");
+ if (input_base64 == NULL) {
+ /* IMAPLOGINTAG environment is compatible with mailfront */
+ i_zero(&request);
+ request.tag = getenv("IMAPLOGINTAG");
+ } else {
+ const buffer_t *input_buf = t_base64_decode_str(input_base64);
+ client_parse_imap_login_request(input_buf->data, input_buf->used,
+ &request);
+ if (request.input_size > 0) {
+ client_add_istream_prefix(client, request.input,
+ request.input_size);
+ }
+ }
+
+ client_create_finish_io(client);
+ client_send_login_reply(client->output,
+ str_c(client->capability_string),
+ client->user->username, &request);
+ if (client_create_finish(client, &error) < 0)
+ i_fatal("%s", error);
+ client_add_input_finalize(client);
+ /* client may be destroyed now */
+}
+
+static void
+login_client_connected(const struct master_login_client *login_client,
+ const char *username, const char *const *extra_fields)
+{
+#define MSG_BYE_INTERNAL_ERROR "* BYE "MAIL_ERRSTR_CRITICAL_MSG"\r\n"
+ struct mail_storage_service_input input;
+ struct client *client;
+ struct imap_login_request request;
+ enum mail_auth_request_flags flags = login_client->auth_req.flags;
+ const char *error;
+
+ i_zero(&input);
+ input.module = input.service = "imap";
+ input.local_ip = login_client->auth_req.local_ip;
+ input.remote_ip = login_client->auth_req.remote_ip;
+ input.local_port = login_client->auth_req.local_port;
+ input.remote_port = login_client->auth_req.remote_port;
+ input.username = username;
+ input.userdb_fields = extra_fields;
+ input.session_id = login_client->session_id;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SECURED) != 0)
+ input.conn_secured = TRUE;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED) != 0)
+ input.conn_ssl_secured = TRUE;
+
+ client_parse_imap_login_request(login_client->data,
+ login_client->auth_req.data_size,
+ &request);
+
+ if (client_create_from_input(&input, login_client->fd, login_client->fd,
+ FALSE, &client, &error) < 0) {
+ int fd = login_client->fd;
+ struct ostream *output =
+ o_stream_create_fd_autoclose(&fd, IO_BLOCK_SIZE);
+ client_send_login_reply(output, NULL, NULL, &request);
+ o_stream_destroy(&output);
+
+ i_error("%s", error);
+ master_service_client_connection_destroyed(master_service);
+ return;
+ }
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_TLS_COMPRESSION) != 0)
+ client->tls_compression = TRUE;
+ if (request.input_size > 0) {
+ client_add_istream_prefix(client, request.input,
+ request.input_size);
+ }
+
+ /* The order here is important:
+ 1. Finish setting up rawlog, so all input/output is written there.
+ 2. Send tagged reply to login before any potentially long-running
+ work (during which client could disconnect due to timeout).
+ 3. Finish initializing user, which can potentially take a long time.
+ */
+ client_create_finish_io(client);
+ client_send_login_reply(client->output,
+ str_c(client->capability_string),
+ NULL, &request);
+ if (client_create_finish(client, &error) < 0) {
+ if (write_full(login_client->fd, MSG_BYE_INTERNAL_ERROR,
+ strlen(MSG_BYE_INTERNAL_ERROR)) < 0)
+ if (errno != EAGAIN && errno != EPIPE &&
+ errno != ECONNRESET)
+ e_error(client->event,
+ "write_full(client) failed: %m");
+
+ e_error(client->event, "%s", error);
+ client_destroy(client, error);
+ return;
+ }
+
+ client_add_input_finalize(client);
+ /* client may be destroyed now */
+}
+
+static void login_client_failed(const struct master_login_client *client,
+ const char *errormsg)
+{
+ struct imap_login_request request;
+ const char *msg;
+
+ client_parse_imap_login_request(client->data,
+ client->auth_req.data_size, &request);
+ msg = t_strdup_printf("%s NO ["IMAP_RESP_CODE_UNAVAILABLE"] %s\r\n",
+ request.tag, errormsg);
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ /* when running standalone, we shouldn't even get here */
+ i_assert(master_login != NULL);
+
+ master_service_client_connection_accept(conn);
+ if (strcmp(conn->name, "imap-master") == 0) {
+ /* restoring existing IMAP connection (e.g. from imap-idle) */
+ imap_master_client_create(conn->fd);
+ } else {
+ master_login_add(master_login, conn->fd);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &smtp_submit_setting_parser_info,
+ &imap_setting_parser_info,
+ NULL
+ };
+ struct master_login_settings login_set;
+ enum master_service_flags service_flags = 0;
+ enum mail_storage_service_flags storage_service_flags =
+ MAIL_STORAGE_SERVICE_FLAG_NO_SSL_CA |
+ /*
+ * We include MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES so
+ * that the mail_user initialization is fast and we can
+ * quickly send back the OK response to LOGIN/AUTHENTICATE.
+ * Otherwise we risk a very slow namespace initialization to
+ * cause client timeouts on login.
+ */
+ MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES;
+ const char *username = NULL, *auth_socket_path = "auth-master";
+ int c;
+
+ i_zero(&login_set);
+ login_set.postlogin_timeout_secs = MASTER_POSTLOGIN_TIMEOUT_DEFAULT;
+ login_set.request_auth_token = TRUE;
+
+ if (IS_STANDALONE() && getuid() == 0 &&
+ net_getpeername(1, NULL, NULL) == 0) {
+ printf("* BAD [ALERT] imap binary must not be started from "
+ "inetd, use imap-login instead.\n");
+ return 1;
+ }
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ }
+
+ master_service = master_service_init("imap", service_flags,
+ &argc, &argv, "a:Dt:u:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a':
+ auth_socket_path = optarg;
+ break;
+ case 't':
+ if (str_to_uint(optarg, &login_set.postlogin_timeout_secs) < 0 ||
+ login_set.postlogin_timeout_secs == 0)
+ i_fatal("Invalid -t parameter: %s", optarg);
+ break;
+ case 'u':
+ storage_service_flags |=
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ username = optarg;
+ break;
+ case 'D':
+ imap_debug = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ master_service_set_die_callback(master_service, imap_die);
+
+ /* plugins may want to add commands, so this needs to be called early */
+ commands_init();
+ imap_fetch_handlers_init();
+ imap_features_init();
+ clients_init();
+ imap_master_clients_init();
+ /* this is needed before settings are read */
+ verbose_proctitle = !IS_STANDALONE() &&
+ getenv(MASTER_VERBOSE_PROCTITLE_ENV) != NULL;
+
+ const char *error;
+ if (t_abspath(auth_socket_path, &login_set.auth_socket_path, &error) < 0)
+ i_fatal("t_abspath(%s) failed: %s", auth_socket_path, error);
+
+ if (argv[optind] != NULL) {
+ if (t_abspath(argv[optind], &login_set.postlogin_socket_path, &error) < 0)
+ i_fatal("t_abspath(%s) failed: %s", argv[optind], error);
+ }
+ login_set.callback = login_client_connected;
+ login_set.failure_callback = login_client_failed;
+ login_set.update_proctitle = verbose_proctitle &&
+ master_service_get_client_limit(master_service) == 1;
+
+ if (!IS_STANDALONE())
+ master_login = master_login_init(master_service, &login_set);
+
+ storage_service =
+ mail_storage_service_init(master_service,
+ set_roots, storage_service_flags);
+ master_service_init_finish(master_service);
+ /* NOTE: login_set.*_socket_path are now invalid due to data stack
+ having been freed */
+
+ /* fake that we're running, so we know if client was destroyed
+ while handling its initial input */
+ io_loop_set_running(current_ioloop);
+
+ if (IS_STANDALONE()) {
+ T_BEGIN {
+ main_stdio_run(username);
+ } T_END;
+ } else {
+ io_loop_set_running(current_ioloop);
+ }
+
+ if (io_loop_is_running(current_ioloop))
+ master_service_run(master_service, client_connected);
+ clients_destroy_all();
+
+ if (master_login != NULL)
+ master_login_deinit(&master_login);
+ mail_storage_service_deinit(&storage_service);
+
+ imap_fetch_handlers_deinit();
+ imap_features_deinit();
+
+ commands_deinit();
+ imap_master_clients_deinit();
+
+ timeout_remove(&to_proctitle);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/imap/test-imap-client-hibernate.c b/src/imap/test-imap-client-hibernate.c
new file mode 100644
index 0000000..9b90e1b
--- /dev/null
+++ b/src/imap/test-imap-client-hibernate.c
@@ -0,0 +1,292 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "strescape.h"
+#include "path-util.h"
+#include "unlink-directory.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "smtp-submit.h"
+#include "mail-storage-service.h"
+#include "mail-storage-private.h"
+#include "imap-common.h"
+#include "imap-settings.h"
+#include "imap-client.h"
+
+#include <sys/stat.h>
+
+#define TEMP_DIRNAME ".test-imap-client-hibernate"
+
+#define EVILSTR "\t\r\n\001"
+
+struct test_imap_client_hibernate {
+ struct client *client;
+ int fd_listen;
+ bool has_mailbox;
+ const char *reply;
+};
+
+imap_client_created_func_t *hook_client_created = NULL;
+bool imap_debug = FALSE;
+
+static const char *tmpdir;
+static struct mail_storage_service_ctx *storage_service;
+
+void imap_refresh_proctitle(void) { }
+void imap_refresh_proctitle_delayed(void) { }
+int client_create_from_input(const struct mail_storage_service_input *input ATTR_UNUSED,
+ int fd_in ATTR_UNUSED, int fd_out ATTR_UNUSED,
+ bool unhibernated ATTR_UNUSED,
+ struct client **client_r ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED) { return -1; }
+
+static int imap_hibernate_server(struct test_imap_client_hibernate *ctx)
+{
+ i_set_failure_prefix("SERVER: ");
+
+ int fd = net_accept(ctx->fd_listen, NULL, NULL);
+ i_assert(fd > 0);
+ struct istream *input = i_stream_create_unix(fd, SIZE_MAX);
+ i_stream_unix_set_read_fd(input);
+
+ /* send handshake */
+ const char *str = "VERSION\timap-hibernate\t1\t0\n";
+ if (write(fd, str, strlen(str)) != (ssize_t)strlen(str))
+ i_fatal("write(imap-hibernate client handshake) failed: %m");
+
+ /* read handshake */
+ const char *line;
+ if ((line = i_stream_read_next_line(input)) == NULL)
+ i_fatal("read(imap-hibernate client handshake) failed: %s",
+ i_stream_get_error(input));
+ if (strcmp(line, "VERSION\timap-hibernate\t1\t0") != 0)
+ i_fatal("VERSION not received");
+ /* read command */
+ if ((line = i_stream_read_next_line(input)) == NULL)
+ i_fatal("read(imap-hibernate client command) failed: %s",
+ i_stream_get_error(input));
+ int fd2 = i_stream_unix_get_read_fd(input);
+ test_assert(fd2 != -1);
+ i_close_fd(&fd2);
+ const char *const *args = t_strsplit_tabescaped(line);
+
+ /* write reply */
+ if (write(fd, ctx->reply, strlen(ctx->reply)) != (ssize_t)strlen(ctx->reply))
+ i_fatal("write(imap-hibernate client command) failed: %m");
+
+ if (ctx->has_mailbox) {
+ /* read mailbox notify fd */
+ i_stream_unix_set_read_fd(input);
+ if (i_stream_read_next_line(input) == NULL)
+ i_fatal("read(imap-hibernate notify fd) failed: %s",
+ i_stream_get_error(input));
+
+ fd2 = i_stream_unix_get_read_fd(input);
+ test_assert(fd2 != -1);
+ i_close_fd(&fd2);
+
+ if (write(fd, "+\n", 2) != 2)
+ i_fatal("write(imap-hibernate client command) failed: %m");
+ }
+
+ unsigned int i = 0;
+ test_assert_strcmp(args[i++], EVILSTR"testuser");
+ test_assert_strcmp(args[i++], EVILSTR"%u");
+ test_assert_strcmp(args[i++], "idle_notify_interval=120");
+ test_assert(str_begins(args[i++], "peer_dev_major="));
+ test_assert(str_begins(args[i++], "peer_dev_minor="));
+ test_assert(str_begins(args[i++], "peer_ino="));
+ test_assert_strcmp(args[i++], "session="EVILSTR"session");
+ test_assert(str_begins(args[i++], "session_created="));
+ test_assert_strcmp(args[i++], "lip=127.0.0.1");
+ test_assert_strcmp(args[i++], "lport=1234");
+ test_assert_strcmp(args[i++], "rip=127.0.0.2");
+ test_assert_strcmp(args[i++], "rport=5678");
+ test_assert(str_begins(args[i++], "uid="));
+ test_assert(str_begins(args[i++], "gid="));
+ if (ctx->has_mailbox)
+ test_assert_strcmp(args[i++], "mailbox="EVILSTR"mailbox");
+ test_assert_strcmp(args[i++], "tag="EVILSTR"tag");
+ test_assert(str_begins(args[i++], "stats="));
+ test_assert_strcmp(args[i++], "idle-cmd");
+ if (ctx->has_mailbox)
+ test_assert_strcmp(args[i++], "notify_fd");
+ test_assert(str_begins(args[i++], "state="));
+ test_assert(args[i] == NULL);
+
+ i_stream_unref(&input);
+ i_close_fd(&ctx->fd_listen);
+ i_close_fd(&fd);
+
+ ctx->client->hibernated = TRUE; /* prevent disconnect Info message */
+ client_destroy(ctx->client, NULL);
+
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit_forked(&master_service);
+ return 0;
+}
+
+static void
+mailbox_notify_callback(struct mailbox *box ATTR_UNUSED,
+ struct client *client ATTR_UNUSED)
+{
+}
+
+static void test_imap_client_hibernate(void)
+{
+ struct client *client;
+ struct smtp_submit_settings smtp_set;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *mail_user;
+ struct test_imap_client_hibernate ctx;
+ const char *error;
+
+ storage_service = mail_storage_service_init(master_service, NULL,
+ MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR |
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS);
+
+ const char *const input_userdb[] = {
+ "mailbox_list_index=no",
+ t_strdup_printf("mail=mbox:%s/mbox", tmpdir),
+ NULL
+ };
+ struct mail_storage_service_input input = {
+ .username = EVILSTR"testuser",
+ .local_port = 1234,
+ .remote_port = 5678,
+ .userdb_fields = input_userdb,
+ };
+ test_assert(net_addr2ip("127.0.0.1", &input.local_ip) == 0);
+ test_assert(net_addr2ip("127.0.0.2", &input.remote_ip) == 0);
+ test_assert(mail_storage_service_lookup_next(storage_service, &input,
+ &service_user, &mail_user, &error) == 1);
+ mail_user->set->base_dir = tmpdir;
+ mail_user->set->mail_log_prefix = EVILSTR"%u";
+ mail_user->session_id = EVILSTR"session";
+ i_zero(&smtp_set);
+ i_zero(&ctx);
+
+ struct event *event = event_create(NULL);
+ int client_fd = dup(dev_null_fd);
+ client = client_create(client_fd, client_fd, FALSE, event,
+ mail_user, service_user,
+ imap_setting_parser_info.defaults, &smtp_set);
+ ctx.client = client;
+
+ /* can't hibernate without IDLE */
+ test_begin("imap client hibernate: non-IDLE");
+ test_assert(!imap_client_hibernate(&client, &error));
+ test_assert_strcmp(error, "Non-IDLE connections not supported currently");
+ test_end();
+
+ struct client_command_context *cmd = client_command_alloc(client);
+ cmd->tag = EVILSTR"tag";
+ cmd->name = "IDLE";
+ event_unref(&event);
+
+ /* imap-hibernate socket doesn't exist */
+ test_begin("imap client hibernate: socket not found");
+ test_expect_error_string("/"TEMP_DIRNAME"/imap-hibernate) failed: No such file or directory");
+ test_assert(!imap_client_hibernate(&client, &error));
+ test_expect_no_more_errors();
+ test_assert(strstr(error, "net_connect_unix") != NULL);
+ test_end();
+
+ /* imap-hibernate socket times out */
+ const char *socket_path = t_strdup_printf("%s/imap-hibernate", tmpdir);
+ ctx.fd_listen = net_listen_unix(socket_path, 1);
+ if (ctx.fd_listen == -1)
+ i_fatal("net_listen_unix(%s) failed: %m", socket_path);
+ fd_set_nonblock(ctx.fd_listen, FALSE);
+
+ /* imap-hibernate socket returns failure */
+ test_begin("imap client hibernate: error returned");
+ ctx.reply = "-notgood\n";
+ test_subprocess_fork(imap_hibernate_server, &ctx, FALSE);
+
+ test_expect_error_string(TEMP_DIRNAME"/imap-hibernate returned failure: notgood");
+ test_assert(!imap_client_hibernate(&client, &error));
+ test_expect_no_more_errors();
+ test_assert(strstr(error, "notgood") != NULL);
+ test_end();
+
+ /* create and open evil mailbox */
+ client->mailbox = mailbox_alloc(client->user->namespaces->list,
+ "testbox", 0);
+ struct mailbox_update update = {
+ .uid_validity = 12345678,
+ };
+ memset(update.mailbox_guid, 0x12, sizeof(update.mailbox_guid));
+ test_assert(mailbox_create(client->mailbox, &update, FALSE) == 0);
+ test_assert(mailbox_open(client->mailbox) == 0);
+ client->mailbox->vname = EVILSTR"mailbox";
+
+ /* successful hibernation */
+ test_begin("imap client hibernate: success");
+ ctx.reply = "+\n";
+ ctx.has_mailbox = TRUE;
+ test_subprocess_fork(imap_hibernate_server, &ctx, FALSE);
+ /* start notification only after forking or we'll have trouble
+ deinitializing cleanly */
+ mailbox_notify_changes(client->mailbox, mailbox_notify_callback, client);
+ test_assert(imap_client_hibernate(&client, &error));
+ test_end();
+
+ i_close_fd(&ctx.fd_listen);
+ mail_storage_service_deinit(&storage_service);
+}
+
+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 *cwd, *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() failed: %m");
+
+ test_subprocesses_init(FALSE);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS |
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ int ret;
+
+ master_service = master_service_init("test-imap-client-hibernate",
+ service_flags, &argc, &argv, "D");
+
+ master_service_init_finish(master_service);
+ test_init();
+
+ static void (*const test_functions[])(void) = {
+ test_imap_client_hibernate,
+ NULL
+ };
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ test_cleanup();
+ master_service_deinit(&master_service);
+ return ret;
+}
diff --git a/src/indexer/Makefile.am b/src/indexer/Makefile.am
new file mode 100644
index 0000000..8bfc891
--- /dev/null
+++ b/src/indexer/Makefile.am
@@ -0,0 +1,47 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = indexer indexer-worker
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+indexer_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+indexer_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+indexer_SOURCES = \
+ indexer.c \
+ indexer-client.c \
+ indexer-queue.c \
+ indexer-settings.c \
+ worker-connection.c \
+ worker-pool.c
+
+indexer_worker_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+indexer_worker_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+indexer_worker_SOURCES = \
+ indexer-worker.c \
+ indexer-worker-settings.c \
+ master-connection.c
+
+noinst_HEADERS = \
+ indexer.h \
+ indexer-client.h \
+ indexer-queue.h \
+ master-connection.h \
+ worker-connection.h \
+ worker-pool.h
+
diff --git a/src/indexer/Makefile.in b/src/indexer/Makefile.in
new file mode 100644
index 0000000..0300523
--- /dev/null
+++ b/src/indexer/Makefile.in
@@ -0,0 +1,869 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = indexer$(EXEEXT) indexer-worker$(EXEEXT)
+subdir = src/indexer
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_indexer_OBJECTS = indexer.$(OBJEXT) indexer-client.$(OBJEXT) \
+ indexer-queue.$(OBJEXT) indexer-settings.$(OBJEXT) \
+ worker-connection.$(OBJEXT) worker-pool.$(OBJEXT)
+indexer_OBJECTS = $(am_indexer_OBJECTS)
+am__DEPENDENCIES_1 =
+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_indexer_worker_OBJECTS = indexer-worker.$(OBJEXT) \
+ indexer-worker-settings.$(OBJEXT) master-connection.$(OBJEXT)
+indexer_worker_OBJECTS = $(am_indexer_worker_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)/indexer-client.Po \
+ ./$(DEPDIR)/indexer-queue.Po ./$(DEPDIR)/indexer-settings.Po \
+ ./$(DEPDIR)/indexer-worker-settings.Po \
+ ./$(DEPDIR)/indexer-worker.Po ./$(DEPDIR)/indexer.Po \
+ ./$(DEPDIR)/master-connection.Po \
+ ./$(DEPDIR)/worker-connection.Po ./$(DEPDIR)/worker-pool.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 = $(indexer_SOURCES) $(indexer_worker_SOURCES)
+DIST_SOURCES = $(indexer_SOURCES) $(indexer_worker_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+indexer_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+indexer_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+indexer_SOURCES = \
+ indexer.c \
+ indexer-client.c \
+ indexer-queue.c \
+ indexer-settings.c \
+ worker-connection.c \
+ worker-pool.c
+
+indexer_worker_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+indexer_worker_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+indexer_worker_SOURCES = \
+ indexer-worker.c \
+ indexer-worker-settings.c \
+ master-connection.c
+
+noinst_HEADERS = \
+ indexer.h \
+ indexer-client.h \
+ indexer-queue.h \
+ master-connection.h \
+ worker-connection.h \
+ worker-pool.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/indexer/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/indexer/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+indexer$(EXEEXT): $(indexer_OBJECTS) $(indexer_DEPENDENCIES) $(EXTRA_indexer_DEPENDENCIES)
+ @rm -f indexer$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(indexer_OBJECTS) $(indexer_LDADD) $(LIBS)
+
+indexer-worker$(EXEEXT): $(indexer_worker_OBJECTS) $(indexer_worker_DEPENDENCIES) $(EXTRA_indexer_worker_DEPENDENCIES)
+ @rm -f indexer-worker$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(indexer_worker_OBJECTS) $(indexer_worker_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer-queue.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer-worker-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer-worker.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/indexer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/worker-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/worker-pool.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/indexer-client.Po
+ -rm -f ./$(DEPDIR)/indexer-queue.Po
+ -rm -f ./$(DEPDIR)/indexer-settings.Po
+ -rm -f ./$(DEPDIR)/indexer-worker-settings.Po
+ -rm -f ./$(DEPDIR)/indexer-worker.Po
+ -rm -f ./$(DEPDIR)/indexer.Po
+ -rm -f ./$(DEPDIR)/master-connection.Po
+ -rm -f ./$(DEPDIR)/worker-connection.Po
+ -rm -f ./$(DEPDIR)/worker-pool.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-pkglibexecPROGRAMS
+
+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)/indexer-client.Po
+ -rm -f ./$(DEPDIR)/indexer-queue.Po
+ -rm -f ./$(DEPDIR)/indexer-settings.Po
+ -rm -f ./$(DEPDIR)/indexer-worker-settings.Po
+ -rm -f ./$(DEPDIR)/indexer-worker.Po
+ -rm -f ./$(DEPDIR)/indexer.Po
+ -rm -f ./$(DEPDIR)/master-connection.Po
+ -rm -f ./$(DEPDIR)/worker-connection.Po
+ -rm -f ./$(DEPDIR)/worker-pool.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/indexer/indexer-client.c b/src/indexer/indexer-client.c
new file mode 100644
index 0000000..ea557c0
--- /dev/null
+++ b/src/indexer/indexer-client.c
@@ -0,0 +1,223 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "connection.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "indexer-queue.h"
+#include "indexer-client.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE (1024*64)
+
+#define INDEXER_CLIENT_PROTOCOL_MAJOR_VERSION 1
+#define INDEXER_CLIENT_PROTOCOL_MINOR_VERSION 0
+
+struct indexer_client {
+ struct connection conn;
+
+ int refcount;
+ struct indexer_queue *queue;
+};
+
+struct indexer_client_request {
+ struct indexer_client *client;
+ unsigned int tag;
+};
+
+static void indexer_client_destroy(struct connection *conn);
+static void indexer_client_ref(struct indexer_client *client);
+static void indexer_client_unref(struct indexer_client *client);
+
+static int
+indexer_client_request_queue(struct indexer_client *client, bool append,
+ const char *const *args, const char **error_r)
+{
+ struct indexer_client_request *ctx = NULL;
+ const char *session_id = NULL;
+ unsigned int tag, max_recent_msgs;
+
+ /* <tag> <user> <mailbox> [<max_recent_msgs> [<session ID>]] */
+ if (str_array_length(args) < 3) {
+ *error_r = "Wrong parameter count";
+ return -1;
+ }
+ if (str_to_uint(args[0], &tag) < 0) {
+ *error_r = "Invalid tag";
+ return -1;
+ }
+ if (args[3] == NULL)
+ max_recent_msgs = 0;
+ else if (str_to_uint(args[3], &max_recent_msgs) < 0) {
+ *error_r = "Invalid max_recent_msgs";
+ return -1;
+ } else {
+ session_id = args[4];
+ }
+
+ if (tag != 0) {
+ ctx = i_new(struct indexer_client_request, 1);
+ ctx->client = client;
+ ctx->tag = tag;
+ indexer_client_ref(client);
+ }
+
+ indexer_queue_append(client->queue, append, args[1], args[2],
+ session_id, max_recent_msgs, ctx);
+ o_stream_nsend_str(client->conn.output, t_strdup_printf("%u\tOK\n", tag));
+ return 0;
+}
+
+static int
+indexer_client_request_optimize(struct indexer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct indexer_client_request *ctx = NULL;
+ unsigned int tag;
+
+ /* <tag> <user> <mailbox> */
+ if (str_array_length(args) != 3) {
+ *error_r = "Wrong parameter count";
+ return -1;
+ }
+ if (str_to_uint(args[0], &tag) < 0) {
+ *error_r = "Invalid tag";
+ return -1;
+ }
+
+ if (tag != 0) {
+ ctx = i_new(struct indexer_client_request, 1);
+ ctx->client = client;
+ ctx->tag = tag;
+ indexer_client_ref(client);
+ }
+
+ indexer_queue_append_optimize(client->queue, args[1], args[2], ctx);
+ o_stream_nsend_str(client->conn.output, t_strdup_printf("%u\tOK\n", tag));
+ return 0;
+}
+
+static int
+indexer_client_request(struct indexer_client *client,
+ const char *const *args, const char **error_r)
+{
+ const char *cmd = args[0];
+
+ args++;
+
+ if (strcmp(cmd, "APPEND") == 0)
+ return indexer_client_request_queue(client, TRUE, args, error_r);
+ else if (strcmp(cmd, "PREPEND") == 0)
+ return indexer_client_request_queue(client, FALSE, args, error_r);
+ else if (strcmp(cmd, "OPTIMIZE") == 0)
+ return indexer_client_request_optimize(client, args, error_r);
+ else {
+ *error_r = t_strconcat("Unknown command: ", cmd, NULL);
+ return -1;
+ }
+}
+
+static int
+indexer_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct indexer_client *client =
+ container_of(conn, struct indexer_client, conn);
+ const char *error;
+
+ if (indexer_client_request(client, args, &error) < 0) {
+ i_error("Client input error: %s", error);
+ return -1;
+ }
+ return 1;
+}
+
+void indexer_client_status_callback(int percentage, void *context)
+{
+ struct indexer_client_request *ctx = context;
+
+ if (ctx->client->conn.output != NULL) T_BEGIN {
+ o_stream_nsend_str(ctx->client->conn.output,
+ t_strdup_printf("%u\t%d\n", ctx->tag, percentage));
+ } T_END;
+ if (percentage < 0 || percentage == 100) {
+ indexer_client_unref(ctx->client);
+ i_free(ctx);
+ }
+}
+
+static struct connection_list *indexer_client_list = NULL;
+
+static const struct connection_vfuncs indexer_client_vfuncs = {
+ .destroy = indexer_client_destroy,
+ .input_args = indexer_client_input_args,
+};
+
+static const struct connection_settings indexer_client_set = {
+ .service_name_in = "indexer",
+ .service_name_out = "indexer",
+ .major_version = INDEXER_CLIENT_PROTOCOL_MAJOR_VERSION,
+ .minor_version = INDEXER_CLIENT_PROTOCOL_MINOR_VERSION,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = IO_BLOCK_SIZE,
+};
+
+
+void indexer_client_create(struct master_service_connection *conn,
+ struct indexer_queue *queue)
+{
+ struct indexer_client *client;
+
+ if (indexer_client_list == NULL) {
+ indexer_client_list =
+ connection_list_init(&indexer_client_set,
+ &indexer_client_vfuncs);
+ }
+
+ client = i_new(struct indexer_client, 1);
+ client->refcount = 1;
+ client->queue = queue;
+ connection_init_server(indexer_client_list, &client->conn,
+ conn->name, conn->fd, conn->fd);
+ indexer_refresh_proctitle();
+}
+
+static void indexer_client_destroy(struct connection *conn)
+{
+ struct indexer_client *client =
+ container_of(conn, struct indexer_client, conn);
+ connection_deinit(&client->conn);
+ master_service_client_connection_destroyed(master_service);
+ indexer_client_unref(client);
+ indexer_refresh_proctitle();
+}
+
+static void indexer_client_ref(struct indexer_client *client)
+{
+ i_assert(client->refcount > 0);
+
+ client->refcount++;
+}
+
+static void indexer_client_unref(struct indexer_client *client)
+{
+ i_assert(client->refcount > 0);
+
+ if (--client->refcount > 0)
+ return;
+ i_free(client);
+}
+
+unsigned int indexer_clients_get_count(void)
+{
+ if (indexer_client_list == NULL)
+ return 0;
+ return indexer_client_list->connections_count;
+}
+
+void indexer_clients_destroy_all(void)
+{
+ connection_list_deinit(&indexer_client_list);
+}
diff --git a/src/indexer/indexer-client.h b/src/indexer/indexer-client.h
new file mode 100644
index 0000000..609c678
--- /dev/null
+++ b/src/indexer/indexer-client.h
@@ -0,0 +1,13 @@
+#ifndef INDEXER_CLIENT_H
+#define INDEXER_CLIENT_H
+
+struct indexer_queue;
+
+void indexer_client_create(struct master_service_connection *conn,
+ struct indexer_queue *queue);
+void indexer_client_status_callback(int percentage, void *context);
+
+unsigned int indexer_clients_get_count(void);
+void indexer_clients_destroy_all(void);
+
+#endif
diff --git a/src/indexer/indexer-queue.c b/src/indexer/indexer-queue.c
new file mode 100644
index 0000000..3532b8b
--- /dev/null
+++ b/src/indexer/indexer-queue.c
@@ -0,0 +1,275 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "hash.h"
+#include "indexer-queue.h"
+
+struct indexer_queue {
+ indexer_queue_callback_t *callback;
+ void (*listen_callback)(struct indexer_queue *);
+
+ /* username+mailbox -> indexer_request */
+ HASH_TABLE(struct indexer_request *, struct indexer_request *) requests;
+ struct indexer_request *head, *tail;
+};
+
+static unsigned int
+indexer_request_hash(const struct indexer_request *request)
+{
+ return str_hash(request->username) ^ str_hash(request->mailbox);
+}
+
+static int indexer_request_cmp(const struct indexer_request *r1,
+ const struct indexer_request *r2)
+{
+ return strcmp(r1->username, r2->username) == 0 &&
+ strcmp(r1->mailbox, r2->mailbox) == 0 ? 0 : 1;
+}
+
+struct indexer_queue *
+indexer_queue_init(indexer_queue_callback_t *callback)
+{
+ struct indexer_queue *queue;
+
+ queue = i_new(struct indexer_queue, 1);
+ queue->callback = callback;
+ hash_table_create(&queue->requests, default_pool, 0,
+ indexer_request_hash, indexer_request_cmp);
+ return queue;
+}
+
+void indexer_queue_deinit(struct indexer_queue **_queue)
+{
+ struct indexer_queue *queue = *_queue;
+
+ *_queue = NULL;
+
+ i_assert(indexer_queue_is_empty(queue));
+
+ hash_table_destroy(&queue->requests);
+ i_free(queue);
+}
+
+void indexer_queue_set_listen_callback(struct indexer_queue *queue,
+ void (*callback)(struct indexer_queue *))
+{
+ queue->listen_callback = callback;
+}
+
+static struct indexer_request *
+indexer_queue_lookup(struct indexer_queue *queue,
+ const char *username, const char *mailbox)
+{
+ struct indexer_request lookup_request;
+
+ lookup_request.username = (void *)username;
+ lookup_request.mailbox = (void *)mailbox;
+ return hash_table_lookup(queue->requests, &lookup_request);
+}
+
+static void request_add_context(struct indexer_request *request, void *context)
+{
+ if (context == NULL)
+ return;
+
+ if (!array_is_created(&request->contexts))
+ i_array_init(&request->contexts, 2);
+ array_push_back(&request->contexts, &context);
+}
+
+static struct indexer_request *
+indexer_queue_append_request(struct indexer_queue *queue, bool append,
+ const char *username, const char *mailbox,
+ const char *session_id,
+ unsigned int max_recent_msgs, void *context)
+{
+ struct indexer_request *request;
+
+ request = indexer_queue_lookup(queue, username, mailbox);
+ if (request == NULL) {
+ request = i_new(struct indexer_request, 1);
+ request->username = i_strdup(username);
+ request->mailbox = i_strdup(mailbox);
+ request->session_id = i_strdup(session_id);
+ request->max_recent_msgs = max_recent_msgs;
+ request_add_context(request, context);
+ hash_table_insert(queue->requests, request, request);
+ } else {
+ if (request->max_recent_msgs > max_recent_msgs)
+ request->max_recent_msgs = max_recent_msgs;
+ request_add_context(request, context);
+ if (request->working) {
+ /* we're already indexing this mailbox. */
+ if (append)
+ request->reindex_tail = TRUE;
+ else
+ request->reindex_head = TRUE;
+ return request;
+ }
+ if (append) {
+ /* keep the request in its old position */
+ return request;
+ }
+ /* move request to beginning of the queue */
+ DLLIST2_REMOVE(&queue->head, &queue->tail, request);
+ }
+
+ if (append)
+ DLLIST2_APPEND(&queue->head, &queue->tail, request);
+ else
+ DLLIST2_PREPEND(&queue->head, &queue->tail, request);
+ return request;
+}
+
+static void indexer_queue_append_finish(struct indexer_queue *queue)
+{
+ if (queue->listen_callback != NULL)
+ queue->listen_callback(queue);
+ indexer_refresh_proctitle();
+}
+
+void indexer_queue_append(struct indexer_queue *queue, bool append,
+ const char *username, const char *mailbox,
+ const char *session_id, unsigned int max_recent_msgs,
+ void *context)
+{
+ struct indexer_request *request;
+
+ request = indexer_queue_append_request(queue, append, username, mailbox,
+ session_id, max_recent_msgs,
+ context);
+ request->index = TRUE;
+ indexer_queue_append_finish(queue);
+}
+
+void indexer_queue_append_optimize(struct indexer_queue *queue,
+ const char *username, const char *mailbox,
+ void *context)
+{
+ struct indexer_request *request;
+
+ request = indexer_queue_append_request(queue, TRUE, username, mailbox,
+ NULL, 0, context);
+ request->optimize = TRUE;
+ indexer_queue_append_finish(queue);
+}
+
+struct indexer_request *indexer_queue_request_peek(struct indexer_queue *queue)
+{
+ return queue->head;
+}
+
+void indexer_queue_request_remove(struct indexer_queue *queue)
+{
+ struct indexer_request *request = queue->head;
+
+ i_assert(request != NULL);
+
+ DLLIST2_REMOVE(&queue->head, &queue->tail, request);
+}
+
+static void indexer_queue_request_status_int(struct indexer_queue *queue,
+ struct indexer_request *request,
+ int percentage)
+{
+ void *context;
+ unsigned int i;
+
+ for (i = 0; i < request->working_context_idx; i++) {
+ context = array_idx_elem(&request->contexts, i);
+ queue->callback(percentage, context);
+ }
+}
+
+void indexer_queue_request_status(struct indexer_queue *queue,
+ struct indexer_request *request,
+ int percentage)
+{
+ i_assert(percentage >= 0 && percentage < 100);
+
+ indexer_queue_request_status_int(queue, request, percentage);
+}
+
+void indexer_queue_move_head_to_tail(struct indexer_queue *queue)
+{
+ struct indexer_request *request = queue->head;
+
+ indexer_queue_request_remove(queue);
+ DLLIST2_APPEND(&queue->head, &queue->tail, request);
+}
+
+void indexer_queue_request_work(struct indexer_request *request)
+{
+ request->working = TRUE;
+ request->working_context_idx =
+ !array_is_created(&request->contexts) ? 0 :
+ array_count(&request->contexts);
+}
+
+void indexer_queue_request_finish(struct indexer_queue *queue,
+ struct indexer_request **_request,
+ bool success)
+{
+ struct indexer_request *request = *_request;
+
+ *_request = NULL;
+
+ indexer_queue_request_status_int(queue, request, success ? 100 : -1);
+
+ if (request->reindex_head || request->reindex_tail) {
+ i_assert(request->working);
+ request->working = FALSE;
+ request->reindex_head = FALSE;
+ request->reindex_tail = FALSE;
+ if (request->working_context_idx > 0) {
+ array_delete(&request->contexts, 0,
+ request->working_context_idx);
+ }
+ if (request->reindex_head)
+ DLLIST2_PREPEND(&queue->head, &queue->tail, request);
+ else
+ DLLIST2_APPEND(&queue->head, &queue->tail, request);
+ return;
+ }
+
+ hash_table_remove(queue->requests, request);
+ if (array_is_created(&request->contexts))
+ array_free(&request->contexts);
+ i_free(request->username);
+ i_free(request->mailbox);
+ i_free(request->session_id);
+ i_free(request);
+
+ indexer_refresh_proctitle();
+}
+
+void indexer_queue_cancel_all(struct indexer_queue *queue)
+{
+ struct indexer_request *request;
+ struct hash_iterate_context *iter;
+
+ /* remove all reindex-markers so when the current requests finish
+ (or are cancelled) we don't try to retry them (especially during
+ deinit where it crashes) */
+ iter = hash_table_iterate_init(queue->requests);
+ while (hash_table_iterate(iter, queue->requests, &request, &request))
+ request->reindex_head = request->reindex_tail = FALSE;
+ hash_table_iterate_deinit(&iter);
+
+ while ((request = indexer_queue_request_peek(queue)) != NULL) {
+ indexer_queue_request_remove(queue);
+ indexer_queue_request_finish(queue, &request, FALSE);
+ }
+}
+
+bool indexer_queue_is_empty(struct indexer_queue *queue)
+{
+ return queue->head == NULL;
+}
+
+unsigned int indexer_queue_count(struct indexer_queue *queue)
+{
+ return hash_table_count(queue->requests);
+}
diff --git a/src/indexer/indexer-queue.h b/src/indexer/indexer-queue.h
new file mode 100644
index 0000000..a9d5b9d
--- /dev/null
+++ b/src/indexer/indexer-queue.h
@@ -0,0 +1,72 @@
+#ifndef INDEXER_QUEUE_H
+#define INDEXER_QUEUE_H
+
+#include "indexer.h"
+
+typedef void indexer_queue_callback_t(int status, void *context);
+
+struct indexer_request {
+ struct indexer_request *prev, *next;
+
+ char *username;
+ char *mailbox;
+ char *session_id;
+ unsigned int max_recent_msgs;
+
+ /* index messages in this mailbox */
+ bool index:1;
+ /* optimize this mailbox */
+ bool optimize:1;
+ /* currently indexing this mailbox */
+ bool working:1;
+ /* after indexing is finished, add this request back to the queue and
+ reindex it (i.e. a new indexing request came while we were
+ working.) */
+ bool reindex_head:1;
+ bool reindex_tail:1;
+
+ /* when working finished, call this number of contexts and leave the
+ rest to the reindexing. */
+ unsigned int working_context_idx;
+
+ ARRAY(void *) contexts;
+};
+
+struct indexer_queue *indexer_queue_init(indexer_queue_callback_t *callback);
+void indexer_queue_deinit(struct indexer_queue **queue);
+
+/* The callback is called whenever a new request is added to the queue. */
+void indexer_queue_set_listen_callback(struct indexer_queue *queue,
+ void (*callback)(struct indexer_queue *));
+
+void indexer_queue_append(struct indexer_queue *queue, bool append,
+ const char *username, const char *mailbox,
+ const char *session_id, unsigned int max_recent_msgs,
+ void *context);
+void indexer_queue_append_optimize(struct indexer_queue *queue,
+ const char *username, const char *mailbox,
+ void *context);
+void indexer_queue_cancel_all(struct indexer_queue *queue);
+
+bool indexer_queue_is_empty(struct indexer_queue *queue);
+unsigned int indexer_queue_count(struct indexer_queue *queue);
+
+/* Return the next request from the queue, without removing it. */
+struct indexer_request *indexer_queue_request_peek(struct indexer_queue *queue);
+/* Remove the next request from the queue. You must call
+ indexer_queue_request_finish() to free its memory. */
+void indexer_queue_request_remove(struct indexer_queue *queue);
+/* Give a status update about how far the indexing is going on. */
+void indexer_queue_request_status(struct indexer_queue *queue,
+ struct indexer_request *request,
+ int percentage);
+/* Move the next request to the end of the queue. */
+void indexer_queue_move_head_to_tail(struct indexer_queue *queue);
+/* Start working on a request */
+void indexer_queue_request_work(struct indexer_request *request);
+/* Finish the request and free its memory. */
+void indexer_queue_request_finish(struct indexer_queue *queue,
+ struct indexer_request **request,
+ bool success);
+
+#endif
diff --git a/src/indexer/indexer-settings.c b/src/indexer/indexer-settings.c
new file mode 100644
index 0000000..8375829
--- /dev/null
+++ b/src/indexer/indexer-settings.c
@@ -0,0 +1,50 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+extern const struct setting_parser_info service_setting_parser_info;
+
+/* <settings checks> */
+static struct file_listener_settings indexer_unix_listeners_array[] = {
+ { "indexer", 0666, "", "" }
+};
+static struct file_listener_settings *indexer_unix_listeners[] = {
+ &indexer_unix_listeners_array[0]
+};
+static buffer_t indexer_unix_listeners_buf = {
+ { { indexer_unix_listeners, sizeof(indexer_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings indexer_service_settings = {
+ .name = "indexer",
+ .protocol = "",
+ .type = "",
+ .executable = "indexer",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &indexer_unix_listeners_buf,
+ sizeof(indexer_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
diff --git a/src/indexer/indexer-worker-settings.c b/src/indexer/indexer-worker-settings.c
new file mode 100644
index 0000000..8627954
--- /dev/null
+++ b/src/indexer/indexer-worker-settings.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings indexer_worker_unix_listeners_array[] = {
+ { "indexer-worker", 0600, "$default_internal_user", "" }
+};
+static struct file_listener_settings *indexer_worker_unix_listeners[] = {
+ &indexer_worker_unix_listeners_array[0]
+};
+static buffer_t indexer_worker_unix_listeners_buf = {
+ { { indexer_worker_unix_listeners, sizeof(indexer_worker_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings indexer_worker_service_settings = {
+ .name = "indexer-worker",
+ .protocol = "",
+ .type = "worker",
+ .executable = "indexer-worker",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 10,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &indexer_worker_unix_listeners_buf,
+ sizeof(indexer_worker_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
diff --git a/src/indexer/indexer-worker.c b/src/indexer/indexer-worker.c
new file mode 100644
index 0000000..ef39aeb
--- /dev/null
+++ b/src/indexer/indexer-worker.c
@@ -0,0 +1,82 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "mail-storage-service.h"
+#include "mail-storage-settings.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-connection.h"
+
+static struct mail_storage_service_ctx *storage_service;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+
+ if (!master_connection_create(conn, storage_service)) {
+ i_error("indexer-worker must be configured with client_limit=1");
+ i_close_fd(&conn->fd);
+ master_service_client_connection_destroyed(master_service);
+ }
+}
+
+static void drop_privileges(void)
+{
+ struct restrict_access_settings set;
+ const char *error;
+
+ /* by default we don't drop any privileges, but keep running as root. */
+ restrict_access_get_env(&set);
+ if (set.uid != 0) {
+ /* open config connection before dropping privileges */
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+
+ i_zero(&input);
+ input.module = "mail";
+ input.service = "indexer-worker";
+ (void)master_service_settings_read(master_service,
+ &input, &output, &error);
+ }
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ enum mail_storage_service_flags storage_service_flags =
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP |
+ MAIL_STORAGE_SERVICE_FLAG_NO_IDLE_TIMEOUT;
+ int c;
+
+ master_service = master_service_init("indexer-worker", service_flags,
+ &argc, &argv, "D");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ storage_service_flags |=
+ MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ drop_privileges();
+ master_service_init_log_with_pid(master_service);
+
+ storage_service = mail_storage_service_init(master_service, NULL,
+ storage_service_flags);
+ restrict_access_allow_coredumps(TRUE);
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ master_connections_destroy();
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/indexer/indexer.c b/src/indexer/indexer.c
new file mode 100644
index 0000000..6175b54
--- /dev/null
+++ b/src/indexer/indexer.c
@@ -0,0 +1,137 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "restrict-access.h"
+#include "process-title.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "indexer-client.h"
+#include "indexer-queue.h"
+#include "worker-pool.h"
+#include "worker-connection.h"
+
+static const struct master_service_settings *set;
+static struct indexer_queue *queue;
+static struct worker_pool *worker_pool;
+
+void indexer_refresh_proctitle(void)
+{
+ if (!set->verbose_proctitle)
+ return;
+
+ process_title_set(t_strdup_printf("[%u clients, %u requests]",
+ indexer_clients_get_count(),
+ indexer_queue_count(queue)));
+}
+
+static bool idle_die(void)
+{
+ return indexer_queue_is_empty(queue) &&
+ !worker_pool_have_connections(worker_pool);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ indexer_client_create(conn, queue);
+}
+
+static void worker_send_request(struct connection *conn,
+ struct indexer_request *request)
+{
+ indexer_queue_request_work(request);
+ worker_connection_request(conn, request);
+}
+
+static void queue_try_send_more(struct indexer_queue *queue)
+{
+ struct connection *conn;
+ struct indexer_request *request, *first_moved_request = NULL;
+
+ while ((request = indexer_queue_request_peek(queue)) != NULL) {
+ conn = worker_pool_find_username_connection(worker_pool,
+ request->username);
+ if (conn != NULL) {
+ /* There is already a connection handling a request
+ * for this user. Move the request to the back of the
+ * queue and handle requests from other users.
+ * Terminate if we went through all requests. */
+ if (request == first_moved_request) {
+ /* all requests are waiting for existing users
+ to finish. */
+ break;
+ }
+ if (first_moved_request == NULL)
+ first_moved_request = request;
+ indexer_queue_move_head_to_tail(queue);
+ continue;
+ } else {
+ /* create a new connection to a worker */
+ if (!worker_pool_get_connection(worker_pool, &conn))
+ break;
+ }
+ indexer_queue_request_remove(queue);
+ worker_send_request(conn, request);
+ }
+}
+
+static void queue_listen_callback(struct indexer_queue *queue)
+{
+ queue_try_send_more(queue);
+}
+
+static void
+worker_status_callback(int percentage, struct indexer_request *request)
+{
+ if (percentage >= 0 && percentage < 100) {
+ indexer_queue_request_status(queue, request,
+ percentage);
+ return;
+ }
+
+ indexer_queue_request_finish(queue, &request,
+ percentage == 100);
+}
+
+static void worker_avail_callback(void)
+{
+ /* A new worker became available. Try to shrink the queue. */
+ queue_try_send_more(queue);
+}
+
+int main(int argc, char *argv[])
+{
+ const char *error;
+
+ master_service = master_service_init("indexer", 0, &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ if (master_service_settings_read_simple(master_service, NULL,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ set = master_service_settings_get(master_service);
+
+ master_service_init_log(master_service);
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+ master_service_set_idle_die_callback(master_service, idle_die);
+
+ queue = indexer_queue_init(indexer_client_status_callback);
+ indexer_queue_set_listen_callback(queue, queue_listen_callback);
+ worker_pool = worker_pool_init("indexer-worker",
+ worker_status_callback,
+ worker_avail_callback);
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ indexer_queue_cancel_all(queue);
+ indexer_clients_destroy_all();
+ worker_pool_deinit(&worker_pool);
+ indexer_queue_deinit(&queue);
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/indexer/indexer.h b/src/indexer/indexer.h
new file mode 100644
index 0000000..2234f2d
--- /dev/null
+++ b/src/indexer/indexer.h
@@ -0,0 +1,12 @@
+#ifndef INDEXER_H
+#define INDEXER_H
+
+struct indexer_request;
+
+/* percentage: -1 = failed, 0..99 = indexing in progress, 100 = done */
+typedef void
+indexer_status_callback_t(int percentage, struct indexer_request *request);
+
+void indexer_refresh_proctitle(void);
+
+#endif
diff --git a/src/indexer/master-connection.c b/src/indexer/master-connection.c
new file mode 100644
index 0000000..14f3c7d
--- /dev/null
+++ b/src/indexer/master-connection.c
@@ -0,0 +1,365 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "connection.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "process-title.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mail-storage-service.h"
+#include "mail-search-build.h"
+#include "master-connection.h"
+
+#include <unistd.h>
+
+#define INDEXER_PROTOCOL_MAJOR_VERSION 1
+#define INDEXER_PROTOCOL_MINOR_VERSION 0
+
+#define INDEXER_MASTER_NAME "indexer-master-worker"
+#define INDEXER_WORKER_NAME "indexer-worker-master"
+
+static struct event_category event_category_indexer_worker = {
+ .name = "indexer-worker",
+};
+
+struct master_connection {
+ struct connection conn;
+ struct mail_storage_service_ctx *storage_service;
+
+ bool version_received:1;
+};
+
+static void ATTR_NULL(1, 2)
+indexer_worker_refresh_proctitle(const char *username, const char *mailbox,
+ uint32_t seq1, uint32_t seq2)
+{
+ if (!master_service_settings_get(master_service)->verbose_proctitle)
+ return;
+
+ if (username == NULL)
+ process_title_set("[idling]");
+ else if (seq1 == 0)
+ process_title_set(t_strdup_printf("[%s %s]", username, mailbox));
+ else {
+ process_title_set(t_strdup_printf("[%s %s - %u/%u]",
+ username, mailbox, seq1, seq2));
+ }
+}
+
+static const char *
+get_attempt_error(unsigned int counter, uint32_t first_uid, uint32_t last_uid)
+{
+ if (counter == 0)
+ return " (no mails indexed)";
+ return t_strdup_printf(
+ " (attempted to index %u messages between UIDs %u..%u)",
+ counter, first_uid, last_uid);
+}
+
+static int
+index_mailbox_precache(struct master_connection *conn, struct mailbox *box)
+{
+ struct mail_storage *storage = mailbox_get_storage(box);
+ const char *username = mail_storage_get_user(storage)->username;
+ const char *box_vname = mailbox_get_vname(box);
+ const char *errstr;
+ enum mail_error error;
+ struct mailbox_status status;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct mailbox_metadata metadata;
+ uint32_t seq, first_uid = 0, last_uid = 0;
+ char percentage_str[2+1+1];
+ unsigned int counter = 0, max, percentage, percentage_sent = 0;
+ int ret = 0;
+ struct event *index_event = event_create(box->event);
+ event_add_category(index_event, &event_category_indexer_worker);
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_PRECACHE_FIELDS,
+ &metadata) < 0) {
+ e_error(index_event, "Precache-fields lookup failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ event_unref(&index_event);
+ return -1;
+ }
+ if (mailbox_get_status(box, STATUS_MESSAGES | STATUS_LAST_CACHED_SEQ,
+ &status) < 0) {
+ e_error(index_event, "Status lookup failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ event_unref(&index_event);
+ return -1;
+ }
+ seq = status.last_cached_seq + 1;
+
+ trans = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_NO_CACHE_DEC,
+ "indexing");
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq, status.messages);
+
+ event_enable_user_cpu_usecs(index_event);
+
+ ctx = mailbox_search_init(trans, search_args, NULL,
+ metadata.precache_fields, NULL);
+ mail_search_args_unref(&search_args);
+
+ max = status.messages + 1 - seq;
+ while (mailbox_search_next(ctx, &mail)) {
+ if (first_uid == 0)
+ first_uid = mail->uid;
+ last_uid = mail->uid;
+ e_debug(index_event, "Indexing UID=%u", mail->uid);
+
+ if (mail_precache(mail) < 0) {
+ e_error(index_event, "Precache for UID=%u failed: %s%s",
+ mail->uid,
+ mailbox_get_last_internal_error(box, NULL),
+ get_attempt_error(counter, first_uid, last_uid));
+ ret = -1;
+ break;
+ }
+ if (++counter % 100 == 0) {
+ percentage = counter*100 / max;
+ if (percentage != percentage_sent && percentage < 100) {
+ percentage_sent = percentage;
+ if (i_snprintf(percentage_str,
+ sizeof(percentage_str), "%u\n",
+ percentage) < 0)
+ i_unreached();
+ o_stream_nsend_str(conn->conn.output,
+ percentage_str);
+ }
+ indexer_worker_refresh_proctitle(username, box_vname,
+ counter, max);
+ }
+ }
+ if (mailbox_search_deinit(&ctx) < 0) {
+ e_error(index_event, "Mail search failed: %s%s",
+ mailbox_get_last_internal_error(box, NULL),
+ get_attempt_error(counter, first_uid, last_uid));
+ ret = -1;
+ }
+ const char *uids = first_uid == 0 ? "" :
+ t_strdup_printf(" (UIDs %u..%u)", first_uid, last_uid);
+ event_add_int(index_event, "message_count", counter);
+ event_add_int(index_event, "first_uid", first_uid);
+ event_add_int(index_event, "last_uid", last_uid);
+
+#define FINISHED_EVENT_NAME "indexer_worker_indexing_finished"
+ if (mailbox_transaction_commit(&trans) < 0) {
+ struct event_passthrough *e = event_create_passthrough(index_event)->
+ set_name(FINISHED_EVENT_NAME);
+ errstr = t_strdup_printf("Transaction commit failed: %s",
+ mailbox_get_last_internal_error(box, &error));
+ e->add_str("error", errstr);
+ const char *log_error = t_strdup_printf("%s (attempted to index %u messages%s)",
+ errstr, counter, uids);
+ if (error != MAIL_ERROR_NOTFOUND)
+ e_error(e->event(), "%s", log_error);
+ else
+ e_debug(e->event(), "%s", log_error);
+ ret = -1;
+ } else {
+ struct event_passthrough *e = event_create_passthrough(index_event)->
+ set_name(FINISHED_EVENT_NAME);
+ e_debug(e->event(), "Indexed %u messages%s", counter, uids);
+ }
+ event_unref(&index_event);
+ return ret;
+}
+
+static int
+index_mailbox(struct master_connection *conn, struct mail_user *user,
+ const char *mailbox, unsigned int max_recent_msgs,
+ const char *what)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mailbox_status status;
+ const char *path, *errstr;
+ enum mail_error error;
+ enum mailbox_sync_flags sync_flags = MAILBOX_SYNC_FLAG_FULL_READ;
+ int ret;
+
+ ns = mail_namespace_find(user->namespaces, mailbox);
+ box = mailbox_alloc(ns->list, mailbox, 0);
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path);
+ if (ret < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_NOTFOUND)
+ e_error(box->event, "Getting path failed: %s", errstr);
+ mailbox_free(&box);
+ return -1;
+ }
+ if (ret == 0) {
+ e_info(box->event, "Indexes disabled, skipping");
+ mailbox_free(&box);
+ return 0;
+ }
+ ret = 0;
+
+ if (max_recent_msgs != 0) {
+ /* index only if there aren't too many recent messages.
+ don't bother syncing the mailbox, that alone can take a
+ while with large maildirs. */
+ if (mailbox_open(box) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_NOTFOUND)
+ e_error(box->event, "Opening failed: %s",
+ errstr);
+ ret = -1;
+ } else {
+ mailbox_get_open_status(box, STATUS_RECENT, &status);
+ }
+ if (ret < 0 || status.recent > max_recent_msgs) {
+ mailbox_free(&box);
+ return ret;
+ }
+ }
+
+ if (strchr(what, 'o') != NULL)
+ sync_flags |= MAILBOX_SYNC_FLAG_OPTIMIZE;
+
+ if (mailbox_sync(box, sync_flags) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_NOTFOUND) {
+ e_error(box->event, "Syncing failed: %s", errstr);
+ } else {
+ e_debug(box->event, "Syncing failed: %s", errstr);
+ }
+ ret = -1;
+ } else if (strchr(what, 'i') != NULL) {
+ if (index_mailbox_precache(conn, box) < 0)
+ ret = -1;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int
+master_connection_input_args(struct connection *_conn, const char *const *args)
+{
+ struct master_connection *conn =
+ container_of(_conn, struct master_connection, conn);
+ struct mail_storage_service_input input;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ const char *str, *error;
+ unsigned int max_recent_msgs;
+ int ret;
+
+ /* <username> <mailbox> <session ID> <max_recent_msgs> [i][o] */
+ if (str_array_length(args) != 5 ||
+ str_to_uint(args[3], &max_recent_msgs) < 0 || args[4][0] == '\0') {
+ e_error(conn->conn.event, "Invalid input from master: %s",
+ t_strarray_join(args, "\t"));
+ return -1;
+ }
+
+ i_zero(&input);
+ input.module = "mail";
+ input.service = "indexer-worker";
+ input.username = args[0];
+ /* if session-id is given, use it as a prefix to a unique session ID.
+ we can't use the session-id directly or stats process will complain
+ about duplicates. (especially LMTP would use the same session-id for
+ multiple users' indexing at the same time.) */
+ if (args[2][0] != '\0')
+ input.session_id_prefix = args[2];
+
+ if (mail_storage_service_lookup_next(conn->storage_service, &input,
+ &service_user, &user, &error) <= 0) {
+ e_error(conn->conn.event, "User %s lookup failed: %s", args[0], error);
+ ret = -1;
+ } else {
+ indexer_worker_refresh_proctitle(user->username, args[1], 0, 0);
+ struct event_reason *reason =
+ event_reason_begin("indexer:index_mailbox");
+ ret = index_mailbox(conn, user, args[1],
+ max_recent_msgs, args[4]);
+ event_reason_end(&reason);
+ /* refresh proctitle before a potentially long-running
+ user unref */
+ indexer_worker_refresh_proctitle(user->username, "(deinit)", 0, 0);
+ mail_user_deinit(&user);
+ mail_storage_service_user_unref(&service_user);
+ indexer_worker_refresh_proctitle(NULL, NULL, 0, 0);
+ }
+
+ str = ret < 0 ? "-1\n" : "100\n";
+ o_stream_nsend_str(conn->conn.output, str);
+ return ret;
+}
+
+static void master_connection_destroy(struct connection *connection)
+{
+ connection_deinit(connection);
+ i_free(connection);
+ master_service_client_connection_destroyed(master_service);
+}
+
+static int master_connection_handshake_args(struct connection *connection,
+ const char *const *args)
+{
+ int ret;
+ if ((ret = connection_handshake_args_default(connection, args)) < 1)
+ return ret;
+ const char *limit = t_strdup_printf("%u\n",
+ master_service_get_process_limit(master_service));
+ o_stream_nsend_str(connection->output, limit);
+ return 1;
+}
+
+static struct connection_list *master_connection_list = NULL;
+
+static const struct connection_vfuncs master_connection_vfuncs = {
+ .destroy = master_connection_destroy,
+ .input_args = master_connection_input_args,
+ .handshake_args = master_connection_handshake_args,
+};
+
+static const struct connection_settings master_connection_set = {
+ .service_name_in = INDEXER_MASTER_NAME,
+ .service_name_out = INDEXER_WORKER_NAME,
+ .major_version = INDEXER_PROTOCOL_MAJOR_VERSION,
+ .minor_version = INDEXER_PROTOCOL_MINOR_VERSION,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+};
+
+bool
+master_connection_create(struct master_service_connection *master,
+ struct mail_storage_service_ctx *storage_service)
+{
+ struct master_connection *conn;
+
+ if (master_connection_list == NULL) {
+ master_connection_list =
+ connection_list_init(&master_connection_set,
+ &master_connection_vfuncs);
+ } else if (master_connection_list->connections_count > 0) {
+ return FALSE;
+ }
+
+ conn = i_new(struct master_connection, 1);
+ conn->storage_service = storage_service;
+ conn->conn.event_parent = event_create(NULL);
+ event_add_category(conn->conn.event_parent, &event_category_indexer_worker);
+ connection_init_server(master_connection_list, &conn->conn,
+ master->name, master->fd, master->fd);
+
+ event_unref(&conn->conn.event_parent);
+ return TRUE;
+}
+
+void master_connections_destroy(void)
+{
+ if (master_connection_list != NULL)
+ connection_list_deinit(&master_connection_list);
+}
diff --git a/src/indexer/master-connection.h b/src/indexer/master-connection.h
new file mode 100644
index 0000000..7dcb70e
--- /dev/null
+++ b/src/indexer/master-connection.h
@@ -0,0 +1,8 @@
+#ifndef MASTER_CONNECTION_H
+#define MASTER_CONNECTION_H
+
+bool master_connection_create(struct master_service_connection *conn,
+ struct mail_storage_service_ctx *storage_service);
+void master_connections_destroy(void);
+
+#endif
diff --git a/src/indexer/worker-connection.c b/src/indexer/worker-connection.c
new file mode 100644
index 0000000..19e2a41
--- /dev/null
+++ b/src/indexer/worker-connection.c
@@ -0,0 +1,196 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "aqueue.h"
+#include "connection.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "llist.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "indexer-queue.h"
+#include "worker-connection.h"
+
+#include <unistd.h>
+
+#define INDEXER_PROTOCOL_MAJOR_VERSION 1
+#define INDEXER_PROTOCOL_MINOR_VERSION 0
+
+#define INDEXER_MASTER_NAME "indexer-master-worker"
+#define INDEXER_WORKER_NAME "indexer-worker-master"
+
+struct worker_connection {
+ struct connection conn;
+
+ indexer_status_callback_t *callback;
+ worker_available_callback_t *avail_callback;
+
+ char *request_username;
+ struct indexer_request *request;
+};
+
+static unsigned int worker_last_process_limit = 0;
+
+static void worker_connection_call_callback(struct worker_connection *worker,
+ int percentage)
+{
+ if (worker->request != NULL)
+ worker->callback(percentage, worker->request);
+ if (percentage < 0 || percentage == 100)
+ worker->request = NULL;
+}
+
+void worker_connection_destroy(struct connection *conn)
+{
+ struct worker_connection *worker =
+ container_of(conn, struct worker_connection, conn);
+
+ worker_connection_call_callback(worker, -1);
+ i_free_and_null(worker->request_username);
+ connection_deinit(conn);
+
+ worker->avail_callback();
+ i_free(conn);
+}
+
+static int
+worker_connection_handshake_args(struct connection *conn, const char *const *args)
+{
+ unsigned int process_limit;
+ int ret;
+ if (!conn->version_received) {
+ if ((ret = connection_handshake_args_default(conn, args)) < 1)
+ return ret;
+ /* we are not done yet */
+ return 0;
+ }
+ if (str_to_uint(args[0], &process_limit) < 0 ||
+ process_limit == 0) {
+ e_error(conn->event, "Worker sent invalid process limit '%s'",
+ args[0]);
+ return -1;
+ }
+ worker_last_process_limit = process_limit;
+ return 1;
+}
+
+static int
+worker_connection_input_args(struct connection *conn, const char *const *args)
+{
+ struct worker_connection *worker =
+ container_of(conn, struct worker_connection, conn);
+ int percentage;
+ int ret = 1;
+
+ if (str_to_int(args[0], &percentage) < 0 ||
+ percentage < -1 || percentage > 100) {
+ e_error(conn->event, "Worker sent invalid progress '%s'", args[0]);
+ return -1;
+ }
+
+ if (percentage < 0)
+ ret = -1;
+
+ worker_connection_call_callback(worker, percentage);
+ if (worker->request == NULL) {
+ /* disconnect after each request */
+ ret = -1;
+ }
+
+ return ret;
+}
+
+bool worker_connection_is_connected(struct connection *conn)
+{
+ return !conn->disconnected;
+}
+
+unsigned int worker_connections_get_process_limit(void)
+{
+ return worker_last_process_limit;
+}
+
+void worker_connection_request(struct connection *conn,
+ struct indexer_request *request)
+{
+ struct worker_connection *worker =
+ container_of(conn, struct worker_connection, conn);
+
+ i_assert(worker_connection_is_connected(conn));
+ i_assert(request->index || request->optimize);
+
+ if (worker->request_username == NULL)
+ worker->request_username = i_strdup(request->username);
+ else {
+ i_assert(strcmp(worker->request_username,
+ request->username) == 0);
+ }
+
+ worker->request = request;
+
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+
+ str_append_tabescaped(str, request->username);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, request->mailbox);
+ str_append_c(str, '\t');
+ if (request->session_id != NULL)
+ str_append_tabescaped(str, request->session_id);
+ str_printfa(str, "\t%u\t", request->max_recent_msgs);
+ if (request->index)
+ str_append_c(str, 'i');
+ if (request->optimize)
+ str_append_c(str, 'o');
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ } T_END;
+}
+
+const char *worker_connection_get_username(struct connection *conn)
+{
+ struct worker_connection *worker =
+ container_of(conn, struct worker_connection, conn);
+ return worker->request_username;
+}
+
+static const struct connection_vfuncs worker_connection_vfuncs = {
+ .destroy = worker_connection_destroy,
+ .input_args = worker_connection_input_args,
+ .handshake_args = worker_connection_handshake_args,
+};
+
+static const struct connection_settings worker_connection_set = {
+ .service_name_in = INDEXER_WORKER_NAME,
+ .service_name_out = INDEXER_MASTER_NAME,
+ .major_version = INDEXER_PROTOCOL_MAJOR_VERSION,
+ .minor_version = INDEXER_PROTOCOL_MINOR_VERSION,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+};
+
+struct connection_list *worker_connection_list_create(void)
+{
+ return connection_list_init(&worker_connection_set,
+ &worker_connection_vfuncs);
+}
+
+struct connection *
+worker_connection_create(const char *socket_path,
+ indexer_status_callback_t *callback,
+ worker_available_callback_t *avail_callback,
+ struct connection_list *list)
+{
+ struct worker_connection *conn;
+
+ conn = i_new(struct worker_connection, 1);
+ conn->callback = callback;
+ conn->avail_callback = avail_callback;
+ connection_init_client_unix(list, &conn->conn, socket_path);
+
+ return &conn->conn;
+}
diff --git a/src/indexer/worker-connection.h b/src/indexer/worker-connection.h
new file mode 100644
index 0000000..e664c7e
--- /dev/null
+++ b/src/indexer/worker-connection.h
@@ -0,0 +1,36 @@
+#ifndef WORKER_CONNECTION_H
+#define WORKER_CONNECTION_H
+
+#include "indexer.h"
+
+struct indexer_request;
+struct connection_list;
+
+typedef void worker_available_callback_t(void);
+
+struct connection *
+worker_connection_create(const char *socket_path,
+ indexer_status_callback_t *callback,
+ worker_available_callback_t *avail_callback,
+ struct connection_list *list);
+void worker_connection_destroy(struct connection *conn);
+
+struct connection_list *worker_connection_list_create(void);
+
+/* Returns TRUE if worker is connected to (not necessarily handshaked yet) */
+bool worker_connection_is_connected(struct connection *conn);
+
+/* Returns the last process_limit returned by a worker connection handshake.
+ If no handshakes have been received yet, returns 0. */
+unsigned int worker_connections_get_process_limit(void);
+
+/* Send a new indexing request for username+mailbox. The status callback is
+ called as necessary. Requests can be queued, but only for the same
+ username. */
+void worker_connection_request(struct connection *conn,
+ struct indexer_request *request);
+/* Returns username of the currently pending requests,
+ or NULL if there are none. */
+const char *worker_connection_get_username(struct connection *conn);
+
+#endif
diff --git a/src/indexer/worker-pool.c b/src/indexer/worker-pool.c
new file mode 100644
index 0000000..e0de136
--- /dev/null
+++ b/src/indexer/worker-pool.c
@@ -0,0 +1,98 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "llist.h"
+#include "connection.h"
+#include "master-service.h"
+#include "worker-connection.h"
+#include "worker-pool.h"
+
+#define MAX_WORKER_IDLE_SECS (60*5)
+
+struct worker_pool {
+ char *socket_path;
+ indexer_status_callback_t *callback;
+ worker_available_callback_t *avail_callback;
+
+ struct connection_list *connection_list;
+};
+
+struct worker_pool *
+worker_pool_init(const char *socket_path, indexer_status_callback_t *callback,
+ worker_available_callback_t *avail_callback)
+{
+ struct worker_pool *pool;
+
+ pool = i_new(struct worker_pool, 1);
+ pool->socket_path = i_strdup(socket_path);
+ pool->callback = callback;
+ pool->avail_callback = avail_callback;
+ pool->connection_list = worker_connection_list_create();
+ return pool;
+}
+
+void worker_pool_deinit(struct worker_pool **_pool)
+{
+ struct worker_pool *pool = *_pool;
+
+ *_pool = NULL;
+
+ if (pool->connection_list != NULL)
+ connection_list_deinit(&pool->connection_list);
+
+ i_free(pool->connection_list);
+ i_free(pool->socket_path);
+ i_free(pool);
+}
+
+bool worker_pool_have_connections(struct worker_pool *pool)
+{
+ return pool->connection_list->connections != NULL;
+}
+
+static int worker_pool_add_connection(struct worker_pool *pool,
+ struct connection **conn_r)
+{
+ struct connection *conn;
+
+ conn = worker_connection_create(pool->socket_path, pool->callback,
+ pool->avail_callback,
+ pool->connection_list);
+ if (connection_client_connect(conn) < 0) {
+ worker_connection_destroy(conn);
+ return -1;
+ }
+
+ *conn_r = conn;
+ return 0;
+}
+
+bool worker_pool_get_connection(struct worker_pool *pool,
+ struct connection **conn_r)
+{
+ unsigned int max_connections;
+
+ max_connections = I_MAX(1, worker_connections_get_process_limit());
+ if (pool->connection_list->connections_count >= max_connections)
+ return FALSE;
+ if (worker_pool_add_connection(pool, conn_r) < 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+struct connection *
+worker_pool_find_username_connection(struct worker_pool *pool,
+ const char *username)
+{
+ struct connection *list;
+ const char *worker_user;
+
+ for (list = pool->connection_list->connections; list != NULL; list = list->next) {
+ worker_user = worker_connection_get_username(list);
+ if (worker_user != NULL && strcmp(worker_user, username) == 0)
+ return list;
+ }
+ return NULL;
+}
diff --git a/src/indexer/worker-pool.h b/src/indexer/worker-pool.h
new file mode 100644
index 0000000..bf739bb
--- /dev/null
+++ b/src/indexer/worker-pool.h
@@ -0,0 +1,23 @@
+#ifndef WORKER_POOL_H
+#define WORKER_POOL_H
+
+#include "indexer.h"
+#include "worker-connection.h"
+
+struct connection;
+
+struct worker_pool *
+worker_pool_init(const char *socket_path, indexer_status_callback_t *callback,
+ worker_available_callback_t *avail_callback);
+void worker_pool_deinit(struct worker_pool **pool);
+
+bool worker_pool_have_connections(struct worker_pool *pool);
+
+bool worker_pool_get_connection(struct worker_pool *pool,
+ struct connection **conn_r);
+
+struct connection *
+worker_pool_find_username_connection(struct worker_pool *pool,
+ const char *username);
+
+#endif
diff --git a/src/ipc/Makefile.am b/src/ipc/Makefile.am
new file mode 100644
index 0000000..a12609e
--- /dev/null
+++ b/src/ipc/Makefile.am
@@ -0,0 +1,25 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = ipc
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+ipc_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+ipc_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+ipc_SOURCES = \
+ client.c \
+ main.c \
+ ipc-connection.c \
+ ipc-group.c \
+ ipc-settings.c
+
+noinst_HEADERS = \
+ client.h \
+ ipc-connection.h \
+ ipc-group.h
diff --git a/src/ipc/Makefile.in b/src/ipc/Makefile.in
new file mode 100644
index 0000000..33510a1
--- /dev/null
+++ b/src/ipc/Makefile.in
@@ -0,0 +1,825 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = ipc$(EXEEXT)
+subdir = src/ipc
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_ipc_OBJECTS = client.$(OBJEXT) main.$(OBJEXT) \
+ ipc-connection.$(OBJEXT) ipc-group.$(OBJEXT) \
+ ipc-settings.$(OBJEXT)
+ipc_OBJECTS = $(am_ipc_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/client.Po \
+ ./$(DEPDIR)/ipc-connection.Po ./$(DEPDIR)/ipc-group.Po \
+ ./$(DEPDIR)/ipc-settings.Po ./$(DEPDIR)/main.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 = $(ipc_SOURCES)
+DIST_SOURCES = $(ipc_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+ipc_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+ipc_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+ipc_SOURCES = \
+ client.c \
+ main.c \
+ ipc-connection.c \
+ ipc-group.c \
+ ipc-settings.c
+
+noinst_HEADERS = \
+ client.h \
+ ipc-connection.h \
+ ipc-group.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/ipc/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/ipc/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+ipc$(EXEEXT): $(ipc_OBJECTS) $(ipc_DEPENDENCIES) $(EXTRA_ipc_DEPENDENCIES)
+ @rm -f ipc$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(ipc_OBJECTS) $(ipc_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipc-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipc-group.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipc-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/ipc-connection.Po
+ -rm -f ./$(DEPDIR)/ipc-group.Po
+ -rm -f ./$(DEPDIR)/ipc-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+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)/client.Po
+ -rm -f ./$(DEPDIR)/ipc-connection.Po
+ -rm -f ./$(DEPDIR)/ipc-group.Po
+ -rm -f ./$(DEPDIR)/ipc-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/ipc/client.c b/src/ipc/client.c
new file mode 100644
index 0000000..483452e
--- /dev/null
+++ b/src/ipc/client.c
@@ -0,0 +1,157 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "ipc-group.h"
+#include "ipc-connection.h"
+#include "client.h"
+
+#include <unistd.h>
+
+struct client {
+ struct client *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+};
+
+static struct client *clients;
+
+static void client_input(struct client *client);
+
+static void
+client_cmd_input(enum ipc_cmd_status status, const char *line, void *context)
+{
+ struct client *client = context;
+ char chr = '\0';
+
+ switch (status) {
+ case IPC_CMD_STATUS_REPLY:
+ chr = ':';
+ break;
+ case IPC_CMD_STATUS_OK:
+ chr = '+';
+ break;
+ case IPC_CMD_STATUS_ERROR:
+ chr = '-';
+ break;
+ }
+
+ T_BEGIN {
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("%c%s\n", chr, line));
+ } T_END;
+
+ if (status != IPC_CMD_STATUS_REPLY && client->io == NULL) {
+ client->io = io_add(client->fd, IO_READ, client_input, client);
+ client_input(client);
+ }
+}
+
+static void client_input(struct client *client)
+{
+ struct ipc_group *group;
+ struct ipc_connection *conn;
+ char *line, *id, *data;
+ unsigned int id_num;
+ bool ret;
+
+ while ((line = i_stream_read_next_line(client->input)) != NULL) {
+ /* <ipc name> *|<id> <command> */
+ id = strchr(line, '\t');
+ if (id == NULL)
+ data = NULL;
+ else {
+ *id++ = '\0';
+ data = strchr(id, '\t');
+ }
+ if (data == NULL || data[1] == '\0') {
+ o_stream_nsend_str(client->output, "-Invalid input\n");
+ continue;
+ }
+ *data++ = '\0';
+
+ group = ipc_group_lookup_name(line);
+
+ ret = FALSE;
+ if (strcmp(id, "*") == 0) {
+ /* send to everyone */
+ if (group == NULL) {
+ client_cmd_input(IPC_CMD_STATUS_OK, "", client);
+ } else {
+ ret = ipc_group_cmd(group, data,
+ client_cmd_input, client);
+ }
+ } else if (str_to_uint(id, &id_num) < 0) {
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("-Invalid IPC connection id: %s\n", id));
+ continue;
+ } else if (group == NULL) {
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("-Unknown IPC group: %s\n", line));
+ } else if ((conn = ipc_connection_lookup_id(group, id_num)) == NULL) {
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("-Unknown IPC connection id: %u\n", id_num));
+ continue;
+ } else {
+ ipc_connection_cmd(conn, data, client_cmd_input, client);
+ ret = TRUE;
+ }
+
+ if (ret) {
+ /* we'll handle commands one at a time. stop reading
+ input until this command is finished. */
+ io_remove(&client->io);
+ break;
+ }
+ }
+ if (client->input->eof || client->input->stream_errno != 0)
+ client_destroy(&client);
+}
+
+struct client *client_create(int fd)
+{
+ struct client *client;
+
+ client = i_new(struct client, 1);
+ client->fd = fd;
+ client->io = io_add(fd, IO_READ, client_input, client);
+ client->input = i_stream_create_fd(fd, SIZE_MAX);
+ client->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+
+ DLLIST_PREPEND(&clients, client);
+ return client;
+}
+
+void client_destroy(struct client **_client)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ DLLIST_REMOVE(&clients, client);
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ if (close(client->fd) < 0)
+ i_error("close(client) failed: %m");
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void clients_destroy_all(void)
+{
+ while (clients != NULL) {
+ struct client *client = clients;
+
+ client_destroy(&client);
+ }
+}
diff --git a/src/ipc/client.h b/src/ipc/client.h
new file mode 100644
index 0000000..a5936a6
--- /dev/null
+++ b/src/ipc/client.h
@@ -0,0 +1,9 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+struct client *client_create(int fd);
+void client_destroy(struct client **client);
+
+void clients_destroy_all(void);
+
+#endif
diff --git a/src/ipc/ipc-connection.c b/src/ipc/ipc-connection.c
new file mode 100644
index 0000000..763752a
--- /dev/null
+++ b/src/ipc/ipc-connection.c
@@ -0,0 +1,253 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "ipc-group.h"
+#include "ipc-connection.h"
+
+#include <unistd.h>
+
+#define IPC_SERVER_PROTOCOL_MAJOR_VERSION 1
+#define IPC_SERVER_PROTOCOL_MINOR_VERSION 0
+
+#define IPC_SERVER_HANDSHAKE "VERSION\tipc-proxy\t1\t0\n"
+
+static unsigned int connection_id_counter;
+
+static void ipc_connection_cmd_free(struct ipc_connection_cmd **_cmd,
+ const char *reason)
+{
+ struct ipc_connection_cmd *cmd = *_cmd;
+ struct ipc_connection_cmd **cmds;
+ unsigned int i, count;
+
+ *_cmd = NULL;
+
+ cmds = array_get_modifiable(&cmd->conn->cmds, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i] == cmd) {
+ array_delete(&cmd->conn->cmds, i, 1);
+ break;
+ }
+ }
+ if (cmd->callback != NULL) {
+ i_assert(reason != NULL);
+ cmd->callback(IPC_CMD_STATUS_ERROR, reason, cmd->context);
+ }
+ i_free(cmd);
+}
+
+static struct ipc_connection_cmd *
+ipc_connection_cmd_find(struct ipc_connection *conn, unsigned int tag)
+{
+ struct ipc_connection_cmd *cmd;
+
+ array_foreach_elem(&conn->cmds, cmd) {
+ if (cmd->tag == tag)
+ return cmd;
+ }
+ return NULL;
+}
+
+static int
+ipc_connection_input_line(struct ipc_connection *conn, char *line)
+{
+ struct ipc_connection_cmd *cmd;
+ unsigned int tag;
+ enum ipc_cmd_status status;
+ char *data;
+
+ /* <tag> [:+-]<data> */
+ data = strchr(line, '\t');
+ if (data == NULL)
+ return -1;
+
+ *data++ = '\0';
+ if (str_to_uint(line, &tag) < 0)
+ return -1;
+
+ switch (data[0]) {
+ case ':':
+ status = IPC_CMD_STATUS_REPLY;
+ break;
+ case '+':
+ status = IPC_CMD_STATUS_OK;
+ break;
+ case '-':
+ status = IPC_CMD_STATUS_ERROR;
+ break;
+ default:
+ return -1;
+ }
+ data++;
+
+ cmd = ipc_connection_cmd_find(conn, tag);
+ if (cmd == NULL) {
+ i_error("IPC server: Input for unexpected command tag %u", tag);
+ return 0;
+ }
+ cmd->callback(status, data, cmd->context);
+ if (status != IPC_CMD_STATUS_REPLY) {
+ cmd->callback = NULL;
+ ipc_connection_cmd_free(&cmd, NULL);
+ }
+ return 0;
+}
+
+static void ipc_connection_input(struct ipc_connection *conn)
+{
+ const char *const *args;
+ char *line;
+ int ret;
+
+ if (i_stream_read(conn->input) < 0) {
+ ipc_connection_destroy(&conn, FALSE,
+ i_stream_get_disconnect_reason(conn->input));
+ return;
+ }
+
+ if (!conn->version_received) {
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "ipc-server",
+ IPC_SERVER_PROTOCOL_MAJOR_VERSION)) {
+ ipc_connection_destroy(&conn, TRUE,
+ "IPC server not compatible with this server "
+ "(mixed old and new binaries?)");
+ return;
+ }
+ conn->version_received = TRUE;
+ }
+ if (!conn->handshake_received) {
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return;
+
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 3 ||
+ strcmp(args[0], "HANDSHAKE") != 0) {
+ ipc_connection_destroy(&conn, TRUE,
+ "IPC server sent invalid handshake");
+ return;
+ }
+ if (ipc_group_update_name(conn->group, args[1]) < 0) {
+ ipc_connection_destroy(&conn, TRUE, t_strdup_printf(
+ "IPC server named itself unexpectedly: %s "
+ "(existing ones were %s)",
+ args[1], conn->group->name));
+ return;
+ }
+ if (str_to_pid(args[2], &conn->pid) < 0) {
+ ipc_connection_destroy(&conn, TRUE, t_strdup_printf(
+ "IPC server gave broken PID: %s", args[2]));
+ return;
+ }
+ conn->handshake_received = TRUE;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ ret = ipc_connection_input_line(conn, line);
+ } T_END;
+ if (ret < 0) {
+ ipc_connection_destroy(&conn, TRUE, t_strdup_printf(
+ "Invalid input from IPC server '%s': %s",
+ conn->group->name, line));
+ break;
+ }
+ }
+}
+
+struct ipc_connection *ipc_connection_create(int listen_fd, int fd)
+{
+ struct ipc_connection *conn;
+
+ conn = i_new(struct ipc_connection, 1);
+ conn->group = ipc_group_lookup_listen_fd(listen_fd);
+ if (conn->group == NULL)
+ conn->group = ipc_group_alloc(listen_fd);
+ conn->id = ++connection_id_counter;
+ if (conn->id == 0)
+ conn->id = ++connection_id_counter;
+ conn->fd = fd;
+ conn->io = io_add(fd, IO_READ, ipc_connection_input, conn);
+ conn->input = i_stream_create_fd(fd, SIZE_MAX);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ i_array_init(&conn->cmds, 8);
+ o_stream_nsend_str(conn->output, IPC_SERVER_HANDSHAKE);
+
+ DLLIST_PREPEND(&conn->group->connections, conn);
+ return conn;
+}
+
+void ipc_connection_destroy(struct ipc_connection **_conn,
+ bool log_error, const char *error)
+{
+ struct ipc_connection *conn = *_conn;
+ struct ipc_connection_cmd *const *cmdp, *cmd;
+ const char *group_name = conn->group->name != NULL ?
+ conn->group->name :
+ t_strdup_printf("#%d", conn->group->listen_fd);
+
+ *_conn = NULL;
+
+ error = t_strdup_printf("IPC: '%s' PID %d server connection: %s",
+ group_name, conn->pid, error);
+ if (log_error)
+ i_error("%s", error);
+ DLLIST_REMOVE(&conn->group->connections, conn);
+
+ while (array_count(&conn->cmds) > 0) {
+ cmdp = array_front(&conn->cmds);
+ cmd = *cmdp;
+
+ ipc_connection_cmd_free(&cmd, error);
+ }
+ array_free(&conn->cmds);
+
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(ipc connection) failed: %m");
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+struct ipc_connection *
+ipc_connection_lookup_id(struct ipc_group *group, unsigned int id)
+{
+ struct ipc_connection *conn;
+
+ for (conn = group->connections; conn != NULL; conn = conn->next) {
+ if (conn->id == id)
+ return conn;
+ }
+ return NULL;
+}
+
+void ipc_connection_cmd(struct ipc_connection *conn, const char *cmd,
+ ipc_cmd_callback_t *callback, void *context)
+{
+ struct ipc_connection_cmd *ipc_cmd;
+
+ ipc_cmd = i_new(struct ipc_connection_cmd, 1);
+ ipc_cmd->tag = ++conn->cmd_tag_counter;
+ ipc_cmd->conn = conn;
+ ipc_cmd->callback = callback;
+ ipc_cmd->context = context;
+ array_push_back(&conn->cmds, &ipc_cmd);
+
+ T_BEGIN {
+ o_stream_nsend_str(conn->output,
+ t_strdup_printf("%u\t%s\n", ipc_cmd->tag, cmd));
+ } T_END;
+}
diff --git a/src/ipc/ipc-connection.h b/src/ipc/ipc-connection.h
new file mode 100644
index 0000000..d848f81
--- /dev/null
+++ b/src/ipc/ipc-connection.h
@@ -0,0 +1,46 @@
+#ifndef IPC_CONNECTION_H
+#define IPC_CONNECTION_H
+
+#include "ipc-group.h"
+
+struct ipc_connection_cmd {
+ unsigned int tag;
+ struct ipc_connection *conn;
+
+ ipc_cmd_callback_t *callback;
+ void *context;
+};
+
+struct ipc_connection {
+ struct ipc_group *group;
+ /* prev/next within group */
+ struct ipc_connection *prev, *next;
+
+ unsigned int id;
+ pid_t pid;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ unsigned int cmd_tag_counter;
+
+ /* running commands */
+ ARRAY(struct ipc_connection_cmd *) cmds;
+
+ bool version_received:1;
+ bool handshake_received:1;
+};
+
+struct ipc_connection *ipc_connection_create(int listen_fd, int fd);
+void ipc_connection_destroy(struct ipc_connection **conn,
+ bool log_error, const char *error);
+
+struct ipc_connection *
+ipc_connection_lookup_id(struct ipc_group *group, unsigned int id);
+
+void ipc_connection_cmd(struct ipc_connection *conn, const char *cmd,
+ ipc_cmd_callback_t *callback, void *context);
+
+#endif
diff --git a/src/ipc/ipc-group.c b/src/ipc/ipc-group.c
new file mode 100644
index 0000000..9451419
--- /dev/null
+++ b/src/ipc/ipc-group.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ipc-connection.h"
+#include "ipc-group.h"
+
+struct ipc_group_cmd {
+ ipc_cmd_callback_t *callback;
+ void *context;
+
+ int refcount;
+ char *first_error;
+};
+
+static ARRAY(struct ipc_group *) ipc_groups;
+
+struct ipc_group *ipc_group_alloc(int listen_fd)
+{
+ struct ipc_group *group;
+
+ i_assert(ipc_group_lookup_listen_fd(listen_fd) == NULL);
+
+ group = i_new(struct ipc_group, 1);
+ group->listen_fd = listen_fd;
+ array_push_back(&ipc_groups, &group);
+ return group;
+}
+
+void ipc_group_free(struct ipc_group **_group)
+{
+ struct ipc_group *const *groups, *group = *_group;
+ unsigned int i, count;
+
+ i_assert(group->connections == NULL);
+
+ *_group = NULL;
+ groups = array_get(&ipc_groups, &count);
+ for (i = 0; i < count; i++) {
+ if (groups[i] == group) {
+ array_delete(&ipc_groups, i, 1);
+ break;
+ }
+ }
+ i_free(group->name);
+ i_free(group);
+}
+
+struct ipc_group *ipc_group_lookup_listen_fd(int listen_fd)
+{
+ struct ipc_group *group;
+
+ array_foreach_elem(&ipc_groups, group) {
+ if (group->listen_fd == listen_fd)
+ return group;
+ }
+ return NULL;
+}
+
+struct ipc_group *ipc_group_lookup_name(const char *name)
+{
+ struct ipc_group *group;
+
+ array_foreach_elem(&ipc_groups, group) {
+ if (group->name != NULL &&
+ strcmp(group->name, name) == 0)
+ return group;
+ }
+ return NULL;
+}
+
+int ipc_group_update_name(struct ipc_group *group, const char *name)
+{
+ if (group->name == NULL)
+ group->name = i_strdup(name);
+ else if (strcmp(group->name, name) != 0)
+ return -1;
+ return 0;
+}
+
+static void ipc_group_cmd_callback(enum ipc_cmd_status status,
+ const char *line, void *context)
+{
+ struct ipc_group_cmd *group_cmd = context;
+
+ i_assert(group_cmd->refcount > 0);
+
+ switch (status) {
+ case IPC_CMD_STATUS_REPLY:
+ group_cmd->callback(IPC_CMD_STATUS_REPLY, line,
+ group_cmd->context);
+ break;
+ case IPC_CMD_STATUS_ERROR:
+ if (group_cmd->first_error == NULL)
+ group_cmd->first_error = i_strdup(line);
+ /* fall through */
+ case IPC_CMD_STATUS_OK:
+ if (--group_cmd->refcount > 0)
+ break;
+
+ if (group_cmd->first_error == NULL) {
+ group_cmd->callback(IPC_CMD_STATUS_OK, line,
+ group_cmd->context);
+ } else {
+ group_cmd->callback(IPC_CMD_STATUS_ERROR,
+ group_cmd->first_error,
+ group_cmd->context);
+ i_free(group_cmd->first_error);
+ }
+ i_free(group_cmd);
+ break;
+ }
+
+}
+
+bool ipc_group_cmd(struct ipc_group *group, const char *cmd,
+ ipc_cmd_callback_t *callback, void *context)
+{
+ struct ipc_connection *conn, *next;
+ struct ipc_group_cmd *group_cmd;
+
+ if (group->connections == NULL) {
+ callback(IPC_CMD_STATUS_OK, NULL, context);
+ return FALSE;
+ }
+
+ group_cmd = i_new(struct ipc_group_cmd, 1);
+ group_cmd->callback = callback;
+ group_cmd->context = context;
+
+ for (conn = group->connections; conn != NULL; conn = next) {
+ next = conn->next;
+
+ group_cmd->refcount++;
+ ipc_connection_cmd(conn, cmd,
+ ipc_group_cmd_callback, group_cmd);
+ }
+ return TRUE;
+}
+
+void ipc_groups_init(void)
+{
+ i_array_init(&ipc_groups, 16);
+}
+
+void ipc_groups_disconnect_all(void)
+{
+ struct ipc_group *const *groupp, *group;
+
+ while (array_count(&ipc_groups) > 0) {
+ groupp = array_front(&ipc_groups);
+ group = *groupp;
+
+ while ((*groupp)->connections != NULL) {
+ struct ipc_connection *conn = (*groupp)->connections;
+ ipc_connection_destroy(&conn, FALSE, "Shutting down");
+ }
+ ipc_group_free(&group);
+ }
+}
+
+void ipc_groups_deinit(void)
+{
+ ipc_groups_disconnect_all();
+ array_free(&ipc_groups);
+}
diff --git a/src/ipc/ipc-group.h b/src/ipc/ipc-group.h
new file mode 100644
index 0000000..c993f5a
--- /dev/null
+++ b/src/ipc/ipc-group.h
@@ -0,0 +1,45 @@
+#ifndef IPC_GROUP_H
+#define IPC_GROUP_H
+
+enum ipc_cmd_status {
+ /* Command received a reply line */
+ IPC_CMD_STATUS_REPLY,
+ /* Command finished successfully */
+ IPC_CMD_STATUS_OK,
+ /* Command finished with errors */
+ IPC_CMD_STATUS_ERROR
+};
+
+struct ipc_group {
+ int listen_fd;
+ char *name;
+
+ /* connections list also acts as a refcount */
+ struct ipc_connection *connections;
+};
+
+/* line is non-NULL only with IPC_CMD_STATUS_REPLY.
+ Each line begins with the connection ID and TAB. */
+typedef void ipc_cmd_callback_t(enum ipc_cmd_status status,
+ const char *line, void *context);
+
+struct ipc_group *ipc_group_alloc(int listen_fd);
+void ipc_group_free(struct ipc_group **group);
+
+struct ipc_group *ipc_group_lookup_listen_fd(int listen_fd);
+struct ipc_group *ipc_group_lookup_name(const char *name);
+
+/* Returns 0 if name is ok, -1 if name doesn't match the existing one. */
+int ipc_group_update_name(struct ipc_group *group, const char *name);
+
+/* Send a command to all connections in a group. All connections are expected
+ to answer something. If there are no connections, callback() is called
+ immediately and FALSE is returned. */
+bool ipc_group_cmd(struct ipc_group *group, const char *cmd,
+ ipc_cmd_callback_t *callback, void *context);
+
+void ipc_groups_init(void);
+void ipc_groups_deinit(void);
+void ipc_groups_disconnect_all(void);
+
+#endif
diff --git a/src/ipc/ipc-settings.c b/src/ipc/ipc-settings.c
new file mode 100644
index 0000000..664565c
--- /dev/null
+++ b/src/ipc/ipc-settings.c
@@ -0,0 +1,50 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings ipc_unix_listeners_array[] = {
+ { "ipc", 0600, "$default_internal_user", "" },
+ { "login/ipc-proxy", 0600, "$default_login_user", "" }
+};
+static struct file_listener_settings *ipc_unix_listeners[] = {
+ &ipc_unix_listeners_array[0],
+ &ipc_unix_listeners_array[1]
+};
+static buffer_t ipc_unix_listeners_buf = {
+ { { ipc_unix_listeners, sizeof(ipc_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings ipc_service_settings = {
+ .name = "ipc",
+ .protocol = "",
+ .type = "",
+ .executable = "ipc",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &ipc_unix_listeners_buf,
+ sizeof(ipc_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
diff --git a/src/ipc/main.c b/src/ipc/main.c
new file mode 100644
index 0000000..ebd4b22
--- /dev/null
+++ b/src/ipc/main.c
@@ -0,0 +1,73 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "ipc-group.h"
+#include "ipc-connection.h"
+#include "client.h"
+
+static bool ipc_socket_is_client(const char *name)
+{
+ size_t len;
+
+ if (strcmp(name, "ipc") == 0)
+ return TRUE;
+
+ len = strlen(name);
+ if (len > 7 && strcmp(name + len - 7, "-client") == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+
+ if (ipc_socket_is_client(conn->name))
+ (void)client_create(conn->fd);
+ else
+ (void)ipc_connection_create(conn->listen_fd, conn->fd);
+}
+
+static void ipc_die(void)
+{
+ clients_destroy_all();
+ ipc_groups_disconnect_all();
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ /* This process likely won't need to send any stats. It's also
+ problematic because it's chrooted to empty directory, so it
+ can't reconnect to stats if it gets disconnected. So at
+ least for now disable connecting to stats entirely. */
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+ const char *error;
+
+ master_service = master_service_init("ipc", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ if (master_service_settings_read_simple(master_service,
+ NULL, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+ master_service_set_die_with_master(master_service, TRUE);
+ master_service_set_die_callback(master_service, ipc_die);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+ ipc_groups_init();
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ clients_destroy_all();
+ ipc_groups_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/lda/Makefile.am b/src/lda/Makefile.am
new file mode 100644
index 0000000..eb18079
--- /dev/null
+++ b/src/lda/Makefile.am
@@ -0,0 +1,38 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = dovecot-lda
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-lda \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ $(BINARY_CFLAGS)
+
+dovecot_lda_LDFLAGS = -export-dynamic
+
+dovecot_lda_LDADD = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dovecot_lda_DEPENDENCIES = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+dovecot_lda_SOURCES = \
+ main.c
+
+install-exec-local:
+ rm -f $(DESTDIR)$(pkglibexecdir)/deliver
+ $(LN_S) dovecot-lda $(DESTDIR)$(pkglibexecdir)/deliver
diff --git a/src/lda/Makefile.in b/src/lda/Makefile.in
new file mode 100644
index 0000000..79deb2c
--- /dev/null
+++ b/src/lda/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = dovecot-lda$(EXEEXT)
+subdir = src/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 $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_dovecot_lda_OBJECTS = main.$(OBJEXT)
+dovecot_lda_OBJECTS = $(am_dovecot_lda_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+dovecot_lda_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(dovecot_lda_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)/main.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 = $(dovecot_lda_SOURCES)
+DIST_SOURCES = $(dovecot_lda_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-lda \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ $(BINARY_CFLAGS)
+
+dovecot_lda_LDFLAGS = -export-dynamic
+dovecot_lda_LDADD = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dovecot_lda_DEPENDENCIES = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+dovecot_lda_SOURCES = \
+ main.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/lda/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+dovecot-lda$(EXEEXT): $(dovecot_lda_OBJECTS) $(dovecot_lda_DEPENDENCIES) $(EXTRA_dovecot_lda_DEPENDENCIES)
+ @rm -f dovecot-lda$(EXEEXT)
+ $(AM_V_CCLD)$(dovecot_lda_LINK) $(dovecot_lda_OBJECTS) $(dovecot_lda_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/main.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-exec-local install-pkglibexecPROGRAMS
+
+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)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS
+
+.PRECIOUS: Makefile
+
+
+install-exec-local:
+ rm -f $(DESTDIR)$(pkglibexecdir)/deliver
+ $(LN_S) dovecot-lda $(DESTDIR)$(pkglibexecdir)/deliver
+
+# 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/lda/main.c b/src/lda/main.c
new file mode 100644
index 0000000..5cb1d4d
--- /dev/null
+++ b/src/lda/main.c
@@ -0,0 +1,596 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "ioloop.h"
+#include "env-util.h"
+#include "istream.h"
+#include "istream-seekable.h"
+#include "path-util.h"
+#include "safe-mkstemp.h"
+#include "eacces-error.h"
+#include "ipwd.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "unichar.h"
+#include "rfc822-parser.h"
+#include "message-address.h"
+#include "smtp-address.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "raw-storage.h"
+#include "mail-deliver.h"
+#include "mail-send.h"
+#include "mbox-from.h"
+#include "smtp-submit-settings.h"
+#include "lda-settings.h"
+
+#include <stdio.h>
+#include <sysexits.h>
+
+const struct smtp_address default_envelope_sender = {
+ .localpart = "MAILER-DAEMON",
+};
+
+/* After buffer grows larger than this, create a temporary file to /tmp
+ where to read the mail. */
+#define MAIL_MAX_MEMORY_BUFFER (1024*128)
+
+struct event_category event_category_lda = {
+ .name = "lda",
+};
+
+static const char *wanted_headers[] = {
+ "From", "To", "Message-ID", "Subject", "Return-Path",
+ NULL
+};
+
+static int seekable_fd_callback(const char **path_r, void *context)
+{
+ struct mail_deliver_input *dinput = context;
+ string_t *path;
+ int fd;
+
+ path = t_str_new(128);
+ mail_user_set_get_temp_prefix(path, dinput->rcpt_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 struct istream *
+create_raw_stream(struct mail_deliver_input *dinput,
+ int fd, time_t *mtime_r)
+{
+ struct istream *input, *input2, *input_list[2];
+ const unsigned char *data;
+ const char *error;
+ char *sender = NULL;
+ size_t i, size;
+ int ret, tz;
+
+ *mtime_r = (time_t)-1;
+ fd_set_nonblock(fd, FALSE);
+
+ input = i_stream_create_fd(fd, 4096);
+ input->blocking = TRUE;
+ /* If input begins with a From-line, drop it */
+ ret = i_stream_read_bytes(input, &data, &size, 5);
+ if (ret > 0 && memcmp(data, "From ", 5) == 0) {
+ /* skip until the first LF */
+ i_stream_skip(input, 5);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\n')
+ break;
+ }
+ if (i != size) {
+ (void)mbox_from_parse(data, i, mtime_r, &tz,
+ &sender);
+ i_stream_skip(input, i + 1);
+ break;
+ }
+ i_stream_skip(input, size);
+ }
+ }
+
+ if (sender != NULL && dinput->mail_from == NULL) {
+ struct smtp_address *mail_from = NULL;
+ /* use the envelope sender from From_-line, but only if it
+ hasn't been specified with -f already. */
+ if (smtp_address_parse_mailbox(pool_datastack_create(),
+ sender, 0, &mail_from,
+ &error) < 0) {
+ i_warning("Failed to parse address from `From_'-line: %s",
+ error);
+ }
+ dinput->mail_from = mail_from;
+ }
+ i_free(sender);
+
+ if (input->v_offset == 0) {
+ input2 = input;
+ i_stream_ref(input2);
+ } else {
+ input2 = i_stream_create_limit(input, UOFF_T_MAX);
+ }
+ i_stream_unref(&input);
+
+ input_list[0] = input2; input_list[1] = NULL;
+ input = i_stream_create_seekable(input_list, MAIL_MAX_MEMORY_BUFFER,
+ seekable_fd_callback, dinput);
+ i_stream_unref(&input2);
+ return input;
+}
+
+static struct mail *
+lda_raw_mail_open(struct mail_deliver_input *dinput, const char *path)
+{
+ struct mail_user *raw_mail_user;
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ const struct smtp_address *mail_from;
+ struct istream *input;
+ void **sets;
+ time_t mtime;
+ int ret;
+
+ sets = master_service_settings_get_others(master_service);
+ raw_mail_user = raw_storage_create_from_set(dinput->rcpt_user->set_info,
+ sets[0]);
+
+ mail_from = (dinput->mail_from != NULL ?
+ dinput->mail_from : &default_envelope_sender);
+ if (path == NULL) {
+ input = create_raw_stream(dinput, 0, &mtime);
+ i_stream_set_name(input, "stdin");
+ ret = raw_mailbox_alloc_stream(raw_mail_user, input, mtime,
+ smtp_address_encode(mail_from), &box);
+ i_stream_unref(&input);
+ } else {
+ ret = raw_mailbox_alloc_path(raw_mail_user, path, (time_t)-1,
+ smtp_address_encode(mail_from), &box);
+ }
+ if (ret < 0) {
+ i_fatal("Can't open delivery mail as raw: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ mail_user_unref(&raw_mail_user);
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ headers_ctx = mailbox_header_lookup_init(box, wanted_headers);
+ mail = mail_alloc(t, 0, headers_ctx);
+ mailbox_header_lookup_unref(&headers_ctx);
+ mail_set_seq(mail, 1);
+ return mail;
+}
+
+static void
+lda_set_rcpt_to(struct mail_deliver_input *dinput,
+ const struct smtp_address *rcpt_to, const char *user,
+ const char *rcpt_to_source)
+{
+ const char *error;
+
+ if (rcpt_to == NULL &&
+ *dinput->set->lda_original_recipient_header != '\0') {
+ rcpt_to = mail_deliver_get_address(
+ dinput->src_mail,
+ dinput->set->lda_original_recipient_header);
+ rcpt_to_source = t_strconcat(
+ dinput->set->lda_original_recipient_header,
+ " header", NULL);
+ }
+ if (rcpt_to == NULL) {
+ struct smtp_address *user_addr;
+
+ if (smtp_address_parse_username(pool_datastack_create(), user,
+ &user_addr, &error) < 0) {
+ i_fatal_status(EX_USAGE,
+ "Cannot obtain SMTP address from username `%s': %s",
+ user, error);
+ }
+ if (user_addr->domain == NULL)
+ user_addr->domain = dinput->set->hostname;
+ rcpt_to = user_addr;
+ rcpt_to_source = "user@hostname";
+ }
+
+ dinput->rcpt_params.orcpt.addr = rcpt_to;
+ if (dinput->rcpt_to == NULL)
+ dinput->rcpt_to = rcpt_to;
+
+ e_debug(dinput->rcpt_user->event,
+ "Destination address: %s (source: %s)",
+ smtp_address_encode_path(rcpt_to), rcpt_to_source);
+}
+
+static int
+lda_do_deliver(struct mail_deliver_context *ctx, bool stderr_rejection)
+{
+ enum mail_deliver_error error_code;
+ const char *error;
+ int ret;
+
+ if (mail_deliver(ctx, &error_code, &error) >= 0)
+ return EX_OK;
+
+ if (error_code == MAIL_DELIVER_ERROR_INTERNAL) {
+ /* This shouldn't happen */
+ return EX_TEMPFAIL;
+ }
+
+ if (stderr_rejection) {
+ /* write to stderr also for tempfails so that MTA
+ can log the reason if it wants to. */
+ fprintf(stderr, "%s\n", error);
+ }
+
+ switch (error_code) {
+ case MAIL_DELIVER_ERROR_NONE:
+ i_unreached();
+ case MAIL_DELIVER_ERROR_TEMPORARY:
+ return EX_TEMPFAIL;
+ case MAIL_DELIVER_ERROR_REJECTED:
+ break;
+ case MAIL_DELIVER_ERROR_NOQUOTA:
+ if (ctx->set->quota_full_tempfail)
+ return EX_TEMPFAIL;
+ ctx->mailbox_full = TRUE;
+ break;
+ case MAIL_DELIVER_ERROR_INTERNAL:
+ i_unreached();
+ }
+
+ /* Rejected */
+
+ ctx->dsn = TRUE;
+
+ /* we'll have to reply with permanent failure */
+ mail_deliver_log(ctx, "rejected: %s",
+ str_sanitize(error, 512));
+
+ if (stderr_rejection)
+ return EX_NOPERM;
+ ret = mail_send_rejection(ctx, ctx->rcpt_to, error);
+ if (ret != 0)
+ return ret < 0 ? EX_TEMPFAIL : ret;
+ /* ok, rejection sent */
+
+ return EX_OK;
+}
+
+static int
+lda_deliver(struct mail_deliver_input *dinput,
+ struct mail_storage_service_user *service_user,
+ const char *user, const char *path,
+ struct smtp_address *rcpt_to, const char *rcpt_to_source,
+ bool stderr_rejection)
+{
+ struct mail_deliver_context ctx;
+ const struct var_expand_table *var_table;
+ struct lda_settings *lda_set;
+ struct smtp_submit_settings *smtp_set;
+ const char *errstr;
+ int ret;
+
+ var_table = mail_user_var_expand_table(dinput->rcpt_user);
+ smtp_set = mail_storage_service_user_get_set(service_user)[1];
+ lda_set = mail_storage_service_user_get_set(service_user)[2];
+ ret = settings_var_expand(
+ &lda_setting_parser_info,
+ lda_set, dinput->rcpt_user->pool, var_table,
+ &errstr);
+ if (ret > 0) {
+ ret = settings_var_expand(
+ &smtp_submit_setting_parser_info,
+ smtp_set, dinput->rcpt_user->pool, var_table,
+ &errstr);
+ }
+ if (ret <= 0)
+ i_fatal("Failed to expand settings: %s", errstr);
+ dinput->set = lda_set;
+ dinput->smtp_set = smtp_set;
+
+ dinput->src_mail = lda_raw_mail_open(dinput, path);
+ lda_set_rcpt_to(dinput, rcpt_to, user, rcpt_to_source);
+
+ mail_deliver_init(&ctx, dinput);
+ ret = lda_do_deliver(&ctx, stderr_rejection);
+ mail_deliver_deinit(&ctx);
+
+ return ret;
+}
+
+static void failure_exit_callback(int *status)
+{
+ /* we want all our exit codes to be sysexits.h compatible.
+ if we failed because of a logging related error, we most likely
+ aren't writing to stderr, so try writing there to give some kind of
+ a clue what's wrong. FATAL_LOGOPEN failure already wrote to
+ stderr, so don't duplicate it. */
+ switch (*status) {
+ case FATAL_LOGWRITE:
+ fputs("Failed to write to log file", stderr);
+ break;
+ case FATAL_LOGERROR:
+ fputs("Internal logging error", stderr);
+ break;
+ case FATAL_LOGOPEN:
+ case FATAL_OUTOFMEM:
+ case FATAL_EXEC:
+ case FATAL_DEFAULT:
+ break;
+ default:
+ return;
+ }
+ *status = EX_TEMPFAIL;
+}
+
+static void print_help(void)
+{
+ printf(
+"Usage: dovecot-lda [-c <config file>] [-d <username>] [-p <path>]\n"
+" [-m <mailbox>] [-e] [-k] [-f <envelope sender>]\n"
+" [-a <original envelope recipient>]\n"
+" [-r <final envelope recipient>] \n");
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &smtp_submit_setting_parser_info,
+ &lda_setting_parser_info,
+ NULL
+ };
+ struct mail_deliver_input dinput;
+ enum mail_storage_service_flags service_flags = 0;
+ const char *user, *errstr, *path;
+ struct smtp_address *rcpt_to, *final_rcpt_to, *mail_from;
+ struct mail_storage_service_ctx *storage_service;
+ struct mail_storage_service_user *service_user;
+ struct mail_storage_service_input service_input;
+ struct event *event;
+ const char *user_source = "", *rcpt_to_source = "", *mail_from_error;
+ uid_t process_euid;
+ bool stderr_rejection = FALSE;
+ int ret, c;
+
+ if (getuid() != geteuid() && geteuid() == 0) {
+ /* running setuid - don't allow this if the binary is
+ executable by anyone */
+ struct stat st;
+
+ if (stat(argv[0], &st) < 0) {
+ fprintf(stderr, "stat(%s) failed: %s\n",
+ argv[0], strerror(errno));
+ return EX_TEMPFAIL;
+ } else if ((st.st_mode & 1) != 0 && (st.st_mode & 04000) != 0) {
+ fprintf(stderr, "%s must not be both world-executable "
+ "and setuid-root. This allows root exploits. "
+ "See http://wiki2.dovecot.org/LDA#multipleuids\n",
+ argv[0]);
+ return EX_TEMPFAIL;
+ }
+ }
+
+ i_set_failure_exit_callback(failure_exit_callback);
+
+ master_service = master_service_init("lda",
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME,
+ &argc, &argv, "a:d:ef:m:p:r:");
+
+ event = event_create(NULL);
+ event_add_category(event, &event_category_lda);
+
+ i_zero(&dinput);
+ dinput.session = mail_deliver_session_init();
+ dinput.rcpt_default_mailbox = "INBOX";
+ path = NULL;
+
+ user = getenv("USER");
+ mail_from = final_rcpt_to = rcpt_to = NULL;
+ mail_from_error = NULL;
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a':
+ /* original recipient address */
+ if (smtp_address_parse_path(
+ pool_datastack_create(), optarg,
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ &rcpt_to, &errstr) < 0) {
+ i_fatal_status(EX_USAGE,
+ "Invalid -a parameter: %s", errstr);
+ }
+ rcpt_to_source = "-a parameter";
+ break;
+ case 'd':
+ /* destination user */
+ user = optarg;
+ service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ break;
+ case 'e':
+ stderr_rejection = TRUE;
+ break;
+ case 'f':
+ /* envelope sender address */
+ ret = smtp_address_parse_path(
+ pool_datastack_create(), optarg,
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ &mail_from, &errstr);
+ if (ret < 0 && !smtp_address_is_broken(mail_from)) {
+ i_fatal_status(EX_USAGE,
+ "Invalid -f parameter: %s", errstr);
+ }
+ if (ret < 0)
+ mail_from_error = errstr;
+ break;
+ case 'm':
+ /* destination mailbox.
+ Ignore -m "". This allows doing -m ${extension}
+ in Postfix to handle user+mailbox */
+ if (*optarg != '\0') T_BEGIN {
+ if (!uni_utf8_str_is_valid(optarg)) {
+ i_fatal("Mailbox name not UTF-8: %s",
+ optarg);
+ }
+ dinput.rcpt_default_mailbox = optarg;
+ } T_END;
+ break;
+ case 'p':
+ /* input path */
+ if (t_abspath(optarg, &path, &errstr) < 0) {
+ i_fatal("t_abspath(%s) failed: %s",
+ optarg, errstr);
+ }
+ break;
+ case 'r':
+ /* final recipient address */
+ if (smtp_address_parse_path(
+ pool_datastack_create(), optarg,
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ &final_rcpt_to, &errstr) < 0) {
+ i_fatal_status(EX_USAGE,
+ "Invalid -r parameter: %s", errstr);
+ }
+ break;
+ default:
+ print_help();
+ return EX_USAGE;
+ }
+ }
+ if (optind != argc) {
+ print_help();
+ i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]);
+ }
+
+ process_euid = geteuid();
+ if ((service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0)
+ ;
+ else if (process_euid != 0) {
+ /* we're non-root. get our username and possibly our home. */
+ struct passwd pw;
+ const char *home;
+
+ home = getenv("HOME");
+ if (user != NULL && home != NULL) {
+ /* no need for a pw lookup */
+ user_source = "USER environment";
+ } else if ((ret = i_getpwuid(process_euid, &pw)) > 0) {
+ user = t_strdup(pw.pw_name);
+ if (home == NULL)
+ env_put("HOME", pw.pw_dir);
+ user_source = "passwd lookup for process euid";
+ } else if (ret < 0) {
+ /* temporary failure */
+ i_fatal("getpwuid() failed: %m");
+ } else if (user == NULL) {
+ i_fatal_status(EX_USAGE,
+ "Couldn't lookup our username (uid=%s)",
+ dec2str(process_euid));
+ }
+ } else {
+ i_fatal_status(EX_USAGE,
+ "destination user parameter (-d user) not given");
+ }
+ master_service_init_finish(master_service);
+
+ dinput.mail_from = mail_from;
+ dinput.rcpt_to = final_rcpt_to;
+
+ event_add_str(event, "protocol", "lda");
+ event_add_str(event, "user", user);
+ if (mail_from != NULL) {
+ event_add_str(event, "mail_from",
+ smtp_address_encode(mail_from));
+ }
+ if (final_rcpt_to != NULL) {
+ event_add_str(event, "rcpt_to",
+ smtp_address_encode(final_rcpt_to));
+ }
+ dinput.event_parent = event;
+
+ i_zero(&service_input);
+ service_input.module = "lda";
+ service_input.service = "lda";
+ service_input.username = user;
+ service_input.event_parent = event;
+
+ service_flags |= MAIL_STORAGE_SERVICE_FLAG_USE_SYSEXITS;
+ storage_service = mail_storage_service_init(master_service, set_roots,
+ service_flags);
+ mail_deliver_hooks_init();
+ /* set before looking up the user (or ideally we'd do this between
+ _lookup() and _next(), but don't bother) */
+ dinput.delivery_time_started = ioloop_timeval;
+ ret = mail_storage_service_lookup_next(storage_service,
+ &service_input, &service_user,
+ &dinput.rcpt_user,
+ &errstr);
+ if (ret <= 0) {
+ if (ret < 0)
+ i_fatal("%s", errstr);
+ ret = EX_NOUSER;
+ } else {
+#ifdef SIGXFSZ
+ lib_signals_ignore(SIGXFSZ, TRUE);
+#endif
+ if (*user_source != '\0') {
+ e_debug(dinput.rcpt_user->event,
+ "userdb lookup skipped, username taken from %s",
+ user_source);
+ }
+ if (mail_from_error != NULL) {
+ e_debug(event, "Broken -f parameter: %s "
+ "(proceeding with <> as sender)",
+ mail_from_error);
+ }
+
+ ret = lda_deliver(&dinput, service_user, user, path,
+ rcpt_to, rcpt_to_source, stderr_rejection);
+
+ struct mailbox_transaction_context *t =
+ dinput.src_mail->transaction;
+ struct mailbox *box = dinput.src_mail->box;
+
+ mail_free(&dinput.src_mail);
+ mailbox_transaction_rollback(&t);
+ mailbox_free(&box);
+
+ mail_user_deinit(&dinput.rcpt_user);
+ mail_storage_service_user_unref(&service_user);
+ }
+
+ mail_deliver_session_deinit(&dinput.session);
+ mail_storage_service_deinit(&storage_service);
+
+ event_unref(&event);
+ master_service_deinit(&master_service);
+ return ret;
+}
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..6803fa9
--- /dev/null
+++ b/src/lib-auth/Makefile.in
@@ -0,0 +1,877 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..eb48cec
--- /dev/null
+++ b/src/lib-auth/auth-client-request.c
@@ -0,0 +1,366 @@
+/* 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;
+}
+
+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_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");
+ }
+}
+
+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;
+ } else if (str_begins(*tmp, "event_")) {
+ const char *key = *tmp + 6;
+ const char *value = strchr(key, '=');
+ if (value != NULL) {
+ event_add_str(request->event,
+ t_strdup_until(key, value), value+1);
+ }
+ }
+ 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..d3c0402
--- /dev/null
+++ b/src/lib-charset/Makefile.in
@@ -0,0 +1,862 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..9ead33e
--- /dev/null
+++ b/src/lib-compression/Makefile.in
@@ -0,0 +1,976 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..1c8effe
--- /dev/null
+++ b/src/lib-dcrypt/Makefile.in
@@ -0,0 +1,1142 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..48a2cf4
--- /dev/null
+++ b/src/lib-dict-backend/Makefile.in
@@ -0,0 +1,1016 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..4ceee01
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql.c
@@ -0,0 +1,1566 @@
+/* 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);
+ if (ctx->ctx.set.hide_log_values)
+ sql_statement_set_no_log_expanded_values(stmt, ctx->ctx.set.hide_log_values);
+ 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..4f14eb0
--- /dev/null
+++ b/src/lib-dict-extra/Makefile.in
@@ -0,0 +1,796 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..3c55975
--- /dev/null
+++ b/src/lib-dict/Makefile.in
@@ -0,0 +1,970 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..2c688ef
--- /dev/null
+++ b/src/lib-dict/dict-client.c
@@ -0,0 +1,1508 @@
+/* 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);
+}
+
+static void client_dict_set_hide_log_values(struct dict_transaction_context *_ctx,
+ bool hide_log_values)
+{
+ struct client_dict_transaction_context *ctx =
+ container_of(_ctx, struct client_dict_transaction_context, ctx);
+ const char *query;
+
+ query = t_strdup_printf("%c%u\t%s",
+ DICT_PROTOCOL_CMD_HIDE_LOG_VALUES,
+ ctx->id,
+ hide_log_values ? "yes" : "no");
+
+ 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,
+ .set_hide_log_values = client_dict_set_hide_log_values,
+ }
+};
diff --git a/src/lib-dict/dict-client.h b/src/lib-dict/dict-client.h
new file mode 100644
index 0000000..95e2fb0
--- /dev/null
+++ b/src/lib-dict/dict-client.h
@@ -0,0 +1,48 @@
+#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> */
+ DICT_PROTOCOL_CMD_HIDE_LOG_VALUES = 'V', /* <id> <hide_log_values> */
+};
+
+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..7354175
--- /dev/null
+++ b/src/lib-dict/dict-private.h
@@ -0,0 +1,127 @@
+#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);
+ void (*set_hide_log_values)(struct dict_transaction_context *ctx,
+ bool hide_log_values);
+};
+
+struct dict_commit_callback_ctx;
+
+struct dict_op_settings_private {
+ char *username;
+ char *home_dir;
+
+ bool hide_log_values;
+};
+
+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..51eb322
--- /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)
+{
+ i_assert(reply->callback != NULL);
+ 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;
+ callback(&result, context);
+ } 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);
+ } else {
+ 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;
+
+ 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");
+ 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..bda34ae
--- /dev/null
+++ b/src/lib-dict/dict.c
@@ -0,0 +1,769 @@
+/* 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_hide_log_values(struct dict_transaction_context *ctx,
+ bool hide_log_values)
+{
+ /* Apply hide_log_values to the current transactions dict op settings */
+ ctx->set.hide_log_values = hide_log_values;
+ if (ctx->dict->v.set_hide_log_values != NULL)
+ ctx->dict->v.set_hide_log_values(ctx, hide_log_values);
+}
+
+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);
+ dest_r->hide_log_values = source->hide_log_values;
+}
+
+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..8cac9c7
--- /dev/null
+++ b/src/lib-dict/dict.h
@@ -0,0 +1,208 @@
+#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;
+
+ /* Hide values when logging about this transaction. */
+ bool hide_log_values;
+};
+
+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);
+
+/* Set hide_log_values for the transaction. Currently only
+ dict-sql with Cassandra backend does anything with this. */
+void dict_transaction_set_hide_log_values(struct dict_transaction_context *ctx,
+ bool hide_log_values);
+/* 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..4d4ca68
--- /dev/null
+++ b/src/lib-dict/test-dict-client.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "ioloop.h"
+#include "dict-private.h"
+
+#include <stdio.h>
+
+static int pending = 0;
+static volatile bool stop = FALSE;
+
+static void sig_die(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ stop = TRUE;
+}
+
+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;
+ char key[1000], value[100];
+
+ lib_init();
+ lib_signals_init();
+ ioloop = io_loop_create();
+ lib_signals_set_handler(SIGINT, LIBSIG_FLAG_RESTART, sig_die, NULL);
+ 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);
+
+ while (!stop) {
+ 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, &opset, 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_wait(dict);
+ dict_deinit(&dict);
+ dict_driver_unregister(&dict_driver_client);
+
+ io_loop_destroy(&ioloop);
+ lib_signals_deinit();
+ 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..503949f
--- /dev/null
+++ b/src/lib-dns/Makefile.in
@@ -0,0 +1,874 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..8fc46b1
--- /dev/null
+++ b/src/lib-dovecot/Makefile.in
@@ -0,0 +1,705 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..19cbe6b
--- /dev/null
+++ b/src/lib-fs/Makefile.in
@@ -0,0 +1,955 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..c53e142
--- /dev/null
+++ b/src/lib-fs/fs-api.c
@@ -0,0 +1,1412 @@
+/* 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);
+}
+
+struct event *fs_get_event(struct fs *fs)
+{
+ return fs->event;
+}
diff --git a/src/lib-fs/fs-api.h b/src/lib-fs/fs-api.h
new file mode 100644
index 0000000..f69cc79
--- /dev/null
+++ b/src/lib-fs/fs-api.h
@@ -0,0 +1,399 @@
+#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);
+/* Returns the fs's event. */
+struct event *fs_get_event(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..9f6fe91
--- /dev/null
+++ b/src/lib-fts/Makefile.in
@@ -0,0 +1,1141 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) 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-exec \
+ 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..0a3c05e
--- /dev/null
+++ b/src/lib-http/Makefile.in
@@ -0,0 +1,1310 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..ad704ea
--- /dev/null
+++ b/src/lib-http/http-server-connection.c
@@ -0,0 +1,1183 @@
+/* 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)
+ return;
+ conn->output_halted = FALSE;
+
+ if (conn->conn.output == NULL)
+ return;
+ 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..e5bf5f5
--- /dev/null
+++ b/src/lib-http/http-server-response.c
@@ -0,0 +1,809 @@
+/* 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_flush(resp->payload_output);
+ else
+ ret = o_stream_finish(resp->payload_output);
+
+ if (ret < 0)
+ http_server_connection_handle_output_error(conn);
+ else if (ret == 0)
+ http_server_connection_start_idle_timeout(conn);
+ return ret;
+}
+
+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");
+ conn->output_locked = TRUE;
+ return 0;
+ }
+ o_stream_unref(&resp->payload_output);
+ resp->payload_output = NULL;
+ }
+ if (conn->conn.output != NULL &&
+ o_stream_get_buffer_used_size(conn->conn.output) > 0) {
+ e_debug(resp->event,
+ "Not quite finished sending response");
+ conn->output_locked = TRUE;
+ return 0;
+ }
+
+ 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;
+
+ if (resp->payload_finished) {
+ e_debug(resp->event, "Finish sending payload (more)");
+ return http_server_response_finish_payload_out(resp);
+ }
+
+ i_assert(resp->payload_output != NULL);
+
+ 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..5cad093
--- /dev/null
+++ b/src/lib-http/test-http-payload.c
@@ -0,0 +1,2475 @@
+/* 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 "ostream-final-trickle.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-private.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 trickle_final_byte;
+
+ 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;
+ }
+
+ struct istream *input = i_stream_create_fd_autoclose(&fd, 40960);
+ i_stream_set_name(input, path);
+ return input;
+}
+
+/*
+ * 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);
+ if (!tset.trickle_final_byte)
+ http_server_response_submit(resp);
+ else {
+ /* close connection immediately, so ostream-delay can
+ catch bugs with too early disconnects. */
+ http_server_response_submit_close(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_http_server_connection_init(struct connection *conn)
+{
+ if (!tset.trickle_final_byte)
+ return;
+ struct ostream *output = o_stream_create_final_trickle(conn->output);
+ o_stream_unref(&conn->output);
+ conn->output = output;
+}
+
+/* */
+
+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);
+ http_server->conn_list->v.init = test_http_server_connection_init;
+}
+
+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 "
+ "(%s, %"PRIuUOFF_T":%"PRIuUOFF_T")",
+ i_stream_get_name(tcreq->file_in),
+ 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 "
+ "(%s, %"PRIuUOFF_T":%"PRIuUOFF_T")",
+ i_stream_get_name(tcreq->file_in),
+ 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();
+
+ test_begin("http payload download (server non-blocking; trickle final byte)");
+ test_init_defaults();
+ tset.trickle_final_byte = TRUE;
+ 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..d3fe475
--- /dev/null
+++ b/src/lib-imap-client/Makefile.in
@@ -0,0 +1,880 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..5806a80
--- /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, FALSE);
+
+ 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..f025403
--- /dev/null
+++ b/src/lib-imap-client/imapc-connection.c
@@ -0,0 +1,2557 @@
+/* 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;
+
+ /* If QRESYNC isn't used, this is set immediately after issuing
+ SELECT/EXAMINE. We could differentiate better whether a mailbox is
+ "being selected" vs "fully selected", but that code is already in
+ the imapc-storage side so it would have to be moved or duplicated
+ here. And since nothing actually cares about this distinction (yet),
+ don't bother with it for now. This is set to NULL when the mailbox
+ is closed from imapc-storage point of view, even if the server is
+ still in selected state (see selected_on_server). */
+ struct imapc_client_mailbox *selected_box;
+ /* If QRESYNC is used, this is set when SELECT/EXAMINE is issued.
+ If the server is already in selected state, the selected_box is most
+ likely already NULL at this point, because imapc-storage has closed
+ it. */
+ struct imapc_client_mailbox *qresync_selecting_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;
+ /* TRUE if IMAP server is in SELECTED state. select_box may be NULL
+ though, if we already closed the mailbox from client point of
+ view. */
+ bool selected_on_server: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->qresync_selecting_box = NULL;
+ conn->selected_box = NULL;
+ conn->selected_on_server = FALSE;
+ /* 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);
+ if (conn->reconnect_command_count == 0)
+ imapc_connection_abort_commands(conn, NULL,
+ reconnecting);
+ 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->qresync_selecting_box != NULL) {
+ conn->selected_box = conn->qresync_selecting_box;
+ conn->qresync_selecting_box = NULL;
+ } else {
+ conn->selected_on_server = FALSE;
+ }
+ }
+ 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, TRUE);
+ }
+
+ 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->qresync_selecting_box == NULL);
+
+ if (conn->selected_on_server &&
+ (conn->capabilities & IMAPC_CAPABILITY_QRESYNC) != 0) {
+ /* server will send a [CLOSED] once selected mailbox is
+ closed */
+ conn->qresync_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->selected_on_server = TRUE;
+ }
+ 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->qresync_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,
+ bool via_tagged_reply)
+{
+ struct imapc_connection *conn = box->conn;
+
+ if (conn->select_waiting_reply) {
+ /* Mailbox closing was requested before SELECT/EXAMINE
+ replied. The connection state is now unknown and
+ shouldn't be used anymore. */
+ imapc_connection_disconnect(conn);
+ } else if (conn->qresync_selecting_box == NULL &&
+ conn->selected_box == NULL) {
+ /* There is no mailbox selected currently. */
+ i_assert(!via_tagged_reply);
+ } else {
+ /* Mailbox was closed in a known state. Either due to
+ SELECT/EXAMINE failing (via_tagged_reply) or by
+ imapc-storage after the mailbox was already fully
+ selected. */
+ i_assert(conn->qresync_selecting_box == box ||
+ conn->selected_box == box);
+ conn->qresync_selecting_box = NULL;
+ conn->selected_box = NULL;
+ if (via_tagged_reply)
+ conn->selected_on_server = FALSE;
+ else {
+ /* We didn't actually send UNSELECT command, so don't
+ touch selected_on_server state. */
+ }
+ }
+
+ 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->qresync_selecting_box != NULL)
+ return conn->qresync_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..20c249e
--- /dev/null
+++ b/src/lib-imap-client/imapc-connection.h
@@ -0,0 +1,64 @@
+#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,
+ bool via_tagged_reply);
+
+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..7f8f26b
--- /dev/null
+++ b/src/lib-imap-storage/Makefile.in
@@ -0,0 +1,825 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..9ff6efa
--- /dev/null
+++ b/src/lib-imap-urlauth/Makefile.in
@@ -0,0 +1,835 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..f5e6f36
--- /dev/null
+++ b/src/lib-imap/Makefile.in
@@ -0,0 +1,1196 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..d4f4220
--- /dev/null
+++ b/src/lib-imap/imap-bodystructure.c
@@ -0,0 +1,972 @@
+/* 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);
+}
+
+
+static void imap_bodystructure_reset_data(struct message_part *parts)
+{
+ for (; parts != NULL; parts = parts->next) {
+ parts->data = NULL;
+ imap_bodystructure_reset_data(parts->children);
+ }
+}
+
+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);
+ }
+
+ if (ret < 0) {
+ /* Don't leave partially filled data to message_parts. Some of
+ the code expects that if the first message_part->data is
+ filled, they all are. */
+ imap_bodystructure_reset_data(*parts);
+ }
+
+ 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..736eb99
--- /dev/null
+++ b/src/lib-imap/test-imap-bodystructure.c
@@ -0,0 +1,734 @@
+/* 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(parts->data == NULL);
+ 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..86ecf93
--- /dev/null
+++ b/src/lib-index/Makefile.in
@@ -0,0 +1,1285 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..fa5874d
--- /dev/null
+++ b/src/lib-index/mail-cache-purge.c
@@ -0,0 +1,724 @@
+/* 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);
+
+ bool file_too_large =
+ output->offset > cache->index->optimization_set.cache.max_size;
+ if (!file_too_large) {
+ 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 (file_too_large || o_stream_finish(output) < 0) {
+ if (!file_too_large) {
+ errno = output->stream_errno;
+ mail_cache_set_syscall_error(cache, "write()");
+ } else {
+ /* start from a new empty cache file */
+ mail_index_set_error(cache->index,
+ "Cache file %s: File is too large - deleting",
+ cache->filepath);
+ i_unlink(cache->filepath);
+ }
+ 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) {
+ event_unref(&event);
+ return -1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ mail_cache_set_syscall_error(cache, "fstat()");
+ array_free(&ext_offsets);
+ event_unref(&event);
+ return -1;
+ }
+ if (rename(temp_path, cache->filepath) < 0) {
+ mail_cache_set_syscall_error(cache, "rename()");
+ array_free(&ext_offsets);
+ event_unref(&event);
+ 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..8c3b56d
--- /dev/null
+++ b/src/lib-index/mail-index-sync-update.c
@@ -0,0 +1,1088 @@
+/* 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 (ret == 0 &&
+ 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..c847f1c
--- /dev/null
+++ b/src/lib-index/mail-index-sync.c
@@ -0,0 +1,1068 @@
+/* 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 (log_seq < view->map->hdr.log_file_seq ||
+ ((log_seq == view->map->hdr.log_file_seq &&
+ log_offset < view->map->hdr.log_file_tail_offset))) {
+ /* invalid offsets - let the syncing handle the error */
+ return TRUE;
+ }
+ 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..7ed2984
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-append.c
@@ -0,0 +1,254 @@
+/* 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);
+ }
+
+ 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;
+ if (ctx->sync_includes_this)
+ file->max_tail_offset = file->sync_offset;
+ 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)];
+
+ offset = file->max_tail_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 */
+ offset = 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. */
+ }
+
+ 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..22d0a33
--- /dev/null
+++ b/src/lib-lda/Makefile.in
@@ -0,0 +1,878 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..88e0d16
--- /dev/null
+++ b/src/lib-ldap/Makefile.in
@@ -0,0 +1,890 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..6e7f153
--- /dev/null
+++ b/src/lib-lua/Makefile.in
@@ -0,0 +1,971 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..a0feefd
--- /dev/null
+++ b/src/lib-lua/dlua-dovecot-http.c
@@ -0,0 +1,522 @@
+/* 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-settings.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;
+ const struct master_service_settings *master_set =
+ master_service_settings_get(master_service);
+ /* need to figure out socket dir */
+ set->dns_client_socket_path = t_strconcat(master_set->base_dir, "/dns-client", NULL);
+ 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..d656695
--- /dev/null
+++ b/src/lib-mail/Makefile.in
@@ -0,0 +1,1629 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..3a96c7b
--- /dev/null
+++ b/src/lib-master/Makefile.in
@@ -0,0 +1,967 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..befeab0
--- /dev/null
+++ b/src/lib-master/master-interface.h
@@ -0,0 +1,121 @@
+#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_VERBOSE_PROCTITLE_ENV) is non-NULL if verbose_proctitle=yes.
+ This is used by lib-master during initialization. */
+#define MASTER_VERBOSE_PROCTITLE_ENV "VERBOSE_PROCTITLE"
+
+/* 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..1301e26
--- /dev/null
+++ b/src/lib-master/master-login.c
@@ -0,0 +1,598 @@
+/* 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 "process-title.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 update_proctitle:1;
+ 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);
+
+static void login_server_proctitle_refresh(struct master_login *login)
+{
+ if (!login->update_proctitle)
+ return;
+ /* This function assumes that client_limit=1. With a higher limit
+ it just returns the first client's state, which isn't too bad
+ either. */
+ if (login->conns == NULL)
+ process_title_set("[idling]");
+ else if (login->conns->clients == NULL)
+ process_title_set("[waiting on client]");
+ else if (login->conns->clients->postlogin_client == NULL)
+ process_title_set("[auth lookup]");
+ else
+ process_title_set("[post-login script]");
+}
+
+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;
+ login->update_proctitle = set->update_proctitle;
+
+ i_assert(service->login == NULL);
+ service->login = login;
+ return login;
+}
+
+void master_login_deinit(struct master_login **_login)
+{
+ struct master_login *login = *_login;
+ struct master_login_connection *conn, *next;
+
+ *_login = NULL;
+
+ i_assert(login->service->login == login);
+ login->service->login = NULL;
+
+ master_login_auth_deinit(&login->auth);
+ for (conn = login->conns; conn != NULL; conn = next) {
+ next = conn->next;
+ if (!master_login_conn_is_closed(conn)) {
+ master_login_conn_close(conn);
+ master_login_conn_unref(&conn);
+ } else {
+ /* FIXME: auth request or post-login script is still
+ running - we don't currently support aborting them */
+ i_assert(conn->clients != NULL);
+ }
+ }
+ 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;
+
+ if (login->update_proctitle)
+ process_title_set("[post-login script]");
+
+ 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;
+
+ login_server_proctitle_refresh(login);
+ 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);
+ login_server_proctitle_refresh(login);
+
+ 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);
+ login_server_proctitle_refresh(login);
+
+ /* 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;
+
+ 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);
+
+ DLLIST_REMOVE(&conn->login->conns, conn);
+ login_server_proctitle_refresh(conn->login);
+
+ 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..cb85d08
--- /dev/null
+++ b/src/lib-master/master-login.h
@@ -0,0 +1,51 @@
+#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 update_proctitle:1;
+ 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..181a83e
--- /dev/null
+++ b/src/lib-master/master-service-ssl-settings.c
@@ -0,0 +1,279 @@
+/* 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->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;
+ /* client-side CRL checking not supported currently */
+ set_r->skip_crl_check = TRUE;
+}
+
+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;
+ /* ssl_require_crl is used only for checking client-provided SSL
+ certificate's CRL. */
+ set_r->skip_crl_check = !ssl_set->ssl_require_crl;
+}
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..a7f5467
--- /dev/null
+++ b/src/lib-master/master-service.c
@@ -0,0 +1,1557 @@
+/* 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);
+ if ((flags & MASTER_SERVICE_FLAG_STANDALONE) == 0 &&
+ getenv(MASTER_VERBOSE_PROCTITLE_ENV) != NULL)
+ process_title_set("[initializing]");
+
+ /* 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");
+ }
+ /* If nothing else has updated the process title yet, it should still
+ be [initializing]. Remove it here. */
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) == 0 &&
+ process_title_get_counter() == 1 &&
+ getenv(MASTER_VERBOSE_PROCTITLE_ENV) != NULL)
+ process_title_set("");
+}
+
+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..1f5037a
--- /dev/null
+++ b/src/lib-master/stats-client.c
@@ -0,0 +1,399 @@
+/* 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 || str_len(str) >= IO_BLOCK_SIZE) {
+ 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) {
+ if (o_stream_uncork_flush(client->conn.output) < 0) {
+ e_error(client->conn.event, "write() failed: %s",
+ o_stream_get_error(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 || client->conn.output->closed)
+ 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(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(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;
+}
+
+static int stats_client_deinit_callback(struct connection *conn)
+{
+ struct ostream *output = conn->output;
+ int ret = o_stream_flush(output);
+ if (ret < 0) {
+ e_error(conn->event, "write() failed: %s",
+ o_stream_get_error(output));
+ }
+ if (ret != 0)
+ io_loop_stop(current_ioloop);
+ return ret;
+}
+
+void stats_client_deinit(struct stats_client **_client)
+{
+ struct stats_client *client = *_client;
+
+ *_client = NULL;
+
+ if (client->conn.output != NULL && !client->conn.output->closed &&
+ o_stream_get_buffer_used_size(client->conn.output) > 0) {
+ o_stream_set_flush_callback(client->conn.output,
+ stats_client_deinit_callback,
+ &client->conn);
+ o_stream_uncork(client->conn.output);
+ stats_client_wait(client);
+ }
+
+ 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..2308619
--- /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(; *lines_ref != NULL && *lines_act != NULL; 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..20fe218
--- /dev/null
+++ b/src/lib-oauth2/Makefile.in
@@ -0,0 +1,919 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..bc7779f
--- /dev/null
+++ b/src/lib-oauth2/oauth2-jwt.c
@@ -0,0 +1,525 @@
+/* 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 *alg = get_field(tree, "alg");
+ const char *kid = get_field(tree, "kid");
+
+ 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 bool check_scope(const char *req, const char *got)
+{
+ const char *const *scope_req = t_strsplit_spaces(req, " ,");
+ const char *const *scope_got = t_strsplit_spaces(got, " ,");
+
+ for (; *scope_req != NULL; scope_req++)
+ if (!str_array_icase_find(scope_got, *scope_req))
+ return FALSE;
+ return TRUE;
+}
+
+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;
+ }
+ }
+
+ const char *aud = get_field(tree, "aud");
+ /* if there is client_id configured, then aud should be present */
+ if (set->client_id != NULL && *set->client_id != '\0') {
+ if (aud == NULL) {
+ *error_r = "client_id set but aud is missing";
+ return -1;
+
+ }
+ const char *const *auds = t_strsplit_spaces(aud, " ");
+ if (!str_array_find(auds, set->client_id)) {
+ *error_r = "client_id not found in aud field";
+ return -1;
+ }
+ }
+
+ const char *got_scope = get_field(tree, "scope");
+ const char *req_scope = set->scope;
+
+ if (req_scope != NULL && *req_scope != '\0') {
+ if (got_scope == NULL) {
+ *error_r = "scope set but not found in token";
+ return -1;
+ }
+
+ if (!check_scope(req_scope, got_scope)) {
+ *error_r = t_strdup_printf("configured scope '%s' missing from token scope '%s'",
+ req_scope, got_scope);
+ 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..96def56
--- /dev/null
+++ b/src/lib-oauth2/oauth2-request.c
@@ -0,0 +1,377 @@
+/* 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_fail(struct oauth2_request *req)
+{
+ struct oauth2_request_result res = {
+ .error = "No token provided",
+ .valid = FALSE,
+ };
+ oauth2_request_callback(req, &res);
+}
+
+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)
+{
+ 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;
+
+ if (!oauth2_valid_token(input->token)) {
+ req->to_delayed_error =
+ timeout_add_short(0, oauth2_request_fail, req);
+ return req;
+ }
+
+ 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, "grant_type=refresh_token&refresh_token=");
+ http_url_escape_param(payload, input->token);
+
+ 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);
+ if (*set->client_id != '\0') {
+ str_append(enc, "&client_id=");
+ http_url_escape_param(enc, set->client_id);
+ }
+ if (*set->client_secret != '\0') {
+ 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);
+ 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);
+ if (*set->client_id != '\0') {
+ str_append(payload, "&client_id=");
+ http_url_escape_param(payload, set->client_id);
+ }
+ if (*set->client_secret != '\0') {
+ 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..021c91f
--- /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.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.e30.e30",
+ .is_jwt = FALSE
+ },
+ { /* typ field is wrong */
+ .token = "e3R5cDogamtzLCBhbGc6IEhTMjU2fQ."
+ "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
+ },
+ { /* algorithm is 'none' */
+ .token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0."
+ "eyJleHAiOjE1ODEzMzA3OTN9.",
+ .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..3f949bc
--- /dev/null
+++ b/src/lib-old-stats/Makefile.in
@@ -0,0 +1,820 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..0999098
--- /dev/null
+++ b/src/lib-otp/Makefile.in
@@ -0,0 +1,771 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..7fdc17d
--- /dev/null
+++ b/src/lib-program-client/Makefile.in
@@ -0,0 +1,912 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..8c3493a
--- /dev/null
+++ b/src/lib-sasl/Makefile.in
@@ -0,0 +1,870 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..ae5caa7
--- /dev/null
+++ b/src/lib-settings/Makefile.in
@@ -0,0 +1,870 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..c6c7c66
--- /dev/null
+++ b/src/lib-smtp/Makefile.in
@@ -0,0 +1,1356 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..e7d8964
--- /dev/null
+++ b/src/lib-sql/Makefile.in
@@ -0,0 +1,1159 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..2b86a12
--- /dev/null
+++ b/src/lib-sql/driver-cassandra.c
@@ -0,0 +1,2588 @@
+/* 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 *log_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;
+ char *log_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->log_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->log_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->log_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->log_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 *log_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->log_query = i_strdup(log_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->log_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->log_query);
+ 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, ctx->log_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,
+ sql_statement_get_log_query(&ctx->stmt->stmt),
+ 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);
+ /* When log_query is set here it can contain expanded values even
+ if stmt->no_log_expanded_values is set. */
+ ctx->log_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,
+ sql_statement_get_log_query(_stmt),
+ 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->log_query = i_strdup(sql_statement_get_log_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..2693a07
--- /dev/null
+++ b/src/lib-sql/driver-mysql.c
@@ -0,0 +1,844 @@
+/* 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 = container_of(_db, struct mysql_db, api);
+ 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 (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);
+ sql_disconnect(&db->api);
+ 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 = container_of(_db, struct mysql_db, api);
+ if (db->mysql != NULL)
+ mysql_close(db->mysql);
+}
+
+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;
+ }
+ if (db->mysql == NULL) {
+ db->mysql = p_new(db->pool, MYSQL, 1);
+ MYSQL *ptr = mysql_init(db->mysql);
+ if (ptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "mysql_init() failed");
+ }
+ 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 = container_of(_db, struct mysql_db, api);
+
+ _db->no_reconnect = TRUE;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+
+ driver_mysql_disconnect(_db);
+
+ 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 = container_of(_db, struct mysql_db, api);
+ size_t len = strlen(string);
+ char *to;
+
+ if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+ /* try connecting */
+ (void)sql_connect(&db->api);
+ }
+
+ if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+ /* 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 = container_of(_db, struct mysql_db, api);
+ 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 = container_of(_db, struct mysql_db, api);
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ 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 =
+ container_of(_result, struct mysql_result, api);
+ struct mysql_db *db = container_of(_result->db, struct mysql_db, api);
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ 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 =
+ container_of(_result, struct mysql_result, api);
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ 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 =
+ container_of(_result, struct mysql_result, api);
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ return (const char *const *)result->row;
+}
+
+static const char *driver_mysql_result_get_error(struct sql_result *_result)
+{
+ struct mysql_db *db = container_of(_result->db, struct mysql_db, api);
+ 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 =
+ container_of(_result, struct mysql_result, api);
+
+ 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 =
+ container_of(_ctx, struct mysql_transaction_context, ctx);
+ struct mysql_db *db = container_of(_ctx->db, struct mysql_db, api);
+ 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 =
+ container_of(_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 =
+ container_of(_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..63188c0
--- /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;
+
+ db->orig_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(db->orig_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..3026512
--- /dev/null
+++ b/src/lib-sql/sql-api-private.h
@@ -0,0 +1,255 @@
+#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;
+
+ /* Tell the driver to not log this query with expanded values. */
+ bool no_log_expanded_values;
+};
+
+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_log_query(struct sql_statement *stmt);
+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..cd16a5f
--- /dev/null
+++ b/src/lib-sql/sql-api.c
@@ -0,0 +1,846 @@
+/* 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_log_query(struct sql_statement *stmt)
+{
+ if (stmt->no_log_expanded_values)
+ return stmt->query_template;
+ return sql_statement_get_query(stmt);
+}
+
+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_set_no_log_expanded_values(struct sql_statement *stmt,
+ bool no_expand)
+{
+ stmt->no_log_expanded_values = no_expand;
+}
+
+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..669a851
--- /dev/null
+++ b/src/lib-sql/sql-api.h
@@ -0,0 +1,251 @@
+#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_set_no_log_expanded_values(struct sql_statement *stmt,
+ bool no_expand);
+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..3929741
--- /dev/null
+++ b/src/lib-ssl-iostream/Makefile.in
@@ -0,0 +1,970 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..9986450
--- /dev/null
+++ b/src/lib-storage/Makefile.in
@@ -0,0 +1,1509 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..09bbdfa
--- /dev/null
+++ b/src/lib-storage/index/Makefile.in
@@ -0,0 +1,1061 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..9c0eed4
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/Makefile.in
@@ -0,0 +1,843 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..3087174
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-storage.c
@@ -0,0 +1,465 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "crc32.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 time_t cleanup_interval(struct mail_user *user)
+{
+ const struct mail_storage_settings *set =
+ mail_user_set_get_storage_set(user);
+ time_t interval = set->mail_temp_scan_interval;
+
+ /* No need for a cryptographic-quality hash here. */
+ unsigned int hash = crc32_str(user->username);
+
+ /* spread from 0.00 to to 30.00% more than the base interval */
+ unsigned int spread_factor = 100000 + hash % 30001;
+ return (interval * spread_factor) / 100000;
+}
+
+static bool
+dbox_cleanup_temp_files(struct mail_user *user, const char *path,
+ time_t last_scan_time, time_t last_change_time)
+{
+ /* check once in a while if there are temp files to clean up */
+ time_t interval = cleanup_interval(user);
+ if (interval == 0) {
+ /* disabled */
+ return FALSE;
+ }
+
+ time_t deadline = ioloop_time - interval;
+ if (last_scan_time >= deadline) {
+ /* not the time to scan it yet */
+ return FALSE;
+ }
+
+ 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;
+ }
+
+ (void)unlink_old_files(path, DBOX_TEMP_FILE_PREFIX,
+ ioloop_time - DBOX_TMP_DELETE_SECS);
+ return TRUE;
+}
+
+int dbox_mailbox_check_existence(struct mailbox *box)
+{
+ const char *index_path, *box_path = mailbox_get_path(box);
+ struct stat st;
+ int ret = -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) {
+ 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)
+{
+ 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);
+ return 0;
+}
+
+int dbox_mailbox_list_cleanup(struct mail_user *user, const char *path,
+ time_t last_temp_file_scan)
+{
+ time_t change_time = -1;
+
+ if (last_temp_file_scan == 0) {
+ /* Try to fetch the scan time from the directory's atime
+ if the directory exists. In case, get also the ctime */
+ struct stat stats;
+ if (stat(path, &stats) == 0) {
+ last_temp_file_scan = stats.st_atim.tv_sec;
+ change_time = stats.st_ctim.tv_sec;
+ } else {
+ if (errno != ENOENT)
+ e_error(user->event, "stat(%s) failed: %m", path);
+ return -1;
+ }
+ }
+
+ if (dbox_cleanup_temp_files(user, path, last_temp_file_scan, change_time) ||
+ last_temp_file_scan == 0) {
+ /* temp files were scanned. update the last scan timestamp. */
+ return 1;
+ }
+ return 0;
+}
+
+void dbox_mailbox_close_cleanup(struct mailbox *box)
+{
+ if (box->view == NULL)
+ return;
+
+ const struct mail_index_header *hdr =
+ mail_index_get_header(box->view);
+ if (dbox_mailbox_list_cleanup(box->storage->user,
+ mailbox_get_path(box),
+ hdr->last_temp_file_scan) > 0)
+ index_mailbox_update_last_temp_file_scan(box);
+}
+
+void dbox_mailbox_close(struct mailbox *box)
+{
+ index_storage_mailbox_close(box);
+}
+
+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 */
+ }
+ if (dbox_mailbox_create_indexes(box, update) < 0)
+ return -1;
+ return index_mailbox_update_last_temp_file_scan(box);
+}
+
+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..8e8aaa1
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-storage.h
@@ -0,0 +1,89 @@
+#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);
+int dbox_mailbox_open(struct mailbox *box);
+void dbox_mailbox_close(struct mailbox *box);
+void dbox_mailbox_close_cleanup(struct mailbox *box);
+int dbox_mailbox_list_cleanup(struct mail_user *user, const char *path,
+ time_t last_temp_file_scan);
+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..bf482ae
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/Makefile.in
@@ -0,0 +1,866 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..f9bac0b
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-map.c
@@ -0,0 +1,1492 @@
+/* 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;
+}
+
+static void mdbox_map_deinit_cleanup(struct mdbox_map *map)
+{
+ if (map->view == NULL)
+ return;
+
+ const struct mail_index_header *hdr =
+ mail_index_get_header(map->view);
+ if (dbox_mailbox_list_cleanup(map->storage->storage.storage.user,
+ map->path, hdr->last_temp_file_scan) > 0)
+ index_mailbox_view_update_last_temp_file_scan(map->view);
+}
+
+void mdbox_map_deinit(struct mdbox_map **_map)
+{
+ struct mdbox_map *map = *_map;
+
+ *_map = NULL;
+
+ mdbox_map_deinit_cleanup(map);
+ 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 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);
+
+ 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..268f77c
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-save.c
@@ -0,0 +1,493 @@
+/* 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 mailbox *box = _ctx->transaction->box;
+ struct mail_storage *_storage = box->storage;
+ struct mdbox_storage *storage =
+ container_of(_storage, struct mdbox_storage, storage.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) {
+ if (fdatasync_path(storage->storage_dir) < 0) {
+ mailbox_set_critical(box,
+ "fdatasync_path(%s) failed: %m",
+ storage->storage_dir);
+ }
+ }
+ 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..d1f2d15
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-storage.c
@@ -0,0 +1,529 @@
+/* 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);
+
+ if (dbox_mailbox_check_existence(box) < 0)
+ return -1;
+ if (dbox_mailbox_open(box) < 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);
+
+ dbox_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..03b6f53
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/Makefile.in
@@ -0,0 +1,848 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..a25c325
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-storage.c
@@ -0,0 +1,535 @@
+/* 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;
+
+ if (dbox_mailbox_check_existence(box) < 0)
+ return -1;
+
+ if (sdbox_mailbox_alloc_index(mbox) < 0)
+ return -1;
+
+ if (dbox_mailbox_open(box) < 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);
+
+ dbox_mailbox_close_cleanup(box);
+ dbox_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..2698ea9
--- /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..4d73707
--- /dev/null
+++ b/src/lib-storage/index/imapc/Makefile.in
@@ -0,0 +1,861 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..d987538
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-list.c
@@ -0,0 +1,1013 @@
+/* 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) &&
+ (strcasecmp(vname, "INBOX") != 0 ||
+ (ctx->info.ns->flags & NAMESPACE_FLAG_INBOX_ANY) == 0)));
+
+ 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..73a38e2
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-mailbox.c
@@ -0,0 +1,1015 @@
+/* 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);
+ } else {
+ /* We don't know the latest flags, refresh them. */
+ (void)imapc_mailbox_fetch_state(mbox, 1);
+ }
+ 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 (IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox) &&
+ 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 && 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;
+ if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) {
+ /* SELECTing another mailbox - this FETCH is still for the
+ previous selected mailbox. */
+ 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;
+ if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) {
+ /* SELECTing another mailbox - this EXPUNGE is still for the
+ previous selected mailbox. */
+ 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)
+ return;
+ if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) {
+ /* SELECTing another mailbox - this SEARCH is still for the
+ previous selected mailbox. */
+ return;
+ }
+ 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;
+ if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) {
+ /* SELECTing another mailbox - this ESEARCH is still for the
+ previous selected mailbox. */
+ 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..98fa8b5
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-search.c
@@ -0,0 +1,332 @@
+/* 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;
+ case SEARCH_NIL:
+ i_unreached();
+ }
+ 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..2c9fcdf
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-storage.c
@@ -0,0 +1,1353 @@
+/* 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 this mailbox has private indexes make sure to check
+ STATUS_UNSEEN from there. */
+ if (box->list->set.index_pvt_dir != NULL &&
+ (items & (STATUS_UNSEEN)) != 0) {
+ struct mailbox_status pvt_idx_status;
+ index_storage_get_status(box, STATUS_UNSEEN,
+ &pvt_idx_status);
+ status_r->unseen = pvt_idx_status.unseen;
+ }
+ }
+
+ 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..6624a0d
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-storage.h
@@ -0,0 +1,274 @@
+#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)
+
+/* Returns TRUE if we can assume from now on that untagged EXPUNGE, FETCH, etc.
+ replies belong to this mailbox instead of to the previously selected
+ mailbox. */
+#define IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox) \
+ ((mbox)->sync_uid_validity != 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..a5a280d
--- /dev/null
+++ b/src/lib-storage/index/index-mail.c
@@ -0,0 +1,2625 @@
+/* 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)
+{
+ if (mail->data.attachment_flags_updating) {
+ /* We can get here from mail_get_parts() */
+ return;
+ }
+ mail->data.attachment_flags_updating = TRUE;
+ 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;
+ mail->data.attachment_flags_updating = FALSE;
+}
+
+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..deb2831
--- /dev/null
+++ b/src/lib-storage/index/index-mail.h
@@ -0,0 +1,293 @@
+#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;
+ bool attachment_flags_updating: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..05eaa39
--- /dev/null
+++ b/src/lib-storage/index/index-search.c
@@ -0,0 +1,1923 @@
+/* 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:
+ case SEARCH_NIL:
+ 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..f699dfb
--- /dev/null
+++ b/src/lib-storage/index/index-storage.c
@@ -0,0 +1,1296 @@
+/* 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_view_update_last_temp_file_scan(struct mail_index_view *view)
+{
+ uint32_t last_temp_file_scan = ioloop_time;
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(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);
+ return mail_index_transaction_commit(&trans);
+}
+
+int index_mailbox_update_last_temp_file_scan(struct mailbox *box)
+{
+ if (index_mailbox_view_update_last_temp_file_scan(box->view) < 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..716e725
--- /dev/null
+++ b/src/lib-storage/index/index-storage.h
@@ -0,0 +1,194 @@
+#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_view_update_last_temp_file_scan(struct mail_index_view *view);
+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..e3138b6
--- /dev/null
+++ b/src/lib-storage/index/index-transaction.c
@@ -0,0 +1,230 @@
+/* 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) {
+ } else if (ret >= 0) {
+ i_assert(t->save_ctx->dest_mail == NULL);
+ t->box->v.transaction_save_commit_post(t->save_ctx, result_r);
+ } else {
+ t->box->v.transaction_save_rollback(t->save_ctx);
+ }
+
+ 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..fdc56d1
--- /dev/null
+++ b/src/lib-storage/index/maildir/Makefile.in
@@ -0,0 +1,875 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..f6537d8
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-storage.c
@@ -0,0 +1,795 @@
+/* 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 bool maildir_has_any_subdir(const char *box_path,
+ const char **error_path)
+{
+ const char *path;
+ unsigned int i;
+ struct stat st;
+
+ for (i = 0; i < N_ELEMENTS(maildir_subdirs); i++) {
+ path = t_strconcat(box_path, "/", maildir_subdirs[i], NULL);
+ if (stat(path, &st) == 0) {
+ /* Return if there is any subdir */
+ return TRUE;
+ } else if (errno == ENOENT || errno == ENAMETOOLONG) {
+ /* Some subdirs may not exist. */
+ } else {
+ *error_path = path;
+ return FALSE;
+ }
+ }
+
+ return FALSE;
+}
+
+static bool maildir_is_selectable(const char *box_path,
+ const char *root_dir,
+ const char **error_path_r)
+{
+ struct stat st;
+
+ *error_path_r = box_path;
+ if (strcmp(box_path, root_dir) == 0)
+ return maildir_has_any_subdir(box_path, error_path_r);
+ else
+ return stat(box_path, &st) == 0;
+}
+
+static int maildir_mailbox_open(struct mailbox *box)
+{
+ const char *box_path = mailbox_get_path(box);
+ const char *root_dir;
+ 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);
+
+ /* This code path is executed only for Maildir++ and imapdir layouts,
+ which don't support \Noselect mailboxes. If the mailbox root
+ directory exists, automatically create any missing cur/new/tmp
+ directories. Otherwise the mailbox would show up as selectable
+ in the mailbox list, but not actually be selectable.
+
+ As a special case we don't do this when the mailbox root directory
+ is the same as the namespace root directory. This especially means
+ that we don't autocreate Maildir INBOX when ~/Maildir directory
+ exists. Instead, we return that mailbox doesn't exist, so the
+ caller goes to the INBOX autocreation code path similarly as with
+ other mailboxes. This is needed e.g. for welcome plugin to work. */
+ const char *error_path;
+ if (maildir_is_selectable(box_path, root_dir, &error_path)) {
+ /* 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", error_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 (ret == 0 && 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..c0b98cb
--- /dev/null
+++ b/src/lib-storage/index/mbox/Makefile.in
@@ -0,0 +1,884 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..81f62db
--- /dev/null
+++ b/src/lib-storage/index/pop3c/Makefile.in
@@ -0,0 +1,837 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..a425fce
--- /dev/null
+++ b/src/lib-storage/index/raw/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..b1aeaa5
--- /dev/null
+++ b/src/lib-storage/index/shared/Makefile.in
@@ -0,0 +1,817 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..37ec2ca
--- /dev/null
+++ b/src/lib-storage/list/Makefile.in
@@ -0,0 +1,916 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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..c55cf82
--- /dev/null
+++ b/src/lib-storage/list/mail-storage-list-index-rebuild.c
@@ -0,0 +1,615 @@
+/* Copyright (c) 2021 Dovecot Authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "guid.h"
+#include "mail-namespace.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hex-binary.h"
+#include "randgen.h"
+#include "fs-api.h"
+#include "mail-index.h"
+#include "mailbox-list-index.h"
+#include "mailbox-list-index-sync.h"
+#include "mailbox-tree.h"
+#include "mail-storage-private.h"
+#include "strfuncs.h"
+
+struct mail_storage_list_index_rebuild_mailbox {
+ guid_128_t guid;
+ const char *index_name;
+ const char *storage_name;
+ struct mailbox_list *list;
+};
+
+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;
+ HASH_TABLE(char*, 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;
+
+ 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 bool try_get_mailbox_name(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mailbox_list *list, const char *path,
+ const char **name_r)
+{
+ struct mail_index *index =
+ mail_index_alloc(ctx->storage->event, path, MAIL_INDEX_PREFIX);
+ struct mail_index_view *view;
+ uint32_t box_name_hdr_ext_id;
+ bool ret = FALSE;
+ int rc;
+ if ((rc = mail_index_open(index, MAIL_INDEX_OPEN_FLAG_READONLY)) > 0) {
+ if (mail_index_ext_lookup(index, "box-name", &box_name_hdr_ext_id)) {
+ view = mail_index_view_open(index);
+ const void *name_hdr;
+ size_t name_hdr_size;
+ mail_index_get_header_ext(view, box_name_hdr_ext_id,
+ &name_hdr, &name_hdr_size);
+ *name_r = mailbox_name_hdr_decode_storage_name(list,
+ name_hdr, name_hdr_size);
+ ret = TRUE;
+ mail_index_view_close(&view);
+ } else {
+ e_debug(ctx->storage->event,
+ "Cannot find box-name extension in mailbox index at %s", path);
+ }
+ mail_index_close(index);
+ } else if (rc == 0) {
+ e_debug(ctx->storage->event, "Cannot open mailbox index at %s: Not found", path);
+ } else if (rc < 0) {
+ e_debug(ctx->storage->event, "Cannot open mailbox index at %s: %s",
+ path, mail_index_get_error_message(index));
+ }
+ mail_index_free(&index);
+ return ret;
+}
+
+static const char *get_box_name(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mail_storage_list_index_rebuild_mailbox *box)
+{
+ const char *path =
+ t_strdup_printf("%s/%s",
+ mailbox_list_get_root_forced(box->list, MAILBOX_LIST_PATH_TYPE_MAILBOX),
+ guid_128_to_string(box->guid));
+ const char *box_name;
+ bool inbox_ns = (box->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0;
+
+ if (try_get_mailbox_name(ctx, box->list, path, &box_name)) {
+ /* special case handling */
+ if (inbox_ns && strcmp(box_name, "INBOX") == 0)
+ box_name = "INBOX";
+ e_debug(ctx->storage->event, "Found '%s' from storage %s",
+ box_name, path);
+ } else {
+ e_debug(ctx->storage->event, "Found GUID '%s' from storage %s, "
+ "but could not recover mailbox name",
+ guid_128_to_string(box->guid), path);
+ box_name = t_strdup_printf("%s%s",
+ ctx->storage->lost_mailbox_prefix,
+ guid_128_to_string(box->guid));
+ }
+ return box_name;
+}
+
+static int
+mail_storage_list_index_fill_storage_mailboxes(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mailbox_list *list)
+{
+ struct mail_storage_list_index_rebuild_mailbox *box;
+ struct fs_iter *iter;
+ const char *path, *fname, *error;
+ guid_128_t guid;
+
+ path = mailbox_list_get_root_forced(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) T_BEGIN {
+ if (guid_128_from_string(fname, guid) == 0) {
+ box = p_new(ctx->pool, struct mail_storage_list_index_rebuild_mailbox, 1);
+ guid_128_copy(box->guid, guid);
+ e_debug(ctx->storage->event,
+ "Found GUID '%s' from storage %s",
+ guid_128_to_string(guid), path);
+ char *hk = p_strdup_printf(ctx->pool, "%s%s",
+ list->ns->prefix,
+ guid_128_to_string(guid));
+ box->list = list;
+ hash_table_update(ctx->mailboxes, hk, box);
+ }
+ } T_END;
+
+ 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;
+ 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 {
+ const char *hk = t_strdup_printf("%s%s", info->ns->prefix,
+ guid_128_to_string(metadata.guid));
+ rebuild_box = hash_table_lookup(ctx->mailboxes, hk);
+ 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(metadata.guid));
+ /* Add it there so we can delete the duplicate */
+ char *hk_dup = p_strdup(ctx->pool, hk);
+ rebuild_box = p_new(ctx->pool, struct mail_storage_list_index_rebuild_mailbox, 1);
+ rebuild_box->list = info->ns->list;
+ rebuild_box->index_name = p_strdup(ctx->pool, box->name);
+ guid_128_copy(rebuild_box->guid, metadata.guid);
+ hash_table_insert(ctx->mailboxes, hk_dup, rebuild_box);
+ } 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(metadata.guid));
+ } 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,
+ struct mailbox_list *list,
+ const uint8_t *guid_p,
+ const char *boxname,
+ bool retry)
+{
+ struct mail_storage *storage = ctx->storage;
+ struct mailbox *box;
+ struct mailbox_update update;
+ string_t *name = t_str_new(128);
+ unsigned char randomness[8];
+ int ret;
+
+ i_zero(&update);
+ guid_128_copy(update.mailbox_guid, guid_p);
+
+ str_append(name, boxname);
+ 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 ((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,
+ struct mailbox_list *list,
+ const char *boxname,
+ const uint8_t *guid_p)
+{
+ int i, ret = 0;
+ /* FIXME: we should find out the mailbox's original namespace from the
+ mailbox index's header. */
+ for (i = 0; i < 100; i++) {
+ T_BEGIN {
+ ret = mail_storage_list_index_try_create(ctx, list, guid_p,
+ boxname, 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;
+}
+
+struct mailbox_sort_node {
+ struct mailbox_node node;
+ struct mail_storage_list_index_rebuild_mailbox *box;
+};
+
+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;
+ char *key ATTR_UNUSED;
+ struct mailbox_node *_node;
+ unsigned int num_created = 0;
+ char sep = mail_namespaces_get_root_sep(ctx->storage->user->namespaces);
+ int ret = 0;
+
+ iter = hash_table_iterate_init(ctx->mailboxes);
+ /* we need to sort the boxes so that they end up created in right order
+ in case we have total loss of indexes */
+
+ e_debug(ctx->storage->event, "Sorting mailbox tree");
+ struct mailbox_tree_context *tree =
+ mailbox_tree_init_size(sep, sizeof(struct mailbox_sort_node));
+ while (hash_table_iterate(iter, ctx->mailboxes, &key, &box)) {
+ bool created;
+ const char *name = box->index_name;
+ if (name == NULL)
+ name = get_box_name(ctx, box);
+ const char *vname =
+ t_strconcat(box->list->ns->prefix, name, NULL);
+ _node = mailbox_tree_get(tree, vname, &created);
+ struct mailbox_sort_node *node =
+ container_of(_node, struct mailbox_sort_node, node);
+ node->box = box;
+ }
+ hash_table_iterate_deinit(&iter);
+
+ mailbox_tree_sort(tree);
+
+ struct mailbox_tree_iterate_context *tree_iter =
+ mailbox_tree_iterate_init(tree, NULL, 0);
+ const char *box_name;
+ e_debug(ctx->storage->event, "Recovering lost mailboxes");
+ while ((_node = mailbox_tree_iterate_next(tree_iter, &box_name)) != NULL) {
+ struct mailbox_sort_node *node =
+ container_of(_node, struct mailbox_sort_node, node);
+ /* skip any intermediate levels that might get created
+ into the tree */
+ if (node->box == NULL)
+ continue;
+ /* this node needs to be created */
+ if (node->box->index_name == NULL) {
+ if (mail_storage_list_index_create(ctx, node->box->list,
+ box_name,
+ node->box->guid) < 0)
+ ret = -1;
+ else
+ num_created++;
+ }
+ }
+ mailbox_tree_iterate_deinit(&tree_iter);
+ if (num_created > 0) {
+ e_warning(ctx->storage->event,
+ "Mailbox list rescan found %u lost mailboxes",
+ num_created);
+ }
+ mailbox_tree_deinit(&tree);
+ 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;
+
+ 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_fill_storage_mailboxes(ctx, rebuild_ns->ns->list) < 0)
+ return -1;
+ 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, str_hash, strcmp);
+
+ /* 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;
+ }
+
+ /* 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);
+
+ 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..e088f3b
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-backend.c
@@ -0,0 +1,970 @@
+/* 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 and unesacped. */
+ size_t box_name_len;
+ const unsigned char *box_zerosep_name =
+ mailbox_name_hdr_encode(box->list, box->name, &box_name_len);
+
+ /* 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. */
+ const char *newname =
+ mailbox_name_hdr_decode_storage_name(
+ box->list, name_hdr, name_hdr_size);
+ 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..680808e
--- /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) T_BEGIN {
+ /* 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--;
+ } T_END;
+ 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..9c8a718
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index.c
@@ -0,0 +1,1234 @@
+/* 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;
+}
+
+const unsigned char *
+mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
+ size_t *name_len_r)
+{
+ const char sep[] = {
+ mailbox_list_get_hierarchy_sep(list),
+ '\0'
+ };
+ const char **name_parts =
+ (const char **)p_strsplit(unsafe_data_stack_pool, storage_name, sep);
+ if (list->set.storage_name_escape_char != '\0') {
+ for (unsigned int i = 0; name_parts[i] != NULL; i++) {
+ mailbox_list_name_unescape(&name_parts[i],
+ list->set.storage_name_escape_char);
+ }
+ }
+
+ string_t *str = t_str_new(64);
+ str_append(str, name_parts[0]);
+ for (unsigned int i = 1; name_parts[i] != NULL; i++) {
+ str_append_c(str, '\0');
+ str_append(str, name_parts[i]);
+ }
+ *name_len_r = str_len(str);
+ return str_data(str);
+}
+
+const char *
+mailbox_name_hdr_decode_storage_name(struct mailbox_list *list,
+ const unsigned char *name_hdr,
+ size_t name_hdr_size)
+{
+ const char list_sep = mailbox_list_get_hierarchy_sep(list);
+ const char escape_char = list->set.storage_name_escape_char;
+ string_t *storage_name = t_str_new(name_hdr_size);
+ while (name_hdr_size > 0) {
+ const unsigned char *p = memchr(name_hdr, '\0', name_hdr_size);
+ size_t name_part_len;
+ if (p == NULL) {
+ name_part_len = name_hdr_size;
+ name_hdr_size = 0;
+ } else {
+ name_part_len = p - name_hdr;
+ i_assert(name_hdr_size > name_part_len);
+ name_hdr_size -= name_part_len + 1;
+ }
+
+ if (escape_char == '\0')
+ str_append_data(storage_name, name_hdr, name_part_len);
+ else {
+ const char *name_part =
+ t_strndup(name_hdr, name_part_len);
+ str_append(storage_name,
+ mailbox_list_escape_name_params(name_part,
+ "", '\0', list_sep, escape_char,
+ list->set.maildir_name));
+ }
+
+ if (p != NULL) {
+ name_hdr += name_part_len + 1;
+ str_append_c(storage_name, list_sep);
+ }
+ }
+ return str_c(storage_name);
+}
+
+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..0109f5d
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index.h
@@ -0,0 +1,250 @@
+#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);
+
+/* Return mailbox name encoded into box-name header. */
+const unsigned char *
+mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
+ size_t *name_len_r);
+/* Return mailbox name decoded from box-name header. */
+const char *
+mailbox_name_hdr_decode_storage_name(struct mailbox_list *list,
+ const unsigned char *name_hdr,
+ size_t name_hdr_size);
+
+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..4d3d135
--- /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..23da429
--- /dev/null
+++ b/src/lib-storage/mail-search-args-cmdline.c
@@ -0,0 +1,108 @@
+/* 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;
+ case SEARCH_NIL:
+ i_unreached();
+ }
+ 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..faf0a43
--- /dev/null
+++ b/src/lib-storage/mail-search-args-imap.c
@@ -0,0 +1,329 @@
+/* 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;
+ case SEARCH_NIL:
+ str_append(dest, "NIL ");
+ 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..e6490b5
--- /dev/null
+++ b/src/lib-storage/mail-search-args-simplify.c
@@ -0,0 +1,921 @@
+/* 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;
+}
+
+/* Absorptive Law - This law enables a reduction in a complicated expression to
+ a simpler one by absorbing like terms.
+
+ A + (A.B) = (A.1) + (A.B) = A(1 + B) = A (OR Absorption Law)
+ A(A + B) = (A + 0).(A + B) = A + (0.B) = A (AND Absorption Law)
+
+ Cases with multiple shared terms (duals appy as well)
+
+ A + B + (A.C) + (B.C) = (A + (A.C)) + (B + B.C)) (apply law to sides of external sum))
+ = A + B
+ A + B + (A.B.C) = (A + (A.(B.C))) + B (X = B.C)
+ = (A + (A.(X)) + B (apply law to X)
+ = A + B
+*/
+static bool
+mail_search_args_simplify_drop_redundant_args(struct mail_search_args *all_args,
+ struct mail_search_arg **argsp,
+ bool and_arg)
+{
+ if (*argsp == NULL || (*argsp)->next == NULL)
+ return FALSE;
+
+ struct mail_search_arg *arg, **argp;
+ enum mail_search_arg_type child_subargs_type;
+ bool changed = FALSE;
+
+ ARRAY(const struct mail_search_arg *) candidates;
+ t_array_init(&candidates, 1);
+
+ child_subargs_type = and_arg ? SEARCH_OR : SEARCH_SUB;
+ for (arg = *argsp; arg != NULL; arg = arg->next) {
+ if (arg->type == child_subargs_type) {
+ const struct mail_search_arg *entry = arg->value.subargs;
+ if (entry == NULL ||
+ array_lsearch(&candidates, &entry,
+ mail_search_arg_equals_p) != NULL)
+ continue;
+ array_push_back(&candidates, &entry);
+ } else {
+ struct mail_search_arg *copy = t_new(struct mail_search_arg, 1);
+ *copy = *arg;
+ copy->next = NULL;
+ const struct mail_search_arg *entry = copy;
+ array_push_back(&candidates, &entry);
+ }
+ }
+
+ const struct mail_search_arg *candidate;
+ array_foreach_elem(&candidates, candidate) {
+ /* if there are any args that include the candidate - EXCEPT the
+ one that originally contained it - drop the arg, since it is
+ redundant. (non-SUB duplicates are dropped elsewhere.) */
+ for (argp = argsp; *argp != NULL; ) {
+ if (*argp != candidate &&
+ (*argp)->type == child_subargs_type &&
+ (*argp)->value.subargs != candidate &&
+ mail_search_args_have_all_equal(*argp, candidate)) {
+ if (all_args->init_refcount > 0)
+ mail_search_arg_one_deinit(*argp);
+ *argp = (*argp)->next;
+ changed = TRUE;
+ } else {
+ argp = &(*argp)->next;
+ }
+ }
+ }
+ return changed;
+}
+
+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_nils_removable(enum mail_search_arg_type type) {
+ switch(type) {
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS:
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ case SEARCH_GUID:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ case SEARCH_MODSEQ:
+ case SEARCH_REAL_UID:
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ case SEARCH_MIMEPART:
+ case SEARCH_SAVEDATESUPPORTED:
+ /* these want NILs to become NOT ALL */
+ return FALSE;
+
+ case SEARCH_ALL:
+ case SEARCH_NIL:
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ /* these allow to remove NILs */
+ return TRUE;
+
+ case SEARCH_INTHREAD:
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ /* these are handled in the caller as they need
+ insight on the tree under that expression */
+ i_unreached();
+
+ default:
+ i_unreached();
+ }
+}
+
+static bool
+mail_search_args_handle_nils(struct mail_search_arg **argsp, bool *remove_nils_r) {
+ /* allow_remove + deny_remove + NILs count = args count */
+ int allow_remove = 0;
+ int deny_remove = 0;
+ bool changed = FALSE;
+
+ for (struct mail_search_arg *arg = *argsp; arg != NULL; arg = arg->next) {
+
+ switch(arg->type) {
+ case SEARCH_INTHREAD:
+ case SEARCH_SUB:
+ case SEARCH_OR: {
+ bool term_remove_nils;
+ if (mail_search_args_handle_nils(
+ &arg->value.subargs, &term_remove_nils))
+ changed = TRUE;
+
+ if (arg->value.subargs == NULL)
+ arg->type = SEARCH_NIL;
+ else if (term_remove_nils)
+ allow_remove++;
+ else
+ deny_remove++;
+ break;
+ }
+
+ case SEARCH_NIL:
+ break;
+
+ default:
+ if (mail_search_args_nils_removable(arg->type))
+ allow_remove++;
+ else
+ deny_remove++;
+ }
+ }
+
+ /* The NILs can be removed if either:
+ (a) no other terms deny the removal
+ (b) or at least one other term allows the removal
+ otherwise, they are replaced with NOT ALL
+
+ [DENY, NIL] -> [DENY, NOT ALL] -- NIL replaced with NOT ALL
+ [DENY, ALLOW, NIL] -> [DENY ALLOW] -- NIL removed
+ [ALLOW, NIL] -> [ALLOW] -- NIL removed
+ [NIL] -> [] -- NIL removed */
+ bool remove_nils = deny_remove == 0 || allow_remove > 0;
+ if (remove_nils_r != NULL) *remove_nils_r = remove_nils;
+
+ while (*argsp != NULL) {
+ bool is_nil = (*argsp)->type == SEARCH_NIL;
+ if (is_nil && remove_nils) {
+ changed = TRUE;
+ *argsp = (*argsp)->next;
+ } else if (is_nil) {
+ changed = TRUE;
+ (*argsp)->type = SEARCH_ALL;
+ (*argsp)->match_not = TRUE;
+ argsp = &(*argsp)->next;
+ } else {
+ argsp = &(*argsp)->next;
+ }
+ }
+
+ return changed;
+}
+
+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)
+{
+ args->simplified = TRUE;
+
+ bool removals = mail_search_args_handle_nils(&args->args, NULL);
+ if (mail_search_args_simplify_sub(args, args->pool, &args->args, TRUE))
+ removals = 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..7df0500
--- /dev/null
+++ b/src/lib-storage/mail-search-register-imap.c
@@ -0,0 +1,644 @@
+/* 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_nil(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_new(ctx, SEARCH_NIL);
+}
+
+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 },
+ { "NIL", imap_search_nil },
+
+ /* 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..e0f595d
--- /dev/null
+++ b/src/lib-storage/mail-search.c
@@ -0,0 +1,818 @@
+/* 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;
+ case SEARCH_NIL:
+ i_unreached();
+ }
+ 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_NIL:
+ return TRUE;
+ 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;
+}
+
+int mail_search_arg_equals_p(const struct mail_search_arg *const *arg1,
+ const struct mail_search_arg *const *arg2)
+{
+ if (arg1 == NULL && arg2 == NULL) return 0;
+ if (arg1 == NULL) return 1;
+ return mail_search_arg_equals(*arg1, *arg2) ? 0 : 1;
+}
+
+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..46bcbcb
--- /dev/null
+++ b/src/lib-storage/mail-search.h
@@ -0,0 +1,278 @@
+#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,
+
+ /* This term is allowed only in SEARCH_OR and SEARCH_SUB sublists.
+ When it is encountered during the simplification, it must be removed */
+ SEARCH_NIL,
+};
+
+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);
+int mail_search_arg_equals_p(const struct mail_search_arg *const *arg1,
+ const struct mail_search_arg *const *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..dd31cbc
--- /dev/null
+++ b/src/lib-storage/mail-storage-service.c
@@ -0,0 +1,1811 @@
+/* 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);
+
+ mail_user_add_event_fields(mail_user);
+
+ 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->service_ctx->log_initialized && 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->service_ctx->log_initialized && 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 = NULL;
+ 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 (input->unexpanded_set_parser != NULL)
+ set_parser = input->unexpanded_set_parser;
+ else 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..29a6f7d
--- /dev/null
+++ b/src/lib-storage/mail-storage-service.h
@@ -0,0 +1,190 @@
+#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;
+
+ /* Use this settings parser instead of looking it up. */
+ struct setting_parser_context *unexpanded_set_parser;
+
+ /* 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..1abf5d4
--- /dev/null
+++ b/src/lib-storage/mail-storage.h
@@ -0,0 +1,1030 @@
+#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);
+/* Try to retrieve a properly parsed Message-ID header, if that fails return
+ the raw header value as it is. */
+int mail_get_message_id_no_validation(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..1382e71
--- /dev/null
+++ b/src/lib-storage/mail-user.c
@@ -0,0 +1,865 @@
+/* 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)
+{
+}
+
+void mail_user_add_event_fields(struct mail_user *user)
+{
+ if (user->userdb_fields == NULL)
+ return;
+ for (unsigned int i = 0; user->userdb_fields[i] != NULL; i++) {
+ const char *field = user->userdb_fields[i];
+ if (!str_begins(field, "event_"))
+ continue;
+ const char *key = field + 6;
+ const char *value = strchr(key, '=');
+ if (value != NULL) {
+ event_add_str(user->event, t_strdup_until(key, value),
+ value+1);
+ }
+ }
+}
+
+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..166c425
--- /dev/null
+++ b/src/lib-storage/mail-user.h
@@ -0,0 +1,262 @@
+#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);
+
+/* Import any event_ fields from userdb fields to mail user event. */
+void mail_user_add_event_fields(struct mail_user *user);
+
+/* 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..0054ba8
--- /dev/null
+++ b/src/lib-storage/mail.c
@@ -0,0 +1,713 @@
+/* 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);
+}
+
+static int mail_get_message_id_full(struct mail *mail,
+ const char **value_r,
+ bool require_valid)
+{
+ const char *hdr_value, *msgid_bare;
+ int ret;
+
+ ret = mail_get_first_header(mail, "Message-ID", &hdr_value);
+ if (ret <= 0) {
+ *value_r = NULL;
+ return ret;
+ }
+
+ *value_r = hdr_value; /* save it now as next function alters it */
+ msgid_bare = message_id_get_next(&hdr_value);
+
+ if (msgid_bare != NULL) {
+ /* Complete the message ID with surrounding `<' and `>'. */
+ *value_r = t_strconcat("<", msgid_bare, ">", NULL);
+ return 1;
+ } else if (!require_valid) {
+ /* *value_r already set above */
+ return 1;
+ } else {
+ *value_r = NULL;
+ return 0;
+ }
+}
+
+int mail_get_message_id(struct mail *mail, const char **value_r)
+{
+ return mail_get_message_id_full(mail, value_r, TRUE/*require_valid*/);
+}
+
+int mail_get_message_id_no_validation(struct mail *mail, const char **value_r)
+{
+ return mail_get_message_id_full(mail, value_r, FALSE/*require_valid*/);
+}
+
+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..f27df7f
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute.c
@@ -0,0 +1,587 @@
+/* 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;
+
+ if (key[0] == '\0') {
+ /* never exists, and may cause dict lookup errors */
+ return 0;
+ }
+
+ 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..a480392
--- /dev/null
+++ b/src/lib-storage/test-mail-search-args-simplify.c
@@ -0,0 +1,416 @@
+/* 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" },
+
+ /* extra for simplifications on 2nd and later terms */
+ { "( OR BODY z BODY x ) BODY x BODY y", "BODY x BODY y" },
+ { "( OR BODY x BODY y ) ( OR BODY x BODY z )", "OR (BODY y BODY z) BODY x" },
+ { "( OR BODY z BODY x ) BODY x BODY y", "BODY x BODY y" },
+ { "( OR BODY z NOT BODY x ) BODY x BODY y", "OR BODY z NOT BODY x BODY x BODY y" },
+
+ { "( OR BODY z BODY y ) BODY x BODY y", "BODY x BODY y" },
+ { "( OR BODY z BODY y ) ( OR BODY z BODY w ) BODY x BODY y BODY w", "BODY x BODY y BODY w" },
+
+ { "subject y", "SUBJECT y"},
+
+ /* NIL cases */
+ { "NIL", ""},
+ { "NOT NIL", ""},
+ { "( ( NIL ) )", ""},
+ { "( ( NOT NIL ) )", ""},
+
+ { "NIL NIL", "" },
+ { "( ( NIL NIL ) )", "" },
+ { "OR NIL NIL", "" },
+
+ { "OR ( NIL NIL ) OR NIL NIL", "" },
+ { "( NIL NIL ) OR NIL NIL", "" },
+
+ { "NIL LARGER 0", "NOT ALL"},
+ { "NOT NIL LARGER 0", "NOT ALL"},
+ { "NIL NOT LARGER 0", "NOT ALL"},
+ { "NOT NIL NOT LARGER 0", "NOT ALL"},
+
+ { "( NIL NIL ) TEXT uno", "TEXT uno" },
+ { "OR NIL NIL TEXT uno", "TEXT uno" },
+
+ { "TEXT foo NIL", "TEXT foo"},
+ { "NIL TEXT foo", "TEXT foo"},
+
+ { "NOT ( TEXT foo NIL )", "NOT TEXT foo"},
+ { "NOT ( NIL TEXT foo )", "NOT TEXT foo"},
+
+ { "TEXT foo NOT NIL", "TEXT foo"},
+ { "NOT NIL TEXT foo", "TEXT foo"},
+
+ { "NOT TEXT foo NIL", "NOT TEXT foo"},
+ { "NIL NOT TEXT foo", "NOT TEXT foo"},
+
+ { "OR TEXT foo NIL", "TEXT foo"},
+ { "OR NIL TEXT foo", "TEXT foo"},
+
+ { "NOT OR TEXT foo NIL", "NOT TEXT foo"},
+ { "NOT OR NIL TEXT foo", "NOT TEXT foo"},
+
+ { "OR TEXT foo NOT NIL", "TEXT foo"},
+ { "OR NOT NIL TEXT foo", "TEXT foo"},
+
+ { "OR NOT TEXT foo NIL", "NOT TEXT foo"},
+ { "OR NIL NOT TEXT foo", "NOT TEXT foo"},
+
+ // avec & and & uno & due
+ // -> {en}(avec & NIL & uno & due) | {fr}(NIL & and & uno & (du | due)
+ { "OR ( TEXT avec NIL TEXT uno TEXT due ) ( NIL TEXT and TEXT uno OR TEXT du TEXT due )",
+ "OR (TEXT avec TEXT due) (TEXT and OR TEXT du TEXT due) TEXT uno" },
+
+ // avec | and | uno | due
+ // -> {en}(avec | NIL | uno | due) | {fr}(NIL | and | uno | (du | due)
+ { "OR ( OR TEXT avec OR NIL OR TEXT uno TEXT due ) ( OR NIL OR TEXT and OR TEXT uno OR TEXT du TEXT due )",
+ "OR TEXT avec OR TEXT uno OR TEXT due OR TEXT and TEXT du" },
+
+ // (avec | and) & (uno | due)
+ // -> {en}((avec | NIL) & (uno | due)) | {fr}((NIL | and) & (uno | (du | due)))
+ { "OR ( OR TEXT avec NIL OR TEXT uno TEXT due ) ( OR NIL TEXT and OR TEXT uno ( OR TEXT du TEXT due ) )",
+ "OR (TEXT avec OR TEXT uno TEXT due) (TEXT and OR TEXT uno OR TEXT du TEXT due)" },
+
+ // (avec | uno) & (and | due)
+ // -> {en}((avec | uno) & (NIL | due)) | {fr}((NIL | uno) & (and | (du | due)))
+ { "OR ( OR TEXT avec TEXT uno OR NIL TEXT due ) ( OR NIL TEXT uno OR TEXT and OR TEXT du TEXT due )",
+ "OR (OR TEXT avec TEXT uno TEXT due) (TEXT uno OR TEXT and OR TEXT du TEXT due)"},
+
+ // (uno | due) & ( tre | NIL)
+ { "OR TEXT uno TEXT due OR TEXT tre NIL",
+ "OR TEXT uno TEXT due TEXT tre" },
+
+ // (uno & due) | ( tre & NIL)
+ { "OR ( TEXT uno TEXT due ) ( TEXT tre NIL )",
+ "OR (TEXT uno TEXT due) TEXT tre" },
+
+ // (uno | due) & ( uno | NIL)
+ { "OR TEXT uno TEXT due OR TEXT uno NIL",
+ "TEXT uno" },
+
+ // (uno & due) | ( uno & NIL)
+ { "OR ( TEXT uno TEXT due ) ( TEXT uno NIL )",
+ "TEXT uno" },
+};
+
+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..cb6c66e
--- /dev/null
+++ b/src/lib-test/Makefile.am
@@ -0,0 +1,22 @@
+noinst_LTLIBRARIES = libtest.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-charset
+
+libtest_la_SOURCES = \
+ fuzzer.c \
+ ostream-final-trickle.c \
+ test-common.c \
+ test-istream.c \
+ test-ostream.c \
+ test-subprocess.c
+
+headers = \
+ fuzzer.h \
+ ostream-final-trickle.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..b515d58
--- /dev/null
+++ b/src/lib-test/Makefile.in
@@ -0,0 +1,836 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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 ostream-final-trickle.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)/ostream-final-trickle.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 \
+ ostream-final-trickle.c \
+ test-common.c \
+ test-istream.c \
+ test-ostream.c \
+ test-subprocess.c
+
+headers = \
+ fuzzer.h \
+ ostream-final-trickle.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)/ostream-final-trickle.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)/ostream-final-trickle.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)/ostream-final-trickle.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/ostream-final-trickle.c b/src/lib-test/ostream-final-trickle.c
new file mode 100644
index 0000000..fc8a00f
--- /dev/null
+++ b/src/lib-test/ostream-final-trickle.c
@@ -0,0 +1,148 @@
+/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream-private.h"
+#include "ostream-final-trickle.h"
+
+struct final_trickle_ostream {
+ struct ostream_private ostream;
+ struct timeout *to;
+
+ unsigned char buffer_char;
+ bool buffer_used;
+};
+
+static void
+o_stream_final_trickle_close(struct iostream_private *stream, bool close_parent)
+{
+ struct final_trickle_ostream *dstream =
+ container_of(stream, struct final_trickle_ostream,
+ ostream.iostream);
+
+ timeout_remove(&dstream->to);
+ if (close_parent)
+ o_stream_close(dstream->ostream.parent);
+}
+
+static int
+o_stream_final_trickle_flush_buffer(struct final_trickle_ostream *dstream)
+{
+ int ret = 1;
+
+ if (dstream->buffer_used) {
+ if ((ret = o_stream_send(dstream->ostream.parent,
+ &dstream->buffer_char, 1)) < 0)
+ o_stream_copy_error_from_parent(&dstream->ostream);
+ else if (ret > 0)
+ dstream->buffer_used = FALSE;
+ if (ret != 0)
+ timeout_remove(&dstream->to);
+ }
+ return ret;
+}
+
+static void
+o_stream_final_trickle_timeout(struct final_trickle_ostream *dstream)
+{
+ i_assert(dstream->buffer_used);
+
+ (void)o_stream_final_trickle_flush_buffer(dstream);
+}
+
+static int o_stream_final_trickle_flush(struct ostream_private *stream)
+{
+ struct final_trickle_ostream *dstream =
+ container_of(stream, struct final_trickle_ostream, ostream);
+
+ if (dstream->buffer_used)
+ return 0;
+ return o_stream_flush_parent(stream);
+}
+
+static ssize_t
+o_stream_final_trickle_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct final_trickle_ostream *dstream =
+ container_of(stream, struct final_trickle_ostream, ostream);
+ ssize_t ret;
+
+ if ((ret = o_stream_final_trickle_flush_buffer(dstream)) <= 0)
+ return ret;
+ i_assert(!dstream->buffer_used);
+
+ /* send all but the last byte */
+ struct const_iovec iov_copy[iov_count];
+ memcpy(iov_copy, iov, iov_count * sizeof(*iov));
+ struct const_iovec *last_iov = &iov_copy[iov_count-1];
+
+ i_assert(last_iov->iov_len > 0);
+ last_iov->iov_len--;
+ const unsigned char *last_iov_data = last_iov->iov_base;
+ dstream->buffer_char = last_iov_data[last_iov->iov_len];
+ dstream->buffer_used = TRUE;
+ if (dstream->to == NULL) {
+ dstream->to = timeout_add_short(0,
+ o_stream_final_trickle_timeout, dstream);
+ }
+ if (last_iov->iov_len == 0)
+ iov_count--;
+
+ if (iov_count > 0) {
+ size_t full_size = 0;
+ for (unsigned int i = 0; i < iov_count; i++)
+ full_size += iov_copy[i].iov_len;
+ if ((ret = o_stream_sendv(stream->parent, iov_copy, iov_count)) < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ if ((size_t)ret < full_size) {
+ dstream->buffer_used = FALSE;
+ timeout_remove(&dstream->to);
+ }
+ }
+ if (dstream->buffer_used)
+ ret++;
+
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+static size_t
+o_stream_final_trickle_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct final_trickle_ostream *dstream =
+ container_of(stream, const struct final_trickle_ostream, ostream);
+
+ return (dstream->buffer_used ? 1 : 0) +
+ o_stream_get_buffer_used_size(stream->parent);
+}
+
+static void
+o_stream_final_trickle_switch_ioloop_to(struct ostream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct final_trickle_ostream *dstream =
+ container_of(stream, struct final_trickle_ostream, ostream);
+
+ if (dstream->to != NULL)
+ dstream->to = io_loop_move_timeout_to(ioloop, &dstream->to);
+ if (stream->parent != NULL)
+ o_stream_switch_ioloop_to(stream->parent, ioloop);
+}
+
+struct ostream *o_stream_create_final_trickle(struct ostream *output)
+{
+ struct final_trickle_ostream *dstream;
+
+ dstream = i_new(struct final_trickle_ostream, 1);
+ dstream->ostream.iostream.close = o_stream_final_trickle_close;
+ dstream->ostream.sendv = o_stream_final_trickle_sendv;
+ dstream->ostream.flush = o_stream_final_trickle_flush;
+ dstream->ostream.get_buffer_used_size = o_stream_final_trickle_get_buffer_used_size;
+ dstream->ostream.switch_ioloop_to = o_stream_final_trickle_switch_ioloop_to;
+
+ return o_stream_create(&dstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib-test/ostream-final-trickle.h b/src/lib-test/ostream-final-trickle.h
new file mode 100644
index 0000000..c97faf9
--- /dev/null
+++ b/src/lib-test/ostream-final-trickle.h
@@ -0,0 +1,9 @@
+#ifndef OSTREAM_FINAL_TRICKLE_H
+#define OSTREAM_FINAL_TRICKLE_H
+
+/* Creates a wrapper istream that delays sending the final byte until the next
+ ioloop run. This can catch bugs where caller doesn't expect the final bytes
+ to be delayed. */
+struct ostream *o_stream_create_final_trickle(struct ostream *output);
+
+#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..1773595
--- /dev/null
+++ b/src/lib/Makefile.in
@@ -0,0 +1,3711 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) 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-exec \
+ 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..f154f9e
--- /dev/null
+++ b/src/lib/connection.c
@@ -0,0 +1,962 @@
+/* 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);
+ if (conn->v.init != NULL)
+ conn->v.init(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);
+
+ if (conn->v.init != NULL)
+ conn->v.init(conn);
+ 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);
+
+ if (conn->v.init != NULL)
+ conn->v.init(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);
+
+ if (conn->v.init != NULL)
+ conn->v.init(conn);
+}
+
+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..612c540
--- /dev/null
+++ b/src/lib/connection.h
@@ -0,0 +1,260 @@
+#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 (*init)(struct connection *conn);
+ 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..ba5dc39
--- /dev/null
+++ b/src/lib/event-filter-lexer.c
@@ -0,0 +1,2297 @@
+#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);
+
+#pragma GCC diagnostic push
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+# pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
+/* ignore sign comparison errors (buggy flex) */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma GCC diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
+#line 695 "event-filter-lexer.c"
+
+#line 697 "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 44 "event-filter-lexer.l"
+
+#line 46 "event-filter-lexer.l"
+ string_t *str_buf = NULL;
+
+#line 975 "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 48 "event-filter-lexer.l"
+{
+ BEGIN(string);
+
+ str_buf = t_str_new(128);
+ }
+ YY_BREAK
+case 2:
+YY_RULE_SETUP
+#line 53 "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 59 "event-filter-lexer.l"
+{ str_append(str_buf, yytext); }
+ YY_BREAK
+case 4:
+YY_RULE_SETUP
+#line 60 "event-filter-lexer.l"
+{ str_append_c(str_buf, '\\'); }
+ YY_BREAK
+case 5:
+YY_RULE_SETUP
+#line 61 "event-filter-lexer.l"
+{ str_append_c(str_buf, '"'); }
+ YY_BREAK
+case 6:
+YY_RULE_SETUP
+#line 62 "event-filter-lexer.l"
+{ str_append(str_buf, yytext); }
+ YY_BREAK
+case 7:
+YY_RULE_SETUP
+#line 64 "event-filter-lexer.l"
+{ return AND; }
+ YY_BREAK
+case 8:
+YY_RULE_SETUP
+#line 65 "event-filter-lexer.l"
+{ return OR; }
+ YY_BREAK
+case 9:
+YY_RULE_SETUP
+#line 66 "event-filter-lexer.l"
+{ return NOT; }
+ YY_BREAK
+case 10:
+YY_RULE_SETUP
+#line 67 "event-filter-lexer.l"
+{ return *yytext; }
+ YY_BREAK
+case 11:
+YY_RULE_SETUP
+#line 68 "event-filter-lexer.l"
+{ yylval->str = t_strdup(yytext); return TOKEN; }
+ YY_BREAK
+case 12:
+/* rule 12 can match eol */
+YY_RULE_SETUP
+#line 69 "event-filter-lexer.l"
+{ /* ignore */ }
+ YY_BREAK
+case 13:
+YY_RULE_SETUP
+#line 70 "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 96 "event-filter-lexer.l"
+ECHO;
+ YY_BREAK
+#line 1134 "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 96 "event-filter-lexer.l"
+
+
+#pragma GCC diagnostic pop
+
+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..8df4ddc
--- /dev/null
+++ b/src/lib/event-filter-lexer.l
@@ -0,0 +1,141 @@
+/* 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);
+
+#pragma GCC diagnostic push
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+# pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
+/* ignore sign comparison errors (buggy flex) */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma GCC diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
+%}
+
+%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;
+ }
+%%
+
+#pragma GCC diagnostic pop
+
+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..9689617
--- /dev/null
+++ b/src/lib/event-filter-parser.c
@@ -0,0 +1,1852 @@
+/* A Bison parser, made by GNU Bison 3.7.5. */
+
+/* Bison implementation for Yacc-like parsers in C
+
+ Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2021 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. */
+
+/* DO NOT RELY ON FEATURES THAT ARE NOT DOCUMENTED in the manual,
+ especially those whose name start with YY_ or yy_. They are
+ private implementation details that can be changed or removed. */
+
+/* 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. */
+
+/* Identify Bison output, and Bison version. */
+#define YYBISON 30705
+
+/* Bison version string. */
+#define YYBISON_VERSION "3.7.5"
+
+/* 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"
+
+#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;
+}
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+# pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
+
+
+#line 205 "event-filter-parser.c"
+
+# ifndef YY_CAST
+# ifdef __cplusplus
+# define YY_CAST(Type, Val) static_cast<Type> (Val)
+# define YY_REINTERPRET_CAST(Type, Val) reinterpret_cast<Type> (Val)
+# else
+# define YY_CAST(Type, Val) ((Type) (Val))
+# define YY_REINTERPRET_CAST(Type, Val) ((Type) (Val))
+# endif
+# endif
+# 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
+
+#include "event-filter-parser.h"
+/* Symbol kind. */
+enum yysymbol_kind_t
+{
+ YYSYMBOL_YYEMPTY = -2,
+ YYSYMBOL_YYEOF = 0, /* "end of file" */
+ YYSYMBOL_YYerror = 1, /* error */
+ YYSYMBOL_YYUNDEF = 2, /* "invalid token" */
+ YYSYMBOL_TOKEN = 3, /* TOKEN */
+ YYSYMBOL_STRING = 4, /* STRING */
+ YYSYMBOL_AND = 5, /* AND */
+ YYSYMBOL_OR = 6, /* OR */
+ YYSYMBOL_NOT = 7, /* NOT */
+ YYSYMBOL_8_ = 8, /* '(' */
+ YYSYMBOL_9_ = 9, /* ')' */
+ YYSYMBOL_10_ = 10, /* '=' */
+ YYSYMBOL_11_ = 11, /* '>' */
+ YYSYMBOL_12_ = 12, /* '<' */
+ YYSYMBOL_YYACCEPT = 13, /* $accept */
+ YYSYMBOL_filter = 14, /* filter */
+ YYSYMBOL_expr = 15, /* expr */
+ YYSYMBOL_key_value = 16, /* key_value */
+ YYSYMBOL_key = 17, /* key */
+ YYSYMBOL_value = 18, /* value */
+ YYSYMBOL_op = 19 /* op */
+};
+typedef enum yysymbol_kind_t yysymbol_kind_t;
+
+
+
+
+#ifdef short
+# undef short
+#endif
+
+/* On compilers that do not define __PTRDIFF_MAX__ etc., make sure
+ <limits.h> and (if available) <stdint.h> are included
+ so that the code can choose integer types of a good width. */
+
+#ifndef __PTRDIFF_MAX__
+# include <limits.h> /* INFRINGES ON USER NAME SPACE */
+# if defined __STDC_VERSION__ && 199901 <= __STDC_VERSION__
+# include <stdint.h> /* INFRINGES ON USER NAME SPACE */
+# define YY_STDINT_H
+# endif
+#endif
+
+/* Narrow types that promote to a signed type and that can represent a
+ signed or unsigned integer of at least N bits. In tables they can
+ save space and decrease cache pressure. Promoting to a signed type
+ helps avoid bugs in integer arithmetic. */
+
+#ifdef __INT_LEAST8_MAX__
+typedef __INT_LEAST8_TYPE__ yytype_int8;
+#elif defined YY_STDINT_H
+typedef int_least8_t yytype_int8;
+#else
+typedef signed char yytype_int8;
+#endif
+
+#ifdef __INT_LEAST16_MAX__
+typedef __INT_LEAST16_TYPE__ yytype_int16;
+#elif defined YY_STDINT_H
+typedef int_least16_t yytype_int16;
+#else
+typedef short yytype_int16;
+#endif
+
+/* Work around bug in HP-UX 11.23, which defines these macros
+ incorrectly for preprocessor constants. This workaround can likely
+ be removed in 2023, as HPE has promised support for HP-UX 11.23
+ (aka HP-UX 11i v2) only through the end of 2022; see Table 2 of
+ <https://h20195.www2.hpe.com/V2/getpdf.aspx/4AA4-7673ENW.pdf>. */
+#ifdef __hpux
+# undef UINT_LEAST8_MAX
+# undef UINT_LEAST16_MAX
+# define UINT_LEAST8_MAX 255
+# define UINT_LEAST16_MAX 65535
+#endif
+
+#if defined __UINT_LEAST8_MAX__ && __UINT_LEAST8_MAX__ <= __INT_MAX__
+typedef __UINT_LEAST8_TYPE__ yytype_uint8;
+#elif (!defined __UINT_LEAST8_MAX__ && defined YY_STDINT_H \
+ && UINT_LEAST8_MAX <= INT_MAX)
+typedef uint_least8_t yytype_uint8;
+#elif !defined __UINT_LEAST8_MAX__ && UCHAR_MAX <= INT_MAX
+typedef unsigned char yytype_uint8;
+#else
+typedef short yytype_uint8;
+#endif
+
+#if defined __UINT_LEAST16_MAX__ && __UINT_LEAST16_MAX__ <= __INT_MAX__
+typedef __UINT_LEAST16_TYPE__ yytype_uint16;
+#elif (!defined __UINT_LEAST16_MAX__ && defined YY_STDINT_H \
+ && UINT_LEAST16_MAX <= INT_MAX)
+typedef uint_least16_t yytype_uint16;
+#elif !defined __UINT_LEAST16_MAX__ && USHRT_MAX <= INT_MAX
+typedef unsigned short yytype_uint16;
+#else
+typedef int yytype_uint16;
+#endif
+
+#ifndef YYPTRDIFF_T
+# if defined __PTRDIFF_TYPE__ && defined __PTRDIFF_MAX__
+# define YYPTRDIFF_T __PTRDIFF_TYPE__
+# define YYPTRDIFF_MAXIMUM __PTRDIFF_MAX__
+# elif defined PTRDIFF_MAX
+# ifndef ptrdiff_t
+# include <stddef.h> /* INFRINGES ON USER NAME SPACE */
+# endif
+# define YYPTRDIFF_T ptrdiff_t
+# define YYPTRDIFF_MAXIMUM PTRDIFF_MAX
+# else
+# define YYPTRDIFF_T long
+# define YYPTRDIFF_MAXIMUM LONG_MAX
+# endif
+#endif
+
+#ifndef YYSIZE_T
+# ifdef __SIZE_TYPE__
+# define YYSIZE_T __SIZE_TYPE__
+# elif defined size_t
+# define YYSIZE_T size_t
+# elif defined __STDC_VERSION__ && 199901 <= __STDC_VERSION__
+# include <stddef.h> /* INFRINGES ON USER NAME SPACE */
+# define YYSIZE_T size_t
+# else
+# define YYSIZE_T unsigned
+# endif
+#endif
+
+#define YYSIZE_MAXIMUM \
+ YY_CAST (YYPTRDIFF_T, \
+ (YYPTRDIFF_MAXIMUM < YY_CAST (YYSIZE_T, -1) \
+ ? YYPTRDIFF_MAXIMUM \
+ : YY_CAST (YYSIZE_T, -1)))
+
+#define YYSIZEOF(X) YY_CAST (YYPTRDIFF_T, sizeof (X))
+
+
+/* Stored state numbers (used for stacks). */
+typedef yytype_int8 yy_state_t;
+
+/* State numbers in computations. */
+typedef int yy_state_fast_t;
+
+#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_PURE
+# if defined __GNUC__ && 2 < __GNUC__ + (96 <= __GNUC_MINOR__)
+# define YY_ATTRIBUTE_PURE __attribute__ ((__pure__))
+# else
+# define YY_ATTRIBUTE_PURE
+# endif
+#endif
+
+#ifndef YY_ATTRIBUTE_UNUSED
+# if defined __GNUC__ && 2 < __GNUC__ + (7 <= __GNUC_MINOR__)
+# define YY_ATTRIBUTE_UNUSED __attribute__ ((__unused__))
+# else
+# define YY_ATTRIBUTE_UNUSED
+# endif
+#endif
+
+/* Suppress unused-variable warnings by "using" E. */
+#if ! defined lint || defined __GNUC__
+# define YY_USE(E) ((void) (E))
+#else
+# define YY_USE(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 __cplusplus && defined __GNUC__ && ! defined __ICC && 6 <= __GNUC__
+# define YY_IGNORE_USELESS_CAST_BEGIN \
+ _Pragma ("GCC diagnostic push") \
+ _Pragma ("GCC diagnostic ignored \"-Wuseless-cast\"")
+# define YY_IGNORE_USELESS_CAST_END \
+ _Pragma ("GCC diagnostic pop")
+#endif
+#ifndef YY_IGNORE_USELESS_CAST_BEGIN
+# define YY_IGNORE_USELESS_CAST_BEGIN
+# define YY_IGNORE_USELESS_CAST_END
+#endif
+
+
+#define YY_ASSERT(E) ((void) (0 && (E)))
+
+#if 1
+
+/* 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 /* 1 */
+
+#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
+{
+ yy_state_t yyss_alloc;
+ YYSTYPE yyvs_alloc;
+};
+
+/* The size of the maximum gap between one aligned stack and the next. */
+# define YYSTACK_GAP_MAXIMUM (YYSIZEOF (union yyalloc) - 1)
+
+/* The size of an array large to enough to hold all stacks, each with
+ N elements. */
+# define YYSTACK_BYTES(N) \
+ ((N) * (YYSIZEOF (yy_state_t) + YYSIZEOF (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 \
+ { \
+ YYPTRDIFF_T yynewbytes; \
+ YYCOPY (&yyptr->Stack_alloc, Stack, yysize); \
+ Stack = &yyptr->Stack_alloc; \
+ yynewbytes = yystacksize * YYSIZEOF (*Stack) + YYSTACK_GAP_MAXIMUM; \
+ yyptr += yynewbytes / YYSIZEOF (*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, YY_CAST (YYSIZE_T, (Count)) * sizeof (*(Src)))
+# else
+# define YYCOPY(Dst, Src, Count) \
+ do \
+ { \
+ YYPTRDIFF_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
+
+/* YYMAXUTOK -- Last valid token kind. */
+#define YYMAXUTOK 262
+
+
+/* YYTRANSLATE(TOKEN-NUM) -- Symbol number corresponding to TOKEN-NUM
+ as returned by yylex, with out-of-bounds checking. */
+#define YYTRANSLATE(YYX) \
+ (0 <= (YYX) && (YYX) <= YYMAXUTOK \
+ ? YY_CAST (yysymbol_kind_t, yytranslate[YYX]) \
+ : YYSYMBOL_YYUNDEF)
+
+/* YYTRANSLATE[TOKEN-NUM] -- Symbol number corresponding to TOKEN-NUM
+ as returned by yylex. */
+static const yytype_int8 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, 156, 156, 157, 160, 161, 162, 163, 164, 167,
+ 178, 179, 182, 183, 184, 185, 186, 189, 190, 191,
+ 192, 193
+};
+#endif
+
+/** Accessing symbol of state STATE. */
+#define YY_ACCESSING_SYMBOL(State) YY_CAST (yysymbol_kind_t, yystos[State])
+
+#if 1
+/* The user-facing name of the symbol whose (internal) number is
+ YYSYMBOL. No bounds checking. */
+static const char *yysymbol_name (yysymbol_kind_t yysymbol) YY_ATTRIBUTE_UNUSED;
+
+/* YYTNAME[SYMBOL-NUM] -- String name of the symbol SYMBOL-NUM.
+ First, the terminals, then, starting at YYNTOKENS, nonterminals. */
+static const char *const yytname[] =
+{
+ "\"end of file\"", "error", "\"invalid token\"", "TOKEN", "STRING",
+ "AND", "OR", "NOT", "'('", "')'", "'='", "'>'", "'<'", "$accept",
+ "filter", "expr", "key_value", "key", "value", "op", YY_NULLPTR
+};
+
+static const char *
+yysymbol_name (yysymbol_kind_t yysymbol)
+{
+ return yytname[yysymbol];
+}
+#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_int16 yytoknum[] =
+{
+ 0, 256, 257, 258, 259, 260, 261, 262, 40, 41,
+ 61, 62, 60
+};
+#endif
+
+#define YYPACT_NINF (-6)
+
+#define yypact_value_is_default(Yyn) \
+ ((Yyn) == YYPACT_NINF)
+
+#define YYTABLE_NINF (-1)
+
+#define yytable_value_is_error(Yyn) \
+ 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_int8 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[] =
+{
+ 0, 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_int8 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_int8 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_int8 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_int8 yyr2[] =
+{
+ 0, 2, 1, 0, 3, 3, 2, 3, 1, 3,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 2, 2
+};
+
+
+enum { YYENOMEM = -2 };
+
+#define yyerrok (yyerrstatus = 0)
+#define yyclearin (yychar = EVENT_FILTER_PARSER_EMPTY)
+
+#define YYACCEPT goto yyacceptlab
+#define YYABORT goto yyabortlab
+#define YYERROR goto yyerrorlab
+
+
+#define YYRECOVERING() (!!yyerrstatus)
+
+#define YYBACKUP(Token, Value) \
+ do \
+ if (yychar == EVENT_FILTER_PARSER_EMPTY) \
+ { \
+ yychar = (Token); \
+ yylval = (Value); \
+ YYPOPSTACK (yylen); \
+ yystate = *yyssp; \
+ goto yybackup; \
+ } \
+ else \
+ { \
+ yyerror (state, YY_("syntax error: cannot back up")); \
+ YYERROR; \
+ } \
+ while (0)
+
+/* Backward compatibility with an undocumented macro.
+ Use EVENT_FILTER_PARSER_error or EVENT_FILTER_PARSER_UNDEF. */
+#define YYERRCODE EVENT_FILTER_PARSER_UNDEF
+
+
+/* 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, Kind, Value, Location) \
+do { \
+ if (yydebug) \
+ { \
+ YYFPRINTF (stderr, "%s ", Title); \
+ yy_symbol_print (stderr, \
+ Kind, Value, state); \
+ YYFPRINTF (stderr, "\n"); \
+ } \
+} while (0)
+
+
+/*-----------------------------------.
+| Print this symbol's value on YYO. |
+`-----------------------------------*/
+
+static void
+yy_symbol_value_print (FILE *yyo,
+ yysymbol_kind_t yykind, YYSTYPE const * const yyvaluep, struct event_filter_parser_state *state)
+{
+ FILE *yyoutput = yyo;
+ YY_USE (yyoutput);
+ YY_USE (state);
+ if (!yyvaluep)
+ return;
+# ifdef YYPRINT
+ if (yykind < YYNTOKENS)
+ YYPRINT (yyo, yytoknum[yykind], *yyvaluep);
+# endif
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ YY_USE (yykind);
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+}
+
+
+/*---------------------------.
+| Print this symbol on YYO. |
+`---------------------------*/
+
+static void
+yy_symbol_print (FILE *yyo,
+ yysymbol_kind_t yykind, YYSTYPE const * const yyvaluep, struct event_filter_parser_state *state)
+{
+ YYFPRINTF (yyo, "%s %s (",
+ yykind < YYNTOKENS ? "token" : "nterm", yysymbol_name (yykind));
+
+ yy_symbol_value_print (yyo, yykind, yyvaluep, state);
+ YYFPRINTF (yyo, ")");
+}
+
+/*------------------------------------------------------------------.
+| yy_stack_print -- Print the state stack from its BOTTOM up to its |
+| TOP (included). |
+`------------------------------------------------------------------*/
+
+static void
+yy_stack_print (yy_state_t *yybottom, yy_state_t *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 (yy_state_t *yyssp, YYSTYPE *yyvsp,
+ int yyrule, struct event_filter_parser_state *state)
+{
+ int yylno = yyrline[yyrule];
+ int yynrhs = yyr2[yyrule];
+ int yyi;
+ YYFPRINTF (stderr, "Reducing stack by rule %d (line %d):\n",
+ yyrule - 1, yylno);
+ /* The symbols being reduced. */
+ for (yyi = 0; yyi < yynrhs; yyi++)
+ {
+ YYFPRINTF (stderr, " $%d = ", yyi + 1);
+ yy_symbol_print (stderr,
+ YY_ACCESSING_SYMBOL (+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) ((void) 0)
+# define YY_SYMBOL_PRINT(Title, Kind, 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
+
+
+/* Context of a parse error. */
+typedef struct
+{
+ yy_state_t *yyssp;
+ yysymbol_kind_t yytoken;
+} yypcontext_t;
+
+/* Put in YYARG at most YYARGN of the expected tokens given the
+ current YYCTX, and return the number of tokens stored in YYARG. If
+ YYARG is null, return the number of expected tokens (guaranteed to
+ be less than YYNTOKENS). Return YYENOMEM on memory exhaustion.
+ Return 0 if there are more than YYARGN expected tokens, yet fill
+ YYARG up to YYARGN. */
+static int
+yypcontext_expected_tokens (const yypcontext_t *yyctx,
+ yysymbol_kind_t yyarg[], int yyargn)
+{
+ /* Actual size of YYARG. */
+ int yycount = 0;
+ int yyn = yypact[+*yyctx->yyssp];
+ 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 != YYSYMBOL_YYerror
+ && !yytable_value_is_error (yytable[yyx + yyn]))
+ {
+ if (!yyarg)
+ ++yycount;
+ else if (yycount == yyargn)
+ return 0;
+ else
+ yyarg[yycount++] = YY_CAST (yysymbol_kind_t, yyx);
+ }
+ }
+ if (yyarg && yycount == 0 && 0 < yyargn)
+ yyarg[0] = YYSYMBOL_YYEMPTY;
+ return yycount;
+}
+
+
+
+
+#ifndef yystrlen
+# if defined __GLIBC__ && defined _STRING_H
+# define yystrlen(S) (YY_CAST (YYPTRDIFF_T, strlen (S)))
+# else
+/* Return the length of YYSTR. */
+static YYPTRDIFF_T
+yystrlen (const char *yystr)
+{
+ YYPTRDIFF_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 YYPTRDIFF_T
+yytnamerr (char *yyres, const char *yystr)
+{
+ if (*yystr == '"')
+ {
+ YYPTRDIFF_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 yystpcpy (yyres, yystr) - yyres;
+ else
+ return yystrlen (yystr);
+}
+#endif
+
+
+static int
+yy_syntax_error_arguments (const yypcontext_t *yyctx,
+ yysymbol_kind_t yyarg[], int yyargn)
+{
+ /* Actual size of YYARG. */
+ 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 (yyctx->yytoken != YYSYMBOL_YYEMPTY)
+ {
+ int yyn;
+ if (yyarg)
+ yyarg[yycount] = yyctx->yytoken;
+ ++yycount;
+ yyn = yypcontext_expected_tokens (yyctx,
+ yyarg ? yyarg + 1 : yyarg, yyargn - 1);
+ if (yyn == YYENOMEM)
+ return YYENOMEM;
+ else
+ yycount += yyn;
+ }
+ return yycount;
+}
+
+/* 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 YYENOMEM if the
+ required number of bytes is too large to store. */
+static int
+yysyntax_error (YYPTRDIFF_T *yymsg_alloc, char **yymsg,
+ const yypcontext_t *yyctx)
+{
+ enum { YYARGS_MAX = 5 };
+ /* Internationalized format string. */
+ const char *yyformat = YY_NULLPTR;
+ /* Arguments of yyformat: reported tokens (one for the "unexpected",
+ one per "expected"). */
+ yysymbol_kind_t yyarg[YYARGS_MAX];
+ /* Cumulated lengths of YYARG. */
+ YYPTRDIFF_T yysize = 0;
+
+ /* Actual size of YYARG. */
+ int yycount = yy_syntax_error_arguments (yyctx, yyarg, YYARGS_MAX);
+ if (yycount == YYENOMEM)
+ return YYENOMEM;
+
+ 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_
+ }
+
+ /* Compute error message size. Don't count the "%s"s, but reserve
+ room for the terminator. */
+ yysize = yystrlen (yyformat) - 2 * yycount + 1;
+ {
+ int yyi;
+ for (yyi = 0; yyi < yycount; ++yyi)
+ {
+ YYPTRDIFF_T yysize1
+ = yysize + yytnamerr (YY_NULLPTR, yytname[yyarg[yyi]]);
+ if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM)
+ yysize = yysize1;
+ else
+ return YYENOMEM;
+ }
+ }
+
+ 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, yytname[yyarg[yyi++]]);
+ yyformat += 2;
+ }
+ else
+ {
+ ++yyp;
+ ++yyformat;
+ }
+ }
+ return 0;
+}
+
+
+/*-----------------------------------------------.
+| Release the memory associated to this symbol. |
+`-----------------------------------------------*/
+
+static void
+yydestruct (const char *yymsg,
+ yysymbol_kind_t yykind, YYSTYPE *yyvaluep, struct event_filter_parser_state *state)
+{
+ YY_USE (yyvaluep);
+ YY_USE (state);
+ if (!yymsg)
+ yymsg = "Deleting";
+ YY_SYMBOL_PRINT (yymsg, yykind, yyvaluep, yylocationp);
+
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ YY_USE (yykind);
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+}
+
+
+
+
+
+
+/*----------.
+| yyparse. |
+`----------*/
+
+int
+yyparse (struct event_filter_parser_state *state)
+{
+/* Lookahead token kind. */
+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 = 0;
+
+ yy_state_fast_t yystate = 0;
+ /* Number of tokens to shift before error messages enabled. */
+ int yyerrstatus = 0;
+
+ /* Refer to the stacks through separate pointers, to allow yyoverflow
+ to reallocate them elsewhere. */
+
+ /* Their size. */
+ YYPTRDIFF_T yystacksize = YYINITDEPTH;
+
+ /* The state stack: array, bottom, top. */
+ yy_state_t yyssa[YYINITDEPTH];
+ yy_state_t *yyss = yyssa;
+ yy_state_t *yyssp = yyss;
+
+ /* The semantic value stack: array, bottom, top. */
+ YYSTYPE yyvsa[YYINITDEPTH];
+ YYSTYPE *yyvs = yyvsa;
+ YYSTYPE *yyvsp = yyvs;
+
+ int yyn;
+ /* The return value of yyparse. */
+ int yyresult;
+ /* Lookahead symbol kind. */
+ yysymbol_kind_t yytoken = YYSYMBOL_YYEMPTY;
+ /* The variables used to return semantic value and location from the
+ action routines. */
+ YYSTYPE yyval;
+
+ /* Buffer for error messages, and its allocated size. */
+ char yymsgbuf[128];
+ char *yymsg = yymsgbuf;
+ YYPTRDIFF_T yymsg_alloc = sizeof yymsgbuf;
+
+#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;
+
+ YYDPRINTF ((stderr, "Starting parse\n"));
+
+ yychar = EVENT_FILTER_PARSER_EMPTY; /* 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++;
+
+
+/*--------------------------------------------------------------------.
+| yysetstate -- set current state (the top of the stack) to yystate. |
+`--------------------------------------------------------------------*/
+yysetstate:
+ YYDPRINTF ((stderr, "Entering state %d\n", yystate));
+ YY_ASSERT (0 <= yystate && yystate < YYNSTATES);
+ YY_IGNORE_USELESS_CAST_BEGIN
+ *yyssp = YY_CAST (yy_state_t, yystate);
+ YY_IGNORE_USELESS_CAST_END
+ YY_STACK_PRINT (yyss, yyssp);
+
+ 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. */
+ YYPTRDIFF_T yysize = 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. */
+ yy_state_t *yyss1 = yyss;
+ YYSTYPE *yyvs1 = yyvs;
+
+ /* 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 * YYSIZEOF (*yyssp),
+ &yyvs1, yysize * YYSIZEOF (*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;
+
+ {
+ yy_state_t *yyss1 = yyss;
+ union yyalloc *yyptr =
+ YY_CAST (union yyalloc *,
+ YYSTACK_ALLOC (YY_CAST (YYSIZE_T, 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;
+
+ YY_IGNORE_USELESS_CAST_BEGIN
+ YYDPRINTF ((stderr, "Stack size increased to %ld\n",
+ YY_CAST (long, yystacksize)));
+ YY_IGNORE_USELESS_CAST_END
+
+ if (yyss + yystacksize - 1 <= yyssp)
+ YYABORT;
+ }
+#endif /* !defined yyoverflow && !defined YYSTACK_RELOCATE */
+
+ 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 empty, or end-of-input, or a valid lookahead. */
+ if (yychar == EVENT_FILTER_PARSER_EMPTY)
+ {
+ YYDPRINTF ((stderr, "Reading a token\n"));
+ yychar = yylex (&yylval, scanner);
+ }
+
+ if (yychar <= EVENT_FILTER_PARSER_EOF)
+ {
+ yychar = EVENT_FILTER_PARSER_EOF;
+ yytoken = YYSYMBOL_YYEOF;
+ YYDPRINTF ((stderr, "Now at end of input.\n"));
+ }
+ else if (yychar == EVENT_FILTER_PARSER_error)
+ {
+ /* The scanner already issued an error message, process directly
+ to error recovery. But do not keep the error token as
+ lookahead, it is too special and may lead us to an endless
+ loop in error recovery. */
+ yychar = EVENT_FILTER_PARSER_UNDEF;
+ yytoken = YYSYMBOL_YYerror;
+ goto yyerrlab1;
+ }
+ 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);
+ yystate = yyn;
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ *++yyvsp = yylval;
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+
+ /* Discard the shifted token. */
+ yychar = EVENT_FILTER_PARSER_EMPTY;
+ 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: /* filter: expr */
+#line 156 "event-filter-parser.y"
+ { state->output = (yyvsp[0].node); }
+#line 1501 "event-filter-parser.c"
+ break;
+
+ case 3: /* filter: %empty */
+#line 157 "event-filter-parser.y"
+ { state->output = NULL; }
+#line 1507 "event-filter-parser.c"
+ break;
+
+ case 4: /* expr: expr AND expr */
+#line 160 "event-filter-parser.y"
+ { (yyval.node) = logic(state, (yyvsp[-2].node), (yyvsp[0].node), EVENT_FILTER_OP_AND); }
+#line 1513 "event-filter-parser.c"
+ break;
+
+ case 5: /* expr: expr OR expr */
+#line 161 "event-filter-parser.y"
+ { (yyval.node) = logic(state, (yyvsp[-2].node), (yyvsp[0].node), EVENT_FILTER_OP_OR); }
+#line 1519 "event-filter-parser.c"
+ break;
+
+ case 6: /* expr: NOT expr */
+#line 162 "event-filter-parser.y"
+ { (yyval.node) = logic(state, (yyvsp[0].node), NULL, EVENT_FILTER_OP_NOT); }
+#line 1525 "event-filter-parser.c"
+ break;
+
+ case 7: /* expr: '(' expr ')' */
+#line 163 "event-filter-parser.y"
+ { (yyval.node) = (yyvsp[-1].node); }
+#line 1531 "event-filter-parser.c"
+ break;
+
+ case 8: /* expr: key_value */
+#line 164 "event-filter-parser.y"
+ { (yyval.node) = (yyvsp[0].node); }
+#line 1537 "event-filter-parser.c"
+ break;
+
+ case 9: /* key_value: key op value */
+#line 167 "event-filter-parser.y"
+ {
+ (yyval.node) = key_value(state, (yyvsp[-2].str), (yyvsp[0].str), (yyvsp[-1].op));
+ if ((yyval.node) == NULL) {
+ yyerror(state, state->error);
+ /* avoid compiler warning about yynerrs being set, but not used */
+ (void)yynerrs;
+ YYERROR;
+ }
+ }
+#line 1551 "event-filter-parser.c"
+ break;
+
+ case 10: /* key: TOKEN */
+#line 178 "event-filter-parser.y"
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1557 "event-filter-parser.c"
+ break;
+
+ case 11: /* key: STRING */
+#line 179 "event-filter-parser.y"
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1563 "event-filter-parser.c"
+ break;
+
+ case 12: /* value: TOKEN */
+#line 182 "event-filter-parser.y"
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1569 "event-filter-parser.c"
+ break;
+
+ case 13: /* value: STRING */
+#line 183 "event-filter-parser.y"
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1575 "event-filter-parser.c"
+ break;
+
+ case 14: /* value: AND */
+#line 184 "event-filter-parser.y"
+ { (yyval.str) = "and"; }
+#line 1581 "event-filter-parser.c"
+ break;
+
+ case 15: /* value: OR */
+#line 185 "event-filter-parser.y"
+ { (yyval.str) = "or"; }
+#line 1587 "event-filter-parser.c"
+ break;
+
+ case 16: /* value: NOT */
+#line 186 "event-filter-parser.y"
+ { (yyval.str) = "not"; }
+#line 1593 "event-filter-parser.c"
+ break;
+
+ case 17: /* op: '=' */
+#line 189 "event-filter-parser.y"
+ { (yyval.op) = EVENT_FILTER_OP_CMP_EQ; }
+#line 1599 "event-filter-parser.c"
+ break;
+
+ case 18: /* op: '>' */
+#line 190 "event-filter-parser.y"
+ { (yyval.op) = EVENT_FILTER_OP_CMP_GT; }
+#line 1605 "event-filter-parser.c"
+ break;
+
+ case 19: /* op: '<' */
+#line 191 "event-filter-parser.y"
+ { (yyval.op) = EVENT_FILTER_OP_CMP_LT; }
+#line 1611 "event-filter-parser.c"
+ break;
+
+ case 20: /* op: '>' '=' */
+#line 192 "event-filter-parser.y"
+ { (yyval.op) = EVENT_FILTER_OP_CMP_GE; }
+#line 1617 "event-filter-parser.c"
+ break;
+
+ case 21: /* op: '<' '=' */
+#line 193 "event-filter-parser.y"
+ { (yyval.op) = EVENT_FILTER_OP_CMP_LE; }
+#line 1623 "event-filter-parser.c"
+ break;
+
+
+#line 1627 "event-filter-parser.c"
+
+ 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 ("-> $$ =", YY_CAST (yysymbol_kind_t, yyr1[yyn]), &yyval, &yyloc);
+
+ YYPOPSTACK (yylen);
+ yylen = 0;
+
+ *++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 == EVENT_FILTER_PARSER_EMPTY ? YYSYMBOL_YYEMPTY : YYTRANSLATE (yychar);
+ /* If not already recovering from an error, report this error. */
+ if (!yyerrstatus)
+ {
+ ++yynerrs;
+ {
+ yypcontext_t yyctx
+ = {yyssp, yytoken};
+ char const *yymsgp = YY_("syntax error");
+ int yysyntax_error_status;
+ yysyntax_error_status = yysyntax_error (&yymsg_alloc, &yymsg, &yyctx);
+ if (yysyntax_error_status == 0)
+ yymsgp = yymsg;
+ else if (yysyntax_error_status == -1)
+ {
+ if (yymsg != yymsgbuf)
+ YYSTACK_FREE (yymsg);
+ yymsg = YY_CAST (char *,
+ YYSTACK_ALLOC (YY_CAST (YYSIZE_T, yymsg_alloc)));
+ if (yymsg)
+ {
+ yysyntax_error_status
+ = yysyntax_error (&yymsg_alloc, &yymsg, &yyctx);
+ yymsgp = yymsg;
+ }
+ else
+ {
+ yymsg = yymsgbuf;
+ yymsg_alloc = sizeof yymsgbuf;
+ yysyntax_error_status = YYENOMEM;
+ }
+ }
+ yyerror (state, yymsgp);
+ if (yysyntax_error_status == YYENOMEM)
+ goto yyexhaustedlab;
+ }
+ }
+
+ if (yyerrstatus == 3)
+ {
+ /* If just tried and failed to reuse lookahead token after an
+ error, discard it. */
+
+ if (yychar <= EVENT_FILTER_PARSER_EOF)
+ {
+ /* Return failure if at end of input. */
+ if (yychar == EVENT_FILTER_PARSER_EOF)
+ YYABORT;
+ }
+ else
+ {
+ yydestruct ("Error: discarding",
+ yytoken, &yylval, state);
+ yychar = EVENT_FILTER_PARSER_EMPTY;
+ }
+ }
+
+ /* 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. */
+
+ /* Pop stack until we find a state that shifts the error token. */
+ for (;;)
+ {
+ yyn = yypact[yystate];
+ if (!yypact_value_is_default (yyn))
+ {
+ yyn += YYSYMBOL_YYerror;
+ if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYSYMBOL_YYerror)
+ {
+ 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",
+ YY_ACCESSING_SYMBOL (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", YY_ACCESSING_SYMBOL (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 1
+/*-------------------------------------------------.
+| yyexhaustedlab -- memory exhaustion comes here. |
+`-------------------------------------------------*/
+yyexhaustedlab:
+ yyerror (state, YY_("memory exhausted"));
+ yyresult = 2;
+ goto yyreturn;
+#endif
+
+
+/*-------------------------------------------------------.
+| yyreturn -- parsing is finished, clean up and return. |
+`-------------------------------------------------------*/
+yyreturn:
+ if (yychar != EVENT_FILTER_PARSER_EMPTY)
+ {
+ /* 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",
+ YY_ACCESSING_SYMBOL (+*yyssp), yyvsp, state);
+ YYPOPSTACK (1);
+ }
+#ifndef yyoverflow
+ if (yyss != yyssa)
+ YYSTACK_FREE (yyss);
+#endif
+ if (yymsg != yymsgbuf)
+ YYSTACK_FREE (yymsg);
+ return yyresult;
+}
+
+#line 195 "event-filter-parser.y"
+
diff --git a/src/lib/event-filter-parser.h b/src/lib/event-filter-parser.h
new file mode 100644
index 0000000..4df919d
--- /dev/null
+++ b/src/lib/event-filter-parser.h
@@ -0,0 +1,96 @@
+/* A Bison parser, made by GNU Bison 3.7.5. */
+
+/* Bison interface for Yacc-like parsers in C
+
+ Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2021 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. */
+
+/* DO NOT RELY ON FEATURES THAT ARE NOT DOCUMENTED in the manual,
+ especially those whose name start with YY_ or yy_. They are
+ private implementation details that can be changed or removed. */
+
+#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 kinds. */
+#ifndef EVENT_FILTER_PARSER_TOKENTYPE
+# define EVENT_FILTER_PARSER_TOKENTYPE
+ enum event_filter_parser_tokentype
+ {
+ EVENT_FILTER_PARSER_EMPTY = -2,
+ EVENT_FILTER_PARSER_EOF = 0, /* "end of file" */
+ EVENT_FILTER_PARSER_error = 256, /* error */
+ EVENT_FILTER_PARSER_UNDEF = 257, /* "invalid token" */
+ TOKEN = 258, /* TOKEN */
+ STRING = 259, /* STRING */
+ AND = 260, /* AND */
+ OR = 261, /* OR */
+ NOT = 262 /* NOT */
+ };
+ typedef enum event_filter_parser_tokentype event_filter_parser_token_kind_t;
+#endif
+
+/* Value type. */
+#if ! defined EVENT_FILTER_PARSER_STYPE && ! defined EVENT_FILTER_PARSER_STYPE_IS_DECLARED
+union EVENT_FILTER_PARSER_STYPE
+{
+#line 139 "event-filter-parser.y"
+
+ const char *str;
+ enum event_filter_node_op op;
+ struct event_filter_node *node;
+
+#line 85 "event-filter-parser.h"
+
+};
+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..2b75a49
--- /dev/null
+++ b/src/lib/event-filter-parser.y
@@ -0,0 +1,195 @@
+/* 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;
+}
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+# pragma GCC 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);
+ /* avoid compiler warning about yynerrs being set, but not used */
+ (void)yynerrs;
+ 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..d12d878
--- /dev/null
+++ b/src/lib/ioloop-notify-kqueue.c
@@ -0,0 +1,224 @@
+/*
+ * 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 "time-util.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..864562d
--- /dev/null
+++ b/src/lib/lib-event.c
@@ -0,0 +1,1776 @@
+/* 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;
+}
+
+#undef event_unset_log_message_callback
+void event_unset_log_message_callback(struct event *event,
+ event_log_message_callback_t *callback,
+ void *context)
+{
+ i_assert(event->log_message_callback == callback);
+ i_assert(event->log_message_callback_context == context);
+
+ event->log_message_callback = NULL;
+ event->log_message_callback_context = NULL;
+}
+
+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..2059501
--- /dev/null
+++ b/src/lib/lib-event.h
@@ -0,0 +1,440 @@
+#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 *)))
+
+/* Unsets the event message amendment callback. */
+void event_unset_log_message_callback(struct event *event,
+ event_log_message_callback_t *callback,
+ void *context);
+#define event_unset_log_message_callback(event, callback, context) \
+ event_unset_log_message_callback(event, \
+ (event_log_message_callback_t*)callback, context)
+
+/* 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..15192ce
--- /dev/null
+++ b/src/lib/process-title.c
@@ -0,0 +1,194 @@
+/* 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;
+static unsigned int process_title_counter = 0;
+
+#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);
+
+ process_title_counter++;
+ 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;
+}
+
+unsigned int process_title_get_counter(void)
+{
+ return process_title_counter;
+}
+
+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..0f5480f
--- /dev/null
+++ b/src/lib/process-title.h
@@ -0,0 +1,19 @@
+#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);
+/* Return the number of times process_title_set() has been called. */
+unsigned int process_title_get_counter(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..0eb330b
--- /dev/null
+++ b/src/lib/test-mempool-allocfree.c
@@ -0,0 +1,128 @@
+/* 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;
+ 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;
+ }
+ last_alloc = 0;
+ p_free(pool, mem);
+ /* grow previous allocation */
+ } else if ((i % 5) == 0) {
+ if (mem != NULL)
+ used -= last_alloc;
+ 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;
+ 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;
+ 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
diff --git a/src/lmtp/Makefile.am b/src/lmtp/Makefile.am
new file mode 100644
index 0000000..316e5cc
--- /dev/null
+++ b/src/lmtp/Makefile.am
@@ -0,0 +1,55 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = lmtp
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-lda \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ $(BINARY_CFLAGS)
+
+lmtp_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+lmtp_LDADD = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+lmtp_DEPENDENCIES = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+lmtp_SOURCES = \
+ main.c \
+ lmtp-client.c \
+ lmtp-commands.c \
+ lmtp-recipient.c \
+ lmtp-local.c \
+ lmtp-proxy.c \
+ lmtp-settings.c
+
+noinst_HEADERS = \
+ lmtp-local.h \
+ lmtp-proxy.h
+
+headers = \
+ lmtp-common.h \
+ lmtp-commands.h \
+ lmtp-recipient.h \
+ lmtp-client.h \
+ lmtp-settings.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lmtp/Makefile.in b/src/lmtp/Makefile.in
new file mode 100644
index 0000000..7cd9eba
--- /dev/null
+++ b/src/lmtp/Makefile.in
@@ -0,0 +1,916 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = lmtp$(EXEEXT)
+subdir = src/lmtp
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_lmtp_OBJECTS = main.$(OBJEXT) lmtp-client.$(OBJEXT) \
+ lmtp-commands.$(OBJEXT) lmtp-recipient.$(OBJEXT) \
+ lmtp-local.$(OBJEXT) lmtp-proxy.$(OBJEXT) \
+ lmtp-settings.$(OBJEXT)
+lmtp_OBJECTS = $(am_lmtp_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+lmtp_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(lmtp_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)/lmtp-client.Po \
+ ./$(DEPDIR)/lmtp-commands.Po ./$(DEPDIR)/lmtp-local.Po \
+ ./$(DEPDIR)/lmtp-proxy.Po ./$(DEPDIR)/lmtp-recipient.Po \
+ ./$(DEPDIR)/lmtp-settings.Po ./$(DEPDIR)/main.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 = $(lmtp_SOURCES)
+DIST_SOURCES = $(lmtp_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; }; \
+ }
+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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-lda \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ -DMODULEDIR=\""$(moduledir)"\" \
+ $(BINARY_CFLAGS)
+
+lmtp_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+lmtp_LDADD = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+lmtp_DEPENDENCIES = \
+ $(LIBDOVECOT_LDA) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+lmtp_SOURCES = \
+ main.c \
+ lmtp-client.c \
+ lmtp-commands.c \
+ lmtp-recipient.c \
+ lmtp-local.c \
+ lmtp-proxy.c \
+ lmtp-settings.c
+
+noinst_HEADERS = \
+ lmtp-local.h \
+ lmtp-proxy.h
+
+headers = \
+ lmtp-common.h \
+ lmtp-commands.h \
+ lmtp-recipient.h \
+ lmtp-client.h \
+ lmtp-settings.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/lmtp/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lmtp/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+lmtp$(EXEEXT): $(lmtp_OBJECTS) $(lmtp_DEPENDENCIES) $(EXTRA_lmtp_DEPENDENCIES)
+ @rm -f lmtp$(EXEEXT)
+ $(AM_V_CCLD)$(lmtp_LINK) $(lmtp_OBJECTS) $(lmtp_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-local.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-proxy.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-recipient.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lmtp-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/lmtp-client.Po
+ -rm -f ./$(DEPDIR)/lmtp-commands.Po
+ -rm -f ./$(DEPDIR)/lmtp-local.Po
+ -rm -f ./$(DEPDIR)/lmtp-proxy.Po
+ -rm -f ./$(DEPDIR)/lmtp-recipient.Po
+ -rm -f ./$(DEPDIR)/lmtp-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+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)/lmtp-client.Po
+ -rm -f ./$(DEPDIR)/lmtp-commands.Po
+ -rm -f ./$(DEPDIR)/lmtp-local.Po
+ -rm -f ./$(DEPDIR)/lmtp-proxy.Po
+ -rm -f ./$(DEPDIR)/lmtp-recipient.Po
+ -rm -f ./$(DEPDIR)/lmtp-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/lmtp/lmtp-client.c b/src/lmtp/lmtp-client.c
new file mode 100644
index 0000000..2757c80
--- /dev/null
+++ b/src/lmtp/lmtp-client.c
@@ -0,0 +1,439 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "base64.h"
+#include "str.h"
+#include "llist.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hostpid.h"
+#include "process-title.h"
+#include "var-expand.h"
+#include "module-dir.h"
+#include "master-service-ssl.h"
+#include "master-service-settings.h"
+#include "iostream-ssl.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "raw-storage.h"
+#include "lda-settings.h"
+#include "lmtp-local.h"
+#include "lmtp-proxy.h"
+#include "lmtp-commands.h"
+
+#include <unistd.h>
+
+#define CLIENT_IDLE_TIMEOUT_MSECS (1000*60*5)
+
+static const struct smtp_server_callbacks lmtp_callbacks;
+static const struct lmtp_client_vfuncs lmtp_client_vfuncs;
+
+struct lmtp_module_register lmtp_module_register = { 0 };
+
+static struct client *clients = NULL;
+static unsigned int clients_count = 0;
+
+static bool verbose_proctitle = FALSE;
+
+static const char *client_remote_id(struct client *client)
+{
+ const char *addr;
+
+ addr = net_ip2addr(&client->remote_ip);
+ if (addr[0] == '\0')
+ addr = "local";
+ return addr;
+}
+
+static void refresh_proctitle(void)
+{
+ struct client *client;
+ string_t *title;
+
+ if (!verbose_proctitle)
+ return;
+
+ title = t_str_new(128);
+ str_append_c(title, '[');
+ switch (clients_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ client = clients;
+ str_append(title, client_remote_id(client));
+ str_append_c(title, ' ');
+ str_append(title, smtp_server_state_names[client->state.state]);
+ if (client->state.args != NULL && *client->state.args != '\0') {
+ str_append_c(title, ' ');
+ str_append(title, client->state.args);
+ }
+ break;
+ default:
+ str_printfa(title, "%u connections", clients_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void client_load_modules(struct client *client)
+{
+ struct module_dir_load_settings mod_set;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ mod_set.binary_name = "lmtp";
+
+ /* pre-load all configured mail plugins */
+ mail_storage_service_modules =
+ module_dir_load_missing(mail_storage_service_modules,
+ client->lmtp_set->mail_plugin_dir,
+ client->lmtp_set->mail_plugins,
+ &mod_set);
+ module_dir_init(mail_storage_service_modules);
+}
+
+static void client_raw_user_create(struct client *client)
+{
+ void **sets;
+
+ sets = master_service_settings_get_others(master_service);
+ client->raw_mail_user =
+ raw_storage_create_from_set(client->user_set_info, sets[0]);
+}
+
+static void client_read_settings(struct client *client, bool ssl)
+{
+ struct mail_storage_service_input input;
+ const struct setting_parser_context *set_parser;
+ struct mail_user_settings *user_set;
+ struct lmtp_settings *lmtp_set;
+ struct lda_settings *lda_set;
+ const char *error;
+
+ i_zero(&input);
+ input.module = input.service = "lmtp";
+ input.local_ip = client->local_ip;
+ input.remote_ip = client->remote_ip;
+ input.local_port = client->local_port;
+ input.remote_port = client->remote_port;
+ input.conn_secured = ssl;
+ input.conn_ssl_secured = ssl;
+ input.username = "";
+
+ if (mail_storage_service_read_settings(storage_service, &input,
+ client->pool,
+ &client->user_set_info,
+ &set_parser, &error) < 0)
+ i_fatal("%s", error);
+
+ lmtp_settings_dup(set_parser, client->pool,
+ &user_set, &lmtp_set, &lda_set);
+ const struct var_expand_table *tab =
+ mail_storage_service_get_var_expand_table(storage_service, &input);
+ if (settings_var_expand(&lmtp_setting_parser_info, lmtp_set,
+ client->pool, tab, &error) <= 0)
+ i_fatal("Failed to expand settings: %s", error);
+ client->service_set = master_service_settings_get(master_service);
+ client->user_set = user_set;
+ client->lmtp_set = lmtp_set;
+ client->unexpanded_lda_set = lda_set;
+}
+
+struct client *client_create(int fd_in, int fd_out,
+ const struct master_service_connection *conn)
+{
+ static const char *rcpt_param_extensions[] = {
+ LMTP_RCPT_FORWARD_PARAMETER, NULL };
+ static const struct smtp_capability_extra cap_rcpt_forward = {
+ .name = LMTP_RCPT_FORWARD_CAPABILITY };
+ enum lmtp_client_workarounds workarounds;
+ struct smtp_server_settings lmtp_set;
+ struct client *client;
+ pool_t pool;
+
+ pool = pool_alloconly_create("lmtp client", 2048);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+ client->v = lmtp_client_vfuncs;
+ client->remote_ip = conn->remote_ip;
+ client->remote_port = conn->remote_port;
+ client->local_ip = conn->local_ip;
+ client->local_port = conn->local_port;
+ client->real_local_ip = conn->real_local_ip;
+ client->real_local_port = conn->real_local_port;
+ client->real_remote_ip = conn->real_remote_ip;
+ client->real_remote_port = conn->real_remote_port;
+ client->state_pool = pool_alloconly_create("client state", 4096);
+
+ client->event = event_create(NULL);
+ event_add_category(client->event, &event_category_lmtp);
+
+ client_read_settings(client, conn->ssl);
+ client_raw_user_create(client);
+ client_load_modules(client);
+ client->my_domain = client->unexpanded_lda_set->hostname;
+
+ if (client->service_set->verbose_proctitle)
+ verbose_proctitle = TRUE;
+
+ p_array_init(&client->module_contexts, client->pool, 5);
+
+ i_zero(&lmtp_set);
+ lmtp_set.capabilities =
+ SMTP_CAPABILITY_PIPELINING |
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES |
+ SMTP_CAPABILITY_8BITMIME |
+ SMTP_CAPABILITY_CHUNKING |
+ SMTP_CAPABILITY_XCLIENT |
+ SMTP_CAPABILITY__ORCPT;
+ if (!conn->ssl && master_service_ssl_is_enabled(master_service))
+ lmtp_set.capabilities |= SMTP_CAPABILITY_STARTTLS;
+ lmtp_set.hostname = client->unexpanded_lda_set->hostname;
+ lmtp_set.login_greeting = client->lmtp_set->login_greeting;
+ lmtp_set.max_message_size = UOFF_T_MAX;
+ lmtp_set.rcpt_param_extensions = rcpt_param_extensions;
+ lmtp_set.rcpt_domain_optional = TRUE;
+ lmtp_set.max_client_idle_time_msecs = CLIENT_IDLE_TIMEOUT_MSECS;
+ lmtp_set.rawlog_dir = client->lmtp_set->lmtp_rawlog_dir;
+ lmtp_set.event_parent = client->event;
+
+ workarounds = client->lmtp_set->parsed_workarounds;
+ if ((workarounds & LMTP_WORKAROUND_WHITESPACE_BEFORE_PATH) != 0) {
+ lmtp_set.workarounds |=
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH;
+ }
+ if ((workarounds & LMTP_WORKAROUND_MAILBOX_FOR_PATH) != 0) {
+ lmtp_set.workarounds |=
+ SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH;
+ }
+
+ client->conn = smtp_server_connection_create(
+ lmtp_server, fd_in, fd_out,
+ &conn->remote_ip, conn->remote_port, conn->ssl,
+ &lmtp_set, &lmtp_callbacks, client);
+ if (smtp_server_connection_is_trusted(client->conn)) {
+ smtp_server_connection_add_extra_capability(
+ client->conn, &cap_rcpt_forward);
+ }
+
+ DLLIST_PREPEND(&clients, client);
+ clients_count++;
+
+ e_info(client->event, "Connect from %s", client_remote_id(client));
+
+ if (hook_client_created != NULL)
+ hook_client_created(&client);
+
+ smtp_server_connection_start(client->conn);
+
+ refresh_proctitle();
+ return client;
+}
+
+void client_state_reset(struct client *client)
+{
+ i_free(client->state.args);
+
+ if (client->local != NULL)
+ lmtp_local_deinit(&client->local);
+ if (client->proxy != NULL)
+ lmtp_proxy_deinit(&client->proxy);
+
+ o_stream_unref(&client->state.mail_data_output);
+
+ i_zero(&client->state);
+ p_clear(client->state_pool);
+}
+
+void client_destroy(struct client **_client, const char *enh_code,
+ const char *reason)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ smtp_server_connection_terminate(&client->conn,
+ (enh_code == NULL ? "4.0.0" : enh_code), reason);
+}
+
+static void
+client_default_destroy(struct client *client)
+{
+ if (client->destroyed)
+ return;
+ client->destroyed = TRUE;
+
+ clients_count--;
+ DLLIST_REMOVE(&clients, client);
+
+ if (client->raw_mail_user != NULL)
+ mail_user_deinit(&client->raw_mail_user);
+
+ client_state_reset(client);
+ event_unref(&client->event);
+ pool_unref(&client->state_pool);
+ pool_unref(&client->pool);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+client_connection_trans_start(void *context,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = context;
+
+ client->v.trans_start(client, trans);
+}
+
+static void
+client_default_trans_start(struct client *client ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void
+client_connection_trans_free(void *context,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = (struct client *)context;
+
+ client->v.trans_free(client, trans);
+}
+
+static void
+client_default_trans_free(struct client *client,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ client_state_reset(client);
+}
+
+static void
+client_connection_state_changed(void *context,
+ enum smtp_server_state new_state,
+ const char *new_args)
+{
+ struct client *client = (struct client *)context;
+
+ i_free(client->state.args);
+
+ client->state.state = new_state;
+ client->state.args = i_strdup(new_args);
+
+ if (clients_count == 1)
+ refresh_proctitle();
+}
+
+void client_update_data_state(struct client *client, const char *new_args)
+{
+ i_assert(client->state.state == SMTP_SERVER_STATE_DATA);
+ i_free(client->state.args);
+ client->state.args = i_strdup(new_args);
+
+ if (clients_count == 1)
+ refresh_proctitle();
+}
+
+static void
+client_connection_proxy_data_updated(void *context,
+ const struct smtp_proxy_data *data)
+{
+ struct client *client = (struct client *)context;
+
+ client->remote_ip = data->source_ip;
+ client->remote_port = data->source_port;
+
+ if (clients_count == 1)
+ refresh_proctitle();
+}
+
+static void client_connection_disconnect(void *context, const char *reason)
+{
+ struct client *client = (struct client *)context;
+
+ if (client->disconnected)
+ return;
+ client->disconnected = TRUE;
+
+ if (reason == NULL)
+ reason = "Connection closed";
+ e_info(client->event, "Disconnect from %s: %s",
+ client_remote_id(client), reason);
+}
+
+static void client_connection_free(void *context)
+{
+ struct client *client = (struct client *)context;
+
+ client->v.destroy(client);
+}
+
+static bool client_connection_is_trusted(void *context)
+{
+ struct client *client = (struct client *)context;
+ const char *const *net;
+ struct ip_addr net_ip;
+ unsigned int bits;
+
+ if (client->lmtp_set->login_trusted_networks == NULL)
+ return FALSE;
+
+ net = t_strsplit_spaces(client->lmtp_set->login_trusted_networks, ", ");
+ for (; *net != NULL; net++) {
+ if (net_parse_range(*net, &net_ip, &bits) < 0) {
+ e_error(client->event, "login_trusted_networks: "
+ "Invalid network '%s'", *net);
+ break;
+ }
+
+ if (net_is_in_network(&client->real_remote_ip, &net_ip, bits))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void clients_destroy(void)
+{
+ while (clients != NULL) {
+ struct client *client = clients;
+ client_destroy(&client, "4.3.2", "Shutting down");
+ }
+}
+
+static const struct smtp_server_callbacks lmtp_callbacks = {
+ .conn_cmd_mail = cmd_mail,
+ .conn_cmd_rcpt = cmd_rcpt,
+ .conn_cmd_data_begin = cmd_data_begin,
+ .conn_cmd_data_continue = cmd_data_continue,
+
+ .conn_trans_start = client_connection_trans_start,
+ .conn_trans_free = client_connection_trans_free,
+
+ .conn_state_changed = client_connection_state_changed,
+
+ .conn_proxy_data_updated = client_connection_proxy_data_updated,
+
+ .conn_disconnect = client_connection_disconnect,
+ .conn_free = client_connection_free,
+
+ .conn_is_trusted = client_connection_is_trusted
+};
+
+static const struct lmtp_client_vfuncs lmtp_client_vfuncs = {
+ .destroy = client_default_destroy,
+
+ .trans_start = client_default_trans_start,
+ .trans_free = client_default_trans_free,
+
+ .cmd_mail = client_default_cmd_mail,
+ .cmd_rcpt = client_default_cmd_rcpt,
+ .cmd_data = client_default_cmd_data,
+
+ .local_deliver = lmtp_local_default_deliver,
+};
diff --git a/src/lmtp/lmtp-client.h b/src/lmtp/lmtp-client.h
new file mode 100644
index 0000000..0e7d963
--- /dev/null
+++ b/src/lmtp/lmtp-client.h
@@ -0,0 +1,124 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "net.h"
+#include "smtp-server.h"
+
+#define CLIENT_MAIL_DATA_MAX_INMEMORY_SIZE (1024*128)
+
+struct mail_storage;
+struct mail_deliver_context;
+union lmtp_module_context;
+struct lmtp_recipient;
+struct client;
+
+struct lmtp_local_deliver_context {
+ struct mail *src_mail;
+ const char *session_id;
+ struct timeval delivery_time_started;
+
+ struct mail_user *rcpt_user;
+ const char *rcpt_default_mailbox;
+
+ const struct mail_storage_settings *mail_set;
+ const struct smtp_submit_settings *smtp_set;
+ const struct lda_settings *lda_set;
+
+ struct mail_deliver_session *session;
+};
+
+struct client_state {
+ enum smtp_server_state state;
+ char *args;
+ unsigned int session_id_seq;
+
+ struct istream *data_input;
+ uoff_t data_size;
+
+ struct timeval data_end_timeval;
+
+ struct ostream *mail_data_output;
+
+ const char *added_headers_local;
+ const char *added_headers_proxy;
+};
+
+struct lmtp_client_vfuncs {
+ void (*destroy)(struct client *client);
+
+ void (*trans_start)(struct client *client,
+ struct smtp_server_transaction *trans);
+ void (*trans_free)(struct client *client,
+ struct smtp_server_transaction *trans);
+
+ int (*cmd_mail)(struct client *client, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+ int (*cmd_rcpt)(struct client *client, struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt);
+ int (*cmd_data)(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+ int (*local_deliver)(struct client *client,
+ struct lmtp_recipient *lrcpt,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct lmtp_local_deliver_context *lldctx);
+};
+
+struct client {
+ struct client *prev, *next;
+ pool_t pool;
+
+ struct lmtp_client_vfuncs v;
+ struct event *event;
+
+ const struct setting_parser_info *user_set_info;
+ const struct mail_user_settings *user_set;
+ const struct lda_settings *unexpanded_lda_set;
+ const struct lmtp_settings *lmtp_set;
+ const struct master_service_settings *service_set;
+
+ struct smtp_server_connection *conn;
+
+ struct ip_addr remote_ip, local_ip, real_local_ip, real_remote_ip;
+ in_port_t remote_port, local_port, real_local_port, real_remote_port;
+
+ struct mail_user *raw_mail_user;
+ const char *my_domain;
+
+ pool_t state_pool;
+ struct client_state state;
+ struct istream *dot_input;
+ struct lmtp_local *local;
+ struct lmtp_proxy *proxy;
+
+ /* Module-specific contexts. */
+ ARRAY(union lmtp_module_context *) module_contexts;
+
+ bool disconnected:1;
+ bool destroyed:1;
+};
+
+struct lmtp_module_register {
+ unsigned int id;
+};
+
+union lmtp_module_context {
+ struct lmtp_client_vfuncs super;
+ struct lmtp_module_register *reg;
+};
+extern struct lmtp_module_register lmtp_module_register;
+
+struct client *client_create(int fd_in, int fd_out,
+ const struct master_service_connection *conn);
+void client_destroy(struct client **client, const char *enh_code,
+ const char *reason) ATTR_NULL(2, 3);
+
+void client_state_reset(struct client *client);
+void client_update_data_state(struct client *client, const char *new_args);
+
+void clients_destroy(void);
+
+#endif
diff --git a/src/lmtp/lmtp-commands.c b/src/lmtp/lmtp-commands.c
new file mode 100644
index 0000000..dd8b791
--- /dev/null
+++ b/src/lmtp/lmtp-commands.c
@@ -0,0 +1,340 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "master-service.h"
+#include "settings-parser.h"
+#include "lda-settings.h"
+#include "mail-user.h"
+#include "smtp-address.h"
+#include "mail-deliver.h"
+#include "mail-error.h"
+#include "lmtp-recipient.h"
+#include "lmtp-proxy.h"
+#include "lmtp-local.h"
+#include "lmtp-commands.h"
+
+/*
+ * MAIL command
+ */
+
+int cmd_mail(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ struct client *client = (struct client *)conn_ctx;
+
+ return client->v.cmd_mail(client, cmd, data);
+}
+
+int client_default_cmd_mail(struct client *client,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_mail *data ATTR_UNUSED)
+{
+ if (client->lmtp_set->lmtp_user_concurrency_limit > 0) {
+ /* Connect to anvil before dropping privileges */
+ lmtp_anvil_init();
+ }
+ return 1;
+}
+
+/*
+ * RCPT command
+ */
+
+static int
+cmd_rcpt_handle_forward_fields(struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt)
+{
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ string_t *xforward;
+ const char *error;
+ int ret;
+
+ ret = smtp_params_rcpt_decode_extra(&rcpt->params,
+ LMTP_RCPT_FORWARD_PARAMETER,
+ &xforward, FALSE, &error);
+ if (ret < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid "LMTP_RCPT_FORWARD_PARAMETER"= "
+ "parameter: %s", error);
+ return -1;
+ }
+ if (ret == 0)
+ return 0;
+
+ /* Drop the parameter */
+ (void)smtp_params_rcpt_drop_extra(&rcpt->params,
+ LMTP_RCPT_FORWARD_PARAMETER, NULL);
+
+ /* Check the real IP rather than the proxied client IP, since XCLIENT
+ command will update that, thereby making it untrusted. Unlike the
+ XCLIENT command, the RCPT forward parameter needs to be used after
+ the XCLIENT is first issued. */
+ if (!smtp_server_connection_is_trusted(rcpt->conn)) {
+ smtp_server_reply(cmd, 550, "5.7.14",
+ "Unacceptable "LMTP_RCPT_FORWARD_PARAMETER"= "
+ "parameter: You are not from trusted IP");
+ return -1;
+ }
+
+ lrcpt->forward_fields = p_strdup(rcpt->pool, str_c(xforward));
+ return 0;
+}
+
+int cmd_rcpt(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_recipient *rcpt)
+{
+ struct client *client = (struct client *)conn_ctx;
+ struct smtp_server_transaction *trans;
+ struct lmtp_recipient *lrcpt;
+
+ trans = smtp_server_connection_get_transaction(rcpt->conn);
+ i_assert(trans != NULL); /* MAIL command is synchronous */
+
+ lrcpt = lmtp_recipient_create(client, trans, rcpt);
+
+ if (cmd_rcpt_handle_forward_fields(cmd, lrcpt) < 0)
+ return -1;
+
+ return client->v.cmd_rcpt(client, cmd, lrcpt);
+}
+
+int client_default_cmd_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt)
+{
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ const char *username, *detail;
+ char delim = '\0';
+ int ret;
+
+ i_assert(!smtp_address_isnull(rcpt->path));
+ if (*rcpt->path->localpart == '\0' && rcpt->path->domain == NULL) {
+ smtp_server_recipient_reply(
+ rcpt, 550, "5.1.1",
+ "Unacceptable TO: Empty path not allowed");
+ return -1;
+ }
+
+ smtp_address_detail_parse_temp(
+ client->unexpanded_lda_set->recipient_delimiter,
+ rcpt->path, &username, &delim, &detail);
+ i_assert(*username != '\0');
+
+ /* Make user name and detail available in the recipient event. The
+ mail_user event (for local delivery) also adds the user field, but
+ adding it here makes it available to the recipient event in general.
+ Additionally, the auth lookups performed for local and proxy delivery
+ can further override the "user" recipient event when the auth service
+ returns a different user name. In any case, we provide the initial
+ value here.
+ */
+ event_add_str(rcpt->event, "user", username);
+ if (detail[0] != '\0')
+ event_add_str(rcpt->event, "detail", detail);
+
+ if (client->lmtp_set->lmtp_proxy) {
+ /* proxied? */
+ if ((ret=lmtp_proxy_rcpt(client, cmd, lrcpt,
+ username, detail, delim)) != 0)
+ return (ret < 0 ? -1 : 0);
+ /* no */
+ }
+
+ /* local delivery */
+ return lmtp_local_rcpt(client, cmd, lrcpt, username, detail);
+}
+
+/*
+ * DATA command
+ */
+
+static void
+cmd_data_create_added_headers(struct client *client,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans)
+{
+ size_t proxy_offset = 0;
+ string_t *str;
+
+ str = t_str_new(512);
+
+ /* Headers for local deliveries only */
+ if (client->local != NULL)
+ lmtp_local_add_headers(client->local, trans, str);
+
+ /* Headers for local and proxied messages */
+ proxy_offset = str_len(str);
+ if (client->lmtp_set->lmtp_add_received_header) {
+ const struct lmtp_settings *lmtp_set = client->lmtp_set;
+ enum smtp_server_trace_rcpt_to_address rcpt_to_address =
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL;
+
+ switch (lmtp_set->parsed_lmtp_hdr_delivery_address) {
+ case LMTP_HDR_DELIVERY_ADDRESS_NONE:
+ rcpt_to_address =
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_NONE;
+ break;
+ case LMTP_HDR_DELIVERY_ADDRESS_FINAL:
+ rcpt_to_address =
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL;
+ break;
+ case LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL:
+ rcpt_to_address =
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_ORIGINAL;
+ break;
+ }
+
+ smtp_server_transaction_write_trace_record(
+ str, trans, rcpt_to_address);
+ }
+
+ client->state.added_headers_local =
+ p_strdup(client->state_pool, str_c(str));
+ client->state.added_headers_proxy =
+ client->state.added_headers_local + proxy_offset;
+}
+
+static int
+cmd_data_finish(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct client_state *state = &client->state;
+ struct istream *input_msg;
+ int ret;
+
+ i_assert(HAS_ALL_BITS(trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT));
+
+ client->state.data_end_timeval = ioloop_timeval;
+
+ /* finish the message */
+ input_msg = iostream_temp_finish(&state->mail_data_output,
+ IO_BLOCK_SIZE);
+
+ ret = client->v.cmd_data(client, cmd, trans,
+ input_msg, client->state.data_size);
+ i_stream_unref(&input_msg);
+
+ return ret;
+}
+
+int cmd_data_continue(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = (struct client *)conn_ctx;
+ struct client_state *state = &client->state;
+ struct istream *data_input = state->data_input;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ i_assert(state->mail_data_output != NULL);
+
+ while ((ret = i_stream_read(data_input)) > 0 || ret == -2) {
+ data = i_stream_get_data(data_input, &size);
+ if (o_stream_send(state->mail_data_output,
+ data, size) != (ssize_t)size) {
+ e_error(client->event, "write(%s) failed: %s",
+ o_stream_get_name(state->mail_data_output),
+ o_stream_get_error(state->mail_data_output));
+ smtp_server_reply(cmd, 451, "4.3.0",
+ "Temporary internal failure");
+ return -1;
+ }
+
+ 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;
+ }
+
+ /* Current data stream position is the data size */
+ client->state.data_size = data_input->v_offset;
+
+ /* The ending "." line was seen. finish delivery. */
+ return cmd_data_finish(client, cmd, trans);
+}
+
+int cmd_data_begin(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input)
+{
+ struct client *client = (struct client *)conn_ctx;
+ string_t *path;
+
+ i_assert(client->state.mail_data_output == NULL);
+
+ path = t_str_new(256);
+ mail_user_set_get_temp_prefix(path, client->raw_mail_user->set);
+ client->state.mail_data_output =
+ iostream_temp_create_named(str_c(path), 0, "(lmtp data)");
+
+ client->state.data_input = data_input;
+ return 0;
+}
+
+int client_default_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input,
+ uoff_t data_size ATTR_UNUSED)
+{
+ struct client_state *state = &client->state;
+ struct istream *input_local, *input_proxy;
+ struct istream *inputs[3];
+
+ /* Formulate prepended headers for both local and proxy delivery */
+ cmd_data_create_added_headers(client, cmd, trans);
+
+ /* Construct message streams for local and proxy delivery */
+ input_local = input_proxy = NULL;
+ if (client->local != NULL) {
+ inputs[0] = i_stream_create_from_data(
+ state->added_headers_local,
+ strlen(state->added_headers_local));
+ inputs[1] = data_input;
+ inputs[2] = NULL;
+
+ input_local = i_stream_create_concat(inputs);
+ i_stream_set_name(input_local, "<lmtp DATA local>");
+ i_stream_unref(&inputs[0]);
+ }
+ if (client->proxy != NULL) {
+ inputs[0] = i_stream_create_from_data(
+ state->added_headers_proxy,
+ strlen(state->added_headers_proxy));
+ inputs[1] = data_input;
+ inputs[2] = NULL;
+
+ input_proxy = i_stream_create_concat(inputs);
+ i_stream_set_name(input_proxy, "<lmtp DATA proxy>");
+ i_stream_unref(&inputs[0]);
+ }
+
+ /* local delivery */
+ if (client->local != NULL) {
+ lmtp_local_data(client, cmd, trans, input_local);
+ i_stream_unref(&input_local);
+ }
+ /* proxy delivery */
+ if (client->proxy != NULL) {
+ lmtp_proxy_data(client, cmd, trans, input_proxy);
+ i_stream_unref(&input_proxy);
+ }
+ return 0;
+}
diff --git a/src/lmtp/lmtp-commands.h b/src/lmtp/lmtp-commands.h
new file mode 100644
index 0000000..f580c86
--- /dev/null
+++ b/src/lmtp/lmtp-commands.h
@@ -0,0 +1,45 @@
+#ifndef COMMANDS_H
+#define COMMANDS_H
+
+struct client;
+struct smtp_server_cmd_ctx;
+struct smtp_server_cmd_helo;
+
+/*
+ * MAIL command
+ */
+
+int cmd_mail(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+
+int client_default_cmd_mail(struct client *client,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_mail *data ATTR_UNUSED);
+
+/*
+ * RCPT command
+ */
+
+int cmd_rcpt(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_recipient *rcpt);
+
+int client_default_cmd_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt);
+
+/*
+ * DATA command
+ */
+
+int cmd_data_continue(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans);
+int cmd_data_begin(void *conn_ctx, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input);
+
+int client_default_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+#endif
diff --git a/src/lmtp/lmtp-common.h b/src/lmtp/lmtp-common.h
new file mode 100644
index 0000000..8e1729c
--- /dev/null
+++ b/src/lmtp/lmtp-common.h
@@ -0,0 +1,35 @@
+#ifndef LMTP_COMMON_H
+#define LMTP_COMMON_H
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "smtp-reply.h"
+#include "smtp-server.h"
+#include "lmtp-client.h"
+#include "lmtp-settings.h"
+
+#define LMTP_RCPT_FORWARD_CAPABILITY "XRCPTFORWARD"
+#define LMTP_RCPT_FORWARD_PARAMETER "XRCPTFORWARD"
+
+typedef void lmtp_client_created_func_t(struct client **client);
+
+extern lmtp_client_created_func_t *hook_client_created;
+extern struct event_category event_category_lmtp;
+
+extern char *dns_client_socket_path, *base_dir;
+extern struct mail_storage_service_ctx *storage_service;
+extern struct anvil_client *anvil;
+
+extern struct smtp_server *lmtp_server;
+
+/* Sets the hook_client_created and returns the previous hook,
+ which the new_hook should call if it's non-NULL. */
+lmtp_client_created_func_t *
+lmtp_client_created_hook_set(lmtp_client_created_func_t *new_hook);
+
+void lmtp_anvil_init(void);
+
+#endif
diff --git a/src/lmtp/lmtp-local.c b/src/lmtp/lmtp-local.c
new file mode 100644
index 0000000..c20b619
--- /dev/null
+++ b/src/lmtp/lmtp-local.c
@@ -0,0 +1,766 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "str.h"
+#include "istream.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "restrict-access.h"
+#include "anvil-client.h"
+#include "settings-parser.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "mail-deliver.h"
+#include "mail-autoexpunge.h"
+#include "index/raw/raw-storage.h"
+#include "smtp-common.h"
+#include "smtp-params.h"
+#include "smtp-address.h"
+#include "smtp-submit-settings.h"
+#include "lda-settings.h"
+#include "lmtp-settings.h"
+#include "lmtp-recipient.h"
+#include "lmtp-local.h"
+
+struct lmtp_local_recipient {
+ struct lmtp_recipient *rcpt;
+
+ char *detail;
+
+ struct mail_storage_service_user *service_user;
+ struct anvil_query *anvil_query;
+
+ struct lmtp_local_recipient *duplicate;
+
+ bool anvil_connect_sent:1;
+};
+
+struct lmtp_local {
+ struct client *client;
+
+ ARRAY(struct lmtp_local_recipient *) rcpt_to;
+
+ struct mail *raw_mail, *first_saved_mail;
+ struct mail_user *rcpt_user;
+};
+
+/*
+ * LMTP local
+ */
+
+static struct lmtp_local *
+lmtp_local_init(struct client *client)
+{
+ struct lmtp_local *local;
+
+ local = i_new(struct lmtp_local, 1);
+ local->client = client;
+ i_array_init(&local->rcpt_to, 8);
+
+ return local;
+}
+
+void lmtp_local_deinit(struct lmtp_local **_local)
+{
+ struct lmtp_local *local = *_local;
+
+ *_local = NULL;
+
+ if (array_is_created(&local->rcpt_to))
+ array_free(&local->rcpt_to);
+
+ if (local->raw_mail != NULL) {
+ struct mailbox_transaction_context *raw_trans =
+ local->raw_mail->transaction;
+ struct mailbox *raw_box = local->raw_mail->box;
+
+ mail_free(&local->raw_mail);
+ mailbox_transaction_rollback(&raw_trans);
+ mailbox_free(&raw_box);
+ }
+
+ i_free(local);
+}
+
+/*
+ * Recipient
+ */
+
+static void
+lmtp_local_rcpt_anvil_disconnect(struct lmtp_local_recipient *llrcpt)
+{
+ const struct mail_storage_service_input *input;
+
+ if (!llrcpt->anvil_connect_sent)
+ return;
+ llrcpt->anvil_connect_sent = FALSE;
+
+ input = mail_storage_service_user_get_input(llrcpt->service_user);
+ master_service_anvil_send(master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\t", master_service_get_name(master_service),
+ "/", input->username, "\n", NULL));
+}
+
+static void
+lmtp_local_rcpt_destroy(struct smtp_server_recipient *rcpt ATTR_UNUSED,
+ struct lmtp_local_recipient *llrcpt)
+{
+ if (llrcpt->anvil_query != NULL)
+ anvil_client_query_abort(anvil, &llrcpt->anvil_query);
+ lmtp_local_rcpt_anvil_disconnect(llrcpt);
+ mail_storage_service_user_unref(&llrcpt->service_user);
+}
+
+static void
+lmtp_local_rcpt_reply_overquota(struct lmtp_local_recipient *llrcpt,
+ const char *error)
+{
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+ struct lda_settings *lda_set =
+ mail_storage_service_user_get_set(llrcpt->service_user)[2];
+
+ if (lda_set->quota_full_tempfail)
+ smtp_server_recipient_reply(rcpt, 452, "4.2.2", "%s", error);
+ else
+ smtp_server_recipient_reply(rcpt, 552, "5.2.2", "%s", error);
+}
+
+static void ATTR_FORMAT(4,5)
+lmtp_local_rcpt_fail_all(struct lmtp_local *local,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ struct lmtp_local_recipient *const *llrcpts;
+ const char *msg;
+ unsigned int count, i;
+ va_list args;
+
+ va_start(args, fmt);
+ msg = t_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ llrcpts = array_get(&local->rcpt_to, &count);
+ for (i = 0; i < count; i++) {
+ struct smtp_server_recipient *rcpt = llrcpts[i]->rcpt->rcpt;
+
+ smtp_server_recipient_reply(rcpt, status, enh_code, "%s", msg);
+ }
+}
+
+/*
+ * RCPT command
+ */
+
+static int
+lmtp_local_rcpt_check_quota(struct lmtp_local_recipient *llrcpt)
+{
+ struct client *client = llrcpt->rcpt->client;
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+ struct smtp_address *address = rcpt->path;
+ struct mail_user *user;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct mailbox_status status;
+ enum mail_error mail_error;
+ const char *error;
+ int ret;
+
+ if (!client->lmtp_set->lmtp_rcpt_check_quota)
+ return 0;
+
+ /* mail user will be created second time when mail is saved,
+ so it's session_id needs to be different,
+ but second time session_id needs to be the same as rcpt session_id and
+ mail user session id for the first rcpt should not overlap with session id
+ of the second recipient, so add custom ":quota" suffix to the session_id without
+ session_id counter increment, so next time mail user will get
+ the same session id as rcpt */
+ ret = mail_storage_service_next_with_session_suffix(storage_service,
+ llrcpt->service_user,
+ "quota",
+ &user, &error);
+
+ if (ret < 0) {
+ e_error(rcpt->event, "Failed to initialize user %s: %s",
+ smtp_address_encode(address), error);
+ ret = -1;
+ } else {
+ /* Set the log prefix for the user. The default log prefix is
+ automatically restored later when user context gets
+ deactivated. */
+ i_set_failure_prefix("%s",
+ mail_storage_service_user_get_log_prefix(llrcpt->service_user));
+ ns = mail_namespace_find_inbox(user->namespaces);
+ box = mailbox_alloc(ns->list, "INBOX", 0);
+ ret = mailbox_get_status(box, STATUS_CHECK_OVER_QUOTA, &status);
+ if (ret < 0) {
+ error = mailbox_get_last_error(box, &mail_error);
+ if (mail_error == MAIL_ERROR_NOQUOTA) {
+ lmtp_local_rcpt_reply_overquota(llrcpt, error);
+ } else {
+ e_error(rcpt->event,
+ "mailbox_get_status(%s, STATUS_CHECK_OVER_QUOTA) "
+ "failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ ret = -1;
+ }
+ mailbox_free(&box);
+ mail_user_deinit(&user);
+ mail_storage_service_io_deactivate_user(llrcpt->service_user);
+ }
+
+ if (ret < 0 && !smtp_server_recipient_is_replied(rcpt)) {
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0",
+ "Temporary internal error");
+ }
+ return ret;
+}
+
+static void
+lmtp_local_rcpt_approved(struct smtp_server_recipient *rcpt,
+ struct lmtp_local_recipient *llrcpt)
+{
+ struct client *client = llrcpt->rcpt->client;
+ struct lmtp_recipient *drcpt;
+
+ /* resolve duplicate recipient */
+ drcpt = lmtp_recipient_find_duplicate(llrcpt->rcpt, rcpt->trans);
+ if (drcpt != NULL) {
+ i_assert(drcpt->type == LMTP_RECIPIENT_TYPE_LOCAL);
+ llrcpt->duplicate = drcpt->backend_context;
+ i_assert(llrcpt->duplicate->duplicate == NULL);
+ }
+
+ /* add to local recipients */
+ array_push_back(&client->local->rcpt_to, &llrcpt);
+}
+
+static bool
+lmtp_local_rcpt_anvil_finish(struct lmtp_local_recipient *llrcpt)
+{
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+ struct smtp_server_cmd_ctx *cmd = rcpt->cmd;
+
+ if (lmtp_local_rcpt_check_quota(llrcpt) < 0)
+ return FALSE;
+
+ smtp_server_cmd_rcpt_reply_success(cmd);
+ return TRUE;
+}
+
+static void
+lmtp_local_rcpt_anvil_cb(const char *reply, void *context)
+{
+ struct lmtp_local_recipient *llrcpt =
+ (struct lmtp_local_recipient *)context;
+ struct client *client = llrcpt->rcpt->client;
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+ const struct mail_storage_service_input *input;
+ unsigned int parallel_count = 0;
+
+ llrcpt->anvil_query = NULL;
+ if (reply == NULL) {
+ /* lookup failed */
+ } else if (str_to_uint(reply, &parallel_count) < 0) {
+ e_error(rcpt->event, "Invalid reply from anvil: %s", reply);
+ }
+
+ if (parallel_count >= client->lmtp_set->lmtp_user_concurrency_limit) {
+ smtp_server_recipient_reply(
+ rcpt, 451, "4.3.0",
+ "Too many concurrent deliveries for user");
+ } else if (lmtp_local_rcpt_anvil_finish(llrcpt)) {
+ llrcpt->anvil_connect_sent = TRUE;
+ input = mail_storage_service_user_get_input(llrcpt->service_user);
+ master_service_anvil_send(master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\t", master_service_get_name(master_service),
+ "/", input->username, "\n", NULL));
+ }
+}
+
+int lmtp_local_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct lmtp_recipient *lrcpt, const char *username,
+ const char *detail)
+{
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct lmtp_local_recipient *llrcpt;
+ struct mail_storage_service_input input;
+ struct mail_storage_service_user *service_user;
+ const char *error = NULL;
+ int ret = 0;
+
+ i_zero(&input);
+ input.module = input.service = "lmtp";
+ input.username = username;
+ input.local_ip = client->local_ip;
+ input.remote_ip = client->remote_ip;
+ input.local_port = client->local_port;
+ input.remote_port = client->remote_port;
+ input.session_id = lrcpt->session_id;
+ input.conn_ssl_secured =
+ smtp_server_connection_is_ssl_secured(client->conn);
+ input.conn_secured = input.conn_ssl_secured ||
+ smtp_server_connection_is_trusted(client->conn);
+ input.forward_fields = lrcpt->forward_fields;
+ input.event_parent = rcpt->event;
+
+ ret = mail_storage_service_lookup(storage_service, &input,
+ &service_user, &error);
+ if (ret < 0) {
+ e_error(rcpt->event, "Failed to lookup user %s: %s",
+ username, error);
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0",
+ "Temporary internal error");
+ return -1;
+ }
+ if (ret == 0) {
+ smtp_server_recipient_reply(rcpt, 550, "5.1.1",
+ "User doesn't exist: %s",
+ username);
+ return -1;
+ }
+
+ if (client->local == NULL)
+ client->local = lmtp_local_init(client);
+
+ llrcpt = p_new(rcpt->pool, struct lmtp_local_recipient, 1);
+ llrcpt->rcpt = lrcpt;
+ llrcpt->detail = p_strdup(rcpt->pool, detail);
+ llrcpt->service_user = service_user;
+
+ lrcpt->type = LMTP_RECIPIENT_TYPE_LOCAL;
+ lrcpt->backend_context = llrcpt;
+
+ smtp_server_recipient_add_hook(
+ rcpt, SMTP_SERVER_RECIPIENT_HOOK_DESTROY,
+ lmtp_local_rcpt_destroy, llrcpt);
+ smtp_server_recipient_add_hook(
+ rcpt, SMTP_SERVER_RECIPIENT_HOOK_APPROVED,
+ lmtp_local_rcpt_approved, llrcpt);
+
+ if (client->lmtp_set->lmtp_user_concurrency_limit == 0) {
+ (void)lmtp_local_rcpt_anvil_finish(llrcpt);
+ } else {
+ /* NOTE: username may change as the result of the userdb
+ lookup. Look up the new one via service_user. */
+ const struct mail_storage_service_input *input =
+ mail_storage_service_user_get_input(llrcpt->service_user);
+ const char *query = t_strconcat("LOOKUP\t",
+ master_service_get_name(master_service),
+ "/", str_tabescape(input->username), NULL);
+ llrcpt->anvil_query = anvil_client_query(anvil, query,
+ lmtp_local_rcpt_anvil_cb, llrcpt);
+ return 0;
+ }
+
+ return 1;
+}
+
+/*
+ * DATA command
+ */
+
+void lmtp_local_add_headers(struct lmtp_local *local,
+ struct smtp_server_transaction *trans,
+ string_t *headers)
+{
+ struct client *client = local->client;
+ const struct lmtp_settings *lmtp_set = client->lmtp_set;
+ struct lmtp_local_recipient *const *llrcpts;
+ const struct smtp_address *rcpt_to = NULL;
+ unsigned int count;
+
+ str_printfa(headers, "Return-Path: <%s>\r\n",
+ smtp_address_encode(trans->mail_from));
+
+ llrcpts = array_get(&local->rcpt_to, &count);
+ if (count == 1) {
+ struct smtp_server_recipient *rcpt = llrcpts[0]->rcpt->rcpt;
+
+ switch (lmtp_set->parsed_lmtp_hdr_delivery_address) {
+ case LMTP_HDR_DELIVERY_ADDRESS_NONE:
+ break;
+ case LMTP_HDR_DELIVERY_ADDRESS_FINAL:
+ rcpt_to = rcpt->path;
+ break;
+ case LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL:
+ rcpt_to = rcpt->params.orcpt.addr;
+ break;
+ }
+ }
+ if (rcpt_to != NULL) {
+ str_printfa(headers, "Delivered-To: %s\r\n",
+ smtp_address_encode(rcpt_to));
+ }
+}
+
+static int
+lmtp_local_deliver(struct lmtp_local *local,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans,
+ struct lmtp_local_recipient *llrcpt,
+ struct mail *src_mail,
+ struct mail_deliver_session *session)
+{
+ struct client *client = local->client;
+ struct lmtp_recipient *lrcpt = llrcpt->rcpt;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct mail_storage_service_user *service_user = llrcpt->service_user;
+ struct lmtp_local_deliver_context lldctx;
+ struct mail_user *rcpt_user;
+ const struct mail_storage_service_input *input;
+ const struct mail_storage_settings *mail_set;
+ struct smtp_submit_settings *smtp_set;
+ struct smtp_proxy_data proxy_data;
+ struct lda_settings *lda_set;
+ struct mail_namespace *ns;
+ struct setting_parser_context *set_parser;
+ const struct var_expand_table *var_table;
+ void **sets;
+ const char *line, *error, *username;
+ int ret;
+
+ input = mail_storage_service_user_get_input(service_user);
+ username = t_strdup(input->username);
+
+ mail_set = mail_storage_service_user_get_mail_set(service_user);
+ set_parser = mail_storage_service_user_get_settings_parser(service_user);
+
+ smtp_server_connection_get_proxy_data
+ (client->conn, &proxy_data);
+ if (proxy_data.timeout_secs > 0 &&
+ (mail_set->mail_max_lock_timeout == 0 ||
+ mail_set->mail_max_lock_timeout > proxy_data.timeout_secs)) {
+ /* set lock timeout waits to be less than when proxy has
+ advertised that it's going to timeout the connection.
+ this avoids duplicate deliveries in case the delivery
+ succeeds after the proxy has already disconnected from us. */
+ line = t_strdup_printf("mail_max_lock_timeout=%us",
+ proxy_data.timeout_secs <= 1 ? 1 :
+ proxy_data.timeout_secs-1);
+ if (settings_parse_line(set_parser, line) < 0)
+ i_unreached();
+ }
+
+ i_zero(&lldctx);
+ lldctx.session_id = lrcpt->session_id;
+ lldctx.src_mail = src_mail;
+ lldctx.session = session;
+
+ /* get the timestamp before user is created, since it starts the I/O */
+ io_loop_time_refresh();
+ lldctx.delivery_time_started = ioloop_timeval;
+
+ client_update_data_state(client, username);
+ if (mail_storage_service_next(storage_service, service_user,
+ &rcpt_user, &error) < 0) {
+ e_error(rcpt->event, "Failed to initialize user: %s", error);
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0",
+ "Temporary internal error");
+ return -1;
+ }
+ local->rcpt_user = rcpt_user;
+
+ sets = mail_storage_service_user_get_set(service_user);
+ var_table = mail_user_var_expand_table(rcpt_user);
+ smtp_set = sets[1];
+ lda_set = sets[2];
+ ret = settings_var_expand(
+ &smtp_submit_setting_parser_info,
+ smtp_set, client->pool, var_table,
+ &error);
+ if (ret > 0) {
+ ret = settings_var_expand(
+ &lda_setting_parser_info,
+ lda_set, client->pool, var_table,
+ &error);
+ }
+ if (ret <= 0) {
+ e_error(rcpt->event, "Failed to expand settings: %s", error);
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0",
+ "Temporary internal error");
+ return -1;
+ }
+
+ /* Set the log prefix for the user. The default log prefix is
+ automatically restored later when user context gets deactivated. */
+ i_set_failure_prefix("%s",
+ mail_storage_service_user_get_log_prefix(service_user));
+
+ lldctx.rcpt_user = rcpt_user;
+ lldctx.smtp_set = smtp_set;
+ lldctx.lda_set = lda_set;
+
+ if (*llrcpt->detail == '\0' ||
+ !client->lmtp_set->lmtp_save_to_detail_mailbox)
+ lldctx.rcpt_default_mailbox = "INBOX";
+ else {
+ ns = mail_namespace_find_inbox(rcpt_user->namespaces);
+ lldctx.rcpt_default_mailbox =
+ t_strconcat(ns->prefix, llrcpt->detail, NULL);
+ }
+
+ ret = client->v.local_deliver(client, lrcpt, cmd, trans, &lldctx);
+
+ lmtp_local_rcpt_anvil_disconnect(llrcpt);
+ return ret;
+}
+
+static int
+lmtp_local_default_do_deliver(struct lmtp_local *local,
+ struct lmtp_local_recipient *llrcpt,
+ struct lmtp_local_deliver_context *lldctx,
+ struct mail_deliver_context *dctx)
+{
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+ enum mail_deliver_error error_code;
+ const char *error;
+
+ if (mail_deliver(dctx, &error_code, &error) == 0) {
+ if (dctx->dest_mail != NULL) {
+ i_assert(local->first_saved_mail == NULL);
+ local->first_saved_mail = dctx->dest_mail;
+ }
+ smtp_server_recipient_reply(rcpt, 250, "2.0.0", "%s Saved",
+ lldctx->session_id);
+ return 0;
+ }
+
+ switch (error_code) {
+ case MAIL_DELIVER_ERROR_NONE:
+ i_unreached();
+ case MAIL_DELIVER_ERROR_TEMPORARY:
+ smtp_server_recipient_reply(rcpt, 451, "4.2.0", "%s", error);
+ break;
+ case MAIL_DELIVER_ERROR_REJECTED:
+ smtp_server_recipient_reply(rcpt, 552, "5.2.0", "%s", error);
+ break;
+ case MAIL_DELIVER_ERROR_NOQUOTA:
+ lmtp_local_rcpt_reply_overquota(llrcpt, error);
+ break;
+ case MAIL_DELIVER_ERROR_INTERNAL:
+ /* This shouldn't happen */
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0", "%s", error);
+ break;
+ }
+
+ return -1;
+}
+
+int lmtp_local_default_deliver(struct client *client,
+ struct lmtp_recipient *lrcpt,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans,
+ struct lmtp_local_deliver_context *lldctx)
+{
+ struct lmtp_local *local = client->local;
+ struct lmtp_local_recipient *llrcpt = lrcpt->backend_context;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct smtp_address *rcpt_to = rcpt->path;
+ struct mail_deliver_input dinput;
+ struct mail_deliver_context dctx;
+ struct event *event;
+ int ret;
+
+ event = event_create(rcpt->event);
+ event_drop_parent_log_prefixes(event, 3);
+
+ i_zero(&dinput);
+ dinput.session = lldctx->session;
+ dinput.set = lldctx->lda_set;
+ dinput.smtp_set = lldctx->smtp_set;
+ dinput.session_id = lldctx->session_id;
+ dinput.event_parent = event;
+ dinput.src_mail = lldctx->src_mail;
+
+ /* MAIL FROM */
+ dinput.mail_from = trans->mail_from;
+ dinput.mail_params = trans->params;
+
+ /* RCPT TO */
+ dinput.rcpt_user = lldctx->rcpt_user;
+ dinput.rcpt_params = rcpt->params;
+ if (dinput.rcpt_params.orcpt.addr == NULL &&
+ *dinput.set->lda_original_recipient_header != '\0') {
+ dinput.rcpt_params.orcpt.addr =
+ mail_deliver_get_address(
+ lldctx->src_mail,
+ dinput.set->lda_original_recipient_header);
+ }
+ if (dinput.rcpt_params.orcpt.addr == NULL)
+ dinput.rcpt_params.orcpt.addr = rcpt_to;
+ dinput.rcpt_to = rcpt_to;
+ dinput.rcpt_default_mailbox = lldctx->rcpt_default_mailbox;
+
+ dinput.save_dest_mail = array_count(&trans->rcpt_to) > 1 &&
+ local->first_saved_mail == NULL;
+
+ dinput.session_time_msecs =
+ timeval_diff_msecs(&client->state.data_end_timeval,
+ &trans->timestamp);
+ dinput.delivery_time_started = lldctx->delivery_time_started;
+
+ mail_deliver_init(&dctx, &dinput);
+ ret = lmtp_local_default_do_deliver(local, llrcpt, lldctx, &dctx);
+ mail_deliver_deinit(&dctx);
+ event_unref(&event);
+
+ return ret;
+}
+
+static uid_t
+lmtp_local_deliver_to_rcpts(struct lmtp_local *local,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct mail_deliver_session *session)
+{
+ struct client *client = local->client;
+ uid_t first_uid = (uid_t)-1;
+ struct mail *src_mail;
+ struct lmtp_local_recipient *const *llrcpts;
+ unsigned int count, i;
+ int ret;
+
+ src_mail = local->raw_mail;
+ llrcpts = array_get(&local->rcpt_to, &count);
+ for (i = 0; i < count; i++) {
+ struct lmtp_local_recipient *llrcpt = llrcpts[i];
+ struct smtp_server_recipient *rcpt = llrcpt->rcpt->rcpt;
+
+ if (llrcpt->duplicate != NULL) {
+ struct smtp_server_recipient *drcpt =
+ llrcpt->duplicate->rcpt->rcpt;
+ /* don't deliver more than once to the same recipient */
+ smtp_server_reply_submit_duplicate(cmd, rcpt->index,
+ drcpt->index);
+ continue;
+ }
+
+ ret = lmtp_local_deliver(local, cmd,
+ trans, llrcpt, src_mail, session);
+ client_update_data_state(client, NULL);
+
+ /* succeeded and mail_user is not saved in first_saved_mail */
+ if ((ret == 0 &&
+ (local->first_saved_mail == NULL ||
+ local->first_saved_mail == src_mail)) ||
+ /* failed. try the next one. */
+ (ret != 0 && local->rcpt_user != NULL)) {
+ if (i == (count - 1))
+ mail_user_autoexpunge(local->rcpt_user);
+ mail_storage_service_io_deactivate_user(local->rcpt_user->_service_user);
+ mail_user_deinit(&local->rcpt_user);
+ } else if (ret == 0) {
+ /* use the first saved message to save it elsewhere too.
+ this might allow hard linking the files.
+ mail_user is saved in first_saved_mail,
+ will be unreferenced later on */
+ mail_storage_service_io_deactivate_user(local->rcpt_user->_service_user);
+ local->rcpt_user = NULL;
+ src_mail = local->first_saved_mail;
+ first_uid = geteuid();
+ i_assert(first_uid != 0);
+ } else if (local->rcpt_user != NULL) {
+ mail_storage_service_io_deactivate_user(local->rcpt_user->_service_user);
+ }
+ }
+ return first_uid;
+}
+
+static int
+lmtp_local_open_raw_mail(struct lmtp_local *local,
+ struct smtp_server_transaction *trans,
+ struct istream *input)
+{
+ static const char *wanted_headers[] = {
+ "From", "To", "Message-ID", "Subject", "Return-Path",
+ NULL
+ };
+ struct client *client = local->client;
+ struct mailbox *box;
+ struct mailbox_transaction_context *mtrans;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ enum mail_error error;
+
+ if (raw_mailbox_alloc_stream(client->raw_mail_user, input,
+ (time_t)-1, smtp_address_encode(trans->mail_from),
+ &box) < 0) {
+ e_error(client->event, "Can't open delivery mail as raw: %s",
+ mailbox_get_last_internal_error(box, &error));
+ mailbox_free(&box);
+ lmtp_local_rcpt_fail_all(local, 451, "4.3.0",
+ "Temporary internal error");
+ return -1;
+ }
+
+ mtrans = mailbox_transaction_begin(box, 0, __func__);
+
+ headers_ctx = mailbox_header_lookup_init(box, wanted_headers);
+ local->raw_mail = mail_alloc(mtrans, 0, headers_ctx);
+ mailbox_header_lookup_unref(&headers_ctx);
+ mail_set_seq(local->raw_mail, 1);
+ return 0;
+}
+
+void lmtp_local_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *input)
+{
+ struct lmtp_local *local = client->local;
+ struct mail_deliver_session *session;
+ uid_t old_uid, first_uid;
+
+ if (lmtp_local_open_raw_mail(local, trans, input) < 0)
+ return;
+
+ session = mail_deliver_session_init();
+ old_uid = geteuid();
+ first_uid = lmtp_local_deliver_to_rcpts(local, cmd, trans, session);
+ mail_deliver_session_deinit(&session);
+
+ if (local->first_saved_mail != NULL) {
+ struct mail *mail = local->first_saved_mail;
+ struct mailbox_transaction_context *trans = mail->transaction;
+ struct mailbox *box = trans->box;
+ struct mail_user *user = box->storage->user;
+
+ /* just in case these functions are going to write anything,
+ change uid back to user's own one */
+ if (first_uid != old_uid) {
+ if (seteuid(0) < 0)
+ i_fatal("seteuid(0) failed: %m");
+ if (seteuid(first_uid) < 0)
+ i_fatal("seteuid() failed: %m");
+ }
+
+ mail_storage_service_io_activate_user(user->_service_user);
+ mail_free(&mail);
+ mailbox_transaction_rollback(&trans);
+ mailbox_free(&box);
+ mail_user_autoexpunge(user);
+ mail_storage_service_io_deactivate_user(user->_service_user);
+ mail_user_deinit(&user);
+ }
+
+ if (old_uid == 0) {
+ /* switch back to running as root, since that's what we're
+ practically doing anyway. it's also important in case we
+ lose e.g. config connection and need to reconnect to it. */
+ if (seteuid(0) < 0)
+ i_fatal("seteuid(0) failed: %m");
+ /* enable core dumping again. we need to chdir also to
+ root-owned directory to get core dumps. */
+ restrict_access_allow_coredumps(TRUE);
+ if (chdir(base_dir) < 0) {
+ e_error(client->event,
+ "chdir(%s) failed: %m", base_dir);
+ }
+ }
+}
diff --git a/src/lmtp/lmtp-local.h b/src/lmtp/lmtp-local.h
new file mode 100644
index 0000000..8ef942f
--- /dev/null
+++ b/src/lmtp/lmtp-local.h
@@ -0,0 +1,34 @@
+#ifndef LMTP_LOCAL_H
+#define LMTP_LOCAL_H
+
+#include "net.h"
+
+struct mail_deliver_session;
+struct smtp_server_cmd_ctx;
+struct smtp_server_cmd_rcpt;
+struct lmtp_local;
+struct client;
+
+void lmtp_local_deinit(struct lmtp_local **_local);
+
+int lmtp_local_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt, const char *username,
+ const char *detail);
+
+void lmtp_local_add_headers(struct lmtp_local *local,
+ struct smtp_server_transaction *trans,
+ string_t *headers);
+
+int lmtp_local_default_deliver(struct client *client,
+ struct lmtp_recipient *lrcpt,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct lmtp_local_deliver_context *lldctx);
+
+void lmtp_local_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *input);
+
+#endif
diff --git a/src/lmtp/lmtp-proxy.c b/src/lmtp/lmtp-proxy.c
new file mode 100644
index 0000000..bf03c75
--- /dev/null
+++ b/src/lmtp/lmtp-proxy.c
@@ -0,0 +1,860 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "istream.h"
+#include "istream-sized.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "smtp-common.h"
+#include "smtp-params.h"
+#include "smtp-address.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+#include "auth-master.h"
+#include "master-service-ssl-settings.h"
+#include "mail-storage-service.h"
+#include "lda-settings.h"
+#include "lmtp-recipient.h"
+#include "lmtp-proxy.h"
+
+#define LMTP_MAX_REPLY_SIZE 4096
+#define LMTP_PROXY_DEFAULT_TIMEOUT_MSECS (1000*125)
+
+enum lmtp_proxy_ssl_flags {
+ /* Use SSL/TLS enabled */
+ PROXY_SSL_FLAG_YES = 0x01,
+ /* Don't do SSL handshake immediately after connected */
+ PROXY_SSL_FLAG_STARTTLS = 0x02,
+ /* Don't require that the received certificate is valid */
+ PROXY_SSL_FLAG_ANY_CERT = 0x04
+};
+
+struct lmtp_proxy_rcpt_settings {
+ enum smtp_protocol protocol;
+ const char *host;
+ struct ip_addr hostip, source_ip;
+ in_port_t port;
+ enum lmtp_proxy_ssl_flags ssl_flags;
+ unsigned int timeout_msecs;
+ struct smtp_params_rcpt params;
+
+ bool proxy_not_trusted:1;
+};
+
+struct lmtp_proxy_recipient {
+ struct lmtp_recipient *rcpt;
+ struct lmtp_proxy_connection *conn;
+
+ struct smtp_address *address;
+
+ const unsigned char *forward_fields;
+ size_t forward_fields_size;
+
+ bool rcpt_to_failed:1;
+ bool data_reply_received:1;
+};
+
+struct lmtp_proxy_connection {
+ struct lmtp_proxy *proxy;
+ struct lmtp_proxy_rcpt_settings set;
+ char *host;
+
+ struct smtp_client_connection *lmtp_conn;
+ struct smtp_client_transaction *lmtp_trans;
+ struct istream *data_input;
+ struct timeout *to;
+
+ bool finished:1;
+ bool failed:1;
+};
+
+struct lmtp_proxy {
+ struct client *client;
+
+ struct smtp_server_transaction *trans;
+
+ struct smtp_client *lmtp_client;
+
+ ARRAY(struct lmtp_proxy_connection *) connections;
+ ARRAY(struct lmtp_proxy_recipient *) rcpt_to;
+ unsigned int next_data_reply_idx;
+
+ struct timeout *to_finish;
+ struct istream *data_input;
+
+ unsigned int max_timeout_msecs;
+ unsigned int proxy_session_seq;
+
+ bool finished:1;
+};
+
+static void
+lmtp_proxy_data_cb(const struct smtp_reply *reply,
+ struct lmtp_proxy_recipient *lprcpt);
+
+/*
+ * LMTP proxy
+ */
+
+static struct lmtp_proxy *
+lmtp_proxy_init(struct client *client,
+ struct smtp_server_transaction *trans)
+{
+ const char *extra_capabilities[] = {
+ LMTP_RCPT_FORWARD_CAPABILITY,
+ NULL };
+ struct smtp_client_settings lmtp_set;
+ struct lmtp_proxy *proxy;
+
+ proxy = i_new(struct lmtp_proxy, 1);
+ proxy->client = client;
+ proxy->trans = trans;
+ i_array_init(&proxy->rcpt_to, 32);
+ i_array_init(&proxy->connections, 32);
+
+ i_zero(&lmtp_set);
+ lmtp_set.my_hostname = client->my_domain;
+ lmtp_set.extra_capabilities = extra_capabilities;
+ lmtp_set.dns_client_socket_path = dns_client_socket_path;
+ lmtp_set.max_reply_size = LMTP_MAX_REPLY_SIZE;
+ lmtp_set.rawlog_dir = client->lmtp_set->lmtp_proxy_rawlog_dir;
+
+ smtp_server_connection_get_proxy_data(client->conn,
+ &lmtp_set.proxy_data);
+ lmtp_set.proxy_data.source_ip = client->remote_ip;
+ lmtp_set.proxy_data.source_port = client->remote_port;
+ /* This initial session_id is used only locally by lib-smtp. Each LMTP
+ proxy connection gets a more specific updated session_id. */
+ lmtp_set.proxy_data.session = trans->id;
+ if (lmtp_set.proxy_data.ttl_plus_1 == 0)
+ lmtp_set.proxy_data.ttl_plus_1 = LMTP_PROXY_DEFAULT_TTL + 1;
+ else
+ lmtp_set.proxy_data.ttl_plus_1--;
+ lmtp_set.event_parent = client->event;
+
+ proxy->lmtp_client = smtp_client_init(&lmtp_set);
+
+ return proxy;
+}
+
+static void lmtp_proxy_connection_deinit(struct lmtp_proxy_connection *conn)
+{
+ if (conn->lmtp_trans != NULL)
+ smtp_client_transaction_destroy(&conn->lmtp_trans);
+ if (conn->lmtp_conn != NULL)
+ smtp_client_connection_close(&conn->lmtp_conn);
+ timeout_remove(&conn->to);
+ i_stream_unref(&conn->data_input);
+ i_free(conn->host);
+ i_free(conn);
+}
+
+void lmtp_proxy_deinit(struct lmtp_proxy **_proxy)
+{
+ struct lmtp_proxy *proxy = *_proxy;
+ struct lmtp_proxy_connection *conn;
+
+ *_proxy = NULL;
+
+ array_foreach_elem(&proxy->connections, conn)
+ lmtp_proxy_connection_deinit(conn);
+
+ smtp_client_deinit(&proxy->lmtp_client);
+ i_stream_unref(&proxy->data_input);
+ timeout_remove(&proxy->to_finish);
+ array_free(&proxy->rcpt_to);
+ array_free(&proxy->connections);
+ i_free(proxy);
+}
+
+static void
+lmtp_proxy_mail_cb(const struct smtp_reply *proxy_reply ATTR_UNUSED,
+ struct lmtp_proxy_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void lmtp_proxy_connection_finish(struct lmtp_proxy_connection *conn)
+{
+ conn->finished = TRUE;
+ conn->lmtp_trans = NULL;
+}
+
+static void
+lmtp_proxy_connection_init_ssl(struct lmtp_proxy_connection *conn,
+ struct ssl_iostream_settings *ssl_set_r,
+ enum smtp_client_connection_ssl_mode *ssl_mode_r)
+{
+ const struct master_service_ssl_settings *master_ssl_set;
+
+ *ssl_mode_r = SMTP_CLIENT_SSL_MODE_NONE;
+
+ if ((conn->set.ssl_flags & PROXY_SSL_FLAG_YES) == 0) {
+ i_zero(ssl_set_r);
+ return;
+ }
+
+ 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_r);
+ if ((conn->set.ssl_flags & PROXY_SSL_FLAG_ANY_CERT) != 0)
+ ssl_set_r->allow_invalid_cert = TRUE;
+
+ if ((conn->set.ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0)
+ *ssl_mode_r = SMTP_CLIENT_SSL_MODE_IMMEDIATE;
+ else
+ *ssl_mode_r = SMTP_CLIENT_SSL_MODE_STARTTLS;
+}
+
+static bool
+lmtp_proxy_connection_has_rcpt_forward(struct lmtp_proxy_connection *conn)
+{
+ const struct smtp_capability_extra *cap_extra =
+ smtp_client_connection_get_extra_capability(
+ conn->lmtp_conn, LMTP_RCPT_FORWARD_CAPABILITY);
+
+ return (cap_extra != NULL);
+}
+
+static struct lmtp_proxy_connection *
+lmtp_proxy_get_connection(struct lmtp_proxy *proxy,
+ const struct lmtp_proxy_rcpt_settings *set)
+{
+ static const char *rcpt_param_extensions[] =
+ { LMTP_RCPT_FORWARD_PARAMETER, NULL };
+ static const struct smtp_client_capability_extra cap_rcpt_forward = {
+ .name = LMTP_RCPT_FORWARD_CAPABILITY,
+ .rcpt_param_extensions = rcpt_param_extensions,
+ };
+ struct smtp_client_settings lmtp_set;
+ struct smtp_server_transaction *trans = proxy->trans;
+ struct client *client = proxy->client;
+ struct lmtp_proxy_connection *conn;
+ enum smtp_client_connection_ssl_mode ssl_mode;
+ struct ssl_iostream_settings ssl_set;
+
+ i_assert(set->timeout_msecs > 0);
+
+ array_foreach_elem(&proxy->connections, conn) {
+ if (conn->set.protocol == set->protocol &&
+ conn->set.port == set->port &&
+ strcmp(conn->set.host, set->host) == 0 &&
+ net_ip_compare(&conn->set.hostip, &set->hostip) &&
+ net_ip_compare(&conn->set.source_ip, &set->source_ip) &&
+ conn->set.ssl_flags == set->ssl_flags)
+ return conn;
+ }
+
+ conn = i_new(struct lmtp_proxy_connection, 1);
+ conn->proxy = proxy;
+ conn->set.protocol = set->protocol;
+ conn->set.hostip = set->hostip;
+ conn->host = i_strdup(set->host);
+ conn->set.host = conn->host;
+ conn->set.source_ip = set->source_ip;
+ conn->set.port = set->port;
+ conn->set.ssl_flags = set->ssl_flags;
+ conn->set.timeout_msecs = set->timeout_msecs;
+ array_push_back(&proxy->connections, &conn);
+
+ lmtp_proxy_connection_init_ssl(conn, &ssl_set, &ssl_mode);
+
+ i_zero(&lmtp_set);
+ lmtp_set.my_ip = conn->set.source_ip;
+ lmtp_set.ssl = &ssl_set;
+ lmtp_set.peer_trusted = !conn->set.proxy_not_trusted;
+ lmtp_set.forced_capabilities = SMTP_CAPABILITY__ORCPT;
+ lmtp_set.mail_send_broken_path = TRUE;
+ lmtp_set.verbose_user_errors = client->lmtp_set->lmtp_verbose_replies;
+
+ if (conn->set.hostip.family != 0) {
+ conn->lmtp_conn = smtp_client_connection_create_ip(
+ proxy->lmtp_client, set->protocol,
+ &conn->set.hostip, conn->set.port,
+ conn->set.host, ssl_mode, &lmtp_set);
+ } else {
+ conn->lmtp_conn = smtp_client_connection_create(
+ proxy->lmtp_client, set->protocol,
+ conn->set.host, conn->set.port,
+ ssl_mode, &lmtp_set);
+ }
+ struct smtp_proxy_data proxy_data = {
+ .session = t_strdup_printf("%s:P%u", proxy->trans->id,
+ ++proxy->proxy_session_seq),
+ };
+ smtp_client_connection_update_proxy_data(conn->lmtp_conn, &proxy_data);
+ smtp_client_connection_accept_extra_capability(conn->lmtp_conn,
+ &cap_rcpt_forward);
+ smtp_client_connection_connect(conn->lmtp_conn, NULL, NULL);
+
+ conn->lmtp_trans = smtp_client_transaction_create(
+ conn->lmtp_conn, trans->mail_from, &trans->params, 0,
+ lmtp_proxy_connection_finish, conn);
+
+ smtp_client_transaction_start(conn->lmtp_trans,
+ lmtp_proxy_mail_cb, conn);
+
+ if (proxy->max_timeout_msecs < set->timeout_msecs)
+ proxy->max_timeout_msecs = set->timeout_msecs;
+ return conn;
+}
+
+static void
+lmtp_proxy_handle_connection_error(struct lmtp_proxy_recipient *lprcpt,
+ const struct smtp_reply *reply)
+{
+ struct lmtp_recipient *lrcpt = lprcpt->rcpt;
+ struct client *client = lrcpt->client;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ const char *detail = "";
+
+ if (client->lmtp_set->lmtp_verbose_replies) {
+ smtp_server_command_fail(rcpt->cmd->cmd, 451, "4.4.0",
+ "Proxy failed: %s (session=%s)",
+ smtp_reply_log(reply),
+ lrcpt->session_id);
+ return;
+ }
+
+ switch (reply->status) {
+ case SMTP_CLIENT_COMMAND_ERROR_ABORTED:
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED:
+ detail = "DNS lookup, ";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED:
+ case SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED:
+ detail = "connect, ";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST:
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED:
+ detail = "connection lost, ";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY:
+ detail = "bad reply, ";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT:
+ detail = "timed out, ";
+ break;
+ default:
+ break;
+ }
+
+ smtp_server_command_fail(rcpt->cmd->cmd, 451, "4.4.0",
+ "Proxy failed (%ssession=%s)",
+ detail, lrcpt->session_id);
+}
+
+static bool
+lmtp_proxy_handle_reply(struct lmtp_proxy_recipient *lprcpt,
+ const struct smtp_reply *reply,
+ struct smtp_reply *reply_r)
+{
+ *reply_r = *reply;
+
+ if (!smtp_reply_is_remote(reply) ||
+ reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED) {
+ lmtp_proxy_handle_connection_error(lprcpt, reply);
+ return FALSE;
+ }
+
+ if (!smtp_reply_has_enhanced_code(reply)) {
+ reply_r->enhanced_code =
+ SMTP_REPLY_ENH_CODE(reply->status / 100, 0, 0);
+ }
+ return TRUE;
+}
+
+/*
+ * RCPT command
+ */
+
+static bool
+lmtp_proxy_rcpt_parse_fields(struct lmtp_recipient *lrcpt,
+ struct lmtp_proxy_rcpt_settings *set,
+ const char *const *args, const char **address)
+{
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ const char *p, *key, *value;
+ bool proxying = FALSE, port_set = FALSE;
+
+ for (; *args != NULL; args++) {
+ p = strchr(*args, '=');
+ if (p == NULL) {
+ key = *args;
+ value = "";
+ } else {
+ key = t_strdup_until(*args, p);
+ value = p + 1;
+ }
+
+ if (strcmp(key, "proxy") == 0)
+ proxying = TRUE;
+ else if (strcmp(key, "host") == 0)
+ set->host = value;
+ else if (strcmp(key, "hostip") == 0) {
+ if (net_addr2ip(value, &set->hostip) < 0) {
+ e_error(rcpt->event,
+ "proxy: Invalid hostip %s", value);
+ return FALSE;
+ }
+ } else if (strcmp(key, "source_ip") == 0) {
+ if (net_addr2ip(value, &set->source_ip) < 0) {
+ e_error(rcpt->event,
+ "proxy: Invalid source_ip %s", value);
+ return FALSE;
+ }
+ } else if (strcmp(key, "port") == 0) {
+ if (net_str2port(value, &set->port) < 0) {
+ e_error(rcpt->event,
+ "proxy: Invalid port number %s", value);
+ return FALSE;
+ }
+ port_set = TRUE;
+ } else if (strcmp(key, "proxy_timeout") == 0) {
+ if (str_to_uint(value, &set->timeout_msecs) < 0) {
+ e_error(rcpt->event,"proxy: "
+ "Invalid proxy_timeout value %s", value);
+ return FALSE;
+ }
+ set->timeout_msecs *= 1000;
+ } else if (strcmp(key, "proxy_not_trusted") == 0) {
+ set->proxy_not_trusted = TRUE;
+ } else if (strcmp(key, "protocol") == 0) {
+ if (strcmp(value, "lmtp") == 0) {
+ set->protocol = SMTP_PROTOCOL_LMTP;
+ if (!port_set)
+ set->port = 24;
+ } else if (strcmp(value, "smtp") == 0) {
+ set->protocol = SMTP_PROTOCOL_SMTP;
+ if (!port_set)
+ set->port = 25;
+ } else {
+ e_error(rcpt->event,
+ "proxy: Unknown protocol %s", value);
+ return FALSE;
+ }
+ } else if (strcmp(key, "ssl") == 0) {
+ set->ssl_flags |= PROXY_SSL_FLAG_YES;
+ if (strcmp(value, "any-cert") == 0)
+ set->ssl_flags |= PROXY_SSL_FLAG_ANY_CERT;
+ } else if (strcmp(key, "starttls") == 0) {
+ set->ssl_flags |= PROXY_SSL_FLAG_YES |
+ PROXY_SSL_FLAG_STARTTLS;
+ if (strcmp(value, "any-cert") == 0)
+ set->ssl_flags |= PROXY_SSL_FLAG_ANY_CERT;
+ } else if (strcmp(key, "user") == 0 ||
+ strcmp(key, "destuser") == 0) {
+ /* Changing the username */
+ *address = value;
+ } else {
+ /* Just ignore it */
+ }
+ }
+ if (proxying && set->host == NULL) {
+ e_error(rcpt->event, "proxy: host not given");
+ return FALSE;
+ }
+ return proxying;
+}
+
+static bool
+lmtp_proxy_is_ourself(const struct client *client,
+ const struct lmtp_proxy_rcpt_settings *set)
+{
+ struct ip_addr ip;
+
+ if (set->port != client->local_port)
+ return FALSE;
+
+ if (set->hostip.family != 0)
+ ip = set->hostip;
+ else {
+ if (net_addr2ip(set->host, &ip) < 0)
+ return FALSE;
+ }
+ if (!net_ip_compare(&ip, &client->local_ip))
+ return FALSE;
+ return TRUE;
+}
+
+static void
+lmtp_proxy_rcpt_approved(struct smtp_server_recipient *rcpt ATTR_UNUSED,
+ struct lmtp_proxy_recipient *lprcpt)
+{
+ struct client *client = lprcpt->rcpt->client;
+
+ /* Add to proxy recipients */
+ array_push_back(&client->proxy->rcpt_to, &lprcpt);
+}
+
+static void
+lmtp_proxy_rcpt_cb(const struct smtp_reply *proxy_reply,
+ struct lmtp_proxy_recipient *lprcpt)
+{
+ struct smtp_server_recipient *rcpt = lprcpt->rcpt->rcpt;
+ struct smtp_reply reply;
+
+ if (!lmtp_proxy_handle_reply(lprcpt, proxy_reply, &reply))
+ return;
+
+ if (smtp_reply_is_success(proxy_reply)) {
+ /* If backend accepts it, we accept it too */
+
+ /* The default 2.0.0 code won't do */
+ if (!smtp_reply_has_enhanced_code(proxy_reply))
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(2, 1, 0);
+ }
+
+ /* Forward reply */
+ smtp_server_recipient_reply_forward(rcpt, &reply);
+}
+
+static void
+lmtp_proxy_rcpt_login_cb(const struct smtp_reply *proxy_reply, void *context)
+{
+ struct lmtp_proxy_recipient *lprcpt = context;
+ struct lmtp_recipient *lrcpt = lprcpt->rcpt;
+ struct lmtp_proxy_connection *conn = lprcpt->conn;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct smtp_reply reply;
+ struct smtp_client_transaction_rcpt *relay_rcpt;
+ struct smtp_params_rcpt *rcpt_params = &rcpt->params;
+ bool add_orcpt_param = FALSE, add_xrcptforward_param = FALSE;
+ pool_t param_pool;
+
+ if (!lmtp_proxy_handle_reply(lprcpt, proxy_reply, &reply))
+ return;
+ if (!smtp_reply_is_success(proxy_reply)) {
+ smtp_server_recipient_reply_forward(rcpt, &reply);
+ return;
+ }
+
+ /* Add an ORCPT parameter when passdb changed the username (and
+ therefore the RCPT address changed) and there is no ORCPT parameter
+ yet. */
+ if (!smtp_params_rcpt_has_orcpt(rcpt_params) &&
+ !smtp_address_equals(lprcpt->address, rcpt->path))
+ add_orcpt_param = TRUE;
+
+ /* Add forward fields parameter when passdb returned forward_* fields */
+ if (lprcpt->forward_fields != NULL &&
+ lmtp_proxy_connection_has_rcpt_forward(conn))
+ add_xrcptforward_param = TRUE;
+
+ /* Copy params when changes are pending */
+ param_pool = NULL;
+ if (add_orcpt_param || add_xrcptforward_param) {
+ param_pool = pool_datastack_create();
+ rcpt_params = p_new(param_pool, struct smtp_params_rcpt, 1);
+ smtp_params_rcpt_copy(param_pool, rcpt_params, &rcpt->params);
+ }
+
+ /* Add ORCPT */
+ if (add_orcpt_param) {
+ smtp_params_rcpt_set_orcpt(rcpt_params, param_pool,
+ rcpt->path);
+ }
+ /* Add forward fields parameter */
+ if (add_xrcptforward_param) {
+ smtp_params_rcpt_encode_extra(
+ rcpt_params, param_pool, LMTP_RCPT_FORWARD_PARAMETER,
+ lprcpt->forward_fields, lprcpt->forward_fields_size);
+ }
+
+ smtp_server_recipient_add_hook(
+ rcpt, SMTP_SERVER_RECIPIENT_HOOK_APPROVED,
+ lmtp_proxy_rcpt_approved, lprcpt);
+
+ relay_rcpt = smtp_client_transaction_add_pool_rcpt(
+ conn->lmtp_trans, rcpt->pool, lprcpt->address, rcpt_params,
+ lmtp_proxy_rcpt_cb, lprcpt);
+ smtp_client_transaction_rcpt_set_data_callback(
+ relay_rcpt, lmtp_proxy_data_cb, lprcpt);
+}
+
+int lmtp_proxy_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *lrcpt,
+ const char *username, const char *detail,
+ char delim)
+{
+ struct auth_master_connection *auth_conn;
+ struct lmtp_proxy_rcpt_settings set;
+ struct lmtp_proxy_connection *conn;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct lmtp_proxy_recipient *lprcpt;
+ struct smtp_server_transaction *trans;
+ struct smtp_address *address = rcpt->path;
+ struct auth_user_info info;
+ struct mail_storage_service_input input;
+ const char *const *fields, *errstr, *orig_username = username;
+ struct smtp_proxy_data proxy_data;
+ struct smtp_address *user;
+ string_t *fwfields;
+ pool_t auth_pool;
+ int ret;
+
+ trans = smtp_server_connection_get_transaction(cmd->conn);
+ i_assert(trans != NULL); /* MAIL command is synchronous */
+
+ i_zero(&input);
+ input.module = input.service = "lmtp";
+ mail_storage_service_init_settings(storage_service, &input);
+
+ i_zero(&info);
+ info.service = master_service_get_name(master_service);
+ info.local_ip = client->local_ip;
+ info.real_local_ip = client->real_local_ip;
+ info.remote_ip = client->remote_ip;
+ info.real_remote_ip = client->real_remote_ip;
+ info.local_port = client->local_port;
+ info.real_local_port = client->real_local_port;
+ info.remote_port = client->remote_port;
+ info.real_remote_port = client->real_remote_port;
+ info.forward_fields = lrcpt->forward_fields;
+
+ // FIXME: make this async
+ auth_pool = pool_alloconly_create("auth lookup", 1024);
+ auth_conn = mail_storage_service_get_auth_conn(storage_service);
+ ret = auth_master_pass_lookup(auth_conn, username, &info,
+ auth_pool, &fields);
+ if (ret <= 0) {
+ errstr = (ret < 0 && fields[0] != NULL ?
+ t_strdup(fields[0]) :
+ "Temporary user lookup failure");
+ pool_unref(&auth_pool);
+ if (ret < 0) {
+ smtp_server_recipient_reply(rcpt, 451, "4.3.0", "%s",
+ errstr);
+ return -1;
+ } else {
+ /* User not found from passdb: revert to local delivery.
+ */
+ return 0;
+ }
+ }
+
+ i_zero(&set);
+ set.port = client->local_port;
+ set.protocol = SMTP_PROTOCOL_LMTP;
+ set.timeout_msecs = LMTP_PROXY_DEFAULT_TIMEOUT_MSECS;
+
+ if (!lmtp_proxy_rcpt_parse_fields(lrcpt, &set, fields, &username)) {
+ /* Not proxying this user */
+ pool_unref(&auth_pool);
+ return 0;
+ }
+ if (strcmp(username, orig_username) != 0) {
+ /* The existing "user" event field is overridden with the new
+ user name, while old username is available as "orig_user" */
+ event_add_str(rcpt->event, "user", username);
+ event_add_str(rcpt->event, "original_user", orig_username);
+
+ if (smtp_address_parse_username(pool_datastack_create(),
+ username, &user, &errstr) < 0) {
+ e_error(rcpt->event, "%s: "
+ "Username `%s' returned by passdb lookup is not a valid SMTP address",
+ orig_username, username);
+ smtp_server_recipient_reply(
+ rcpt, 550, "5.3.5",
+ "Internal user lookup failure");
+ pool_unref(&auth_pool);
+ return -1;
+ }
+ /* Username changed. change the address as well */
+ if (*detail == '\0') {
+ address = user;
+ } else {
+ address = smtp_address_add_detail_temp(
+ user, detail, delim);
+ }
+ } else if (lmtp_proxy_is_ourself(client, &set)) {
+ e_error(rcpt->event, "Proxying to <%s> loops to itself",
+ username);
+ smtp_server_recipient_reply(rcpt, 554, "5.4.6",
+ "Proxying loops to itself");
+ pool_unref(&auth_pool);
+ return -1;
+ }
+
+ smtp_server_connection_get_proxy_data(cmd->conn, &proxy_data);
+ if (proxy_data.ttl_plus_1 == 1) {
+ e_error(rcpt->event,
+ "Proxying to <%s> appears to be looping (TTL=0)",
+ username);
+ smtp_server_recipient_reply(rcpt, 554, "5.4.6",
+ "Proxying appears to be looping "
+ "(TTL=0)");
+ pool_unref(&auth_pool);
+ return -1;
+ }
+
+ if (client->proxy == NULL)
+ client->proxy = lmtp_proxy_init(client, trans);
+
+ conn = lmtp_proxy_get_connection(client->proxy, &set);
+
+ lprcpt = p_new(rcpt->pool, struct lmtp_proxy_recipient, 1);
+ lprcpt->rcpt = lrcpt;
+ lprcpt->address = smtp_address_clone(rcpt->pool, address);
+ lprcpt->conn = conn;
+
+ lrcpt->type = LMTP_RECIPIENT_TYPE_PROXY;
+ lrcpt->backend_context = lprcpt;
+
+ /* Copy forward fields returned from passdb */
+ fwfields = NULL;
+ for (const char *const *ptr = fields; *ptr != NULL; ptr++) {
+ if (strncasecmp(*ptr, "forward_", 8) != 0)
+ continue;
+
+ if (fwfields == NULL)
+ fwfields = t_str_new(128);
+ else
+ str_append_c(fwfields, '\t');
+
+ str_append_tabescaped(fwfields, (*ptr) + 8);
+ }
+ if (fwfields != NULL) {
+ lprcpt->forward_fields = p_memdup(
+ rcpt->pool, str_data(fwfields), str_len(fwfields));
+ lprcpt->forward_fields_size = str_len(fwfields);
+ }
+
+ pool_unref(&auth_pool);
+
+ smtp_client_connection_connect(conn->lmtp_conn,
+ lmtp_proxy_rcpt_login_cb, lprcpt);
+ return 1;
+}
+
+/*
+ * DATA command
+ */
+
+static void
+lmtp_proxy_data_cb(const struct smtp_reply *proxy_reply,
+ struct lmtp_proxy_recipient *lprcpt)
+{
+ struct lmtp_proxy_connection *conn = lprcpt->conn;
+ struct lmtp_recipient *lrcpt = lprcpt->rcpt;
+ struct smtp_server_recipient *rcpt = lrcpt->rcpt;
+ struct lmtp_proxy *proxy = conn->proxy;
+ struct smtp_server_transaction *trans = proxy->trans;
+ struct smtp_address *address = lprcpt->address;
+ const struct smtp_client_transaction_times *times =
+ smtp_client_transaction_get_times(conn->lmtp_trans);
+ unsigned int rcpt_index = rcpt->index;
+ struct smtp_reply reply;
+ string_t *msg;
+
+ /* Compose log message */
+ msg = t_str_new(128);
+ str_printfa(msg, "<%s>: ", lrcpt->session_id);
+ if (smtp_reply_is_success(proxy_reply))
+ str_append(msg, "Sent message to");
+ else
+ str_append(msg, "Failed to send message to");
+ str_printfa(msg, " <%s> at %s:%u: %s (%u/%u at %u ms)",
+ smtp_address_encode(address),
+ conn->set.host, conn->set.port,
+ smtp_reply_log(proxy_reply),
+ rcpt_index + 1, array_count(&trans->rcpt_to),
+ timeval_diff_msecs(&ioloop_timeval, &times->started));
+
+ /* Handle reply */
+ if (smtp_reply_is_success(proxy_reply)) {
+ /* If backend accepts it, we accept it too */
+ e_info(rcpt->event, "%s", str_c(msg));
+
+ /* Substitute our own success message */
+ smtp_reply_printf(&reply, 250, "%s Saved", lrcpt->session_id);
+ /* Do let the enhanced code through */
+ if (!smtp_reply_has_enhanced_code(proxy_reply))
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(2, 0, 0);
+ else
+ reply.enhanced_code = proxy_reply->enhanced_code;
+
+ } else {
+ if (smtp_reply_is_remote(proxy_reply)) {
+ /* The problem isn't with the proxy, it's with the
+ remote side. so the remote side will log an error,
+ while for us this is just an info event */
+ e_info(rcpt->event, "%s", str_c(msg));
+ } else {
+ e_error(rcpt->event, "%s", str_c(msg));
+ }
+
+ if (!lmtp_proxy_handle_reply(lprcpt, proxy_reply, &reply))
+ return;
+ }
+
+ /* Forward reply */
+ smtp_server_recipient_reply_forward(rcpt, &reply);
+}
+
+static void
+lmtp_proxy_data_dummy_cb(const struct smtp_reply *proxy_reply ATTR_UNUSED,
+ struct lmtp_proxy_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+void lmtp_proxy_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input)
+{
+ struct lmtp_proxy *proxy = client->proxy;
+ struct lmtp_proxy_connection *conn;
+ uoff_t size;
+
+ i_assert(data_input->seekable);
+ i_assert(proxy->data_input == NULL);
+
+ client_update_data_state(client, "proxying");
+
+ proxy->data_input = data_input;
+ i_stream_ref(proxy->data_input);
+ if (i_stream_get_size(proxy->data_input, TRUE, &size) < 0) {
+ e_error(client->event,
+ "i_stream_get_size(data_input) failed: %s",
+ i_stream_get_error(proxy->data_input));
+ size = UOFF_T_MAX;
+ }
+
+ /* Create the data_input streams first */
+ array_foreach_elem(&proxy->connections, conn) {
+ if (conn->finished) {
+ /* This connection had already failed */
+ continue;
+ }
+
+ if (size == UOFF_T_MAX) {
+ conn->data_input =
+ i_stream_create_limit(data_input, UOFF_T_MAX);
+ } else {
+ conn->data_input =
+ i_stream_create_sized(data_input, size);
+ }
+ }
+ /* Now that all the streams are created, start reading them
+ (reading them earlier could have caused the data_input parent's
+ offset to change) */
+ array_foreach_elem(&proxy->connections, conn) {
+ if (conn->finished) {
+ /* This connection had already failed */
+ continue;
+ }
+
+ smtp_client_transaction_set_timeout(conn->lmtp_trans,
+ proxy->max_timeout_msecs);
+ smtp_client_transaction_send(conn->lmtp_trans, conn->data_input,
+ lmtp_proxy_data_dummy_cb, conn);
+ }
+}
diff --git a/src/lmtp/lmtp-proxy.h b/src/lmtp/lmtp-proxy.h
new file mode 100644
index 0000000..18d27b7
--- /dev/null
+++ b/src/lmtp/lmtp-proxy.h
@@ -0,0 +1,29 @@
+#ifndef LMTP_PROXY_H
+#define LMTP_PROXY_H
+
+#include "net.h"
+
+#include "smtp-common.h"
+#include "smtp-params.h"
+#include "smtp-client.h"
+
+#define LMTP_PROXY_DEFAULT_TTL 5
+
+struct smtp_server_cmd_ctx;
+struct smtp_server_cmd_rcpt;
+struct lmtp_proxy;
+struct client;
+
+void lmtp_proxy_deinit(struct lmtp_proxy **proxy);
+
+int lmtp_proxy_rcpt(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct lmtp_recipient *rcpt, const char *username,
+ const char *detail, char delim);
+
+void lmtp_proxy_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input);
+
+#endif
diff --git a/src/lmtp/lmtp-recipient.c b/src/lmtp/lmtp-recipient.c
new file mode 100644
index 0000000..aaad29b
--- /dev/null
+++ b/src/lmtp/lmtp-recipient.c
@@ -0,0 +1,57 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "array.h"
+#include "smtp-server.h"
+#include "lmtp-recipient.h"
+
+struct lmtp_recipient_module_register
+lmtp_recipient_module_register = { 0 };
+
+struct lmtp_recipient *
+lmtp_recipient_create(struct client *client,
+ struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt)
+{
+ struct lmtp_recipient *lrcpt;
+
+ lrcpt = p_new(rcpt->pool, struct lmtp_recipient, 1);
+ lrcpt->rcpt = rcpt;
+ lrcpt->client = client;
+
+ rcpt->context = lrcpt;
+
+ p_array_init(&lrcpt->module_contexts, rcpt->pool, 5);
+
+ /* Use a unique session_id for each mail delivery. This is especially
+ important for stats process to not see duplicate sessions. */
+ if (client->state.session_id_seq++ == 0)
+ lrcpt->session_id = trans->id;
+ else {
+ lrcpt->session_id = p_strdup_printf(rcpt->pool, "%s:R%u",
+ trans->id, client->state.session_id_seq);
+ }
+ event_add_str(rcpt->event, "session", lrcpt->session_id);
+
+ return lrcpt;
+}
+
+struct lmtp_recipient *
+lmtp_recipient_find_duplicate(struct lmtp_recipient *lrcpt,
+ struct smtp_server_transaction *trans)
+{
+ struct smtp_server_recipient *drcpt;
+ struct lmtp_recipient *dup_lrcpt;
+
+ i_assert(lrcpt->rcpt != NULL);
+ drcpt = smtp_server_transaction_find_rcpt_duplicate(trans, lrcpt->rcpt);
+ if (drcpt == NULL)
+ return NULL;
+
+ dup_lrcpt = drcpt->context;
+ i_assert(dup_lrcpt->rcpt == drcpt);
+ i_assert(dup_lrcpt->type == lrcpt->type);
+
+ return dup_lrcpt;
+}
+
diff --git a/src/lmtp/lmtp-recipient.h b/src/lmtp/lmtp-recipient.h
new file mode 100644
index 0000000..d760440
--- /dev/null
+++ b/src/lmtp/lmtp-recipient.h
@@ -0,0 +1,48 @@
+#ifndef LMTP_RECIPIENT_H
+#define LMTP_RECIPIENT_H
+
+struct smtp_address;
+struct smtp_server_cmd_ctx;
+struct smtp_server_cmd_rcpt;
+struct smtp_server_recipient;
+union lmtp_recipient_module_context;
+struct client;
+
+enum lmtp_recipient_type {
+ LMTP_RECIPIENT_TYPE_LOCAL,
+ LMTP_RECIPIENT_TYPE_PROXY,
+};
+
+struct lmtp_recipient {
+ struct client *client;
+ struct smtp_server_recipient *rcpt;
+
+ enum lmtp_recipient_type type;
+ void *backend_context;
+
+ const char *session_id;
+ const char *forward_fields;
+
+ /* Module-specific contexts. */
+ ARRAY(union lmtp_recipient_module_context *) module_contexts;
+};
+
+struct lmtp_recipient_module_register {
+ unsigned int id;
+};
+
+union lmtp_recipient_module_context {
+ struct lmtp_recipient_module_register *reg;
+};
+extern struct lmtp_recipient_module_register lmtp_recipient_module_register;
+
+struct lmtp_recipient *
+lmtp_recipient_create(struct client *client,
+ struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt);
+
+struct lmtp_recipient *
+lmtp_recipient_find_duplicate(struct lmtp_recipient *lrcpt,
+ struct smtp_server_transaction *trans);
+
+#endif
diff --git a/src/lmtp/lmtp-settings.c b/src/lmtp/lmtp-settings.c
new file mode 100644
index 0000000..1d9ecb2
--- /dev/null
+++ b/src/lmtp/lmtp-settings.c
@@ -0,0 +1,205 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "lda-settings.h"
+#include "lmtp-settings.h"
+#include "mail-storage-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+static bool lmtp_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings lmtp_unix_listeners_array[] = {
+ { "lmtp", 0666, "", "" }
+};
+static struct file_listener_settings *lmtp_unix_listeners[] = {
+ &lmtp_unix_listeners_array[0]
+};
+static buffer_t lmtp_unix_listeners_buf = {
+ { { lmtp_unix_listeners, sizeof(lmtp_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings lmtp_service_settings = {
+ .name = "lmtp",
+ .protocol = "lmtp",
+ .type = "",
+ .executable = "lmtp",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &lmtp_unix_listeners_buf,
+ sizeof(lmtp_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct lmtp_settings)
+
+static const struct setting_define lmtp_setting_defines[] = {
+ DEF(BOOL, lmtp_proxy),
+ DEF(BOOL, lmtp_save_to_detail_mailbox),
+ DEF(BOOL, lmtp_rcpt_check_quota),
+ DEF(BOOL, lmtp_add_received_header),
+ DEF(BOOL, lmtp_verbose_replies),
+ DEF(UINT, lmtp_user_concurrency_limit),
+ DEF(ENUM, lmtp_hdr_delivery_address),
+ DEF(STR_VARS, lmtp_rawlog_dir),
+ DEF(STR_VARS, lmtp_proxy_rawlog_dir),
+
+ DEF(STR, lmtp_client_workarounds),
+
+ DEF(STR_VARS, login_greeting),
+ DEF(STR, login_trusted_networks),
+
+ DEF(STR, mail_plugins),
+ DEF(STR, mail_plugin_dir),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct lmtp_settings lmtp_default_settings = {
+ .lmtp_proxy = FALSE,
+ .lmtp_save_to_detail_mailbox = FALSE,
+ .lmtp_rcpt_check_quota = FALSE,
+ .lmtp_add_received_header = TRUE,
+ .lmtp_verbose_replies = FALSE,
+ .lmtp_user_concurrency_limit = 0,
+ .lmtp_hdr_delivery_address = "final:none:original",
+ .lmtp_rawlog_dir = "",
+ .lmtp_proxy_rawlog_dir = "",
+
+ .lmtp_client_workarounds = "",
+
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_trusted_networks = "",
+
+ .mail_plugins = "",
+ .mail_plugin_dir = MODULEDIR,
+};
+
+static const struct setting_parser_info *lmtp_setting_dependencies[] = {
+ &lda_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info lmtp_setting_parser_info = {
+ .module_name = "lmtp",
+ .defines = lmtp_setting_defines,
+ .defaults = &lmtp_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct lmtp_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = lmtp_settings_check,
+ .dependencies = lmtp_setting_dependencies
+};
+
+/* <settings checks> */
+struct lmtp_client_workaround_list {
+ const char *name;
+ enum lmtp_client_workarounds num;
+};
+
+static const struct lmtp_client_workaround_list
+lmtp_client_workaround_list[] = {
+ { "whitespace-before-path", LMTP_WORKAROUND_WHITESPACE_BEFORE_PATH },
+ { "mailbox-for-path", LMTP_WORKAROUND_MAILBOX_FOR_PATH },
+ { NULL, 0 }
+};
+
+static int
+lmtp_settings_parse_workarounds(struct lmtp_settings *set,
+ const char **error_r)
+{
+ enum lmtp_client_workarounds client_workarounds = 0;
+ const struct lmtp_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->lmtp_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = lmtp_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "lmtp_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool lmtp_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct lmtp_settings *set = _set;
+
+ if (lmtp_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+ if (strcmp(set->lmtp_hdr_delivery_address, "none") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_NONE;
+ } else if (strcmp(set->lmtp_hdr_delivery_address, "final") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_FINAL;
+ } else if (strcmp(set->lmtp_hdr_delivery_address, "original") == 0) {
+ set->parsed_lmtp_hdr_delivery_address =
+ LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL;
+ } else {
+ *error_r = t_strdup_printf("Unknown lmtp_hdr_delivery_address: %s",
+ set->lmtp_hdr_delivery_address);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
+
+void lmtp_settings_dup(const struct setting_parser_context *set_parser,
+ pool_t pool,
+ struct mail_user_settings **user_set_r,
+ struct lmtp_settings **lmtp_set_r,
+ struct lda_settings **lda_set_r)
+{
+ const char *error;
+ void **sets;
+
+ sets = master_service_settings_parser_get_others(master_service,
+ set_parser);
+ *user_set_r = settings_dup(&mail_user_setting_parser_info, sets[0], pool);
+ *lda_set_r = settings_dup(&lda_setting_parser_info, sets[2], pool);
+ *lmtp_set_r = settings_dup(&lmtp_setting_parser_info, sets[3], pool);
+ if (!lmtp_settings_check(*lmtp_set_r, pool, &error))
+ i_unreached();
+}
diff --git a/src/lmtp/lmtp-settings.h b/src/lmtp/lmtp-settings.h
new file mode 100644
index 0000000..3b2eaa3
--- /dev/null
+++ b/src/lmtp/lmtp-settings.h
@@ -0,0 +1,53 @@
+#ifndef LMTP_SETTINGS_H
+#define LMTP_SETTINGS_H
+
+struct mail_user_settings;
+struct lda_settings;
+struct lmtp_settings;
+
+/* <settings checks> */
+enum lmtp_hdr_delivery_address {
+ LMTP_HDR_DELIVERY_ADDRESS_NONE,
+ LMTP_HDR_DELIVERY_ADDRESS_FINAL,
+ LMTP_HDR_DELIVERY_ADDRESS_ORIGINAL
+};
+
+enum lmtp_client_workarounds {
+ LMTP_WORKAROUND_WHITESPACE_BEFORE_PATH = BIT(0),
+ LMTP_WORKAROUND_MAILBOX_FOR_PATH = BIT(1),
+};
+/* </settings checks> */
+
+struct lmtp_settings {
+ bool lmtp_proxy;
+ bool lmtp_save_to_detail_mailbox;
+ bool lmtp_rcpt_check_quota;
+ bool lmtp_add_received_header;
+ bool lmtp_verbose_replies;
+ unsigned int lmtp_user_concurrency_limit;
+ const char *lmtp_hdr_delivery_address;
+ const char *lmtp_rawlog_dir;
+ const char *lmtp_proxy_rawlog_dir;
+
+ const char *lmtp_client_workarounds;
+
+ const char *login_greeting;
+ const char *login_trusted_networks;
+
+ const char *mail_plugins;
+ const char *mail_plugin_dir;
+
+ enum lmtp_hdr_delivery_address parsed_lmtp_hdr_delivery_address;
+
+ enum lmtp_client_workarounds parsed_workarounds;
+};
+
+extern const struct setting_parser_info lmtp_setting_parser_info;
+
+void lmtp_settings_dup(const struct setting_parser_context *set_parser,
+ pool_t pool,
+ struct mail_user_settings **user_set_r,
+ struct lmtp_settings **lmtp_set_r,
+ struct lda_settings **lda_set_r);
+
+#endif
diff --git a/src/lmtp/main.c b/src/lmtp/main.c
new file mode 100644
index 0000000..fb9ada8
--- /dev/null
+++ b/src/lmtp/main.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lmtp-common.h"
+#include "ioloop.h"
+#include "path-util.h"
+#include "restrict-access.h"
+#include "anvil-client.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-interface.h"
+#include "mail-deliver.h"
+#include "mail-storage-service.h"
+#include "smtp-submit-settings.h"
+#include "lda-settings.h"
+
+#include <unistd.h>
+
+#define DNS_CLIENT_SOCKET_PATH "dns-client"
+#define LMTP_MASTER_FIRST_LISTEN_FD 3
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+struct smtp_server *lmtp_server = NULL;
+
+char *dns_client_socket_path, *base_dir;
+struct mail_storage_service_ctx *storage_service;
+struct anvil_client *anvil;
+
+lmtp_client_created_func_t *hook_client_created = NULL;
+
+struct event_category event_category_lmtp = {
+ .name = "lmtp",
+};
+
+lmtp_client_created_func_t *
+lmtp_client_created_hook_set(lmtp_client_created_func_t *new_hook)
+{
+ lmtp_client_created_func_t *old_hook = hook_client_created;
+
+ hook_client_created = new_hook;
+ return old_hook;
+}
+
+void lmtp_anvil_init(void)
+{
+ if (anvil == NULL) {
+ const char *path = t_strdup_printf("%s/anvil", base_dir);
+ anvil = anvil_client_init(path, NULL, 0);
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ (void)client_create(conn->fd, conn->fd, conn);
+}
+
+static void drop_privileges(void)
+{
+ struct restrict_access_settings set;
+ const char *error;
+
+ /* by default we don't drop any privileges, but keep running as root. */
+ restrict_access_get_env(&set);
+ /* open config connection before dropping privileges */
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+
+ i_zero(&input);
+ input.module = "lmtp";
+ input.service = "lmtp";
+ if (master_service_settings_read(master_service,
+ &input, &output, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+}
+
+static void main_init(void)
+{
+ struct master_service_connection conn;
+ struct smtp_server_settings lmtp_set;
+
+ i_zero(&lmtp_set);
+ lmtp_set.protocol = SMTP_PROTOCOL_LMTP;
+ lmtp_set.auth_optional = TRUE;
+ lmtp_set.rcpt_domain_optional = TRUE;
+ lmtp_set.mail_path_allow_broken = TRUE;
+ lmtp_set.reason_code_module = "lmtp";
+
+ lmtp_server = smtp_server_init(&lmtp_set);
+
+ if (IS_STANDALONE()) {
+ i_zero(&conn);
+ (void)client_create(STDIN_FILENO, STDOUT_FILENO, &conn);
+ }
+
+ const char *error, *tmp_socket_path;
+ if (t_abspath(DNS_CLIENT_SOCKET_PATH, &tmp_socket_path, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", DNS_CLIENT_SOCKET_PATH, error);
+ }
+ dns_client_socket_path = i_strdup(tmp_socket_path);
+ mail_deliver_hooks_init();
+}
+
+static void main_deinit(void)
+{
+ clients_destroy();
+ if (anvil != NULL)
+ anvil_client_deinit(&anvil);
+ i_free(dns_client_socket_path);
+ i_free(base_dir);
+ smtp_server_deinit(&lmtp_server);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &smtp_submit_setting_parser_info,
+ &lda_setting_parser_info,
+ &lmtp_setting_parser_info,
+ NULL
+ };
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_HAVE_STARTTLS;
+ enum mail_storage_service_flags storage_service_flags =
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP |
+ MAIL_STORAGE_SERVICE_FLAG_NO_IDLE_TIMEOUT;
+ const char *tmp_base_dir;
+ int c;
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN ;
+ }
+
+ master_service = master_service_init("lmtp", service_flags,
+ &argc, &argv, "D");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ storage_service_flags |=
+ MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ const char *error;
+ if (t_get_working_dir(&tmp_base_dir, &error) < 0)
+ i_fatal("Could not get working directory: %s", error);
+ base_dir = i_strdup(tmp_base_dir);
+
+ drop_privileges();
+ master_service_init_log_with_pid(master_service);
+
+ storage_service = mail_storage_service_init(master_service, set_roots,
+ storage_service_flags);
+ restrict_access_allow_coredumps(TRUE);
+
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+
+ main_deinit();
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/log/Makefile.am b/src/log/Makefile.am
new file mode 100644
index 0000000..9888fcd
--- /dev/null
+++ b/src/log/Makefile.am
@@ -0,0 +1,26 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = log
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+log_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+log_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+log_SOURCES = \
+ doveadm-connection.c \
+ log-connection.c \
+ log-error-buffer.c \
+ log-settings.c \
+ main.c
+
+noinst_HEADERS = \
+ doveadm-connection.h \
+ log-connection.h \
+ log-error-buffer.h
diff --git a/src/log/Makefile.in b/src/log/Makefile.in
new file mode 100644
index 0000000..a37fef3
--- /dev/null
+++ b/src/log/Makefile.in
@@ -0,0 +1,825 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = log$(EXEEXT)
+subdir = src/log
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_log_OBJECTS = doveadm-connection.$(OBJEXT) log-connection.$(OBJEXT) \
+ log-error-buffer.$(OBJEXT) log-settings.$(OBJEXT) \
+ main.$(OBJEXT)
+log_OBJECTS = $(am_log_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/doveadm-connection.Po \
+ ./$(DEPDIR)/log-connection.Po ./$(DEPDIR)/log-error-buffer.Po \
+ ./$(DEPDIR)/log-settings.Po ./$(DEPDIR)/main.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 = $(log_SOURCES)
+DIST_SOURCES = $(log_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ $(BINARY_CFLAGS)
+
+log_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+log_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+log_SOURCES = \
+ doveadm-connection.c \
+ log-connection.c \
+ log-error-buffer.c \
+ log-settings.c \
+ main.c
+
+noinst_HEADERS = \
+ doveadm-connection.h \
+ log-connection.h \
+ log-error-buffer.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/log/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/log/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+log$(EXEEXT): $(log_OBJECTS) $(log_DEPENDENCIES) $(EXTRA_log_DEPENDENCIES)
+ @rm -f log$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(log_OBJECTS) $(log_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/log-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/log-error-buffer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/log-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/log-connection.Po
+ -rm -f ./$(DEPDIR)/log-error-buffer.Po
+ -rm -f ./$(DEPDIR)/log-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+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)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/log-connection.Po
+ -rm -f ./$(DEPDIR)/log-error-buffer.Po
+ -rm -f ./$(DEPDIR)/log-settings.Po
+ -rm -f ./$(DEPDIR)/main.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/log/doveadm-connection.c b/src/log/doveadm-connection.c
new file mode 100644
index 0000000..77872ac
--- /dev/null
+++ b/src/log/doveadm-connection.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "log-error-buffer.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+struct doveadm_connection {
+ struct log_error_buffer *errorbuf;
+
+ int fd;
+ struct ostream *output;
+};
+
+static void doveadm_connection_destroy(struct doveadm_connection **_conn);
+
+static int doveadm_connection_send_errors(struct doveadm_connection *conn)
+{
+ struct log_error_buffer_iter *iter;
+ const struct log_error *error;
+ string_t *str = t_str_new(256);
+ int ret = 0;
+
+ iter = log_error_buffer_iter_init(conn->errorbuf);
+ while ((error = log_error_buffer_iter_next(iter)) != NULL) {
+ str_truncate(str, 0);
+ str_printfa(str, "%s\t%ld\t",
+ failure_log_type_names[error->type],
+ (long)error->timestamp);
+ str_append_tabescaped(str, error->prefix);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, error->text);
+ str_append_c(str, '\n');
+ if (o_stream_send(conn->output,
+ str_data(str), str_len(str)) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ log_error_buffer_iter_deinit(&iter);
+ return ret;
+}
+
+static int doveadm_output(struct doveadm_connection *conn)
+{
+ if (o_stream_flush(conn->output) != 0) {
+ /* error / finished */
+ doveadm_connection_destroy(&conn);
+ }
+ return 1;
+}
+
+void doveadm_connection_create(struct log_error_buffer *errorbuf, int fd)
+{
+ struct doveadm_connection *conn;
+
+ conn = i_new(struct doveadm_connection, 1);
+ conn->errorbuf = errorbuf;
+ conn->fd = fd;
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ if (doveadm_connection_send_errors(conn) < 0)
+ doveadm_connection_destroy(&conn);
+ else {
+ o_stream_set_flush_callback(conn->output, doveadm_output, conn);
+ o_stream_set_flush_pending(conn->output, TRUE);
+ }
+}
+
+static void doveadm_connection_destroy(struct doveadm_connection **_conn)
+{
+ struct doveadm_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(doveadm connection) failed: %m");
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
diff --git a/src/log/doveadm-connection.h b/src/log/doveadm-connection.h
new file mode 100644
index 0000000..c37eee4
--- /dev/null
+++ b/src/log/doveadm-connection.h
@@ -0,0 +1,6 @@
+#ifndef DOVEADM_CONNECTION_H
+#define DOVEADM_CONNECTION_H
+
+void doveadm_connection_create(struct log_error_buffer *errorbuf, int fd);
+
+#endif
diff --git a/src/log/log-connection.c b/src/log/log-connection.c
new file mode 100644
index 0000000..82c9374
--- /dev/null
+++ b/src/log/log-connection.c
@@ -0,0 +1,533 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "llist.h"
+#include "hash.h"
+#include "time-util.h"
+#include "process-title.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "log-error-buffer.h"
+#include "log-connection.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define FATAL_QUEUE_TIMEOUT_MSECS 500
+#define MAX_MSECS_PER_CONNECTION 100
+
+/* Log a warning after 1 secs when we've been all the time busy writing the
+ log connection. */
+#define LOG_WARN_PENDING_COUNT (1000 / MAX_MSECS_PER_CONNECTION)
+/* If we keep being busy, log a warning every 60 seconds. */
+#define LOG_WARN_PENDING_INTERVAL (60 * LOG_WARN_PENDING_COUNT)
+
+struct log_client {
+ struct ip_addr ip;
+ char *prefix;
+ bool fatal_logged:1;
+};
+
+struct log_line_metadata {
+ bool continues;
+ enum log_type log_type;
+ pid_t pid;
+ char *log_prefix;
+};
+
+struct log_connection {
+ struct log_connection *prev, *next;
+
+ struct log_error_buffer *errorbuf;
+ int fd;
+ int listen_fd;
+ struct io *io;
+ struct istream *input;
+
+ struct log_line_metadata partial_line;
+
+ char *default_prefix;
+ HASH_TABLE(void *, struct log_client *) clients;
+
+ unsigned int pending_count;
+
+ bool master:1;
+ bool handshaked:1;
+};
+
+static struct log_connection *log_connections = NULL;
+static ARRAY(struct log_connection *) logs_by_fd;
+static unsigned int global_pending_count;
+static struct log_connection *last_pending_log;
+
+static void
+log_connection_destroy(struct log_connection *log, bool shutting_down);
+
+static void log_refresh_proctitle(void)
+{
+ if (!verbose_proctitle)
+ return;
+
+ if (global_pending_count == 0)
+ process_title_set("");
+ else if (last_pending_log == NULL) {
+ process_title_set(t_strdup_printf(
+ "[%u services too fast]", global_pending_count));
+ } else if (global_pending_count > 1) {
+ process_title_set(t_strdup_printf(
+ "[%u services too fast, last: %d/%d/%s]",
+ global_pending_count,
+ last_pending_log->fd,
+ last_pending_log->listen_fd,
+ last_pending_log->default_prefix));
+ } else {
+ process_title_set(t_strdup_printf(
+ "[service too fast: %d/%d/%s]",
+ last_pending_log->fd,
+ last_pending_log->listen_fd,
+ last_pending_log->default_prefix));
+ }
+}
+
+static struct log_client *log_client_get(struct log_connection *log, pid_t pid)
+{
+ struct log_client *client;
+
+ client = hash_table_lookup(log->clients, POINTER_CAST(pid));
+ if (client == NULL) {
+ client = i_new(struct log_client, 1);
+ hash_table_insert(log->clients, POINTER_CAST(pid), client);
+ }
+ return client;
+}
+
+static void log_client_free(struct log_connection *log,
+ struct log_client *client, pid_t pid)
+{
+ hash_table_remove(log->clients, POINTER_CAST(pid));
+
+ i_free(client->prefix);
+ i_free(client);
+}
+
+static void log_parse_option(struct log_connection *log,
+ const struct failure_line *failure)
+{
+ struct log_client *client;
+
+ client = log_client_get(log, failure->pid);
+ if (str_begins(failure->text, "ip="))
+ (void)net_addr2ip(failure->text + 3, &client->ip);
+ else if (str_begins(failure->text, "prefix=")) {
+ i_free(client->prefix);
+ client->prefix = i_strdup(failure->text + 7);
+ }
+}
+
+static void
+client_log_ctx(struct log_connection *log,
+ const struct failure_context *ctx,
+ const struct timeval *log_time,
+ const char *prefix, const char *text)
+{
+ struct log_error err;
+
+ switch (ctx->type) {
+ case LOG_TYPE_DEBUG:
+ case LOG_TYPE_INFO:
+ break;
+ case LOG_TYPE_WARNING:
+ case LOG_TYPE_ERROR:
+ case LOG_TYPE_FATAL:
+ case LOG_TYPE_PANIC:
+ i_zero(&err);
+ err.type = ctx->type;
+ err.timestamp = log_time->tv_sec;
+ err.prefix = ctx->log_prefix != NULL ? ctx->log_prefix : prefix;
+ err.text = text;
+ log_error_buffer_add(log->errorbuf, &err);
+ break;
+ case LOG_TYPE_OPTION:
+ case LOG_TYPE_COUNT:
+ i_unreached();
+ }
+ /* log_prefix overrides the global prefix. Don't bother changing the
+ global prefix in that case. */
+ if (ctx->log_prefix == NULL)
+ i_set_failure_prefix("%s", prefix);
+ i_log_type(ctx, "%s", text);
+ if (ctx->log_prefix == NULL)
+ i_set_failure_prefix("%s", global_log_prefix);
+}
+
+static void
+client_log_fatal(struct log_connection *log, struct log_client *client,
+ const char *line, const struct timeval *log_time,
+ const struct tm *tm)
+{
+ struct failure_context failure_ctx;
+ const char *prefix = log->default_prefix;
+
+ i_zero(&failure_ctx);
+ failure_ctx.type = LOG_TYPE_FATAL;
+ failure_ctx.timestamp = tm;
+ failure_ctx.timestamp_usecs = log_time->tv_usec;
+
+ if (client != NULL) {
+ if (client->prefix != NULL)
+ prefix = client->prefix;
+ else if (client->ip.family != 0) {
+ line = t_strdup_printf("%s [last ip=%s]",
+ line, net_ip2addr(&client->ip));
+ }
+ }
+ client_log_ctx(log, &failure_ctx, log_time, prefix,
+ t_strconcat("master: ", line, NULL));
+}
+
+static void
+log_parse_master_line(const char *line, const struct timeval *log_time,
+ const struct tm *tm)
+{
+ struct log_connection *const *logs, *log;
+ struct log_client *client;
+ const char *p, *p2, *cmd, *pidstr;
+ unsigned int count;
+ unsigned int service_fd;
+ pid_t pid;
+
+ p = strchr(line, ' ');
+ if (p == NULL || (p2 = strchr(++p, ' ')) == NULL ||
+ str_to_uint(t_strcut(line, ' '), &service_fd) < 0) {
+ i_error("Received invalid input from master: %s", line);
+ return;
+ }
+ pidstr = t_strcut(p, ' ');
+ if (str_to_pid(pidstr, &pid) < 0) {
+ i_error("Received invalid pid from master: %s", pidstr);
+ return;
+ }
+ cmd = p2 + 1;
+
+ logs = array_get(&logs_by_fd, &count);
+ if (service_fd >= count || logs[service_fd] == NULL) {
+ if (strcmp(cmd, "BYE") == 0 && service_fd < count) {
+ /* master is probably shutting down and we already
+ noticed the log fd closing */
+ return;
+ }
+ i_error("Received master input for invalid service_fd %u: %s",
+ service_fd, line);
+ return;
+ }
+ log = logs[service_fd];
+ client = hash_table_lookup(log->clients, POINTER_CAST(pid));
+
+ if (strcmp(cmd, "BYE") == 0) {
+ if (client == NULL) {
+ /* we haven't seen anything important from this client.
+ it's not an error. */
+ return;
+ }
+ log_client_free(log, client, pid);
+ } else if (str_begins(cmd, "FATAL ")) {
+ client_log_fatal(log, client, cmd + 6, log_time, tm);
+ } else if (str_begins(cmd, "DEFAULT-FATAL ")) {
+ /* If the client has logged a fatal/panic, don't log this
+ message. */
+ if (client == NULL || !client->fatal_logged)
+ client_log_fatal(log, client, cmd + 14, log_time, tm);
+ } else {
+ i_error("Received unknown command from master: %s", cmd);
+ }
+}
+
+static void log_partial_line_free(struct log_connection *log)
+{
+ if (!log->partial_line.continues)
+ return;
+ i_free(log->partial_line.log_prefix);
+ i_zero(&log->partial_line);
+}
+
+static void
+log_it(struct log_connection *log, const char *line,
+ const struct timeval *log_time, const struct tm *tm, bool partial_line)
+{
+ struct failure_line failure;
+ struct failure_context failure_ctx;
+ struct log_client *client = NULL;
+ const char *prefix = "";
+
+ if (log->master) {
+ if (partial_line) {
+ /* really not expected */
+ i_error("Received partial line from master: %s", line);
+ }
+ log_parse_master_line(line, log_time, tm);
+ return;
+ }
+
+ if (log->partial_line.continues && line[0] != '\001') {
+ /* The previous line didn't have LF, and it's now continued
+ here. We have the extra \001 check in here, because the
+ write was larger than PIPE_BUF and there's no guarantee
+ that the full write is sequential. The second line could
+ be an unrelated line, and the continuation could happen
+ sometimes after it. */
+ i_zero(&failure);
+ failure.log_type = log->partial_line.log_type;
+ failure.pid = log->partial_line.pid;
+ failure.text = t_strconcat("<line continued> ", line, NULL);
+ if (log->partial_line.log_prefix != NULL)
+ prefix = t_strdup(log->partial_line.log_prefix);
+ if (!partial_line)
+ log_partial_line_free(log);
+ } else {
+ i_failure_parse_line(line, &failure);
+ if (partial_line) {
+ log->partial_line.continues = TRUE;
+ log->partial_line.log_type = failure.log_type;
+ log->partial_line.pid = failure.pid;
+ } else {
+ log_partial_line_free(log);
+ }
+ }
+ switch (failure.log_type) {
+ case LOG_TYPE_FATAL:
+ case LOG_TYPE_PANIC:
+ if (failure.pid != 0) {
+ client = log_client_get(log, failure.pid);
+ client->fatal_logged = TRUE;
+ }
+ break;
+ case LOG_TYPE_OPTION:
+ log_parse_option(log, &failure);
+ return;
+ default:
+ client = failure.pid == 0 ? NULL :
+ hash_table_lookup(log->clients,
+ POINTER_CAST(failure.pid));
+ break;
+ }
+ i_assert(failure.log_type < LOG_TYPE_COUNT);
+
+ i_zero(&failure_ctx);
+ failure_ctx.type = failure.log_type;
+ failure_ctx.timestamp = tm;
+ failure_ctx.timestamp_usecs = log_time->tv_usec;
+ if (failure.log_prefix_len != 0) {
+ failure_ctx.log_prefix =
+ t_strndup(failure.text, failure.log_prefix_len);
+ failure.text += failure.log_prefix_len;
+ if (partial_line && log->partial_line.log_prefix == NULL) {
+ log->partial_line.log_prefix =
+ i_strdup(failure_ctx.log_prefix);
+ }
+ } else if (failure.disable_log_prefix) {
+ failure_ctx.log_prefix = "";
+ } else {
+ prefix = client != NULL && client->prefix != NULL ?
+ client->prefix : log->default_prefix;
+ }
+ client_log_ctx(log, &failure_ctx, log_time, prefix, failure.text);
+}
+
+static int log_connection_handshake(struct log_connection *log)
+{
+ struct log_service_handshake handshake;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ /* we're reading from a FIFO, so we're assuming that we're getting a
+ full handshake packet immediately. if not, treat it as an error
+ message that we want to log. */
+ ret = i_stream_read(log->input);
+ if (ret == -1) {
+ i_error("read(log %s) failed: %s", log->default_prefix,
+ i_stream_get_error(log->input));
+ return -1;
+ }
+ data = i_stream_get_data(log->input, &size);
+ if (size < sizeof(handshake)) {
+ /* this isn't a handshake */
+ return 0;
+ }
+
+ i_assert(size >= sizeof(handshake));
+ memcpy(&handshake, data, sizeof(handshake));
+
+ if (handshake.log_magic != MASTER_LOG_MAGIC) {
+ /* this isn't a handshake */
+ return 0;
+ }
+
+ if (handshake.prefix_len > size - sizeof(handshake)) {
+ i_error("Missing prefix data in handshake");
+ return -1;
+ }
+ i_free(log->default_prefix);
+ log->default_prefix = i_strndup(data + sizeof(handshake),
+ handshake.prefix_len);
+ i_stream_skip(log->input, sizeof(handshake) + handshake.prefix_len);
+
+ if (strcmp(log->default_prefix, MASTER_LOG_PREFIX_NAME) == 0) {
+ if (log->listen_fd != MASTER_LISTEN_FD_FIRST) {
+ i_error("Received master prefix in handshake "
+ "from non-master fd %d", log->fd);
+ return -1;
+ }
+ log->master = TRUE;
+ }
+ log->handshaked = TRUE;
+ return 0;
+}
+
+static void log_connection_input(struct log_connection *log)
+{
+ const char *line;
+ ssize_t ret;
+ struct timeval now, start_timeval;
+ struct tm tm;
+ bool too_much = FALSE;
+
+ if (!log->handshaked) {
+ if (log_connection_handshake(log) < 0) {
+ log_connection_destroy(log, FALSE);
+ return;
+ }
+ /* come back here even if we read something else besides a
+ handshake. the first few lines could be coming from e.g.
+ libc before the proper handshake line is sent. */
+ }
+
+ io_loop_time_refresh();
+ start_timeval = ioloop_timeval;
+ while ((ret = i_stream_read(log->input)) > 0 || ret == -2) {
+ /* get new timestamps for every read() */
+ now = ioloop_timeval;
+ tm = *localtime(&now.tv_sec);
+
+ if (ret != -2) {
+ while ((line = i_stream_next_line(log->input)) != NULL) T_BEGIN {
+ log_it(log, line, &now, &tm, FALSE);
+ } T_END;
+ } else T_BEGIN {
+ /* LF not found, but buffer is full. Just log what we
+ have for now. */
+ size_t size;
+ const unsigned char *data =
+ i_stream_get_data(log->input, &size);
+ line = t_strndup(data, size);
+ log_it(log, line, &now, &tm, TRUE);
+ i_stream_skip(log->input, size);
+ } T_END;
+ io_loop_time_refresh();
+ if (timeval_diff_msecs(&ioloop_timeval, &start_timeval) > MAX_MSECS_PER_CONNECTION) {
+ too_much = TRUE;
+ break;
+ }
+ }
+
+ if (log->input->eof) {
+ if (log->input->stream_errno != 0)
+ i_error("read(log %s) failed: %m", log->default_prefix);
+ log_connection_destroy(log, FALSE);
+ } else {
+ i_assert(!log->input->closed);
+ if (!too_much) {
+ if (log->pending_count > 0) {
+ log->pending_count = 0;
+ i_assert(global_pending_count > 0);
+ global_pending_count--;
+ if (log == last_pending_log)
+ last_pending_log = NULL;
+ log_refresh_proctitle();
+ }
+ return;
+ }
+ last_pending_log = log;
+ if (log->pending_count++ == 0) {
+ global_pending_count++;
+ log_refresh_proctitle();
+ }
+ if (log->pending_count == LOG_WARN_PENDING_COUNT ||
+ (log->pending_count % LOG_WARN_PENDING_INTERVAL) == 0) {
+ i_warning("Log connection fd %d listen_fd %d prefix '%s' is sending input faster than we can write",
+ log->fd, log->listen_fd, log->default_prefix);
+ }
+ }
+}
+
+void log_connection_create(struct log_error_buffer *errorbuf,
+ int fd, int listen_fd)
+{
+ struct log_connection *log;
+
+ log = i_new(struct log_connection, 1);
+ log->errorbuf = errorbuf;
+ log->fd = fd;
+ log->listen_fd = listen_fd;
+ log->io = io_add(fd, IO_READ, log_connection_input, log);
+ log->input = i_stream_create_fd(fd, PIPE_BUF);
+ log->default_prefix = i_strdup_printf("listen_fd(%d): ", listen_fd);
+ hash_table_create_direct(&log->clients, default_pool, 0);
+ array_idx_set(&logs_by_fd, listen_fd, &log);
+
+ DLLIST_PREPEND(&log_connections, log);
+ log_connection_input(log);
+}
+
+static void
+log_connection_destroy(struct log_connection *log, bool shutting_down)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct log_client *client;
+ unsigned int client_count = 0;
+
+ array_idx_clear(&logs_by_fd, log->listen_fd);
+
+ DLLIST_REMOVE(&log_connections, log);
+
+ iter = hash_table_iterate_init(log->clients);
+ while (hash_table_iterate(iter, log->clients, &key, &client)) {
+ i_free(client);
+ client_count++;
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&log->clients);
+
+ if (client_count > 0 && shutting_down) {
+ i_warning("Shutting down logging for '%s' with %u clients",
+ log->default_prefix, client_count);
+ }
+
+ i_stream_unref(&log->input);
+ io_remove(&log->io);
+ if (close(log->fd) < 0)
+ i_error("close(log connection fd) failed: %m");
+ log_partial_line_free(log);
+ i_free(log->default_prefix);
+ i_free(log);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void log_connections_init(void)
+{
+ i_array_init(&logs_by_fd, 64);
+}
+
+void log_connections_deinit(void)
+{
+ /* normally we don't exit until all log connections are gone,
+ but we could get here when we're being killed by a signal */
+ while (log_connections != NULL)
+ log_connection_destroy(log_connections, TRUE);
+ array_free(&logs_by_fd);
+}
diff --git a/src/log/log-connection.h b/src/log/log-connection.h
new file mode 100644
index 0000000..2885399
--- /dev/null
+++ b/src/log/log-connection.h
@@ -0,0 +1,15 @@
+#ifndef LOG_CONNECTION_H
+#define LOG_CONNECTION_H
+
+struct log_connection;
+
+extern bool verbose_proctitle;
+extern char *global_log_prefix;
+
+void log_connection_create(struct log_error_buffer *errorbuf,
+ int fd, int listen_fd);
+
+void log_connections_init(void);
+void log_connections_deinit(void);
+
+#endif
diff --git a/src/log/log-error-buffer.c b/src/log/log-error-buffer.c
new file mode 100644
index 0000000..608d1ad
--- /dev/null
+++ b/src/log/log-error-buffer.c
@@ -0,0 +1,122 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "log-error-buffer.h"
+
+#define LOG_ERROR_BUFFER_MAX_LINES 1000
+
+struct log_error_data {
+ struct log_error_data *next;
+
+ enum log_type type;
+ time_t timestamp;
+ unsigned char prefix_text[FLEXIBLE_ARRAY_MEMBER];
+};
+
+struct log_error_buffer {
+ struct log_error_data *head, *tail;
+ unsigned int count;
+};
+
+struct log_error_buffer_iter {
+ struct log_error_buffer *buf;
+ struct log_error_data *cur;
+ struct log_error error;
+};
+
+struct log_error_buffer *log_error_buffer_init(void)
+{
+ struct log_error_buffer *buf;
+
+ buf = i_new(struct log_error_buffer, 1);
+ return buf;
+}
+
+static void log_error_buffer_delete_head(struct log_error_buffer *buf)
+{
+ struct log_error_data *data;
+
+ i_assert(buf->head != NULL);
+
+ buf->count--;
+ data = buf->head;
+ buf->head = data->next;
+ if (buf->tail == data) {
+ /* last one */
+ buf->tail = NULL;
+ }
+ i_free(data);
+}
+
+void log_error_buffer_add(struct log_error_buffer *buf,
+ const struct log_error *error)
+{
+ size_t prefix_size = strlen(error->prefix)+1;
+ size_t text_size = strlen(error->text)+1;
+ struct log_error_data *data;
+
+ if (buf->count == LOG_ERROR_BUFFER_MAX_LINES)
+ log_error_buffer_delete_head(buf);
+
+ /* @UNSAFE */
+ data = i_malloc(MALLOC_ADD(sizeof(*data),
+ MALLOC_ADD(prefix_size, text_size)));
+ data->type = error->type;
+ data->timestamp = error->timestamp;
+ memcpy(data->prefix_text, error->prefix, prefix_size);
+ memcpy(data->prefix_text + prefix_size, error->text, text_size);
+
+ if (buf->tail != NULL)
+ buf->tail->next = data;
+ else
+ buf->head = data;
+ buf->tail = data;
+ buf->count++;
+}
+
+void log_error_buffer_deinit(struct log_error_buffer **_buf)
+{
+ struct log_error_buffer *buf = *_buf;
+
+ *_buf = NULL;
+ while (buf->count > 0)
+ log_error_buffer_delete_head(buf);
+ i_free(buf);
+}
+
+struct log_error_buffer_iter *
+log_error_buffer_iter_init(struct log_error_buffer *buf)
+{
+ struct log_error_buffer_iter *iter;
+
+ iter = i_new(struct log_error_buffer_iter, 1);
+ iter->buf = buf;
+ iter->cur = buf->head;
+ return iter;
+}
+
+struct log_error *
+log_error_buffer_iter_next(struct log_error_buffer_iter *iter)
+{
+ struct log_error_data *data = iter->cur;
+
+ if (data == NULL)
+ return NULL;
+ iter->cur = iter->cur->next;
+
+ iter->error.type = data->type;
+ iter->error.timestamp = data->timestamp;
+ iter->error.prefix = (const void *)data->prefix_text;
+ iter->error.text = (const void *)(data->prefix_text +
+ strlen(iter->error.prefix) + 1);
+ return &iter->error;
+}
+
+void log_error_buffer_iter_deinit(struct log_error_buffer_iter **_iter)
+{
+ struct log_error_buffer_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ i_free(iter);
+}
diff --git a/src/log/log-error-buffer.h b/src/log/log-error-buffer.h
new file mode 100644
index 0000000..e863c35
--- /dev/null
+++ b/src/log/log-error-buffer.h
@@ -0,0 +1,24 @@
+#ifndef LOG_ERROR_BUFFER_H
+#define LOG_ERROR_BUFFER_H
+
+struct log_error_buffer;
+
+struct log_error {
+ enum log_type type;
+ time_t timestamp;
+ const char *prefix;
+ const char *text;
+};
+
+struct log_error_buffer *log_error_buffer_init(void);
+void log_error_buffer_add(struct log_error_buffer *buf,
+ const struct log_error *error);
+void log_error_buffer_deinit(struct log_error_buffer **buf);
+
+struct log_error_buffer_iter *
+log_error_buffer_iter_init(struct log_error_buffer *buf);
+struct log_error *
+log_error_buffer_iter_next(struct log_error_buffer_iter *iter);
+void log_error_buffer_iter_deinit(struct log_error_buffer_iter **iter);
+
+#endif
diff --git a/src/log/log-settings.c b/src/log/log-settings.c
new file mode 100644
index 0000000..7c55544
--- /dev/null
+++ b/src/log/log-settings.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct file_listener_settings log_unix_listeners_array[] = {
+ { "log-errors", 0600, "", "" }
+};
+static struct file_listener_settings *log_unix_listeners[] = {
+ &log_unix_listeners_array[0]
+};
+static buffer_t log_unix_listeners_buf = {
+ { { log_unix_listeners, sizeof(log_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings log_service_settings = {
+ .name = "log",
+ .protocol = "",
+ .type = "log",
+ .executable = "log",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &log_unix_listeners_buf,
+ sizeof(log_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+
diff --git a/src/log/main.c b/src/log/main.c
new file mode 100644
index 0000000..a6ac950
--- /dev/null
+++ b/src/log/main.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "hostpid.h"
+#include "restrict-access.h"
+#include "master-interface.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "log-error-buffer.h"
+#include "log-connection.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+bool verbose_proctitle;
+char *global_log_prefix;
+static struct log_error_buffer *errorbuf;
+
+static void
+sig_reopen_logs(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ master_service->log_initialized = FALSE;
+ master_service_init_log_with_prefix(master_service, global_log_prefix);
+}
+
+static void main_init(void)
+{
+ lib_signals_set_handler(SIGUSR1, LIBSIG_FLAGS_SAFE,
+ sig_reopen_logs, NULL);
+
+ errorbuf = log_error_buffer_init();
+ log_connections_init();
+}
+
+static void main_deinit(void)
+{
+ log_connections_deinit();
+ log_error_buffer_deinit(&errorbuf);
+ i_free(global_log_prefix);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (conn->fifo) {
+ log_connection_create(errorbuf, conn->fd, conn->listen_fd);
+ /* kludge: normally FIFOs aren't counted as connections,
+ but here we want log process to stay open until all writers
+ have closed */
+ conn->fifo = FALSE;
+ } else if (strcmp(conn->name, "log-errors") == 0)
+ doveadm_connection_create(errorbuf, conn->fd);
+ else {
+ i_error("Unknown listener name: %s", conn->name);
+ return;
+ }
+
+ master_service_client_connection_accept(conn);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ const char *error;
+
+ master_service = master_service_init("log", service_flags,
+ &argc, &argv, "");
+
+ /* use log prefix and log to stderr until we've configured the real
+ logging */
+ global_log_prefix = i_strdup_printf("log(%s): ", my_pid);
+ i_set_failure_file("/dev/stderr", global_log_prefix);
+
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ if (master_service_settings_read_simple(master_service,
+ NULL, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log_with_prefix(master_service, global_log_prefix);
+
+ verbose_proctitle = master_service_settings_get(master_service)->verbose_proctitle;
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ /* logging should never die if there are some clients */
+ master_service_set_die_with_master(master_service, FALSE);
+
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/login-common/Makefile.am b/src/login-common/Makefile.am
new file mode 100644
index 0000000..9b99e0f
--- /dev/null
+++ b/src/login-common/Makefile.am
@@ -0,0 +1,40 @@
+noinst_LTLIBRARIES = liblogin.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\"
+
+liblogin_la_SOURCES = \
+ access-lookup.c \
+ client-common.c \
+ client-common-auth.c \
+ login-proxy.c \
+ login-proxy-state.c \
+ login-settings.c \
+ main.c \
+ sasl-server.c
+
+headers = \
+ access-lookup.h \
+ client-common.h \
+ login-common.h \
+ login-proxy.h \
+ login-proxy-state.h \
+ login-settings.h \
+ sasl-server.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+pkglib_LTLIBRARIES = libdovecot-login.la
+libdovecot_login_la_SOURCES =
+libdovecot_login_la_LIBADD = liblogin.la ../lib-dovecot/libdovecot.la $(SSL_LIBS)
+libdovecot_login_la_DEPENDENCIES = liblogin.la
+libdovecot_login_la_LDFLAGS = -export-dynamic
diff --git a/src/login-common/Makefile.in b/src/login-common/Makefile.in
new file mode 100644
index 0000000..4282d21
--- /dev/null
+++ b/src/login-common/Makefile.in
@@ -0,0 +1,908 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/login-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 =
+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 =
+am_libdovecot_login_la_OBJECTS =
+libdovecot_login_la_OBJECTS = $(am_libdovecot_login_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_login_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_login_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+liblogin_la_LIBADD =
+am_liblogin_la_OBJECTS = access-lookup.lo client-common.lo \
+ client-common-auth.lo login-proxy.lo login-proxy-state.lo \
+ login-settings.lo main.lo sasl-server.lo
+liblogin_la_OBJECTS = $(am_liblogin_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)/access-lookup.Plo \
+ ./$(DEPDIR)/client-common-auth.Plo \
+ ./$(DEPDIR)/client-common.Plo \
+ ./$(DEPDIR)/login-proxy-state.Plo ./$(DEPDIR)/login-proxy.Plo \
+ ./$(DEPDIR)/login-settings.Plo ./$(DEPDIR)/main.Plo \
+ ./$(DEPDIR)/sasl-server.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_login_la_SOURCES) $(liblogin_la_SOURCES)
+DIST_SOURCES = $(libdovecot_login_la_SOURCES) $(liblogin_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 = liblogin.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\"
+
+liblogin_la_SOURCES = \
+ access-lookup.c \
+ client-common.c \
+ client-common-auth.c \
+ login-proxy.c \
+ login-proxy-state.c \
+ login-settings.c \
+ main.c \
+ sasl-server.c
+
+headers = \
+ access-lookup.h \
+ client-common.h \
+ login-common.h \
+ login-proxy.h \
+ login-proxy-state.h \
+ login-settings.h \
+ sasl-server.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+pkglib_LTLIBRARIES = libdovecot-login.la
+libdovecot_login_la_SOURCES =
+libdovecot_login_la_LIBADD = liblogin.la ../lib-dovecot/libdovecot.la $(SSL_LIBS)
+libdovecot_login_la_DEPENDENCIES = liblogin.la
+libdovecot_login_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/login-common/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/login-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}; \
+ }
+
+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-login.la: $(libdovecot_login_la_OBJECTS) $(libdovecot_login_la_DEPENDENCIES) $(EXTRA_libdovecot_login_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_login_la_LINK) -rpath $(pkglibdir) $(libdovecot_login_la_OBJECTS) $(libdovecot_login_la_LIBADD) $(LIBS)
+
+liblogin.la: $(liblogin_la_OBJECTS) $(liblogin_la_DEPENDENCIES) $(EXTRA_liblogin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(liblogin_la_OBJECTS) $(liblogin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/access-lookup.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-common-auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/login-proxy-state.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/login-proxy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/login-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sasl-server.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)/access-lookup.Plo
+ -rm -f ./$(DEPDIR)/client-common-auth.Plo
+ -rm -f ./$(DEPDIR)/client-common.Plo
+ -rm -f ./$(DEPDIR)/login-proxy-state.Plo
+ -rm -f ./$(DEPDIR)/login-proxy.Plo
+ -rm -f ./$(DEPDIR)/login-settings.Plo
+ -rm -f ./$(DEPDIR)/main.Plo
+ -rm -f ./$(DEPDIR)/sasl-server.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)/access-lookup.Plo
+ -rm -f ./$(DEPDIR)/client-common-auth.Plo
+ -rm -f ./$(DEPDIR)/client-common.Plo
+ -rm -f ./$(DEPDIR)/login-proxy-state.Plo
+ -rm -f ./$(DEPDIR)/login-proxy.Plo
+ -rm -f ./$(DEPDIR)/login-settings.Plo
+ -rm -f ./$(DEPDIR)/main.Plo
+ -rm -f ./$(DEPDIR)/sasl-server.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/login-common/access-lookup.c b/src/login-common/access-lookup.c
new file mode 100644
index 0000000..692b43c
--- /dev/null
+++ b/src/login-common/access-lookup.c
@@ -0,0 +1,118 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "fdpass.h"
+#include "access-lookup.h"
+
+#include <unistd.h>
+
+#define ACCESS_LOOKUP_TIMEOUT_MSECS (1000*60)
+
+struct access_lookup {
+ int refcount;
+
+ int fd;
+ char *path;
+
+ struct io *io;
+ struct timeout *to;
+
+ access_lookup_callback_t *callback;
+ void *context;
+};
+
+static void access_lookup_input(struct access_lookup *lookup)
+{
+ unsigned char buf[3];
+ ssize_t ret;
+ bool success = FALSE;
+
+ ret = read(lookup->fd, buf, sizeof(buf));
+ if (ret < 0) {
+ i_error("read(%s) failed: %m", lookup->path);
+ } else if (ret == 0) {
+ /* connection close -> no success */
+ } else if (ret == 2 && buf[0] == '0' && buf[1] == '\n') {
+ /* no success */
+ } else if (ret == 2 && buf[0] == '1' && buf[1] == '\n') {
+ success = TRUE;
+ } else {
+ i_error("access(%s): Invalid input", lookup->path);
+ }
+
+ lookup->refcount++;
+ lookup->callback(success, lookup->context);
+ if (lookup->refcount > 1)
+ access_lookup_destroy(&lookup);
+ access_lookup_destroy(&lookup);
+}
+
+static void access_lookup_timeout(struct access_lookup *lookup)
+{
+ i_error("access(%s): Timed out while waiting for reply", lookup->path);
+
+ lookup->refcount++;
+ lookup->callback(FALSE, lookup->context);
+ if (lookup->refcount > 1)
+ access_lookup_destroy(&lookup);
+ access_lookup_destroy(&lookup);
+}
+
+struct access_lookup *
+access_lookup(const char *path, int client_fd, const char *daemon_name,
+ access_lookup_callback_t *callback, void *context)
+{
+ struct access_lookup *lookup;
+ const char *cmd;
+ ssize_t ret;
+ int fd;
+
+ fd = net_connect_unix(path);
+ if (fd == -1) {
+ i_error("connect(%s) failed: %m", path);
+ return NULL;
+ }
+
+ cmd = t_strconcat(daemon_name, "\n", NULL);
+ ret = fd_send(fd, client_fd, cmd, strlen(cmd));
+ if (ret != (ssize_t)strlen(cmd)) {
+ if (ret < 0)
+ i_error("fd_send(%s) failed: %m", path);
+ else
+ i_error("fd_send(%s) didn't write enough bytes", path);
+ i_close_fd(&fd);
+ return NULL;
+ }
+
+ lookup = i_new(struct access_lookup, 1);
+ lookup->refcount = 1;
+ lookup->fd = fd;
+ lookup->path = i_strdup(path);
+ lookup->io = io_add(fd, IO_READ, access_lookup_input, lookup);
+ lookup->to = timeout_add(ACCESS_LOOKUP_TIMEOUT_MSECS,
+ access_lookup_timeout, lookup);
+ lookup->callback = callback;
+ lookup->context = context;
+ return lookup;
+}
+
+void access_lookup_destroy(struct access_lookup **_lookup)
+{
+ struct access_lookup *lookup = *_lookup;
+
+ i_assert(lookup->refcount > 0);
+ if (--lookup->refcount > 0)
+ return;
+
+ *_lookup = NULL;
+
+ timeout_remove(&lookup->to);
+ io_remove(&lookup->io);
+ if (close(lookup->fd) < 0)
+ i_error("close(%s) failed: %m", lookup->path);
+
+ i_free(lookup->path);
+ i_free(lookup);
+}
diff --git a/src/login-common/access-lookup.h b/src/login-common/access-lookup.h
new file mode 100644
index 0000000..6ebb582
--- /dev/null
+++ b/src/login-common/access-lookup.h
@@ -0,0 +1,11 @@
+#ifndef ACCESS_LOOKUP_H
+#define ACCESS_LOOKUP_H
+
+typedef void access_lookup_callback_t(bool success, void *context);
+
+struct access_lookup *
+access_lookup(const char *path, int client_fd, const char *daemon_name,
+ access_lookup_callback_t *callback, void *context);
+void access_lookup_destroy(struct access_lookup **lookup);
+
+#endif
diff --git a/src/login-common/client-common-auth.c b/src/login-common/client-common-auth.c
new file mode 100644
index 0000000..ec5194b
--- /dev/null
+++ b/src/login-common/client-common-auth.c
@@ -0,0 +1,953 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "hostpid.h"
+#include "login-common.h"
+#include "array.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "safe-memset.h"
+#include "time-util.h"
+#include "settings-parser.h"
+#include "login-proxy.h"
+#include "auth-client.h"
+#include "dsasl-client.h"
+#include "master-service-ssl-settings.h"
+#include "client-common.h"
+
+/* If we've been waiting auth server to respond for over this many milliseconds,
+ send a "waiting" message. */
+#define AUTH_WAITING_TIMEOUT_MSECS (30*1000)
+#define AUTH_WAITING_WARNING_TIMEOUT_MSECS (10*1000)
+
+struct client_auth_fail_code_id {
+ const char *id;
+ enum client_auth_fail_code code;
+};
+
+static const struct client_auth_fail_code_id client_auth_fail_codes[] = {
+ { AUTH_CLIENT_FAIL_CODE_AUTHZFAILED,
+ CLIENT_AUTH_FAIL_CODE_AUTHZFAILED },
+ { AUTH_CLIENT_FAIL_CODE_TEMPFAIL,
+ CLIENT_AUTH_FAIL_CODE_TEMPFAIL },
+ { AUTH_CLIENT_FAIL_CODE_USER_DISABLED,
+ CLIENT_AUTH_FAIL_CODE_USER_DISABLED },
+ { AUTH_CLIENT_FAIL_CODE_PASS_EXPIRED,
+ CLIENT_AUTH_FAIL_CODE_PASS_EXPIRED },
+ { AUTH_CLIENT_FAIL_CODE_INVALID_BASE64,
+ CLIENT_AUTH_FAIL_CODE_INVALID_BASE64 },
+ { AUTH_CLIENT_FAIL_CODE_MECH_INVALID,
+ CLIENT_AUTH_FAIL_CODE_MECH_INVALID },
+ { AUTH_CLIENT_FAIL_CODE_MECH_SSL_REQUIRED,
+ CLIENT_AUTH_FAIL_CODE_MECH_SSL_REQUIRED },
+ { AUTH_CLIENT_FAIL_CODE_ANONYMOUS_DENIED,
+ CLIENT_AUTH_FAIL_CODE_ANONYMOUS_DENIED },
+ { NULL, CLIENT_AUTH_FAIL_CODE_NONE }
+};
+
+static enum client_auth_fail_code
+client_auth_fail_code_lookup(const char *fail_code)
+{
+ const struct client_auth_fail_code_id *fail = client_auth_fail_codes;
+
+ while (fail->id != NULL) {
+ if (strcmp(fail->id, fail_code) == 0)
+ return fail->code;
+ fail++;
+ }
+
+ return CLIENT_AUTH_FAIL_CODE_NONE;
+}
+
+static void client_auth_failed(struct client *client)
+{
+ i_free_and_null(client->master_data_prefix);
+ if (client->auth_response != NULL)
+ str_truncate(client->auth_response, 0);
+
+ if (client->auth_initializing || client->destroyed)
+ return;
+
+ io_remove(&client->io);
+
+ if (!client_does_custom_io(client)) {
+ client->io = io_add_istream(client->input, client_input, client);
+ io_set_pending(client->io);
+ }
+}
+
+static void client_auth_waiting_timeout(struct client *client)
+{
+ if (!client->notified_auth_ready) {
+ e_warning(client->event, "Auth process not responding, "
+ "delayed sending initial response (greeting)");
+ }
+ client_notify_status(client, FALSE, client->master_tag == 0 ?
+ AUTH_SERVER_WAITING_MSG : AUTH_MASTER_WAITING_MSG);
+ timeout_remove(&client->to_auth_waiting);
+}
+
+void client_set_auth_waiting(struct client *client)
+{
+ i_assert(client->to_auth_waiting == NULL);
+ client->to_auth_waiting =
+ timeout_add(!client->notified_auth_ready ?
+ AUTH_WAITING_WARNING_TIMEOUT_MSECS :
+ AUTH_WAITING_TIMEOUT_MSECS,
+ client_auth_waiting_timeout, client);
+}
+
+static void alt_username_set(ARRAY_TYPE(const_string) *alt_usernames, pool_t pool,
+ const char *key, const char *value)
+{
+ char *const *fields;
+ unsigned int i, count;
+
+ fields = array_get(&global_alt_usernames, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(fields[i], key) == 0)
+ break;
+ }
+ if (i == count) {
+ char *new_key = i_strdup(key);
+ array_push_back(&global_alt_usernames, &new_key);
+ }
+
+ value = p_strdup(pool, value);
+ if (i < array_count(alt_usernames)) {
+ array_idx_set(alt_usernames, i, &value);
+ return;
+ }
+
+ /* array is NULL-terminated, so if there are unused fields in
+ the middle set them as "" */
+ while (array_count(alt_usernames) < i) {
+ const char *empty_str = "";
+ array_push_back(alt_usernames, &empty_str);
+ }
+ array_push_back(alt_usernames, &value);
+}
+
+static void client_auth_parse_args(struct client *client, bool success,
+ const char *const *args,
+ struct client_auth_reply *reply_r)
+{
+ const char *key, *value, *p, *error;
+ ARRAY_TYPE(const_string) alt_usernames;
+
+ t_array_init(&alt_usernames, 4);
+ i_zero(reply_r);
+ reply_r->proxy_host_immediate_failure_after_secs =
+ LOGIN_PROXY_DEFAULT_HOST_IMMEDIATE_FAILURE_AFTER_SECS;
+
+ for (; *args != NULL; args++) {
+ p = strchr(*args, '=');
+ if (p == NULL) {
+ key = *args;
+ value = "";
+ } else {
+ key = t_strdup_until(*args, p);
+ value = p + 1;
+ }
+ if (strcmp(key, "nologin") == 0) {
+ reply_r->nologin = TRUE;
+ reply_r->fail_code = CLIENT_AUTH_FAIL_CODE_LOGIN_DISABLED;
+ } else if (strcmp(key, "proxy") == 0)
+ reply_r->proxy = TRUE;
+ else if (strcmp(key, "reason") == 0)
+ reply_r->reason = value;
+ else if (strcmp(key, "host") == 0)
+ reply_r->host = value;
+ else if (strcmp(key, "hostip") == 0)
+ reply_r->hostip = value;
+ else if (strcmp(key, "source_ip") == 0)
+ reply_r->source_ip = value;
+ else if (strcmp(key, "port") == 0) {
+ if (net_str2port(value, &reply_r->port) < 0) {
+ e_error(client->event,
+ "Auth service returned invalid "
+ "port number: %s", value);
+ }
+ } else if (strcmp(key, "destuser") == 0)
+ reply_r->destuser = value;
+ else if (strcmp(key, "pass") == 0)
+ reply_r->password = value;
+ else if (strcmp(key, "proxy_timeout") == 0) {
+ /* backwards compatibility: plain number is seconds */
+ if (str_to_uint(value, &reply_r->proxy_timeout_msecs) == 0)
+ reply_r->proxy_timeout_msecs *= 1000;
+ else if (settings_get_time_msecs(value,
+ &reply_r->proxy_timeout_msecs, &error) < 0) {
+ e_error(client->event,
+ "BUG: Auth service returned invalid "
+ "proxy_timeout value '%s': %s",
+ value, error);
+ }
+ } else if (strcmp(key, "proxy_host_immediate_failure_after") == 0) {
+ if (settings_get_time(value,
+ &reply_r->proxy_host_immediate_failure_after_secs,
+ &error) < 0) {
+ e_error(client->event,
+ "BUG: Auth service returned invalid "
+ "proxy_host_immediate_failure_after value '%s': %s",
+ value, error);
+ }
+ } else if (strcmp(key, "proxy_refresh") == 0) {
+ if (str_to_uint(value, &reply_r->proxy_refresh_secs) < 0) {
+ e_error(client->event,
+ "BUG: Auth service returned invalid "
+ "proxy_refresh value: %s", value);
+ }
+ } else if (strcmp(key, "proxy_mech") == 0)
+ reply_r->proxy_mech = value;
+ else if (strcmp(key, "proxy_noauth") == 0)
+ reply_r->proxy_noauth = TRUE;
+ else if (strcmp(key, "proxy_nopipelining") == 0)
+ reply_r->proxy_nopipelining = TRUE;
+ else if (strcmp(key, "proxy_not_trusted") == 0)
+ reply_r->proxy_not_trusted = TRUE;
+ else if (strcmp(key, "master") == 0) {
+ /* ignore empty master field */
+ if (*value != '\0')
+ reply_r->master_user = value;
+ } else if (strcmp(key, "ssl") == 0) {
+ reply_r->ssl_flags |= PROXY_SSL_FLAG_YES;
+ if (strcmp(value, "any-cert") == 0)
+ reply_r->ssl_flags |= PROXY_SSL_FLAG_ANY_CERT;
+ if (reply_r->port == 0)
+ reply_r->port = login_binary->default_ssl_port;
+ } else if (strcmp(key, "starttls") == 0) {
+ reply_r->ssl_flags |= PROXY_SSL_FLAG_YES |
+ PROXY_SSL_FLAG_STARTTLS;
+ if (strcmp(value, "any-cert") == 0)
+ reply_r->ssl_flags |= PROXY_SSL_FLAG_ANY_CERT;
+ } else if (strcmp(key, "code") == 0) {
+ if (reply_r->fail_code != CLIENT_AUTH_FAIL_CODE_NONE) {
+ /* code already assigned */
+ } else {
+ reply_r->fail_code = client_auth_fail_code_lookup(value);
+ }
+ } else if (strcmp(key, "user") == 0 ||
+ strcmp(key, "postlogin_socket") == 0) {
+ /* already handled in sasl-server.c */
+ } else if (str_begins(key, "user_")) {
+ if (success) {
+ alt_username_set(&alt_usernames, client->pool,
+ key, value);
+ }
+ } else if (str_begins(key, "forward_")) {
+ /* these are passed to upstream */
+ } else if (str_begins(key, "event_")) {
+ /* add name to event */
+ event_add_str(client->event, key + 6, value);
+ } else if (strcmp(key, "resp") == 0) {
+ /* ignore final response */
+ continue;
+ } else
+ e_debug(event_auth, "Ignoring unknown passdb extra field: %s", key);
+ }
+ if (array_count(&alt_usernames) > 0) {
+ const char **alt;
+
+ alt = p_new(client->pool, const char *,
+ array_count(&alt_usernames) + 1);
+ memcpy(alt, array_front(&alt_usernames),
+ sizeof(*alt) * array_count(&alt_usernames));
+ client->alt_usernames = alt;
+ }
+ if (reply_r->port == 0)
+ reply_r->port = login_binary->default_port;
+
+ if (reply_r->destuser == NULL)
+ reply_r->destuser = client->virtual_user;
+}
+
+static void proxy_free_password(struct client *client)
+{
+ if (client->proxy_password == NULL)
+ return;
+
+ safe_memset(client->proxy_password, 0, strlen(client->proxy_password));
+ i_free_and_null(client->proxy_password);
+}
+
+static void client_proxy_append_conn_info(string_t *str, struct client *client)
+{
+ const char *source_host;
+
+ source_host = login_proxy_get_source_host(client->login_proxy);
+ if (source_host[0] != '\0')
+ str_printfa(str, " from %s", source_host);
+ if (strcmp(client->virtual_user, client->proxy_user) != 0) {
+ /* remote username is different, log it */
+ str_printfa(str, " as user %s", client->proxy_user);
+ }
+ if (client->proxy_master_user != NULL)
+ str_printfa(str, " (master %s)", client->proxy_master_user);
+}
+
+void client_proxy_finish_destroy_client(struct client *client)
+{
+ string_t *str = t_str_new(128);
+
+ if (client->input->closed) {
+ /* input stream got closed in client_send_raw_data().
+ In most places we don't have to check for this explicitly,
+ but login_proxy_detach() attempts to get and use the
+ istream's fd, which is now -1. */
+ client_destroy_iostream_error(client);
+ return;
+ }
+
+ /* Include hostname in the log message in case it's different from the
+ IP address in the prefix. */
+ const char *ip_str = login_proxy_get_ip_str(client->login_proxy);
+ const char *host = login_proxy_get_host(client->login_proxy);
+ str_printfa(str, "Started proxying to <%s>",
+ login_proxy_get_ip_str(client->login_proxy));
+ if (strcmp(ip_str, host) != 0)
+ str_printfa(str, " (<%s>)", host);
+
+ client_proxy_append_conn_info(str, client);
+
+ struct event *proxy_event = login_proxy_get_event(client->login_proxy);
+ login_proxy_append_success_log_info(client->login_proxy, str);
+ struct event_passthrough *e = event_create_passthrough(proxy_event)->
+ set_name("proxy_session_established");
+ e_info(e->event(), "%s", str_c(str));
+ login_proxy_detach(client->login_proxy);
+ client_destroy_success(client, NULL);
+}
+
+const char *client_proxy_get_state(struct client *client)
+{
+ return client->v.proxy_get_state(client);
+}
+
+void client_proxy_log_failure(struct client *client, const char *line)
+{
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "Login failed");
+ client_proxy_append_conn_info(str, client);
+ str_append(str, ": ");
+ str_append(str, line);
+ e_info(login_proxy_get_event(client->login_proxy), "%s", str_c(str));
+}
+
+static void client_proxy_failed(struct client *client)
+{
+ login_proxy_free(&client->login_proxy);
+ proxy_free_password(client);
+ i_free_and_null(client->proxy_user);
+ i_free_and_null(client->proxy_master_user);
+
+ client_auth_failed(client);
+}
+
+static void proxy_input(struct client *client)
+{
+ struct istream *input;
+ struct ostream *output;
+ const char *line;
+ unsigned int duration;
+
+ input = login_proxy_get_istream(client->login_proxy);
+ switch (i_stream_read(input)) {
+ case -2:
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL,
+ "Too long input line");
+ return;
+ case -1:
+ line = i_stream_next_line(input);
+ duration = ioloop_time - client->created.tv_sec;
+ const char *reason = t_strdup_printf(
+ "Disconnected by server: %s "
+ "(state=%s, duration=%us)%s",
+ io_stream_get_disconnect_reason(input, NULL),
+ client_proxy_get_state(client), duration,
+ line == NULL ? "" : t_strdup_printf(
+ " - BUG: line not read: %s", line));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_CONNECT, reason);
+ return;
+ }
+
+ output = client->output;
+ /* The "line" variable is allocated from the istream, but the istream
+ may be freed by proxy_parse_line(). Keep the istream referenced to
+ make sure the line isn't freed too early. */
+ i_stream_ref(input);
+ o_stream_ref(output);
+ o_stream_cork(output);
+ while ((line = i_stream_next_line(input)) != NULL) {
+ if (client->v.proxy_parse_line(client, line) != 0)
+ break;
+ }
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+ i_stream_unref(&input);
+}
+
+void client_common_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason ATTR_UNUSED,
+ bool reconnecting)
+{
+ dsasl_client_free(&client->proxy_sasl_client);
+ if (reconnecting) {
+ client->v.proxy_reset(client);
+ return;
+ }
+
+ switch (type) {
+ case LOGIN_PROXY_FAILURE_TYPE_CONNECT:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH:
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL:
+ client->proxy_auth_failed = TRUE;
+ break;
+ }
+ client_proxy_failed(client);
+}
+
+static bool
+proxy_check_start(struct client *client, struct event *event,
+ const struct client_auth_reply *reply,
+ const struct dsasl_client_mech **sasl_mech_r,
+ struct ip_addr *ip_r)
+{
+ if (reply->password == NULL) {
+ e_error(event, "password not given");
+ return FALSE;
+ }
+ if (reply->host == NULL || *reply->host == '\0') {
+ e_error(event, "host not given");
+ return FALSE;
+ }
+
+ if (reply->hostip != NULL && reply->hostip[0] != '\0') {
+ if (net_addr2ip(reply->hostip, ip_r) < 0) {
+ e_error(event, "Invalid hostip %s", reply->hostip);
+ return FALSE;
+ }
+ } else if (net_addr2ip(reply->host, ip_r) < 0) {
+ e_error(event,
+ "BUG: host %s is not an IP (auth should have changed it)",
+ reply->host);
+ return FALSE;
+ }
+
+ if (reply->proxy_mech != NULL) {
+ *sasl_mech_r = dsasl_client_mech_find(reply->proxy_mech);
+ if (*sasl_mech_r == NULL) {
+ e_error(event, "Unsupported SASL mechanism %s",
+ reply->proxy_mech);
+ return FALSE;
+ }
+ } else if (reply->master_user != NULL) {
+ /* have to use PLAIN authentication with master user logins */
+ *sasl_mech_r = &dsasl_client_mech_plain;
+ }
+
+ if (login_proxy_is_ourself(client, reply->host, reply->port,
+ reply->destuser)) {
+ e_error(event, "Proxying loops to itself");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int proxy_start(struct client *client,
+ const struct client_auth_reply *reply)
+{
+ struct login_proxy_settings proxy_set;
+ const struct dsasl_client_mech *sasl_mech = NULL;
+ struct ip_addr ip;
+ struct event *event;
+
+ i_assert(reply->destuser != NULL);
+ i_assert(client->refcount > 1);
+ i_assert(!client->destroyed);
+ i_assert(client->proxy_sasl_client == NULL);
+
+ client->proxy_mech = NULL;
+ client->v.proxy_reset(client);
+ event = event_create(client->event);
+ event_set_append_log_prefix(event, t_strdup_printf(
+ "proxy(%s): ", client->virtual_user));
+
+ if (!proxy_check_start(client, event, reply, &sasl_mech, &ip)) {
+ client->v.proxy_failed(client,
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL,
+ LOGIN_PROXY_FAILURE_MSG, FALSE);
+ event_unref(&event);
+ return -1;
+ }
+
+ i_zero(&proxy_set);
+ proxy_set.host = reply->host;
+ proxy_set.ip = ip;
+ if (reply->source_ip != NULL) {
+ if (net_addr2ip(reply->source_ip, &proxy_set.source_ip) < 0)
+ proxy_set.source_ip.family = 0;
+ } else if (login_source_ips_count > 0) {
+ /* select the next source IP with round robin. */
+ proxy_set.source_ip = login_source_ips[login_source_ips_idx];
+ login_source_ips_idx =
+ (login_source_ips_idx + 1) % login_source_ips_count;
+ }
+ proxy_set.port = reply->port;
+ proxy_set.connect_timeout_msecs = reply->proxy_timeout_msecs;
+ if (proxy_set.connect_timeout_msecs == 0)
+ proxy_set.connect_timeout_msecs = client->set->login_proxy_timeout;
+ proxy_set.notify_refresh_secs = reply->proxy_refresh_secs;
+ proxy_set.ssl_flags = reply->ssl_flags;
+ proxy_set.host_immediate_failure_after_secs =
+ reply->proxy_host_immediate_failure_after_secs;
+ proxy_set.rawlog_dir = client->set->login_proxy_rawlog_dir;
+
+ /* Include destination ip:port also in the log prefix */
+ event_set_append_log_prefix(event, t_strdup_printf(
+ "proxy(%s,%s:%u): ", client->virtual_user,
+ net_ip2addr(&proxy_set.ip), proxy_set.port));
+
+ client->proxy_mech = sasl_mech;
+ client->proxy_user = i_strdup(reply->destuser);
+ client->proxy_master_user = i_strdup(reply->master_user);
+ client->proxy_password = i_strdup(reply->password);
+ client->proxy_noauth = reply->proxy_noauth;
+ client->proxy_nopipelining = reply->proxy_nopipelining;
+ client->proxy_not_trusted = reply->proxy_not_trusted;
+
+ if (login_proxy_new(client, event, &proxy_set, proxy_input,
+ client->v.proxy_failed) < 0) {
+ event_unref(&event);
+ return -1;
+ }
+ event_unref(&event);
+
+ /* disable input until authentication is finished */
+ io_remove(&client->io);
+ return 0;
+}
+
+static void ATTR_NULL(3, 4)
+client_auth_result(struct client *client, enum client_auth_result result,
+ const struct client_auth_reply *reply, const char *text)
+{
+ o_stream_cork(client->output);
+ client->v.auth_result(client, result, reply, text);
+ o_stream_uncork(client->output);
+}
+
+static bool
+client_auth_handle_reply(struct client *client,
+ const struct client_auth_reply *reply, bool success)
+{
+ if (reply->proxy) {
+ /* we want to proxy the connection to another server.
+ don't do this unless authentication succeeded. with
+ master user proxying we can get FAIL with proxy still set.
+
+ proxy host=.. [port=..] [destuser=..] pass=.. */
+ if (!success)
+ return FALSE;
+ if (proxy_start(client, reply) < 0)
+ client_auth_failed(client);
+ else {
+ /* this for plugins being able th hook into auth reply
+ when proxying is used */
+ client_auth_result(client, CLIENT_AUTH_RESULT_SUCCESS,
+ reply, NULL);
+ }
+ return TRUE;
+ }
+
+ if (reply->host != NULL) {
+ const char *reason;
+
+ if (reply->reason != NULL)
+ reason = reply->reason;
+ else if (reply->nologin)
+ reason = "Try this server instead.";
+ else
+ reason = "Logged in, but you should use this server instead.";
+
+ if (reply->nologin) {
+ client_auth_result(client,
+ CLIENT_AUTH_RESULT_REFERRAL_NOLOGIN,
+ reply, reason);
+ } else {
+ client_auth_result(client,
+ CLIENT_AUTH_RESULT_REFERRAL_SUCCESS,
+ reply, reason);
+ return TRUE;
+ }
+ } else if (reply->nologin) {
+ enum client_auth_result result = CLIENT_AUTH_RESULT_AUTHFAILED;
+ const char *timestamp, *reason = reply->reason;
+
+ /* Either failed or user login is disabled */
+ switch (reply->fail_code) {
+ case CLIENT_AUTH_FAIL_CODE_AUTHZFAILED:
+ result = CLIENT_AUTH_RESULT_AUTHZFAILED;
+ if (reason == NULL)
+ reason = "Authorization failed";
+ break;
+ case CLIENT_AUTH_FAIL_CODE_TEMPFAIL:
+ result = CLIENT_AUTH_RESULT_TEMPFAIL;
+ timestamp = t_strflocaltime("%Y-%m-%d %H:%M:%S", ioloop_time);
+ reason = t_strdup_printf(AUTH_TEMP_FAILED_MSG" [%s:%s]",
+ my_hostname, timestamp);
+ break;
+ case CLIENT_AUTH_FAIL_CODE_PASS_EXPIRED:
+ result = CLIENT_AUTH_RESULT_PASS_EXPIRED;
+ break;
+ case CLIENT_AUTH_FAIL_CODE_INVALID_BASE64:
+ result = CLIENT_AUTH_RESULT_INVALID_BASE64;
+ break;
+ case CLIENT_AUTH_FAIL_CODE_MECH_INVALID:
+ result = CLIENT_AUTH_RESULT_MECH_INVALID;
+ break;
+ case CLIENT_AUTH_FAIL_CODE_MECH_SSL_REQUIRED:
+ result = CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED;
+ break;
+ case CLIENT_AUTH_FAIL_CODE_ANONYMOUS_DENIED:
+ result = CLIENT_AUTH_RESULT_ANONYMOUS_DENIED;
+ break;
+ case CLIENT_AUTH_FAIL_CODE_LOGIN_DISABLED:
+ result = CLIENT_AUTH_RESULT_LOGIN_DISABLED;
+ if (reason == NULL)
+ reason = "Login disabled for this user";
+ break;
+ case CLIENT_AUTH_FAIL_CODE_USER_DISABLED:
+ default:
+ if (reason != NULL)
+ result = CLIENT_AUTH_RESULT_AUTHFAILED_REASON;
+ else
+ result = CLIENT_AUTH_RESULT_AUTHFAILED;
+ }
+
+ if (reason == NULL)
+ reason = AUTH_FAILED_MSG;
+ client_auth_result(client, result, reply, reason);
+ } else {
+ /* normal login/failure */
+ return FALSE;
+ }
+
+ i_assert(reply->nologin);
+
+ if (!client->destroyed)
+ client_auth_failed(client);
+ return TRUE;
+}
+
+void client_auth_respond(struct client *client, const char *response)
+{
+ client->auth_waiting = FALSE;
+ client_set_auth_waiting(client);
+ auth_client_request_continue(client->auth_request, response);
+ if (!client_does_custom_io(client))
+ io_remove(&client->io);
+}
+
+void client_auth_abort(struct client *client)
+{
+ sasl_server_auth_abort(client);
+}
+
+void client_auth_fail(struct client *client, const char *text)
+{
+ sasl_server_auth_failed(client, text, NULL);
+}
+
+int client_auth_read_line(struct client *client)
+{
+ const unsigned char *data;
+ size_t i, size, len;
+
+ if (i_stream_read_more(client->input, &data, &size) == -1) {
+ client_destroy_iostream_error(client);
+ return -1;
+ }
+
+ /* see if we have a full line */
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\n')
+ break;
+ }
+ if (client->auth_response == NULL)
+ client->auth_response = str_new(default_pool, I_MAX(i+1, 256));
+ if (str_len(client->auth_response) + i > LOGIN_MAX_AUTH_BUF_SIZE) {
+ client_destroy(client, "Authentication response too large");
+ return -1;
+ }
+ str_append_data(client->auth_response, data, i);
+ i_stream_skip(client->input, i == size ? size : i+1);
+
+ /* drop trailing \r */
+ len = str_len(client->auth_response);
+ if (len > 0 && str_c(client->auth_response)[len-1] == '\r')
+ str_truncate(client->auth_response, len-1);
+
+ return i < size ? 1 : 0;
+}
+
+void client_auth_parse_response(struct client *client)
+{
+ if (client_auth_read_line(client) <= 0)
+ return;
+
+ /* This has to happen before * handling, otherwise
+ client can abort failed request. */
+ if (client->final_response) {
+ sasl_server_auth_delayed_final(client);
+ return;
+ }
+
+ if (strcmp(str_c(client->auth_response), "*") == 0) {
+ sasl_server_auth_abort(client);
+ return;
+ }
+
+ client_auth_respond(client, str_c(client->auth_response));
+ memset(str_c_modifiable(client->auth_response), 0,
+ str_len(client->auth_response));
+}
+
+static void client_auth_input(struct client *client)
+{
+ i_assert(client->v.auth_parse_response != NULL);
+ client->v.auth_parse_response(client);
+}
+
+void client_auth_send_challenge(struct client *client, const char *data)
+{
+ struct const_iovec iov[3];
+
+ iov[0].iov_base = "+ ";
+ iov[0].iov_len = 2;
+ iov[1].iov_base = data;
+ iov[1].iov_len = strlen(data);
+ iov[2].iov_base = "\r\n";
+ iov[2].iov_len = 2;
+
+ o_stream_nsendv(client->output, iov, 3);
+}
+
+static void
+sasl_callback(struct client *client, enum sasl_server_reply sasl_reply,
+ const char *data, const char *const *args)
+{
+ struct client_auth_reply reply;
+
+ i_assert(!client->destroyed ||
+ sasl_reply == SASL_SERVER_REPLY_AUTH_ABORTED ||
+ sasl_reply == SASL_SERVER_REPLY_MASTER_FAILED);
+
+ client->last_auth_fail = CLIENT_AUTH_FAIL_CODE_NONE;
+ i_zero(&reply);
+ switch (sasl_reply) {
+ case SASL_SERVER_REPLY_SUCCESS:
+ timeout_remove(&client->to_auth_waiting);
+ if (args != NULL) {
+ client_auth_parse_args(client, TRUE, args, &reply);
+ reply.all_fields = args;
+ client->last_auth_fail = reply.fail_code;
+ if (client_auth_handle_reply(client, &reply, TRUE))
+ break;
+ }
+ client_auth_result(client, CLIENT_AUTH_RESULT_SUCCESS,
+ &reply, NULL);
+ client_destroy_success(client, "Login");
+ break;
+ case SASL_SERVER_REPLY_AUTH_FAILED:
+ case SASL_SERVER_REPLY_AUTH_ABORTED:
+ timeout_remove(&client->to_auth_waiting);
+ if (args != NULL) {
+ client_auth_parse_args(client, FALSE, args, &reply);
+ if (reply.reason == NULL)
+ reply.reason = data;
+ client->last_auth_fail = reply.fail_code;
+ reply.nologin = TRUE;
+ reply.all_fields = args;
+ if (client_auth_handle_reply(client, &reply, FALSE))
+ break;
+ }
+
+ if (sasl_reply == SASL_SERVER_REPLY_AUTH_ABORTED) {
+ client_auth_result(client, CLIENT_AUTH_RESULT_ABORTED,
+ &reply, "Authentication aborted by client.");
+ } else if (data == NULL) {
+ client_auth_result(client,
+ CLIENT_AUTH_RESULT_AUTHFAILED, &reply,
+ AUTH_FAILED_MSG);
+ } else {
+ client_auth_result(client,
+ CLIENT_AUTH_RESULT_AUTHFAILED_REASON, &reply,
+ data);
+ }
+
+ if (!client->destroyed)
+ client_auth_failed(client);
+ break;
+ case SASL_SERVER_REPLY_MASTER_FAILED:
+ if (data != NULL) {
+ /* authentication itself succeeded, we just hit some
+ internal failure. */
+ client_auth_result(client, CLIENT_AUTH_RESULT_TEMPFAIL,
+ &reply, data);
+ }
+
+ /* the fd may still be hanging somewhere in kernel or another
+ process. make sure the client gets disconnected. */
+ if (shutdown(client->fd, SHUT_RDWR) < 0 && errno != ENOTCONN)
+ e_error(client->event, "shutdown() failed: %m");
+
+ if (data != NULL) {
+ /* e.g. mail_max_userip_connections is reached */
+ } else {
+ /* The error should have been logged already.
+ The client will only see a generic internal error. */
+ client_notify_disconnect(client, CLIENT_DISCONNECT_INTERNAL_ERROR,
+ "Internal login failure. "
+ "Refer to server log for more information.");
+ data = t_strdup_printf("Internal login failure (pid=%s id=%u)",
+ my_pid, client->master_auth_id);
+ }
+ client->no_extra_disconnect_reason = TRUE;
+ client_destroy(client, data);
+ break;
+ case SASL_SERVER_REPLY_CONTINUE:
+ i_assert(client->v.auth_send_challenge != NULL);
+ client->v.auth_send_challenge(client, data);
+
+ timeout_remove(&client->to_auth_waiting);
+
+ if (client->auth_response != NULL)
+ str_truncate(client->auth_response, 0);
+
+ i_assert(client->io == NULL);
+ client->auth_waiting = TRUE;
+ if (!client_does_custom_io(client)) {
+ client->io = io_add_istream(client->input,
+ client_auth_input, client);
+ client_auth_input(client);
+ }
+ return;
+ }
+
+ client_unref(&client);
+}
+
+static int
+client_auth_begin_common(struct client *client, const char *mech_name,
+ enum sasl_server_auth_flags auth_flags,
+ const char *init_resp)
+{
+ if (!client->secured && strcmp(client->ssl_set->ssl, "required") == 0) {
+ if (client->set->auth_verbose) {
+ e_info(client->event, "Login failed: "
+ "SSL required for authentication");
+ }
+ client->auth_attempts++;
+ client_auth_result(client, CLIENT_AUTH_RESULT_SSL_REQUIRED, NULL,
+ "Authentication not allowed until SSL/TLS is enabled.");
+ return 1;
+ }
+
+
+ client_ref(client);
+ client->auth_initializing = TRUE;
+ sasl_server_auth_begin(client, login_binary->protocol, mech_name,
+ auth_flags, init_resp, sasl_callback);
+ client->auth_initializing = FALSE;
+ if (!client->authenticating)
+ return 1;
+
+ /* don't handle input until we get the initial auth reply */
+ io_remove(&client->io);
+ client_set_auth_waiting(client);
+ return 0;
+}
+
+int client_auth_begin(struct client *client, const char *mech_name,
+ const char *init_resp)
+{
+ return client_auth_begin_common(client, mech_name, 0, init_resp);
+}
+
+int client_auth_begin_private(struct client *client, const char *mech_name,
+ const char *init_resp)
+{
+ return client_auth_begin_common(client, mech_name,
+ SASL_SERVER_AUTH_FLAG_PRIVATE,
+ init_resp);
+}
+
+int client_auth_begin_implicit(struct client *client, const char *mech_name,
+ const char *init_resp)
+{
+ return client_auth_begin_common(client, mech_name,
+ SASL_SERVER_AUTH_FLAG_IMPLICIT,
+ init_resp);
+}
+
+bool client_check_plaintext_auth(struct client *client, bool pass_sent)
+{
+ bool ssl_required = (strcmp(client->ssl_set->ssl, "required") == 0);
+
+ if (client->secured || (!client->set->disable_plaintext_auth &&
+ !ssl_required))
+ return TRUE;
+
+ if (client->set->auth_verbose) {
+ e_info(client->event, "Login failed: "
+ "Plaintext authentication disabled");
+ }
+ if (pass_sent) {
+ client_notify_status(client, TRUE,
+ "Plaintext authentication not allowed "
+ "without SSL/TLS, but your client did it anyway. "
+ "If anyone was listening, the password was exposed.");
+ }
+
+ if (ssl_required) {
+ client_auth_result(client, CLIENT_AUTH_RESULT_SSL_REQUIRED, NULL,
+ AUTH_PLAINTEXT_DISABLED_MSG);
+ } else {
+ client_auth_result(client, CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED, NULL,
+ AUTH_PLAINTEXT_DISABLED_MSG);
+ }
+ client->auth_attempts++;
+ return FALSE;
+}
+
+void clients_notify_auth_connected(void)
+{
+ struct client *client, *next;
+
+ for (client = clients; client != NULL; client = next) {
+ next = client->next;
+
+ timeout_remove(&client->to_auth_waiting);
+
+ client_notify_auth_ready(client);
+
+ if (!client_does_custom_io(client) && client->input_blocked) {
+ client->input_blocked = FALSE;
+ io_set_pending(client->io);
+ }
+ }
+}
diff --git a/src/login-common/client-common.c b/src/login-common/client-common.c
new file mode 100644
index 0000000..fc44d2b
--- /dev/null
+++ b/src/login-common/client-common.c
@@ -0,0 +1,1212 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "array.h"
+#include "hostpid.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream.h"
+#include "iostream-ssl.h"
+#include "iostream-proxy.h"
+#include "iostream-rawlog.h"
+#include "process-title.h"
+#include "hook-build.h"
+#include "buffer.h"
+#include "str.h"
+#include "strescape.h"
+#include "base64.h"
+#include "str-sanitize.h"
+#include "safe-memset.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "master-auth.h"
+#include "anvil-client.h"
+#include "auth-client.h"
+#include "dsasl-client.h"
+#include "login-proxy.h"
+#include "client-common.h"
+
+struct client *clients = NULL;
+struct client *destroyed_clients = NULL;
+static struct client *last_client = NULL;
+static unsigned int clients_count = 0;
+
+static struct client *client_fd_proxies = NULL;
+static unsigned int client_fd_proxies_count = 0;
+
+struct login_client_module_hooks {
+ struct module *module;
+ const struct login_client_hooks *hooks;
+};
+
+static ARRAY(struct login_client_module_hooks) module_hooks = ARRAY_INIT;
+
+static const char *client_get_log_str(struct client *client, const char *msg);
+
+void login_client_hooks_add(struct module *module,
+ const struct login_client_hooks *hooks)
+{
+ struct login_client_module_hooks *hook;
+
+ hook = array_append_space(&module_hooks);
+ hook->module = module;
+ hook->hooks = hooks;
+}
+
+void login_client_hooks_remove(const struct login_client_hooks *hooks)
+{
+ const struct login_client_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);
+}
+
+static void hook_login_client_allocated(struct client *client)
+{
+ const struct login_client_module_hooks *module_hook;
+ struct hook_build_context *ctx;
+
+ ctx = hook_build_init((void *)&client->v, sizeof(client->v));
+ client->vlast = &client->v;
+ array_foreach(&module_hooks, module_hook) {
+ if (module_hook->hooks->client_allocated != NULL) T_BEGIN {
+ module_hook->hooks->client_allocated(client);
+ hook_build_update(ctx, client->vlast);
+ } T_END;
+ }
+ client->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
+
+static void client_idle_disconnect_timeout(struct client *client)
+{
+ const char *user_reason, *destroy_reason;
+ unsigned int secs;
+
+ if (client->master_tag != 0) {
+ secs = ioloop_time - client->auth_finished;
+ user_reason = "Timeout while finishing login.";
+ destroy_reason = t_strdup_printf(
+ "Timeout while finishing login (waited %u secs)", secs);
+ e_error(client->event, "%s", destroy_reason);
+ } else if (client->auth_request != NULL) {
+ user_reason =
+ "Disconnected for inactivity during authentication.";
+ destroy_reason = "Inactivity during authentication";
+ } else if (client->login_proxy != NULL) {
+ secs = ioloop_time - client->created.tv_sec;
+ user_reason = "Timeout while finishing login.";
+ destroy_reason = t_strdup_printf(
+ "Logging in timed out "
+ "(state=%s, duration=%us)",
+ client_proxy_get_state(client), secs);
+ e_error(login_proxy_get_event(client->login_proxy),
+ "%s", destroy_reason);
+ } else {
+ user_reason = "Disconnected for inactivity.";
+ destroy_reason = "Inactivity";
+ }
+ client_notify_disconnect(client, CLIENT_DISCONNECT_TIMEOUT, user_reason);
+ client_destroy(client, destroy_reason);
+}
+
+static void client_open_streams(struct client *client)
+{
+ client->input = i_stream_create_fd(client->fd, LOGIN_MAX_INBUF_SIZE);
+ client->output = o_stream_create_fd(client->fd, LOGIN_MAX_OUTBUF_SIZE);
+ o_stream_set_no_error_handling(client->output, TRUE);
+
+ if (login_rawlog_dir != NULL) {
+ if (iostream_rawlog_create(login_rawlog_dir, &client->input,
+ &client->output) < 0)
+ login_rawlog_dir = NULL;
+ }
+}
+
+static const char *
+client_log_msg_callback(struct client *client,
+ enum log_type log_type ATTR_UNUSED,
+ const char *message)
+{
+ return client_get_log_str(client, message);
+}
+
+static bool client_is_trusted(struct client *client)
+{
+ const char *const *net;
+ struct ip_addr net_ip;
+ unsigned int bits;
+
+ if (client->set->login_trusted_networks == NULL)
+ return FALSE;
+
+ net = t_strsplit_spaces(client->set->login_trusted_networks, ", ");
+ for (; *net != NULL; net++) {
+ if (net_parse_range(*net, &net_ip, &bits) < 0) {
+ e_error(client->event, "login_trusted_networks: "
+ "Invalid network '%s'", *net);
+ break;
+ }
+
+ if (net_is_in_network(&client->ip, &net_ip, bits))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+struct client *
+client_alloc(int fd, pool_t pool,
+ const struct master_service_connection *conn,
+ const struct login_settings *set,
+ const struct master_service_ssl_settings *ssl_set,
+ const struct master_service_ssl_server_settings *ssl_server_set)
+{
+ struct client *client;
+
+ i_assert(fd != -1);
+
+ client = login_binary->client_vfuncs->alloc(pool);
+ client->v = *login_binary->client_vfuncs;
+ if (client->v.auth_send_challenge == NULL)
+ client->v.auth_send_challenge = client_auth_send_challenge;
+ if (client->v.auth_parse_response == NULL)
+ client->v.auth_parse_response = client_auth_parse_response;
+
+ client->created = ioloop_timeval;
+ client->refcount = 1;
+
+ client->pool = pool;
+ client->preproxy_pool = pool_alloconly_create(MEMPOOL_GROWING"preproxy pool", 256);
+ client->set = set;
+ client->ssl_set = ssl_set;
+ client->ssl_server_set = ssl_server_set;
+ p_array_init(&client->module_contexts, client->pool, 5);
+
+ client->fd = fd;
+ client->local_ip = conn->local_ip;
+ client->local_port = conn->local_port;
+ client->ip = conn->remote_ip;
+ client->remote_port = conn->remote_port;
+ client->real_local_ip = conn->real_local_ip;
+ client->real_local_port = conn->real_local_port;
+ client->real_remote_ip = conn->real_remote_ip;
+ client->real_remote_port = conn->real_remote_port;
+ client->listener_name = p_strdup(client->pool, conn->name);
+ client->trusted = client_is_trusted(client);
+
+ if (conn->proxied) {
+ client->proxied_ssl = conn->proxy.ssl;
+ client->secured = conn->proxy.ssl || client->trusted;
+ client->ssl_secured = conn->proxy.ssl;
+ client->local_name = conn->proxy.hostname;
+ client->client_cert_common_name = conn->proxy.cert_common_name;
+ } else {
+ client->secured = client->trusted ||
+ net_ip_compare(&conn->real_remote_ip, &conn->real_local_ip);
+ }
+ client->proxy_ttl = LOGIN_PROXY_TTL;
+
+ client->event = event_create(NULL);
+ event_add_category(client->event, &login_binary->event_category);
+ event_add_str(client->event, "local_ip", net_ip2addr(&conn->local_ip));
+ event_add_int(client->event, "local_port", conn->local_port);
+ event_add_str(client->event, "remote_ip", net_ip2addr(&conn->remote_ip));
+ event_add_int(client->event, "remote_port", conn->remote_port);
+ event_add_str(client->event, "service", login_binary->protocol);
+ event_set_log_message_callback(client->event, client_log_msg_callback,
+ client);
+
+ client_open_streams(client);
+ return client;
+}
+
+void client_init(struct client *client, void **other_sets)
+{
+ if (last_client == NULL)
+ last_client = client;
+ client->list_type = CLIENT_LIST_TYPE_ACTIVE;
+ DLLIST_PREPEND(&clients, client);
+ clients_count++;
+
+ client->to_disconnect =
+ timeout_add(CLIENT_LOGIN_TIMEOUT_MSECS,
+ client_idle_disconnect_timeout, client);
+
+ hook_login_client_allocated(client);
+ client->v.create(client, other_sets);
+ client->create_finished = TRUE;
+
+ if (auth_client_is_connected(auth_client))
+ client_notify_auth_ready(client);
+ else
+ client_set_auth_waiting(client);
+
+ login_refresh_proctitle();
+}
+
+void client_disconnect(struct client *client, const char *reason,
+ bool add_disconnected_prefix)
+{
+ if (client->disconnected)
+ return;
+ client->disconnected = TRUE;
+
+ if (!client->login_success &&
+ !client->no_extra_disconnect_reason && reason != NULL) {
+ const char *extra_reason =
+ client_get_extra_disconnect_reason(client);
+ if (extra_reason[0] != '\0')
+ reason = t_strconcat(reason, " ", extra_reason, NULL);
+ }
+ if (reason != NULL) {
+ struct event *event = client->login_proxy == NULL ?
+ client->event :
+ login_proxy_get_event(client->login_proxy);
+ if (add_disconnected_prefix)
+ e_info(event, "Disconnected: %s", reason);
+ else
+ e_info(event, "%s", reason);
+ }
+
+ if (client->output != NULL)
+ o_stream_uncork(client->output);
+ if (!client->login_success) {
+ bool unref = FALSE;
+
+ io_remove(&client->io);
+ ssl_iostream_destroy(&client->ssl_iostream);
+ if (client->iostream_fd_proxy != NULL) {
+ iostream_proxy_unref(&client->iostream_fd_proxy);
+ unref = TRUE;
+ }
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+ i_close_fd(&client->fd);
+ if (unref) {
+ i_assert(client->refcount > 1);
+ client_unref(&client);
+ }
+ } else {
+ /* Login was successful. We may now be proxying the connection,
+ so don't disconnect the client until client_unref(). */
+ if (client->iostream_fd_proxy != NULL) {
+ i_assert(!client->fd_proxying);
+ client->fd_proxying = TRUE;
+ i_assert(client->list_type == CLIENT_LIST_TYPE_DESTROYED);
+ DLLIST_REMOVE(&destroyed_clients, client);
+ client->list_type = CLIENT_LIST_TYPE_FD_PROXY;
+ DLLIST_PREPEND(&client_fd_proxies, client);
+ client_fd_proxies_count++;
+ }
+ }
+}
+
+void client_destroy(struct client *client, const char *reason)
+{
+ i_assert(client->create_finished);
+
+ if (client->destroyed)
+ return;
+ client->destroyed = TRUE;
+
+ if (last_client == client)
+ last_client = client->prev;
+ /* move to destroyed_clients linked list before it's potentially
+ added to client_fd_proxies. */
+ i_assert(!client->fd_proxying);
+ i_assert(client->list_type == CLIENT_LIST_TYPE_ACTIVE);
+ DLLIST_REMOVE(&clients, client);
+ client->list_type = CLIENT_LIST_TYPE_DESTROYED;
+ DLLIST_PREPEND(&destroyed_clients, client);
+
+ client_disconnect(client, reason, !client->login_success);
+
+ pool_unref(&client->preproxy_pool);
+ client->forward_fields = NULL;
+ client->client_id = NULL;
+
+ if (client->master_tag != 0) {
+ i_assert(client->auth_request == NULL);
+ i_assert(client->authenticating);
+ i_assert(client->refcount > 1);
+ client->authenticating = FALSE;
+ master_auth_request_abort(master_auth, client->master_tag);
+ client->refcount--;
+ } else if (client->auth_request != NULL ||
+ client->anvil_query != NULL ||
+ client->final_response) {
+ i_assert(client->authenticating);
+ sasl_server_auth_abort(client);
+ }
+ i_assert(!client->authenticating);
+ i_assert(client->auth_request == NULL);
+ i_assert(client->anvil_query == NULL);
+
+ timeout_remove(&client->to_disconnect);
+ timeout_remove(&client->to_auth_waiting);
+ str_free(&client->auth_response);
+
+ if (client->proxy_password != NULL) {
+ safe_memset(client->proxy_password, 0,
+ strlen(client->proxy_password));
+ i_free_and_null(client->proxy_password);
+ }
+
+ dsasl_client_free(&client->proxy_sasl_client);
+ if (client->login_proxy != NULL)
+ login_proxy_free(&client->login_proxy);
+ if (client->v.destroy != NULL)
+ client->v.destroy(client);
+ if (client_unref(&client) && initial_service_count == 1) {
+ /* as soon as this connection is done with proxying
+ (or whatever), the process will die. there's no need for
+ authentication anymore, so close the connection.
+ do this only with initial service_count=1, in case there
+ are other clients with pending authentications */
+ auth_client_disconnect(auth_client, "unnecessary connection");
+ }
+ login_client_destroyed();
+ login_refresh_proctitle();
+}
+
+void client_destroy_iostream_error(struct client *client)
+{
+ const char *reason =
+ io_stream_get_disconnect_reason(client->input, client->output);
+ client_destroy(client, reason);
+}
+
+void client_destroy_success(struct client *client, const char *reason)
+{
+ client->login_success = TRUE;
+ client_destroy(client, reason);
+}
+
+void client_ref(struct client *client)
+{
+ client->refcount++;
+}
+
+bool client_unref(struct client **_client)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ i_assert(client->refcount > 0);
+ if (--client->refcount > 0)
+ return TRUE;
+
+ if (!client->create_finished) {
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+ pool_unref(&client->preproxy_pool);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+ return FALSE;
+ }
+
+ i_assert(client->destroyed);
+ i_assert(client->login_proxy == NULL);
+
+ if (client->v.free != NULL)
+ client->v.free(client);
+
+ ssl_iostream_destroy(&client->ssl_iostream);
+ iostream_proxy_unref(&client->iostream_fd_proxy);
+ if (client->fd_proxying) {
+ i_assert(client->list_type == CLIENT_LIST_TYPE_FD_PROXY);
+ DLLIST_REMOVE(&client_fd_proxies, client);
+ i_assert(client_fd_proxies_count > 0);
+ client_fd_proxies_count--;
+ } else {
+ i_assert(client->list_type == CLIENT_LIST_TYPE_DESTROYED);
+ DLLIST_REMOVE(&destroyed_clients, client);
+ }
+ client->list_type = CLIENT_LIST_TYPE_NONE;
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+ i_close_fd(&client->fd);
+ event_unref(&client->event);
+
+ i_free(client->proxy_user);
+ i_free(client->proxy_master_user);
+ i_free(client->virtual_user);
+ i_free(client->virtual_user_orig);
+ i_free(client->virtual_auth_user);
+ i_free(client->auth_mech_name);
+ i_free(client->master_data_prefix);
+ pool_unref(&client->pool);
+
+ i_assert(clients_count > 0);
+ clients_count--;
+
+ master_service_client_connection_destroyed(master_service);
+ login_refresh_proctitle();
+ return FALSE;
+}
+
+void client_common_default_free(struct client *client ATTR_UNUSED)
+{
+}
+
+bool client_destroy_oldest(bool kill, struct timeval *created_r)
+{
+ struct client *client;
+
+ if (last_client == NULL) {
+ /* we have no clients */
+ return FALSE;
+ }
+
+ /* destroy the last client that hasn't successfully authenticated yet.
+ this is usually the last client, but don't kill it if it's just
+ waiting for master to finish its job. Also prefer to kill clients
+ that can immediately be killed (i.e. refcount=1) */
+ for (client = last_client; client != NULL; client = client->prev) {
+ if (client->master_tag == 0 && client->refcount == 1)
+ break;
+ }
+ if (client == NULL)
+ client = last_client;
+
+ *created_r = client->created;
+ if (!kill)
+ return TRUE;
+
+ client_notify_disconnect(client, CLIENT_DISCONNECT_RESOURCE_CONSTRAINT,
+ "Connection queue full");
+ client_ref(client);
+ client_destroy(client, "Connection queue full");
+ /* return TRUE only if the client was actually freed */
+ i_assert(client->create_finished);
+ return !client_unref(&client);
+}
+
+void clients_destroy_all_reason(const char *reason)
+{
+ struct client *client, *next;
+
+ for (client = clients; client != NULL; client = next) {
+ next = client->next;
+ client_notify_disconnect(client,
+ CLIENT_DISCONNECT_SYSTEM_SHUTDOWN, reason);
+ client_destroy(client, reason);
+ }
+}
+
+void clients_destroy_all(void)
+{
+ clients_destroy_all_reason("Shutting down");
+}
+
+static int client_sni_callback(const char *name, const char **error_r,
+ void *context)
+{
+ struct client *client = context;
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream_settings ssl_set;
+ void **other_sets;
+ const char *error;
+
+ if (client->ssl_servername_settings_read)
+ return 0;
+ client->ssl_servername_settings_read = TRUE;
+
+ client->local_name = p_strdup(client->pool, name);
+ client->set = login_settings_read(client->pool, &client->local_ip,
+ &client->ip, name,
+ &client->ssl_set,
+ &client->ssl_server_set, &other_sets);
+
+ master_service_ssl_server_settings_to_iostream_set(client->ssl_set,
+ client->ssl_server_set, pool_datastack_create(), &ssl_set);
+ if (ssl_iostream_server_context_cache_get(&ssl_set, &ssl_ctx, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Failed to initialize SSL server context: %s", error);
+ return -1;
+ }
+ ssl_iostream_change_context(client->ssl_iostream, ssl_ctx);
+ ssl_iostream_context_unref(&ssl_ctx);
+ return 0;
+}
+
+int client_init_ssl(struct client *client)
+{
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ i_assert(client->fd != -1);
+
+ if (strcmp(client->ssl_set->ssl, "no") == 0) {
+ e_info(client->event, "SSL is disabled (ssl=no)");
+ return -1;
+ }
+
+ master_service_ssl_server_settings_to_iostream_set(client->ssl_set,
+ client->ssl_server_set, pool_datastack_create(), &ssl_set);
+ /* If the client cert is invalid, we'll reply NO to the login
+ command. */
+ ssl_set.allow_invalid_cert = TRUE;
+ if (ssl_iostream_server_context_cache_get(&ssl_set, &ssl_ctx, &error) < 0) {
+ e_error(client->event,
+ "Failed to initialize SSL server context: %s", error);
+ return -1;
+ }
+ if (io_stream_create_ssl_server(ssl_ctx, &ssl_set,
+ &client->input, &client->output,
+ &client->ssl_iostream, &error) < 0) {
+ e_error(client->event,
+ "Failed to initialize SSL connection: %s", error);
+ ssl_iostream_context_unref(&ssl_ctx);
+ return -1;
+ }
+ ssl_iostream_context_unref(&ssl_ctx);
+ ssl_iostream_set_sni_callback(client->ssl_iostream,
+ client_sni_callback, client);
+
+ client->tls = TRUE;
+ client->secured = TRUE;
+ client->ssl_secured = TRUE;
+
+ if (client->starttls) {
+ io_remove(&client->io);
+ if (!client_does_custom_io(client)) {
+ client->io = io_add_istream(client->input,
+ client_input, client);
+ }
+ }
+ return 0;
+}
+
+static void client_start_tls(struct client *client)
+{
+ client->starttls = TRUE;
+ if (client_init_ssl(client) < 0) {
+ client_notify_disconnect(client,
+ CLIENT_DISCONNECT_INTERNAL_ERROR,
+ "TLS initialization failed.");
+ client_destroy(client, "TLS initialization failed.");
+ return;
+ }
+ login_refresh_proctitle();
+
+ client->v.starttls(client);
+}
+
+static int client_output_starttls(struct client *client)
+{
+ int ret;
+
+ if ((ret = o_stream_flush(client->output)) < 0) {
+ client_destroy_iostream_error(client);
+ return 1;
+ }
+
+ if (ret > 0) {
+ o_stream_unset_flush_callback(client->output);
+ client_start_tls(client);
+ }
+ return 1;
+}
+
+void client_cmd_starttls(struct client *client)
+{
+ if (client->tls) {
+ client->v.notify_starttls(client, FALSE, "TLS is already active.");
+ return;
+ }
+
+ if (!client_is_tls_enabled(client)) {
+ client->v.notify_starttls(client, FALSE, "TLS support isn't enabled.");
+ return;
+ }
+
+ /* remove input handler, SSL proxy gives us a new fd. we also have to
+ remove it in case we have to wait for buffer to be flushed */
+ io_remove(&client->io);
+
+ client->v.notify_starttls(client, TRUE, "Begin TLS negotiation now.");
+
+ /* uncork the old fd */
+ o_stream_uncork(client->output);
+
+ if (o_stream_flush(client->output) <= 0) {
+ /* the buffer has to be flushed */
+ o_stream_set_flush_pending(client->output, TRUE);
+ o_stream_set_flush_callback(client->output,
+ client_output_starttls, client);
+ } else {
+ client_start_tls(client);
+ }
+}
+
+static void
+iostream_fd_proxy_finished(enum iostream_proxy_side side ATTR_UNUSED,
+ enum iostream_proxy_status status ATTR_UNUSED,
+ struct client *client)
+{
+ /* Destroy the proxy now. The other side of the proxy is still
+ unfinished and we don't want to get back here and unreference
+ the client twice. */
+ iostream_proxy_unref(&client->iostream_fd_proxy);
+ client_unref(&client);
+}
+
+int client_get_plaintext_fd(struct client *client, int *fd_r, bool *close_fd_r)
+{
+ int fds[2];
+
+ if (!client->tls) {
+ /* Plaintext connection - We can send the fd directly to
+ the post-login process without any proxying. */
+ *fd_r = client->fd;
+ *close_fd_r = FALSE;
+ return 0;
+ }
+
+ /* We'll have to start proxying from now on until either side
+ disconnects. Create a socketpair where login process is proxying on
+ one side and the other side is sent to the post-login process. */
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
+ e_error(client->event, "socketpair() failed: %m");
+ return -1;
+ }
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+
+ struct ostream *output = o_stream_create_fd(fds[0], IO_BLOCK_SIZE);
+ struct istream *input =
+ i_stream_create_fd_autoclose(&fds[0], IO_BLOCK_SIZE);
+ o_stream_set_no_error_handling(output, TRUE);
+
+ i_assert(client->io == NULL);
+
+ client_ref(client);
+ client->iostream_fd_proxy =
+ iostream_proxy_create(input, output,
+ client->input, client->output);
+ i_stream_unref(&input);
+ o_stream_unref(&output);
+
+ iostream_proxy_set_completion_callback(client->iostream_fd_proxy,
+ iostream_fd_proxy_finished,
+ client);
+ iostream_proxy_start(client->iostream_fd_proxy);
+
+ *fd_r = fds[1];
+ *close_fd_r = TRUE;
+ return 0;
+}
+
+unsigned int clients_get_count(void)
+{
+ return clients_count;
+}
+
+unsigned int clients_get_fd_proxies_count(void)
+{
+ return client_fd_proxies_count;
+}
+
+struct client *clients_get_first_fd_proxy(void)
+{
+ return client_fd_proxies;
+}
+
+void client_add_forward_field(struct client *client, const char *key,
+ const char *value)
+{
+ if (client->forward_fields == NULL)
+ client->forward_fields = str_new(client->preproxy_pool, 32);
+ else
+ str_append_c(client->forward_fields, '\t');
+ /* prefixing is done by auth process */
+ str_append_tabescaped(client->forward_fields, key);
+ str_append_c(client->forward_fields, '=');
+ str_append_tabescaped(client->forward_fields, value);
+}
+
+const char *client_get_session_id(struct client *client)
+{
+ buffer_t *buf, *base64_buf;
+ struct timeval tv;
+ uint64_t timestamp;
+ unsigned int i;
+
+ if (client->session_id != NULL)
+ return client->session_id;
+
+ buf = t_buffer_create(24);
+ base64_buf = t_buffer_create(24*2);
+
+ i_gettimeofday(&tv);
+ timestamp = tv.tv_usec + (long long)tv.tv_sec * 1000ULL*1000ULL;
+
+ /* add lowest 48 bits of the timestamp. this gives us a bit less than
+ 9 years until it wraps */
+ for (i = 0; i < 48; i += 8)
+ buffer_append_c(buf, (timestamp >> i) & 0xff);
+
+ buffer_append_c(buf, client->remote_port & 0xff);
+ buffer_append_c(buf, (client->remote_port >> 8) & 0xff);
+ if (IPADDR_IS_V6(&client->ip))
+ buffer_append(buf, &client->ip.u.ip6, sizeof(client->ip.u.ip6));
+ else
+ buffer_append(buf, &client->ip.u.ip4, sizeof(client->ip.u.ip4));
+ base64_encode(buf->data, buf->used, base64_buf);
+ client->session_id = p_strdup(client->pool, str_c(base64_buf));
+ return client->session_id;
+}
+
+/* increment index if new proper login variables are added
+ * make sure the aliases stay in the current order */
+#define VAR_EXPAND_ALIAS_INDEX_START 27
+
+static struct var_expand_table login_var_expand_empty_tab[] = {
+ { 'u', NULL, "user" },
+ { 'n', NULL, "username" },
+ { 'd', NULL, "domain" },
+
+ { 's', NULL, "service" },
+ { 'h', NULL, "home" },
+ { 'l', NULL, "lip" },
+ { 'r', NULL, "rip" },
+ { 'p', NULL, "pid" },
+ { 'm', NULL, "mech" },
+ { 'a', NULL, "lport" },
+ { 'b', NULL, "rport" },
+ { 'c', NULL, "secured" },
+ { 'k', NULL, "ssl_security" },
+ { 'e', NULL, "mail_pid" },
+ { '\0', NULL, "session" },
+ { '\0', NULL, "real_lip" },
+ { '\0', NULL, "real_rip" },
+ { '\0', NULL, "real_lport" },
+ { '\0', NULL, "real_rport" },
+ { '\0', NULL, "orig_user" },
+ { '\0', NULL, "orig_username" },
+ { '\0', NULL, "orig_domain" },
+ { '\0', NULL, "auth_user" },
+ { '\0', NULL, "auth_username" },
+ { '\0', NULL, "auth_domain" },
+ { '\0', NULL, "listener" },
+ { '\0', NULL, "local_name" },
+
+ /* aliases: */
+ { '\0', NULL, "local_ip" },
+ { '\0', NULL, "remote_ip" },
+ { '\0', NULL, "local_port" },
+ { '\0', NULL, "remote_port" },
+ { '\0', NULL, "real_local_ip" },
+ { '\0', NULL, "real_remote_ip" },
+ { '\0', NULL, "real_local_port" },
+ { '\0', NULL, "real_remote_port" },
+ { '\0', NULL, "mechanism" },
+ { '\0', NULL, "original_user" },
+ { '\0', NULL, "original_username" },
+ { '\0', NULL, "original_domain" },
+
+ { '\0', NULL, NULL }
+};
+
+static void
+get_var_expand_users(struct var_expand_table *tab, const char *user)
+{
+ unsigned int i;
+
+ tab[0].value = user;
+ tab[1].value = t_strcut(user, '@');
+ tab[2].value = i_strchr_to_next(user, '@');
+
+ for (i = 0; i < 3; i++)
+ tab[i].value = str_sanitize(tab[i].value, 80);
+}
+
+static const struct var_expand_table *
+get_var_expand_table(struct client *client)
+{
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(login_var_expand_empty_tab));
+ memcpy(tab, login_var_expand_empty_tab,
+ sizeof(login_var_expand_empty_tab));
+
+ if (client->virtual_user != NULL)
+ get_var_expand_users(tab, client->virtual_user);
+ tab[3].value = login_binary->protocol;
+ tab[4].value = getenv("HOME");
+ tab[VAR_EXPAND_ALIAS_INDEX_START].value = tab[5].value =
+ net_ip2addr(&client->local_ip);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 1].value = tab[6].value =
+ net_ip2addr(&client->ip);
+ tab[7].value = my_pid;
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 8].value = tab[8].value =
+ client->auth_mech_name == NULL ? NULL :
+ str_sanitize(client->auth_mech_name, MAX_MECH_NAME);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 2].value = tab[9].value =
+ dec2str(client->local_port);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 3].value = tab[10].value =
+ dec2str(client->remote_port);
+ if (!client->tls) {
+ tab[11].value = client->secured ? "secured" : NULL;
+ tab[12].value = "";
+ } else if (client->proxied_ssl) {
+ tab[11].value = "TLS";
+ tab[12].value = "(proxied)";
+ } else if (client->ssl_iostream != NULL) {
+ const char *ssl_state =
+ ssl_iostream_is_handshaked(client->ssl_iostream) ?
+ "TLS" : "TLS handshaking";
+ const char *ssl_error =
+ ssl_iostream_get_last_error(client->ssl_iostream);
+
+ tab[11].value = ssl_error == NULL ? ssl_state :
+ t_strdup_printf("%s: %s", ssl_state, ssl_error);
+ tab[12].value =
+ ssl_iostream_get_security_string(client->ssl_iostream);
+ } else {
+ tab[11].value = "TLS";
+ tab[12].value = "";
+ }
+ tab[13].value = client->mail_pid == 0 ? "" :
+ dec2str(client->mail_pid);
+ tab[14].value = client_get_session_id(client);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 4].value = tab[15].value =
+ net_ip2addr(&client->real_local_ip);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 5].value = tab[16].value =
+ net_ip2addr(&client->real_remote_ip);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 6].value = tab[17].value =
+ dec2str(client->real_local_port);
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 7].value = tab[18].value =
+ dec2str(client->real_remote_port);
+ if (client->virtual_user_orig != NULL)
+ get_var_expand_users(tab+19, client->virtual_user_orig);
+ else {
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 9].value = tab[19].value = tab[0].value;
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 10].value = tab[20].value = tab[1].value;
+ tab[VAR_EXPAND_ALIAS_INDEX_START + 11].value = tab[21].value = tab[2].value;
+ }
+ if (client->virtual_auth_user != NULL)
+ get_var_expand_users(tab+22, client->virtual_auth_user);
+ else {
+ tab[22].value = tab[19].value;
+ tab[23].value = tab[20].value;
+ tab[24].value = tab[21].value;
+ }
+ tab[25].value = client->listener_name;
+ tab[26].value = str_sanitize(client->local_name, 256);
+ return tab;
+}
+
+static bool have_username_key(const char *str)
+{
+ char key;
+
+ for (; *str != '\0'; str++) {
+ if (str[0] == '%' && str[1] != '\0') {
+ str++;
+ key = var_get_key(str);
+ if (key == 'u' || key == 'n')
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static int
+client_var_expand_func_passdb(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct client *client = context;
+ const char *field_name = data;
+ unsigned int i;
+ size_t field_name_len;
+
+ *value_r = NULL;
+
+ if (client->auth_passdb_args == NULL)
+ return 1;
+
+ field_name_len = strlen(field_name);
+ for (i = 0; client->auth_passdb_args[i] != NULL; i++) {
+ if (strncmp(client->auth_passdb_args[i], field_name,
+ field_name_len) == 0 &&
+ client->auth_passdb_args[i][field_name_len] == '=') {
+ *value_r = client->auth_passdb_args[i] + field_name_len+1;
+ return 1;
+ }
+ }
+ return 1;
+}
+
+static const char *
+client_get_log_str(struct client *client, const char *msg)
+{
+ static const struct var_expand_func_table func_table[] = {
+ { "passdb", client_var_expand_func_passdb },
+ { NULL, NULL }
+ };
+ static bool expand_error_logged = FALSE;
+ const struct var_expand_table *var_expand_table;
+ char *const *e;
+ const char *error;
+ string_t *str, *str2;
+ unsigned int pos;
+
+ var_expand_table = get_var_expand_table(client);
+
+ str = t_str_new(256);
+ str2 = t_str_new(128);
+ for (e = client->set->log_format_elements_split; *e != NULL; e++) {
+ pos = str_len(str);
+ if (var_expand_with_funcs(str, *e, var_expand_table,
+ func_table, client, &error) <= 0 &&
+ !expand_error_logged) {
+ /* NOTE: Don't log via client->event - it would cause
+ recursion */
+ i_error("Failed to expand log_format_elements=%s: %s",
+ *e, error);
+ expand_error_logged = TRUE;
+ }
+ if (have_username_key(*e)) {
+ /* username is added even if it's empty */
+ } else {
+ str_truncate(str2, 0);
+ if (var_expand(str2, *e, login_var_expand_empty_tab,
+ &error) <= 0) {
+ /* we just logged this error above. no need
+ to do it again. */
+ }
+ if (strcmp(str_c(str)+pos, str_c(str2)) == 0) {
+ /* empty %variables, don't add */
+ str_truncate(str, pos);
+ continue;
+ }
+ }
+
+ if (str_len(str) > 0)
+ str_append(str, ", ");
+ }
+
+ if (str_len(str) > 0)
+ str_truncate(str, str_len(str)-2);
+
+ const struct var_expand_table tab[3] = {
+ { 's', t_strdup(str_c(str)), NULL },
+ { '$', msg, NULL },
+ { '\0', NULL, NULL }
+ };
+
+ str_truncate(str, 0);
+ if (var_expand(str, client->set->login_log_format, tab, &error) <= 0) {
+ /* NOTE: Don't log via client->event - it would cause
+ recursion */
+ i_error("Failed to expand login_log_format=%s: %s",
+ client->set->login_log_format, error);
+ expand_error_logged = TRUE;
+ }
+ return str_c(str);
+}
+
+bool client_is_tls_enabled(struct client *client)
+{
+ return login_ssl_initialized && strcmp(client->ssl_set->ssl, "no") != 0;
+}
+
+const char *client_get_extra_disconnect_reason(struct client *client)
+{
+ unsigned int auth_secs = client->auth_first_started == 0 ? 0 :
+ ioloop_time - client->auth_first_started;
+
+ if (client->set->auth_ssl_require_client_cert &&
+ client->ssl_iostream != NULL) {
+ if (ssl_iostream_has_broken_client_cert(client->ssl_iostream))
+ return "(client sent an invalid cert)";
+ if (!ssl_iostream_has_valid_client_cert(client->ssl_iostream))
+ return "(client didn't send a cert)";
+ }
+
+ if (!client->notified_auth_ready)
+ return t_strdup_printf(
+ "(disconnected before auth was ready, waited %u secs)",
+ (unsigned int)(ioloop_time - client->created.tv_sec));
+
+ if (client->auth_attempts == 0) {
+ if (!client->banner_sent) {
+ /* disconnected by a plugin */
+ return "";
+ }
+ return t_strdup_printf("(no auth attempts in %u secs)",
+ (unsigned int)(ioloop_time - client->created.tv_sec));
+ }
+
+ /* some auth attempts without SSL/TLS */
+ if (client->set->auth_ssl_require_client_cert &&
+ client->ssl_iostream == NULL)
+ return "(cert required, client didn't start TLS)";
+
+ if (client->auth_waiting && client->auth_attempts == 1) {
+ return t_strdup_printf("(client didn't finish SASL auth, "
+ "waited %u secs)", auth_secs);
+ }
+ if (client->auth_request != NULL && client->auth_attempts == 1) {
+ return t_strdup_printf("(disconnected while authenticating, "
+ "waited %u secs)", auth_secs);
+ }
+ if (client->authenticating && client->auth_attempts == 1) {
+ return t_strdup_printf("(disconnected while finishing login, "
+ "waited %u secs)", auth_secs);
+ }
+ if (client->auth_try_aborted && client->auth_attempts == 1)
+ return "(aborted authentication)";
+ if (client->auth_process_comm_fail)
+ return "(auth process communication failure)";
+
+ if (client->proxy_auth_failed)
+ return "(proxy dest auth failed)";
+ if (client->auth_successes > 0) {
+ return t_strdup_printf("(internal failure, %u successful auths)",
+ client->auth_successes);
+ }
+
+ switch (client->last_auth_fail) {
+ case CLIENT_AUTH_FAIL_CODE_AUTHZFAILED:
+ return t_strdup_printf(
+ "(authorization failed, %u attempts in %u secs)",
+ client->auth_attempts, auth_secs);
+ case CLIENT_AUTH_FAIL_CODE_TEMPFAIL:
+ return "(auth service reported temporary failure)";
+ case CLIENT_AUTH_FAIL_CODE_USER_DISABLED:
+ return "(user disabled)";
+ case CLIENT_AUTH_FAIL_CODE_PASS_EXPIRED:
+ return "(password expired)";
+ case CLIENT_AUTH_FAIL_CODE_INVALID_BASE64:
+ return "(sent invalid base64 in response)";
+ case CLIENT_AUTH_FAIL_CODE_LOGIN_DISABLED:
+ return "(login disabled)";
+ case CLIENT_AUTH_FAIL_CODE_MECH_INVALID:
+ return "(tried to use unsupported auth mechanism)";
+ case CLIENT_AUTH_FAIL_CODE_MECH_SSL_REQUIRED:
+ return "(tried to use disallowed plaintext auth)";
+ default:
+ break;
+ }
+
+ return t_strdup_printf("(auth failed, %u attempts in %u secs)",
+ client->auth_attempts, auth_secs);
+}
+
+void client_notify_disconnect(struct client *client,
+ enum client_disconnect_reason reason,
+ const char *text)
+{
+ if (!client->notified_disconnect) {
+ if (client->v.notify_disconnect != NULL)
+ client->v.notify_disconnect(client, reason, text);
+ client->notified_disconnect = TRUE;
+ }
+}
+
+void client_notify_auth_ready(struct client *client)
+{
+ if (!client->notified_auth_ready) {
+ if (client->v.notify_auth_ready != NULL)
+ client->v.notify_auth_ready(client);
+ client->notified_auth_ready = TRUE;
+ }
+}
+
+void client_notify_status(struct client *client, bool bad, const char *text)
+{
+ if (client->v.notify_status != NULL)
+ client->v.notify_status(client, bad, text);
+}
+
+void client_common_send_raw_data(struct client *client,
+ const void *data, size_t size)
+{
+ ssize_t ret;
+
+ ret = o_stream_send(client->output, data, size);
+ if (ret < 0 || (size_t)ret != size) {
+ /* either disconnection or buffer full. in either case we want
+ this connection destroyed. however destroying it here might
+ break things if client is still tried to be accessed without
+ being referenced.. */
+ i_stream_close(client->input);
+ }
+}
+
+void client_send_raw_data(struct client *client, const void *data, size_t size)
+{
+ client->v.send_raw_data(client, data, size);
+}
+
+void client_send_raw(struct client *client, const char *data)
+{
+ client_send_raw_data(client, data, strlen(data));
+}
+
+bool client_read(struct client *client)
+{
+ switch (i_stream_read(client->input)) {
+ case -2:
+ /* buffer full */
+ client_notify_disconnect(client,
+ CLIENT_DISCONNECT_RESOURCE_CONSTRAINT,
+ "Input buffer full, aborting");
+ client_destroy(client, "Input buffer full");
+ return FALSE;
+ case -1:
+ /* disconnected */
+ client_destroy_iostream_error(client);
+ return FALSE;
+ case 0:
+ /* nothing new read */
+ return i_stream_get_data_size(client->input) > 0;
+ default:
+ /* something was read */
+ return TRUE;
+ }
+}
+
+void client_input(struct client *client)
+{
+ i_assert(client->v.input != NULL);
+ client->v.input(client);
+}
+
+void client_common_init(void)
+{
+ i_array_init(&module_hooks, 32);
+}
+
+void client_destroy_fd_proxies(void)
+{
+ while (client_fd_proxies != NULL) {
+ struct client *client = client_fd_proxies;
+ client_unref(&client);
+ }
+ i_assert(client_fd_proxies_count == 0);
+}
+
+void client_common_deinit(void)
+{
+ i_assert(destroyed_clients == NULL);
+ array_free(&module_hooks);
+}
diff --git a/src/login-common/client-common.h b/src/login-common/client-common.h
new file mode 100644
index 0000000..a21dea1
--- /dev/null
+++ b/src/login-common/client-common.h
@@ -0,0 +1,373 @@
+#ifndef CLIENT_COMMON_H
+#define CLIENT_COMMON_H
+
+struct module;
+
+#include "net.h"
+#include "login-proxy.h"
+#include "sasl-server.h"
+#include "master-login.h" /* for LOGIN_MAX_SESSION_ID_LEN */
+
+#define LOGIN_MAX_SESSION_ID_LEN 64
+#define LOGIN_MAX_MASTER_PREFIX_LEN 128
+#define LOGIN_MAX_CLIENT_ID_LEN 256
+
+/* max. size of input buffer. this means:
+
+ IMAP: Max. length of command's all parameters. SASL-IR is read into
+ a separate larger buffer.
+ POP3: Max. length of a command line (spec says 512 would be enough)
+*/
+#define LOGIN_MAX_INBUF_SIZE \
+ (MASTER_AUTH_MAX_DATA_SIZE - LOGIN_MAX_MASTER_PREFIX_LEN - \
+ LOGIN_MAX_SESSION_ID_LEN)
+/* max. size of output buffer. if it gets full, the client is disconnected.
+ SASL authentication gives the largest output. */
+#define LOGIN_MAX_OUTBUF_SIZE 4096
+
+/* Max. length of SASL authentication buffer. */
+#define LOGIN_MAX_AUTH_BUF_SIZE 8192
+
+/* Disconnect client after this many milliseconds if it hasn't managed
+ to log in yet. */
+#define CLIENT_LOGIN_TIMEOUT_MSECS (MASTER_LOGIN_TIMEOUT_SECS*1000)
+
+#define AUTH_SERVER_WAITING_MSG \
+ "Waiting for authentication process to respond.."
+#define AUTH_MASTER_WAITING_MSG \
+ "Waiting for authentication master process to respond.."
+
+/* Client logged out without having successfully authenticated. */
+#define CLIENT_UNAUTHENTICATED_LOGOUT_MSG \
+ "Aborted login by logging out"
+
+struct master_service_connection;
+
+enum client_disconnect_reason {
+ CLIENT_DISCONNECT_TIMEOUT,
+ CLIENT_DISCONNECT_SYSTEM_SHUTDOWN,
+ CLIENT_DISCONNECT_RESOURCE_CONSTRAINT,
+ CLIENT_DISCONNECT_INTERNAL_ERROR
+};
+
+enum client_auth_fail_code {
+ CLIENT_AUTH_FAIL_CODE_NONE = 0,
+ CLIENT_AUTH_FAIL_CODE_AUTHZFAILED,
+ CLIENT_AUTH_FAIL_CODE_TEMPFAIL,
+ CLIENT_AUTH_FAIL_CODE_USER_DISABLED,
+ CLIENT_AUTH_FAIL_CODE_PASS_EXPIRED,
+ CLIENT_AUTH_FAIL_CODE_INVALID_BASE64,
+ CLIENT_AUTH_FAIL_CODE_LOGIN_DISABLED,
+ CLIENT_AUTH_FAIL_CODE_MECH_INVALID,
+ CLIENT_AUTH_FAIL_CODE_MECH_SSL_REQUIRED,
+ CLIENT_AUTH_FAIL_CODE_ANONYMOUS_DENIED,
+};
+
+enum client_auth_result {
+ CLIENT_AUTH_RESULT_SUCCESS,
+ CLIENT_AUTH_RESULT_REFERRAL_SUCCESS,
+ CLIENT_AUTH_RESULT_REFERRAL_NOLOGIN,
+ CLIENT_AUTH_RESULT_ABORTED,
+ CLIENT_AUTH_RESULT_AUTHFAILED,
+ CLIENT_AUTH_RESULT_AUTHFAILED_REASON,
+ CLIENT_AUTH_RESULT_AUTHZFAILED,
+ CLIENT_AUTH_RESULT_TEMPFAIL,
+ CLIENT_AUTH_RESULT_PASS_EXPIRED,
+ CLIENT_AUTH_RESULT_SSL_REQUIRED,
+ CLIENT_AUTH_RESULT_INVALID_BASE64,
+ CLIENT_AUTH_RESULT_LOGIN_DISABLED,
+ CLIENT_AUTH_RESULT_MECH_INVALID,
+ CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED,
+ CLIENT_AUTH_RESULT_ANONYMOUS_DENIED
+};
+
+enum client_list_type {
+ CLIENT_LIST_TYPE_NONE = 0,
+ /* clients (disconnected=FALSE, fd_proxying=FALSE, destroyed=FALSE) */
+ CLIENT_LIST_TYPE_ACTIVE,
+ /* destroyed_clients (destroyed=TRUE, fd_proxying=FALSE). Either the
+ client will soon be freed or it's only referenced via
+ "login_proxies". */
+ CLIENT_LIST_TYPE_DESTROYED,
+ /* client_fd_proxies (fd_proxying=TRUE) */
+ CLIENT_LIST_TYPE_FD_PROXY,
+};
+
+struct client_auth_reply {
+ const char *master_user, *reason;
+ enum client_auth_fail_code fail_code;
+
+ /* for proxying */
+ const char *host, *hostip, *source_ip;
+ const char *destuser, *password, *proxy_mech;
+ in_port_t port;
+ unsigned int proxy_timeout_msecs;
+ unsigned int proxy_refresh_secs;
+ unsigned int proxy_host_immediate_failure_after_secs;
+ enum login_proxy_ssl_flags ssl_flags;
+
+ /* all the key=value fields returned by passdb */
+ const char *const *all_fields;
+
+ bool proxy:1;
+ bool proxy_noauth:1;
+ bool proxy_nopipelining:1;
+ bool proxy_not_trusted:1;
+ bool nologin:1;
+};
+
+struct client_vfuncs {
+ struct client *(*alloc)(pool_t pool);
+ void (*create)(struct client *client, void **other_sets);
+ void (*destroy)(struct client *client);
+ void (*notify_auth_ready)(struct client *client);
+ void (*notify_disconnect)(struct client *client,
+ enum client_disconnect_reason reason,
+ const char *text);
+ void (*notify_status)(struct client *client,
+ bool bad, const char *text);
+ void (*notify_starttls)(struct client *client,
+ bool success, const char *text);
+ void (*starttls)(struct client *client);
+ void (*input)(struct client *client);
+ bool (*sasl_filter_mech)(struct client *client,
+ struct auth_mech_desc *mech);
+ bool (*sasl_check_login)(struct client *client);
+ void (*auth_send_challenge)(struct client *client, const char *data);
+ void (*auth_parse_response)(struct client *client);
+ void (*auth_result)(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply,
+ const char *text);
+ void (*proxy_reset)(struct client *client);
+ int (*proxy_parse_line)(struct client *client, const char *line);
+ void (*proxy_failed)(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting);
+ const char *(*proxy_get_state)(struct client *client);
+ void (*send_raw_data)(struct client *client,
+ const void *data, size_t size);
+ bool (*input_next_cmd)(struct client *client);
+ void (*free)(struct client *client);
+};
+
+struct client {
+ struct client *prev, *next;
+ /* Specifies which linked list the client is in */
+ enum client_list_type list_type;
+
+ pool_t pool;
+ /* this pool gets free'd once proxying starts */
+ pool_t preproxy_pool;
+ struct client_vfuncs v;
+ struct client_vfuncs *vlast;
+
+ struct timeval created;
+ int refcount;
+ struct event *event;
+
+ struct ip_addr local_ip;
+ struct ip_addr ip;
+ struct ip_addr real_remote_ip, real_local_ip;
+ in_port_t local_port, remote_port;
+ in_port_t real_local_port, real_remote_port;
+ struct ssl_iostream *ssl_iostream;
+ const struct login_settings *set;
+ const struct master_service_ssl_settings *ssl_set;
+ const struct master_service_ssl_server_settings *ssl_server_set;
+ const char *session_id, *listener_name, *postlogin_socket_path;
+ const char *local_name;
+ const char *client_cert_common_name;
+
+ string_t *client_id;
+ string_t *forward_fields;
+
+ int fd;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+ struct iostream_proxy *iostream_fd_proxy;
+ struct timeout *to_auth_waiting;
+ struct timeout *to_disconnect;
+
+ unsigned char *master_data_prefix;
+ unsigned int master_data_prefix_len;
+
+ struct login_proxy *login_proxy;
+ char *proxy_user, *proxy_master_user, *proxy_password;
+ const struct dsasl_client_mech *proxy_mech;
+ struct dsasl_client *proxy_sasl_client;
+ unsigned int proxy_ttl;
+
+ char *auth_mech_name;
+ enum sasl_server_auth_flags auth_flags;
+ struct auth_client_request *auth_request;
+ string_t *auth_response;
+ time_t auth_first_started, auth_finished;
+ const char *sasl_final_resp;
+ const char *sasl_final_delayed_resp;
+ enum sasl_server_reply delayed_final_reply;
+ const char *const *final_args;
+ const char *const *auth_passdb_args;
+ struct anvil_query *anvil_query;
+ struct anvil_request *anvil_request;
+
+ unsigned int master_auth_id;
+ unsigned int master_tag;
+ sasl_server_callback_t *sasl_callback;
+
+ unsigned int bad_counter;
+ unsigned int auth_attempts, auth_successes;
+ enum client_auth_fail_code last_auth_fail;
+ pid_t mail_pid;
+
+ /* Module-specific contexts. */
+ ARRAY(union login_client_module_context *) module_contexts;
+
+ char *virtual_user, *virtual_user_orig, *virtual_auth_user;
+ /* passdb user_* fields are set here after a successful auth.
+ This is a NULL-terminated array where fields are in the same order
+ as in global_alt_usernames. If some field doesn't exist, it's "".
+ Can also be NULL if there are no user_* fields. */
+ const char **alt_usernames;
+ /* director_username_hash cached, if non-zero */
+ unsigned int director_username_hash_cache;
+
+ bool create_finished:1;
+ bool disconnected:1;
+ bool destroyed:1;
+ bool input_blocked:1;
+ bool login_success:1;
+ bool no_extra_disconnect_reason:1;
+ bool starttls:1;
+ bool tls:1;
+ bool proxied_ssl:1;
+ bool secured:1;
+ bool ssl_secured:1;
+ bool trusted:1;
+ bool ssl_servername_settings_read:1;
+ bool banner_sent:1;
+ bool authenticating:1;
+ bool auth_client_continue_pending:1;
+ bool auth_try_aborted:1;
+ bool auth_initializing:1;
+ bool auth_process_comm_fail:1;
+ bool auth_anonymous:1;
+ bool proxy_auth_failed:1;
+ bool proxy_noauth:1;
+ bool proxy_nopipelining:1;
+ bool proxy_not_trusted:1;
+ bool auth_waiting:1;
+ bool notified_auth_ready:1;
+ bool notified_disconnect:1;
+ bool fd_proxying:1;
+ bool final_response:1;
+ /* ... */
+};
+
+union login_client_module_context {
+ struct client_vfuncs super;
+ struct login_module_register *reg;
+};
+
+struct login_client_hooks {
+ void (*client_allocated)(struct client *client);
+};
+
+extern struct client *clients;
+
+typedef void login_client_allocated_func_t(struct client *client);
+
+void login_client_hooks_add(struct module *module,
+ const struct login_client_hooks *hooks);
+void login_client_hooks_remove(const struct login_client_hooks *hooks);
+
+struct client *
+client_alloc(int fd, pool_t pool,
+ const struct master_service_connection *conn,
+ const struct login_settings *set,
+ const struct master_service_ssl_settings *ssl_set,
+ const struct master_service_ssl_server_settings *ssl_server_set);
+void client_init(struct client *client, void **other_sets);
+void client_disconnect(struct client *client, const char *reason,
+ bool add_disconnected_prefix);
+void client_destroy(struct client *client, const char *reason);
+void client_destroy_iostream_error(struct client *client);
+/* Destroy the client after a successful login. Either the client fd was
+ sent to the post-login process, or the connection will be proxied. */
+void client_destroy_success(struct client *client, const char *reason);
+
+void client_ref(struct client *client);
+bool client_unref(struct client **client) ATTR_NOWARN_UNUSED_RESULT;
+
+int client_init_ssl(struct client *client);
+void client_cmd_starttls(struct client *client);
+
+int client_get_plaintext_fd(struct client *client, int *fd_r, bool *close_fd_r);
+
+unsigned int clients_get_count(void) ATTR_PURE;
+unsigned int clients_get_fd_proxies_count(void);
+struct client *clients_get_first_fd_proxy(void);
+
+void client_add_forward_field(struct client *client, const char *key,
+ const char *value);
+void client_set_title(struct client *client);
+const char *client_get_extra_disconnect_reason(struct client *client);
+
+void client_auth_respond(struct client *client, const char *response);
+void client_auth_abort(struct client *client);
+bool client_is_tls_enabled(struct client *client);
+void client_auth_fail(struct client *client, const char *text);
+const char *client_get_session_id(struct client *client);
+
+bool client_read(struct client *client);
+
+void client_input(struct client *client);
+
+static inline bool
+client_does_custom_io(struct client *client)
+{
+ return (client->v.input == NULL);
+}
+
+void client_notify_auth_ready(struct client *client);
+void client_notify_status(struct client *client, bool bad, const char *text);
+void client_notify_disconnect(struct client *client,
+ enum client_disconnect_reason reason,
+ const char *text);
+
+void client_send_raw_data(struct client *client, const void *data, size_t size);
+void client_send_raw(struct client *client, const char *data);
+void client_common_send_raw_data(struct client *client,
+ const void *data, size_t size);
+void client_common_default_free(struct client *client);
+void client_common_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting);
+
+void client_set_auth_waiting(struct client *client);
+void client_auth_send_challenge(struct client *client, const char *data);
+void client_auth_parse_response(struct client *client);
+int client_auth_begin(struct client *client, const char *mech_name,
+ const char *init_resp);
+int client_auth_begin_private(struct client *client, const char *mech_name,
+ const char *init_resp);
+int client_auth_begin_implicit(struct client *client, const char *mech_name,
+ const char *init_resp);
+bool client_check_plaintext_auth(struct client *client, bool pass_sent);
+int client_auth_read_line(struct client *client);
+
+void client_proxy_finish_destroy_client(struct client *client);
+void client_proxy_log_failure(struct client *client, const char *line);
+const char *client_proxy_get_state(struct client *client);
+
+void clients_notify_auth_connected(void);
+bool client_destroy_oldest(bool kill, struct timeval *created_r);
+void clients_destroy_all(void);
+void clients_destroy_all_reason(const char *reason);
+
+void client_destroy_fd_proxies(void);
+void client_common_init(void);
+void client_common_deinit(void);
+
+#endif
diff --git a/src/login-common/login-common.h b/src/login-common/login-common.h
new file mode 100644
index 0000000..cd6f5fb
--- /dev/null
+++ b/src/login-common/login-common.h
@@ -0,0 +1,79 @@
+#ifndef LOGIN_COMMON_H
+#define LOGIN_COMMON_H
+
+#include "lib.h"
+#include "net.h"
+#include "login-settings.h"
+
+/* Used only for string sanitization */
+#define MAX_MECH_NAME 64
+
+#define AUTH_FAILED_MSG "Authentication failed."
+#define AUTH_TEMP_FAILED_MSG "Temporary authentication failure."
+#define AUTH_PLAINTEXT_DISABLED_MSG \
+ "Plaintext authentication disallowed on non-secure (SSL/TLS) connections."
+
+#define LOGIN_DEFAULT_SOCKET "login"
+#define LOGIN_TOKEN_DEFAULT_SOCKET "tokenlogin"
+
+struct login_binary {
+ /* e.g. imap, pop3 */
+ const char *protocol;
+ /* e.g. imap-login, pop3-login */
+ const char *process_name;
+
+ /* e.g. 143, 110 */
+ in_port_t default_port;
+ /* e.g. 993, 995. if there is no ssl port, use 0. */
+ in_port_t default_ssl_port;
+
+ /* if value is NULL, LOGIN_DEFAULT_SOCKET is used as the default */
+ const char *default_login_socket;
+
+ struct event_category event_category;
+
+ const struct client_vfuncs *client_vfuncs;
+ void (*preinit)(void);
+ void (*init)(void);
+ void (*deinit)(void);
+
+ bool sasl_support_final_reply:1;
+ bool anonymous_login_acceptable:1;
+};
+
+struct login_module_register {
+ unsigned int id;
+};
+extern struct login_module_register login_module_register;
+
+extern struct login_binary *login_binary;
+extern struct auth_client *auth_client;
+extern struct master_auth *master_auth;
+extern bool closing_down, login_debug;
+extern struct anvil_client *anvil;
+extern const char *login_rawlog_dir;
+extern unsigned int initial_service_count;
+/* NULL-terminated array of all alt_usernames seen so far. Existing fields are
+ never removed. */
+extern ARRAY_TYPE(string) global_alt_usernames;
+extern bool login_ssl_initialized;
+
+extern const struct login_settings *global_login_settings;
+extern const struct master_service_ssl_settings *global_ssl_settings;
+extern void **global_other_settings;
+
+extern const struct ip_addr *login_source_ips;
+extern unsigned int login_source_ips_idx, login_source_ips_count;
+extern struct event *event_auth;
+
+
+void login_refresh_proctitle(void);
+void login_client_destroyed(void);
+
+/* Call to guarantee that the "anvil" global variable is initialized. */
+void login_anvil_init(void);
+
+int login_binary_run(struct login_binary *binary,
+ int argc, char *argv[]);
+
+#endif
diff --git a/src/login-common/login-proxy-state.c b/src/login-common/login-proxy-state.c
new file mode 100644
index 0000000..e97d809
--- /dev/null
+++ b/src/login-common/login-proxy-state.c
@@ -0,0 +1,165 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "strescape.h"
+#include "login-proxy-state.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define NOTIFY_RETRY_REOPEN_MSECS (60*1000)
+
+struct login_proxy_state {
+ HASH_TABLE(struct login_proxy_record *,
+ struct login_proxy_record *) hash;
+ pool_t pool;
+
+ const char *notify_path;
+ int notify_fd;
+
+ struct timeout *to_reopen;
+};
+
+static int login_proxy_state_notify_open(struct login_proxy_state *state);
+
+static unsigned int
+login_proxy_record_hash(const struct login_proxy_record *rec)
+{
+ return net_ip_hash(&rec->ip) ^ rec->port;
+}
+
+static int login_proxy_record_cmp(struct login_proxy_record *rec1,
+ struct login_proxy_record *rec2)
+{
+ if (!net_ip_compare(&rec1->ip, &rec2->ip))
+ return 1;
+
+ return (int)rec1->port - (int)rec2->port;
+}
+
+struct login_proxy_state *login_proxy_state_init(const char *notify_path)
+{
+ struct login_proxy_state *state;
+
+ state = i_new(struct login_proxy_state, 1);
+ state->pool = pool_alloconly_create("login proxy state", 1024);
+ hash_table_create(&state->hash, state->pool, 0,
+ login_proxy_record_hash, login_proxy_record_cmp);
+ state->notify_path = p_strdup(state->pool, notify_path);
+ state->notify_fd = -1;
+ return state;
+}
+
+static void login_proxy_state_close(struct login_proxy_state *state)
+{
+ i_close_fd_path(&state->notify_fd, state->notify_path);
+}
+
+void login_proxy_state_deinit(struct login_proxy_state **_state)
+{
+ struct login_proxy_state *state = *_state;
+ struct hash_iterate_context *iter;
+ struct login_proxy_record *rec;
+
+ *_state = NULL;
+
+ /* sanity check: */
+ iter = hash_table_iterate_init(state->hash);
+ while (hash_table_iterate(iter, state->hash, &rec, &rec))
+ i_assert(rec->num_waiting_connections == 0);
+ hash_table_iterate_deinit(&iter);
+
+ timeout_remove(&state->to_reopen);
+ login_proxy_state_close(state);
+ hash_table_destroy(&state->hash);
+ pool_unref(&state->pool);
+ i_free(state);
+}
+
+struct login_proxy_record *
+login_proxy_state_get(struct login_proxy_state *state,
+ const struct ip_addr *ip, in_port_t port)
+{
+ struct login_proxy_record *rec, key;
+
+ i_zero(&key);
+ key.ip = *ip;
+ key.port = port;
+
+ rec = hash_table_lookup(state->hash, &key);
+ if (rec == NULL) {
+ rec = p_new(state->pool, struct login_proxy_record, 1);
+ rec->ip = *ip;
+ rec->port = port;
+ hash_table_insert(state->hash, rec, rec);
+ }
+ return rec;
+}
+
+static void login_proxy_state_reopen(struct login_proxy_state *state)
+{
+ timeout_remove(&state->to_reopen);
+ (void)login_proxy_state_notify_open(state);
+}
+
+static int login_proxy_state_notify_open(struct login_proxy_state *state)
+{
+ if (state->to_reopen != NULL) {
+ /* reopen later */
+ return -1;
+ }
+
+ state->notify_fd = open(state->notify_path, O_WRONLY);
+ if (state->notify_fd == -1) {
+ i_error("open(%s) failed: %m", state->notify_path);
+ state->to_reopen = timeout_add(NOTIFY_RETRY_REOPEN_MSECS,
+ login_proxy_state_reopen, state);
+ return -1;
+ }
+ fd_set_nonblock(state->notify_fd, TRUE);
+ return 0;
+}
+
+static bool login_proxy_state_try_notify(struct login_proxy_state *state,
+ const char *user)
+{
+ size_t len;
+ ssize_t ret;
+
+ if (state->notify_fd == -1) {
+ if (login_proxy_state_notify_open(state) < 0)
+ return TRUE;
+ i_assert(state->notify_fd != -1);
+ }
+
+ T_BEGIN {
+ const char *cmd;
+
+ cmd = t_strconcat(str_tabescape(user), "\n", NULL);
+ len = strlen(cmd);
+ ret = write(state->notify_fd, cmd, len);
+ } T_END;
+
+ if (ret != (ssize_t)len) {
+ if (ret < 0)
+ i_error("write(%s) failed: %m", state->notify_path);
+ else {
+ i_error("write(%s) wrote partial update",
+ state->notify_path);
+ }
+ login_proxy_state_close(state);
+ /* retry sending */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void login_proxy_state_notify(struct login_proxy_state *state,
+ const char *user)
+{
+ if (!login_proxy_state_try_notify(state, user))
+ (void)login_proxy_state_try_notify(state, user);
+}
diff --git a/src/login-common/login-proxy-state.h b/src/login-common/login-proxy-state.h
new file mode 100644
index 0000000..405cd0d
--- /dev/null
+++ b/src/login-common/login-proxy-state.h
@@ -0,0 +1,40 @@
+#ifndef LOGIN_PROXY_STATE_H
+#define LOGIN_PROXY_STATE_H
+
+#include <sys/time.h>
+
+struct login_proxy_record {
+ struct ip_addr ip;
+ in_port_t port;
+
+ /* These are used to spread client-visible disconnects over longer
+ periods of time to avoid reconnect spikes when a server dies.
+
+ If num_disconnects_since_ts=0 when server disconnects us, it's
+ increased and disconnect_timestamp is updated. Afterwards it's
+ increased for each new disconnection. num_disconnects_since_ts gets
+ reset back to zero whenever a) last_success gets updated or b)
+ num_delayed_client_disconnects drops to 0. */
+ struct timeval disconnect_timestamp;
+ unsigned int num_disconnects_since_ts;
+ unsigned int num_delayed_client_disconnects;
+
+ /* these are tracking connect()s, not necessarily logins: */
+ unsigned int num_waiting_connections;
+ /* number of connections we're proxying now (post-login) */
+ unsigned int num_proxying_connections;
+ struct timeval last_failure;
+ struct timeval last_success;
+};
+
+struct login_proxy_state *login_proxy_state_init(const char *notify_path);
+void login_proxy_state_deinit(struct login_proxy_state **state);
+
+struct login_proxy_record *
+login_proxy_state_get(struct login_proxy_state *state,
+ const struct ip_addr *ip, in_port_t port);
+
+void login_proxy_state_notify(struct login_proxy_state *state,
+ const char *user);
+
+#endif
diff --git a/src/login-common/login-proxy.c b/src/login-common/login-proxy.c
new file mode 100644
index 0000000..1fdb3f0
--- /dev/null
+++ b/src/login-common/login-proxy.c
@@ -0,0 +1,1173 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream.h"
+#include "iostream-proxy.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "llist.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "ipc-server.h"
+#include "mail-user-hash.h"
+#include "client-common.h"
+#include "login-proxy-state.h"
+#include "login-proxy.h"
+
+
+#define MAX_PROXY_INPUT_SIZE 4096
+#define PROXY_MAX_OUTBUF_SIZE 1024
+#define LOGIN_PROXY_DIE_IDLE_SECS 2
+#define LOGIN_PROXY_IPC_PATH "ipc-proxy"
+#define LOGIN_PROXY_IPC_NAME "proxy"
+#define LOGIN_PROXY_KILL_PREFIX "Disconnected by proxy: "
+#define KILLED_BY_ADMIN_REASON "Kicked by admin"
+#define KILLED_BY_DIRECTOR_REASON "Kicked via director"
+#define KILLED_BY_SHUTDOWN_REASON "Process shutting down"
+#define LOGIN_PROXY_SIDE_SELF "proxy"
+/* Wait this long before retrying on reconnect */
+#define PROXY_CONNECT_RETRY_MSECS 1000
+/* Don't even try to reconnect if proxying will timeout in less than this. */
+#define PROXY_CONNECT_RETRY_MIN_MSECS (PROXY_CONNECT_RETRY_MSECS + 100)
+#define PROXY_DISCONNECT_INTERVAL_MSECS 100
+
+#define LOGIN_PROXY_SIDE_CLIENT IOSTREAM_PROXY_SIDE_LEFT
+#define LOGIN_PROXY_SIDE_SERVER IOSTREAM_PROXY_SIDE_RIGHT
+
+enum login_proxy_free_flags {
+ LOGIN_PROXY_FREE_FLAG_DELAYED = BIT(0)
+};
+
+struct login_proxy {
+ struct login_proxy *prev, *next;
+
+ struct client *client;
+ struct event *event;
+ int server_fd;
+ struct io *client_wait_io, *server_io;
+ struct istream *client_input, *server_input;
+ struct ostream *client_output, *server_output;
+ struct iostream_proxy *iostream_proxy;
+ struct ssl_iostream *server_ssl_iostream;
+
+ struct timeval created;
+ struct timeout *to, *to_notify;
+ struct login_proxy_record *state_rec;
+
+ struct ip_addr ip, source_ip;
+ char *host;
+ in_port_t port;
+ unsigned int connect_timeout_msecs;
+ unsigned int notify_refresh_secs;
+ unsigned int host_immediate_failure_after_secs;
+ unsigned int reconnect_count;
+ enum login_proxy_ssl_flags ssl_flags;
+ char *rawlog_dir;
+
+ login_proxy_input_callback_t *input_callback;
+ login_proxy_failure_callback_t *failure_callback;
+
+ bool connected:1;
+ bool detached:1;
+ bool destroying:1;
+ bool delayed_disconnect:1;
+ bool disable_reconnect:1;
+ bool num_waiting_connections_updated:1;
+};
+
+static struct login_proxy_state *proxy_state;
+static struct login_proxy *login_proxies = NULL;
+static struct login_proxy *login_proxies_pending = NULL;
+static struct login_proxy *login_proxies_disconnecting = NULL;
+static struct ipc_server *login_proxy_ipc_server;
+static unsigned int detached_login_proxies_count = 0;
+
+static int login_proxy_connect(struct login_proxy *proxy);
+static void login_proxy_disconnect(struct login_proxy *proxy);
+static void login_proxy_ipc_cmd(struct ipc_cmd *cmd, const char *line);
+static void login_proxy_free_final(struct login_proxy *proxy);
+
+static void ATTR_NULL(2)
+login_proxy_free_full(struct login_proxy **_proxy, const char *log_msg,
+ const char *disconnect_side,
+ const char *disconnect_reason,
+ enum login_proxy_free_flags flags);
+
+static time_t proxy_last_io(struct login_proxy *proxy)
+{
+ struct timeval tv1, tv2, tv3, tv4;
+
+ i_stream_get_last_read_time(proxy->client_input, &tv1);
+ i_stream_get_last_read_time(proxy->server_input, &tv2);
+ o_stream_get_last_write_time(proxy->client_output, &tv3);
+ o_stream_get_last_write_time(proxy->server_output, &tv4);
+ return I_MAX(tv1.tv_sec, I_MAX(tv2.tv_sec, I_MAX(tv3.tv_sec, tv4.tv_sec)));
+}
+
+static void login_proxy_free_errstr(struct login_proxy **_proxy,
+ const char *errstr, bool server)
+{
+ struct login_proxy *proxy = *_proxy;
+ string_t *log_msg = t_str_new(128);
+ const char *disconnect_side = server ? "server" : "client";
+
+ str_printfa(log_msg, "Disconnected by %s", disconnect_side);
+ if (errstr[0] != '\0')
+ str_printfa(log_msg, ": %s", errstr);
+
+ str_printfa(log_msg, " (%ds idle, in=%"PRIuUOFF_T", out=%"PRIuUOFF_T,
+ (int)(ioloop_time - proxy_last_io(proxy)),
+ proxy->server_output->offset, proxy->client_output->offset);
+ if (o_stream_get_buffer_used_size(proxy->client_output) > 0) {
+ str_printfa(log_msg, "+%zu",
+ o_stream_get_buffer_used_size(proxy->client_output));
+ }
+ if (iostream_proxy_is_waiting_output(proxy->iostream_proxy,
+ LOGIN_PROXY_SIDE_SERVER))
+ str_append(log_msg, ", client output blocked");
+ if (iostream_proxy_is_waiting_output(proxy->iostream_proxy,
+ LOGIN_PROXY_SIDE_CLIENT))
+ str_append(log_msg, ", server output blocked");
+
+ str_append_c(log_msg, ')');
+ login_proxy_free_full(_proxy, str_c(log_msg), errstr, disconnect_side,
+ server ? LOGIN_PROXY_FREE_FLAG_DELAYED : 0);
+}
+
+static void proxy_client_disconnected_input(struct login_proxy *proxy)
+{
+ /* we're already disconnected from server. either wait for
+ disconnection timeout or for client to disconnect itself. */
+ if (i_stream_read(proxy->client_input) < 0)
+ login_proxy_free_final(proxy);
+ else {
+ i_stream_skip(proxy->client_input,
+ i_stream_get_data_size(proxy->client_input));
+ }
+}
+
+static void proxy_prelogin_input(struct login_proxy *proxy)
+{
+ proxy->input_callback(proxy->client);
+}
+
+static void proxy_plain_connected(struct login_proxy *proxy)
+{
+ proxy->server_input =
+ i_stream_create_fd(proxy->server_fd, MAX_PROXY_INPUT_SIZE);
+ proxy->server_output =
+ o_stream_create_fd(proxy->server_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(proxy->server_output, TRUE);
+
+ proxy->server_io =
+ io_add(proxy->server_fd, IO_READ, proxy_prelogin_input, proxy);
+
+ if (proxy->rawlog_dir != NULL) {
+ if (iostream_rawlog_create(proxy->rawlog_dir,
+ &proxy->server_input,
+ &proxy->server_output) < 0)
+ i_free(proxy->rawlog_dir);
+ }
+}
+
+static void proxy_fail_connect(struct login_proxy *proxy)
+{
+ i_assert(!proxy->num_waiting_connections_updated);
+
+ if (timeval_cmp(&proxy->created, &proxy->state_rec->last_success) < 0) {
+ /* there was a successful connection done since we started
+ connecting. perhaps this is just a temporary one-off
+ failure. */
+ } else {
+ proxy->state_rec->last_failure = ioloop_timeval;
+ }
+ i_assert(proxy->state_rec->num_waiting_connections > 0);
+ proxy->state_rec->num_waiting_connections--;
+ proxy->num_waiting_connections_updated = TRUE;
+}
+
+void login_proxy_append_success_log_info(struct login_proxy *proxy,
+ string_t *str)
+{
+ int msecs = timeval_diff_msecs(&ioloop_timeval, &proxy->created);
+ str_printfa(str, " (%d.%03d secs", msecs/1000, msecs%1000);
+ if (proxy->reconnect_count > 0)
+ str_printfa(str, ", %u reconnects", proxy->reconnect_count);
+ str_append_c(str, ')');
+}
+
+static void
+proxy_connect_error_append(struct login_proxy *proxy, string_t *str)
+{
+ struct ip_addr local_ip;
+ in_port_t local_port;
+
+ if (!proxy->connected) {
+ str_printfa(str, "connect(%s, %u) failed: %m",
+ net_ip2addr(&proxy->ip), proxy->port);
+ } else {
+ str_printfa(str, "Login timed out in state=%s",
+ client_proxy_get_state(proxy->client));
+ }
+ str_printfa(str, " (after %u secs",
+ (unsigned int)(ioloop_time - proxy->created.tv_sec));
+ if (proxy->reconnect_count > 0)
+ str_printfa(str, ", %u reconnects", proxy->reconnect_count);
+
+ if (proxy->server_fd != -1 &&
+ net_getsockname(proxy->server_fd, &local_ip, &local_port) == 0) {
+ str_printfa(str, ", local=%s:%u",
+ net_ip2addr(&local_ip), local_port);
+ } else if (proxy->source_ip.family != 0) {
+ str_printfa(str, ", local=%s",
+ net_ip2addr(&proxy->source_ip));
+ }
+
+ str_append_c(str, ')');
+}
+
+static void proxy_reconnect_timeout(struct login_proxy *proxy)
+{
+ timeout_remove(&proxy->to);
+ (void)login_proxy_connect(proxy);
+}
+
+static bool proxy_try_reconnect(struct login_proxy *proxy)
+{
+ int since_started_msecs, left_msecs;
+
+ if (proxy->reconnect_count >= proxy->client->set->login_proxy_max_reconnects)
+ return FALSE;
+ if (proxy->disable_reconnect)
+ return FALSE;
+
+ since_started_msecs =
+ timeval_diff_msecs(&ioloop_timeval, &proxy->created);
+ if (since_started_msecs < 0)
+ return FALSE; /* time moved backwards */
+ left_msecs = (int)proxy->connect_timeout_msecs - since_started_msecs;
+ if (left_msecs <= PROXY_CONNECT_RETRY_MIN_MSECS)
+ return FALSE;
+
+ login_proxy_disconnect(proxy);
+ proxy->to = timeout_add(PROXY_CONNECT_RETRY_MSECS,
+ proxy_reconnect_timeout, proxy);
+ proxy->reconnect_count++;
+ return TRUE;
+}
+
+static bool proxy_connect_failed(struct login_proxy *proxy)
+{
+ string_t *str = t_str_new(128);
+
+ if (!proxy->connected)
+ proxy_fail_connect(proxy);
+ proxy_connect_error_append(proxy, str);
+ return login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_CONNECT,
+ str_c(str));
+}
+
+static void proxy_wait_connect(struct login_proxy *proxy)
+{
+ errno = net_geterror(proxy->server_fd);
+ if (errno != 0) {
+ (void)proxy_connect_failed(proxy);
+ return;
+ }
+ proxy->connected = TRUE;
+ proxy->num_waiting_connections_updated = TRUE;
+ proxy->state_rec->last_success = ioloop_timeval;
+ i_assert(proxy->state_rec->num_waiting_connections > 0);
+ proxy->state_rec->num_waiting_connections--;
+ proxy->state_rec->num_proxying_connections++;
+ proxy->state_rec->num_disconnects_since_ts = 0;
+
+ io_remove(&proxy->server_io);
+ proxy_plain_connected(proxy);
+
+ if ((proxy->ssl_flags & PROXY_SSL_FLAG_YES) != 0 &&
+ (proxy->ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0) {
+ if (login_proxy_starttls(proxy) < 0) {
+ /* proxy is already destroyed */
+ }
+ }
+}
+
+static void proxy_connect_timeout(struct login_proxy *proxy)
+{
+ errno = ETIMEDOUT;
+ (void)proxy_connect_failed(proxy);
+}
+
+static int login_proxy_connect(struct login_proxy *proxy)
+{
+ struct login_proxy_record *rec = proxy->state_rec;
+
+ e_debug(proxy->event, "Connecting to <%s>",
+ login_proxy_get_ip_str(proxy->client->login_proxy));
+
+ /* this needs to be done early, since login_proxy_free() shrinks
+ num_waiting_connections. */
+ proxy->num_waiting_connections_updated = FALSE;
+ rec->num_waiting_connections++;
+
+ if (proxy->client->proxy_ttl <= 1) {
+ login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ "TTL reached zero - proxies appear to be looping?");
+ return -1;
+ }
+
+ if (rec->last_success.tv_sec == 0) {
+ /* first connect to this IP. don't start immediately failing
+ the check below. */
+ rec->last_success.tv_sec = ioloop_timeval.tv_sec - 1;
+ }
+ if (proxy->host_immediate_failure_after_secs != 0 &&
+ timeval_cmp(&rec->last_failure, &rec->last_success) > 0 &&
+ (unsigned int)(rec->last_failure.tv_sec - rec->last_success.tv_sec) >
+ proxy->host_immediate_failure_after_secs &&
+ rec->num_waiting_connections > 1) {
+ /* the server is down. fail immediately */
+ proxy->disable_reconnect = TRUE;
+ login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_CONNECT,
+ "Host is down");
+ return -1;
+ }
+
+ proxy->server_fd = net_connect_ip(&proxy->ip, proxy->port,
+ proxy->source_ip.family == 0 ? NULL :
+ &proxy->source_ip);
+ if (proxy->server_fd == -1) {
+ if (!proxy_connect_failed(proxy))
+ return -1;
+ /* trying to reconnect later */
+ return 0;
+ }
+
+ in_port_t source_port;
+ if (net_getsockname(proxy->server_fd, NULL, &source_port) == 0)
+ event_add_int(proxy->event, "source_port", source_port);
+
+ proxy->server_io = io_add(proxy->server_fd, IO_WRITE,
+ proxy_wait_connect, proxy);
+ if (proxy->connect_timeout_msecs != 0) {
+ proxy->to = timeout_add(proxy->connect_timeout_msecs,
+ proxy_connect_timeout, proxy);
+ }
+ return 0;
+}
+
+int login_proxy_new(struct client *client, struct event *event,
+ const struct login_proxy_settings *set,
+ login_proxy_input_callback_t *input_callback,
+ login_proxy_failure_callback_t *failure_callback)
+{
+ struct login_proxy *proxy;
+
+ i_assert(set->host != NULL && set->host[0] != '\0');
+ i_assert(client->login_proxy == NULL);
+
+ proxy = i_new(struct login_proxy, 1);
+ proxy->client = client;
+ proxy->event = event;
+ proxy->server_fd = -1;
+ proxy->created = ioloop_timeval;
+ proxy->ip = set->ip;
+ proxy->source_ip = set->source_ip;
+ proxy->host = i_strdup(set->host);
+ proxy->port = set->port;
+ proxy->connect_timeout_msecs = set->connect_timeout_msecs;
+ proxy->notify_refresh_secs = set->notify_refresh_secs;
+ proxy->host_immediate_failure_after_secs =
+ set->host_immediate_failure_after_secs;
+ proxy->ssl_flags = set->ssl_flags;
+ proxy->state_rec = login_proxy_state_get(proxy_state, &proxy->ip,
+ proxy->port);
+ proxy->rawlog_dir = i_strdup_empty(set->rawlog_dir);
+
+ /* add event fields */
+ event_add_str(proxy->event, "source_ip",
+ login_proxy_get_source_host(proxy));
+ event_add_str(proxy->event, "dest_ip", net_ip2addr(&proxy->ip));
+ event_add_int(proxy->event, "dest_port",
+ login_proxy_get_port(proxy));
+ event_add_str(event, "dest_host", set->host);
+ event_add_str(event, "master_user", client->proxy_master_user);
+
+ client_ref(client);
+ event_ref(proxy->event);
+
+ DLLIST_PREPEND(&login_proxies_pending, proxy);
+
+ proxy->input_callback = input_callback;
+ proxy->failure_callback = failure_callback;
+ client->login_proxy = proxy;
+
+ struct event_passthrough *e = event_create_passthrough(proxy->event)->
+ set_name("proxy_session_started");
+ e_debug(e->event(), "Created proxy session to <%s>",
+ login_proxy_get_ip_str(proxy->client->login_proxy));
+
+ return login_proxy_connect(proxy);
+}
+
+static void login_proxy_disconnect(struct login_proxy *proxy)
+{
+ timeout_remove(&proxy->to);
+ timeout_remove(&proxy->to_notify);
+
+ if (!proxy->num_waiting_connections_updated) {
+ i_assert(proxy->state_rec->num_waiting_connections > 0);
+ proxy->state_rec->num_waiting_connections--;
+ proxy->num_waiting_connections_updated = TRUE;
+ }
+ if (proxy->connected) {
+ i_assert(proxy->state_rec->num_proxying_connections > 0);
+ proxy->state_rec->num_proxying_connections--;
+ }
+
+ iostream_proxy_unref(&proxy->iostream_proxy);
+ ssl_iostream_destroy(&proxy->server_ssl_iostream);
+
+ io_remove(&proxy->server_io);
+ i_stream_destroy(&proxy->server_input);
+ o_stream_destroy(&proxy->server_output);
+ if (proxy->server_fd != -1) {
+ net_disconnect(proxy->server_fd);
+ proxy->server_fd = -1;
+ }
+ proxy->connected = FALSE;
+}
+
+static void login_proxy_free_final(struct login_proxy *proxy)
+{
+ i_assert(proxy->server_ssl_iostream == NULL);
+
+ if (proxy->delayed_disconnect) {
+ DLLIST_REMOVE(&login_proxies_disconnecting, proxy);
+
+ i_assert(proxy->state_rec->num_delayed_client_disconnects > 0);
+ if (--proxy->state_rec->num_delayed_client_disconnects == 0)
+ proxy->state_rec->num_disconnects_since_ts = 0;
+ timeout_remove(&proxy->to);
+ }
+
+ io_remove(&proxy->client_wait_io);
+ i_stream_destroy(&proxy->client_input);
+ o_stream_destroy(&proxy->client_output);
+ client_unref(&proxy->client);
+ event_unref(&proxy->event);
+ i_free(proxy->host);
+ i_free(proxy->rawlog_dir);
+ i_free(proxy);
+}
+
+static unsigned int login_proxy_delay_disconnect(struct login_proxy *proxy)
+{
+ struct login_proxy_record *rec = proxy->state_rec;
+ const unsigned int max_delay =
+ proxy->client->set->login_proxy_max_disconnect_delay;
+ struct timeval disconnect_time_offset;
+ unsigned int max_disconnects_per_sec, delay_msecs_since_ts, max_conns;
+ int delay_msecs;
+
+ if (rec->num_disconnects_since_ts == 0) {
+ rec->disconnect_timestamp = ioloop_timeval;
+ /* start from a slightly random timestamp. this way all proxy
+ processes will disconnect at slightly different times to
+ spread the load. */
+ timeval_add_msecs(&rec->disconnect_timestamp,
+ i_rand_limit(PROXY_DISCONNECT_INTERVAL_MSECS));
+ }
+ rec->num_disconnects_since_ts++;
+ if (proxy->to != NULL) {
+ /* we were already lazily disconnecting this */
+ return 0;
+ }
+ if (max_delay == 0) {
+ /* delaying is disabled */
+ return 0;
+ }
+ max_conns = rec->num_proxying_connections + rec->num_disconnects_since_ts;
+ max_disconnects_per_sec = (max_conns + max_delay-1) / max_delay;
+ if (rec->num_disconnects_since_ts <= max_disconnects_per_sec &&
+ rec->num_delayed_client_disconnects == 0) {
+ /* wait delaying until we have 1 second's worth of clients
+ disconnected */
+ return 0;
+ }
+
+ /* see at which time we should be disconnecting the client.
+ do it in 100ms intervals so the timeouts are triggered together. */
+ disconnect_time_offset = rec->disconnect_timestamp;
+ delay_msecs_since_ts = PROXY_DISCONNECT_INTERVAL_MSECS *
+ (max_delay * rec->num_disconnects_since_ts *
+ (1000/PROXY_DISCONNECT_INTERVAL_MSECS) / max_conns);
+ timeval_add_msecs(&disconnect_time_offset, delay_msecs_since_ts);
+ delay_msecs = timeval_diff_msecs(&disconnect_time_offset, &ioloop_timeval);
+ if (delay_msecs <= 0) {
+ /* we already reached the time */
+ return 0;
+ }
+
+ rec->num_delayed_client_disconnects++;
+ proxy->delayed_disconnect = TRUE;
+ proxy->to = timeout_add(delay_msecs, login_proxy_free_final, proxy);
+ DLLIST_PREPEND(&login_proxies_disconnecting, proxy);
+ return delay_msecs;
+}
+
+static void ATTR_NULL(2)
+login_proxy_free_full(struct login_proxy **_proxy, const char *log_msg,
+ const char *disconnect_reason,
+ const char *disconnect_side,
+ enum login_proxy_free_flags flags)
+{
+ struct login_proxy *proxy = *_proxy;
+ struct client *client = proxy->client;
+ unsigned int delay_ms = 0;
+
+ *_proxy = NULL;
+
+ if (proxy->destroying)
+ return;
+ proxy->destroying = TRUE;
+
+ struct event_passthrough *e = event_create_passthrough(proxy->event)->
+ add_str("disconnect_reason", disconnect_reason)->
+ add_str("disconnect_side", disconnect_side)->
+ set_name("proxy_session_finished");
+
+ if (proxy->detached) {
+ i_assert(proxy->connected);
+ e->add_int("idle_secs", ioloop_time - proxy_last_io(proxy));
+ e->add_int("bytes_in", proxy->server_output->offset);
+ e->add_int("bytes_out", proxy->client_output->offset);
+ }
+
+ /* we'll disconnect server side in any case. */
+ login_proxy_disconnect(proxy);
+
+ if (proxy->detached) {
+ /* detached proxy */
+ i_assert(log_msg != NULL || proxy->client->destroyed);
+ DLLIST_REMOVE(&login_proxies, proxy);
+
+ if ((flags & LOGIN_PROXY_FREE_FLAG_DELAYED) != 0)
+ delay_ms = login_proxy_delay_disconnect(proxy);
+
+ if (delay_ms == 0)
+ e_info(e->event(), "%s", log_msg);
+ else {
+ e_info(e->add_int("delay_ms", delay_ms)->event(),
+ "%s - disconnecting client in %ums",
+ log_msg, delay_ms);
+ }
+ i_assert(detached_login_proxies_count > 0);
+ detached_login_proxies_count--;
+ } else {
+ i_assert(proxy->client_input == NULL);
+ i_assert(proxy->client_output == NULL);
+ if (log_msg != NULL)
+ e_debug(e->event(), "%s", log_msg);
+ else
+ e_debug(e->event(), "Failed to connect to %s",
+ login_proxy_get_ip_str(proxy));
+
+ DLLIST_REMOVE(&login_proxies_pending, proxy);
+ }
+ client->login_proxy = NULL;
+
+ if (delay_ms == 0)
+ login_proxy_free_final(proxy);
+ else {
+ i_assert(proxy->client_wait_io == NULL);
+ proxy->client_wait_io = io_add_istream(proxy->client_input,
+ proxy_client_disconnected_input, proxy);
+ }
+}
+
+void login_proxy_free(struct login_proxy **_proxy)
+{
+ struct login_proxy *proxy = *_proxy;
+
+ i_assert(!proxy->detached || proxy->client->destroyed);
+ /* Note: The NULL error is never even attempted to be used here. */
+ login_proxy_free_full(_proxy, NULL, "", LOGIN_PROXY_SIDE_SELF, 0);
+}
+
+bool login_proxy_failed(struct login_proxy *proxy, struct event *event,
+ enum login_proxy_failure_type type, const char *reason)
+{
+ const char *log_prefix;
+ bool try_reconnect = TRUE;
+ event_add_str(event, "error", reason);
+
+ switch (type) {
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL:
+ log_prefix = "Aborting due to internal error: ";
+ try_reconnect = FALSE;
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+ log_prefix = "";
+ try_reconnect = FALSE;
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_CONNECT:
+ log_prefix = "";
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+ try_reconnect = FALSE;
+ /* fall through */
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE:
+ log_prefix = "Aborting due to remote server: ";
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+ log_prefix = "Remote server sent invalid input: ";
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH:
+ log_prefix = "";
+ try_reconnect = FALSE;
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL:
+ log_prefix = "";
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (try_reconnect && proxy_try_reconnect(proxy)) {
+ event_add_int(event, "reconnect_attempts", proxy->reconnect_count);
+ e_debug(event, "%s%s - reconnecting (attempt #%d)",
+ log_prefix, reason, proxy->reconnect_count);
+ proxy->failure_callback(proxy->client, type, reason, TRUE);
+ return TRUE;
+ }
+
+ if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH &&
+ type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL)
+ e_error(event, "%s%s", log_prefix, reason);
+ else if (proxy->client->set->auth_verbose)
+ client_proxy_log_failure(proxy->client, reason);
+ proxy->failure_callback(proxy->client, type, reason, FALSE);
+ return FALSE;
+}
+
+bool login_proxy_is_ourself(const struct client *client, const char *host,
+ in_port_t port, const char *destuser)
+{
+ struct ip_addr ip;
+
+ if (port != client->local_port)
+ return FALSE;
+
+ if (net_addr2ip(host, &ip) < 0)
+ return FALSE;
+ if (!net_ip_compare(&ip, &client->local_ip))
+ return FALSE;
+
+ return strcmp(client->virtual_user, destuser) == 0;
+}
+
+struct istream *login_proxy_get_istream(struct login_proxy *proxy)
+{
+ return proxy->server_input;
+}
+
+struct ostream *login_proxy_get_ostream(struct login_proxy *proxy)
+{
+ return proxy->server_output;
+}
+
+struct event *login_proxy_get_event(struct login_proxy *proxy)
+{
+ return proxy->event;
+}
+
+const char *login_proxy_get_source_host(const struct login_proxy *proxy)
+{
+ return net_ip2addr(&proxy->source_ip);
+}
+
+const char *login_proxy_get_host(const struct login_proxy *proxy)
+{
+ return proxy->host;
+}
+
+const char *login_proxy_get_ip_str(const struct login_proxy *proxy)
+{
+ return net_ip2addr(&proxy->ip);
+}
+
+in_port_t login_proxy_get_port(const struct login_proxy *proxy)
+{
+ return proxy->port;
+}
+
+enum login_proxy_ssl_flags
+login_proxy_get_ssl_flags(const struct login_proxy *proxy)
+{
+ return proxy->ssl_flags;
+}
+
+static void
+login_proxy_finished(enum iostream_proxy_side side,
+ enum iostream_proxy_status status,
+ struct login_proxy *proxy)
+{
+ const char *errstr;
+ bool server_side;
+
+ server_side = side == LOGIN_PROXY_SIDE_SERVER;
+ switch (status) {
+ case IOSTREAM_PROXY_STATUS_INPUT_EOF:
+ /* success */
+ errstr = "";
+ break;
+ case IOSTREAM_PROXY_STATUS_INPUT_ERROR:
+ errstr = side == LOGIN_PROXY_SIDE_CLIENT ?
+ i_stream_get_error(proxy->client_input) :
+ i_stream_get_error(proxy->server_input);
+ break;
+ case IOSTREAM_PROXY_STATUS_OTHER_SIDE_OUTPUT_ERROR:
+ server_side = !server_side;
+ errstr = side == LOGIN_PROXY_SIDE_CLIENT ?
+ o_stream_get_error(proxy->server_output) :
+ o_stream_get_error(proxy->client_output);
+ break;
+ default:
+ i_unreached();
+ }
+ login_proxy_free_errstr(&proxy, errstr, server_side);
+}
+
+static void login_proxy_notify(struct login_proxy *proxy)
+{
+ login_proxy_state_notify(proxy_state, proxy->client->proxy_user);
+}
+
+void login_proxy_detach(struct login_proxy *proxy)
+{
+ struct client *client = proxy->client;
+
+ pool_unref(&proxy->client->preproxy_pool);
+
+ i_assert(!proxy->detached);
+ i_assert(proxy->server_input != NULL);
+ i_assert(proxy->server_output != NULL);
+
+ timeout_remove(&proxy->to);
+ io_remove(&proxy->server_io);
+
+ proxy->detached = TRUE;
+ proxy->client_input = client->input;
+ proxy->client_output = client->output;
+
+ o_stream_set_max_buffer_size(client->output, PROXY_MAX_OUTBUF_SIZE);
+ client->input = NULL;
+ client->output = NULL;
+
+ /* from now on, just do dummy proxying */
+ proxy->iostream_proxy =
+ iostream_proxy_create(proxy->client_input, proxy->client_output,
+ proxy->server_input, proxy->server_output);
+ iostream_proxy_set_completion_callback(proxy->iostream_proxy,
+ login_proxy_finished, proxy);
+ iostream_proxy_start(proxy->iostream_proxy);
+
+ if (proxy->notify_refresh_secs != 0) {
+ proxy->to_notify =
+ timeout_add(proxy->notify_refresh_secs * 1000,
+ login_proxy_notify, proxy);
+ }
+
+ proxy->input_callback = NULL;
+ proxy->failure_callback = NULL;
+
+ if (login_proxy_ipc_server == NULL) {
+ login_proxy_ipc_server =
+ ipc_server_init(LOGIN_PROXY_IPC_PATH,
+ LOGIN_PROXY_IPC_NAME,
+ login_proxy_ipc_cmd);
+ }
+
+ DLLIST_REMOVE(&login_proxies_pending, proxy);
+ DLLIST_PREPEND(&login_proxies, proxy);
+ detached_login_proxies_count++;
+
+ client->login_proxy = NULL;
+}
+
+int login_proxy_starttls(struct login_proxy *proxy)
+{
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ master_service_ssl_client_settings_to_iostream_set(
+ proxy->client->ssl_set, pool_datastack_create(), &ssl_set);
+ if ((proxy->ssl_flags & PROXY_SSL_FLAG_ANY_CERT) != 0)
+ ssl_set.allow_invalid_cert = TRUE;
+ /* NOTE: We're explicitly disabling ssl_client_ca_* settings for now
+ at least. The main problem is that we're chrooted, so we can't read
+ them at this point anyway. The second problem is that especially
+ ssl_client_ca_dir does blocking disk I/O, which could cause
+ unexpected hangs when login process handles multiple clients. */
+ ssl_set.ca_file = ssl_set.ca_dir = NULL;
+
+ io_remove(&proxy->server_io);
+ if (ssl_iostream_client_context_cache_get(&ssl_set, &ssl_ctx, &error) < 0) {
+ const char *reason = t_strdup_printf(
+ "Failed to create SSL client context: %s", error);
+ login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ return -1;
+ }
+
+ if (io_stream_create_ssl_client(ssl_ctx, proxy->host, &ssl_set,
+ &proxy->server_input,
+ &proxy->server_output,
+ &proxy->server_ssl_iostream,
+ &error) < 0) {
+ const char *reason = t_strdup_printf(
+ "Failed to create SSL client: %s", error);
+ login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ ssl_iostream_context_unref(&ssl_ctx);
+ return -1;
+ }
+ ssl_iostream_context_unref(&ssl_ctx);
+ if (ssl_iostream_handshake(proxy->server_ssl_iostream) < 0) {
+ error = ssl_iostream_get_last_error(proxy->server_ssl_iostream);
+ const char *reason = t_strdup_printf(
+ "Failed to start SSL handshake: %s",
+ ssl_iostream_get_last_error(proxy->server_ssl_iostream));
+ login_proxy_failed(proxy, proxy->event,
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ return -1;
+ }
+
+ proxy->server_io = io_add_istream(proxy->server_input,
+ proxy_prelogin_input, proxy);
+ return 0;
+}
+
+static void proxy_kill_idle(struct login_proxy *proxy)
+{
+ login_proxy_free_full(&proxy,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_SHUTDOWN_REASON,
+ KILLED_BY_SHUTDOWN_REASON,
+ LOGIN_PROXY_SIDE_SELF, 0);
+}
+
+void login_proxy_kill_idle(void)
+{
+ struct login_proxy *proxy, *next;
+ time_t now = time(NULL);
+ time_t stop_timestamp = now - LOGIN_PROXY_DIE_IDLE_SECS;
+ unsigned int stop_msecs;
+
+ for (proxy = login_proxies; proxy != NULL; proxy = next) {
+ next = proxy->next;
+ time_t last_io = proxy_last_io(proxy);
+
+ if (last_io <= stop_timestamp)
+ proxy_kill_idle(proxy);
+ else {
+ i_assert(proxy->to == NULL);
+ stop_msecs = (last_io - stop_timestamp) * 1000;
+ proxy->to = timeout_add(stop_msecs,
+ proxy_kill_idle, proxy);
+ }
+ }
+}
+
+static bool
+want_kick_host(struct login_proxy *proxy, const char *const *args,
+ unsigned int key_idx ATTR_UNUSED)
+{
+ return str_array_find(args, proxy->host);
+}
+
+static bool
+want_kick_virtual_user(struct login_proxy *proxy, const char *const *args,
+ unsigned int key_idx ATTR_UNUSED)
+{
+ return str_array_find(args, proxy->client->virtual_user);
+}
+
+static bool
+want_kick_alt_username(struct login_proxy *proxy, const char *const *args,
+ unsigned int key_idx)
+{
+ unsigned int i;
+ struct client *client = proxy->client;
+
+ if (client->alt_usernames == NULL)
+ return FALSE;
+ for (i = 0; i < key_idx; i++) {
+ if (client->alt_usernames[i] == NULL)
+ return FALSE;
+ }
+ if (client->alt_usernames[i] == NULL)
+ return FALSE;
+ return str_array_find(args, client->alt_usernames[i]);
+}
+
+static void
+login_proxy_cmd_kick_full(struct ipc_cmd *cmd, const char *const *args,
+ bool (*want_kick)(struct login_proxy *, const char *const *,
+ unsigned int), unsigned int key_idx)
+{
+ struct login_proxy *proxy, *next;
+ unsigned int count = 0;
+
+ if (args[0] == NULL) {
+ ipc_cmd_fail(&cmd, "Missing parameter");
+ return;
+ }
+
+ for (proxy = login_proxies; proxy != NULL; proxy = next) T_BEGIN {
+ next = proxy->next;
+
+ if (want_kick(proxy, args, key_idx)) {
+ login_proxy_free_full(&proxy,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_ADMIN_REASON,
+ KILLED_BY_ADMIN_REASON,
+ LOGIN_PROXY_SIDE_SELF,
+ LOGIN_PROXY_FREE_FLAG_DELAYED);
+ count++;
+ }
+ } T_END;
+ for (proxy = login_proxies_pending; proxy != NULL; proxy = next) T_BEGIN {
+ next = proxy->next;
+
+ if (want_kick(proxy, args, key_idx)) {
+ client_disconnect(proxy->client,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_ADMIN_REASON,
+ FALSE);
+ client_destroy(proxy->client, NULL);
+ count++;
+ }
+ } T_END;
+ ipc_cmd_success_reply(&cmd, t_strdup_printf("%u", count));
+}
+
+static void
+login_proxy_cmd_kick(struct ipc_cmd *cmd, const char *const *args)
+{
+ login_proxy_cmd_kick_full(cmd, args, want_kick_virtual_user, 0);
+}
+
+static void
+login_proxy_cmd_kick_host(struct ipc_cmd *cmd, const char *const *args)
+{
+ login_proxy_cmd_kick_full(cmd, args, want_kick_host, 0);
+}
+
+static void
+login_proxy_cmd_kick_alt(struct ipc_cmd *cmd, const char *const *args)
+{
+ char *const *fields;
+ unsigned int i, count;
+
+ if (args[0] == NULL) {
+ ipc_cmd_fail(&cmd, "Missing parameter");
+ return;
+ }
+ fields = array_get(&global_alt_usernames, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(fields[i], args[0]) == 0)
+ break;
+ }
+ if (i == count) {
+ /* field doesn't exist, but it's not an error necessarily */
+ ipc_cmd_success_reply(&cmd, "0");
+ return;
+ }
+
+ login_proxy_cmd_kick_full(cmd, args+1, want_kick_alt_username, i);
+}
+
+static bool director_username_hash(struct client *client, unsigned int *hash_r)
+{
+ const char *error;
+
+ if (client->director_username_hash_cache != 0) {
+ /* already set */
+ } else if (!mail_user_hash(client->virtual_user,
+ client->set->director_username_hash,
+ &client->director_username_hash_cache,
+ &error)) {
+ e_error(client->event,
+ "Failed to expand director_username_hash=%s: %s",
+ client->set->director_username_hash, error);
+ return FALSE;
+ }
+
+ *hash_r = client->director_username_hash_cache;
+ return TRUE;
+}
+
+static void
+login_proxy_cmd_kick_director_hash(struct ipc_cmd *cmd, const char *const *args)
+{
+ struct login_proxy *proxy, *next;
+ struct ip_addr except_ip;
+ unsigned int hash, proxy_hash, count = 0;
+
+ if (args[0] == NULL || str_to_uint(args[0], &hash) < 0) {
+ ipc_cmd_fail(&cmd, "Invalid parameters");
+ return;
+ }
+ /* optional except_ip parameter specifies that we're not killing the
+ connections that are proxying to the except_ip backend */
+ except_ip.family = 0;
+ if (args[1] != NULL && args[1][0] != '\0' &&
+ net_addr2ip(args[1], &except_ip) < 0) {
+ ipc_cmd_fail(&cmd, "Invalid except_ip parameter");
+ return;
+ }
+
+ for (proxy = login_proxies; proxy != NULL; proxy = next) {
+ next = proxy->next;
+
+ if (director_username_hash(proxy->client, &proxy_hash) &&
+ proxy_hash == hash &&
+ !net_ip_compare(&proxy->ip, &except_ip)) {
+ login_proxy_free_full(&proxy,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_DIRECTOR_REASON,
+ KILLED_BY_DIRECTOR_REASON,
+ LOGIN_PROXY_SIDE_SELF,
+ LOGIN_PROXY_FREE_FLAG_DELAYED);
+ count++;
+ }
+ }
+ for (proxy = login_proxies_pending; proxy != NULL; proxy = next) {
+ next = proxy->next;
+
+ if (director_username_hash(proxy->client, &proxy_hash) &&
+ proxy_hash == hash &&
+ !net_ip_compare(&proxy->ip, &except_ip)) {
+ client_disconnect(proxy->client,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_DIRECTOR_REASON,
+ FALSE);
+ client_destroy(proxy->client, NULL);
+ count++;
+ }
+ }
+ ipc_cmd_success_reply(&cmd, t_strdup_printf("%u", count));
+}
+
+static void
+login_proxy_cmd_list_reply(struct ipc_cmd *cmd, string_t *str,
+ struct login_proxy *proxy)
+{
+ unsigned int i, alt_count = array_count(&global_alt_usernames);
+
+ str_truncate(str, 0);
+ str_append_tabescaped(str, proxy->client->virtual_user);
+ str_append_c(str, '\t');
+ i = 0;
+ if (proxy->client->alt_usernames != NULL) {
+ for (; proxy->client->alt_usernames[i] != NULL; i++) {
+ str_append_tabescaped(str, proxy->client->alt_usernames[i]);
+ str_append_c(str, '\t');
+ }
+ i_assert(i <= alt_count);
+ }
+ for (; i < alt_count; i++)
+ str_append_c(str, '\t');
+
+ str_printfa(str, "%s\t%s\t%s\t%u", login_binary->protocol,
+ net_ip2addr(&proxy->client->ip),
+ net_ip2addr(&proxy->ip), proxy->port);
+ ipc_cmd_send(cmd, str_c(str));
+}
+
+static void
+login_proxy_cmd_list(struct ipc_cmd *cmd, const char *const *args ATTR_UNUSED)
+{
+ struct login_proxy *proxy;
+ char *field;
+ string_t *str = t_str_new(64);
+
+ str_append(str, "username\t");
+ array_foreach_elem(&global_alt_usernames, field) {
+ str_append_tabescaped(str, field);
+ str_append_c(str, '\t');
+ }
+ str_append(str, "service\tsrc-ip\tdest-ip\tdest-port");
+
+ ipc_cmd_send(cmd, str_c(str));
+
+ for (proxy = login_proxies; proxy != NULL; proxy = proxy->next)
+ login_proxy_cmd_list_reply(cmd, str, proxy);
+ for (proxy = login_proxies_pending; proxy != NULL; proxy = proxy->next)
+ login_proxy_cmd_list_reply(cmd, str, proxy);
+ ipc_cmd_success(&cmd);
+}
+
+static void login_proxy_ipc_cmd(struct ipc_cmd *cmd, const char *line)
+{
+ const char *const *args = t_strsplit_tabescaped(line);
+ const char *name = args[0];
+
+ args++;
+ if (strcmp(name, "KICK") == 0)
+ login_proxy_cmd_kick(cmd, args);
+ else if (strcmp(name, "KICK-ALT") == 0)
+ login_proxy_cmd_kick_alt(cmd, args);
+ else if (strcmp(name, "KICK-DIRECTOR-HASH") == 0)
+ login_proxy_cmd_kick_director_hash(cmd, args);
+ else if (strcmp(name, "LIST-FULL") == 0)
+ login_proxy_cmd_list(cmd, args);
+ else if (strcmp(name, "KICK-HOST") == 0)
+ login_proxy_cmd_kick_host(cmd, args);
+ else
+ ipc_cmd_fail(&cmd, "Unknown command");
+}
+
+unsigned int login_proxies_get_detached_count(void)
+{
+ return detached_login_proxies_count;
+}
+
+struct client *login_proxies_get_first_detached_client(void)
+{
+ return login_proxies == NULL ? NULL : login_proxies->client;
+}
+
+void login_proxy_init(const char *proxy_notify_pipe_path)
+{
+ proxy_state = login_proxy_state_init(proxy_notify_pipe_path);
+}
+
+void login_proxy_deinit(void)
+{
+ struct login_proxy *proxy;
+
+ while (login_proxies != NULL) {
+ proxy = login_proxies;
+ login_proxy_free_full(&proxy,
+ LOGIN_PROXY_KILL_PREFIX KILLED_BY_SHUTDOWN_REASON,
+ KILLED_BY_SHUTDOWN_REASON,
+ LOGIN_PROXY_SIDE_SELF, 0);
+ }
+ i_assert(detached_login_proxies_count == 0);
+
+ while (login_proxies_disconnecting != NULL)
+ login_proxy_free_final(login_proxies_disconnecting);
+ if (login_proxy_ipc_server != NULL)
+ ipc_server_deinit(&login_proxy_ipc_server);
+ login_proxy_state_deinit(&proxy_state);
+}
diff --git a/src/login-common/login-proxy.h b/src/login-common/login-proxy.h
new file mode 100644
index 0000000..428c3d5
--- /dev/null
+++ b/src/login-common/login-proxy.h
@@ -0,0 +1,120 @@
+#ifndef LOGIN_PROXY_H
+#define LOGIN_PROXY_H
+
+#include "net.h"
+
+/* Max. number of embedded proxying connections until proxying fails.
+ This is intended to avoid an accidental configuration where two proxies
+ keep connecting to each others, both thinking the other one is supposed to
+ handle the user. This only works if both proxies support the Dovecot
+ TTL extension feature. */
+#define LOGIN_PROXY_TTL 5
+#define LOGIN_PROXY_DEFAULT_HOST_IMMEDIATE_FAILURE_AFTER_SECS 30
+
+#define LOGIN_PROXY_FAILURE_MSG "Account is temporarily unavailable."
+
+struct client;
+struct login_proxy;
+
+enum login_proxy_ssl_flags {
+ /* Use SSL/TLS enabled */
+ PROXY_SSL_FLAG_YES = 0x01,
+ /* Don't do SSL handshake immediately after connected */
+ PROXY_SSL_FLAG_STARTTLS = 0x02,
+ /* Don't require that the received certificate is valid */
+ PROXY_SSL_FLAG_ANY_CERT = 0x04
+};
+
+enum login_proxy_failure_type {
+ /* connect() failed or remote disconnected us. */
+ LOGIN_PROXY_FAILURE_TYPE_CONNECT,
+ /* Internal error. */
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL,
+ /* Internal configuration error. */
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG,
+ /* Remote command failed unexpectedly. */
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE,
+ /* Remote isn't configured as expected (e.g. STARTTLS required, but
+ no such capability). */
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ /* Remote server is unexpectedly violating the protocol standard. */
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL,
+ /* Authentication failed to backend. The LOGIN/AUTH command reply was
+ already sent to the client. */
+ LOGIN_PROXY_FAILURE_TYPE_AUTH,
+ /* Authentication failed with a temporary failure code. Attempting it
+ again might work. */
+ LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL,
+};
+
+struct login_proxy_settings {
+ const char *host;
+ struct ip_addr ip, source_ip;
+ in_port_t port;
+ unsigned int connect_timeout_msecs;
+ /* send a notification about proxy connection to proxy-notify pipe
+ every n seconds */
+ unsigned int notify_refresh_secs;
+ unsigned int host_immediate_failure_after_secs;
+ enum login_proxy_ssl_flags ssl_flags;
+ const char *rawlog_dir;
+};
+
+/* Called when new input comes from proxy. */
+typedef void login_proxy_input_callback_t(struct client *client);
+/* Called when proxying fails. If reconnecting=TRUE, this is just an
+ intermediate notification that the proxying will attempt to reconnect soon
+ before failing. */
+typedef void login_proxy_failure_callback_t(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason,
+ bool reconnecting);
+
+/* Create a proxy to given host. Returns NULL if failed. Given callback is
+ called when new input is available from proxy. */
+int login_proxy_new(struct client *client, struct event *event,
+ const struct login_proxy_settings *set,
+ login_proxy_input_callback_t *input_callback,
+ login_proxy_failure_callback_t *failure_callback);
+/* Free the proxy. This should be called if authentication fails. */
+void login_proxy_free(struct login_proxy **proxy);
+
+/* Login proxying session has failed. Returns TRUE if the reconnection is
+ attempted. */
+bool login_proxy_failed(struct login_proxy *proxy, struct event *event,
+ enum login_proxy_failure_type type, const char *reason);
+
+/* Return TRUE if host/port/destuser combination points to same as current
+ connection. */
+bool login_proxy_is_ourself(const struct client *client, const char *host,
+ in_port_t port, const char *destuser);
+
+/* Detach proxy from client. This is done after the authentication is
+ successful and all that is left is the dummy proxying. */
+void login_proxy_detach(struct login_proxy *proxy);
+
+/* STARTTLS command was issued. */
+int login_proxy_starttls(struct login_proxy *proxy);
+
+struct istream *login_proxy_get_istream(struct login_proxy *proxy);
+struct ostream *login_proxy_get_ostream(struct login_proxy *proxy);
+
+void login_proxy_append_success_log_info(struct login_proxy *proxy,
+ string_t *str);
+struct event *login_proxy_get_event(struct login_proxy *proxy);
+const char *login_proxy_get_source_host(const struct login_proxy *proxy) ATTR_PURE;
+const char *login_proxy_get_host(const struct login_proxy *proxy) ATTR_PURE;
+const char *login_proxy_get_ip_str(const struct login_proxy *proxy) ATTR_PURE;
+in_port_t login_proxy_get_port(const struct login_proxy *proxy) ATTR_PURE;
+enum login_proxy_ssl_flags
+login_proxy_get_ssl_flags(const struct login_proxy *proxy) ATTR_PURE;
+
+void login_proxy_kill_idle(void);
+
+unsigned int login_proxies_get_detached_count(void);
+struct client *login_proxies_get_first_detached_client(void);
+
+void login_proxy_init(const char *proxy_notify_pipe_path);
+void login_proxy_deinit(void);
+
+#endif
diff --git a/src/login-common/login-settings.c b/src/login-common/login-settings.c
new file mode 100644
index 0000000..e680677
--- /dev/null
+++ b/src/login-common/login-settings.c
@@ -0,0 +1,227 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "master-service-settings-cache.h"
+#include "login-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+static bool login_settings_check(void *_set, pool_t pool, const char **error_r);
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct login_settings)
+
+static const struct setting_define login_setting_defines[] = {
+ DEF(STR, login_trusted_networks),
+ DEF(STR, login_source_ips),
+ DEF(STR_VARS, login_greeting),
+ DEF(STR, login_log_format_elements),
+ DEF(STR, login_log_format),
+ DEF(STR, login_access_sockets),
+ DEF(STR_VARS, login_proxy_notify_path),
+ DEF(STR, login_plugin_dir),
+ DEF(STR, login_plugins),
+ DEF(TIME_MSECS, login_proxy_timeout),
+ DEF(UINT, login_proxy_max_reconnects),
+ DEF(TIME, login_proxy_max_disconnect_delay),
+ DEF(STR, login_proxy_rawlog_dir),
+ DEF(STR, director_username_hash),
+
+ DEF(BOOL, auth_ssl_require_client_cert),
+ DEF(BOOL, auth_ssl_username_from_cert),
+
+ DEF(BOOL, disable_plaintext_auth),
+ DEF(BOOL, auth_verbose),
+ DEF(BOOL, auth_debug),
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(UINT, mail_max_userip_connections),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct login_settings login_default_settings = {
+ .login_trusted_networks = "",
+ .login_source_ips = "",
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c session=<%{session}>",
+ .login_log_format = "%$: %s",
+ .login_access_sockets = "",
+ .login_proxy_notify_path = "proxy-notify",
+ .login_plugin_dir = MODULEDIR"/login",
+ .login_plugins = "",
+ .login_proxy_timeout = 30*1000,
+ .login_proxy_max_reconnects = 3,
+ .login_proxy_max_disconnect_delay = 0,
+ .login_proxy_rawlog_dir = "",
+ .director_username_hash = "%Lu",
+
+ .auth_ssl_require_client_cert = FALSE,
+ .auth_ssl_username_from_cert = FALSE,
+
+ .disable_plaintext_auth = TRUE,
+ .auth_verbose = FALSE,
+ .auth_debug = FALSE,
+ .verbose_proctitle = FALSE,
+
+ .mail_max_userip_connections = 10
+};
+
+const struct setting_parser_info login_setting_parser_info = {
+ .module_name = "login",
+ .defines = login_setting_defines,
+ .defaults = &login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct login_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = login_settings_check
+};
+
+static const struct setting_parser_info *default_login_set_roots[] = {
+ &login_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info **login_set_roots = default_login_set_roots;
+
+static struct master_service_settings_cache *set_cache;
+
+/* <settings checks> */
+static bool login_settings_check(void *_set, pool_t pool,
+ const char **error_r ATTR_UNUSED)
+{
+ struct login_settings *set = _set;
+
+ set->log_format_elements_split =
+ p_strsplit(pool, set->login_log_format_elements, " ");
+
+ if (set->auth_debug_passwords)
+ set->auth_debug = TRUE;
+ if (set->auth_debug)
+ set->auth_verbose = TRUE;
+ return TRUE;
+}
+/* </settings checks> */
+
+static const struct var_expand_table *
+login_set_var_expand_table(const struct master_service_settings_input *input)
+{
+ const struct var_expand_table stack_tab[] = {
+ { 'l', net_ip2addr(&input->local_ip), "lip" },
+ { 'r', net_ip2addr(&input->remote_ip), "rip" },
+ { 'p', my_pid, "pid" },
+ { 's', input->service, "service" },
+ { '\0', input->local_name, "local_name" },
+ /* aliases */
+ { '\0', net_ip2addr(&input->local_ip), "local_ip" },
+ { '\0', net_ip2addr(&input->remote_ip), "remote_ip" },
+ /* NOTE: Make sure login_log_format_elements_split has all these
+ variables (in client-common.c:get_var_expand_table()). */
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+ return tab;
+}
+
+static void *
+login_setting_dup(pool_t pool, const struct setting_parser_info *info,
+ const void *src_set)
+{
+ const char *error;
+ void *dest;
+
+ dest = settings_dup(info, src_set, pool);
+ if (!settings_check(info, pool, dest, &error)) {
+ const char *name = info->module_name;
+
+ i_fatal("settings_check(%s) failed: %s",
+ name != NULL ? name : "unknown", error);
+ }
+ return dest;
+}
+
+struct login_settings *
+login_settings_read(pool_t pool,
+ const struct ip_addr *local_ip,
+ const struct ip_addr *remote_ip,
+ const char *local_name,
+ const struct master_service_ssl_settings **ssl_set_r,
+ const struct master_service_ssl_server_settings **ssl_server_set_r,
+ void ***other_settings_r)
+{
+ struct master_service_settings_input input;
+ const char *error;
+ const struct setting_parser_context *parser;
+ void *const *cache_sets;
+ void **sets;
+ unsigned int i, count;
+
+ i_zero(&input);
+ input.roots = login_set_roots;
+ input.module = login_binary->process_name;
+ input.service = login_binary->protocol;
+ input.local_name = local_name;
+
+ if (local_ip != NULL)
+ input.local_ip = *local_ip;
+ if (remote_ip != NULL)
+ input.remote_ip = *remote_ip;
+
+ if (set_cache == NULL) {
+ set_cache = master_service_settings_cache_init(master_service,
+ input.module,
+ input.service);
+ /* lookup filters
+
+ this is only enabled if service_count > 1 because otherwise
+ login process will process only one request and this is only
+ useful when more than one request is processed.
+ */
+ if (master_service_get_service_count(master_service) > 1)
+ master_service_settings_cache_init_filter(set_cache);
+ }
+
+ if (master_service_settings_cache_read(set_cache, &input, NULL,
+ &parser, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ cache_sets = master_service_settings_parser_get_others(master_service, parser);
+ for (count = 0; input.roots[count] != NULL; count++) ;
+ i_assert(cache_sets[count] == NULL);
+ sets = p_new(pool, void *, count + 1);
+ for (i = 0; i < count; i++)
+ sets[i] = login_setting_dup(pool, input.roots[i], cache_sets[i]);
+
+ if (settings_var_expand(&login_setting_parser_info, sets[0], pool,
+ login_set_var_expand_table(&input), &error) <= 0)
+ i_fatal("Failed to expand settings: %s", error);
+
+ *ssl_set_r =
+ login_setting_dup(pool, &master_service_ssl_setting_parser_info,
+ settings_parser_get_list(parser)[1]);
+ *ssl_server_set_r =
+ login_setting_dup(pool, &master_service_ssl_server_setting_parser_info,
+ settings_parser_get_list(parser)[2]);
+ *other_settings_r = sets + 1;
+ return sets[0];
+}
+
+void login_settings_deinit(void)
+{
+ if (set_cache != NULL)
+ master_service_settings_cache_deinit(&set_cache);
+}
diff --git a/src/login-common/login-settings.h b/src/login-common/login-settings.h
new file mode 100644
index 0000000..5bff837
--- /dev/null
+++ b/src/login-common/login-settings.h
@@ -0,0 +1,50 @@
+#ifndef LOGIN_SETTINGS_H
+#define LOGIN_SETTINGS_H
+
+struct master_service_ssl_settings;
+struct master_service_ssl_server_settings;
+
+struct login_settings {
+ const char *login_trusted_networks;
+ const char *login_source_ips;
+ const char *login_greeting;
+ const char *login_log_format_elements, *login_log_format;
+ const char *login_access_sockets;
+ const char *login_proxy_notify_path;
+ const char *login_plugin_dir;
+ const char *login_plugins;
+ unsigned int login_proxy_timeout;
+ unsigned int login_proxy_max_reconnects;
+ unsigned int login_proxy_max_disconnect_delay;
+ const char *login_proxy_rawlog_dir;
+ const char *director_username_hash;
+
+ bool auth_ssl_require_client_cert;
+ bool auth_ssl_username_from_cert;
+
+ bool disable_plaintext_auth;
+ bool auth_verbose;
+ bool auth_debug;
+ bool auth_debug_passwords;
+ bool verbose_proctitle;
+
+ unsigned int mail_max_userip_connections;
+
+ /* generated: */
+ char *const *log_format_elements_split;
+};
+
+extern const struct setting_parser_info **login_set_roots;
+extern const struct setting_parser_info login_setting_parser_info;
+
+struct login_settings *
+login_settings_read(pool_t pool,
+ const struct ip_addr *local_ip,
+ const struct ip_addr *remote_ip,
+ const char *local_name,
+ const struct master_service_ssl_settings **ssl_set_r,
+ const struct master_service_ssl_server_settings **ssl_server_set_r,
+ void ***other_settings_r) ATTR_NULL(2, 3, 4);
+void login_settings_deinit(void);
+
+#endif
diff --git a/src/login-common/main.c b/src/login-common/main.c
new file mode 100644
index 0000000..d5622d9
--- /dev/null
+++ b/src/login-common/main.c
@@ -0,0 +1,571 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "randgen.h"
+#include "module-dir.h"
+#include "process-title.h"
+#include "restrict-access.h"
+#include "restrict-process-size.h"
+#include "master-auth.h"
+#include "master-service.h"
+#include "master-interface.h"
+#include "iostream-ssl.h"
+#include "client-common.h"
+#include "access-lookup.h"
+#include "anvil-client.h"
+#include "auth-client.h"
+#include "dsasl-client.h"
+#include "master-service-ssl-settings.h"
+#include "login-proxy.h"
+
+#include <unistd.h>
+#include <syslog.h>
+
+#define AUTH_CLIENT_IDLE_TIMEOUT_MSECS (1000*60)
+
+struct login_access_lookup {
+ struct master_service_connection conn;
+ struct io *io;
+
+ char **sockets, **next_socket;
+ struct access_lookup *access;
+};
+
+struct event *event_auth;
+static struct event_category event_category_auth = {
+ .name = "auth",
+};
+
+struct login_binary *login_binary;
+struct auth_client *auth_client;
+struct master_auth *master_auth;
+bool closing_down, login_debug;
+struct anvil_client *anvil;
+const char *login_rawlog_dir = NULL;
+unsigned int initial_service_count;
+struct login_module_register login_module_register;
+ARRAY_TYPE(string) global_alt_usernames;
+bool login_ssl_initialized;
+
+const struct login_settings *global_login_settings;
+const struct master_service_ssl_settings *global_ssl_settings;
+const struct master_service_ssl_server_settings *global_ssl_server_settings;
+void **global_other_settings;
+
+static ARRAY(struct ip_addr) login_source_ips_array;
+const struct ip_addr *login_source_ips;
+unsigned int login_source_ips_idx, login_source_ips_count;
+
+static struct module *modules;
+static struct timeout *auth_client_to;
+static const char *post_login_socket;
+static bool shutting_down = FALSE;
+static bool ssl_connections = FALSE;
+static bool auth_connected_once = FALSE;
+
+static void login_access_lookup_next(struct login_access_lookup *lookup);
+
+static bool get_first_client(struct client **client_r)
+{
+ struct client *client = clients;
+
+ if (client == NULL)
+ client = login_proxies_get_first_detached_client();
+ if (client == NULL)
+ client = clients_get_first_fd_proxy();
+ *client_r = client;
+ return client != NULL;
+}
+
+void login_refresh_proctitle(void)
+{
+ struct client *client;
+ const char *addr;
+
+ if (!global_login_settings->verbose_proctitle)
+ return;
+
+ /* clients_get_count() includes all the clients being served.
+ Inside that there are 3 groups:
+ 1. pre-login clients
+ 2. post-login clients being proxied to remote hosts
+ 3. post-login clients being proxied to post-login processes
+ Currently the post-login proxying is done only for SSL/TLS
+ connections, so we're assuming that they're the same. */
+ string_t *str = t_str_new(64);
+ if (clients_get_count() == 0) {
+ /* no clients */
+ } else if (clients_get_count() > 1 || !get_first_client(&client)) {
+ str_printfa(str, "[%u pre-login", clients_get_count() -
+ login_proxies_get_detached_count() -
+ clients_get_fd_proxies_count());
+ if (login_proxies_get_detached_count() > 0) {
+ /* show detached proxies only if they exist, so
+ non-proxy servers don't unnecessarily show them. */
+ str_printfa(str, " + %u proxies",
+ login_proxies_get_detached_count());
+ }
+ if (clients_get_fd_proxies_count() > 0) {
+ /* show post-login proxies only if they exist, so
+ proxy-only servers don't unnecessarily show them. */
+ str_printfa(str, " + %u TLS proxies",
+ clients_get_fd_proxies_count());
+ }
+ str_append_c(str, ']');
+ } else {
+ str_append_c(str, '[');
+ addr = net_ip2addr(&client->ip);
+ if (addr[0] != '\0')
+ str_printfa(str, "%s ", addr);
+ if (client->fd_proxying)
+ str_append(str, "TLS proxy");
+ else if (client->destroyed)
+ str_append(str, "proxy");
+ else
+ str_append(str, "pre-login");
+ str_append_c(str, ']');
+ }
+ process_title_set(str_c(str));
+}
+
+static void auth_client_idle_timeout(struct auth_client *auth_client)
+{
+ i_assert(clients == NULL);
+
+ auth_client_disconnect(auth_client, "idle disconnect");
+ timeout_remove(&auth_client_to);
+}
+
+void login_client_destroyed(void)
+{
+ if (clients == NULL && auth_client_to == NULL) {
+ auth_client_to = timeout_add(AUTH_CLIENT_IDLE_TIMEOUT_MSECS,
+ auth_client_idle_timeout,
+ auth_client);
+ }
+}
+
+static void login_die(void)
+{
+ shutting_down = TRUE;
+ login_proxy_kill_idle();
+
+ if (!auth_client_is_connected(auth_client)) {
+ /* we don't have auth client, and we might never get one */
+ clients_destroy_all();
+ }
+}
+
+static void
+client_connected_finish(const struct master_service_connection *conn)
+{
+ struct client *client;
+ const struct login_settings *set;
+ const struct master_service_ssl_settings *ssl_set;
+ const struct master_service_ssl_server_settings *ssl_server_set;
+ pool_t pool;
+ void **other_sets;
+
+ pool = pool_alloconly_create("login client", 8*1024);
+ set = login_settings_read(pool, &conn->local_ip,
+ &conn->remote_ip, NULL,
+ &ssl_set, &ssl_server_set, &other_sets);
+
+ client = client_alloc(conn->fd, pool, conn, set,
+ ssl_set, ssl_server_set);
+ if (ssl_connections || conn->ssl) {
+ if (client_init_ssl(client) < 0) {
+ client_unref(&client);
+ net_disconnect(conn->fd);
+ master_service_client_connection_destroyed(master_service);
+ return;
+ }
+ }
+ client_init(client, other_sets);
+
+ timeout_remove(&auth_client_to);
+}
+
+static void login_access_lookup_free(struct login_access_lookup *lookup)
+{
+ io_remove(&lookup->io);
+ if (lookup->access != NULL)
+ access_lookup_destroy(&lookup->access);
+ if (lookup->conn.fd != -1) {
+ if (close(lookup->conn.fd) < 0)
+ i_error("close(client) failed: %m");
+ master_service_client_connection_destroyed(master_service);
+ }
+
+ p_strsplit_free(default_pool, lookup->sockets);
+ i_free(lookup);
+}
+
+static void login_access_callback(bool success, void *context)
+{
+ struct login_access_lookup *lookup = context;
+
+ if (!success) {
+ i_info("access(%s): Client refused (rip=%s)",
+ *lookup->next_socket,
+ net_ip2addr(&lookup->conn.remote_ip));
+ login_access_lookup_free(lookup);
+ } else {
+ lookup->next_socket++;
+ login_access_lookup_next(lookup);
+ }
+}
+
+static void login_access_lookup_next(struct login_access_lookup *lookup)
+{
+ if (*lookup->next_socket == NULL) {
+ /* last one */
+ io_remove(&lookup->io);
+ client_connected_finish(&lookup->conn);
+ lookup->conn.fd = -1;
+ login_access_lookup_free(lookup);
+ return;
+ }
+ lookup->access = access_lookup(*lookup->next_socket, lookup->conn.fd,
+ login_binary->protocol,
+ login_access_callback, lookup);
+ if (lookup->access == NULL)
+ login_access_lookup_free(lookup);
+}
+
+static void client_input_error(struct login_access_lookup *lookup)
+{
+ char c;
+ int ret;
+
+ ret = recv(lookup->conn.fd, &c, 1, MSG_PEEK);
+ if (ret <= 0) {
+ i_info("access(%s): Client disconnected during lookup (rip=%s)",
+ *lookup->next_socket,
+ net_ip2addr(&lookup->conn.remote_ip));
+ login_access_lookup_free(lookup);
+ } else {
+ /* actual input. stop listening until lookup is done. */
+ io_remove(&lookup->io);
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ const char *access_sockets =
+ global_login_settings->login_access_sockets;
+ struct login_access_lookup *lookup;
+
+ master_service_client_connection_accept(conn);
+ if (conn->remote_ip.family != 0) {
+ /* log the connection's IP address in case we crash. it's of
+ course possible that another earlier client causes the
+ crash, but this is better than nothing. */
+ i_set_failure_send_ip(&conn->remote_ip);
+ }
+
+ /* make sure we're connected (or attempting to connect) to auth */
+ auth_client_connect(auth_client);
+
+ if (*access_sockets == '\0') {
+ /* no access checks */
+ client_connected_finish(conn);
+ return;
+ }
+
+ lookup = i_new(struct login_access_lookup, 1);
+ lookup->conn = *conn;
+ lookup->io = io_add(conn->fd, IO_READ, client_input_error, lookup);
+ lookup->sockets = p_strsplit_spaces(default_pool, access_sockets, " ");
+ lookup->next_socket = lookup->sockets;
+
+ login_access_lookup_next(lookup);
+}
+
+static void auth_connect_notify(struct auth_client *client ATTR_UNUSED,
+ bool connected, void *context ATTR_UNUSED)
+{
+ if (connected) {
+ auth_connected_once = TRUE;
+ clients_notify_auth_connected();
+ } else if (shutting_down)
+ clients_destroy_all();
+ else if (!auth_connected_once) {
+ /* auth disconnected without having ever succeeded, so the
+ auth process is probably misconfigured. no point in
+ keeping the client connections hanging. */
+ clients_destroy_all_reason("Auth process broken");
+ }
+}
+
+static bool anvil_reconnect_callback(void)
+{
+ /* we got disconnected from anvil. we can't reconnect to it since we're
+ chrooted, so just die after we've finished handling the current
+ connections. */
+ master_service_stop_new_connections(master_service);
+ return FALSE;
+}
+
+void login_anvil_init(void)
+{
+ if (anvil != NULL)
+ return;
+
+ anvil = anvil_client_init("anvil", anvil_reconnect_callback, 0);
+ if (anvil_client_connect(anvil, TRUE) < 0)
+ i_fatal("Couldn't connect to anvil");
+}
+
+static void
+parse_login_source_ips(const char *ips_str)
+{
+ const char *const *tmp;
+ struct ip_addr *tmp_ips;
+ bool skip_nonworking = FALSE;
+ unsigned int i, tmp_ips_count;
+ int ret;
+
+ if (ips_str[0] == '?') {
+ /* try binding to the IP immediately. if it doesn't
+ work, skip it. (this allows using the same config file for
+ all the servers.) */
+ skip_nonworking = TRUE;
+ ips_str++;
+ }
+ i_array_init(&login_source_ips_array, 4);
+ for (tmp = t_strsplit_spaces(ips_str, ", "); *tmp != NULL; tmp++) {
+ ret = net_gethostbyname(*tmp, &tmp_ips, &tmp_ips_count);
+ if (ret != 0) {
+ i_error("login_source_ips: net_gethostbyname(%s) failed: %s",
+ *tmp, net_gethosterror(ret));
+ continue;
+ }
+ for (i = 0; i < tmp_ips_count; i++) {
+ if (skip_nonworking && net_try_bind(&tmp_ips[i]) < 0)
+ continue;
+ array_push_back(&login_source_ips_array, &tmp_ips[i]);
+ }
+ }
+
+ /* make the array contents easily accessible */
+ login_source_ips = array_get(&login_source_ips_array,
+ &login_source_ips_count);
+}
+
+static void login_load_modules(void)
+{
+ struct module_dir_load_settings mod_set;
+
+ if (global_login_settings->login_plugins[0] == '\0')
+ return;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.binary_name = login_binary->process_name;
+ mod_set.setting_name = "login_plugins";
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = login_debug;
+
+ modules = module_dir_load(global_login_settings->login_plugin_dir,
+ global_login_settings->login_plugins,
+ &mod_set);
+ module_dir_init(modules);
+}
+
+static void login_ssl_init(void)
+{
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ if (strcmp(global_ssl_settings->ssl, "no") == 0)
+ return;
+
+ master_service_ssl_server_settings_to_iostream_set(global_ssl_settings,
+ global_ssl_server_settings, pool_datastack_create(), &ssl_set);
+ if (io_stream_ssl_global_init(&ssl_set, &error) < 0)
+ i_fatal("Failed to initialize SSL library: %s", error);
+ login_ssl_initialized = TRUE;
+}
+
+static void main_preinit(void)
+{
+ unsigned int max_fds;
+
+ /* Initialize SSL proxy so it can read certificate and private
+ key file. */
+ login_ssl_init();
+ dsasl_clients_init();
+ client_common_init();
+
+ /* set the number of fds we want to use. it may get increased or
+ decreased. leave a couple of extra fds for auth sockets and such.
+
+ worst case each connection can use:
+
+ - 1 for client
+ - 1 for login proxy
+ - 2 for client-side ssl proxy
+ - 2 for server-side ssl proxy (with login proxy)
+
+ However, login process nowadays supports plugins, there are rawlogs
+ and so on. Don't enforce the fd limit anymore, but use this value
+ for optimizing the ioloop's fd table size.
+ */
+ max_fds = MASTER_LISTEN_FD_FIRST + 16 +
+ master_service_get_socket_count(master_service) +
+ master_service_get_client_limit(master_service)*6;
+ io_loop_set_max_fd_count(current_ioloop, max_fds);
+
+ i_assert(strcmp(global_ssl_settings->ssl, "no") == 0 ||
+ login_ssl_initialized);
+
+ if (global_login_settings->mail_max_userip_connections > 0)
+ login_anvil_init();
+
+ /* read the login_source_ips before chrooting so it can access
+ /etc/hosts */
+ parse_login_source_ips(global_login_settings->login_source_ips);
+ if (login_source_ips_count > 0) {
+ /* randomize the initial index in case service_count=1
+ (although in that case it's unlikely this setting is
+ even used..) */
+ login_source_ips_idx = i_rand_limit(login_source_ips_count);
+ }
+
+ login_load_modules();
+
+ restrict_access_by_env(0, NULL);
+ if (login_debug)
+ restrict_access_allow_coredumps(TRUE);
+ initial_service_count = master_service_get_service_count(master_service);
+
+ if (restrict_access_get_current_chroot() == NULL) {
+ if (chdir("login") < 0)
+ i_fatal("chdir(login) failed: %m");
+ }
+
+ if (login_rawlog_dir != NULL &&
+ access(login_rawlog_dir, W_OK | X_OK) < 0) {
+ i_error("access(%s, wx) failed: %m - disabling rawlog",
+ login_rawlog_dir);
+ login_rawlog_dir = NULL;
+ }
+}
+
+static void main_init(const char *login_socket)
+{
+ /* make sure we can't fork() */
+ restrict_process_count(1);
+
+ event_auth = event_create(NULL);
+ event_set_forced_debug(event_auth, global_login_settings->auth_debug);
+ event_add_category(event_auth, &event_category_auth);
+
+ i_array_init(&global_alt_usernames, 4);
+ master_service_set_avail_overflow_callback(master_service,
+ client_destroy_oldest);
+ master_service_set_die_callback(master_service, login_die);
+
+ auth_client = auth_client_init(login_socket, (unsigned int)getpid(),
+ FALSE);
+ auth_client_connect(auth_client);
+ auth_client_set_connect_notify(auth_client, auth_connect_notify, NULL);
+ master_auth = master_auth_init(master_service, post_login_socket);
+
+ login_binary->init();
+
+ login_proxy_init(global_login_settings->login_proxy_notify_path);
+}
+
+static void main_deinit(void)
+{
+ client_destroy_fd_proxies();
+ ssl_iostream_context_cache_free();
+ login_proxy_deinit();
+
+ login_binary->deinit();
+ module_dir_unload(&modules);
+ auth_client_deinit(&auth_client);
+ master_auth_deinit(&master_auth);
+
+ char *str;
+ array_foreach_elem(&global_alt_usernames, str)
+ i_free(str);
+ array_free(&global_alt_usernames);
+
+ if (anvil != NULL)
+ anvil_client_deinit(&anvil);
+ timeout_remove(&auth_client_to);
+ client_common_deinit();
+ dsasl_clients_deinit();
+ login_settings_deinit();
+ event_unref(&event_auth);
+}
+
+int login_binary_run(struct login_binary *binary,
+ int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN |
+ MASTER_SERVICE_FLAG_TRACK_LOGIN_STATE |
+ MASTER_SERVICE_FLAG_HAVE_STARTTLS |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT;
+ pool_t set_pool;
+ const char *login_socket;
+ int c;
+
+ login_binary = binary;
+ login_socket = binary->default_login_socket != NULL ?
+ binary->default_login_socket : LOGIN_DEFAULT_SOCKET;
+ post_login_socket = binary->protocol;
+
+ master_service = master_service_init(login_binary->process_name,
+ service_flags, &argc, &argv,
+ "Dl:R:S");
+ master_service_init_log(master_service);
+
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ login_debug = TRUE;
+ break;
+ case 'l':
+ post_login_socket = optarg;
+ break;
+ case 'R':
+ login_rawlog_dir = optarg;
+ break;
+ case 'S':
+ ssl_connections = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ if (argv[optind] != NULL)
+ login_socket = argv[optind];
+
+ login_binary->preinit();
+
+ set_pool = pool_alloconly_create("global login settings", 4096);
+ global_login_settings =
+ login_settings_read(set_pool, NULL, NULL, NULL,
+ &global_ssl_settings,
+ &global_ssl_server_settings,
+ &global_other_settings);
+
+ main_preinit();
+ main_init(login_socket);
+
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+ array_free(&login_source_ips_array);
+ pool_unref(&set_pool);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/login-common/sasl-server.c b/src/login-common/sasl-server.c
new file mode 100644
index 0000000..2552f1c
--- /dev/null
+++ b/src/login-common/sasl-server.c
@@ -0,0 +1,604 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "array.h"
+#include "md5.h"
+#include "sasl-server.h"
+#include "str.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "strfuncs.h"
+#include "write-full.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "anvil-client.h"
+#include "auth-client.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "master-interface.h"
+#include "master-auth.h"
+#include "client-common.h"
+
+#include <unistd.h>
+
+#define ERR_TOO_MANY_USERIP_CONNECTIONS \
+ "Maximum number of connections from user+IP exceeded " \
+ "(mail_max_userip_connections=%u)"
+
+struct anvil_request {
+ struct client *client;
+ unsigned int auth_pid;
+ unsigned char cookie[MASTER_AUTH_COOKIE_SIZE];
+};
+
+static bool
+sasl_server_filter_mech(struct client *client, struct auth_mech_desc *mech)
+{
+ if (client->v.sasl_filter_mech != NULL &&
+ !client->v.sasl_filter_mech(client, mech))
+ return FALSE;
+ return ((mech->flags & MECH_SEC_ANONYMOUS) == 0 ||
+ login_binary->anonymous_login_acceptable);
+}
+
+const struct auth_mech_desc *
+sasl_server_get_advertised_mechs(struct client *client, unsigned int *count_r)
+{
+ const struct auth_mech_desc *mech;
+ struct auth_mech_desc *ret_mech;
+ unsigned int i, j, count;
+
+ mech = auth_client_get_available_mechs(auth_client, &count);
+ if (count == 0 || (!client->secured &&
+ strcmp(client->ssl_set->ssl, "required") == 0)) {
+ *count_r = 0;
+ return NULL;
+ }
+
+ ret_mech = t_new(struct auth_mech_desc, count);
+ for (i = j = 0; i < count; i++) {
+ struct auth_mech_desc fmech = mech[i];
+
+ if (!sasl_server_filter_mech(client, &fmech))
+ continue;
+
+ /* a) transport is secured
+ b) auth mechanism isn't plaintext
+ c) we allow insecure authentication
+ */
+ if ((fmech.flags & MECH_SEC_PRIVATE) == 0 &&
+ (client->secured || !client->set->disable_plaintext_auth ||
+ (fmech.flags & MECH_SEC_PLAINTEXT) == 0))
+ ret_mech[j++] = fmech;
+ }
+ *count_r = j;
+ return ret_mech;
+}
+
+const struct auth_mech_desc *
+sasl_server_find_available_mech(struct client *client, const char *name)
+{
+ const struct auth_mech_desc *mech;
+ struct auth_mech_desc fmech;
+
+ mech = auth_client_find_mech(auth_client, name);
+ if (mech == NULL)
+ return NULL;
+
+ fmech = *mech;
+ if (!sasl_server_filter_mech(client, &fmech))
+ return NULL;
+ if (memcmp(&fmech, mech, sizeof(fmech)) != 0) {
+ struct auth_mech_desc *nmech = t_new(struct auth_mech_desc, 1);
+
+ *nmech = fmech;
+ mech = nmech;
+ }
+ return mech;
+}
+
+static enum auth_request_flags
+client_get_auth_flags(struct client *client)
+{
+ enum auth_request_flags auth_flags = 0;
+
+ if (client->ssl_iostream != NULL &&
+ ssl_iostream_has_valid_client_cert(client->ssl_iostream))
+ auth_flags |= AUTH_REQUEST_FLAG_VALID_CLIENT_CERT;
+ if (client->tls || client->proxied_ssl)
+ auth_flags |= AUTH_REQUEST_FLAG_TRANSPORT_SECURITY_TLS;
+ if (client->secured)
+ auth_flags |= AUTH_REQUEST_FLAG_SECURED;
+ if (login_binary->sasl_support_final_reply)
+ auth_flags |= AUTH_REQUEST_FLAG_SUPPORT_FINAL_RESP;
+ return auth_flags;
+}
+
+static void ATTR_NULL(3, 4)
+call_client_callback(struct client *client, enum sasl_server_reply reply,
+ const char *data, const char *const *args)
+{
+ sasl_server_callback_t *sasl_callback;
+
+ i_assert(reply != SASL_SERVER_REPLY_CONTINUE);
+
+ sasl_callback = client->sasl_callback;
+ client->sasl_callback = NULL;
+
+ sasl_callback(client, reply, data, args);
+ /* NOTE: client may be destroyed now */
+}
+
+static void
+master_auth_callback(const struct master_auth_reply *reply, void *context)
+{
+ struct client *client = context;
+ enum sasl_server_reply sasl_reply = SASL_SERVER_REPLY_MASTER_FAILED;
+ const char *data = NULL;
+
+ client->master_tag = 0;
+ client->authenticating = FALSE;
+ if (reply != NULL) {
+ switch (reply->status) {
+ case MASTER_AUTH_STATUS_OK:
+ sasl_reply = SASL_SERVER_REPLY_SUCCESS;
+ break;
+ case MASTER_AUTH_STATUS_INTERNAL_ERROR:
+ sasl_reply = SASL_SERVER_REPLY_MASTER_FAILED;
+ break;
+ }
+ client->mail_pid = reply->mail_pid;
+ } else {
+ auth_client_send_cancel(auth_client, client->master_auth_id);
+ }
+ call_client_callback(client, sasl_reply, data, NULL);
+}
+
+static int master_send_request(struct anvil_request *anvil_request)
+{
+ struct client *client = anvil_request->client;
+ struct master_auth_request_params params;
+ struct master_auth_request req;
+ const unsigned char *data;
+ size_t size;
+ buffer_t *buf;
+ const char *session_id = client_get_session_id(client);
+ int fd;
+ bool close_fd;
+
+ if (client_get_plaintext_fd(client, &fd, &close_fd) < 0)
+ return -1;
+
+ i_zero(&req);
+ req.auth_pid = anvil_request->auth_pid;
+ req.auth_id = client->master_auth_id;
+ req.local_ip = client->local_ip;
+ req.remote_ip = client->ip;
+ req.local_port = client->local_port;
+ req.remote_port = client->remote_port;
+ req.client_pid = getpid();
+ if (client->ssl_iostream != NULL &&
+ ssl_iostream_get_compression(client->ssl_iostream) != NULL)
+ req.flags |= MAIL_AUTH_REQUEST_FLAG_TLS_COMPRESSION;
+ if (client->secured)
+ req.flags |= MAIL_AUTH_REQUEST_FLAG_CONN_SECURED;
+ if (client->ssl_secured)
+ req.flags |= MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED;
+ if (HAS_ALL_BITS(client->auth_flags, SASL_SERVER_AUTH_FLAG_IMPLICIT))
+ req.flags |= MAIL_AUTH_REQUEST_FLAG_IMPLICIT;
+ memcpy(req.cookie, anvil_request->cookie, sizeof(req.cookie));
+
+ buf = t_buffer_create(256);
+ /* session ID */
+ buffer_append(buf, session_id, strlen(session_id)+1);
+ /* protocol specific data (e.g. IMAP tag) */
+ buffer_append(buf, client->master_data_prefix,
+ client->master_data_prefix_len);
+ /* buffered client input */
+ data = i_stream_get_data(client->input, &size);
+ buffer_append(buf, data, size);
+ req.data_size = buf->used;
+ i_stream_skip(client->input, size);
+
+ client->auth_finished = ioloop_time;
+
+ i_zero(&params);
+ params.client_fd = fd;
+ params.socket_path = client->postlogin_socket_path;
+ params.request = req;
+ params.data = buf->data;
+ master_auth_request_full(master_auth, &params, master_auth_callback,
+ client, &client->master_tag);
+ if (close_fd)
+ i_close_fd(&fd);
+ return 0;
+}
+
+static void ATTR_NULL(1)
+anvil_lookup_callback(const char *reply, void *context)
+{
+ struct anvil_request *req = context;
+ struct client *client = req->client;
+ const struct login_settings *set = client->set;
+ const char *errmsg;
+ unsigned int conn_count;
+ int ret;
+
+ client->anvil_query = NULL;
+ client->anvil_request = NULL;
+
+ conn_count = 0;
+ if (reply != NULL && str_to_uint(reply, &conn_count) < 0)
+ i_fatal("Received invalid reply from anvil: %s", reply);
+
+ /* reply=NULL if we didn't need to do anvil lookup,
+ or if the anvil lookup failed. allow failed anvil lookups in. */
+ if (reply == NULL || conn_count < set->mail_max_userip_connections) {
+ ret = master_send_request(req);
+ errmsg = NULL; /* client will see internal error */
+ } else {
+ ret = -1;
+ errmsg = t_strdup_printf(ERR_TOO_MANY_USERIP_CONNECTIONS,
+ set->mail_max_userip_connections);
+ }
+ if (ret < 0) {
+ client->authenticating = FALSE;
+ auth_client_send_cancel(auth_client, client->master_auth_id);
+ call_client_callback(client, SASL_SERVER_REPLY_MASTER_FAILED,
+ errmsg, NULL);
+ }
+ i_free(req);
+}
+
+static void
+anvil_check_too_many_connections(struct client *client,
+ struct auth_client_request *request)
+{
+ struct anvil_request *req;
+ const char *query, *cookie;
+ buffer_t buf;
+
+ req = i_new(struct anvil_request, 1);
+ req->client = client;
+ req->auth_pid = auth_client_request_get_server_pid(request);
+
+ buffer_create_from_data(&buf, req->cookie, sizeof(req->cookie));
+ cookie = auth_client_request_get_cookie(request);
+ if (strlen(cookie) == MASTER_AUTH_COOKIE_SIZE*2)
+ (void)hex_to_binary(cookie, &buf);
+
+ if (client->virtual_user == NULL ||
+ client->set->mail_max_userip_connections == 0) {
+ anvil_lookup_callback(NULL, req);
+ return;
+ }
+
+ query = t_strconcat("LOOKUP\t", login_binary->protocol, "/",
+ net_ip2addr(&client->ip), "/",
+ str_tabescape(client->virtual_user), NULL);
+ client->anvil_request = req;
+ client->anvil_query =
+ anvil_client_query(anvil, query, anvil_lookup_callback, req);
+}
+
+static bool
+sasl_server_check_login(struct client *client)
+{
+ if (client->v.sasl_check_login != NULL &&
+ !client->v.sasl_check_login(client))
+ return FALSE;
+ if (client->auth_anonymous &&
+ !login_binary->anonymous_login_acceptable) {
+ sasl_server_auth_failed(client,
+ "Anonymous login denied",
+ AUTH_CLIENT_FAIL_CODE_ANONYMOUS_DENIED);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool args_parse_user(struct client *client, const char *arg)
+{
+ if (str_begins(arg, "user=")) {
+ i_free(client->virtual_user);
+ i_free_and_null(client->virtual_user_orig);
+ i_free_and_null(client->virtual_auth_user);
+ client->virtual_user = i_strdup(arg + 5);
+ event_add_str(client->event, "user", client->virtual_user);
+ } else if (str_begins(arg, "original_user=")) {
+ i_free(client->virtual_user_orig);
+ client->virtual_user_orig = i_strdup(arg + 14);
+ } else if (str_begins(arg, "auth_user=")) {
+ i_free(client->virtual_auth_user);
+ client->virtual_auth_user = i_strdup(arg + 10);
+ } else {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+authenticate_callback(struct auth_client_request *request,
+ enum auth_request_status status, const char *data_base64,
+ const char *const *args, void *context)
+{
+ struct client *client = context;
+ unsigned int i;
+ bool nologin;
+
+ if (!client->authenticating) {
+ /* client aborted */
+ i_assert(status < 0);
+ return;
+ }
+ client->auth_waiting = FALSE;
+
+ i_assert(client->auth_request == request);
+ switch (status) {
+ case AUTH_REQUEST_STATUS_CONTINUE:
+ /* continue */
+ client->sasl_callback(client, SASL_SERVER_REPLY_CONTINUE,
+ data_base64, NULL);
+ break;
+ case AUTH_REQUEST_STATUS_OK:
+ client->master_auth_id = auth_client_request_get_id(request);
+ client->auth_request = NULL;
+ client->auth_successes++;
+ client->auth_passdb_args = p_strarray_dup(client->pool, args);
+ client->postlogin_socket_path = NULL;
+
+ nologin = FALSE;
+ for (i = 0; args[i] != NULL; i++) {
+ if (args_parse_user(client, args[i]))
+ ;
+ else if (str_begins(args[i], "postlogin_socket=")) {
+ client->postlogin_socket_path =
+ p_strdup(client->pool, args[i] + 17);
+ } else if (strcmp(args[i], "nologin") == 0 ||
+ strcmp(args[i], "proxy") == 0) {
+ /* user can't login */
+ nologin = TRUE;
+ } else if (strcmp(args[i], "anonymous") == 0 ) {
+ client->auth_anonymous = TRUE;
+ } else if (str_begins(args[i], "event_")) {
+ const char *key = args[i] + 6;
+ const char *value = strchr(key, '=');
+ if (value != NULL) {
+ event_add_str(client->event,
+ t_strdup_until(key, value),
+ value+1);
+ }
+ } else if (str_begins(args[i], "resp=")) {
+ client->sasl_final_delayed_resp =
+ p_strdup(client->pool, args[i]+5);
+ }
+ }
+
+ if (nologin) {
+ client->authenticating = FALSE;
+ call_client_callback(client, SASL_SERVER_REPLY_SUCCESS,
+ NULL, args);
+ } else if (!sasl_server_check_login(client)) {
+ i_assert(!client->authenticating);
+ } else {
+ anvil_check_too_many_connections(client, request);
+ }
+ break;
+ case AUTH_REQUEST_STATUS_INTERNAL_FAIL:
+ client->auth_process_comm_fail = TRUE;
+ /* fall through */
+ case AUTH_REQUEST_STATUS_FAIL:
+ case AUTH_REQUEST_STATUS_ABORT:
+ client->auth_request = NULL;
+
+ if (args != NULL) {
+ /* parse our username if it's there */
+ for (i = 0; args[i] != NULL; i++) {
+ if (args_parse_user(client, args[i]))
+ continue;
+ if (str_begins(args[i], "resp=")) {
+ client->sasl_final_delayed_resp =
+ p_strdup(client->preproxy_pool,
+ t_strdup(args[i]+5));
+ }
+ }
+ }
+
+ if (client->sasl_final_delayed_resp != NULL &&
+ !login_binary->sasl_support_final_reply) {
+ client->final_response = TRUE;
+ client->final_args = p_strarray_dup(client->preproxy_pool, args);
+ client->delayed_final_reply = SASL_SERVER_REPLY_AUTH_FAILED;
+ client->sasl_callback(client, SASL_SERVER_REPLY_CONTINUE,
+ client->sasl_final_delayed_resp, NULL);
+ } else {
+ client->authenticating = FALSE;
+ call_client_callback(client, SASL_SERVER_REPLY_AUTH_FAILED,
+ NULL, args);
+ }
+ break;
+ }
+}
+
+static bool get_cert_username(struct client *client, const char **username_r,
+ const char **error_r)
+{
+ /* this was proxied connection, so we use the name here */
+ if (client->client_cert_common_name != NULL) {
+ *username_r = client->client_cert_common_name;
+ return TRUE;
+ }
+
+ /* no SSL */
+ if (client->ssl_iostream == NULL) {
+ *username_r = NULL;
+ return TRUE;
+ }
+
+ /* no client certificate */
+ if (!ssl_iostream_has_valid_client_cert(client->ssl_iostream)) {
+ *username_r = NULL;
+ return TRUE;
+ }
+
+ /* get peer name */
+ const char *username = ssl_iostream_get_peer_name(client->ssl_iostream);
+
+ /* if we wanted peer name, but it was not there, fail */
+ if (client->set->auth_ssl_username_from_cert &&
+ (username == NULL || *username == '\0')) {
+ if (client->set->auth_ssl_require_client_cert) {
+ *error_r = "Missing username in certificate";
+ return FALSE;
+ }
+ }
+
+ *username_r = username;
+ return TRUE;
+}
+
+void sasl_server_auth_begin(struct client *client,
+ const char *service, const char *mech_name,
+ enum sasl_server_auth_flags flags,
+ const char *initial_resp_base64,
+ sasl_server_callback_t *callback)
+{
+ struct auth_request_info info;
+ const struct auth_mech_desc *mech;
+ bool private = HAS_ALL_BITS(flags, SASL_SERVER_AUTH_FLAG_PRIVATE);
+ const char *error;
+
+ i_assert(auth_client_is_connected(auth_client));
+
+ client->auth_attempts++;
+ client->authenticating = TRUE;
+ client->master_auth_id = 0;
+ if (client->auth_first_started == 0)
+ client->auth_first_started = ioloop_time;
+ i_free(client->auth_mech_name);
+ client->auth_mech_name = str_ucase(i_strdup(mech_name));
+ client->auth_anonymous = FALSE;
+ client->auth_flags = flags;
+ client->sasl_callback = callback;
+
+ mech = sasl_server_find_available_mech(client, mech_name);
+ if (mech == NULL ||
+ ((mech->flags & MECH_SEC_PRIVATE) != 0 && !private)) {
+ sasl_server_auth_failed(client,
+ "Unsupported authentication mechanism.",
+ AUTH_CLIENT_FAIL_CODE_MECH_INVALID);
+ return;
+ }
+
+ i_assert(!private || (mech->flags & MECH_SEC_PRIVATE) != 0);
+
+ if (!client->secured && client->set->disable_plaintext_auth &&
+ (mech->flags & MECH_SEC_PLAINTEXT) != 0) {
+ sasl_server_auth_failed(client,
+ "Plaintext authentication disabled.",
+ AUTH_CLIENT_FAIL_CODE_MECH_SSL_REQUIRED);
+ return;
+ }
+
+ i_zero(&info);
+ info.mech = mech->name;
+ info.service = service;
+ info.session_id = client_get_session_id(client);
+
+ if (!get_cert_username(client, &info.cert_username, &error)) {
+ e_error(client->event,
+ "Cannot get username from certificate: %s", error);
+ sasl_server_auth_failed(client,
+ "Unable to validate certificate",
+ AUTH_CLIENT_FAIL_CODE_AUTHZFAILED);
+ return;
+ }
+
+ if (client->ssl_iostream != NULL) {
+ info.cert_username = ssl_iostream_get_peer_name(client->ssl_iostream);
+ info.ssl_cipher = ssl_iostream_get_cipher(client->ssl_iostream,
+ &info.ssl_cipher_bits);
+ info.ssl_pfs = ssl_iostream_get_pfs(client->ssl_iostream);
+ info.ssl_protocol =
+ ssl_iostream_get_protocol_name(client->ssl_iostream);
+ }
+ info.flags = client_get_auth_flags(client);
+ info.local_ip = client->local_ip;
+ info.remote_ip = client->ip;
+ info.local_port = client->local_port;
+ info.local_name = client->local_name;
+ info.remote_port = client->remote_port;
+ info.real_local_ip = client->real_local_ip;
+ info.real_remote_ip = client->real_remote_ip;
+ info.real_local_port = client->real_local_port;
+ info.real_remote_port = client->real_remote_port;
+ if (client->client_id != NULL)
+ info.client_id = str_c(client->client_id);
+ if (client->forward_fields != NULL)
+ info.forward_fields = str_c(client->forward_fields);
+ info.initial_resp_base64 = initial_resp_base64;
+
+ client->auth_request =
+ auth_client_request_new(auth_client, &info,
+ authenticate_callback, client);
+}
+
+static void ATTR_NULL(2, 3)
+sasl_server_auth_cancel(struct client *client, const char *reason,
+ const char *code, enum sasl_server_reply reply)
+{
+ i_assert(client->authenticating);
+
+ if (client->set->auth_verbose && reason != NULL) {
+ const char *auth_name =
+ str_sanitize(client->auth_mech_name, MAX_MECH_NAME);
+ e_info(client->event, "Authenticate %s failed: %s",
+ auth_name, reason);
+ }
+
+ client->authenticating = FALSE;
+ client->final_response = FALSE;
+ if (client->auth_request != NULL)
+ auth_client_request_abort(&client->auth_request, reason);
+ if (client->master_auth_id != 0)
+ auth_client_send_cancel(auth_client, client->master_auth_id);
+
+ if (code != NULL) {
+ const char *args[2];
+
+ args[0] = t_strconcat("code=", code, NULL);
+ args[1] = NULL;
+ call_client_callback(client, reply, reason, args);
+ return;
+ }
+
+ call_client_callback(client, reply, reason, NULL);
+}
+
+void sasl_server_auth_failed(struct client *client, const char *reason,
+ const char *code)
+{
+ sasl_server_auth_cancel(client, reason, code, SASL_SERVER_REPLY_AUTH_FAILED);
+}
+
+void sasl_server_auth_abort(struct client *client)
+{
+ client->auth_try_aborted = TRUE;
+ if (client->anvil_query != NULL) {
+ anvil_client_query_abort(anvil, &client->anvil_query);
+ i_free(client->anvil_request);
+ }
+ sasl_server_auth_cancel(client, NULL, NULL, SASL_SERVER_REPLY_AUTH_ABORTED);
+}
+
+void sasl_server_auth_delayed_final(struct client *client)
+{
+ client->final_response = FALSE;
+ client->authenticating = FALSE;
+ client->auth_client_continue_pending = FALSE;
+ call_client_callback(client, client->delayed_final_reply, NULL, client->final_args);
+}
diff --git a/src/login-common/sasl-server.h b/src/login-common/sasl-server.h
new file mode 100644
index 0000000..66d0cb4
--- /dev/null
+++ b/src/login-common/sasl-server.h
@@ -0,0 +1,41 @@
+#ifndef SASL_SERVER_H
+#define SASL_SERVER_H
+
+struct client;
+
+enum sasl_server_reply {
+ SASL_SERVER_REPLY_SUCCESS,
+ SASL_SERVER_REPLY_AUTH_FAILED,
+ SASL_SERVER_REPLY_AUTH_ABORTED,
+ SASL_SERVER_REPLY_MASTER_FAILED,
+ SASL_SERVER_REPLY_CONTINUE
+};
+
+enum sasl_server_auth_flags {
+ /* Allow the use of private mechanism */
+ SASL_SERVER_AUTH_FLAG_PRIVATE = BIT(0),
+ /* Signal to the post-login service that this is an implicit login,
+ meaning that no command success reply is expected. */
+ SASL_SERVER_AUTH_FLAG_IMPLICIT = BIT(1),
+};
+
+typedef void sasl_server_callback_t(struct client *client,
+ enum sasl_server_reply reply,
+ const char *data, const char *const *args);
+
+const struct auth_mech_desc *
+sasl_server_get_advertised_mechs(struct client *client, unsigned int *count_r);
+const struct auth_mech_desc *
+sasl_server_find_available_mech(struct client *client, const char *name);
+
+void sasl_server_auth_begin(struct client *client,
+ const char *service, const char *mech_name,
+ enum sasl_server_auth_flags flags,
+ const char *initial_resp_base64,
+ sasl_server_callback_t *callback);
+void sasl_server_auth_delayed_final(struct client *client);
+void sasl_server_auth_failed(struct client *client, const char *reason,
+ const char *code) ATTR_NULL(3);
+void sasl_server_auth_abort(struct client *client);
+
+#endif
diff --git a/src/master/Makefile.am b/src/master/Makefile.am
new file mode 100644
index 0000000..b467d76
--- /dev/null
+++ b/src/master/Makefile.am
@@ -0,0 +1,99 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+sbin_PROGRAMS = dovecot
+systemd_lib =
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ $(BINARY_CFLAGS)
+
+dovecot_LDADD = \
+ $(SYSTEMD_LIBS) \
+ $(LIBCAP) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dovecot_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+dovecot_SOURCES = \
+ capabilities-posix.c \
+ dup2-array.c \
+ main.c \
+ master-client.c \
+ master-settings.c \
+ service-anvil.c \
+ service-listen.c \
+ service-log.c \
+ service-monitor.c \
+ service-process.c \
+ service-process-notify.c \
+ service.c
+
+noinst_HEADERS = \
+ capabilities.h \
+ common.h \
+ dup2-array.h \
+ master-client.h \
+ master-settings.h \
+ service-anvil.h \
+ service-listen.h \
+ service-log.h \
+ service-monitor.h \
+ service-process.h \
+ service-process-notify.h \
+ service.h
+
+test_programs = \
+ test-auth-client \
+ test-auth-master \
+ test-master-login-auth
+
+test_nocheck_programs =
+
+noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs)
+
+test_libs = \
+ ../lib-auth/libauth.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ ../lib-auth/libauth.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_auth_client_SOURCES = test-auth-client.c
+test_auth_client_LDADD = $(test_libs)
+test_auth_client_DEPENDENCIES = $(test_deps)
+
+test_auth_master_SOURCES = test-auth-master.c
+test_auth_master_LDADD = $(test_libs)
+test_auth_master_DEPENDENCIES = $(test_deps)
+
+test_master_login_auth_SOURCES = test-master-login-auth.c
+test_master_login_auth_LDADD = $(test_libs)
+test_master_login_auth_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/master/Makefile.in b/src/master/Makefile.in
new file mode 100644
index 0000000..2d11716
--- /dev/null
+++ b/src/master/Makefile.in
@@ -0,0 +1,971 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+sbin_PROGRAMS = dovecot$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1) $(am__EXEEXT_2)
+subdir = src/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 $(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-auth-client$(EXEEXT) test-auth-master$(EXEEXT) \
+ test-master-login-auth$(EXEEXT)
+am__EXEEXT_2 =
+am__installdirs = "$(DESTDIR)$(sbindir)"
+PROGRAMS = $(noinst_PROGRAMS) $(sbin_PROGRAMS)
+am_dovecot_OBJECTS = capabilities-posix.$(OBJEXT) dup2-array.$(OBJEXT) \
+ main.$(OBJEXT) master-client.$(OBJEXT) \
+ master-settings.$(OBJEXT) service-anvil.$(OBJEXT) \
+ service-listen.$(OBJEXT) service-log.$(OBJEXT) \
+ service-monitor.$(OBJEXT) service-process.$(OBJEXT) \
+ service-process-notify.$(OBJEXT) service.$(OBJEXT)
+dovecot_OBJECTS = $(am_dovecot_OBJECTS)
+am__DEPENDENCIES_1 =
+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_client_OBJECTS = test-auth-client.$(OBJEXT)
+test_auth_client_OBJECTS = $(am_test_auth_client_OBJECTS)
+am__DEPENDENCIES_2 = ../lib-auth/libauth.la ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la ../lib-test/libtest.la \
+ ../lib/liblib.la $(am__DEPENDENCIES_1)
+am_test_auth_master_OBJECTS = test-auth-master.$(OBJEXT)
+test_auth_master_OBJECTS = $(am_test_auth_master_OBJECTS)
+am_test_master_login_auth_OBJECTS = test-master-login-auth.$(OBJEXT)
+test_master_login_auth_OBJECTS = $(am_test_master_login_auth_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)/capabilities-posix.Po \
+ ./$(DEPDIR)/dup2-array.Po ./$(DEPDIR)/main.Po \
+ ./$(DEPDIR)/master-client.Po ./$(DEPDIR)/master-settings.Po \
+ ./$(DEPDIR)/service-anvil.Po ./$(DEPDIR)/service-listen.Po \
+ ./$(DEPDIR)/service-log.Po ./$(DEPDIR)/service-monitor.Po \
+ ./$(DEPDIR)/service-process-notify.Po \
+ ./$(DEPDIR)/service-process.Po ./$(DEPDIR)/service.Po \
+ ./$(DEPDIR)/test-auth-client.Po \
+ ./$(DEPDIR)/test-auth-master.Po \
+ ./$(DEPDIR)/test-master-login-auth.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 = $(dovecot_SOURCES) $(test_auth_client_SOURCES) \
+ $(test_auth_master_SOURCES) $(test_master_login_auth_SOURCES)
+DIST_SOURCES = $(dovecot_SOURCES) $(test_auth_client_SOURCES) \
+ $(test_auth_master_SOURCES) $(test_master_login_auth_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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@
+systemd_lib =
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ $(BINARY_CFLAGS)
+
+dovecot_LDADD = \
+ $(SYSTEMD_LIBS) \
+ $(LIBCAP) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+dovecot_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+dovecot_SOURCES = \
+ capabilities-posix.c \
+ dup2-array.c \
+ main.c \
+ master-client.c \
+ master-settings.c \
+ service-anvil.c \
+ service-listen.c \
+ service-log.c \
+ service-monitor.c \
+ service-process.c \
+ service-process-notify.c \
+ service.c
+
+noinst_HEADERS = \
+ capabilities.h \
+ common.h \
+ dup2-array.h \
+ master-client.h \
+ master-settings.h \
+ service-anvil.h \
+ service-listen.h \
+ service-log.h \
+ service-monitor.h \
+ service-process.h \
+ service-process-notify.h \
+ service.h
+
+test_programs = \
+ test-auth-client \
+ test-auth-master \
+ test-master-login-auth
+
+test_nocheck_programs =
+test_libs = \
+ ../lib-auth/libauth.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ ../lib-auth/libauth.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_auth_client_SOURCES = test-auth-client.c
+test_auth_client_LDADD = $(test_libs)
+test_auth_client_DEPENDENCIES = $(test_deps)
+test_auth_master_SOURCES = test-auth-master.c
+test_auth_master_LDADD = $(test_libs)
+test_auth_master_DEPENDENCIES = $(test_deps)
+test_master_login_auth_SOURCES = test-master-login-auth.c
+test_master_login_auth_LDADD = $(test_libs)
+test_master_login_auth_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/master/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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
+install-sbinPROGRAMS: $(sbin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(sbindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(sbindir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(sbindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(sbindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-sbinPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(sbindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(sbindir)" && rm -f $$files
+
+clean-sbinPROGRAMS:
+ @list='$(sbin_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
+
+dovecot$(EXEEXT): $(dovecot_OBJECTS) $(dovecot_DEPENDENCIES) $(EXTRA_dovecot_DEPENDENCIES)
+ @rm -f dovecot$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(dovecot_OBJECTS) $(dovecot_LDADD) $(LIBS)
+
+test-auth-client$(EXEEXT): $(test_auth_client_OBJECTS) $(test_auth_client_DEPENDENCIES) $(EXTRA_test_auth_client_DEPENDENCIES)
+ @rm -f test-auth-client$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_auth_client_OBJECTS) $(test_auth_client_LDADD) $(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)
+
+test-master-login-auth$(EXEEXT): $(test_master_login_auth_OBJECTS) $(test_master_login_auth_DEPENDENCIES) $(EXTRA_test_master_login_auth_DEPENDENCIES)
+ @rm -f test-master-login-auth$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_master_login_auth_OBJECTS) $(test_master_login_auth_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/capabilities-posix.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dup2-array.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-anvil.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-listen.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-log.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-monitor.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-process-notify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-process.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-master.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-master-login-auth.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(sbindir)"; 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-sbinPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/capabilities-posix.Po
+ -rm -f ./$(DEPDIR)/dup2-array.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/master-client.Po
+ -rm -f ./$(DEPDIR)/master-settings.Po
+ -rm -f ./$(DEPDIR)/service-anvil.Po
+ -rm -f ./$(DEPDIR)/service-listen.Po
+ -rm -f ./$(DEPDIR)/service-log.Po
+ -rm -f ./$(DEPDIR)/service-monitor.Po
+ -rm -f ./$(DEPDIR)/service-process-notify.Po
+ -rm -f ./$(DEPDIR)/service-process.Po
+ -rm -f ./$(DEPDIR)/service.Po
+ -rm -f ./$(DEPDIR)/test-auth-client.Po
+ -rm -f ./$(DEPDIR)/test-auth-master.Po
+ -rm -f ./$(DEPDIR)/test-master-login-auth.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-sbinPROGRAMS
+
+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)/capabilities-posix.Po
+ -rm -f ./$(DEPDIR)/dup2-array.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/master-client.Po
+ -rm -f ./$(DEPDIR)/master-settings.Po
+ -rm -f ./$(DEPDIR)/service-anvil.Po
+ -rm -f ./$(DEPDIR)/service-listen.Po
+ -rm -f ./$(DEPDIR)/service-log.Po
+ -rm -f ./$(DEPDIR)/service-monitor.Po
+ -rm -f ./$(DEPDIR)/service-process-notify.Po
+ -rm -f ./$(DEPDIR)/service-process.Po
+ -rm -f ./$(DEPDIR)/service.Po
+ -rm -f ./$(DEPDIR)/test-auth-client.Po
+ -rm -f ./$(DEPDIR)/test-auth-master.Po
+ -rm -f ./$(DEPDIR)/test-master-login-auth.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-sbinPROGRAMS
+
+.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-sbinPROGRAMS 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-sbinPROGRAMS 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-sbinPROGRAMS
+
+.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/master/capabilities-posix.c b/src/master/capabilities-posix.c
new file mode 100644
index 0000000..666b072
--- /dev/null
+++ b/src/master/capabilities-posix.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "capabilities.h"
+
+#ifdef HAVE_LIBCAP
+
+#include <sys/capability.h>
+
+void drop_capabilities(void)
+{
+ /* the capabilities that we *need* in order to operate */
+ static cap_value_t suidcaps[] = {
+ CAP_CHOWN,
+ CAP_KILL,
+ CAP_SYS_CHROOT,
+ CAP_SETUID,
+ CAP_SETGID,
+ CAP_NET_BIND_SERVICE,
+ /* we may want to open any config/log files */
+ CAP_DAC_OVERRIDE
+ };
+ cap_t caps;
+
+ caps = cap_init();
+ cap_clear(caps);
+ cap_set_flag(caps, CAP_PERMITTED,
+ N_ELEMENTS(suidcaps), suidcaps, CAP_SET);
+ cap_set_flag(caps, CAP_EFFECTIVE,
+ N_ELEMENTS(suidcaps), suidcaps, CAP_SET);
+ cap_set_proc(caps);
+ cap_free(caps);
+}
+
+#endif
diff --git a/src/master/capabilities.h b/src/master/capabilities.h
new file mode 100644
index 0000000..133eb62
--- /dev/null
+++ b/src/master/capabilities.h
@@ -0,0 +1,14 @@
+#ifndef CAPABILITIES_H
+#define CAPABILITIES_H
+
+#if defined(HAVE_LIBCAP)
+
+void drop_capabilities(void);
+
+#else
+
+static inline void drop_capabilities(void) {}
+
+#endif
+
+#endif
diff --git a/src/master/common.h b/src/master/common.h
new file mode 100644
index 0000000..36bda94
--- /dev/null
+++ b/src/master/common.h
@@ -0,0 +1,27 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+#include "lib.h"
+#include "master-interface.h"
+#include "master-settings.h"
+
+#define LINUX_PROC_FS_SUID_DUMPABLE "/proc/sys/fs/suid_dumpable"
+#define LINUX_PROC_SYS_KERNEL_CORE_PATTERN "/proc/sys/kernel/core_pattern"
+
+extern uid_t master_uid;
+extern gid_t master_gid;
+extern bool core_dumps_disabled;
+extern bool have_proc_fs_suid_dumpable;
+extern bool have_proc_sys_kernel_core_pattern;
+extern const char *ssl_manual_key_password;
+extern int global_master_dead_pipe_fd[2];
+extern struct service_list *services;
+extern bool startup_finished;
+
+void process_exec(const char *cmd) ATTR_NORETURN;
+
+int get_uidgid(const char *user, uid_t *uid_r, gid_t *gid_r,
+ const char **error_r);
+int get_gid(const char *group, gid_t *gid_r, const char **error_r);
+
+#endif
diff --git a/src/master/dup2-array.c b/src/master/dup2-array.c
new file mode 100644
index 0000000..062590a
--- /dev/null
+++ b/src/master/dup2-array.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "dup2-array.h"
+
+#include <unistd.h>
+
+void dup2_append(ARRAY_TYPE(dup2) *dups, int fd_src, int fd_dest)
+{
+ struct dup2 d;
+
+ i_assert(fd_src >= 0);
+ i_assert(fd_dest >= 0);
+
+ d.fd_src = fd_src;
+ d.fd_dest = fd_dest;
+ array_push_back(dups, &d);
+}
+
+int dup2_array(ARRAY_TYPE(dup2) *dups_arr)
+{
+ struct dup2 *dups;
+ bool *moved, moves;
+ unsigned int i, j, count, conflict;
+ int fd;
+
+ dups = array_get_modifiable(dups_arr, &count);
+
+ moved = t_new(bool, count);
+ for (;;) {
+ conflict = count;
+ moves = FALSE;
+ for (i = 0; i < count; i++) {
+ if (moved[i])
+ continue;
+
+ for (j = 0; j < count; j++) {
+ if (dups[j].fd_src == dups[i].fd_dest &&
+ !moved[j]) {
+ conflict = j;
+ break;
+ }
+ }
+
+ if (j == count) {
+ /* no conflicts, move it */
+ moved[i] = TRUE;
+ moves = TRUE;
+ if (dup2(dups[i].fd_src, dups[i].fd_dest) < 0) {
+ i_error("dup2(%d, %d) failed: %m",
+ dups[i].fd_src,
+ dups[i].fd_dest);
+ return -1;
+ }
+ }
+ }
+ if (conflict == count)
+ break;
+
+ if (moves) {
+ /* it's possible that the conflicting fd was
+ moved already. try again. */
+ continue;
+ }
+
+ /* ok, we have to dup() */
+ fd = dup(dups[conflict].fd_src);
+ if (fd == -1) {
+ i_error("dup(%d) failed: %m", dups[conflict].fd_src);
+ return -1;
+ }
+ fd_close_on_exec(fd, TRUE);
+ dups[conflict].fd_src = fd;
+ }
+ return 0;
+}
+
diff --git a/src/master/dup2-array.h b/src/master/dup2-array.h
new file mode 100644
index 0000000..9d967c0
--- /dev/null
+++ b/src/master/dup2-array.h
@@ -0,0 +1,13 @@
+#ifndef DUP2_ARRAY_H
+#define DUP2_ARRAY_H
+
+struct dup2 {
+ int fd_src, fd_dest;
+};
+ARRAY_DEFINE_TYPE(dup2, struct dup2);
+
+void dup2_append(ARRAY_TYPE(dup2) *dups, int fd_src, int fd_dest);
+
+int dup2_array(ARRAY_TYPE(dup2) *dups);
+
+#endif
diff --git a/src/master/main.c b/src/master/main.c
new file mode 100644
index 0000000..d7228e2
--- /dev/null
+++ b/src/master/main.c
@@ -0,0 +1,941 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "ioloop.h"
+#include "lib-signals.h"
+#include "array.h"
+#include "write-full.h"
+#include "env-util.h"
+#include "hostpid.h"
+#include "path-util.h"
+#include "ipwd.h"
+#include "str.h"
+#include "time-util.h"
+#include "execv-const.h"
+#include "restrict-process-size.h"
+#include "master-instance.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "askpass.h"
+#include "capabilities.h"
+#include "master-client.h"
+#include "service.h"
+#include "service-anvil.h"
+#include "service-listen.h"
+#include "service-monitor.h"
+#include "service-process.h"
+#include "service-log.h"
+#include "dovecot-version.h"
+#ifdef HAVE_LIBSYSTEMD
+# include <systemd/sd-daemon.h>
+# define i_sd_notify(unset, message) (void)sd_notify((unset), (message))
+# define i_sd_notifyf(unset, message, ...) \
+ (void)sd_notifyf((unset), (message), __VA_ARGS__)
+#else
+# define i_sd_notify(unset, message)
+# define i_sd_notifyf(unset, message, ...)
+#endif
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define DOVECOT_CONFIG_BIN_PATH BINDIR"/doveconf"
+
+#define MASTER_SERVICE_NAME "master"
+#define FATAL_FILENAME "master-fatal.lastlog"
+#define MASTER_PID_FILE_NAME "master.pid"
+#define SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS (60*3*1000)
+
+struct master_delayed_error {
+ enum log_type type;
+ const char *line;
+};
+
+uid_t master_uid;
+gid_t master_gid;
+bool core_dumps_disabled;
+bool have_proc_fs_suid_dumpable;
+bool have_proc_sys_kernel_core_pattern;
+const char *ssl_manual_key_password;
+int global_master_dead_pipe_fd[2];
+struct service_list *services;
+bool startup_finished = FALSE;
+
+static char *pidfile_path;
+static struct master_instance_list *instances;
+static struct timeout *to_instance;
+
+static ARRAY(struct master_delayed_error) delayed_errors;
+static pool_t delayed_errors_pool;
+static failure_callback_t *orig_fatal_callback;
+static failure_callback_t *orig_error_callback;
+
+static const struct setting_parser_info *set_roots[] = {
+ &master_setting_parser_info,
+ NULL
+};
+
+void process_exec(const char *cmd)
+{
+ const char *executable, *p, **argv;
+
+ argv = t_strsplit(cmd, " ");
+ executable = argv[0];
+
+ /* hide the path, it's ugly */
+ p = strrchr(argv[0], '/');
+ if (p != NULL) argv[0] = p+1;
+
+ /* prefix with dovecot/ */
+ argv[0] = t_strdup_printf("%s/%s", services->set->instance_name,
+ argv[0]);
+ if (!str_begins(argv[0], PACKAGE))
+ argv[0] = t_strconcat(PACKAGE"-", argv[0], NULL);
+ execv_const(executable, argv);
+}
+
+int get_uidgid(const char *user, uid_t *uid_r, gid_t *gid_r,
+ const char **error_r)
+{
+ struct passwd pw;
+
+ if (*user == '\0') {
+ *uid_r = (uid_t)-1;
+ *gid_r = (gid_t)-1;
+ return 0;
+ }
+
+ switch (i_getpwnam(user, &pw)) {
+ case -1:
+ *error_r = t_strdup_printf("getpwnam(%s) failed: %m", user);
+ return -1;
+ case 0:
+ *error_r = t_strdup_printf("User doesn't exist: %s", user);
+ return -1;
+ default:
+ *uid_r = pw.pw_uid;
+ *gid_r = pw.pw_gid;
+ return 0;
+ }
+}
+
+int get_gid(const char *group, gid_t *gid_r, const char **error_r)
+{
+ struct group gr;
+
+ if (*group == '\0') {
+ *gid_r = (gid_t)-1;
+ return 0;
+ }
+
+ switch (i_getgrnam(group, &gr)) {
+ case -1:
+ *error_r = t_strdup_printf("getgrnam(%s) failed: %m", group);
+ return -1;
+ case 0:
+ *error_r = t_strdup_printf("Group doesn't exist: %s", group);
+ return -1;
+ default:
+ *gid_r = gr.gr_gid;
+ return 0;
+ }
+}
+
+static void ATTR_NORETURN ATTR_FORMAT(2, 0)
+master_fatal_callback(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ const char *path, *str;
+ va_list args2;
+ pid_t pid;
+ int fd;
+
+ /* if we already forked a child process, this isn't fatal for the
+ main process and there's no need to write the fatal file. */
+ if (str_to_pid(my_pid, &pid) < 0)
+ i_unreached();
+ if (getpid() == pid) {
+ /* write the error message to a file (we're chdired to
+ base dir) */
+ path = t_strconcat(FATAL_FILENAME, NULL);
+ fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+ if (fd != -1) {
+ VA_COPY(args2, args);
+ str = t_strdup_vprintf(format, args2);
+ va_end(args2);
+ (void)write_full(fd, str, strlen(str));
+ i_close_fd(&fd);
+ }
+ }
+
+ orig_fatal_callback(ctx, format, args);
+ abort(); /* just to silence the noreturn attribute warnings */
+}
+
+static void ATTR_NORETURN ATTR_FORMAT(2, 0)
+startup_fatal_handler(const struct failure_context *ctx,
+ const char *fmt, va_list args)
+{
+ va_list args2;
+
+ VA_COPY(args2, args);
+ fprintf(stderr, "%s%s\n", failure_log_type_prefixes[ctx->type],
+ t_strdup_vprintf(fmt, args2));
+ va_end(args2);
+ orig_fatal_callback(ctx, fmt, args);
+ abort();
+}
+
+static void ATTR_FORMAT(2, 0)
+startup_error_handler(const struct failure_context *ctx,
+ const char *fmt, va_list args)
+{
+ va_list args2;
+
+ VA_COPY(args2, args);
+ fprintf(stderr, "%s%s\n", failure_log_type_prefixes[ctx->type],
+ t_strdup_vprintf(fmt, args2));
+ va_end(args2);
+ orig_error_callback(ctx, fmt, args);
+}
+
+static void ATTR_FORMAT(2, 0)
+startup_early_error_handler(const struct failure_context *ctx,
+ const char *fmt, va_list args)
+{
+ struct master_delayed_error *err;
+ va_list args2;
+
+ VA_COPY(args2, args);
+ if (delayed_errors_pool == NULL) {
+ delayed_errors_pool =
+ pool_alloconly_create("delayed errors", 512);
+ i_array_init(&delayed_errors, 8);
+ }
+ err = array_append_space(&delayed_errors);
+ err->type = ctx->type;
+ err->line = p_strdup_vprintf(delayed_errors_pool, fmt, args2);
+ va_end(args2);
+
+ orig_error_callback(ctx, fmt, args);
+}
+
+static void startup_early_errors_flush(void)
+{
+ struct failure_context ctx;
+ const struct master_delayed_error *err;
+
+ if (delayed_errors_pool == NULL)
+ return;
+
+ i_zero(&ctx);
+ array_foreach(&delayed_errors, err) {
+ ctx.type = err->type;
+ i_log_type(&ctx, "%s", err->line);
+ }
+ array_free(&delayed_errors);
+ pool_unref(&delayed_errors_pool);
+}
+
+static void fatal_log_check(const struct master_settings *set)
+{
+ const char *path;
+ char buf[1024];
+ ssize_t ret;
+ int fd;
+
+ path = t_strconcat(set->base_dir, "/"FATAL_FILENAME, NULL);
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return;
+
+ ret = read(fd, buf, sizeof(buf)-1);
+ if (ret < 0)
+ i_error("read(%s) failed: %m", path);
+ else {
+ buf[ret] = '\0';
+ fprintf(stderr, "Last died with error (see error log for more "
+ "information): %s\n", buf);
+ }
+
+ i_close_fd(&fd);
+ i_unlink(path);
+}
+
+static bool pid_file_read(const char *path, pid_t *pid_r)
+{
+ char buf[32];
+ int fd;
+ ssize_t ret;
+ bool found;
+
+ *pid_r = (pid_t)-1;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return FALSE;
+ i_fatal("open(%s) failed: %m", path);
+ }
+
+ ret = read(fd, buf, sizeof(buf)-1);
+ if (ret <= 0) {
+ if (ret == 0)
+ i_error("Empty PID file in %s, overriding", path);
+ else
+ i_fatal("read(%s) failed: %m", path);
+ found = FALSE;
+ } else {
+ if (buf[ret-1] == '\n')
+ ret--;
+ buf[ret] = '\0';
+ if (str_to_pid(buf, pid_r) < 0) {
+ i_error("PID file contains invalid PID value");
+ found = FALSE;
+ } else {
+ found = !(*pid_r == getpid() ||
+ (kill(*pid_r, 0) < 0 && errno == ESRCH));
+ }
+ }
+ i_close_fd(&fd);
+ return found;
+}
+
+static void pid_file_check_running(const char *path)
+{
+ pid_t pid;
+
+ if (!pid_file_read(path, &pid))
+ return;
+
+ i_fatal("Dovecot is already running with PID %s "
+ "(read from %s)", dec2str(pid), path);
+}
+
+static void create_pid_file(const char *path)
+{
+ const char *pid;
+ int fd;
+
+ pid = t_strconcat(dec2str(getpid()), "\n", NULL);
+
+ fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0644);
+ if (fd == -1)
+ i_fatal("open(%s) failed: %m", path);
+ if (write_full(fd, pid, strlen(pid)) < 0)
+ i_fatal("write() failed in %s: %m", path);
+ i_close_fd(&fd);
+}
+
+static void create_config_symlink(const struct master_settings *set)
+{
+ const char *base_config_path;
+
+ base_config_path = t_strconcat(set->base_dir, "/"PACKAGE".conf", NULL);
+ i_unlink_if_exists(base_config_path);
+
+ if (symlink(services->config->config_file_path, base_config_path) < 0) {
+ i_error("symlink(%s, %s) failed: %m",
+ services->config->config_file_path, base_config_path);
+ }
+}
+
+static void instance_update_now(struct master_instance_list *list)
+{
+ int ret;
+
+ ret = master_instance_list_set_name(list, services->set->base_dir,
+ services->set->instance_name);
+ if (ret == 0) {
+ /* duplicate instance names. allow without warning.. */
+ (void)master_instance_list_update(list, services->set->base_dir);
+ }
+
+ timeout_remove(&to_instance);
+ to_instance = timeout_add((3600 * 12 + i_rand_limit(60 * 30)) * 1000,
+ instance_update_now, list);
+}
+
+static void instance_update(const struct master_settings *set)
+{
+ const char *path;
+
+ path = t_strconcat(set->state_dir, "/"MASTER_INSTANCE_FNAME, NULL);
+ instances = master_instance_list_init(path);
+ instance_update_now(instances);
+}
+
+static void
+sig_settings_reload(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const struct master_settings *set;
+ void **sets;
+ struct service_list *new_services;
+ struct service *service;
+ const char *error;
+
+ i_sd_notify(0, "RELOADING=1");
+ i_warning("SIGHUP received - reloading configuration");
+
+ /* see if hostname changed */
+ hostpid_init();
+
+ if (services->config->process_avail == 0) {
+ /* we can't reload config if there's no config process. */
+ if (service_process_create(services->config) == NULL) {
+ i_error("Can't reload configuration because "
+ "we couldn't create a config process");
+ i_sd_notify(0, "READY=1");
+ return;
+ }
+ }
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.module = MASTER_SERVICE_NAME;
+ input.config_path = services_get_config_socket_path(services);
+ if (master_service_settings_read(master_service, &input,
+ &output, &error) < 0) {
+ i_error("Error reading configuration: %s", error);
+ i_sd_notify(0, "READY=1");
+ return;
+ }
+ sets = master_service_settings_get_others(master_service);
+ set = sets[0];
+
+ if (services_create(set, &new_services, &error) < 0) {
+ /* new configuration is invalid, keep the old */
+ i_error("Config reload failed: %s", error);
+ i_sd_notify(0, "READY=1");
+ return;
+ }
+ new_services->config->config_file_path =
+ p_strdup(new_services->pool,
+ services->config->config_file_path);
+
+ /* switch to new configuration. */
+ services_monitor_stop(services, FALSE);
+ if (services_listen_using(new_services, services) < 0) {
+ services_monitor_start(services);
+ i_sd_notify(0, "READY=1");
+ return;
+ }
+
+ /* anvil never dies. it just gets moved to the new services list */
+ service = service_lookup_type(services, SERVICE_TYPE_ANVIL);
+ if (service != NULL) {
+ while (service->busy_processes != NULL)
+ service_process_destroy(service->busy_processes);
+ while (service->idle_processes_head != NULL)
+ service_process_destroy(service->idle_processes_head);
+ }
+ services_destroy(services, FALSE);
+
+ services = new_services;
+ services_monitor_start(services);
+ i_sd_notify(0, "READY=1");
+}
+
+static void
+sig_log_reopen(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ unsigned int uninitialized_count;
+ service_signal(services->log, SIGUSR1, &uninitialized_count);
+
+ master_service->log_initialized = FALSE;
+ master_service_init_log(master_service);
+ i_set_fatal_handler(master_fatal_callback);
+}
+
+static void
+sig_reap_children(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ services_monitor_reap_children();
+}
+
+static void sig_die(const siginfo_t *si, void *context ATTR_UNUSED)
+{
+ 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));
+ /* make sure new processes won't be created by the currently
+ running ioloop. */
+ services->destroying = TRUE;
+ i_sd_notify(0, "STOPPING=1\nSTATUS=Dovecot stopping...");
+ master_service_stop(master_service);
+}
+
+static struct master_settings *master_settings_read(void)
+{
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const char *error;
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.module = "master";
+ input.parse_full_config = TRUE;
+ input.preserve_environment = TRUE;
+ if (master_service_settings_read(master_service, &input, &output,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ return master_service_settings_get_others(master_service)[0];
+}
+
+static void main_log_startup(char **protocols)
+{
+#define STARTUP_STRING PACKAGE_NAME" v"DOVECOT_VERSION_FULL" starting up"
+ string_t *str = t_str_new(128);
+ rlim_t core_limit;
+ struct stat st;
+
+ str_append(str, STARTUP_STRING);
+ if (protocols[0] == NULL)
+ str_append(str, " without any protocols");
+ else {
+ str_printfa(str, " for %s",
+ t_strarray_join((const char **)protocols, ", "));
+ }
+
+ core_dumps_disabled = restrict_get_core_limit(&core_limit) == 0 &&
+ core_limit == 0;
+ if (core_dumps_disabled)
+ str_append(str, " (core dumps disabled)");
+ if (stat(LINUX_PROC_FS_SUID_DUMPABLE, &st) == 0)
+ have_proc_fs_suid_dumpable = TRUE;
+ if (stat(LINUX_PROC_SYS_KERNEL_CORE_PATTERN, &st) == 0)
+ have_proc_sys_kernel_core_pattern = TRUE;
+ i_info("%s", str_c(str));
+}
+
+static void master_set_process_limit(void)
+{
+ struct service *service;
+ unsigned int process_limit = 0;
+ rlim_t nproc;
+
+ /* we'll just count all the processes that can exist and set the
+ process limit so that we won't reach it. it's usually higher than
+ needed, since we'd only need to set it high enough for each
+ separate UID not to reach the limit, but this is difficult to
+ guess: mail processes should probably be counted together for a
+ common vmail user (unless system users are being used), but
+ we can't really guess what the mail processes are. */
+ array_foreach_elem(&services->services, service)
+ process_limit += service->process_limit;
+
+ if (restrict_get_process_limit(&nproc) == 0 &&
+ process_limit > nproc)
+ restrict_process_count(process_limit);
+}
+
+static void main_init(const struct master_settings *set)
+{
+ master_set_process_limit();
+ drop_capabilities();
+
+ /* deny file access from everyone else except owner */
+ (void)umask(0077);
+
+ main_log_startup(set->protocols_split);
+
+ lib_signals_init();
+ lib_signals_ignore(SIGPIPE, TRUE);
+ lib_signals_ignore(SIGALRM, FALSE);
+ lib_signals_set_handler(SIGHUP, LIBSIG_FLAGS_SAFE,
+ sig_settings_reload, NULL);
+ lib_signals_set_handler(SIGUSR1, LIBSIG_FLAGS_SAFE,
+ sig_log_reopen, NULL);
+ lib_signals_set_handler(SIGCHLD, LIBSIG_FLAGS_SAFE,
+ sig_reap_children, NULL);
+ lib_signals_set_handler(SIGINT, LIBSIG_FLAGS_SAFE, sig_die, NULL);
+ lib_signals_set_handler(SIGTERM, LIBSIG_FLAGS_SAFE, sig_die, NULL);
+
+ create_pid_file(pidfile_path);
+ create_config_symlink(set);
+ instance_update(set);
+ master_clients_init();
+
+ services_monitor_start(services);
+ i_sd_notifyf(0, "READY=1\nSTATUS=v" DOVECOT_VERSION_FULL " running\n"
+ "MAINPID=%u", getpid());
+ startup_finished = TRUE;
+}
+
+static void global_dead_pipe_close(void)
+{
+ if (close(global_master_dead_pipe_fd[0]) < 0)
+ i_error("close(global dead pipe) failed: %m");
+ if (close(global_master_dead_pipe_fd[1]) < 0)
+ i_error("close(global dead pipe) failed: %m");
+ global_master_dead_pipe_fd[0] = -1;
+ global_master_dead_pipe_fd[1] = -1;
+}
+
+static void main_deinit(void)
+{
+ master_clients_deinit();
+ instance_update_now(instances);
+ timeout_remove(&to_instance);
+ master_instance_list_deinit(&instances);
+
+ /* kill services and wait for them to die before unlinking pid file */
+ global_dead_pipe_close();
+ services_destroy(services, TRUE);
+
+ i_unlink(pidfile_path);
+ i_free(pidfile_path);
+
+ service_anvil_global_deinit();
+ service_pids_deinit();
+ /* notify systemd that we are done */
+ i_sd_notify(0, "STATUS=Dovecot stopped");
+}
+
+static const char *get_full_config_path(struct service_list *list)
+{
+ const char *path;
+
+ path = master_service_get_config_path(master_service);
+ if (*path == '/')
+ return path;
+
+ const char *abspath, *error;
+ if (t_abspath(path, &abspath, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", path, error);
+ }
+ return p_strdup(list->pool, abspath);
+}
+
+static void
+master_time_moved(const struct timeval *old_time,
+ const struct timeval *new_time)
+{
+ long long diff = timeval_diff_usecs(old_time, new_time);
+ unsigned int msecs;
+
+ if (diff < 0) {
+ diff = -diff;
+ i_warning("Time moved forwards by %lld.%06lld seconds - adjusting timeouts.",
+ diff / 1000000, diff % 1000000);
+ return;
+ }
+ msecs = (unsigned int)(diff/1000);
+
+ /* time moved backwards. disable launching new service processes
+ until the throttling timeout has reached. */
+ if (msecs > SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS)
+ msecs = SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS;
+ services_throttle_time_sensitives(services, msecs);
+ i_warning("Time moved backwards by %lld.%06lld seconds, waiting for "
+ "%u.%03u seconds until new services are launched again.",
+ diff / 1000000, diff % 1000000, msecs / 1000, msecs % 1000);
+}
+
+static void daemonize(void)
+{
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ i_fatal("fork() failed: %m");
+
+ if (pid != 0)
+ _exit(0);
+
+ if (setsid() < 0)
+ i_fatal("setsid() failed: %m");
+
+ /* update my_pid */
+ hostpid_init();
+}
+
+static void print_help(void)
+{
+ fprintf(stderr,
+"Usage: dovecot [-F] [-c <config file>] [-p] [-n] [-a] [--help] [--version]\n"
+" [--build-options] [--hostdomain] [reload] [stop]\n");
+}
+
+static void print_build_options(void)
+{
+ printf("Build options:"
+#ifdef IOLOOP_EPOLL
+ " ioloop=epoll"
+#endif
+#ifdef IOLOOP_KQUEUE
+ " ioloop=kqueue"
+#endif
+#ifdef IOLOOP_POLL
+ " ioloop=poll"
+#endif
+#ifdef IOLOOP_SELECT
+ " ioloop=select"
+#endif
+#ifdef IOLOOP_NOTIFY_INOTIFY
+ " notify=inotify"
+#endif
+#ifdef IOLOOP_NOTIFY_KQUEUE
+ " notify=kqueue"
+#endif
+#ifdef HAVE_GNUTLS
+ " gnutls"
+#endif
+#ifdef HAVE_OPENSSL
+ " openssl"
+#endif
+ " io_block_size=%u"
+#ifdef SQL_DRIVER_PLUGINS
+ "\nSQL driver plugins:"
+#else
+ "\nSQL drivers:"
+#endif
+#ifdef BUILD_CASSANDRA
+ " cassandra"
+#endif
+#ifdef BUILD_MYSQL
+ " mysql"
+#endif
+#ifdef BUILD_PGSQL
+ " postgresql"
+#endif
+#ifdef BUILD_SQLITE
+ " sqlite"
+#endif
+ "\nPassdb:"
+#ifdef PASSDB_BSDAUTH
+ " bsdauth"
+#endif
+#ifdef PASSDB_CHECKPASSWORD
+ " checkpassword"
+#endif
+#ifdef PASSDB_LDAP
+ " ldap"
+#endif
+#ifdef PASSDB_PAM
+ " pam"
+#endif
+#ifdef PASSDB_PASSWD
+ " passwd"
+#endif
+#ifdef PASSDB_PASSWD_FILE
+ " passwd-file"
+#endif
+#ifdef PASSDB_SHADOW
+ " shadow"
+#endif
+#ifdef PASSDB_SQL
+ " sql"
+#endif
+ "\nUserdb:"
+#ifdef USERDB_CHECKPASSWORD
+ " checkpassword"
+#endif
+#ifdef USERDB_LDAP
+ " ldap"
+#ifndef BUILTIN_LDAP
+ "(plugin)"
+#endif
+#endif
+#ifdef USERDB_NSS
+ " nss"
+#endif
+#ifdef USERDB_PASSWD
+ " passwd"
+#endif
+#ifdef USERDB_PREFETCH
+ " prefetch"
+#endif
+#ifdef USERDB_PASSWD_FILE
+ " passwd-file"
+#endif
+#ifdef USERDB_SQL
+ " sql"
+#endif
+#ifdef USERDB_STATIC
+ " static"
+#endif
+ "\n", IO_BLOCK_SIZE);
+}
+
+int main(int argc, char *argv[])
+{
+ struct master_settings *set;
+ const char *error, *doveconf_arg = NULL;
+ failure_callback_t *orig_info_callback, *orig_debug_callback;
+ bool foreground = FALSE, ask_key_pass = FALSE;
+ bool doubleopts[argc];
+ int i, c;
+
+#ifdef DEBUG
+ if (getenv("GDB") == NULL)
+ fd_debug_verify_leaks(3, 1024);
+#endif
+ /* drop -- prefix from all --args. ugly, but the only way that it
+ works with standard getopt() in all OSes.. */
+ for (i = 1; i < argc; i++) {
+ if (str_begins(argv[i], "--")) {
+ if (argv[i][2] == '\0')
+ break;
+ argv[i] += 2;
+ doubleopts[i] = TRUE;
+ } else {
+ doubleopts[i] = FALSE;
+ }
+ }
+ master_service = master_service_init(MASTER_SERVICE_NAME,
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME |
+ MASTER_SERVICE_FLAG_DISABLE_SSL_SET,
+ &argc, &argv, "+Fanp");
+ i_unset_failure_prefix();
+
+ i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback,
+ &orig_info_callback, &orig_debug_callback);
+ i_set_error_handler(startup_early_error_handler);
+
+ io_loop_set_time_moved_callback(current_ioloop, master_time_moved);
+
+ master_uid = geteuid();
+ master_gid = getegid();
+
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'F':
+ foreground = TRUE;
+ break;
+ case 'a':
+ doveconf_arg = "-a";
+ break;
+ case 'n':
+ doveconf_arg = "-n";
+ break;
+ case 'p':
+ /* Ask SSL private key password */
+ ask_key_pass = TRUE;
+ break;
+ default:
+ if (!master_service_parse_option(master_service,
+ c, optarg)) {
+ print_help();
+ lib_exit(FATAL_DEFAULT);
+ }
+ break;
+ }
+ }
+ i_assert(optind > 0 && optind <= argc);
+
+ if (doveconf_arg != NULL) {
+ const char **args;
+
+ args = t_new(const char *, 5);
+ args[0] = DOVECOT_CONFIG_BIN_PATH;
+ args[1] = doveconf_arg;
+ args[2] = "-c";
+ args[3] = master_service_get_config_path(master_service);
+ args[4] = NULL;
+ execv_const(args[0], args);
+ }
+
+ if (optind == argc) {
+ /* starting Dovecot */
+ } else if (!doubleopts[optind]) {
+ /* dovecot xx -> doveadm xx */
+ (void)execv(BINDIR"/doveadm", argv);
+ i_fatal("execv("BINDIR"/doveadm) failed: %m");
+ } else if (strcmp(argv[optind], "version") == 0) {
+ printf("%s\n", DOVECOT_VERSION_FULL);
+ return 0;
+ } else if (strcmp(argv[optind], "hostdomain") == 0) {
+ printf("%s\n", my_hostdomain());
+ return 0;
+ } else if (strcmp(argv[optind], "build-options") == 0) {
+ print_build_options();
+ return 0;
+ } else if (strcmp(argv[optind], "log-error") == 0) {
+ fprintf(stderr, "Writing to error logs and killing myself..\n");
+ argv[optind] = "log test";
+ (void)execv(BINDIR"/doveadm", argv);
+ i_fatal("execv("BINDIR"/doveadm) failed: %m");
+ } else if (strcmp(argv[optind], "help") == 0) {
+ print_help();
+ return 0;
+ } else {
+ print_help();
+ i_fatal("Unknown argument: --%s", argv[optind]);
+ }
+
+ if (pipe(global_master_dead_pipe_fd) < 0)
+ i_fatal("pipe() failed: %m");
+ fd_close_on_exec(global_master_dead_pipe_fd[0], TRUE);
+ fd_close_on_exec(global_master_dead_pipe_fd[1], TRUE);
+
+ set = master_settings_read();
+ if (ask_key_pass) {
+ ssl_manual_key_password =
+ t_askpass("Give the password for SSL keys: ");
+ }
+
+ if (dup2(dev_null_fd, STDIN_FILENO) < 0)
+ i_fatal("dup2(dev_null_fd) failed: %m");
+ if (!foreground && dup2(dev_null_fd, STDOUT_FILENO) < 0)
+ i_fatal("dup2(dev_null_fd) failed: %m");
+
+ pidfile_path =
+ i_strconcat(set->base_dir, "/"MASTER_PID_FILE_NAME, NULL);
+
+ lib_set_clean_exit(TRUE);
+ master_service_init_log(master_service);
+ startup_early_errors_flush();
+ i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback,
+ &orig_info_callback, &orig_debug_callback);
+ i_set_fatal_handler(startup_fatal_handler);
+ i_set_error_handler(startup_error_handler);
+
+ pid_file_check_running(pidfile_path);
+ master_settings_do_fixes(set);
+ fatal_log_check(set);
+
+ const struct master_service_settings *service_set =
+ master_service_settings_get(master_service);
+ master_service_import_environment(service_set->import_environment);
+ master_service_env_clean();
+
+ /* create service structures from settings. if there are any errors in
+ service configuration we'll catch it here. */
+ service_pids_init();
+ service_anvil_global_init();
+ if (services_create(set, &services, &error) < 0)
+ i_fatal("%s", error);
+
+ services->config->config_file_path = get_full_config_path(services);
+
+ /* if any listening fails, fail completely */
+ if (services_listen(services) <= 0)
+ i_fatal("Failed to start listeners");
+
+ if (chdir(set->base_dir) < 0)
+ i_fatal("chdir(%s) failed: %m", set->base_dir);
+
+ i_set_fatal_handler(master_fatal_callback);
+ i_set_error_handler(orig_error_callback);
+
+ if (!foreground)
+ daemonize();
+
+ T_BEGIN {
+ main_init(set);
+ } T_END;
+ master_service_run(master_service, NULL);
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/master/master-client.c b/src/master/master-client.c
new file mode 100644
index 0000000..39e87f7
--- /dev/null
+++ b/src/master/master-client.c
@@ -0,0 +1,191 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "connection.h"
+#include "service.h"
+#include "service-process.h"
+#include "service-monitor.h"
+#include "master-client.h"
+
+struct master_client {
+ struct connection conn;
+};
+
+static void
+master_client_service_status_output(string_t *str,
+ const struct service *service)
+{
+ str_append_tabescaped(str, service->set->name);
+ str_printfa(str, "\t%u\t%u\t%u\t%u\t%u\t%ld\t%u\t%ld\t%c\t%c\t%c\t%"PRIu64"\n",
+ service->process_count, service->process_avail,
+ service->process_limit, service->client_limit,
+ (service->to_throttle == NULL ?
+ 0 : ((service->throttle_msecs + 999) / 1000)),
+ (long)service->exit_failure_last,
+ service->exit_failures_in_sec,
+ (long)service->last_drop_warning,
+ service->listen_pending ? 'y' : 'n',
+ service->listening ? 'y' : 'n',
+ service->doveadm_stop ? 'y' : 'n',
+ service->process_count_total);
+}
+
+static int
+master_client_service_status(struct master_client *client)
+{
+ struct service *service;
+ string_t *str = t_str_new(128);
+
+ array_foreach_elem(&services->services, service) {
+ str_truncate(str, 0);
+ master_client_service_status_output(str, service);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ }
+ o_stream_nsend_str(client->conn.output, "\n");
+ return 1;
+}
+
+static void
+master_client_process_output(string_t *str,
+ const struct service_process *process)
+{
+ str_append_tabescaped(str, process->service->set->name);
+ str_printfa(str, "\t%ld\t%u\t%u\t%ld\t%ld\t%ld\n",
+ (long)process->pid, process->available_count,
+ process->total_count, (long)process->idle_start,
+ (long)process->last_status_update,
+ (long)process->last_kill_sent);
+}
+
+static void
+master_client_process_status_list(struct master_client *client,
+ struct service_process *processes,
+ string_t *str)
+{
+ struct service_process *p;
+
+ for (p = processes; p != NULL; p = p->next) {
+ str_truncate(str, 0);
+ master_client_process_output(str, p);
+ o_stream_nsend(client->conn.output,
+ str_data(str), str_len(str));
+ }
+}
+
+static int
+master_client_process_status(struct master_client *client,
+ const char *const *args)
+{
+ struct service *service;
+ string_t *str = t_str_new(128);
+
+ array_foreach_elem(&services->services, service) {
+ if (args[0] != NULL && !str_array_find(args, service->set->name))
+ continue;
+ master_client_process_status_list(client,
+ service->busy_processes, str);
+ master_client_process_status_list(client,
+ service->idle_processes_head, str);
+ }
+ o_stream_nsend_str(client->conn.output, "\n");
+ return 1;
+}
+
+static int
+master_client_stop(struct master_client *client, const char *const *args)
+{
+ struct service *service;
+ const char *reply = "+\n";
+
+ for (unsigned int i = 0; args[i] != NULL; i++) {
+ service = service_lookup(services, args[i]);
+ if (service == NULL)
+ reply = t_strdup_printf("-Unknown service: %s\n", args[i]);
+ else {
+ service_monitor_stop_close(service);
+ service->doveadm_stop = TRUE;
+ }
+ }
+ o_stream_nsend_str(client->conn.output, reply);
+ return 1;
+}
+
+static int
+master_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct master_client *client = (struct master_client *)conn;
+ const char *cmd = args[0];
+
+ if (cmd == NULL) {
+ i_error("%s: Empty command", conn->name);
+ return 0;
+ }
+ args++;
+
+ if (strcmp(cmd, "SERVICE-STATUS") == 0)
+ return master_client_service_status(client);
+ if (strcmp(cmd, "PROCESS-STATUS") == 0)
+ return master_client_process_status(client, args);
+ if (strcmp(cmd, "STOP") == 0)
+ return master_client_stop(client, args);
+ i_error("%s: Unknown command: %s", conn->name, cmd);
+ return -1;
+}
+
+static void master_client_destroy(struct connection *conn)
+{
+ struct master_client *client = (struct master_client *)conn;
+
+ connection_deinit(conn);
+ i_free(client);
+}
+
+static const struct connection_settings master_conn_set = {
+ .service_name_in = "master-client",
+ .service_name_out = "master-server",
+ .major_version = 1,
+ .minor_version = 0,
+
+ .input_max_size = 1024,
+ .output_max_size = 1024,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs master_conn_vfuncs = {
+ .destroy = master_client_destroy,
+ .input_args = master_client_input_args
+};
+
+static struct connection_list *master_connections;
+
+void master_client_connected(struct service_list *service_list)
+{
+ struct master_client *client;
+ int fd;
+
+ fd = net_accept(service_list->master_fd, NULL, NULL);
+ if (fd < 0) {
+ if (fd == -2)
+ i_error("net_accept() failed: %m");
+ return;
+ }
+ fd_close_on_exec(fd, TRUE);
+ client = i_new(struct master_client, 1);
+ connection_init_server(master_connections, &client->conn,
+ "master-client", fd, fd);
+}
+
+void master_clients_init(void)
+{
+ master_connections = connection_list_init(&master_conn_set,
+ &master_conn_vfuncs);
+}
+
+void master_clients_deinit(void)
+{
+ connection_list_deinit(&master_connections);
+}
diff --git a/src/master/master-client.h b/src/master/master-client.h
new file mode 100644
index 0000000..7e62ede
--- /dev/null
+++ b/src/master/master-client.h
@@ -0,0 +1,9 @@
+#ifndef MASTER_CLIENT_H
+#define MASTER_CLIENT_H
+
+void master_client_connected(struct service_list *service_list);
+
+void master_clients_init(void);
+void master_clients_deinit(void);
+
+#endif
diff --git a/src/master/master-settings.c b/src/master/master-settings.c
new file mode 100644
index 0000000..7cfaa35
--- /dev/null
+++ b/src/master/master-settings.c
@@ -0,0 +1,806 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "env-util.h"
+#include "istream.h"
+#include "net.h"
+#include "str.h"
+#include "ipwd.h"
+#include "mkdir-parents.h"
+#include "safe-mkdir.h"
+#include "restrict-process-size.h"
+#include "settings-parser.h"
+#include "master-settings.h"
+
+#include <stddef.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+static bool master_settings_verify(void *_set, pool_t pool,
+ const char **error_r);
+
+extern const struct setting_parser_info service_setting_parser_info;
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct file_listener_settings)
+
+static const struct setting_define file_listener_setting_defines[] = {
+ DEF(STR, path),
+ DEF(UINT_OCT, mode),
+ DEF(STR, user),
+ DEF(STR, group),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct file_listener_settings file_listener_default_settings = {
+ .path = "",
+ .mode = 0600,
+ .user = "",
+ .group = "",
+};
+
+static const struct setting_parser_info file_listener_setting_parser_info = {
+ .defines = file_listener_setting_defines,
+ .defaults = &file_listener_default_settings,
+
+ .type_offset = offsetof(struct file_listener_settings, path),
+ .struct_size = sizeof(struct file_listener_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &service_setting_parser_info
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct inet_listener_settings)
+
+static const struct setting_define inet_listener_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, address),
+ DEF(IN_PORT, port),
+ DEF(BOOL, ssl),
+ DEF(BOOL, reuse_port),
+ DEF(BOOL, haproxy),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct inet_listener_settings inet_listener_default_settings = {
+ .name = "",
+ .address = "",
+ .port = 0,
+ .ssl = FALSE,
+ .reuse_port = FALSE,
+ .haproxy = FALSE
+};
+
+static const struct setting_parser_info inet_listener_setting_parser_info = {
+ .defines = inet_listener_setting_defines,
+ .defaults = &inet_listener_default_settings,
+
+ .type_offset = offsetof(struct inet_listener_settings, name),
+ .struct_size = sizeof(struct inet_listener_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &service_setting_parser_info
+};
+
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct service_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct service_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define service_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, protocol),
+ DEF(STR, type),
+ DEF(STR, executable),
+ DEF(STR, user),
+ DEF(STR, group),
+ DEF(STR, privileged_group),
+ DEF(STR, extra_groups),
+ DEF(STR, chroot),
+
+ DEF(BOOL, drop_priv_before_exec),
+
+ DEF(UINT, process_min_avail),
+ DEF(UINT, process_limit),
+ DEF(UINT, client_limit),
+ DEF(UINT, service_count),
+ DEF(TIME, idle_kill),
+ DEF(SIZE, vsz_limit),
+
+ DEFLIST_UNIQUE(unix_listeners, "unix_listener",
+ &file_listener_setting_parser_info),
+ DEFLIST_UNIQUE(fifo_listeners, "fifo_listener",
+ &file_listener_setting_parser_info),
+ DEFLIST_UNIQUE(inet_listeners, "inet_listener",
+ &inet_listener_setting_parser_info),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct service_settings service_default_settings = {
+ .name = "",
+ .protocol = "",
+ .type = "",
+ .executable = "",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+const struct setting_parser_info service_setting_parser_info = {
+ .defines = service_setting_defines,
+ .defaults = &service_default_settings,
+
+ .type_offset = offsetof(struct service_settings, name),
+ .struct_size = sizeof(struct service_settings),
+
+ .parent_offset = offsetof(struct service_settings, master_set),
+ .parent = &master_setting_parser_info
+};
+
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct master_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct master_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define master_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, state_dir),
+ DEF(STR, libexec_dir),
+ DEF(STR, instance_name),
+ DEF(STR, protocols),
+ DEF(STR, listen),
+ DEF(ENUM, ssl),
+ DEF(STR, default_internal_user),
+ DEF(STR, default_internal_group),
+ DEF(STR, default_login_user),
+ DEF(UINT, default_process_limit),
+ DEF(UINT, default_client_limit),
+ DEF(TIME, default_idle_kill),
+ DEF(SIZE, default_vsz_limit),
+
+ DEF(BOOL, version_ignore),
+
+ DEF(UINT, first_valid_uid),
+ DEF(UINT, last_valid_uid),
+ DEF(UINT, first_valid_gid),
+ DEF(UINT, last_valid_gid),
+
+ DEFLIST_UNIQUE(services, "service", &service_setting_parser_info),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct master_settings master_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .state_dir = PKG_STATEDIR,
+ .libexec_dir = PKG_LIBEXECDIR,
+ .instance_name = PACKAGE,
+ .protocols = "imap pop3 lmtp",
+ .listen = "*, ::",
+ .ssl = "yes:no:required",
+ .default_internal_user = "dovecot",
+ .default_internal_group = "dovecot",
+ .default_login_user = "dovenull",
+ .default_process_limit = 100,
+ .default_client_limit = 1000,
+ .default_idle_kill = 60,
+ .default_vsz_limit = 256*1024*1024,
+
+ .version_ignore = FALSE,
+
+ .first_valid_uid = 500,
+ .last_valid_uid = 0,
+ .first_valid_gid = 1,
+ .last_valid_gid = 0,
+
+#ifndef CONFIG_BINARY
+ .services = ARRAY_INIT
+#else
+ .services = { { &config_all_services_buf,
+ sizeof(struct service_settings *) } },
+#endif
+};
+
+const struct setting_parser_info master_setting_parser_info = {
+ .module_name = "master",
+ .defines = master_setting_defines,
+ .defaults = &master_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct master_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = master_settings_verify
+};
+
+/* <settings checks> */
+static void
+expand_user(const char **user, enum service_user_default *default_r,
+ const struct master_settings *set)
+{
+ /* $variable expansion is typically done by doveconf, but these
+ variables can come from built-in settings, so we need to expand
+ them here */
+ if (strcmp(*user, "$default_internal_user") == 0) {
+ *user = set->default_internal_user;
+ *default_r = SERVICE_USER_DEFAULT_INTERNAL;
+ } else if (strcmp(*user, "$default_login_user") == 0) {
+ *user = set->default_login_user;
+ *default_r = SERVICE_USER_DEFAULT_LOGIN;
+ } else {
+ *default_r = SERVICE_USER_DEFAULT_NONE;
+ }
+}
+
+static void
+expand_group(const char **group, const struct master_settings *set)
+{
+ /* $variable expansion is typically done by doveconf, but these
+ variables can come from built-in settings, so we need to expand
+ them here */
+ if (strcmp(*group, "$default_internal_group") == 0)
+ *group = set->default_internal_group;
+}
+
+static bool
+fix_file_listener_paths(ARRAY_TYPE(file_listener_settings) *l,
+ pool_t pool, const struct master_settings *master_set,
+ ARRAY_TYPE(const_string) *all_listeners,
+ const char **error_r)
+{
+ struct file_listener_settings *set;
+ size_t base_dir_len = strlen(master_set->base_dir);
+ enum service_user_default user_default;
+
+ if (!array_is_created(l))
+ return TRUE;
+
+ array_foreach_elem(l, set) {
+ if (set->path[0] == '\0') {
+ *error_r = "path must not be empty";
+ return FALSE;
+ }
+
+ expand_user(&set->user, &user_default, master_set);
+ expand_group(&set->group, master_set);
+ if (*set->path != '/') {
+ set->path = p_strconcat(pool, master_set->base_dir, "/",
+ set->path, NULL);
+ } else if (strncmp(set->path, master_set->base_dir,
+ base_dir_len) == 0 &&
+ set->path[base_dir_len] == '/') {
+ i_warning("You should remove base_dir prefix from "
+ "unix_listener: %s", set->path);
+ }
+ if (set->mode != 0)
+ array_push_back(all_listeners, &set->path);
+ }
+ return TRUE;
+}
+
+static void add_inet_listeners(ARRAY_TYPE(inet_listener_settings) *l,
+ ARRAY_TYPE(const_string) *all_listeners)
+{
+ struct inet_listener_settings *set;
+ const char *str;
+
+ if (!array_is_created(l))
+ return;
+
+ array_foreach_elem(l, set) {
+ if (set->port != 0) {
+ str = t_strdup_printf("%u:%s", set->port, set->address);
+ array_push_back(all_listeners, &str);
+ }
+ }
+}
+
+static bool master_settings_parse_type(struct service_settings *set,
+ const char **error_r)
+{
+ if (*set->type == '\0')
+ set->parsed_type = SERVICE_TYPE_UNKNOWN;
+ else if (strcmp(set->type, "log") == 0)
+ set->parsed_type = SERVICE_TYPE_LOG;
+ else if (strcmp(set->type, "config") == 0)
+ set->parsed_type = SERVICE_TYPE_CONFIG;
+ else if (strcmp(set->type, "anvil") == 0)
+ set->parsed_type = SERVICE_TYPE_ANVIL;
+ else if (strcmp(set->type, "login") == 0)
+ set->parsed_type = SERVICE_TYPE_LOGIN;
+ else if (strcmp(set->type, "startup") == 0)
+ set->parsed_type = SERVICE_TYPE_STARTUP;
+ else if (strcmp(set->type, "worker") == 0)
+ set->parsed_type = SERVICE_TYPE_WORKER;
+ else {
+ *error_r = t_strconcat("Unknown service type: ",
+ set->type, NULL);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void service_set_login_dump_core(struct service_settings *set)
+{
+ const char *p;
+
+ if (set->parsed_type != SERVICE_TYPE_LOGIN)
+ return;
+
+ p = strstr(set->executable, " -D");
+ if (p != NULL && (p[3] == '\0' || p[3] == ' '))
+ set->login_dump_core = TRUE;
+}
+
+static bool
+services_have_protocol(struct master_settings *set, const char *name)
+{
+ struct service_settings *service;
+
+ array_foreach_elem(&set->services, service) {
+ if (strcmp(service->protocol, name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+#ifdef CONFIG_BINARY
+static const struct service_settings *
+master_default_settings_get_service(const char *name)
+{
+ extern struct master_settings master_default_settings;
+ struct service_settings *set;
+
+ array_foreach_elem(&master_default_settings.services, set) {
+ if (strcmp(set->name, name) == 0)
+ return set;
+ }
+ return NULL;
+}
+#endif
+
+static unsigned int
+service_get_client_limit(struct master_settings *set, const char *name)
+{
+ struct service_settings *service;
+
+ array_foreach_elem(&set->services, service) {
+ if (strcmp(service->name, name) == 0) {
+ if (service->client_limit != 0)
+ return service->client_limit;
+ else
+ return set->default_client_limit;
+ }
+ }
+ return set->default_client_limit;
+}
+
+static bool
+master_settings_verify(void *_set, pool_t pool, const char **error_r)
+{
+ static bool warned_auth = FALSE, warned_anvil = FALSE;
+ struct master_settings *set = _set;
+ struct service_settings *const *services;
+ const char *const *strings;
+ ARRAY_TYPE(const_string) all_listeners;
+ struct passwd pw;
+ unsigned int i, j, count, client_limit, process_limit;
+ unsigned int max_auth_client_processes, max_anvil_client_processes;
+ string_t *max_auth_client_processes_reason = t_str_new(64);
+ string_t *max_anvil_client_processes_reason = t_str_new(64);
+ size_t len;
+#ifdef CONFIG_BINARY
+ const struct service_settings *default_service;
+#else
+ rlim_t fd_limit;
+ const char *max_client_limit_source = "default_client_limit";
+ unsigned int max_client_limit = set->default_client_limit;
+#endif
+
+ if (*set->listen == '\0') {
+ *error_r = "listen can't be set empty";
+ return FALSE;
+ }
+
+ len = strlen(set->base_dir);
+ if (len > 0 && set->base_dir[len-1] == '/') {
+ /* drop trailing '/' */
+ set->base_dir = p_strndup(pool, set->base_dir, len - 1);
+ }
+
+ if (set->last_valid_uid != 0 &&
+ set->first_valid_uid > set->last_valid_uid) {
+ *error_r = "first_valid_uid can't be larger than last_valid_uid";
+ return FALSE;
+ }
+ if (set->last_valid_gid != 0 &&
+ set->first_valid_gid > set->last_valid_gid) {
+ *error_r = "first_valid_gid can't be larger than last_valid_gid";
+ return FALSE;
+ }
+
+ if (i_getpwnam(set->default_login_user, &pw) == 0) {
+ *error_r = t_strdup_printf("default_login_user doesn't exist: %s",
+ set->default_login_user);
+ return FALSE;
+ }
+ if (i_getpwnam(set->default_internal_user, &pw) == 0) {
+ *error_r = t_strdup_printf("default_internal_user doesn't exist: %s",
+ set->default_internal_user);
+ return FALSE;
+ }
+
+ /* check that we have at least one service. the actual service
+ structure validity is checked later while creating them. */
+ if (!array_is_created(&set->services) ||
+ array_count(&set->services) == 0) {
+ *error_r = "No services defined";
+ return FALSE;
+ }
+ services = array_get(&set->services, &count);
+ for (i = 0; i < count; i++) {
+ struct service_settings *service = services[i];
+
+ if (*service->name == '\0') {
+ *error_r = t_strdup_printf(
+ "Service #%d is missing name", i);
+ return FALSE;
+ }
+ if (!master_settings_parse_type(service, error_r))
+ return FALSE;
+ for (j = 0; j < i; j++) {
+ if (strcmp(service->name, services[j]->name) == 0) {
+ *error_r = t_strdup_printf(
+ "Duplicate service name: %s",
+ service->name);
+ return FALSE;
+ }
+ }
+ expand_user(&service->user, &service->user_default, set);
+ expand_group(&service->extra_groups, set);
+ service_set_login_dump_core(service);
+ }
+ set->protocols_split = p_strsplit_spaces(pool, set->protocols, " ");
+ if (set->protocols_split[0] != NULL &&
+ strcmp(set->protocols_split[0], "none") == 0 &&
+ set->protocols_split[1] == NULL)
+ set->protocols_split[0] = NULL;
+
+ for (i = 0; set->protocols_split[i] != NULL; i++) {
+ if (!services_have_protocol(set, set->protocols_split[i])) {
+ *error_r = t_strdup_printf("protocols: "
+ "Unknown protocol: %s",
+ set->protocols_split[i]);
+ return FALSE;
+ }
+ }
+ t_array_init(&all_listeners, 64);
+ max_auth_client_processes = 0;
+ max_anvil_client_processes = 2; /* blocking, nonblocking pipes */
+ for (i = 0; i < count; i++) {
+ struct service_settings *service = services[i];
+
+ if (*service->protocol != '\0' &&
+ !str_array_find((const char **)set->protocols_split,
+ service->protocol)) {
+ /* protocol not enabled, ignore its settings */
+ continue;
+ }
+
+ if (*service->executable != '/' &&
+ *service->executable != '\0') {
+ service->executable =
+ p_strconcat(pool, set->libexec_dir, "/",
+ service->executable, NULL);
+ }
+ if (*service->chroot != '/' && *service->chroot != '\0') {
+ service->chroot =
+ p_strconcat(pool, set->base_dir, "/",
+ service->chroot, NULL);
+ }
+ if (service->drop_priv_before_exec &&
+ *service->chroot != '\0') {
+ *error_r = t_strdup_printf("service(%s): "
+ "drop_priv_before_exec=yes can't be "
+ "used with chroot", service->name);
+ return FALSE;
+ }
+ process_limit = service->process_limit;
+ if (process_limit == 0)
+ process_limit = set->default_process_limit;
+ if (service->process_min_avail > process_limit) {
+ *error_r = t_strdup_printf("service(%s): "
+ "process_min_avail is higher than process_limit",
+ service->name);
+ return FALSE;
+ }
+ if (service->vsz_limit < 1024*1024 && service->vsz_limit != 0) {
+ *error_r = t_strdup_printf("service(%s): "
+ "vsz_limit is too low", service->name);
+ return FALSE;
+ }
+
+#ifdef CONFIG_BINARY
+ default_service =
+ master_default_settings_get_service(service->name);
+ if (default_service != NULL &&
+ default_service->process_limit_1 && process_limit > 1) {
+ *error_r = t_strdup_printf("service(%s): "
+ "process_limit must be 1", service->name);
+ return FALSE;
+ }
+#else
+ if (max_client_limit < service->client_limit) {
+ max_client_limit = service->client_limit;
+ max_client_limit_source = t_strdup_printf(
+ "service %s { client_limit }", service->name);
+ }
+#endif
+
+ if (*service->protocol != '\0') {
+ /* each imap/pop3/lmtp process can use up a connection,
+ although if service_count=1 it's only temporary.
+ imap-hibernate doesn't do any auth lookups. */
+ if ((service->service_count != 1 ||
+ strcmp(service->type, "login") == 0) &&
+ strcmp(service->name, "imap-hibernate") != 0) {
+ str_printfa(max_auth_client_processes_reason,
+ " + service %s { process_limit=%u }",
+ service->name, process_limit);
+ max_auth_client_processes += process_limit;
+ }
+ }
+ if (strcmp(service->type, "login") == 0 ||
+ strcmp(service->name, "auth") == 0) {
+ max_anvil_client_processes += process_limit;
+ str_printfa(max_anvil_client_processes_reason,
+ " + service %s { process_limit=%u }",
+ service->name, process_limit);
+ }
+
+ if (!fix_file_listener_paths(&service->unix_listeners, pool,
+ set, &all_listeners, error_r)) {
+ *error_r = t_strdup_printf("service(%s): unix_listener: %s",
+ service->name, *error_r);
+ return FALSE;
+ }
+ if (!fix_file_listener_paths(&service->fifo_listeners, pool,
+ set, &all_listeners, error_r)) {
+ *error_r = t_strdup_printf("service(%s): fifo_listener: %s",
+ service->name, *error_r);
+ return FALSE;
+ }
+ add_inet_listeners(&service->inet_listeners, &all_listeners);
+ }
+
+ client_limit = service_get_client_limit(set, "auth");
+ if (client_limit < max_auth_client_processes && !warned_auth) {
+ warned_auth = TRUE;
+ str_delete(max_auth_client_processes_reason, 0, 3);
+ i_warning("service auth { client_limit=%u } is lower than "
+ "required under max. load (%u). "
+ "Counted for protocol services with service_count != 1: %s",
+ client_limit, max_auth_client_processes,
+ str_c(max_auth_client_processes_reason));
+ }
+
+ client_limit = service_get_client_limit(set, "anvil");
+ if (client_limit < max_anvil_client_processes && !warned_anvil) {
+ warned_anvil = TRUE;
+ str_delete(max_anvil_client_processes_reason, 0, 3);
+ i_warning("service anvil { client_limit=%u } is lower than "
+ "required under max. load (%u). Counted with: %s",
+ client_limit, max_anvil_client_processes,
+ str_c(max_anvil_client_processes_reason));
+ }
+#ifndef CONFIG_BINARY
+ if (restrict_get_fd_limit(&fd_limit) == 0 &&
+ fd_limit < (rlim_t)max_client_limit) {
+ i_warning("fd limit (ulimit -n) is lower than required "
+ "under max. load (%u < %u), because of %s",
+ (unsigned int)fd_limit, max_client_limit,
+ max_client_limit_source);
+ }
+#endif
+
+ /* check for duplicate listeners */
+ array_sort(&all_listeners, i_strcmp_p);
+ strings = array_get(&all_listeners, &count);
+ for (i = 1; i < count; i++) {
+ if (strcmp(strings[i-1], strings[i]) == 0) {
+ *error_r = t_strdup_printf("duplicate listener: %s",
+ strings[i]);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+/* </settings checks> */
+
+static bool
+login_want_core_dumps(const struct master_settings *set, gid_t *gid_r)
+{
+ struct service_settings *service;
+ const char *error;
+ bool cores = FALSE;
+ uid_t uid;
+
+ *gid_r = (gid_t)-1;
+
+ array_foreach_elem(&set->services, service) {
+ if (service->parsed_type == SERVICE_TYPE_LOGIN) {
+ if (service->login_dump_core)
+ cores = TRUE;
+ (void)get_uidgid(service->user, &uid, gid_r, &error);
+ if (*service->group != '\0')
+ (void)get_gid(service->group, gid_r, &error);
+ }
+ }
+ return cores;
+}
+
+static bool
+settings_have_auth_unix_listeners_in(const struct master_settings *set,
+ const char *dir)
+{
+ struct service_settings *service;
+ struct file_listener_settings *u;
+ size_t dir_len = strlen(dir);
+
+ array_foreach_elem(&set->services, service) {
+ if (array_is_created(&service->unix_listeners)) {
+ array_foreach_elem(&service->unix_listeners, u) {
+ if (strncmp(u->path, dir, dir_len) == 0 &&
+ u->path[dir_len] == '/')
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static void unlink_sockets(const char *path, const char *prefix)
+{
+ DIR *dirp;
+ struct dirent *dp;
+ struct stat st;
+ string_t *str;
+ size_t prefix_len;
+
+ dirp = opendir(path);
+ if (dirp == NULL) {
+ i_error("opendir(%s) failed: %m", path);
+ return;
+ }
+
+ prefix_len = strlen(prefix);
+ str = t_str_new(256);
+ while ((dp = readdir(dirp)) != NULL) {
+ if (dp->d_name[0] == '.')
+ continue;
+
+ if (strncmp(dp->d_name, prefix, prefix_len) != 0)
+ continue;
+
+ str_truncate(str, 0);
+ str_printfa(str, "%s/%s", path, dp->d_name);
+ if (lstat(str_c(str), &st) < 0) {
+ if (errno != ENOENT)
+ i_error("lstat(%s) failed: %m", str_c(str));
+ continue;
+ }
+ if (!S_ISSOCK(st.st_mode))
+ continue;
+
+ /* try to avoid unlinking sockets if someone's already
+ listening in them. do this only at startup, because
+ when SIGHUPing a child process might catch the new
+ connection before it notices that it's supposed
+ to die. */
+ if (!startup_finished) {
+ int fd = net_connect_unix(str_c(str));
+ if (fd != -1 || errno != ECONNREFUSED) {
+ i_fatal("Dovecot is already running? "
+ "Socket already exists: %s",
+ str_c(str));
+ }
+ }
+
+ i_unlink_if_exists(str_c(str));
+ }
+ (void)closedir(dirp);
+}
+
+static void
+mkdir_login_dir(const struct master_settings *set, const char *login_dir)
+{
+ mode_t mode;
+ gid_t gid;
+
+ if (settings_have_auth_unix_listeners_in(set, login_dir)) {
+ /* we are not using external authentication, so make sure the
+ login directory exists with correct permissions and it's
+ empty. with external auth we wouldn't want to delete
+ existing sockets or break the permissions required by the
+ auth server. */
+ mode = login_want_core_dumps(set, &gid) ? 0770 : 0750;
+ if (safe_mkdir(login_dir, mode, master_uid, gid) == 0) {
+ i_warning("Corrected permissions for login directory "
+ "%s", login_dir);
+ }
+
+ unlink_sockets(login_dir, "");
+ } else {
+ /* still make sure that login directory exists */
+ if (mkdir(login_dir, 0755) < 0 && errno != EEXIST)
+ i_fatal("mkdir(%s) failed: %m", login_dir);
+ }
+}
+
+void master_settings_do_fixes(const struct master_settings *set)
+{
+ const char *empty_dir;
+ struct stat st;
+
+ /* since base dir is under /var/run by default, it may have been
+ deleted. */
+ if (mkdir_parents(set->base_dir, 0755) < 0 && errno != EEXIST)
+ i_fatal("mkdir(%s) failed: %m", set->base_dir);
+ /* allow base_dir to be a symlink, so don't use lstat() */
+ if (stat(set->base_dir, &st) < 0)
+ i_fatal("stat(%s) failed: %m", set->base_dir);
+ if (!S_ISDIR(st.st_mode))
+ i_fatal("%s is not a directory", set->base_dir);
+ if ((st.st_mode & 0755) != 0755) {
+ i_warning("Fixing permissions of %s to be world-readable",
+ set->base_dir);
+ if (chmod(set->base_dir, 0755) < 0)
+ i_error("chmod(%s) failed: %m", set->base_dir);
+ }
+
+ /* Make sure our permanent state directory exists */
+ if (mkdir_parents(set->state_dir, 0755) < 0 && errno != EEXIST)
+ i_fatal("mkdir(%s) failed: %m", set->state_dir);
+
+ mkdir_login_dir(set, t_strconcat(set->base_dir, "/login", NULL));
+ mkdir_login_dir(set, t_strconcat(set->base_dir, "/token-login", NULL));
+
+ empty_dir = t_strconcat(set->base_dir, "/empty", NULL);
+ if (safe_mkdir(empty_dir, 0755, master_uid, getegid()) == 0) {
+ i_warning("Corrected permissions for empty directory "
+ "%s", empty_dir);
+ }
+}
diff --git a/src/master/master-settings.h b/src/master/master-settings.h
new file mode 100644
index 0000000..1096d15
--- /dev/null
+++ b/src/master/master-settings.h
@@ -0,0 +1,35 @@
+#ifndef MASTER_SETTINGS_H
+#define MASTER_SETTINGS_H
+
+#include "service-settings.h"
+
+struct master_settings {
+ const char *base_dir;
+ const char *state_dir;
+ const char *libexec_dir;
+ const char *instance_name;
+ const char *protocols;
+ const char *listen;
+ const char *ssl;
+ const char *default_internal_user;
+ const char *default_internal_group;
+ const char *default_login_user;
+ unsigned int default_process_limit;
+ unsigned int default_client_limit;
+ unsigned int default_idle_kill;
+ uoff_t default_vsz_limit;
+
+ bool version_ignore;
+
+ unsigned int first_valid_uid, last_valid_uid;
+ unsigned int first_valid_gid, last_valid_gid;
+
+ ARRAY_TYPE(service_settings) services;
+ char **protocols_split;
+};
+
+extern const struct setting_parser_info master_setting_parser_info;
+
+void master_settings_do_fixes(const struct master_settings *set);
+
+#endif
diff --git a/src/master/service-anvil.c b/src/master/service-anvil.c
new file mode 100644
index 0000000..f5f8632
--- /dev/null
+++ b/src/master/service-anvil.c
@@ -0,0 +1,202 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "ioloop.h"
+#include "fdpass.h"
+#include "service.h"
+#include "service-process.h"
+#include "service-process-notify.h"
+#include "service-anvil.h"
+
+#include <unistd.h>
+
+#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n"
+
+struct service_anvil_global *service_anvil_global;
+
+static void
+service_list_anvil_discard_input_stop(struct service_anvil_global *anvil)
+{
+ if (anvil->io_blocking != NULL) {
+ io_remove(&anvil->io_blocking);
+ io_remove(&anvil->io_nonblocking);
+ }
+}
+
+static void anvil_input_fd_discard(struct service_anvil_global *anvil, int fd)
+{
+ char buf[1024];
+ ssize_t ret;
+
+ ret = read(fd, buf, sizeof(buf));
+ if (ret <= 0) {
+ i_error("read(anvil fd) failed: %m");
+ service_list_anvil_discard_input_stop(anvil);
+ }
+}
+
+static void anvil_input_blocking_discard(struct service_anvil_global *anvil)
+{
+ anvil_input_fd_discard(anvil, anvil->blocking_fd[0]);
+}
+
+static void anvil_input_nonblocking_discard(struct service_anvil_global *anvil)
+{
+ anvil_input_fd_discard(anvil, anvil->nonblocking_fd[0]);
+}
+
+static void service_list_anvil_discard_input(struct service_anvil_global *anvil)
+{
+ if (anvil->io_blocking != NULL)
+ return;
+
+ anvil->io_blocking = io_add(anvil->blocking_fd[0], IO_READ,
+ anvil_input_blocking_discard, anvil);
+ anvil->io_nonblocking = io_add(anvil->nonblocking_fd[0], IO_READ,
+ anvil_input_nonblocking_discard, anvil);
+}
+
+static int anvil_send_handshake(int fd, const char **error_r)
+{
+ ssize_t ret;
+
+ ret = write(fd, ANVIL_HANDSHAKE, strlen(ANVIL_HANDSHAKE));
+ if (ret < 0) {
+ *error_r = t_strdup_printf("write(anvil) failed: %m");
+ return -1;
+ }
+ if (ret == 0) {
+ *error_r = t_strdup_printf("write(anvil) returned EOF");
+ return -1;
+ }
+ /* this is a pipe, it either wrote all of it or nothing */
+ i_assert((size_t)ret == strlen(ANVIL_HANDSHAKE));
+ return 0;
+}
+
+static int
+service_process_write_anvil_kill(int fd, struct service_process *process)
+{
+ const char *data;
+
+ data = t_strdup_printf("KILL\t%s\n", dec2str(process->pid));
+ if (write(fd, data, strlen(data)) < 0) {
+ if (errno != EAGAIN)
+ i_error("write(anvil process) failed: %m");
+ return -1;
+ }
+ return 0;
+}
+
+void service_anvil_monitor_start(struct service_list *service_list)
+{
+ struct service *service;
+
+ if (service_anvil_global->process_count == 0)
+ service_list_anvil_discard_input(service_anvil_global);
+ else {
+ service = service_lookup_type(service_list, SERVICE_TYPE_ANVIL);
+ (void)service_process_create(service);
+ }
+}
+
+void service_anvil_process_created(struct service_process *process)
+{
+ struct service_anvil_global *anvil = service_anvil_global;
+ const char *error;
+
+ service_anvil_global->pid = process->pid;
+ service_anvil_global->uid = process->uid;
+ service_anvil_global->process_count++;
+ service_list_anvil_discard_input_stop(anvil);
+
+ if (anvil_send_handshake(anvil->blocking_fd[1], &error) < 0 ||
+ anvil_send_handshake(anvil->nonblocking_fd[1], &error) < 0)
+ service_error(process->service, "%s", error);
+}
+
+void service_anvil_process_destroyed(struct service_process *process)
+{
+ i_assert(service_anvil_global->process_count > 0);
+ if (--service_anvil_global->process_count == 0)
+ service_list_anvil_discard_input(service_anvil_global);
+
+ if (service_anvil_global->pid == process->pid)
+ service_anvil_global->pid = 0;
+ service_anvil_global->restarted = TRUE;
+}
+
+void service_anvil_send_log_fd(void)
+{
+ ssize_t ret;
+ char b = 0;
+
+ if (service_anvil_global->process_count == 0)
+ return;
+
+ ret = fd_send(service_anvil_global->log_fdpass_fd[1],
+ services->anvil->log_fd[1], &b, 1);
+ if (ret < 0)
+ i_error("fd_send(anvil log fd) failed: %m");
+ else if (ret == 0)
+ i_error("fd_send(anvil log fd) failed: disconnected");
+}
+
+void service_anvil_global_init(void)
+{
+ struct service_anvil_global *anvil;
+
+ anvil = i_new(struct service_anvil_global, 1);
+ if (pipe(anvil->status_fd) < 0)
+ i_fatal("pipe() failed: %m");
+ if (pipe(anvil->blocking_fd) < 0)
+ i_fatal("pipe() failed: %m");
+ if (pipe(anvil->nonblocking_fd) < 0)
+ i_fatal("pipe() failed: %m");
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, anvil->log_fdpass_fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ fd_set_nonblock(anvil->status_fd[0], TRUE);
+ fd_set_nonblock(anvil->status_fd[1], TRUE);
+ fd_set_nonblock(anvil->nonblocking_fd[1], TRUE);
+
+ fd_close_on_exec(anvil->status_fd[0], TRUE);
+ fd_close_on_exec(anvil->status_fd[1], TRUE);
+ fd_close_on_exec(anvil->blocking_fd[0], TRUE);
+ fd_close_on_exec(anvil->blocking_fd[1], TRUE);
+ fd_close_on_exec(anvil->nonblocking_fd[0], TRUE);
+ fd_close_on_exec(anvil->nonblocking_fd[1], TRUE);
+ fd_close_on_exec(anvil->log_fdpass_fd[0], TRUE);
+ fd_close_on_exec(anvil->log_fdpass_fd[1], TRUE);
+
+ anvil->kills =
+ service_process_notify_init(anvil->nonblocking_fd[1],
+ service_process_write_anvil_kill);
+ service_anvil_global = anvil;
+}
+
+void service_anvil_global_deinit(void)
+{
+ struct service_anvil_global *anvil = service_anvil_global;
+
+ service_list_anvil_discard_input_stop(anvil);
+ service_process_notify_deinit(&anvil->kills);
+ if (close(anvil->log_fdpass_fd[0]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->log_fdpass_fd[1]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->blocking_fd[0]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->blocking_fd[1]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->nonblocking_fd[0]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->nonblocking_fd[1]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->status_fd[0]) < 0)
+ i_error("close(anvil) failed: %m");
+ if (close(anvil->status_fd[1]) < 0)
+ i_error("close(anvil) failed: %m");
+ i_free(anvil);
+
+ service_anvil_global = NULL;
+}
diff --git a/src/master/service-anvil.h b/src/master/service-anvil.h
new file mode 100644
index 0000000..b749134
--- /dev/null
+++ b/src/master/service-anvil.h
@@ -0,0 +1,36 @@
+#ifndef SERVICE_ANVIL_H
+#define SERVICE_ANVIL_H
+
+struct service_anvil_global {
+ pid_t pid;
+ unsigned int uid;
+
+ int status_fd[2];
+ /* passed to child processes */
+ int blocking_fd[2];
+ /* used by master process to notify about dying processes */
+ int nonblocking_fd[2];
+ /* master process sends new log fds to anvil via this unix socket */
+ int log_fdpass_fd[2];
+
+ struct service_process_notify *kills;
+ struct io *io_blocking, *io_nonblocking;
+
+ unsigned int process_count;
+ /* anvil crashed and we're now restarting it */
+ bool restarted;
+};
+
+extern struct service_anvil_global *service_anvil_global;
+
+void service_anvil_monitor_start(struct service_list *service_list);
+
+void service_anvil_process_created(struct service_process *process);
+void service_anvil_process_destroyed(struct service_process *process);
+
+void service_anvil_send_log_fd(void);
+
+void service_anvil_global_init(void);
+void service_anvil_global_deinit(void);
+
+#endif
diff --git a/src/master/service-listen.c b/src/master/service-listen.c
new file mode 100644
index 0000000..8237aa0
--- /dev/null
+++ b/src/master/service-listen.c
@@ -0,0 +1,492 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#ifdef HAVE_LIBSYSTEMD
+# include <systemd/sd-daemon.h>
+#endif
+#include "service.h"
+#include "service-listen.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+
+#define MIN_BACKLOG 4
+
+static unsigned int service_get_backlog(struct service *service)
+{
+ unsigned int backlog;
+
+ i_assert(service->process_limit > 0);
+ i_assert(service->client_limit > 0);
+
+ /* as unlikely as it is, avoid overflows */
+ if (service->client_limit > INT_MAX / service->process_limit)
+ backlog = INT_MAX;
+ else
+ backlog = service->process_limit * service->client_limit;
+ return I_MAX(backlog, MIN_BACKLOG);
+}
+
+static int
+service_file_chown(const struct service_listener *l)
+{
+ uid_t uid = l->set.fileset.uid;
+ uid_t gid = l->set.fileset.gid;
+
+ if ((uid == (uid_t)-1 || uid == master_uid) &&
+ (gid == (gid_t)-1 || gid == master_gid))
+ return 0;
+
+ if (chown(l->set.fileset.set->path, uid, gid) < 0) {
+ service_error(l->service, "chown(%s, %lld, %lld) failed: %m",
+ l->set.fileset.set->path,
+ (long long)uid, (long long)gid);
+ return -1;
+ }
+ return 0;
+}
+
+static int service_unix_listener_listen(struct service_listener *l)
+{
+ struct service *service = l->service;
+ const struct file_listener_settings *set = l->set.fileset.set;
+ mode_t old_umask;
+ int fd, i;
+
+ old_umask = umask((set->mode ^ 0777) & 0777);
+ for (i = 0;; i++) {
+ fd = net_listen_unix(set->path, service_get_backlog(service));
+ if (fd != -1)
+ break;
+
+ if (errno == EISDIR || errno == ENOENT) {
+ /* looks like the path doesn't exist. */
+ return 0;
+ }
+
+ if (errno != EADDRINUSE) {
+ service_error(service, "net_listen_unix(%s) failed: %m",
+ set->path);
+ return -1;
+ }
+
+ /* already in use - see if it really exists.
+ after 3 times just fail here. */
+ fd = net_connect_unix(set->path);
+ if (fd != -1 || errno != ECONNREFUSED || i >= 3) {
+ i_close_fd(&fd);
+ service_error(service, "Socket already exists: %s",
+ set->path);
+ return 0;
+ }
+
+ /* delete and try again */
+ if (unlink(set->path) < 0 && errno != ENOENT) {
+ service_error(service, "unlink(%s) failed: %m",
+ set->path);
+ return -1;
+ }
+ }
+ umask(old_umask);
+
+ i_assert(fd != -1);
+
+ if (service_file_chown(l) < 0) {
+ i_close_fd(&fd);
+ return -1;
+ }
+ net_set_nonblock(fd, TRUE);
+ fd_close_on_exec(fd, TRUE);
+
+ l->fd = fd;
+ return 1;
+}
+
+static int service_fifo_listener_listen(struct service_listener *l)
+{
+ struct service *service = l->service;
+ const struct file_listener_settings *set = l->set.fileset.set;
+ unsigned int i;
+ mode_t old_umask;
+ int fd, ret;
+
+ for (i = 0;; i++) {
+ old_umask = umask((set->mode ^ 0777) & 0777);
+ ret = mkfifo(set->path, set->mode);
+ umask(old_umask);
+
+ if (ret == 0)
+ break;
+ if (ret < 0 && (errno != EEXIST || i == 1)) {
+ service_error(service, "mkfifo(%s) failed: %m",
+ set->path);
+ return -1;
+ }
+ if (unlink(set->path) < 0) {
+ service_error(service, "unlink(%s) failed: %m",
+ set->path);
+ return -1;
+ }
+ }
+ if (service_file_chown(l) < 0)
+ return -1;
+
+ /* open as RDWR, so that even if the last writer closes,
+ we won't get EOF errors */
+ fd = open(set->path, O_RDWR | O_NONBLOCK);
+ if (fd == -1) {
+ service_error(service, "open(%s) failed: %m", set->path);
+ return -1;
+ }
+
+ fd_close_on_exec(fd, TRUE);
+
+ l->fd = fd;
+ return 1;
+}
+
+#ifdef HAVE_LIBSYSTEMD
+static int
+systemd_listen_fd(const struct ip_addr *ip, in_port_t port, int *fd_r)
+{
+ static int sd_fds = -1;
+ int fd, fd_max;
+
+ if (sd_fds < 0) {
+ sd_fds = sd_listen_fds(0);
+ if (sd_fds < 0) {
+ i_error("sd_listen_fds() failed: %s", strerror(-sd_fds));
+ return -1;
+ }
+ }
+
+ fd_max = SD_LISTEN_FDS_START + sd_fds - 1;
+ for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) {
+ if (sd_is_socket_inet(fd, ip->family, SOCK_STREAM, 1, port) > 0) {
+ *fd_r = fd;
+ return 0;
+ }
+ }
+ /* when systemd didn't provide a usable socket,
+ fall back to the regular socket creation code */
+ *fd_r = -1;
+ return 0;
+}
+#endif
+
+static int service_inet_listener_listen(struct service_listener *l)
+{
+ struct service *service = l->service;
+ enum net_listen_flags flags = 0;
+ const struct inet_listener_settings *set = l->set.inetset.set;
+ in_port_t port = set->port;
+ int fd;
+
+#ifdef HAVE_LIBSYSTEMD
+ if (systemd_listen_fd(&l->set.inetset.ip, port, &fd) < 0)
+ return -1;
+
+ if (fd == -1)
+#endif
+ {
+ if (set->reuse_port)
+ flags |= NET_LISTEN_FLAG_REUSEPORT;
+ fd = net_listen_full(&l->set.inetset.ip, &port, &flags,
+ service_get_backlog(service));
+ if (fd < 0) {
+ service_error(service, "listen(%s, %u) failed: %m",
+ l->inet_address, set->port);
+ return errno == EADDRINUSE ? 0 : -1;
+ }
+ l->reuse_port = (flags & NET_LISTEN_FLAG_REUSEPORT) != 0;
+ }
+ net_set_nonblock(fd, TRUE);
+ fd_close_on_exec(fd, TRUE);
+
+ l->fd = fd;
+ return 1;
+}
+
+int service_listener_listen(struct service_listener *l)
+{
+ switch (l->type) {
+ case SERVICE_LISTENER_UNIX:
+ return service_unix_listener_listen(l);
+ case SERVICE_LISTENER_FIFO:
+ return service_fifo_listener_listen(l);
+ case SERVICE_LISTENER_INET:
+ return service_inet_listener_listen(l);
+ }
+ i_unreached();
+}
+
+static int service_listen(struct service *service)
+{
+ struct service_listener *l;
+ int ret = 1, ret2 = 0;
+
+ array_foreach_elem(&service->listeners, l) {
+ if (l->fd != -1)
+ continue;
+
+ ret2 = service_listener_listen(l);
+ if (ret2 < ret)
+ ret = ret2;
+ }
+ return ret;
+}
+
+#ifdef HAVE_LIBSYSTEMD
+static int get_socket_info(int fd, sa_family_t *family_r, in_port_t *port_r)
+{
+ union sockaddr_union {
+ struct sockaddr sa;
+ struct sockaddr_in in4;
+ struct sockaddr_in6 in6;
+ } sockaddr;
+ socklen_t l;
+
+ if (port_r != NULL)
+ *port_r = 0;
+ if (family_r != NULL)
+ *family_r = AF_UNSPEC;
+
+ i_zero(&sockaddr);
+ l = sizeof(sockaddr);
+
+ if (getsockname(fd, &sockaddr.sa, &l) < 0)
+ return -errno;
+
+ if (family_r != NULL)
+ *family_r = sockaddr.sa.sa_family;
+ if (port_r != NULL) {
+ if (sockaddr.sa.sa_family == AF_INET) {
+ if (l < sizeof(struct sockaddr_in))
+ return -EINVAL;
+
+ *port_r = ntohs(sockaddr.in4.sin_port);
+ } else {
+ if (l < sizeof(struct sockaddr_in6))
+ return -EINVAL;
+
+ *port_r = ntohs(sockaddr.in6.sin6_port);
+ }
+ }
+ return 0;
+}
+
+static int services_verify_systemd(struct service_list *service_list)
+{
+ struct service *service;
+ static int sd_fds = -1;
+ int fd, fd_max;
+
+ if (sd_fds < 0) {
+ sd_fds = sd_listen_fds(0);
+ if (sd_fds == -1) {
+ i_error("sd_listen_fds() failed: %m");
+ return -1;
+ }
+ }
+
+ fd_max = SD_LISTEN_FDS_START + sd_fds - 1;
+ for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) {
+ if (sd_is_socket_inet(fd, 0, SOCK_STREAM, 1, 0) > 0) {
+ bool found = FALSE;
+ in_port_t port;
+ sa_family_t family;
+ get_socket_info(fd, &family, &port);
+
+ array_foreach_elem(&service_list->services, service) {
+ struct service_listener *l;
+
+ array_foreach_elem(&service->listeners, l) {
+ if (l->type != SERVICE_LISTENER_INET)
+ continue;
+ if (l->set.inetset.set->port == port &&
+ l->set.inetset.ip.family == family) {
+ found = TRUE;
+ break;
+ }
+ }
+ if (found)
+ break;
+ }
+ if (!found) {
+ i_error("systemd listens on port %d, "
+ "but it's not configured in Dovecot. "
+ "Closing.", port);
+ if (shutdown(fd, SHUT_RDWR) < 0 &&
+ errno != ENOTCONN)
+ i_error("shutdown(%d) failed: %m", fd);
+ if (dup2(dev_null_fd, fd) < 0)
+ i_error("dup2(%d, %d) failed: %m",
+ dev_null_fd, fd);
+ }
+ }
+ }
+ return 0;
+}
+#endif
+
+static int services_listen_master(struct service_list *service_list)
+{
+ const char *path;
+ mode_t old_umask;
+
+ if (service_list->master_fd != -1)
+ return 1;
+
+ path = t_strdup_printf("%s/master", service_list->set->base_dir);
+ old_umask = umask(0600 ^ 0777);
+ service_list->master_fd = net_listen_unix(path, 16);
+ if (service_list->master_fd == -1 && errno == EADDRINUSE) {
+ /* already in use. all the other sockets were fine, so just
+ delete this and retry. */
+ i_unlink_if_exists(path);
+ service_list->master_fd = net_listen_unix(path, 16);
+ }
+ umask(old_umask);
+
+ if (service_list->master_fd == -1) {
+ i_error("net_listen_unix(%s) failed: %m", path);
+ return 0;
+ }
+ fd_close_on_exec(service_list->master_fd, TRUE);
+ return 1;
+}
+
+int services_listen(struct service_list *service_list)
+{
+ struct service *service;
+ int ret = 1, ret2;
+
+ array_foreach_elem(&service_list->services, service) {
+ ret2 = service_listen(service);
+ if (ret2 < ret)
+ ret = ret2;
+ }
+ /* reloading config wants to continue even when we're returning 0. */
+ if (ret >= 0) {
+ ret2 = services_listen_master(service_list);
+ if (ret2 < ret)
+ ret = ret2;
+ }
+
+#ifdef HAVE_LIBSYSTEMD
+ if (ret > 0)
+ services_verify_systemd(service_list);
+#endif
+ return ret;
+}
+
+static bool listener_equals(const struct service_listener *l1,
+ const struct service_listener *l2)
+{
+ if (l1->type != l2->type)
+ return FALSE;
+
+ switch (l1->type) {
+ case SERVICE_LISTENER_UNIX:
+ case SERVICE_LISTENER_FIFO:
+ /* We could just keep using the same listener, but it's more
+ likely to cause problems if old process accepts a connection
+ before it knows that it should die. So just always unlink
+ and recreate unix/fifo listeners. */
+ return FALSE;
+ case SERVICE_LISTENER_INET:
+ if (memcmp(&l1->set.inetset.ip, &l2->set.inetset.ip,
+ sizeof(l1->set.inetset.ip)) != 0)
+ return FALSE;
+ if (l1->set.inetset.set->port != l2->set.inetset.set->port)
+ return FALSE;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int services_listen_using(struct service_list *new_service_list,
+ struct service_list *old_service_list)
+{
+ struct service *const *services, *old_service, *new_service;
+ ARRAY(struct service_listener *) new_listeners_arr;
+ ARRAY(struct service_listener *) old_listeners_arr;
+ struct service_listener *const *new_listeners, *const *old_listeners;
+ unsigned int i, j, count, new_count, old_count;
+
+ /* copy master listener */
+ new_service_list->master_fd = old_service_list->master_fd;
+ old_service_list->master_fd = -1;
+
+ /* rescue anvil's UNIX socket listener */
+ new_service = service_lookup_type(new_service_list, SERVICE_TYPE_ANVIL);
+ old_service = service_lookup_type(old_service_list, SERVICE_TYPE_ANVIL);
+ if (old_service != NULL && new_service != NULL) {
+ new_listeners = array_get(&new_service->listeners, &new_count);
+ old_listeners = array_get(&old_service->listeners, &old_count);
+ for (i = 0; i < old_count && i < new_count; i++) {
+ if (new_listeners[i]->type != old_listeners[i]->type)
+ break;
+ }
+ if (i != new_count && i != old_count) {
+ i_error("Can't change anvil's listeners on the fly");
+ return -1;
+ }
+ for (i = 0; i < new_count; i++) {
+ new_listeners[i]->fd = old_listeners[i]->fd;
+ old_listeners[i]->fd = -1;
+ }
+ }
+
+ /* first create an arrays of all listeners to make things easier */
+ t_array_init(&new_listeners_arr, 64);
+ services = array_get(&new_service_list->services, &count);
+ for (i = 0; i < count; i++)
+ array_append_array(&new_listeners_arr, &services[i]->listeners);
+
+ t_array_init(&old_listeners_arr, 64);
+ services = array_get(&old_service_list->services, &count);
+ for (i = 0; i < count; i++)
+ array_append_array(&old_listeners_arr, &services[i]->listeners);
+
+ /* then start moving fds */
+ new_listeners = array_get(&new_listeners_arr, &new_count);
+ old_listeners = array_get(&old_listeners_arr, &old_count);
+
+ for (i = 0; i < new_count; i++) {
+ for (j = 0; j < old_count; j++) {
+ if (old_listeners[j]->fd != -1 &&
+ listener_equals(new_listeners[i],
+ old_listeners[j])) {
+ new_listeners[i]->fd = old_listeners[j]->fd;
+ old_listeners[j]->fd = -1;
+ break;
+ }
+ }
+ }
+
+ /* close what's left */
+ for (j = 0; j < old_count; j++) {
+ if (old_listeners[j]->fd == -1)
+ continue;
+
+ i_close_fd(&old_listeners[j]->fd);
+ switch (old_listeners[j]->type) {
+ case SERVICE_LISTENER_UNIX:
+ case SERVICE_LISTENER_FIFO: {
+ i_unlink(old_listeners[j]->set.fileset.set->path);
+ break;
+ }
+ case SERVICE_LISTENER_INET:
+ break;
+ }
+ }
+
+ /* and let services_listen() deal with the remaining fds */
+ return services_listen(new_service_list);
+}
diff --git a/src/master/service-listen.h b/src/master/service-listen.h
new file mode 100644
index 0000000..ffd88bd
--- /dev/null
+++ b/src/master/service-listen.h
@@ -0,0 +1,18 @@
+#ifndef SERVICE_LISTEN_H
+#define SERVICE_LISTEN_H
+
+/* Start listening in all services. Returns -1 for fatal failures,
+ 0 if some of the addresses are already being used or path for
+ unix socket was lost, 1 if all is ok. It's safe to call this function
+ multiple times. */
+int services_listen(struct service_list *service_list);
+
+/* Move common listener fds from old_services to new_services, close those
+ that aren't needed anymore and finally call services_listen() to add
+ missing listeners. */
+int services_listen_using(struct service_list *new_service_list,
+ struct service_list *old_service_list);
+
+int service_listener_listen(struct service_listener *l);
+
+#endif
diff --git a/src/master/service-log.c b/src/master/service-log.c
new file mode 100644
index 0000000..f5af931
--- /dev/null
+++ b/src/master/service-log.c
@@ -0,0 +1,169 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "aqueue.h"
+#include "hash.h"
+#include "ioloop.h"
+#include "service.h"
+#include "service-process.h"
+#include "service-process-notify.h"
+#include "service-anvil.h"
+#include "service-log.h"
+
+#include <unistd.h>
+
+static int service_log_fds_init(const char *log_prefix, int log_fd[2],
+ buffer_t *handshake_buf)
+{
+ struct log_service_handshake handshake;
+ ssize_t ret;
+
+ i_assert(log_fd[0] == -1);
+
+ if (pipe(log_fd) < 0) {
+ i_error("pipe() failed: %m");
+ return -1;
+ }
+ fd_close_on_exec(log_fd[0], TRUE);
+ fd_close_on_exec(log_fd[1], TRUE);
+
+ i_zero(&handshake);
+ handshake.log_magic = MASTER_LOG_MAGIC;
+ handshake.prefix_len = strlen(log_prefix);
+
+ buffer_set_used_size(handshake_buf, 0);
+ buffer_append(handshake_buf, &handshake, sizeof(handshake));
+ buffer_append(handshake_buf, log_prefix, strlen(log_prefix));
+
+ ret = write(log_fd[1], handshake_buf->data, handshake_buf->used);
+ if (ret < 0) {
+ i_error("write(log handshake) failed: %m");
+ return -1;
+ }
+ if ((size_t)ret != handshake_buf->used) {
+ i_error("write(log handshake) didn't write everything");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+service_process_write_log_bye(int fd, struct service_process *process)
+{
+ const char *data;
+
+ if (process->service->log_process_internal_fd == -1) {
+ /* another log process was just destroyed */
+ return 0;
+ }
+
+ data = t_strdup_printf("%d %s BYE\n",
+ process->service->log_process_internal_fd,
+ dec2str(process->pid));
+ if (write(fd, data, strlen(data)) < 0) {
+ if (errno != EAGAIN)
+ i_error("write(log process) failed: %m");
+ return -1;
+ }
+ return 0;
+}
+
+int services_log_init(struct service_list *service_list)
+{
+ struct service *service;
+ const char *log_prefix;
+ buffer_t *handshake_buf;
+ ssize_t ret = 0;
+ int fd;
+
+ handshake_buf = buffer_create_dynamic(default_pool, 256);
+ if (service_log_fds_init(MASTER_LOG_PREFIX_NAME,
+ service_list->master_log_fd,
+ handshake_buf) < 0)
+ ret = -1;
+ else
+ fd_set_nonblock(service_list->master_log_fd[1], TRUE);
+
+ i_assert(service_list->log_byes == NULL);
+ service_list->log_byes =
+ service_process_notify_init(service_list->master_log_fd[1],
+ service_process_write_log_bye);
+
+ fd = MASTER_LISTEN_FD_FIRST + 1;
+ array_foreach_elem(&service_list->services, service) {
+ if (service->type == SERVICE_TYPE_LOG)
+ continue;
+
+ log_prefix = t_strconcat(service->set->name, ": ", NULL);
+ if (service_log_fds_init(log_prefix, service->log_fd,
+ handshake_buf) < 0) {
+ ret = -1;
+ break;
+ }
+ service->log_process_internal_fd = fd++;
+ }
+
+ buffer_free(&handshake_buf);
+ if (ret < 0) {
+ services_log_deinit(service_list);
+ return -1;
+ }
+
+ service_anvil_send_log_fd();
+ return 0;
+}
+
+void services_log_deinit(struct service_list *service_list)
+{
+ struct service *const *services;
+ unsigned int i, count;
+
+ services = array_get(&service_list->services, &count);
+ for (i = 0; i < count; i++) {
+ if (services[i]->log_fd[0] != -1) {
+ if (close(services[i]->log_fd[0]) < 0) {
+ service_error(services[i],
+ "close(log_fd) failed: %m");
+ }
+ if (close(services[i]->log_fd[1]) < 0) {
+ service_error(services[i],
+ "close(log_fd) failed: %m");
+ }
+ services[i]->log_fd[0] = -1;
+ services[i]->log_fd[1] = -1;
+ services[i]->log_process_internal_fd = -1;
+ }
+ }
+ if (service_list->log_byes != NULL)
+ service_process_notify_deinit(&service_list->log_byes);
+ if (service_list->master_log_fd[0] != -1) {
+ if (close(service_list->master_log_fd[0]) < 0)
+ i_error("close(master log fd) failed: %m");
+ if (close(service_list->master_log_fd[1]) < 0)
+ i_error("close(master log fd) failed: %m");
+ service_list->master_log_fd[0] = -1;
+ service_list->master_log_fd[1] = -1;
+ }
+}
+
+void services_log_dup2(ARRAY_TYPE(dup2) *dups,
+ struct service_list *service_list,
+ unsigned int first_fd, unsigned int *fd_count)
+{
+ struct service *service;
+ unsigned int n = 0;
+
+ /* master log fd is always the first one */
+ dup2_append(dups, service_list->master_log_fd[0], first_fd);
+ n++; *fd_count += 1;
+
+ array_foreach_elem(&service_list->services, service) {
+ if (service->log_fd[1] == -1)
+ continue;
+
+ i_assert((int)(first_fd + n) == service->log_process_internal_fd);
+ dup2_append(dups, service->log_fd[0], first_fd + n);
+ n++; *fd_count += 1;
+ }
+}
diff --git a/src/master/service-log.h b/src/master/service-log.h
new file mode 100644
index 0000000..475c91d
--- /dev/null
+++ b/src/master/service-log.h
@@ -0,0 +1,13 @@
+#ifndef SERVICE_LOG_H
+#define SERVICE_LOG_H
+
+#include "dup2-array.h"
+
+int services_log_init(struct service_list *service_list);
+void services_log_deinit(struct service_list *service_list);
+
+void services_log_dup2(ARRAY_TYPE(dup2) *dups,
+ struct service_list *service_list,
+ unsigned int first_fd, unsigned int *fd_count);
+
+#endif
diff --git a/src/master/service-monitor.c b/src/master/service-monitor.c
new file mode 100644
index 0000000..0a65733
--- /dev/null
+++ b/src/master/service-monitor.c
@@ -0,0 +1,845 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "master-client.h"
+#include "service.h"
+#include "service-process.h"
+#include "service-process-notify.h"
+#include "service-anvil.h"
+#include "service-log.h"
+#include "service-monitor.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+#include <syslog.h>
+#include <signal.h>
+
+#define SERVICE_DROP_WARN_INTERVAL_SECS 1
+#define SERVICE_DROP_TIMEOUT_MSECS (10*1000)
+#define SERVICE_LOG_DROP_WARNING_DELAY_MSECS 500
+#define MAX_DIE_WAIT_MSECS 5000
+#define SERVICE_MAX_EXIT_FAILURES_IN_SEC 10
+#define SERVICE_PREFORK_MAX_AT_ONCE 10
+
+static void service_monitor_start_extra_avail(struct service *service);
+static void service_status_more(struct service_process *process,
+ const struct master_status *status);
+static void service_monitor_listen_start_force(struct service *service);
+
+static void service_process_idle_kill_timeout(struct service_process *process)
+{
+ struct master_status status;
+
+ service_error(process->service, "Process %s is ignoring idle SIGINT",
+ dec2str(process->pid));
+
+ /* assume this process is busy */
+ i_zero(&status);
+ service_status_more(process, &status);
+ process->available_count = 0;
+}
+
+static void service_kill_idle(struct service *service)
+{
+ /* Kill extra idling processes to reduce their number. The idea here is
+ that if the load stays the same, by killing the lowwater number of
+ processes there won't be any extra idling processes left. */
+ unsigned int processes_to_kill =
+ service->process_idling_lowwater_since_kills;
+ service->process_idling_lowwater_since_kills = service->process_idling;
+
+ /* Always try to leave process_min_avail processes */
+ i_assert(processes_to_kill <= service->process_avail);
+ if (processes_to_kill <= service->set->process_min_avail) {
+ if (service->process_idling == 0)
+ timeout_remove(&service->to_idle);
+ return;
+ }
+ processes_to_kill -= service->set->process_min_avail;
+
+ /* Now, kill the processes with the oldest idle_start time.
+
+ (It's actually not important which processes get killed. A better
+ way could be to kill the oldest processes since they might have to
+ be restarted anyway soon due to reaching service_count, but we'd
+ have to use priority queue for tracking that, which is more
+ expensive and probably not worth it.) */
+ for (; processes_to_kill > 0; processes_to_kill--) {
+ struct service_process *process = service->idle_processes_head;
+
+ i_assert(process != NULL);
+ if (process->to_idle_kill != NULL) {
+ /* already tried to kill all the idle processes */
+ break;
+ }
+
+ i_assert(process->available_count == service->client_limit);
+ if (kill(process->pid, SIGINT) < 0 && errno != ESRCH) {
+ service_error(service, "kill(%s, SIGINT) failed: %m",
+ dec2str(process->pid));
+ }
+ process->last_kill_sent = ioloop_time;
+ process->to_idle_kill =
+ timeout_add(service->idle_kill * 1000,
+ service_process_idle_kill_timeout, process);
+
+ /* Move it to the end of the list, so it's not tried to be
+ killed again. */
+ DLLIST2_REMOVE(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ DLLIST2_APPEND(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ }
+}
+
+static void service_status_more(struct service_process *process,
+ const struct master_status *status)
+{
+ struct service *service = process->service;
+
+ if (process->idle_start != 0) {
+ /* idling process became busy */
+ DLLIST2_REMOVE(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ DLLIST_PREPEND(&service->busy_processes, process);
+ process->idle_start = 0;
+
+ i_assert(service->process_idling > 0);
+ service->process_idling--;
+ service->process_idling_lowwater_since_kills =
+ I_MIN(service->process_idling_lowwater_since_kills,
+ service->process_idling);
+ }
+ process->total_count +=
+ process->available_count - status->available_count;
+
+ if (status->available_count != 0)
+ return;
+
+ /* process used up all of its clients */
+ i_assert(service->process_avail > 0);
+ service->process_avail--;
+
+ if (service->type == SERVICE_TYPE_LOGIN &&
+ service->process_avail == 0 &&
+ service->process_count == service->process_limit)
+ service_login_notify(service, TRUE);
+
+ /* we may need to start more */
+ service_monitor_start_extra_avail(service);
+ service_monitor_listen_start(service);
+}
+
+static void service_check_idle(struct service_process *process)
+{
+ struct service *service = process->service;
+
+ if (process->available_count != service->client_limit)
+ return;
+
+ if (process->idle_start == 0) {
+ /* busy process started idling */
+ DLLIST_REMOVE(&service->busy_processes, process);
+ service->process_idling++;
+ } else {
+ /* Idling process updated its status again to be idling. Maybe
+ it was busy for a little bit? Update its idle_start time. */
+ DLLIST2_REMOVE(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ }
+ DLLIST2_APPEND(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ process->idle_start = ioloop_time;
+
+ if (service->process_avail > service->set->process_min_avail &&
+ service->to_idle == NULL &&
+ service->idle_kill != UINT_MAX) {
+ /* We have more processes than we really need. Start a timer
+ to trigger idle_kill. */
+ service->to_idle =
+ timeout_add(service->idle_kill * 1000,
+ service_kill_idle, service);
+ }
+}
+
+static void service_status_less(struct service_process *process)
+{
+ struct service *service = process->service;
+
+ /* some process got more connections - remove the delayed warning */
+ timeout_remove(&service->to_drop_warning);
+
+ if (process->available_count == 0) {
+ /* process can accept more clients again */
+ if (service->process_avail++ == 0)
+ service_monitor_listen_stop(service);
+ i_assert(service->process_avail <= service->process_count);
+ }
+ if (service->type == SERVICE_TYPE_LOGIN)
+ service_login_notify(service, FALSE);
+}
+
+static void
+service_status_input_one(struct service *service,
+ const struct master_status *status)
+{
+ struct service_process *process;
+
+ process = hash_table_lookup(service_pids, POINTER_CAST(status->pid));
+ if (process == NULL) {
+ /* we've probably wait()ed it away already. ignore */
+ return;
+ }
+
+ if (process->uid != status->uid || process->service != service) {
+ /* a) Process was closed and another process was created with
+ the same PID, but we're still receiving status update from
+ the old process.
+
+ b) Some process is trying to corrupt our internal state by
+ trying to pretend to be someone else. We could use stronger
+ randomness here, but the worst they can do is DoS and there
+ are already more serious problems if someone is able to do
+ this.. */
+ service_error(service, "Ignoring invalid update from child %s "
+ "(UID=%u)", dec2str(status->pid), status->uid);
+ return;
+ }
+ process->last_status_update = ioloop_time;
+ /* process worked a bit - it may have ignored idle-kill signal */
+ timeout_remove(&process->to_idle_kill);
+
+ /* first status notification */
+ timeout_remove(&process->to_status);
+
+ if (process->available_count != status->available_count) {
+ if (process->available_count > status->available_count) {
+ /* process started servicing some more clients */
+ service_status_more(process, status);
+ } else {
+ /* process finished servicing some clients */
+ service_status_less(process);
+ }
+ process->available_count = status->available_count;
+ }
+ service_check_idle(process);
+}
+
+static void service_status_input(struct service *service)
+{
+ struct master_status status[1024/sizeof(struct master_status)];
+ unsigned int i, count;
+ ssize_t ret;
+
+ ret = read(service->status_fd[0], &status, sizeof(status));
+ if (ret <= 0) {
+ if (ret == 0)
+ service_error(service, "read(status) failed: EOF");
+ else if (errno != EAGAIN)
+ service_error(service, "read(status) failed: %m");
+ else
+ return;
+ service_monitor_stop(service);
+ return;
+ }
+
+ if ((ret % sizeof(struct master_status)) != 0) {
+ service_error(service, "service sent partial status update "
+ "(%d bytes)", (int)ret);
+ return;
+ }
+
+ count = ret / sizeof(struct master_status);
+ for (i = 0; i < count; i++)
+ service_status_input_one(service, &status[i]);
+}
+
+static void service_log_drop_warning(struct service *service)
+{
+ const char *limit_name;
+ unsigned int limit;
+
+ if (service->last_drop_warning +
+ SERVICE_DROP_WARN_INTERVAL_SECS <= ioloop_time) {
+ service->last_drop_warning = ioloop_time;
+ if (service->process_limit > 1) {
+ limit_name = "process_limit";
+ limit = service->process_limit;
+ } else if (service->set->service_count == 1) {
+ i_assert(service->client_limit == 1);
+ limit_name = "client_limit/service_count";
+ limit = 1;
+ } else {
+ limit_name = "client_limit";
+ limit = service->client_limit;
+ }
+ i_warning("service(%s): %s (%u) reached, "
+ "client connections are being dropped",
+ service->set->name, limit_name, limit);
+ }
+}
+
+static void service_monitor_throttle(struct service *service)
+{
+ if (service->to_throttle != NULL || service->list->destroying)
+ return;
+
+ i_assert(service->throttle_msecs > 0);
+
+ service_error(service,
+ "command startup failed, throttling for %u.%03u secs",
+ service->throttle_msecs / 1000,
+ service->throttle_msecs % 1000);
+ service_throttle(service, service->throttle_msecs);
+ service->throttle_msecs *= 2;
+ if (service->throttle_msecs >
+ SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS) {
+ service->throttle_msecs =
+ SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS;
+ }
+}
+
+static void service_drop_timeout(struct service *service)
+{
+ struct service_listener *lp;
+ int fd;
+
+ i_assert(service->process_avail == 0);
+
+ /* drop all pending connections */
+ array_foreach_elem(&service->listeners, lp) {
+ while ((fd = net_accept(lp->fd, NULL, NULL)) > 0)
+ net_disconnect(fd);
+ }
+
+ service_monitor_listen_start_force(service);
+ service->listen_pending = TRUE;
+}
+
+static void service_monitor_listen_pending(struct service *service)
+{
+ i_assert(service->process_avail == 0);
+
+ service_monitor_listen_stop(service);
+ service->listen_pending = TRUE;
+
+ service->to_drop = timeout_add(SERVICE_DROP_TIMEOUT_MSECS,
+ service_drop_timeout, service);
+}
+
+static void service_drop_connections(struct service_listener *l)
+{
+ struct service *service = l->service;
+ int fd;
+
+ if (service->type != SERVICE_TYPE_WORKER)
+ service_log_drop_warning(service);
+
+ if (service->type == SERVICE_TYPE_LOGIN) {
+ /* reached process limit, notify processes that they
+ need to start killing existing connections if they
+ reach connection limit */
+ service_login_notify(service, TRUE);
+
+ service_monitor_listen_pending(service);
+ } else if (!service->listen_pending) {
+ /* maybe this is a temporary peak, stop for a while and
+ see if it goes away */
+ service_monitor_listen_pending(service);
+ if (service->to_drop_warning == NULL &&
+ service->type == SERVICE_TYPE_WORKER) {
+ service->to_drop_warning =
+ timeout_add_short(SERVICE_LOG_DROP_WARNING_DELAY_MSECS,
+ service_log_drop_warning, service);
+ }
+ } else {
+ /* this has been happening for a while now. just accept and
+ close the connection, so it's clear that this is happening
+ because of the limit, rather than because the service
+ processes aren't answering fast enough */
+ fd = net_accept(l->fd, NULL, NULL);
+ if (fd > 0)
+ net_disconnect(fd);
+ }
+}
+
+static void service_accept(struct service_listener *l)
+{
+ struct service *service = l->service;
+
+ i_assert(service->process_avail == 0);
+
+ if (service->process_count == service->process_limit) {
+ /* we've reached our limits, new clients will have to
+ wait until there are more processes available */
+ service_drop_connections(l);
+ return;
+ }
+
+ /* create a child process and let it accept() this connection */
+ if (service_process_create(service) == NULL)
+ service_monitor_throttle(service);
+ else
+ service_monitor_listen_stop(service);
+}
+
+static bool
+service_monitor_start_count(struct service *service, unsigned int limit)
+{
+ unsigned int i, count;
+
+ i_assert(service->set->process_min_avail >= service->process_avail);
+
+ count = service->set->process_min_avail - service->process_avail;
+ if (service->process_count + count > service->process_limit)
+ count = service->process_limit - service->process_count;
+ if (count > limit)
+ count = limit;
+
+ for (i = 0; i < count; i++) {
+ if (service_process_create(service) == NULL) {
+ service_monitor_throttle(service);
+ break;
+ }
+ }
+ if (i > 0) {
+ /* we created some processes, they'll do the listening now */
+ service_monitor_listen_stop(service);
+ }
+ return i >= limit;
+}
+
+static void service_monitor_prefork_timeout(struct service *service)
+{
+ /* don't prefork more processes if other more important processes had
+ been forked while we were waiting for this timeout (= master seems
+ busy) */
+ if (service->list->fork_counter != service->prefork_counter) {
+ service->prefork_counter = service->list->fork_counter;
+ return;
+ }
+ if (service->process_avail < service->set->process_min_avail) {
+ if (service_monitor_start_count(service, SERVICE_PREFORK_MAX_AT_ONCE) &&
+ service->process_avail < service->set->process_min_avail) {
+ /* All SERVICE_PREFORK_MAX_AT_ONCE were created, but
+ it still wasn't enough. Launch more in the next
+ timeout. */
+ return;
+ }
+ }
+ timeout_remove(&service->to_prefork);
+}
+
+static void service_monitor_start_extra_avail(struct service *service)
+{
+ if (service->process_avail >= service->set->process_min_avail ||
+ service->process_count >= service->process_limit ||
+ service->list->destroying)
+ return;
+
+ if (service->process_avail == 0) {
+ /* quickly start one process now */
+ if (!service_monitor_start_count(service, 1))
+ return;
+ if (service->process_avail >= service->set->process_min_avail)
+ return;
+ }
+ if (service->to_prefork == NULL) {
+ /* ioloop handles timeouts before fds (= SIGCHLD callback),
+ so let the first timeout handler call simply update the fork
+ counter and the second one check if we're busy or not. */
+ service->to_prefork =
+ timeout_add_short(0, service_monitor_prefork_timeout, service);
+ }
+}
+
+static void service_monitor_listen_start_force(struct service *service)
+{
+ struct service_listener *l;
+
+ service->listening = TRUE;
+ service->listen_pending = FALSE;
+ timeout_remove(&service->to_drop);
+ timeout_remove(&service->to_drop_warning);
+
+ array_foreach_elem(&service->listeners, l) {
+ if (l->io == NULL && l->fd != -1)
+ l->io = io_add(l->fd, IO_READ, service_accept, l);
+ }
+}
+
+void service_monitor_listen_start(struct service *service)
+{
+ if (service->process_avail > 0 || service->to_throttle != NULL ||
+ (service->process_count == service->process_limit &&
+ service->listen_pending))
+ return;
+
+ service_monitor_listen_start_force(service);
+}
+
+void service_monitor_listen_stop(struct service *service)
+{
+ struct service_listener *l;
+
+ array_foreach_elem(&service->listeners, l)
+ io_remove(&l->io);
+ service->listening = FALSE;
+ service->listen_pending = FALSE;
+ timeout_remove(&service->to_drop);
+ timeout_remove(&service->to_drop_warning);
+}
+
+static int service_login_create_notify_fd(struct service *service)
+{
+ int fd, ret;
+
+ if (service->login_notify_fd != -1)
+ return 0;
+
+ T_BEGIN {
+ string_t *prefix = t_str_new(128);
+ const char *path;
+
+ str_append(prefix, service->set->master_set->base_dir);
+ str_append(prefix, "/login-master-notify");
+
+ fd = safe_mkstemp(prefix, 0600, (uid_t)-1, (gid_t)-1);
+ path = str_c(prefix);
+
+ if (fd == -1) {
+ service_error(service, "safe_mkstemp(%s) failed: %m",
+ path);
+ } else if (unlink(path) < 0) {
+ service_error(service, "unlink(%s) failed: %m", path);
+ } else {
+ fd_close_on_exec(fd, TRUE);
+ service->login_notify_fd = fd;
+ }
+ } T_END;
+
+ ret = fd == -1 ? -1 : 0;
+ if (fd != service->login_notify_fd)
+ i_close_fd(&fd);
+ return ret;
+}
+
+void services_monitor_start(struct service_list *service_list)
+{
+ ARRAY(struct service *) listener_services;
+ struct service *service;
+
+ if (services_log_init(service_list) < 0)
+ return;
+ service_anvil_monitor_start(service_list);
+
+ if (service_list->io_master == NULL &&
+ service_list->master_fd != -1) {
+ service_list->io_master =
+ io_add(service_list->master_fd, IO_READ,
+ master_client_connected, service_list);
+ }
+
+ t_array_init(&listener_services, array_count(&service_list->services));
+ array_foreach_elem(&service_list->services, service) {
+ if (service->type == SERVICE_TYPE_LOGIN) {
+ if (service_login_create_notify_fd(service) < 0)
+ continue;
+ }
+ if (service->master_dead_pipe_fd[0] == -1) {
+ if (pipe(service->master_dead_pipe_fd) < 0) {
+ service_error(service, "pipe() failed: %m");
+ continue;
+ }
+ fd_close_on_exec(service->master_dead_pipe_fd[0], TRUE);
+ fd_close_on_exec(service->master_dead_pipe_fd[1], TRUE);
+ }
+ if (service->status_fd[0] == -1) {
+ /* we haven't yet created status pipe */
+ if (pipe(service->status_fd) < 0) {
+ service_error(service, "pipe() failed: %m");
+ continue;
+ }
+
+ net_set_nonblock(service->status_fd[0], TRUE);
+ fd_close_on_exec(service->status_fd[0], TRUE);
+ net_set_nonblock(service->status_fd[1], TRUE);
+ fd_close_on_exec(service->status_fd[1], TRUE);
+ }
+ if (service->io_status == NULL) {
+ service->io_status =
+ io_add(service->status_fd[0], IO_READ,
+ service_status_input, service);
+ }
+ service_monitor_listen_start(service);
+ array_push_back(&listener_services, &service);
+ }
+
+ /* create processes only after adding all listeners */
+ array_foreach_elem(&listener_services, service)
+ service_monitor_start_extra_avail(service);
+
+ if (service_list->log->status_fd[0] != -1) {
+ if (service_process_create(service_list->log) != NULL)
+ service_monitor_listen_stop(service_list->log);
+ }
+
+ /* start up a process for startup-services */
+ array_foreach_elem(&service_list->services, service) {
+ if (service->type == SERVICE_TYPE_STARTUP &&
+ service->status_fd[0] != -1) {
+ if (service_process_create(service) != NULL)
+ service_monitor_listen_stop(service);
+ }
+ }
+}
+
+static void service_monitor_close_dead_pipe(struct service *service)
+{
+ if (service->master_dead_pipe_fd[0] != -1) {
+ i_close_fd(&service->master_dead_pipe_fd[0]);
+ i_close_fd(&service->master_dead_pipe_fd[1]);
+ }
+}
+
+void service_monitor_stop(struct service *service)
+{
+ int i;
+
+ io_remove(&service->io_status);
+
+ if (service->status_fd[0] != -1 &&
+ service->type != SERVICE_TYPE_ANVIL) {
+ for (i = 0; i < 2; i++) {
+ if (close(service->status_fd[i]) < 0) {
+ service_error(service,
+ "close(status fd) failed: %m");
+ }
+ service->status_fd[i] = -1;
+ }
+ }
+ service_monitor_close_dead_pipe(service);
+ if (service->login_notify_fd != -1) {
+ if (close(service->login_notify_fd) < 0) {
+ service_error(service,
+ "close(login notify fd) failed: %m");
+ }
+ service->login_notify_fd = -1;
+ }
+ timeout_remove(&service->to_login_notify);
+ service_monitor_listen_stop(service);
+
+ timeout_remove(&service->to_throttle);
+ timeout_remove(&service->to_prefork);
+ timeout_remove(&service->to_idle);
+}
+
+void service_monitor_stop_close(struct service *service)
+{
+ struct service_listener *l;
+
+ service_monitor_stop(service);
+
+ array_foreach_elem(&service->listeners, l)
+ i_close_fd(&l->fd);
+}
+
+static void services_monitor_wait(struct service_list *service_list)
+{
+ struct service *service;
+ struct timeval tv_start;
+ bool finished;
+
+ io_loop_time_refresh();
+ tv_start = ioloop_timeval;
+
+ for (;;) {
+ finished = TRUE;
+ services_monitor_reap_children();
+ array_foreach_elem(&service_list->services, service) {
+ if (service->status_fd[0] != -1)
+ service_status_input(service);
+ if (service->process_avail > 0)
+ finished = FALSE;
+ }
+ io_loop_time_refresh();
+ if (finished ||
+ timeval_diff_msecs(&ioloop_timeval, &tv_start) > MAX_DIE_WAIT_MSECS)
+ break;
+ i_sleep_msecs(100);
+ }
+}
+
+static bool
+service_processes_list_close_listeners(struct service *service,
+ struct service_process *processes)
+{
+ struct service_process *process = processes;
+ bool ret = FALSE;
+
+ for (; process != NULL; process = process->next) {
+ if (kill(process->pid, SIGQUIT) == 0)
+ ret = TRUE;
+ else if (errno != ESRCH) {
+ service_error(service, "kill(%s, SIGQUIT) failed: %m",
+ dec2str(process->pid));
+ }
+ }
+ return ret;
+}
+
+static bool service_processes_close_listeners(struct service *service)
+{
+ bool ret = FALSE;
+
+ if (service_processes_list_close_listeners(service,
+ service->busy_processes))
+ ret = TRUE;
+ if (service_processes_list_close_listeners(service,
+ service->idle_processes_head))
+ ret = TRUE;
+ return ret;
+}
+
+static bool
+service_list_processes_close_listeners(struct service_list *service_list)
+{
+ struct service *service;
+ bool ret = FALSE;
+
+ array_foreach_elem(&service_list->services, service) {
+ if (service_processes_close_listeners(service))
+ ret = TRUE;
+ }
+ return ret;
+}
+
+static void services_monitor_wait_and_kill(struct service_list *service_list)
+{
+ /* we've notified all children that the master is dead.
+ now wait for the children to either die or to tell that
+ they're no longer listening for new connections. */
+ services_monitor_wait(service_list);
+
+ /* Even if the waiting stopped early because all the process_avail==0,
+ it can mean that there are processes that have the listener socket
+ open (just not actively being listened to). We'll need to make sure
+ that those sockets are closed before we exit, so that a restart
+ won't fail. Do this by sending SIGQUIT to all the child processes
+ that are left, which are handled by lib-master to immediately close
+ the listener in the signal handler itself. */
+ if (service_list_processes_close_listeners(service_list)) {
+ /* SIGQUITs were sent. wait a little bit to make sure they're
+ also processed before quitting. */
+ i_sleep_msecs(1000);
+ }
+}
+
+void services_monitor_stop(struct service_list *service_list, bool wait)
+{
+ struct service *service;
+
+ array_foreach_elem(&service_list->services, service)
+ service_monitor_close_dead_pipe(service);
+
+ if (wait)
+ services_monitor_wait_and_kill(service_list);
+
+ io_remove(&service_list->io_master);
+
+ array_foreach_elem(&service_list->services, service)
+ service_monitor_stop(service);
+
+ services_log_deinit(service_list);
+}
+
+static bool
+service_process_failure(struct service_process *process, int status)
+{
+ struct service *service = process->service;
+ bool throttle;
+
+ service_process_log_status_error(process, status);
+ throttle = process->to_status != NULL;
+ if (!throttle && !service->have_successful_exits) {
+ /* this service has seen no successful exits yet.
+ try to avoid failure storms by throttling the service if it
+ only keeps failing rapidly. this is no longer done after
+ one success to avoid intentional DoSing, in case attacker
+ finds a way to quickly crash his own session. */
+ if (service->exit_failure_last != ioloop_time) {
+ service->exit_failure_last = ioloop_time;
+ service->exit_failures_in_sec = 0;
+ }
+ if (++service->exit_failures_in_sec > SERVICE_MAX_EXIT_FAILURES_IN_SEC)
+ throttle = TRUE;
+ }
+ service_process_notify_add(service_anvil_global->kills, process);
+ return throttle;
+}
+
+void services_monitor_reap_children(void)
+{
+ struct service_process *process;
+ struct service *service;
+ pid_t pid;
+ int status;
+ bool service_stopped, throttle;
+
+ while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
+ process = hash_table_lookup(service_pids, POINTER_CAST(pid));
+ if (process == NULL) {
+ i_error("waitpid() returned unknown PID %s",
+ dec2str(pid));
+ continue;
+ }
+
+ service = process->service;
+ if (status == 0) {
+ /* success - one success resets all failures */
+ service->have_successful_exits = TRUE;
+ service->exit_failures_in_sec = 0;
+ service->throttle_msecs =
+ SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS;
+ throttle = FALSE;
+ } else {
+ throttle = service_process_failure(process, status);
+ }
+ if (service->type == SERVICE_TYPE_ANVIL)
+ service_anvil_process_destroyed(process);
+
+ /* if we're reloading, we may get here with a service list
+ that's going to be destroyed after this process is
+ destroyed. keep the list referenced until we're done. */
+ service_list_ref(service->list);
+ service_process_destroy(process);
+
+ if (throttle)
+ service_monitor_throttle(service);
+ service_stopped = service->status_fd[0] == -1;
+ if (!service_stopped && !service->list->destroying) {
+ service_monitor_start_extra_avail(service);
+ /* if there are no longer listening processes,
+ start listening for more */
+ if (service->to_throttle != NULL) {
+ /* throttling */
+ } else if (service == service->list->log &&
+ service->process_count == 0) {
+ /* log service must always be running */
+ if (service_process_create(service) == NULL)
+ service_monitor_throttle(service);
+ } else {
+ service_monitor_listen_start(service);
+ }
+ }
+ service_list_unref(service->list);
+ }
+}
diff --git a/src/master/service-monitor.h b/src/master/service-monitor.h
new file mode 100644
index 0000000..7dd1c4d
--- /dev/null
+++ b/src/master/service-monitor.h
@@ -0,0 +1,18 @@
+#ifndef SERVICE_MONITOR_H
+#define SERVICE_MONITOR_H
+
+/* Start listening and monitoring services. */
+void services_monitor_start(struct service_list *service_list);
+
+/* Stop services. */
+void services_monitor_stop(struct service_list *service_list, bool wait);
+
+/* Call after SIGCHLD has been detected */
+void services_monitor_reap_children(void);
+
+void service_monitor_stop(struct service *service);
+void service_monitor_stop_close(struct service *service);
+void service_monitor_listen_start(struct service *service);
+void service_monitor_listen_stop(struct service *service);
+
+#endif
diff --git a/src/master/service-process-notify.c b/src/master/service-process-notify.c
new file mode 100644
index 0000000..1cda677
--- /dev/null
+++ b/src/master/service-process-notify.c
@@ -0,0 +1,101 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "aqueue.h"
+#include "ioloop.h"
+#include "service.h"
+#include "service-process.h"
+#include "service-process-notify.h"
+
+struct service_process_notify {
+ service_process_notify_callback_t *write_callback;
+
+ int fd;
+ struct io *io_write;
+ struct aqueue *process_queue;
+ ARRAY(struct service_process *) processes;
+};
+
+struct service_process_notify *
+service_process_notify_init(int fd,
+ service_process_notify_callback_t *write_callback)
+{
+ struct service_process_notify *notify;
+
+ notify = i_new(struct service_process_notify, 1);
+ notify->fd = fd;
+ notify->write_callback = write_callback;
+
+ i_array_init(&notify->processes, 64);
+ notify->process_queue = aqueue_init(&notify->processes.arr);
+ return notify;
+}
+
+static void service_process_notify_reset(struct service_process_notify *notify)
+{
+ struct service_process *const *processes, *process;
+ unsigned int i, count;
+
+ if (notify->io_write == NULL)
+ return;
+
+ processes = array_front_modifiable(&notify->processes);
+ count = aqueue_count(notify->process_queue);
+ for (i = 0; i < count; i++) {
+ process = processes[aqueue_idx(notify->process_queue, i)];
+ service_process_unref(process);
+ }
+ aqueue_clear(notify->process_queue);
+ array_clear(&notify->processes);
+
+ io_remove(&notify->io_write);
+}
+
+static void notify_flush(struct service_process_notify *notify)
+{
+ struct service_process *const *processes, *process;
+
+ while (aqueue_count(notify->process_queue) > 0) {
+ processes = array_front_modifiable(&notify->processes);
+ process = processes[aqueue_idx(notify->process_queue, 0)];
+
+ if (notify->write_callback(notify->fd, process) < 0) {
+ if (errno != EAGAIN)
+ service_process_notify_reset(notify);
+ return;
+ }
+ service_process_unref(process);
+ aqueue_delete_tail(notify->process_queue);
+ }
+ io_remove(&notify->io_write);
+}
+
+void service_process_notify_deinit(struct service_process_notify **_notify)
+{
+ struct service_process_notify *notify = *_notify;
+
+ *_notify = NULL;
+
+ service_process_notify_reset(notify);
+ io_remove(&notify->io_write);
+ aqueue_deinit(&notify->process_queue);
+ array_free(&notify->processes);
+ i_free(notify);
+}
+
+void service_process_notify_add(struct service_process_notify *notify,
+ struct service_process *process)
+{
+ if (notify->write_callback(notify->fd, process) < 0) {
+ if (errno != EAGAIN)
+ return;
+
+ if (notify->io_write == NULL) {
+ notify->io_write = io_add(notify->fd, IO_WRITE,
+ notify_flush, notify);
+ }
+ aqueue_append(notify->process_queue, &process);
+ service_process_ref(process);
+ }
+}
diff --git a/src/master/service-process-notify.h b/src/master/service-process-notify.h
new file mode 100644
index 0000000..04a17b0
--- /dev/null
+++ b/src/master/service-process-notify.h
@@ -0,0 +1,15 @@
+#ifndef SERVICE_PROCESS_NOTIFY_H
+#define SERVICE_PROCESS_NOTIFY_H
+
+typedef int
+service_process_notify_callback_t(int fd, struct service_process *process);
+
+struct service_process_notify *
+service_process_notify_init(int fd,
+ service_process_notify_callback_t *write_callback);
+void service_process_notify_deinit(struct service_process_notify **notify);
+
+void service_process_notify_add(struct service_process_notify *notify,
+ struct service_process *process);
+
+#endif
diff --git a/src/master/service-process.c b/src/master/service-process.c
new file mode 100644
index 0000000..24e68bf
--- /dev/null
+++ b/src/master/service-process.c
@@ -0,0 +1,676 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "array.h"
+#include "aqueue.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "base64.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "llist.h"
+#include "hostpid.h"
+#include "env-util.h"
+#include "restrict-access.h"
+#include "restrict-process-size.h"
+#include "eacces-error.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "dup2-array.h"
+#include "service.h"
+#include "service-anvil.h"
+#include "service-listen.h"
+#include "service-log.h"
+#include "service-process-notify.h"
+#include "service-process.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <syslog.h>
+#include <signal.h>
+#include <sys/wait.h>
+
+static void service_reopen_inet_listeners(struct service *service)
+{
+ struct service_listener *const *listeners;
+ unsigned int i, count;
+ int old_fd;
+
+ listeners = array_get(&service->listeners, &count);
+ for (i = 0; i < count; i++) {
+ if (!listeners[i]->reuse_port || listeners[i]->fd == -1)
+ continue;
+
+ old_fd = listeners[i]->fd;
+ listeners[i]->fd = -1;
+ if (service_listener_listen(listeners[i]) < 0)
+ listeners[i]->fd = old_fd;
+ }
+}
+
+static void
+service_dup_fds(struct service *service)
+{
+ struct service_listener *const *listeners;
+ ARRAY_TYPE(dup2) dups;
+ string_t *listener_settings;
+ int fd = MASTER_LISTEN_FD_FIRST;
+ unsigned int i, count, socket_listener_count;
+
+ /* stdin/stdout is already redirected to /dev/null. Other master fds
+ should have been opened with fd_close_on_exec() so we don't have to
+ worry about them.
+
+ because the destination fd might be another one's source fd we have
+ to be careful not to overwrite anything. dup() the fd when needed */
+
+ socket_listener_count = 0;
+ listeners = array_get(&service->listeners, &count);
+ t_array_init(&dups, count + 10);
+
+ switch (service->type) {
+ case SERVICE_TYPE_LOG:
+ i_assert(fd == MASTER_LISTEN_FD_FIRST);
+ services_log_dup2(&dups, service->list, fd,
+ &socket_listener_count);
+ fd += socket_listener_count;
+ break;
+ case SERVICE_TYPE_ANVIL:
+ dup2_append(&dups, service_anvil_global->log_fdpass_fd[0],
+ MASTER_ANVIL_LOG_FDPASS_FD);
+ /* nonblocking anvil fd must be the first one. anvil treats it
+ as the master's fd */
+ dup2_append(&dups, service_anvil_global->nonblocking_fd[0], fd++);
+ dup2_append(&dups, service_anvil_global->blocking_fd[0], fd++);
+ socket_listener_count += 2;
+ break;
+ default:
+ break;
+ }
+
+ /* add listeners */
+ listener_settings = t_str_new(256);
+ for (i = 0; i < count; i++) {
+ if (listeners[i]->fd != -1) {
+ str_truncate(listener_settings, 0);
+ str_append_tabescaped(listener_settings, listeners[i]->name);
+
+ if (listeners[i]->type == SERVICE_LISTENER_INET) {
+ if (listeners[i]->set.inetset.set->ssl)
+ str_append(listener_settings, "\tssl");
+ if (listeners[i]->set.inetset.set->haproxy)
+ str_append(listener_settings, "\thaproxy");
+ }
+
+ dup2_append(&dups, listeners[i]->fd, fd++);
+
+ env_put(t_strdup_printf("SOCKET%d_SETTINGS",
+ socket_listener_count),
+ str_c(listener_settings));
+ socket_listener_count++;
+ }
+ }
+
+ if (service->login_notify_fd != -1) {
+ dup2_append(&dups, service->login_notify_fd,
+ MASTER_LOGIN_NOTIFY_FD);
+ }
+ switch (service->type) {
+ case SERVICE_TYPE_LOG:
+ case SERVICE_TYPE_ANVIL:
+ case SERVICE_TYPE_CONFIG:
+ dup2_append(&dups, dev_null_fd, MASTER_ANVIL_FD);
+ break;
+ case SERVICE_TYPE_UNKNOWN:
+ case SERVICE_TYPE_LOGIN:
+ case SERVICE_TYPE_STARTUP:
+ case SERVICE_TYPE_WORKER:
+ dup2_append(&dups, service_anvil_global->blocking_fd[1],
+ MASTER_ANVIL_FD);
+ break;
+ }
+ dup2_append(&dups, service->status_fd[1], MASTER_STATUS_FD);
+ if (service->type != SERVICE_TYPE_ANVIL) {
+ dup2_append(&dups, service->master_dead_pipe_fd[1],
+ MASTER_DEAD_FD);
+ } else {
+ dup2_append(&dups, global_master_dead_pipe_fd[1],
+ MASTER_DEAD_FD);
+ }
+
+ if (service->type == SERVICE_TYPE_LOG) {
+ /* keep stderr as-is. this is especially important when
+ log_path=/dev/stderr, but might be helpful even in other
+ situations for logging startup errors */
+ } else {
+ /* set log file to stderr. dup2() here immediately so that
+ we can set up logging to it without causing any log messages
+ to be lost. */
+ i_assert(service->log_fd[1] != -1);
+
+ env_put("LOG_SERVICE", "1");
+ if (dup2(service->log_fd[1], STDERR_FILENO) < 0)
+ i_fatal("dup2(log fd) failed: %m");
+ i_set_failure_internal();
+ }
+
+ /* Switch log writing back to stderr before the log fds are closed.
+ There's no guarantee that writing to stderr is visible anywhere, but
+ it's better than the process just dying with FATAL_LOGWRITE. */
+ i_set_failure_file("/dev/stderr",
+ t_strdup_printf("service(%s): ", service->set->name));
+
+ /* make sure we don't leak syslog fd. try to do it as late as possible,
+ but also before dup2()s in case syslog fd is one of them. */
+ closelog();
+
+ if (dup2_array(&dups) < 0)
+ i_fatal("service(%s): dup2s failed", service->set->name);
+
+ i_assert(fd == MASTER_LISTEN_FD_FIRST + (int)socket_listener_count);
+ env_put("SOCKET_COUNT", dec2str(socket_listener_count));
+}
+
+static void
+drop_privileges(struct service *service)
+{
+ struct restrict_access_settings rset;
+ bool allow_root;
+ size_t len;
+
+ if (service->vsz_limit != 0)
+ restrict_process_size(service->vsz_limit);
+
+ restrict_access_init(&rset);
+ rset.uid = service->uid;
+ rset.gid = service->gid;
+ rset.privileged_gid = service->privileged_gid;
+ rset.chroot_dir = *service->set->chroot == '\0' ? NULL :
+ service->set->chroot;
+ if (rset.chroot_dir != NULL) {
+ /* drop trailing / if it exists */
+ len = strlen(rset.chroot_dir);
+ if (rset.chroot_dir[len-1] == '/')
+ rset.chroot_dir = t_strndup(rset.chroot_dir, len-1);
+ }
+ rset.extra_groups = service->extra_gids;
+
+ restrict_access_set_env(&rset);
+ if (service->set->drop_priv_before_exec) {
+ allow_root = service->type != SERVICE_TYPE_LOGIN;
+ restrict_access(&rset,
+ allow_root ? RESTRICT_ACCESS_FLAG_ALLOW_ROOT : 0,
+ NULL);
+ }
+}
+
+static void service_process_setup_config_environment(struct service *service)
+{
+ const struct master_service_settings *set = service->list->service_set;
+
+ switch (service->type) {
+ case SERVICE_TYPE_CONFIG:
+ env_put(MASTER_CONFIG_FILE_ENV, service->config_file_path);
+ break;
+ case SERVICE_TYPE_LOG:
+ /* give the log's configuration directly, so it won't depend
+ on config process */
+ env_put("DOVECONF_ENV", "1");
+ env_put("LOG_PATH", set->log_path);
+ env_put("INFO_LOG_PATH", set->info_log_path);
+ env_put("DEBUG_LOG_PATH", set->debug_log_path);
+ env_put("LOG_TIMESTAMP", set->log_timestamp);
+ env_put("SYSLOG_FACILITY", set->syslog_facility);
+ env_put("INSTANCE_NAME", set->instance_name);
+ if (set->verbose_proctitle)
+ env_put("VERBOSE_PROCTITLE", "1");
+ env_put("SSL", "no");
+ break;
+ default:
+ env_put(MASTER_CONFIG_FILE_ENV,
+ services_get_config_socket_path(service->list));
+ break;
+ }
+}
+
+static void
+service_process_setup_environment(struct service *service, unsigned int uid,
+ const char *hostdomain)
+{
+ const struct master_service_settings *service_set =
+ service->list->service_set;
+ master_service_env_clean();
+
+ env_put(MASTER_IS_PARENT_ENV, "1");
+ service_process_setup_config_environment(service);
+ env_put(MASTER_SERVICE_ENV, service->set->name);
+ env_put(MASTER_CLIENT_LIMIT_ENV, dec2str(service->client_limit));
+ env_put(MASTER_PROCESS_LIMIT_ENV, dec2str(service->process_limit));
+ env_put(MASTER_PROCESS_MIN_AVAIL_ENV,
+ dec2str(service->set->process_min_avail));
+ env_put(MASTER_SERVICE_IDLE_KILL_ENV, dec2str(service->idle_kill));
+ if (service->set->service_count != 0) {
+ env_put(MASTER_SERVICE_COUNT_ENV,
+ dec2str(service->set->service_count));
+ }
+ env_put(MASTER_UID_ENV, dec2str(uid));
+ env_put(MY_HOSTNAME_ENV, my_hostname);
+ env_put(MY_HOSTDOMAIN_ENV, hostdomain);
+
+ if (service_set->verbose_proctitle)
+ env_put(MASTER_VERBOSE_PROCTITLE_ENV, "1");
+ if (!service->set->master_set->version_ignore)
+ env_put(MASTER_DOVECOT_VERSION_ENV, PACKAGE_VERSION);
+
+ if (service_set->stats_writer_socket_path[0] == '\0')
+ ; /* stats-writer socket disabled */
+ else if (service->set->chroot[0] != '\0') {
+ /* In a chroot - expect stats-writer socket to be in the
+ current directory. */
+ env_put(DOVECOT_STATS_WRITER_SOCKET_PATH,
+ service_set->stats_writer_socket_path);
+ } else {
+ env_put(DOVECOT_STATS_WRITER_SOCKET_PATH,
+ t_strdup_printf("%s/%s", service_set->base_dir,
+ service_set->stats_writer_socket_path));
+ }
+ if (ssl_manual_key_password != NULL && service->have_inet_listeners) {
+ /* manually given SSL password. give it only to services
+ that have inet listeners. */
+ env_put(MASTER_SSL_KEY_PASSWORD_ENV, ssl_manual_key_password);
+ }
+ if (service->type == SERVICE_TYPE_ANVIL &&
+ service_anvil_global->restarted)
+ env_put("ANVIL_RESTARTED", "1");
+ env_put(DOVECOT_LOG_DEBUG_ENV, service_set->log_debug);
+}
+
+static void service_process_status_timeout(struct service_process *process)
+{
+ service_error(process->service,
+ "Initial status notification not received in %d "
+ "seconds, killing the process",
+ SERVICE_FIRST_STATUS_TIMEOUT_SECS);
+ if (kill(process->pid, SIGKILL) < 0 && errno != ESRCH) {
+ service_error(process->service, "kill(%s, SIGKILL) failed: %m",
+ dec2str(process->pid));
+ }
+ timeout_remove(&process->to_status);
+}
+
+struct service_process *service_process_create(struct service *service)
+{
+ static unsigned int uid_counter = 0;
+ struct service_process *process;
+ unsigned int uid = ++uid_counter;
+ const char *hostdomain;
+ pid_t pid;
+ bool process_forked;
+
+ i_assert(service->status_fd[0] != -1);
+
+ if (service->to_throttle != NULL) {
+ /* throttling service, don't create new processes */
+ return NULL;
+ }
+ if (service->list->destroying) {
+ /* these services are being destroyed, no point in creating
+ new processes now */
+ return NULL;
+ }
+ /* look this up before fork()ing so that it gets cached for all the
+ future lookups. */
+ hostdomain = my_hostdomain();
+
+ if (service->type == SERVICE_TYPE_ANVIL &&
+ service_anvil_global->pid != 0) {
+ pid = service_anvil_global->pid;
+ uid = service_anvil_global->uid;
+ process_forked = FALSE;
+ } else {
+ pid = fork();
+ process_forked = TRUE;
+ service->list->fork_counter++;
+ }
+
+ if (pid < 0) {
+ int fork_errno = errno;
+ rlim_t limit;
+ const char *limit_str = "";
+
+ if (fork_errno == EAGAIN &&
+ restrict_get_process_limit(&limit) == 0) {
+ limit_str = t_strdup_printf(" (ulimit -u %llu reached?)",
+ (unsigned long long)limit);
+ }
+ errno = fork_errno;
+ service_error(service, "fork() failed: %m%s", limit_str);
+ return NULL;
+ }
+ if (pid == 0) {
+ /* child */
+ service_process_setup_environment(service, uid, hostdomain);
+ service_reopen_inet_listeners(service);
+ service_dup_fds(service);
+ drop_privileges(service);
+ process_exec(service->executable);
+ }
+ i_assert(hash_table_lookup(service_pids, POINTER_CAST(pid)) == NULL);
+
+ process = i_new(struct service_process, 1);
+ process->service = service;
+ process->refcount = 1;
+ process->pid = pid;
+ process->uid = uid;
+ if (process_forked) {
+ process->to_status =
+ timeout_add(SERVICE_FIRST_STATUS_TIMEOUT_SECS * 1000,
+ service_process_status_timeout, process);
+ }
+
+ process->available_count = service->client_limit;
+ process->idle_start = ioloop_time;
+ service->process_count_total++;
+ service->process_count++;
+ service->process_avail++;
+ service->process_idling++;
+ DLLIST2_APPEND(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+
+ service_list_ref(service->list);
+ hash_table_insert(service_pids, POINTER_CAST(process->pid), process);
+
+ if (service->type == SERVICE_TYPE_ANVIL && process_forked)
+ service_anvil_process_created(process);
+ return process;
+}
+
+void service_process_destroy(struct service_process *process)
+{
+ struct service *service = process->service;
+ struct service_list *service_list = service->list;
+
+ if (process->idle_start == 0)
+ DLLIST_REMOVE(&service->busy_processes, process);
+ else {
+ DLLIST2_REMOVE(&service->idle_processes_head,
+ &service->idle_processes_tail, process);
+ i_assert(service->process_idling > 0);
+ service->process_idling--;
+ service->process_idling_lowwater_since_kills =
+ I_MIN(service->process_idling_lowwater_since_kills,
+ service->process_idling);
+ }
+ hash_table_remove(service_pids, POINTER_CAST(process->pid));
+
+ if (process->available_count > 0) {
+ i_assert(service->process_avail > 0);
+ service->process_avail--;
+ }
+ i_assert(service->process_count > 0);
+ service->process_count--;
+ i_assert(service->process_avail <= service->process_count);
+
+ timeout_remove(&process->to_status);
+ timeout_remove(&process->to_idle_kill);
+ if (service->list->log_byes != NULL)
+ service_process_notify_add(service->list->log_byes, process);
+
+ process->destroyed = TRUE;
+ service_process_unref(process);
+
+ if (service->process_count < service->process_limit &&
+ service->type == SERVICE_TYPE_LOGIN)
+ service_login_notify(service, FALSE);
+
+ service_list_unref(service_list);
+}
+
+void service_process_ref(struct service_process *process)
+{
+ i_assert(process->refcount > 0);
+
+ process->refcount++;
+}
+
+void service_process_unref(struct service_process *process)
+{
+ i_assert(process->refcount > 0);
+
+ if (--process->refcount > 0)
+ return;
+
+ i_assert(process->destroyed);
+ i_free(process);
+}
+
+static const char *
+get_exit_status_message(struct service *service, enum fatal_exit_status status)
+{
+ string_t *str;
+
+ switch (status) {
+ case FATAL_LOGOPEN:
+ return "Can't open log file";
+ case FATAL_LOGWRITE:
+ return "Can't write to log file";
+ case FATAL_LOGERROR:
+ return "Internal logging error";
+ case FATAL_OUTOFMEM:
+ str = t_str_new(128);
+ str_append(str, "Out of memory");
+ if (service->vsz_limit != 0) {
+ str_printfa(str, " (service %s { vsz_limit=%"PRIuUOFF_T" MB }, "
+ "you may need to increase it)",
+ service->set->name,
+ service->vsz_limit/1024/1024);
+ }
+ if (getenv("CORE_OUTOFMEM") == NULL)
+ str_append(str, " - set CORE_OUTOFMEM=1 environment to get core dump");
+ return str_c(str);
+ case FATAL_EXEC:
+ return "exec() failed";
+
+ case FATAL_DEFAULT:
+ return "Fatal failure";
+ }
+
+ return NULL;
+}
+
+static bool linux_proc_fs_suid_is_dumpable(unsigned int *value_r)
+{
+ int fd = open(LINUX_PROC_FS_SUID_DUMPABLE, O_RDONLY);
+ if (fd == -1) {
+ /* we already checked that it exists - shouldn't get here */
+ i_error("open(%s) failed: %m", LINUX_PROC_FS_SUID_DUMPABLE);
+ have_proc_fs_suid_dumpable = FALSE;
+ return FALSE;
+ }
+ char buf[10];
+ ssize_t ret = read(fd, buf, sizeof(buf)-1);
+ if (ret < 0) {
+ i_error("read(%s) failed: %m", LINUX_PROC_FS_SUID_DUMPABLE);
+ have_proc_fs_suid_dumpable = FALSE;
+ *value_r = 0;
+ } else {
+ buf[ret] = '\0';
+ if (ret > 0 && buf[ret-1] == '\n')
+ buf[ret-1] = '\0';
+ if (str_to_uint(buf, value_r) < 0)
+ *value_r = 0;
+ }
+ i_close_fd(&fd);
+ return *value_r != 0;
+}
+
+static bool linux_is_absolute_core_pattern(void)
+{
+ int fd = open(LINUX_PROC_SYS_KERNEL_CORE_PATTERN, O_RDONLY);
+ if (fd == -1) {
+ /* we already checked that it exists - shouldn't get here */
+ i_error("open(%s) failed: %m", LINUX_PROC_SYS_KERNEL_CORE_PATTERN);
+ have_proc_sys_kernel_core_pattern = FALSE;
+ return FALSE;
+ }
+ char buf[10];
+ ssize_t ret = read(fd, buf, sizeof(buf)-1);
+ if (ret < 0) {
+ i_error("read(%s) failed: %m", LINUX_PROC_SYS_KERNEL_CORE_PATTERN);
+ have_proc_sys_kernel_core_pattern = FALSE;
+ buf[0] = '\0';
+ }
+ i_close_fd(&fd);
+ return buf[0] == '/' || buf[0] == '|';
+}
+
+static void
+log_coredump(struct service *service, string_t *str, int status)
+{
+#define CORE_DUMP_URL "https://dovecot.org/bugreport.html#coredumps"
+#ifdef WCOREDUMP
+ int signum = WTERMSIG(status);
+ unsigned int dumpable;
+
+ if (WCOREDUMP(status) != 0) {
+ str_append(str, " (core dumped)");
+ return;
+ }
+
+ if (signum != SIGABRT && signum != SIGSEGV && signum != SIGBUS)
+ return;
+
+ /* let's try to figure out why we didn't get a core dump */
+ if (core_dumps_disabled) {
+ str_printfa(str, " (core dumps disabled - "CORE_DUMP_URL")");
+ return;
+ }
+ str_append(str, " (core not dumped - "CORE_DUMP_URL);
+
+ /* If we're running on Linux, the best way to get core dumps is to set
+ fs.suid_dumpable=2 and sys.kernel.core_pattern to be an absolute
+ path. */
+ if (!have_proc_fs_suid_dumpable)
+ ;
+ else if (!linux_proc_fs_suid_is_dumpable(&dumpable)) {
+ str_printfa(str, " - set %s to 2)", LINUX_PROC_FS_SUID_DUMPABLE);
+ return;
+ } else if (dumpable == 2 && have_proc_sys_kernel_core_pattern &&
+ !linux_is_absolute_core_pattern()) {
+ str_printfa(str, " - set %s to absolute path)",
+ LINUX_PROC_SYS_KERNEL_CORE_PATTERN);
+ return;
+ } else if (dumpable == 1 || have_proc_sys_kernel_core_pattern) {
+ str_append(str, " - core wasn't writable?)");
+ return;
+ }
+
+#ifndef HAVE_PR_SET_DUMPABLE
+ if (!service->set->drop_priv_before_exec && service->uid != 0) {
+ str_printfa(str, " - set service %s "
+ "{ drop_priv_before_exec=yes })",
+ service->set->name);
+ return;
+ }
+ if (*service->set->privileged_group != '\0' && service->uid != 0) {
+ str_printfa(str, " - service %s "
+ "{ privileged_group } prevented it)",
+ service->set->name);
+ return;
+ }
+#else
+ if (!service->set->login_dump_core &&
+ service->type == SERVICE_TYPE_LOGIN) {
+ str_printfa(str, " - add -D parameter to "
+ "service %s { executable }", service->set->name);
+ return;
+ }
+#endif
+ if (service->set->chroot[0] != '\0') {
+ str_printfa(str, " - try to clear "
+ "service %s { chroot = } )", service->set->name);
+ return;
+ }
+ str_append_c(str, ')');
+#endif
+}
+
+static void
+service_process_get_status_error(string_t *str, struct service_process *process,
+ int status, bool *default_fatal_r)
+{
+ struct service *service = process->service;
+ const char *msg;
+
+ *default_fatal_r = FALSE;
+
+ str_printfa(str, "service(%s): child %s ", service->set->name,
+ dec2str(process->pid));
+ if (WIFSIGNALED(status)) {
+ str_printfa(str, "killed with signal %d", WTERMSIG(status));
+ log_coredump(service, str, status);
+ return;
+ }
+ if (!WIFEXITED(status)) {
+ str_printfa(str, "died with status %d", status);
+ return;
+ }
+
+ status = WEXITSTATUS(status);
+ if (status == 0) {
+ str_truncate(str, 0);
+ return;
+ }
+ str_printfa(str, "returned error %d", status);
+
+ msg = get_exit_status_message(service, status);
+ if (msg != NULL)
+ str_printfa(str, " (%s)", msg);
+
+ if (status == FATAL_DEFAULT)
+ *default_fatal_r = TRUE;
+}
+
+static void service_process_log(struct service_process *process,
+ bool default_fatal, const char *str)
+{
+ const char *data;
+
+ if (process->service->log_fd[1] == -1) {
+ i_error("%s", str);
+ return;
+ }
+
+ /* log it via the log process in charge of handling
+ this process's logging */
+ data = t_strdup_printf("%d %s %s %s\n",
+ process->service->log_process_internal_fd,
+ dec2str(process->pid),
+ default_fatal ? "DEFAULT-FATAL" : "FATAL", str);
+ if (write(process->service->list->master_log_fd[1],
+ data, strlen(data)) < 0) {
+ i_error("write(log process) failed: %m");
+ i_error("%s", str);
+ }
+}
+
+void service_process_log_status_error(struct service_process *process,
+ int status)
+{
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
+ /* fast path */
+ return;
+ }
+ T_BEGIN {
+ string_t *str = t_str_new(256);
+ bool default_fatal;
+
+ service_process_get_status_error(str, process, status,
+ &default_fatal);
+ if (str_len(str) > 0)
+ service_process_log(process, default_fatal, str_c(str));
+ } T_END;
+}
diff --git a/src/master/service-process.h b/src/master/service-process.h
new file mode 100644
index 0000000..f3e47bb
--- /dev/null
+++ b/src/master/service-process.h
@@ -0,0 +1,56 @@
+#ifndef SERVICE_PROCESS_H
+#define SERVICE_PROCESS_H
+
+struct service_process {
+ struct service_process *prev, *next;
+ struct service *service;
+ int refcount;
+
+ 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;
+ /* Approximate number of connections process has ever accepted.
+ This isn't exact, because its calculation is based on
+ available_count updates, which aren't done on every single
+ connection/disconnection. With a busy process it might be a lot
+ smaller than the correct value. */
+ unsigned int total_count;
+
+ /* Time when process started idling, or 0 if we're not idling. This is
+ updated when the process sends a notification via its status pipe
+ about the number of clients it is processing.
+
+ This field also determines whether the process is in the service's
+ "busy" or "idle" processes linked list. */
+ time_t idle_start;
+ /* Timeout for processing idle-kill SIGINT. Either the process will die
+ or it sends a notification about not being idling anymore */
+ struct timeout *to_idle_kill;
+
+ /* time when we last received a status update */
+ time_t last_status_update;
+ /* time when we last sent SIGINT to process */
+ time_t last_kill_sent;
+
+ /* kill the process if it doesn't send initial status notification */
+ struct timeout *to_status;
+
+ bool destroyed:1;
+};
+
+#define SERVICE_PROCESS_IS_INITIALIZED(process) \
+ ((process)->to_status == NULL)
+
+struct service_process *service_process_create(struct service *service);
+void service_process_destroy(struct service_process *process);
+
+void service_process_ref(struct service_process *process);
+void service_process_unref(struct service_process *process);
+
+void service_process_log_status_error(struct service_process *process,
+ int status);
+
+#endif
diff --git a/src/master/service.c b/src/master/service.c
new file mode 100644
index 0000000..e6b9eb6
--- /dev/null
+++ b/src/master/service.c
@@ -0,0 +1,769 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "common.h"
+#include "ioloop.h"
+#include "array.h"
+#include "aqueue.h"
+#include "hash.h"
+#include "str.h"
+#include "net.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "service.h"
+#include "service-anvil.h"
+#include "service-process.h"
+#include "service-monitor.h"
+
+#include <unistd.h>
+#include <signal.h>
+
+#define SERVICE_DIE_TIMEOUT_MSECS (1000*6)
+#define SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS 2
+
+HASH_TABLE_TYPE(pid_process) service_pids;
+
+void service_error(struct service *service, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ i_error("service(%s): %s", service->set->name,
+ t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+static struct service_listener *
+service_create_file_listener(struct service *service,
+ enum service_listener_type type,
+ const struct file_listener_settings *set,
+ const char **error_r)
+{
+ struct service_listener *l;
+ const char *set_name;
+ gid_t gid;
+
+ l = p_new(service->list->pool, struct service_listener, 1);
+ l->service = service;
+ l->type = type;
+ l->fd = -1;
+ l->set.fileset.set = set;
+ l->name = strrchr(set->path, '/');
+ if (l->name != NULL)
+ l->name++;
+ else
+ l->name = set->path;
+
+ if (get_uidgid(set->user, &l->set.fileset.uid, &gid, error_r) < 0)
+ set_name = "user";
+ else if (get_gid(set->group, &l->set.fileset.gid, error_r) < 0)
+ set_name = "group";
+ else
+ return l;
+
+ *error_r = t_strdup_printf(
+ "%s (See service %s { %s_listener %s { %s } } setting)",
+ *error_r, service->set->name,
+ type == SERVICE_LISTENER_UNIX ? "unix" : "fifo",
+ set->path, set_name);
+ return NULL;
+}
+
+static int
+resolve_ip(const char *address, const struct ip_addr **ips_r,
+ unsigned int *ips_count_r, const char **error_r)
+{
+ struct ip_addr *ip_list;
+ unsigned int ips_count;
+ int ret;
+
+ if (address == NULL || strcmp(address, "*") == 0) {
+ /* IPv4 any */
+ ip_list = t_new(struct ip_addr, 1);
+ *ip_list = net_ip4_any;
+ *ips_r = ip_list;
+ *ips_count_r = 1;
+ return 0;
+ }
+
+ if (strcmp(address, "::") == 0 || strcmp(address, "[::]") == 0) {
+ /* IPv6 any */
+ ip_list = t_new(struct ip_addr, 1);
+ *ip_list = net_ip6_any;
+ *ips_r = ip_list;
+ *ips_count_r = 1;
+ return 0;
+ }
+
+ /* Return the first IP if there happens to be multiple. */
+ ret = net_gethostbyname(address, &ip_list, &ips_count);
+ if (ret != 0) {
+ *error_r = t_strdup_printf("Can't resolve address %s: %s",
+ address, net_gethosterror(ret));
+ return -1;
+ }
+
+ if (ips_count < 1) {
+ *error_r = t_strdup_printf("No IPs for address: %s", address);
+ return -1;
+ }
+
+ *ips_r = ip_list;
+ *ips_count_r = ips_count;
+ return 0;
+}
+
+static struct service_listener *
+service_create_one_inet_listener(struct service *service,
+ const struct inet_listener_settings *set,
+ const char *address, const struct ip_addr *ip)
+{
+ struct service_listener *l;
+
+ i_assert(set->port != 0);
+
+ l = p_new(service->list->pool, struct service_listener, 1);
+ l->service = service;
+ l->type = SERVICE_LISTENER_INET;
+ l->fd = -1;
+ l->set.inetset.set = set;
+ l->set.inetset.ip = *ip;
+ l->inet_address = p_strdup(service->list->pool, address);
+ l->name = set->name;
+
+ return l;
+}
+
+static int
+service_create_inet_listeners(struct service *service,
+ const struct inet_listener_settings *set,
+ const char **error_r)
+{
+ static struct service_listener *l;
+ const char *const *tmp, *addresses;
+ const struct ip_addr *ips;
+ unsigned int i, ips_count;
+ bool ssl_disabled = strcmp(service->set->master_set->ssl, "no") == 0;
+
+ if (set->port == 0) {
+ /* disabled */
+ return 0;
+ }
+
+ if (*set->address != '\0')
+ addresses = set->address;
+ else {
+ /* use the default listen address */
+ addresses = service->set->master_set->listen;
+ }
+
+ tmp = t_strsplit_spaces(addresses, ", ");
+ for (; *tmp != NULL; tmp++) {
+ const char *address = *tmp;
+
+ if (set->ssl && ssl_disabled)
+ continue;
+
+ if (resolve_ip(address, &ips, &ips_count, error_r) < 0)
+ return -1;
+
+ for (i = 0; i < ips_count; i++) {
+ l = service_create_one_inet_listener(service, set,
+ address, &ips[i]);
+ array_push_back(&service->listeners, &l);
+ }
+ service->have_inet_listeners = TRUE;
+ }
+ return 0;
+}
+
+static int service_get_groups(const char *groups, pool_t pool,
+ const char **gids_r, const char **error_r)
+{
+ const char *const *tmp;
+ string_t *str;
+ gid_t gid;
+
+ str = t_str_new(64);
+ for (tmp = t_strsplit(groups, ","); *tmp != NULL; tmp++) {
+ if (get_gid(*tmp, &gid, error_r) < 0)
+ return -1;
+
+ if (str_len(str) > 0)
+ str_append_c(str, ',');
+ str_append(str, dec2str(gid));
+ }
+ *gids_r = p_strdup(pool, str_c(str));
+ return 0;
+}
+
+static struct service *
+service_create(pool_t pool, const struct service_settings *set,
+ struct service_list *service_list, const char **error_r)
+{
+ struct file_listener_settings *const *unix_listeners;
+ struct file_listener_settings *const *fifo_listeners;
+ struct inet_listener_settings *const *inet_listeners;
+ struct service *service;
+ struct service_listener *l;
+ unsigned int i, unix_count, fifo_count, inet_count;
+
+ service = p_new(pool, struct service, 1);
+ service->list = service_list;
+ service->set = set;
+ service->throttle_msecs = SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS;
+
+ service->client_limit = set->client_limit != 0 ? set->client_limit :
+ set->master_set->default_client_limit;
+ if (set->service_count > 0 &&
+ service->client_limit > set->service_count)
+ service->client_limit = set->service_count;
+
+ service->vsz_limit = set->vsz_limit != UOFF_T_MAX ? set->vsz_limit :
+ set->master_set->default_vsz_limit;
+ service->idle_kill = set->idle_kill != 0 ? set->idle_kill :
+ set->master_set->default_idle_kill;
+ service->type = service->set->parsed_type;
+
+ if (set->process_limit == 0) {
+ /* use default */
+ service->process_limit =
+ set->master_set->default_process_limit;
+ } else {
+ service->process_limit = set->process_limit;
+ }
+
+ /* default gid to user's primary group */
+ if (get_uidgid(set->user, &service->uid, &service->gid, error_r) < 0) {
+ switch (set->user_default) {
+ case SERVICE_USER_DEFAULT_NONE:
+ *error_r = t_strdup_printf(
+ "%s (See service %s { user } setting)",
+ *error_r, set->name);
+ break;
+ case SERVICE_USER_DEFAULT_INTERNAL:
+ *error_r = t_strconcat(*error_r,
+ " (See default_internal_user setting)", NULL);
+ break;
+ case SERVICE_USER_DEFAULT_LOGIN:
+ *error_r = t_strconcat(*error_r,
+ " (See default_login_user setting)", NULL);
+ break;
+ }
+ return NULL;
+ }
+ if (*set->group != '\0') {
+ if (get_gid(set->group, &service->gid, error_r) < 0) {
+ *error_r = t_strdup_printf(
+ "%s (See service %s { group } setting)",
+ *error_r, set->name);
+ return NULL;
+ }
+ }
+ if (get_gid(set->privileged_group, &service->privileged_gid,
+ error_r) < 0) {
+ *error_r = t_strdup_printf(
+ "%s (See service %s { privileged_group } setting)",
+ *error_r, set->name);
+ return NULL;
+ }
+
+ if (*set->extra_groups != '\0') {
+ if (service_get_groups(set->extra_groups, pool,
+ &service->extra_gids, error_r) < 0) {
+ *error_r = t_strdup_printf(
+ "%s (See service %s { extra_groups } setting)",
+ *error_r, set->name);
+ return NULL;
+ }
+ }
+
+ /* set these later, so if something fails we don't have to worry about
+ closing them */
+ service->log_fd[0] = -1;
+ service->log_fd[1] = -1;
+ service->status_fd[0] = -1;
+ service->status_fd[1] = -1;
+ service->master_dead_pipe_fd[0] = -1;
+ service->master_dead_pipe_fd[1] = -1;
+ service->log_process_internal_fd = -1;
+ service->login_notify_fd = -1;
+
+ if (service->type == SERVICE_TYPE_ANVIL) {
+ service->status_fd[0] = service_anvil_global->status_fd[0];
+ service->status_fd[1] = service_anvil_global->status_fd[1];
+ }
+
+ if (array_is_created(&set->unix_listeners))
+ unix_listeners = array_get(&set->unix_listeners, &unix_count);
+ else {
+ unix_listeners = NULL;
+ unix_count = 0;
+ }
+ if (array_is_created(&set->fifo_listeners))
+ fifo_listeners = array_get(&set->fifo_listeners, &fifo_count);
+ else {
+ fifo_listeners = NULL;
+ fifo_count = 0;
+ }
+ if (array_is_created(&set->inet_listeners))
+ inet_listeners = array_get(&set->inet_listeners, &inet_count);
+ else {
+ inet_listeners = NULL;
+ inet_count = 0;
+ }
+
+ if (unix_count == 0 && service->type == SERVICE_TYPE_CONFIG) {
+ *error_r = "Service must have unix listeners";
+ return NULL;
+ }
+
+ p_array_init(&service->listeners, pool,
+ unix_count + fifo_count + inet_count);
+
+ for (i = 0; i < unix_count; i++) {
+ if (unix_listeners[i]->mode == 0) {
+ /* disabled */
+ continue;
+ }
+
+ l = service_create_file_listener(service, SERVICE_LISTENER_UNIX,
+ unix_listeners[i], error_r);
+ if (l == NULL)
+ return NULL;
+ array_push_back(&service->listeners, &l);
+ }
+ for (i = 0; i < fifo_count; i++) {
+ if (fifo_listeners[i]->mode == 0) {
+ /* disabled */
+ continue;
+ }
+
+ l = service_create_file_listener(service, SERVICE_LISTENER_FIFO,
+ fifo_listeners[i], error_r);
+ if (l == NULL)
+ return NULL;
+ array_push_back(&service->listeners, &l);
+ }
+ for (i = 0; i < inet_count; i++) {
+ if (service_create_inet_listeners(service, inet_listeners[i],
+ error_r) < 0)
+ return NULL;
+ }
+
+ service->executable = set->executable;
+ if (access(t_strcut(service->executable, ' '), X_OK) < 0) {
+ *error_r = t_strdup_printf("access(%s) failed: %m",
+ t_strcut(service->executable, ' '));
+ return NULL;
+ }
+ return service;
+}
+
+struct service *
+service_lookup(struct service_list *service_list, const char *name)
+{
+ struct service *service;
+
+ array_foreach_elem(&service_list->services, service) {
+ if (strcmp(service->set->name, name) == 0)
+ return service;
+ }
+ return NULL;
+}
+
+struct service *
+service_lookup_type(struct service_list *service_list, enum service_type type)
+{
+ struct service *service;
+
+ array_foreach_elem(&service_list->services, service) {
+ if (service->type == type)
+ return service;
+ }
+ return NULL;
+}
+
+static bool service_want(struct service_settings *set)
+{
+ char *const *proto;
+
+ if (*set->executable == '\0') {
+ /* silently allow service {} blocks for disabled extensions
+ (e.g. service managesieve {} block without pigeonhole
+ installed) */
+ return FALSE;
+ }
+
+ if (*set->protocol == '\0')
+ return TRUE;
+
+ for (proto = set->master_set->protocols_split; *proto != NULL; proto++) {
+ if (strcmp(*proto, set->protocol) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+services_create_real(const struct master_settings *set, pool_t pool,
+ struct service_list **services_r, const char **error_r)
+{
+ struct service_list *service_list;
+ struct service *service;
+ struct service_settings *const *service_settings;
+ const char *error;
+ unsigned int i, count;
+
+ service_list = p_new(pool, struct service_list, 1);
+ service_list->refcount = 1;
+ service_list->pool = pool;
+ service_list->service_set = master_service_settings_get(master_service);
+ service_list->set_pool = master_service_settings_detach(master_service);
+ service_list->set = set;
+ service_list->master_log_fd[0] = -1;
+ service_list->master_log_fd[1] = -1;
+ service_list->master_fd = -1;
+
+ service_settings = array_get(&set->services, &count);
+ p_array_init(&service_list->services, pool, count);
+
+ for (i = 0; i < count; i++) {
+ if (!service_want(service_settings[i]))
+ continue;
+ T_BEGIN {
+ service = service_create(pool, service_settings[i],
+ service_list, &error);
+ } T_END_PASS_STR_IF(service == NULL, &error);
+ if (service == NULL) {
+ *error_r = t_strdup_printf("service(%s) %s",
+ service_settings[i]->name, error);
+ return -1;
+ }
+
+ switch (service->type) {
+ case SERVICE_TYPE_LOG:
+ if (service_list->log != NULL) {
+ *error_r = "Multiple log services specified";
+ return -1;
+ }
+ service_list->log = service;
+ break;
+ case SERVICE_TYPE_CONFIG:
+ if (service_list->config != NULL) {
+ *error_r = "Multiple config services specified";
+ return -1;
+ }
+ service_list->config = service;
+ break;
+ case SERVICE_TYPE_ANVIL:
+ if (service_list->anvil != NULL) {
+ *error_r = "Multiple anvil services specified";
+ return -1;
+ }
+ service_list->anvil = service;
+ break;
+ default:
+ break;
+ }
+
+ array_push_back(&service_list->services, &service);
+ }
+
+ if (service_list->log == NULL) {
+ *error_r = "log service not specified";
+ return -1;
+ }
+
+ if (service_list->config == NULL) {
+ *error_r = "config process not specified";
+ return -1;
+ }
+
+ *services_r = service_list;
+ return 0;
+}
+
+int services_create(const struct master_settings *set,
+ struct service_list **services_r, const char **error_r)
+{
+ pool_t pool;
+
+ pool = pool_alloconly_create("services pool", 32768);
+ if (services_create_real(set, pool, services_r, error_r) < 0) {
+ pool_unref(&pool);
+ return -1;
+ }
+ return 0;
+}
+
+static unsigned int
+service_signal_processes(struct service *service, int signo,
+ struct service_process *processes,
+ unsigned int *uninitialized_count)
+{
+ struct service_process *process;
+ unsigned int count = 0;
+
+ for (process = processes; process != NULL; process = process->next) {
+ i_assert(process->service == service);
+
+ if (!SERVICE_PROCESS_IS_INITIALIZED(process) &&
+ signo != SIGKILL) {
+ /* too early to signal it */
+ *uninitialized_count += 1;
+ continue;
+ }
+
+ if (kill(process->pid, signo) == 0)
+ count++;
+ else if (errno != ESRCH) {
+ service_error(service, "kill(%s, %d) failed: %m",
+ dec2str(process->pid), signo);
+ }
+ }
+ if (count > 0 && signo != SIGUSR1) {
+ i_warning("Sent %s to %u %s processes",
+ signo == SIGTERM ? "SIGTERM" : "SIGKILL",
+ count, service->set->name);
+ }
+ return count;
+}
+
+unsigned int service_signal(struct service *service, int signo,
+ unsigned int *uninitialized_count_r)
+{
+ unsigned int count = 0;
+
+ *uninitialized_count_r = 0;
+ count = service_signal_processes(service, signo,
+ service->busy_processes,
+ uninitialized_count_r);
+ count += service_signal_processes(service, signo,
+ service->idle_processes_head,
+ uninitialized_count_r);
+ return count;
+}
+
+static void service_login_notify_send(struct service *service)
+{
+ unsigned int uninitialized_count;
+
+ service->last_login_notify_time = ioloop_time;
+ timeout_remove(&service->to_login_notify);
+
+ service_signal(service, SIGUSR1, &uninitialized_count);
+}
+
+static void service_login_notify_timeout(struct service *service)
+{
+ service_login_notify_send(service);
+}
+
+void service_login_notify(struct service *service, bool all_processes_full)
+{
+ enum master_login_state state;
+ int diff;
+
+ if (service->last_login_full_notify == all_processes_full ||
+ service->login_notify_fd == -1)
+ return;
+
+ /* change the state always immediately. it's cheap. */
+ service->last_login_full_notify = all_processes_full;
+ state = all_processes_full ? MASTER_LOGIN_STATE_FULL :
+ MASTER_LOGIN_STATE_NONFULL;
+ if (lseek(service->login_notify_fd, state, SEEK_SET) < 0)
+ service_error(service, "lseek(notify fd) failed: %m");
+
+ /* but don't send signal to processes too often */
+ diff = ioloop_time - service->last_login_notify_time;
+ if (diff < SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS) {
+ if (service->to_login_notify != NULL)
+ return;
+
+ diff = (SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS - diff) * 1000;
+ service->to_login_notify =
+ timeout_add(diff, service_login_notify_timeout,
+ service);
+ } else {
+ service_login_notify_send(service);
+ }
+}
+
+static void services_kill_timeout(struct service_list *service_list)
+{
+ struct service *service, *log_service;
+ unsigned int service_uninitialized, uninitialized_count = 0;
+ unsigned int signal_count = 0;
+ int sig;
+
+ if (!service_list->sigterm_sent)
+ sig = SIGTERM;
+ else
+ sig = SIGKILL;
+ service_list->sigterm_sent = TRUE;
+
+ log_service = NULL;
+ array_foreach_elem(&service_list->services, service) {
+ if (service->type == SERVICE_TYPE_LOG)
+ log_service = service;
+ else {
+ signal_count += service_signal(service, sig,
+ &service_uninitialized);
+ uninitialized_count += service_uninitialized;
+ }
+ }
+ if (log_service == NULL) {
+ /* log service doesn't exist - shouldn't really happen */
+ } else if (signal_count > 0 || uninitialized_count > 0) {
+ /* kill log service later so the last remaining processes
+ can still have a chance of logging something */
+ } else {
+ if (!service_list->sigterm_sent_to_log)
+ sig = SIGTERM;
+ else
+ sig = SIGKILL;
+ service_list->sigterm_sent_to_log = TRUE;
+ signal_count += service_signal(log_service, sig, &service_uninitialized);
+ uninitialized_count += service_uninitialized;
+ }
+ if (signal_count > 0) {
+ string_t *str = t_str_new(128);
+ str_printfa(str, "Processes aren't dying after reload, "
+ "sent %s to %u processes.",
+ sig == SIGTERM ? "SIGTERM" : "SIGKILL", signal_count);
+ if (uninitialized_count > 0) {
+ str_printfa(str, " (%u processes still uninitialized)",
+ uninitialized_count);
+ }
+ i_warning("%s", str_c(str));
+ }
+}
+
+void services_destroy(struct service_list *service_list, bool wait)
+{
+ /* make sure we log if child processes died unexpectedly */
+ service_list->destroying = TRUE;
+ services_monitor_reap_children();
+
+ services_monitor_stop(service_list, wait);
+
+ if (service_list->refcount > 1 &&
+ service_list->service_set->shutdown_clients) {
+ service_list->to_kill =
+ timeout_add(SERVICE_DIE_TIMEOUT_MSECS,
+ services_kill_timeout, service_list);
+ }
+
+ service_list->destroyed = TRUE;
+ service_list_unref(service_list);
+}
+
+void service_list_ref(struct service_list *service_list)
+{
+ i_assert(service_list->refcount > 0);
+ service_list->refcount++;
+}
+
+void service_list_unref(struct service_list *service_list)
+{
+ struct service *service;
+ struct service_listener *listener;
+
+ i_assert(service_list->refcount > 0);
+ if (--service_list->refcount > 0)
+ return;
+
+ array_foreach_elem(&service_list->services, service) {
+ array_foreach_elem(&service->listeners, listener)
+ i_close_fd(&listener->fd);
+ }
+ i_close_fd(&service_list->master_fd);
+
+ timeout_remove(&service_list->to_kill);
+ pool_unref(&service_list->set_pool);
+ pool_unref(&service_list->pool);
+}
+
+const char *services_get_config_socket_path(struct service_list *service_list)
+{
+ struct service_listener *const *listeners;
+ unsigned int count;
+
+ listeners = array_get(&service_list->config->listeners, &count);
+ i_assert(count > 0);
+ return listeners[0]->set.fileset.set->path;
+}
+
+static void service_throttle_timeout(struct service *service)
+{
+ timeout_remove(&service->to_throttle);
+ service_monitor_listen_start(service);
+}
+
+static void service_drop_listener_connections(struct service *service)
+{
+ struct service_listener *listener;
+ int fd;
+
+ array_foreach_elem(&service->listeners, listener) {
+ switch (listener->type) {
+ case SERVICE_LISTENER_UNIX:
+ case SERVICE_LISTENER_INET:
+ if (listener->fd == -1) {
+ /* already stopped listening */
+ break;
+ }
+ while ((fd = net_accept(listener->fd,
+ NULL, NULL)) >= 0)
+ i_close_fd(&fd);
+ break;
+ case SERVICE_LISTENER_FIFO:
+ break;
+ }
+ }
+}
+
+void service_throttle(struct service *service, unsigned int msecs)
+{
+ if (service->to_throttle != NULL || service->list->destroyed)
+ return;
+
+ if (service->busy_processes == NULL &&
+ service->idle_processes_head == NULL)
+ service_drop_listener_connections(service);
+
+ service_monitor_listen_stop(service);
+ service->to_throttle = timeout_add(msecs, service_throttle_timeout,
+ service);
+}
+
+void services_throttle_time_sensitives(struct service_list *list,
+ unsigned int msecs)
+{
+ struct service *service;
+
+ array_foreach_elem(&list->services, service) {
+ if (service->type == SERVICE_TYPE_UNKNOWN)
+ service_throttle(service, msecs);
+ }
+}
+
+void service_pids_init(void)
+{
+ hash_table_create_direct(&service_pids, default_pool, 0);
+}
+
+void service_pids_deinit(void)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct service_process *process;
+
+ /* free all child process information */
+ iter = hash_table_iterate_init(service_pids);
+ while (hash_table_iterate(iter, service_pids, &key, &process))
+ service_process_destroy(process);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&service_pids);
+}
diff --git a/src/master/service.h b/src/master/service.h
new file mode 100644
index 0000000..c6ea604
--- /dev/null
+++ b/src/master/service.h
@@ -0,0 +1,220 @@
+#ifndef SERVICE_H
+#define SERVICE_H
+
+#include "net.h"
+#include "master-settings.h"
+
+/* If a service process doesn't send its first status notification in
+ this many seconds, kill the process */
+#define SERVICE_FIRST_STATUS_TIMEOUT_SECS 30
+
+#define SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS (2*1000)
+#define SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS (60*1000)
+
+enum service_listener_type {
+ SERVICE_LISTENER_UNIX,
+ SERVICE_LISTENER_FIFO,
+ SERVICE_LISTENER_INET
+};
+
+struct service_listener {
+ struct service *service;
+
+ enum service_listener_type type;
+ int fd; /* may be -1 */
+ struct io *io;
+
+ const char *name;
+ const char *inet_address;
+
+ union {
+ struct {
+ const struct file_listener_settings *set;
+ uid_t uid;
+ gid_t gid;
+ } fileset;
+ struct {
+ const struct inet_listener_settings *set;
+ struct ip_addr ip;
+ } inetset;
+ } set;
+
+ bool reuse_port;
+};
+
+struct service {
+ struct service_list *list;
+
+ enum service_type type;
+
+ const struct service_settings *set;
+ const char *config_file_path;
+
+ const char *executable;
+ uid_t uid;
+ gid_t gid;
+ gid_t privileged_gid;
+ const char *extra_gids; /* comma-separated list */
+
+ /* all listeners, even those that aren't currently listening */
+ ARRAY(struct service_listener *) listeners;
+ /* linked list of processes belonging to this service, which have
+ idle_start == 0. */
+ struct service_process *busy_processes;
+ /* linked list of processes belonging to this service, which have
+ ldle_start != 0. */
+ struct service_process *idle_processes_head, *idle_processes_tail;
+
+ /* number of processes currently created for this service */
+ unsigned int process_count;
+ /* number of processes currently accepting new clients */
+ unsigned int process_avail;
+ /* number of processes currently idling (idle_start != 0) */
+ unsigned int process_idling;
+ /* Lowest number of processes that have been idling at the same time.
+ This is reset to process_idling every idle_kill seconds. */
+ unsigned int process_idling_lowwater_since_kills;
+ /* max number of processes allowed */
+ unsigned int process_limit;
+ /* Total number of processes ever created */
+ uint64_t process_count_total;
+
+ /* Maximum number of client connections a process can handle. */
+ unsigned int client_limit;
+ /* Kill idling processes after this many seconds. */
+ unsigned int idle_kill;
+ /* set->vsz_limit or set->master_set->default_client_limit */
+ uoff_t vsz_limit;
+
+ /* log process pipe file descriptors. */
+ int log_fd[2];
+ /* fd that log process sees log_fd[0] as. can be used to identify
+ service name when sending commands via master_log_fd. */
+ int log_process_internal_fd;
+
+ /* status report pipe file descriptors */
+ int status_fd[2];
+ struct io *io_status;
+
+ int master_dead_pipe_fd[2];
+
+ unsigned int throttle_msecs;
+ time_t exit_failure_last;
+ unsigned int exit_failures_in_sec;
+
+ /* Login process's notify fd. We change its seek position to
+ communicate state to login processes. */
+ int login_notify_fd;
+ time_t last_login_notify_time;
+ struct timeout *to_login_notify;
+
+ /* if a process fails before servicing its first request, assume it's
+ broken and start throttling new process creations */
+ struct timeout *to_throttle;
+ /* when process_limit is reached, wait for a while until we actually
+ start dropping pending connections */
+ struct timeout *to_drop;
+ /* delayed process_limit reached warning with SERVICE_TYPE_WORKER */
+ struct timeout *to_drop_warning;
+ /* next time to try to kill idling processes */
+ struct timeout *to_idle;
+
+ /* prefork processes up to process_min_avail if there's time */
+ struct timeout *to_prefork;
+ unsigned int prefork_counter;
+
+ /* Last time a "dropping client connections" warning was logged */
+ time_t last_drop_warning;
+
+ /* all processes are in use and new connections are coming */
+ bool listen_pending:1;
+ /* service is currently listening for new connections */
+ bool listening:1;
+ /* TRUE if service has at least one inet_listener */
+ bool have_inet_listeners:1;
+ /* service_login_notify()'s last notification state */
+ bool last_login_full_notify:1;
+ /* service has exited at least once with exit code 0 */
+ bool have_successful_exits:1;
+ /* service was stopped via doveadm */
+ bool doveadm_stop:1;
+};
+
+struct service_list {
+ pool_t pool;
+ pool_t set_pool;
+ int refcount;
+ struct timeout *to_kill;
+ unsigned int fork_counter;
+
+ const struct master_settings *set;
+ const struct master_service_settings *service_set;
+
+ struct service *config;
+ struct service *log;
+ struct service *anvil;
+
+ struct file_listener_settings master_listener_set;
+ struct io *io_master;
+ int master_fd;
+
+ /* nonblocking log fds usd by master */
+ int master_log_fd[2];
+ struct service_process_notify *log_byes;
+
+ ARRAY(struct service *) services;
+
+ bool destroying:1;
+ bool destroyed:1;
+ bool sigterm_sent:1;
+ bool sigterm_sent_to_log:1;
+};
+
+HASH_TABLE_DEFINE_TYPE(pid_process, void *, struct service_process *);
+extern HASH_TABLE_TYPE(pid_process) service_pids;
+
+/* Create all services from settings */
+int services_create(const struct master_settings *set,
+ struct service_list **services_r, const char **error_r);
+
+/* Destroy services */
+void services_destroy(struct service_list *service_list, bool wait);
+
+void service_list_ref(struct service_list *service_list);
+void service_list_unref(struct service_list *service_list);
+
+/* Return path to configuration process socket. */
+const char *services_get_config_socket_path(struct service_list *service_list);
+
+/* Send a signal to all processes in a given service. However, if we're sending
+ a SIGTERM and a process hasn't yet sent the initial status notification,
+ that process is skipped. The number of such skipped processes are stored in
+ uninitialized_count_r. Returns the number of processes that a signal was
+ successfully sent to. */
+unsigned int service_signal(struct service *service, int signo,
+ unsigned int *uninitialized_count_r);
+/* Notify all processes (if necessary) that no more connections can be handled
+ by the service without killing existing connections (TRUE) or that they
+ can be (FALSE). */
+void service_login_notify(struct service *service, bool all_processes_full);
+
+/* Prevent service from launching new processes for a while. */
+void service_throttle(struct service *service, unsigned int msecs);
+/* Time moved backwards. Throttle services that care about time. */
+void services_throttle_time_sensitives(struct service_list *list,
+ unsigned int msecs);
+
+/* Find service by name. */
+struct service *
+service_lookup(struct service_list *service_list, const char *name);
+/* Find service by type */
+struct service *
+service_lookup_type(struct service_list *service_list, enum service_type type);
+
+void service_error(struct service *service, const char *format, ...)
+ ATTR_FORMAT(2, 3);
+
+void service_pids_init(void);
+void service_pids_deinit(void);
+
+#endif
diff --git a/src/master/test-auth-client.c b/src/master/test-auth-client.c
new file mode 100644
index 0000000..d161a5b
--- /dev/null
+++ b/src/master/test-auth-client.c
@@ -0,0 +1,1287 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "llist.h"
+#include "base64.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 "master-interface.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include "auth-client.h"
+
+#define TEST_SOCKET "./auth-client-test"
+#define CLIENT_PROGRESS_TIMEOUT 30
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+
+ void *context;
+
+ pool_t pool;
+};
+
+typedef void test_server_init_t(void);
+typedef bool test_client_init_t(void);
+
+/*
+ * State
+ */
+
+/* common */
+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 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(void);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_deinit(void);
+
+static int
+test_client_auth_parallel(const char *mech, const char *username,
+ const char *password, unsigned int concurrency,
+ bool retry, const char **error_r);
+static int
+test_client_auth_simple(const char *mech, const char *username,
+ const char *password, bool retry, const char **error_r);
+
+/* test*/
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test) ATTR_NULL(2);
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void test_server_connection_refused(void)
+{
+ i_close_fd(&fd_listen);
+ i_sleep_intr_secs(500);
+}
+
+/* client */
+
+static bool test_client_connection_refused(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ test_begin("connection refused");
+ test_expect_error_string("Connection refused");
+ test_run_client_server(test_client_connection_refused,
+ test_server_connection_refused);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_connection_timed_out_input(struct server_connection *conn)
+{
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_timed_out(void)
+{
+ test_server_input = test_connection_timed_out_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_connection_timed_out(void)
+{
+ time_t time;
+ const char *error;
+ int ret;
+
+ io_loop_time_refresh();
+ time = ioloop_time;
+
+ ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+
+ io_loop_time_refresh();
+ test_out("timeout", (ioloop_time - time) < 5);
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ test_begin("connection timed out");
+ test_expect_error_string("Timeout waiting for handshake");
+ test_run_client_server(test_client_connection_timed_out,
+ test_server_connection_timed_out);
+ test_end();
+}
+
+/*
+ * Bad VERSION
+ */
+
+/* server */
+
+static void test_bad_version_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_bad_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "VERSION\t666\t666\n"
+ "MECH\tPLAIN\tplaintext\n"
+ "MECH\tLOGIN\tplaintext\n"
+ "SPID\t12296\n"
+ "CUID\t2\n"
+ "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n"
+ "DONE\n");
+}
+
+static void test_server_bad_version(void)
+{
+ test_server_init = test_bad_version_init;
+ test_server_input = test_bad_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_bad_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+ return FALSE;
+}
+
+/* test */
+
+static void test_bad_version(void)
+{
+ test_begin("bad version");
+ test_expect_error_string("Socket supports major version 666");
+ test_run_client_server(test_client_bad_version,
+ test_server_bad_version);
+ test_end();
+}
+
+/*
+ * Disconnect VERSION
+ */
+
+/* server */
+
+static void test_disconnect_version_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_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "VERSION\t1\t2\n"
+ "MECH\tPLAIN\tplaintext\n"
+ "MECH\tLOGIN\tplaintext\n"
+ "SPID\t12296\n"
+ "CUID\t2\n"
+ "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n"
+ "DONE\n");
+}
+
+static void test_server_disconnect_version(void)
+{
+ test_server_init = test_disconnect_version_init;
+ test_server_input = test_disconnect_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_disconnect_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+ return FALSE;
+}
+
+/* test */
+
+static void test_disconnect_version(void)
+{
+ test_begin("disconnect version");
+ test_expect_errors(2);
+ test_run_client_server(test_client_disconnect_version,
+ test_server_disconnect_version);
+ test_end();
+}
+
+/*
+ * Auth handshake
+ */
+
+/* server */
+
+enum _auth_handshake_state {
+ AUTH_HANDSHAKE_STATE_VERSION = 0,
+ AUTH_HANDSHAKE_STATE_CMD
+};
+
+struct _auth_handshake_request {
+ struct _auth_handshake_request *prev, *next;
+
+ unsigned int id;
+ const char *username;
+
+ unsigned int login_state;
+};
+
+struct _auth_handshake_server {
+ enum _auth_handshake_state state;
+
+ struct _auth_handshake_request *requests;
+};
+
+static bool
+test_auth_handshake_auth_plain(struct server_connection *conn, unsigned int id,
+ const unsigned char *data, size_t data_size)
+{
+ const char *authid, *authenid;
+ const char *pass;
+ size_t i, len;
+ int count;
+
+ /* authorization ID \0 authentication ID \0 pass. */
+ authid = (const char *) data;
+ authenid = NULL; pass = NULL;
+
+ count = 0;
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == '\0') {
+ if (++count == 1)
+ authenid = (const char *)data + (i + 1);
+ else {
+ i++;
+ len = data_size - i;
+ pass = t_strndup(data+i, len);
+ break;
+ }
+ }
+ }
+
+ if (count != 2) {
+ i_error("Bad AUTH PLAIN request: Bad data");
+ return FALSE;
+ }
+
+ i_assert(authenid != NULL);
+ if (strcmp(authid, "supremelordoftheuniverse") != 0) {
+ /* unexpected authorization ID */
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("FAIL\t%u\tuser=%s\n", id, authenid));
+ return TRUE;
+ }
+ if (strcmp(authenid, "harrie") == 0 && strcmp(pass, "frop") == 0) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("OK\t%u\tuser=harrie\n", id));
+ return TRUE;
+ }
+ if (strcmp(authenid, "hendrik") == 0)
+ return FALSE;
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("FAIL\t%u\tuser=%s\n", id, authenid));
+
+ return TRUE;
+}
+
+static bool
+test_auth_handshake_auth_login(struct server_connection *conn, unsigned int id,
+ const unsigned char *data ATTR_UNUSED,
+ size_t data_size)
+{
+ static const char *prompt1 = "Username:";
+ struct _auth_handshake_server *ctx =
+ (struct _auth_handshake_server *)conn->context;
+ struct _auth_handshake_request *req;
+ string_t *chal_b64;
+
+ if (data_size != 0) {
+ i_error("Bad AUTH PLAIN request: "
+ "Not expecting initial response");
+ return FALSE;
+ }
+
+ req = p_new(conn->pool, struct _auth_handshake_request, 1);
+ req->id = id;
+ DLLIST_PREPEND(&ctx->requests, req);
+
+ chal_b64 = t_str_new(64);
+ base64_encode(prompt1, strlen(prompt1), chal_b64);
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("CONT\t%u\t%s\n", id, str_c(chal_b64)));
+ return TRUE;
+}
+
+static bool
+test_auth_handshake_cont_login(struct server_connection *conn,
+ struct _auth_handshake_request *req,
+ const unsigned char *data, size_t data_size)
+{
+ static const char *prompt2 = "Password:";
+ struct _auth_handshake_server *ctx =
+ (struct _auth_handshake_server *)conn->context;
+ const char *resp = t_strndup(data, data_size);
+ string_t *chal_b64;
+
+ if (++req->login_state == 1) {
+ req->username = p_strdup(conn->pool, resp);
+ if (strcmp(resp, "harrie") != 0) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("FAIL\t%u\tuser=%s\n",
+ req->id, req->username));
+ return TRUE;
+ }
+ } else {
+ i_assert(req->login_state == 2);
+ DLLIST_REMOVE(&ctx->requests, req);
+ if (strcmp(resp, "frop") != 0) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("FAIL\t%u\tuser=%s\n",
+ req->id, req->username));
+ return TRUE;
+ }
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("OK\t%u\tuser=harrie\n", req->id));
+ return TRUE;
+ }
+
+ chal_b64 = t_str_new(64);
+ base64_encode(prompt2, strlen(prompt2), chal_b64);
+ o_stream_nsend_str(conn->conn.output,
+ t_strdup_printf("CONT\t%u\t%s\n",
+ req->id, str_c(chal_b64)));
+ return TRUE;
+}
+
+
+static bool
+test_auth_handshake_auth(struct server_connection *conn, unsigned int id,
+ const char *const *args)
+{
+ const char *mech, *resp;
+ unsigned int i;
+ buffer_t *data;
+
+ if (args[0] == NULL) {
+ i_error("Bad AUTH request");
+ return FALSE;
+ }
+ mech = args[0];
+ resp = NULL;
+ for (i = 1; args[i] != NULL; i++) {
+ if (str_begins(args[i], "resp=")) {
+ resp = t_strdup(args[i] + 5);
+ break;
+ }
+ }
+ data = t_buffer_create(256);
+ if (resp != NULL) {
+ if (base64_decode(resp, strlen(resp), NULL, data) < 0) {
+ i_error("Bad AUTH request: Bad base64");
+ return FALSE;
+ }
+ }
+
+ if (strcasecmp(mech, "PLAIN") == 0) {
+ return test_auth_handshake_auth_plain(conn, id,
+ data->data, data->used);
+ } else if (strcasecmp(mech, "LOGIN") == 0) {
+ return test_auth_handshake_auth_login(conn, id,
+ data->data, data->used);
+ }
+ i_error("Bad AUTH request: Unknown mechanism");
+ return FALSE;
+}
+
+static bool
+test_auth_handshake_cont(struct server_connection *conn, unsigned int id,
+ const char *const *args)
+{
+ struct _auth_handshake_server *ctx =
+ (struct _auth_handshake_server *)conn->context;
+ struct _auth_handshake_request *req;
+ const char *resp;
+ buffer_t *data;
+
+ if (args[0] == NULL) {
+ i_error("Bad CONT request");
+ return FALSE;
+ }
+ resp = args[0];
+ data = t_buffer_create(256);
+ if (resp != NULL) {
+ if (base64_decode(resp, strlen(resp), NULL, data) < 0) {
+ i_error("Bad CONT request: Bad base64");
+ return FALSE;
+ }
+ }
+
+ req = ctx->requests;
+ while (req != NULL) {
+ if (req->id == id)
+ break;
+ req = req->next;
+ }
+
+ if (req == NULL) {
+ i_error("Bad CONT request: Bad request ID");
+ return FALSE;
+ }
+
+ return test_auth_handshake_cont_login(conn, req,
+ data->data, data->used);
+}
+
+static void test_auth_handshake_input(struct server_connection *conn)
+{
+ struct _auth_handshake_server *ctx =
+ (struct _auth_handshake_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 AUTH_HANDSHAKE_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = AUTH_HANDSHAKE_STATE_CMD;
+ continue;
+ case AUTH_HANDSHAKE_STATE_CMD:
+ args = t_strsplit_tabescaped(line);
+ if (args[0] == NULL || args[1] == NULL) {
+ i_error("Bad request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (str_to_uint(args[1], &id) < 0) {
+ i_error("Bad %s request", args[0]);
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ if (strcmp(args[0], "CPID") == 0) {
+ continue;
+ } else if (strcmp(args[0], "AUTH") == 0) {
+ if (test_auth_handshake_auth(conn, id,
+ args + 2))
+ continue;
+ } else if (strcmp(args[0], "CONT") == 0) {
+ if (test_auth_handshake_cont(conn, id,
+ args + 2))
+ continue;
+ } else {
+ i_error("Bad request: %s", args[0]);
+ server_connection_deinit(&conn);
+ return;
+ }
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_auth_handshake_init(struct server_connection *conn)
+{
+ struct _auth_handshake_server *ctx;
+
+ ctx = p_new(conn->pool, struct _auth_handshake_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(
+ conn->conn.output,
+ "VERSION\t1\t2\n"
+ "MECH\tPLAIN\tplaintext\n"
+ "MECH\tLOGIN\tplaintext\n"
+ "SPID\t12296\n"
+ "CUID\t2\n"
+ "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n"
+ "DONE\n");
+}
+
+static void test_server_auth_handshake(void)
+{
+ test_server_init = test_auth_handshake_init;
+ test_server_input = test_auth_handshake_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_auth_plain_disconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "hendrik", "frop", FALSE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Internal failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_plain_reconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "hendrik", "frop", TRUE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Internal failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_plain_failure(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "henk", "frop", FALSE, &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_plain_success(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_failure1(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("LOGIN", "henk", "frop", FALSE, &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_failure2(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("LOGIN", "harrie", "friep", FALSE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_success(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_simple("LOGIN", "harrie", "frop", FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_plain_parallel_failure(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_parallel("PLAIN", "henk", "frop", 4, FALSE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_plain_parallel_success(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_parallel("PLAIN", "harrie", "frop", 4, FALSE,
+ &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_parallel_failure1(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_parallel("LOGIN", "henk", "frop", 4, FALSE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_parallel_failure2(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_parallel("LOGIN", "harrie", "friep", 4, FALSE,
+ &error);
+ test_out("run (ret < 0)", ret < 0);
+ test_assert(error != NULL && strstr(error, "Login failure") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_auth_login_parallel_success(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_auth_parallel("LOGIN", "harrie", "frop", 4, FALSE,
+ &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_auth_handshake(void)
+{
+ test_begin("auth PLAIN disconnect");
+ test_expect_errors(1);
+ test_run_client_server(test_client_auth_plain_disconnect,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth PLAIN reconnect");
+ test_expect_errors(2);
+ test_run_client_server(test_client_auth_plain_reconnect,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth PLAIN failure");
+ test_run_client_server(test_client_auth_plain_failure,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth PLAIN success");
+ test_run_client_server(test_client_auth_plain_success,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN failure 1");
+ test_run_client_server(test_client_auth_login_failure1,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN failure 2");
+ test_run_client_server(test_client_auth_login_failure2,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN success");
+ test_run_client_server(test_client_auth_login_success,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth PLAIN parallel failure");
+ test_run_client_server(test_client_auth_plain_parallel_failure,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth PLAIN parallel success");
+ test_run_client_server(test_client_auth_plain_parallel_success,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN parallel failure 1");
+ test_run_client_server(test_client_auth_login_parallel_failure1,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN parallel failure 2");
+ test_run_client_server(test_client_auth_login_parallel_failure2,
+ test_server_auth_handshake);
+ test_end();
+
+ test_begin("auth LOGIN parallel success");
+ test_run_client_server(test_client_auth_login_parallel_success,
+ test_server_auth_handshake);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_connection_refused,
+ test_connection_timed_out,
+ test_bad_version,
+ test_disconnect_version,
+ test_auth_handshake,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+struct timeout *to_client_progress = NULL;
+
+static void test_client_deinit(void)
+{
+}
+
+struct login_request {
+ struct login_test *test;
+
+ unsigned int state;
+};
+
+struct login_test {
+ char *error;
+ int status;
+
+ const char *username;
+ const char *password;
+
+ unsigned int requests_pending;
+
+ struct ioloop *ioloop;
+};
+
+static void
+test_client_auth_callback(struct auth_client_request *request,
+ enum auth_request_status status,
+ const char *data_base64 ATTR_UNUSED,
+ const char *const *args ATTR_UNUSED, void *context)
+{
+ struct login_request *login_req = context;
+ struct login_test *login_test = login_req->test;
+ string_t *resp_b64;
+ const char *errormsg = NULL;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ switch (status) {
+ case AUTH_REQUEST_STATUS_ABORT:
+ errormsg = "Abort";
+ break;
+ case AUTH_REQUEST_STATUS_INTERNAL_FAIL:
+ errormsg = "Internal failure";
+ break;
+ case AUTH_REQUEST_STATUS_FAIL:
+ errormsg = "Login failure";
+ break;
+ case AUTH_REQUEST_STATUS_CONTINUE:
+ resp_b64 = t_str_new(64);
+ if (++login_req->state == 1) {
+ base64_encode(login_test->username,
+ strlen(login_test->username), resp_b64);
+ } else {
+ test_assert(login_req->state == 2);
+ base64_encode(login_test->password,
+ strlen(login_test->password), resp_b64);
+ }
+ auth_client_request_continue(request, str_c(resp_b64));
+ return;
+ case AUTH_REQUEST_STATUS_OK:
+ break;
+ }
+
+ if (login_test->status == 0 && errormsg != NULL) {
+ i_assert(login_test->error == NULL);
+ login_test->error = i_strdup(errormsg);
+ login_test->status = -1;
+ }
+
+ if (--login_test->requests_pending == 0)
+ io_loop_stop(login_test->ioloop);
+}
+
+static void
+test_client_auth_connected(struct auth_client *client ATTR_UNUSED,
+ bool connected, void *context)
+{
+ struct login_test *login_test = context;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ if (login_test->status == 0 && !connected) {
+ i_assert(login_test->error == NULL);
+ login_test->error = i_strdup("Connection failed");
+ login_test->status = -1;
+ }
+ io_loop_stop(login_test->ioloop);
+}
+
+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 int
+test_client_auth_run(struct auth_client *auth_client, struct ioloop *ioloop,
+ struct auth_request_info *info,
+ const char *username, const char *password,
+ unsigned int concurrency, const char **error_r)
+{
+ struct login_test login_test;
+ struct login_request *login_reqs;
+ unsigned int i;
+ int ret;
+
+ i_zero(&login_test);
+ login_test.ioloop = ioloop;
+ login_test.username = username;
+ login_test.password = password;
+
+ auth_client_set_connect_timeout(auth_client, 1000);
+ auth_client_connect(auth_client);
+ if (auth_client_is_disconnected(auth_client)) {
+ login_test.error = i_strdup("Connection failed");
+ login_test.status = -1;
+ } else {
+ auth_client_set_connect_notify(
+ auth_client, test_client_auth_connected, &login_test);
+ io_loop_run(ioloop);
+ }
+
+ if (login_test.status >= 0) {
+ io_loop_set_running(ioloop);
+ login_test.requests_pending = concurrency;
+ login_reqs = t_new(struct login_request, concurrency);
+ for (i = 0; i < concurrency; i++) {
+ login_reqs[i].test = &login_test;
+ (void)auth_client_request_new(auth_client, info,
+ test_client_auth_callback,
+ &login_reqs[i]);
+ }
+ if (io_loop_is_running(ioloop))
+ io_loop_run(ioloop);
+ }
+
+ ret = login_test.status;
+ *error_r = t_strdup(login_test.error);
+
+ i_free(login_test.error);
+ auth_client_set_connect_notify(auth_client, NULL, NULL);
+
+ return ret;
+}
+
+static int
+test_client_auth_parallel(const char *mech, const char *username,
+ const char *password, unsigned int concurrency,
+ bool retry, const char **error_r)
+{
+ struct auth_client *auth_client;
+ struct auth_request_info info;
+ struct ioloop *ioloop;
+ int ret;
+
+ i_zero(&info);
+ info.mech = mech;
+ info.service = "test";
+ info.session_id = "23423dfd243daaa223";
+ info.flags = AUTH_REQUEST_FLAG_SECURED;
+
+ (void)net_addr2ip("10.0.0.15", &info.local_ip);
+ info.local_port = 143;
+ (void)net_addr2ip("10.0.0.211", &info.remote_ip);
+ info.remote_port = 45546;
+ (void)net_addr2ip("10.1.0.54", &info.real_local_ip);
+ info.real_local_port = 143;
+ (void)net_addr2ip("10.1.0.221", &info.real_remote_ip);
+ info.real_remote_port = 23246;
+
+ if (strcasecmp(mech, "PLAIN") == 0) {
+ string_t *resp_b64, *resp;
+
+ resp = t_str_new(64);
+ str_append(resp, "supremelordoftheuniverse");
+ str_append_c(resp, '\0');
+ str_append(resp, username);
+ str_append_c(resp, '\0');
+ str_append(resp, password);
+
+ resp_b64 = t_str_new(64);
+ base64_encode(str_data(resp), str_len(resp), resp_b64);
+ info.initial_resp_base64 = str_c(resp_b64);
+ } else if (strcasecmp(mech, "LOGIN") == 0) {
+ /* no intial response */
+ } else {
+ i_unreached();
+ }
+
+ ioloop = io_loop_create();
+ to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+
+ auth_client = auth_client_init(TEST_SOCKET, 2234, debug);
+ ret = test_client_auth_run(auth_client, ioloop, &info,
+ username, password, concurrency,
+ error_r);
+ if (ret < 0 && retry) {
+ ret = test_client_auth_run(auth_client, ioloop, &info,
+ username, password, concurrency,
+ error_r);
+ }
+ auth_client_deinit(&auth_client);
+
+ timeout_remove(&to_client_progress);
+ io_loop_destroy(&ioloop);
+
+ return ret;
+}
+
+static int
+test_client_auth_simple(const char *mech, const char *username,
+ const char *password, bool retry, const char **error_r)
+{
+ return test_client_auth_parallel(mech, username, password, 1, retry,
+ 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(void)
+{
+ /* 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
+ */
+
+static int test_open_server_fd(void)
+{
+ int fd;
+ i_unlink_if_exists(TEST_SOCKET);
+ fd = net_listen_unix(TEST_SOCKET, 128);
+ if (debug)
+ i_debug("server listening on "TEST_SOCKET);
+ if (fd == -1)
+ i_fatal("listen("TEST_SOCKET") failed: %m");
+ return fd;
+}
+
+static int test_run_server(test_server_init_t *server_test)
+{
+ main_deinit();
+ master_service_deinit_forked(&master_service);
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ server_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ return 0;
+}
+
+static void test_run_client(test_client_init_t *client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_sleep_intr_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ if (client_test())
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test)
+{
+ if (server_test != NULL) {
+ /* Fork server */
+ fd_listen = test_open_server_fd();
+ test_subprocess_fork(test_run_server, server_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+}
+
+/*
+ * Main
+ */
+
+static void main_cleanup(void)
+{
+ i_unlink_if_exists(TEST_SOCKET);
+}
+
+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 |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT;
+ int c;
+ int ret;
+
+ master_service = master_service_init("test-auth-master", 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);
+ test_subprocess_set_cleanup_callback(main_cleanup);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ master_service_deinit(&master_service);
+
+ return ret;
+}
diff --git a/src/master/test-auth-master.c b/src/master/test-auth-master.c
new file mode 100644
index 0000000..08112d4
--- /dev/null
+++ b/src/master/test-auth-master.c
@@ -0,0 +1,1390 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.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 "master-interface.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include "auth-master.h"
+
+#define TEST_SOCKET "./auth-master-test"
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+
+ void *context;
+
+ pool_t pool;
+};
+
+typedef void test_server_init_t(void);
+typedef bool test_client_init_t(void);
+
+/*
+ * State
+ */
+
+/* common */
+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 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(void);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_deinit(void);
+
+static int
+test_client_passdb_lookup_simple(const char *user, bool retry,
+ const char **error_r);
+static int
+test_client_userdb_lookup_simple(const char *user, bool retry,
+ const char **error_r);
+static int test_client_user_list_simple(void);
+
+/* test*/
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test) ATTR_NULL(2);
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void test_server_connection_refused(void)
+{
+ i_close_fd(&fd_listen);
+ i_sleep_intr_secs(500);
+}
+
+/* client */
+
+static bool test_client_connection_refused(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ test_begin("connection refused");
+ test_expect_error_string("Connection refused");
+ test_run_client_server(test_client_connection_refused,
+ test_server_connection_refused);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_connection_timed_out_input(struct server_connection *conn)
+{
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_timed_out(void)
+{
+ test_server_input = test_connection_timed_out_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_connection_timed_out(void)
+{
+ time_t time;
+ const char *error;
+ int ret;
+
+ io_loop_time_refresh();
+ time = ioloop_time;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+
+ io_loop_time_refresh();
+ test_out("timeout", (ioloop_time - time) < 5);
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ test_begin("connection timed out");
+ test_expect_error_string("Connecting timed out");
+ test_run_client_server(test_client_connection_timed_out,
+ test_server_connection_timed_out);
+ test_end();
+}
+
+/*
+ * Bad VERSION
+ */
+
+/* server */
+
+static void test_bad_version_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_bad_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "VERSION\t666\t666\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_bad_version(void)
+{
+ test_server_init = test_bad_version_init;
+ test_server_input = test_bad_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_bad_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+ return FALSE;
+}
+
+/* test */
+
+static void test_bad_version(void)
+{
+ test_begin("bad version");
+ test_expect_error_string("Socket supports major version 666");
+ test_run_client_server(test_client_bad_version,
+ test_server_bad_version);
+ test_end();
+}
+
+/*
+ * Disconnect VERSION
+ */
+
+/* server */
+
+static void test_disconnect_version_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_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_disconnect_version(void)
+{
+ test_server_init = test_disconnect_version_init;
+ test_server_input = test_disconnect_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_disconnect_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out_reason("run (ret == -1)", ret == -1, error);
+ return FALSE;
+}
+
+/* test */
+
+static void test_disconnect_version(void)
+{
+ test_begin("disconnect version");
+ test_expect_error_string("Disconnected unexpectedly");
+ test_run_client_server(test_client_disconnect_version,
+ test_server_disconnect_version);
+ test_end();
+}
+
+/*
+ * Passdb FAIL
+ */
+
+/* server */
+
+enum _passdb_fail_state {
+ PASSDB_FAIL_STATE_VERSION = 0,
+ PASSDB_FAIL_STATE_PASS
+};
+
+struct _passdb_fail_server {
+ enum _passdb_fail_state state;
+
+ bool not_found:1;
+};
+
+static void test_passdb_fail_input(struct server_connection *conn)
+{
+ struct _passdb_fail_server *ctx =
+ (struct _passdb_fail_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 PASSDB_FAIL_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = PASSDB_FAIL_STATE_PASS;
+ continue;
+ case PASSDB_FAIL_STATE_PASS:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "PASS") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0 || args[2] == NULL) {
+ i_error("Bad PASS request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (strcmp(args[2], "henk") == 0) {
+ line = t_strdup_printf("NOTFOUND\t%u\n", id);
+ } else if (strcmp(args[2], "holger") == 0) {
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+ return;
+ } else if (strcmp(args[2], "hendrik") == 0) {
+ server_connection_deinit(&conn);
+ return;
+ } else {
+ line = t_strdup_printf(
+ "FAIL\t%u\t"
+ "reason=You shall not pass!!\n", id);
+ }
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_passdb_fail_init(struct server_connection *conn)
+{
+ struct _passdb_fail_server *ctx;
+
+ ctx = p_new(conn->pool, struct _passdb_fail_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_passdb_fail(void)
+{
+ test_server_init = test_passdb_fail_init;
+ test_server_input = test_passdb_fail_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_passdb_fail(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out("run (ret == -2)", ret == -2);
+ test_assert(error != NULL &&
+ strcmp(error, "You shall not pass!!") == 0);
+
+ return FALSE;
+}
+
+static bool test_client_passdb_notfound(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("henk", FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_passdb_timeout(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("holger", FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_passdb_disconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("hendrik", FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_passdb_reconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("hendrik", TRUE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_passdb_fail(void)
+{
+ test_begin("passdb fail");
+ test_run_client_server(test_client_passdb_fail,
+ test_server_passdb_fail);
+ test_end();
+
+ test_begin("passdb notfound");
+ test_run_client_server(test_client_passdb_notfound,
+ test_server_passdb_fail);
+ test_end();
+
+ test_begin("passdb timeout");
+ test_expect_error_string("Request timed out");
+ test_run_client_server(test_client_passdb_timeout,
+ test_server_passdb_fail);
+ test_end();
+
+ test_begin("passdb disconnect");
+ test_expect_error_string("Disconnected unexpectedly");
+ test_run_client_server(test_client_passdb_disconnect,
+ test_server_passdb_fail);
+ test_end();
+
+ test_begin("passdb reconnect");
+ test_expect_errors(2);
+ test_run_client_server(test_client_passdb_reconnect,
+ test_server_passdb_fail);
+ test_end();
+}
+
+/*
+ * Userdb FAIL
+ */
+
+/* server */
+
+enum _userdb_fail_state {
+ USERDB_FAIL_STATE_VERSION = 0,
+ USERDB_FAIL_STATE_USER
+};
+
+struct _userdb_fail_server {
+ enum _userdb_fail_state state;
+
+ bool not_found:1;
+};
+
+static void test_userdb_fail_input(struct server_connection *conn)
+{
+ struct _userdb_fail_server *ctx =
+ (struct _userdb_fail_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 USERDB_FAIL_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = USERDB_FAIL_STATE_USER;
+ continue;
+ case USERDB_FAIL_STATE_USER:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "USER") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ i_error("Bad USER request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (strcmp(args[2], "henk") == 0) {
+ line = t_strdup_printf("NOTFOUND\t%u\n", id);
+ } else if (strcmp(args[2], "holger") == 0) {
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+ return;
+ } else if (strcmp(args[2], "hendrik") == 0) {
+ server_connection_deinit(&conn);
+ return;
+ } else {
+ line = t_strdup_printf("FAIL\t%u\t"
+ "reason=It is no use!\n", id);
+ }
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_userdb_fail_init(struct server_connection *conn)
+{
+ struct _userdb_fail_server *ctx;
+
+ ctx = p_new(conn->pool, struct _userdb_fail_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_userdb_fail(void)
+{
+ test_server_init = test_userdb_fail_init;
+ test_server_input = test_userdb_fail_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_userdb_fail(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("harrie", FALSE, &error);
+ test_out("run (ret == -2)", ret == -2);
+ test_assert(error != NULL &&
+ strcmp(error, "It is no use!") == 0);
+
+ return FALSE;
+}
+
+static bool test_client_userdb_notfound(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("henk", FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_userdb_timeout(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("holger", FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_userdb_disconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("hendrik", FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+static bool test_client_userdb_reconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("hendrik", TRUE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error == NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_userdb_fail(void)
+{
+ test_begin("userdb fail");
+ test_run_client_server(test_client_userdb_fail,
+ test_server_userdb_fail);
+ test_end();
+
+ test_begin("userdb notfound");
+ test_run_client_server(test_client_userdb_notfound,
+ test_server_userdb_fail);
+ test_end();
+
+ test_begin("userdb timeout");
+ test_expect_error_string("Request timed out");
+ test_run_client_server(test_client_userdb_timeout,
+ test_server_userdb_fail);
+ test_end();
+
+ test_begin("userdb disconnect");
+ test_expect_error_string("Disconnected unexpectedly");
+ test_run_client_server(test_client_userdb_disconnect,
+ test_server_userdb_fail);
+ test_end();
+
+ test_begin("userdb reconnect");
+ test_expect_errors(2);
+ test_run_client_server(test_client_userdb_reconnect,
+ test_server_userdb_fail);
+ test_end();
+}
+
+/*
+ * User list FAIL
+ */
+
+/* server */
+
+enum _user_list_fail_state {
+ USER_LIST_FAIL_STATE_VERSION = 0,
+ USER_LIST_FAIL_STATE_USER
+};
+
+struct _user_list_fail_server {
+ enum _user_list_fail_state state;
+};
+
+static void test_user_list_fail_input(struct server_connection *conn)
+{
+ struct _user_list_fail_server *ctx =
+ (struct _user_list_fail_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 USER_LIST_FAIL_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = USER_LIST_FAIL_STATE_USER;
+ continue;
+ case USER_LIST_FAIL_STATE_USER:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "LIST") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ i_error("Bad LIST request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ line = t_strdup_printf("DONE\t%u\tfail\n", id);
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_user_list_fail_init(struct server_connection *conn)
+{
+ struct _user_list_fail_server *ctx;
+
+ ctx = p_new(conn->pool, struct _user_list_fail_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_user_list_fail(void)
+{
+ test_server_init = test_user_list_fail_init;
+ test_server_input = test_user_list_fail_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_user_list_fail(void)
+{
+ int ret;
+
+ ret = test_client_user_list_simple();
+ test_out("run (ret < 0)", ret < 0);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_user_list_fail(void)
+{
+ test_begin("user list fail");
+ test_expect_errors(1);
+ test_run_client_server(test_client_user_list_fail,
+ test_server_user_list_fail);
+ test_end();
+}
+
+/*
+ * Passdb lookup
+ */
+
+/* server */
+
+enum _passdb_lookup_state {
+ PASSDB_LOOKUP_STATE_VERSION = 0,
+ PASSDB_LOOKUP_STATE_PASS
+};
+
+struct _passdb_lookup_server {
+ enum _passdb_lookup_state state;
+};
+
+static void test_passdb_lookup_input(struct server_connection *conn)
+{
+ struct _passdb_lookup_server *ctx =
+ (struct _passdb_lookup_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 PASSDB_LOOKUP_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = PASSDB_LOOKUP_STATE_PASS;
+ continue;
+ case PASSDB_LOOKUP_STATE_PASS:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "PASS") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ i_error("Bad PASS request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ line = t_strdup_printf("PASS\t%u\tuser=frop\n", id);
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_passdb_lookup_init(struct server_connection *conn)
+{
+ struct _passdb_lookup_server *ctx;
+
+ ctx = p_new(conn->pool, struct _passdb_lookup_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_passdb_lookup(void)
+{
+ test_server_init = test_passdb_lookup_init;
+ test_server_input = test_passdb_lookup_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_passdb_lookup(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_passdb_lookup_simple("harrie", FALSE, &error);
+ test_out("run (ret > 0)", ret > 0);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_passdb_lookup(void)
+{
+ test_begin("passdb lookup");
+ test_run_client_server(test_client_passdb_lookup,
+ test_server_passdb_lookup);
+ test_end();
+}
+
+/*
+ * Userdb lookup
+ */
+
+/* server */
+
+enum _userdb_lookup_state {
+ USERDB_LOOKUP_STATE_VERSION = 0,
+ USERDB_LOOKUP_STATE_PASS
+};
+
+struct _userdb_lookup_server {
+ enum _userdb_lookup_state state;
+};
+
+static void test_userdb_lookup_input(struct server_connection *conn)
+{
+ struct _userdb_lookup_server *ctx =
+ (struct _userdb_lookup_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ const char *line;
+
+ 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 USERDB_LOOKUP_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = USERDB_LOOKUP_STATE_PASS;
+ continue;
+ case USERDB_LOOKUP_STATE_PASS:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "USER") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ i_error("Bad PASS request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ line = t_strdup_printf(
+ "USER\t%u\tharrie\t"
+ "uid=1000\tgid=110\thome=/home/harrie\n", id);
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_userdb_lookup_init(struct server_connection *conn)
+{
+ struct _userdb_lookup_server *ctx;
+
+ ctx = p_new(conn->pool, struct _userdb_lookup_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_userdb_lookup(void)
+{
+ test_server_init = test_userdb_lookup_init;
+ test_server_input = test_userdb_lookup_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_userdb_lookup(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_userdb_lookup_simple("harrie", FALSE, &error);
+ test_out("run (ret > 0)", ret > 0);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_userdb_lookup(void)
+{
+ test_begin("userdb lookup");
+ test_run_client_server(test_client_userdb_lookup,
+ test_server_userdb_lookup);
+ test_end();
+}
+
+/*
+ * User list
+ */
+
+/* server */
+
+enum _user_list_state {
+ USER_LIST_STATE_VERSION = 0,
+ USER_LIST_STATE_USER
+};
+
+struct _user_list_server {
+ enum _user_list_state state;
+};
+
+static void test_user_list_input(struct server_connection *conn)
+{
+ struct _user_list_server *ctx =
+ (struct _user_list_server *)conn->context;
+ const char *line;
+ const char *const *args;
+ unsigned int id;
+ string_t *str;
+
+ 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 USER_LIST_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = USER_LIST_STATE_USER;
+ continue;
+ case USER_LIST_STATE_USER:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "LIST") != 0 || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ i_error("Bad LIST request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ str = t_str_new(256);
+ str_printfa(str, "LIST\t%u\tuser1\n", id);
+ str_printfa(str, "LIST\t%u\tuser2\n", id);
+ str_printfa(str, "LIST\t%u\tuser3\n", id);
+ str_printfa(str, "LIST\t%u\tuser4\n", id);
+ str_printfa(str, "DONE\t%u\n", id);
+ o_stream_nsend_str(conn->conn.output, str_c(str));
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_user_list_init(struct server_connection *conn)
+{
+ struct _user_list_server *ctx;
+
+ ctx = p_new(conn->pool, struct _user_list_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_user_list(void)
+{
+ test_server_init = test_user_list_init;
+ test_server_input = test_user_list_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_user_list(void)
+{
+ int ret;
+
+ ret = test_client_user_list_simple();
+ test_out("run (ret == 0)", ret == 0);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_user_list(void)
+{
+ test_begin("user list");
+ test_expect_errors(0);
+ test_run_client_server(test_client_user_list,
+ test_server_user_list);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_connection_refused,
+ test_connection_timed_out,
+ test_bad_version,
+ test_disconnect_version,
+ test_passdb_fail,
+ test_userdb_fail,
+ test_user_list_fail,
+ test_passdb_lookup,
+ test_userdb_lookup,
+ test_user_list,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_deinit(void)
+{
+}
+
+static int
+test_client_passdb_lookup_simple(const char *username, bool retry,
+ const char **error_r)
+{
+ struct auth_master_connection *auth_conn;
+ enum auth_master_flags flags = 0;
+ struct auth_user_info info;
+ const char *const *fields;
+ pool_t pool;
+ int ret;
+
+ i_zero(&info);
+ info.service = "test";
+ info.debug = debug;
+
+ if (debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+
+ pool = pool_alloconly_create("test", 1024);
+
+ auth_conn = auth_master_init(TEST_SOCKET, flags);
+ auth_master_set_timeout(auth_conn, 1000);
+ ret = auth_master_pass_lookup(auth_conn, username, &info,
+ pool, &fields);
+ if (ret < 0 && retry) {
+ ret = auth_master_pass_lookup(auth_conn, username, &info,
+ pool, &fields);
+ }
+ auth_master_deinit(&auth_conn);
+
+ *error_r = (ret < 0 ? t_strdup(fields[0]) : NULL);
+ pool_unref(&pool);
+
+ return ret;
+}
+
+static int
+test_client_userdb_lookup_simple(const char *username, bool retry,
+ const char **error_r)
+{
+ struct auth_master_connection *auth_conn;
+ enum auth_master_flags flags = 0;
+ struct auth_user_info info;
+ const char *const *fields;
+ const char *username_out;
+ pool_t pool;
+ int ret;
+
+ i_zero(&info);
+ info.service = "test";
+ info.debug = debug;
+
+ if (debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+
+ pool = pool_alloconly_create("test", 1024);
+
+ auth_conn = auth_master_init(TEST_SOCKET, flags);
+ auth_master_set_timeout(auth_conn, 1000);
+ ret = auth_master_user_lookup(auth_conn, username, &info,
+ pool, &username_out, &fields);
+ if (ret < 0 && retry) {
+ ret = auth_master_user_lookup(auth_conn, username, &info,
+ pool, &username_out, &fields);
+ }
+ auth_master_deinit(&auth_conn);
+
+ *error_r = (ret < 0 ? t_strdup(fields[0]) : NULL);
+ pool_unref(&pool);
+
+ return ret;
+}
+
+static int test_client_user_list_simple(void)
+{
+ struct auth_master_connection *auth_conn;
+ struct auth_master_user_list_ctx *list_ctx;
+ enum auth_master_flags flags = 0;
+ struct auth_user_info info;
+ int ret;
+
+ i_zero(&info);
+ info.service = "test";
+ info.debug = debug;
+
+ if (debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+
+ auth_conn = auth_master_init(TEST_SOCKET, flags);
+ auth_master_set_timeout(auth_conn, 1000);
+ list_ctx = auth_master_user_list_init(auth_conn, "*", &info);
+ while (auth_master_user_list_next(list_ctx) != NULL);
+ ret = auth_master_user_list_deinit(&list_ctx);
+ auth_master_deinit(&auth_conn);
+
+ return ret;
+}
+
+/*
+ * 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(void)
+{
+ /* 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
+ */
+
+static int test_open_server_fd(void)
+{
+ int fd;
+ i_unlink_if_exists(TEST_SOCKET);
+ fd = net_listen_unix(TEST_SOCKET, 128);
+ if (debug)
+ i_debug("server listening on "TEST_SOCKET);
+ if (fd == -1)
+ i_fatal("listen("TEST_SOCKET") failed: %m");
+ return fd;
+}
+
+static int test_run_server(test_server_init_t *server_test)
+{
+ main_deinit();
+ master_service_deinit_forked(&master_service);
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ server_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ return 0;
+}
+
+static void test_run_client(test_client_init_t *client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_sleep_intr_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ if (client_test())
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test)
+{
+ if (server_test != NULL) {
+ /* Fork server */
+ fd_listen = test_open_server_fd();
+ test_subprocess_fork(test_run_server, server_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+}
+
+/*
+ * Main
+ */
+
+static void main_cleanup(void)
+{
+ i_unlink_if_exists(TEST_SOCKET);
+}
+
+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 |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT;
+ int c;
+ int ret;
+
+ master_service = master_service_init("test-auth-master", 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);
+ test_subprocess_set_cleanup_callback(main_cleanup);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ master_service_deinit(&master_service);
+
+ return ret;
+}
diff --git a/src/master/test-master-login-auth.c b/src/master/test-master-login-auth.c
new file mode 100644
index 0000000..f0478c7
--- /dev/null
+++ b/src/master/test-master-login-auth.c
@@ -0,0 +1,994 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.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 "randgen.h"
+#include "connection.h"
+#include "master-service.h"
+#include "master-interface.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include "master-auth.h"
+#include "master-login-auth.h"
+
+#define TEST_SOCKET "./master-login-auth-test"
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+
+ void *context;
+
+ pool_t pool;
+};
+
+typedef void test_server_init_t(void);
+typedef bool test_client_init_t(void);
+
+/*
+ * State
+ */
+
+/* common */
+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 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(void);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_deinit(void);
+
+static int
+test_client_request_parallel(pid_t client_pid, unsigned int concurrency,
+ bool retry, const char **error_r);
+static int
+test_client_request_simple(pid_t client_pid, bool retry, const char **error_r);
+
+/* test*/
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test) ATTR_NULL(2);
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void test_server_connection_refused(void)
+{
+ i_close_fd(&fd_listen);
+ i_sleep_intr_secs(500);
+}
+
+/* client */
+
+static bool test_client_connection_refused(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ test_begin("connection refused");
+ test_expect_error_string_n_times("Connection refused", 2);
+ test_run_client_server(test_client_connection_refused,
+ test_server_connection_refused);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_connection_timed_out_input(struct server_connection *conn)
+{
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_timed_out(void)
+{
+ test_server_input = test_connection_timed_out_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_connection_timed_out(void)
+{
+ time_t time;
+ const char *error;
+ int ret;
+
+ io_loop_time_refresh();
+ time = ioloop_time;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ io_loop_time_refresh();
+ test_out("timeout", (ioloop_time - time) < 5);
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ test_begin("connection timed out");
+ test_expect_error_string("Auth server request timed out");
+ test_run_client_server(test_client_connection_timed_out,
+ test_server_connection_timed_out);
+ test_end();
+}
+
+/*
+ * Bad VERSION
+ */
+
+/* server */
+
+static void test_bad_version_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_bad_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "VERSION\t666\t666\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_bad_version(void)
+{
+ test_server_init = test_bad_version_init;
+ test_server_input = test_bad_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_bad_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_bad_version(void)
+{
+ test_begin("bad version");
+ test_expect_errors(2);
+ test_run_client_server(test_client_bad_version,
+ test_server_bad_version);
+ test_end();
+}
+
+/*
+ * Disconnect VERSION
+ */
+
+/* server */
+
+static void test_disconnect_version_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_version_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_disconnect_version(void)
+{
+ test_server_init = test_disconnect_version_init;
+ test_server_input = test_disconnect_version_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_disconnect_version(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_disconnect_version(void)
+{
+ test_begin("disconnect version");
+ test_expect_error_string("Disconnected from auth server");
+ test_run_client_server(test_client_disconnect_version,
+ test_server_disconnect_version);
+ test_end();
+}
+
+/*
+ * Changed SPID
+ */
+
+/* server */
+
+static void test_changed_spid_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_changed_spid_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t35341\n");
+}
+
+static void test_server_changed_spid(void)
+{
+ test_server_init = test_changed_spid_init;
+ test_server_input = test_changed_spid_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_changed_spid(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_changed_spid(void)
+{
+ test_begin("changed spid");
+ test_expect_errors(2);
+ test_run_client_server(test_client_changed_spid,
+ test_server_changed_spid);
+ test_end();
+}
+
+/*
+ * REQUEST FAIL
+ */
+
+/* server */
+
+enum _request_fail_state {
+ REQUEST_FAIL_STATE_VERSION = 0,
+ REQUEST_FAIL_STATE_REQUEST
+};
+
+struct _request_fail_server {
+ enum _request_fail_state state;
+
+ bool not_found:1;
+};
+
+static void test_request_fail_input(struct server_connection *conn)
+{
+ struct _request_fail_server *ctx =
+ (struct _request_fail_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ pid_t client_pid;
+ const char *line;
+
+ 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 REQUEST_FAIL_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = REQUEST_FAIL_STATE_REQUEST;
+ continue;
+ case REQUEST_FAIL_STATE_REQUEST:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "REQUEST") != 0 ||
+ args[1] == NULL || str_to_uint(args[1], &id) < 0 ||
+ args[2] == NULL ||
+ str_to_pid(args[2], &client_pid) < 0) {
+ i_error("Bad REQUEST");
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (client_pid == 2324) {
+ line = t_strdup_printf("NOTFOUND\t%u\n", id);
+ } else if (client_pid == 2325) {
+ i_sleep_intr_secs(5);
+ server_connection_deinit(&conn);
+ return;
+ } else if (client_pid == 2326) {
+ server_connection_deinit(&conn);
+ return;
+ } else {
+ line = t_strdup_printf(
+ "FAIL\t%u\t"
+ "reason=REQUEST DENIED\n", id);
+ }
+ o_stream_nsend_str(conn->conn.output, line);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_request_fail_init(struct server_connection *conn)
+{
+ struct _request_fail_server *ctx;
+
+ ctx = p_new(conn->pool, struct _request_fail_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_request_fail(void)
+{
+ test_server_init = test_request_fail_init;
+ test_server_input = test_request_fail_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_request_fail(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strcmp(error, "REQUEST DENIED") == 0);
+
+ return FALSE;
+}
+
+static bool test_client_request_notfound(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2324, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_request_timeout(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2325, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_request_disconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2326, FALSE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+static bool test_client_request_reconnect(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2326, TRUE, &error);
+ test_out("run (ret == -1)", ret == -1);
+ test_assert(error != NULL &&
+ strstr(error, "Internal error occurred.") != NULL);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_request_fail(void)
+{
+ test_begin("request fail");
+ test_expect_error_string("REQUEST DENIED");
+ test_run_client_server(test_client_request_fail,
+ test_server_request_fail);
+ test_end();
+
+ test_begin("request notfound");
+ test_expect_error_string("Authenticated user not found from userdb");
+ test_run_client_server(test_client_request_notfound,
+ test_server_request_fail);
+ test_end();
+
+ test_begin("request timeout");
+ test_expect_error_string("Auth server request timed out");
+ test_run_client_server(test_client_request_timeout,
+ test_server_request_fail);
+ test_end();
+
+ test_begin("request disconnect");
+ test_expect_error_string("Disconnected from auth server");
+ test_run_client_server(test_client_request_disconnect,
+ test_server_request_fail);
+ test_end();
+
+ test_begin("request reconnect");
+ test_expect_errors(2);
+ test_run_client_server(test_client_request_reconnect,
+ test_server_request_fail);
+ test_end();
+}
+
+/*
+ * REQUEST
+ */
+
+/* server */
+
+enum _request_login_state {
+ REQUEST_LOGIN_STATE_VERSION = 0,
+ REQUEST_LOGIN_STATE_REQUEST
+};
+
+struct _request_login_server {
+ enum _request_login_state state;
+};
+
+static void test_request_login_input(struct server_connection *conn)
+{
+ struct _request_login_server *ctx =
+ (struct _request_login_server *)conn->context;
+ const char *const *args;
+ unsigned int id;
+ pid_t client_pid;
+ const char *line;
+
+ 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 REQUEST_LOGIN_STATE_VERSION:
+ if (!str_begins(line, "VERSION\t")) {
+ i_error("Bad VERSION");
+ server_connection_deinit(&conn);
+ return;
+ }
+ ctx->state = REQUEST_LOGIN_STATE_REQUEST;
+ continue;
+ case REQUEST_LOGIN_STATE_REQUEST:
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "REQUEST") != 0 ||
+ args[1] == NULL || str_to_uint(args[1], &id) < 0 ||
+ args[2] == NULL ||
+ str_to_pid(args[2], &client_pid) < 0) {
+ i_error("Bad PASS request");
+ server_connection_deinit(&conn);
+ return;
+ }
+ line = t_strdup_printf("USER\t%u\tfrop\n", id);
+ o_stream_nsend_str(conn->conn.output, line);
+ continue;
+ }
+ i_unreached();
+ }
+}
+
+static void test_request_login_init(struct server_connection *conn)
+{
+ struct _request_login_server *ctx;
+
+ ctx = p_new(conn->pool, struct _request_login_server, 1);
+ conn->context = (void*)ctx;
+
+ o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n");
+ o_stream_nsend_str(conn->conn.output, "SPID\t23234\n");
+}
+
+static void test_server_request_login(void)
+{
+ test_server_init = test_request_login_init;
+ test_server_input = test_request_login_input;
+ test_server_run();
+}
+
+/* client */
+
+static bool test_client_request_login(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_simple(2323, FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+
+ return FALSE;
+}
+
+static bool test_client_request_login_parallel(void)
+{
+ const char *error;
+ int ret;
+
+ ret = test_client_request_parallel(2323, 4, FALSE, &error);
+ test_out("run (ret == 0)", ret == 0);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_request_login(void)
+{
+ test_begin("request login");
+ test_run_client_server(test_client_request_login,
+ test_server_request_login);
+ test_end();
+
+ test_begin("request login parallel");
+ test_run_client_server(test_client_request_login_parallel,
+ test_server_request_login);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_connection_refused,
+ test_connection_timed_out,
+ test_bad_version,
+ test_disconnect_version,
+ test_changed_spid,
+ test_request_fail,
+ test_request_login,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_deinit(void)
+{
+}
+
+struct login_test {
+ char *error;
+ int status;
+
+ unsigned int pending_requests;
+
+ struct ioloop *ioloop;
+};
+
+static void
+test_client_request_callback(const char *const *auth_args ATTR_UNUSED,
+ const char *errormsg, void *context)
+{
+ struct login_test *login_test = context;
+
+ if (errormsg != NULL) {
+ login_test->error = i_strdup(errormsg);
+ login_test->status = -1;
+ }
+
+ if (--login_test->pending_requests == 0)
+ io_loop_stop(login_test->ioloop);
+}
+
+static int
+test_client_request_run(struct master_login_auth *auth, struct ioloop *ioloop,
+ struct master_auth_request *auth_req,
+ unsigned int concurrency, const char **error_r)
+{
+ struct login_test login_test;
+ unsigned int i;
+
+ io_loop_set_running(ioloop);
+
+ i_zero(&login_test);
+ login_test.ioloop = ioloop;
+
+ master_login_auth_set_timeout(auth, 1000);
+
+ login_test.pending_requests = concurrency;
+ for (i = 0; i < concurrency; i++) {
+ master_login_auth_request(auth, auth_req,
+ test_client_request_callback,
+ &login_test);
+ }
+
+ if (io_loop_is_running(ioloop))
+ io_loop_run(ioloop);
+
+ *error_r = t_strdup(login_test.error);
+ i_free(login_test.error);
+
+ return login_test.status;
+}
+
+static int
+test_client_request_parallel(pid_t client_pid, unsigned int concurrency,
+ bool retry, const char **error_r)
+{
+ struct master_login_auth *auth;
+ struct master_auth_request auth_req;
+ struct ioloop *ioloop;
+ int ret;
+
+ i_zero(&auth_req);
+ auth_req.tag = 99033;
+ auth_req.auth_pid = 23234;
+ auth_req.auth_id = 45521;
+ auth_req.client_pid = client_pid;
+ random_fill(auth_req.cookie, sizeof(auth_req.cookie));
+ (void)net_addr2ip("10.0.0.15", &auth_req.local_ip);
+ auth_req.local_port = 143;
+ (void)net_addr2ip("10.0.0.211", &auth_req.remote_ip);
+ auth_req.remote_port = 45546;
+ auth_req.flags = MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED;
+
+ ioloop = io_loop_create();
+
+ auth = master_login_auth_init(TEST_SOCKET, TRUE);
+ ret = test_client_request_run(auth, ioloop, &auth_req, concurrency,
+ error_r);
+ if (ret < 0 && retry) {
+ ret = test_client_request_run(auth, ioloop, &auth_req,
+ concurrency, error_r);
+ }
+ master_login_auth_deinit(&auth);
+
+ io_loop_destroy(&ioloop);
+
+ return ret;
+}
+
+static int
+test_client_request_simple(pid_t client_pid, bool retry, const char **error_r)
+{
+ return test_client_request_parallel(client_pid, 1, retry, 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(void)
+{
+ /* 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
+ */
+
+static int test_open_server_fd(void)
+{
+ int fd;
+ i_unlink_if_exists(TEST_SOCKET);
+ fd = net_listen_unix(TEST_SOCKET, 128);
+ if (debug)
+ i_debug("server listening on "TEST_SOCKET);
+ if (fd == -1)
+ i_fatal("listen("TEST_SOCKET") failed: %m");
+ return fd;
+}
+
+static int test_run_server(test_server_init_t *server_test)
+{
+ main_deinit();
+ master_service_deinit_forked(&master_service);
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ server_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ return 0;
+}
+
+static void test_run_client(test_client_init_t *client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_sleep_intr_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ if (client_test())
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(test_client_init_t *client_test,
+ test_server_init_t *server_test)
+{
+ if (server_test != NULL) {
+ /* Fork server */
+ fd_listen = test_open_server_fd();
+ test_subprocess_fork(test_run_server, server_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+}
+
+/*
+ * Main
+ */
+
+static void main_cleanup(void)
+{
+ i_unlink_if_exists(TEST_SOCKET);
+}
+
+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 |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT;
+ int c;
+ int ret;
+
+ master_service = master_service_init("test-auth-master", 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);
+ test_subprocess_set_cleanup_callback(main_cleanup);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ master_service_deinit(&master_service);
+
+ return ret;
+}
diff --git a/src/old-stats/Makefile.am b/src/old-stats/Makefile.am
new file mode 100644
index 0000000..8cb9c2f
--- /dev/null
+++ b/src/old-stats/Makefile.am
@@ -0,0 +1,48 @@
+old_stats_moduledir = $(moduledir)/old-stats
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = old-stats
+
+AM_CPPFLAGS = \
+ -DSTATS_MODULE_DIR=\""$(old_stats_moduledir)"\" \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-old-stats \
+ $(BINARY_CFLAGS)
+
+old_stats_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+old_stats_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+old_stats_SOURCES = \
+ client.c \
+ client-export.c \
+ client-reset.c \
+ fifo-input-connection.c \
+ global-memory.c \
+ mail-command.c \
+ mail-domain.c \
+ mail-ip.c \
+ mail-session.c \
+ mail-stats.c \
+ mail-user.c \
+ main.c \
+ stats-carbon.c \
+ stats-settings.c
+
+noinst_HEADERS = \
+ client.h \
+ client-export.h \
+ client-reset.h \
+ fifo-input-connection.h \
+ global-memory.h \
+ mail-command.h \
+ mail-domain.h \
+ mail-ip.h \
+ mail-session.h \
+ mail-stats.h \
+ mail-user.h \
+ stats-carbon.h \
+ stats-settings.h
diff --git a/src/old-stats/Makefile.in b/src/old-stats/Makefile.in
new file mode 100644
index 0000000..6225c10
--- /dev/null
+++ b/src/old-stats/Makefile.in
@@ -0,0 +1,882 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = old-stats$(EXEEXT)
+subdir = src/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 $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_old_stats_OBJECTS = client.$(OBJEXT) client-export.$(OBJEXT) \
+ client-reset.$(OBJEXT) fifo-input-connection.$(OBJEXT) \
+ global-memory.$(OBJEXT) mail-command.$(OBJEXT) \
+ mail-domain.$(OBJEXT) mail-ip.$(OBJEXT) mail-session.$(OBJEXT) \
+ mail-stats.$(OBJEXT) mail-user.$(OBJEXT) main.$(OBJEXT) \
+ stats-carbon.$(OBJEXT) stats-settings.$(OBJEXT)
+old_stats_OBJECTS = $(am_old_stats_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/client-export.Po \
+ ./$(DEPDIR)/client-reset.Po ./$(DEPDIR)/client.Po \
+ ./$(DEPDIR)/fifo-input-connection.Po \
+ ./$(DEPDIR)/global-memory.Po ./$(DEPDIR)/mail-command.Po \
+ ./$(DEPDIR)/mail-domain.Po ./$(DEPDIR)/mail-ip.Po \
+ ./$(DEPDIR)/mail-session.Po ./$(DEPDIR)/mail-stats.Po \
+ ./$(DEPDIR)/mail-user.Po ./$(DEPDIR)/main.Po \
+ ./$(DEPDIR)/stats-carbon.Po ./$(DEPDIR)/stats-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 = $(old_stats_SOURCES)
+DIST_SOURCES = $(old_stats_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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@
+old_stats_moduledir = $(moduledir)/old-stats
+AM_CPPFLAGS = \
+ -DSTATS_MODULE_DIR=\""$(old_stats_moduledir)"\" \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-old-stats \
+ $(BINARY_CFLAGS)
+
+old_stats_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+old_stats_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+old_stats_SOURCES = \
+ client.c \
+ client-export.c \
+ client-reset.c \
+ fifo-input-connection.c \
+ global-memory.c \
+ mail-command.c \
+ mail-domain.c \
+ mail-ip.c \
+ mail-session.c \
+ mail-stats.c \
+ mail-user.c \
+ main.c \
+ stats-carbon.c \
+ stats-settings.c
+
+noinst_HEADERS = \
+ client.h \
+ client-export.h \
+ client-reset.h \
+ fifo-input-connection.h \
+ global-memory.h \
+ mail-command.h \
+ mail-domain.h \
+ mail-ip.h \
+ mail-session.h \
+ mail-stats.h \
+ mail-user.h \
+ stats-carbon.h \
+ stats-settings.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/old-stats/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+old-stats$(EXEEXT): $(old_stats_OBJECTS) $(old_stats_DEPENDENCIES) $(EXTRA_old_stats_DEPENDENCIES)
+ @rm -f old-stats$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(old_stats_OBJECTS) $(old_stats_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-export.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-reset.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fifo-input-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/global-memory.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-command.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-domain.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-ip.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-session.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-stats.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-user.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-carbon.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-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
+
+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 $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client-export.Po
+ -rm -f ./$(DEPDIR)/client-reset.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/fifo-input-connection.Po
+ -rm -f ./$(DEPDIR)/global-memory.Po
+ -rm -f ./$(DEPDIR)/mail-command.Po
+ -rm -f ./$(DEPDIR)/mail-domain.Po
+ -rm -f ./$(DEPDIR)/mail-ip.Po
+ -rm -f ./$(DEPDIR)/mail-session.Po
+ -rm -f ./$(DEPDIR)/mail-stats.Po
+ -rm -f ./$(DEPDIR)/mail-user.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/stats-carbon.Po
+ -rm -f ./$(DEPDIR)/stats-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-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS
+
+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)/client-export.Po
+ -rm -f ./$(DEPDIR)/client-reset.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/fifo-input-connection.Po
+ -rm -f ./$(DEPDIR)/global-memory.Po
+ -rm -f ./$(DEPDIR)/mail-command.Po
+ -rm -f ./$(DEPDIR)/mail-domain.Po
+ -rm -f ./$(DEPDIR)/mail-ip.Po
+ -rm -f ./$(DEPDIR)/mail-session.Po
+ -rm -f ./$(DEPDIR)/mail-stats.Po
+ -rm -f ./$(DEPDIR)/mail-user.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/stats-carbon.Po
+ -rm -f ./$(DEPDIR)/stats-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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/old-stats/client-export.c b/src/old-stats/client-export.c
new file mode 100644
index 0000000..e020d13
--- /dev/null
+++ b/src/old-stats/client-export.c
@@ -0,0 +1,657 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "wildcard-match.h"
+#include "mail-stats.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "client.h"
+#include "client-export.h"
+
+enum mail_export_level {
+ MAIL_EXPORT_LEVEL_COMMAND,
+ MAIL_EXPORT_LEVEL_SESSION,
+ MAIL_EXPORT_LEVEL_USER,
+ MAIL_EXPORT_LEVEL_DOMAIN,
+ MAIL_EXPORT_LEVEL_IP,
+ MAIL_EXPORT_LEVEL_GLOBAL
+};
+static const char *mail_export_level_names[] = {
+ "command", "session", "user", "domain", "ip", "global"
+};
+
+struct mail_export_filter {
+ const char *user, *domain, *session;
+ struct ip_addr ip;
+ unsigned int ip_bits;
+ time_t since;
+ bool connected;
+};
+
+struct client_export_cmd {
+ enum mail_export_level level;
+ struct mail_export_filter filter;
+ string_t *str;
+ int (*export_iter)(struct client *client);
+ bool header_sent;
+};
+
+static int
+mail_export_level_parse(const char *str, enum mail_export_level *level_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(mail_export_level_names); i++) {
+ if (strcmp(mail_export_level_names[i], str) == 0) {
+ *level_r = (enum mail_export_level)i;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static int
+mail_export_parse_filter(const char *const *args, pool_t pool,
+ struct mail_export_filter *filter_r,
+ const char **error_r)
+{
+ unsigned long l;
+
+ /* filters:
+ user=<wildcard> | domain=<wildcard> | session=<str>
+ ip=<ip>[/<mask>]
+ since=<timestamp>
+ connected
+ */
+ i_zero(filter_r);
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "user="))
+ filter_r->user = p_strdup(pool, *args + 5);
+ else if (str_begins(*args, "domain="))
+ filter_r->domain = p_strdup(pool, *args + 7);
+ else if (str_begins(*args, "session="))
+ filter_r->session = p_strdup(pool, *args + 8);
+ else if (str_begins(*args, "ip=")) {
+ if (net_parse_range(*args + 3, &filter_r->ip,
+ &filter_r->ip_bits) < 0) {
+ *error_r = "Invalid ip filter";
+ return -1;
+ }
+ } else if (str_begins(*args, "since=")) {
+ if (str_to_ulong(*args + 6, &l) < 0) {
+ *error_r = "Invalid since filter";
+ return -1;
+ }
+ filter_r->since = (time_t)l;
+ } else if (strcmp(*args, "connected") == 0) {
+ filter_r->connected = TRUE;
+ }
+ }
+ return 0;
+}
+
+static void
+client_export_stats_headers(struct client *client)
+{
+ unsigned int i, count = stats_field_count();
+ string_t *str = t_str_new(128);
+
+ i_assert(count > 0);
+
+ str_append(str, stats_field_name(0));
+ for (i = 1; i < count; i++) {
+ str_append_c(str, '\t');
+ str_append(str, stats_field_name(i));
+ }
+ str_append_c(str, '\n');
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+}
+
+static void
+client_export_stats(string_t *str, const struct stats *stats)
+{
+ unsigned int i, count = stats_field_count();
+
+ i_assert(count > 0);
+
+ stats_field_value(str, stats, 0);
+ for (i = 1; i < count; i++) {
+ str_append_c(str, '\t');
+ stats_field_value(str, stats, i);
+ }
+}
+
+static bool
+mail_export_filter_match_session(const struct mail_export_filter *filter,
+ const struct mail_session *session)
+{
+ if (filter->connected && session->disconnected)
+ return FALSE;
+ if (filter->since > session->last_update.tv_sec)
+ return FALSE;
+ if (filter->session != NULL &&
+ strcmp(session->id, filter->session) != 0)
+ return FALSE;
+ if (filter->user != NULL &&
+ !wildcard_match(session->user->name, filter->user))
+ return FALSE;
+ if (filter->domain != NULL &&
+ !wildcard_match(session->user->domain->name, filter->domain))
+ return FALSE;
+ if (filter->ip_bits > 0 &&
+ !net_is_in_network(&session->ip->ip, &filter->ip, filter->ip_bits))
+ return FALSE;
+ return TRUE;
+}
+
+static bool
+mail_export_filter_match_user_common(const struct mail_export_filter *filter,
+ const struct mail_user *user)
+{
+ struct mail_session *s;
+ bool connected = FALSE, ip_ok = FALSE;
+
+ if (filter->user != NULL &&
+ !wildcard_match(user->name, filter->user))
+ return FALSE;
+
+ if (filter->connected || filter->ip_bits > 0) {
+ for (s = user->sessions; s != NULL; s = s->user_next) {
+ if (!s->disconnected)
+ connected = TRUE;
+ if (filter->ip_bits > 0 &&
+ net_is_in_network(&s->ip->ip, &filter->ip,
+ filter->ip_bits))
+ ip_ok = TRUE;
+
+ }
+ if (filter->connected && !connected)
+ return FALSE;
+ if (filter->ip_bits > 0 && !ip_ok)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+mail_export_filter_match_user(const struct mail_export_filter *filter,
+ const struct mail_user *user)
+{
+ if (filter->since > user->last_update.tv_sec)
+ return FALSE;
+ if (filter->domain != NULL &&
+ !wildcard_match(user->domain->name, filter->domain))
+ return FALSE;
+ return mail_export_filter_match_user_common(filter, user);
+}
+
+static bool
+mail_export_filter_match_domain(const struct mail_export_filter *filter,
+ const struct mail_domain *domain)
+{
+ struct mail_user *user;
+
+ if (filter->since > domain->last_update.tv_sec)
+ return FALSE;
+ if (filter->domain != NULL &&
+ !wildcard_match(domain->name, filter->domain))
+ return FALSE;
+
+ if (filter->user != NULL || filter->connected || filter->ip_bits > 0) {
+ for (user = domain->users; user != NULL; user = user->domain_next) {
+ if (mail_export_filter_match_user_common(filter, user))
+ break;
+ }
+ if (user == NULL)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+mail_export_filter_match_ip(const struct mail_export_filter *filter,
+ const struct mail_ip *ip)
+{
+ struct mail_session *s;
+ bool connected = FALSE, user_ok = FALSE, domain_ok = FALSE;
+
+ if (filter->connected || filter->ip_bits > 0) {
+ for (s = ip->sessions; s != NULL; s = s->ip_next) {
+ if (!s->disconnected)
+ connected = TRUE;
+ if (filter->user != NULL &&
+ wildcard_match(s->user->name, filter->user))
+ user_ok = TRUE;
+ if (filter->domain != NULL &&
+ wildcard_match(s->user->domain->name, filter->domain))
+ domain_ok = TRUE;
+ }
+ if (filter->connected && !connected)
+ return FALSE;
+ if (filter->user != NULL && !user_ok)
+ return FALSE;
+ if (filter->domain != NULL && !domain_ok)
+ return FALSE;
+ }
+ if (filter->since > ip->last_update.tv_sec)
+ return FALSE;
+ if (filter->ip_bits > 0 &&
+ !net_is_in_network(&ip->ip, &filter->ip, filter->ip_bits))
+ return FALSE;
+ return TRUE;
+}
+
+static void client_export_timeval(string_t *str, const struct timeval *tv)
+{
+ str_printfa(str, "\t%ld.%06u", (long)tv->tv_sec,
+ (unsigned int)tv->tv_usec);
+}
+
+static int client_export_iter_command(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_command *command = client->mail_cmd_iter;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_COMMAND);
+ mail_command_unref(&client->mail_cmd_iter);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "cmd\targs\tsession\tuser\tlast_update\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ for (; command != NULL; command = command->stable_next) {
+ if (client_is_busy(client))
+ break;
+ if (!mail_export_filter_match_session(&cmd->filter,
+ command->session))
+ continue;
+
+ str_truncate(cmd->str, 0);
+ str_append_tabescaped(cmd->str, command->name);
+ str_append_c(cmd->str, '\t');
+ str_append_tabescaped(cmd->str, command->args);
+ str_append_c(cmd->str, '\t');
+ str_append(cmd->str, command->session->id);
+ str_append_c(cmd->str, '\t');
+ str_append_tabescaped(cmd->str,
+ command->session->user->name);
+ client_export_timeval(cmd->str, &command->last_update);
+ str_append_c(cmd->str, '\t');
+ client_export_stats(cmd->str, command->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ }
+
+ if (command != NULL) {
+ client->mail_cmd_iter = command;
+ mail_command_ref(command);
+ return 0;
+ }
+ return 1;
+}
+
+static int client_export_iter_session(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_session *session = client->mail_session_iter;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_SESSION);
+ mail_session_unref(&client->mail_session_iter);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "session\tuser\tip\tservice\tpid\tconnected"
+ "\tlast_update\tnum_cmds\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ for (; session != NULL; session = session->stable_next) {
+ if (client_is_busy(client))
+ break;
+ if (!mail_export_filter_match_session(&cmd->filter, session))
+ continue;
+
+ str_truncate(cmd->str, 0);
+ str_append(cmd->str, session->id);
+ str_append_c(cmd->str, '\t');
+ str_append_tabescaped(cmd->str, session->user->name);
+ str_append_c(cmd->str, '\t');
+ if (session->ip != NULL) T_BEGIN {
+ str_append(cmd->str, net_ip2addr(&session->ip->ip));
+ } T_END;
+ str_append_c(cmd->str, '\t');
+ str_append_tabescaped(cmd->str, session->service);
+ str_printfa(cmd->str, "\t%ld", (long)session->pid);
+ str_printfa(cmd->str, "\t%d", !session->disconnected);
+ client_export_timeval(cmd->str, &session->last_update);
+ str_printfa(cmd->str, "\t%u\t", session->num_cmds);
+ client_export_stats(cmd->str, session->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ }
+
+ if (session != NULL) {
+ client->mail_session_iter = session;
+ mail_session_ref(session);
+ return 0;
+ }
+ return 1;
+}
+
+static int client_export_iter_user(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_user *user = client->mail_user_iter;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_USER);
+ mail_user_unref(&client->mail_user_iter);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "user\treset_timestamp\tlast_update"
+ "\tnum_logins\tnum_cmds\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ for (; user != NULL; user = user->stable_next) {
+ if (client_is_busy(client))
+ break;
+ if (!mail_export_filter_match_user(&cmd->filter, user))
+ continue;
+
+ str_truncate(cmd->str, 0);
+ str_append_tabescaped(cmd->str, user->name);
+ str_printfa(cmd->str, "\t%ld", (long)user->reset_timestamp);
+ client_export_timeval(cmd->str, &user->last_update);
+ str_printfa(cmd->str, "\t%u\t%u\t",
+ user->num_logins, user->num_cmds);
+ client_export_stats(cmd->str, user->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ }
+
+ if (user != NULL) {
+ client->mail_user_iter = user;
+ mail_user_ref(user);
+ return 0;
+ }
+ return 1;
+}
+
+static int client_export_iter_domain(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_domain *domain = client->mail_domain_iter;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_DOMAIN);
+ mail_domain_unref(&client->mail_domain_iter);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "domain\treset_timestamp\tlast_update"
+ "\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ for (; domain != NULL; domain = domain->stable_next) {
+ if (client_is_busy(client))
+ break;
+ if (!mail_export_filter_match_domain(&cmd->filter, domain))
+ continue;
+
+ str_truncate(cmd->str, 0);
+ str_append_tabescaped(cmd->str, domain->name);
+ str_printfa(cmd->str, "\t%ld", (long)domain->reset_timestamp);
+ client_export_timeval(cmd->str, &domain->last_update);
+ str_printfa(cmd->str, "\t%u\t%u\t%u\t",
+ domain->num_logins, domain->num_cmds,
+ domain->num_connected_sessions);
+ client_export_stats(cmd->str, domain->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ }
+
+ if (domain != NULL) {
+ client->mail_domain_iter = domain;
+ mail_domain_ref(domain);
+ return 0;
+ }
+ return 1;
+}
+
+static int client_export_iter_ip(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_ip *ip = client->mail_ip_iter;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_IP);
+ mail_ip_unref(&client->mail_ip_iter);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "ip\treset_timestamp\tlast_update"
+ "\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ for (; ip != NULL; ip = ip->stable_next) {
+ if (client_is_busy(client))
+ break;
+ if (!mail_export_filter_match_ip(&cmd->filter, ip))
+ continue;
+
+ str_truncate(cmd->str, 0);
+ T_BEGIN {
+ str_append(cmd->str, net_ip2addr(&ip->ip));
+ } T_END;
+ str_printfa(cmd->str, "\t%ld", (long)ip->reset_timestamp);
+ client_export_timeval(cmd->str, &ip->last_update);
+ str_printfa(cmd->str, "\t%u\t%u\t%u\t",
+ ip->num_logins, ip->num_cmds, ip->num_connected_sessions);
+ client_export_stats(cmd->str, ip->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ }
+
+ if (ip != NULL) {
+ client->mail_ip_iter = ip;
+ mail_ip_ref(ip);
+ return 0;
+ }
+ return 1;
+}
+
+static int client_export_iter_global(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+ struct mail_global *g = &mail_global_stats;
+
+ i_assert(cmd->level == MAIL_EXPORT_LEVEL_GLOBAL);
+
+ if (!cmd->header_sent) {
+ o_stream_nsend_str(client->output,
+ "reset_timestamp\tlast_update"
+ "\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
+ client_export_stats_headers(client);
+ cmd->header_sent = TRUE;
+ }
+
+ str_truncate(cmd->str, 0);
+ str_printfa(cmd->str, "%ld", (long)g->reset_timestamp);
+ client_export_timeval(cmd->str, &g->last_update);
+ str_printfa(cmd->str, "\t%u\t%u\t%u\t",
+ g->num_logins, g->num_cmds, g->num_connected_sessions);
+ client_export_stats(cmd->str, g->stats);
+ str_append_c(cmd->str, '\n');
+ o_stream_nsend(client->output, str_data(cmd->str),
+ str_len(cmd->str));
+ return 1;
+}
+
+static int client_export_more(struct client *client)
+{
+ if (client->cmd_export->export_iter(client) == 0)
+ return 0;
+ o_stream_nsend_str(client->output, "\n");
+ return 1;
+}
+
+static bool client_export_iter_init(struct client *client)
+{
+ struct client_export_cmd *cmd = client->cmd_export;
+
+ if (cmd->filter.user != NULL && strchr(cmd->filter.user, '*') == NULL &&
+ (cmd->level == MAIL_EXPORT_LEVEL_USER ||
+ cmd->level == MAIL_EXPORT_LEVEL_SESSION)) {
+ /* exact user */
+ struct mail_user *user = mail_user_lookup(cmd->filter.user);
+ if (user == NULL)
+ return FALSE;
+ if (cmd->level == MAIL_EXPORT_LEVEL_SESSION) {
+ client->mail_session_iter = user->sessions;
+ if (client->mail_session_iter == NULL)
+ return FALSE;
+ mail_session_ref(client->mail_session_iter);
+ cmd->export_iter = client_export_iter_session;
+ } else {
+ client->mail_user_iter = user;
+ mail_user_ref(user);
+ cmd->export_iter = client_export_iter_user;
+ }
+ return TRUE;
+ }
+ if (cmd->filter.ip_bits == IPADDR_BITS(&cmd->filter.ip) &&
+ (cmd->level == MAIL_EXPORT_LEVEL_IP ||
+ cmd->level == MAIL_EXPORT_LEVEL_SESSION)) {
+ /* exact IP address */
+ struct mail_ip *ip = mail_ip_lookup(&cmd->filter.ip);
+ if (ip == NULL)
+ return FALSE;
+ if (cmd->level == MAIL_EXPORT_LEVEL_SESSION) {
+ client->mail_session_iter = ip->sessions;
+ if (client->mail_session_iter == NULL)
+ return FALSE;
+ mail_session_ref(client->mail_session_iter);
+ cmd->export_iter = client_export_iter_session;
+ } else {
+ client->mail_ip_iter = ip;
+ mail_ip_ref(ip);
+ cmd->export_iter = client_export_iter_ip;
+ }
+ return TRUE;
+ }
+ if (cmd->filter.domain != NULL &&
+ strchr(cmd->filter.domain, '*') == NULL &&
+ (cmd->level == MAIL_EXPORT_LEVEL_DOMAIN ||
+ cmd->level == MAIL_EXPORT_LEVEL_USER)) {
+ /* exact domain */
+ struct mail_domain *domain =
+ mail_domain_lookup(cmd->filter.domain);
+ if (domain == NULL)
+ return FALSE;
+ if (cmd->level == MAIL_EXPORT_LEVEL_USER) {
+ client->mail_user_iter = domain->users;
+ mail_user_ref(client->mail_user_iter);
+ cmd->export_iter = client_export_iter_user;
+ } else {
+ client->mail_domain_iter = domain;
+ mail_domain_ref(domain);
+ cmd->export_iter = client_export_iter_domain;
+ }
+ return TRUE;
+ }
+
+ switch (cmd->level) {
+ case MAIL_EXPORT_LEVEL_COMMAND:
+ client->mail_cmd_iter = stable_mail_commands_head;
+ if (client->mail_cmd_iter == NULL)
+ return FALSE;
+ mail_command_ref(client->mail_cmd_iter);
+ cmd->export_iter = client_export_iter_command;
+ break;
+ case MAIL_EXPORT_LEVEL_SESSION:
+ client->mail_session_iter = stable_mail_sessions;
+ if (client->mail_session_iter == NULL)
+ return FALSE;
+ mail_session_ref(client->mail_session_iter);
+ cmd->export_iter = client_export_iter_session;
+ break;
+ case MAIL_EXPORT_LEVEL_USER:
+ client->mail_user_iter = stable_mail_users;
+ if (client->mail_user_iter == NULL)
+ return FALSE;
+ mail_user_ref(client->mail_user_iter);
+ cmd->export_iter = client_export_iter_user;
+ break;
+ case MAIL_EXPORT_LEVEL_DOMAIN:
+ client->mail_domain_iter = stable_mail_domains;
+ if (client->mail_domain_iter == NULL)
+ return FALSE;
+ mail_domain_ref(client->mail_domain_iter);
+ cmd->export_iter = client_export_iter_domain;
+ break;
+ case MAIL_EXPORT_LEVEL_IP:
+ client->mail_ip_iter = stable_mail_ips;
+ if (client->mail_ip_iter == NULL)
+ return FALSE;
+ mail_ip_ref(client->mail_ip_iter);
+ cmd->export_iter = client_export_iter_ip;
+ break;
+ case MAIL_EXPORT_LEVEL_GLOBAL:
+ cmd->export_iter = client_export_iter_global;
+ break;
+ }
+ i_assert(cmd->export_iter != NULL);
+ return TRUE;
+}
+
+int client_export(struct client *client, const char *const *args,
+ const char **error_r)
+{
+ const char *level_str = args[0];
+ struct client_export_cmd *cmd;
+
+ p_clear(client->cmd_pool);
+ cmd = p_new(client->cmd_pool, struct client_export_cmd, 1);
+ cmd->str = str_new(client->cmd_pool, 256);
+
+ if (level_str == NULL) {
+ *error_r = "Missing level parameter";
+ return -1;
+ }
+ if (mail_export_level_parse(level_str, &cmd->level) < 0) {
+ *error_r = "Invalid level";
+ return -1;
+ }
+ if (mail_export_parse_filter(args + 1, client->cmd_pool,
+ &cmd->filter, error_r) < 0)
+ return -1;
+
+ client->cmd_export = cmd;
+ if (!client_export_iter_init(client)) {
+ /* nothing to export */
+ o_stream_nsend_str(client->output, "\n");
+ return 1;
+ }
+ client->cmd_more = client_export_more;
+ return client_export_more(client);
+}
diff --git a/src/old-stats/client-export.h b/src/old-stats/client-export.h
new file mode 100644
index 0000000..ddca138
--- /dev/null
+++ b/src/old-stats/client-export.h
@@ -0,0 +1,9 @@
+#ifndef CLIENT_EXPORT_H
+#define CLIENT_EXPORT_H
+
+struct client;
+
+int client_export(struct client *client, const char *const *args,
+ const char **error_r);
+
+#endif
diff --git a/src/old-stats/client-reset.c b/src/old-stats/client-reset.c
new file mode 100644
index 0000000..965bb22
--- /dev/null
+++ b/src/old-stats/client-reset.c
@@ -0,0 +1,21 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "mail-stats.h"
+#include "client.h"
+#include "client-reset.h"
+
+int client_stats_reset(struct client *client, const char *const *args ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ struct mail_global *g = &mail_global_stats;
+ stats_reset(g->stats);
+ g->num_logins = 0;
+ g->num_cmds = 0;
+ g->reset_timestamp = ioloop_time;
+ i_zero(&g->last_update);
+ o_stream_nsend_str(client->output, "OK\n");
+ return 0;
+}
diff --git a/src/old-stats/client-reset.h b/src/old-stats/client-reset.h
new file mode 100644
index 0000000..7a84d61
--- /dev/null
+++ b/src/old-stats/client-reset.h
@@ -0,0 +1,9 @@
+#ifndef CLIENT_RESET_H
+#define CLIENT_RESET_H
+
+struct client;
+
+int client_stats_reset(struct client *client, const char *const *args,
+ const char **error_r);
+
+#endif
diff --git a/src/old-stats/client.c b/src/old-stats/client.c
new file mode 100644
index 0000000..5a6d5cb
--- /dev/null
+++ b/src/old-stats/client.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "client-export.h"
+#include "client-reset.h"
+#include "client.h"
+
+#include <unistd.h>
+
+#define CLIENT_MAX_SIMULTANEOUS_ITER_COUNT 1000
+#define MAX_INBUF_SIZE 1024
+#define OUTBUF_THROTTLE_SIZE (1024*64)
+
+static struct client *clients;
+
+bool client_is_busy(struct client *client)
+{
+ client->iter_count++;
+ if (client->iter_count % CLIENT_MAX_SIMULTANEOUS_ITER_COUNT == 0)
+ return TRUE;
+ if (o_stream_get_buffer_used_size(client->output) < OUTBUF_THROTTLE_SIZE)
+ return FALSE;
+ if (o_stream_flush(client->output) < 0)
+ return TRUE;
+ return o_stream_get_buffer_used_size(client->output) >= OUTBUF_THROTTLE_SIZE;
+}
+
+static int
+client_handle_request(struct client *client, const char *const *args,
+ const char **error_r)
+{
+ const char *cmd = args[0];
+
+ if (cmd == NULL) {
+ *error_r = "Missing command";
+ return -1;
+ }
+ args++;
+
+ if (strcmp(cmd, "EXPORT") == 0)
+ return client_export(client, args, error_r);
+ if (strcmp(cmd, "RESET") == 0)
+ return client_stats_reset(client, args, error_r);
+
+ *error_r = "Unknown command";
+ return -1;
+}
+
+static const char *const*
+client_read_next_line(struct client *client)
+{
+ const char *line;
+
+ line = i_stream_next_line(client->input);
+ if (line == NULL)
+ return NULL;
+
+ return t_strsplit_tabescaped(line);
+}
+
+static void client_input(struct client *client)
+{
+ const char *const *args, *error;
+ int ret;
+
+ timeout_remove(&client->to_pending);
+
+ switch (i_stream_read(client->input)) {
+ case -2:
+ i_error("BUG: Stats client sent too much data");
+ client_destroy(&client);
+ return;
+ case -1:
+ client_destroy(&client);
+ return;
+ }
+
+ o_stream_cork(client->output);
+ while ((args = client_read_next_line(client)) != NULL) {
+ ret = client_handle_request(client, args, &error);
+ if (ret < 0) {
+ i_error("Stats client input error: %s", error);
+ client_destroy(&client);
+ return;
+ }
+ if (ret == 0) {
+ o_stream_set_flush_pending(client->output, TRUE);
+ io_remove(&client->io);
+ break;
+ }
+ client->cmd_more = NULL;
+ }
+ o_stream_uncork(client->output);
+}
+
+static int client_output(struct client *client)
+{
+ int ret = 1;
+
+ if (o_stream_flush(client->output) < 0) {
+ client_destroy(&client);
+ return 1;
+ }
+ if (client->cmd_more != NULL)
+ ret = client->cmd_more(client);
+
+ if (ret > 0) {
+ client->cmd_more = NULL;
+ if (client->io == NULL)
+ client_enable_io(client);
+ }
+ return ret;
+}
+
+void client_enable_io(struct client *client)
+{
+ i_assert(client->io == NULL);
+
+ client->io = io_add(client->fd, IO_READ, client_input, client);
+ if (client->to_pending == NULL)
+ client->to_pending = timeout_add(0, client_input, client);
+}
+
+struct client *client_create(int fd)
+{
+ struct client *client;
+
+ client = i_new(struct client, 1);
+ client->fd = fd;
+ client->io = io_add(fd, IO_READ, client_input, client);
+ client->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ client->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ o_stream_set_flush_callback(client->output, client_output, client);
+ client->cmd_pool = pool_alloconly_create("cmd pool", 1024);
+
+ DLLIST_PREPEND(&clients, client);
+ return client;
+}
+
+static void client_unref_iters(struct client *client)
+{
+ if (client->mail_cmd_iter != NULL)
+ mail_command_unref(&client->mail_cmd_iter);
+ if (client->mail_session_iter != NULL)
+ mail_session_unref(&client->mail_session_iter);
+ if (client->mail_user_iter != NULL)
+ mail_user_unref(&client->mail_user_iter);
+ if (client->mail_domain_iter != NULL)
+ mail_domain_unref(&client->mail_domain_iter);
+ if (client->mail_ip_iter != NULL)
+ mail_ip_unref(&client->mail_ip_iter);
+}
+
+void client_destroy(struct client **_client)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ DLLIST_REMOVE(&clients, client);
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ if (close(client->fd) < 0)
+ i_error("close(client) failed: %m");
+
+ client_unref_iters(client);
+ pool_unref(&client->cmd_pool);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void clients_destroy_all(void)
+{
+ while (clients != NULL) {
+ struct client *client = clients;
+
+ client_destroy(&client);
+ }
+}
diff --git a/src/old-stats/client.h b/src/old-stats/client.h
new file mode 100644
index 0000000..5650d7f
--- /dev/null
+++ b/src/old-stats/client.h
@@ -0,0 +1,35 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+struct client {
+ struct client *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_pending;
+
+ pool_t cmd_pool;
+ struct client_export_cmd *cmd_export;
+ int (*cmd_more)(struct client *client);
+
+ /* command iterators. while non-NULL, they've increased the
+ struct's refcount so it won't be deleted during iteration */
+ unsigned int iter_count;
+ struct mail_command *mail_cmd_iter;
+ struct mail_session *mail_session_iter;
+ struct mail_user *mail_user_iter;
+ struct mail_domain *mail_domain_iter;
+ struct mail_ip *mail_ip_iter;
+};
+
+struct client *client_create(int fd);
+void client_destroy(struct client **client);
+
+bool client_is_busy(struct client *client);
+void client_enable_io(struct client *client);
+
+void clients_destroy_all(void);
+
+#endif
diff --git a/src/old-stats/fifo-input-connection.c b/src/old-stats/fifo-input-connection.c
new file mode 100644
index 0000000..1b9759d
--- /dev/null
+++ b/src/old-stats/fifo-input-connection.c
@@ -0,0 +1,108 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "strescape.h"
+#include "istream.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-command.h"
+#include "fifo-input-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE (PIPE_BUF*2)
+
+struct fifo_input_connection {
+ struct fifo_input_connection *prev, *next;
+
+ int fd;
+ struct istream *input;
+ struct io *io;
+};
+
+static struct fifo_input_connection *fifo_conns = NULL;
+
+static int
+fifo_input_connection_request(const char *const *args, const char **error_r)
+{
+ const char *cmd = args[0];
+
+ if (cmd == NULL) {
+ *error_r = "Missing command";
+ return -1;
+ }
+ args++;
+
+ if (strcmp(cmd, "CONNECT") == 0)
+ return mail_session_connect_parse(args, error_r);
+ if (strcmp(cmd, "DISCONNECT") == 0)
+ return mail_session_disconnect_parse(args, error_r);
+ if (strcmp(cmd, "UPDATE-SESSION") == 0)
+ return mail_session_update_parse(args, error_r);
+ if (strcmp(cmd, "ADD-USER") == 0)
+ return mail_user_add_parse(args, error_r);
+ if (strcmp(cmd, "UPDATE-CMD") == 0)
+ return mail_command_update_parse(args, error_r);
+
+ *error_r = "Unknown command";
+ return -1;
+}
+
+static void fifo_input_connection_input(struct fifo_input_connection *conn)
+{
+ const char *line, *const *args, *error;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ i_error("BUG: Mail server sent too much data");
+ fifo_input_connection_destroy(&conn);
+ return;
+ case -1:
+ fifo_input_connection_destroy(&conn);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) T_BEGIN {
+ args = t_strsplit_tabescaped(line);
+ if (fifo_input_connection_request(args, &error) < 0)
+ i_error("FIFO input error: %s", error);
+ } T_END;
+}
+
+struct fifo_input_connection *fifo_input_connection_create(int fd)
+{
+ struct fifo_input_connection *conn;
+
+ conn = i_new(struct fifo_input_connection, 1);
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->io = io_add(fd, IO_READ, fifo_input_connection_input, conn);
+ DLLIST_PREPEND(&fifo_conns, conn);
+ return conn;
+}
+
+void fifo_input_connection_destroy(struct fifo_input_connection **_conn)
+{
+ struct fifo_input_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ DLLIST_REMOVE(&fifo_conns, conn);
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ if (close(conn->fd) < 0)
+ i_error("close(conn) failed: %m");
+ i_free(conn);
+}
+
+void fifo_input_connections_destroy_all(void)
+{
+ while (fifo_conns != NULL) {
+ struct fifo_input_connection *conn = fifo_conns;
+
+ fifo_input_connection_destroy(&conn);
+ }
+}
diff --git a/src/old-stats/fifo-input-connection.h b/src/old-stats/fifo-input-connection.h
new file mode 100644
index 0000000..ff394a5
--- /dev/null
+++ b/src/old-stats/fifo-input-connection.h
@@ -0,0 +1,9 @@
+#ifndef FIFO_INPUT_CONNECTION_H
+#define FIFO_INPUT_CONNECTION_H
+
+struct fifo_input_connection *fifo_input_connection_create(int fd);
+void fifo_input_connection_destroy(struct fifo_input_connection **conn);
+
+void fifo_input_connections_destroy_all(void);
+
+#endif
diff --git a/src/old-stats/global-memory.c b/src/old-stats/global-memory.c
new file mode 100644
index 0000000..dc04bfb
--- /dev/null
+++ b/src/old-stats/global-memory.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "stats-settings.h"
+#include "global-memory.h"
+
+size_t global_used_memory = 0;
+
+static bool global_memory_free_something(void)
+{
+ size_t orig_used_memory = global_used_memory;
+
+ mail_commands_free_memory();
+ if (global_used_memory > stats_settings->memory_limit)
+ mail_sessions_free_memory();
+ if (global_used_memory > stats_settings->memory_limit)
+ mail_users_free_memory();
+ if (global_used_memory > stats_settings->memory_limit)
+ mail_ips_free_memory();
+ if (global_used_memory > stats_settings->memory_limit)
+ mail_domains_free_memory();
+
+ return global_used_memory < orig_used_memory;
+}
+
+void global_memory_alloc(size_t size)
+{
+ i_assert(size < SIZE_MAX - global_used_memory);
+ global_used_memory += size;
+
+ while (global_used_memory > stats_settings->memory_limit) {
+ if (!global_memory_free_something())
+ break;
+ }
+}
+
+void global_memory_free(size_t size)
+{
+ i_assert(size <= global_used_memory);
+ global_used_memory -= size;
+}
diff --git a/src/old-stats/global-memory.h b/src/old-stats/global-memory.h
new file mode 100644
index 0000000..840ffc7
--- /dev/null
+++ b/src/old-stats/global-memory.h
@@ -0,0 +1,9 @@
+#ifndef GLOBAL_MEMORY_H
+#define GLOBAL_MEMORY_H
+
+extern size_t global_used_memory;
+
+void global_memory_alloc(size_t size);
+void global_memory_free(size_t size);
+
+#endif
diff --git a/src/old-stats/mail-command.c b/src/old-stats/mail-command.c
new file mode 100644
index 0000000..98fdc9d
--- /dev/null
+++ b/src/old-stats/mail-command.c
@@ -0,0 +1,247 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-session.h"
+#include "mail-command.h"
+
+#define MAIL_COMMAND_TIMEOUT_SECS (60*15)
+
+/* commands are sorted by their last_update timestamp, oldest first */
+struct mail_command *stable_mail_commands_head;
+struct mail_command *stable_mail_commands_tail;
+
+static size_t mail_command_memsize(const struct mail_command *cmd)
+{
+ return sizeof(*cmd) + strlen(cmd->name) + 1 + strlen(cmd->args) + 1;
+}
+
+static struct mail_command *
+mail_command_find(struct mail_session *session, unsigned int id)
+{
+ struct mail_command *cmd;
+
+ i_assert(id != 0);
+
+ if (id > session->highest_cmd_id) {
+ /* fast path for new commands */
+ return NULL;
+ }
+ for (cmd = session->commands; cmd != NULL; cmd = cmd->session_next) {
+ if (cmd->id == id)
+ return cmd;
+ }
+ /* expired */
+ return NULL;
+}
+
+static struct mail_command *
+mail_command_add(struct mail_session *session, const char *name,
+ const char *args)
+{
+ struct mail_command *cmd;
+
+ cmd = i_malloc(MALLOC_ADD(sizeof(struct mail_command), stats_alloc_size()));
+ cmd->stats = (void *)(cmd + 1);
+ cmd->refcount = 1; /* unrefed at "done" */
+ cmd->session = session;
+ cmd->name = i_strdup(name);
+ cmd->args = i_strdup(args);
+ cmd->last_update = ioloop_timeval;
+
+ DLLIST2_APPEND_FULL(&stable_mail_commands_head,
+ &stable_mail_commands_tail, cmd,
+ stable_prev, stable_next);
+ DLLIST_PREPEND_FULL(&session->commands, cmd,
+ session_prev, session_next);
+ mail_session_ref(cmd->session);
+ global_memory_alloc(mail_command_memsize(cmd));
+ return cmd;
+}
+
+static void mail_command_free(struct mail_command *cmd)
+{
+ i_assert(cmd->refcount == 0);
+
+ global_memory_free(mail_command_memsize(cmd));
+
+ DLLIST2_REMOVE_FULL(&stable_mail_commands_head,
+ &stable_mail_commands_tail, cmd,
+ stable_prev, stable_next);
+ DLLIST_REMOVE_FULL(&cmd->session->commands, cmd,
+ session_prev, session_next);
+ mail_session_unref(&cmd->session);
+ i_free(cmd->name);
+ i_free(cmd->args);
+ i_free(cmd);
+}
+
+void mail_command_ref(struct mail_command *cmd)
+{
+ cmd->refcount++;
+}
+
+void mail_command_unref(struct mail_command **_cmd)
+{
+ struct mail_command *cmd = *_cmd;
+
+ i_assert(cmd->refcount > 0);
+ cmd->refcount--;
+
+ *_cmd = NULL;
+}
+
+int mail_command_update_parse(const char *const *args, const char **error_r)
+{
+ struct mail_session *session;
+ struct mail_command *cmd;
+ struct stats *new_stats, *diff_stats;
+ buffer_t *buf;
+ const char *error;
+ unsigned int i, cmd_id;
+ bool done = FALSE, continued = FALSE;
+
+ /* <session guid> <cmd id> [d] <name> <args> <stats>
+ <session guid> <cmd id> c[d] <stats> */
+ if (str_array_length(args) < 3) {
+ *error_r = "UPDATE-CMD: Too few parameters";
+ return -1;
+ }
+ if (mail_session_get(args[0], &session, error_r) < 0)
+ return -1;
+
+ if (str_to_uint(args[1], &cmd_id) < 0 || cmd_id == 0) {
+ *error_r = "UPDATE-CMD: Invalid command id";
+ return -1;
+ }
+ for (i = 0; args[2][i] != '\0'; i++) {
+ switch (args[2][i]) {
+ case 'd':
+ done = TRUE;
+ break;
+ case 'c':
+ continued = TRUE;
+ break;
+ default:
+ *error_r = "UPDATE-CMD: Invalid flags parameter";
+ return -1;
+ }
+ }
+
+ cmd = mail_command_find(session, cmd_id);
+ if (!continued) {
+ /* new command */
+ if (cmd != NULL) {
+ *error_r = "UPDATE-CMD: Duplicate new command id";
+ return -1;
+ }
+ if (str_array_length(args) < 5) {
+ *error_r = "UPDATE-CMD: Too few parameters";
+ return -1;
+ }
+ cmd = mail_command_add(session, args[3], args[4]);
+ cmd->id = cmd_id;
+
+ session->highest_cmd_id =
+ I_MAX(session->highest_cmd_id, cmd_id);
+ session->num_cmds++;
+ session->user->num_cmds++;
+ session->user->domain->num_cmds++;
+ if (session->ip != NULL)
+ session->ip->num_cmds++;
+ mail_global_stats.num_cmds++;
+ args += 5;
+ } else {
+ if (cmd == NULL) {
+ /* already expired command, ignore */
+ i_warning("UPDATE-CMD: Already expired");
+ return 0;
+ }
+ args += 3;
+ cmd->last_update = ioloop_timeval;
+ }
+ buf = t_buffer_create(256);
+ if (args[0] == NULL ||
+ base64_decode(args[0], strlen(args[0]), NULL, buf) < 0) {
+ *error_r = t_strdup_printf("UPDATE-CMD: Invalid base64 input");
+ return -1;
+ }
+
+ new_stats = stats_alloc(pool_datastack_create());
+ diff_stats = stats_alloc(pool_datastack_create());
+
+ if (!stats_import(buf->data, buf->used, cmd->stats, new_stats, &error)) {
+ *error_r = t_strdup_printf("UPDATE-CMD: %s", error);
+ return -1;
+ }
+
+ if (!stats_diff(cmd->stats, new_stats, diff_stats, &error)) {
+ *error_r = t_strdup_printf("UPDATE-CMD: stats shrank: %s", error);
+ return -1;
+ }
+ stats_add(cmd->stats, diff_stats);
+
+ if (done) {
+ cmd->id = 0;
+ mail_command_unref(&cmd);
+ }
+ mail_session_refresh(session, NULL);
+ return 0;
+}
+
+static bool mail_command_is_timed_out(struct mail_command *cmd)
+{
+ /* some commands like IDLE can run forever */
+ return ioloop_time - cmd->last_update.tv_sec >
+ MAIL_COMMAND_TIMEOUT_SECS;
+}
+
+void mail_commands_free_memory(void)
+{
+ unsigned int diff;
+
+ while (stable_mail_commands_head != NULL) {
+ struct mail_command *cmd = stable_mail_commands_head;
+
+ if (cmd->refcount == 0)
+ i_assert(cmd->id == 0);
+ else if (cmd->refcount == 1 &&
+ (cmd->session->disconnected ||
+ mail_command_is_timed_out(cmd))) {
+ /* session was probably lost */
+ mail_command_unref(&cmd);
+ } else {
+ break;
+ }
+ mail_command_free(stable_mail_commands_head);
+
+ if (global_used_memory < stats_settings->memory_limit ||
+ stable_mail_commands_head == NULL)
+ break;
+
+ diff = ioloop_time - stable_mail_commands_head->last_update.tv_sec;
+ if (diff < stats_settings->command_min_time)
+ break;
+ }
+}
+
+void mail_commands_init(void)
+{
+}
+
+void mail_commands_deinit(void)
+{
+ while (stable_mail_commands_head != NULL) {
+ struct mail_command *cmd = stable_mail_commands_head;
+
+ if (cmd->id != 0)
+ mail_command_unref(&cmd);
+ mail_command_free(stable_mail_commands_head);
+ }
+}
diff --git a/src/old-stats/mail-command.h b/src/old-stats/mail-command.h
new file mode 100644
index 0000000..681af83
--- /dev/null
+++ b/src/old-stats/mail-command.h
@@ -0,0 +1,18 @@
+#ifndef MAIL_COMMAND_H
+#define MAIL_COMMAND_H
+
+struct mail_command;
+
+extern struct mail_command *stable_mail_commands_head;
+extern struct mail_command *stable_mail_commands_tail;
+
+int mail_command_update_parse(const char *const *args, const char **error_r);
+
+void mail_command_ref(struct mail_command *cmd);
+void mail_command_unref(struct mail_command **cmd);
+
+void mail_commands_free_memory(void);
+void mail_commands_init(void);
+void mail_commands_deinit(void);
+
+#endif
diff --git a/src/old-stats/mail-domain.c b/src/old-stats/mail-domain.c
new file mode 100644
index 0000000..140fc15
--- /dev/null
+++ b/src/old-stats/mail-domain.c
@@ -0,0 +1,133 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-domain.h"
+
+static HASH_TABLE(char *, struct mail_domain *) mail_domains_hash;
+/* domains are sorted by their last_update timestamp, oldest first */
+static struct mail_domain *mail_domains_head, *mail_domains_tail;
+struct mail_domain *stable_mail_domains;
+
+static size_t mail_domain_memsize(const struct mail_domain *domain)
+{
+ return sizeof(*domain) + strlen(domain->name) + 1;
+}
+
+struct mail_domain *mail_domain_login_create(const char *name)
+{
+ struct mail_domain *domain;
+
+ domain = hash_table_lookup(mail_domains_hash, name);
+ if (domain != NULL) {
+ return domain;
+ }
+
+ domain = i_malloc(MALLOC_ADD(sizeof(struct mail_domain), stats_alloc_size()));
+ domain->stats = (void *)(domain + 1);
+ domain->name = i_strdup(name);
+ domain->reset_timestamp = ioloop_time;
+
+ hash_table_insert(mail_domains_hash, domain->name, domain);
+ DLLIST_PREPEND_FULL(&stable_mail_domains, domain,
+ stable_prev, stable_next);
+ global_memory_alloc(mail_domain_memsize(domain));
+ return domain;
+}
+
+void mail_domain_login(struct mail_domain *domain)
+{
+ domain->num_logins++;
+ domain->num_connected_sessions++;
+ mail_domain_refresh(domain, NULL);
+}
+
+void mail_domain_disconnected(struct mail_domain *domain)
+{
+ i_assert(domain->num_connected_sessions > 0);
+ domain->num_connected_sessions--;
+ mail_global_disconnected();
+}
+
+struct mail_domain *mail_domain_lookup(const char *name)
+{
+ return hash_table_lookup(mail_domains_hash, name);
+}
+
+void mail_domain_ref(struct mail_domain *domain)
+{
+ domain->refcount++;
+}
+
+void mail_domain_unref(struct mail_domain **_domain)
+{
+ struct mail_domain *domain = *_domain;
+
+ i_assert(domain->refcount > 0);
+ domain->refcount--;
+
+ *_domain = NULL;
+}
+
+static void mail_domain_free(struct mail_domain *domain)
+{
+ i_assert(domain->refcount == 0);
+ i_assert(domain->users == NULL);
+
+ global_memory_free(mail_domain_memsize(domain));
+ hash_table_remove(mail_domains_hash, domain->name);
+ DLLIST_REMOVE_FULL(&stable_mail_domains, domain,
+ stable_prev, stable_next);
+ DLLIST2_REMOVE_FULL(&mail_domains_head, &mail_domains_tail, domain,
+ sorted_prev, sorted_next);
+
+ i_free(domain->name);
+ i_free(domain);
+}
+
+void mail_domain_refresh(struct mail_domain *domain,
+ const struct stats *diff_stats)
+{
+ if (diff_stats != NULL)
+ stats_add(domain->stats, diff_stats);
+ domain->last_update = ioloop_timeval;
+ DLLIST2_REMOVE_FULL(&mail_domains_head, &mail_domains_tail, domain,
+ sorted_prev, sorted_next);
+ DLLIST2_APPEND_FULL(&mail_domains_head, &mail_domains_tail, domain,
+ sorted_prev, sorted_next);
+ mail_global_refresh(diff_stats);
+}
+
+void mail_domains_free_memory(void)
+{
+ unsigned int diff;
+
+ while (mail_domains_head != NULL && mail_domains_head->refcount == 0) {
+ mail_domain_free(mail_domains_head);
+
+ if (global_used_memory < stats_settings->memory_limit ||
+ mail_domains_head == NULL)
+ break;
+
+ diff = ioloop_time - mail_domains_head->last_update.tv_sec;
+ if (diff < stats_settings->domain_min_time)
+ break;
+ }
+}
+
+void mail_domains_init(void)
+{
+ hash_table_create(&mail_domains_hash, default_pool, 0, str_hash, strcmp);
+}
+
+void mail_domains_deinit(void)
+{
+ while (mail_domains_head != NULL)
+ mail_domain_free(mail_domains_head);
+ hash_table_destroy(&mail_domains_hash);
+}
diff --git a/src/old-stats/mail-domain.h b/src/old-stats/mail-domain.h
new file mode 100644
index 0000000..23e2fbe
--- /dev/null
+++ b/src/old-stats/mail-domain.h
@@ -0,0 +1,22 @@
+#ifndef MAIL_DOMAIN_H
+#define MAIL_DOMAIN_H
+
+struct stats;
+
+extern struct mail_domain *stable_mail_domains;
+
+struct mail_domain *mail_domain_login_create(const char *name);
+void mail_domain_login(struct mail_domain *domain);
+void mail_domain_disconnected(struct mail_domain *domain);
+struct mail_domain *mail_domain_lookup(const char *name);
+void mail_domain_refresh(struct mail_domain *domain,
+ const struct stats *diff_stats) ATTR_NULL(2);
+
+void mail_domain_ref(struct mail_domain *domain);
+void mail_domain_unref(struct mail_domain **domain);
+
+void mail_domains_free_memory(void);
+void mail_domains_init(void);
+void mail_domains_deinit(void);
+
+#endif
diff --git a/src/old-stats/mail-ip.c b/src/old-stats/mail-ip.c
new file mode 100644
index 0000000..bb0209f
--- /dev/null
+++ b/src/old-stats/mail-ip.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-ip.h"
+
+static HASH_TABLE(struct ip_addr *, struct mail_ip *) mail_ips_hash;
+/* ips are sorted by their last_update timestamp, oldest first */
+static struct mail_ip *mail_ips_head, *mail_ips_tail;
+struct mail_ip *stable_mail_ips;
+
+static size_t mail_ip_memsize(const struct mail_ip *ip)
+{
+ return sizeof(*ip);
+}
+
+struct mail_ip *mail_ip_login(const struct ip_addr *ip_addr)
+{
+ struct mail_ip *ip;
+
+ ip = hash_table_lookup(mail_ips_hash, ip_addr);
+ if (ip != NULL) {
+ ip->num_logins++;
+ ip->num_connected_sessions++;
+ mail_ip_refresh(ip, NULL);
+ return ip;
+ }
+
+ ip = i_malloc(MALLOC_ADD(sizeof(struct mail_ip), stats_alloc_size()));
+ ip->stats = (void *)(ip + 1);
+ ip->ip = *ip_addr;
+ ip->reset_timestamp = ioloop_time;
+
+ hash_table_insert(mail_ips_hash, &ip->ip, ip);
+ DLLIST_PREPEND_FULL(&stable_mail_ips, ip, stable_prev, stable_next);
+ DLLIST2_APPEND_FULL(&mail_ips_head, &mail_ips_tail, ip,
+ sorted_prev, sorted_next);
+ ip->num_logins++;
+ ip->num_connected_sessions++;
+ ip->last_update = ioloop_timeval;
+ global_memory_alloc(mail_ip_memsize(ip));
+ return ip;
+}
+
+void mail_ip_disconnected(struct mail_ip *ip)
+{
+ i_assert(ip->num_connected_sessions > 0);
+ ip->num_connected_sessions--;
+}
+
+struct mail_ip *mail_ip_lookup(const struct ip_addr *ip_addr)
+{
+ return hash_table_lookup(mail_ips_hash, ip_addr);
+}
+
+void mail_ip_ref(struct mail_ip *ip)
+{
+ ip->refcount++;
+}
+
+void mail_ip_unref(struct mail_ip **_ip)
+{
+ struct mail_ip *ip = *_ip;
+
+ i_assert(ip->refcount > 0);
+ ip->refcount--;
+
+ *_ip = NULL;
+}
+
+static void mail_ip_free(struct mail_ip *ip)
+{
+ i_assert(ip->refcount == 0);
+ i_assert(ip->sessions == NULL);
+
+ global_memory_free(mail_ip_memsize(ip));
+ hash_table_remove(mail_ips_hash, &ip->ip);
+ DLLIST_REMOVE_FULL(&stable_mail_ips, ip, stable_prev, stable_next);
+ DLLIST2_REMOVE_FULL(&mail_ips_head, &mail_ips_tail, ip,
+ sorted_prev, sorted_next);
+
+ i_free(ip);
+}
+
+void mail_ip_refresh(struct mail_ip *ip, const struct stats *diff_stats)
+{
+ if (diff_stats != NULL)
+ stats_add(ip->stats, diff_stats);
+ ip->last_update = ioloop_timeval;
+ DLLIST2_REMOVE_FULL(&mail_ips_head, &mail_ips_tail, ip,
+ sorted_prev, sorted_next);
+ DLLIST2_APPEND_FULL(&mail_ips_head, &mail_ips_tail, ip,
+ sorted_prev, sorted_next);
+}
+
+void mail_ips_free_memory(void)
+{
+ unsigned int diff;
+
+ while (mail_ips_head != NULL && mail_ips_head->refcount == 0) {
+ mail_ip_free(mail_ips_head);
+
+ if (global_used_memory < stats_settings->memory_limit ||
+ mail_ips_head == NULL)
+ break;
+
+ diff = ioloop_time - mail_ips_head->last_update.tv_sec;
+ if (diff < stats_settings->ip_min_time)
+ break;
+ }
+}
+
+void mail_ips_init(void)
+{
+ hash_table_create(&mail_ips_hash, default_pool, 0,
+ net_ip_hash, net_ip_cmp);
+}
+
+void mail_ips_deinit(void)
+{
+ while (mail_ips_head != NULL)
+ mail_ip_free(mail_ips_head);
+ hash_table_destroy(&mail_ips_hash);
+}
diff --git a/src/old-stats/mail-ip.h b/src/old-stats/mail-ip.h
new file mode 100644
index 0000000..f7ff864
--- /dev/null
+++ b/src/old-stats/mail-ip.h
@@ -0,0 +1,19 @@
+#ifndef MAIL_IP_H
+#define MAIL_IP_H
+
+extern struct mail_ip *stable_mail_ips;
+
+struct mail_ip *mail_ip_login(const struct ip_addr *ip_addr);
+void mail_ip_disconnected(struct mail_ip *ip);
+struct mail_ip *mail_ip_lookup(const struct ip_addr *ip_addr);
+void mail_ip_refresh(struct mail_ip *ip, const struct stats *diff_stats)
+ ATTR_NULL(2);
+
+void mail_ip_ref(struct mail_ip *ip);
+void mail_ip_unref(struct mail_ip **ip);
+
+void mail_ips_free_memory(void);
+void mail_ips_init(void);
+void mail_ips_deinit(void);
+
+#endif
diff --git a/src/old-stats/mail-session.c b/src/old-stats/mail-session.c
new file mode 100644
index 0000000..db202b3
--- /dev/null
+++ b/src/old-stats/mail-session.c
@@ -0,0 +1,343 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "str-table.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-user.h"
+#include "mail-ip.h"
+#include "mail-session.h"
+#include "mail-domain.h"
+
+/* If session doesn't receive any updates for this long, assume that the
+ process associated with it has crashed, and forcibly disconnect the
+ session. Must be larger than SESSION_STATS_FORCE_REFRESH_SECS in
+ stats plugin */
+#define MAIL_SESSION_IDLE_TIMEOUT_MSECS (1000*60*15)
+/* If stats process crashes/restarts, existing processes keep sending status
+ updates to it, but this process doesn't know their session IDs. If these
+ missing IDs are found within this many seconds of starting the stats process,
+ don't log a warning about them. (On a larger installation this avoids
+ flooding the error log with hundreds of warnings.) */
+#define SESSION_ID_WARN_HIDE_SECS (60*5)
+
+static HASH_TABLE(char *, struct mail_session *) mail_sessions_hash;
+/* sessions are sorted by their last_update timestamp, oldest first */
+static struct mail_session *mail_sessions_head, *mail_sessions_tail;
+static time_t session_id_warn_hide_until;
+static bool session_id_hide_warned = FALSE;
+static struct str_table *services;
+
+struct mail_session *stable_mail_sessions;
+
+static size_t mail_session_memsize(const struct mail_session *session)
+{
+ return sizeof(*session) + strlen(session->id) + 1;
+}
+
+static void mail_session_disconnect(struct mail_session *session)
+{
+ i_assert(!session->disconnected);
+
+ mail_user_disconnected(session->user);
+ if (session->ip != NULL)
+ mail_ip_disconnected(session->ip);
+
+ hash_table_remove(mail_sessions_hash, session->id);
+ session->disconnected = TRUE;
+ timeout_remove(&session->to_idle);
+ mail_session_unref(&session);
+}
+
+static void mail_session_idle_timeout(struct mail_session *session)
+{
+ /* user="" service="" pid=0 is used for incoming sessions that were
+ received after we detected a stats process crash/restart. there's
+ no point in logging anything about them, since they contain no
+ useful information. */
+ if (session->user->name[0] == '\0' && session->service[0] != '\0' &&
+ session->pid == 0) {
+ i_warning("Session %s (user %s, service %s) "
+ "appears to have crashed, disconnecting it",
+ session->id, session->user->name, session->service);
+ }
+ mail_session_disconnect(session);
+}
+
+int mail_session_connect_parse(const char *const *args, const char **error_r)
+{
+ struct mail_session *session;
+ const char *session_id;
+ pid_t pid;
+ struct ip_addr ip;
+ unsigned int i;
+
+ /* <session id> <username> <service> <pid> [key=value ..] */
+ if (str_array_length(args) < 4) {
+ *error_r = "CONNECT: Too few parameters";
+ return -1;
+ }
+ session_id = args[0];
+ if (str_to_pid(args[3], &pid) < 0) {
+ *error_r = t_strdup_printf("CONNECT: Invalid pid %s for session ID %s",
+ args[3], session_id);
+ return -1;
+ }
+
+ session = hash_table_lookup(mail_sessions_hash, session_id);
+ if (session != NULL) {
+ *error_r = t_strdup_printf(
+ "CONNECT: Duplicate session ID %s for user %s service %s (old PID %ld, new PID %ld)",
+ session_id, args[1], args[2], (long)session->pid, (long)pid);
+ return -1;
+ }
+ session = i_malloc(MALLOC_ADD(sizeof(struct mail_session), stats_alloc_size()));
+ session->stats = (void *)(session + 1);
+ session->refcount = 1; /* unrefed at disconnect */
+ session->id = i_strdup(session_id);
+ session->service = str_table_ref(services, args[2]);
+ session->pid = pid;
+ session->last_update = ioloop_timeval;
+ session->to_idle = timeout_add(MAIL_SESSION_IDLE_TIMEOUT_MSECS,
+ mail_session_idle_timeout, session);
+
+ session->user = mail_user_login(args[1]);
+ session->user->num_logins++;
+ mail_domain_login(session->user->domain);
+
+ for (i = 3; args[i] != NULL; i++) {
+ if (str_begins(args[i], "rip=") &&
+ net_addr2ip(args[i] + 4, &ip) == 0)
+ session->ip = mail_ip_login(&ip);
+ }
+
+ hash_table_insert(mail_sessions_hash, session->id, session);
+ DLLIST_PREPEND_FULL(&stable_mail_sessions, session,
+ stable_prev, stable_next);
+ DLLIST2_APPEND_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+ sorted_prev, sorted_next);
+ DLLIST_PREPEND_FULL(&session->user->sessions, session,
+ user_prev, user_next);
+ mail_user_ref(session->user);
+ if (session->ip != NULL) {
+ DLLIST_PREPEND_FULL(&session->ip->sessions, session,
+ ip_prev, ip_next);
+ mail_ip_ref(session->ip);
+ }
+ global_memory_alloc(mail_session_memsize(session));
+
+ mail_global_login();
+ return 0;
+}
+
+void mail_session_ref(struct mail_session *session)
+{
+ session->refcount++;
+}
+
+void mail_session_unref(struct mail_session **_session)
+{
+ struct mail_session *session = *_session;
+
+ i_assert(session->refcount > 0);
+ session->refcount--;
+
+ *_session = NULL;
+}
+
+static void mail_session_free(struct mail_session *session)
+{
+ i_assert(session->refcount == 0);
+
+ global_memory_free(mail_session_memsize(session));
+
+ timeout_remove(&session->to_idle);
+ if (!session->disconnected)
+ hash_table_remove(mail_sessions_hash, session->id);
+ DLLIST_REMOVE_FULL(&stable_mail_sessions, session,
+ stable_prev, stable_next);
+ DLLIST2_REMOVE_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+ sorted_prev, sorted_next);
+ DLLIST_REMOVE_FULL(&session->user->sessions, session,
+ user_prev, user_next);
+ mail_user_unref(&session->user);
+ if (session->ip != NULL) {
+ DLLIST_REMOVE_FULL(&session->ip->sessions, session,
+ ip_prev, ip_next);
+ mail_ip_unref(&session->ip);
+ }
+
+ str_table_unref(services, &session->service);
+ i_free(session->id);
+ i_free(session);
+}
+
+static void mail_session_id_lost(const char *session_id)
+{
+ if (ioloop_time < session_id_warn_hide_until) {
+ if (session_id_hide_warned)
+ return;
+ session_id_hide_warned = TRUE;
+ i_warning("stats process appears to have crashed/restarted, "
+ "hiding missing session ID warnings for %d seconds",
+ (int)(session_id_warn_hide_until - ioloop_time));
+ return;
+ }
+ i_warning("Couldn't find session ID: %s", session_id);
+}
+
+int mail_session_lookup(const char *id, struct mail_session **session_r,
+ const char **error_r)
+{
+ if (id == NULL) {
+ *error_r = "Too few parameters";
+ return -1;
+ }
+ *session_r = hash_table_lookup(mail_sessions_hash, id);
+ if (*session_r == NULL) {
+ mail_session_id_lost(id);
+ return 0;
+ }
+ return 1;
+}
+
+int mail_session_get(const char *id, struct mail_session **session_r,
+ const char **error_r)
+{
+ const char *new_args[5];
+ int ret;
+
+ if ((ret = mail_session_lookup(id, session_r, error_r)) != 0)
+ return ret;
+
+ /* Create a new dummy session to avoid repeated warnings */
+ new_args[0] = id;
+ new_args[1] = ""; /* username */
+ new_args[2] = ""; /* service */
+ new_args[3] = "0"; /* pid */
+ new_args[4] = NULL;
+ if (mail_session_connect_parse(new_args, error_r) < 0)
+ i_unreached();
+ if (mail_session_lookup(id, session_r, error_r) != 1)
+ i_unreached();
+ return 0;
+}
+
+int mail_session_disconnect_parse(const char *const *args, const char **error_r)
+{
+ struct mail_session *session;
+ int ret;
+
+ /* <session id> */
+ if ((ret = mail_session_lookup(args[0], &session, error_r)) <= 0)
+ return ret;
+
+ if (!session->disconnected)
+ mail_session_disconnect(session);
+ return 0;
+}
+
+void mail_session_refresh(struct mail_session *session,
+ const struct stats *diff_stats)
+{
+ timeout_reset(session->to_idle);
+
+ if (diff_stats != NULL)
+ stats_add(session->stats, diff_stats);
+ session->last_update = ioloop_timeval;
+ DLLIST2_REMOVE_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+ sorted_prev, sorted_next);
+ DLLIST2_APPEND_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+ sorted_prev, sorted_next);
+
+ mail_user_refresh(session->user, diff_stats);
+ if (session->ip != NULL)
+ mail_ip_refresh(session->ip, diff_stats);
+}
+
+int mail_session_update_parse(const char *const *args, const char **error_r)
+{
+ struct mail_session *session;
+ struct stats *new_stats, *diff_stats;
+ buffer_t *buf;
+ const char *error;
+
+ /* <session id> <stats> */
+ if (mail_session_get(args[0], &session, error_r) < 0)
+ return -1;
+
+ buf = t_buffer_create(256);
+ if (args[1] == NULL ||
+ base64_decode(args[1], strlen(args[1]), NULL, buf) < 0) {
+ *error_r = t_strdup_printf("UPDATE-SESSION %s %s %s: Invalid base64 input",
+ session->user->name,
+ session->service, session->id);
+ return -1;
+ }
+
+ new_stats = stats_alloc(pool_datastack_create());
+ diff_stats = stats_alloc(pool_datastack_create());
+
+ if (!stats_import(buf->data, buf->used, session->stats, new_stats, &error)) {
+ *error_r = t_strdup_printf("UPDATE-SESSION %s %s %s: %s",
+ session->user->name,
+ session->service, session->id, error);
+ return -1;
+ }
+
+ if (!stats_diff(session->stats, new_stats, diff_stats, &error)) {
+ *error_r = t_strdup_printf("UPDATE-SESSION %s %s %s: stats shrank: %s",
+ session->user->name,
+ session->service, session->id, error);
+ return -1;
+ }
+ mail_session_refresh(session, diff_stats);
+ return 0;
+}
+
+void mail_sessions_free_memory(void)
+{
+ unsigned int diff;
+
+ while (mail_sessions_head != NULL &&
+ mail_sessions_head->refcount == 0) {
+ i_assert(mail_sessions_head->disconnected);
+ mail_session_free(mail_sessions_head);
+
+ if (global_used_memory < stats_settings->memory_limit ||
+ mail_sessions_head == NULL)
+ break;
+
+ diff = ioloop_time - mail_sessions_head->last_update.tv_sec;
+ if (diff < stats_settings->session_min_time)
+ break;
+ }
+}
+
+void mail_sessions_init(void)
+{
+ session_id_warn_hide_until =
+ ioloop_time + SESSION_ID_WARN_HIDE_SECS;
+ hash_table_create(&mail_sessions_hash, default_pool, 0,
+ str_hash, strcmp);
+ services = str_table_init();
+}
+
+void mail_sessions_deinit(void)
+{
+ while (mail_sessions_head != NULL) {
+ struct mail_session *session = mail_sessions_head;
+
+ if (!session->disconnected)
+ mail_session_unref(&session);
+ mail_session_free(mail_sessions_head);
+ }
+ hash_table_destroy(&mail_sessions_hash);
+ str_table_deinit(&services);
+}
diff --git a/src/old-stats/mail-session.h b/src/old-stats/mail-session.h
new file mode 100644
index 0000000..efdc82a
--- /dev/null
+++ b/src/old-stats/mail-session.h
@@ -0,0 +1,28 @@
+#ifndef MAIL_SESSION_H
+#define MAIL_SESSION_H
+
+struct stats;
+struct mail_session;
+
+extern struct mail_session *stable_mail_sessions;
+
+int mail_session_connect_parse(const char *const *args, const char **error_r);
+int mail_session_disconnect_parse(const char *const *args, const char **error_r);
+int mail_session_update_parse(const char *const *args, const char **error_r);
+int mail_session_cmd_update_parse(const char *const *args, const char **error_r);
+
+void mail_session_ref(struct mail_session *session);
+void mail_session_unref(struct mail_session **session);
+
+int mail_session_lookup(const char *guid, struct mail_session **session_r,
+ const char **error_r);
+int mail_session_get(const char *guid, struct mail_session **session_r,
+ const char **error_r);
+void mail_session_refresh(struct mail_session *session,
+ const struct stats *diff_stats) ATTR_NULL(2);
+
+void mail_sessions_free_memory(void);
+void mail_sessions_init(void);
+void mail_sessions_deinit(void);
+
+#endif
diff --git a/src/old-stats/mail-stats.c b/src/old-stats/mail-stats.c
new file mode 100644
index 0000000..6ba3250
--- /dev/null
+++ b/src/old-stats/mail-stats.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mail-stats.h"
+#include "stats-carbon.h"
+#include "stats-settings.h"
+#include "str.h"
+
+struct mail_global mail_global_stats;
+
+static void
+mail_global_stats_sent(void *ctx)
+{
+ struct mail_global *stats = ctx;
+ stats_carbon_destroy(&stats->stats_send_ctx);
+}
+
+static void
+mail_global_stats_send(void *u0 ATTR_UNUSED)
+{
+ time_t ts = ioloop_time;
+ if (*stats_settings->carbon_name != '\0' &&
+ *stats_settings->carbon_server != '\0') {
+ string_t *str = t_str_new(256);
+ const char *prefix = t_strdup_printf("dovecot.%s.global",
+ stats_settings->carbon_name);
+ str_printfa(str, "%s.logins %u %"PRIdTIME_T"\r\n", prefix,
+ mail_global_stats.num_logins, ts);
+ str_printfa(str, "%s.cmds %u %"PRIdTIME_T"\r\n", prefix,
+ mail_global_stats.num_cmds, ts);
+ str_printfa(str, "%s.connected_sessions %u %"PRIdTIME_T"\r\n",
+ prefix, mail_global_stats.num_connected_sessions,
+ ts);
+ str_printfa(str, "%s.last_reset %"PRIdTIME_T" %"PRIdTIME_T"\r\n",
+ prefix, mail_global_stats.reset_timestamp, ts);
+ /* then export rest of the stats */
+ for(size_t i = 0; i < stats_field_count(); i++) {
+ str_printfa(str, "%s.%s ", prefix,
+ stats_field_name(i));
+ stats_field_value(str, mail_global_stats.stats, i);
+ str_printfa(str, " %"PRIdTIME_T"\r\n", ts);
+ }
+
+ /* and send them along */
+ (void)stats_carbon_send(stats_settings->carbon_server, str_c(str),
+ mail_global_stats_sent, &mail_global_stats,
+ &mail_global_stats.stats_send_ctx);
+ }
+}
+
+void mail_global_init(void)
+{
+ mail_global_stats.reset_timestamp = ioloop_time;
+ mail_global_stats.stats = stats_alloc(default_pool);
+ mail_global_stats.to_stats_send = timeout_add(stats_settings->carbon_interval*1000,
+ mail_global_stats_send,
+ NULL);
+}
+
+void mail_global_deinit(void)
+{
+ if (mail_global_stats.stats_send_ctx != NULL)
+ stats_carbon_destroy(&mail_global_stats.stats_send_ctx);
+ timeout_remove(&mail_global_stats.to_stats_send);
+ i_free(mail_global_stats.stats);
+}
+
+void mail_global_login(void)
+{
+ mail_global_stats.num_logins++;
+ mail_global_stats.num_connected_sessions++;
+}
+
+void mail_global_disconnected(void)
+{
+ i_assert(mail_global_stats.num_connected_sessions > 0);
+ mail_global_stats.num_connected_sessions--;
+}
+
+void mail_global_refresh(const struct stats *diff_stats)
+{
+ if (diff_stats != NULL)
+ stats_add(mail_global_stats.stats, diff_stats);
+ mail_global_stats.last_update = ioloop_timeval;
+}
diff --git a/src/old-stats/mail-stats.h b/src/old-stats/mail-stats.h
new file mode 100644
index 0000000..efd0824
--- /dev/null
+++ b/src/old-stats/mail-stats.h
@@ -0,0 +1,123 @@
+#ifndef MAIL_STATS_H
+#define MAIL_STATS_H
+
+#include <sys/time.h>
+
+#include "net.h"
+#include "guid.h"
+#include "stats.h"
+
+struct stats_send_ctx;
+
+struct mail_command {
+ struct mail_command *stable_prev, *stable_next;
+ struct mail_command *session_prev, *session_next;
+
+ struct mail_session *session;
+ char *name, *args;
+ /* non-zero id means the command is still running */
+ unsigned int id;
+
+ struct timeval last_update;
+ struct stats *stats;
+
+ int refcount;
+};
+
+struct mail_session {
+ struct mail_session *stable_prev, *stable_next;
+ struct mail_session *sorted_prev, *sorted_next;
+ struct mail_session *user_prev, *user_next;
+ struct mail_session *ip_prev, *ip_next;
+
+ /* if id="", the session no longer exists */
+ char *id;
+ struct mail_user *user;
+ const char *service;
+ pid_t pid;
+ /* ip address may be NULL if there's none */
+ struct mail_ip *ip;
+ struct timeout *to_idle;
+
+ struct stats *stats;
+ struct timeval last_update;
+ unsigned int num_cmds;
+
+ bool disconnected;
+ unsigned int highest_cmd_id;
+ int refcount;
+ struct mail_command *commands;
+};
+
+struct mail_user {
+ struct mail_user *stable_prev, *stable_next;
+ struct mail_user *sorted_prev, *sorted_next;
+ struct mail_user *domain_prev, *domain_next;
+ char *name;
+ struct mail_domain *domain;
+ time_t reset_timestamp;
+
+ struct timeval last_update;
+ struct stats *stats;
+ unsigned int num_logins;
+ unsigned int num_cmds;
+
+ int refcount;
+ struct mail_session *sessions;
+};
+
+struct mail_domain {
+ struct mail_domain *stable_prev, *stable_next;
+ struct mail_domain *sorted_prev, *sorted_next;
+ char *name;
+ time_t reset_timestamp;
+
+ struct timeval last_update;
+ struct stats *stats;
+ unsigned int num_logins;
+ unsigned int num_cmds;
+ unsigned int num_connected_sessions;
+
+ int refcount;
+ struct mail_user *users;
+};
+
+struct mail_ip {
+ struct mail_ip *stable_prev, *stable_next;
+ struct mail_ip *sorted_prev, *sorted_next;
+ struct ip_addr ip;
+ time_t reset_timestamp;
+
+ struct timeval last_update;
+ struct stats *stats;
+ unsigned int num_logins;
+ unsigned int num_cmds;
+ unsigned int num_connected_sessions;
+
+ int refcount;
+ struct mail_session *sessions;
+};
+
+struct mail_global {
+ time_t reset_timestamp;
+
+ struct timeval last_update;
+ struct stats *stats;
+ unsigned int num_logins;
+ unsigned int num_cmds;
+ unsigned int num_connected_sessions;
+
+ struct timeout *to_stats_send;
+ struct stats_send_ctx *stats_send_ctx;
+};
+
+extern struct mail_global mail_global_stats;
+
+void mail_global_init(void);
+void mail_global_deinit(void);
+
+void mail_global_login(void);
+void mail_global_disconnected(void);
+void mail_global_refresh(const struct stats *diff_stats);
+
+#endif
diff --git a/src/old-stats/mail-user.c b/src/old-stats/mail-user.c
new file mode 100644
index 0000000..7b3bc7a
--- /dev/null
+++ b/src/old-stats/mail-user.c
@@ -0,0 +1,177 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "base64.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-domain.h"
+#include "mail-user.h"
+
+static HASH_TABLE(char *, struct mail_user *) mail_users_hash;
+/* users are sorted by their last_update timestamp, oldest first */
+static struct mail_user *mail_users_head, *mail_users_tail;
+struct mail_user *stable_mail_users;
+
+static size_t mail_user_memsize(const struct mail_user *user)
+{
+ return sizeof(*user) + strlen(user->name) + 1;
+}
+
+struct mail_user *mail_user_login(const char *username)
+{
+ struct mail_user *user;
+ const char *domain;
+
+ user = hash_table_lookup(mail_users_hash, username);
+ if (user != NULL) {
+ mail_user_refresh(user, NULL);
+ return user;
+ }
+
+ domain = i_strchr_to_next(username, '@');
+ if (domain == NULL)
+ domain = "";
+
+ user = i_malloc(MALLOC_ADD(sizeof(struct mail_user), stats_alloc_size()));
+ user->stats = (void *)(user + 1);
+ user->name = i_strdup(username);
+ user->reset_timestamp = ioloop_time;
+ user->domain = mail_domain_login_create(domain);
+
+ hash_table_insert(mail_users_hash, user->name, user);
+ DLLIST_PREPEND_FULL(&stable_mail_users, user,
+ stable_prev, stable_next);
+ DLLIST2_APPEND_FULL(&mail_users_head, &mail_users_tail, user,
+ sorted_prev, sorted_next);
+ DLLIST_PREPEND_FULL(&user->domain->users, user,
+ domain_prev, domain_next);
+ mail_domain_ref(user->domain);
+
+ user->last_update = ioloop_timeval;
+ global_memory_alloc(mail_user_memsize(user));
+ return user;
+}
+
+void mail_user_disconnected(struct mail_user *user)
+{
+ mail_domain_disconnected(user->domain);
+}
+
+struct mail_user *mail_user_lookup(const char *username)
+{
+ return hash_table_lookup(mail_users_hash, username);
+}
+
+void mail_user_ref(struct mail_user *user)
+{
+ user->refcount++;
+}
+
+void mail_user_unref(struct mail_user **_user)
+{
+ struct mail_user *user = *_user;
+
+ i_assert(user->refcount > 0);
+ user->refcount--;
+
+ *_user = NULL;
+}
+
+static void mail_user_free(struct mail_user *user)
+{
+ i_assert(user->refcount == 0);
+ i_assert(user->sessions == NULL);
+
+ global_memory_free(mail_user_memsize(user));
+ hash_table_remove(mail_users_hash, user->name);
+ DLLIST_REMOVE_FULL(&stable_mail_users, user,
+ stable_prev, stable_next);
+ DLLIST2_REMOVE_FULL(&mail_users_head, &mail_users_tail, user,
+ sorted_prev, sorted_next);
+ DLLIST_REMOVE_FULL(&user->domain->users, user,
+ domain_prev, domain_next);
+ mail_domain_unref(&user->domain);
+
+ i_free(user->name);
+ i_free(user);
+}
+
+void mail_user_refresh(struct mail_user *user,
+ const struct stats *diff_stats)
+{
+ if (diff_stats != NULL)
+ stats_add(user->stats, diff_stats);
+ user->last_update = ioloop_timeval;
+ DLLIST2_REMOVE_FULL(&mail_users_head, &mail_users_tail, user,
+ sorted_prev, sorted_next);
+ DLLIST2_APPEND_FULL(&mail_users_head, &mail_users_tail, user,
+ sorted_prev, sorted_next);
+ mail_domain_refresh(user->domain, diff_stats);
+}
+
+int mail_user_add_parse(const char *const *args, const char **error_r)
+{
+ struct mail_user *user;
+ struct stats *empty_stats, *diff_stats;
+ buffer_t *buf;
+ const char *service, *error;
+
+ /* <user> <service> <diff stats> */
+ if (str_array_length(args) < 3) {
+ *error_r = "ADD-USER: Too few parameters";
+ return -1;
+ }
+
+ user = mail_user_login(args[0]);
+ service = args[1];
+
+ buf = t_buffer_create(256);
+ if (base64_decode(args[2], strlen(args[2]), NULL, buf) < 0) {
+ *error_r = t_strdup_printf("ADD-USER %s %s: Invalid base64 input",
+ user->name, service);
+ return -1;
+ }
+ empty_stats = stats_alloc(pool_datastack_create());
+ diff_stats = stats_alloc(pool_datastack_create());
+ if (!stats_import(buf->data, buf->used, empty_stats, diff_stats, &error)) {
+ *error_r = t_strdup_printf("ADD-USER %s %s: %s",
+ user->name, service, error);
+ return -1;
+ }
+ mail_user_refresh(user, diff_stats);
+ return 0;
+}
+
+void mail_users_free_memory(void)
+{
+ unsigned int diff;
+
+ while (mail_users_head != NULL && mail_users_head->refcount == 0) {
+ mail_user_free(mail_users_head);
+
+ if (global_used_memory < stats_settings->memory_limit ||
+ mail_users_head == NULL)
+ break;
+
+ diff = ioloop_time - mail_users_head->last_update.tv_sec;
+ if (diff < stats_settings->user_min_time)
+ break;
+ }
+}
+
+void mail_users_init(void)
+{
+ hash_table_create(&mail_users_hash, default_pool, 0, str_hash, strcmp);
+}
+
+void mail_users_deinit(void)
+{
+ while (mail_users_head != NULL)
+ mail_user_free(mail_users_head);
+ hash_table_destroy(&mail_users_hash);
+}
diff --git a/src/old-stats/mail-user.h b/src/old-stats/mail-user.h
new file mode 100644
index 0000000..ff1949e
--- /dev/null
+++ b/src/old-stats/mail-user.h
@@ -0,0 +1,23 @@
+#ifndef MAIL_USER_H
+#define MAIL_USER_H
+
+struct stats;
+
+extern struct mail_user *stable_mail_users;
+
+struct mail_user *mail_user_login(const char *username);
+void mail_user_disconnected(struct mail_user *user);
+struct mail_user *mail_user_lookup(const char *username);
+
+void mail_user_refresh(struct mail_user *user,
+ const struct stats *diff_stats) ATTR_NULL(2);
+int mail_user_add_parse(const char *const *args, const char **error_r);
+
+void mail_user_ref(struct mail_user *user);
+void mail_user_unref(struct mail_user **user);
+
+void mail_users_free_memory(void);
+void mail_users_init(void);
+void mail_users_deinit(void);
+
+#endif
diff --git a/src/old-stats/main.c b/src/old-stats/main.c
new file mode 100644
index 0000000..49f50f4
--- /dev/null
+++ b/src/old-stats/main.c
@@ -0,0 +1,95 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "fifo-input-connection.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "mail-stats.h"
+#include "client.h"
+
+static struct module *modules = NULL;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (conn->fifo)
+ (void)fifo_input_connection_create(conn->fd);
+ else
+ (void)client_create(conn->fd);
+ master_service_client_connection_accept(conn);
+}
+
+static void main_preinit(void)
+{
+ struct module_dir_load_settings mod_set;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+
+ modules = module_dir_load(STATS_MODULE_DIR, NULL, &mod_set);
+ module_dir_init(modules);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &old_stats_setting_parser_info,
+ NULL
+ };
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE |
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+ const char *error;
+ void **sets;
+
+ master_service = master_service_init("stats", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+
+ main_preinit();
+
+ sets = master_service_settings_get_others(master_service);
+ stats_settings = sets[0];
+
+ mail_commands_init();
+ mail_sessions_init();
+ mail_users_init();
+ mail_domains_init();
+ mail_ips_init();
+ mail_global_init();
+
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+
+ clients_destroy_all();
+ fifo_input_connections_destroy_all();
+ mail_commands_deinit();
+ mail_sessions_deinit();
+ mail_users_deinit();
+ mail_domains_deinit();
+ mail_ips_deinit();
+ mail_global_deinit();
+
+ module_dir_unload(&modules);
+ i_assert(global_used_memory == 0);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/old-stats/stats-carbon.c b/src/old-stats/stats-carbon.c
new file mode 100644
index 0000000..7e984cf
--- /dev/null
+++ b/src/old-stats/stats-carbon.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "str.h"
+#include "write-full.h"
+#include "stats-carbon.h"
+
+#define CARBON_SERVER_DEFAULT_PORT 2003
+
+struct stats_send_ctx {
+ pool_t pool;
+ int fd;
+ unsigned long to_msecs;
+ const char *endpoint;
+ const char *str;
+ struct io *io;
+ struct timeout *to;
+
+ void (*callback)(void *);
+ void *ctx;
+};
+
+void
+stats_carbon_destroy(struct stats_send_ctx **_ctx)
+{
+ struct stats_send_ctx *ctx = *_ctx;
+ *_ctx = NULL;
+
+ io_remove(&ctx->io);
+ timeout_remove(&ctx->to);
+ i_close_fd(&ctx->fd);
+ pool_unref(&ctx->pool);
+}
+
+static void
+stats_carbon_callback(struct stats_send_ctx *ctx)
+{
+ i_assert(ctx->callback != NULL);
+ void (*callback)(void *) = ctx->callback;
+ ctx->callback = NULL;
+ callback(ctx->ctx);
+}
+
+static void
+stats_carbon_timeout(struct stats_send_ctx *ctx)
+{
+ i_error("Stats submit(%s) failed: endpoint timeout after %lu msecs",
+ ctx->endpoint, ctx->to_msecs);
+ stats_carbon_callback(ctx);
+}
+
+static void
+stats_carbon_connected(struct stats_send_ctx *ctx)
+{
+ io_remove(&ctx->io);
+ if ((errno = net_geterror(ctx->fd)) != 0) {
+ i_error("connect(%s) failed: %m",
+ ctx->endpoint);
+ stats_carbon_callback(ctx);
+ return;
+ }
+ if (write_full(ctx->fd, ctx->str, strlen(ctx->str)) < 0)
+ i_error("write(%s) failed: %m",
+ ctx->endpoint);
+ stats_carbon_callback(ctx);
+}
+
+int
+stats_carbon_send(const char *endpoint, const char *data,
+ void (*callback)(void *), void *cb_ctx,
+ struct stats_send_ctx **ctx_r)
+{
+ const char *host;
+ in_port_t port;
+ struct ip_addr ip;
+
+ if (net_str2hostport(endpoint, CARBON_SERVER_DEFAULT_PORT,
+ &host, &port) < 0 ||
+ net_addr2ip(host, &ip) < 0) {
+ i_error("stats_submit: Cannot parse endpoint '%s'",
+ endpoint);
+ return -1;
+ }
+
+ pool_t pool = pool_alloconly_create("stats carbon send", 1024);
+ struct stats_send_ctx *ctx = p_new(pool,
+ struct stats_send_ctx, 1);
+ ctx->pool = pool;
+ ctx->str = p_strdup(ctx->pool, data);
+
+ ctx->fd = net_connect_ip(&ip, port, NULL);
+ if (ctx->fd < 0) {
+ i_error("connect(%s) failed: %m", endpoint);
+ stats_carbon_callback(ctx);
+ return -1;
+ }
+ ctx->io = io_add(ctx->fd, IO_WRITE,
+ stats_carbon_connected,
+ ctx);
+
+ /* give time for almost until next update
+ this is to ensure we leave a little pause between
+ attempts. Multiplier 800 gives us 20% window, and
+ ensures the number stays positive. */
+ ctx->to_msecs = stats_settings->carbon_interval*800;
+ ctx->to = timeout_add(ctx->to_msecs,
+ stats_carbon_timeout,
+ ctx);
+ if (net_ipport2str(&ip, port, &host) < 0)
+ i_unreached();
+ ctx->endpoint = p_strdup(ctx->pool, host);
+ ctx->callback = callback;
+ ctx->ctx = cb_ctx;
+
+ *ctx_r = ctx;
+
+ return 0;
+}
diff --git a/src/old-stats/stats-carbon.h b/src/old-stats/stats-carbon.h
new file mode 100644
index 0000000..0785976
--- /dev/null
+++ b/src/old-stats/stats-carbon.h
@@ -0,0 +1,13 @@
+#ifndef STATS_CARBON
+#define STATS_CARBON 1
+
+struct stats_send_ctx;
+
+int
+stats_carbon_send(const char *endpoint, const char *data,
+ void (*callback)(void *), void *cb_ctx,
+ struct stats_send_ctx **ctx_r);
+void
+stats_carbon_destroy(struct stats_send_ctx **ctx);
+
+#endif
diff --git a/src/old-stats/stats-settings.c b/src/old-stats/stats-settings.c
new file mode 100644
index 0000000..3aa65af
--- /dev/null
+++ b/src/old-stats/stats-settings.c
@@ -0,0 +1,104 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "stats-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings old_stats_unix_listeners_array[] = {
+ { "old-stats", 0600, "", "" }
+};
+static struct file_listener_settings *old_stats_unix_listeners[] = {
+ &old_stats_unix_listeners_array[0]
+};
+static buffer_t old_stats_unix_listeners_buf = {
+ { { old_stats_unix_listeners, sizeof(old_stats_unix_listeners) } }
+};
+static struct file_listener_settings old_stats_fifo_listeners_array[] = {
+ { "old-stats-mail", 0600, "", "" },
+ { "old-stats-user", 0600, "", "" }
+};
+static struct file_listener_settings *old_stats_fifo_listeners[] = {
+ &old_stats_fifo_listeners_array[0],
+ &old_stats_fifo_listeners_array[1]
+};
+static buffer_t old_stats_fifo_listeners_buf = {
+ { { old_stats_fifo_listeners, sizeof(old_stats_fifo_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings old_stats_service_settings = {
+ .name = "old-stats",
+ .protocol = "",
+ .type = "",
+ .executable = "old-stats",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "empty",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &old_stats_unix_listeners_buf,
+ sizeof(old_stats_unix_listeners[0]) } },
+ .fifo_listeners = { { &old_stats_fifo_listeners_buf,
+ sizeof(old_stats_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+
+/* we're kind of kludging here to avoid "stats_" prefix in the struct fields */
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type("old_stats_"#name, name, struct old_stats_settings)
+
+static const struct setting_define old_stats_setting_defines[] = {
+ DEF(SIZE, memory_limit),
+ DEF(TIME, command_min_time),
+ DEF(TIME, session_min_time),
+ DEF(TIME, user_min_time),
+ DEF(TIME, domain_min_time),
+ DEF(TIME, ip_min_time),
+ DEF(STR, carbon_server),
+ DEF(TIME, carbon_interval),
+ DEF(STR, carbon_name),
+ SETTING_DEFINE_LIST_END
+};
+
+const struct old_stats_settings old_stats_default_settings = {
+ .memory_limit = 1024*1024*16,
+
+ .command_min_time = 60,
+ .session_min_time = 60*15,
+ .user_min_time = 60*60,
+ .domain_min_time = 60*60*12,
+ .ip_min_time = 60*60*12,
+
+ .carbon_interval = 30,
+ .carbon_server = "",
+ .carbon_name = ""
+};
+
+const struct setting_parser_info old_stats_setting_parser_info = {
+ .module_name = "stats",
+ .defines = old_stats_setting_defines,
+ .defaults = &old_stats_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct old_stats_settings),
+
+ .parent_offset = SIZE_MAX
+};
+
+const struct old_stats_settings *stats_settings;
diff --git a/src/old-stats/stats-settings.h b/src/old-stats/stats-settings.h
new file mode 100644
index 0000000..6b50b64
--- /dev/null
+++ b/src/old-stats/stats-settings.h
@@ -0,0 +1,22 @@
+#ifndef STATS_SETTINGS_H
+#define STATS_SETTINGS_H
+
+struct old_stats_settings {
+ uoff_t memory_limit;
+
+ unsigned int command_min_time;
+ unsigned int session_min_time;
+ unsigned int user_min_time;
+ unsigned int domain_min_time;
+ unsigned int ip_min_time;
+
+ unsigned int carbon_interval;
+ const char *carbon_server;
+ const char *carbon_name;
+};
+
+extern const struct setting_parser_info old_stats_setting_parser_info;
+extern const struct old_stats_settings *stats_settings;
+
+#endif
+
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
new file mode 100644
index 0000000..ab0b9d1
--- /dev/null
+++ b/src/plugins/Makefile.am
@@ -0,0 +1,53 @@
+if BUILD_ZLIB_PLUGIN
+ZLIB = zlib imap-zlib
+endif
+
+if BUILD_LUCENE
+FTS_LUCENE = fts-lucene
+endif
+
+if BUILD_SOLR
+FTS_SOLR = fts-solr
+endif
+
+if HAVE_APPARMOR
+APPARMOR = apparmor
+endif
+
+if HAVE_LUA
+MAIL_LUA = mail-lua
+endif
+
+SUBDIRS = \
+ acl \
+ imap-acl \
+ fts \
+ fts-squat \
+ last-login \
+ lazy-expunge \
+ listescape \
+ notify \
+ notify-status \
+ push-notification \
+ mail-log \
+ $(MAIL_LUA) \
+ mailbox-alias \
+ quota \
+ quota-clone \
+ imap-quota \
+ pop3-migration \
+ replication \
+ old-stats \
+ imap-old-stats \
+ mail-crypt \
+ trash \
+ virtual \
+ welcome \
+ $(ZLIB) \
+ $(FTS_LUCENE) \
+ $(FTS_SOLR) \
+ $(DICT_LDAP) \
+ $(APPARMOR) \
+ fs-compress \
+ var-expand-crypt \
+ charset-alias
diff --git a/src/plugins/Makefile.in b/src/plugins/Makefile.in
new file mode 100644
index 0000000..49ef03c
--- /dev/null
+++ b/src/plugins/Makefile.in
@@ -0,0 +1,810 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins
+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_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 =
+SOURCES =
+DIST_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
+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 = acl imap-acl fts fts-squat last-login lazy-expunge \
+ listescape notify notify-status push-notification mail-log \
+ mail-lua mailbox-alias quota quota-clone imap-quota \
+ pop3-migration replication old-stats imap-old-stats mail-crypt \
+ trash virtual welcome zlib imap-zlib fts-lucene fts-solr \
+ apparmor fs-compress var-expand-crypt charset-alias
+am__DIST_COMMON = $(srcdir)/Makefile.in
+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@
+@BUILD_ZLIB_PLUGIN_TRUE@ZLIB = zlib imap-zlib
+@BUILD_LUCENE_TRUE@FTS_LUCENE = fts-lucene
+@BUILD_SOLR_TRUE@FTS_SOLR = fts-solr
+@HAVE_APPARMOR_TRUE@APPARMOR = apparmor
+@HAVE_LUA_TRUE@MAIL_LUA = mail-lua
+SUBDIRS = \
+ acl \
+ imap-acl \
+ fts \
+ fts-squat \
+ last-login \
+ lazy-expunge \
+ listescape \
+ notify \
+ notify-status \
+ push-notification \
+ mail-log \
+ $(MAIL_LUA) \
+ mailbox-alias \
+ quota \
+ quota-clone \
+ imap-quota \
+ pop3-migration \
+ replication \
+ old-stats \
+ imap-old-stats \
+ mail-crypt \
+ trash \
+ virtual \
+ welcome \
+ $(ZLIB) \
+ $(FTS_LUCENE) \
+ $(FTS_SOLR) \
+ $(DICT_LDAP) \
+ $(APPARMOR) \
+ fs-compress \
+ var-expand-crypt \
+ charset-alias
+
+all: all-recursive
+
+.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/plugins/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/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):
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+# 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
+installdirs: installdirs-recursive
+installdirs-am:
+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 mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f Makefile
+distclean-am: clean-am distclean-generic distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+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 Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \
+ check-am clean clean-generic clean-libtool cscopelist-am ctags \
+ ctags-am distclean 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 \
+ installdirs-am maintainer-clean maintainer-clean-generic \
+ mostlyclean 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/plugins/acl/Makefile.am b/src/plugins/acl/Makefile.am
new file mode 100644
index 0000000..8a5323e
--- /dev/null
+++ b/src/plugins/acl/Makefile.am
@@ -0,0 +1,78 @@
+doveadm_moduledir = $(moduledir)/doveadm
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -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/list \
+ -I$(top_srcdir)/src/doveadm
+
+NOPLUGIN_LDFLAGS =
+lib10_doveadm_acl_plugin_la_LDFLAGS = -module -avoid-version
+lib01_acl_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib01_acl_plugin.la
+
+lib01_acl_plugin_la_SOURCES = \
+ acl-api.c \
+ acl-attributes.c \
+ acl-backend.c \
+ acl-backend-vfile.c \
+ acl-backend-vfile-acllist.c \
+ acl-backend-vfile-update.c \
+ acl-cache.c \
+ acl-global-file.c \
+ acl-lookup-dict.c \
+ acl-mailbox.c \
+ acl-mailbox-list.c \
+ acl-plugin.c \
+ acl-shared-storage.c \
+ acl-storage.c
+
+noinst_HEADERS = \
+ acl-backend-vfile.h \
+ acl-shared-storage.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ acl-api.h \
+ acl-api-private.h \
+ acl-cache.h \
+ acl-global-file.h \
+ acl-lookup-dict.h \
+ acl-plugin.h \
+ acl-storage.h
+
+doveadm_module_LTLIBRARIES = \
+ lib10_doveadm_acl_plugin.la
+
+lib10_doveadm_acl_plugin_la_SOURCES = \
+ doveadm-acl.c
+
+test_programs = \
+ test-acl
+
+test_libs = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+test_deps = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+test_acl_SOURCES = test-acl.c
+test_acl_LDADD = $(test_libs)
+test_acl_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+noinst_PROGRAMS = $(test_programs)
diff --git a/src/plugins/acl/Makefile.in b/src/plugins/acl/Makefile.in
new file mode 100644
index 0000000..2c56fb3
--- /dev/null
+++ b/src/plugins/acl/Makefile.in
@@ -0,0 +1,1035 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/acl
+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-acl$(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)$(doveadm_moduledir)" \
+ "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES)
+lib01_acl_plugin_la_LIBADD =
+am_lib01_acl_plugin_la_OBJECTS = acl-api.lo acl-attributes.lo \
+ acl-backend.lo acl-backend-vfile.lo \
+ acl-backend-vfile-acllist.lo acl-backend-vfile-update.lo \
+ acl-cache.lo acl-global-file.lo acl-lookup-dict.lo \
+ acl-mailbox.lo acl-mailbox-list.lo acl-plugin.lo \
+ acl-shared-storage.lo acl-storage.lo
+lib01_acl_plugin_la_OBJECTS = $(am_lib01_acl_plugin_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 =
+lib01_acl_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib01_acl_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+lib10_doveadm_acl_plugin_la_LIBADD =
+am_lib10_doveadm_acl_plugin_la_OBJECTS = doveadm-acl.lo
+lib10_doveadm_acl_plugin_la_OBJECTS = \
+ $(am_lib10_doveadm_acl_plugin_la_OBJECTS)
+lib10_doveadm_acl_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib10_doveadm_acl_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_acl_OBJECTS = test-acl.$(OBJEXT)
+test_acl_OBJECTS = $(am_test_acl_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(module_LTLIBRARIES) $(am__DEPENDENCIES_1) \
+ $(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)/acl-api.Plo \
+ ./$(DEPDIR)/acl-attributes.Plo \
+ ./$(DEPDIR)/acl-backend-vfile-acllist.Plo \
+ ./$(DEPDIR)/acl-backend-vfile-update.Plo \
+ ./$(DEPDIR)/acl-backend-vfile.Plo ./$(DEPDIR)/acl-backend.Plo \
+ ./$(DEPDIR)/acl-cache.Plo ./$(DEPDIR)/acl-global-file.Plo \
+ ./$(DEPDIR)/acl-lookup-dict.Plo \
+ ./$(DEPDIR)/acl-mailbox-list.Plo ./$(DEPDIR)/acl-mailbox.Plo \
+ ./$(DEPDIR)/acl-plugin.Plo ./$(DEPDIR)/acl-shared-storage.Plo \
+ ./$(DEPDIR)/acl-storage.Plo ./$(DEPDIR)/doveadm-acl.Plo \
+ ./$(DEPDIR)/test-acl.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 = $(lib01_acl_plugin_la_SOURCES) \
+ $(lib10_doveadm_acl_plugin_la_SOURCES) $(test_acl_SOURCES)
+DIST_SOURCES = $(lib01_acl_plugin_la_SOURCES) \
+ $(lib10_doveadm_acl_plugin_la_SOURCES) $(test_acl_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 =
+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@
+doveadm_moduledir = $(moduledir)/doveadm
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -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/list \
+ -I$(top_srcdir)/src/doveadm
+
+lib10_doveadm_acl_plugin_la_LDFLAGS = -module -avoid-version
+lib01_acl_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib01_acl_plugin.la
+
+lib01_acl_plugin_la_SOURCES = \
+ acl-api.c \
+ acl-attributes.c \
+ acl-backend.c \
+ acl-backend-vfile.c \
+ acl-backend-vfile-acllist.c \
+ acl-backend-vfile-update.c \
+ acl-cache.c \
+ acl-global-file.c \
+ acl-lookup-dict.c \
+ acl-mailbox.c \
+ acl-mailbox-list.c \
+ acl-plugin.c \
+ acl-shared-storage.c \
+ acl-storage.c
+
+noinst_HEADERS = \
+ acl-backend-vfile.h \
+ acl-shared-storage.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ acl-api.h \
+ acl-api-private.h \
+ acl-cache.h \
+ acl-global-file.h \
+ acl-lookup-dict.h \
+ acl-plugin.h \
+ acl-storage.h
+
+doveadm_module_LTLIBRARIES = \
+ lib10_doveadm_acl_plugin.la
+
+lib10_doveadm_acl_plugin_la_SOURCES = \
+ doveadm-acl.c
+
+test_programs = \
+ test-acl
+
+test_libs = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+test_deps = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+test_acl_SOURCES = test-acl.c
+test_acl_LDADD = $(test_libs)
+test_acl_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/plugins/acl/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/acl/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-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_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)$(doveadm_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \
+ }
+
+uninstall-doveadm_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \
+ done
+
+clean-doveadm_moduleLTLIBRARIES:
+ -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES)
+ @list='$(doveadm_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}; \
+ }
+
+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}; \
+ }
+
+lib01_acl_plugin.la: $(lib01_acl_plugin_la_OBJECTS) $(lib01_acl_plugin_la_DEPENDENCIES) $(EXTRA_lib01_acl_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib01_acl_plugin_la_LINK) -rpath $(moduledir) $(lib01_acl_plugin_la_OBJECTS) $(lib01_acl_plugin_la_LIBADD) $(LIBS)
+
+lib10_doveadm_acl_plugin.la: $(lib10_doveadm_acl_plugin_la_OBJECTS) $(lib10_doveadm_acl_plugin_la_DEPENDENCIES) $(EXTRA_lib10_doveadm_acl_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib10_doveadm_acl_plugin_la_LINK) -rpath $(doveadm_moduledir) $(lib10_doveadm_acl_plugin_la_OBJECTS) $(lib10_doveadm_acl_plugin_la_LIBADD) $(LIBS)
+
+test-acl$(EXEEXT): $(test_acl_OBJECTS) $(test_acl_DEPENDENCIES) $(EXTRA_test_acl_DEPENDENCIES)
+ @rm -f test-acl$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_acl_OBJECTS) $(test_acl_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-api.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-attributes.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-backend-vfile-acllist.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-backend-vfile-update.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-backend-vfile.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-backend.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-global-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-lookup-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-mailbox-list.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-mailbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-shared-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/acl-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-acl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-acl.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)$(doveadm_moduledir)" "$(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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/acl-api.Plo
+ -rm -f ./$(DEPDIR)/acl-attributes.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile-acllist.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile-update.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile.Plo
+ -rm -f ./$(DEPDIR)/acl-backend.Plo
+ -rm -f ./$(DEPDIR)/acl-cache.Plo
+ -rm -f ./$(DEPDIR)/acl-global-file.Plo
+ -rm -f ./$(DEPDIR)/acl-lookup-dict.Plo
+ -rm -f ./$(DEPDIR)/acl-mailbox-list.Plo
+ -rm -f ./$(DEPDIR)/acl-mailbox.Plo
+ -rm -f ./$(DEPDIR)/acl-plugin.Plo
+ -rm -f ./$(DEPDIR)/acl-shared-storage.Plo
+ -rm -f ./$(DEPDIR)/acl-storage.Plo
+ -rm -f ./$(DEPDIR)/doveadm-acl.Plo
+ -rm -f ./$(DEPDIR)/test-acl.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-doveadm_moduleLTLIBRARIES \
+ 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)/acl-api.Plo
+ -rm -f ./$(DEPDIR)/acl-attributes.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile-acllist.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile-update.Plo
+ -rm -f ./$(DEPDIR)/acl-backend-vfile.Plo
+ -rm -f ./$(DEPDIR)/acl-backend.Plo
+ -rm -f ./$(DEPDIR)/acl-cache.Plo
+ -rm -f ./$(DEPDIR)/acl-global-file.Plo
+ -rm -f ./$(DEPDIR)/acl-lookup-dict.Plo
+ -rm -f ./$(DEPDIR)/acl-mailbox-list.Plo
+ -rm -f ./$(DEPDIR)/acl-mailbox.Plo
+ -rm -f ./$(DEPDIR)/acl-plugin.Plo
+ -rm -f ./$(DEPDIR)/acl-shared-storage.Plo
+ -rm -f ./$(DEPDIR)/acl-storage.Plo
+ -rm -f ./$(DEPDIR)/doveadm-acl.Plo
+ -rm -f ./$(DEPDIR)/test-acl.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-doveadm_moduleLTLIBRARIES \
+ 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-doveadm_moduleLTLIBRARIES \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ 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-doveadm_moduleLTLIBRARIES 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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(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/plugins/acl/acl-api-private.h b/src/plugins/acl/acl-api-private.h
new file mode 100644
index 0000000..6859779
--- /dev/null
+++ b/src/plugins/acl/acl-api-private.h
@@ -0,0 +1,135 @@
+#ifndef ACL_API_PRIVATE_H
+#define ACL_API_PRIVATE_H
+
+#include "acl-api.h"
+
+#define ACL_ID_NAME_ANYONE "anyone"
+#define ACL_ID_NAME_AUTHENTICATED "authenticated"
+#define ACL_ID_NAME_OWNER "owner"
+#define ACL_ID_NAME_USER_PREFIX "user="
+#define ACL_ID_NAME_GROUP_PREFIX "group="
+#define ACL_ID_NAME_GROUP_OVERRIDE_PREFIX "group-override="
+
+struct acl_backend_vfuncs {
+ struct acl_backend *(*alloc)(void);
+ int (*init)(struct acl_backend *backend, const char *data);
+ void (*deinit)(struct acl_backend *backend);
+
+ struct acl_mailbox_list_context *
+ (*nonowner_lookups_iter_init)(struct acl_backend *backend);
+ bool (*nonowner_lookups_iter_next)(struct acl_mailbox_list_context *ctx,
+ const char **name_r);
+ int (*nonowner_lookups_iter_deinit)
+ (struct acl_mailbox_list_context *ctx);
+ int (*nonowner_lookups_rebuild)(struct acl_backend *backend);
+
+ struct acl_object *(*object_init)(struct acl_backend *backend,
+ const char *name);
+ struct acl_object *(*object_init_parent)(struct acl_backend *backend,
+ const char *child_name);
+ void (*object_deinit)(struct acl_object *aclobj);
+
+ int (*object_refresh_cache)(struct acl_object *aclobj);
+ int (*object_update)(struct acl_object *aclobj,
+ const struct acl_rights_update *update);
+ int (*last_changed)(struct acl_object *aclobj, time_t *last_changed_r);
+
+ struct acl_object_list_iter *
+ (*object_list_init)(struct acl_object *aclobj);
+ bool (*object_list_next)(struct acl_object_list_iter *iter,
+ struct acl_rights *rights_r);
+ int (*object_list_deinit)(struct acl_object_list_iter *iter);
+};
+
+struct acl_backend {
+ pool_t pool;
+ const char *username;
+ const char **groups;
+ unsigned int group_count;
+
+ struct mailbox_list *list;
+ struct acl_cache *cache;
+ struct acl_global_file *global_file;
+
+ struct acl_object *default_aclobj;
+ struct acl_mask *default_aclmask;
+ const char *const *default_rights;
+
+ struct acl_backend_vfuncs v;
+
+ bool owner:1;
+ bool debug:1;
+ bool globals_only:1;
+};
+
+struct acl_mailbox_list_context {
+ struct acl_backend *backend;
+
+ bool empty:1;
+ bool failed:1;
+ const char *error;
+};
+
+struct acl_object {
+ struct acl_backend *backend;
+ char *name;
+
+ pool_t rights_pool;
+ ARRAY_TYPE(acl_rights) rights;
+};
+
+struct acl_object_list_iter {
+ struct acl_object *aclobj;
+ pool_t pool;
+
+ struct acl_rights *rights;
+ unsigned int idx, count;
+
+ bool empty:1;
+ bool failed:1;
+ const char *error;
+};
+
+extern const char *const all_mailbox_rights[];
+
+struct acl_object_list_iter *
+acl_default_object_list_init(struct acl_object *aclobj);
+bool acl_default_object_list_next(struct acl_object_list_iter *iter,
+ struct acl_rights *rights_r);
+int acl_default_object_list_deinit(struct acl_object_list_iter *iter);
+
+const char *const *
+acl_backend_mask_get_names(struct acl_backend *backend,
+ const struct acl_mask *mask, pool_t pool);
+struct acl_object *acl_backend_get_default_object(struct acl_backend *backend);
+int acl_backend_get_default_rights(struct acl_backend *backend,
+ const struct acl_mask **mask_r);
+void acl_rights_write_id(string_t *dest, const struct acl_rights *right);
+bool acl_rights_has_nonowner_lookup_changes(const struct acl_rights *rights);
+
+int acl_identifier_parse(const char *line, struct acl_rights *rights);
+int acl_rights_update_import(struct acl_rights_update *update,
+ const char *id, const char *const *rights,
+ const char **error_r);
+const char *acl_rights_export(const struct acl_rights *rights);
+int acl_rights_parse_line(const char *line, pool_t pool,
+ struct acl_rights *rights_r, const char **error_r);
+void acl_rights_dup(const struct acl_rights *src,
+ pool_t pool, struct acl_rights *dest_r);
+int acl_rights_cmp(const struct acl_rights *r1, const struct acl_rights *r2);
+void acl_rights_sort(struct acl_object *aclobj);
+
+const char *const *
+acl_right_names_parse(pool_t pool, const char *acl, const char **error_r);
+void acl_right_names_write(string_t *dest, const char *const *rights);
+void acl_right_names_merge(pool_t pool, const char *const **destp,
+ const char *const *src, bool dup_strings);
+bool acl_right_names_modify(pool_t pool,
+ const char *const **rightsp,
+ const char *const *modify_rights,
+ enum acl_modify_mode modify_mode);
+void acl_object_rebuild_cache(struct acl_object *aclobj);
+void acl_object_remove_all_access(struct acl_object *aclobj);
+void acl_object_add_global_acls(struct acl_object *aclobj);
+
+#endif
diff --git a/src/plugins/acl/acl-api.c b/src/plugins/acl/acl-api.c
new file mode 100644
index 0000000..2e422ea
--- /dev/null
+++ b/src/plugins/acl/acl-api.c
@@ -0,0 +1,847 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "hash.h"
+#include "mail-user.h"
+#include "mailbox-list.h"
+#include "acl-global-file.h"
+#include "acl-cache.h"
+#include "acl-api-private.h"
+
+struct acl_letter_map {
+ char letter;
+ const char *name;
+};
+
+static const struct acl_letter_map acl_letter_map[] = {
+ { 'l', MAIL_ACL_LOOKUP },
+ { 'r', MAIL_ACL_READ },
+ { 'w', MAIL_ACL_WRITE },
+ { 's', MAIL_ACL_WRITE_SEEN },
+ { 't', MAIL_ACL_WRITE_DELETED },
+ { 'i', MAIL_ACL_INSERT },
+ { 'p', MAIL_ACL_POST },
+ { 'e', MAIL_ACL_EXPUNGE },
+ { 'k', MAIL_ACL_CREATE },
+ { 'x', MAIL_ACL_DELETE },
+ { 'a', MAIL_ACL_ADMIN },
+ { '\0', NULL }
+};
+
+struct acl_object *acl_object_init_from_name(struct acl_backend *backend,
+ const char *name)
+{
+ return backend->v.object_init(backend, name);
+}
+
+struct acl_object *acl_object_init_from_parent(struct acl_backend *backend,
+ const char *child_name)
+{
+ return backend->v.object_init_parent(backend, child_name);
+}
+
+void acl_object_deinit(struct acl_object **_aclobj)
+{
+ struct acl_object *aclobj = *_aclobj;
+
+ *_aclobj = NULL;
+ aclobj->backend->v.object_deinit(aclobj);
+}
+
+int acl_object_have_right(struct acl_object *aclobj, unsigned int right_idx)
+{
+ struct acl_backend *backend = aclobj->backend;
+ const struct acl_mask *have_mask;
+ unsigned int read_idx;
+
+ if (backend->v.object_refresh_cache(aclobj) < 0)
+ return -1;
+
+ have_mask = acl_cache_get_my_rights(backend->cache, aclobj->name);
+ if (have_mask == NULL) {
+ if (acl_backend_get_default_rights(backend, &have_mask) < 0)
+ return -1;
+ }
+
+ if (acl_cache_mask_isset(have_mask, right_idx))
+ return 1;
+
+ if (mailbox_list_get_user(aclobj->backend->list)->dsyncing) {
+ /* when dsync is running on a shared mailbox, it must be able
+ to do everything inside it. however, dsync shouldn't touch
+ mailboxes where user doesn't have any read access, because
+ that could make them readable on the replica. */
+ read_idx = acl_backend_lookup_right(aclobj->backend,
+ MAIL_ACL_READ);
+ if (acl_cache_mask_isset(have_mask, read_idx))
+ return 1;
+ }
+ return 0;
+}
+
+const char *const *
+acl_backend_mask_get_names(struct acl_backend *backend,
+ const struct acl_mask *mask, pool_t pool)
+{
+ const char *const *names;
+ const char **buf, **rights;
+ unsigned int names_count, count, i, j, name_idx;
+
+ names = acl_cache_get_names(backend->cache, &names_count);
+ buf = t_new(const char *, (mask->size * CHAR_BIT) + 1);
+ count = 0;
+ for (i = 0, name_idx = 0; i < mask->size; i++) {
+ if (mask->mask[i] == 0)
+ name_idx += CHAR_BIT;
+ else {
+ for (j = 1; j < (1 << CHAR_BIT); j <<= 1, name_idx++) {
+ if ((mask->mask[i] & j) == 0)
+ continue;
+
+ /* @UNSAFE */
+ i_assert(name_idx < names_count);
+ buf[count++] = p_strdup(pool, names[name_idx]);
+ }
+ }
+ }
+
+ /* @UNSAFE */
+ rights = p_new(pool, const char *, count + 1);
+ memcpy(rights, buf, count * sizeof(const char *));
+ return rights;
+}
+
+static int acl_object_get_my_rights_real(struct acl_object *aclobj, pool_t pool,
+ const char *const **rights_r)
+{
+ struct acl_backend *backend = aclobj->backend;
+ const struct acl_mask *mask;
+
+ if (backend->v.object_refresh_cache(aclobj) < 0)
+ return -1;
+
+ mask = acl_cache_get_my_rights(backend->cache, aclobj->name);
+ if (mask == NULL) {
+ if (acl_backend_get_default_rights(backend, &mask) < 0)
+ return -1;
+ }
+
+ *rights_r = acl_backend_mask_get_names(backend, mask, pool);
+ return 0;
+}
+
+int acl_object_get_my_rights(struct acl_object *aclobj, pool_t pool,
+ const char *const **rights_r)
+{
+ int ret;
+
+ if (pool->datastack_pool)
+ return acl_object_get_my_rights_real(aclobj, pool, rights_r);
+ T_BEGIN {
+ ret = acl_object_get_my_rights_real(aclobj, pool, rights_r);
+ } T_END;
+ return ret;
+}
+
+const char *const *acl_object_get_default_rights(struct acl_object *aclobj)
+{
+ return acl_backend_mask_get_names(aclobj->backend,
+ aclobj->backend->default_aclmask,
+ pool_datastack_create());
+}
+
+int acl_object_last_changed(struct acl_object *aclobj, time_t *last_changed_r)
+{
+ return aclobj->backend->v.last_changed(aclobj, last_changed_r);
+}
+
+int acl_object_update(struct acl_object *aclobj,
+ const struct acl_rights_update *update)
+{
+ return aclobj->backend->v.object_update(aclobj, update);
+}
+
+struct acl_object_list_iter *acl_object_list_init(struct acl_object *aclobj)
+{
+ return aclobj->backend->v.object_list_init(aclobj);
+}
+
+bool acl_object_list_next(struct acl_object_list_iter *iter,
+ struct acl_rights *rights_r)
+{
+ if (iter->failed)
+ return FALSE;
+
+ return iter->aclobj->backend->v.object_list_next(iter, rights_r);
+}
+
+int acl_object_list_deinit(struct acl_object_list_iter **_iter)
+{
+ struct acl_object_list_iter *iter = *_iter;
+
+ *_iter = NULL;
+ return iter->aclobj->backend->v.object_list_deinit(iter);
+}
+
+struct acl_object_list_iter *
+acl_default_object_list_init(struct acl_object *aclobj)
+{
+ struct acl_object_list_iter *iter;
+ const struct acl_rights *aclobj_rights;
+ unsigned int i;
+ pool_t pool;
+
+ pool = pool_alloconly_create("acl object list", 512);
+ iter = p_new(pool, struct acl_object_list_iter, 1);
+ iter->pool = pool;
+ iter->aclobj = aclobj;
+
+ if (!array_is_created(&aclobj->rights)) {
+ /* we may have the object cached, but we don't have all the
+ rights read into memory */
+ acl_cache_flush(aclobj->backend->cache, aclobj->name);
+ }
+
+ if (aclobj->backend->v.object_refresh_cache(aclobj) < 0)
+ iter->failed = TRUE;
+
+ aclobj_rights = array_get(&aclobj->rights, &iter->count);
+ if (iter->count > 0) {
+ iter->rights = p_new(pool, struct acl_rights, iter->count);
+ for (i = 0; i < iter->count; i++)
+ acl_rights_dup(&aclobj_rights[i], pool, &iter->rights[i]);
+ } else
+ iter->empty = TRUE;
+ return iter;
+}
+
+bool acl_default_object_list_next(struct acl_object_list_iter *iter,
+ struct acl_rights *rights_r)
+{
+ if (iter->failed)
+ return FALSE;
+
+ if (iter->idx == iter->count)
+ return FALSE;
+ *rights_r = iter->rights[iter->idx++];
+ return TRUE;
+}
+
+int acl_default_object_list_deinit(struct acl_object_list_iter *iter)
+{
+ int ret = 0;
+ if (iter->failed)
+ ret = -1;
+ else if (iter->empty)
+ ret = 0;
+ else
+ ret = 1;
+
+ pool_unref(&iter->pool);
+ return ret;
+}
+
+struct acl_mailbox_list_context *
+acl_backend_nonowner_lookups_iter_init(struct acl_backend *backend)
+{
+ return backend->v.nonowner_lookups_iter_init(backend);
+}
+
+bool acl_backend_nonowner_lookups_iter_next(struct acl_mailbox_list_context *ctx,
+ const char **name_r)
+{
+ return ctx->backend->v.nonowner_lookups_iter_next(ctx, name_r);
+}
+
+int acl_backend_nonowner_lookups_iter_deinit(struct acl_mailbox_list_context **_ctx)
+{
+ struct acl_mailbox_list_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ return ctx->backend->v.nonowner_lookups_iter_deinit(ctx);
+}
+
+int acl_backend_nonowner_lookups_rebuild(struct acl_backend *backend)
+{
+ return backend->v.nonowner_lookups_rebuild(backend);
+}
+
+void acl_rights_write_id(string_t *dest, const struct acl_rights *right)
+{
+ switch (right->id_type) {
+ case ACL_ID_ANYONE:
+ str_append(dest, ACL_ID_NAME_ANYONE);
+ break;
+ case ACL_ID_AUTHENTICATED:
+ str_append(dest, ACL_ID_NAME_AUTHENTICATED);
+ break;
+ case ACL_ID_OWNER:
+ str_append(dest, ACL_ID_NAME_OWNER);
+ break;
+ case ACL_ID_USER:
+ str_append(dest, ACL_ID_NAME_USER_PREFIX);
+ str_append(dest, right->identifier);
+ break;
+ case ACL_ID_GROUP:
+ str_append(dest, ACL_ID_NAME_GROUP_PREFIX);
+ str_append(dest, right->identifier);
+ break;
+ case ACL_ID_GROUP_OVERRIDE:
+ str_append(dest, ACL_ID_NAME_GROUP_OVERRIDE_PREFIX);
+ str_append(dest, right->identifier);
+ break;
+ case ACL_ID_TYPE_COUNT:
+ i_unreached();
+ }
+}
+
+const char *acl_rights_get_id(const struct acl_rights *right)
+{
+ string_t *str = t_str_new(32);
+
+ acl_rights_write_id(str, right);
+ return str_c(str);
+}
+
+static bool is_standard_right(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; all_mailbox_rights[i] != NULL; i++) {
+ if (strcmp(all_mailbox_rights[i], name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int acl_rights_update_import(struct acl_rights_update *update,
+ const char *id, const char *const *rights,
+ const char **error_r)
+{
+ ARRAY_TYPE(const_string) dest_rights, dest_neg_rights, *dest;
+ unsigned int i, j;
+
+ if (acl_identifier_parse(id, &update->rights) < 0) {
+ *error_r = t_strdup_printf("Invalid ID: %s", id);
+ return -1;
+ }
+ if (rights == NULL) {
+ update->modify_mode = ACL_MODIFY_MODE_CLEAR;
+ update->neg_modify_mode = ACL_MODIFY_MODE_CLEAR;
+ return 0;
+ }
+
+ t_array_init(&dest_rights, 8);
+ t_array_init(&dest_neg_rights, 8);
+ for (i = 0; rights[i] != NULL; i++) {
+ const char *right = rights[i];
+
+ if (right[0] != '-')
+ dest = &dest_rights;
+ else {
+ right++;
+ dest = &dest_neg_rights;
+ }
+ if (strcmp(right, "all") != 0) {
+ if (*right == ':') {
+ /* non-standard right */
+ right++;
+ array_push_back(dest, &right);
+ } else if (is_standard_right(right)) {
+ array_push_back(dest, &right);
+ } else {
+ *error_r = t_strdup_printf("Invalid right '%s'",
+ right);
+ return -1;
+ }
+ } else {
+ for (j = 0; all_mailbox_rights[j] != NULL; j++)
+ array_push_back(dest, &all_mailbox_rights[j]);
+ }
+ }
+ if (array_count(&dest_rights) > 0) {
+ array_append_zero(&dest_rights);
+ update->rights.rights = array_front(&dest_rights);
+ } else if (update->modify_mode == ACL_MODIFY_MODE_REPLACE) {
+ update->modify_mode = ACL_MODIFY_MODE_CLEAR;
+ }
+ if (array_count(&dest_neg_rights) > 0) {
+ array_append_zero(&dest_neg_rights);
+ update->rights.neg_rights = array_front(&dest_neg_rights);
+ } else if (update->neg_modify_mode == ACL_MODIFY_MODE_REPLACE) {
+ update->neg_modify_mode = ACL_MODIFY_MODE_CLEAR;
+ }
+ return 0;
+}
+
+const char *acl_rights_export(const struct acl_rights *rights)
+{
+ string_t *str = t_str_new(128);
+
+ if (rights->rights != NULL)
+ str_append(str, t_strarray_join(rights->rights, " "));
+ if (rights->neg_rights != NULL && rights->neg_rights[0] != NULL) {
+ if (str_len(str) > 0)
+ str_append_c(str, ' ');
+ str_append_c(str, '-');
+ str_append(str, t_strarray_join(rights->neg_rights, " -"));
+ }
+ return str_c(str);
+}
+
+int acl_rights_parse_line(const char *line, pool_t pool,
+ struct acl_rights *rights_r, const char **error_r)
+{
+ const char *id_str, *const *right_names, *error = NULL;
+
+ /* <id> [<imap acls>] [:<named acls>] */
+ if (*line == '"') {
+ line++;
+ if (str_unescape_next(&line, &id_str) < 0 ||
+ (line[0] != ' ' && line[0] != '\0')) {
+ *error_r = "Invalid quoted ID";
+ return -1;
+ }
+ if (line[0] == ' ')
+ line++;
+ } else {
+ id_str = line;
+ line = strchr(id_str, ' ');
+ if (line == NULL)
+ line = "";
+ else
+ id_str = t_strdup_until(id_str, line++);
+ }
+
+ i_zero(rights_r);
+
+ right_names = acl_right_names_parse(pool, line, &error);
+ if (*id_str != '-')
+ rights_r->rights = right_names;
+ else {
+ id_str++;
+ rights_r->neg_rights = right_names;
+ }
+
+ if (acl_identifier_parse(id_str, rights_r) < 0)
+ error = t_strdup_printf("Unknown ID '%s'", id_str);
+
+ if (error != NULL) {
+ *error_r = error;
+ return -1;
+ }
+
+ rights_r->identifier = p_strdup(pool, rights_r->identifier);
+ return 0;
+}
+
+void acl_rights_dup(const struct acl_rights *src,
+ pool_t pool, struct acl_rights *dest_r)
+{
+ i_zero(dest_r);
+ dest_r->id_type = src->id_type;
+ dest_r->identifier = p_strdup(pool, src->identifier);
+ dest_r->rights = src->rights == NULL ? NULL :
+ p_strarray_dup(pool, src->rights);
+ dest_r->neg_rights = src->neg_rights == NULL ? NULL :
+ p_strarray_dup(pool, src->neg_rights);
+ dest_r->global = src->global;
+}
+
+int acl_rights_cmp(const struct acl_rights *r1, const struct acl_rights *r2)
+{
+ int ret;
+
+ if (r1->global != r2->global) {
+ /* globals have higher priority than locals */
+ return r1->global ? 1 : -1;
+ }
+
+ ret = (int)r1->id_type - (int)r2->id_type;
+ if (ret != 0)
+ return ret;
+
+ return null_strcmp(r1->identifier, r2->identifier);
+}
+
+void acl_rights_sort(struct acl_object *aclobj)
+{
+ struct acl_rights *rights;
+ unsigned int i, dest, count;
+
+ if (!array_is_created(&aclobj->rights))
+ return;
+
+ array_sort(&aclobj->rights, acl_rights_cmp);
+
+ /* merge identical identifiers */
+ rights = array_get_modifiable(&aclobj->rights, &count);
+ for (dest = 0, i = 1; i < count; i++) {
+ if (acl_rights_cmp(&rights[i], &rights[dest]) == 0) {
+ /* add i's rights to dest and delete i */
+ acl_right_names_merge(aclobj->rights_pool,
+ &rights[dest].rights,
+ rights[i].rights, FALSE);
+ acl_right_names_merge(aclobj->rights_pool,
+ &rights[dest].neg_rights,
+ rights[i].neg_rights, FALSE);
+ } else {
+ if (++dest != i)
+ rights[dest] = rights[i];
+ }
+ }
+ if (++dest < count)
+ array_delete(&aclobj->rights, dest, count - dest);
+}
+
+bool acl_rights_has_nonowner_lookup_changes(const struct acl_rights *rights)
+{
+ const char *const *p;
+
+ if (rights->id_type == ACL_ID_OWNER) {
+ /* ignore owner rights */
+ return FALSE;
+ }
+
+ if (rights->rights == NULL)
+ return FALSE;
+
+ for (p = rights->rights; *p != NULL; p++) {
+ if (strcmp(*p, MAIL_ACL_LOOKUP) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int acl_identifier_parse(const char *line, struct acl_rights *rights)
+{
+ if (str_begins(line, ACL_ID_NAME_USER_PREFIX)) {
+ rights->id_type = ACL_ID_USER;
+ rights->identifier = line + 5;
+ } else if (strcmp(line, ACL_ID_NAME_OWNER) == 0) {
+ rights->id_type = ACL_ID_OWNER;
+ } else if (str_begins(line, ACL_ID_NAME_GROUP_PREFIX)) {
+ rights->id_type = ACL_ID_GROUP;
+ rights->identifier = line + 6;
+ } else if (str_begins(line, ACL_ID_NAME_GROUP_OVERRIDE_PREFIX)) {
+ rights->id_type = ACL_ID_GROUP_OVERRIDE;
+ rights->identifier = line + 15;
+ } else if (strcmp(line, ACL_ID_NAME_AUTHENTICATED) == 0) {
+ rights->id_type = ACL_ID_AUTHENTICATED;
+ } else if (strcmp(line, ACL_ID_NAME_ANYONE) == 0 ||
+ strcmp(line, "anonymous") == 0) {
+ rights->id_type = ACL_ID_ANYONE;
+ } else {
+ return -1;
+ }
+ return 0;
+}
+
+static const char *const *
+acl_right_names_alloc(pool_t pool, ARRAY_TYPE(const_string) *rights_arr,
+ bool dup_strings)
+{
+ const char **ret, *const *rights;
+ unsigned int i, dest, count;
+
+ /* sort the rights first so we can easily drop duplicates */
+ array_sort(rights_arr, i_strcmp_p);
+
+ /* @UNSAFE */
+ rights = array_get(rights_arr, &count);
+ ret = p_new(pool, const char *, count + 1);
+ if (count > 0) {
+ ret[0] = rights[0];
+ for (i = dest = 1; i < count; i++) {
+ if (strcmp(rights[i-1], rights[i]) != 0)
+ ret[dest++] = rights[i];
+ }
+ ret[dest] = NULL;
+ if (dup_strings) {
+ for (i = 0; i < dest; i++)
+ ret[i] = p_strdup(pool, ret[i]);
+ }
+ }
+ return ret;
+}
+
+const char *const *
+acl_right_names_parse(pool_t pool, const char *acl, const char **error_r)
+{
+ ARRAY_TYPE(const_string) rights;
+ const char *const *names;
+ unsigned int i;
+
+ /* parse IMAP ACL list */
+ while (*acl == ' ' || *acl == '\t')
+ acl++;
+
+ t_array_init(&rights, 64);
+ while (*acl != '\0' && *acl != ' ' && *acl != '\t' && *acl != ':') {
+ for (i = 0; acl_letter_map[i].letter != '\0'; i++) {
+ if (acl_letter_map[i].letter == *acl)
+ break;
+ }
+
+ if (acl_letter_map[i].letter == '\0') {
+ *error_r = t_strdup_printf("Unknown ACL '%c'", *acl);
+ return NULL;
+ }
+
+ array_push_back(&rights, &acl_letter_map[i].name);
+ acl++;
+ }
+ while (*acl == ' ' || *acl == '\t') acl++;
+
+ if (*acl != '\0') {
+ /* parse our own extended ACLs */
+ if (*acl != ':') {
+ *error_r = "Missing ':' prefix in ACL extensions";
+ return NULL;
+ }
+
+ names = t_strsplit_spaces(acl + 1, ", \t");
+ for (; *names != NULL; names++) {
+ const char *name = p_strdup(pool, *names);
+ array_push_back(&rights, &name);
+ }
+ }
+
+ return acl_right_names_alloc(pool, &rights, FALSE);
+}
+
+void acl_right_names_write(string_t *dest, const char *const *rights)
+{
+ char c2[2];
+ unsigned int i, j, pos;
+
+ c2[1] = '\0';
+ pos = str_len(dest);
+ for (i = 0; rights[i] != NULL; i++) {
+ /* use letters if possible */
+ for (j = 0; acl_letter_map[j].name != NULL; j++) {
+ if (strcmp(rights[i], acl_letter_map[j].name) == 0) {
+ c2[0] = acl_letter_map[j].letter;
+ str_insert(dest, pos, c2);
+ pos++;
+ break;
+ }
+ }
+ if (acl_letter_map[j].name == NULL) {
+ /* fallback to full name */
+ str_append_c(dest, ' ');
+ str_append(dest, rights[i]);
+ }
+ }
+ if (pos + 1 < str_len(dest)) {
+ c2[0] = ':';
+ str_insert(dest, pos + 1, c2);
+ }
+}
+
+void acl_right_names_merge(pool_t pool, const char *const **destp,
+ const char *const *src, bool dup_strings)
+{
+ const char *const *dest = *destp;
+ ARRAY_TYPE(const_string) rights;
+ unsigned int i;
+
+ t_array_init(&rights, 64);
+ if (dest != NULL) {
+ for (i = 0; dest[i] != NULL; i++)
+ array_push_back(&rights, &dest[i]);
+ }
+ if (src != NULL) {
+ for (i = 0; src[i] != NULL; i++)
+ array_push_back(&rights, &src[i]);
+ }
+
+ *destp = acl_right_names_alloc(pool, &rights, dup_strings);
+}
+
+bool acl_right_names_modify(pool_t pool,
+ const char *const **rightsp,
+ const char *const *modify_rights,
+ enum acl_modify_mode modify_mode)
+{
+ const char *const *old_rights = *rightsp;
+ const char *const *new_rights = NULL;
+ const char *null = NULL;
+ ARRAY_TYPE(const_string) rights;
+ unsigned int i, j;
+
+ if (modify_rights == NULL && modify_mode != ACL_MODIFY_MODE_CLEAR) {
+ /* nothing to do here */
+ return FALSE;
+ }
+
+ switch (modify_mode) {
+ case ACL_MODIFY_MODE_REMOVE:
+ if (old_rights == NULL || *old_rights == NULL) {
+ /* nothing to do */
+ return FALSE;
+ }
+ t_array_init(&rights, 64);
+ for (i = 0; old_rights[i] != NULL; i++) {
+ for (j = 0; modify_rights[j] != NULL; j++) {
+ if (strcmp(old_rights[i], modify_rights[j]) == 0)
+ break;
+ }
+ if (modify_rights[j] == NULL)
+ array_push_back(&rights, &old_rights[i]);
+ }
+ new_rights = &null;
+ modify_rights = array_count(&rights) == 0 ? NULL :
+ array_front(&rights);
+ acl_right_names_merge(pool, &new_rights, modify_rights, TRUE);
+ break;
+ case ACL_MODIFY_MODE_ADD:
+ new_rights = old_rights;
+ acl_right_names_merge(pool, &new_rights, modify_rights, TRUE);
+ break;
+ case ACL_MODIFY_MODE_REPLACE:
+ new_rights = &null;
+ acl_right_names_merge(pool, &new_rights, modify_rights, TRUE);
+ break;
+ case ACL_MODIFY_MODE_CLEAR:
+ if (*rightsp == NULL) {
+ /* ACL didn't exist before either */
+ return FALSE;
+ }
+ *rightsp = NULL;
+ return TRUE;
+ }
+ i_assert(new_rights != NULL);
+ *rightsp = new_rights;
+
+ if (old_rights == NULL)
+ return new_rights[0] != NULL;
+
+ /* see if anything changed */
+ for (i = 0; old_rights[i] != NULL && new_rights[i] != NULL; i++) {
+ if (strcmp(old_rights[i], new_rights[i]) != 0)
+ return TRUE;
+ }
+ return old_rights[i] != NULL || new_rights[i] != NULL;
+}
+
+static void apply_owner_default_rights(struct acl_object *aclobj)
+{
+ struct acl_rights_update ru;
+ const char *null = NULL;
+
+ i_zero(&ru);
+ ru.modify_mode = ACL_MODIFY_MODE_REPLACE;
+ ru.neg_modify_mode = ACL_MODIFY_MODE_REPLACE;
+ ru.rights.id_type = ACL_ID_OWNER;
+ ru.rights.rights = aclobj->backend->default_rights;
+ ru.rights.neg_rights = &null;
+ acl_cache_update(aclobj->backend->cache, aclobj->name, &ru);
+}
+
+void acl_object_rebuild_cache(struct acl_object *aclobj)
+{
+ struct acl_rights_update ru;
+ enum acl_modify_mode add_mode;
+ const struct acl_rights *rights, *prev_match = NULL;
+ unsigned int i, count;
+ bool first_global = TRUE;
+
+ acl_cache_flush(aclobj->backend->cache, aclobj->name);
+
+ if (!array_is_created(&aclobj->rights))
+ return;
+
+ /* Rights are sorted by their 1) locals first, globals next,
+ 2) acl_id_type. We'll apply only the rights matching ourself.
+
+ Every time acl_id_type or local/global changes, the new ACLs will
+ replace all of the existing ACLs. Basically this means that if
+ user belongs to multiple matching groups or group-overrides, their
+ ACLs are merged. In all other situations the ACLs are replaced
+ (because there aren't duplicate rights entries and a user can't
+ match multiple usernames). */
+ i_zero(&ru);
+ rights = array_get(&aclobj->rights, &count);
+ if (!acl_backend_user_is_owner(aclobj->backend))
+ i = 0;
+ else {
+ /* we're the owner. skip over all rights entries until we
+ reach ACL_ID_OWNER or higher, or alternatively when we
+ reach a global ACL (even ACL_ID_ANYONE overrides owner's
+ rights if it's global) */
+ for (i = 0; i < count; i++) {
+ if (rights[i].id_type >= ACL_ID_OWNER ||
+ rights[i].global)
+ break;
+ }
+ apply_owner_default_rights(aclobj);
+ /* now continue applying the rest of the rights,
+ if there are any */
+ }
+ for (; i < count; i++) {
+ if (!acl_backend_rights_match_me(aclobj->backend, &rights[i]))
+ continue;
+
+ if (prev_match == NULL ||
+ prev_match->id_type != rights[i].id_type ||
+ prev_match->global != rights[i].global) {
+ /* replace old ACLs */
+ add_mode = ACL_MODIFY_MODE_REPLACE;
+ } else {
+ /* merging to existing ACLs */
+ i_assert(rights[i].id_type == ACL_ID_GROUP ||
+ rights[i].id_type == ACL_ID_GROUP_OVERRIDE);
+ add_mode = ACL_MODIFY_MODE_ADD;
+ }
+ prev_match = &rights[i];
+
+ /* If [neg_]rights is NULL it needs to be ignored.
+ The easiest way to do that is to just mark it with
+ REMOVE mode */
+ ru.modify_mode = rights[i].rights == NULL ?
+ ACL_MODIFY_MODE_REMOVE : add_mode;
+ ru.neg_modify_mode = rights[i].neg_rights == NULL ?
+ ACL_MODIFY_MODE_REMOVE : add_mode;
+ ru.rights = rights[i];
+ if (rights[i].global && first_global) {
+ /* first global: reset negative ACLs so local ACLs
+ can't mess things up via them */
+ first_global = FALSE;
+ ru.neg_modify_mode = ACL_MODIFY_MODE_REPLACE;
+ }
+ acl_cache_update(aclobj->backend->cache, aclobj->name, &ru);
+ }
+}
+
+void acl_object_remove_all_access(struct acl_object *aclobj)
+{
+ static const char *null = NULL;
+ struct acl_rights rights;
+
+ i_zero(&rights);
+ rights.id_type = ACL_ID_ANYONE;
+ rights.rights = &null;
+ array_push_back(&aclobj->rights, &rights);
+
+ rights.id_type = ACL_ID_OWNER;
+ rights.rights = &null;
+ array_push_back(&aclobj->rights, &rights);
+}
+
+void acl_object_add_global_acls(struct acl_object *aclobj)
+{
+ struct acl_backend *backend = aclobj->backend;
+ const char *vname, *error;
+
+ if (mailbox_list_is_valid_name(backend->list, aclobj->name, &error))
+ vname = mailbox_list_get_vname(backend->list, aclobj->name);
+ else
+ vname = "";
+
+ acl_global_file_get(backend->global_file, vname,
+ aclobj->rights_pool, &aclobj->rights);
+}
diff --git a/src/plugins/acl/acl-api.h b/src/plugins/acl/acl-api.h
new file mode 100644
index 0000000..7b19a98
--- /dev/null
+++ b/src/plugins/acl/acl-api.h
@@ -0,0 +1,167 @@
+#ifndef ACL_API_H
+#define ACL_API_H
+
+#include <sys/stat.h>
+
+struct mailbox_list;
+struct mail_storage;
+struct mailbox;
+struct acl_object;
+
+/* Show mailbox in mailbox list. Allow subscribing to it. */
+#define MAIL_ACL_LOOKUP "lookup"
+/* Allow opening mailbox for reading */
+#define MAIL_ACL_READ "read"
+/* Allow permanent flag changes (except for seen/deleted).
+ If not set, doesn't allow save/copy to set any flags either. */
+#define MAIL_ACL_WRITE "write"
+/* Allow permanent seen-flag changes */
+#define MAIL_ACL_WRITE_SEEN "write-seen"
+/* Allow permanent deleted-flag changes */
+#define MAIL_ACL_WRITE_DELETED "write-deleted"
+/* Allow saving and copying mails into the mailbox */
+#define MAIL_ACL_INSERT "insert"
+/* Allow posting mails to the mailbox (e.g. Sieve fileinto) */
+#define MAIL_ACL_POST "post"
+/* Allow expunging mails */
+#define MAIL_ACL_EXPUNGE "expunge"
+/* Allow creating child mailboxes */
+#define MAIL_ACL_CREATE "create"
+/* Allow deleting this mailbox */
+#define MAIL_ACL_DELETE "delete"
+/* Allow changing ACL state in this mailbox */
+#define MAIL_ACL_ADMIN "admin"
+
+#define MAILBOX_ATTRIBUTE_PREFIX_ACL \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"acl/"
+
+/* ACL identifiers in override order */
+enum acl_id_type {
+ /* Anyone's rights, including anonymous's.
+ identifier name is ignored. */
+ ACL_ID_ANYONE,
+ /* Authenticate users' rights. identifier name is ignored. */
+ ACL_ID_AUTHENTICATED,
+ /* Group's rights */
+ ACL_ID_GROUP,
+ /* Owner's rights, used when user is the storage's owner.
+ identifier name is ignored. */
+ ACL_ID_OWNER,
+ /* User's rights */
+ ACL_ID_USER,
+ /* Same as group's rights, but also overrides user's rights */
+ ACL_ID_GROUP_OVERRIDE,
+
+ ACL_ID_TYPE_COUNT
+};
+
+enum acl_modify_mode {
+ /* Remove rights from existing ACL */
+ ACL_MODIFY_MODE_REMOVE = 0,
+ /* Add rights to existing ACL (or create a new one) */
+ ACL_MODIFY_MODE_ADD,
+ /* Replace existing ACL with given rights */
+ ACL_MODIFY_MODE_REPLACE,
+ /* Clear all the rights from an existing ACL */
+ ACL_MODIFY_MODE_CLEAR
+};
+
+struct acl_rights {
+ /* Type of the identifier, user/group */
+ enum acl_id_type id_type;
+ /* Identifier, eg. username / group name */
+ const char *identifier;
+
+ /* Rights assigned. NULL entry can be ignored, but { NULL } means user
+ has no rights. */
+ const char *const *rights;
+ /* Negative rights assigned */
+ const char *const *neg_rights;
+
+ /* These rights are global for all users */
+ bool global:1;
+};
+ARRAY_DEFINE_TYPE(acl_rights, struct acl_rights);
+
+struct acl_rights_update {
+ struct acl_rights rights;
+
+ enum acl_modify_mode modify_mode;
+ enum acl_modify_mode neg_modify_mode;
+ /* These changes' "last changed" timestamp */
+ time_t last_change;
+};
+
+/* data contains the information needed to initialize ACL backend. If username
+ is NULL, it means the user is anonymous. Username and groups are matched
+ case-sensitively. */
+struct acl_backend *
+acl_backend_init(const char *data, struct mailbox_list *list,
+ const char *acl_username, const char *const *groups,
+ bool owner);
+void acl_backend_deinit(struct acl_backend **backend);
+
+/* Returns the acl_username passed to acl_backend_init(). Note that with
+ anonymous users NULL is returned. */
+const char *acl_backend_get_acl_username(struct acl_backend *backend);
+
+/* Returns TRUE if user isn't anonymous. */
+bool acl_backend_user_is_authenticated(struct acl_backend *backend);
+/* Returns TRUE if user owns the storage. */
+bool acl_backend_user_is_owner(struct acl_backend *backend);
+/* Returns TRUE if given name matches the ACL user name. */
+bool acl_backend_user_name_equals(struct acl_backend *backend,
+ const char *username);
+/* Returns TRUE if ACL user is in given group. */
+bool acl_backend_user_is_in_group(struct acl_backend *backend,
+ const char *group_name);
+/* Returns index for the right name. If it doesn't exist, it's created. */
+unsigned int acl_backend_lookup_right(struct acl_backend *backend,
+ const char *right);
+/* Returns TRUE if acl_rights matches backend user. */
+bool acl_backend_rights_match_me(struct acl_backend *backend,
+ const struct acl_rights *rights);
+
+/* List mailboxes that have lookup right to some non-owners. */
+struct acl_mailbox_list_context *
+acl_backend_nonowner_lookups_iter_init(struct acl_backend *backend);
+bool acl_backend_nonowner_lookups_iter_next(struct acl_mailbox_list_context *ctx,
+ const char **name_r);
+int
+acl_backend_nonowner_lookups_iter_deinit(struct acl_mailbox_list_context **ctx);
+
+/* Force a rebuild for nonowner lookups index */
+int acl_backend_nonowner_lookups_rebuild(struct acl_backend *backend);
+
+struct acl_object *acl_object_init_from_name(struct acl_backend *backend,
+ const char *name);
+struct acl_object *acl_object_init_from_parent(struct acl_backend *backend,
+ const char *child_name);
+void acl_object_deinit(struct acl_object **aclobj);
+
+/* Returns 1 if we have the requested rights, 0 if not, or -1 if internal
+ error occurred. */
+int acl_object_have_right(struct acl_object *aclobj, unsigned int right_idx);
+/* Returns 0 = ok, -1 = internal error */
+int acl_object_get_my_rights(struct acl_object *aclobj, pool_t pool,
+ const char *const **rights_r);
+/* Returns the default rights for the object. */
+const char *const *acl_object_get_default_rights(struct acl_object *aclobj);
+/* Returns timestamp of when the ACLs were last changed for this object,
+ or 0 = never. */
+int acl_object_last_changed(struct acl_object *aclobj, time_t *last_changed_r);
+
+/* Update ACL of given object. */
+int acl_object_update(struct acl_object *aclobj,
+ const struct acl_rights_update *update);
+
+/* List all identifiers. */
+struct acl_object_list_iter *acl_object_list_init(struct acl_object *aclobj);
+bool acl_object_list_next(struct acl_object_list_iter *iter,
+ struct acl_rights *rights_r);
+int acl_object_list_deinit(struct acl_object_list_iter **iter);
+
+/* Returns the canonical ID for the right. */
+const char *acl_rights_get_id(const struct acl_rights *right);
+
+#endif
diff --git a/src/plugins/acl/acl-attributes.c b/src/plugins/acl/acl-attributes.c
new file mode 100644
index 0000000..515ff42
--- /dev/null
+++ b/src/plugins/acl/acl-attributes.c
@@ -0,0 +1,233 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-storage-private.h"
+#include "acl-api-private.h"
+#include "acl-plugin.h"
+#include "acl-storage.h"
+
+struct acl_mailbox_attribute_iter {
+ struct mailbox_attribute_iter iter;
+ struct mailbox_attribute_iter *super;
+
+ struct acl_object_list_iter *acl_iter;
+ string_t *acl_name;
+
+ bool failed;
+};
+
+static int
+acl_attribute_update_acl(struct mailbox_transaction_context *t, const char *key,
+ const struct mail_attribute_value *value)
+{
+ const char *value_str, *id, *const *rights, *error;
+ struct acl_rights_update update;
+
+ /* for now allow only dsync to update ACLs this way.
+ if this check is removed, it should be replaced by a setting, since
+ some admins may still have configured Dovecot using dovecot-acl
+ files directly that they don't want users to update. and in any case
+ ACL_STORAGE_RIGHT_ADMIN must be checked then. */
+ if (!t->box->storage->user->dsyncing) {
+ mail_storage_set_error(t->box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ return -1;
+ }
+
+ if (mailbox_attribute_value_to_string(t->box->storage, value,
+ &value_str) < 0)
+ return -1;
+
+ i_zero(&update);
+ update.modify_mode = ACL_MODIFY_MODE_REPLACE;
+ update.neg_modify_mode = ACL_MODIFY_MODE_REPLACE;
+ update.last_change = value->last_change;
+ id = key + strlen(MAILBOX_ATTRIBUTE_PREFIX_ACL);
+ rights = value_str == NULL ? NULL : t_strsplit(value_str, " ");
+ if (acl_rights_update_import(&update, id, rights, &error) < 0) {
+ mail_storage_set_error(t->box->storage, MAIL_ERROR_PARAMS, error);
+ return -1;
+ }
+ /* FIXME: this should actually be done only at commit().. */
+ return acl_mailbox_update_acl(t, &update);
+}
+
+static int acl_attribute_get_acl(struct mailbox *box, const char *key,
+ struct mail_attribute_value *value_r)
+{
+ struct acl_object *aclobj = acl_mailbox_get_aclobj(box);
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights, wanted_rights;
+ const char *id;
+ int ret = 0;
+
+ i_zero(value_r);
+
+ if (!box->storage->user->dsyncing) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ return -1;
+ }
+ /* set last_change for all ACL objects, even if they don't exist
+ (because they could have been removed by the last change, and dsync
+ can use this information) */
+ (void)acl_object_last_changed(aclobj, &value_r->last_change);
+
+ i_zero(&wanted_rights);
+ id = key + strlen(MAILBOX_ATTRIBUTE_PREFIX_ACL);
+ if (acl_identifier_parse(id, &wanted_rights) < 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ t_strdup_printf("Invalid ID: %s", id));
+ return -1;
+ }
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (!rights.global &&
+ rights.id_type == wanted_rights.id_type &&
+ null_strcmp(rights.identifier, wanted_rights.identifier) == 0) {
+ value_r->value = acl_rights_export(&rights);
+ ret = 1;
+ break;
+ }
+ }
+ /* the return value here cannot be used, because this function
+ needs to return whether it actually matched something
+ or not */
+ if (acl_object_list_deinit(&iter) < 0) {
+ mail_storage_set_internal_error(box->storage);
+ ret = -1;
+ }
+ return ret;
+}
+
+static int acl_have_attribute_rights(struct mailbox *box)
+{
+ int ret;
+
+ if (box->deleting) {
+ /* deleting attributes during mailbox deletion */
+ return 1;
+ }
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_LOOKUP);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ }
+
+ return acl_mailbox_have_extra_attribute_rights(box) ? 0 : -1;
+}
+
+int acl_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type, const char *key,
+ const struct mail_attribute_value *value)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(t->box);
+
+ if (acl_have_attribute_rights(t->box) < 0)
+ return -1;
+ if (str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_ACL))
+ return acl_attribute_update_acl(t, key, value);
+ return abox->module_ctx.super.attribute_set(t, type, key, value);
+}
+
+int acl_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type, const char *key,
+ struct mail_attribute_value *value_r)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+
+ if (acl_have_attribute_rights(box) < 0)
+ return -1;
+ if (str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_ACL))
+ return acl_attribute_get_acl(box, key, value_r);
+ return abox->module_ctx.super.attribute_get(box, type, key, value_r);
+}
+
+struct mailbox_attribute_iter *
+acl_attribute_iter_init(struct mailbox *box, enum mail_attribute_type type,
+ const char *prefix)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ struct acl_mailbox_attribute_iter *aiter;
+
+ aiter = i_new(struct acl_mailbox_attribute_iter, 1);
+ aiter->iter.box = box;
+ if (acl_have_attribute_rights(box) < 0)
+ aiter->failed = TRUE;
+ else {
+ aiter->super = abox->module_ctx.super.
+ attribute_iter_init(box, type, prefix);
+ if (box->storage->user->dsyncing &&
+ type == MAIL_ATTRIBUTE_TYPE_SHARED &&
+ str_begins(MAILBOX_ATTRIBUTE_PREFIX_ACL, prefix)) {
+ aiter->acl_iter = acl_object_list_init(abox->aclobj);
+ aiter->acl_name = str_new(default_pool, 128);
+ str_append(aiter->acl_name, MAILBOX_ATTRIBUTE_PREFIX_ACL);
+ }
+ }
+ return &aiter->iter;
+}
+
+static const char *
+acl_attribute_iter_next_acl(struct acl_mailbox_attribute_iter *aiter)
+{
+ struct acl_rights rights;
+
+ if (aiter->failed)
+ return NULL;
+
+ while (acl_object_list_next(aiter->acl_iter, &rights)) {
+ if (rights.global)
+ continue;
+ str_truncate(aiter->acl_name, strlen(MAILBOX_ATTRIBUTE_PREFIX_ACL));
+ acl_rights_write_id(aiter->acl_name, &rights);
+ return str_c(aiter->acl_name);
+ }
+ if (acl_object_list_deinit(&aiter->acl_iter) < 0) {
+ mail_storage_set_internal_error(aiter->iter.box->storage);
+ aiter->failed = TRUE;
+ }
+ return NULL;
+}
+
+const char *acl_attribute_iter_next(struct mailbox_attribute_iter *iter)
+{
+ struct acl_mailbox_attribute_iter *aiter =
+ (struct acl_mailbox_attribute_iter *)iter;
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(iter->box);
+ const char *key;
+
+ if (aiter->super == NULL)
+ return NULL;
+ if (aiter->acl_iter != NULL) {
+ if ((key = acl_attribute_iter_next_acl(aiter)) != NULL)
+ return key;
+ }
+ return abox->module_ctx.super.attribute_iter_next(aiter->super);
+}
+
+int acl_attribute_iter_deinit(struct mailbox_attribute_iter *iter)
+{
+ struct acl_mailbox_attribute_iter *aiter =
+ (struct acl_mailbox_attribute_iter *)iter;
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(iter->box);
+ int ret = aiter->failed ? -1 : 0;
+
+ if (aiter->super != NULL) {
+ if (abox->module_ctx.super.attribute_iter_deinit(aiter->super) < 0)
+ ret = -1;
+ }
+ if (aiter->acl_iter != NULL && acl_object_list_deinit(&aiter->acl_iter) < 0) {
+ mail_storage_set_internal_error(aiter->iter.box->storage);
+ ret = -1;
+ }
+ str_free(&aiter->acl_name);
+ i_free(aiter);
+ return ret;
+}
diff --git a/src/plugins/acl/acl-backend-vfile-acllist.c b/src/plugins/acl/acl-backend-vfile-acllist.c
new file mode 100644
index 0000000..c6029a2
--- /dev/null
+++ b/src/plugins/acl/acl-backend-vfile-acllist.c
@@ -0,0 +1,424 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "istream.h"
+#include "ostream.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "acl-plugin.h"
+#include "acl-cache.h"
+#include "acl-lookup-dict.h"
+#include "acl-backend-vfile.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+struct acl_mailbox_list_context_vfile {
+ struct acl_mailbox_list_context ctx;
+
+ unsigned int idx;
+};
+
+static void
+acllist_clear(struct acl_backend_vfile *backend, uoff_t file_size)
+{
+ if (backend->acllist_pool == NULL) {
+ backend->acllist_pool =
+ pool_alloconly_create("vfile acllist",
+ I_MAX(file_size / 2, 128));
+ i_array_init(&backend->acllist, I_MAX(16, file_size / 60));
+ } else {
+ p_clear(backend->acllist_pool);
+ array_clear(&backend->acllist);
+ }
+}
+
+static bool acl_list_get_root_dir(struct acl_backend_vfile *backend,
+ const char **root_dir_r,
+ enum mailbox_list_path_type *type_r)
+{
+ struct mail_storage *storage;
+ const char *rootdir, *maildir;
+ enum mailbox_list_path_type type;
+
+ if (backend->backend.globals_only)
+ return FALSE;
+
+ storage = mailbox_list_get_namespace(backend->backend.list)->storage;
+ type = mail_storage_get_acl_list_path_type(storage);
+ if (!mailbox_list_get_root_path(backend->backend.list, type, &rootdir))
+ return FALSE;
+ *type_r = type;
+
+ if (type == MAILBOX_LIST_PATH_TYPE_DIR &&
+ mail_storage_is_mailbox_file(storage)) {
+ maildir = mailbox_list_get_root_forced(backend->backend.list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (strcmp(maildir, rootdir) == 0) {
+ /* dovecot-acl-list would show up as a mailbox if we
+ created it to root dir. since we don't really have
+ any other good alternatives, place it to control
+ dir */
+ rootdir = mailbox_list_get_root_forced(backend->backend.list,
+ MAILBOX_LIST_PATH_TYPE_CONTROL);
+ *type_r = MAILBOX_LIST_PATH_TYPE_CONTROL;
+ }
+ }
+ *root_dir_r = rootdir;
+ return TRUE;
+}
+
+static bool acl_list_get_path(struct acl_backend_vfile *backend,
+ const char **path_r)
+{
+ enum mailbox_list_path_type type;
+ const char *root_dir;
+
+ if (!acl_list_get_root_dir(backend, &root_dir, &type))
+ return FALSE;
+ *path_r = t_strconcat(root_dir, "/"ACLLIST_FILENAME, NULL);
+ return TRUE;
+}
+
+static int acl_backend_vfile_acllist_read(struct acl_backend_vfile *backend)
+{
+ struct acl_backend_vfile_acllist acllist;
+ struct istream *input;
+ struct stat st;
+ const char *path, *line, *p;
+ int fd, ret = 0;
+
+ backend->acllist_last_check = ioloop_time;
+
+ if (!acl_list_get_path(backend, &path)) {
+ /* we're never going to build acllist for this namespace. */
+ acllist_clear(backend, 0);
+ return 0;
+ }
+
+ if (backend->acllist_mtime != 0) {
+ /* see if the file's mtime has changed */
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ backend->acllist_mtime = 0;
+ else
+ i_error("stat(%s) failed: %m", path);
+ return -1;
+ }
+ if (st.st_mtime == backend->acllist_mtime)
+ return 0;
+ }
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT) {
+ backend->acllist_mtime = 0;
+ return -1;
+ }
+ i_error("open(%s) failed: %m", path);
+ return -1;
+ }
+ if (fstat(fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ backend->acllist_mtime = st.st_mtime;
+ acllist_clear(backend, st.st_size);
+
+ input = i_stream_create_fd(fd, SIZE_MAX);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ acllist.mtime = 0;
+ for (p = line; *p >= '0' && *p <= '9'; p++)
+ acllist.mtime = acllist.mtime * 10 + (*p - '0');
+
+ if (p == line || *p != ' ' || p[1] == '\0') {
+ i_error("Broken acllist file: %s", path);
+ i_unlink_if_exists(path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ acllist.name = p_strdup(backend->acllist_pool, p + 1);
+ array_push_back(&backend->acllist, &acllist);
+ }
+ if (input->stream_errno != 0)
+ ret = -1;
+ i_stream_destroy(&input);
+
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", path);
+ return ret;
+}
+
+void acl_backend_vfile_acllist_refresh(struct acl_backend_vfile *backend)
+{
+ i_assert(!backend->iterating_acllist);
+
+ if (backend->acllist_last_check +
+ (time_t)backend->cache_secs > ioloop_time)
+ return;
+
+ if (acl_backend_vfile_acllist_read(backend) < 0) {
+ acllist_clear(backend, 0);
+ if (!backend->rebuilding_acllist)
+ (void)acl_backend_vfile_acllist_rebuild(backend);
+ }
+}
+
+static int
+acllist_append(struct acl_backend_vfile *backend, struct ostream *output,
+ const char *vname)
+{
+ struct acl_object *aclobj;
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ struct acl_backend_vfile_acllist acllist;
+ const char *name;
+ int ret;
+
+ name = mailbox_list_get_storage_name(backend->backend.list, vname);
+ acl_cache_flush(backend->backend.cache, name);
+ aclobj = acl_object_init_from_name(&backend->backend, name);
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (acl_rights_has_nonowner_lookup_changes(&rights))
+ break;
+ }
+ ret = acl_object_list_deinit(&iter);
+
+ if (acl_backend_vfile_object_get_mtime(aclobj, &acllist.mtime) < 0)
+ ret = -1;
+
+ if (ret > 0) {
+ acllist.name = p_strdup(backend->acllist_pool, name);
+ array_push_back(&backend->acllist, &acllist);
+
+ o_stream_nsend_str(output, t_strdup_printf(
+ "%s %s\n", dec2str(acllist.mtime), name));
+ }
+ acl_object_deinit(&aclobj);
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+acl_backend_vfile_acllist_try_rebuild(struct acl_backend_vfile *backend)
+{
+ struct mailbox_list *list = backend->backend.list;
+ struct mail_namespace *ns;
+ struct mailbox_list_iterate_context *iter;
+ enum mailbox_list_path_type type;
+ const struct mailbox_info *info;
+ const char *rootdir, *acllist_path;
+ struct ostream *output;
+ struct stat st;
+ struct mailbox_permissions perm;
+ string_t *path;
+ int fd, ret;
+
+ i_assert(!backend->rebuilding_acllist);
+
+ if (!acl_list_get_root_dir(backend, &rootdir, &type))
+ return 0;
+
+ ns = mailbox_list_get_namespace(list);
+ if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0) {
+ /* we can't write anything here */
+ return 0;
+ }
+
+ path = t_str_new(256);
+ str_printfa(path, "%s/%s", rootdir, mailbox_list_get_temp_prefix(list));
+
+ /* Build it into a temporary file and rename() over. There's no need
+ to use locking, because even if multiple processes are rebuilding
+ the file at the same time the result should be the same. */
+ mailbox_list_get_root_permissions(list, &perm);
+ fd = safe_mkstemp_group(path, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ if (fd == -1 && errno == ENOENT) {
+ if (mailbox_list_mkdir_root(backend->backend.list,
+ rootdir, type) < 0)
+ return -1;
+ fd = safe_mkstemp_group(path, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ }
+ if (fd == -1) {
+ if (errno == EACCES) {
+ /* Ignore silently if we can't create it */
+ return 0;
+ }
+ i_error("dovecot-acl-list creation failed: "
+ "safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+ output = o_stream_create_fd_file(fd, 0, FALSE);
+ o_stream_cork(output);
+
+ ret = 0;
+ acllist_clear(backend, 0);
+
+ backend->rebuilding_acllist = TRUE;
+ iter = mailbox_list_iter_init(list, "*",
+ MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while (ret == 0 && (info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ ret = acllist_append(backend, output, info->vname);
+ } T_END;
+
+ if (o_stream_finish(output) < 0) {
+ i_error("write(%s) failed: %s", str_c(path),
+ o_stream_get_error(output));
+ ret = -1;
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ o_stream_destroy(&output);
+
+ if (ret == 0) {
+ if (fstat(fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", str_c(path));
+ ret = -1;
+ }
+ }
+ if (close(fd) < 0) {
+ i_error("close(%s) failed: %m", str_c(path));
+ ret = -1;
+ }
+
+ if (ret == 0) {
+ if (!acl_list_get_path(backend, &acllist_path))
+ i_unreached();
+ if (rename(str_c(path), acllist_path) < 0) {
+ i_error("rename(%s, %s) failed: %m",
+ str_c(path), acllist_path);
+ ret = -1;
+ }
+ }
+ if (ret == 0) {
+ struct acl_user *auser = ACL_USER_CONTEXT(ns->user);
+ i_assert(auser != NULL);
+ backend->acllist_mtime = st.st_mtime;
+ backend->acllist_last_check = ioloop_time;
+ /* FIXME: dict rebuild is expensive, try to avoid it */
+ (void)acl_lookup_dict_rebuild(auser->acl_lookup_dict);
+ } else {
+ acllist_clear(backend, 0);
+ i_unlink_if_exists(str_c(path));
+ }
+ backend->rebuilding_acllist = FALSE;
+ return ret;
+}
+
+int acl_backend_vfile_acllist_rebuild(struct acl_backend_vfile *backend)
+{
+ const char *acllist_path;
+
+ if (acl_backend_vfile_acllist_try_rebuild(backend) == 0)
+ return 0;
+ else {
+ /* delete it to make sure it gets rebuilt later */
+ if (!acl_list_get_path(backend, &acllist_path))
+ i_unreached();
+ i_unlink_if_exists(acllist_path);
+ return -1;
+ }
+}
+
+static const struct acl_backend_vfile_acllist *
+acl_backend_vfile_acllist_find(struct acl_backend_vfile *backend,
+ const char *name)
+{
+ const struct acl_backend_vfile_acllist *acllist;
+
+ array_foreach(&backend->acllist, acllist) {
+ if (strcmp(acllist->name, name) == 0)
+ return acllist;
+ }
+ return NULL;
+}
+
+void acl_backend_vfile_acllist_verify(struct acl_backend_vfile *backend,
+ const char *name, time_t mtime)
+{
+ const struct acl_backend_vfile_acllist *acllist;
+
+ if (backend->rebuilding_acllist || backend->iterating_acllist)
+ return;
+
+ acl_backend_vfile_acllist_refresh(backend);
+ acllist = acl_backend_vfile_acllist_find(backend, name);
+ if (acllist != NULL && acllist->mtime != mtime)
+ (void)acl_backend_vfile_acllist_rebuild(backend);
+}
+
+struct acl_mailbox_list_context *
+acl_backend_vfile_nonowner_iter_init(struct acl_backend *_backend)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+ struct acl_mailbox_list_context_vfile *ctx;
+
+ acl_backend_vfile_acllist_refresh(backend);
+
+ ctx = i_new(struct acl_mailbox_list_context_vfile, 1);
+ ctx->ctx.backend = _backend;
+ backend->iterating_acllist = TRUE;
+ return &ctx->ctx;
+}
+
+bool acl_backend_vfile_nonowner_iter_next(struct acl_mailbox_list_context *_ctx,
+ const char **name_r)
+{
+ struct acl_mailbox_list_context_vfile *ctx =
+ (struct acl_mailbox_list_context_vfile *)_ctx;
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_ctx->backend;
+ const struct acl_backend_vfile_acllist *acllist;
+ unsigned int count;
+
+ if (_ctx->failed)
+ return FALSE;
+
+ acllist = array_get(&backend->acllist, &count);
+ if (count == 0)
+ _ctx->empty = TRUE;
+ if (ctx->idx == count)
+ return FALSE;
+
+ *name_r = acllist[ctx->idx++].name;
+ return TRUE;
+}
+
+int
+acl_backend_vfile_nonowner_iter_deinit(struct acl_mailbox_list_context *ctx)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)ctx->backend;
+ int ret;
+
+ backend->iterating_acllist = FALSE;
+ if (ctx->failed)
+ ret = -1;
+ else if (ctx->empty)
+ ret = 0;
+ else
+ ret = 1;
+ i_free(ctx);
+ return ret;
+}
+
+int acl_backend_vfile_nonowner_lookups_rebuild(struct acl_backend *_backend)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+
+ return acl_backend_vfile_acllist_rebuild(backend);
+}
diff --git a/src/plugins/acl/acl-backend-vfile-update.c b/src/plugins/acl/acl-backend-vfile-update.c
new file mode 100644
index 0000000..7c48c4e
--- /dev/null
+++ b/src/plugins/acl/acl-backend-vfile-update.c
@@ -0,0 +1,260 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "ioloop.h"
+#include "str.h"
+#include "strescape.h"
+#include "file-dotlock.h"
+#include "ostream.h"
+#include "mail-storage.h"
+#include "acl-cache.h"
+#include "acl-backend-vfile.h"
+
+#include <utime.h>
+#include <sys/stat.h>
+
+static struct dotlock_settings dotlock_set = {
+ .timeout = 30,
+ .stale_timeout = 120
+};
+
+static int acl_backend_vfile_update_begin(struct acl_object_vfile *aclobj,
+ struct dotlock **dotlock_r)
+{
+ struct acl_object *_aclobj = &aclobj->aclobj;
+ struct mailbox_permissions perm;
+ int fd;
+
+ if (aclobj->local_path == NULL) {
+ i_error("Can't update acl object '%s': No local acl file path",
+ aclobj->aclobj.name);
+ return -1;
+ }
+
+ /* first lock the ACL file */
+ mailbox_list_get_permissions(_aclobj->backend->list,
+ _aclobj->name, &perm);
+ fd = file_dotlock_open_group(&dotlock_set, aclobj->local_path, 0,
+ perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin, dotlock_r);
+ if (fd == -1) {
+ i_error("file_dotlock_open(%s) failed: %m", aclobj->local_path);
+ return -1;
+ }
+
+ /* locked successfully, re-read the existing file to make sure we
+ don't lose any changes. */
+ acl_cache_flush(_aclobj->backend->cache, _aclobj->name);
+ if (_aclobj->backend->v.object_refresh_cache(_aclobj) < 0) {
+ file_dotlock_delete(dotlock_r);
+ return -1;
+ }
+ return fd;
+}
+
+static bool
+vfile_object_modify_right(struct acl_object *aclobj, unsigned int idx,
+ const struct acl_rights_update *update)
+{
+ struct acl_rights *right;
+ bool c1, c2;
+
+ right = array_idx_modifiable(&aclobj->rights, idx);
+ c1 = acl_right_names_modify(aclobj->rights_pool, &right->rights,
+ update->rights.rights, update->modify_mode);
+ c2 = acl_right_names_modify(aclobj->rights_pool, &right->neg_rights,
+ update->rights.neg_rights,
+ update->neg_modify_mode);
+
+ if (right->rights == NULL && right->neg_rights == NULL) {
+ /* this identifier no longer exists */
+ array_delete(&aclobj->rights, idx, 1);
+ c1 = TRUE;
+ }
+ return c1 || c2;
+}
+
+static bool
+vfile_object_add_right(struct acl_object *aclobj, unsigned int idx,
+ const struct acl_rights_update *update)
+{
+ struct acl_rights right;
+ bool c1, c2;
+
+ if (update->modify_mode == ACL_MODIFY_MODE_REMOVE &&
+ update->neg_modify_mode == ACL_MODIFY_MODE_REMOVE) {
+ /* nothing to do */
+ return FALSE;
+ }
+
+ i_zero(&right);
+ right.id_type = update->rights.id_type;
+ right.identifier = p_strdup(aclobj->rights_pool,
+ update->rights.identifier);
+
+ c1 = acl_right_names_modify(aclobj->rights_pool, &right.rights,
+ update->rights.rights, update->modify_mode);
+ c2 = acl_right_names_modify(aclobj->rights_pool, &right.neg_rights,
+ update->rights.neg_rights,
+ update->neg_modify_mode);
+ if (c1 || c2) {
+ array_insert(&aclobj->rights, idx, &right, 1);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+vfile_write_right(string_t *dest, const struct acl_rights *right,
+ bool neg)
+{
+ const char *const *rights = neg ? right->neg_rights : right->rights;
+
+ if (neg) str_append_c(dest,'-');
+ acl_rights_write_id(dest, right);
+
+ if (strchr(str_c(dest), ' ') != NULL) T_BEGIN {
+ /* need to escape it */
+ const char *escaped = t_strdup(str_escape(str_c(dest)));
+ str_truncate(dest, 0);
+ str_printfa(dest, "\"%s\"", escaped);
+ } T_END;
+
+ str_append_c(dest, ' ');
+ acl_right_names_write(dest, rights);
+ str_append_c(dest, '\n');
+}
+
+static int
+acl_backend_vfile_update_write(struct acl_object *aclobj,
+ int fd, const char *path)
+{
+ struct ostream *output;
+ string_t *str;
+ const struct acl_rights *rights;
+ unsigned int i, count;
+ int ret = 0;
+
+ output = o_stream_create_fd_file(fd, 0, FALSE);
+ o_stream_cork(output);
+
+ str = str_new(default_pool, 256);
+ /* rights are sorted with globals at the end, so we can stop at the
+ first global */
+ rights = array_get(&aclobj->rights, &count);
+ for (i = 0; i < count && !rights[i].global; i++) {
+ if (rights[i].rights != NULL) {
+ vfile_write_right(str, &rights[i], FALSE);
+ o_stream_nsend(output, str_data(str), str_len(str));
+ str_truncate(str, 0);
+ }
+ if (rights[i].neg_rights != NULL) {
+ vfile_write_right(str, &rights[i], TRUE);
+ o_stream_nsend(output, str_data(str), str_len(str));
+ str_truncate(str, 0);
+ }
+ }
+ str_free(&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);
+ /* we really don't want to lose ACL files' contents, so fsync() always
+ before renaming */
+ if (fsync(fd) < 0) {
+ i_error("fsync(%s) failed: %m", path);
+ ret = -1;
+ }
+ return ret;
+}
+
+static void acl_backend_vfile_update_cache(struct acl_object *_aclobj, int fd)
+{
+ struct acl_backend_vfile_validity *validity;
+ struct stat st;
+
+ if (fstat(fd, &st) < 0) {
+ /* we'll just recalculate or fail it later */
+ acl_cache_flush(_aclobj->backend->cache, _aclobj->name);
+ return;
+ }
+
+ validity = acl_cache_get_validity(_aclobj->backend->cache,
+ _aclobj->name);
+ validity->local_validity.last_read_time = ioloop_time;
+ validity->local_validity.last_mtime = st.st_mtime;
+ validity->local_validity.last_size = st.st_size;
+}
+
+int acl_backend_vfile_object_update(struct acl_object *_aclobj,
+ const struct acl_rights_update *update)
+{
+ struct acl_object_vfile *aclobj =
+ (struct acl_object_vfile *)_aclobj;
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_aclobj->backend;
+ struct acl_backend_vfile_validity *validity;
+ struct dotlock *dotlock;
+ struct utimbuf ut;
+ time_t orig_mtime;
+ const char *path;
+ unsigned int i;
+ int fd;
+ bool changed;
+
+ /* global ACLs can't be updated here */
+ i_assert(!update->rights.global);
+
+ fd = acl_backend_vfile_update_begin(aclobj, &dotlock);
+ if (fd == -1)
+ return -1;
+
+ if (!array_bsearch_insert_pos(&_aclobj->rights, &update->rights,
+ acl_rights_cmp, &i))
+ changed = vfile_object_add_right(_aclobj, i, update);
+ else
+ changed = vfile_object_modify_right(_aclobj, i, update);
+ if (!changed) {
+ file_dotlock_delete(&dotlock);
+ return 0;
+ }
+
+ validity = acl_cache_get_validity(_aclobj->backend->cache,
+ _aclobj->name);
+ orig_mtime = validity->local_validity.last_mtime;
+
+ /* ACLs were really changed, write the new ones */
+ path = file_dotlock_get_lock_path(dotlock);
+ if (acl_backend_vfile_update_write(_aclobj, fd, path) < 0) {
+ file_dotlock_delete(&dotlock);
+ acl_cache_flush(_aclobj->backend->cache, _aclobj->name);
+ return -1;
+ }
+ if (orig_mtime < update->last_change && update->last_change != 0) {
+ /* set mtime to last_change, if it's higher than the file's
+ original mtime. if original mtime is higher, then we're
+ merging some changes and it's better for the mtime to get
+ updated. */
+ ut.actime = ioloop_time;
+ ut.modtime = update->last_change;
+ if (utime(path, &ut) < 0)
+ i_error("utime(%s) failed: %m", path);
+ }
+ acl_backend_vfile_update_cache(_aclobj, fd);
+ if (file_dotlock_replace(&dotlock, 0) < 0) {
+ acl_cache_flush(_aclobj->backend->cache, _aclobj->name);
+ return -1;
+ }
+ /* make sure dovecot-acl-list gets updated if we changed any
+ lookup rights. */
+ if (acl_rights_has_nonowner_lookup_changes(&update->rights) ||
+ update->modify_mode == ACL_MODIFY_MODE_REPLACE ||
+ update->modify_mode == ACL_MODIFY_MODE_CLEAR)
+ (void)acl_backend_vfile_acllist_rebuild(backend);
+ return 0;
+}
diff --git a/src/plugins/acl/acl-backend-vfile.c b/src/plugins/acl/acl-backend-vfile.c
new file mode 100644
index 0000000..3ee0af9
--- /dev/null
+++ b/src/plugins/acl/acl-backend-vfile.c
@@ -0,0 +1,659 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "istream.h"
+#include "nfs-workarounds.h"
+#include "mail-storage-private.h"
+#include "acl-global-file.h"
+#include "acl-cache.h"
+#include "acl-backend-vfile.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#define ACL_ESTALE_RETRY_COUNT NFS_ESTALE_RETRY_COUNT
+#define ACL_VFILE_DEFAULT_CACHE_SECS 30
+
+static struct acl_backend *acl_backend_vfile_alloc(void)
+{
+ struct acl_backend_vfile *backend;
+ pool_t pool;
+
+ pool = pool_alloconly_create("ACL backend", 512);
+ backend = p_new(pool, struct acl_backend_vfile, 1);
+ backend->backend.pool = pool;
+ return &backend->backend;
+}
+
+static int
+acl_backend_vfile_init(struct acl_backend *_backend, const char *data)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+ struct stat st;
+ const char *const *tmp;
+
+ tmp = t_strsplit(data, ":");
+ backend->global_path = p_strdup_empty(_backend->pool, *tmp);
+ backend->cache_secs = ACL_VFILE_DEFAULT_CACHE_SECS;
+
+ if (*tmp != NULL)
+ tmp++;
+ for (; *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "cache_secs=")) {
+ if (str_to_uint(*tmp + 11, &backend->cache_secs) < 0) {
+ i_error("acl vfile: Invalid cache_secs value: %s",
+ *tmp + 11);
+ return -1;
+ }
+ } else {
+ i_error("acl vfile: Unknown parameter: %s", *tmp);
+ return -1;
+ }
+ }
+ if (backend->global_path != NULL) {
+ if (stat(backend->global_path, &st) < 0) {
+ if (errno != ENOENT) {
+ i_error("acl vfile: stat(%s) failed: %m",
+ backend->global_path);
+ return -1;
+ }
+ } else if (!S_ISDIR(st.st_mode)) {
+ _backend->global_file =
+ acl_global_file_init(backend->global_path, backend->cache_secs,
+ _backend->debug);
+ }
+ }
+ if (_backend->debug) {
+ if (backend->global_path == NULL)
+ i_debug("acl vfile: Global ACLs disabled");
+ else if (_backend->global_file != NULL) {
+ i_debug("acl vfile: Global ACL file: %s",
+ backend->global_path);
+ } else {
+ i_debug("acl vfile: Global ACL legacy directory: %s",
+ backend->global_path);
+ }
+ }
+
+ _backend->cache =
+ acl_cache_init(_backend,
+ sizeof(struct acl_backend_vfile_validity));
+ return 0;
+}
+
+static void acl_backend_vfile_deinit(struct acl_backend *_backend)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+
+ if (backend->acllist_pool != NULL) {
+ array_free(&backend->acllist);
+ pool_unref(&backend->acllist_pool);
+ }
+ if (_backend->global_file != NULL)
+ acl_global_file_deinit(&_backend->global_file);
+ pool_unref(&backend->backend.pool);
+}
+
+static const char *
+acl_backend_vfile_get_local_dir(struct acl_backend *backend,
+ const char *name, const char *vname)
+{
+ struct mail_namespace *ns = mailbox_list_get_namespace(backend->list);
+ struct mailbox_list *list = ns->list;
+ struct mail_storage *storage;
+ enum mailbox_list_path_type type;
+ const char *dir, *inbox;
+
+ if (*name == '\0')
+ name = NULL;
+
+ if (backend->globals_only)
+ return NULL;
+
+ /* ACL files are very important. try to keep them among the main
+ mail files. that's not possible though with a) if the mailbox is
+ a file or b) if the mailbox path doesn't point to filesystem. */
+ if (mailbox_list_get_storage(&list, vname, &storage) < 0)
+ return NULL;
+ i_assert(list == ns->list);
+
+ type = mail_storage_get_acl_list_path_type(storage);
+ if (name == NULL) {
+ if (!mailbox_list_get_root_path(list, type, &dir))
+ return NULL;
+ } else {
+ if (mailbox_list_get_path(list, name, type, &dir) <= 0)
+ return NULL;
+ }
+
+ /* verify that the directory isn't same as INBOX's directory.
+ this is mainly for Maildir. */
+ if (name == NULL &&
+ mailbox_list_get_path(list, "INBOX",
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &inbox) > 0 &&
+ strcmp(inbox, dir) == 0) {
+ /* can't have default ACLs with this setup */
+ return NULL;
+ }
+ return dir;
+}
+
+static struct acl_object *
+acl_backend_vfile_object_init(struct acl_backend *_backend,
+ const char *name)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+ struct acl_object_vfile *aclobj;
+ const char *dir, *vname, *error;
+
+ aclobj = i_new(struct acl_object_vfile, 1);
+ aclobj->aclobj.backend = _backend;
+ aclobj->aclobj.name = i_strdup(name);
+
+ T_BEGIN {
+ if (*name == '\0' ||
+ mailbox_list_is_valid_name(_backend->list, name, &error)) {
+ vname = *name == '\0' ? "" :
+ mailbox_list_get_vname(_backend->list, name);
+
+ dir = acl_backend_vfile_get_local_dir(_backend, name, vname);
+ aclobj->local_path = dir == NULL ? NULL :
+ i_strconcat(dir, "/"ACL_FILENAME, NULL);
+ if (backend->global_path != NULL &&
+ _backend->global_file == NULL) {
+ aclobj->global_path =
+ i_strconcat(backend->global_path, "/", vname, NULL);
+ }
+ } else {
+ /* Invalid mailbox name, just use the default
+ global ACL files */
+ }
+ } T_END;
+ return &aclobj->aclobj;
+}
+
+static const char *
+get_parent_mailbox(struct acl_backend *backend, const char *name)
+{
+ const char *p;
+
+ p = strrchr(name, mailbox_list_get_hierarchy_sep(backend->list));
+ return p == NULL ? NULL : t_strdup_until(name, p);
+}
+
+static int
+acl_backend_vfile_exists(struct acl_backend_vfile *backend, const char *path,
+ struct acl_vfile_validity *validity)
+{
+ struct stat st;
+
+ if (validity->last_check + (time_t)backend->cache_secs > ioloop_time) {
+ /* use the cached value */
+ return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOTFOUND ? 1 : 0;
+ }
+
+ validity->last_check = ioloop_time;
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT || errno == ENOTDIR) {
+ validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOTFOUND;
+ return 0;
+ }
+ if (errno == EACCES) {
+ validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOACCESS;
+ return 1;
+ }
+ i_error("stat(%s) failed: %m", path);
+ return -1;
+ }
+ validity->last_mtime = st.st_mtime;
+ validity->last_size = st.st_size;
+ return 1;
+}
+
+static bool
+acl_backend_vfile_has_acl(struct acl_backend *_backend, const char *name)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_backend;
+ struct acl_backend_vfile_validity *old_validity, new_validity;
+ const char *global_path, *vname;
+ int ret;
+
+ old_validity = acl_cache_get_validity(_backend->cache, name);
+ if (old_validity != NULL)
+ new_validity = *old_validity;
+ else
+ i_zero(&new_validity);
+
+ /* The caller wants to stop whenever a parent mailbox exists, even if
+ it has no ACL file. Also, if a mailbox doesn't exist then it can't
+ have a local ACL file. First check if there's a matching global ACL.
+ If not, check if the mailbox exists. */
+ vname = *name == '\0' ? "" :
+ mailbox_list_get_vname(_backend->list, name);
+ struct mailbox *box =
+ mailbox_alloc(_backend->list, vname,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ if (backend->global_path == NULL) {
+ /* global ACLs disabled */
+ ret = 0;
+ } else if (_backend->global_file != NULL) {
+ /* check global ACL file */
+ ret = acl_global_file_refresh(_backend->global_file);
+ if (ret == 0 && acl_global_file_have_any(_backend->global_file, box->vname))
+ ret = 1;
+ } else {
+ /* check global ACL directory */
+ global_path = t_strconcat(backend->global_path, "/", name, NULL);
+ ret = acl_backend_vfile_exists(backend, global_path,
+ &new_validity.global_validity);
+ }
+
+ if (ret != 0) {
+ /* error / global ACL found */
+ } else if (mailbox_open(box) == 0) {
+ /* mailbox exists */
+ ret = 1;
+ } else {
+ enum mail_error error;
+ const char *errstr =
+ mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_NOTFOUND)
+ ret = 0;
+ else {
+ e_error(box->event, "acl: Failed to open mailbox: %s",
+ errstr);
+ ret = -1;
+ }
+ }
+
+ acl_cache_set_validity(_backend->cache, name, &new_validity);
+ mailbox_free(&box);
+ return ret > 0;
+}
+
+static struct acl_object *
+acl_backend_vfile_object_init_parent(struct acl_backend *backend,
+ const char *child_name)
+{
+ const char *parent;
+
+ /* stop at the first parent that
+ a) has global ACL file
+ b) has local ACL file
+ c) exists */
+ while ((parent = get_parent_mailbox(backend, child_name)) != NULL) {
+ if (acl_backend_vfile_has_acl(backend, parent))
+ break;
+ child_name = parent;
+ }
+ if (parent == NULL) {
+ /* use the root */
+ parent = acl_backend_get_default_object(backend)->name;
+ }
+ return acl_backend_vfile_object_init(backend, parent);
+}
+
+static void acl_backend_vfile_object_deinit(struct acl_object *_aclobj)
+{
+ struct acl_object_vfile *aclobj = (struct acl_object_vfile *)_aclobj;
+
+ i_free(aclobj->local_path);
+ i_free(aclobj->global_path);
+
+ if (array_is_created(&aclobj->aclobj.rights))
+ array_free(&aclobj->aclobj.rights);
+ pool_unref(&aclobj->aclobj.rights_pool);
+ i_free(aclobj->aclobj.name);
+ i_free(aclobj);
+}
+
+static int
+acl_backend_vfile_read(struct acl_object *aclobj, bool global, const char *path,
+ struct acl_vfile_validity *validity, bool try_retry,
+ bool *is_dir_r)
+{
+ struct istream *input;
+ struct stat st;
+ struct acl_rights rights;
+ const char *line, *error;
+ unsigned int linenum;
+ int fd, ret = 0;
+
+ *is_dir_r = FALSE;
+
+ fd = nfs_safe_open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT || errno == ENOTDIR) {
+ if (aclobj->backend->debug)
+ i_debug("acl vfile: file %s not found", path);
+ validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOTFOUND;
+ } else if (errno == EACCES) {
+ if (aclobj->backend->debug)
+ i_debug("acl vfile: no access to file %s",
+ path);
+
+ acl_object_remove_all_access(aclobj);
+ validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOACCESS;
+ } else {
+ i_error("open(%s) failed: %m", path);
+ return -1;
+ }
+
+ validity->last_size = 0;
+ validity->last_read_time = ioloop_time;
+ return 1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ if (errno == ESTALE && try_retry) {
+ i_close_fd(&fd);
+ return 0;
+ }
+
+ i_error("fstat(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ /* we opened a directory. */
+ *is_dir_r = TRUE;
+ i_close_fd(&fd);
+ return 0;
+ }
+
+ if (aclobj->backend->debug)
+ i_debug("acl vfile: reading file %s", path);
+
+ input = i_stream_create_fd(fd, SIZE_MAX);
+ i_stream_set_return_partial_line(input, TRUE);
+ linenum = 0;
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ linenum++;
+ if (line[0] == '\0' || line[0] == '#')
+ continue;
+ T_BEGIN {
+ ret = acl_rights_parse_line(line, aclobj->rights_pool,
+ &rights, &error);
+ rights.global = global;
+ if (ret < 0) {
+ i_error("ACL file %s line %u: %s",
+ path, linenum, error);
+ } else {
+ array_push_back(&aclobj->rights, &rights);
+ }
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+
+ if (ret < 0) {
+ /* parsing failure */
+ } else if (input->stream_errno != 0) {
+ if (input->stream_errno == ESTALE && try_retry)
+ ret = 0;
+ else {
+ ret = -1;
+ i_error("read(%s) failed: %s", path,
+ i_stream_get_error(input));
+ }
+ } else {
+ if (fstat(fd, &st) < 0) {
+ if (errno == ESTALE && try_retry)
+ ret = 0;
+ else {
+ ret = -1;
+ i_error("fstat(%s) failed: %m", path);
+ }
+ } else {
+ ret = 1;
+ validity->last_read_time = ioloop_time;
+ validity->last_mtime = st.st_mtime;
+ validity->last_size = st.st_size;
+ }
+ }
+
+ i_stream_unref(&input);
+ if (close(fd) < 0) {
+ if (errno == ESTALE && try_retry)
+ return 0;
+
+ i_error("close(%s) failed: %m", path);
+ return -1;
+ }
+ return ret;
+}
+
+static int
+acl_backend_vfile_read_with_retry(struct acl_object *aclobj,
+ bool global, const char *path,
+ struct acl_vfile_validity *validity)
+{
+ unsigned int i;
+ int ret;
+ bool is_dir;
+
+ if (path == NULL)
+ return 0;
+
+ for (i = 0;; i++) {
+ ret = acl_backend_vfile_read(aclobj, global, path, validity,
+ i < ACL_ESTALE_RETRY_COUNT,
+ &is_dir);
+ if (ret != 0)
+ break;
+
+ if (is_dir) {
+ /* opened a directory. use dir/.DEFAULT instead */
+ path = t_strconcat(path, "/.DEFAULT", NULL);
+ } else {
+ /* ESTALE - try again */
+ }
+ }
+
+ return ret <= 0 ? -1 : 0;
+}
+
+static bool
+acl_vfile_validity_has_changed(struct acl_backend_vfile *backend,
+ const struct acl_vfile_validity *validity,
+ const struct stat *st)
+{
+ if (st->st_mtime == validity->last_mtime &&
+ st->st_size == validity->last_size) {
+ /* same timestamp, but if it was modified within the
+ same second we want to refresh it again later (but
+ do it only after a couple of seconds so we don't
+ keep re-reading it all the time within those
+ seconds) */
+ time_t cache_secs = backend->cache_secs;
+
+ if (validity->last_read_time != 0 &&
+ (st->st_mtime < validity->last_read_time - cache_secs ||
+ ioloop_time - validity->last_read_time <= cache_secs))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+acl_backend_vfile_refresh(struct acl_object *aclobj, const char *path,
+ struct acl_vfile_validity *validity)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)aclobj->backend;
+ struct stat st;
+ int ret;
+
+ if (validity == NULL)
+ return 1;
+ if (path == NULL ||
+ validity->last_check + (time_t)backend->cache_secs > ioloop_time)
+ return 0;
+
+ validity->last_check = ioloop_time;
+ ret = stat(path, &st);
+ if (ret == 0 && S_ISDIR(st.st_mode)) {
+ /* it's a directory. use dir/.DEFAULT instead */
+ path = t_strconcat(path, "/.DEFAULT", NULL);
+ ret = stat(path, &st);
+ }
+
+ if (ret < 0) {
+ if (errno == ENOENT || errno == ENOTDIR) {
+ /* if the file used to exist, we have to re-read it */
+ return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOTFOUND ? 1 : 0;
+ }
+ if (errno == EACCES)
+ return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOACCESS ? 1 : 0;
+ i_error("stat(%s) failed: %m", path);
+ return -1;
+ }
+ return acl_vfile_validity_has_changed(backend, validity, &st) ? 1 : 0;
+}
+
+int acl_backend_vfile_object_get_mtime(struct acl_object *aclobj,
+ time_t *mtime_r)
+{
+ struct acl_backend_vfile_validity *validity;
+
+ validity = acl_cache_get_validity(aclobj->backend->cache, aclobj->name);
+ if (validity == NULL)
+ return -1;
+
+ if (validity->local_validity.last_mtime != 0)
+ *mtime_r = validity->local_validity.last_mtime;
+ else if (validity->global_validity.last_mtime != 0)
+ *mtime_r = validity->global_validity.last_mtime;
+ else
+ *mtime_r = 0;
+ return 0;
+}
+
+static int
+acl_backend_global_file_refresh(struct acl_object *_aclobj,
+ struct acl_vfile_validity *validity)
+{
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_aclobj->backend;
+ struct stat st;
+
+ if (acl_global_file_refresh(_aclobj->backend->global_file) < 0)
+ return -1;
+
+ acl_global_file_last_stat(_aclobj->backend->global_file, &st);
+ if (validity == NULL)
+ return 1;
+ return acl_vfile_validity_has_changed(backend, validity, &st) ? 1 : 0;
+}
+
+static int acl_backend_vfile_object_refresh_cache(struct acl_object *_aclobj)
+{
+ struct acl_object_vfile *aclobj = (struct acl_object_vfile *)_aclobj;
+ struct acl_backend_vfile *backend =
+ (struct acl_backend_vfile *)_aclobj->backend;
+ struct acl_backend_vfile_validity *old_validity;
+ struct acl_backend_vfile_validity validity;
+ time_t mtime;
+ int ret;
+
+ old_validity = acl_cache_get_validity(_aclobj->backend->cache,
+ _aclobj->name);
+ ret = _aclobj->backend->global_file != NULL ?
+ acl_backend_global_file_refresh(_aclobj, old_validity == NULL ? NULL :
+ &old_validity->global_validity) :
+ acl_backend_vfile_refresh(_aclobj, aclobj->global_path,
+ old_validity == NULL ? NULL :
+ &old_validity->global_validity);
+ if (ret == 0) {
+ ret = acl_backend_vfile_refresh(_aclobj, aclobj->local_path,
+ old_validity == NULL ? NULL :
+ &old_validity->local_validity);
+ }
+ if (ret <= 0)
+ return ret;
+
+ /* either global or local ACLs changed, need to re-read both */
+ if (!array_is_created(&_aclobj->rights)) {
+ _aclobj->rights_pool =
+ pool_alloconly_create("acl rights", 256);
+ i_array_init(&_aclobj->rights, 16);
+ } else {
+ array_clear(&_aclobj->rights);
+ p_clear(_aclobj->rights_pool);
+ }
+
+ i_zero(&validity);
+ if (_aclobj->backend->global_file != NULL) {
+ struct stat st;
+
+ acl_object_add_global_acls(_aclobj);
+ acl_global_file_last_stat(_aclobj->backend->global_file, &st);
+ validity.global_validity.last_read_time = ioloop_time;
+ validity.global_validity.last_mtime = st.st_mtime;
+ validity.global_validity.last_size = st.st_size;
+ } else {
+ if (acl_backend_vfile_read_with_retry(_aclobj, TRUE, aclobj->global_path,
+ &validity.global_validity) < 0)
+ return -1;
+ }
+ if (acl_backend_vfile_read_with_retry(_aclobj, FALSE, aclobj->local_path,
+ &validity.local_validity) < 0)
+ return -1;
+
+ acl_rights_sort(_aclobj);
+ /* update cache only after we've successfully read everything */
+ acl_object_rebuild_cache(_aclobj);
+ acl_cache_set_validity(_aclobj->backend->cache,
+ _aclobj->name, &validity);
+
+ if (acl_backend_vfile_object_get_mtime(_aclobj, &mtime) == 0)
+ acl_backend_vfile_acllist_verify(backend, _aclobj->name, mtime);
+ return 0;
+}
+
+static int acl_backend_vfile_object_last_changed(struct acl_object *_aclobj,
+ time_t *last_changed_r)
+{
+ struct acl_backend_vfile_validity *old_validity;
+
+ *last_changed_r = 0;
+
+ old_validity = acl_cache_get_validity(_aclobj->backend->cache,
+ _aclobj->name);
+ if (old_validity == NULL) {
+ if (acl_backend_vfile_object_refresh_cache(_aclobj) < 0)
+ return -1;
+ old_validity = acl_cache_get_validity(_aclobj->backend->cache,
+ _aclobj->name);
+ if (old_validity == NULL)
+ return 0;
+ }
+ *last_changed_r = old_validity->local_validity.last_mtime;
+ return 0;
+}
+
+struct acl_backend_vfuncs acl_backend_vfile = {
+ acl_backend_vfile_alloc,
+ acl_backend_vfile_init,
+ acl_backend_vfile_deinit,
+ acl_backend_vfile_nonowner_iter_init,
+ acl_backend_vfile_nonowner_iter_next,
+ acl_backend_vfile_nonowner_iter_deinit,
+ acl_backend_vfile_nonowner_lookups_rebuild,
+ acl_backend_vfile_object_init,
+ acl_backend_vfile_object_init_parent,
+ acl_backend_vfile_object_deinit,
+ acl_backend_vfile_object_refresh_cache,
+ acl_backend_vfile_object_update,
+ acl_backend_vfile_object_last_changed,
+ acl_default_object_list_init,
+ acl_default_object_list_next,
+ acl_default_object_list_deinit
+};
diff --git a/src/plugins/acl/acl-backend-vfile.h b/src/plugins/acl/acl-backend-vfile.h
new file mode 100644
index 0000000..c5aaa25
--- /dev/null
+++ b/src/plugins/acl/acl-backend-vfile.h
@@ -0,0 +1,88 @@
+#ifndef ACL_BACKEND_VFILE_H
+#define ACL_BACKEND_VFILE_H
+
+#include "acl-api-private.h"
+#include "mail-storage-private.h"
+
+#define ACL_FILENAME "dovecot-acl"
+#define ACLLIST_FILENAME "dovecot-acl-list"
+
+#define ACL_VFILE_VALIDITY_MTIME_NOTFOUND 0
+#define ACL_VFILE_VALIDITY_MTIME_NOACCESS -1
+
+struct acl_vfile_validity {
+ time_t last_check;
+
+ time_t last_read_time;
+ time_t last_mtime;
+ off_t last_size;
+};
+
+struct acl_backend_vfile_validity {
+ struct acl_vfile_validity global_validity, local_validity;
+};
+
+struct acl_object_vfile {
+ struct acl_object aclobj;
+
+ /* if backend->global_file is NULL, assume legacy separate global
+ ACL file per mailbox */
+ char *global_path, *local_path;
+};
+
+struct acl_backend_vfile_acllist {
+ time_t mtime;
+ const char *name;
+};
+
+struct acl_backend_vfile {
+ struct acl_backend backend;
+ const char *global_path;
+
+ pool_t acllist_pool;
+ ARRAY(struct acl_backend_vfile_acllist) acllist;
+
+ time_t acllist_last_check;
+ time_t acllist_mtime;
+ unsigned int acllist_change_counter;
+
+ unsigned int cache_secs;
+ bool rebuilding_acllist:1;
+ bool iterating_acllist:1;
+};
+
+void acl_vfile_write_rights_list(string_t *dest, const char *const *rights);
+int acl_backend_vfile_object_update(struct acl_object *aclobj,
+ const struct acl_rights_update *update);
+
+void acl_backend_vfile_acllist_refresh(struct acl_backend_vfile *backend);
+int acl_backend_vfile_acllist_rebuild(struct acl_backend_vfile *backend);
+void acl_backend_vfile_acllist_verify(struct acl_backend_vfile *backend,
+ const char *name, time_t mtime);
+
+struct acl_mailbox_list_context *
+acl_backend_vfile_nonowner_iter_init(struct acl_backend *backend);
+bool acl_backend_vfile_nonowner_iter_next(struct acl_mailbox_list_context *ctx,
+ const char **name_r);
+int
+acl_backend_vfile_nonowner_iter_deinit(struct acl_mailbox_list_context *ctx);
+int acl_backend_vfile_nonowner_lookups_rebuild(struct acl_backend *backend);
+
+int acl_backend_vfile_object_get_mtime(struct acl_object *aclobj,
+ time_t *mtime_r);
+
+static inline enum mailbox_list_path_type
+mail_storage_get_acl_list_path_type(struct mail_storage *storage)
+{
+ if (mail_storage_is_mailbox_file(storage)) {
+ /* mailbox is a directory (e.g. mbox) */
+ return MAILBOX_LIST_PATH_TYPE_CONTROL;
+ }
+ if ((storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_ROOT) != 0) {
+ /* there is no local mailbox directory */
+ return MAILBOX_LIST_PATH_TYPE_CONTROL;
+ }
+ return MAILBOX_LIST_PATH_TYPE_MAILBOX;
+}
+
+#endif
diff --git a/src/plugins/acl/acl-backend.c b/src/plugins/acl/acl-backend.c
new file mode 100644
index 0000000..0514dc7
--- /dev/null
+++ b/src/plugins/acl/acl-backend.c
@@ -0,0 +1,194 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "sort.h"
+#include "mail-storage-settings.h"
+#include "mailbox-list.h"
+#include "mail-namespace.h"
+#include "mail-user.h"
+#include "acl-cache.h"
+#include "acl-api-private.h"
+
+
+extern struct acl_backend_vfuncs acl_backend_vfile;
+
+const char *const all_mailbox_rights[] = {
+ MAIL_ACL_LOOKUP,
+ MAIL_ACL_READ,
+ MAIL_ACL_WRITE,
+ MAIL_ACL_WRITE_SEEN,
+ MAIL_ACL_WRITE_DELETED,
+ MAIL_ACL_INSERT,
+ MAIL_ACL_POST,
+ MAIL_ACL_EXPUNGE,
+ MAIL_ACL_CREATE,
+ MAIL_ACL_DELETE,
+ MAIL_ACL_ADMIN,
+ NULL
+};
+
+static const char *const *owner_mailbox_rights = all_mailbox_rights;
+static const char *const non_owner_mailbox_rights[] = { NULL };
+
+struct acl_backend *
+acl_backend_init(const char *data, struct mailbox_list *list,
+ const char *acl_username, const char *const *groups,
+ bool owner)
+{
+ struct mail_user *user = mailbox_list_get_user(list);
+ struct acl_backend *backend;
+ unsigned int i, group_count;
+
+ e_debug(user->event, "acl: initializing backend with data: %s", data);
+ e_debug(user->event, "acl: acl username = %s", acl_username);
+ e_debug(user->event, "acl: owner = %d", owner ? 1 : 0);
+
+ group_count = str_array_length(groups);
+
+ if (str_begins(data, "vfile:"))
+ data += 6;
+ else if (strcmp(data, "vfile") == 0)
+ data = "";
+ else
+ i_fatal("Unknown ACL backend: %s", t_strcut(data, ':'));
+
+ backend = acl_backend_vfile.alloc();
+ backend->debug = user->mail_debug;
+ backend->v = acl_backend_vfile;
+ backend->list = list;
+ backend->username = p_strdup(backend->pool, acl_username);
+ backend->owner = owner;
+ backend->globals_only =
+ mail_user_plugin_getenv_bool(user, "acl_globals_only");
+
+ if (group_count > 0) {
+ backend->group_count = group_count;
+ backend->groups =
+ p_new(backend->pool, const char *, group_count);
+ for (i = 0; i < group_count; i++) {
+ backend->groups[i] = p_strdup(backend->pool, groups[i]);
+ e_debug(user->event, "acl: group added: %s", groups[i]);
+ }
+ i_qsort(backend->groups, group_count, sizeof(const char *),
+ i_strcmp_p);
+ }
+
+ T_BEGIN {
+ if (acl_backend_vfile.init(backend, data) < 0)
+ i_fatal("acl: backend vfile init failed with data: %s",
+ data);
+ } T_END;
+
+ backend->default_rights = owner ? owner_mailbox_rights :
+ non_owner_mailbox_rights;
+ backend->default_aclmask =
+ acl_cache_mask_init(backend->cache, backend->pool,
+ backend->default_rights);
+ return backend;
+}
+
+void acl_backend_deinit(struct acl_backend **_backend)
+{
+ struct acl_backend *backend = *_backend;
+
+ *_backend = NULL;
+
+ if (backend->default_aclobj != NULL)
+ acl_object_deinit(&backend->default_aclobj);
+ acl_cache_deinit(&backend->cache);
+ backend->v.deinit(backend);
+}
+
+const char *acl_backend_get_acl_username(struct acl_backend *backend)
+{
+ return backend->username;
+}
+
+bool acl_backend_user_is_authenticated(struct acl_backend *backend)
+{
+ return backend->username != NULL;
+}
+
+bool acl_backend_user_is_owner(struct acl_backend *backend)
+{
+ return backend->owner;
+}
+
+bool acl_backend_user_name_equals(struct acl_backend *backend,
+ const char *username)
+{
+ if (backend->username == NULL) {
+ /* anonymous user never matches */
+ return FALSE;
+ }
+
+ return strcmp(backend->username, username) == 0;
+}
+
+bool acl_backend_user_is_in_group(struct acl_backend *backend,
+ const char *group_name)
+{
+ return i_bsearch(group_name, backend->groups, backend->group_count,
+ sizeof(const char *), bsearch_strcmp) != NULL;
+}
+
+bool acl_backend_rights_match_me(struct acl_backend *backend,
+ const struct acl_rights *rights)
+{
+ switch (rights->id_type) {
+ case ACL_ID_ANYONE:
+ return TRUE;
+ case ACL_ID_AUTHENTICATED:
+ return acl_backend_user_is_authenticated(backend);
+ case ACL_ID_GROUP:
+ case ACL_ID_GROUP_OVERRIDE:
+ return acl_backend_user_is_in_group(backend, rights->identifier);
+ case ACL_ID_USER:
+ return acl_backend_user_name_equals(backend, rights->identifier);
+ case ACL_ID_OWNER:
+ return acl_backend_user_is_owner(backend);
+ case ACL_ID_TYPE_COUNT:
+ break;
+ }
+ i_unreached();
+}
+
+unsigned int acl_backend_lookup_right(struct acl_backend *backend,
+ const char *right)
+{
+ return acl_cache_right_lookup(backend->cache, right);
+}
+
+struct acl_object *acl_backend_get_default_object(struct acl_backend *backend)
+{
+ struct mail_user *user = mailbox_list_get_user(backend->list);
+ struct mail_namespace *ns = mailbox_list_get_namespace(backend->list);
+ const char *default_name = "";
+
+ if (backend->default_aclobj != NULL)
+ return backend->default_aclobj;
+
+ if (mail_user_plugin_getenv_bool(user, "acl_defaults_from_inbox")) {
+ if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE ||
+ ns->type == MAIL_NAMESPACE_TYPE_SHARED)
+ default_name = "INBOX";
+ }
+ backend->default_aclobj =
+ acl_object_init_from_name(backend, default_name);
+ return backend->default_aclobj;
+}
+
+int acl_backend_get_default_rights(struct acl_backend *backend,
+ const struct acl_mask **mask_r)
+{
+ struct acl_object *aclobj = acl_backend_get_default_object(backend);
+
+ if (backend->v.object_refresh_cache(aclobj) < 0)
+ return -1;
+
+ *mask_r = acl_cache_get_my_rights(backend->cache, aclobj->name);
+ if (*mask_r == NULL)
+ *mask_r = backend->default_aclmask;
+ return 0;
+}
diff --git a/src/plugins/acl/acl-cache.c b/src/plugins/acl/acl-cache.c
new file mode 100644
index 0000000..8f07d56
--- /dev/null
+++ b/src/plugins/acl/acl-cache.c
@@ -0,0 +1,395 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "acl-cache.h"
+#include "acl-api-private.h"
+
+/* Give more than enough so that the arrays should never have to be grown.
+ IMAP ACLs define only 10 standard rights and 10 user-defined rights. */
+#define DEFAULT_ACL_RIGHTS_COUNT 64
+
+#define ACL_GLOBAL_COUNT 2
+
+struct acl_object_cache {
+ char *name;
+
+ struct acl_mask *my_rights, *my_neg_rights;
+ struct acl_mask *my_current_rights;
+};
+
+struct acl_cache {
+ struct acl_backend *backend;
+ /* name => object */
+ HASH_TABLE(char *, struct acl_object_cache *) objects;
+
+ size_t validity_rec_size;
+
+ /* Right names mapping is used for faster rights checking. Note that
+ acl_mask bitmask relies on the order to never change, so only new
+ rights can be added to the mapping. */
+ pool_t right_names_pool;
+ /* idx => right name. */
+ ARRAY(const char *) right_idx_name_map;
+ /* name => idx+1 */
+ HASH_TABLE(char *, void *) right_name_idx_map;
+};
+
+static struct acl_mask negative_cache_entry;
+
+struct acl_cache *acl_cache_init(struct acl_backend *backend,
+ size_t validity_rec_size)
+{
+ struct acl_cache *cache;
+
+ cache = i_new(struct acl_cache, 1);
+ cache->backend = backend;
+ cache->validity_rec_size = validity_rec_size;
+ cache->right_names_pool =
+ pool_alloconly_create("ACL right names", 1024);
+ hash_table_create(&cache->objects, default_pool, 0, str_hash, strcmp);
+ hash_table_create(&cache->right_name_idx_map,
+ cache->right_names_pool, 0, str_hash, strcmp);
+ i_array_init(&cache->right_idx_name_map, DEFAULT_ACL_RIGHTS_COUNT);
+ return cache;
+}
+
+void acl_cache_deinit(struct acl_cache **_cache)
+{
+ struct acl_cache *cache = *_cache;
+
+ *_cache = NULL;
+
+ acl_cache_flush_all(cache);
+ array_free(&cache->right_idx_name_map);
+ hash_table_destroy(&cache->right_name_idx_map);
+ hash_table_destroy(&cache->objects);
+ pool_unref(&cache->right_names_pool);
+ i_free(cache);
+}
+
+static void acl_cache_free_object_cache(struct acl_object_cache *obj_cache)
+{
+ if (obj_cache->my_current_rights != NULL &&
+ obj_cache->my_current_rights != &negative_cache_entry)
+ acl_cache_mask_deinit(&obj_cache->my_current_rights);
+ if (obj_cache->my_rights != NULL)
+ acl_cache_mask_deinit(&obj_cache->my_rights);
+ if (obj_cache->my_neg_rights != NULL)
+ acl_cache_mask_deinit(&obj_cache->my_neg_rights);
+ i_free(obj_cache->name);
+ i_free(obj_cache);
+}
+
+static struct acl_mask *
+acl_cache_mask_init_real(struct acl_cache *cache, pool_t pool,
+ const char *const *rights)
+{
+ struct acl_mask *mask;
+ unsigned int rights_count, i, idx;
+ unsigned char *p;
+ buffer_t *bitmask;
+
+ rights_count = str_array_length(rights);
+ bitmask = t_buffer_create(DEFAULT_ACL_RIGHTS_COUNT / CHAR_BIT);
+ for (i = 0; i < rights_count; i++) {
+ idx = acl_cache_right_lookup(cache, rights[i]);
+ p = buffer_get_space_unsafe(bitmask, idx / CHAR_BIT, 1);
+ *p |= 1 << (idx % CHAR_BIT);
+ }
+
+ /* @UNSAFE */
+ mask = p_malloc(pool, SIZEOF_ACL_MASK(bitmask->used));
+ memcpy(mask->mask, bitmask->data, bitmask->used);
+ mask->pool = pool;
+ mask->size = bitmask->used;
+ return mask;
+}
+
+struct acl_mask *acl_cache_mask_init(struct acl_cache *cache, pool_t pool,
+ const char *const *rights)
+{
+ struct acl_mask *mask;
+
+ T_BEGIN {
+ mask = acl_cache_mask_init_real(cache, pool, rights);
+ } T_END;
+ return mask;
+}
+
+static struct acl_mask *
+acl_cache_mask_dup(pool_t pool, const struct acl_mask *src)
+{
+ struct acl_mask *mask;
+
+ mask = p_malloc(pool, SIZEOF_ACL_MASK(src->size));
+ memcpy(mask->mask, src->mask, src->size);
+ mask->pool = pool;
+ mask->size = src->size;
+ return mask;
+}
+
+void acl_cache_mask_deinit(struct acl_mask **_mask)
+{
+ struct acl_mask *mask = *_mask;
+
+ *_mask = NULL;
+ p_free(mask->pool, mask);
+}
+
+unsigned int acl_cache_right_lookup(struct acl_cache *cache, const char *right)
+{
+ unsigned int idx;
+ void *idx_p;
+ char *name;
+ const char *const_name;
+
+ /* use +1 for right_name_idx_map values because we can't add NULL
+ values. */
+ idx_p = hash_table_lookup(cache->right_name_idx_map, right);
+ if (idx_p == NULL) {
+ /* new right name, add it */
+ const_name = name = p_strdup(cache->right_names_pool, right);
+
+ idx = array_count(&cache->right_idx_name_map);
+ array_push_back(&cache->right_idx_name_map, &const_name);
+ hash_table_insert(cache->right_name_idx_map, name,
+ POINTER_CAST(idx + 1));
+ } else {
+ idx = POINTER_CAST_TO(idx_p, unsigned int)-1;
+ }
+ return idx;
+}
+
+void acl_cache_flush(struct acl_cache *cache, const char *objname)
+{
+ struct acl_object_cache *obj_cache;
+
+ obj_cache = hash_table_lookup(cache->objects, objname);
+ if (obj_cache != NULL) {
+ hash_table_remove(cache->objects, objname);
+ acl_cache_free_object_cache(obj_cache);
+ }
+}
+
+void acl_cache_flush_all(struct acl_cache *cache)
+{
+ struct hash_iterate_context *iter;
+ char *key;
+ struct acl_object_cache *obj_cache;
+
+ iter = hash_table_iterate_init(cache->objects);
+ while (hash_table_iterate(iter, cache->objects, &key, &obj_cache))
+ acl_cache_free_object_cache(obj_cache);
+ hash_table_iterate_deinit(&iter);
+
+ hash_table_clear(cache->objects, FALSE);
+}
+
+static void
+acl_cache_update_rights_mask(struct acl_cache *cache,
+ struct acl_object_cache *obj_cache,
+ enum acl_modify_mode modify_mode,
+ const char *const *rights,
+ struct acl_mask **mask_p)
+{
+ struct acl_mask *change_mask, *old_mask, *new_mask;
+ unsigned int i, size;
+ bool changed = TRUE;
+
+ change_mask = rights == NULL ? NULL :
+ acl_cache_mask_init(cache, default_pool, rights);
+ old_mask = *mask_p;
+ new_mask = old_mask;
+
+ switch (modify_mode) {
+ case ACL_MODIFY_MODE_ADD:
+ if (old_mask == NULL) {
+ new_mask = change_mask;
+ break;
+ }
+
+ if (change_mask == NULL) {
+ /* no changes */
+ changed = FALSE;
+ break;
+ }
+
+ /* merge the masks */
+ if (old_mask->size >= change_mask->size) {
+ /* keep using the old mask */
+ for (i = 0; i < change_mask->size; i++)
+ old_mask->mask[i] |= change_mask->mask[i];
+ } else {
+ /* use the new mask, put old changes into it */
+ for (i = 0; i < old_mask->size; i++)
+ change_mask->mask[i] |= old_mask->mask[i];
+ new_mask = change_mask;
+ }
+ break;
+ case ACL_MODIFY_MODE_REMOVE:
+ if (old_mask == NULL || change_mask == NULL) {
+ changed = FALSE;
+ break;
+ }
+
+ /* remove changed bits from old mask */
+ size = I_MIN(old_mask->size, change_mask->size);
+ for (i = 0; i < size; i++)
+ old_mask->mask[i] &= ENUM_NEGATE(change_mask->mask[i]);
+ break;
+ case ACL_MODIFY_MODE_REPLACE:
+ if (old_mask == NULL && change_mask == NULL)
+ changed = FALSE;
+ new_mask = change_mask;
+ break;
+ case ACL_MODIFY_MODE_CLEAR:
+ i_unreached();
+ }
+
+ if (new_mask != old_mask) {
+ *mask_p = new_mask;
+ if (old_mask != NULL)
+ acl_cache_mask_deinit(&old_mask);
+ }
+ if (new_mask != change_mask && change_mask != NULL)
+ acl_cache_mask_deinit(&change_mask);
+
+ if (changed && obj_cache->my_current_rights != NULL) {
+ /* current rights need to be recalculated */
+ if (obj_cache->my_current_rights == &negative_cache_entry)
+ obj_cache->my_current_rights = NULL;
+ else
+ acl_cache_mask_deinit(&obj_cache->my_current_rights);
+ }
+}
+
+static struct acl_object_cache *
+acl_cache_object_get(struct acl_cache *cache, const char *objname,
+ bool *created_r)
+{
+ struct acl_object_cache *obj_cache;
+
+ obj_cache = hash_table_lookup(cache->objects, objname);
+ if (obj_cache == NULL) {
+ obj_cache = i_malloc(MALLOC_ADD(sizeof(struct acl_object_cache),
+ cache->validity_rec_size));
+ obj_cache->name = i_strdup(objname);
+ hash_table_insert(cache->objects, obj_cache->name, obj_cache);
+ *created_r = TRUE;
+ } else {
+ *created_r = FALSE;
+ }
+ return obj_cache;
+}
+
+void acl_cache_update(struct acl_cache *cache, const char *objname,
+ const struct acl_rights_update *update)
+{
+ struct acl_object_cache *obj_cache;
+ bool created;
+
+ obj_cache = acl_cache_object_get(cache, objname, &created);
+ i_assert(obj_cache->my_current_rights != &negative_cache_entry);
+
+ if (created && update->modify_mode != ACL_MODIFY_MODE_REPLACE) {
+ /* since the rights aren't being replaced, start with our
+ default rights */
+ obj_cache->my_rights =
+ acl_cache_mask_dup(default_pool,
+ cache->backend->default_aclmask);
+ }
+
+ acl_cache_update_rights_mask(cache, obj_cache, update->modify_mode,
+ update->rights.rights,
+ &obj_cache->my_rights);
+ acl_cache_update_rights_mask(cache, obj_cache, update->neg_modify_mode,
+ update->rights.neg_rights,
+ &obj_cache->my_neg_rights);
+}
+
+void acl_cache_set_validity(struct acl_cache *cache, const char *objname,
+ const void *validity)
+{
+ struct acl_object_cache *obj_cache;
+ bool created;
+
+ obj_cache = acl_cache_object_get(cache, objname, &created);
+
+ /* @UNSAFE: validity is stored after the cache record */
+ memcpy(obj_cache + 1, validity, cache->validity_rec_size);
+
+ if (created) {
+ /* negative cache entry */
+ obj_cache->my_current_rights = &negative_cache_entry;
+ }
+}
+
+void *acl_cache_get_validity(struct acl_cache *cache, const char *objname)
+{
+ struct acl_object_cache *obj_cache;
+
+ obj_cache = hash_table_lookup(cache->objects, objname);
+ return obj_cache == NULL ? NULL : (obj_cache + 1);
+}
+
+const char *const *acl_cache_get_names(struct acl_cache *cache,
+ unsigned int *count_r)
+{
+ *count_r = array_count(&cache->right_idx_name_map);
+ return array_front(&cache->right_idx_name_map);
+}
+
+static void
+acl_cache_my_current_rights_recalculate(struct acl_object_cache *obj_cache)
+{
+ struct acl_mask *mask;
+ unsigned int i, size;
+
+ /* @UNSAFE */
+ size = obj_cache->my_rights == NULL ? 0 :
+ obj_cache->my_rights->size;
+ mask = i_malloc(SIZEOF_ACL_MASK(size));
+ mask->pool = default_pool;
+ mask->size = size;
+
+ /* apply the positive rights */
+ if (obj_cache->my_rights != NULL)
+ memcpy(mask->mask, obj_cache->my_rights->mask, mask->size);
+ if (obj_cache->my_neg_rights != NULL) {
+ /* apply the negative rights. they override positive rights. */
+ size = I_MIN(mask->size, obj_cache->my_neg_rights->size);
+ for (i = 0; i < size; i++)
+ mask->mask[i] &= ENUM_NEGATE(obj_cache->my_neg_rights->mask[i]);
+ }
+
+ obj_cache->my_current_rights = mask;
+}
+
+const struct acl_mask *
+acl_cache_get_my_rights(struct acl_cache *cache, const char *objname)
+{
+ struct acl_object_cache *obj_cache;
+
+ obj_cache = hash_table_lookup(cache->objects, objname);
+ if (obj_cache == NULL ||
+ obj_cache->my_current_rights == &negative_cache_entry)
+ return NULL;
+
+ if (obj_cache->my_current_rights == NULL) {
+ T_BEGIN {
+ acl_cache_my_current_rights_recalculate(obj_cache);
+ } T_END;
+ }
+ return obj_cache->my_current_rights;
+}
+
+bool acl_cache_mask_isset(const struct acl_mask *mask, unsigned int right_idx)
+{
+ unsigned int mask_idx;
+
+ mask_idx = right_idx / CHAR_BIT;
+ return mask_idx < mask->size &&
+ (mask->mask[mask_idx] & (1 << (right_idx % CHAR_BIT))) != 0;
+}
diff --git a/src/plugins/acl/acl-cache.h b/src/plugins/acl/acl-cache.h
new file mode 100644
index 0000000..b7c2065
--- /dev/null
+++ b/src/plugins/acl/acl-cache.h
@@ -0,0 +1,57 @@
+#ifndef ACL_CACHE_H
+#define ACL_CACHE_H
+
+struct acl_backend;
+struct acl_rights_update;
+
+struct acl_mask {
+ pool_t pool;
+
+ /* mask[] size as bytes */
+ unsigned int size;
+
+ /* variable length bitmask */
+ unsigned char mask[1];
+};
+#define SIZEOF_ACL_MASK(bitmask_size) \
+ (MALLOC_ADD((bitmask_size), sizeof(pool_t) + sizeof(unsigned int)))
+
+struct acl_cache *acl_cache_init(struct acl_backend *backend,
+ size_t validity_rec_size);
+void acl_cache_deinit(struct acl_cache **cache);
+
+struct acl_mask *acl_cache_mask_init(struct acl_cache *cache, pool_t pool,
+ const char *const *rights);
+void acl_cache_mask_deinit(struct acl_mask **mask);
+unsigned int acl_cache_right_lookup(struct acl_cache *cache,
+ const char *right);
+
+/* Flush cache for given object name */
+void acl_cache_flush(struct acl_cache *cache, const char *objname);
+/* Flush cache for all objects */
+void acl_cache_flush_all(struct acl_cache *cache);
+
+/* Update object ACLs. The new rights are always applied on top of the
+ existing rights. The ordering by acl_id_type must be done by the caller. */
+void acl_cache_update(struct acl_cache *cache, const char *objname,
+ const struct acl_rights_update *update);
+/* Return ACL object validity, or NULL if object doesn't exit. */
+void *acl_cache_get_validity(struct acl_cache *cache, const char *objname);
+/* Update ACL object validity, creating the object if needed. */
+void acl_cache_set_validity(struct acl_cache *cache, const char *objname,
+ const void *validity);
+
+/* Returns all the right names currently created. The returned pointer may
+ change after calling acl_cache_update(). */
+const char *const *acl_cache_get_names(struct acl_cache *cache,
+ unsigned int *count_r);
+
+/* Returns user's current rights, or NULL if no rights have been specified
+ for this object. */
+const struct acl_mask *
+acl_cache_get_my_rights(struct acl_cache *cache, const char *objname);
+
+/* Returns TRUE if given right index is set in mask. */
+bool acl_cache_mask_isset(const struct acl_mask *mask, unsigned int right_idx);
+
+#endif
diff --git a/src/plugins/acl/acl-global-file.c b/src/plugins/acl/acl-global-file.c
new file mode 100644
index 0000000..02f6643
--- /dev/null
+++ b/src/plugins/acl/acl-global-file.c
@@ -0,0 +1,246 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "strescape.h"
+#include "wildcard-match.h"
+#include "acl-api-private.h"
+#include "acl-global-file.h"
+
+#include <sys/stat.h>
+
+struct acl_global_rights {
+ const char *vpattern;
+ ARRAY_TYPE(acl_rights) rights;
+};
+
+struct acl_global_parse_rights {
+ const char *vpattern;
+ struct acl_rights rights;
+};
+
+struct acl_global_file {
+ char *path;
+ struct stat prev_st;
+ time_t last_refresh_time;
+
+ pool_t rights_pool;
+ ARRAY(struct acl_global_rights) rights;
+
+ unsigned int refresh_interval_secs;
+ bool debug;
+};
+
+struct acl_global_file *
+acl_global_file_init(const char *path, unsigned int refresh_interval_secs,
+ bool debug)
+{
+ struct acl_global_file *file;
+
+ file = i_new(struct acl_global_file, 1);
+ file->path = i_strdup(path);
+ file->refresh_interval_secs = refresh_interval_secs;
+ file->debug = debug;
+ i_array_init(&file->rights, 32);
+ file->rights_pool = pool_alloconly_create("acl global file rights", 1024);
+ return file;
+}
+
+void acl_global_file_deinit(struct acl_global_file **_file)
+{
+ struct acl_global_file *file = *_file;
+
+ *_file = NULL;
+
+ array_free(&file->rights);
+ pool_unref(&file->rights_pool);
+ i_free(file->path);
+ i_free(file);
+}
+
+static int acl_global_parse_rights_cmp(const struct acl_global_parse_rights *r1,
+ const struct acl_global_parse_rights *r2)
+{
+ return strcmp(r1->vpattern, r2->vpattern);
+}
+
+struct acl_global_file_parse_ctx {
+ struct acl_global_file *file;
+ ARRAY(struct acl_global_parse_rights) parse_rights;
+};
+
+static int
+acl_global_file_parse_line(struct acl_global_file_parse_ctx *ctx,
+ const char *line, const char **error_r)
+{
+ struct acl_global_parse_rights *pright;
+ const char *p, *vpattern;
+
+ if (*line == '"') {
+ line++;
+ if (str_unescape_next(&line, &vpattern) < 0) {
+ *error_r = "Missing '\"'";
+ return -1;
+ }
+ if (line[0] != ' ') {
+ *error_r = "Expecting space after '\"'";
+ return -1;
+ }
+ line++;
+ } else {
+ p = strchr(line, ' ');
+ if (p == NULL) {
+ *error_r = "Missing ACL rights";
+ return -1;
+ }
+ if (p == line) {
+ *error_r = "Empty ACL pattern";
+ return -1;
+ }
+ vpattern = t_strdup_until(line, p);
+ line = p + 1;
+ }
+
+ pright = array_append_space(&ctx->parse_rights);
+ pright->vpattern = p_strdup(ctx->file->rights_pool, vpattern);
+ if (acl_rights_parse_line(line, ctx->file->rights_pool,
+ &pright->rights, error_r) < 0)
+ return -1;
+ pright->rights.global = TRUE;
+ return 0;
+}
+
+static int acl_global_file_read(struct acl_global_file *file)
+{
+ struct acl_global_file_parse_ctx ctx;
+ struct acl_global_parse_rights *pright;
+ struct acl_global_rights *right;
+ struct istream *input;
+ const char *line, *error, *prev_vpattern;
+ unsigned int linenum = 0;
+ int ret = 0;
+
+ array_clear(&file->rights);
+ p_clear(file->rights_pool);
+
+ i_zero(&ctx);
+ ctx.file = file;
+ i_array_init(&ctx.parse_rights, 32);
+
+ input = i_stream_create_file(file->path, SIZE_MAX);
+ i_stream_set_return_partial_line(input, TRUE);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ linenum++;
+ if (line[0] == '\0' || line[0] == '#')
+ continue;
+ T_BEGIN {
+ ret = acl_global_file_parse_line(&ctx, line, &error);
+ if (ret < 0) {
+ i_error("Global ACL file %s line %u: %s",
+ file->path, linenum, error);
+ }
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ if (ret == 0 && input->stream_errno != 0) {
+ i_error("Couldn't read global ACL file %s: %s",
+ file->path, i_stream_get_error(input));
+ ret = -1;
+ }
+ if (ret == 0) {
+ const struct stat *st;
+
+ if (i_stream_stat(input, TRUE, &st) < 0) {
+ i_error("Couldn't stat global ACL file %s: %s",
+ file->path, i_stream_get_error(input));
+ ret = -1;
+ } else {
+ file->prev_st = *st;
+ }
+ }
+ i_stream_destroy(&input);
+
+ /* sort all parsed rights */
+ array_sort(&ctx.parse_rights, acl_global_parse_rights_cmp);
+ /* combine identical patterns into same structs */
+ prev_vpattern = ""; right = NULL;
+ array_foreach_modifiable(&ctx.parse_rights, pright) {
+ if (right == NULL ||
+ strcmp(prev_vpattern, pright->vpattern) != 0) {
+ right = array_append_space(&file->rights);
+ right->vpattern = pright->vpattern;
+ p_array_init(&right->rights, file->rights_pool, 4);
+ }
+ array_push_back(&right->rights, &pright->rights);
+ }
+
+ array_free(&ctx.parse_rights);
+ return ret;
+}
+
+int acl_global_file_refresh(struct acl_global_file *file)
+{
+ struct stat st;
+
+ if (file->last_refresh_time + (time_t)file->refresh_interval_secs > ioloop_time)
+ return 0;
+ if (file->last_refresh_time != 0) {
+ if (stat(file->path, &st) < 0) {
+ i_error("stat(%s) failed: %m", file->path);
+ return -1;
+ }
+ if (st.st_ino == file->prev_st.st_ino &&
+ st.st_size == file->prev_st.st_size &&
+ CMP_ST_MTIME(&st, &file->prev_st)) {
+ /* no change to the file */
+ file->last_refresh_time = ioloop_time;
+ return 0;
+ }
+ }
+ if (acl_global_file_read(file) < 0)
+ return -1;
+ file->last_refresh_time = ioloop_time;
+ return 0;
+}
+
+void acl_global_file_last_stat(struct acl_global_file *file, struct stat *st_r)
+{
+ *st_r = file->prev_st;
+}
+
+void acl_global_file_get(struct acl_global_file *file, const char *vname,
+ pool_t pool, ARRAY_TYPE(acl_rights) *rights_r)
+{
+ struct acl_global_rights *global_rights;
+ const struct acl_rights *rights;
+ struct acl_rights *new_rights;
+
+ array_foreach_modifiable(&file->rights, global_rights) {
+ if (!wildcard_match(vname, global_rights->vpattern))
+ continue;
+ if (file->debug) {
+ i_debug("Mailbox '%s' matches global ACL pattern '%s'",
+ vname, global_rights->vpattern);
+ }
+ array_foreach(&global_rights->rights, rights) {
+ new_rights = array_append_space(rights_r);
+ acl_rights_dup(rights, pool, new_rights);
+ }
+ }
+}
+
+bool acl_global_file_have_any(struct acl_global_file *file, const char *vname)
+{
+ struct acl_global_rights *rights;
+
+ i_assert(file->last_refresh_time != 0);
+
+ array_foreach_modifiable(&file->rights, rights) {
+ if (wildcard_match(vname, rights->vpattern))
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/plugins/acl/acl-global-file.h b/src/plugins/acl/acl-global-file.h
new file mode 100644
index 0000000..d393dec
--- /dev/null
+++ b/src/plugins/acl/acl-global-file.h
@@ -0,0 +1,23 @@
+#ifndef ACL_GLOBAL_FILE_H
+#define ACL_GLOBAL_FILE_H
+
+#include "acl-api.h"
+
+struct acl_global_file *
+acl_global_file_init(const char *path, unsigned int refresh_interval_secs,
+ bool debug);
+void acl_global_file_deinit(struct acl_global_file **file);
+
+/* Read the global ACLs into memory. */
+int acl_global_file_refresh(struct acl_global_file *file);
+/* Return stat data for the last refresh. */
+void acl_global_file_last_stat(struct acl_global_file *file, struct stat *st_r);
+
+/* Return global ACL rights matching the mailbox name. The file must already
+ have been refreshed at least once. */
+void acl_global_file_get(struct acl_global_file *file, const char *vname,
+ pool_t pool, ARRAY_TYPE(acl_rights) *rights_r);
+/* Returns TRUE if there are any global ACLs matching the mailbox name. */
+bool acl_global_file_have_any(struct acl_global_file *file, const char *vname);
+
+#endif
diff --git a/src/plugins/acl/acl-lookup-dict.c b/src/plugins/acl/acl-lookup-dict.c
new file mode 100644
index 0000000..638a767
--- /dev/null
+++ b/src/plugins/acl/acl-lookup-dict.c
@@ -0,0 +1,373 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "dict.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "acl-api-private.h"
+#include "acl-storage.h"
+#include "acl-plugin.h"
+#include "acl-lookup-dict.h"
+
+
+#define DICT_SHARED_BOXES_PATH "shared-boxes/"
+
+struct acl_lookup_dict {
+ struct mail_user *user;
+ struct dict *dict;
+};
+
+struct acl_lookup_dict_iter {
+ pool_t pool;
+ struct acl_lookup_dict *dict;
+
+ pool_t iter_value_pool;
+ ARRAY_TYPE(const_string) iter_ids;
+ ARRAY_TYPE(const_string) iter_values;
+ unsigned int iter_idx, iter_value_idx;
+
+ bool failed:1;
+};
+
+struct acl_lookup_dict *acl_lookup_dict_init(struct mail_user *user)
+{
+ struct acl_lookup_dict *dict;
+ const char *uri, *error;
+
+ dict = i_new(struct acl_lookup_dict, 1);
+ dict->user = user;
+
+ uri = mail_user_plugin_getenv(user, "acl_shared_dict");
+ if (uri != NULL) {
+ struct dict_settings dict_set;
+
+ i_zero(&dict_set);
+ dict_set.base_dir = user->set->base_dir;
+ dict_set.event_parent = user->event;
+ if (dict_init(uri, &dict_set, &dict->dict, &error) < 0)
+ i_error("acl: dict_init(%s) failed: %s", uri, error);
+ } else {
+ e_debug(user->event, "acl: No acl_shared_dict setting - "
+ "shared mailbox listing is disabled");
+ }
+ return dict;
+}
+
+void acl_lookup_dict_deinit(struct acl_lookup_dict **_dict)
+{
+ struct acl_lookup_dict *dict = *_dict;
+
+ *_dict = NULL;
+ if (dict->dict != NULL)
+ dict_deinit(&dict->dict);
+ i_free(dict);
+}
+
+bool acl_lookup_dict_is_enabled(struct acl_lookup_dict *dict)
+{
+ return dict->dict != NULL;
+}
+
+static void
+acl_lookup_dict_write_rights_id(string_t *dest, const struct acl_rights *right)
+{
+ switch (right->id_type) {
+ case ACL_ID_ANYONE:
+ case ACL_ID_AUTHENTICATED:
+ /* don't bother separating these */
+ str_append(dest, "anyone");
+ break;
+ case ACL_ID_USER:
+ str_append(dest, "user/");
+ str_append(dest, right->identifier);
+ break;
+ case ACL_ID_GROUP:
+ case ACL_ID_GROUP_OVERRIDE:
+ str_append(dest, "group/");
+ str_append(dest, right->identifier);
+ break;
+ case ACL_ID_OWNER:
+ case ACL_ID_TYPE_COUNT:
+ i_unreached();
+ }
+}
+
+static bool
+acl_rights_is_same_user(const struct acl_rights *right, struct mail_user *user)
+{
+ return right->id_type == ACL_ID_USER &&
+ strcmp(right->identifier, user->username) == 0;
+}
+
+static int acl_lookup_dict_rebuild_add_backend(struct mail_namespace *ns,
+ ARRAY_TYPE(const_string) *ids)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(ns->list);
+ struct acl_backend *backend;
+ struct acl_mailbox_list_context *ctx;
+ struct acl_object *aclobj;
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ const char *name, *id_dup;
+ string_t *id;
+ int ret = 0;
+
+ if ((ns->flags & NAMESPACE_FLAG_NOACL) != 0 || ns->owner == NULL ||
+ alist == NULL || alist->ignore_acls)
+ return 0;
+
+ id = t_str_new(128);
+ backend = acl_mailbox_list_get_backend(ns->list);
+ ctx = acl_backend_nonowner_lookups_iter_init(backend);
+ while (acl_backend_nonowner_lookups_iter_next(ctx, &name)) {
+ aclobj = acl_object_init_from_name(backend, name);
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ /* avoid pointless user -> user entries,
+ which some clients do */
+ if (acl_rights_has_nonowner_lookup_changes(&rights) &&
+ !acl_rights_is_same_user(&rights, ns->owner)) {
+ str_truncate(id, 0);
+ acl_lookup_dict_write_rights_id(id, &rights);
+ str_append_c(id, '/');
+ str_append(id, ns->owner->username);
+ id_dup = t_strdup(str_c(id));
+ array_push_back(ids, &id_dup);
+ }
+ }
+ if (acl_object_list_deinit(&iter) < 0) ret = -1;
+ acl_object_deinit(&aclobj);
+ }
+ if (acl_backend_nonowner_lookups_iter_deinit(&ctx) < 0) ret = -1;
+ return ret;
+}
+
+static int
+acl_lookup_dict_rebuild_update(struct acl_lookup_dict *dict,
+ const ARRAY_TYPE(const_string) *new_ids_arr,
+ bool no_removes)
+{
+ const char *username = dict->user->username;
+ struct dict_iterate_context *iter;
+ struct dict_transaction_context *dt = NULL;
+ const char *prefix, *key, *value, *const *old_ids, *const *new_ids, *p;
+ const char *error;
+ ARRAY_TYPE(const_string) old_ids_arr;
+ unsigned int newi, oldi, old_count, new_count;
+ string_t *path;
+ size_t prefix_len;
+ int ret;
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(dict->user);
+
+ /* get all existing identifiers for the user. we might be able to
+ sync identifiers also for other users whose shared namespaces we
+ have, but it's possible that the other users have other namespaces
+ that aren't visible to us, so we don't want to remove anything
+ that could break them. */
+ t_array_init(&old_ids_arr, 128);
+ prefix = DICT_PATH_SHARED DICT_SHARED_BOXES_PATH;
+ prefix_len = strlen(prefix);
+ iter = dict_iterate_init(dict->dict, set, prefix, DICT_ITERATE_FLAG_RECURSE);
+ while (dict_iterate(iter, &key, &value)) {
+ /* prefix/$type/$dest/$source */
+ key += prefix_len;
+ p = strrchr(key, '/');
+ if (p != NULL && strcmp(p + 1, username) == 0) {
+ key = t_strdup_until(key, p);
+ array_push_back(&old_ids_arr, &key);
+ }
+ }
+ if (dict_iterate_deinit(&iter, &error) < 0) {
+ i_error("acl: dict iteration failed: %s - can't update dict", error);
+ return -1;
+ }
+
+ /* sort the existing identifiers */
+ array_sort(&old_ids_arr, i_strcmp_p);
+
+ /* sync the identifiers */
+ path = t_str_new(256);
+ str_append(path, prefix);
+
+ old_ids = array_get(&old_ids_arr, &old_count);
+ new_ids = array_get(new_ids_arr, &new_count);
+ for (newi = oldi = 0; newi < new_count || oldi < old_count; ) {
+ ret = newi == new_count ? 1 :
+ oldi == old_count ? -1 :
+ strcmp(new_ids[newi], old_ids[oldi]);
+ if (ret == 0) {
+ newi++; oldi++;
+ } else if (ret < 0) {
+ /* new identifier, add it */
+ str_truncate(path, prefix_len);
+ str_append(path, new_ids[newi]);
+ dt = dict_transaction_begin(dict->dict, set);
+ dict_set(dt, str_c(path), "1");
+ newi++;
+ } else if (!no_removes) {
+ /* old identifier removed */
+ str_truncate(path, prefix_len);
+ str_append(path, old_ids[oldi]);
+ str_append_c(path, '/');
+ str_append(path, username);
+ dt = dict_transaction_begin(dict->dict, set);
+ dict_unset(dt, str_c(path));
+ oldi++;
+ }
+ if (dt != NULL && dict_transaction_commit(&dt, &error) < 0) {
+ i_error("acl: dict commit failed: %s", error);
+ return -1;
+ }
+ i_assert(dt == NULL);
+ }
+ return 0;
+}
+
+int acl_lookup_dict_rebuild(struct acl_lookup_dict *dict)
+{
+ struct mail_namespace *ns;
+ ARRAY_TYPE(const_string) ids_arr;
+ const char **ids;
+ unsigned int i, dest, count;
+ int ret = 0;
+
+ if (dict->dict == NULL)
+ return 0;
+
+ /* get all ACL identifiers with a positive lookup right */
+ t_array_init(&ids_arr, 128);
+ for (ns = dict->user->namespaces; ns != NULL; ns = ns->next) {
+ if (acl_lookup_dict_rebuild_add_backend(ns, &ids_arr) < 0)
+ ret = -1;
+ }
+
+ /* sort identifiers and remove duplicates */
+ array_sort(&ids_arr, i_strcmp_p);
+
+ ids = array_get_modifiable(&ids_arr, &count);
+ for (i = 1, dest = 0; i < count; i++) {
+ if (strcmp(ids[dest], ids[i]) != 0) {
+ if (++dest != i)
+ ids[dest] = ids[i];
+ }
+ }
+ if (++dest < count)
+ array_delete(&ids_arr, dest, count-dest);
+
+ /* if lookup failed at some point we can still add new ids,
+ but we can't remove any existing ones */
+ if (acl_lookup_dict_rebuild_update(dict, &ids_arr, ret < 0) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void acl_lookup_dict_iterate_read(struct acl_lookup_dict_iter *iter)
+{
+ struct dict_iterate_context *dict_iter;
+ const char *id, *prefix, *key, *value, *error;
+ size_t prefix_len;
+
+ id = array_idx_elem(&iter->iter_ids, iter->iter_idx);
+ iter->iter_idx++;
+ iter->iter_value_idx = 0;
+
+ prefix = t_strconcat(DICT_PATH_SHARED DICT_SHARED_BOXES_PATH,
+ id, "/", NULL);
+ prefix_len = strlen(prefix);
+
+ /* read all of it to memory. at least currently dict-proxy can support
+ only one iteration at a time, but the acl code can end up rebuilding
+ the dict, which opens another iteration. */
+ p_clear(iter->iter_value_pool);
+ array_clear(&iter->iter_values);
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(iter->dict->user);
+ dict_iter = dict_iterate_init(iter->dict->dict, set, prefix,
+ DICT_ITERATE_FLAG_RECURSE);
+ while (dict_iterate(dict_iter, &key, &value)) {
+ i_assert(prefix_len < strlen(key));
+
+ key = p_strdup(iter->iter_value_pool, key + prefix_len);
+ array_push_back(&iter->iter_values, &key);
+ }
+ if (dict_iterate_deinit(&dict_iter, &error) < 0) {
+ i_error("%s", error);
+ iter->failed = TRUE;
+ }
+}
+
+struct acl_lookup_dict_iter *
+acl_lookup_dict_iterate_visible_init(struct acl_lookup_dict *dict)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(dict->user);
+ struct acl_lookup_dict_iter *iter;
+ const char *id;
+ unsigned int i;
+ pool_t pool;
+
+ i_assert(auser != NULL);
+
+ pool = pool_alloconly_create("acl lookup dict iter", 1024);
+ iter = p_new(pool, struct acl_lookup_dict_iter, 1);
+ iter->pool = pool;
+ iter->dict = dict;
+
+ p_array_init(&iter->iter_ids, pool, 16);
+ id = "anyone";
+ array_push_back(&iter->iter_ids, &id);
+ id = p_strconcat(pool, "user/", dict->user->username, NULL);
+ array_push_back(&iter->iter_ids, &id);
+
+ i_array_init(&iter->iter_values, 64);
+ iter->iter_value_pool =
+ pool_alloconly_create("acl lookup dict iter values", 1024);
+
+ /* get all groups we belong to */
+ if (auser->groups != NULL) {
+ for (i = 0; auser->groups[i] != NULL; i++) {
+ id = p_strconcat(pool, "group/", auser->groups[i],
+ NULL);
+ array_push_back(&iter->iter_ids, &id);
+ }
+ }
+
+ /* iterate through all identifiers that match us, start with the
+ first one */
+ if (dict->dict != NULL)
+ acl_lookup_dict_iterate_read(iter);
+ else
+ array_clear(&iter->iter_ids);
+ return iter;
+}
+
+const char *
+acl_lookup_dict_iterate_visible_next(struct acl_lookup_dict_iter *iter)
+{
+ const char *const *keys;
+ unsigned int count;
+
+ keys = array_get(&iter->iter_values, &count);
+ if (iter->iter_value_idx < count)
+ return keys[iter->iter_value_idx++];
+
+ if (iter->iter_idx < array_count(&iter->iter_ids)) {
+ /* get to the next iterator */
+ acl_lookup_dict_iterate_read(iter);
+ return acl_lookup_dict_iterate_visible_next(iter);
+ }
+ return NULL;
+}
+
+int acl_lookup_dict_iterate_visible_deinit(struct acl_lookup_dict_iter **_iter)
+{
+ struct acl_lookup_dict_iter *iter = *_iter;
+ int ret = iter->failed ? -1 : 0;
+
+ *_iter = NULL;
+ array_free(&iter->iter_values);
+ pool_unref(&iter->iter_value_pool);
+ pool_unref(&iter->pool);
+ return ret;
+}
diff --git a/src/plugins/acl/acl-lookup-dict.h b/src/plugins/acl/acl-lookup-dict.h
new file mode 100644
index 0000000..856aaf4
--- /dev/null
+++ b/src/plugins/acl/acl-lookup-dict.h
@@ -0,0 +1,17 @@
+#ifndef ACL_LOOKUP_DICT_H
+#define ACL_LOOKUP_DICT_H
+
+struct acl_lookup_dict *acl_lookup_dict_init(struct mail_user *user);
+void acl_lookup_dict_deinit(struct acl_lookup_dict **dict);
+
+bool acl_lookup_dict_is_enabled(struct acl_lookup_dict *dict);
+
+int acl_lookup_dict_rebuild(struct acl_lookup_dict *dict);
+
+struct acl_lookup_dict_iter *
+acl_lookup_dict_iterate_visible_init(struct acl_lookup_dict *dict);
+const char *
+acl_lookup_dict_iterate_visible_next(struct acl_lookup_dict_iter *iter);
+int acl_lookup_dict_iterate_visible_deinit(struct acl_lookup_dict_iter **iter);
+
+#endif
diff --git a/src/plugins/acl/acl-mailbox-list.c b/src/plugins/acl/acl-mailbox-list.c
new file mode 100644
index 0000000..de0b00c
--- /dev/null
+++ b/src/plugins/acl/acl-mailbox-list.c
@@ -0,0 +1,633 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "imap-match.h"
+#include "wildcard-match.h"
+#include "mailbox-tree.h"
+#include "mail-namespace.h"
+#include "mailbox-list-iter-private.h"
+#include "acl-api-private.h"
+#include "acl-cache.h"
+#include "acl-shared-storage.h"
+#include "acl-plugin.h"
+
+#define MAILBOX_FLAG_MATCHED 0x40000000
+
+struct acl_mailbox_list_iterate_context {
+ union mailbox_list_iterate_module_context module_ctx;
+
+ struct mailbox_tree_context *lookup_boxes;
+ struct mailbox_info info;
+
+ char sep;
+ bool hide_nonlistable_subscriptions:1;
+ bool simple_star_glob:1;
+ bool autocreate_acls_checked:1;
+};
+
+static const char *acl_storage_right_names[ACL_STORAGE_RIGHT_COUNT] = {
+ MAIL_ACL_LOOKUP,
+ MAIL_ACL_READ,
+ MAIL_ACL_WRITE,
+ MAIL_ACL_WRITE_SEEN,
+ MAIL_ACL_WRITE_DELETED,
+ MAIL_ACL_INSERT,
+ MAIL_ACL_POST,
+ MAIL_ACL_EXPUNGE,
+ MAIL_ACL_CREATE,
+ MAIL_ACL_DELETE,
+ MAIL_ACL_ADMIN
+};
+
+#define ACL_LIST_ITERATE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, acl_mailbox_list_module)
+
+struct acl_mailbox_list_module acl_mailbox_list_module =
+ MODULE_CONTEXT_INIT(&mailbox_list_module_register);
+
+struct acl_backend *acl_mailbox_list_get_backend(struct mailbox_list *list)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(list);
+
+ return alist->rights.backend;
+}
+
+int acl_mailbox_list_have_right(struct mailbox_list *list, const char *name,
+ bool parent, unsigned int acl_storage_right_idx,
+ bool *can_see_r)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(list);
+ struct acl_backend *backend = alist->rights.backend;
+ const unsigned int *idx_arr = alist->rights.acl_storage_right_idx;
+ struct acl_object *aclobj;
+ int ret, ret2;
+
+ if (alist->ignore_acls)
+ return 1;
+
+ aclobj = !parent ?
+ acl_object_init_from_name(backend, name) :
+ acl_object_init_from_parent(backend, name);
+ ret = acl_object_have_right(aclobj, idx_arr[acl_storage_right_idx]);
+
+ if (can_see_r != NULL) {
+ ret2 = acl_object_have_right(aclobj,
+ idx_arr[ACL_STORAGE_RIGHT_LOOKUP]);
+ if (ret2 < 0)
+ ret = -1;
+ *can_see_r = ret2 > 0;
+ }
+ acl_object_deinit(&aclobj);
+
+ if (ret < 0)
+ mailbox_list_set_internal_error(list);
+ return ret;
+}
+
+static void
+acl_mailbox_try_list_fast(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(_ctx->list);
+ struct acl_backend *backend = alist->rights.backend;
+ const unsigned int *idxp;
+ const struct acl_mask *acl_mask;
+ struct acl_mailbox_list_context *nonowner_list_ctx;
+ struct mail_namespace *ns = _ctx->list->ns;
+ struct mailbox_list_iter_update_context update_ctx;
+ const char *name;
+
+ if ((_ctx->flags & (MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_SELECT_SUBSCRIBED)) != 0)
+ return;
+
+ if (ns->type == MAIL_NAMESPACE_TYPE_PUBLIC) {
+ /* mailboxes in public namespace should all be listable to
+ someone. we don't benefit from fast listing. */
+ return;
+ }
+
+ /* If ACLs are ignored for this namespace don't try fast listing. */
+ if (alist->ignore_acls)
+ return;
+
+ /* if this namespace's default rights contain LOOKUP, we'll need to
+ go through all mailboxes in any case. */
+ idxp = alist->rights.acl_storage_right_idx + ACL_STORAGE_RIGHT_LOOKUP;
+ if (acl_backend_get_default_rights(backend, &acl_mask) < 0 ||
+ acl_cache_mask_isset(acl_mask, *idxp))
+ return;
+
+ /* no LOOKUP right by default, we can optimize this */
+ i_zero(&update_ctx);
+ update_ctx.iter_ctx = _ctx;
+ update_ctx.glob =
+ imap_match_init(pool_datastack_create(), "*",
+ (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0,
+ ctx->sep);
+ update_ctx.match_parents = TRUE;
+ update_ctx.tree_ctx = mailbox_tree_init(ctx->sep);
+
+ nonowner_list_ctx = acl_backend_nonowner_lookups_iter_init(backend);
+ while (acl_backend_nonowner_lookups_iter_next(nonowner_list_ctx,
+ &name)) {
+ T_BEGIN {
+ const char *vname =
+ mailbox_list_get_vname(ns->list, name);
+ mailbox_list_iter_update(&update_ctx, vname);
+ } T_END;
+ }
+
+ if (acl_backend_nonowner_lookups_iter_deinit(&nonowner_list_ctx) >= 0)
+ ctx->lookup_boxes = update_ctx.tree_ctx;
+ else
+ mailbox_tree_deinit(&update_ctx.tree_ctx);
+}
+
+static struct mailbox_list_iterate_context *
+acl_mailbox_list_iter_init_shared(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_iterate_context *ctx;
+ int ret;
+
+ /* before listing anything add namespaces for all users
+ who may have visible mailboxes */
+ ret = acl_shared_namespaces_add(list->ns);
+
+ ctx = alist->module_ctx.super.iter_init(list, patterns, flags);
+ if (ret < 0)
+ ctx->failed = TRUE;
+ return ctx;
+}
+
+static struct mailbox_list_iterate_context *
+acl_mailbox_list_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_iterate_context *_ctx;
+ struct acl_mailbox_list_iterate_context *ctx;
+ const char *p;
+ unsigned int i;
+
+ _ctx = alist->module_ctx.super.iter_init(list, patterns, flags);
+
+ ctx = p_new(_ctx->pool, struct acl_mailbox_list_iterate_context, 1);
+
+ if (list->ns->type != MAIL_NAMESPACE_TYPE_PRIVATE &&
+ (list->ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) != 0) {
+ /* non-private namespace with subscriptions=yes. this could be
+ a site-global subscriptions file, so hide subscriptions for
+ mailboxes the user doesn't see. */
+ ctx->hide_nonlistable_subscriptions = TRUE;
+ }
+
+ ctx->sep = mail_namespace_get_sep(list->ns);
+ /* see if all patterns have only a single '*' and it's at the end.
+ we can use it to do some optimizations. */
+ ctx->simple_star_glob = TRUE;
+ for (i = 0; patterns[i] != NULL; i++) {
+ p = strchr(patterns[i], '*');
+ if (p == NULL || p[1] != '\0') {
+ ctx->simple_star_glob = FALSE;
+ break;
+ }
+ }
+
+ MODULE_CONTEXT_SET(_ctx, acl_mailbox_list_module, ctx);
+
+ /* Try to avoid reading ACLs from all mailboxes by getting a smaller
+ list of mailboxes that have even potential to be visible. If we
+ couldn't get such a list, we'll go through all mailboxes. */
+ T_BEGIN {
+ acl_mailbox_try_list_fast(_ctx);
+ } T_END;
+
+ return _ctx;
+}
+
+static const struct mailbox_info *
+acl_mailbox_list_iter_next_info(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(_ctx->list);
+ const struct mailbox_info *info;
+
+ for (;;) {
+ /* Normally the data stack frame is in mailbox_list_iter_next(),
+ but we're bypassing it here by calling super.iter_next()
+ directly. */
+ T_BEGIN {
+ info = alist->module_ctx.super.iter_next(_ctx);
+ } T_END;
+ if (info == NULL)
+ break;
+
+ /* if we've a list of mailboxes with LOOKUP rights, skip the
+ mailboxes not in the list (since we know they can't be
+ visible to us). */
+ if (ctx->lookup_boxes == NULL ||
+ mailbox_tree_lookup(ctx->lookup_boxes, info->vname) != NULL)
+ break;
+ e_debug(_ctx->list->ns->user->event,
+ "acl: Mailbox not in dovecot-acl-list: %s", info->vname);
+ }
+
+ return info;
+}
+
+static const char *
+acl_mailbox_list_iter_get_name(struct mailbox_list_iterate_context *ctx,
+ const char *vname)
+{
+ struct mail_namespace *ns = ctx->list->ns;
+ const char *name;
+ size_t len;
+
+ name = mailbox_list_get_storage_name(ns->list, vname);
+ len = strlen(name);
+ if (len > 0 && name[len-1] == mailbox_list_get_hierarchy_sep(ns->list)) {
+ /* name ends with separator. this can happen if doing e.g.
+ LIST "" foo/% and it lists "foo/". */
+ name = t_strndup(name, len-1);
+ }
+ return name;
+}
+
+static bool
+iter_is_listing_all_children(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ const char *child;
+
+ /* If all patterns (with '.' separator) are in "name*", "name.*" or
+ "%.*" style format, simple_star_glob=TRUE and we can easily test
+ this by simply checking if name/child mailbox matches. */
+ child = t_strdup_printf("%s%cx", ctx->info.vname, ctx->sep);
+ return ctx->simple_star_glob &&
+ imap_match(_ctx->glob, child) == IMAP_MATCH_YES;
+}
+
+static bool
+iter_mailbox_has_visible_children(struct mailbox_list_iterate_context *_ctx,
+ bool only_nonpatterns, bool subscribed)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ string_t *pattern;
+ size_t i, prefix_len;
+ bool wildcards = FALSE, ret = FALSE;
+
+ /* do we have child mailboxes with LOOKUP right that don't match
+ the list pattern? */
+ if (ctx->lookup_boxes != NULL) {
+ /* we have a list of mailboxes with LOOKUP rights. before
+ starting the slow list iteration, check check first
+ if there even are any children with LOOKUP rights. */
+ struct mailbox_node *node;
+
+ node = mailbox_tree_lookup(ctx->lookup_boxes, ctx->info.vname);
+ i_assert(node != NULL);
+ if (node->children == NULL)
+ return FALSE;
+ }
+
+ /* if mailbox name has '*' characters in it, they'll conflict with the
+ LIST wildcard. replace then with '%' and verify later that all
+ results have the correct prefix. */
+ pattern = t_str_new(128);
+ for (i = 0; ctx->info.vname[i] != '\0'; i++) {
+ if (ctx->info.vname[i] != '*' && ctx->info.vname[i] != '%')
+ str_append_c(pattern, ctx->info.vname[i]);
+ else {
+ wildcards = TRUE;
+ str_append_c(pattern, '%');
+ }
+ }
+ if (i > 0 && ctx->info.vname[i-1] != ctx->sep)
+ str_append_c(pattern, ctx->sep);
+ str_append_c(pattern, '*');
+ prefix_len = str_len(pattern) - 1;
+ if (prefix_len == 0) {
+ /* empty mailbox names shouldn't be possible */
+ return FALSE;
+ }
+
+ iter = mailbox_list_iter_init(_ctx->list, str_c(pattern),
+ (!subscribed ? 0 :
+ MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if (only_nonpatterns &&
+ imap_match(_ctx->glob, info->vname) == IMAP_MATCH_YES) {
+ /* at least one child matches also the original list
+ patterns. we don't need to show this mailbox. */
+ ret = FALSE;
+ break;
+ }
+ if (!wildcards ||
+ (strncmp(info->vname, ctx->info.vname, prefix_len-1) == 0 &&
+ info->vname[prefix_len-1] == ctx->sep))
+ ret = TRUE;
+ }
+ (void)mailbox_list_iter_deinit(&iter);
+ return ret;
+}
+
+static int
+acl_mailbox_list_info_is_visible(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+#define PRESERVE_MAILBOX_FLAGS (MAILBOX_SUBSCRIBED | MAILBOX_CHILD_SUBSCRIBED)
+ struct mailbox_info *info = &ctx->info;
+ const char *acl_name;
+ int ret;
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_RAW_LIST) != 0) {
+ /* skip ACL checks. */
+ return 1;
+ }
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0 &&
+ (_ctx->flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) != 0 &&
+ !ctx->hide_nonlistable_subscriptions) {
+ /* don't waste time doing an ACL check. we're going to list
+ all subscriptions anyway. */
+ info->flags &= MAILBOX_SUBSCRIBED | MAILBOX_CHILD_SUBSCRIBED;
+ return 1;
+ }
+
+ acl_name = acl_mailbox_list_iter_get_name(_ctx, info->vname);
+ ret = acl_mailbox_list_have_right(_ctx->list, acl_name, FALSE,
+ ACL_STORAGE_RIGHT_LOOKUP,
+ NULL);
+ if (ret != 0) {
+ if ((_ctx->flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) != 0) {
+ /* don't waste time checking if there are visible
+ children, but also don't return incorrect flags */
+ info->flags &= ENUM_NEGATE(MAILBOX_CHILDREN);
+ } else if ((info->flags & MAILBOX_CHILDREN) != 0 &&
+ !iter_mailbox_has_visible_children(_ctx, FALSE, FALSE)) {
+ info->flags &= ENUM_NEGATE(MAILBOX_CHILDREN);
+ info->flags |= MAILBOX_NOCHILDREN;
+ }
+ return ret;
+ }
+
+ /* no permission to see this mailbox */
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* we're listing subscribed mailboxes. this one or its child
+ is subscribed, so we'll need to list it. but since we don't
+ have LOOKUP right, we'll need to show it as nonexistent. */
+ i_assert((info->flags & PRESERVE_MAILBOX_FLAGS) != 0);
+ info->flags = MAILBOX_NONEXISTENT |
+ (info->flags & PRESERVE_MAILBOX_FLAGS);
+ if (ctx->hide_nonlistable_subscriptions) {
+ /* global subscriptions file. hide this entry if there
+ are no visible subscribed children or if we're going
+ to list the subscribed children anyway. */
+ if ((info->flags & MAILBOX_CHILD_SUBSCRIBED) == 0)
+ return 0;
+ if (iter_is_listing_all_children(_ctx) ||
+ !iter_mailbox_has_visible_children(_ctx, TRUE, TRUE))
+ return 0;
+ /* e.g. LSUB "" % with visible subscribed children */
+ }
+ return 1;
+ }
+
+ if (!iter_is_listing_all_children(_ctx) &&
+ iter_mailbox_has_visible_children(_ctx, TRUE, FALSE)) {
+ /* no child mailboxes match the list pattern(s), but mailbox
+ has visible children. we'll need to show this as
+ non-existent. */
+ info->flags = MAILBOX_NONEXISTENT | MAILBOX_CHILDREN |
+ (info->flags & PRESERVE_MAILBOX_FLAGS);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+acl_mailbox_list_iter_check_autocreate_acls(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ struct mailbox_settings *const *box_sets;
+ unsigned int i, count;
+ int ret;
+
+ ctx->autocreate_acls_checked = TRUE;
+ if (_ctx->autocreate_ctx == NULL)
+ return 0;
+ if ((_ctx->flags & MAILBOX_LIST_ITER_RAW_LIST) != 0) {
+ /* skip ACL checks. */
+ return 0;
+ }
+
+ box_sets = array_get(&_ctx->autocreate_ctx->box_sets, &count);
+ i_assert(array_count(&_ctx->autocreate_ctx->boxes) == count);
+
+ for (i = 0; i < count; ) {
+ const char *acl_name =
+ acl_mailbox_list_iter_get_name(_ctx, box_sets[i]->name);
+ ret = acl_mailbox_list_have_right(_ctx->list, acl_name, FALSE,
+ ACL_STORAGE_RIGHT_LOOKUP,
+ NULL);
+ if (ret < 0)
+ return -1;
+ if (ret > 0)
+ i++;
+ else {
+ /* no list right - remove the whole autobox */
+ array_delete(&_ctx->autocreate_ctx->box_sets, i, 1);
+ array_delete(&_ctx->autocreate_ctx->boxes, i, 1);
+ box_sets = array_get(&_ctx->autocreate_ctx->box_sets, &count);
+ }
+ }
+ return 0;
+}
+
+static const struct mailbox_info *
+acl_mailbox_list_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ const struct mailbox_info *info;
+ int ret;
+
+ if (!ctx->autocreate_acls_checked) {
+ if (acl_mailbox_list_iter_check_autocreate_acls(_ctx) < 0) {
+ _ctx->failed = TRUE;
+ return NULL;
+ }
+ }
+
+ while ((info = acl_mailbox_list_iter_next_info(_ctx)) != NULL) {
+ ctx->info = *info;
+ T_BEGIN {
+ ret = acl_mailbox_list_info_is_visible(_ctx);
+ } T_END;
+ if (ret > 0)
+ break;
+ if (ret < 0) {
+ _ctx->failed = TRUE;
+ return NULL;
+ }
+ /* skip to next one */
+ e_debug(_ctx->list->ns->user->event,
+ "acl: No lookup right to mailbox: %s", info->vname);
+ }
+ return info == NULL ? NULL : &ctx->info;
+}
+
+static int
+acl_mailbox_list_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct acl_mailbox_list_iterate_context *ctx =
+ ACL_LIST_ITERATE_CONTEXT(_ctx);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(_ctx->list);
+ int ret = _ctx->failed ? -1 : 0;
+
+ if (ctx->lookup_boxes != NULL)
+ mailbox_tree_deinit(&ctx->lookup_boxes);
+ if (alist->module_ctx.super.iter_deinit(_ctx) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void acl_mailbox_list_deinit(struct mailbox_list *list)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(list);
+
+ if (alist->rights.backend != NULL)
+ acl_backend_deinit(&alist->rights.backend);
+ alist->module_ctx.super.deinit(list);
+}
+
+static void acl_mailbox_list_init_shared(struct mailbox_list *list)
+{
+ struct acl_mailbox_list *alist;
+ struct mailbox_list_vfuncs *v = list->vlast;
+
+ alist = p_new(list->pool, struct acl_mailbox_list, 1);
+ alist->module_ctx.super = *v;
+ list->vlast = &alist->module_ctx.super;
+ v->deinit = acl_mailbox_list_deinit;
+ v->iter_init = acl_mailbox_list_iter_init_shared;
+
+ MODULE_CONTEXT_SET(list, acl_mailbox_list_module, alist);
+}
+
+static void acl_storage_rights_ctx_init(struct acl_storage_rights_context *ctx,
+ struct acl_backend *backend)
+{
+ unsigned int i;
+
+ ctx->backend = backend;
+ for (i = 0; i < ACL_STORAGE_RIGHT_COUNT; i++) {
+ ctx->acl_storage_right_idx[i] =
+ acl_backend_lookup_right(backend,
+ acl_storage_right_names[i]);
+ }
+}
+
+static bool acl_namespace_is_ignored(struct mailbox_list *list)
+{
+ const char *value =
+ mail_user_plugin_getenv(list->ns->user, "acl_ignore_namespace");
+ for (unsigned int i = 2; value != NULL; i++) {
+ if (wildcard_match(list->ns->prefix, value))
+ return TRUE;
+ value = mail_user_plugin_getenv(list->ns->user,
+ t_strdup_printf("acl_ignore_namespace%u", i));
+ }
+ return FALSE;
+}
+
+static void acl_mailbox_list_init_default(struct mailbox_list *list)
+{
+ struct mailbox_list_vfuncs *v = list->vlast;
+ struct acl_mailbox_list *alist;
+
+ if (list->mail_set->mail_full_filesystem_access) {
+ /* not necessarily, but safer to do this for now. */
+ i_fatal("mail_full_filesystem_access=yes is "
+ "incompatible with ACLs");
+ }
+
+ alist = p_new(list->pool, struct acl_mailbox_list, 1);
+ alist->module_ctx.super = *v;
+ list->vlast = &alist->module_ctx.super;
+ v->deinit = acl_mailbox_list_deinit;
+ v->iter_init = acl_mailbox_list_iter_init;
+ v->iter_next = acl_mailbox_list_iter_next;
+ v->iter_deinit = acl_mailbox_list_iter_deinit;
+ if (acl_namespace_is_ignored(list))
+ alist->ignore_acls = TRUE;
+
+ MODULE_CONTEXT_SET(list, acl_mailbox_list_module, alist);
+}
+
+void acl_mail_namespace_storage_added(struct mail_namespace *ns)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(ns->list);
+ struct acl_backend *backend;
+ const char *current_username, *owner_username;
+ bool owner = TRUE;
+
+ if (alist == NULL)
+ return;
+ struct acl_user *auser = ACL_USER_CONTEXT_REQUIRE(ns->user);
+
+ owner_username = ns->user->username;
+ current_username = auser->acl_user;
+ if (current_username == NULL)
+ current_username = owner_username;
+ else
+ owner = strcmp(current_username, owner_username) == 0;
+
+ /* We don't care about the username for non-private mailboxes.
+ It's used only when checking if we're the mailbox owner. We never
+ are for shared/public mailboxes. */
+ if (ns->type != MAIL_NAMESPACE_TYPE_PRIVATE)
+ owner = FALSE;
+
+ /* we need to know the storage when initializing backend */
+ backend = acl_backend_init(auser->acl_env, ns->list, current_username,
+ auser->groups, owner);
+ if (backend == NULL)
+ i_fatal("ACL backend initialization failed");
+ acl_storage_rights_ctx_init(&alist->rights, backend);
+}
+
+void acl_mailbox_list_created(struct mailbox_list *list)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(list->ns->user);
+
+ if (auser == NULL) {
+ /* ACLs disabled for this user */
+ } else if ((list->ns->flags & NAMESPACE_FLAG_NOACL) != 0) {
+ /* no ACL checks for internal namespaces (lda, shared) */
+ if (list->ns->type == MAIL_NAMESPACE_TYPE_SHARED)
+ acl_mailbox_list_init_shared(list);
+ } else if ((list->ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0) {
+ /* this namespace is empty. don't attempt to lookup ACLs,
+ because they're not going to work anyway and we could
+ crash doing it. */
+ } else {
+ acl_mailbox_list_init_default(list);
+ }
+}
diff --git a/src/plugins/acl/acl-mailbox.c b/src/plugins/acl/acl-mailbox.c
new file mode 100644
index 0000000..134be7f
--- /dev/null
+++ b/src/plugins/acl/acl-mailbox.c
@@ -0,0 +1,714 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+/* FIXME: If we don't have permission to change flags/keywords, the changes
+ should still be stored temporarily for this session. However most clients
+ don't care and it's a huge job, so I currently this isn't done. The same
+ problem actually exists when opening read-only mailboxes. */
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "mailbox-list-private.h"
+#include "acl-api-private.h"
+#include "acl-plugin.h"
+
+#include <sys/stat.h>
+
+#define ACL_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, acl_mail_module)
+
+struct acl_transaction_context {
+ union mailbox_transaction_module_context module_ctx;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(acl_mail_module, &mail_module_register);
+static struct acl_transaction_context acl_transaction_failure;
+
+struct acl_object *acl_mailbox_get_aclobj(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+
+ return abox->aclobj;
+}
+
+int acl_mailbox_right_lookup(struct mailbox *box, unsigned int right_idx)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ int ret;
+
+ if (abox->skip_acl_checks)
+ return 1;
+
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(box->list);
+
+ /* If acls are ignored for this namespace do not check if
+ there are rights. */
+ if (alist->ignore_acls)
+ return 1;
+
+ ret = acl_object_have_right(abox->aclobj,
+ alist->rights.acl_storage_right_idx[right_idx]);
+ if (ret > 0)
+ return 1;
+ if (ret < 0) {
+ mail_storage_set_internal_error(box->storage);
+ return -1;
+ }
+
+ mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ return 0;
+}
+
+static bool acl_is_readonly(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ enum acl_storage_rights save_right;
+
+ if (abox->module_ctx.super.is_readonly(box))
+ return TRUE;
+
+ save_right = (box->flags & MAILBOX_FLAG_POST_SESSION) != 0 ?
+ ACL_STORAGE_RIGHT_POST : ACL_STORAGE_RIGHT_INSERT;
+ if (acl_mailbox_right_lookup(box, save_right) > 0)
+ return FALSE;
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_EXPUNGE) > 0)
+ return FALSE;
+
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE) > 0)
+ return FALSE;
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_DELETED) > 0)
+ return FALSE;
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_SEEN) > 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+static void acl_mailbox_free(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+
+ if (abox->aclobj != NULL)
+ acl_object_deinit(&abox->aclobj);
+ abox->module_ctx.super.free(box);
+}
+
+static void acl_mailbox_copy_acls_from_parent(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(box->list);
+ struct acl_object *parent_aclobj;
+ struct acl_object_list_iter *iter;
+ struct acl_rights_update update;
+
+ i_zero(&update);
+ update.modify_mode = ACL_MODIFY_MODE_REPLACE;
+ update.neg_modify_mode = ACL_MODIFY_MODE_REPLACE;
+
+ parent_aclobj = acl_object_init_from_parent(alist->rights.backend,
+ box->name);
+ iter = acl_object_list_init(parent_aclobj);
+ while (acl_object_list_next(iter, &update.rights)) {
+ /* don't copy global ACL rights. */
+ if (!update.rights.global)
+ (void)acl_object_update(abox->aclobj, &update);
+ }
+ /* FIXME: Add error handling */
+ (void)acl_object_list_deinit(&iter);
+ acl_object_deinit(&parent_aclobj);
+}
+
+static int
+acl_mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ int ret;
+
+ if (!mailbox_is_autocreated(box)) {
+ /* we're looking up CREATE permission from our parent's rights */
+ ret = acl_mailbox_list_have_right(box->list, box->name, TRUE,
+ ACL_STORAGE_RIGHT_CREATE, NULL);
+ } else {
+ /* mailbox is autocreated, so we need to treat it as if it
+ already exists. ignore the "create" ACL here. */
+ ret = 1;
+ }
+ if (ret <= 0) {
+ if (ret < 0) {
+ mail_storage_set_internal_error(box->storage);
+ return -1;
+ }
+ /* Note that if user didn't have LOOKUP permission to parent
+ mailbox, this may reveal the mailbox's existence to user.
+ Can't help it. */
+ mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ return -1;
+ }
+
+ /* ignore ACLs in this mailbox until creation is complete, because
+ super.create() may call e.g. mailbox_open() which will fail since
+ we haven't yet copied ACLs to this mailbox. */
+ abox->skip_acl_checks = TRUE;
+ ret = abox->module_ctx.super.create_box(box, update, directory);
+ abox->skip_acl_checks = FALSE;
+ /* update local acl object, otherwise with LAYOUT=INDEX, we end up
+ without local path to acl file, and copying fails. */
+ struct acl_backend *acl_be = abox->aclobj->backend;
+ acl_object_deinit(&abox->aclobj);
+ abox->aclobj = acl_object_init_from_name(acl_be, box->name);
+
+ if (ret == 0)
+ acl_mailbox_copy_acls_from_parent(box);
+ return ret;
+}
+
+static int
+acl_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ int ret;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_ADMIN);
+ if (ret <= 0)
+ return -1;
+ return abox->module_ctx.super.update_box(box, update);
+}
+
+static void acl_mailbox_fail_not_found(struct mailbox *box)
+{
+ int ret;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_LOOKUP);
+ if (ret > 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ } else if (ret == 0) {
+ box->acl_no_lookup_right = TRUE;
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ }
+}
+
+static int
+acl_mailbox_delete(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ int ret;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_DELETE);
+ if (ret <= 0) {
+ if (ret == 0)
+ acl_mailbox_fail_not_found(box);
+ return -1;
+ }
+
+ return abox->module_ctx.super.delete_box(box);
+}
+
+static int
+acl_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(src);
+ int ret;
+
+ /* renaming requires rights to delete the old mailbox */
+ ret = acl_mailbox_right_lookup(src, ACL_STORAGE_RIGHT_DELETE);
+ if (ret <= 0) {
+ if (ret == 0)
+ acl_mailbox_fail_not_found(src);
+ return -1;
+ }
+
+ /* and create the new one under the parent mailbox */
+ T_BEGIN {
+ ret = acl_mailbox_list_have_right(dest->list, dest->name, TRUE,
+ ACL_STORAGE_RIGHT_CREATE, NULL);
+ } T_END;
+
+ if (ret <= 0) {
+ if (ret == 0) {
+ /* Note that if the mailbox didn't have LOOKUP
+ permission, this now reveals to user the mailbox's
+ existence. Can't help it. */
+ mail_storage_set_error(src->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ } else {
+ mail_storage_set_internal_error(src->storage);
+ }
+ return -1;
+ }
+
+ return abox->module_ctx.super.rename_box(src, dest);
+}
+
+static int
+acl_get_write_rights(struct mailbox *box,
+ bool *flags_r, bool *flag_seen_r, bool *flag_del_r)
+{
+ int ret;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE);
+ if (ret < 0)
+ return -1;
+ *flags_r = ret > 0;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_SEEN);
+ if (ret < 0)
+ return -1;
+ *flag_seen_r = ret > 0;
+
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_DELETED);
+ if (ret < 0)
+ return -1;
+ *flag_del_r = ret > 0;
+ return 0;
+}
+
+static void acl_transaction_set_failure(struct mailbox_transaction_context *t)
+{
+ MODULE_CONTEXT_SET(t, acl_storage_module,
+ &acl_transaction_failure);
+}
+
+static void
+acl_mail_update_flags(struct mail *_mail, enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *amail = ACL_MAIL_CONTEXT(mail);
+ bool acl_flags, acl_flag_seen, acl_flag_del;
+
+ if (acl_get_write_rights(_mail->box, &acl_flags, &acl_flag_seen,
+ &acl_flag_del) < 0) {
+ acl_transaction_set_failure(_mail->transaction);
+ return;
+ }
+
+ if (modify_type != MODIFY_REPLACE) {
+ /* adding/removing flags. just remove the disallowed
+ flags from the mask. */
+ if (!acl_flags)
+ flags &= MAIL_SEEN | MAIL_DELETED;
+ if (!acl_flag_seen)
+ flags &= ENUM_NEGATE(MAIL_SEEN);
+ if (!acl_flag_del)
+ flags &= ENUM_NEGATE(MAIL_DELETED);
+ } else if (!acl_flags || !acl_flag_seen || !acl_flag_del) {
+ /* we don't have permission to replace all the flags. */
+ if (!acl_flags && !acl_flag_seen && !acl_flag_del) {
+ /* no flag changes allowed. ignore silently. */
+ return;
+ }
+
+ /* handle this by first removing the allowed flags and
+ then adding the allowed flags */
+ acl_mail_update_flags(_mail, MODIFY_REMOVE,
+ ENUM_NEGATE(flags));
+ if (flags != 0)
+ acl_mail_update_flags(_mail, MODIFY_ADD, flags);
+ return;
+ }
+
+ amail->super.update_flags(_mail, modify_type, flags);
+}
+
+static void
+acl_mail_update_keywords(struct mail *_mail, enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *amail = ACL_MAIL_CONTEXT(mail);
+ int ret;
+
+ ret = acl_mailbox_right_lookup(_mail->box, ACL_STORAGE_RIGHT_WRITE);
+ if (ret <= 0) {
+ /* if we don't have permission, just silently return success. */
+ if (ret < 0)
+ acl_transaction_set_failure(_mail->transaction);
+ return;
+ }
+
+ amail->super.update_keywords(_mail, modify_type, keywords);
+}
+
+static void acl_mail_expunge(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *amail = ACL_MAIL_CONTEXT(mail);
+ int ret;
+
+ ret = acl_mailbox_right_lookup(_mail->box, ACL_STORAGE_RIGHT_EXPUNGE);
+ if (ret <= 0) {
+ /* if we don't have permission, silently return success so
+ users won't see annoying error messages in case their
+ clients try automatic expunging. */
+ acl_transaction_set_failure(_mail->transaction);
+ return;
+ }
+
+ amail->super.expunge(_mail);
+}
+
+void acl_mail_allocated(struct mail *_mail)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT(_mail->box);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ union mail_module_context *amail;
+
+ if (abox == NULL || !abox->acl_enabled)
+ return;
+
+ amail = p_new(mail->pool, union mail_module_context, 1);
+ amail->super = *v;
+ mail->vlast = &amail->super;
+
+ v->update_flags = acl_mail_update_flags;
+ v->update_keywords = acl_mail_update_keywords;
+ v->expunge = acl_mail_expunge;
+ MODULE_CONTEXT_SET_SELF(mail, acl_mail_module, amail);
+}
+
+static int
+acl_save_get_flags(struct mailbox *box, enum mail_flags *flags,
+ enum mail_flags *pvt_flags, struct mail_keywords **keywords)
+{
+ bool acl_flags, acl_flag_seen, acl_flag_del;
+
+ if (acl_get_write_rights(box, &acl_flags, &acl_flag_seen,
+ &acl_flag_del) < 0)
+ return -1;
+
+ if (!acl_flag_seen) {
+ *flags &= ENUM_NEGATE(MAIL_SEEN);
+ *pvt_flags &= ENUM_NEGATE(MAIL_SEEN);
+ }
+ if (!acl_flag_del) {
+ *flags &= ENUM_NEGATE(MAIL_DELETED);
+ *pvt_flags &= ENUM_NEGATE(MAIL_DELETED);
+ }
+ if (!acl_flags) {
+ *flags &= MAIL_SEEN | MAIL_DELETED;
+ *pvt_flags &= MAIL_SEEN | MAIL_DELETED;
+ *keywords = NULL;
+ }
+ return 0;
+}
+
+static int
+acl_save_begin(struct mail_save_context *ctx, struct istream *input)
+{
+ struct mailbox *box = ctx->transaction->box;
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ enum acl_storage_rights save_right;
+
+ save_right = (box->flags & MAILBOX_FLAG_POST_SESSION) != 0 ?
+ ACL_STORAGE_RIGHT_POST : ACL_STORAGE_RIGHT_INSERT;
+ if (acl_mailbox_right_lookup(box, save_right) <= 0)
+ return -1;
+ if (acl_save_get_flags(box, &ctx->data.flags,
+ &ctx->data.pvt_flags, &ctx->data.keywords) < 0)
+ return -1;
+
+ return abox->module_ctx.super.save_begin(ctx, input);
+}
+
+static bool
+acl_copy_has_rights(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox *destbox = ctx->transaction->box;
+ enum acl_storage_rights save_right;
+
+ if (ctx->moving) {
+ if (acl_mailbox_right_lookup(mail->box,
+ ACL_STORAGE_RIGHT_EXPUNGE) <= 0)
+ return FALSE;
+ }
+
+ save_right = (destbox->flags & MAILBOX_FLAG_POST_SESSION) != 0 ?
+ ACL_STORAGE_RIGHT_POST : ACL_STORAGE_RIGHT_INSERT;
+ if (acl_mailbox_right_lookup(destbox, save_right) <= 0)
+ return FALSE;
+ if (acl_save_get_flags(destbox, &ctx->data.flags,
+ &ctx->data.pvt_flags, &ctx->data.keywords) < 0)
+ return FALSE;
+ return TRUE;
+}
+
+static int
+acl_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(t->box);
+
+ if (!acl_copy_has_rights(ctx, mail)) {
+ mailbox_save_cancel(&ctx);
+ return -1;
+ }
+
+ return abox->module_ctx.super.copy(ctx, mail);
+}
+
+static int
+acl_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(ctx->box);
+ void *at = ACL_CONTEXT(ctx);
+ int ret;
+
+ if (at != NULL) {
+ abox->module_ctx.super.transaction_rollback(ctx);
+ return -1;
+ }
+
+ ret = abox->module_ctx.super.transaction_commit(ctx, changes_r);
+ if (abox->no_read_right) {
+ /* don't allow IMAP client to see what UIDs the messages got */
+ changes_r->no_read_perm = TRUE;
+ }
+ return ret;
+}
+
+static int acl_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ const char *const *rights;
+ unsigned int i;
+
+ if (acl_object_get_my_rights(abox->aclobj, pool_datastack_create(), &rights) < 0) {
+ mail_storage_set_internal_error(box->storage);
+ return -1;
+ }
+
+ /* for now this is used only by IMAP SUBSCRIBE. we'll intentionally
+ violate RFC 4314 here, because it says SUBSCRIBE should succeed only
+ when mailbox has 'l' right. But there's no point in not allowing
+ a subscribe for a mailbox that can be selected anyway. Just the
+ opposite: subscribing to such mailboxes is a very useful feature. */
+ for (i = 0; rights[i] != NULL; i++) {
+ if (strcmp(rights[i], MAIL_ACL_LOOKUP) == 0 ||
+ strcmp(rights[i], MAIL_ACL_READ) == 0 ||
+ strcmp(rights[i], MAIL_ACL_INSERT) == 0)
+ return abox->module_ctx.super.exists(box, auto_boxes,
+ existence_r);
+ }
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+}
+
+bool acl_mailbox_have_extra_attribute_rights(struct mailbox *box)
+{
+ /* RFC 5464:
+
+ When the ACL extension [RFC4314] is present, users can only set and
+ retrieve private or shared mailbox annotations on a mailbox on which
+ they have the "l" right and any one of the "r", "s", "w", "i", or "p"
+ rights.
+ */
+ const enum acl_storage_rights check_rights[] = {
+ ACL_STORAGE_RIGHT_READ,
+ ACL_STORAGE_RIGHT_WRITE_SEEN,
+ ACL_STORAGE_RIGHT_WRITE,
+ ACL_STORAGE_RIGHT_INSERT,
+ ACL_STORAGE_RIGHT_POST,
+ };
+ for (unsigned int i = 0; i < N_ELEMENTS(check_rights); i++) {
+ int ret = acl_mailbox_right_lookup(box, check_rights[i]);
+ if (ret > 0)
+ return TRUE;
+ if (ret < 0) {
+ /* unexpected error - stop checking further */
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+static int acl_mailbox_open_check_acl(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT_REQUIRE(box->list);
+ const unsigned int *idx_arr = alist->rights.acl_storage_right_idx;
+ enum acl_storage_rights open_right;
+ int ret;
+
+ /* mailbox can be opened either for reading or appending new messages */
+ if ((box->flags & MAILBOX_FLAG_IGNORE_ACLS) != 0 ||
+ (box->list->ns->flags & NAMESPACE_FLAG_NOACL) != 0 ||
+ abox->skip_acl_checks)
+ return 0;
+
+ if ((box->flags & MAILBOX_FLAG_SAVEONLY) != 0) {
+ open_right = (box->flags & MAILBOX_FLAG_POST_SESSION) != 0 ?
+ ACL_STORAGE_RIGHT_POST : ACL_STORAGE_RIGHT_INSERT;
+ } else if (box->deleting) {
+ open_right = ACL_STORAGE_RIGHT_DELETE;
+ } else if ((box->flags & MAILBOX_FLAG_ATTRIBUTE_SESSION) != 0) {
+ /* GETMETADATA/SETMETADATA requires "l" right and another one
+ which is checked afterwards. */
+ open_right = ACL_STORAGE_RIGHT_LOOKUP;
+ } else {
+ open_right = ACL_STORAGE_RIGHT_READ;
+ }
+
+ ret = acl_object_have_right(abox->aclobj, idx_arr[open_right]);
+ if (ret <= 0) {
+ if (ret == 0) {
+ /* no access. */
+ acl_mailbox_fail_not_found(box);
+ }
+ return -1;
+ }
+ if (open_right != ACL_STORAGE_RIGHT_READ) {
+ ret = acl_object_have_right(abox->aclobj,
+ idx_arr[ACL_STORAGE_RIGHT_READ]);
+ if (ret < 0)
+ return -1;
+ if (ret == 0)
+ abox->no_read_right = TRUE;
+ }
+ if ((box->flags & MAILBOX_FLAG_ATTRIBUTE_SESSION) != 0) {
+ if (!acl_mailbox_have_extra_attribute_rights(box))
+ return -1;
+ }
+ return 0;
+}
+
+static int acl_mailbox_open(struct mailbox *box)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+
+ if (acl_mailbox_open_check_acl(box) < 0)
+ return -1;
+
+ return abox->module_ctx.super.open(box);
+}
+
+static int acl_mailbox_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct acl_mailbox *abox = ACL_CONTEXT_REQUIRE(box);
+
+ if (abox->module_ctx.super.get_status(box, items, status_r) < 0)
+ return -1;
+
+ if ((items & STATUS_PERMANENT_FLAGS) != 0) {
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE) <= 0) {
+ status_r->permanent_flags &= MAIL_DELETED|MAIL_SEEN;
+ status_r->permanent_keywords = FALSE;
+ status_r->allow_new_keywords = FALSE;
+ }
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_DELETED) <= 0)
+ status_r->permanent_flags &= ENUM_NEGATE(MAIL_DELETED);
+ if (acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_WRITE_SEEN) <= 0)
+ status_r->permanent_flags &= ENUM_NEGATE(MAIL_SEEN);
+ }
+ return 0;
+}
+
+void acl_mailbox_allocated(struct mailbox *box)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(box->list);
+ struct mailbox_vfuncs *v = box->vlast;
+ struct acl_mailbox *abox;
+ bool ignore_acls = (box->flags & MAILBOX_FLAG_IGNORE_ACLS) != 0;
+
+ if (alist == NULL) {
+ /* ACLs disabled */
+ return;
+ }
+
+ if (mail_namespace_is_shared_user_root(box->list->ns) || alist->ignore_acls) {
+ /* this is the root shared namespace, which itself doesn't
+ have any existing mailboxes. */
+ ignore_acls = TRUE;
+ }
+
+ abox = p_new(box->pool, struct acl_mailbox, 1);
+ abox->module_ctx.super = *v;
+ box->vlast = &abox->module_ctx.super;
+ /* aclobj can be used for setting ACLs, even when mailbox is opened
+ with IGNORE_ACLS flag */
+ if (alist->rights.backend != NULL)
+ abox->aclobj = acl_object_init_from_name(alist->rights.backend,
+ mailbox_get_name(box));
+ else
+ i_assert(ignore_acls);
+
+ v->free = acl_mailbox_free;
+ if (!ignore_acls) {
+ abox->acl_enabled = TRUE;
+ v->is_readonly = acl_is_readonly;
+ v->exists = acl_mailbox_exists;
+ v->open = acl_mailbox_open;
+ v->get_status = acl_mailbox_get_status;
+ v->create_box = acl_mailbox_create;
+ v->update_box = acl_mailbox_update;
+ v->delete_box = acl_mailbox_delete;
+ v->rename_box = acl_mailbox_rename;
+ v->save_begin = acl_save_begin;
+ v->copy = acl_copy;
+ v->transaction_commit = acl_transaction_commit;
+ v->attribute_set = acl_attribute_set;
+ v->attribute_get = acl_attribute_get;
+ v->attribute_iter_init = acl_attribute_iter_init;
+ v->attribute_iter_next = acl_attribute_iter_next;
+ v->attribute_iter_deinit = acl_attribute_iter_deinit;
+ }
+ MODULE_CONTEXT_SET(box, acl_storage_module, abox);
+}
+
+static bool
+acl_mailbox_update_removed_id(struct acl_object *aclobj,
+ const struct acl_rights_update *update)
+{
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+
+ if (update->modify_mode != ACL_MODIFY_MODE_CLEAR &&
+ update->neg_modify_mode != ACL_MODIFY_MODE_CLEAR)
+ return FALSE;
+ if (update->modify_mode == ACL_MODIFY_MODE_CLEAR &&
+ update->neg_modify_mode == ACL_MODIFY_MODE_CLEAR)
+ return TRUE;
+
+ /* mixed clear/non-clear. see if the identifier exists anymore */
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (rights.id_type == update->rights.id_type &&
+ null_strcmp(rights.identifier, update->rights.identifier) == 0)
+ break;
+ }
+ return acl_object_list_deinit(&iter) >= 0;
+}
+
+int acl_mailbox_update_acl(struct mailbox_transaction_context *t,
+ const struct acl_rights_update *update)
+{
+ struct acl_object *aclobj;
+ const char *key;
+ time_t ts = update->last_change != 0 ?
+ update->last_change : ioloop_time;
+
+ key = t_strdup_printf(MAILBOX_ATTRIBUTE_PREFIX_ACL"%s",
+ acl_rights_get_id(&update->rights));
+ aclobj = acl_mailbox_get_aclobj(t->box);
+ if (acl_object_update(aclobj, update) < 0) {
+ mailbox_set_critical(t->box, "Failed to set ACL");
+ return -1;
+ }
+
+ /* FIXME: figure out some value lengths, so maybe some day
+ quota could apply to ACLs as well. */
+ if (acl_mailbox_update_removed_id(aclobj, update))
+ mail_index_attribute_unset(t->itrans, FALSE, key, ts);
+ else
+ mail_index_attribute_set(t->itrans, FALSE, key, ts, 0);
+ return 0;
+}
diff --git a/src/plugins/acl/acl-plugin.c b/src/plugins/acl/acl-plugin.c
new file mode 100644
index 0000000..b446dcd
--- /dev/null
+++ b/src/plugins/acl/acl-plugin.c
@@ -0,0 +1,27 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mailbox-list-private.h"
+#include "acl-api.h"
+#include "acl-plugin.h"
+
+
+const char *acl_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct mail_storage_hooks acl_mail_storage_hooks = {
+ .mail_user_created = acl_mail_user_created,
+ .mailbox_list_created = acl_mailbox_list_created,
+ .mail_namespace_storage_added = acl_mail_namespace_storage_added,
+ .mailbox_allocated = acl_mailbox_allocated,
+ .mail_allocated = acl_mail_allocated
+};
+
+void acl_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &acl_mail_storage_hooks);
+}
+
+void acl_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&acl_mail_storage_hooks);
+}
diff --git a/src/plugins/acl/acl-plugin.h b/src/plugins/acl/acl-plugin.h
new file mode 100644
index 0000000..6acfe89
--- /dev/null
+++ b/src/plugins/acl/acl-plugin.h
@@ -0,0 +1,73 @@
+#ifndef ACL_PLUGIN_H
+#define ACL_PLUGIN_H
+
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "acl-storage.h"
+
+#define ACL_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, acl_storage_module)
+#define ACL_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, acl_storage_module)
+#define ACL_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, acl_mailbox_list_module)
+#define ACL_LIST_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, acl_mailbox_list_module)
+#define ACL_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, acl_user_module)
+#define ACL_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, acl_user_module)
+
+struct acl_user {
+ union mail_user_module_context module_ctx;
+
+ const char *acl_user;
+ const char *acl_env;
+ const char *const *groups;
+
+ struct acl_lookup_dict *acl_lookup_dict;
+};
+
+struct acl_storage_rights_context {
+ struct acl_backend *backend;
+ unsigned int acl_storage_right_idx[ACL_STORAGE_RIGHT_COUNT];
+};
+
+struct acl_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+ struct acl_storage_rights_context rights;
+
+ time_t last_shared_add_check;
+ bool ignore_acls;
+};
+
+struct acl_mailbox {
+ union mailbox_module_context module_ctx;
+ struct acl_object *aclobj;
+ bool skip_acl_checks;
+ bool acl_enabled;
+ bool no_read_right;
+};
+
+extern MODULE_CONTEXT_DEFINE(acl_storage_module, &mail_storage_module_register);
+extern MODULE_CONTEXT_DEFINE(acl_user_module, &mail_user_module_register);
+extern MODULE_CONTEXT_DEFINE(acl_mailbox_list_module,
+ &mailbox_list_module_register);
+
+void acl_mailbox_list_created(struct mailbox_list *list);
+void acl_mail_namespace_storage_added(struct mail_namespace *ns);
+void acl_mail_user_created(struct mail_user *list);
+
+void acl_mailbox_allocated(struct mailbox *box);
+void acl_mail_allocated(struct mail *mail);
+
+struct acl_backend *acl_mailbox_list_get_backend(struct mailbox_list *list);
+int acl_mailbox_list_have_right(struct mailbox_list *list, const char *name,
+ bool parent, unsigned int acl_storage_right_idx,
+ bool *can_see_r) ATTR_NULL(5);
+
+void acl_plugin_init(struct module *module);
+void acl_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/acl/acl-shared-storage.c b/src/plugins/acl/acl-shared-storage.c
new file mode 100644
index 0000000..1329cd3
--- /dev/null
+++ b/src/plugins/acl/acl-shared-storage.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "var-expand.h"
+#include "acl-plugin.h"
+#include "acl-lookup-dict.h"
+#include "acl-shared-storage.h"
+#include "index/shared/shared-storage.h"
+
+#define SHARED_NS_RETRY_SECS (60*60)
+
+static bool acl_ns_prefix_exists(struct mail_namespace *ns)
+{
+ struct mailbox *box;
+ const char *vname;
+ enum mailbox_existence existence;
+ bool ret;
+
+ if (ns->list->mail_set->mail_shared_explicit_inbox)
+ return FALSE;
+
+ vname = t_strndup(ns->prefix, ns->prefix_len-1);
+ box = mailbox_alloc(ns->list, vname, 0);
+ ret = mailbox_exists(box, FALSE, &existence) == 0 &&
+ existence == MAILBOX_EXISTENCE_SELECT;
+ mailbox_free(&box);
+ return ret;
+}
+
+static void
+acl_shared_namespace_add(struct mail_namespace *ns,
+ struct mail_storage *storage, const char *userdomain)
+{
+ struct shared_storage *sstorage = (struct shared_storage *)storage;
+ struct mail_namespace *new_ns = ns;
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ const char *mailbox, *error;
+ string_t *str;
+
+ if (strcmp(ns->user->username, userdomain) == 0) {
+ /* skip ourself */
+ return;
+ }
+
+ const struct var_expand_table tab[] = {
+ { 'u', userdomain, "user" },
+ { 'n', t_strcut(userdomain, '@'), "username" },
+ { 'd', i_strchr_to_next(userdomain, '@'), "domain" },
+ { '\0', NULL, NULL }
+ };
+
+ str = t_str_new(128);
+ if (var_expand(str, sstorage->ns_prefix_pattern, tab, &error) <= 0) {
+ i_error("Failed to expand namespace prefix %s: %s",
+ sstorage->ns_prefix_pattern, error);
+ return;
+ }
+ mailbox = str_c(str);
+ if (shared_storage_get_namespace(&new_ns, &mailbox) < 0)
+ return;
+
+ /* check if there are any mailboxes really visible to us */
+ iter = mailbox_list_iter_init(new_ns->list, "*",
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ info = mailbox_list_iter_next(iter);
+ (void)mailbox_list_iter_deinit(&iter);
+
+ if (info == NULL && !acl_ns_prefix_exists(new_ns)) {
+ /* no visible mailboxes, remove the namespace */
+ mail_namespace_destroy(new_ns);
+ }
+}
+
+int acl_shared_namespaces_add(struct mail_namespace *ns)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(ns->user);
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(ns->list);
+ struct mail_storage *storage = mail_namespace_get_default_storage(ns);
+ struct acl_lookup_dict_iter *iter;
+ const char *name;
+
+ i_assert(auser != NULL && alist != NULL);
+ i_assert(ns->type == MAIL_NAMESPACE_TYPE_SHARED);
+ i_assert(strcmp(storage->name, MAIL_SHARED_STORAGE_NAME) == 0);
+
+ if (ioloop_time < alist->last_shared_add_check + SHARED_NS_RETRY_SECS) {
+ /* already added, don't bother rechecking */
+ return 0;
+ }
+ alist->last_shared_add_check = ioloop_time;
+
+ iter = acl_lookup_dict_iterate_visible_init(auser->acl_lookup_dict);
+ while ((name = acl_lookup_dict_iterate_visible_next(iter)) != NULL) {
+ T_BEGIN {
+ acl_shared_namespace_add(ns, storage, name);
+ } T_END;
+ }
+ return acl_lookup_dict_iterate_visible_deinit(&iter);
+}
diff --git a/src/plugins/acl/acl-shared-storage.h b/src/plugins/acl/acl-shared-storage.h
new file mode 100644
index 0000000..6e7736b
--- /dev/null
+++ b/src/plugins/acl/acl-shared-storage.h
@@ -0,0 +1,6 @@
+#ifndef ACL_SHARED_STORAGE_H
+#define ACL_SHARED_STORAGE_H
+
+int acl_shared_namespaces_add(struct mail_namespace *ns);
+
+#endif
diff --git a/src/plugins/acl/acl-storage.c b/src/plugins/acl/acl-storage.c
new file mode 100644
index 0000000..988c0b8
--- /dev/null
+++ b/src/plugins/acl/acl-storage.c
@@ -0,0 +1,62 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "mail-namespace.h"
+#include "mailbox-list-private.h"
+#include "acl-api-private.h"
+#include "acl-lookup-dict.h"
+#include "acl-plugin.h"
+
+
+struct acl_storage_module acl_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+struct acl_user_module acl_user_module =
+ MODULE_CONTEXT_INIT(&mail_user_module_register);
+
+static void acl_user_deinit(struct mail_user *user)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(user);
+
+ i_assert(auser != NULL);
+ acl_lookup_dict_deinit(&auser->acl_lookup_dict);
+ auser->module_ctx.super.deinit(user);
+}
+
+static void acl_mail_user_create(struct mail_user *user, const char *env)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct acl_user *auser;
+
+ auser = p_new(user->pool, struct acl_user, 1);
+ auser->module_ctx.super = *v;
+ user->vlast = &auser->module_ctx.super;
+ v->deinit = acl_user_deinit;
+ auser->acl_lookup_dict = acl_lookup_dict_init(user);
+
+ auser->acl_env = env;
+ auser->acl_user = mail_user_plugin_getenv(user, "acl_user");
+ if (auser->acl_user == NULL)
+ auser->acl_user = mail_user_plugin_getenv(user, "master_user");
+
+ env = mail_user_plugin_getenv(user, "acl_groups");
+ if (env != NULL) {
+ auser->groups =
+ (const char *const *)p_strsplit(user->pool, env, ",");
+ }
+
+ MODULE_CONTEXT_SET(user, acl_user_module, auser);
+}
+
+void acl_mail_user_created(struct mail_user *user)
+{
+ const char *env;
+
+ env = mail_user_plugin_getenv(user, "acl");
+ if (env != NULL && *env != '\0')
+ acl_mail_user_create(user, env);
+ else {
+ e_debug(user->event, "acl: No acl setting - ACLs are disabled");
+ }
+}
diff --git a/src/plugins/acl/acl-storage.h b/src/plugins/acl/acl-storage.h
new file mode 100644
index 0000000..8be4c26
--- /dev/null
+++ b/src/plugins/acl/acl-storage.h
@@ -0,0 +1,50 @@
+#ifndef ACL_STORAGE_H
+#define ACL_STORAGE_H
+
+#include "mail-storage.h"
+
+struct acl_rights_update;
+
+enum acl_storage_rights {
+ ACL_STORAGE_RIGHT_LOOKUP,
+ ACL_STORAGE_RIGHT_READ,
+ ACL_STORAGE_RIGHT_WRITE,
+ ACL_STORAGE_RIGHT_WRITE_SEEN,
+ ACL_STORAGE_RIGHT_WRITE_DELETED,
+ ACL_STORAGE_RIGHT_INSERT,
+ ACL_STORAGE_RIGHT_POST,
+ ACL_STORAGE_RIGHT_EXPUNGE,
+ ACL_STORAGE_RIGHT_CREATE,
+ ACL_STORAGE_RIGHT_DELETE,
+ ACL_STORAGE_RIGHT_ADMIN,
+
+ ACL_STORAGE_RIGHT_COUNT
+};
+
+/* Returns acl_object for the given mailbox. */
+struct acl_object *acl_mailbox_get_aclobj(struct mailbox *box);
+/* Returns 1 if we have the requested right. If not, returns 0 and sets storage
+ error to MAIL_ERROR_PERM. Returns -1 if internal error occurred and also
+ sets storage error. */
+int acl_mailbox_right_lookup(struct mailbox *box, unsigned int right_idx);
+
+/* Returns TRUE if mailbox has the necessary extra ACL for accessing
+ attributes. The caller must have checked the LOOKUP right already. */
+bool acl_mailbox_have_extra_attribute_rights(struct mailbox *box);
+
+int acl_mailbox_update_acl(struct mailbox_transaction_context *t,
+ const struct acl_rights_update *update);
+
+int acl_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type, const char *key,
+ const struct mail_attribute_value *value);
+int acl_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type, const char *key,
+ struct mail_attribute_value *value_r);
+struct mailbox_attribute_iter *
+acl_attribute_iter_init(struct mailbox *box, enum mail_attribute_type type,
+ const char *prefix);
+const char *acl_attribute_iter_next(struct mailbox_attribute_iter *iter);
+int acl_attribute_iter_deinit(struct mailbox_attribute_iter *iter);
+
+#endif
diff --git a/src/plugins/acl/doveadm-acl.c b/src/plugins/acl/doveadm-acl.c
new file mode 100644
index 0000000..0ab416a
--- /dev/null
+++ b/src/plugins/acl/doveadm-acl.c
@@ -0,0 +1,629 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "module-dir.h"
+#include "imap-util.h"
+#include "acl-plugin.h"
+#include "acl-api-private.h"
+#include "acl-lookup-dict.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+
+struct doveadm_acl_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ bool get_match_me;
+ enum acl_modify_mode modify_mode;
+};
+
+const char *doveadm_acl_plugin_version = DOVECOT_ABI_VERSION;
+
+void doveadm_acl_plugin_init(struct module *module);
+void doveadm_acl_plugin_deinit(void);
+
+static int
+cmd_acl_mailbox_open(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user, const char *mailbox,
+ struct mailbox **box_r)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(user);
+ struct mail_namespace *ns;
+ struct mailbox *box;
+
+ if (auser == NULL) {
+ i_error("ACL not enabled for %s", user->username);
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+
+ ns = mail_namespace_find(user->namespaces, mailbox);
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ if (mailbox_open(box) < 0) {
+ i_error("Can't open mailbox %s: %s", mailbox,
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_mailbox(ctx, box);
+ mailbox_free(&box);
+ return -1;
+ }
+ *box_r = box;
+ return 0;
+}
+
+static void cmd_acl_get_right(const struct acl_rights *rights)
+{
+ string_t *str;
+
+ doveadm_print(acl_rights_get_id(rights));
+
+ if (rights->global)
+ doveadm_print("global");
+ else
+ doveadm_print("");
+
+ str = t_str_new(256);
+ if (rights->rights != NULL)
+ str_append(str, t_strarray_join(rights->rights, " "));
+ if (rights->neg_rights != NULL) {
+ if (str_len(str) > 0)
+ str_append_c(str, ' ');
+ str_append_c(str, '-');
+ str_append(str, t_strarray_join(rights->neg_rights, " -"));
+ }
+ doveadm_print(str_c(str));
+}
+
+static int cmd_acl_get_mailbox(struct doveadm_acl_cmd_context *ctx,
+ struct mailbox *box)
+{
+ struct acl_object *aclobj = acl_mailbox_get_aclobj(box);
+ struct acl_backend *backend;
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ int ret;
+
+ backend = acl_mailbox_list_get_backend(box->list);
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) T_BEGIN {
+ if (!ctx->get_match_me ||
+ acl_backend_rights_match_me(backend, &rights))
+ cmd_acl_get_right(&rights);
+ } T_END;
+
+ if ((ret = acl_object_list_deinit(&iter))<0) {
+ i_error("ACL iteration failed");
+ doveadm_mail_failed_error(&ctx->ctx, MAIL_ERROR_TEMP);
+ }
+ return ret;
+}
+
+static int
+cmd_acl_get_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct doveadm_acl_cmd_context *ctx =
+ (struct doveadm_acl_cmd_context *)_ctx;
+ const char *mailbox = _ctx->args[0];
+ struct mailbox *box;
+ int ret;
+
+ if (cmd_acl_mailbox_open(_ctx, user, mailbox, &box) < 0)
+ return -1;
+
+ ret = cmd_acl_get_mailbox(ctx, box);
+ mailbox_free(&box);
+ return ret;
+}
+
+static bool cmd_acl_get_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct doveadm_acl_cmd_context *ctx =
+ (struct doveadm_acl_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'm':
+ ctx->get_match_me = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void cmd_acl_get_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("acl get");
+ doveadm_print_header("id", "ID", 0);
+ doveadm_print_header("global", "Global", 0);
+ doveadm_print_header("rights", "Rights", 0);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_get_alloc(void)
+{
+ struct doveadm_acl_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_acl_cmd_context);
+ ctx->ctx.getopt_args = "m";
+ ctx->ctx.v.parse_arg = cmd_acl_get_parse_arg;
+ ctx->ctx.v.run = cmd_acl_get_run;
+ ctx->ctx.v.init = cmd_acl_get_init;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return &ctx->ctx;
+}
+
+static int
+cmd_acl_rights_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const char *mailbox = ctx->args[0];
+ struct mailbox *box;
+ struct acl_object *aclobj;
+ const char *const *rights;
+ int ret = 0;
+
+ if (cmd_acl_mailbox_open(ctx, user, mailbox, &box) < 0)
+ return -1;
+
+ aclobj = acl_mailbox_get_aclobj(box);
+ if (acl_object_get_my_rights(aclobj, pool_datastack_create(),
+ &rights) < 0) {
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ i_error("Failed to get rights");
+ ret = -1;
+ } else {
+ doveadm_print(t_strarray_join(rights, " "));
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static void cmd_acl_rights_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("acl rights");
+ doveadm_print_header("rights", "Rights", 0);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_rights_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_acl_rights_run;
+ ctx->v.init = cmd_acl_rights_init;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return ctx;
+}
+
+static int
+cmd_acl_mailbox_update(struct doveadm_mail_cmd_context *ctx, struct mailbox *box,
+ const struct acl_rights_update *update)
+{
+ struct mailbox_transaction_context *t;
+ int ret;
+
+ t = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_EXTERNAL |
+ ctx->transaction_flags, __func__);
+ ret = acl_mailbox_update_acl(t, update);
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int
+cmd_acl_set_run(struct doveadm_mail_cmd_context *_ctx, struct mail_user *user)
+{
+ struct doveadm_acl_cmd_context *ctx =
+ (struct doveadm_acl_cmd_context *)_ctx;
+ const char *mailbox = _ctx->args[0], *id = _ctx->args[1];
+ const char *const *rights = _ctx->args + 2;
+ struct mailbox *box;
+ struct acl_rights_update update;
+ const char *error;
+ int ret;
+
+ if (cmd_acl_mailbox_open(_ctx, user, mailbox, &box) < 0)
+ return -1;
+
+ i_zero(&update);
+ update.modify_mode = ctx->modify_mode;
+ update.neg_modify_mode = ctx->modify_mode;
+ if (acl_rights_update_import(&update, id, rights, &error) < 0)
+ i_fatal_status(EX_USAGE, "%s", error);
+ if ((ret = cmd_acl_mailbox_update(&ctx->ctx, box, &update)) < 0) {
+ i_error("Failed to set ACL: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_error(_ctx, MAIL_ERROR_TEMP);
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static void cmd_acl_set_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (str_array_length(args) < 3)
+ doveadm_mail_help_name("acl set");
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_change_alloc(enum acl_modify_mode modify_mode)
+{
+ struct doveadm_acl_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_acl_cmd_context);
+ ctx->ctx.v.run = cmd_acl_set_run;
+ ctx->ctx.v.init = cmd_acl_set_init;
+ ctx->modify_mode = modify_mode;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_acl_set_alloc(void)
+{
+ return cmd_acl_change_alloc(ACL_MODIFY_MODE_REPLACE);
+}
+
+static struct doveadm_mail_cmd_context *cmd_acl_add_alloc(void)
+{
+ return cmd_acl_change_alloc(ACL_MODIFY_MODE_ADD);
+}
+
+static struct doveadm_mail_cmd_context *cmd_acl_remove_alloc(void)
+{
+ return cmd_acl_change_alloc(ACL_MODIFY_MODE_REMOVE);
+}
+
+static int
+cmd_acl_delete_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const char *mailbox = ctx->args[0], *id = ctx->args[1];
+ struct mailbox *box;
+ struct acl_rights_update update;
+ const char *error;
+ int ret = 0;
+
+ if (cmd_acl_mailbox_open(ctx, user, mailbox, &box) < 0)
+ return -1;
+
+ i_zero(&update);
+ if (acl_rights_update_import(&update, id, NULL, &error) < 0)
+ i_fatal_status(EX_USAGE, "%s", error);
+ if ((ret = cmd_acl_mailbox_update(ctx, box, &update)) < 0) {
+ i_error("Failed to delete ACL: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static void cmd_acl_delete_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (str_array_length(args) < 2)
+ doveadm_mail_help_name("acl delete");
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_delete_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_acl_delete_run;
+ ctx->v.init = cmd_acl_delete_init;
+ return ctx;
+}
+
+static int
+cmd_acl_recalc_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT(user);
+
+ if (auser == NULL) {
+ i_error("ACL not enabled for %s", user->username);
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+ if (acl_lookup_dict_rebuild(auser->acl_lookup_dict) < 0) {
+ i_error("Failed to recalculate ACL dicts");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ return -1;
+ }
+ return 0;
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_recalc_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_acl_recalc_run;
+ return ctx;
+}
+
+static int
+cmd_acl_debug_mailbox_open(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user, const char *mailbox,
+ struct mailbox **box_r)
+{
+ struct acl_user *auser = ACL_USER_CONTEXT_REQUIRE(user);
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *path, *errstr;
+ enum mail_error error;
+
+ ns = mail_namespace_find(user->namespaces, mailbox);
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ if (mailbox_open(box) < 0) {
+ errstr = mail_storage_get_last_internal_error(box->storage, &error);
+ errstr = t_strdup(errstr);
+ doveadm_mail_failed_error(ctx, error);
+
+ if (error != MAIL_ERROR_NOTFOUND ||
+ mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) <= 0)
+ i_error("Can't open mailbox %s: %s", mailbox, errstr);
+ else {
+ i_error("Mailbox '%s' in namespace '%s' doesn't exist in %s",
+ box->name, ns->prefix, path);
+ }
+ mailbox_free(&box);
+ return -1;
+ }
+
+ if (auser == NULL) {
+ i_info("ACL not enabled for user %s, mailbox can be accessed",
+ user->username);
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ mailbox_free(&box);
+ return -1;
+ }
+
+ *box_r = box;
+ return 0;
+}
+
+static bool cmd_acl_debug_mailbox(struct mailbox *box, bool *retry_r)
+{
+ struct mail_namespace *ns = mailbox_get_namespace(box);
+ struct acl_user *auser = ACL_USER_CONTEXT_REQUIRE(ns->user);
+ struct acl_object *aclobj = acl_mailbox_get_aclobj(box);
+ struct acl_backend *backend = acl_mailbox_list_get_backend(box->list);
+ struct acl_mailbox_list_context *iter;
+ struct acl_lookup_dict_iter *diter;
+ const char *const *rights, *name, *path;
+ enum mail_flags private_flags_mask;
+ string_t *str;
+ int ret;
+ bool all_ok = TRUE;
+
+ *retry_r = FALSE;
+
+ i_info("Mailbox '%s' is in namespace '%s'",
+ box->name, box->list->ns->prefix);
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, &path) > 0)
+ i_info("Mailbox path: %s", path);
+
+ private_flags_mask = mailbox_get_private_flags_mask(box);
+ if (private_flags_mask == 0)
+ i_info("All message flags are shared across users in mailbox");
+ else {
+ str = t_str_new(64);
+ imap_write_flags(str, private_flags_mask, NULL);
+ i_info("Per-user private flags in mailbox: %s", str_c(str));
+ }
+
+ /* check if user has lookup right */
+ if (acl_object_get_my_rights(aclobj, pool_datastack_create(),
+ &rights) < 0)
+ i_fatal("Failed to get rights");
+
+ if (rights[0] == NULL)
+ i_info("User %s has no rights for mailbox", ns->user->username);
+ else {
+ i_info("User %s has rights: %s",
+ ns->user->username, t_strarray_join(rights, " "));
+ }
+ if (!str_array_find(rights, MAIL_ACL_LOOKUP)) {
+ i_error("User %s is missing 'lookup' right",
+ ns->user->username);
+ return FALSE;
+ }
+
+ /* check if mailbox is listable */
+ if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) {
+ i_info("Mailbox in user's private namespace");
+ return TRUE;
+ }
+
+ iter = acl_backend_nonowner_lookups_iter_init(backend);
+ while (acl_backend_nonowner_lookups_iter_next(iter, &name)) {
+ if (strcmp(name, box->name) == 0)
+ break;
+ }
+ if ((ret = acl_backend_nonowner_lookups_iter_deinit(&iter))<0)
+ i_fatal("ACL non-owner iteration failed");
+ if (ret == 0) {
+ i_error("Mailbox not found from dovecot-acl-list, rebuilding");
+ if (acl_backend_nonowner_lookups_rebuild(backend) < 0)
+ i_fatal("dovecot-acl-list rebuilding failed");
+ all_ok = FALSE;
+ *retry_r = TRUE;
+ } else {
+ i_info("Mailbox found from dovecot-acl-list");
+ }
+
+ if (ns->type == MAIL_NAMESPACE_TYPE_PUBLIC) {
+ i_info("Mailbox is in public namespace");
+ return TRUE;
+ }
+
+ if (!acl_lookup_dict_is_enabled(auser->acl_lookup_dict)) {
+ i_error("acl_lookup_dict not enabled");
+ return FALSE;
+ }
+
+ /* shared namespace. see if it's in acl lookup dict */
+ diter = acl_lookup_dict_iterate_visible_init(auser->acl_lookup_dict);
+ while ((name = acl_lookup_dict_iterate_visible_next(diter)) != NULL) {
+ if (strcmp(name, ns->owner->username) == 0)
+ break;
+ }
+ if (acl_lookup_dict_iterate_visible_deinit(&diter) < 0)
+ i_fatal("ACL shared dict iteration failed");
+ if (name == NULL) {
+ i_error("User %s not found from ACL shared dict, rebuilding",
+ ns->owner->username);
+ if (acl_lookup_dict_rebuild(auser->acl_lookup_dict) < 0)
+ i_fatal("ACL lookup dict rebuild failed");
+ all_ok = FALSE;
+ *retry_r = TRUE;
+ } else {
+ i_info("User %s found from ACL shared dict",
+ ns->owner->username);
+ }
+ return all_ok;
+}
+
+static int
+cmd_acl_debug_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const char *mailbox = ctx->args[0];
+ struct mailbox *box;
+ bool ret, retry;
+
+ if (cmd_acl_debug_mailbox_open(ctx, user, mailbox, &box) < 0)
+ return -1;
+
+ ret = cmd_acl_debug_mailbox(box, &retry);
+ if (!ret && retry) {
+ i_info("Retrying after rebuilds:");
+ ret = cmd_acl_debug_mailbox(box, &retry);
+ }
+ if (ret)
+ i_info("Mailbox %s is visible in LIST", box->vname);
+ else
+ i_info("Mailbox %s is NOT visible in LIST", box->vname);
+ mailbox_free(&box);
+ return 0;
+}
+
+static void cmd_acl_debug_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("acl debug");
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_acl_debug_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_acl_debug_run;
+ ctx->v.init = cmd_acl_debug_init;
+ return ctx;
+}
+
+static struct doveadm_cmd_ver2 acl_commands[] = {
+{
+ .name = "acl get",
+ .mail_cmd = cmd_acl_get_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-m] <mailbox>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('m', "match-me", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl rights",
+ .mail_cmd = cmd_acl_rights_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl set",
+ .mail_cmd = cmd_acl_set_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox> <id> <right> [<right> ...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "id", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "right", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl add",
+ .mail_cmd = cmd_acl_add_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox> <id> <right> [<right> ...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "id", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "right", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl remove",
+ .mail_cmd = cmd_acl_remove_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox> <id> <right> [<right> ...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "id", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "right", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl delete",
+ .mail_cmd = cmd_acl_delete_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox> <id>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "id", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl recalc",
+ .mail_cmd = cmd_acl_recalc_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "acl debug",
+ .mail_cmd = cmd_acl_debug_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+void doveadm_acl_plugin_init(struct module *module ATTR_UNUSED)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(acl_commands); i++)
+ doveadm_cmd_register_ver2(&acl_commands[i]);
+}
+
+void doveadm_acl_plugin_deinit(void)
+{
+}
diff --git a/src/plugins/acl/test-acl.c b/src/plugins/acl/test-acl.c
new file mode 100644
index 0000000..1fafd7a
--- /dev/null
+++ b/src/plugins/acl/test-acl.c
@@ -0,0 +1,66 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "acl-api-private.h"
+
+static void test_acl_rights_sort(void)
+{
+ struct acl_rights rights1 = {
+ .rights = t_strsplit("a b a c d b", " "),
+ .neg_rights = t_strsplit("e d c a a d b e", " "),
+ };
+ struct acl_rights rights2 = {
+ .rights = t_strsplit("a c x", " "),
+ .neg_rights = t_strsplit("b c y", " "),
+ };
+ struct acl_object obj = {
+ .rights_pool = pool_alloconly_create("acl rights", 256)
+ };
+ const struct acl_rights *rights;
+
+ test_begin("acl_rights_sort");
+ t_array_init(&obj.rights, 8);
+
+ /* try with zero rights */
+ acl_rights_sort(&obj);
+ test_assert(array_count(&obj.rights) == 0);
+
+ /* try with just one right */
+ array_push_back(&obj.rights, &rights1);
+ acl_rights_sort(&obj);
+ test_assert(array_count(&obj.rights) == 1);
+ rights = array_idx(&obj.rights, 0);
+ test_assert(acl_rights_cmp(rights, &rights1) == 0);
+
+ /* try with two rights that don't have equal ID */
+ struct acl_rights rights1_id2 = rights1;
+ rights1_id2.identifier = "id2";
+ array_push_back(&obj.rights, &rights1_id2);
+ acl_rights_sort(&obj);
+ test_assert(array_count(&obj.rights) == 2);
+ rights = array_idx(&obj.rights, 0);
+ test_assert(acl_rights_cmp(&rights[0], &rights1) == 0);
+ test_assert(acl_rights_cmp(&rights[1], &rights1_id2) == 0);
+
+ /* try with 3 rights where first has equal ID */
+ array_push_back(&obj.rights, &rights2);
+ acl_rights_sort(&obj);
+ test_assert(array_count(&obj.rights) == 2);
+ rights = array_idx(&obj.rights, 0);
+ test_assert_strcmp(t_strarray_join(rights[0].rights, " "), "a b c d x");
+ test_assert_strcmp(t_strarray_join(rights[0].neg_rights, " "), "a b c d e y");
+
+ pool_unref(&obj.rights_pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_acl_rights_sort,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/plugins/apparmor/Makefile.am b/src/plugins/apparmor/Makefile.am
new file mode 100644
index 0000000..510053b
--- /dev/null
+++ b/src/plugins/apparmor/Makefile.am
@@ -0,0 +1,14 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib01_apparmor_plugin_la_LDFLAGS = -module -avoid-version
+lib01_apparmor_plugin_la_LIBADD = $(APPARMOR_LIBS)
+lib01_apparmor_plugin_la_SOURCES = \
+ apparmor-plugin.c
+
+module_LTLIBRARIES = \
+ lib01_apparmor_plugin.la
diff --git a/src/plugins/apparmor/Makefile.in b/src/plugins/apparmor/Makefile.in
new file mode 100644
index 0000000..17041ac
--- /dev/null
+++ b/src/plugins/apparmor/Makefile.in
@@ -0,0 +1,816 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/apparmor
+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)$(moduledir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+lib01_apparmor_plugin_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_lib01_apparmor_plugin_la_OBJECTS = apparmor-plugin.lo
+lib01_apparmor_plugin_la_OBJECTS = \
+ $(am_lib01_apparmor_plugin_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 =
+lib01_apparmor_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib01_apparmor_plugin_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)/apparmor-plugin.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 = $(lib01_apparmor_plugin_la_SOURCES)
+DIST_SOURCES = $(lib01_apparmor_plugin_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)
+# 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@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib01_apparmor_plugin_la_LDFLAGS = -module -avoid-version
+lib01_apparmor_plugin_la_LIBADD = $(APPARMOR_LIBS)
+lib01_apparmor_plugin_la_SOURCES = \
+ apparmor-plugin.c
+
+module_LTLIBRARIES = \
+ lib01_apparmor_plugin.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/plugins/apparmor/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/apparmor/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-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}; \
+ }
+
+lib01_apparmor_plugin.la: $(lib01_apparmor_plugin_la_OBJECTS) $(lib01_apparmor_plugin_la_DEPENDENCIES) $(EXTRA_lib01_apparmor_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib01_apparmor_plugin_la_LINK) -rpath $(moduledir) $(lib01_apparmor_plugin_la_OBJECTS) $(lib01_apparmor_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/apparmor-plugin.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)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/apparmor-plugin.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-moduleLTLIBRARIES
+
+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)/apparmor-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/apparmor/apparmor-plugin.c b/src/plugins/apparmor/apparmor-plugin.c
new file mode 100644
index 0000000..410c652
--- /dev/null
+++ b/src/plugins/apparmor/apparmor-plugin.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "module-dir.h"
+#include "randgen.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "mail-storage-hooks.h"
+#include <sys/apparmor.h>
+
+#define APPARMOR_PLUGIN_SETTING_HAT_PREFIX "apparmor_hat"
+
+const char *apparmor_plugin_version = DOVECOT_ABI_VERSION;
+
+/* hooks into user creation and deinit, will try to use
+ hats provided by apparmor_hat, apparmor_hat1... etc */
+
+#define APPARMOR_USER_CONTEXT(obj) \
+ (struct apparmor_mail_user*)MODULE_CONTEXT(obj, apparmor_mail_user_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(apparmor_mail_user_module,
+ &mail_user_module_register);
+
+struct apparmor_mail_user {
+ union mail_user_module_context module_ctx;
+ unsigned long token;
+};
+
+void apparmor_plugin_init(struct module*);
+void apparmor_plugin_deinit(void);
+
+static void apparmor_log_current_context(struct mail_user *user)
+{
+ char *con, *mode;
+
+ if (aa_getcon(&con, &mode) < 0) {
+ e_debug(user->event, "aa_getcon() failed: %m");
+ } else {
+ e_debug(user->event, "apparmor: Current context=%s, mode=%s",
+ con, mode);
+ free(con);
+ }
+}
+
+static void apparmor_mail_user_deinit(struct mail_user *user)
+{
+ struct apparmor_mail_user *auser = APPARMOR_USER_CONTEXT(user);
+
+ i_assert(auser != NULL);
+ auser->module_ctx.super.deinit(user);
+
+ if (aa_change_hat(NULL, auser->token)<0)
+ i_fatal("aa_change_hat(NULL) failed: %m");
+
+ apparmor_log_current_context(user);
+}
+
+static void apparmor_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct apparmor_mail_user *auser;
+ ARRAY_TYPE(const_string) hats;
+ /* see if we can find any hats */
+ const char *hat =
+ mail_user_plugin_getenv(user, APPARMOR_PLUGIN_SETTING_HAT_PREFIX);
+ if (hat == NULL)
+ return;
+
+ t_array_init(&hats, 8);
+ array_push_back(&hats, &hat);
+ for(unsigned int i = 2;; i++) {
+ hat = mail_user_plugin_getenv(user, t_strdup_printf("%s%u",
+ APPARMOR_PLUGIN_SETTING_HAT_PREFIX, i));
+ if (hat == NULL) break;
+ array_push_back(&hats, &hat);
+ }
+ array_append_zero(&hats);
+
+ /* we got hat(s) to try */
+ auser = p_new(user->pool, struct apparmor_mail_user, 1);
+ auser->module_ctx.super = *v;
+ user->vlast = &auser->module_ctx.super;
+ v->deinit = apparmor_mail_user_deinit;
+ MODULE_CONTEXT_SET(user, apparmor_mail_user_module, auser);
+
+ /* generate a magic token */
+ random_fill(&auser->token, sizeof(auser->token));
+
+ /* try change hat */
+ if (aa_change_hatv(array_front_modifiable(&hats), auser->token) < 0) {
+ i_fatal("aa_change_hatv(%s) failed: %m",
+ t_array_const_string_join(&hats, ","));
+ }
+
+ apparmor_log_current_context(user);
+}
+
+static const struct mail_storage_hooks apparmor_hooks = {
+ .mail_user_created = apparmor_mail_user_created
+};
+
+void apparmor_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &apparmor_hooks);
+}
+
+void apparmor_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&apparmor_hooks);
+}
diff --git a/src/plugins/charset-alias/Makefile.am b/src/plugins/charset-alias/Makefile.am
new file mode 100644
index 0000000..da99095
--- /dev/null
+++ b/src/plugins/charset-alias/Makefile.am
@@ -0,0 +1,19 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib20_charset_alias_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_charset_alias_plugin.la
+
+lib20_charset_alias_plugin_la_SOURCES = \
+ charset-alias-plugin.c
+
+noinst_HEADERS = \
+ charset-alias-plugin.h
diff --git a/src/plugins/charset-alias/Makefile.in b/src/plugins/charset-alias/Makefile.in
new file mode 100644
index 0000000..4cb5bcb
--- /dev/null
+++ b/src/plugins/charset-alias/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/charset-alias
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_charset_alias_plugin_la_LIBADD =
+am_lib20_charset_alias_plugin_la_OBJECTS = charset-alias-plugin.lo
+lib20_charset_alias_plugin_la_OBJECTS = \
+ $(am_lib20_charset_alias_plugin_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 =
+lib20_charset_alias_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_charset_alias_plugin_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)/charset-alias-plugin.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 = $(lib20_charset_alias_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_charset_alias_plugin_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 =
+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-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib20_charset_alias_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_charset_alias_plugin.la
+
+lib20_charset_alias_plugin_la_SOURCES = \
+ charset-alias-plugin.c
+
+noinst_HEADERS = \
+ charset-alias-plugin.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/plugins/charset-alias/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/charset-alias/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-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}; \
+ }
+
+lib20_charset_alias_plugin.la: $(lib20_charset_alias_plugin_la_OBJECTS) $(lib20_charset_alias_plugin_la_DEPENDENCIES) $(EXTRA_lib20_charset_alias_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_charset_alias_plugin_la_LINK) -rpath $(moduledir) $(lib20_charset_alias_plugin_la_OBJECTS) $(lib20_charset_alias_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/charset-alias-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/charset-alias-plugin.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-moduleLTLIBRARIES
+
+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-alias-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/charset-alias/charset-alias-plugin.c b/src/plugins/charset-alias/charset-alias-plugin.c
new file mode 100644
index 0000000..612223e
--- /dev/null
+++ b/src/plugins/charset-alias/charset-alias-plugin.c
@@ -0,0 +1,198 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "mail-storage-hooks.h"
+#include "charset-utf8-private.h"
+#include "charset-alias-plugin.h"
+
+
+#define CHARSET_ALIAS_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, charset_alias_user_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(charset_alias_user_module,
+ &mail_user_module_register);
+
+const char *charset_alias_plugin_version = DOVECOT_ABI_VERSION;
+
+static int charset_alias_to_utf8_begin(const char *charset,
+ normalizer_func_t *normalizer,
+ struct charset_translation **t_r);
+
+static void charset_alias_to_utf8_end(struct charset_translation *t);
+
+static void charset_alias_to_utf8_reset(struct charset_translation *t);
+
+static enum charset_result charset_alias_to_utf8(struct charset_translation *t,
+ const unsigned char *src,
+ size_t *src_size, buffer_t *dest);
+
+/* charset_utf8_vfuncs is defined in lib-charset/charset-utf8.c */
+extern const struct charset_utf8_vfuncs *charset_utf8_vfuncs;
+
+static const struct charset_utf8_vfuncs *original_charset_utf8_vfuncs;
+
+static const struct charset_utf8_vfuncs charset_alias_utf8_vfuncs = {
+ charset_alias_to_utf8_begin,
+ charset_alias_to_utf8_end,
+ charset_alias_to_utf8_reset,
+ charset_alias_to_utf8
+};
+
+struct charset_alias {
+ const char *charset;
+ const char *alias;
+};
+
+static ARRAY(struct charset_alias) charset_aliases;
+static pool_t charset_alias_pool;
+static int charset_alias_user_refcount = 0;
+
+struct charset_alias_user {
+ union mail_user_module_context module_ctx;
+};
+
+
+static const char *charset_alias_get_alias(const char *charset)
+{
+ const struct charset_alias* elem;
+ const char *key;
+
+ if (array_is_created(&charset_aliases)) {
+ key = t_str_lcase(charset);
+ array_foreach(&charset_aliases, elem) {
+ if (strcmp(key, elem->charset) == 0) {
+ return elem->alias;
+ }
+ }
+ }
+ return charset;
+}
+
+static int charset_alias_to_utf8_begin(const char *charset,
+ normalizer_func_t *normalizer,
+ struct charset_translation **t_r)
+{
+ i_assert(original_charset_utf8_vfuncs != NULL);
+ charset = charset_alias_get_alias(charset);
+ return original_charset_utf8_vfuncs->to_utf8_begin(charset, normalizer, t_r);
+}
+static void charset_alias_to_utf8_end(struct charset_translation *t)
+{
+ i_assert(original_charset_utf8_vfuncs != NULL);
+ original_charset_utf8_vfuncs->to_utf8_end(t);
+}
+
+static void charset_alias_to_utf8_reset(struct charset_translation *t)
+{
+ i_assert(original_charset_utf8_vfuncs != NULL);
+ original_charset_utf8_vfuncs->to_utf8_reset(t);
+}
+
+static enum charset_result charset_alias_to_utf8(struct charset_translation *t,
+ const unsigned char *src,
+ size_t *src_size, buffer_t *dest)
+{
+ i_assert(original_charset_utf8_vfuncs != NULL);
+ return original_charset_utf8_vfuncs->to_utf8(t, src, src_size, dest);
+}
+
+static unsigned int charset_aliases_init(struct mail_user *user, pool_t pool, const char *str)
+{
+ const char *key, *value, *const *keyvalues;
+ struct charset_alias alias;
+ int i;
+
+ i_assert(!array_is_created(&charset_aliases));
+
+ p_array_init(&charset_aliases, pool, 1);
+ keyvalues = t_strsplit_spaces(str, " ");
+ for (i = 0; keyvalues[i] != NULL; i++) {
+ value = strchr(keyvalues[i], '=');
+ if (value == NULL) {
+ i_error("charset_alias: Missing '=' in charset_aliases setting");
+ continue;
+ }
+ key = t_strdup_until(keyvalues[i], value++);
+ if (key[0] == '\0' || value[0] == '\0') {
+ i_error("charset_alias: charset or alias missing in charset_aliases setting");
+ continue;
+ }
+ if (strcasecmp(key, value) != 0) {
+ e_debug(user->event, "charset_alias: add charset-alias %s for %s", value, key);
+ alias.charset = p_strdup(pool, t_str_lcase(key));
+ alias.alias = p_strdup(pool, value);
+ array_push_back(&charset_aliases, &alias);
+ }
+ }
+ return array_count(&charset_aliases);
+}
+
+static void charset_alias_utf8_vfuncs_set(void)
+{
+ original_charset_utf8_vfuncs = charset_utf8_vfuncs;
+ charset_utf8_vfuncs = &charset_alias_utf8_vfuncs;
+}
+
+static void charset_alias_utf8_vfuncs_reset(void)
+{
+ if (original_charset_utf8_vfuncs != NULL) {
+ charset_utf8_vfuncs = original_charset_utf8_vfuncs;
+ original_charset_utf8_vfuncs = NULL;
+ }
+}
+
+static void charset_alias_mail_user_deinit(struct mail_user *user)
+{
+ struct charset_alias_user *cuser = CHARSET_ALIAS_USER_CONTEXT(user);
+
+ i_assert(charset_alias_user_refcount > 0);
+ if (--charset_alias_user_refcount == 0) {
+ charset_alias_utf8_vfuncs_reset();
+ array_free(&charset_aliases);
+ pool_unref(&charset_alias_pool);
+ }
+
+ cuser->module_ctx.super.deinit(user);
+}
+
+static void charset_alias_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct charset_alias_user *cuser;
+ const char *str;
+
+ cuser = p_new(user->pool, struct charset_alias_user, 1);
+ cuser->module_ctx.super = *v;
+ user->vlast = &cuser->module_ctx.super;
+ v->deinit = charset_alias_mail_user_deinit;
+
+ if (charset_alias_user_refcount++ == 0) {
+ charset_alias_pool = pool_alloconly_create("charset_alias alias list", 128);
+ str = mail_user_plugin_getenv(user, "charset_aliases");
+ if (str != NULL && str[0] != '\0') {
+ if (charset_aliases_init(user, charset_alias_pool, str) > 0) {
+ charset_alias_utf8_vfuncs_set();
+ }
+ }
+ }
+
+ MODULE_CONTEXT_SET(user, charset_alias_user_module, cuser);
+}
+
+static struct mail_storage_hooks charset_alias_mail_storage_hooks = {
+ .mail_user_created = charset_alias_mail_user_created
+};
+
+void charset_alias_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &charset_alias_mail_storage_hooks);
+}
+
+void charset_alias_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&charset_alias_mail_storage_hooks);
+}
diff --git a/src/plugins/charset-alias/charset-alias-plugin.h b/src/plugins/charset-alias/charset-alias-plugin.h
new file mode 100644
index 0000000..0a1d518
--- /dev/null
+++ b/src/plugins/charset-alias/charset-alias-plugin.h
@@ -0,0 +1,7 @@
+#ifndef CHARSET_ALIAS_PLUGIN_H
+#define CHARSET_ALIAS_PLUGIN_H
+
+void charset_alias_plugin_init(struct module *module);
+void charset_alias_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fs-compress/Makefile.am b/src/plugins/fs-compress/Makefile.am
new file mode 100644
index 0000000..77ba733
--- /dev/null
+++ b/src/plugins/fs-compress/Makefile.am
@@ -0,0 +1,14 @@
+fs_moduledir = $(moduledir)
+fs_module_LTLIBRARIES = \
+ libfs_compress.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-compression \
+ -I$(top_srcdir)/src/lib-fs
+
+NOPLUGIN_LDFLAGS =
+libfs_compress_la_SOURCES = fs-compress.c
+libfs_compress_la_LIBADD = ../../lib-compression/libdovecot-compression.la
+libfs_compress_la_DEPENDENCIES = ../../lib-compression/libdovecot-compression.la
+libfs_compress_la_LDFLAGS = -module -avoid-version
diff --git a/src/plugins/fs-compress/Makefile.in b/src/plugins/fs-compress/Makefile.in
new file mode 100644
index 0000000..7e41442
--- /dev/null
+++ b/src/plugins/fs-compress/Makefile.in
@@ -0,0 +1,811 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/fs-compress
+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)$(fs_moduledir)"
+LTLIBRARIES = $(fs_module_LTLIBRARIES)
+am_libfs_compress_la_OBJECTS = fs-compress.lo
+libfs_compress_la_OBJECTS = $(am_libfs_compress_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 =
+libfs_compress_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libfs_compress_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)/fs-compress.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 = $(libfs_compress_la_SOURCES)
+DIST_SOURCES = $(libfs_compress_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)
+# 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@
+fs_moduledir = $(moduledir)
+fs_module_LTLIBRARIES = \
+ libfs_compress.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-compression \
+ -I$(top_srcdir)/src/lib-fs
+
+libfs_compress_la_SOURCES = fs-compress.c
+libfs_compress_la_LIBADD = ../../lib-compression/libdovecot-compression.la
+libfs_compress_la_DEPENDENCIES = ../../lib-compression/libdovecot-compression.la
+libfs_compress_la_LDFLAGS = -module -avoid-version
+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/plugins/fs-compress/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/fs-compress/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-fs_moduleLTLIBRARIES: $(fs_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(fs_module_LTLIBRARIES)'; test -n "$(fs_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)$(fs_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(fs_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(fs_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(fs_moduledir)"; \
+ }
+
+uninstall-fs_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(fs_module_LTLIBRARIES)'; test -n "$(fs_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(fs_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(fs_moduledir)/$$f"; \
+ done
+
+clean-fs_moduleLTLIBRARIES:
+ -test -z "$(fs_module_LTLIBRARIES)" || rm -f $(fs_module_LTLIBRARIES)
+ @list='$(fs_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}; \
+ }
+
+libfs_compress.la: $(libfs_compress_la_OBJECTS) $(libfs_compress_la_DEPENDENCIES) $(EXTRA_libfs_compress_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libfs_compress_la_LINK) -rpath $(fs_moduledir) $(libfs_compress_la_OBJECTS) $(libfs_compress_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-compress.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)
+installdirs:
+ for dir in "$(DESTDIR)$(fs_moduledir)"; 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-fs_moduleLTLIBRARIES clean-generic clean-libtool \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fs-compress.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-fs_moduleLTLIBRARIES
+
+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-compress.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-fs_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-fs_moduleLTLIBRARIES clean-generic clean-libtool \
+ 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-fs_moduleLTLIBRARIES \
+ 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 uninstall-fs_moduleLTLIBRARIES
+
+.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/plugins/fs-compress/fs-compress.c b/src/plugins/fs-compress/fs-compress.c
new file mode 100644
index 0000000..ab03076
--- /dev/null
+++ b/src/plugins/fs-compress/fs-compress.c
@@ -0,0 +1,285 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-tee.h"
+#include "istream-try.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "compression.h"
+#include "fs-api-private.h"
+
+struct compress_fs {
+ struct fs fs;
+ const struct compression_handler *compress_handler;
+ int compress_level;
+ bool try_plain;
+};
+
+struct compress_fs_file {
+ struct fs_file file;
+ struct compress_fs *fs;
+ struct fs_file *super_read;
+ enum fs_open_mode open_mode;
+ struct istream *input;
+
+ struct ostream *super_output;
+ struct ostream *temp_output;
+};
+
+#define COMPRESS_FS(ptr) container_of((ptr), struct compress_fs, fs)
+#define COMPRESS_FILE(ptr) container_of((ptr), struct compress_fs_file, file)
+
+extern const struct fs fs_class_compress;
+
+static struct fs *fs_compress_alloc(void)
+{
+ struct compress_fs *fs;
+
+ fs = i_new(struct compress_fs, 1);
+ fs->fs = fs_class_compress;
+ return &fs->fs;
+}
+
+static int
+fs_compress_init(struct fs *_fs, const char *args,
+ const struct fs_settings *set, const char **error_r)
+{
+ struct compress_fs *fs = COMPRESS_FS(_fs);
+ const char *p, *compression_name, *level_str;
+ const char *parent_name, *parent_args;
+ int ret;
+
+ /* get compression handler name */
+ if (str_begins(args, "maybe-")) {
+ fs->try_plain = TRUE;
+ args += 6;
+ }
+
+ p = strchr(args, ':');
+ if (p == NULL) {
+ *error_r = "Compression method not given as parameter";
+ return -1;
+ }
+ compression_name = t_strdup_until(args, p++);
+ args = p;
+
+ /* get compression level */
+ p = strchr(args, ':');
+ if (p == NULL || p[1] == '\0') {
+ *error_r = "Parent filesystem not given as parameter";
+ return -1;
+ }
+
+ level_str = t_strdup_until(args, p++);
+ args = p;
+ ret = compression_lookup_handler(compression_name, &fs->compress_handler);
+ if (ret <= 0) {
+ *error_r = t_strdup_printf("Compression method '%s' %s.",
+ compression_name, ret == 0 ?
+ "not supported" : "unknown");
+ return -1;
+ }
+ if (str_to_int(level_str, &fs->compress_level) < 0 ||
+ fs->compress_level < fs->compress_handler->get_min_level() ||
+ fs->compress_level > fs->compress_handler->get_max_level()) {
+ *error_r = t_strdup_printf(
+ "Invalid compression level parameter '%s': "
+ "Level must be between %d..%d", level_str,
+ fs->compress_handler->get_min_level(),
+ fs->compress_handler->get_max_level());
+ 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++;
+ }
+ return fs_init(parent_name, parent_args, set, &_fs->parent, error_r);
+}
+
+static void fs_compress_free(struct fs *_fs)
+{
+ struct compress_fs *fs = COMPRESS_FS(_fs);
+
+ i_free(fs);
+}
+
+static struct fs_file *fs_compress_file_alloc(void)
+{
+ struct compress_fs_file *file = i_new(struct compress_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_compress_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct compress_fs *fs = COMPRESS_FS(_file->fs);
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+
+ 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 (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;
+ }
+}
+
+static void fs_compress_file_deinit(struct fs_file *_file)
+{
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+
+ if (file->super_read != _file->parent)
+ fs_file_deinit(&file->super_read);
+ fs_file_free(_file);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_compress_file_close(struct fs_file *_file)
+{
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+
+ i_stream_unref(&file->input);
+ fs_file_close(file->super_read);
+ fs_file_close(_file->parent);
+}
+
+static struct istream *
+fs_compress_read_stream(struct fs_file *_file, size_t max_buffer_size)
+{
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+ struct istream *input;
+ enum istream_decompress_flags flags = 0;
+
+ 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);
+ if (file->fs->try_plain)
+ flags |= ISTREAM_DECOMPRESS_FLAG_TRY;
+ file->input = i_stream_create_decompress(input, flags);
+ i_stream_unref(&input);
+ i_stream_ref(file->input);
+ return file->input;
+}
+
+static void fs_compress_write_stream(struct fs_file *_file)
+{
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+
+ if (file->fs->compress_level == 0) {
+ fs_wrapper_write_stream(_file);
+ return;
+ }
+
+ i_assert(_file->output == NULL);
+
+ file->temp_output =
+ iostream_temp_create_named(_file->fs->temp_path_prefix,
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP,
+ fs_file_path(_file));
+ _file->output = file->fs->compress_handler->
+ create_ostream(file->temp_output, file->fs->compress_level);
+}
+
+static int fs_compress_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct compress_fs_file *file = COMPRESS_FILE(_file);
+ struct istream *input;
+ int ret;
+
+ if (file->fs->compress_level == 0)
+ return fs_wrapper_write_stream_finish(_file, success);
+
+ if (_file->output != NULL) {
+ if (_file->output->closed)
+ success = FALSE;
+ if (_file->output == file->super_output)
+ _file->output = NULL;
+ else
+ o_stream_unref(&_file->output);
+ }
+ if (!success) {
+ o_stream_destroy(&file->temp_output);
+ if (file->super_output != NULL)
+ fs_write_stream_abort_parent(_file, &file->super_output);
+ return -1;
+ }
+
+ if (file->super_output != NULL) {
+ 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(_file->parent, &file->temp_output);
+ }
+ /* finish writing the temporary file */
+ input = iostream_temp_finish(&file->temp_output, IO_BLOCK_SIZE);
+ file->super_output = fs_write_stream(_file->parent);
+ o_stream_nsend_istream(file->super_output, input);
+ ret = fs_write_stream_finish(_file->parent, &file->super_output);
+ i_stream_unref(&input);
+ return ret;
+}
+
+const struct fs fs_class_compress = {
+ .name = "compress",
+ .v = {
+ fs_compress_alloc,
+ fs_compress_init,
+ NULL,
+ fs_compress_free,
+ fs_wrapper_get_properties,
+ fs_compress_file_alloc,
+ fs_compress_file_init,
+ fs_compress_file_deinit,
+ fs_compress_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_read_via_stream,
+ fs_compress_read_stream,
+ fs_write_via_stream,
+ fs_compress_write_stream,
+ fs_compress_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_wrapper_stat,
+ fs_wrapper_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/plugins/fts-lucene/Makefile.am b/src/plugins/fts-lucene/Makefile.am
new file mode 100644
index 0000000..d68e6ae
--- /dev/null
+++ b/src/plugins/fts-lucene/Makefile.am
@@ -0,0 +1,61 @@
+doveadm_moduledir = $(moduledir)/doveadm
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/fts \
+ -I$(top_srcdir)/src/doveadm
+
+AM_CXXFLAGS = \
+ $(CLUCENE_CFLAGS) \
+ $(LIBEXTTEXTCAT_CFLAGS)
+
+NOPLUGIN_LDFLAGS =
+lib21_fts_lucene_plugin_la_LDFLAGS = -module -avoid-version
+lib20_doveadm_fts_lucene_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib21_fts_lucene_plugin.la
+
+if BUILD_FTS_STEMMER
+STEMMER_LIBS = -lstemmer
+SHOWBALL_SOURCES = Snowball.cc
+endif
+
+if BUILD_FTS_EXTTEXTCAT
+TEXTCAT_LIBS = $(LIBEXTTEXTCAT_LIBS)
+else
+if BUILD_FTS_TEXTCAT
+TEXTCAT_LIBS = -ltextcat
+endif
+endif
+
+lib21_fts_lucene_plugin_la_LIBADD = \
+ $(CLUCENE_LIBS) $(TEXTCAT_LIBS) $(STEMMER_LIBS)
+
+lib21_fts_lucene_plugin_la_SOURCES = \
+ fts-lucene-plugin.c \
+ fts-backend-lucene.c \
+ lucene-wrapper.cc \
+ $(SHOWBALL_SOURCES)
+
+noinst_HEADERS = \
+ fts-lucene-plugin.h \
+ lucene-wrapper.h \
+ SnowballAnalyzer.h \
+ SnowballFilter.h
+
+if BUILD_FTS_TEXTCAT
+exampledir = $(docdir)/example-config
+example_DATA = \
+ textcat.conf
+endif
+EXTRA_DIST = textcat.conf
+
+doveadm_module_LTLIBRARIES = \
+ lib20_doveadm_fts_lucene_plugin.la
+
+lib20_doveadm_fts_lucene_plugin_la_SOURCES = \
+ doveadm-fts-lucene.c
diff --git a/src/plugins/fts-lucene/Makefile.in b/src/plugins/fts-lucene/Makefile.in
new file mode 100644
index 0000000..3359f08
--- /dev/null
+++ b/src/plugins/fts-lucene/Makefile.in
@@ -0,0 +1,990 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/fts-lucene
+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__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)$(doveadm_moduledir)" \
+ "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(exampledir)"
+LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES)
+lib20_doveadm_fts_lucene_plugin_la_LIBADD =
+am_lib20_doveadm_fts_lucene_plugin_la_OBJECTS = doveadm-fts-lucene.lo
+lib20_doveadm_fts_lucene_plugin_la_OBJECTS = \
+ $(am_lib20_doveadm_fts_lucene_plugin_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 =
+lib20_doveadm_fts_lucene_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) \
+ --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link \
+ $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_doveadm_fts_lucene_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+@BUILD_FTS_EXTTEXTCAT_TRUE@am__DEPENDENCIES_2 = $(am__DEPENDENCIES_1)
+lib21_fts_lucene_plugin_la_DEPENDENCIES = $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_2) $(am__DEPENDENCIES_1)
+am__lib21_fts_lucene_plugin_la_SOURCES_DIST = fts-lucene-plugin.c \
+ fts-backend-lucene.c lucene-wrapper.cc Snowball.cc
+@BUILD_FTS_STEMMER_TRUE@am__objects_1 = Snowball.lo
+am_lib21_fts_lucene_plugin_la_OBJECTS = fts-lucene-plugin.lo \
+ fts-backend-lucene.lo lucene-wrapper.lo $(am__objects_1)
+lib21_fts_lucene_plugin_la_OBJECTS = \
+ $(am_lib21_fts_lucene_plugin_la_OBJECTS)
+lib21_fts_lucene_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) \
+ $(lib21_fts_lucene_plugin_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)/Snowball.Plo \
+ ./$(DEPDIR)/doveadm-fts-lucene.Plo \
+ ./$(DEPDIR)/fts-backend-lucene.Plo \
+ ./$(DEPDIR)/fts-lucene-plugin.Plo \
+ ./$(DEPDIR)/lucene-wrapper.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 =
+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 = $(lib20_doveadm_fts_lucene_plugin_la_SOURCES) \
+ $(lib21_fts_lucene_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_doveadm_fts_lucene_plugin_la_SOURCES) \
+ $(am__lib21_fts_lucene_plugin_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
+DATA = $(example_DATA)
+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@
+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@
+doveadm_moduledir = $(moduledir)/doveadm
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/fts \
+ -I$(top_srcdir)/src/doveadm
+
+AM_CXXFLAGS = \
+ $(CLUCENE_CFLAGS) \
+ $(LIBEXTTEXTCAT_CFLAGS)
+
+lib21_fts_lucene_plugin_la_LDFLAGS = -module -avoid-version
+lib20_doveadm_fts_lucene_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib21_fts_lucene_plugin.la
+
+@BUILD_FTS_STEMMER_TRUE@STEMMER_LIBS = -lstemmer
+@BUILD_FTS_STEMMER_TRUE@SHOWBALL_SOURCES = Snowball.cc
+@BUILD_FTS_EXTTEXTCAT_FALSE@@BUILD_FTS_TEXTCAT_TRUE@TEXTCAT_LIBS = -ltextcat
+@BUILD_FTS_EXTTEXTCAT_TRUE@TEXTCAT_LIBS = $(LIBEXTTEXTCAT_LIBS)
+lib21_fts_lucene_plugin_la_LIBADD = \
+ $(CLUCENE_LIBS) $(TEXTCAT_LIBS) $(STEMMER_LIBS)
+
+lib21_fts_lucene_plugin_la_SOURCES = \
+ fts-lucene-plugin.c \
+ fts-backend-lucene.c \
+ lucene-wrapper.cc \
+ $(SHOWBALL_SOURCES)
+
+noinst_HEADERS = \
+ fts-lucene-plugin.h \
+ lucene-wrapper.h \
+ SnowballAnalyzer.h \
+ SnowballFilter.h
+
+@BUILD_FTS_TEXTCAT_TRUE@exampledir = $(docdir)/example-config
+@BUILD_FTS_TEXTCAT_TRUE@example_DATA = \
+@BUILD_FTS_TEXTCAT_TRUE@ textcat.conf
+
+EXTRA_DIST = textcat.conf
+doveadm_module_LTLIBRARIES = \
+ lib20_doveadm_fts_lucene_plugin.la
+
+lib20_doveadm_fts_lucene_plugin_la_SOURCES = \
+ doveadm-fts-lucene.c
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .cc .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/plugins/fts-lucene/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/fts-lucene/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-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_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)$(doveadm_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \
+ }
+
+uninstall-doveadm_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \
+ done
+
+clean-doveadm_moduleLTLIBRARIES:
+ -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES)
+ @list='$(doveadm_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}; \
+ }
+
+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}; \
+ }
+
+lib20_doveadm_fts_lucene_plugin.la: $(lib20_doveadm_fts_lucene_plugin_la_OBJECTS) $(lib20_doveadm_fts_lucene_plugin_la_DEPENDENCIES) $(EXTRA_lib20_doveadm_fts_lucene_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_doveadm_fts_lucene_plugin_la_LINK) -rpath $(doveadm_moduledir) $(lib20_doveadm_fts_lucene_plugin_la_OBJECTS) $(lib20_doveadm_fts_lucene_plugin_la_LIBADD) $(LIBS)
+
+lib21_fts_lucene_plugin.la: $(lib21_fts_lucene_plugin_la_OBJECTS) $(lib21_fts_lucene_plugin_la_DEPENDENCIES) $(EXTRA_lib21_fts_lucene_plugin_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(lib21_fts_lucene_plugin_la_LINK) -rpath $(moduledir) $(lib21_fts_lucene_plugin_la_OBJECTS) $(lib21_fts_lucene_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/Snowball.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-fts-lucene.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-backend-lucene.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-lucene-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lucene-wrapper.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 $@ $<
+
+.cc.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 $@ $<
+
+.cc.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) '$<'`
+
+.cc.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 $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-exampleDATA: $(example_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(example_DATA)'; test -n "$(exampledir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(exampledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(exampledir)" || 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)$(exampledir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(exampledir)" || exit $$?; \
+ done
+
+uninstall-exampleDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(example_DATA)'; test -n "$(exampledir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(exampledir)'; $(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) $(DATA) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(exampledir)"; 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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/Snowball.Plo
+ -rm -f ./$(DEPDIR)/doveadm-fts-lucene.Plo
+ -rm -f ./$(DEPDIR)/fts-backend-lucene.Plo
+ -rm -f ./$(DEPDIR)/fts-lucene-plugin.Plo
+ -rm -f ./$(DEPDIR)/lucene-wrapper.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-doveadm_moduleLTLIBRARIES install-exampleDATA \
+ install-moduleLTLIBRARIES
+
+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)/Snowball.Plo
+ -rm -f ./$(DEPDIR)/doveadm-fts-lucene.Plo
+ -rm -f ./$(DEPDIR)/fts-backend-lucene.Plo
+ -rm -f ./$(DEPDIR)/fts-lucene-plugin.Plo
+ -rm -f ./$(DEPDIR)/lucene-wrapper.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-doveadm_moduleLTLIBRARIES \
+ uninstall-exampleDATA uninstall-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-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-doveadm_moduleLTLIBRARIES install-dvi install-dvi-am \
+ install-exampleDATA 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-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-doveadm_moduleLTLIBRARIES uninstall-exampleDATA \
+ uninstall-moduleLTLIBRARIES
+
+.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/plugins/fts-lucene/Snowball.cc b/src/plugins/fts-lucene/Snowball.cc
new file mode 100644
index 0000000..43b54e3
--- /dev/null
+++ b/src/plugins/fts-lucene/Snowball.cc
@@ -0,0 +1,151 @@
+/*------------------------------------------------------------------------------
+* Copyright (C) 2003-2006 Ben van Klinken and the CLucene Team
+*
+* Distributable under the terms of either the Apache License (Version 2.0) or
+* the GNU Lesser General Public License, as specified in the COPYING file.
+------------------------------------------------------------------------------*/
+#include <CLucene.h>
+#include "SnowballAnalyzer.h"
+#include "SnowballFilter.h"
+#include <CLucene/util/CLStreams.h>
+#include <CLucene/analysis/Analyzers.h>
+#include <CLucene/analysis/standard/StandardTokenizer.h>
+#include <CLucene/analysis/standard/StandardFilter.h>
+
+extern "C" {
+#include "lib.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "lucene-wrapper.h"
+};
+
+CL_NS_USE(analysis)
+CL_NS_USE(util)
+CL_NS_USE2(analysis,standard)
+
+CL_NS_DEF2(analysis,snowball)
+
+ /** Builds the named analyzer with no stop words. */
+ SnowballAnalyzer::SnowballAnalyzer(normalizer_func_t *_normalizer, const char* _language)
+ : language(i_strdup(_language)),
+ normalizer(_normalizer),
+ stopSet(NULL),
+ prevstream(NULL)
+ {
+ }
+
+ SnowballAnalyzer::~SnowballAnalyzer()
+ {
+ if (prevstream)
+ _CLDELETE(prevstream);
+ i_free(language);
+ if ( stopSet != NULL )
+ _CLDELETE(stopSet);
+ }
+
+ /** Builds the named analyzer with the given stop words.
+ */
+ SnowballAnalyzer::SnowballAnalyzer(const char* language, const TCHAR** stopWords)
+ : language(i_strdup(language)),
+ normalizer(NULL),
+ stopSet(_CLNEW CLTCSetList(true)),
+ prevstream(NULL)
+ {
+ StopFilter::fillStopTable(stopSet,stopWords);
+ }
+
+ TokenStream* SnowballAnalyzer::tokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader) {
+ return this->tokenStream(fieldName,reader,false);
+ }
+
+ /** Constructs a {@link StandardTokenizer} filtered by a {@link
+ StandardFilter}, a {@link LowerCaseFilter} and a {@link StopFilter}. */
+ TokenStream* SnowballAnalyzer::tokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader, bool deleteReader) {
+ BufferedReader* bufferedReader = reader->__asBufferedReader();
+ TokenStream* result;
+
+ if ( bufferedReader == NULL )
+ result = _CLNEW StandardTokenizer( _CLNEW FilteredBufferedReader(reader, deleteReader), true );
+ else
+ result = _CLNEW StandardTokenizer(bufferedReader, deleteReader);
+
+ result = _CLNEW StandardFilter(result, true);
+ result = _CLNEW CL_NS(analysis)::LowerCaseFilter(result, true);
+ if (stopSet != NULL)
+ result = _CLNEW CL_NS(analysis)::StopFilter(result, true, stopSet);
+ result = _CLNEW SnowballFilter(result, normalizer, language, true);
+ return result;
+ }
+
+ TokenStream* SnowballAnalyzer::reusableTokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader) {
+ if (prevstream) _CLDELETE(prevstream);
+ prevstream = this->tokenStream(fieldName, reader);
+ return prevstream;
+ }
+
+
+
+
+
+
+ /** Construct the named stemming filter.
+ *
+ * @param in the input tokens to stem
+ * @param name the name of a stemmer
+ */
+ SnowballFilter::SnowballFilter(TokenStream* in, normalizer_func_t *normalizer, const char* language, bool deleteTS):
+ TokenFilter(in,deleteTS)
+ {
+ stemmer = sb_stemmer_new(language, NULL); //use utf8 encoding
+ this->normalizer = normalizer;
+
+ if ( stemmer == NULL ){
+ _CLTHROWA(CL_ERR_IllegalArgument, "language not available for stemming\n"); //todo: richer error
+ }
+ }
+
+ SnowballFilter::~SnowballFilter(){
+ sb_stemmer_delete(stemmer);
+ }
+
+ /** Returns the next input Token, after being stemmed */
+ Token* SnowballFilter::next(Token* token){
+ if (input->next(token) == NULL)
+ return NULL;
+
+ unsigned char utf8text[LUCENE_MAX_WORD_LEN*5+1];
+ unsigned int len = I_MIN(LUCENE_MAX_WORD_LEN, token->termLength());
+
+ buffer_t buf = { { 0, 0 } };
+ i_assert(sizeof(wchar_t) == sizeof(unichar_t));
+ buffer_create_from_data(&buf, utf8text, sizeof(utf8text));
+ uni_ucs4_to_utf8((const unichar_t *)token->termBuffer(), len, &buf);
+
+ const sb_symbol* stemmed = sb_stemmer_stem(stemmer, utf8text, buf.used);
+ if ( stemmed == NULL )
+ _CLTHROWA(CL_ERR_Runtime,"Out of memory");
+
+ int stemmedLen=sb_stemmer_length(stemmer);
+
+ if (normalizer == NULL) {
+ unsigned int tchartext_size =
+ uni_utf8_strlen_n(stemmed, stemmedLen) + 1;
+ TCHAR tchartext[tchartext_size];
+ lucene_utf8_n_to_tchar(stemmed, stemmedLen, tchartext, tchartext_size);
+ token->set(tchartext,token->startOffset(), token->endOffset(), token->type());
+ } else T_BEGIN {
+ buffer_t *norm_buf = t_buffer_create(stemmedLen);
+ normalizer(stemmed, stemmedLen, norm_buf);
+
+ unsigned int tchartext_size =
+ uni_utf8_strlen_n(norm_buf->data, norm_buf->used) + 1;
+ TCHAR tchartext[tchartext_size];
+ lucene_utf8_n_to_tchar((const unsigned char *)norm_buf->data,
+ norm_buf->used, tchartext, tchartext_size);
+ token->set(tchartext,token->startOffset(), token->endOffset(), token->type());
+ } T_END;
+ return token;
+ }
+
+
+CL_NS_END2
diff --git a/src/plugins/fts-lucene/SnowballAnalyzer.h b/src/plugins/fts-lucene/SnowballAnalyzer.h
new file mode 100644
index 0000000..45455c5
--- /dev/null
+++ b/src/plugins/fts-lucene/SnowballAnalyzer.h
@@ -0,0 +1,51 @@
+/*------------------------------------------------------------------------------
+* Copyright (C) 2003-2006 Ben van Klinken and the CLucene Team
+*
+* Distributable under the terms of either the Apache License (Version 2.0) or
+* the GNU Lesser General Public License, as specified in the COPYING file.
+------------------------------------------------------------------------------*/
+#ifndef _lucene_analysis_snowball_analyser_
+#define _lucene_analysis_snowball_analyser_
+
+extern "C" {
+#include "lib.h"
+#include "unichar.h"
+};
+#include "CLucene/analysis/AnalysisHeader.h"
+
+CL_CLASS_DEF(util,BufferedReader)
+CL_NS_DEF2(analysis,snowball)
+
+/** Filters {@link StandardTokenizer} with {@link StandardFilter}, {@link
+ * LowerCaseFilter}, {@link StopFilter} and {@link SnowballFilter}.
+ *
+ * Available stemmers are listed in {@link net.sf.snowball.ext}. The name of a
+ * stemmer is the part of the class name before "Stemmer", e.g., the stemmer in
+ * {@link EnglishStemmer} is named "English".
+ */
+class CLUCENE_CONTRIBS_EXPORT SnowballAnalyzer: public Analyzer {
+ char* language;
+ normalizer_func_t *normalizer;
+ CLTCSetList* stopSet;
+ TokenStream *prevstream;
+
+public:
+ /** Builds the named analyzer with no stop words. */
+ SnowballAnalyzer(normalizer_func_t *normalizer, const char* language="english");
+
+ /** Builds the named analyzer with the given stop words.
+ */
+ SnowballAnalyzer(const char* language, const TCHAR** stopWords);
+
+ ~SnowballAnalyzer();
+
+ /** Constructs a {@link StandardTokenizer} filtered by a {@link
+ StandardFilter}, a {@link LowerCaseFilter} and a {@link StopFilter}. */
+ TokenStream* tokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader);
+ TokenStream* tokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader, bool deleteReader);
+ TokenStream* reusableTokenStream(const TCHAR* fieldName, CL_NS(util)::Reader* reader);
+};
+
+CL_NS_END2
+#endif
+
diff --git a/src/plugins/fts-lucene/SnowballFilter.h b/src/plugins/fts-lucene/SnowballFilter.h
new file mode 100644
index 0000000..6a0ed12
--- /dev/null
+++ b/src/plugins/fts-lucene/SnowballFilter.h
@@ -0,0 +1,42 @@
+/*------------------------------------------------------------------------------
+* Copyright (C) 2003-2006 Ben van Klinken and the CLucene Team
+*
+* Distributable under the terms of either the Apache License (Version 2.0) or
+* the GNU Lesser General Public License, as specified in the COPYING file.
+------------------------------------------------------------------------------*/
+#ifndef _lucene_analysis_snowball_filter_
+#define _lucene_analysis_snowball_filter_
+
+#include "CLucene/analysis/AnalysisHeader.h"
+#include "libstemmer.h"
+
+CL_NS_DEF2(analysis,snowball)
+
+/** A filter that stems words using a Snowball-generated stemmer.
+ *
+ * Available stemmers are listed in {@link net.sf.snowball.ext}. The name of a
+ * stemmer is the part of the class name before "Stemmer", e.g., the stemmer in
+ * {@link EnglishStemmer} is named "English".
+ *
+ * Note: todo: This is not thread safe...
+ */
+class CLUCENE_CONTRIBS_EXPORT SnowballFilter: public TokenFilter {
+ struct sb_stemmer * stemmer;
+ normalizer_func_t *normalizer;
+public:
+
+ /** Construct the named stemming filter.
+ *
+ * @param in the input tokens to stem
+ * @param name the name of a stemmer
+ */
+ SnowballFilter(TokenStream* in, normalizer_func_t *normalizer, const char* language, bool deleteTS);
+
+ ~SnowballFilter();
+
+ /** Returns the next input Token, after being stemmed */
+ Token* next(Token* token);
+};
+
+CL_NS_END2
+#endif
diff --git a/src/plugins/fts-lucene/doveadm-fts-lucene.c b/src/plugins/fts-lucene/doveadm-fts-lucene.c
new file mode 100644
index 0000000..a761907
--- /dev/null
+++ b/src/plugins/fts-lucene/doveadm-fts-lucene.c
@@ -0,0 +1,70 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "doveadm-dump.h"
+#include "doveadm-fts.h"
+#include "lucene-wrapper.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+const char *doveadm_fts_lucene_plugin_version = DOVECOT_ABI_VERSION;
+
+void doveadm_fts_lucene_plugin_init(struct module *module);
+void doveadm_fts_lucene_plugin_deinit(void);
+
+static void
+cmd_dump_fts_lucene(const char *path, const char *const *args ATTR_UNUSED)
+{
+ struct lucene_index *index;
+ struct lucene_index_iter *iter;
+ guid_128_t prev_guid;
+ const struct lucene_index_record *rec;
+ bool first = TRUE;
+
+ i_zero(&prev_guid);
+ index = lucene_index_init(path, NULL, NULL);
+ iter = lucene_index_iter_init(index);
+ while ((rec = lucene_index_iter_next(iter)) != NULL) {
+ if (memcmp(prev_guid, rec->mailbox_guid,
+ sizeof(prev_guid)) != 0) {
+ if (first)
+ first = FALSE;
+ else
+ printf("\n");
+ memcpy(prev_guid, rec->mailbox_guid, sizeof(prev_guid));
+ printf("%s: ", guid_128_to_string(prev_guid));
+ }
+ printf("%u", rec->uid);
+ if (rec->part_num != 0)
+ printf("[%u]", rec->part_num);
+ printf("\n");
+ }
+ printf("\n");
+ if (lucene_index_iter_deinit(&iter) < 0)
+ i_error("Lucene index iteration failed");
+ lucene_index_deinit(index);
+}
+
+static bool test_dump_fts_lucene(const char *path)
+{
+ struct stat st;
+
+ path = t_strconcat(path, "/segments.gen", NULL);
+ return stat(path, &st) == 0;
+}
+
+static const struct doveadm_cmd_dump doveadm_cmd_dump_fts_lucene = {
+ "fts-lucene",
+ test_dump_fts_lucene,
+ cmd_dump_fts_lucene
+};
+
+void doveadm_fts_lucene_plugin_init(struct module *module ATTR_UNUSED)
+{
+ doveadm_dump_register(&doveadm_cmd_dump_fts_lucene);
+}
+
+void doveadm_fts_lucene_plugin_deinit(void)
+{
+}
diff --git a/src/plugins/fts-lucene/fts-backend-lucene.c b/src/plugins/fts-lucene/fts-backend-lucene.c
new file mode 100644
index 0000000..963dbdf
--- /dev/null
+++ b/src/plugins/fts-lucene/fts-backend-lucene.c
@@ -0,0 +1,605 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "strescape.h"
+#include "message-part.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "fts-expunge-log.h"
+#include "lucene-wrapper.h"
+#include "fts-indexer.h"
+#include "fts-lucene-plugin.h"
+
+#include <wchar.h>
+
+#define LUCENE_INDEX_DIR_NAME "lucene-indexes"
+#define LUCENE_EXPUNGE_LOG_NAME "dovecot-expunges.log"
+#define LUCENE_OPTIMIZE_BATCH_MSGS_COUNT 100
+
+struct lucene_fts_backend {
+ struct fts_backend backend;
+ char *dir_path;
+
+ struct lucene_index *index;
+ struct mailbox *selected_box;
+ unsigned int selected_box_generation;
+ guid_128_t selected_box_guid;
+
+ struct fts_expunge_log *expunge_log;
+
+ bool dir_created:1;
+ bool updating:1;
+};
+
+struct lucene_fts_backend_update_context {
+ struct fts_backend_update_context ctx;
+
+ struct mailbox *box;
+ uint32_t last_uid;
+ uint32_t last_indexed_uid;
+ char *first_box_vname;
+
+ uint32_t uid, part_num;
+ char *hdr_name;
+
+ unsigned int added_msgs;
+ struct fts_expunge_log_append_ctx *expunge_ctx;
+
+ bool lucene_opened;
+ bool last_indexed_uid_set;
+ bool mime_parts;
+};
+
+static int fts_backend_lucene_mkdir(struct lucene_fts_backend *backend)
+{
+ if (backend->dir_created)
+ return 0;
+
+ backend->dir_created = TRUE;
+ if (mailbox_list_mkdir_root(backend->backend.ns->list,
+ backend->dir_path,
+ MAILBOX_LIST_PATH_TYPE_INDEX) < 0)
+ return -1;
+ return 0;
+}
+
+static int
+fts_lucene_get_mailbox_guid(struct mailbox *box, guid_128_t guid_r)
+{
+ struct mailbox_metadata metadata;
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ i_error("lucene: Couldn't get mailbox %s GUID: %s",
+ box->vname, mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+ memcpy(guid_r, metadata.guid, GUID_128_SIZE);
+ return 0;
+}
+
+static int
+fts_backend_select(struct lucene_fts_backend *backend, struct mailbox *box)
+{
+ guid_128_t guid;
+ unsigned char guid_hex[MAILBOX_GUID_HEX_LENGTH];
+ wchar_t wguid_hex[MAILBOX_GUID_HEX_LENGTH];
+ buffer_t buf;
+ unsigned int i;
+
+ i_assert(box != NULL);
+
+ if (backend->selected_box == box &&
+ backend->selected_box_generation == box->generation_sequence)
+ return 0;
+
+ if (fts_lucene_get_mailbox_guid(box, guid) < 0)
+ return -1;
+ buffer_create_from_data(&buf, guid_hex, MAILBOX_GUID_HEX_LENGTH);
+ binary_to_hex_append(&buf, guid, GUID_128_SIZE);
+ for (i = 0; i < N_ELEMENTS(wguid_hex); i++)
+ wguid_hex[i] = guid_hex[i];
+
+ lucene_index_select_mailbox(backend->index, wguid_hex);
+
+ backend->selected_box = box;
+ memcpy(backend->selected_box_guid, guid,
+ sizeof(backend->selected_box_guid));
+ backend->selected_box_generation = box->generation_sequence;
+ return 0;
+}
+
+static struct fts_backend *fts_backend_lucene_alloc(void)
+{
+ struct lucene_fts_backend *backend;
+
+ backend = i_new(struct lucene_fts_backend, 1);
+ backend->backend = fts_backend_lucene;
+ return &backend->backend;
+}
+
+static int
+fts_backend_lucene_init(struct fts_backend *_backend, const char **error_r)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ struct fts_lucene_user *fuser =
+ FTS_LUCENE_USER_CONTEXT(_backend->ns->user);
+ const char *path;
+
+ if (fuser == NULL) {
+ /* invalid settings */
+ *error_r = "Invalid fts_lucene settings";
+ return -1;
+ }
+ /* fts already checked that index exists */
+
+ if (fuser->set.use_libfts) {
+ /* change our flags so we get proper input */
+ _backend->flags &= ENUM_NEGATE(FTS_BACKEND_FLAG_FUZZY_SEARCH);
+ _backend->flags |= FTS_BACKEND_FLAG_TOKENIZED_INPUT;
+ }
+ path = mailbox_list_get_root_forced(_backend->ns->list,
+ MAILBOX_LIST_PATH_TYPE_INDEX);
+
+ backend->dir_path = i_strconcat(path, "/"LUCENE_INDEX_DIR_NAME, NULL);
+ backend->index = lucene_index_init(backend->dir_path,
+ _backend->ns->list,
+ &fuser->set);
+
+ path = t_strconcat(backend->dir_path, "/"LUCENE_EXPUNGE_LOG_NAME, NULL);
+ backend->expunge_log = fts_expunge_log_init(path);
+ return 0;
+}
+
+static void fts_backend_lucene_deinit(struct fts_backend *_backend)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+
+ if (backend->index != NULL)
+ lucene_index_deinit(backend->index);
+ if (backend->expunge_log != NULL)
+ fts_expunge_log_deinit(&backend->expunge_log);
+ i_free(backend->dir_path);
+ i_free(backend);
+}
+
+static int
+fts_backend_lucene_get_last_uid(struct fts_backend *_backend,
+ struct mailbox *box, uint32_t *last_uid_r)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ struct fts_lucene_user *fuser =
+ FTS_LUCENE_USER_CONTEXT_REQUIRE(_backend->ns->user);
+ struct fts_index_header hdr;
+ uint32_t set_checksum;
+ int ret;
+
+ if (fts_index_get_header(box, &hdr)) {
+ set_checksum = fts_lucene_settings_checksum(&fuser->set);
+ ret = fts_index_have_compatible_settings(_backend->ns->list,
+ set_checksum);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ /* need to rebuild the index */
+ *last_uid_r = 0;
+ } else {
+ *last_uid_r = hdr.last_indexed_uid;
+ }
+ return 0;
+ }
+
+ /* either nothing has been indexed, or the index was corrupted.
+ do it the slow way. */
+ if (fts_backend_select(backend, box) < 0)
+ return -1;
+ if (lucene_index_get_last_uid(backend->index, last_uid_r) < 0)
+ return -1;
+
+ fts_index_set_last_uid(box, *last_uid_r);
+ return 0;
+}
+
+static struct fts_backend_update_context *
+fts_backend_lucene_update_init(struct fts_backend *_backend)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ struct lucene_fts_backend_update_context *ctx;
+ struct fts_lucene_user *fuser =
+ FTS_LUCENE_USER_CONTEXT_REQUIRE(_backend->ns->user);
+
+ i_assert(!backend->updating);
+
+ ctx = i_new(struct lucene_fts_backend_update_context, 1);
+ ctx->ctx.backend = _backend;
+ ctx->mime_parts = fuser->set.mime_parts;
+ backend->updating = TRUE;
+ return &ctx->ctx;
+}
+
+static bool
+fts_backend_lucene_need_optimize(struct lucene_fts_backend_update_context *ctx)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)ctx->ctx.backend;
+ unsigned int expunges;
+ uint32_t numdocs;
+
+ if (ctx->added_msgs >= LUCENE_OPTIMIZE_BATCH_MSGS_COUNT)
+ return TRUE;
+ if (lucene_index_get_doc_count(backend->index, &numdocs) < 0)
+ return FALSE;
+
+ if (fts_expunge_log_uid_count(backend->expunge_log, &expunges) < 0)
+ return FALSE;
+ return expunges > 0 &&
+ numdocs / expunges <= 50; /* >2% of index has been expunged */
+}
+
+static int
+fts_backend_lucene_update_deinit(struct fts_backend_update_context *_ctx)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_ctx->backend;
+ int ret = _ctx->failed ? -1 : 0;
+
+ i_assert(backend->updating);
+
+ backend->updating = FALSE;
+ if (ctx->lucene_opened) {
+ if (lucene_index_build_deinit(backend->index) < 0)
+ ret = -1;
+ }
+
+ if (ctx->expunge_ctx != NULL) {
+ if (fts_expunge_log_append_commit(&ctx->expunge_ctx) < 0) {
+ struct stat st;
+ ret = -1;
+
+ if (stat(backend->dir_path, &st) < 0 && errno == ENOENT) {
+ /* lucene-indexes directory doesn't even exist,
+ so dovecot.index's last_index_uid is wrong.
+ rescan to update them. */
+ (void)lucene_index_rescan(backend->index);
+ ret = 0;
+ }
+ }
+ }
+
+ if (fts_backend_lucene_need_optimize(ctx)) {
+ if (ctx->lucene_opened)
+ (void)fts_backend_optimize(_ctx->backend);
+ else if (ctx->first_box_vname != NULL) {
+ struct mail_user *user = backend->backend.ns->user;
+ const char *cmd, *path;
+ int fd;
+
+ /* the optimize affects all mailboxes within namespace,
+ so just use any mailbox name in it */
+ cmd = t_strdup_printf("OPTIMIZE\t0\t%s\t%s\n",
+ str_tabescape(user->username),
+ str_tabescape(ctx->first_box_vname));
+ fd = fts_indexer_cmd(user, cmd, &path);
+ i_close_fd(&fd);
+ }
+ }
+
+ i_free(ctx->first_box_vname);
+ i_free(ctx);
+ return ret;
+}
+
+static void
+fts_backend_lucene_update_set_mailbox(struct fts_backend_update_context *_ctx,
+ struct mailbox *box)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+
+ if (ctx->last_uid != 0) {
+ fts_index_set_last_uid(ctx->box, ctx->last_uid);
+ ctx->last_uid = 0;
+ }
+ if (ctx->first_box_vname == NULL && box != NULL)
+ ctx->first_box_vname = i_strdup(box->vname);
+ ctx->box = box;
+ ctx->last_indexed_uid_set = FALSE;
+}
+
+static void
+fts_backend_lucene_update_expunge(struct fts_backend_update_context *_ctx,
+ uint32_t uid)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_ctx->backend;
+ struct fts_index_header hdr;
+
+ if (!ctx->last_indexed_uid_set) {
+ if (!fts_index_get_header(ctx->box, &hdr))
+ ctx->last_indexed_uid = 0;
+ else
+ ctx->last_indexed_uid = hdr.last_indexed_uid;
+ ctx->last_indexed_uid_set = TRUE;
+ }
+ if (ctx->last_indexed_uid == 0 ||
+ uid > ctx->last_indexed_uid + 100) {
+ /* don't waste time adding expunge to log for a message that
+ isn't even indexed. this check is racy, because indexer may
+ just be in the middle of indexing this message. we'll
+ attempt to avoid that by skipping the expunging only if
+ indexing hasn't been done for a while (100 msgs). */
+ return;
+ }
+
+ if (ctx->expunge_ctx == NULL) {
+ ctx->expunge_ctx =
+ fts_expunge_log_append_begin(backend->expunge_log);
+ }
+
+ if (fts_backend_select(backend, ctx->box) < 0)
+ _ctx->failed = TRUE;
+
+ fts_expunge_log_append_next(ctx->expunge_ctx,
+ backend->selected_box_guid, uid);
+}
+
+static bool
+fts_backend_lucene_update_set_build_key(struct fts_backend_update_context *_ctx,
+ const struct fts_backend_build_key *key)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_ctx->backend;
+
+ if (!ctx->lucene_opened) {
+ if (fts_backend_lucene_mkdir(backend) < 0)
+ ctx->ctx.failed = TRUE;
+ if (lucene_index_build_init(backend->index) < 0)
+ ctx->ctx.failed = TRUE;
+ ctx->lucene_opened = TRUE;
+ }
+
+ if (fts_backend_select(backend, ctx->box) < 0)
+ _ctx->failed = TRUE;
+
+ switch (key->type) {
+ case FTS_BACKEND_BUILD_KEY_HDR:
+ case FTS_BACKEND_BUILD_KEY_MIME_HDR:
+ i_assert(key->hdr_name != NULL);
+
+ i_free(ctx->hdr_name);
+ ctx->hdr_name = i_strdup(key->hdr_name);
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART:
+ i_free_and_null(ctx->hdr_name);
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY:
+ i_unreached();
+ }
+
+ if (key->uid != ctx->last_uid) {
+ i_assert(key->uid >= ctx->last_uid);
+ ctx->last_uid = key->uid;
+ ctx->added_msgs++;
+ }
+
+ ctx->uid = key->uid;
+ if (ctx->mime_parts)
+ ctx->part_num = message_part_to_idx(key->part);
+ return TRUE;
+}
+
+static void
+fts_backend_lucene_update_unset_build_key(struct fts_backend_update_context *_ctx)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+
+ ctx->uid = 0;
+ ctx->part_num = 0;
+ i_free_and_null(ctx->hdr_name);
+}
+
+static int
+fts_backend_lucene_update_build_more(struct fts_backend_update_context *_ctx,
+ const unsigned char *data, size_t size)
+{
+ struct lucene_fts_backend_update_context *ctx =
+ (struct lucene_fts_backend_update_context *)_ctx;
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_ctx->backend;
+ int ret;
+
+ i_assert(ctx->uid != 0);
+
+ if (_ctx->failed)
+ return -1;
+
+ T_BEGIN {
+ ret = lucene_index_build_more(backend->index, ctx->uid,
+ ctx->part_num, data, size,
+ ctx->hdr_name);
+ } T_END;
+ return ret;
+}
+
+static int
+fts_backend_lucene_refresh(struct fts_backend *_backend)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+
+ if (backend->index != NULL)
+ lucene_index_close(backend->index);
+ return 0;
+}
+
+static int fts_backend_lucene_rescan(struct fts_backend *_backend)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+
+ if (lucene_index_rescan(backend->index) < 0)
+ return -1;
+ return lucene_index_optimize(backend->index);
+}
+
+static int fts_backend_lucene_optimize(struct fts_backend *_backend)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ int ret;
+
+ ret = lucene_index_expunge_from_log(backend->index,
+ backend->expunge_log);
+ if (ret == 0) {
+ /* log was corrupted, need to rescan */
+ ret = lucene_index_rescan(backend->index);
+ }
+ if (ret >= 0)
+ ret = lucene_index_optimize(backend->index);
+ return ret;
+}
+
+static int
+fts_backend_lucene_lookup(struct fts_backend *_backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ int ret;
+
+ if (fts_backend_select(backend, box) < 0)
+ return -1;
+ T_BEGIN {
+ ret = lucene_index_lookup(backend->index, args, flags, result);
+ } T_END;
+ return ret;
+}
+
+/* a char* hash function from ASU -- from glib */
+static unsigned int wstr_hash(const wchar_t *s)
+{
+ 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;
+}
+
+static int
+mailboxes_get_guids(struct mailbox *const boxes[],
+ HASH_TABLE_TYPE(wguid_result) guids,
+ struct fts_multi_result *result)
+{
+ ARRAY(struct fts_result) box_results;
+ struct fts_result *box_result;
+ const char *guid;
+ wchar_t *guid_dup;
+ unsigned int i, j;
+
+ p_array_init(&box_results, result->pool, 32);
+ /* first create the box_results - we'll be using pointers to them
+ later on and appending to the array changes the pointers */
+ for (i = 0; boxes[i] != NULL; i++) {
+ box_result = array_append_space(&box_results);
+ box_result->box = boxes[i];
+ }
+ for (i = 0; boxes[i] != NULL; i++) {
+ if (fts_mailbox_get_guid(boxes[i], &guid) < 0)
+ return -1;
+
+ i_assert(strlen(guid) == MAILBOX_GUID_HEX_LENGTH);
+ guid_dup = t_new(wchar_t, MAILBOX_GUID_HEX_LENGTH + 1);
+ for (j = 0; j < MAILBOX_GUID_HEX_LENGTH; j++)
+ guid_dup[j] = guid[j];
+
+ box_result = array_idx_modifiable(&box_results, i);
+ hash_table_insert(guids, guid_dup, box_result);
+ }
+
+ array_append_zero(&box_results);
+ result->box_results = array_front_modifiable(&box_results);
+ return 0;
+}
+
+static int
+fts_backend_lucene_lookup_multi(struct fts_backend *_backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ struct lucene_fts_backend *backend =
+ (struct lucene_fts_backend *)_backend;
+ int ret;
+
+ T_BEGIN {
+ HASH_TABLE_TYPE(wguid_result) guids;
+
+ hash_table_create(&guids, default_pool, 0, wstr_hash, wcscmp);
+ ret = mailboxes_get_guids(boxes, guids, result);
+ if (ret == 0) {
+ ret = lucene_index_lookup_multi(backend->index,
+ guids, args, flags,
+ result);
+ }
+ hash_table_destroy(&guids);
+ } T_END;
+ return ret;
+}
+
+static void fts_backend_lucene_lookup_done(struct fts_backend *_backend)
+{
+ /* the next refresh is going to close the index anyway, so we might as
+ well do it now */
+ (void)fts_backend_lucene_refresh(_backend);
+}
+
+struct fts_backend fts_backend_lucene = {
+ .name = "lucene",
+ .flags = FTS_BACKEND_FLAG_BUILD_FULL_WORDS |
+ FTS_BACKEND_FLAG_FUZZY_SEARCH,
+
+ {
+ fts_backend_lucene_alloc,
+ fts_backend_lucene_init,
+ fts_backend_lucene_deinit,
+ fts_backend_lucene_get_last_uid,
+ fts_backend_lucene_update_init,
+ fts_backend_lucene_update_deinit,
+ fts_backend_lucene_update_set_mailbox,
+ fts_backend_lucene_update_expunge,
+ fts_backend_lucene_update_set_build_key,
+ fts_backend_lucene_update_unset_build_key,
+ fts_backend_lucene_update_build_more,
+ fts_backend_lucene_refresh,
+ fts_backend_lucene_rescan,
+ fts_backend_lucene_optimize,
+ fts_backend_default_can_lookup,
+ fts_backend_lucene_lookup,
+ fts_backend_lucene_lookup_multi,
+ fts_backend_lucene_lookup_done
+ }
+};
diff --git a/src/plugins/fts-lucene/fts-lucene-plugin.c b/src/plugins/fts-lucene/fts-lucene-plugin.c
new file mode 100644
index 0000000..7c58fa7
--- /dev/null
+++ b/src/plugins/fts-lucene/fts-lucene-plugin.c
@@ -0,0 +1,146 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "crc32.h"
+#include "mail-storage-hooks.h"
+#include "lucene-wrapper.h"
+#include "fts-user.h"
+#include "fts-lucene-plugin.h"
+
+const char *fts_lucene_plugin_version = DOVECOT_ABI_VERSION;
+
+struct fts_lucene_user_module fts_lucene_user_module =
+ MODULE_CONTEXT_INIT(&mail_user_module_register);
+
+static int
+fts_lucene_plugin_init_settings(struct mail_user *user,
+ struct fts_lucene_settings *set,
+ const char *str)
+{
+ const char *const *tmp;
+
+ for (tmp = t_strsplit_spaces(str, " "); *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "default_language=")) {
+ set->default_language =
+ p_strdup(user->pool, *tmp + 17);
+ } else if (str_begins(*tmp, "textcat_conf=")) {
+ set->textcat_conf = p_strdup(user->pool, *tmp + 13);
+ } else if (str_begins(*tmp, "textcat_dir=")) {
+ set->textcat_dir = p_strdup(user->pool, *tmp + 12);
+ } else if (str_begins(*tmp, "whitespace_chars=")) {
+ set->whitespace_chars = p_strdup(user->pool, *tmp + 17);
+ } else if (strcmp(*tmp, "normalize") == 0) {
+ set->normalize = TRUE;
+ } else if (strcmp(*tmp, "no_snowball") == 0) {
+ set->no_snowball = TRUE;
+ } else if (strcmp(*tmp, "mime_parts") == 0) {
+ set->mime_parts = TRUE;
+ } else if (strcmp(*tmp, "use_libfts") == 0) {
+ set->use_libfts = TRUE;
+ } else {
+ i_error("fts_lucene: Invalid setting: %s", *tmp);
+ return -1;
+ }
+ }
+ if (set->textcat_conf != NULL && set->textcat_dir == NULL) {
+ i_error("fts_lucene: textcat_conf set, but textcat_dir unset");
+ return -1;
+ }
+ if (set->textcat_conf == NULL && set->textcat_dir != NULL) {
+ i_error("fts_lucene: textcat_dir set, but textcat_conf unset");
+ return -1;
+ }
+ if (set->whitespace_chars == NULL)
+ set->whitespace_chars = "";
+#ifndef HAVE_FTS_STEMMER
+ if (set->default_language != NULL) {
+ i_error("fts_lucene: default_language set, "
+ "but Dovecot built without stemmer support");
+ return -1;
+ }
+#else
+ if (set->default_language == NULL)
+ set->default_language = "english";
+#endif
+#ifndef HAVE_FTS_TEXTCAT
+ if (set->textcat_conf != NULL) {
+ i_error("fts_lucene: textcat_dir set, "
+ "but Dovecot built without textcat support");
+ return -1;
+ }
+#endif
+ return 0;
+}
+
+uint32_t fts_lucene_settings_checksum(const struct fts_lucene_settings *set)
+{
+ uint32_t crc;
+
+ if (set->use_libfts)
+ return crc32_str("l");
+
+ /* checksum is always different when compiling with/without stemmer */
+ crc = set->default_language == NULL ? 0 :
+ crc32_str(set->default_language);
+ crc = crc32_str_more(crc, set->whitespace_chars);
+ if (set->normalize)
+ crc = crc32_str_more(crc, "n");
+ if (set->no_snowball)
+ crc = crc32_str_more(crc, "s");
+ /* don't include mime_parts here, since changing it doesn't
+ necessarily need the index to be rebuilt */
+ return crc;
+}
+
+static void fts_lucene_mail_user_deinit(struct mail_user *user)
+{
+ struct fts_lucene_user *fuser = FTS_LUCENE_USER_CONTEXT_REQUIRE(user);
+
+ fts_mail_user_deinit(user);
+ fuser->module_ctx.super.deinit(user);
+}
+
+static void fts_lucene_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct fts_lucene_user *fuser;
+ const char *env, *error;
+
+ fuser = p_new(user->pool, struct fts_lucene_user, 1);
+ env = mail_user_plugin_getenv(user, "fts_lucene");
+ if (env == NULL)
+ env = "";
+
+ if (fts_lucene_plugin_init_settings(user, &fuser->set, env) < 0) {
+ /* invalid settings, disabling */
+ return;
+ }
+ if (fts_mail_user_init(user, fuser->set.use_libfts, &error) < 0) {
+ i_error("fts_lucene: %s", error);
+ return;
+ }
+
+ fuser->module_ctx.super = *v;
+ user->vlast = &fuser->module_ctx.super;
+ v->deinit = fts_lucene_mail_user_deinit;
+ MODULE_CONTEXT_SET(user, fts_lucene_user_module, fuser);
+}
+
+static struct mail_storage_hooks fts_lucene_mail_storage_hooks = {
+ .mail_user_created = fts_lucene_mail_user_created
+};
+
+void fts_lucene_plugin_init(struct module *module ATTR_UNUSED)
+{
+ fts_backend_register(&fts_backend_lucene);
+ mail_storage_hooks_add(module, &fts_lucene_mail_storage_hooks);
+}
+
+void fts_lucene_plugin_deinit(void)
+{
+ fts_backend_unregister(fts_backend_lucene.name);
+ mail_storage_hooks_remove(&fts_lucene_mail_storage_hooks);
+ lucene_shutdown();
+}
+
+const char *fts_lucene_plugin_dependencies[] = { "fts", NULL };
diff --git a/src/plugins/fts-lucene/fts-lucene-plugin.h b/src/plugins/fts-lucene/fts-lucene-plugin.h
new file mode 100644
index 0000000..69440fb
--- /dev/null
+++ b/src/plugins/fts-lucene/fts-lucene-plugin.h
@@ -0,0 +1,36 @@
+#ifndef FTS_LUCENE_PLUGIN_H
+#define FTS_LUCENE_PLUGIN_H
+
+#include "module-context.h"
+#include "mail-user.h"
+#include "fts-api-private.h"
+
+#define FTS_LUCENE_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_lucene_user_module)
+#define FTS_LUCENE_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, fts_lucene_user_module)
+
+struct fts_lucene_settings {
+ const char *default_language;
+ const char *textcat_conf, *textcat_dir;
+ const char *whitespace_chars;
+ bool normalize;
+ bool no_snowball;
+ bool mime_parts;
+ bool use_libfts;
+};
+
+struct fts_lucene_user {
+ union mail_user_module_context module_ctx;
+ struct fts_lucene_settings set;
+};
+
+extern struct fts_backend fts_backend_lucene;
+extern MODULE_CONTEXT_DEFINE(fts_lucene_user_module, &mail_user_module_register);
+
+uint32_t fts_lucene_settings_checksum(const struct fts_lucene_settings *set);
+
+void fts_lucene_plugin_init(struct module *module);
+void fts_lucene_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fts-lucene/lucene-wrapper.cc b/src/plugins/fts-lucene/lucene-wrapper.cc
new file mode 100644
index 0000000..7446693
--- /dev/null
+++ b/src/plugins/fts-lucene/lucene-wrapper.cc
@@ -0,0 +1,1639 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+extern "C" {
+#include "lib.h"
+#include "array.h"
+#include "unichar.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "ioloop.h"
+#include "unlink-directory.h"
+#include "ioloop.h"
+#include "mail-index.h"
+#include "mail-search.h"
+#include "mail-namespace.h"
+#include "mailbox-list-private.h"
+#include "mail-storage.h"
+#include "fts-expunge-log.h"
+#include "fts-lucene-plugin.h"
+#include "lucene-wrapper.h"
+
+#include <sys/stat.h>
+#ifdef HAVE_LIBEXTTEXTCAT_TEXTCAT_H
+# include <libexttextcat/textcat.h>
+#elif defined (HAVE_LIBTEXTCAT_TEXTCAT_H)
+# include <libtextcat/textcat.h>
+#elif defined (HAVE_FTS_TEXTCAT)
+# include <textcat.h>
+#endif
+};
+#include <CLucene.h>
+#include <CLucene/util/CLStreams.h>
+#include <CLucene/search/MultiPhraseQuery.h>
+#include "SnowballAnalyzer.h"
+
+/* Lucene's default is 10000. Use it here also.. */
+#define MAX_TERMS_PER_DOCUMENT 10000
+#define FTS_LUCENE_MAX_SEARCH_TERMS 1000
+
+#define LUCENE_LOCK_OVERRIDE_SECS 60
+#define LUCENE_INDEX_CLOSE_TIMEOUT_MSECS (120*1000)
+
+using namespace lucene::document;
+using namespace lucene::index;
+using namespace lucene::search;
+using namespace lucene::queryParser;
+using namespace lucene::analysis;
+using namespace lucene::analysis;
+using namespace lucene::util;
+
+struct lucene_query {
+ Query *query;
+ BooleanClause::Occur occur;
+};
+ARRAY_DEFINE_TYPE(lucene_query, struct lucene_query);
+
+struct lucene_analyzer {
+ char *lang;
+ Analyzer *analyzer;
+};
+
+struct lucene_index {
+ char *path;
+ struct mailbox_list *list;
+ struct fts_lucene_settings set;
+ normalizer_func_t *normalizer;
+
+ wchar_t mailbox_guid[MAILBOX_GUID_HEX_LENGTH + 1];
+
+ IndexReader *reader;
+ IndexWriter *writer;
+ IndexSearcher *searcher;
+ struct timeout *to_close;
+
+ buffer_t *normalizer_buf;
+ Analyzer *default_analyzer, *cur_analyzer;
+ ARRAY(struct lucene_analyzer) analyzers;
+
+ Document *doc;
+ uint32_t prev_uid, prev_part_idx;
+ bool no_analyzer;
+};
+
+struct rescan_context {
+ struct lucene_index *index;
+
+ struct mailbox *box;
+ guid_128_t box_guid;
+ int box_ret;
+
+ pool_t pool;
+ HASH_TABLE(uint8_t *, uint8_t *) seen_mailbox_guids;
+
+ ARRAY_TYPE(seq_range) uids;
+ struct seq_range_iter uids_iter;
+ unsigned int uids_iter_n;
+
+ uint32_t last_existing_uid;
+ bool warned;
+};
+
+static void *textcat = NULL;
+#ifdef HAVE_FTS_TEXTCAT
+static bool textcat_broken = FALSE;
+#endif
+static int textcat_refcount = 0;
+
+static void lucene_handle_error(struct lucene_index *index, CLuceneError &err,
+ const char *msg);
+static void rescan_clear_unseen_mailboxes(struct lucene_index *index,
+ struct rescan_context *rescan_ctx);
+
+struct lucene_index *lucene_index_init(const char *path,
+ struct mailbox_list *list,
+ const struct fts_lucene_settings *set)
+{
+ struct lucene_index *index;
+
+ index = i_new(struct lucene_index, 1);
+ index->path = i_strdup(path);
+ index->list = list;
+ if (set != NULL) {
+ index->set = *set;
+ index->normalizer = !set->normalize ? NULL :
+ mailbox_list_get_namespace(list)->user->default_normalizer;
+ } else {
+ /* this is valid only for doveadm dump, so it doesn't matter */
+ index->set.default_language = "";
+ }
+ if (index->set.use_libfts) {
+ index->default_analyzer = _CLNEW KeywordAnalyzer();
+ } else
+#ifdef HAVE_FTS_STEMMER
+ if (set == NULL || !set->no_snowball) {
+ index->default_analyzer =
+ _CLNEW snowball::SnowballAnalyzer(index->normalizer,
+ index->set.default_language);
+ } else
+#endif
+ {
+ index->default_analyzer = _CLNEW standard::StandardAnalyzer();
+ if (index->normalizer != NULL) {
+ index->normalizer_buf =
+ buffer_create_dynamic(default_pool, 1024);
+ }
+ }
+
+ i_array_init(&index->analyzers, 32);
+ textcat_refcount++;
+
+ return index;
+}
+
+void lucene_index_close(struct lucene_index *index)
+{
+ timeout_remove(&index->to_close);
+
+ _CLDELETE(index->searcher);
+ if (index->writer != NULL) {
+ try {
+ index->writer->close();
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter::close");
+ }
+ _CLDELETE(index->writer);
+ }
+ if (index->reader != NULL) {
+ try {
+ index->reader->close();
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexReader::close");
+ }
+ _CLDELETE(index->reader);
+ }
+}
+
+void lucene_index_deinit(struct lucene_index *index)
+{
+ struct lucene_analyzer *a;
+
+ lucene_index_close(index);
+ array_foreach_modifiable(&index->analyzers, a) {
+ i_free(a->lang);
+ _CLDELETE(a->analyzer);
+ }
+ array_free(&index->analyzers);
+ if (--textcat_refcount == 0 && textcat != NULL) {
+#ifdef HAVE_FTS_TEXTCAT
+ textcat_Done(textcat);
+#endif
+ textcat = NULL;
+ }
+ _CLDELETE(index->default_analyzer);
+ if (index->normalizer_buf != NULL)
+ buffer_free(&index->normalizer_buf);
+ i_free(index->path);
+ i_free(index);
+}
+
+static void lucene_data_translate(struct lucene_index *index,
+ wchar_t *data, unsigned int len)
+{
+ const char *whitespace_chars = index->set.whitespace_chars;
+ unsigned int i;
+
+ if (*whitespace_chars == '\0' || index->set.use_libfts)
+ return;
+
+ for (i = 0; i < len; i++) {
+ if (strchr(whitespace_chars, data[i]) != NULL)
+ data[i] = ' ';
+ }
+}
+
+void lucene_utf8_n_to_tchar(const unsigned char *src, size_t srcsize,
+ wchar_t *dest, size_t destsize)
+{
+ ARRAY_TYPE(unichars) dest_arr;
+ buffer_t buf = { { 0, 0 } };
+
+ i_assert(sizeof(wchar_t) == sizeof(unichar_t));
+
+ buffer_create_from_data(&buf, dest, sizeof(wchar_t) * destsize);
+ array_create_from_buffer(&dest_arr, &buf, sizeof(wchar_t));
+ if (uni_utf8_to_ucs4_n(src, srcsize, &dest_arr) < 0)
+ i_unreached();
+ i_assert(array_count(&dest_arr)+1 == destsize);
+ dest[destsize-1] = 0;
+}
+
+static const wchar_t *
+t_lucene_utf8_to_tchar(struct lucene_index *index, const char *str)
+{
+ ARRAY_TYPE(unichars) dest_arr;
+ const unichar_t *chars;
+ wchar_t *ret;
+ unsigned int len;
+
+ i_assert(sizeof(wchar_t) == sizeof(unichar_t));
+
+ t_array_init(&dest_arr, strlen(str) + 1);
+ if (uni_utf8_to_ucs4(str, &dest_arr) < 0)
+ i_unreached();
+ (void)array_append_space(&dest_arr);
+
+ chars = array_get_modifiable(&dest_arr, &len);
+ ret = (wchar_t *)chars;
+ lucene_data_translate(index, ret, len - 1);
+ return ret;
+}
+
+void lucene_index_select_mailbox(struct lucene_index *index,
+ const wchar_t guid[MAILBOX_GUID_HEX_LENGTH])
+{
+ memcpy(index->mailbox_guid, guid,
+ MAILBOX_GUID_HEX_LENGTH * sizeof(wchar_t));
+ index->mailbox_guid[MAILBOX_GUID_HEX_LENGTH] = '\0';
+}
+
+void lucene_index_unselect_mailbox(struct lucene_index *index)
+{
+ memset(index->mailbox_guid, 0, sizeof(index->mailbox_guid));
+}
+
+static void lucene_handle_error(struct lucene_index *index, CLuceneError &err,
+ const char *msg)
+{
+ const char *error, *what = err.what();
+
+ i_error("lucene index %s: %s failed (#%d): %s",
+ index->path, msg, err.number(), what);
+
+ if (index->list != NULL &&
+ (err.number() == CL_ERR_CorruptIndex ||
+ err.number() == CL_ERR_IO)) {
+ /* delete corrupted index. most IO errors are also about
+ missing files and other such corruption.. */
+ if (unlink_directory(index->path, (enum unlink_directory_flags)0, &error) < 0)
+ i_error("unlink_directory(%s) failed: %s", index->path, error);
+ rescan_clear_unseen_mailboxes(index, NULL);
+ }
+}
+
+static int lucene_index_open(struct lucene_index *index)
+{
+ if (index->reader != NULL) {
+ i_assert(index->to_close != NULL);
+ timeout_reset(index->to_close);
+ return 1;
+ }
+
+ if (!IndexReader::indexExists(index->path))
+ return 0;
+
+ try {
+ index->reader = IndexReader::open(index->path);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexReader::open()");
+ return -1;
+ }
+ i_assert(index->to_close == NULL);
+ index->to_close = timeout_add(LUCENE_INDEX_CLOSE_TIMEOUT_MSECS,
+ lucene_index_close, index);
+ return 1;
+}
+
+static int lucene_index_open_search(struct lucene_index *index)
+{
+ int ret;
+
+ if (index->searcher != NULL)
+ return 1;
+
+ if ((ret = lucene_index_open(index)) <= 0)
+ return ret;
+
+ index->searcher = _CLNEW IndexSearcher(index->reader);
+ return 1;
+}
+
+static int
+lucene_doc_get_uid(struct lucene_index *index, Document *doc, uint32_t *uid_r)
+{
+ Field *field = doc->getField(_T("uid"));
+ const TCHAR *uid = field == NULL ? NULL : field->stringValue();
+ if (uid == NULL) {
+ i_error("lucene: Corrupted FTS index %s: No UID for document",
+ index->path);
+ return -1;
+ }
+
+ uint32_t num = 0;
+ while (*uid != 0) {
+ num = num*10 + (*uid - '0');
+ uid++;
+ }
+ *uid_r = num;
+ return 0;
+}
+
+static uint32_t
+lucene_doc_get_part(struct lucene_index *index, Document *doc)
+{
+ Field *field = doc->getField(_T("part"));
+ const TCHAR *part = field == NULL ? NULL : field->stringValue();
+ if (part == NULL)
+ return 0;
+
+ uint32_t num = 0;
+ while (*part != 0) {
+ num = num*10 + (*part - '0');
+ part++;
+ }
+ return num;
+}
+
+int lucene_index_get_last_uid(struct lucene_index *index, uint32_t *last_uid_r)
+{
+ int ret = 0;
+
+ *last_uid_r = 0;
+
+ if ((ret = lucene_index_open_search(index)) <= 0)
+ return ret;
+
+ Term mailbox_term(_T("box"), index->mailbox_guid);
+ TermQuery query(&mailbox_term);
+
+ uint32_t last_uid = 0;
+ try {
+ Hits *hits = index->searcher->search(&query);
+
+ for (size_t i = 0; i < hits->length(); i++) {
+ uint32_t uid;
+
+ if (lucene_doc_get_uid(index, &hits->doc(i),
+ &uid) < 0) {
+ ret = -1;
+ break;
+ }
+
+ if (uid > last_uid)
+ last_uid = uid;
+ }
+ _CLDELETE(hits);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "last_uid search");
+ ret = -1;
+ }
+ *last_uid_r = last_uid;
+ return ret;
+}
+
+int lucene_index_get_doc_count(struct lucene_index *index, uint32_t *count_r)
+{
+ int ret;
+
+ if (index->reader == NULL) {
+ lucene_index_close(index);
+ if ((ret = lucene_index_open(index)) < 0)
+ return -1;
+ if (ret == 0) {
+ *count_r = 0;
+ return 0;
+ }
+ }
+ *count_r = index->reader->numDocs();
+ return 0;
+}
+
+static int lucene_settings_check(struct lucene_index *index)
+{
+ uint32_t set_checksum;
+ const char *error;
+ int ret = 0;
+
+ set_checksum = fts_lucene_settings_checksum(&index->set);
+ ret = fts_index_have_compatible_settings(index->list, set_checksum);
+ if (ret != 0)
+ return ret;
+
+ i_warning("fts-lucene: Settings have changed, rebuilding index for mailbox");
+
+ /* settings changed, rebuild index */
+ if (unlink_directory(index->path, (enum unlink_directory_flags)0, &error) < 0) {
+ i_error("unlink_directory(%s) failed: %s", index->path, error);
+ ret = -1;
+ } else {
+ rescan_clear_unseen_mailboxes(index, NULL);
+ }
+ return ret;
+}
+
+int lucene_index_build_init(struct lucene_index *index)
+{
+ const char *lock_path;
+ struct stat st;
+
+ lucene_index_close(index);
+
+ lock_path = t_strdup_printf("%s/write.lock", index->path);
+ if (stat(lock_path, &st) == 0 &&
+ st.st_mtime < time(NULL) - LUCENE_LOCK_OVERRIDE_SECS) {
+ if (unlink(lock_path) < 0)
+ i_error("unlink(%s) failed: %m", lock_path);
+ }
+
+ if (lucene_settings_check(index) < 0)
+ return -1;
+
+ bool exists = IndexReader::indexExists(index->path);
+ try {
+ index->writer = _CLNEW IndexWriter(index->path,
+ index->default_analyzer,
+ !exists);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter()");
+ return -1;
+ }
+ index->writer->setMaxFieldLength(MAX_TERMS_PER_DOCUMENT);
+ return 0;
+}
+
+#ifdef HAVE_FTS_TEXTCAT
+static Analyzer *get_analyzer(struct lucene_index *index, const char *lang)
+{
+ normalizer_func_t *normalizer = index->normalizer;
+ const struct lucene_analyzer *a;
+ struct lucene_analyzer new_analyzer;
+ Analyzer *analyzer;
+
+ array_foreach(&index->analyzers, a) {
+ if (strcmp(a->lang, lang) == 0)
+ return a->analyzer;
+ }
+
+ memset(&new_analyzer, 0, sizeof(new_analyzer));
+ new_analyzer.lang = i_strdup(lang);
+ new_analyzer.analyzer =
+ _CLNEW snowball::SnowballAnalyzer(normalizer, lang);
+ array_append_i(&index->analyzers.arr, &new_analyzer, 1);
+ return new_analyzer.analyzer;
+}
+
+static void *textcat_init(struct lucene_index *index)
+{
+ const char *textcat_dir = index->set.textcat_dir;
+ unsigned int len;
+
+ if (textcat_dir == NULL)
+ return NULL;
+
+ /* textcat really wants the '/' suffix */
+ len = strlen(textcat_dir);
+ if (len > 0 && textcat_dir[len-1] != '/')
+ textcat_dir = t_strconcat(textcat_dir, "/", NULL);
+
+ return special_textcat_Init(index->set.textcat_conf, textcat_dir);
+}
+
+static Analyzer *
+guess_analyzer(struct lucene_index *index, const void *data, size_t size)
+{
+ const char *lang;
+
+ if (textcat_broken)
+ return NULL;
+
+ if (textcat == NULL) {
+ textcat = textcat_init(index);
+ if (textcat == NULL) {
+ textcat_broken = TRUE;
+ return NULL;
+ }
+ }
+
+ /* try to guess the language */
+ lang = textcat_Classify(textcat, (const char *)data,
+ I_MIN(size, 500));
+ const char *p = strchr(lang, ']');
+ if (lang[0] != '[' || p == NULL)
+ return NULL;
+ lang = t_strdup_until(lang+1, p);
+ if (strcmp(lang, index->set.default_language) == 0)
+ return index->default_analyzer;
+
+ return get_analyzer(index, lang);
+}
+#else
+static Analyzer *
+guess_analyzer(struct lucene_index *index ATTR_UNUSED,
+ const void *data ATTR_UNUSED, size_t size ATTR_UNUSED)
+{
+ return NULL;
+}
+#endif
+
+static int lucene_index_build_flush(struct lucene_index *index)
+{
+ int ret = 0;
+
+ if (index->doc == NULL)
+ return 0;
+
+ try {
+ CL_NS(analysis)::Analyzer *analyzer = NULL;
+
+ if (!index->set.use_libfts) {
+ analyzer = index->cur_analyzer != NULL ?
+ index->cur_analyzer : index->default_analyzer;
+ }
+ index->writer->addDocument(index->doc, analyzer);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter::addDocument()");
+ ret = -1;
+ }
+
+ _CLDELETE(index->doc);
+ index->doc = NULL;
+ index->cur_analyzer = NULL;
+ return ret;
+}
+
+int lucene_index_build_more(struct lucene_index *index, uint32_t uid,
+ uint32_t part_idx, const unsigned char *data,
+ size_t size, const char *hdr_name)
+{
+ wchar_t id[MAX_INT_STRLEN];
+ size_t namesize, datasize;
+
+ if (uid != index->prev_uid || part_idx != index->prev_part_idx) {
+ if (lucene_index_build_flush(index) < 0)
+ return -1;
+ index->prev_uid = uid;
+ index->prev_part_idx = part_idx;
+
+ index->doc = _CLNEW Document();
+ swprintf(id, N_ELEMENTS(id), L"%u", uid);
+ index->doc->add(*_CLNEW Field(_T("uid"), id, Field::STORE_YES | Field::INDEX_UNTOKENIZED));
+ if (part_idx != 0) {
+ swprintf(id, N_ELEMENTS(id), L"%u", part_idx);
+ index->doc->add(*_CLNEW Field(_T("part"), id, Field::STORE_YES | Field::INDEX_UNTOKENIZED));
+ }
+ index->doc->add(*_CLNEW Field(_T("box"), index->mailbox_guid, Field::STORE_YES | Field::INDEX_UNTOKENIZED));
+ }
+
+ if (index->normalizer_buf != NULL && !index->set.use_libfts) {
+ buffer_set_used_size(index->normalizer_buf, 0);
+ index->normalizer(data, size, index->normalizer_buf);
+ data = (const unsigned char *)index->normalizer_buf->data;
+ size = index->normalizer_buf->used;
+ }
+
+ datasize = uni_utf8_strlen_n(data, size) + 1;
+ wchar_t *dest, *dest_free = NULL;
+ if (datasize < 4096)
+ dest = t_new(wchar_t, datasize);
+ else
+ dest = dest_free = i_new(wchar_t, datasize);
+ lucene_utf8_n_to_tchar(data, size, dest, datasize);
+ lucene_data_translate(index, dest, datasize-1);
+
+ int token_flag = index->set.use_libfts ?
+ Field::INDEX_UNTOKENIZED : Field::INDEX_TOKENIZED;
+ if (hdr_name != NULL) {
+ /* hdr_name should be ASCII, but don't break in case it isn't */
+ hdr_name = t_str_lcase(hdr_name);
+ namesize = uni_utf8_strlen(hdr_name) + 1;
+ wchar_t wname[namesize];
+ lucene_utf8_n_to_tchar((const unsigned char *)hdr_name,
+ strlen(hdr_name), wname, namesize);
+ if (!index->set.use_libfts)
+ index->doc->add(*_CLNEW Field(_T("hdr"), wname, Field::STORE_NO | token_flag));
+ index->doc->add(*_CLNEW Field(_T("hdr"), dest, Field::STORE_NO | token_flag));
+
+ if (fts_header_want_indexed(hdr_name))
+ index->doc->add(*_CLNEW Field(wname, dest, Field::STORE_NO | token_flag));
+ } else if (size > 0) {
+ if (index->cur_analyzer == NULL && !index->set.use_libfts)
+ index->cur_analyzer = guess_analyzer(index, data, size);
+ index->doc->add(*_CLNEW Field(_T("body"), dest, Field::STORE_NO | token_flag));
+ }
+ i_free(dest_free);
+ return 0;
+}
+
+int lucene_index_build_deinit(struct lucene_index *index)
+{
+ int ret = 0;
+
+ if (index->prev_uid == 0) {
+ /* no changes. */
+ return 0;
+ }
+ index->prev_uid = 0;
+ index->prev_part_idx = 0;
+
+ if (index->writer == NULL) {
+ lucene_index_close(index);
+ return -1;
+ }
+
+ if (lucene_index_build_flush(index) < 0)
+ ret = -1;
+
+ try {
+ index->writer->close();
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter::close()");
+ ret = -1;
+ }
+
+ lucene_index_close(index);
+ return ret;
+}
+
+static int
+wcharguid_to_guid(guid_128_t dest, const wchar_t *src)
+{
+ buffer_t buf = { { 0, 0 } };
+ char src_chars[GUID_128_SIZE*2 + 1];
+ unsigned int i;
+
+ for (i = 0; i < sizeof(src_chars)-1; i++) {
+ if ((src[i] >= '0' && src[i] <= '9') ||
+ (src[i] >= 'a' && src[i] <= 'f'))
+ src_chars[i] = src[i];
+ else
+ return -1;
+ }
+ if (src[i] != '\0')
+ return -1;
+ src_chars[i] = '\0';
+
+ buffer_create_from_data(&buf, dest, GUID_128_SIZE);
+ return hex_to_binary(src_chars, &buf);
+}
+
+static int
+rescan_get_uids(struct mailbox *box, ARRAY_TYPE(seq_range) *uids)
+{
+ struct mailbox_status status;
+
+ if (mailbox_get_status(box, STATUS_MESSAGES, &status) < 0)
+ return -1;
+
+ if (status.messages > 0) T_BEGIN {
+ ARRAY_TYPE(seq_range) seqs;
+
+ t_array_init(&seqs, 2);
+ seq_range_array_add_range(&seqs, 1, status.messages);
+ mailbox_get_uid_range(box, &seqs, uids);
+ } T_END;
+ return 0;
+}
+
+static int rescan_finish(struct rescan_context *ctx)
+{
+ int ret;
+
+ ret = fts_index_set_last_uid(ctx->box, ctx->last_existing_uid);
+ mailbox_free(&ctx->box);
+ return ret;
+}
+
+static int
+fts_lucene_get_mailbox_guid(struct lucene_index *index, Document *doc,
+ guid_128_t guid_r)
+{
+ Field *field = doc->getField(_T("box"));
+ const TCHAR *box_guid = field == NULL ? NULL : field->stringValue();
+ if (box_guid == NULL) {
+ i_error("lucene: Corrupted FTS index %s: No mailbox for document",
+ index->path);
+ return -1;
+ }
+
+ if (wcharguid_to_guid(guid_r, box_guid) < 0) {
+ i_error("lucene: Corrupted FTS index %s: "
+ "box field not in expected format", index->path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+rescan_open_mailbox(struct rescan_context *ctx, Document *doc)
+{
+ guid_128_t guid, *guidp;
+ int ret;
+
+ if (fts_lucene_get_mailbox_guid(ctx->index, doc, guid) < 0)
+ return 0;
+
+ if (memcmp(guid, ctx->box_guid, sizeof(guid)) == 0) {
+ /* same as last one */
+ return ctx->box_ret;
+ }
+ memcpy(ctx->box_guid, guid, sizeof(ctx->box_guid));
+
+ guidp = p_new(ctx->pool, guid_128_t, 1);
+ memcpy(guidp, guid, sizeof(*guidp));
+ hash_table_insert(ctx->seen_mailbox_guids, guidp, guidp);
+
+ if (ctx->box != NULL)
+ rescan_finish(ctx);
+ ctx->box = mailbox_alloc_guid(ctx->index->list, guid,
+ (enum mailbox_flags)0);
+ if (mailbox_open(ctx->box) < 0) {
+ enum mail_error error;
+ const char *errstr;
+
+ errstr = mailbox_get_last_internal_error(ctx->box, &error);
+ if (error == MAIL_ERROR_NOTFOUND)
+ ret = 0;
+ else {
+ i_error("lucene: Couldn't open mailbox %s: %s",
+ mailbox_get_vname(ctx->box), errstr);
+ ret = -1;
+ }
+ mailbox_free(&ctx->box);
+ ctx->box_ret = ret;
+ return ret;
+ }
+ if (mailbox_sync(ctx->box, (enum mailbox_sync_flags)0) < 0) {
+ i_error("lucene: Failed to sync mailbox %s: %s",
+ mailbox_get_vname(ctx->box),
+ mailbox_get_last_internal_error(ctx->box, NULL));
+ mailbox_free(&ctx->box);
+ ctx->box_ret = -1;
+ return -1;
+ }
+
+ array_clear(&ctx->uids);
+ rescan_get_uids(ctx->box, &ctx->uids);
+
+ ctx->warned = FALSE;
+ ctx->last_existing_uid = 0;
+ ctx->uids_iter_n = 0;
+ seq_range_array_iter_init(&ctx->uids_iter, &ctx->uids);
+
+ ctx->box_ret = 1;
+ return 1;
+}
+
+static int
+rescan_next(struct rescan_context *ctx, Document *doc)
+{
+ uint32_t lucene_uid, idx_uid;
+
+ if (lucene_doc_get_uid(ctx->index, doc, &lucene_uid) < 0)
+ return 0;
+
+ if (seq_range_array_iter_nth(&ctx->uids_iter, ctx->uids_iter_n,
+ &idx_uid)) {
+ if (idx_uid == lucene_uid) {
+ ctx->uids_iter_n++;
+ ctx->last_existing_uid = idx_uid;
+ return 1;
+ }
+ if (idx_uid < lucene_uid) {
+ /* lucene is missing an UID from the middle. delete
+ the rest of the messages from this mailbox and
+ reindex. */
+ if (!ctx->warned) {
+ i_warning("lucene: Mailbox %s "
+ "missing UIDs in the middle",
+ mailbox_get_vname(ctx->box));
+ ctx->warned = TRUE;
+ }
+ } else {
+ /* UID has been expunged from index. delete from
+ lucene as well. */
+ }
+ return 0;
+ } else {
+ /* the rest of the messages have been expunged from index */
+ return 0;
+ }
+}
+
+static void
+rescan_clear_unseen_mailbox(struct lucene_index *index,
+ struct rescan_context *rescan_ctx,
+ const char *vname,
+ const struct fts_index_header *hdr)
+{
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+
+ box = mailbox_alloc(index->list, vname,
+ (enum mailbox_flags)0);
+ if (mailbox_open(box) == 0 &&
+ mailbox_get_metadata(box, MAILBOX_METADATA_GUID,
+ &metadata) == 0 &&
+ (rescan_ctx == NULL ||
+ hash_table_lookup(rescan_ctx->seen_mailbox_guids,
+ metadata.guid) == NULL)) {
+ /* this mailbox had no records in lucene index.
+ make sure its last indexed uid is 0 */
+ (void)fts_index_set_header(box, hdr);
+ }
+ mailbox_free(&box);
+}
+
+static void rescan_clear_unseen_mailboxes(struct lucene_index *index,
+ struct rescan_context *rescan_ctx)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ (enum mailbox_list_iter_flags)
+ (MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ struct fts_index_header hdr;
+ struct mail_namespace *ns = index->list->ns;
+ const char *vname;
+
+ memset(&hdr, 0, sizeof(hdr));
+ hdr.settings_checksum = fts_lucene_settings_checksum(&index->set);
+
+ iter = mailbox_list_iter_init(index->list, "*", iter_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL)
+ rescan_clear_unseen_mailbox(index, rescan_ctx, info->vname, &hdr);
+ (void)mailbox_list_iter_deinit(&iter);
+
+ if (ns->prefix_len > 0 &&
+ ns->prefix[ns->prefix_len-1] == mail_namespace_get_sep(ns)) {
+ /* namespace prefix itself isn't returned by the listing */
+ vname = t_strndup(index->list->ns->prefix,
+ index->list->ns->prefix_len-1);
+ rescan_clear_unseen_mailbox(index, rescan_ctx, vname, &hdr);
+ }
+}
+
+int lucene_index_rescan(struct lucene_index *index)
+{
+ static const TCHAR *sort_fields[] = { _T("box"), _T("uid"), NULL };
+ struct rescan_context ctx;
+ bool failed = false;
+ int ret;
+
+ i_assert(index->list != NULL);
+
+ if ((ret = lucene_index_open_search(index)) < 0)
+ return ret;
+
+ Term term(_T("box"), _T("*"));
+ WildcardQuery query(&term);
+ Sort sort(sort_fields);
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.index = index;
+ ctx.pool = pool_alloconly_create("guids", 1024);
+ hash_table_create(&ctx.seen_mailbox_guids, ctx.pool, 0,
+ guid_128_hash, guid_128_cmp);
+ i_array_init(&ctx.uids, 128);
+
+ if (ret > 0) try {
+ Hits *hits = index->searcher->search(&query, &sort);
+
+ for (size_t i = 0; i < hits->length(); i++) {
+ ret = rescan_open_mailbox(&ctx, &hits->doc(i));
+ if (ret > 0)
+ ret = rescan_next(&ctx, &hits->doc(i));
+ if (ret < 0)
+ failed = true;
+ else if (ret == 0)
+ index->reader->deleteDocument(hits->id(i));
+ }
+ _CLDELETE(hits);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "rescan search");
+ failed = true;
+ }
+ lucene_index_close(index);
+ if (ctx.box != NULL)
+ rescan_finish(&ctx);
+ array_free(&ctx.uids);
+
+ rescan_clear_unseen_mailboxes(index, &ctx);
+ hash_table_destroy(&ctx.seen_mailbox_guids);
+ pool_unref(&ctx.pool);
+ return failed ? -1 : 0;
+}
+
+static void guid128_to_wguid(const guid_128_t guid,
+ wchar_t wguid_hex[MAILBOX_GUID_HEX_LENGTH + 1])
+{
+ buffer_t buf = { { 0, 0 } };
+ unsigned char guid_hex[MAILBOX_GUID_HEX_LENGTH];
+ unsigned int i;
+
+ buffer_create_from_data(&buf, guid_hex, MAILBOX_GUID_HEX_LENGTH);
+ binary_to_hex_append(&buf, guid, GUID_128_SIZE);
+ for (i = 0; i < MAILBOX_GUID_HEX_LENGTH; i++)
+ wguid_hex[i] = guid_hex[i];
+ wguid_hex[i] = '\0';
+}
+
+static bool
+lucene_index_add_uid_filter(BooleanQuery *query,
+ const struct fts_expunge_log_read_record *rec)
+{
+ struct seq_range_iter iter;
+ wchar_t wuid[MAX_INT_STRLEN];
+ unsigned int n;
+ uint32_t uid;
+
+ /* RangeQuery and WildcardQuery work by enumerating through all terms
+ that match them, and then adding TermQueries for them. So we can
+ simply do the same directly, and if it looks like there are too
+ many terms just go through everything. */
+
+ if (seq_range_count(&rec->uids) > FTS_LUCENE_MAX_SEARCH_TERMS)
+ return false;
+
+ seq_range_array_iter_init(&iter, &rec->uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ swprintf(wuid, N_ELEMENTS(wuid), L"%u", uid);
+
+ Term *term = _CLNEW Term(_T("uid"), wuid);
+ query->add(_CLNEW TermQuery(term), true, BooleanClause::SHOULD);
+ _CLDECDELETE(term);
+ }
+ return true;
+}
+
+static int
+lucene_index_expunge_record(struct lucene_index *index,
+ const struct fts_expunge_log_read_record *rec)
+{
+ int ret;
+
+ if ((ret = lucene_index_open_search(index)) <= 0)
+ return ret;
+
+ BooleanQuery query;
+ BooleanQuery uids_query;
+
+ if (lucene_index_add_uid_filter(&uids_query, rec))
+ query.add(&uids_query, BooleanClause::MUST);
+
+ wchar_t wguid[MAILBOX_GUID_HEX_LENGTH + 1];
+ guid128_to_wguid(rec->mailbox_guid, wguid);
+ Term term(_T("box"), wguid);
+ TermQuery mailbox_query(&term);
+ query.add(&mailbox_query, BooleanClause::MUST);
+
+ try {
+ Hits *hits = index->searcher->search(&query);
+
+ for (size_t i = 0; i < hits->length(); i++) {
+ uint32_t uid;
+
+ if (lucene_doc_get_uid(index, &hits->doc(i),
+ &uid) < 0 ||
+ seq_range_exists(&rec->uids, uid))
+ index->reader->deleteDocument(hits->id(i));
+ }
+ _CLDELETE(hits);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "expunge search");
+ ret = -1;
+ }
+ return ret < 0 ? -1 : 0;
+}
+
+int lucene_index_expunge_from_log(struct lucene_index *index,
+ struct fts_expunge_log *log)
+{
+ struct fts_expunge_log_read_ctx *ctx;
+ const struct fts_expunge_log_read_record *rec;
+ int ret = 0, ret2;
+
+ ctx = fts_expunge_log_read_begin(log);
+ while ((rec = fts_expunge_log_read_next(ctx)) != NULL) {
+ if (lucene_index_expunge_record(index, rec) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ lucene_index_close(index);
+
+ ret2 = fts_expunge_log_read_end(&ctx);
+ if (ret < 0 || ret2 < 0)
+ return -1;
+ return ret2;
+}
+
+int lucene_index_optimize(struct lucene_index *index)
+{
+ int ret = 0;
+
+ if (!IndexReader::indexExists(index->path))
+ return 0;
+ if (IndexReader::isLocked(index->path))
+ IndexReader::unlock(index->path);
+
+ IndexWriter *writer = NULL;
+ try {
+ writer = _CLNEW IndexWriter(index->path, index->default_analyzer, false);
+ writer->optimize();
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter::optimize()");
+ ret = -1;
+ }
+ try {
+ writer->close();
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "IndexWriter::close()");
+ ret = -1;
+ }
+ if (writer != NULL)
+ _CLDELETE(writer);
+ return ret;
+}
+
+// Mostly copy&pasted from CLucene's QueryParser
+static Query* getFieldQuery(Analyzer *analyzer, const TCHAR* _field, const TCHAR* queryText, bool fuzzy) {
+ // Use the analyzer to get all the tokens, and then build a TermQuery,
+ // PhraseQuery, or nothing based on the term count
+
+ StringReader reader(queryText);
+ TokenStream* source = analyzer->tokenStream(_field, &reader);
+
+ CLVector<CL_NS(analysis)::Token*, Deletor::Object<CL_NS(analysis)::Token> > v;
+ CL_NS(analysis)::Token* t = NULL;
+ int32_t positionCount = 0;
+ bool severalTokensAtSamePosition = false;
+
+ while (true) {
+ t = _CLNEW Token();
+ try {
+ Token* _t = source->next(t);
+ if (_t == NULL) _CLDELETE(t);
+ }_CLCATCH_ERR(CL_ERR_IO, _CLLDELETE(source);_CLLDELETE(t);,{
+ t = NULL;
+ });
+ if (t == NULL)
+ break;
+ v.push_back(t);
+ if (t->getPositionIncrement() != 0)
+ positionCount += t->getPositionIncrement();
+ else
+ severalTokensAtSamePosition = true;
+ }
+ try {
+ source->close();
+ }
+ _CLCATCH_ERR_CLEANUP(CL_ERR_IO, {_CLLDELETE(source);_CLLDELETE(t);} ); /* cleanup */
+ _CLLDELETE(source);
+
+ if (v.size() == 0)
+ return NULL;
+ else if (v.size() == 1) {
+ Term* tm = _CLNEW Term(_field, v.at(0)->termBuffer());
+ Query* ret;
+ if (fuzzy)
+ ret = _CLNEW FuzzyQuery( tm );
+ else
+ ret = _CLNEW TermQuery( tm );
+ _CLDECDELETE(tm);
+ return ret;
+ } else {
+ if (severalTokensAtSamePosition) {
+ if (positionCount == 1) {
+ // no phrase query:
+ BooleanQuery* q = _CLNEW BooleanQuery(true);
+ for(size_t i=0; i<v.size(); i++ ){
+ Term* tm = _CLNEW Term(_field, v.at(i)->termBuffer());
+ q->add(_CLNEW TermQuery(tm), true, BooleanClause::SHOULD);
+ _CLDECDELETE(tm);
+ }
+ return q;
+ }else {
+ MultiPhraseQuery* mpq = _CLNEW MultiPhraseQuery();
+ CLArrayList<Term*> multiTerms;
+ int32_t position = -1;
+ for (size_t i = 0; i < v.size(); i++) {
+ t = v.at(i);
+ if (t->getPositionIncrement() > 0 && multiTerms.size() > 0) {
+ ValueArray<Term*> termsArray(multiTerms.size());
+ multiTerms.toArray(termsArray.values);
+ mpq->add(&termsArray,position);
+ multiTerms.clear();
+ }
+ position += t->getPositionIncrement();
+ multiTerms.push_back(_CLNEW Term(_field, t->termBuffer()));
+ }
+ ValueArray<Term*> termsArray(multiTerms.size());
+ multiTerms.toArray(termsArray.values);
+ mpq->add(&termsArray,position);
+ return mpq;
+ }
+ }else {
+ PhraseQuery* pq = _CLNEW PhraseQuery();
+ int32_t position = -1;
+
+ for (size_t i = 0; i < v.size(); i++) {
+ t = v.at(i);
+ Term* tm = _CLNEW Term(_field, t->termBuffer());
+ position += t->getPositionIncrement();
+ pq->add(tm,position);
+ _CLDECDELETE(tm);
+ }
+ return pq;
+ }
+ }
+}
+
+static Query *
+lucene_get_query_str(struct lucene_index *index,
+ const TCHAR *key, const char *str, bool fuzzy)
+{
+ const TCHAR *wvalue;
+ Analyzer *analyzer;
+
+ if (index->set.use_libfts) {
+ const wchar_t *wstr = t_lucene_utf8_to_tchar(index, str);
+ Term* tm = _CLNEW Term(key, wstr);
+ Query* ret;
+ if (fuzzy)
+ ret = _CLNEW FuzzyQuery( tm );
+ else
+ ret = _CLNEW TermQuery( tm );
+ _CLDECDELETE(tm);
+ return ret;
+ }
+
+ if (index->normalizer_buf != NULL) {
+ buffer_set_used_size(index->normalizer_buf, 0);
+ index->normalizer(str, strlen(str), index->normalizer_buf);
+ buffer_append_c(index->normalizer_buf, '\0');
+ str = (const char *)index->normalizer_buf->data;
+ }
+
+ wvalue = t_lucene_utf8_to_tchar(index, str);
+ analyzer = guess_analyzer(index, str, strlen(str));
+ if (analyzer == NULL) {
+ analyzer = index->default_analyzer;
+ i_assert(analyzer != NULL);
+ }
+
+ return getFieldQuery(analyzer, key, wvalue, fuzzy);
+}
+
+static Query *
+lucene_get_query(struct lucene_index *index,
+ const TCHAR *key, const struct mail_search_arg *arg)
+{
+ return lucene_get_query_str(index, key, arg->value.str, arg->fuzzy);
+}
+
+static bool
+lucene_add_definite_query(struct lucene_index *index,
+ ARRAY_TYPE(lucene_query) &queries,
+ struct mail_search_arg *arg,
+ enum fts_lookup_flags flags)
+{
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ Query *q;
+
+ if (arg->no_fts)
+ return false;
+
+ if (arg->match_not && !and_args) {
+ /* FIXME: we could handle this by doing multiple queries.. */
+ return false;
+ }
+
+ switch (arg->type) {
+ case SEARCH_TEXT: {
+ Query *q1 = lucene_get_query(index, _T("hdr"), arg);
+ Query *q2 = lucene_get_query(index, _T("body"), arg);
+
+ if (q1 == NULL && q2 == NULL)
+ q = NULL;
+ else {
+ BooleanQuery *bq = _CLNEW BooleanQuery();
+ if (q1 != NULL)
+ bq->add(q1, true, BooleanClause::SHOULD);
+ if (q2 != NULL)
+ bq->add(q2, true, BooleanClause::SHOULD);
+ q = bq;
+ }
+ break;
+ }
+ case SEARCH_BODY:
+ q = lucene_get_query(index, _T("body"), arg);
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (!fts_header_want_indexed(arg->hdr_field_name) ||
+ *arg->value.str == '\0')
+ return false;
+
+ q = lucene_get_query(index,
+ t_lucene_utf8_to_tchar(index, t_str_lcase(arg->hdr_field_name)),
+ arg);
+ break;
+ default:
+ return false;
+ }
+
+ if (q == NULL) {
+ /* couldn't handle this search after all (e.g. trying to search
+ a stop word) */
+ return false;
+ }
+
+ struct lucene_query *lq = array_append_space(&queries);
+ lq->query = q;
+ if (!and_args)
+ lq->occur = BooleanClause::SHOULD;
+ else if (!arg->match_not)
+ lq->occur = BooleanClause::MUST;
+ else
+ lq->occur = BooleanClause::MUST_NOT;
+ return true;
+}
+
+static bool
+lucene_add_maybe_query(struct lucene_index *index,
+ ARRAY_TYPE(lucene_query) &queries,
+ struct mail_search_arg *arg,
+ enum fts_lookup_flags flags)
+{
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ Query *q = NULL;
+
+ if (arg->no_fts)
+ return false;
+
+ if (arg->match_not) {
+ /* FIXME: we could handle this by doing multiple queries.. */
+ return false;
+ }
+
+ switch (arg->type) {
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (*arg->value.str == '\0' && !index->set.use_libfts) {
+ /* checking potential existence of the header name */
+ q = lucene_get_query_str(index, _T("hdr"),
+ t_str_lcase(arg->hdr_field_name), FALSE);
+ break;
+ }
+
+ if (fts_header_want_indexed(arg->hdr_field_name))
+ return false;
+
+ /* we can check if the search key exists in some header and
+ filter out the messages that have no chance of matching */
+ q = lucene_get_query(index, _T("hdr"), arg);
+ break;
+ default:
+ return false;
+ }
+
+ if (q == NULL) {
+ /* couldn't handle this search after all (e.g. trying to search
+ a stop word) */
+ return false;
+ }
+ struct lucene_query *lq = array_append_space(&queries);
+ lq->query = q;
+ if (!and_args)
+ lq->occur = BooleanClause::SHOULD;
+ else if (!arg->match_not)
+ lq->occur = BooleanClause::MUST;
+ else
+ lq->occur = BooleanClause::MUST_NOT;
+ return true;
+}
+
+static bool queries_have_non_must_nots(ARRAY_TYPE(lucene_query) &queries)
+{
+ const struct lucene_query *lq;
+
+ array_foreach(&queries, lq) {
+ if (lq->occur != BooleanClause::MUST_NOT)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void search_query_add(BooleanQuery &query,
+ ARRAY_TYPE(lucene_query) &queries)
+{
+ BooleanQuery *search_query = _CLNEW BooleanQuery();
+ const struct lucene_query *lq;
+
+ if (queries_have_non_must_nots(queries)) {
+ array_foreach(&queries, lq)
+ search_query->add(lq->query, true, lq->occur);
+ query.add(search_query, true, BooleanClause::MUST);
+ } else {
+ array_foreach(&queries, lq)
+ search_query->add(lq->query, true, BooleanClause::SHOULD);
+ query.add(search_query, true, BooleanClause::MUST_NOT);
+ }
+}
+
+static int
+lucene_index_search(struct lucene_index *index,
+ ARRAY_TYPE(lucene_query) &queries,
+ struct fts_result *result, ARRAY_TYPE(seq_range) *uids_r)
+{
+ struct fts_score_map *score;
+ int ret = 0;
+
+ BooleanQuery query;
+ search_query_add(query, queries);
+
+ Term mailbox_term(_T("box"), index->mailbox_guid);
+ TermQuery mailbox_query(&mailbox_term);
+ query.add(&mailbox_query, BooleanClause::MUST);
+
+ try {
+ Hits *hits = index->searcher->search(&query);
+
+ uint32_t last_uid = 0;
+ if (result != NULL)
+ result->scores_sorted = true;
+
+ for (size_t i = 0; i < hits->length(); i++) {
+ uint32_t uid;
+
+ if (lucene_doc_get_uid(index, &hits->doc(i),
+ &uid) < 0) {
+ ret = -1;
+ break;
+ }
+
+ if (seq_range_array_add(uids_r, uid)) {
+ /* duplicate result */
+ } else if (result != NULL) {
+ if (uid < last_uid)
+ result->scores_sorted = false;
+ last_uid = uid;
+
+ score = array_append_space(&result->scores);
+ score->uid = uid;
+ score->score = hits->score(i);
+ }
+ }
+ _CLDELETE(hits);
+ return ret;
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "search");
+ return -1;
+ }
+}
+
+int lucene_index_lookup(struct lucene_index *index,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ struct mail_search_arg *arg;
+
+ if (lucene_index_open_search(index) <= 0)
+ return -1;
+
+ ARRAY_TYPE(lucene_query) def_queries;
+ t_array_init(&def_queries, 16);
+ bool have_definites = false;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (lucene_add_definite_query(index, def_queries, arg, flags)) {
+ arg->match_always = true;
+ have_definites = true;
+ }
+ }
+
+ if (have_definites) {
+ ARRAY_TYPE(seq_range) *uids_arr =
+ (flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0 ?
+ &result->definite_uids : &result->maybe_uids;
+ if (lucene_index_search(index, def_queries, result,
+ uids_arr) < 0)
+ return -1;
+ }
+
+ if (have_definites) {
+ /* FIXME: mixing up definite + maybe queries is broken. if the
+ definite query matched, it'll just assume that the maybe
+ queries matched as well */
+ return 0;
+ }
+
+ ARRAY_TYPE(lucene_query) maybe_queries;
+ t_array_init(&maybe_queries, 16);
+ bool have_maybies = false;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (lucene_add_maybe_query(index, maybe_queries, arg, flags)) {
+ arg->match_always = true;
+ have_maybies = true;
+ }
+ }
+
+ if (have_maybies) {
+ if (lucene_index_search(index, maybe_queries, NULL,
+ &result->maybe_uids) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+lucene_index_search_multi(struct lucene_index *index,
+ HASH_TABLE_TYPE(wguid_result) guids,
+ ARRAY_TYPE(lucene_query) &queries,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ struct fts_score_map *score;
+ int ret = 0;
+
+ BooleanQuery query;
+ search_query_add(query, queries);
+
+ BooleanQuery mailbox_query;
+ struct hash_iterate_context *iter;
+ void *key, *value;
+ iter = hash_table_iterate_init(guids);
+ while (hash_table_iterate(iter, guids, &key, &value)) {
+ Term *term = _CLNEW Term(_T("box"), (wchar_t *)key);
+ TermQuery *q = _CLNEW TermQuery(term);
+ mailbox_query.add(q, true, BooleanClause::SHOULD);
+ }
+ hash_table_iterate_deinit(&iter);
+
+ query.add(&mailbox_query, BooleanClause::MUST);
+ try {
+ Hits *hits = index->searcher->search(&query);
+
+ for (size_t i = 0; i < hits->length(); i++) {
+ uint32_t uid;
+
+ Field *field = hits->doc(i).getField(_T("box"));
+ const TCHAR *box_guid = field == NULL ? NULL : field->stringValue();
+ if (box_guid == NULL) {
+ i_error("lucene: Corrupted FTS index %s: No mailbox for document",
+ index->path);
+ ret = -1;
+ break;
+ }
+ struct fts_result *br =
+ hash_table_lookup(guids, box_guid);
+ if (br == NULL) {
+ i_warning("lucene: Returned unexpected mailbox with GUID %ls", box_guid);
+ continue;
+ }
+
+ if (lucene_doc_get_uid(index, &hits->doc(i),
+ &uid) < 0) {
+ ret = -1;
+ break;
+ }
+
+ ARRAY_TYPE(seq_range) *uids_arr =
+ (flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0 ?
+ &br->maybe_uids : &br->definite_uids;
+ if (!array_is_created(uids_arr)) {
+ p_array_init(uids_arr, result->pool, 32);
+ p_array_init(&br->scores, result->pool, 32);
+ }
+ if (seq_range_array_add(uids_arr, uid)) {
+ /* duplicate result */
+ } else {
+ score = array_append_space(&br->scores);
+ score->uid = uid;
+ score->score = hits->score(i);
+ }
+ }
+ _CLDELETE(hits);
+ return ret;
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "multi search");
+ return -1;
+ }
+}
+
+int lucene_index_lookup_multi(struct lucene_index *index,
+ HASH_TABLE_TYPE(wguid_result) guids,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ struct mail_search_arg *arg;
+
+ if (lucene_index_open_search(index) <= 0)
+ return -1;
+
+ ARRAY_TYPE(lucene_query) def_queries;
+ t_array_init(&def_queries, 16);
+ bool have_definites = false;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (lucene_add_definite_query(index, def_queries, arg, flags)) {
+ arg->match_always = true;
+ have_definites = true;
+ }
+ }
+
+ if (have_definites) {
+ if (lucene_index_search_multi(index, guids, def_queries, flags,
+ result) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+struct lucene_index_iter {
+ struct lucene_index *index;
+ struct lucene_index_record rec;
+
+ Term *term;
+ WildcardQuery *query;
+ Sort *sort;
+
+ Hits *hits;
+ size_t i;
+ bool failed;
+};
+
+struct lucene_index_iter *
+lucene_index_iter_init(struct lucene_index *index)
+{
+ static const TCHAR *sort_fields[] = { _T("box"), _T("uid"), NULL };
+ struct lucene_index_iter *iter;
+ int ret;
+
+ iter = i_new(struct lucene_index_iter, 1);
+ iter->index = index;
+ if ((ret = lucene_index_open_search(index)) <= 0) {
+ if (ret < 0)
+ iter->failed = true;
+ return iter;
+ }
+
+ iter->term = _CLNEW Term(_T("box"), _T("*"));
+ iter->query = _CLNEW WildcardQuery(iter->term);
+ iter->sort = _CLNEW Sort(sort_fields);
+
+ try {
+ iter->hits = index->searcher->search(iter->query, iter->sort);
+ } catch (CLuceneError &err) {
+ lucene_handle_error(index, err, "rescan search");
+ iter->failed = true;
+ }
+ return iter;
+}
+
+const struct lucene_index_record *
+lucene_index_iter_next(struct lucene_index_iter *iter)
+{
+ if (iter->hits == NULL)
+ return NULL;
+ if (iter->i == iter->hits->length())
+ return NULL;
+
+ Document *doc = &iter->hits->doc(iter->i);
+ iter->i++;
+
+ memset(&iter->rec, 0, sizeof(iter->rec));
+ (void)fts_lucene_get_mailbox_guid(iter->index, doc,
+ iter->rec.mailbox_guid);
+ (void)lucene_doc_get_uid(iter->index, doc, &iter->rec.uid);
+ iter->rec.part_num = lucene_doc_get_part(iter->index, doc);
+ return &iter->rec;
+}
+
+int lucene_index_iter_deinit(struct lucene_index_iter **_iter)
+{
+ struct lucene_index_iter *iter = *_iter;
+ int ret = iter->failed ? -1 : 0;
+
+ *_iter = NULL;
+ if (iter->hits != NULL)
+ _CLDELETE(iter->hits);
+ if (iter->query != NULL) {
+ _CLDELETE(iter->query);
+ _CLDELETE(iter->sort);
+ _CLDELETE(iter->term);
+ }
+ i_free(iter);
+ return ret;
+}
+
+void lucene_shutdown(void)
+{
+ _lucene_shutdown();
+}
diff --git a/src/plugins/fts-lucene/lucene-wrapper.h b/src/plugins/fts-lucene/lucene-wrapper.h
new file mode 100644
index 0000000..270e902
--- /dev/null
+++ b/src/plugins/fts-lucene/lucene-wrapper.h
@@ -0,0 +1,67 @@
+#ifndef LUCENE_WRAPPER_H
+#define LUCENE_WRAPPER_H
+
+#include "fts-api-private.h"
+#include "guid.h"
+
+struct mailbox_list;
+struct fts_expunge_log;
+struct fts_lucene_settings;
+
+#define MAILBOX_GUID_HEX_LENGTH (GUID_128_SIZE*2)
+
+struct lucene_index_record {
+ guid_128_t mailbox_guid;
+ uint32_t uid, part_num;
+};
+
+HASH_TABLE_DEFINE_TYPE(wguid_result, wchar_t *, struct fts_result *);
+
+struct lucene_index *
+lucene_index_init(const char *path, struct mailbox_list *list,
+ const struct fts_lucene_settings *set)
+ ATTR_NULL(2, 3);
+void lucene_index_deinit(struct lucene_index *index);
+
+void lucene_index_select_mailbox(struct lucene_index *index,
+ const wchar_t guid[MAILBOX_GUID_HEX_LENGTH]);
+void lucene_index_unselect_mailbox(struct lucene_index *index);
+int lucene_index_get_last_uid(struct lucene_index *index, uint32_t *last_uid_r);
+int lucene_index_get_doc_count(struct lucene_index *index, uint32_t *count_r);
+
+int lucene_index_build_init(struct lucene_index *index);
+int lucene_index_build_more(struct lucene_index *index, uint32_t uid,
+ uint32_t part_num, const unsigned char *data,
+ size_t size, const char *hdr_name);
+int lucene_index_build_deinit(struct lucene_index *index);
+
+void lucene_index_close(struct lucene_index *index);
+int lucene_index_rescan(struct lucene_index *index);
+int lucene_index_expunge_from_log(struct lucene_index *index,
+ struct fts_expunge_log *log);
+int lucene_index_optimize(struct lucene_index *index);
+
+int lucene_index_lookup(struct lucene_index *index,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result);
+
+int lucene_index_lookup_multi(struct lucene_index *index,
+ HASH_TABLE_TYPE(wguid_result) guids,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result);
+
+struct lucene_index_iter *
+lucene_index_iter_init(struct lucene_index *index);
+const struct lucene_index_record *
+lucene_index_iter_next(struct lucene_index_iter *iter);
+int lucene_index_iter_deinit(struct lucene_index_iter **iter);
+
+/* internal: */
+void lucene_utf8_n_to_tchar(const unsigned char *src, size_t srcsize,
+ wchar_t *dest, size_t destsize);
+
+void lucene_shutdown(void);
+
+#endif
diff --git a/src/plugins/fts-lucene/textcat.conf b/src/plugins/fts-lucene/textcat.conf
new file mode 100644
index 0000000..d75c4fe
--- /dev/null
+++ b/src/plugins/fts-lucene/textcat.conf
@@ -0,0 +1,25 @@
+#
+# A sample config file for the language models
+# provided with Gertjan van Noords language guesser
+# (http://odur.let.rug.nl/~vannoord/TextCat/)
+#
+# Notes:
+# - You may consider eliminating a couple of small languages from this
+# list because they cause false positives with big languages and are
+# bad for performance. (Do you really want to recognize Drents?)
+# - Putting the most probable languages at the top of the list
+# improves performance, because this will raise the threshold for
+# likely candidates more quickly.
+#
+LM/english.lm english
+LM/italian.lm italian
+LM/danish.lm danish
+LM/dutch.lm dutch
+LM/finnish.lm finnish
+LM/french.lm french
+LM/german.lm german
+LM/norwegian.lm norwegian
+LM/portuguese.lm portuguese
+LM/russian.lm russian
+LM/spanish.lm spanish
+LM/swedish.lm swedish
diff --git a/src/plugins/fts-solr/Makefile.am b/src/plugins/fts-solr/Makefile.am
new file mode 100644
index 0000000..cd1e17b
--- /dev/null
+++ b/src/plugins/fts-solr/Makefile.am
@@ -0,0 +1,64 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-http \
+ -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/plugins/fts
+
+NOPLUGIN_LDFLAGS =
+lib21_fts_solr_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib21_fts_solr_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+fts_plugin_dep = ../fts/lib20_fts_plugin.la
+endif
+
+lib21_fts_solr_plugin_la_LIBADD = \
+ $(fts_plugin_dep) \
+ -lexpat
+
+lib21_fts_solr_plugin_la_SOURCES = \
+ fts-backend-solr.c \
+ fts-backend-solr-old.c \
+ fts-solr-plugin.c \
+ solr-response.c \
+ solr-connection.c
+
+noinst_HEADERS = \
+ fts-solr-plugin.h \
+ solr-response.h \
+ solr-connection.h
+
+test_programs = \
+ test-solr-response
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib-charset/libcharset.la \
+ ../../lib/liblib.la \
+ $(MODULE_LIBS)
+
+noinst_PROGRAMS = test-solr-response
+
+test_solr_response_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+test_solr_response_SOURCES = \
+ solr-response.c \
+ test-solr-response.c
+test_solr_response_LDADD = \
+ $(test_libs) -lexpat
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+check: check-am check-test
+check-test: all-am
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/plugins/fts-solr/Makefile.in b/src/plugins/fts-solr/Makefile.in
new file mode 100644
index 0000000..42857e1
--- /dev/null
+++ b/src/plugins/fts-solr/Makefile.in
@@ -0,0 +1,965 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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 = test-solr-response$(EXEEXT)
+subdir = src/plugins/fts-solr
+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 =
+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)
+lib21_fts_solr_plugin_la_DEPENDENCIES = $(fts_plugin_dep)
+am_lib21_fts_solr_plugin_la_OBJECTS = fts-backend-solr.lo \
+ fts-backend-solr-old.lo fts-solr-plugin.lo solr-response.lo \
+ solr-connection.lo
+lib21_fts_solr_plugin_la_OBJECTS = \
+ $(am_lib21_fts_solr_plugin_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 =
+lib21_fts_solr_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib21_fts_solr_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_solr_response_OBJECTS = \
+ test_solr_response-solr-response.$(OBJEXT) \
+ test_solr_response-test-solr-response.$(OBJEXT)
+test_solr_response_OBJECTS = $(am_test_solr_response_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = ../../lib-test/libtest.la \
+ ../../lib-charset/libcharset.la ../../lib/liblib.la \
+ $(am__DEPENDENCIES_1)
+test_solr_response_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)/fts-backend-solr-old.Plo \
+ ./$(DEPDIR)/fts-backend-solr.Plo \
+ ./$(DEPDIR)/fts-solr-plugin.Plo \
+ ./$(DEPDIR)/solr-connection.Plo ./$(DEPDIR)/solr-response.Plo \
+ ./$(DEPDIR)/test_solr_response-solr-response.Po \
+ ./$(DEPDIR)/test_solr_response-test-solr-response.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 = $(lib21_fts_solr_plugin_la_SOURCES) \
+ $(test_solr_response_SOURCES)
+DIST_SOURCES = $(lib21_fts_solr_plugin_la_SOURCES) \
+ $(test_solr_response_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 =
+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-ssl-iostream \
+ -I$(top_srcdir)/src/lib-http \
+ -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/plugins/fts
+
+lib21_fts_solr_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib21_fts_solr_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@fts_plugin_dep = ../fts/lib20_fts_plugin.la
+lib21_fts_solr_plugin_la_LIBADD = \
+ $(fts_plugin_dep) \
+ -lexpat
+
+lib21_fts_solr_plugin_la_SOURCES = \
+ fts-backend-solr.c \
+ fts-backend-solr-old.c \
+ fts-solr-plugin.c \
+ solr-response.c \
+ solr-connection.c
+
+noinst_HEADERS = \
+ fts-solr-plugin.h \
+ solr-response.h \
+ solr-connection.h
+
+test_programs = \
+ test-solr-response
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib-charset/libcharset.la \
+ ../../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_solr_response_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+
+test_solr_response_SOURCES = \
+ solr-response.c \
+ test-solr-response.c
+
+test_solr_response_LDADD = \
+ $(test_libs) -lexpat
+
+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/plugins/fts-solr/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/fts-solr/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}; \
+ }
+
+lib21_fts_solr_plugin.la: $(lib21_fts_solr_plugin_la_OBJECTS) $(lib21_fts_solr_plugin_la_DEPENDENCIES) $(EXTRA_lib21_fts_solr_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib21_fts_solr_plugin_la_LINK) -rpath $(moduledir) $(lib21_fts_solr_plugin_la_OBJECTS) $(lib21_fts_solr_plugin_la_LIBADD) $(LIBS)
+
+test-solr-response$(EXEEXT): $(test_solr_response_OBJECTS) $(test_solr_response_DEPENDENCIES) $(EXTRA_test_solr_response_DEPENDENCIES)
+ @rm -f test-solr-response$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_solr_response_OBJECTS) $(test_solr_response_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-backend-solr-old.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-backend-solr.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-solr-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/solr-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/solr-response.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_solr_response-solr-response.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_solr_response-test-solr-response.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_solr_response-solr-response.o: solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_solr_response-solr-response.o -MD -MP -MF $(DEPDIR)/test_solr_response-solr-response.Tpo -c -o test_solr_response-solr-response.o `test -f 'solr-response.c' || echo '$(srcdir)/'`solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_solr_response-solr-response.Tpo $(DEPDIR)/test_solr_response-solr-response.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='solr-response.c' object='test_solr_response-solr-response.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_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_solr_response-solr-response.o `test -f 'solr-response.c' || echo '$(srcdir)/'`solr-response.c
+
+test_solr_response-solr-response.obj: solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_solr_response-solr-response.obj -MD -MP -MF $(DEPDIR)/test_solr_response-solr-response.Tpo -c -o test_solr_response-solr-response.obj `if test -f 'solr-response.c'; then $(CYGPATH_W) 'solr-response.c'; else $(CYGPATH_W) '$(srcdir)/solr-response.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_solr_response-solr-response.Tpo $(DEPDIR)/test_solr_response-solr-response.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='solr-response.c' object='test_solr_response-solr-response.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_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_solr_response-solr-response.obj `if test -f 'solr-response.c'; then $(CYGPATH_W) 'solr-response.c'; else $(CYGPATH_W) '$(srcdir)/solr-response.c'; fi`
+
+test_solr_response-test-solr-response.o: test-solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_solr_response-test-solr-response.o -MD -MP -MF $(DEPDIR)/test_solr_response-test-solr-response.Tpo -c -o test_solr_response-test-solr-response.o `test -f 'test-solr-response.c' || echo '$(srcdir)/'`test-solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_solr_response-test-solr-response.Tpo $(DEPDIR)/test_solr_response-test-solr-response.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-solr-response.c' object='test_solr_response-test-solr-response.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_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_solr_response-test-solr-response.o `test -f 'test-solr-response.c' || echo '$(srcdir)/'`test-solr-response.c
+
+test_solr_response-test-solr-response.obj: test-solr-response.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_solr_response-test-solr-response.obj -MD -MP -MF $(DEPDIR)/test_solr_response-test-solr-response.Tpo -c -o test_solr_response-test-solr-response.obj `if test -f 'test-solr-response.c'; then $(CYGPATH_W) 'test-solr-response.c'; else $(CYGPATH_W) '$(srcdir)/test-solr-response.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_solr_response-test-solr-response.Tpo $(DEPDIR)/test_solr_response-test-solr-response.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-solr-response.c' object='test_solr_response-test-solr-response.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_solr_response_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_solr_response-test-solr-response.obj `if test -f 'test-solr-response.c'; then $(CYGPATH_W) 'test-solr-response.c'; else $(CYGPATH_W) '$(srcdir)/test-solr-response.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
+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-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fts-backend-solr-old.Plo
+ -rm -f ./$(DEPDIR)/fts-backend-solr.Plo
+ -rm -f ./$(DEPDIR)/fts-solr-plugin.Plo
+ -rm -f ./$(DEPDIR)/solr-connection.Plo
+ -rm -f ./$(DEPDIR)/solr-response.Plo
+ -rm -f ./$(DEPDIR)/test_solr_response-solr-response.Po
+ -rm -f ./$(DEPDIR)/test_solr_response-test-solr-response.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)/fts-backend-solr-old.Plo
+ -rm -f ./$(DEPDIR)/fts-backend-solr.Plo
+ -rm -f ./$(DEPDIR)/fts-solr-plugin.Plo
+ -rm -f ./$(DEPDIR)/solr-connection.Plo
+ -rm -f ./$(DEPDIR)/solr-response.Plo
+ -rm -f ./$(DEPDIR)/test_solr_response-solr-response.Po
+ -rm -f ./$(DEPDIR)/test_solr_response-test-solr-response.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: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ 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
+
+
+check: check-am check-test
+check-test: all-am
+ 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/plugins/fts-solr/fts-backend-solr-old.c b/src/plugins/fts-solr/fts-backend-solr-old.c
new file mode 100644
index 0000000..20b891b
--- /dev/null
+++ b/src/plugins/fts-solr/fts-backend-solr-old.c
@@ -0,0 +1,879 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "strescape.h"
+#include "unichar.h"
+#include "iostream-ssl.h"
+#include "http-url.h"
+#include "imap-utf7.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "mail-search.h"
+#include "fts-api.h"
+#include "solr-connection.h"
+#include "fts-solr-plugin.h"
+
+#include <ctype.h>
+
+#define SOLR_CMDBUF_SIZE (1024*64)
+#define SOLR_MAX_MULTI_ROWS 100000
+
+struct solr_fts_backend {
+ struct fts_backend backend;
+ struct solr_connection *solr_conn;
+ char *id_username, *id_namespace;
+ struct mail_namespace *default_ns;
+};
+
+struct solr_fts_backend_update_context {
+ struct fts_backend_update_context ctx;
+
+ struct mailbox *cur_box;
+ char *id_box_name;
+
+ struct solr_connection_post *post;
+ uint32_t prev_uid, uid_validity;
+ string_t *cmd, *hdr;
+
+ bool headers_open;
+ bool body_open;
+ bool documents_added;
+};
+
+static const char *solr_escape_chars = "+-&|!(){}[]^\"~*?:\\/ ";
+
+static bool is_valid_xml_char(unichar_t chr)
+{
+ /* Valid characters in XML:
+
+ #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
+ [#x10000-#x10FFFF]
+
+ This function gets called only for #x80 and higher */
+ if (chr > 0xd7ff && chr < 0xe000)
+ return FALSE;
+ if (chr > 0xfffd && chr < 0x10000)
+ return FALSE;
+ return chr < 0x10ffff;
+}
+
+static void
+xml_encode_data(string_t *dest, const unsigned char *data, size_t len)
+{
+ unichar_t chr;
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ switch (data[i]) {
+ case '&':
+ str_append(dest, "&amp;");
+ break;
+ case '<':
+ str_append(dest, "&lt;");
+ break;
+ case '>':
+ str_append(dest, "&gt;");
+ break;
+ case '\t':
+ case '\n':
+ case '\r':
+ /* exceptions to the following control char check */
+ str_append_c(dest, data[i]);
+ break;
+ default:
+ if (data[i] < 32) {
+ /* SOLR doesn't like control characters.
+ replace them with spaces. */
+ str_append_c(dest, ' ');
+ } else if (data[i] >= 0x80) {
+ /* make sure the character is valid for XML
+ so we don't get XML parser errors */
+ int char_len =
+ uni_utf8_get_char_n(data + i, len - i, &chr);
+ i_assert(char_len > 0); /* input is valid UTF8 */
+ if (is_valid_xml_char(chr))
+ str_append_data(dest, data + i, char_len);
+ else {
+ str_append_data(dest, utf8_replacement_char,
+ UTF8_REPLACEMENT_CHAR_LEN);
+ }
+ i += char_len - 1;
+ } else {
+ str_append_c(dest, data[i]);
+ }
+ break;
+ }
+ }
+}
+
+static void xml_encode(string_t *dest, const char *str)
+{
+ xml_encode_data(dest, (const unsigned char *)str, strlen(str));
+}
+
+static const char *solr_escape_id_str(const char *str)
+{
+ string_t *tmp;
+ const char *p;
+
+ for (p = str; *p != '\0'; p++) {
+ if (*p == '/' || *p == '!')
+ break;
+ }
+ if (*p == '\0')
+ return str;
+
+ tmp = t_str_new(64);
+ for (p = str; *p != '\0'; p++) {
+ switch (*p) {
+ case '/':
+ str_append(tmp, "!\\");
+ break;
+ case '!':
+ str_append(tmp, "!!");
+ break;
+ default:
+ str_append_c(tmp, *p);
+ break;
+ }
+ }
+ return str_c(tmp);
+}
+
+static const char *solr_escape(const char *str)
+{
+ string_t *ret;
+ unsigned int i;
+
+ if (str[0] == '\0')
+ return "\"\"";
+
+ ret = t_str_new(strlen(str) + 16);
+ for (i = 0; str[i] != '\0'; i++) {
+ if (strchr(solr_escape_chars, str[i]) != NULL)
+ str_append_c(ret, '\\');
+ str_append_c(ret, str[i]);
+ }
+ return str_c(ret);
+}
+
+static void solr_quote(string_t *dest, const char *str)
+{
+ str_append(dest, solr_escape(str));
+}
+
+static void solr_quote_http(string_t *dest, const char *str)
+{
+ http_url_escape_param(dest, solr_escape(str));
+}
+
+static void fts_solr_set_default_ns(struct solr_fts_backend *backend)
+{
+ struct mail_namespace *ns = backend->backend.ns;
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT_REQUIRE(ns->user);
+ const struct fts_solr_settings *set = &fuser->set;
+ const char *str;
+
+ if (backend->default_ns != NULL)
+ return;
+
+ if (set->default_ns_prefix != NULL) {
+ backend->default_ns =
+ mail_namespace_find_prefix(ns->user->namespaces,
+ set->default_ns_prefix);
+ if (backend->default_ns == NULL) {
+ i_error("fts_solr: default_ns setting points to "
+ "nonexistent namespace");
+ }
+ }
+ if (backend->default_ns == NULL) {
+ backend->default_ns =
+ mail_namespace_find_inbox(ns->user->namespaces);
+ }
+ while (backend->default_ns->alias_for != NULL)
+ backend->default_ns = backend->default_ns->alias_for;
+
+ if (ns != backend->default_ns) {
+ str = solr_escape_id_str(ns->prefix);
+ backend->id_namespace = i_strdup(str);
+ }
+}
+
+static void fts_box_name_get_root(struct mail_namespace **ns, const char **name)
+{
+ struct mail_namespace *orig_ns = *ns;
+
+ while ((*ns)->alias_for != NULL)
+ *ns = (*ns)->alias_for;
+
+ if (**name == '\0' && *ns != orig_ns &&
+ ((*ns)->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* ugly workaround to allow selecting INBOX from a Maildir/
+ when it's not in the inbox=yes namespace. */
+ *name = "INBOX";
+ }
+}
+
+static const char *
+fts_box_get_root(struct mailbox *box, struct mail_namespace **ns_r)
+{
+ struct mail_namespace *ns = mailbox_get_namespace(box);
+ const char *name;
+
+ if (t_imap_utf8_to_utf7(box->name, &name) < 0)
+ i_unreached();
+
+ fts_box_name_get_root(&ns, &name);
+ *ns_r = ns;
+ return name;
+}
+
+static struct fts_backend *fts_backend_solr_alloc(void)
+{
+ struct solr_fts_backend *backend;
+
+ backend = i_new(struct solr_fts_backend, 1);
+ backend->backend = fts_backend_solr_old;
+ return &backend->backend;
+}
+
+static int
+fts_backend_solr_init(struct fts_backend *_backend, const char **error_r)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT(_backend->ns->user);
+ struct ssl_iostream_settings ssl_set;
+ const char *str;
+
+ if (fuser == NULL) {
+ *error_r = "Invalid fts_solr setting";
+ return -1;
+ }
+
+ mail_user_init_ssl_client_settings(_backend->ns->user, &ssl_set);
+ if (solr_connection_init(&fuser->set, &ssl_set,
+ _backend->ns->user->event,
+ &backend->solr_conn, error_r) < 0)
+ return -1;
+
+ str = solr_escape_id_str(_backend->ns->user->username);
+ backend->id_username = i_strdup(str);
+ return 0;
+}
+
+static void fts_backend_solr_deinit(struct fts_backend *_backend)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+
+ solr_connection_deinit(&backend->solr_conn);
+ i_free(backend->id_namespace);
+ i_free(backend->id_username);
+ i_free(backend);
+}
+
+static void
+solr_add_ns_query(string_t *str, struct solr_fts_backend *backend,
+ struct mail_namespace *ns, bool neg)
+{
+ while (ns->alias_for != NULL)
+ ns = ns->alias_for;
+
+ if (ns == backend->default_ns || *ns->prefix == '\0') {
+ if (!neg)
+ str_append(str, " -ns:[* TO *]");
+ else
+ str_append(str, " +ns:[* TO *]");
+ } else {
+ if (!neg)
+ str_append(str, " +ns:");
+ else
+ str_append(str, " -ns:");
+ solr_quote(str, ns->prefix);
+ }
+}
+
+static void
+solr_add_ns_query_http(string_t *str, struct solr_fts_backend *backend,
+ struct mail_namespace *ns)
+{
+ string_t *tmp;
+
+ tmp = t_str_new(64);
+ solr_add_ns_query(tmp, backend, ns, FALSE);
+ http_url_escape_param(str, str_c(tmp));
+}
+
+static int
+fts_backend_solr_get_last_uid_fallback(struct solr_fts_backend *backend,
+ struct mailbox *box,
+ uint32_t *last_uid_r)
+{
+ struct mail_namespace *ns;
+ struct mailbox_status status;
+ struct solr_result **results;
+ const struct seq_range *uidvals;
+ const char *box_name;
+ unsigned int count;
+ string_t *str;
+ pool_t pool;
+ int ret = 0;
+
+ str = t_str_new(256);
+ str_append(str, "fl=uid&rows=1&sort=uid+desc&q=");
+
+ box_name = fts_box_get_root(box, &ns);
+
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY, &status);
+ str_printfa(str, "uidv:%u+AND+box:", status.uidvalidity);
+ solr_quote_http(str, box_name);
+ solr_add_ns_query_http(str, backend, ns);
+ str_append(str, "+AND+user:");
+ solr_quote_http(str, ns->user->username);
+
+ pool = pool_alloconly_create("solr last uid lookup", 1024);
+ if (solr_connection_select(backend->solr_conn, str_c(str),
+ pool, &results) < 0)
+ ret = -1;
+ else if (results[0] == NULL) {
+ /* no UIDs */
+ *last_uid_r = 0;
+ } else {
+ uidvals = array_get(&results[0]->uids, &count);
+ i_assert(count > 0);
+ if (count == 1 && uidvals[0].seq1 == uidvals[0].seq2) {
+ *last_uid_r = uidvals[0].seq1;
+ } else {
+ i_error("fts_solr: Last UID lookup returned multiple rows");
+ ret = -1;
+ }
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+static int
+fts_backend_solr_get_last_uid(struct fts_backend *_backend,
+ struct mailbox *box, uint32_t *last_uid_r)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_backend;
+ struct fts_index_header hdr;
+
+ if (fts_index_get_header(box, &hdr)) {
+ *last_uid_r = hdr.last_indexed_uid;
+ return 0;
+ }
+
+ /* either nothing has been indexed, or the index was corrupted.
+ do it the slow way. */
+ if (fts_backend_solr_get_last_uid_fallback(backend, box, last_uid_r) < 0)
+ return -1;
+
+ fts_index_set_last_uid(box, *last_uid_r);
+ return 0;
+}
+
+static struct fts_backend_update_context *
+fts_backend_solr_update_init(struct fts_backend *_backend)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_backend;
+ struct solr_fts_backend_update_context *ctx;
+
+ ctx = i_new(struct solr_fts_backend_update_context, 1);
+ ctx->ctx.backend = _backend;
+ ctx->cmd = str_new(default_pool, SOLR_CMDBUF_SIZE);
+ ctx->hdr = str_new(default_pool, 4096);
+ fts_solr_set_default_ns(backend);
+ return &ctx->ctx;
+}
+
+static void xml_encode_id(struct solr_fts_backend_update_context *ctx,
+ string_t *str, uint32_t uid)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)ctx->ctx.backend;
+
+ if (uid != 0)
+ str_printfa(str, "%u/", uid);
+ else
+ str_append(str, "L/");
+
+ if (backend->id_namespace != NULL) {
+ xml_encode(str, backend->id_namespace);
+ str_append_c(str, '/');
+ }
+ str_printfa(str, "%u/", ctx->uid_validity);
+ xml_encode(str, backend->id_username);
+ str_append_c(str, '/');
+ xml_encode(str, ctx->id_box_name);
+}
+
+static void
+fts_backend_solr_add_doc_prefix(struct solr_fts_backend_update_context *ctx,
+ uint32_t uid)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)ctx->ctx.backend;
+ struct mailbox *box = ctx->cur_box;
+ struct mail_namespace *ns;
+ const char *box_name;
+
+ ctx->documents_added = TRUE;
+
+ str_printfa(ctx->cmd, "<doc>"
+ "<field name=\"uid\">%u</field>"
+ "<field name=\"uidv\">%u</field>",
+ uid, ctx->uid_validity);
+
+ box_name = fts_box_get_root(box, &ns);
+
+ if (ns != backend->default_ns) {
+ str_append(ctx->cmd, "<field name=\"ns\">");
+ xml_encode(ctx->cmd, ns->prefix);
+ str_append(ctx->cmd, "</field>");
+ }
+ str_append(ctx->cmd, "<field name=\"box\">");
+ xml_encode(ctx->cmd, box_name);
+ str_append(ctx->cmd, "</field><field name=\"user\">");
+ xml_encode(ctx->cmd, ns->user->username);
+ str_append(ctx->cmd, "</field>");
+}
+
+static int
+fts_backed_solr_build_commit(struct solr_fts_backend_update_context *ctx)
+{
+ if (ctx->post == NULL)
+ return 0;
+
+ str_append(ctx->cmd, "</doc></add>");
+
+ solr_connection_post_more(ctx->post, str_data(ctx->cmd),
+ str_len(ctx->cmd));
+ return solr_connection_post_end(&ctx->post);
+}
+
+static int
+fts_backend_solr_update_deinit(struct fts_backend_update_context *_ctx)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_ctx->backend;
+ const char *str;
+ int ret;
+
+ ret = fts_backed_solr_build_commit(ctx);
+
+ /* commit and wait until the documents we just indexed are
+ visible to the following search */
+ str = t_strdup_printf("<commit waitFlush=\"false\" "
+ "waitSearcher=\"%s\"/>",
+ ctx->documents_added ? "true" : "false");
+ if (solr_connection_post(backend->solr_conn, str) < 0)
+ ret = -1;
+
+ str_free(&ctx->cmd);
+ str_free(&ctx->hdr);
+ i_free(ctx->id_box_name);
+ i_free(ctx);
+ return ret;
+}
+
+static void
+fts_backend_solr_update_set_mailbox(struct fts_backend_update_context *_ctx,
+ struct mailbox *box)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ struct mailbox_status status;
+ struct mail_namespace *ns;
+
+ if (ctx->prev_uid != 0) {
+ fts_index_set_last_uid(ctx->cur_box, ctx->prev_uid);
+ ctx->prev_uid = 0;
+ }
+
+ ctx->cur_box = box;
+ ctx->uid_validity = 0;
+ i_free_and_null(ctx->id_box_name);
+
+ if (box != NULL) {
+ ctx->id_box_name = i_strdup(fts_box_get_root(box, &ns));
+
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY, &status);
+ ctx->uid_validity = status.uidvalidity;
+ }
+}
+
+static void
+fts_backend_solr_update_expunge(struct fts_backend_update_context *_ctx,
+ uint32_t uid)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_ctx->backend;
+
+ T_BEGIN {
+ string_t *cmd;
+
+ cmd = t_str_new(256);
+ str_append(cmd, "<delete><id>");
+ xml_encode_id(ctx, cmd, uid);
+ str_append(cmd, "</id></delete>");
+
+ (void)solr_connection_post(backend->solr_conn, str_c(cmd));
+ } T_END;
+}
+
+static void
+fts_backend_solr_uid_changed(struct solr_fts_backend_update_context *ctx,
+ uint32_t uid)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)ctx->ctx.backend;
+
+ if (ctx->post == NULL) {
+ i_assert(ctx->prev_uid == 0);
+
+ ctx->post = solr_connection_post_begin(backend->solr_conn);
+ str_append(ctx->cmd, "<add>");
+ } else {
+ ctx->headers_open = FALSE;
+ if (ctx->body_open) {
+ ctx->body_open = FALSE;
+ str_append(ctx->cmd, "</field>");
+ }
+ str_append(ctx->cmd, "<field name=\"hdr\">");
+ str_append_str(ctx->cmd, ctx->hdr);
+ str_append(ctx->cmd, "</field>");
+ str_truncate(ctx->hdr, 0);
+
+ str_append(ctx->cmd, "</doc>");
+ }
+ ctx->prev_uid = uid;
+
+ fts_backend_solr_add_doc_prefix(ctx, uid);
+ str_printfa(ctx->cmd, "<field name=\"id\">");
+ xml_encode_id(ctx, ctx->cmd, uid);
+ str_append(ctx->cmd, "</field>");
+}
+
+static bool
+fts_backend_solr_update_set_build_key(struct fts_backend_update_context *_ctx,
+ const struct fts_backend_build_key *key)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+
+ if (key->uid != ctx->prev_uid)
+ fts_backend_solr_uid_changed(ctx, key->uid);
+
+ switch (key->type) {
+ case FTS_BACKEND_BUILD_KEY_HDR:
+ case FTS_BACKEND_BUILD_KEY_MIME_HDR:
+ xml_encode(ctx->hdr, key->hdr_name);
+ str_append(ctx->hdr, ": ");
+ ctx->headers_open = TRUE;
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART:
+ ctx->headers_open = FALSE;
+ if (!ctx->body_open) {
+ ctx->body_open = TRUE;
+ str_append(ctx->cmd, "<field name=\"body\">");
+ }
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY:
+ i_unreached();
+ }
+ return TRUE;
+}
+
+static void
+fts_backend_solr_update_unset_build_key(struct fts_backend_update_context *_ctx)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+
+ if (ctx->headers_open)
+ str_append_c(ctx->hdr, '\n');
+ else {
+ i_assert(ctx->body_open);
+ str_append_c(ctx->cmd, '\n');
+ }
+}
+
+static int
+fts_backend_solr_update_build_more(struct fts_backend_update_context *_ctx,
+ const unsigned char *data, size_t size)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+
+ xml_encode_data(ctx->cmd, data, size);
+ if (str_len(ctx->cmd) > SOLR_CMDBUF_SIZE-128) {
+ solr_connection_post_more(ctx->post, str_data(ctx->cmd),
+ str_len(ctx->cmd));
+ str_truncate(ctx->cmd, 0);
+ }
+ return 0;
+}
+
+static int fts_backend_solr_refresh(struct fts_backend *backend ATTR_UNUSED)
+{
+ return 0;
+}
+
+static int fts_backend_solr_optimize(struct fts_backend *backend ATTR_UNUSED)
+{
+ return 0;
+}
+
+static bool
+solr_add_definite_query(string_t *str, struct mail_search_arg *arg)
+{
+ if (arg->no_fts)
+ return FALSE;
+ switch (arg->type) {
+ case SEARCH_TEXT: {
+ if (arg->match_not)
+ str_append_c(str, '-');
+ str_append(str, "(hdr:");
+ solr_quote_http(str, arg->value.str);
+ str_append(str, "+OR+body:");
+ solr_quote_http(str, arg->value.str);
+ str_append(str, ")");
+ break;
+ }
+ case SEARCH_BODY:
+ if (arg->match_not)
+ str_append_c(str, '-');
+ str_append(str, "body:");
+ solr_quote_http(str, arg->value.str);
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+solr_add_definite_query_args(string_t *str, struct mail_search_arg *arg,
+ bool and_args)
+{
+ size_t last_len;
+
+ last_len = str_len(str);
+ for (; arg != NULL; arg = arg->next) {
+ if (solr_add_definite_query(str, arg)) {
+ arg->match_always = TRUE;
+ last_len = str_len(str);
+ if (and_args)
+ str_append(str, "+AND+");
+ else
+ str_append(str, "+OR+");
+ }
+ }
+ if (str_len(str) == last_len)
+ return FALSE;
+
+ str_truncate(str, last_len);
+ return TRUE;
+}
+
+static int
+fts_backend_solr_lookup(struct fts_backend *_backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_backend;
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ struct mail_namespace *ns;
+ struct mailbox_status status;
+ string_t *str;
+ const char *box_name;
+ pool_t pool;
+ struct solr_result **results;
+ int ret;
+
+ fts_solr_set_default_ns(backend);
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT,
+ &status);
+
+ str = t_str_new(256);
+ str_printfa(str, "fl=uid,score&rows=%u&sort=uid+asc&q=%%7b!lucene+q.op%%3dAND%%7d",
+ status.uidnext);
+
+ if (!solr_add_definite_query_args(str, args, and_args)) {
+ /* can't search this query */
+ return 0;
+ }
+
+ /* use a separate filter query for selecting the mailbox. it shouldn't
+ affect the score and there could be some caching benefits too. */
+ str_append(str, "&fq=%2Buser:");
+ solr_quote_http(str, box->storage->user->username);
+ box_name = fts_box_get_root(box, &ns);
+ str_printfa(str, "+%%2Buidv:%u+%%2Bbox:", status.uidvalidity);
+ solr_quote_http(str, box_name);
+ solr_add_ns_query_http(str, backend, ns);
+
+ pool = pool_alloconly_create("fts solr search", 1024);
+ ret = solr_connection_select(backend->solr_conn, str_c(str),
+ pool, &results);
+ if (ret == 0 && results[0] != NULL) {
+ if ((flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0)
+ array_append_array(&result->definite_uids, &results[0]->uids);
+ else
+ array_append_array(&result->maybe_uids, &results[0]->uids);
+ array_append_array(&result->scores, &results[0]->scores);
+ }
+ result->scores_sorted = TRUE;
+ pool_unref(&pool);
+ return ret;
+}
+
+static char *
+mailbox_get_id(struct solr_fts_backend *backend, struct mail_namespace *ns,
+ const char *mailbox, uint32_t uidvalidity)
+{
+ string_t *str = t_str_new(64);
+
+ str_printfa(str, "%u\001", uidvalidity);
+ str_append(str, mailbox);
+ if (ns != backend->default_ns)
+ str_printfa(str, "\001%s", ns->prefix);
+ return str_c_modifiable(str);
+}
+
+static int
+solr_search_multi(struct solr_fts_backend *backend, string_t *str,
+ struct mailbox *const boxes[],
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ struct solr_result **solr_results;
+ struct fts_result *fts_result;
+ ARRAY(struct fts_result) fts_results;
+ struct mail_namespace *ns;
+ struct mailbox_status status;
+ HASH_TABLE(char *, struct mailbox *) mailboxes;
+ struct mailbox *box;
+ const char *box_name;
+ char *box_id;
+ unsigned int i;
+ size_t len;
+
+ /* use a separate filter query for selecting the mailbox. it shouldn't
+ affect the score and there could be some caching benefits too. */
+ str_append(str, "&fq=%2Buser:");
+ if (backend->backend.ns->owner != NULL)
+ solr_quote_http(str, backend->backend.ns->owner->username);
+ else
+ str_append(str, "%22%22");
+
+ hash_table_create(&mailboxes, default_pool, 0, str_hash, strcmp);
+ str_append(str, "%2B(");
+ len = str_len(str);
+ for (i = 0; boxes[i] != NULL; i++) {
+ if (str_len(str) != len)
+ str_append(str, "+OR+");
+
+ box_name = fts_box_get_root(boxes[i], &ns);
+ mailbox_get_open_status(boxes[i], STATUS_UIDVALIDITY, &status);
+ str_printfa(str, "%%2B(%%2Buidv:%u+%%2Bbox:", status.uidvalidity);
+ solr_quote_http(str, box_name);
+ solr_add_ns_query_http(str, backend, ns);
+ str_append_c(str, ')');
+
+ box_id = mailbox_get_id(backend, ns, box_name, status.uidvalidity);
+ hash_table_insert(mailboxes, box_id, boxes[i]);
+ }
+ str_append_c(str, ')');
+
+ if (solr_connection_select(backend->solr_conn, str_c(str),
+ result->pool, &solr_results) < 0) {
+ hash_table_destroy(&mailboxes);
+ return -1;
+ }
+
+ p_array_init(&fts_results, result->pool, 32);
+ for (i = 0; solr_results[i] != NULL; i++) {
+ box = hash_table_lookup(mailboxes, solr_results[i]->box_id);
+ if (box == NULL) {
+ i_warning("fts_solr: Lookup returned unexpected mailbox "
+ "with id=%s", solr_results[i]->box_id);
+ continue;
+ }
+ fts_result = array_append_space(&fts_results);
+ fts_result->box = box;
+ if ((flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0)
+ fts_result->definite_uids = solr_results[i]->uids;
+ else
+ fts_result->maybe_uids = solr_results[i]->uids;
+ fts_result->scores = solr_results[i]->scores;
+ fts_result->scores_sorted = TRUE;
+ }
+ array_append_zero(&fts_results);
+ result->box_results = array_front_modifiable(&fts_results);
+ hash_table_destroy(&mailboxes);
+ return 0;
+}
+
+static int
+fts_backend_solr_lookup_multi(struct fts_backend *_backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_backend;
+ string_t *str;
+
+ fts_solr_set_default_ns(backend);
+
+ str = t_str_new(256);
+ str_printfa(str, "fl=ns,box,uidv,uid,score&rows=%u&sort=box+asc,uid+asc&q=%%7b!lucene+q.op%%3dAND%%7d",
+ SOLR_MAX_MULTI_ROWS);
+
+ if (solr_add_definite_query_args(str, args, and_args)) {
+ if (solr_search_multi(backend, str, boxes, flags, result) < 0)
+ return -1;
+ }
+ /* FIXME: maybe_uids could be handled also with some more work.. */
+ return 0;
+}
+
+struct fts_backend fts_backend_solr_old = {
+ .name = "solr_old",
+ .flags = 0,
+
+ {
+ fts_backend_solr_alloc,
+ fts_backend_solr_init,
+ fts_backend_solr_deinit,
+ fts_backend_solr_get_last_uid,
+ fts_backend_solr_update_init,
+ fts_backend_solr_update_deinit,
+ fts_backend_solr_update_set_mailbox,
+ fts_backend_solr_update_expunge,
+ fts_backend_solr_update_set_build_key,
+ fts_backend_solr_update_unset_build_key,
+ fts_backend_solr_update_build_more,
+ fts_backend_solr_refresh,
+ NULL,
+ fts_backend_solr_optimize,
+ fts_backend_default_can_lookup,
+ fts_backend_solr_lookup,
+ fts_backend_solr_lookup_multi,
+ NULL
+ }
+};
diff --git a/src/plugins/fts-solr/fts-backend-solr.c b/src/plugins/fts-solr/fts-backend-solr.c
new file mode 100644
index 0000000..0ac0f18
--- /dev/null
+++ b/src/plugins/fts-solr/fts-backend-solr.c
@@ -0,0 +1,984 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "strescape.h"
+#include "unichar.h"
+#include "iostream-ssl.h"
+#include "http-url.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "mail-search.h"
+#include "fts-api.h"
+#include "solr-connection.h"
+#include "fts-solr-plugin.h"
+
+#include <ctype.h>
+
+#define SOLR_CMDBUF_SIZE (1024*64)
+#define SOLR_CMDBUF_FLUSH_SIZE (SOLR_CMDBUF_SIZE-128)
+#define SOLR_MAX_MULTI_ROWS 100000
+
+/* If header is larger than this, truncate it. */
+#define SOLR_HEADER_MAX_SIZE (1024*1024)
+/* If SOLR_HEADER_MAX_SIZE was already reached, write still to individual
+ header fields as long as they're smaller than this */
+#define SOLR_HEADER_LINE_MAX_TRUNC_SIZE 1024
+
+#define SOLR_QUERY_MAX_MAILBOX_COUNT 10
+
+struct solr_fts_backend {
+ struct fts_backend backend;
+ struct solr_connection *solr_conn;
+};
+
+struct solr_fts_field {
+ char *key;
+ string_t *value;
+};
+
+struct solr_fts_backend_update_context {
+ struct fts_backend_update_context ctx;
+
+ struct mailbox *cur_box;
+ char box_guid[MAILBOX_GUID_HEX_LENGTH+1];
+
+ struct solr_connection_post *post;
+ uint32_t prev_uid;
+ string_t *cmd, *cur_value, *cur_value2;
+ string_t *cmd_expunge;
+ ARRAY(struct solr_fts_field) fields;
+
+ uint32_t last_indexed_uid;
+ unsigned int mails_since_flush;
+
+ bool tokenized_input:1;
+ bool last_indexed_uid_set:1;
+ bool body_open:1;
+ bool documents_added:1;
+ bool expunges:1;
+ bool truncate_header:1;
+};
+
+static const char *solr_escape_chars = "+-&|!(){}[]^\"~*?:\\/ ";
+
+static bool is_valid_xml_char(unichar_t chr)
+{
+ /* Valid characters in XML:
+
+ #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
+ [#x10000-#x10FFFF]
+
+ This function gets called only for #x80 and higher */
+ if (chr > 0xd7ff && chr < 0xe000)
+ return FALSE;
+ if (chr > 0xfffd && chr < 0x10000)
+ return FALSE;
+ return chr < 0x10ffff;
+}
+
+static size_t
+xml_encode_data_max(string_t *dest, const unsigned char *data, size_t len,
+ unsigned int max_len)
+{
+ unichar_t chr;
+ size_t i;
+
+ i_assert(max_len > 0 || len == 0);
+
+ if (max_len > len)
+ max_len = len;
+ for (i = 0; i < max_len; i++) {
+ switch (data[i]) {
+ case '&':
+ str_append(dest, "&amp;");
+ break;
+ case '<':
+ str_append(dest, "&lt;");
+ break;
+ case '>':
+ str_append(dest, "&gt;");
+ break;
+ case '\t':
+ case '\n':
+ case '\r':
+ /* exceptions to the following control char check */
+ str_append_c(dest, data[i]);
+ break;
+ default:
+ if (data[i] < 32) {
+ /* SOLR doesn't like control characters.
+ replace them with spaces. */
+ str_append_c(dest, ' ');
+ } else if (data[i] >= 0x80) {
+ /* make sure the character is valid for XML
+ so we don't get XML parser errors */
+ int char_len =
+ uni_utf8_get_char_n(data + i, len - i, &chr);
+ i_assert(char_len > 0); /* input is valid UTF8 */
+ if (is_valid_xml_char(chr))
+ str_append_data(dest, data + i, char_len);
+ else {
+ str_append_data(dest, utf8_replacement_char,
+ UTF8_REPLACEMENT_CHAR_LEN);
+ }
+ i += char_len - 1;
+ } else {
+ str_append_c(dest, data[i]);
+ }
+ break;
+ }
+ }
+ return i;
+}
+
+static void
+xml_encode_data(string_t *dest, const unsigned char *data, size_t len)
+{
+ (void)xml_encode_data_max(dest, data, len, len);
+}
+
+static void xml_encode(string_t *dest, const char *str)
+{
+ xml_encode_data(dest, (const unsigned char *)str, strlen(str));
+}
+
+static const char *solr_escape(const char *str)
+{
+ string_t *ret;
+ unsigned int i;
+
+ ret = t_str_new(strlen(str) + 16);
+ for (i = 0; str[i] != '\0'; i++) {
+ if (strchr(solr_escape_chars, str[i]) != NULL)
+ str_append_c(ret, '\\');
+ str_append_c(ret, str[i]);
+ }
+ return str_c(ret);
+}
+
+static void solr_quote_http(string_t *dest, const char *str)
+{
+ if (str[0] != '\0')
+ http_url_escape_param(dest, solr_escape(str));
+ else
+ str_append(dest, "%22%22");
+}
+
+static struct fts_backend *fts_backend_solr_alloc(void)
+{
+ struct solr_fts_backend *backend;
+
+ backend = i_new(struct solr_fts_backend, 1);
+ backend->backend = fts_backend_solr;
+ return &backend->backend;
+}
+
+static int
+fts_backend_solr_init(struct fts_backend *_backend, const char **error_r)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT(_backend->ns->user);
+ struct ssl_iostream_settings ssl_set;
+
+ if (fuser == NULL) {
+ *error_r = "Invalid fts_solr setting";
+ return -1;
+ }
+ if (fuser->set.use_libfts) {
+ /* change our flags so we get proper input */
+ _backend->flags &= ENUM_NEGATE(FTS_BACKEND_FLAG_FUZZY_SEARCH);
+ _backend->flags |= FTS_BACKEND_FLAG_TOKENIZED_INPUT;
+ }
+
+ mail_user_init_ssl_client_settings(_backend->ns->user, &ssl_set);
+ return solr_connection_init(&fuser->set, &ssl_set,
+ _backend->ns->user->event,
+ &backend->solr_conn, error_r);
+}
+
+static void fts_backend_solr_deinit(struct fts_backend *_backend)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+
+ solr_connection_deinit(&backend->solr_conn);
+ i_free(backend);
+}
+
+static int
+get_last_uid_fallback(struct fts_backend *_backend, struct mailbox *box,
+ uint32_t *last_uid_r)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+ const struct seq_range *uidvals;
+ const char *box_guid;
+ unsigned int count;
+ struct solr_result **results;
+ string_t *str;
+ pool_t pool;
+ int ret = 0;
+
+ str = t_str_new(256);
+ str_append(str, "wt=xml&fl=uid&rows=1&sort=uid+desc&q=");
+
+ if (fts_mailbox_get_guid(box, &box_guid) < 0)
+ return -1;
+
+ str_printfa(str, "box:%s+AND+user:", box_guid);
+ if (_backend->ns->owner != NULL)
+ solr_quote_http(str, _backend->ns->owner->username);
+ else
+ str_append(str, "%22%22");
+
+ pool = pool_alloconly_create("solr last uid lookup", 1024);
+ if (solr_connection_select(backend->solr_conn, str_c(str),
+ pool, &results) < 0)
+ ret = -1;
+ else if (results[0] == NULL) {
+ /* no UIDs */
+ *last_uid_r = 0;
+ } else {
+ uidvals = array_get(&results[0]->uids, &count);
+ i_assert(count > 0);
+ if (count == 1 && uidvals[0].seq1 == uidvals[0].seq2) {
+ *last_uid_r = uidvals[0].seq1;
+ } else {
+ i_error("fts_solr: Last UID lookup returned multiple rows");
+ ret = -1;
+ }
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+static int
+fts_backend_solr_get_last_uid(struct fts_backend *_backend,
+ struct mailbox *box, uint32_t *last_uid_r)
+{
+ struct fts_index_header hdr;
+
+ if (fts_index_get_header(box, &hdr)) {
+ *last_uid_r = hdr.last_indexed_uid;
+ return 0;
+ }
+
+ /* either nothing has been indexed, or the index was corrupted.
+ do it the slow way. */
+ if (get_last_uid_fallback(_backend, box, last_uid_r) < 0)
+ return -1;
+
+ fts_index_set_last_uid(box, *last_uid_r);
+ return 0;
+}
+
+static struct fts_backend_update_context *
+fts_backend_solr_update_init(struct fts_backend *_backend)
+{
+ struct solr_fts_backend_update_context *ctx;
+
+ ctx = i_new(struct solr_fts_backend_update_context, 1);
+ ctx->ctx.backend = _backend;
+ ctx->tokenized_input =
+ (_backend->flags & FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0;
+ i_array_init(&ctx->fields, 16);
+ return &ctx->ctx;
+}
+
+static void xml_encode_id(struct solr_fts_backend_update_context *ctx,
+ string_t *str, uint32_t uid)
+{
+ str_printfa(str, "%u/%s", uid, ctx->box_guid);
+ if (ctx->ctx.backend->ns->owner != NULL) {
+ str_append_c(str, '/');
+ xml_encode(str, ctx->ctx.backend->ns->owner->username);
+ }
+}
+
+static void
+fts_backend_solr_doc_open(struct solr_fts_backend_update_context *ctx,
+ uint32_t uid)
+{
+ ctx->documents_added = TRUE;
+
+ str_printfa(ctx->cmd, "<doc>"
+ "<field name=\"uid\">%u</field>"
+ "<field name=\"box\">%s</field>",
+ uid, ctx->box_guid);
+ str_append(ctx->cmd, "<field name=\"user\">");
+ if (ctx->ctx.backend->ns->owner != NULL)
+ xml_encode(ctx->cmd, ctx->ctx.backend->ns->owner->username);
+ str_append(ctx->cmd, "</field>");
+
+ str_printfa(ctx->cmd, "<field name=\"id\">");
+ xml_encode_id(ctx, ctx->cmd, uid);
+ str_append(ctx->cmd, "</field>");
+}
+
+static string_t *
+fts_solr_field_get(struct solr_fts_backend_update_context *ctx, const char *key)
+{
+ const struct solr_fts_field *field;
+ struct solr_fts_field new_field;
+
+ /* there are only a few fields. this lookup is fast enough. */
+ array_foreach(&ctx->fields, field) {
+ if (strcasecmp(field->key, key) == 0)
+ return field->value;
+ }
+
+ i_zero(&new_field);
+ new_field.key = str_lcase(i_strdup(key));
+ new_field.value = str_new(default_pool, 128);
+ array_push_back(&ctx->fields, &new_field);
+ return new_field.value;
+}
+
+static void
+fts_backend_solr_doc_close(struct solr_fts_backend_update_context *ctx)
+{
+ struct solr_fts_field *field;
+
+ if (ctx->body_open) {
+ ctx->body_open = FALSE;
+ str_append(ctx->cmd, "</field>");
+ }
+ array_foreach_modifiable(&ctx->fields, field) {
+ str_printfa(ctx->cmd, "<field name=\"%s\">", field->key);
+ /* the values are already xml-escaped */
+ str_append_str(ctx->cmd, field->value);
+ str_append(ctx->cmd, "</field>");
+ str_truncate(field->value, 0);
+ }
+ str_append(ctx->cmd, "</doc>");
+}
+
+static int
+fts_backed_solr_build_flush(struct solr_fts_backend_update_context *ctx)
+{
+ if (ctx->post == NULL)
+ return 0;
+
+ fts_backend_solr_doc_close(ctx);
+ str_append(ctx->cmd, "</add>");
+ ctx->mails_since_flush = 0;
+
+ solr_connection_post_more(ctx->post, str_data(ctx->cmd),
+ str_len(ctx->cmd));
+ str_truncate(ctx->cmd, 0);
+ return solr_connection_post_end(&ctx->post);
+}
+
+static void
+fts_backend_solr_expunge_flush(struct solr_fts_backend_update_context *ctx)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)ctx->ctx.backend;
+
+ str_append(ctx->cmd_expunge, "</delete>");
+ (void)solr_connection_post(backend->solr_conn, str_c(ctx->cmd_expunge));
+ str_truncate(ctx->cmd_expunge, 0);
+ str_append(ctx->cmd_expunge, "<delete>");
+}
+
+static int
+fts_backend_solr_update_deinit(struct fts_backend_update_context *_ctx)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)_ctx->backend;
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT(_ctx->backend->ns->user);
+ struct solr_fts_field *field;
+ const char *str;
+ int ret = _ctx->failed ? -1 : 0;
+
+ if (fts_backed_solr_build_flush(ctx) < 0)
+ ret = -1;
+
+ if (ctx->documents_added || ctx->expunges) {
+ /* commit and wait until the documents we just indexed are
+ visible to the following search */
+ if (ctx->expunges)
+ fts_backend_solr_expunge_flush(ctx);
+ if (fuser->set.soft_commit) {
+ str = t_strdup_printf("<commit softCommit=\"true\" waitSearcher=\"%s\"/>",
+ ctx->documents_added ? "true" : "false");
+ if (solr_connection_post(backend->solr_conn, str) < 0)
+ ret = -1;
+ }
+ }
+
+ str_free(&ctx->cmd);
+ str_free(&ctx->cmd_expunge);
+ array_foreach_modifiable(&ctx->fields, field) {
+ str_free(&field->value);
+ i_free(field->key);
+ }
+ array_free(&ctx->fields);
+ i_free(ctx);
+ return ret;
+}
+
+static void
+fts_backend_solr_update_set_mailbox(struct fts_backend_update_context *_ctx,
+ struct mailbox *box)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ const char *box_guid;
+
+ if (ctx->prev_uid != 0) {
+ i_assert(ctx->cur_box != NULL);
+
+ /* flush solr between mailboxes, so we don't wrongly update
+ last_uid before we know it has succeeded */
+ if (fts_backed_solr_build_flush(ctx) < 0)
+ _ctx->failed = TRUE;
+ else if (!_ctx->failed)
+ fts_index_set_last_uid(ctx->cur_box, ctx->prev_uid);
+ ctx->prev_uid = 0;
+ }
+
+ if (box != NULL) {
+ if (fts_mailbox_get_guid(box, &box_guid) < 0)
+ _ctx->failed = TRUE;
+
+ i_assert(strlen(box_guid) == sizeof(ctx->box_guid)-1);
+ memcpy(ctx->box_guid, box_guid, sizeof(ctx->box_guid)-1);
+ } else {
+ memset(ctx->box_guid, 0, sizeof(ctx->box_guid));
+ }
+ ctx->cur_box = box;
+}
+
+static void
+fts_backend_solr_update_expunge(struct fts_backend_update_context *_ctx,
+ uint32_t uid)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ struct fts_index_header hdr;
+
+ if (!ctx->last_indexed_uid_set) {
+ if (!fts_index_get_header(ctx->cur_box, &hdr))
+ ctx->last_indexed_uid = 0;
+ else
+ ctx->last_indexed_uid = hdr.last_indexed_uid;
+ ctx->last_indexed_uid_set = TRUE;
+ }
+ if (ctx->last_indexed_uid == 0 ||
+ uid > ctx->last_indexed_uid + 100) {
+ /* don't waste time asking Solr to expunge a message that is
+ highly unlikely to be indexed at this time. */
+ return;
+ }
+ if (!ctx->expunges) {
+ ctx->expunges = TRUE;
+ ctx->cmd_expunge = str_new(default_pool, 1024);
+ str_append(ctx->cmd_expunge, "<delete>");
+ }
+
+ if (str_len(ctx->cmd_expunge) >= SOLR_CMDBUF_FLUSH_SIZE)
+ fts_backend_solr_expunge_flush(ctx);
+
+ str_append(ctx->cmd_expunge, "<id>");
+ xml_encode_id(ctx, ctx->cmd_expunge, uid);
+ str_append(ctx->cmd_expunge, "</id>");
+}
+
+static void
+fts_backend_solr_uid_changed(struct solr_fts_backend_update_context *ctx,
+ uint32_t uid)
+{
+ struct solr_fts_backend *backend =
+ (struct solr_fts_backend *)ctx->ctx.backend;
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT(ctx->ctx.backend->ns->user);
+
+ if (ctx->mails_since_flush >= fuser->set.batch_size) {
+ if (fts_backed_solr_build_flush(ctx) < 0)
+ ctx->ctx.failed = TRUE;
+ }
+ ctx->mails_since_flush++;
+ if (ctx->post == NULL) {
+ if (ctx->cmd == NULL)
+ ctx->cmd = str_new(default_pool, SOLR_CMDBUF_SIZE);
+ ctx->post = solr_connection_post_begin(backend->solr_conn);
+ str_append(ctx->cmd, "<add>");
+ } else {
+ fts_backend_solr_doc_close(ctx);
+ }
+ ctx->prev_uid = uid;
+ ctx->truncate_header = FALSE;
+ fts_backend_solr_doc_open(ctx, uid);
+}
+
+static bool
+fts_backend_solr_update_set_build_key(struct fts_backend_update_context *_ctx,
+ const struct fts_backend_build_key *key)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+
+ if (key->uid != ctx->prev_uid)
+ fts_backend_solr_uid_changed(ctx, key->uid);
+
+ switch (key->type) {
+ case FTS_BACKEND_BUILD_KEY_HDR:
+ if (fts_header_want_indexed(key->hdr_name)) {
+ ctx->cur_value2 =
+ fts_solr_field_get(ctx, key->hdr_name);
+ }
+ /* fall through */
+ case FTS_BACKEND_BUILD_KEY_MIME_HDR:
+ ctx->cur_value = fts_solr_field_get(ctx, "hdr");
+ xml_encode(ctx->cur_value, key->hdr_name);
+ str_append(ctx->cur_value, ": ");
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART:
+ if (!ctx->body_open) {
+ ctx->body_open = TRUE;
+ str_append(ctx->cmd, "<field name=\"body\">");
+ }
+ ctx->cur_value = ctx->cmd;
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY:
+ i_unreached();
+ }
+ return TRUE;
+}
+
+static void
+fts_backend_solr_update_unset_build_key(struct fts_backend_update_context *_ctx)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+
+ /* There can be multiple duplicate keys (duplicate header lines,
+ multiple MIME body parts). Make sure they are separated by
+ whitespace. */
+ str_append_c(ctx->cur_value, '\n');
+ ctx->cur_value = NULL;
+ if (ctx->cur_value2 != NULL) {
+ str_append_c(ctx->cur_value2, '\n');
+ ctx->cur_value2 = NULL;
+ }
+}
+
+static int
+fts_backend_solr_update_build_more(struct fts_backend_update_context *_ctx,
+ const unsigned char *data, size_t size)
+{
+ struct solr_fts_backend_update_context *ctx =
+ (struct solr_fts_backend_update_context *)_ctx;
+ size_t len;
+
+ if (_ctx->failed)
+ return -1;
+
+ if (ctx->cur_value2 == NULL && ctx->cur_value == ctx->cmd) {
+ /* we're writing to message body. if size is huge,
+ flush it once in a while */
+ while (size >= SOLR_CMDBUF_FLUSH_SIZE) {
+ if (str_len(ctx->cmd) >= SOLR_CMDBUF_FLUSH_SIZE) {
+ solr_connection_post_more(ctx->post,
+ str_data(ctx->cmd),
+ str_len(ctx->cmd));
+ str_truncate(ctx->cmd, 0);
+ }
+ len = xml_encode_data_max(ctx->cmd, data, size,
+ SOLR_CMDBUF_FLUSH_SIZE -
+ str_len(ctx->cmd));
+ i_assert(len > 0);
+ i_assert(len <= size);
+ data += len;
+ size -= len;
+ }
+ xml_encode_data(ctx->cmd, data, size);
+ if (ctx->tokenized_input)
+ str_append_c(ctx->cmd, ' ');
+ } else {
+ if (!ctx->truncate_header) {
+ xml_encode_data(ctx->cur_value, data, size);
+ if (ctx->tokenized_input)
+ str_append_c(ctx->cur_value, ' ');
+ }
+ if (ctx->cur_value2 != NULL &&
+ (!ctx->truncate_header ||
+ str_len(ctx->cur_value2) < SOLR_HEADER_LINE_MAX_TRUNC_SIZE)) {
+ xml_encode_data(ctx->cur_value2, data, size);
+ if (ctx->tokenized_input)
+ str_append_c(ctx->cur_value2, ' ');
+ }
+ }
+
+ if (str_len(ctx->cmd) >= SOLR_CMDBUF_FLUSH_SIZE) {
+ solr_connection_post_more(ctx->post, str_data(ctx->cmd),
+ str_len(ctx->cmd));
+ str_truncate(ctx->cmd, 0);
+ }
+ if (!ctx->truncate_header &&
+ str_len(ctx->cur_value) >= SOLR_HEADER_MAX_SIZE) {
+ /* a large header */
+ i_assert(ctx->cur_value != ctx->cmd);
+
+ i_warning("fts-solr(%s): Mailbox %s UID=%u header size is huge, truncating",
+ ctx->cur_box->storage->user->username,
+ mailbox_get_vname(ctx->cur_box), ctx->prev_uid);
+ ctx->truncate_header = TRUE;
+ }
+ return 0;
+}
+
+static int fts_backend_solr_refresh(struct fts_backend *backend ATTR_UNUSED)
+{
+ return 0;
+}
+
+static int fts_backend_solr_rescan(struct fts_backend *backend)
+{
+ /* FIXME: proper rescan needed. for now we'll just reset the
+ last-uids */
+ return fts_backend_reset_last_uids(backend);
+}
+
+static int fts_backend_solr_optimize(struct fts_backend *backend ATTR_UNUSED)
+{
+ return 0;
+}
+
+static bool solr_need_escaping(const char *str)
+{
+ for (; *str != '\0'; str++) {
+ if (strchr(solr_escape_chars, *str) != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void solr_add_str_arg(string_t *str, struct mail_search_arg *arg)
+{
+ /* currently we'll just disable fuzzy searching if there are any
+ parameters that need escaping. solr doesn't seem to give good
+ fuzzy results even if we did escape them.. */
+ if (!arg->fuzzy || arg->value.str[0] == '\0' ||
+ solr_need_escaping(arg->value.str))
+ solr_quote_http(str, arg->value.str);
+ else {
+ http_url_escape_param(str, arg->value.str);
+ str_append_c(str, '~');
+ }
+}
+
+static bool
+solr_add_definite_query(string_t *str, struct mail_search_arg *arg)
+{
+ if (arg->no_fts)
+ return FALSE;
+ switch (arg->type) {
+ case SEARCH_TEXT: {
+ if (arg->match_not)
+ str_append_c(str, '-');
+ str_append(str, "(hdr:");
+ solr_add_str_arg(str, arg);
+ str_append(str, "+OR+body:");
+ solr_add_str_arg(str, arg);
+ str_append(str, ")");
+ break;
+ }
+ case SEARCH_BODY:
+ if (arg->match_not)
+ str_append_c(str, '-');
+ str_append(str, "body:");
+ solr_add_str_arg(str, arg);
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (!fts_header_want_indexed(arg->hdr_field_name))
+ return FALSE;
+
+ if (arg->match_not)
+ str_append_c(str, '-');
+ str_append(str, t_str_lcase(arg->hdr_field_name));
+ str_append_c(str, ':');
+ solr_add_str_arg(str, arg);
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+solr_add_definite_query_args(string_t *str, struct mail_search_arg *arg,
+ bool and_args)
+{
+ size_t last_len;
+
+ last_len = str_len(str);
+ for (; arg != NULL; arg = arg->next) {
+ if (solr_add_definite_query(str, arg)) {
+ arg->match_always = TRUE;
+ last_len = str_len(str);
+ if (and_args)
+ str_append(str, "+AND+");
+ else
+ str_append(str, "+OR+");
+ }
+ }
+ if (str_len(str) == last_len)
+ return FALSE;
+
+ str_truncate(str, last_len);
+ return TRUE;
+}
+
+static bool
+solr_add_maybe_query(string_t *str, struct mail_search_arg *arg)
+{
+ if (arg->no_fts)
+ return FALSE;
+ switch (arg->type) {
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (fts_header_want_indexed(arg->hdr_field_name))
+ return FALSE;
+ if (arg->match_not) {
+ /* all matches would be definite, but all non-matches
+ would be maybies. too much trouble to optimize. */
+ return FALSE;
+ }
+
+ /* we can check if the search key exists in some header and
+ filter out the messages that have no chance of matching */
+ str_append(str, "hdr:");
+ if (*arg->value.str != '\0')
+ solr_quote_http(str, arg->value.str);
+ else {
+ /* checking potential existence of the header name */
+ solr_quote_http(str, t_str_lcase(arg->hdr_field_name));
+ }
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+solr_add_maybe_query_args(string_t *str, struct mail_search_arg *arg,
+ bool and_args)
+{
+ size_t last_len;
+
+ last_len = str_len(str);
+ for (; arg != NULL; arg = arg->next) {
+ if (solr_add_maybe_query(str, arg)) {
+ arg->match_always = TRUE;
+ last_len = str_len(str);
+ if (and_args)
+ str_append(str, "+AND+");
+ else
+ str_append(str, "+OR+");
+ }
+ }
+ if (str_len(str) == last_len)
+ return FALSE;
+
+ str_truncate(str, last_len);
+ return TRUE;
+}
+
+static int solr_search(struct fts_backend *_backend, string_t *str,
+ const char *box_guid, ARRAY_TYPE(seq_range) *uids_r,
+ ARRAY_TYPE(fts_score_map) *scores_r)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+ pool_t pool = pool_alloconly_create("fts solr search", 1024);
+ struct solr_result **results;
+ int ret;
+
+ /* use a separate filter query for selecting the mailbox. it shouldn't
+ affect the score and there could be some caching benefits too. */
+ str_printfa(str, "&fq=%%2Bbox:%s+%%2Buser:", box_guid);
+ if (_backend->ns->owner != NULL)
+ solr_quote_http(str, _backend->ns->owner->username);
+ else
+ str_append(str, "%22%22");
+
+ ret = solr_connection_select(backend->solr_conn, str_c(str),
+ pool, &results);
+ if (ret == 0 && results[0] != NULL) {
+ array_append_array(uids_r, &results[0]->uids);
+ array_append_array(scores_r, &results[0]->scores);
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+static int
+fts_backend_solr_lookup(struct fts_backend *_backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ struct mailbox_status status;
+ string_t *str;
+ const char *box_guid;
+ size_t prefix_len;
+
+ if (fts_mailbox_get_guid(box, &box_guid) < 0)
+ return -1;
+ mailbox_get_open_status(box, STATUS_UIDNEXT, &status);
+
+ str = t_str_new(256);
+ str_printfa(str, "wt=xml&fl=uid,score&rows=%u&sort=uid+asc&q=%%7b!lucene+q.op%%3dAND%%7d",
+ status.uidnext);
+ prefix_len = str_len(str);
+
+ if (solr_add_definite_query_args(str, args, and_args)) {
+ ARRAY_TYPE(seq_range) *uids_arr =
+ (flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0 ?
+ &result->definite_uids : &result->maybe_uids;
+ if (solr_search(_backend, str, box_guid,
+ uids_arr, &result->scores) < 0)
+ return -1;
+ }
+ str_truncate(str, prefix_len);
+ if (solr_add_maybe_query_args(str, args, and_args)) {
+ if (solr_search(_backend, str, box_guid,
+ &result->maybe_uids, &result->scores) < 0)
+ return -1;
+ }
+ result->scores_sorted = TRUE;
+ return 0;
+}
+
+static int
+solr_search_multi(struct fts_backend *_backend, string_t *str,
+ struct mailbox *const boxes[], enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ struct solr_fts_backend *backend = (struct solr_fts_backend *)_backend;
+ struct solr_result **solr_results;
+ struct fts_result *fts_result;
+ ARRAY(struct fts_result) fts_results;
+ HASH_TABLE(char *, struct mailbox *) mailboxes;
+ struct mailbox *box;
+ const char *box_guid;
+ unsigned int i;
+ size_t len;
+ bool search_all_mailboxes;
+
+ /* use a separate filter query for selecting the mailbox. it shouldn't
+ affect the score and there could be some caching benefits too. */
+ str_append(str, "&fq=%2Buser:");
+ if (_backend->ns->owner != NULL)
+ solr_quote_http(str, _backend->ns->owner->username);
+ else
+ str_append(str, "%22%22");
+
+ hash_table_create(&mailboxes, default_pool, 0, str_hash, strcmp);
+ for (i = 0; boxes[i] != NULL; i++) ;
+ search_all_mailboxes = i > SOLR_QUERY_MAX_MAILBOX_COUNT;
+ if (!search_all_mailboxes)
+ str_append(str, "+%2B(");
+ len = str_len(str);
+
+ for (i = 0; boxes[i] != NULL; i++) {
+ if (fts_mailbox_get_guid(boxes[i], &box_guid) < 0)
+ continue;
+
+ if (!search_all_mailboxes) {
+ if (str_len(str) != len)
+ str_append(str, "+OR+");
+ str_printfa(str, "box:%s", box_guid);
+ }
+ hash_table_insert(mailboxes, t_strdup_noconst(box_guid),
+ boxes[i]);
+ }
+ if (!search_all_mailboxes)
+ str_append_c(str, ')');
+
+ if (solr_connection_select(backend->solr_conn, str_c(str),
+ result->pool, &solr_results) < 0) {
+ hash_table_destroy(&mailboxes);
+ return -1;
+ }
+
+ p_array_init(&fts_results, result->pool, 32);
+ for (i = 0; solr_results[i] != NULL; i++) {
+ box = hash_table_lookup(mailboxes, solr_results[i]->box_id);
+ if (box == NULL) {
+ if (!search_all_mailboxes) {
+ i_warning("fts_solr: Lookup returned unexpected mailbox "
+ "with guid=%s", solr_results[i]->box_id);
+ }
+ continue;
+ }
+ fts_result = array_append_space(&fts_results);
+ fts_result->box = box;
+ if ((flags & FTS_LOOKUP_FLAG_NO_AUTO_FUZZY) == 0)
+ fts_result->definite_uids = solr_results[i]->uids;
+ else
+ fts_result->maybe_uids = solr_results[i]->uids;
+ fts_result->scores = solr_results[i]->scores;
+ fts_result->scores_sorted = TRUE;
+ }
+ array_append_zero(&fts_results);
+ result->box_results = array_front_modifiable(&fts_results);
+ hash_table_destroy(&mailboxes);
+ return 0;
+}
+
+static int
+fts_backend_solr_lookup_multi(struct fts_backend *backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ string_t *str;
+
+ str = t_str_new(256);
+ str_printfa(str, "wt=xml&fl=box,uid,score&rows=%u&sort=box+asc,uid+asc&q=%%7b!lucene+q.op%%3dAND%%7d",
+ SOLR_MAX_MULTI_ROWS);
+
+ if (solr_add_definite_query_args(str, args, and_args)) {
+ if (solr_search_multi(backend, str, boxes, flags, result) < 0)
+ return -1;
+ }
+ /* FIXME: maybe_uids could be handled also with some more work.. */
+ return 0;
+}
+
+struct fts_backend fts_backend_solr = {
+ .name = "solr",
+ .flags = FTS_BACKEND_FLAG_FUZZY_SEARCH,
+
+ {
+ fts_backend_solr_alloc,
+ fts_backend_solr_init,
+ fts_backend_solr_deinit,
+ fts_backend_solr_get_last_uid,
+ fts_backend_solr_update_init,
+ fts_backend_solr_update_deinit,
+ fts_backend_solr_update_set_mailbox,
+ fts_backend_solr_update_expunge,
+ fts_backend_solr_update_set_build_key,
+ fts_backend_solr_update_unset_build_key,
+ fts_backend_solr_update_build_more,
+ fts_backend_solr_refresh,
+ fts_backend_solr_rescan,
+ fts_backend_solr_optimize,
+ fts_backend_default_can_lookup,
+ fts_backend_solr_lookup,
+ fts_backend_solr_lookup_multi,
+ NULL
+ }
+};
diff --git a/src/plugins/fts-solr/fts-solr-plugin.c b/src/plugins/fts-solr/fts-solr-plugin.c
new file mode 100644
index 0000000..5899784
--- /dev/null
+++ b/src/plugins/fts-solr/fts-solr-plugin.c
@@ -0,0 +1,131 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "http-client.h"
+#include "mail-user.h"
+#include "mail-storage-hooks.h"
+#include "solr-connection.h"
+#include "fts-user.h"
+#include "fts-solr-plugin.h"
+
+#define DEFAULT_SOLR_BATCH_SIZE 1000
+
+const char *fts_solr_plugin_version = DOVECOT_ABI_VERSION;
+struct http_client *solr_http_client = NULL;
+
+struct fts_solr_user_module fts_solr_user_module =
+ MODULE_CONTEXT_INIT(&mail_user_module_register);
+
+static int
+fts_solr_plugin_init_settings(struct mail_user *user,
+ struct fts_solr_settings *set, const char *str)
+{
+ const char *const *tmp;
+
+ if (str == NULL)
+ str = "";
+
+ set->batch_size = DEFAULT_SOLR_BATCH_SIZE;
+ set->soft_commit = TRUE;
+
+ for (tmp = t_strsplit_spaces(str, " "); *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "url=")) {
+ set->url = p_strdup(user->pool, *tmp + 4);
+ } else if (strcmp(*tmp, "debug") == 0) {
+ set->debug = TRUE;
+ } else if (strcmp(*tmp, "use_libfts") == 0) {
+ set->use_libfts = TRUE;
+ } else if (str_begins(*tmp, "default_ns=")) {
+ set->default_ns_prefix =
+ p_strdup(user->pool, *tmp + 11);
+ } else if (str_begins(*tmp, "rawlog_dir=")) {
+ set->rawlog_dir = p_strdup(user->pool, *tmp + 11);
+ } else if (str_begins(*tmp, "batch_size=")) {
+ if (str_to_uint(*tmp+11, &set->batch_size) < 0 ||
+ set->batch_size == 0) {
+ i_error("fts_solr: batch_size must be a positive integer");
+ return -1;
+ }
+ } else if (str_begins(*tmp, "soft_commit=")) {
+ if (strcmp(*tmp + 12, "yes") == 0) {
+ set->soft_commit = TRUE;
+ } else if (strcmp(*tmp + 12, "no") == 0) {
+ set->soft_commit = FALSE;
+ } else {
+ i_error("fts_solr: Invalid setting for soft_commit: %s", *tmp+12);
+ return -1;
+ }
+ } else {
+ i_error("fts_solr: Invalid setting: %s", *tmp);
+ return -1;
+ }
+ }
+ if (set->url == NULL) {
+ i_error("fts_solr: url setting missing");
+ return -1;
+ }
+ return 0;
+}
+
+static void fts_solr_mail_user_deinit(struct mail_user *user)
+{
+ struct fts_solr_user *fuser = FTS_SOLR_USER_CONTEXT_REQUIRE(user);
+
+ fts_mail_user_deinit(user);
+ fuser->module_ctx.super.deinit(user);
+}
+
+static void fts_solr_mail_user_create(struct mail_user *user, const char *env)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct fts_solr_user *fuser;
+ const char *error;
+
+ fuser = p_new(user->pool, struct fts_solr_user, 1);
+ if (fts_solr_plugin_init_settings(user, &fuser->set, env) < 0) {
+ /* invalid settings, disabling */
+ return;
+ }
+ if (fts_mail_user_init(user, fuser->set.use_libfts, &error) < 0) {
+ i_error("fts-solr: %s", error);
+ return;
+ }
+
+ fuser->module_ctx.super = *v;
+ user->vlast = &fuser->module_ctx.super;
+ v->deinit = fts_solr_mail_user_deinit;
+ MODULE_CONTEXT_SET(user, fts_solr_user_module, fuser);
+}
+
+static void fts_solr_mail_user_created(struct mail_user *user)
+{
+ const char *env;
+
+ env = mail_user_plugin_getenv(user, "fts_solr");
+ if (env != NULL)
+ fts_solr_mail_user_create(user, env);
+}
+
+static struct mail_storage_hooks fts_solr_mail_storage_hooks = {
+ .mail_user_created = fts_solr_mail_user_created
+};
+
+void fts_solr_plugin_init(struct module *module)
+{
+ fts_backend_register(&fts_backend_solr);
+ fts_backend_register(&fts_backend_solr_old);
+ mail_storage_hooks_add(module, &fts_solr_mail_storage_hooks);
+}
+
+void fts_solr_plugin_deinit(void)
+{
+ fts_backend_unregister(fts_backend_solr.name);
+ fts_backend_unregister(fts_backend_solr_old.name);
+ mail_storage_hooks_remove(&fts_solr_mail_storage_hooks);
+ if (solr_http_client != NULL)
+ http_client_deinit(&solr_http_client);
+
+}
+
+const char *fts_solr_plugin_dependencies[] = { "fts", NULL };
diff --git a/src/plugins/fts-solr/fts-solr-plugin.h b/src/plugins/fts-solr/fts-solr-plugin.h
new file mode 100644
index 0000000..abc1b66
--- /dev/null
+++ b/src/plugins/fts-solr/fts-solr-plugin.h
@@ -0,0 +1,35 @@
+#ifndef FTS_SOLR_PLUGIN_H
+#define FTS_SOLR_PLUGIN_H
+
+#include "module-context.h"
+#include "mail-user.h"
+#include "fts-api-private.h"
+
+#define FTS_SOLR_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_solr_user_module)
+#define FTS_SOLR_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, fts_solr_user_module)
+
+struct fts_solr_settings {
+ const char *url, *default_ns_prefix, *rawlog_dir;
+ unsigned int batch_size;
+ bool use_libfts;
+ bool debug;
+ bool soft_commit;
+};
+
+struct fts_solr_user {
+ union mail_user_module_context module_ctx;
+ struct fts_solr_settings set;
+};
+
+extern const char *fts_solr_plugin_dependencies[];
+extern struct fts_backend fts_backend_solr;
+extern struct fts_backend fts_backend_solr_old;
+extern MODULE_CONTEXT_DEFINE(fts_solr_user_module, &mail_user_module_register);
+extern struct http_client *solr_http_client;
+
+void fts_solr_plugin_init(struct module *module);
+void fts_solr_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fts-solr/solr-connection.c b/src/plugins/fts-solr/solr-connection.c
new file mode 100644
index 0000000..41a4fee
--- /dev/null
+++ b/src/plugins/fts-solr/solr-connection.c
@@ -0,0 +1,327 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "fts-solr-plugin.h"
+#include "solr-connection.h"
+
+#include <expat.h>
+
+struct solr_lookup_context {
+ pool_t result_pool;
+ struct istream *payload;
+ struct io *io;
+
+ int request_status;
+
+ struct solr_response_parser *parser;
+ struct solr_result **results;
+};
+
+struct solr_connection_post {
+ struct solr_connection *conn;
+
+ struct http_client_request *http_req;
+ int request_status;
+
+ bool failed:1;
+};
+
+struct solr_connection {
+ struct event *event;
+ char *http_host;
+ in_port_t http_port;
+ char *http_base_url;
+ char *http_failure;
+ char *http_user;
+ char *http_password;
+
+ bool debug:1;
+ bool posting:1;
+ bool http_ssl:1;
+};
+
+/* Regardless of the specified URL, make sure path ends in '/' */
+static char *solr_connection_create_http_base_url(struct http_url *http_url)
+{
+ if (http_url->path == NULL)
+ return i_strconcat("/", http_url->enc_query, NULL);
+ size_t len = strlen(http_url->path);
+ if (len > 0 && http_url->path[len-1] != '/')
+ return i_strconcat(http_url->path, "/",
+ http_url->enc_query, NULL);
+ /* http_url->path is NULL on empty path, so this is impossible. */
+ i_assert(len != 0);
+ return i_strconcat(http_url->path, http_url->enc_query, NULL);
+}
+
+int solr_connection_init(const struct fts_solr_settings *solr_set,
+ const struct ssl_iostream_settings *ssl_client_set,
+ struct event *event_parent,
+ struct solr_connection **conn_r, const char **error_r)
+{
+ struct http_client_settings http_set;
+ struct solr_connection *conn;
+ struct http_url *http_url;
+ const char *error;
+
+ if (http_url_parse(solr_set->url, NULL, HTTP_URL_ALLOW_USERINFO_PART,
+ pool_datastack_create(), &http_url, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "fts_solr: Failed to parse HTTP url: %s", error);
+ return -1;
+ }
+
+ conn = i_new(struct solr_connection, 1);
+ conn->event = event_create(event_parent);
+ conn->http_host = i_strdup(http_url->host.name);
+ conn->http_port = http_url->port;
+ conn->http_base_url = solr_connection_create_http_base_url(http_url);
+ conn->http_ssl = http_url->have_ssl;
+ if (http_url->user != NULL) {
+ conn->http_user = i_strdup(http_url->user);
+ /* allow empty password */
+ conn->http_password = i_strdup(http_url->password != NULL ?
+ http_url->password : "");
+ }
+
+ conn->debug = solr_set->debug;
+
+ if (solr_http_client == NULL) {
+ 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 = 1;
+ http_set.max_attempts = 3;
+ http_set.connect_timeout_msecs = 5*1000;
+ http_set.request_timeout_msecs = 60*1000;
+ http_set.ssl = ssl_client_set;
+ http_set.debug = solr_set->debug;
+ http_set.rawlog_dir = solr_set->rawlog_dir;
+ http_set.event_parent = conn->event;
+
+ /* FIXME: We should initialize a shared client instead. However,
+ this is currently not possible due to an obscure bug
+ in the blocking HTTP payload API, which causes
+ conflicts with other HTTP applications like FTS Tika.
+ Using a private client will provide a quick fix for
+ now. */
+ solr_http_client = http_client_init_private(&http_set);
+ }
+
+ *conn_r = conn;
+ return 0;
+}
+
+void solr_connection_deinit(struct solr_connection **_conn)
+{
+ struct solr_connection *conn = *_conn;
+
+ *_conn = NULL;
+ event_unref(&conn->event);
+ i_free(conn->http_host);
+ i_free(conn->http_base_url);
+ i_free(conn->http_user);
+ i_free(conn->http_password);
+ i_free(conn);
+}
+
+static void solr_connection_payload_input(struct solr_lookup_context *lctx)
+{
+ int ret;
+
+ /* read payload */
+ ret = solr_response_parse(lctx->parser, &lctx->results);
+
+ if (ret == 0) {
+ /* we will be called again for more data */
+ } else {
+ if (lctx->payload->stream_errno != 0) {
+ i_assert(ret < 0);
+ i_error("fts_solr: "
+ "failed to read payload from HTTP server: %s",
+ i_stream_get_error(lctx->payload));
+ }
+ if (ret < 0)
+ lctx->request_status = -1;
+ solr_response_parser_deinit(&lctx->parser);
+ io_remove(&lctx->io);
+ }
+}
+
+static void
+solr_connection_select_response(const struct http_response *response,
+ struct solr_lookup_context *lctx)
+{
+ if (response->status / 100 != 2) {
+ i_error("fts_solr: Lookup failed: %s",
+ http_response_get_message(response));
+ lctx->request_status = -1;
+ return;
+ }
+
+ if (response->payload == NULL) {
+ i_error("fts_solr: Lookup failed: Empty response payload");
+ lctx->request_status = -1;
+ return;
+ }
+
+ lctx->parser = solr_response_parser_init(lctx->result_pool,
+ response->payload);
+ lctx->payload = response->payload;
+ lctx->io = io_add_istream(response->payload,
+ solr_connection_payload_input, lctx);
+ solr_connection_payload_input(lctx);
+}
+
+int solr_connection_select(struct solr_connection *conn, const char *query,
+ pool_t pool, struct solr_result ***box_results_r)
+{
+ struct solr_lookup_context lctx;
+ struct http_client_request *http_req;
+ const char *url;
+
+ i_zero(&lctx);
+ lctx.result_pool = pool;
+
+ i_free_and_null(conn->http_failure);
+ url = t_strconcat(conn->http_base_url, "select?", query, NULL);
+
+ http_req = http_client_request(solr_http_client, "GET",
+ conn->http_host, url,
+ solr_connection_select_response,
+ &lctx);
+ if (conn->http_user != NULL) {
+ http_client_request_set_auth_simple(
+ http_req, conn->http_user, conn->http_password);
+ }
+ http_client_request_set_port(http_req, conn->http_port);
+ http_client_request_set_ssl(http_req, conn->http_ssl);
+ http_client_request_submit(http_req);
+
+ lctx.request_status = 0;
+ http_client_wait(solr_http_client);
+
+ if (lctx.request_status < 0)
+ return -1;
+
+ *box_results_r = lctx.results;
+ return 0;
+}
+
+static void
+solr_connection_update_response(const struct http_response *response,
+ struct solr_connection_post *post)
+{
+ if (response->status / 100 != 2) {
+ i_error("fts_solr: Indexing failed: %s",
+ http_response_get_message(response));
+ post->request_status = -1;
+ }
+}
+
+static struct http_client_request *
+solr_connection_post_request(struct solr_connection_post *post)
+{
+ struct solr_connection *conn = post->conn;
+ struct http_client_request *http_req;
+ const char *url;
+
+ url = t_strconcat(conn->http_base_url, "update", NULL);
+
+ http_req = http_client_request(solr_http_client, "POST",
+ conn->http_host, url,
+ solr_connection_update_response, post);
+ if (conn->http_user != NULL) {
+ http_client_request_set_auth_simple(
+ http_req, conn->http_user, conn->http_password);
+ }
+ http_client_request_set_port(http_req, conn->http_port);
+ http_client_request_set_ssl(http_req, conn->http_ssl);
+ http_client_request_add_header(http_req, "Content-Type", "text/xml");
+ return http_req;
+}
+
+struct solr_connection_post *
+solr_connection_post_begin(struct solr_connection *conn)
+{
+ struct solr_connection_post *post;
+
+ i_assert(!conn->posting);
+ conn->posting = TRUE;
+
+ post = i_new(struct solr_connection_post, 1);
+ post->conn = conn;
+ post->http_req = solr_connection_post_request(post);
+ return post;
+}
+
+void solr_connection_post_more(struct solr_connection_post *post,
+ const unsigned char *data, size_t size)
+{
+ i_assert(post->conn->posting);
+
+ if (post->failed)
+ return;
+
+ if (post->request_status == 0) {
+ (void)http_client_request_send_payload(
+ &post->http_req, data, size);
+ }
+ if (post->request_status < 0)
+ post->failed = TRUE;
+}
+
+int solr_connection_post_end(struct solr_connection_post **_post)
+{
+ struct solr_connection_post *post = *_post;
+ struct solr_connection *conn = post->conn;
+ int ret = post->failed ? -1 : 0;
+
+ i_assert(conn->posting);
+
+ *_post = NULL;
+
+ if (!post->failed) {
+ if (http_client_request_finish_payload(&post->http_req) < 0 ||
+ post->request_status < 0) {
+ ret = -1;
+ }
+ } else {
+ http_client_request_abort(&post->http_req);
+ }
+ i_free(post);
+
+ conn->posting = FALSE;
+ return ret;
+}
+
+int solr_connection_post(struct solr_connection *conn, const char *cmd)
+{
+ struct istream *post_payload;
+ struct solr_connection_post post;
+
+ i_assert(!conn->posting);
+
+ i_zero(&post);
+ post.conn = conn;
+
+ post.http_req = solr_connection_post_request(&post);
+ post_payload = i_stream_create_from_data(cmd, strlen(cmd));
+ http_client_request_set_payload(post.http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_submit(post.http_req);
+
+ post.request_status = 0;
+ http_client_wait(solr_http_client);
+
+ return post.request_status;
+}
diff --git a/src/plugins/fts-solr/solr-connection.h b/src/plugins/fts-solr/solr-connection.h
new file mode 100644
index 0000000..ebad8be
--- /dev/null
+++ b/src/plugins/fts-solr/solr-connection.h
@@ -0,0 +1,26 @@
+#ifndef SOLR_CONNECTION_H
+#define SOLR_CONNECTION_H
+
+#include "solr-response.h"
+
+struct solr_connection;
+struct fts_solr_settings;
+
+int solr_connection_init(const struct fts_solr_settings *solr_set,
+ const struct ssl_iostream_settings *ssl_client_set,
+ struct event *event_parent,
+ struct solr_connection **conn_r,
+ const char **error_r);
+void solr_connection_deinit(struct solr_connection **conn);
+
+int solr_connection_select(struct solr_connection *conn, const char *query,
+ pool_t pool, struct solr_result ***box_results_r);
+int solr_connection_post(struct solr_connection *conn, const char *cmd);
+
+struct solr_connection_post *
+solr_connection_post_begin(struct solr_connection *conn);
+void solr_connection_post_more(struct solr_connection_post *post,
+ const unsigned char *data, size_t size);
+int solr_connection_post_end(struct solr_connection_post **post);
+
+#endif
diff --git a/src/plugins/fts-solr/solr-response.c b/src/plugins/fts-solr/solr-response.c
new file mode 100644
index 0000000..65a6a1f
--- /dev/null
+++ b/src/plugins/fts-solr/solr-response.c
@@ -0,0 +1,372 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "istream.h"
+#include "solr-response.h"
+
+#include <expat.h>
+
+#define MAX_VALUE_LEN 2048
+
+enum solr_xml_response_state {
+ SOLR_XML_RESPONSE_STATE_ROOT,
+ SOLR_XML_RESPONSE_STATE_RESPONSE,
+ SOLR_XML_RESPONSE_STATE_RESULT,
+ SOLR_XML_RESPONSE_STATE_DOC,
+ SOLR_XML_RESPONSE_STATE_CONTENT
+};
+
+enum solr_xml_content_state {
+ SOLR_XML_CONTENT_STATE_NONE = 0,
+ SOLR_XML_CONTENT_STATE_UID,
+ SOLR_XML_CONTENT_STATE_SCORE,
+ SOLR_XML_CONTENT_STATE_MAILBOX,
+ SOLR_XML_CONTENT_STATE_NAMESPACE,
+ SOLR_XML_CONTENT_STATE_UIDVALIDITY,
+ SOLR_XML_CONTENT_STATE_ERROR
+};
+
+struct solr_response_parser {
+ XML_Parser xml_parser;
+ struct istream *input;
+
+ enum solr_xml_response_state state;
+ enum solr_xml_content_state content_state;
+ int depth;
+ string_t *buffer;
+
+ uint32_t uid, uidvalidity;
+ float score;
+ char *mailbox, *ns;
+
+ pool_t result_pool;
+ /* box_id -> solr_result */
+ HASH_TABLE(char *, struct solr_result *) mailboxes;
+ ARRAY(struct solr_result *) results;
+
+ bool xml_failed:1;
+};
+
+static int
+solr_xml_parse(struct solr_response_parser *parser,
+ const void *data, size_t size, bool done)
+{
+ enum XML_Error err;
+ int line, col;
+
+ if (parser->xml_failed)
+ return -1;
+
+ if (XML_Parse(parser->xml_parser, data, size, done ? 1 : 0) != 0)
+ return 0;
+
+ err = XML_GetErrorCode(parser->xml_parser);
+ if (err != XML_ERROR_FINISHED) {
+ line = XML_GetCurrentLineNumber(parser->xml_parser);
+ col = XML_GetCurrentColumnNumber(parser->xml_parser);
+ i_error("fts_solr: Invalid XML input at %d:%d: %s "
+ "(near: %.*s)", line, col, XML_ErrorString(err),
+ (int)I_MIN(size, 128), (const char *)data);
+ parser->xml_failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static const char *attrs_get_name(const char **attrs)
+{
+ for (; *attrs != NULL; attrs += 2) {
+ if (strcmp(attrs[0], "name") == 0)
+ return attrs[1];
+ }
+ return "";
+}
+
+static void
+solr_lookup_xml_start(void *context, const char *name, const char **attrs)
+{
+ struct solr_response_parser *parser = context;
+ const char *name_attr;
+
+ i_assert(parser->depth >= (int)parser->state);
+
+ parser->depth++;
+ if (parser->depth - 1 > (int)parser->state) {
+ /* skipping over unwanted elements */
+ return;
+ }
+
+ str_truncate(parser->buffer, 0);
+
+ /* response -> result -> doc */
+ switch (parser->state) {
+ case SOLR_XML_RESPONSE_STATE_ROOT:
+ if (strcmp(name, "response") == 0)
+ parser->state++;
+ break;
+ case SOLR_XML_RESPONSE_STATE_RESPONSE:
+ if (strcmp(name, "result") == 0)
+ parser->state++;
+ break;
+ case SOLR_XML_RESPONSE_STATE_RESULT:
+ if (strcmp(name, "doc") == 0) {
+ parser->state++;
+ parser->uid = 0;
+ parser->score = 0;
+ i_free_and_null(parser->mailbox);
+ i_free_and_null(parser->ns);
+ parser->uidvalidity = 0;
+ }
+ break;
+ case SOLR_XML_RESPONSE_STATE_DOC:
+ name_attr = attrs_get_name(attrs);
+ if (strcmp(name_attr, "uid") == 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_UID;
+ else if (strcmp(name_attr, "score") == 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_SCORE;
+ else if (strcmp(name_attr, "box") == 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_MAILBOX;
+ else if (strcmp(name_attr, "ns") == 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_NAMESPACE;
+ else if (strcmp(name_attr, "uidv") == 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_UIDVALIDITY;
+ else
+ break;
+ parser->state++;
+ break;
+ case SOLR_XML_RESPONSE_STATE_CONTENT:
+ break;
+ }
+}
+
+static struct solr_result *
+solr_result_get(struct solr_response_parser *parser, const char *box_id)
+{
+ struct solr_result *result;
+ char *box_id_dup;
+
+ result = hash_table_lookup(parser->mailboxes, box_id);
+ if (result != NULL)
+ return result;
+
+ box_id_dup = p_strdup(parser->result_pool, box_id);
+ result = p_new(parser->result_pool, struct solr_result, 1);
+ result->box_id = box_id_dup;
+ p_array_init(&result->uids, parser->result_pool, 32);
+ p_array_init(&result->scores, parser->result_pool, 32);
+ hash_table_insert(parser->mailboxes, box_id_dup, result);
+ array_push_back(&parser->results, &result);
+ return result;
+}
+
+static int solr_lookup_add_doc(struct solr_response_parser *parser)
+{
+ struct fts_score_map *score;
+ struct solr_result *result;
+ const char *box_id;
+
+ if (parser->uid == 0) {
+ i_error("fts_solr: uid missing from inside doc");
+ return -1;
+ }
+
+ if (parser->mailbox == NULL) {
+ /* looking up from a single mailbox only */
+ box_id = "";
+ } else if (parser->uidvalidity != 0) {
+ /* old style lookup */
+ string_t *str = t_str_new(64);
+ str_printfa(str, "%u\001", parser->uidvalidity);
+ str_append(str, parser->mailbox);
+ if (parser->ns != NULL)
+ str_printfa(str, "\001%s", parser->ns);
+ box_id = str_c(str);
+ } else {
+ /* new style lookup */
+ box_id = parser->mailbox;
+ }
+ result = solr_result_get(parser, box_id);
+
+ if (seq_range_array_add(&result->uids, parser->uid)) {
+ /* duplicate result */
+ } else if (parser->score != 0) {
+ score = array_append_space(&result->scores);
+ score->uid = parser->uid;
+ score->score = parser->score;
+ }
+ return 0;
+}
+
+static void solr_lookup_xml_end(void *context, const char *name ATTR_UNUSED)
+{
+ struct solr_response_parser *parser = context;
+ string_t *buf = parser->buffer;
+ int ret;
+
+ switch (parser->content_state) {
+ case SOLR_XML_CONTENT_STATE_NONE:
+ break;
+ case SOLR_XML_CONTENT_STATE_UID:
+ if (str_to_uint32(str_c(buf), &parser->uid) < 0 ||
+ parser->uid == 0) {
+ i_error("fts_solr: received invalid uid '%s'",
+ str_c(buf));
+ parser->content_state = SOLR_XML_CONTENT_STATE_ERROR;
+ }
+ break;
+ case SOLR_XML_CONTENT_STATE_SCORE:
+ parser->score = strtod(str_c(buf), NULL);
+ break;
+ case SOLR_XML_CONTENT_STATE_MAILBOX:
+ parser->mailbox = i_strdup(str_c(buf));
+ break;
+ case SOLR_XML_CONTENT_STATE_NAMESPACE:
+ parser->ns = i_strdup(str_c(buf));
+ break;
+ case SOLR_XML_CONTENT_STATE_UIDVALIDITY:
+ if (str_to_uint32(str_c(buf), &parser->uidvalidity) < 0)
+ i_error("fts_solr: received invalid uidvalidity");
+ break;
+ case SOLR_XML_CONTENT_STATE_ERROR:
+ return;
+ }
+
+ i_assert(parser->depth >= (int)parser->state);
+
+ if (parser->state == SOLR_XML_RESPONSE_STATE_CONTENT &&
+ parser->content_state == SOLR_XML_CONTENT_STATE_MAILBOX &&
+ parser->mailbox == NULL) {
+ /* mailbox is namespace prefix */
+ parser->mailbox = i_strdup("");
+ }
+
+ if (parser->depth == (int)parser->state) {
+ ret = 0;
+ if (parser->state == SOLR_XML_RESPONSE_STATE_DOC) {
+ T_BEGIN {
+ ret = solr_lookup_add_doc(parser);
+ } T_END;
+ }
+ parser->state--;
+ if (ret < 0)
+ parser->content_state = SOLR_XML_CONTENT_STATE_ERROR;
+ else
+ parser->content_state = SOLR_XML_CONTENT_STATE_NONE;
+ }
+ parser->depth--;
+}
+
+static void solr_lookup_xml_data(void *context, const char *str, int len)
+{
+ struct solr_response_parser *parser = context;
+
+ switch (parser->content_state) {
+ case SOLR_XML_CONTENT_STATE_NONE:
+ case SOLR_XML_CONTENT_STATE_ERROR:
+ /* ignore element data */
+ return;
+ case SOLR_XML_CONTENT_STATE_UID:
+ case SOLR_XML_CONTENT_STATE_SCORE:
+ case SOLR_XML_CONTENT_STATE_MAILBOX:
+ case SOLR_XML_CONTENT_STATE_NAMESPACE:
+ case SOLR_XML_CONTENT_STATE_UIDVALIDITY:
+ break;
+ }
+
+ if (str_len(parser->buffer) + len > MAX_VALUE_LEN) {
+ i_error("fts_solr: XML element data length out of range");
+ parser->content_state = SOLR_XML_CONTENT_STATE_ERROR;
+ return;
+ }
+
+ str_append_data(parser->buffer, str, len);
+}
+
+struct solr_response_parser *
+solr_response_parser_init(pool_t result_pool, struct istream *input)
+{
+ struct solr_response_parser *parser;
+
+ parser = i_new(struct solr_response_parser, 1);
+
+ parser->xml_parser = XML_ParserCreate("UTF-8");
+ if (parser->xml_parser == NULL) {
+ i_fatal_status(FATAL_OUTOFMEM,
+ "fts_solr: Failed to allocate XML parser");
+ }
+
+ parser->buffer = str_new(default_pool, 256);
+ hash_table_create(&parser->mailboxes, default_pool, 0,
+ str_hash, strcmp);
+
+ parser->result_pool = result_pool;
+ pool_ref(result_pool);
+ p_array_init(&parser->results, result_pool, 32);
+
+ parser->input = input;
+ i_stream_ref(input);
+
+ parser->xml_failed = FALSE;
+ XML_SetElementHandler(parser->xml_parser,
+ solr_lookup_xml_start, solr_lookup_xml_end);
+ XML_SetCharacterDataHandler(parser->xml_parser, solr_lookup_xml_data);
+ XML_SetUserData(parser->xml_parser, parser);
+
+ return parser;
+}
+
+void solr_response_parser_deinit(struct solr_response_parser **_parser)
+{
+ struct solr_response_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ if (parser == NULL)
+ return;
+
+ str_free(&parser->buffer);
+ hash_table_destroy(&parser->mailboxes);
+ XML_ParserFree(parser->xml_parser);
+ i_stream_unref(&parser->input);
+ pool_unref(&parser->result_pool);
+ i_free(parser);
+}
+
+int solr_response_parse(struct solr_response_parser *parser,
+ struct solr_result ***box_results_r)
+{
+ const unsigned char *data;
+ size_t size;
+ int stream_errno, ret;
+
+ i_assert(parser->input != NULL);
+ i_zero(box_results_r);
+
+ /* read payload */
+ while ((ret = i_stream_read_more(parser->input, &data, &size)) > 0) {
+ (void)solr_xml_parse(parser, data, size, FALSE);
+ i_stream_skip(parser->input, size);
+ }
+
+ if (ret == 0) {
+ /* we will be called again for more data */
+ return 0;
+ }
+
+ stream_errno = parser->input->stream_errno;
+ i_stream_unref(&parser->input);
+
+ if (parser->content_state == SOLR_XML_CONTENT_STATE_ERROR)
+ return -1;
+ if (stream_errno != 0)
+ return -1;
+
+ ret = solr_xml_parse(parser, "", 0, TRUE);
+
+ array_append_zero(&parser->results);
+ *box_results_r = array_front_modifiable(&parser->results);
+ return (ret == 0 ? 1 : -1);
+}
diff --git a/src/plugins/fts-solr/solr-response.h b/src/plugins/fts-solr/solr-response.h
new file mode 100644
index 0000000..1d5cdd5
--- /dev/null
+++ b/src/plugins/fts-solr/solr-response.h
@@ -0,0 +1,23 @@
+#ifndef SOLR_RESPONSE_H
+#define SOLR_RESPONSE_H
+
+#include "seq-range-array.h"
+#include "fts-api.h"
+
+struct solr_response_parser;
+
+struct solr_result {
+ const char *box_id;
+
+ ARRAY_TYPE(seq_range) uids;
+ ARRAY_TYPE(fts_score_map) scores;
+};
+
+struct solr_response_parser *
+solr_response_parser_init(pool_t result_pool, struct istream *input);
+void solr_response_parser_deinit(struct solr_response_parser **_parser);
+
+int solr_response_parse(struct solr_response_parser *parser,
+ struct solr_result ***box_results_r);
+
+#endif
diff --git a/src/plugins/fts-solr/test-solr-response.c b/src/plugins/fts-solr/test-solr-response.c
new file mode 100644
index 0000000..8add6db
--- /dev/null
+++ b/src/plugins/fts-solr/test-solr-response.c
@@ -0,0 +1,295 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "solr-response.h"
+#include "test-common.h"
+
+#include <unistd.h>
+
+static bool debug = FALSE;
+
+struct solr_response_test_result {
+ const char *box_id;
+ struct fts_score_map *scores;
+};
+
+struct solr_response_test {
+ const char *input;
+
+ struct solr_response_test_result *results;
+};
+
+struct fts_score_map test_results1_scores[] = {
+ { .score = 0.042314477, .uid = 1 },
+ { .score = 0.06996078, .uid = 2, },
+ { .score = 0.020381179, .uid = 3 },
+ { .score = 0.020381179, .uid = 4 },
+ { .score = 5.510487E-4, .uid = 6 },
+ { .score = 0.0424253, .uid = 7 },
+ { .score = 0.04215967, .uid = 8 },
+ { .score = 0.02470572, .uid = 9 },
+ { .score = 0.05936369, .uid = 10 },
+ { .score = 0.048221838, .uid = 11 },
+ { .score = 7.793006E-4, .uid = 12 },
+ { .score = 2.7900032E-4, .uid = 13 },
+ { .score = 0.02088323, .uid = 14 },
+ { .score = 0.011646388, .uid = 15 },
+ { .score = 1.3776218E-4, .uid = 17 },
+ { .score = 2.386111E-4, .uid = 19 },
+ { .score = 2.7552436E-4, .uid = 20 },
+ { .score = 4.772222E-4, .uid = 23 },
+ { .score = 4.772222E-4, .uid = 24 },
+ { .score = 5.965277E-4, .uid = 25 },
+ { .score = 0.0471366, .uid = 26 },
+ { .score = 0.0471366, .uid = 50 },
+ { .score = 0.047274362, .uid = 51 },
+ { .score = 0.053303234, .uid = 56 },
+ { .score = 5.445528E-4, .uid = 62 },
+ { .score = 2.922377E-4, .uid = 66 },
+ { .score = 0.02623833, .uid = 68 },
+ { .score = 3.4440547E-4, .uid = 70 },
+ { .score = 2.922377E-4, .uid = 74 },
+ { .score = 2.7552436E-4, .uid = 76 },
+ { .score = 1.3776218E-4, .uid = 77 },
+ { .score = 0, .uid = 0 },
+};
+
+struct solr_response_test_result test_results1[] = {
+ {
+ .box_id = "",
+ .scores = test_results1_scores,
+ },
+ {
+ .box_id = NULL
+ }
+};
+
+static const struct solr_response_test tests[] = {
+ {
+ .input =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<response>\n"
+ "<lst name=\"responseHeader\"><int name=\"status\""
+ ">0</int><int name=\"QTime\">3</int><lst name=\"pa"
+ "rams\"><str name=\"wt\">xml</str><str name=\"fl\""
+ ">uid,score</str><str name=\"rows\">4023</str><str"
+ " name=\"sort\">uid asc</str><str name=\"q\">{!luc"
+ "ene q.op=AND}subject:pierreserveur OR from:pierre"
+ "serveur OR to:pierreserveur OR cc:pierreserveur O"
+ "R bcc:pierreserveur OR body:pierreserveur</str><s"
+ "tr name=\"fq\">+box:fa74101044cb607d5f0900001de14"
+ "712 +user:jpierreserveur</str></lst></lst><result"
+ " name=\"response\" numFound=\"31\" start=\"0\" ma"
+ "xScore=\"0.06996078\"><doc><float name=\"score\">"
+ "0.042314477</float><long name=\"uid\">1</long></d"
+ "oc><doc><float name=\"score\">0.06996078</float><"
+ "long name=\"uid\">2</long></doc><doc><float name="
+ "\"score\">0.020381179</float><long name=\"uid\">3"
+ "</long></doc><doc><float name=\"score\">0.0203811"
+ "79</float><long name=\"uid\">4</long></doc><doc><"
+ "float name=\"score\">5.510487E-4</float><long nam"
+ "e=\"uid\">6</long></doc><doc><float name=\"score\""
+ ">0.0424253</float><long name=\"uid\">7</long></do"
+ "c><doc><float name=\"score\">0.04215967</float><l"
+ "ong name=\"uid\">8</long></doc><doc><float name=\""
+ "score\">0.02470572</float><long name=\"uid\">9</l"
+ "ong></doc><doc><float name=\"score\">0.05936369</"
+ "float><long name=\"uid\">10</long></doc><doc><flo"
+ "at name=\"score\">0.048221838</float><long name=\""
+ "uid\">11</long></doc><doc><float name=\"score\">7"
+ ".793006E-4</float><long name=\"uid\">12</long></d"
+ "oc><doc><float name=\"score\">2.7900032E-4</float"
+ "><long name=\"uid\">13</long></doc><doc><float na"
+ "me=\"score\">0.02088323</float><long name=\"uid\""
+ ">14</long></doc><doc><float name=\"score\">0.0116"
+ "46388</float><long name=\"uid\">15</long></doc><d"
+ "oc><float name=\"score\">1.3776218E-4</float><lon"
+ "g name=\"uid\">17</long></doc><doc><float name=\""
+ "score\">2.386111E-4</float><long name=\"uid\">19<"
+ "/long></doc><doc><float name=\"score\">2.7552436E"
+ "-4</float><long name=\"uid\">20</long></doc><doc>"
+ "<float name=\"score\">4.772222E-4</float><long na"
+ "me=\"uid\">23</long></doc><doc><float name=\"scor"
+ "e\">4.772222E-4</float><long name=\"uid\">24</lon"
+ "g></doc><doc><float name=\"score\">5.965277E-4</f"
+ "loat><long name=\"uid\">25</long></doc><doc><floa"
+ "t name=\"score\">0.0471366</float><long name=\"ui"
+ "d\">26</long></doc><doc><float name=\"score\">0.0"
+ "471366</float><long name=\"uid\">50</long></doc><"
+ "doc><float name=\"score\">0.047274362</float><lon"
+ "g name=\"uid\">51</long></doc><doc><float name=\""
+ "score\">0.053303234</float><long name=\"uid\">56<"
+ "/long></doc><doc><float name=\"score\">5.445528E-"
+ "4</float><long name=\"uid\">62</long></doc><doc><"
+ "float name=\"score\">2.922377E-4</float><long nam"
+ "e=\"uid\">66</long></doc><doc><float name=\"score"
+ "\">0.02623833</float><long name=\"uid\">68</long>"
+ "</doc><doc><float name=\"score\">3.4440547E-4</fl"
+ "oat><long name=\"uid\">70</long></doc><doc><float"
+ " name=\"score\">2.922377E-4</float><long name=\"u"
+ "id\">74</long></doc><doc><float name=\"score\">2."
+ "7552436E-4</float><long name=\"uid\">76</long></d"
+ "oc><doc><float name=\"score\">1.3776218E-4</float"
+ "><long name=\"uid\">77</long></doc></result>\n"
+ "</response>\n",
+ .results = test_results1,
+ },
+};
+
+static const unsigned tests_count = N_ELEMENTS(tests);
+
+static void
+test_solr_result(const struct solr_response_test_result *test_results,
+ struct solr_result **parse_results)
+{
+ unsigned int rcount, i;
+
+ for (i = 0; test_results[i].box_id != NULL; i++);
+ rcount = i;
+
+ for (i = 0; parse_results[i] != NULL; i++);
+
+ test_out_quiet("result count equal", i == rcount);
+ if (test_has_failed())
+ return;
+
+ for (i = 0; i < rcount && parse_results[i] != NULL; i++) {
+ unsigned int scount, j;
+ const struct fts_score_map *tscores = test_results[i].scores;
+ const struct fts_score_map *pscores =
+ array_get(&parse_results[i]->scores, &scount);
+
+ test_out_quiet(t_strdup_printf("box id equal[%u]", i),
+ strcmp(test_results[i].box_id,
+ parse_results[i]->box_id) == 0);
+
+ for (j = 0; tscores[j].uid != 0; j++);
+ test_out_quiet(t_strdup_printf("scores count equal[%u]", i),
+ j == scount);
+ if (j != scount)
+ continue;
+
+ for (j = 0; j < scount; j++) {
+ test_out_quiet(
+ t_strdup_printf("score uid equal[%u/%u]", i, j),
+ pscores[j].uid == tscores[j].uid);
+ test_out_quiet(
+ t_strdup_printf("score value equal[%u/%u]", i, j),
+ pscores[j].score == tscores[j].score);
+ }
+ }
+}
+
+static void test_solr_response_parser(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < tests_count; i++) T_BEGIN {
+ const struct solr_response_test *test;
+ const char *text;
+ unsigned int pos, text_len;
+ struct istream *input;
+ struct solr_response_parser *parser;
+ struct solr_result **box_results;
+ const char *error = NULL;
+ pool_t pool;
+ int ret = 0;
+
+ test = &tests[i];
+ text = test->input;
+ text_len = strlen(text);
+
+ test_begin(t_strdup_printf("solr response [%d]", i));
+
+ input = test_istream_create_data(text, text_len);
+ pool = pool_alloconly_create("solr response", 4096);
+ parser = solr_response_parser_init(pool, input);
+
+ ret = solr_response_parse(parser, &box_results);
+
+ test_out_reason("parse ok (buffer)", ret > 0, error);
+ if (ret > 0)
+ test_solr_result(test->results, box_results);
+
+ solr_response_parser_deinit(&parser);
+ pool_unref(&pool);
+ i_stream_unref(&input);
+
+ input = test_istream_create_data(text, text_len);
+ pool = pool_alloconly_create("solr response", 4096);
+ parser = solr_response_parser_init(pool, input);
+
+ ret = 0;
+ for (pos = 0; pos <= text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = solr_response_parse(parser, &box_results);
+ }
+
+ test_out_reason("parse ok (trickle)", ret > 0, error);
+ if (ret > 0)
+ test_solr_result(test->results, box_results);
+
+ solr_response_parser_deinit(&parser);
+ pool_unref(&pool);
+ i_stream_unref(&input);
+
+ test_end();
+
+ } T_END;
+}
+
+static void test_solr_response_file(const char *file)
+{
+ pool_t pool;
+ struct istream *input;
+ struct solr_response_parser *parser;
+ struct solr_result **box_results;
+ int ret = 0;
+
+ pool = pool_alloconly_create("solr response", 4096);
+ input = i_stream_create_file(file, 1024);
+ parser = solr_response_parser_init(pool, input);
+
+ while ((ret = solr_response_parse(parser, &box_results)) == 0);
+
+ if (ret < 0)
+ i_fatal("Failed to read response");
+
+ solr_response_parser_deinit(&parser);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+
+ static void (*test_functions[])(void) = {
+ test_solr_response_parser,
+ NULL
+ };
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 0) {
+ test_solr_response_file(argv[0]);
+ return 0;
+ }
+
+ return test_run(test_functions);
+}
+
+
diff --git a/src/plugins/fts-squat/Makefile.am b/src/plugins/fts-squat/Makefile.am
new file mode 100644
index 0000000..6c8181c
--- /dev/null
+++ b/src/plugins/fts-squat/Makefile.am
@@ -0,0 +1,47 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/fts
+
+NOPLUGIN_LDFLAGS =
+lib21_fts_squat_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib21_fts_squat_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib21_fts_squat_plugin_la_LIBADD = \
+ ../fts/lib20_fts_plugin.la
+endif
+
+lib21_fts_squat_plugin_la_SOURCES = \
+ fts-squat-plugin.c \
+ fts-backend-squat.c \
+ squat-trie.c \
+ squat-uidlist.c
+
+noinst_HEADERS = \
+ fts-squat-plugin.h \
+ squat-trie.h \
+ squat-trie-private.h \
+ squat-uidlist.h
+
+noinst_PROGRAMS = squat-test
+
+squat_test_SOURCES = \
+ squat-test.c
+
+common_objects = \
+ squat-trie.lo \
+ squat-uidlist.lo
+
+squat_test_LDADD = \
+ $(common_objects) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+squat_test_DEPENDENCIES = \
+ $(common_objects) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
diff --git a/src/plugins/fts-squat/Makefile.in b/src/plugins/fts-squat/Makefile.in
new file mode 100644
index 0000000..bbcbeba
--- /dev/null
+++ b/src/plugins/fts-squat/Makefile.in
@@ -0,0 +1,883 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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 = squat-test$(EXEEXT)
+subdir = src/plugins/fts-squat
+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 =
+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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib21_fts_squat_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../fts/lib20_fts_plugin.la
+am_lib21_fts_squat_plugin_la_OBJECTS = fts-squat-plugin.lo \
+ fts-backend-squat.lo squat-trie.lo squat-uidlist.lo
+lib21_fts_squat_plugin_la_OBJECTS = \
+ $(am_lib21_fts_squat_plugin_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 =
+lib21_fts_squat_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib21_fts_squat_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_squat_test_OBJECTS = squat-test.$(OBJEXT)
+squat_test_OBJECTS = $(am_squat_test_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)/fts-backend-squat.Plo \
+ ./$(DEPDIR)/fts-squat-plugin.Plo ./$(DEPDIR)/squat-test.Po \
+ ./$(DEPDIR)/squat-trie.Plo ./$(DEPDIR)/squat-uidlist.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 = $(lib21_fts_squat_plugin_la_SOURCES) $(squat_test_SOURCES)
+DIST_SOURCES = $(lib21_fts_squat_plugin_la_SOURCES) \
+ $(squat_test_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@
+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-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/fts
+
+lib21_fts_squat_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib21_fts_squat_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib21_fts_squat_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../fts/lib20_fts_plugin.la
+
+lib21_fts_squat_plugin_la_SOURCES = \
+ fts-squat-plugin.c \
+ fts-backend-squat.c \
+ squat-trie.c \
+ squat-uidlist.c
+
+noinst_HEADERS = \
+ fts-squat-plugin.h \
+ squat-trie.h \
+ squat-trie-private.h \
+ squat-uidlist.h
+
+squat_test_SOURCES = \
+ squat-test.c
+
+common_objects = \
+ squat-trie.lo \
+ squat-uidlist.lo
+
+squat_test_LDADD = \
+ $(common_objects) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+squat_test_DEPENDENCIES = \
+ $(common_objects) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_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/plugins/fts-squat/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/fts-squat/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}; \
+ }
+
+lib21_fts_squat_plugin.la: $(lib21_fts_squat_plugin_la_OBJECTS) $(lib21_fts_squat_plugin_la_DEPENDENCIES) $(EXTRA_lib21_fts_squat_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib21_fts_squat_plugin_la_LINK) -rpath $(moduledir) $(lib21_fts_squat_plugin_la_OBJECTS) $(lib21_fts_squat_plugin_la_LIBADD) $(LIBS)
+
+squat-test$(EXEEXT): $(squat_test_OBJECTS) $(squat_test_DEPENDENCIES) $(EXTRA_squat_test_DEPENDENCIES)
+ @rm -f squat-test$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(squat_test_OBJECTS) $(squat_test_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-backend-squat.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-squat-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/squat-test.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/squat-trie.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/squat-uidlist.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 $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)"; 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-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fts-backend-squat.Plo
+ -rm -f ./$(DEPDIR)/fts-squat-plugin.Plo
+ -rm -f ./$(DEPDIR)/squat-test.Po
+ -rm -f ./$(DEPDIR)/squat-trie.Plo
+ -rm -f ./$(DEPDIR)/squat-uidlist.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-moduleLTLIBRARIES
+
+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)/fts-backend-squat.Plo
+ -rm -f ./$(DEPDIR)/fts-squat-plugin.Plo
+ -rm -f ./$(DEPDIR)/squat-test.Po
+ -rm -f ./$(DEPDIR)/squat-trie.Plo
+ -rm -f ./$(DEPDIR)/squat-uidlist.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ 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-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
+
+.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/plugins/fts-squat/fts-backend-squat.c b/src/plugins/fts-squat/fts-backend-squat.c
new file mode 100644
index 0000000..fbd7bbe
--- /dev/null
+++ b/src/plugins/fts-squat/fts-backend-squat.c
@@ -0,0 +1,497 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mail-search-build.h"
+#include "squat-trie.h"
+#include "fts-squat-plugin.h"
+
+
+#define SQUAT_FILE_PREFIX "dovecot.index.search"
+
+struct squat_fts_backend {
+ struct fts_backend backend;
+
+ struct mailbox *box;
+ struct squat_trie *trie;
+
+ unsigned int partial_len, full_len;
+ bool refresh;
+};
+
+struct squat_fts_backend_update_context {
+ struct fts_backend_update_context ctx;
+ struct squat_trie_build_context *build_ctx;
+
+ enum squat_index_type squat_type;
+ uint32_t uid;
+ string_t *hdr;
+
+ bool failed;
+};
+
+static struct fts_backend *fts_backend_squat_alloc(void)
+{
+ struct squat_fts_backend *backend;
+
+ backend = i_new(struct squat_fts_backend, 1);
+ backend->backend = fts_backend_squat;
+ return &backend->backend;
+}
+
+static int
+fts_backend_squat_init(struct fts_backend *_backend, const char **error_r)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)_backend;
+ const char *const *tmp, *env;
+ unsigned int len;
+
+ env = mail_user_plugin_getenv(_backend->ns->user, "fts_squat");
+ if (env == NULL)
+ return 0;
+
+ for (tmp = t_strsplit_spaces(env, " "); *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "partial=")) {
+ if (str_to_uint(*tmp + 8, &len) < 0 || len == 0) {
+ *error_r = t_strdup_printf(
+ "Invalid partial length: %s", *tmp + 8);
+ return -1;
+ }
+ backend->partial_len = len;
+ } else if (str_begins(*tmp, "full=")) {
+ if (str_to_uint(*tmp + 5, &len) < 0 || len == 0) {
+ *error_r = t_strdup_printf(
+ "Invalid full length: %s", *tmp + 5);
+ return -1;
+ }
+ backend->full_len = len;
+ } else {
+ *error_r = t_strdup_printf("Invalid setting: %s", *tmp);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+fts_backend_squat_unset_box(struct squat_fts_backend *backend)
+{
+ if (backend->trie != NULL)
+ squat_trie_deinit(&backend->trie);
+ backend->box = NULL;
+}
+
+static void fts_backend_squat_deinit(struct fts_backend *_backend)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)_backend;
+
+ fts_backend_squat_unset_box(backend);
+ i_free(backend);
+}
+
+static int
+fts_backend_squat_set_box(struct squat_fts_backend *backend,
+ struct mailbox *box)
+{
+ const struct mailbox_permissions *perm;
+ struct mail_storage *storage;
+ struct mailbox_status status;
+ const char *path;
+ enum squat_index_flags flags = 0;
+ int ret;
+
+ if (backend->box == box)
+ {
+ if (backend->refresh) {
+ ret = squat_trie_refresh(backend->trie);
+ if (ret < 0)
+ return ret;
+ backend->refresh = FALSE;
+ }
+ return 0;
+ }
+ fts_backend_squat_unset_box(backend);
+ backend->refresh = FALSE;
+ if (box == NULL)
+ return 0;
+
+ perm = mailbox_get_permissions(box);
+ storage = mailbox_get_storage(box);
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path) <= 0)
+ i_unreached(); /* fts already checked this */
+
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY, &status);
+ if (storage->set->mmap_disable)
+ flags |= SQUAT_INDEX_FLAG_MMAP_DISABLE;
+ if (storage->set->mail_nfs_index)
+ flags |= SQUAT_INDEX_FLAG_NFS_FLUSH;
+ if (storage->set->dotlock_use_excl)
+ flags |= SQUAT_INDEX_FLAG_DOTLOCK_USE_EXCL;
+
+ backend->trie =
+ squat_trie_init(t_strconcat(path, "/"SQUAT_FILE_PREFIX, NULL),
+ status.uidvalidity,
+ storage->set->parsed_lock_method,
+ flags, perm->file_create_mode,
+ perm->file_create_gid);
+
+ if (backend->partial_len != 0)
+ squat_trie_set_partial_len(backend->trie, backend->partial_len);
+ if (backend->full_len != 0)
+ squat_trie_set_full_len(backend->trie, backend->full_len);
+ backend->box = box;
+ return squat_trie_open(backend->trie);
+}
+
+static int
+fts_backend_squat_get_last_uid(struct fts_backend *_backend,
+ struct mailbox *box, uint32_t *last_uid_r)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)_backend;
+
+ int ret = fts_backend_squat_set_box(backend, box);
+ if (ret < 0)
+ return -1;
+ return squat_trie_get_last_uid(backend->trie, last_uid_r);
+}
+
+static struct fts_backend_update_context *
+fts_backend_squat_update_init(struct fts_backend *_backend)
+{
+ struct squat_fts_backend_update_context *ctx;
+
+ ctx = i_new(struct squat_fts_backend_update_context, 1);
+ ctx->ctx.backend = _backend;
+ ctx->hdr = str_new(default_pool, 1024*32);
+ return &ctx->ctx;
+}
+
+static int get_all_msg_uids(struct mailbox *box, ARRAY_TYPE(seq_range) *uids)
+{
+ struct mailbox_transaction_context *t;
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct mail *mail;
+ int ret;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+ search_ctx = mailbox_search_init(t, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ /* *2 because even/odd is for body/header */
+ seq_range_array_add_range(uids, mail->uid * 2,
+ mail->uid * 2 + 1);
+ }
+ ret = mailbox_search_deinit(&search_ctx);
+ (void)mailbox_transaction_commit(&t);
+ return ret;
+}
+
+static int
+fts_backend_squat_update_uid_changed(struct squat_fts_backend_update_context *ctx)
+{
+ int ret = 0;
+
+ if (ctx->uid == 0)
+ return 0;
+
+ if (squat_trie_build_more(ctx->build_ctx, ctx->uid,
+ SQUAT_INDEX_TYPE_HEADER,
+ str_data(ctx->hdr), str_len(ctx->hdr)) < 0)
+ ret = -1;
+ str_truncate(ctx->hdr, 0);
+ return ret;
+}
+
+static int
+fts_backend_squat_build_deinit(struct squat_fts_backend_update_context *ctx)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)ctx->ctx.backend;
+ ARRAY_TYPE(seq_range) uids;
+ int ret = 0;
+
+ if (ctx->build_ctx == NULL)
+ return 0;
+
+ if (fts_backend_squat_update_uid_changed(ctx) < 0)
+ ret = -1;
+
+ i_array_init(&uids, 1024);
+ if (get_all_msg_uids(backend->box, &uids) < 0) {
+ (void)squat_trie_build_deinit(&ctx->build_ctx, NULL);
+ ret = -1;
+ } else {
+ seq_range_array_invert(&uids, 2, (uint32_t)-2);
+ if (squat_trie_build_deinit(&ctx->build_ctx, &uids) < 0)
+ ret = -1;
+ }
+ array_free(&uids);
+ return ret;
+}
+
+static int
+fts_backend_squat_update_deinit(struct fts_backend_update_context *_ctx)
+{
+ struct squat_fts_backend_update_context *ctx =
+ (struct squat_fts_backend_update_context *)_ctx;
+ int ret = ctx->failed ? -1 : 0;
+
+ if (fts_backend_squat_build_deinit(ctx) < 0)
+ ret = -1;
+ str_free(&ctx->hdr);
+ i_free(ctx);
+ return ret;
+}
+
+static void
+fts_backend_squat_update_set_mailbox(struct fts_backend_update_context *_ctx,
+ struct mailbox *box)
+{
+ struct squat_fts_backend_update_context *ctx =
+ (struct squat_fts_backend_update_context *)_ctx;
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)ctx->ctx.backend;
+
+ if (fts_backend_squat_build_deinit(ctx) < 0)
+ ctx->failed = TRUE;
+ if (fts_backend_squat_set_box(backend, box) < 0)
+ ctx->failed = TRUE;
+ else if (box != NULL) {
+ if (squat_trie_build_init(backend->trie, &ctx->build_ctx) < 0)
+ ctx->failed = TRUE;
+ }
+}
+
+static void
+fts_backend_squat_update_expunge(struct fts_backend_update_context *_ctx ATTR_UNUSED,
+ uint32_t last_uid ATTR_UNUSED)
+{
+ /* FIXME */
+}
+
+static bool
+fts_backend_squat_update_set_build_key(struct fts_backend_update_context *_ctx,
+ const struct fts_backend_build_key *key)
+{
+ struct squat_fts_backend_update_context *ctx =
+ (struct squat_fts_backend_update_context *)_ctx;
+
+ if (ctx->failed)
+ return FALSE;
+
+ if (key->uid != ctx->uid) {
+ if (fts_backend_squat_update_uid_changed(ctx) < 0)
+ ctx->failed = TRUE;
+ }
+
+ switch (key->type) {
+ case FTS_BACKEND_BUILD_KEY_HDR:
+ case FTS_BACKEND_BUILD_KEY_MIME_HDR:
+ str_printfa(ctx->hdr, "%s: ", key->hdr_name);
+ ctx->squat_type = SQUAT_INDEX_TYPE_HEADER;
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART:
+ ctx->squat_type = SQUAT_INDEX_TYPE_BODY;
+ break;
+ case FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY:
+ i_unreached();
+ }
+ ctx->uid = key->uid;
+ return TRUE;
+}
+
+static void
+fts_backend_squat_update_unset_build_key(struct fts_backend_update_context *_ctx)
+{
+ struct squat_fts_backend_update_context *ctx =
+ (struct squat_fts_backend_update_context *)_ctx;
+
+ if (ctx->squat_type == SQUAT_INDEX_TYPE_HEADER)
+ str_append_c(ctx->hdr, '\n');
+}
+
+static int
+fts_backend_squat_update_build_more(struct fts_backend_update_context *_ctx,
+ const unsigned char *data, size_t size)
+{
+ struct squat_fts_backend_update_context *ctx =
+ (struct squat_fts_backend_update_context *)_ctx;
+
+ if (ctx->squat_type == SQUAT_INDEX_TYPE_HEADER) {
+ str_append_data(ctx->hdr, data, size);
+ return 0;
+ }
+ return squat_trie_build_more(ctx->build_ctx, ctx->uid, ctx->squat_type,
+ data, size);
+}
+
+static int fts_backend_squat_refresh(struct fts_backend *_backend)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)_backend;
+
+ backend->refresh = TRUE;
+ return 0;
+}
+
+static int fts_backend_squat_optimize(struct fts_backend *_backend ATTR_UNUSED)
+{
+ /* FIXME: drop expunged messages */
+ return 0;
+}
+
+static int squat_lookup_arg(struct squat_fts_backend *backend,
+ const struct mail_search_arg *arg, bool and_args,
+ ARRAY_TYPE(seq_range) *definite_uids,
+ ARRAY_TYPE(seq_range) *maybe_uids)
+{
+ enum squat_index_type squat_type;
+ ARRAY_TYPE(seq_range) tmp_definite_uids, tmp_maybe_uids;
+ string_t *dtc;
+ uint32_t last_uid;
+ int ret;
+
+ switch (arg->type) {
+ case SEARCH_TEXT:
+ squat_type = SQUAT_INDEX_TYPE_HEADER |
+ SQUAT_INDEX_TYPE_BODY;
+ break;
+ case SEARCH_BODY:
+ squat_type = SQUAT_INDEX_TYPE_BODY;
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ squat_type = SQUAT_INDEX_TYPE_HEADER;
+ break;
+ default:
+ return 0;
+ }
+
+ i_array_init(&tmp_definite_uids, 128);
+ i_array_init(&tmp_maybe_uids, 128);
+
+ dtc = t_str_new(128);
+ if (backend->backend.ns->user->
+ default_normalizer(arg->value.str, strlen(arg->value.str), dtc) < 0)
+ i_panic("squat: search key not utf8");
+
+ ret = squat_trie_lookup(backend->trie, str_c(dtc), squat_type,
+ &tmp_definite_uids, &tmp_maybe_uids);
+ if (arg->match_not) {
+ /* definite -> non-match
+ maybe -> maybe
+ non-match -> maybe */
+ array_clear(&tmp_maybe_uids);
+
+ if (squat_trie_get_last_uid(backend->trie, &last_uid) < 0)
+ i_unreached();
+ seq_range_array_add_range(&tmp_maybe_uids, 1, last_uid);
+ seq_range_array_remove_seq_range(&tmp_maybe_uids,
+ &tmp_definite_uids);
+ array_clear(&tmp_definite_uids);
+ }
+
+ if (and_args) {
+ /* AND:
+ definite && definite -> definite
+ definite && maybe -> maybe
+ maybe && maybe -> maybe */
+
+ /* put definites among maybies, so they can be intersected */
+ seq_range_array_merge(maybe_uids, definite_uids);
+ seq_range_array_merge(&tmp_maybe_uids, &tmp_definite_uids);
+
+ seq_range_array_intersect(maybe_uids, &tmp_maybe_uids);
+ seq_range_array_intersect(definite_uids, &tmp_definite_uids);
+ /* remove duplicate maybies that are also definites */
+ seq_range_array_remove_seq_range(maybe_uids, definite_uids);
+ } else {
+ /* OR:
+ definite || definite -> definite
+ definite || maybe -> definite
+ maybe || maybe -> maybe */
+
+ /* remove maybies that are now definites */
+ seq_range_array_remove_seq_range(&tmp_maybe_uids,
+ definite_uids);
+ seq_range_array_remove_seq_range(maybe_uids,
+ &tmp_definite_uids);
+
+ seq_range_array_merge(definite_uids, &tmp_definite_uids);
+ seq_range_array_merge(maybe_uids, &tmp_maybe_uids);
+ }
+
+ array_free(&tmp_definite_uids);
+ array_free(&tmp_maybe_uids);
+ return ret < 0 ? -1 : 1;
+}
+
+static int
+fts_backend_squat_lookup(struct fts_backend *_backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ struct squat_fts_backend *backend =
+ (struct squat_fts_backend *)_backend;
+ bool and_args = (flags & FTS_LOOKUP_FLAG_AND_ARGS) != 0;
+ bool first = TRUE;
+ int ret;
+
+ ret = fts_backend_squat_set_box(backend, box);
+ if (ret < 0)
+ return -1;
+
+ for (; args != NULL; args = args->next) {
+ ret = squat_lookup_arg(backend, args, first ? FALSE : and_args,
+ &result->definite_uids,
+ &result->maybe_uids);
+ if (ret < 0)
+ return -1;
+ if (ret > 0) {
+ args->match_always = TRUE;
+ first = FALSE;
+ }
+ }
+ return 0;
+}
+
+struct fts_backend fts_backend_squat = {
+ .name = "squat",
+ .flags = FTS_BACKEND_FLAG_NORMALIZE_INPUT,
+
+ {
+ fts_backend_squat_alloc,
+ fts_backend_squat_init,
+ fts_backend_squat_deinit,
+ fts_backend_squat_get_last_uid,
+ fts_backend_squat_update_init,
+ fts_backend_squat_update_deinit,
+ fts_backend_squat_update_set_mailbox,
+ fts_backend_squat_update_expunge,
+ fts_backend_squat_update_set_build_key,
+ fts_backend_squat_update_unset_build_key,
+ fts_backend_squat_update_build_more,
+ fts_backend_squat_refresh,
+ NULL,
+ fts_backend_squat_optimize,
+ fts_backend_default_can_lookup,
+ fts_backend_squat_lookup,
+ NULL,
+ NULL
+ }
+};
diff --git a/src/plugins/fts-squat/fts-squat-plugin.c b/src/plugins/fts-squat/fts-squat-plugin.c
new file mode 100644
index 0000000..59d9383
--- /dev/null
+++ b/src/plugins/fts-squat/fts-squat-plugin.c
@@ -0,0 +1,18 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fts-squat-plugin.h"
+
+const char *fts_squat_plugin_version = DOVECOT_ABI_VERSION;
+
+void fts_squat_plugin_init(struct module *module ATTR_UNUSED)
+{
+ fts_backend_register(&fts_backend_squat);
+}
+
+void fts_squat_plugin_deinit(void)
+{
+ fts_backend_unregister(fts_backend_squat.name);
+}
+
+const char *fts_squat_plugin_dependencies[] = { "fts", NULL };
diff --git a/src/plugins/fts-squat/fts-squat-plugin.h b/src/plugins/fts-squat/fts-squat-plugin.h
new file mode 100644
index 0000000..0d6bfcb
--- /dev/null
+++ b/src/plugins/fts-squat/fts-squat-plugin.h
@@ -0,0 +1,14 @@
+#ifndef FTS_SQUAT_PLUGIN_H
+#define FTS_SQUAT_PLUGIN_H
+
+#include "fts-api-private.h"
+
+struct module;
+
+extern const char *fts_squat_plugin_dependencies[];
+extern struct fts_backend fts_backend_squat;
+
+void fts_squat_plugin_init(struct module *module);
+void fts_squat_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fts-squat/squat-test.c b/src/plugins/fts-squat/squat-test.c
new file mode 100644
index 0000000..b55646c
--- /dev/null
+++ b/src/plugins/fts-squat/squat-test.c
@@ -0,0 +1,197 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "file-lock.h"
+#include "istream.h"
+#include "time-util.h"
+#include "unichar.h"
+#include "squat-trie.h"
+#include "squat-uidlist.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#include <sys/time.h>
+
+static void result_print(ARRAY_TYPE(seq_range) *result)
+{
+ const struct seq_range *range;
+ unsigned int i, count;
+
+ range = array_get(result, &count);
+ for (i = 0; i < count; i++) {
+ if (i != 0)
+ printf(",");
+ printf("%u", range[i].seq1);
+ if (range[i].seq1 != range[i].seq2)
+ printf("-%u", range[i].seq2);
+ }
+ printf("\n");
+}
+
+int main(int argc ATTR_UNUSED, char *argv[])
+{
+ const char *trie_path = "/tmp/squat-test-index.search";
+ const char *uidlist_path = "/tmp/squat-test-index.search.uids";
+ struct squat_trie *trie;
+ struct squat_trie_build_context *build_ctx;
+ struct istream *input;
+ struct stat trie_st, uidlist_st;
+ ARRAY_TYPE(seq_range) definite_uids, maybe_uids;
+ char *line, *str, buf[4096];
+ buffer_t *valid;
+ int ret, fd;
+ unsigned int last = 0, seq = 1, node_count, uidlist_count;
+ size_t len;
+ enum squat_index_type index_type;
+ bool data_header = TRUE, first = TRUE, skip_body = FALSE;
+ bool mime_header = TRUE;
+ size_t trie_mem, uidlist_mem;
+ clock_t clock_start, clock_end;
+ struct timeval tv_start, tv_end;
+ double cputime;
+
+ lib_init();
+ i_unlink_if_exists(trie_path);
+ i_unlink_if_exists(uidlist_path);
+ trie = squat_trie_init(trie_path, time(NULL),
+ FILE_LOCK_METHOD_FCNTL, 0, 0600, (gid_t)-1);
+
+ clock_start = clock();
+ i_gettimeofday(&tv_start);
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd == -1)
+ return 1;
+
+ if (squat_trie_build_init(trie, &build_ctx) < 0)
+ return 1;
+
+ valid = buffer_create_dynamic(default_pool, 4096);
+ input = i_stream_create_fd(fd, SIZE_MAX);
+ ret = 0;
+ while (ret == 0 && (line = i_stream_read_next_line(input)) != NULL) {
+ if (last != input->v_offset/(1024*100)) {
+ fprintf(stderr, "\r%ukB", (unsigned)(input->v_offset/1024));
+ fflush(stderr);
+ last = input->v_offset/(1024*100);
+ }
+ if (str_begins(line, "From ")) {
+ if (!first)
+ seq++;
+ data_header = TRUE;
+ skip_body = FALSE;
+ mime_header = TRUE;
+ continue;
+ }
+ first = FALSE;
+
+ if (str_begins(line, "--")) {
+ skip_body = FALSE;
+ mime_header = TRUE;
+ }
+
+ if (mime_header) {
+ if (*line == '\0') {
+ data_header = FALSE;
+ mime_header = FALSE;
+ continue;
+ }
+
+ if (strncasecmp(line, "Content-Type:", 13) == 0 &&
+ strncasecmp(line, "Content-Type: text/", 19) != 0 &&
+ strncasecmp(line, "Content-Type: message/", 22) != 0)
+ skip_body = TRUE;
+ else if (strncasecmp(line, "Content-Transfer-Encoding: base64", 33) == 0)
+ skip_body = TRUE;
+ } else if (skip_body)
+ continue;
+ if (*line == '\0')
+ continue;
+
+ /* we're actually indexing here headers as bodies and bodies
+ as headers. it doesn't really matter in this test, and
+ fixing it would require storing headers temporarily
+ elsewhere and index them only after the body */
+ index_type = !data_header ? SQUAT_INDEX_TYPE_HEADER :
+ SQUAT_INDEX_TYPE_BODY;
+
+ buffer_set_used_size(valid, 0);
+ len = strlen(line);
+ if (uni_utf8_get_valid_data((const unsigned char *)line,
+ len, valid)) {
+ ret = squat_trie_build_more(build_ctx, seq, index_type,
+ (const void *)line, len);
+ } else if (valid->used > 0) {
+ ret = squat_trie_build_more(build_ctx, seq, index_type,
+ valid->data, valid->used);
+ }
+ }
+ buffer_free(&valid);
+ if (squat_trie_build_deinit(&build_ctx, NULL) < 0)
+ ret = -1;
+ if (ret < 0) {
+ printf("build broken\n");
+ return 1;
+ }
+
+ clock_end = clock();
+ i_gettimeofday(&tv_end);
+
+ cputime = (double)(clock_end - clock_start) / CLOCKS_PER_SEC;
+ fprintf(stderr, "\n - Index time: %.2f CPU seconds, "
+ "%.2f real seconds (%.02fMB/CPUs)\n", cputime,
+ timeval_diff_msecs(&tv_end, &tv_start)/1000.0,
+ input->v_offset / cputime / (1024*1024));
+
+ if (stat(trie_path, &trie_st) < 0)
+ i_error("stat(%s) failed: %m", trie_path);
+ if (stat(uidlist_path, &uidlist_st) < 0)
+ i_error("stat(%s) failed: %m", uidlist_path);
+
+ trie_mem = squat_trie_mem_used(trie, &node_count);
+ uidlist_mem = squat_uidlist_mem_used(squat_trie_get_uidlist(trie),
+ &uidlist_count);
+ fprintf(stderr, " - memory: %uk for trie, %uk for uidlist\n",
+ (unsigned)(trie_mem/1024), (unsigned)(uidlist_mem/1024));
+ fprintf(stderr, " - %"PRIuUOFF_T" bytes in %u nodes (%.02f%%)\n",
+ trie_st.st_size, node_count,
+ trie_st.st_size / (float)input->v_offset * 100.0);
+ fprintf(stderr, " - %"PRIuUOFF_T" bytes in %u UID lists (%.02f%%)\n",
+ uidlist_st.st_size, uidlist_count,
+ uidlist_st.st_size / (float)input->v_offset * 100.0);
+ fprintf(stderr, " - %"PRIuUOFF_T" bytes total of %"
+ PRIuUOFF_T" (%.02f%%)\n",
+ (trie_st.st_size + uidlist_st.st_size), input->v_offset,
+ (trie_st.st_size + uidlist_st.st_size) /
+ (float)input->v_offset * 100.0);
+
+ i_stream_unref(&input);
+ i_close_fd(&fd);
+
+ i_array_init(&definite_uids, 128);
+ i_array_init(&maybe_uids, 128);
+ while ((str = fgets(buf, sizeof(buf), stdin)) != NULL) {
+ ret = strlen(str)-1;
+ str[ret] = 0;
+
+ i_gettimeofday(&tv_start);
+ ret = squat_trie_lookup(trie, str, SQUAT_INDEX_TYPE_HEADER |
+ SQUAT_INDEX_TYPE_BODY,
+ &definite_uids, &maybe_uids);
+ if (ret < 0)
+ printf("error\n");
+ else {
+ i_gettimeofday(&tv_end);
+ printf(" - Search took %.05f CPU seconds\n",
+ timeval_diff_usecs(&tv_end, &tv_start)/1000000.0);
+ printf(" - definite uids: ");
+ result_print(&definite_uids);
+ printf(" - maybe uids: ");
+ result_print(&maybe_uids);
+ }
+ }
+ return 0;
+}
diff --git a/src/plugins/fts-squat/squat-trie-private.h b/src/plugins/fts-squat/squat-trie-private.h
new file mode 100644
index 0000000..b079554
--- /dev/null
+++ b/src/plugins/fts-squat/squat-trie-private.h
@@ -0,0 +1,192 @@
+#ifndef SQUAT_TRIE_PRIVATE_H
+#define SQUAT_TRIE_PRIVATE_H
+
+#include "file-dotlock.h"
+#include "squat-trie.h"
+
+#define SQUAT_TRIE_VERSION 2
+#define SQUAT_TRIE_LOCK_TIMEOUT 60
+#define SQUAT_TRIE_DOTLOCK_STALE_TIMEOUT (15*60)
+
+struct squat_file_header {
+ uint8_t version;
+ uint8_t unused[3];
+
+ uint32_t indexid;
+ uint32_t uidvalidity;
+ uint32_t used_file_size;
+ uint32_t deleted_space;
+ uint32_t node_count;
+
+ uint32_t root_offset;
+ uint32_t root_unused_uids;
+ uint32_t root_next_uid;
+ uint32_t root_uidlist_idx;
+
+ uint8_t partial_len;
+ uint8_t full_len;
+ uint8_t normalize_map[256];
+};
+
+/*
+ node file: FIXME: no up-to-date
+
+ struct squat_file_header;
+
+ // children are written before their parents
+ node[] {
+ uint8_t child_count;
+ unsigned char chars[child_count];
+ packed neg_diff_to_first_child_offset; // relative to node
+ packed diff_to_prev_offset[child_count-1];
+ packed[child_count] {
+ // unused_uids_count == uid if have_uid_offset bit is zero
+ (unused_uids_count << 1) | (have_uid_offset);
+ [diff_to_prev_uid_offset;] // first one is relative to zero
+ }
+ }
+*/
+
+struct squat_node {
+ unsigned int child_count:8;
+
+ /* children.leaf_string contains this many bytes */
+ unsigned int leaf_string_length:16;
+
+ /* TRUE = children.data contains our children.
+ FALSE = children.offset contains offset to our children in the
+ index file. */
+ bool children_not_mapped:1;
+ /* When allocating our children, use a sequential array. */
+ bool want_sequential:1;
+ /* This node's children are in a sequential array, meaning that the
+ first SEQUENTIAL_COUNT children have chars[n] = n. */
+ bool have_sequential:1;
+
+ /* Number of UIDs that exists in parent node but not in this one
+ (i.e. number of UIDs [0..next_uid-1] not in this node's uidlist).
+ This is mainly used when adding new UIDs to our children to set
+ the UID to be relative to this node's UID list. */
+ uint32_t unused_uids;
+
+ /* next_uid=0 means there are no UIDs in this node, otherwise
+ next_uid-1 is the last UID added to this node. */
+ uint32_t next_uid;
+ uint32_t uid_list_idx;
+
+ /*
+ struct {
+ unsigned char chars[child_count];
+ struct squat_node[child_count];
+ } *children;
+ */
+ union {
+ /* children_not_mapped determines if data or offset should
+ be used. */
+ void *data;
+ unsigned char *leaf_string;
+ unsigned char static_leaf_string[sizeof(void *)];
+ uint32_t offset;
+ } children;
+};
+/* Return pointer to node.children.chars[] */
+#define NODE_CHILDREN_CHARS(node) \
+ ((unsigned char *)(node)->children.data)
+/* Return pointer to node.children.node[] */
+#define NODE_CHILDREN_NODES(_node) \
+ ((struct squat_node *)(NODE_CHILDREN_CHARS(_node) + \
+ MEM_ALIGN((_node)->child_count)))
+/* Return number of bytes allocated in node.children.data */
+#define NODE_CHILDREN_ALLOC_SIZE(child_count) \
+ (MEM_ALIGN(child_count) + \
+ ((child_count) / 8 + 1) * 8 * sizeof(struct squat_node))
+/* Return TRUE if children.leaf_string is set. */
+#define NODE_IS_DYNAMIC_LEAF(node) \
+ ((node)->leaf_string_length > \
+ sizeof((node)->children.static_leaf_string))
+/* Return node's leaf string. Assumes that it is set. */
+#define NODE_LEAF_STRING(node) \
+ (NODE_IS_DYNAMIC_LEAF(node) ? \
+ (node)->children.leaf_string : (node)->children.static_leaf_string)
+struct squat_trie {
+ struct squat_node root;
+ struct squat_uidlist *uidlist;
+
+ struct squat_file_header hdr;
+ size_t node_alloc_size;
+ unsigned int unmapped_child_count;
+
+ enum squat_index_flags flags;
+ enum file_lock_method lock_method;
+ mode_t create_mode;
+ gid_t create_gid;
+ uint32_t uidvalidity;
+
+ char *path;
+ int fd;
+ struct file_cache *file_cache;
+ struct dotlock_settings dotlock_set;
+
+ uoff_t locked_file_size;
+ const void *data;
+ size_t data_size;
+
+ void *mmap_base;
+ size_t mmap_size;
+
+ unsigned char default_normalize_map[256];
+ unsigned int default_partial_len;
+ unsigned int default_full_len;
+
+ bool corrupted:1;
+};
+
+#define SQUAT_PACK_MAX_SIZE ((sizeof(uint32_t) * 8 + 7) / 7)
+
+static inline void squat_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;
+}
+
+static inline uint32_t squat_unpack_num(const uint8_t **p, const uint8_t *end)
+{
+ const uint8_t *c = *p;
+ uint32_t value = 0;
+ unsigned int bits = 0;
+
+ for (;;) {
+ if (unlikely(c == end)) {
+ /* we should never see EOF */
+ return 0;
+ }
+
+ value |= (*c & 0x7f) << bits;
+ if (*c < 0x80)
+ break;
+
+ bits += 7;
+ c++;
+ }
+
+ if (unlikely(bits >= 32)) {
+ /* broken input */
+ *p = end;
+ return 0;
+ }
+
+ *p = c + 1;
+ return value;
+}
+
+int squat_trie_create_fd(struct squat_trie *trie, const char *path, int flags);
+void squat_trie_delete(struct squat_trie *trie);
+
+#endif
diff --git a/src/plugins/fts-squat/squat-trie.c b/src/plugins/fts-squat/squat-trie.c
new file mode 100644
index 0000000..d006817
--- /dev/null
+++ b/src/plugins/fts-squat/squat-trie.c
@@ -0,0 +1,2096 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "read-full.h"
+#include "istream.h"
+#include "ostream.h"
+#include "unichar.h"
+#include "nfs-workarounds.h"
+#include "file-cache.h"
+#include "seq-range-array.h"
+#include "squat-uidlist.h"
+#include "squat-trie-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#define DEFAULT_NORMALIZE_MAP_CHARS \
+ "EOTIRSACDNLMVUGPHBFWYXKJQZ0123456789@.-+#$%_&"
+#define DEFAULT_PARTIAL_LEN 4
+#define DEFAULT_FULL_LEN 4
+
+#define MAX_FAST_LEVEL 3
+#define SEQUENTIAL_COUNT 46
+
+#define TRIE_BYTES_LEFT(n) \
+ ((n) * SQUAT_PACK_MAX_SIZE)
+#define TRIE_READAHEAD_SIZE \
+ I_MAX(4096, 1 + 256 + TRIE_BYTES_LEFT(256))
+
+struct squat_trie_build_context {
+ struct squat_trie *trie;
+ struct ostream *output;
+ struct squat_uidlist_build_context *uidlist_build_ctx;
+
+ struct file_lock *file_lock;
+ struct dotlock *dotlock;
+
+ uint32_t first_uid;
+ bool compress_nodes:1;
+};
+
+struct squat_trie_iterate_node {
+ struct squat_node *node;
+ ARRAY_TYPE(seq_range) shifts;
+ unsigned int idx;
+};
+
+struct squat_trie_iterate_context {
+ struct squat_trie *trie;
+ struct squat_trie_iterate_node cur;
+ ARRAY(struct squat_trie_iterate_node) parents;
+ bool failed;
+};
+
+static int squat_trie_map(struct squat_trie *trie, bool building);
+
+void squat_trie_delete(struct squat_trie *trie)
+{
+ i_unlink_if_exists(trie->path);
+ squat_uidlist_delete(trie->uidlist);
+}
+
+static void squat_trie_set_corrupted(struct squat_trie *trie)
+{
+ trie->corrupted = TRUE;
+ i_error("Corrupted file %s", trie->path);
+ squat_trie_delete(trie);
+}
+
+static void squat_trie_normalize_map_build(struct squat_trie *trie)
+{
+ static unsigned char valid_chars[] =
+ DEFAULT_NORMALIZE_MAP_CHARS;
+ unsigned int i, j;
+
+ memset(trie->default_normalize_map, 0,
+ sizeof(trie->default_normalize_map));
+
+#if 1
+ for (i = 0, j = 1; i < sizeof(valid_chars)-1; i++) {
+ unsigned char chr = valid_chars[i];
+
+ if (chr >= 'A' && chr <= 'Z')
+ trie->default_normalize_map[chr-'A'+'a'] = j;
+ trie->default_normalize_map[chr] = j++;
+ }
+ i_assert(j <= SEQUENTIAL_COUNT);
+
+ for (i = 128; i < 256; i++)
+ trie->default_normalize_map[i] = j++;
+#else
+ for (i = 0; i < sizeof(valid_chars)-1; i++) {
+ unsigned char chr = valid_chars[i];
+
+ if (chr >= 'A' && chr <= 'Z')
+ trie->default_normalize_map[chr-'A'+'a'] = chr;
+ trie->default_normalize_map[chr] = chr;
+ }
+ for (i = 128; i < 256; i++)
+ trie->default_normalize_map[i] = i_toupper(i);
+#endif
+}
+
+static void node_free(struct squat_trie *trie, struct squat_node *node)
+{
+ struct squat_node *children;
+ unsigned int i;
+
+ if (node->leaf_string_length > 0) {
+ if (NODE_IS_DYNAMIC_LEAF(node))
+ i_free(node->children.leaf_string);
+ } else if (!node->children_not_mapped) {
+ children = NODE_CHILDREN_NODES(node);
+
+ trie->node_alloc_size -=
+ NODE_CHILDREN_ALLOC_SIZE(node->child_count);
+ for (i = 0; i < node->child_count; i++)
+ node_free(trie, &children[i]);
+
+ i_free(node->children.data);
+ }
+}
+
+struct squat_trie *
+squat_trie_init(const char *path, uint32_t uidvalidity,
+ enum file_lock_method lock_method, enum squat_index_flags flags,
+ mode_t mode, gid_t gid)
+{
+ struct squat_trie *trie;
+
+ trie = i_new(struct squat_trie, 1);
+ trie->path = i_strdup(path);
+ trie->uidlist = squat_uidlist_init(trie);
+ trie->fd = -1;
+ trie->lock_method = lock_method;
+ trie->uidvalidity = uidvalidity;
+ trie->flags = flags;
+ trie->create_mode = mode;
+ trie->create_gid = gid;
+ squat_trie_normalize_map_build(trie);
+
+ trie->dotlock_set.use_excl_lock =
+ (flags & SQUAT_INDEX_FLAG_DOTLOCK_USE_EXCL) != 0;
+ trie->dotlock_set.nfs_flush = (flags & SQUAT_INDEX_FLAG_NFS_FLUSH) != 0;
+ trie->dotlock_set.timeout = SQUAT_TRIE_LOCK_TIMEOUT;
+ trie->dotlock_set.stale_timeout = SQUAT_TRIE_DOTLOCK_STALE_TIMEOUT;
+ trie->default_partial_len = DEFAULT_PARTIAL_LEN;
+ trie->default_full_len = DEFAULT_FULL_LEN;
+ return trie;
+}
+
+static void squat_trie_close_fd(struct squat_trie *trie)
+{
+ trie->data = NULL;
+ trie->data_size = 0;
+
+ if (trie->mmap_size != 0) {
+ if (munmap(trie->mmap_base, trie->mmap_size) < 0)
+ i_error("munmap(%s) failed: %m", trie->path);
+ trie->mmap_base = NULL;
+ trie->mmap_size = 0;
+ }
+ i_close_fd_path(&trie->fd, trie->path);
+}
+
+static void squat_trie_close(struct squat_trie *trie)
+{
+ trie->corrupted = FALSE;
+ node_free(trie, &trie->root);
+ i_zero(&trie->root);
+ i_zero(&trie->hdr);
+
+ squat_trie_close_fd(trie);
+ if (trie->file_cache != NULL)
+ file_cache_free(&trie->file_cache);
+ trie->locked_file_size = 0;
+}
+
+void squat_trie_deinit(struct squat_trie **_trie)
+{
+ struct squat_trie *trie = *_trie;
+
+ *_trie = NULL;
+ squat_trie_close(trie);
+ squat_uidlist_deinit(trie->uidlist);
+ i_free(trie->path);
+ i_free(trie);
+}
+
+void squat_trie_set_partial_len(struct squat_trie *trie, unsigned int len)
+{
+ trie->default_partial_len = len;
+}
+
+void squat_trie_set_full_len(struct squat_trie *trie, unsigned int len)
+{
+ trie->default_full_len = len;
+}
+
+static void squat_trie_header_init(struct squat_trie *trie)
+{
+ i_zero(&trie->hdr);
+ trie->hdr.version = SQUAT_TRIE_VERSION;
+ trie->hdr.indexid = time(NULL);
+ trie->hdr.uidvalidity = trie->uidvalidity;
+ trie->hdr.partial_len = trie->default_partial_len;
+ trie->hdr.full_len = trie->default_full_len;
+
+ i_assert(sizeof(trie->hdr.normalize_map) ==
+ sizeof(trie->default_normalize_map));
+ memcpy(trie->hdr.normalize_map, trie->default_normalize_map,
+ sizeof(trie->hdr.normalize_map));
+}
+
+static int squat_trie_open_fd(struct squat_trie *trie)
+{
+ trie->fd = open(trie->path, O_RDWR);
+ if (trie->fd == -1) {
+ if (errno == ENOENT) {
+ squat_trie_header_init(trie);
+ return 0;
+ }
+ i_error("open(%s) failed: %m", trie->path);
+ return -1;
+ }
+ if (trie->file_cache != NULL)
+ file_cache_set_fd(trie->file_cache, trie->fd);
+ return 0;
+}
+
+int squat_trie_open(struct squat_trie *trie)
+{
+ squat_trie_close(trie);
+
+ if (squat_trie_open_fd(trie) < 0)
+ return -1;
+ return squat_trie_map(trie, FALSE);
+}
+
+static int squat_trie_is_file_stale(struct squat_trie *trie)
+{
+ struct stat st, st2;
+
+ if ((trie->flags & SQUAT_INDEX_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_file_handle_cache(trie->path);
+ if (nfs_safe_stat(trie->path, &st) < 0) {
+ if (errno == ENOENT)
+ return 1;
+
+ i_error("stat(%s) failed: %m", trie->path);
+ return -1;
+ }
+ if (fstat(trie->fd, &st2) < 0) {
+ if (errno == ESTALE)
+ return 1;
+ i_error("fstat(%s) failed: %m", trie->path);
+ return -1;
+ }
+ trie->locked_file_size = st2.st_size;
+
+ if (st.st_ino == st2.st_ino && CMP_DEV_T(st.st_dev, st2.st_dev)) {
+ i_assert(trie->locked_file_size >= trie->data_size);
+ return 0;
+ }
+ return 1;
+}
+
+int squat_trie_refresh(struct squat_trie *trie)
+{
+ int ret;
+
+ ret = squat_trie_is_file_stale(trie);
+ if (ret > 0)
+ ret = squat_trie_open(trie);
+ return ret;
+}
+
+static int squat_trie_lock(struct squat_trie *trie, int lock_type,
+ struct file_lock **file_lock_r,
+ struct dotlock **dotlock_r)
+{
+ const char *error;
+ int ret;
+
+ i_assert(trie->fd != -1);
+
+ *file_lock_r = NULL;
+ *dotlock_r = NULL;
+
+ for (;;) {
+ if (trie->lock_method != FILE_LOCK_METHOD_DOTLOCK) {
+ struct file_lock_settings lock_set = {
+ .lock_method = trie->lock_method,
+ };
+ ret = file_wait_lock(trie->fd, trie->path, lock_type,
+ &lock_set, SQUAT_TRIE_LOCK_TIMEOUT,
+ file_lock_r, &error);
+ if (ret < 0) {
+ i_error("squat trie %s: %s",
+ trie->path, error);
+ }
+ } else {
+ ret = file_dotlock_create(&trie->dotlock_set,
+ trie->path, 0, dotlock_r);
+ }
+ if (ret == 0) {
+ i_error("squat trie %s: Locking timed out", trie->path);
+ return 0;
+ }
+ if (ret < 0)
+ return -1;
+
+ /* if the trie has been compressed, we need to reopen the
+ file and try to lock again */
+ ret = squat_trie_is_file_stale(trie);
+ if (ret == 0)
+ break;
+
+ if (*file_lock_r != NULL)
+ file_unlock(file_lock_r);
+ else
+ file_dotlock_delete(dotlock_r);
+ if (ret < 0)
+ return -1;
+
+ squat_trie_close(trie);
+ if (squat_trie_open_fd(trie) < 0)
+ return -1;
+ if (trie->fd == -1)
+ return 0;
+ }
+
+ if ((trie->flags & SQUAT_INDEX_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_read_cache_locked(trie->path, trie->fd);
+ return 1;
+}
+
+static void
+node_make_sequential(struct squat_trie *trie, struct squat_node *node, int level)
+{
+ const unsigned int alloc_size =
+ NODE_CHILDREN_ALLOC_SIZE(SEQUENTIAL_COUNT);
+ struct squat_node *children;
+ unsigned char *chars;
+ unsigned int i;
+
+ i_assert(node->child_count == 0);
+
+ trie->node_alloc_size += alloc_size;
+
+ node->want_sequential = FALSE;
+ node->have_sequential = TRUE;
+
+ node->child_count = SEQUENTIAL_COUNT;
+ node->children.data = i_malloc(alloc_size);
+
+ chars = NODE_CHILDREN_CHARS(node);
+ for (i = 0; i < SEQUENTIAL_COUNT; i++)
+ chars[i] = i;
+
+ if (level < MAX_FAST_LEVEL) {
+ children = NODE_CHILDREN_NODES(node);
+ for (i = 0; i < SEQUENTIAL_COUNT; i++)
+ children[i].want_sequential = TRUE;
+ }
+}
+
+static unsigned int
+node_add_child(struct squat_trie *trie, struct squat_node *node,
+ unsigned char chr, int level)
+{
+ unsigned int old_child_count = node->child_count;
+ struct squat_node *children, *old_children;
+ unsigned char *chars;
+ size_t old_size, new_size;
+
+ i_assert(node->leaf_string_length == 0);
+
+ if (node->want_sequential) {
+ node_make_sequential(trie, node, level);
+
+ if (chr < SEQUENTIAL_COUNT)
+ return chr;
+ old_child_count = SEQUENTIAL_COUNT;
+ }
+
+ node->child_count++;
+ new_size = NODE_CHILDREN_ALLOC_SIZE(node->child_count);
+
+ if (old_child_count == 0) {
+ /* first child */
+ node->children.data = i_malloc(new_size);
+ trie->node_alloc_size += new_size;
+ } else {
+ old_size = NODE_CHILDREN_ALLOC_SIZE(old_child_count);
+ if (old_size != new_size) {
+ trie->node_alloc_size += new_size - old_size;
+ node->children.data = i_realloc(node->children.data,
+ old_size, new_size);
+ }
+
+ children = NODE_CHILDREN_NODES(node);
+ old_children = (void *)(NODE_CHILDREN_CHARS(node) +
+ MEM_ALIGN(old_child_count));
+ if (children != old_children) {
+ memmove(children, old_children,
+ old_child_count * sizeof(struct squat_node));
+ }
+ }
+
+ chars = NODE_CHILDREN_CHARS(node);
+ i_assert(chars != NULL);
+ chars[node->child_count - 1] = chr;
+ return node->child_count - 1;
+}
+
+static int
+trie_file_cache_read(struct squat_trie *trie, size_t offset, size_t size)
+{
+ if (trie->file_cache == NULL)
+ return 0;
+
+ if (file_cache_read(trie->file_cache, offset, size) < 0) {
+ i_error("read(%s) failed: %m", trie->path);
+ return -1;
+ }
+ trie->data = file_cache_get_map(trie->file_cache, &trie->data_size);
+ return 0;
+}
+
+static int
+node_read_children(struct squat_trie *trie, struct squat_node *node, int level)
+{
+ const uint8_t *data, *end;
+ const unsigned char *child_chars;
+ struct squat_node *child, *children = NULL;
+ uoff_t node_offset;
+ unsigned int i, child_idx, child_count;
+ uoff_t base_offset;
+ uint32_t num;
+
+ i_assert(node->children_not_mapped);
+ i_assert(!node->have_sequential);
+ i_assert(trie->unmapped_child_count > 0);
+ i_assert(trie->data_size <= trie->locked_file_size);
+
+ trie->unmapped_child_count--;
+ node_offset = node->children.offset;
+ node->children_not_mapped = FALSE;
+ node->children.data = NULL;
+
+ if (trie_file_cache_read(trie, node_offset, TRIE_READAHEAD_SIZE) < 0)
+ return -1;
+ if (unlikely(node_offset >= trie->data_size)) {
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+
+ data = CONST_PTR_OFFSET(trie->data, node_offset);
+ end = CONST_PTR_OFFSET(trie->data, trie->data_size);
+ child_count = *data++;
+ if (unlikely(node_offset + child_count >= trie->data_size)) {
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+
+ if (child_count == 0)
+ return 0;
+
+ child_chars = data;
+ data += child_count;
+
+ /* get child offsets */
+ base_offset = node_offset;
+ for (i = 0; i < child_count; i++) {
+ /* we always start with !have_sequential, so at i=0 this
+ check always goes to add the first child */
+ if (node->have_sequential && child_chars[i] < SEQUENTIAL_COUNT)
+ child_idx = child_chars[i];
+ else {
+ child_idx = node_add_child(trie, node, child_chars[i],
+ level);
+ children = NODE_CHILDREN_NODES(node);
+ }
+
+ i_assert(children != NULL);
+
+ child = &children[child_idx];
+
+ /* 1) child offset */
+ num = squat_unpack_num(&data, end);
+ if (num == 0) {
+ /* no children */
+ } else {
+ if ((num & 1) != 0) {
+ base_offset += num >> 1;
+ } else {
+ base_offset -= num >> 1;
+ }
+ if (base_offset >= trie->locked_file_size) {
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+ trie->unmapped_child_count++;
+ child->children_not_mapped = TRUE;
+ child->children.offset = base_offset;
+ }
+
+ /* 2) uidlist */
+ child->uid_list_idx = squat_unpack_num(&data, end);
+ if (child->uid_list_idx == 0) {
+ /* we don't write nodes with empty uidlists */
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+ if (!UIDLIST_IS_SINGLETON(child->uid_list_idx)) {
+ /* 3) next uid */
+ child->next_uid = squat_unpack_num(&data, end) + 1;
+ } else {
+ uint32_t idx = child->uid_list_idx;
+
+ child->next_uid = 1 +
+ squat_uidlist_singleton_last_uid(idx);
+ }
+
+ /* 4) unused uids + leaf string flag */
+ num = squat_unpack_num(&data, end);
+ child->unused_uids = num >> 1;
+ if ((num & 1) != 0) {
+ /* leaf string */
+ unsigned int len;
+ unsigned char *dest;
+
+ /* 5) leaf string length */
+ len = child->leaf_string_length =
+ squat_unpack_num(&data, end) + 1;
+ if (!NODE_IS_DYNAMIC_LEAF(child))
+ dest = child->children.static_leaf_string;
+ else {
+ dest = child->children.leaf_string =
+ i_malloc(len);
+ }
+
+ if (trie->file_cache != NULL) {
+ /* the string may be long -
+ recalculate the end pos */
+ size_t offset, size;
+
+ offset = (const char *)data -
+ (const char *)trie->data;
+ size = len + TRIE_BYTES_LEFT(child_count - i);
+
+ if (trie_file_cache_read(trie, offset,
+ size) < 0)
+ return -1;
+ data = CONST_PTR_OFFSET(trie->data, offset);
+ end = CONST_PTR_OFFSET(trie->data,
+ trie->data_size);
+ child_chars = CONST_PTR_OFFSET(trie->data,
+ node_offset + 1);
+ }
+
+ if ((size_t)(end - data) < len) {
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+ memcpy(dest, data, len);
+ data += len;
+ }
+ }
+ if (unlikely(data == end)) {
+ /* we should never get this far */
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+node_write_children(struct squat_trie_build_context *ctx,
+ struct squat_node *node, const uoff_t *node_offsets)
+{
+ struct squat_node *children;
+ const unsigned char *chars;
+ uint8_t child_count, buf[SQUAT_PACK_MAX_SIZE * 5], *bufp;
+ uoff_t base_offset;
+ unsigned int i;
+
+ chars = NODE_CHILDREN_CHARS(node);
+ children = NODE_CHILDREN_NODES(node);
+
+ base_offset = ctx->output->offset;
+ child_count = node->child_count;
+ o_stream_nsend(ctx->output, &child_count, 1);
+ o_stream_nsend(ctx->output, chars, child_count);
+
+ for (i = 0; i < child_count; i++) {
+ bufp = buf;
+ /* 1) child offset */
+ if (node_offsets[i] == 0)
+ *bufp++ = 0;
+ else if (node_offsets[i] >= base_offset) {
+ squat_pack_num(&bufp,
+ ((node_offsets[i] - base_offset) << 1) | 1);
+ base_offset = node_offsets[i];
+ } else {
+ squat_pack_num(&bufp,
+ (base_offset - node_offsets[i]) << 1);
+ base_offset = node_offsets[i];
+ }
+
+ /* 2) uidlist */
+ squat_pack_num(&bufp, children[i].uid_list_idx);
+ if (!UIDLIST_IS_SINGLETON(children[i].uid_list_idx)) {
+ /* 3) next uid */
+ squat_pack_num(&bufp, children[i].next_uid - 1);
+ }
+
+ if (children[i].leaf_string_length == 0) {
+ /* 4a) unused uids */
+ squat_pack_num(&bufp, children[i].unused_uids << 1);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+ } else {
+ i_assert(node_offsets[i] == 0);
+ /* 4b) unused uids + flag */
+ squat_pack_num(&bufp, (children[i].unused_uids << 1) | 1);
+ /* 5) leaf string length */
+ squat_pack_num(&bufp, children[i].leaf_string_length - 1);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+ o_stream_nsend(ctx->output,
+ NODE_LEAF_STRING(&children[i]),
+ children[i].leaf_string_length);
+ }
+ }
+}
+
+static inline void
+node_add_uid(struct squat_trie_build_context *ctx, uint32_t uid,
+ struct squat_node *node)
+{
+ if (uid < node->next_uid) {
+ /* duplicate */
+ return;
+ }
+ node->unused_uids += uid - node->next_uid;
+ node->next_uid = uid + 1;
+
+ node->uid_list_idx =
+ squat_uidlist_build_add_uid(ctx->uidlist_build_ctx,
+ node->uid_list_idx, uid);
+}
+
+static void
+node_split_string(struct squat_trie_build_context *ctx, struct squat_node *node)
+{
+ struct squat_node *child;
+ unsigned char *str;
+ unsigned int uid, idx, leafstr_len = node->leaf_string_length;
+
+ i_assert(leafstr_len > 0);
+
+ /* make a copy of the leaf string and convert to normal node by
+ removing it. */
+ str = t_malloc_no0(leafstr_len);
+ if (!NODE_IS_DYNAMIC_LEAF(node))
+ memcpy(str, node->children.static_leaf_string, leafstr_len);
+ else {
+ memcpy(str, node->children.leaf_string, leafstr_len);
+ i_free(node->children.leaf_string);
+ }
+ node->leaf_string_length = 0;
+
+ /* create a new child node for the rest of the string */
+ idx = node_add_child(ctx->trie, node, str[0], MAX_FAST_LEVEL);
+ child = NODE_CHILDREN_NODES(node) + idx;
+
+ /* update uidlist to contain all of parent's UIDs */
+ child->next_uid = node->next_uid - node->unused_uids;
+ for (uid = 0; uid < child->next_uid; uid++) {
+ child->uid_list_idx =
+ squat_uidlist_build_add_uid(ctx->uidlist_build_ctx,
+ child->uid_list_idx, uid);
+ }
+
+ i_assert(!child->have_sequential && child->children.data == NULL);
+ if (leafstr_len > 1) {
+ /* make the child a leaf string */
+ leafstr_len--;
+ child->leaf_string_length = leafstr_len;
+ if (!NODE_IS_DYNAMIC_LEAF(child)) {
+ memcpy(child->children.static_leaf_string,
+ str + 1, leafstr_len);
+ } else {
+ child->children.leaf_string = i_malloc(leafstr_len);
+ memcpy(child->children.leaf_string,
+ str + 1, leafstr_len);
+ }
+ }
+}
+
+static bool
+node_leaf_string_add_or_split(struct squat_trie_build_context *ctx,
+ struct squat_node *node,
+ const unsigned char *data, unsigned int data_len)
+{
+ const unsigned char *str = NODE_LEAF_STRING(node);
+ const unsigned int leafstr_len = node->leaf_string_length;
+ unsigned int i;
+
+ if (data_len != leafstr_len) {
+ /* different lengths, can't match */
+ T_BEGIN {
+ node_split_string(ctx, node);
+ } T_END;
+ return FALSE;
+ }
+
+ for (i = 0; i < data_len; i++) {
+ if (data[i] != str[i]) {
+ /* non-match */
+ T_BEGIN {
+ node_split_string(ctx, node);
+ } T_END;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static int squat_build_add(struct squat_trie_build_context *ctx, uint32_t uid,
+ const unsigned char *data, unsigned int size)
+{
+ struct squat_trie *trie = ctx->trie;
+ struct squat_node *node = &trie->root;
+ const unsigned char *end = data + size;
+ unsigned char *chars;
+ unsigned int idx;
+ int level = 0;
+
+ for (;;) {
+ if (node->children_not_mapped) {
+ if (unlikely(node_read_children(trie, node, level) < 0))
+ return -1;
+ }
+
+ if (node->leaf_string_length != 0) {
+ /* the whole string must match or we need to split
+ the node */
+ if (node_leaf_string_add_or_split(ctx, node, data,
+ end - data)) {
+ node_add_uid(ctx, uid, node);
+ return 0;
+ }
+ }
+
+ node_add_uid(ctx, uid, node);
+
+ if (unlikely(uid < node->unused_uids)) {
+ squat_trie_set_corrupted(trie);
+ return -1;
+ }
+ /* child node's UIDs are relative to ours. so for example if
+ we're adding UID 4 and this node now has [2,4] UIDs,
+ unused_uids=3 and so the child node will be adding
+ UID 4-3 = 1. */
+ uid -= node->unused_uids;
+
+ if (data == end)
+ return 0;
+ level++;
+
+ if (node->have_sequential) {
+ i_assert(node->child_count >= SEQUENTIAL_COUNT);
+ if (*data < SEQUENTIAL_COUNT) {
+ idx = *data;
+ goto found;
+ }
+ idx = SEQUENTIAL_COUNT;
+ } else {
+ idx = 0;
+ }
+ chars = NODE_CHILDREN_CHARS(node);
+ for (; idx < node->child_count; idx++) {
+ if (chars[idx] == *data)
+ goto found;
+ }
+ break;
+ found:
+ data++;
+ node = NODE_CHILDREN_NODES(node) + idx;
+ }
+
+ /* create new children */
+ i_assert(node->leaf_string_length == 0);
+
+ for (;;) {
+ idx = node_add_child(trie, node, *data,
+ size - (end - data) + 1);
+ node = NODE_CHILDREN_NODES(node) + idx;
+
+ node_add_uid(ctx, uid, node);
+ uid = 0;
+
+ if (++data == end)
+ break;
+
+ if (!node->have_sequential) {
+ /* convert the node into a leaf string */
+ unsigned int len = end - data;
+
+ i_assert(node->children.data == NULL);
+ node->leaf_string_length = len;
+ if (!NODE_IS_DYNAMIC_LEAF(node)) {
+ memcpy(node->children.static_leaf_string,
+ data, len);
+ } else {
+ node->children.leaf_string = i_malloc(len);
+ memcpy(node->children.leaf_string, data, len);
+ }
+ break;
+ }
+ }
+ return 0;
+}
+
+static int
+squat_build_word_bytes(struct squat_trie_build_context *ctx, uint32_t uid,
+ const unsigned char *data, unsigned int size)
+{
+ struct squat_trie *trie = ctx->trie;
+ unsigned int i;
+
+ if (trie->hdr.full_len <= trie->hdr.partial_len)
+ i = 0;
+ else {
+ /* the first word is longer than others */
+ if (squat_build_add(ctx, uid, data,
+ I_MIN(size, trie->hdr.full_len)) < 0)
+ return -1;
+ i = 1;
+ }
+
+ for (; i < size; i++) {
+ if (squat_build_add(ctx, uid, data + i,
+ I_MIN(trie->hdr.partial_len, size-i)) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+squat_build_word(struct squat_trie_build_context *ctx, uint32_t uid,
+ const unsigned char *data, const uint8_t *char_lengths,
+ unsigned int size)
+{
+ struct squat_trie *trie = ctx->trie;
+ unsigned int i, j, bytelen;
+
+ if (char_lengths == NULL) {
+ /* optimization path: all characters are bytes */
+ return squat_build_word_bytes(ctx, uid, data, size);
+ }
+
+ if (trie->hdr.full_len <= trie->hdr.partial_len)
+ i = 0;
+ else {
+ /* the first word is longer than others */
+ bytelen = 0;
+ for (j = 0; j < trie->hdr.full_len && bytelen < size; j++)
+ bytelen += char_lengths[bytelen];
+ i_assert(bytelen <= size);
+
+ if (squat_build_add(ctx, uid, data, bytelen) < 0)
+ return -1;
+ i = char_lengths[0];
+ }
+
+ for (; i < size; i += char_lengths[i]) {
+ bytelen = 0;
+ for (j = 0; j < trie->hdr.partial_len && i+bytelen < size; j++)
+ bytelen += char_lengths[i + bytelen];
+ i_assert(i + bytelen <= size);
+
+ if (squat_build_add(ctx, uid, data + i, bytelen) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static unsigned char *
+squat_data_normalize(struct squat_trie *trie, const unsigned char *data,
+ unsigned int size)
+{
+ static const unsigned char replacement_utf8[] = { 0xef, 0xbf, 0xbd };
+ unsigned char *dest;
+ unsigned int i;
+
+ dest = t_malloc_no0(size);
+ for (i = 0; i < size; i++) {
+ if (data[i] == replacement_utf8[0] && i + 2 < size &&
+ data[i+1] == replacement_utf8[1] &&
+ data[i+2] == replacement_utf8[2]) {
+ /* Don't index replacement character */
+ dest[i++] = 0;
+ dest[i++] = 0;
+ dest[i] = 0;
+ } else {
+ dest[i] = trie->hdr.normalize_map[data[i]];
+ }
+ }
+ return dest;
+}
+
+static int
+squat_trie_build_more_real(struct squat_trie_build_context *ctx,
+ uint32_t uid, enum squat_index_type type,
+ const unsigned char *input, unsigned int size)
+{
+ struct squat_trie *trie = ctx->trie;
+ const unsigned char *data;
+ uint8_t *char_lengths;
+ unsigned int i, start = 0;
+ bool multibyte_chars = FALSE;
+ int ret = 0;
+
+ uid = uid * 2 + (type == SQUAT_INDEX_TYPE_HEADER ? 1 : 0);
+
+ char_lengths = t_malloc_no0(size);
+ data = squat_data_normalize(trie, input, size);
+ for (i = 0; i < size; i++) {
+ char_lengths[i] = uni_utf8_char_bytes(input[i]);
+ if (char_lengths[i] != 1)
+ multibyte_chars = TRUE;
+ if (data[i] != '\0')
+ continue;
+
+ while (start < i && data[start] == '\0')
+ start++;
+ if (i != start) {
+ if (squat_build_word(ctx, uid, data + start,
+ !multibyte_chars ? NULL :
+ char_lengths + start,
+ i - start) < 0) {
+ ret = -1;
+ start = i;
+ break;
+ }
+ }
+ start = i + 1;
+ }
+ while (start < i && data[start] == '\0')
+ start++;
+ if (i != start) {
+ if (squat_build_word(ctx, uid, data + start,
+ !multibyte_chars ? NULL :
+ char_lengths + start, i - start) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+int squat_trie_build_more(struct squat_trie_build_context *ctx,
+ uint32_t uid, enum squat_index_type type,
+ const unsigned char *input, unsigned int size)
+{
+ int ret = 0;
+
+ if (size != 0) T_BEGIN {
+ ret = squat_trie_build_more_real(ctx, uid, type, input, size);
+ } T_END;
+ return ret;
+}
+
+static void
+node_drop_unused_children(struct squat_trie *trie, struct squat_node *node)
+{
+ unsigned char *chars;
+ struct squat_node *children_src, *children_dest;
+ unsigned int i, j, orig_child_count = node->child_count;
+
+ chars = NODE_CHILDREN_CHARS(node);
+ children_src = NODE_CHILDREN_NODES(node);
+
+ /* move chars */
+ for (i = j = 0; i < orig_child_count; i++) {
+ if (children_src[i].next_uid != 0)
+ chars[j++] = chars[i];
+ }
+ node->child_count = j;
+
+ /* move children. note that children_dest may point to different
+ location than children_src, although they both point to the
+ same node. */
+ children_dest = NODE_CHILDREN_NODES(node);
+ for (i = j = 0; i < orig_child_count; i++) {
+ if (children_src[i].next_uid != 0)
+ children_dest[j++] = children_src[i];
+ else
+ node_free(trie, &children_src[i]);
+ }
+}
+
+static int
+squat_write_node(struct squat_trie_build_context *ctx, struct squat_node *node,
+ uoff_t *node_offset_r, int level)
+{
+ struct squat_trie *trie = ctx->trie;
+ struct squat_node *children;
+ unsigned int i;
+ uoff_t *node_offsets;
+ uint8_t child_count;
+ int ret;
+
+ i_assert(node->next_uid != 0);
+
+ if (node->children_not_mapped && ctx->compress_nodes) {
+ if (node_read_children(trie, node, MAX_FAST_LEVEL) < 0)
+ return -1;
+ }
+
+ node->have_sequential = FALSE;
+ node_drop_unused_children(trie, node);
+
+ child_count = node->child_count;
+ if (child_count == 0) {
+ i_assert(!node->children_not_mapped ||
+ node->leaf_string_length == 0);
+ *node_offset_r = !node->children_not_mapped ? 0 :
+ node->children.offset;
+ return 0;
+ }
+ i_assert(!node->children_not_mapped);
+
+ trie->hdr.node_count++;
+
+ children = NODE_CHILDREN_NODES(node);
+ node_offsets = t_new(uoff_t, child_count);
+ for (i = 0; i < child_count; i++) {
+ T_BEGIN {
+ ret = squat_write_node(ctx, &children[i],
+ &node_offsets[i], level + 1);
+ } T_END;
+ if (ret < 0)
+ return -1;
+ }
+
+ *node_offset_r = ctx->output->offset;
+ node_write_children(ctx, node, node_offsets);
+ return 0;
+}
+
+static int squat_write_nodes(struct squat_trie_build_context *ctx)
+{
+ struct squat_trie *trie = ctx->trie;
+ uoff_t node_offset;
+ int ret;
+
+ if (ctx->trie->root.next_uid == 0)
+ return 0;
+
+ T_BEGIN {
+ ret = squat_write_node(ctx, &ctx->trie->root, &node_offset, 0);
+ } T_END;
+ if (ret < 0)
+ return -1;
+
+ trie->hdr.root_offset = node_offset;
+ trie->hdr.root_unused_uids = trie->root.unused_uids;
+ trie->hdr.root_next_uid = trie->root.next_uid;
+ trie->hdr.root_uidlist_idx = trie->root.uid_list_idx;
+ return 0;
+}
+
+static struct squat_trie_iterate_context *
+squat_trie_iterate_init(struct squat_trie *trie)
+{
+ struct squat_trie_iterate_context *ctx;
+
+ ctx = i_new(struct squat_trie_iterate_context, 1);
+ ctx->trie = trie;
+ ctx->cur.node = &trie->root;
+ i_array_init(&ctx->parents, trie->hdr.partial_len*2);
+ return ctx;
+}
+
+static int
+squat_trie_iterate_deinit(struct squat_trie_iterate_context *ctx)
+{
+ struct squat_trie_iterate_node *node;
+ int ret = ctx->failed ? -1 : 0;
+
+ if (array_is_created(&ctx->cur.shifts)) {
+ array_foreach_modifiable(&ctx->parents, node)
+ array_free(&node->shifts);
+ array_free(&ctx->cur.shifts);
+ }
+ array_free(&ctx->parents);
+ i_free(ctx);
+ return ret;
+}
+
+static struct squat_node *
+squat_trie_iterate_first(struct squat_trie_iterate_context *ctx)
+{
+ if (ctx->cur.node->children_not_mapped) {
+ if (node_read_children(ctx->trie, ctx->cur.node, 1) < 0) {
+ ctx->failed = TRUE;
+ return NULL;
+ }
+ }
+ return ctx->cur.node;
+}
+
+static struct squat_node *
+squat_trie_iterate_next(struct squat_trie_iterate_context *ctx,
+ ARRAY_TYPE(seq_range) *shifts_r)
+{
+ struct squat_trie_iterate_node *iter_nodes;
+ struct squat_node *children;
+ unsigned int count, shift_count = 0;
+
+ while (ctx->cur.idx == ctx->cur.node->child_count ||
+ ctx->cur.node->uid_list_idx == 0)
+ {
+ iter_nodes = array_get_modifiable(&ctx->parents, &count);
+ if (count == 0)
+ return NULL;
+
+ if (array_is_created(&ctx->cur.shifts))
+ array_free(&ctx->cur.shifts);
+ ctx->cur = iter_nodes[count-1];
+ array_delete(&ctx->parents, count-1, 1);
+ }
+
+ *shifts_r = ctx->cur.shifts;
+ if (array_is_created(&ctx->cur.shifts))
+ shift_count = array_count(&ctx->cur.shifts);
+
+ children = NODE_CHILDREN_NODES(ctx->cur.node);
+ while (children[ctx->cur.idx++].uid_list_idx == 0) {
+ if (ctx->cur.idx == ctx->cur.node->child_count) {
+ /* no more non-empty children in this node */
+ return squat_trie_iterate_next(ctx, shifts_r);
+ }
+ }
+ array_push_back(&ctx->parents, &ctx->cur);
+ ctx->cur.node = &children[ctx->cur.idx-1];
+ ctx->cur.idx = 0;
+ if (shift_count != 0)
+ i_array_init(&ctx->cur.shifts, shift_count);
+ else
+ i_zero(&ctx->cur.shifts);
+ return squat_trie_iterate_first(ctx);
+}
+
+static void
+squat_uidlist_update_expunged_uids(const ARRAY_TYPE(seq_range) *shifts_arr,
+ ARRAY_TYPE(seq_range) *child_shifts,
+ ARRAY_TYPE(seq_range) *uids_arr,
+ struct squat_trie *trie,
+ struct squat_node *node, bool do_shifts)
+{
+ const struct seq_range *shifts;
+ struct seq_range *uids, shift;
+ unsigned int i, uid_idx, uid_count, shift_count;
+ uint32_t child_shift_seq1, child_shift_count, seq_high;
+ unsigned int shift_sum = 0, child_sum = 0;
+
+ if (!array_is_created(shifts_arr)) {
+ i_assert(node->uid_list_idx != 0 || node->child_count == 0);
+ return;
+ }
+
+ /* we'll recalculate this */
+ node->unused_uids = 0;
+
+ uids = array_get_modifiable(uids_arr, &uid_count);
+ shifts = array_get(shifts_arr, &shift_count);
+ for (i = 0, uid_idx = 0, seq_high = 0;; ) {
+ /* skip UID ranges until we skip/overlap shifts */
+ while (uid_idx < uid_count &&
+ (i == shift_count ||
+ I_MAX(shifts[i].seq1, seq_high) > uids[uid_idx].seq2))
+ {
+ i_assert(uids[uid_idx].seq1 >= shift_sum);
+ uids[uid_idx].seq1 -= shift_sum;
+ uids[uid_idx].seq2 -= shift_sum;
+ child_sum += uids[uid_idx].seq2 -
+ uids[uid_idx].seq1 + 1;
+
+ if (uid_idx > 0 &&
+ uids[uid_idx-1].seq2 >= uids[uid_idx].seq1 - 1) {
+ /* we can merge this and the previous range */
+ i_assert(uids[uid_idx-1].seq2 ==
+ uids[uid_idx].seq1 - 1);
+ uids[uid_idx-1].seq2 = uids[uid_idx].seq2;
+ array_delete(uids_arr, uid_idx, 1);
+ uids = array_get_modifiable(uids_arr,
+ &uid_count);
+ } else {
+ if (uid_idx == 0)
+ node->unused_uids += uids[0].seq1;
+ else {
+ node->unused_uids +=
+ uids[uid_idx].seq1 -
+ uids[uid_idx-1].seq2 - 1;
+ }
+ uid_idx++;
+ }
+ }
+ if (uid_idx == uid_count)
+ break;
+
+ shift.seq1 = I_MAX(shifts[i].seq1, seq_high);
+ shift.seq2 = shifts[i].seq2;
+ if (shift.seq2 < uids[uid_idx].seq1) {
+ /* shift is entirely before UID range */
+ shift_sum += shift.seq2 - shift.seq1 + 1;
+ i++;
+ } else {
+ /* handle shifts before UID range */
+ if (shift.seq1 < uids[uid_idx].seq1) {
+ shift_sum += uids[uid_idx].seq1 - shift.seq1;
+ shift.seq1 = uids[uid_idx].seq1;
+ }
+ /* update child shifts */
+ child_shift_seq1 = child_sum +
+ shift.seq1 - uids[uid_idx].seq1;
+ child_shift_count =
+ I_MIN(shift.seq2, uids[uid_idx].seq2) -
+ shift.seq1 + 1;
+ seq_range_array_add_range(child_shifts,
+ child_shift_seq1,
+ child_shift_seq1 +
+ child_shift_count - 1);
+ child_sum += child_shift_count;
+
+ /* if the shifts continue after the UID range,
+ treat it in the next loop iteration */
+ if (shift.seq2 <= uids[uid_idx].seq2)
+ i++;
+ else
+ seq_high = uids[uid_idx].seq2 + 1;
+
+ /* update UIDs - no matter where within the UID range
+ the shifts happened, the result is the same:
+ shift number of UIDs are removed, and the rest
+ are decreased by shift_sum.
+
+ 123 uids child_shifts
+ a) s -> 12 1
+ b) s -> 12 2
+ c) s -> 12 3
+ */
+ if (uids[uid_idx].seq1 +
+ child_shift_count > uids[uid_idx].seq2) {
+ /* removed completely */
+ array_delete(uids_arr, uid_idx, 1);
+ uids = array_get_modifiable(uids_arr,
+ &uid_count);
+ } else if (do_shifts) {
+ /* the next loop iteration fixes the UIDs */
+ uids[uid_idx].seq1 += child_shift_count;
+ } else {
+ seq_range_array_remove_range(uids_arr,
+ shift.seq1,
+ I_MIN(shift.seq2, uids[uid_idx].seq2));
+ uids = array_get_modifiable(uids_arr,
+ &uid_count);
+ }
+ shift_sum += child_shift_count;
+ }
+ if (!do_shifts) {
+ /* root node - UIDs are only removed, not shifted */
+ shift_sum = 0;
+ }
+ }
+
+ if (uid_count == 0) {
+ /* no UIDs left, delete the node's children and mark it
+ unused */
+ if (!NODE_IS_DYNAMIC_LEAF(node))
+ node_free(trie, node);
+
+ node->child_count = 0;
+ node->have_sequential = FALSE;
+ node->next_uid = 0;
+ } else {
+ if (do_shifts)
+ node->next_uid = uids[uid_count-1].seq2 + 1;
+ else {
+ node->unused_uids += (node->next_uid - 1) -
+ uids[uid_count-1].seq2;
+ }
+ }
+}
+
+static int
+squat_trie_expunge_uidlists(struct squat_trie_build_context *ctx,
+ struct squat_uidlist_rebuild_context *rebuild_ctx,
+ struct squat_trie_iterate_context *iter,
+ const ARRAY_TYPE(seq_range) *expunged_uids)
+{
+ struct squat_node *node;
+ ARRAY_TYPE(seq_range) uid_range, root_shifts, shifts;
+ bool shift = FALSE;
+ int ret = 0;
+
+ node = squat_trie_iterate_first(iter);
+ if (node->uid_list_idx == 0)
+ return 0;
+
+ i_array_init(&uid_range, 1024);
+ i_array_init(&root_shifts, array_count(expunged_uids));
+ array_append_array(&root_shifts, expunged_uids);
+
+ if (array_count(expunged_uids) > 0)
+ i_array_init(&iter->cur.shifts, array_count(expunged_uids));
+
+ shifts = root_shifts;
+ do {
+ i_assert(node->uid_list_idx != 0);
+ array_clear(&uid_range);
+ if (squat_uidlist_get_seqrange(ctx->trie->uidlist,
+ node->uid_list_idx,
+ &uid_range) < 0) {
+ ret = -1;
+ break;
+ }
+ squat_uidlist_update_expunged_uids(&shifts, &iter->cur.shifts,
+ &uid_range, ctx->trie, node,
+ shift);
+ node->uid_list_idx =
+ squat_uidlist_rebuild_nextu(rebuild_ctx, &uid_range);
+ i_assert(node->uid_list_idx != 0 || node->next_uid == 0);
+
+ node = squat_trie_iterate_next(iter, &shifts);
+ shift = TRUE;
+ } while (node != NULL);
+ array_free(&uid_range);
+ array_free(&root_shifts);
+ return ret;
+}
+
+static int
+squat_trie_renumber_uidlists2(struct squat_trie_build_context *ctx,
+ struct squat_uidlist_rebuild_context *rebuild_ctx,
+ struct squat_trie_iterate_context *iter)
+{
+ struct squat_node *node;
+ ARRAY_TYPE(seq_range) shifts;
+ ARRAY_TYPE(uint32_t) uids;
+ int ret = 0;
+
+ node = squat_trie_iterate_first(iter);
+ if (node->uid_list_idx == 0)
+ return 0;
+
+ i_array_init(&uids, 1024);
+ while (node != NULL) {
+ i_assert(node->uid_list_idx != 0);
+ if (!UIDLIST_IS_SINGLETON(node->uid_list_idx)) {
+ /* rebuild the uidlist */
+ array_clear(&uids);
+ if (squat_uidlist_get(ctx->trie->uidlist,
+ node->uid_list_idx, &uids) < 0) {
+ ret = -1;
+ break;
+ }
+ node->uid_list_idx =
+ squat_uidlist_rebuild_next(rebuild_ctx, &uids);
+ }
+ node = squat_trie_iterate_next(iter, &shifts);
+ }
+ array_free(&uids);
+ return ret;
+}
+
+static int
+squat_trie_renumber_uidlists(struct squat_trie_build_context *ctx,
+ const ARRAY_TYPE(seq_range) *expunged_uids,
+ bool compress)
+{
+ struct squat_trie_iterate_context *iter;
+ struct squat_uidlist_rebuild_context *rebuild_ctx;
+ time_t now;
+ int ret = 0;
+
+ if ((ret = squat_uidlist_rebuild_init(ctx->uidlist_build_ctx,
+ compress, &rebuild_ctx)) <= 0)
+ return ret;
+
+ now = time(NULL);
+ ctx->trie->hdr.indexid =
+ I_MAX((unsigned int)now, ctx->trie->hdr.indexid + 1);
+
+ iter = squat_trie_iterate_init(ctx->trie);
+ if (expunged_uids != NULL) {
+ ret = squat_trie_expunge_uidlists(ctx, rebuild_ctx, iter,
+ expunged_uids);
+ } else {
+ ret = squat_trie_renumber_uidlists2(ctx, rebuild_ctx, iter);
+ }
+ if (squat_trie_iterate_deinit(iter) < 0)
+ ret = -1;
+
+ /* lock the trie before we rename uidlist */
+ i_assert(ctx->file_lock == NULL && ctx->dotlock == NULL);
+ if (squat_trie_lock(ctx->trie, F_WRLCK,
+ &ctx->file_lock, &ctx->dotlock) <= 0)
+ ret = -1;
+ return squat_uidlist_rebuild_finish(rebuild_ctx, ret < 0);
+}
+
+static bool squat_trie_check_header(struct squat_trie *trie)
+{
+ if (trie->hdr.version != SQUAT_TRIE_VERSION ||
+ trie->hdr.uidvalidity != trie->uidvalidity)
+ return FALSE;
+
+ if (trie->hdr.partial_len > trie->hdr.full_len) {
+ i_error("Corrupted %s: partial len > full len", trie->path);
+ return FALSE;
+ }
+ if (trie->hdr.full_len == 0) {
+ i_error("Corrupted %s: full len=0", trie->path);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int squat_trie_map_header(struct squat_trie *trie)
+{
+ int ret;
+
+ if (trie->locked_file_size == 0) {
+ /* newly created file */
+ squat_trie_header_init(trie);
+ return 1;
+ }
+ i_assert(trie->fd != -1);
+
+ if ((trie->flags & SQUAT_INDEX_FLAG_MMAP_DISABLE) != 0) {
+ ret = pread_full(trie->fd, &trie->hdr, sizeof(trie->hdr), 0);
+ if (ret <= 0) {
+ if (ret < 0) {
+ i_error("pread(%s) failed: %m", trie->path);
+ return -1;
+ }
+ i_error("Corrupted %s: File too small", trie->path);
+ return 0;
+ }
+ trie->data = NULL;
+ trie->data_size = 0;
+ } else {
+ if (trie->locked_file_size < sizeof(trie->hdr)) {
+ i_error("Corrupted %s: File too small", trie->path);
+ return 0;
+ }
+ if (trie->mmap_size != 0) {
+ if (munmap(trie->mmap_base, trie->mmap_size) < 0)
+ i_error("munmap(%s) failed: %m", trie->path);
+ }
+
+ trie->mmap_size = trie->locked_file_size;
+ trie->mmap_base = mmap(NULL, trie->mmap_size,
+ PROT_READ | PROT_WRITE,
+ MAP_SHARED, trie->fd, 0);
+ if (trie->mmap_base == MAP_FAILED) {
+ trie->data = trie->mmap_base = NULL;
+ trie->data_size = trie->mmap_size = 0;
+ i_error("mmap(%s) failed: %m", trie->path);
+ return -1;
+ }
+ memcpy(&trie->hdr, trie->mmap_base, sizeof(trie->hdr));
+ trie->data = trie->mmap_base;
+ trie->data_size = trie->mmap_size;
+ }
+
+ return squat_trie_check_header(trie) ? 1 : 0;
+}
+
+static int squat_trie_map(struct squat_trie *trie, bool building)
+{
+ struct file_lock *file_lock = NULL;
+ struct dotlock *dotlock = NULL;
+ bool changed;
+ int ret;
+
+ if (trie->fd != -1) {
+ if (squat_trie_lock(trie, F_RDLCK, &file_lock, &dotlock) <= 0)
+ return -1;
+ if ((trie->flags & SQUAT_INDEX_FLAG_MMAP_DISABLE) != 0 &&
+ trie->file_cache == NULL)
+ trie->file_cache = file_cache_new_path(trie->fd, trie->path);
+ }
+
+ ret = squat_trie_map_header(trie);
+ if (ret == 0) {
+ if (file_lock != NULL)
+ file_unlock(&file_lock);
+ else
+ file_dotlock_delete(&dotlock);
+ squat_trie_delete(trie);
+ squat_trie_close(trie);
+ squat_trie_header_init(trie);
+ }
+ changed = trie->root.children.offset != trie->hdr.root_offset;
+
+ if (changed || trie->hdr.root_offset == 0) {
+ node_free(trie, &trie->root);
+ i_zero(&trie->root);
+ trie->root.want_sequential = TRUE;
+ trie->root.unused_uids = trie->hdr.root_unused_uids;
+ trie->root.next_uid = trie->hdr.root_next_uid;
+ trie->root.uid_list_idx = trie->hdr.root_uidlist_idx;
+ trie->root.children.offset = trie->hdr.root_offset;
+
+ if (trie->hdr.root_offset == 0) {
+ trie->unmapped_child_count = 0;
+ trie->root.children_not_mapped = FALSE;
+ } else {
+ trie->unmapped_child_count = 1;
+ trie->root.children_not_mapped = TRUE;
+ }
+ }
+
+ if (ret >= 0 && !building) {
+ /* do this while we're still locked */
+ ret = squat_uidlist_refresh(trie->uidlist);
+ }
+
+ if (file_lock != NULL)
+ file_unlock(&file_lock);
+ if (dotlock != NULL)
+ file_dotlock_delete(&dotlock);
+ if (ret < 0)
+ return -1;
+
+ return trie->hdr.root_offset == 0 || !changed ? 0 :
+ node_read_children(trie, &trie->root, 1);
+}
+
+int squat_trie_create_fd(struct squat_trie *trie, const char *path, int flags)
+{
+ mode_t old_mask;
+ int fd;
+
+ old_mask = umask(0);
+ fd = open(path, O_RDWR | O_CREAT | flags, trie->create_mode);
+ umask(old_mask);
+ if (fd == -1) {
+ i_error("creat(%s) failed: %m", path);
+ return -1;
+ }
+ if (trie->create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, trie->create_gid) < 0) {
+ i_error("fchown(%s, -1, %ld) failed: %m",
+ path, (long)trie->create_gid);
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+ return fd;
+}
+
+int squat_trie_build_init(struct squat_trie *trie,
+ struct squat_trie_build_context **ctx_r)
+{
+ struct squat_trie_build_context *ctx;
+ struct squat_uidlist_build_context *uidlist_build_ctx;
+
+ if (trie->fd == -1) {
+ trie->fd = squat_trie_create_fd(trie, trie->path, 0);
+ if (trie->fd == -1)
+ return -1;
+
+ if (trie->file_cache != NULL)
+ file_cache_set_fd(trie->file_cache, trie->fd);
+ i_assert(trie->locked_file_size == 0);
+ }
+
+ /* uidlist locks building */
+ if (squat_uidlist_build_init(trie->uidlist, &uidlist_build_ctx) < 0)
+ return -1;
+
+ if (squat_trie_map(trie, TRUE) < 0) {
+ squat_uidlist_build_deinit(&uidlist_build_ctx);
+ return -1;
+ }
+
+ ctx = i_new(struct squat_trie_build_context, 1);
+ ctx->trie = trie;
+ ctx->uidlist_build_ctx = uidlist_build_ctx;
+ ctx->first_uid = trie->root.next_uid;
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+static int squat_trie_write_lock(struct squat_trie_build_context *ctx)
+{
+ if (ctx->file_lock != NULL || ctx->dotlock != NULL)
+ return 0;
+
+ if (squat_trie_lock(ctx->trie, F_WRLCK,
+ &ctx->file_lock, &ctx->dotlock) <= 0)
+ return -1;
+ return 0;
+}
+
+static int squat_trie_write(struct squat_trie_build_context *ctx)
+{
+ struct squat_trie *trie = ctx->trie;
+ struct file_lock *file_lock = NULL;
+ struct ostream *output;
+ const char *path, *error;
+ int fd = -1, ret = 0;
+
+ if ((trie->hdr.used_file_size > sizeof(trie->hdr) &&
+ trie->unmapped_child_count < trie->hdr.node_count/4) || 1) {
+ /* we might as well recreate the file */
+ ctx->compress_nodes = TRUE;
+
+ path = t_strconcat(trie->path, ".tmp", NULL);
+ fd = squat_trie_create_fd(trie, path, O_TRUNC);
+ if (fd == -1)
+ return -1;
+
+ if (trie->lock_method != FILE_LOCK_METHOD_DOTLOCK) {
+ struct file_lock_settings lock_set = {
+ .lock_method = trie->lock_method,
+ };
+ ret = file_wait_lock(fd, path, F_WRLCK, &lock_set,
+ SQUAT_TRIE_LOCK_TIMEOUT,
+ &file_lock, &error);
+ if (ret <= 0) {
+ i_error("file_wait_lock(%s) failed: %s",
+ path, error);
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+
+ output = o_stream_create_fd(fd, 0);
+ o_stream_cork(output);
+ o_stream_nsend(output, &trie->hdr, sizeof(trie->hdr));
+ } else {
+ /* we need to lock only while header is being written */
+ path = trie->path;
+ ctx->compress_nodes =
+ trie->hdr.used_file_size == sizeof(trie->hdr);
+
+ if (trie->hdr.used_file_size == 0) {
+ /* lock before opening the file, in case we reopen it */
+ if (squat_trie_write_lock(ctx) < 0)
+ return -1;
+ }
+ output = o_stream_create_fd(trie->fd, 0);
+ o_stream_cork(output);
+
+ if (trie->hdr.used_file_size != 0)
+ (void)o_stream_seek(output, trie->hdr.used_file_size);
+ else
+ o_stream_nsend(output, &trie->hdr, sizeof(trie->hdr));
+ }
+
+ ctx->output = output;
+ ret = squat_write_nodes(ctx);
+ ctx->output = NULL;
+
+ /* write 1 byte guard at the end of file, so that we can verify broken
+ squat_unpack_num() input by checking if data==end */
+ o_stream_nsend(output, "", 1);
+
+ if (trie->corrupted)
+ ret = -1;
+ if (ret == 0)
+ ret = squat_trie_write_lock(ctx);
+ if (ret == 0) {
+ trie->hdr.used_file_size = output->offset;
+ (void)o_stream_seek(output, 0);
+ o_stream_nsend(output, &trie->hdr, sizeof(trie->hdr));
+ }
+ if (o_stream_finish(output) < 0) {
+ i_error("write(%s) failed: %s", path,
+ o_stream_get_error(output));
+ ret = -1;
+ }
+ o_stream_destroy(&output);
+
+ if (fd == -1) {
+ /* appended to the existing file */
+ i_assert(file_lock == NULL);
+ return ret;
+ }
+
+ /* recreating the trie file */
+ if (ret < 0) {
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", path);
+ fd = -1;
+ } else if (rename(path, trie->path) < 0) {
+ i_error("rename(%s, %s) failed: %m", path, trie->path);
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ i_unlink_if_exists(path);
+ file_lock_free(&file_lock);
+ } else {
+ squat_trie_close_fd(trie);
+ trie->fd = fd;
+ trie->locked_file_size = trie->hdr.used_file_size;
+ if (trie->file_cache != NULL)
+ file_cache_set_fd(trie->file_cache, trie->fd);
+
+ file_lock_free(&ctx->file_lock);
+ ctx->file_lock = file_lock;
+ }
+ return ret;
+}
+
+int squat_trie_build_deinit(struct squat_trie_build_context **_ctx,
+ const ARRAY_TYPE(seq_range) *expunged_uids)
+{
+ struct squat_trie_build_context *ctx = *_ctx;
+ bool compress, unlock = TRUE;
+ int ret;
+
+ *_ctx = NULL;
+
+ compress = (ctx->trie->root.next_uid - ctx->first_uid) > 10;
+
+ /* keep trie locked while header is being written and when files are
+ being renamed, so that while trie is read locked, uidlist can't
+ change under. */
+ squat_uidlist_build_flush(ctx->uidlist_build_ctx);
+ ret = squat_trie_renumber_uidlists(ctx, expunged_uids, compress);
+ if (ret == 0) {
+ ret = squat_trie_write(ctx);
+ if (ret < 0)
+ unlock = FALSE;
+ }
+
+ if (ret == 0)
+ ret = squat_uidlist_build_finish(ctx->uidlist_build_ctx);
+ if (ctx->file_lock != NULL) {
+ if (unlock)
+ file_unlock(&ctx->file_lock);
+ else
+ file_lock_free(&ctx->file_lock);
+ }
+ if (ctx->dotlock != NULL)
+ file_dotlock_delete(&ctx->dotlock);
+ squat_uidlist_build_deinit(&ctx->uidlist_build_ctx);
+
+ i_free(ctx);
+ return ret;
+}
+
+int squat_trie_get_last_uid(struct squat_trie *trie, uint32_t *last_uid_r)
+{
+ if (trie->fd == -1) {
+ if (squat_trie_open(trie) < 0)
+ return -1;
+ }
+
+ *last_uid_r = I_MAX((trie->root.next_uid+1)/2, 1) - 1;
+ return 0;
+}
+
+static int
+squat_trie_lookup_data(struct squat_trie *trie, const unsigned char *data,
+ unsigned int size, ARRAY_TYPE(seq_range) *uids)
+{
+ struct squat_node *node = &trie->root;
+ unsigned char *chars;
+ unsigned int idx;
+ int level = 0;
+
+ array_clear(uids);
+
+ for (;;) {
+ if (node->children_not_mapped) {
+ if (node_read_children(trie, node, level) < 0)
+ return -1;
+ }
+ if (node->leaf_string_length != 0) {
+ unsigned int len = node->leaf_string_length;
+ const unsigned char *str;
+
+ if (len > sizeof(node->children.static_leaf_string))
+ str = node->children.leaf_string;
+ else
+ str = node->children.static_leaf_string;
+
+ if (size > len || memcmp(data, str, size) != 0)
+ return 0;
+
+ /* match */
+ break;
+ }
+
+ if (size == 0)
+ break;
+ level++;
+
+ if (node->have_sequential) {
+ if (*data < SEQUENTIAL_COUNT) {
+ idx = *data;
+ goto found;
+ }
+ idx = SEQUENTIAL_COUNT;
+ } else {
+ idx = 0;
+ }
+ chars = NODE_CHILDREN_CHARS(node);
+ for (; idx < node->child_count; idx++) {
+ if (chars[idx] == *data)
+ goto found;
+ }
+ return 0;
+ found:
+ /* follow to children */
+ if (level == 1) {
+ /* root level, add all UIDs */
+ if (squat_uidlist_get_seqrange(trie->uidlist,
+ node->uid_list_idx,
+ uids) < 0)
+ return -1;
+ } else {
+ if (squat_uidlist_filter(trie->uidlist,
+ node->uid_list_idx, uids) < 0)
+ return -1;
+ }
+ data++;
+ size--;
+ node = NODE_CHILDREN_NODES(node) + idx;
+ }
+
+ if (squat_uidlist_filter(trie->uidlist, node->uid_list_idx, uids) < 0)
+ return -1;
+ return 1;
+}
+
+static void
+squat_trie_filter_type(enum squat_index_type type,
+ const ARRAY_TYPE(seq_range) *src,
+ ARRAY_TYPE(seq_range) *dest)
+{
+ const struct seq_range *src_range;
+ struct seq_range new_range;
+ unsigned int i, count, mask;
+ uint32_t next_seq, uid;
+
+ array_clear(dest);
+ src_range = array_get(src, &count);
+ if (count == 0)
+ return;
+
+ if ((type & SQUAT_INDEX_TYPE_HEADER) != 0 &&
+ (type & SQUAT_INDEX_TYPE_BODY) != 0) {
+ /* everything is fine, just fix the UIDs */
+ new_range.seq1 = src_range[0].seq1 / 2;
+ new_range.seq2 = src_range[0].seq2 / 2;
+ for (i = 1; i < count; i++) {
+ next_seq = src_range[i].seq1 / 2;
+ if (next_seq == new_range.seq2 + 1) {
+ /* we can continue the previous range */
+ } else {
+ array_push_back(dest, &new_range);
+ new_range.seq1 = src_range[i].seq1 / 2;
+ }
+ new_range.seq2 = src_range[i].seq2 / 2;
+ }
+ array_push_back(dest, &new_range);
+ return;
+ }
+
+ /* we'll have to drop either header or body UIDs */
+ mask = (type & SQUAT_INDEX_TYPE_HEADER) != 0 ? 1 : 0;
+ for (i = 0; i < count; i++) {
+ for (uid = src_range[i].seq1; uid <= src_range[i].seq2; uid++) {
+ if ((uid & 1) == mask)
+ seq_range_array_add(dest, uid/2);
+ }
+ }
+}
+
+struct squat_trie_lookup_context {
+ struct squat_trie *trie;
+ enum squat_index_type type;
+
+ ARRAY_TYPE(seq_range) *definite_uids, *maybe_uids;
+ ARRAY_TYPE(seq_range) tmp_uids, tmp_uids2;
+ bool first;
+};
+
+static int
+squat_trie_lookup_partial(struct squat_trie_lookup_context *ctx,
+ const unsigned char *data, uint8_t *char_lengths,
+ unsigned int size)
+{
+ const unsigned int partial_len = ctx->trie->hdr.partial_len;
+ unsigned int char_idx, max_chars, i, j, bytelen;
+ int ret;
+
+ for (i = 0, max_chars = 0; i < size; max_chars++)
+ i += char_lengths[i];
+ i_assert(max_chars > 0);
+
+ i = 0; char_idx = 0;
+ do {
+ bytelen = 0;
+ for (j = 0; j < partial_len && i+bytelen < size; j++)
+ bytelen += char_lengths[i + bytelen];
+
+ ret = squat_trie_lookup_data(ctx->trie, data + i, bytelen,
+ &ctx->tmp_uids);
+ if (ret <= 0) {
+ array_clear(ctx->maybe_uids);
+ return ret;
+ }
+
+ if (ctx->first) {
+ squat_trie_filter_type(ctx->type, &ctx->tmp_uids,
+ ctx->maybe_uids);
+ ctx->first = FALSE;
+ } else {
+ squat_trie_filter_type(ctx->type, &ctx->tmp_uids,
+ &ctx->tmp_uids2);
+ seq_range_array_intersect(ctx->maybe_uids,
+ &ctx->tmp_uids2);
+ }
+ i += char_lengths[i];
+ char_idx++;
+ } while (max_chars - char_idx >= partial_len);
+ return 1;
+}
+
+static void squat_trie_add_unknown(struct squat_trie *trie,
+ ARRAY_TYPE(seq_range) *maybe_uids)
+{
+ struct seq_range *range, new_range;
+ unsigned int count;
+ uint32_t last_uid;
+
+ last_uid = I_MAX((trie->root.next_uid+1)/2, 1) - 1;
+
+ range = array_get_modifiable(maybe_uids, &count);
+ if (count > 0 && range[count-1].seq2 == last_uid) {
+ /* increase the range */
+ range[count-1].seq2 = (uint32_t)-1;
+ } else {
+ new_range.seq1 = last_uid + 1;
+ new_range.seq2 = (uint32_t)-1;
+ array_push_back(maybe_uids, &new_range);
+ }
+}
+
+static int
+squat_trie_lookup_real(struct squat_trie *trie, const char *str,
+ enum squat_index_type type,
+ ARRAY_TYPE(seq_range) *definite_uids,
+ ARRAY_TYPE(seq_range) *maybe_uids)
+{
+ struct squat_trie_lookup_context ctx;
+ unsigned char *data;
+ uint8_t *char_lengths;
+ unsigned int i, start, bytes, str_bytelen, str_charlen;
+ bool searched = FALSE;
+ int ret = 0;
+
+ array_clear(definite_uids);
+ array_clear(maybe_uids);
+
+ i_zero(&ctx);
+ ctx.trie = trie;
+ ctx.type = type;
+ ctx.definite_uids = definite_uids;
+ ctx.maybe_uids = maybe_uids;
+ i_array_init(&ctx.tmp_uids, 128);
+ i_array_init(&ctx.tmp_uids2, 128);
+ ctx.first = TRUE;
+
+ str_bytelen = strlen(str);
+ char_lengths = str_bytelen == 0 ? NULL : t_malloc0(str_bytelen);
+ for (i = 0, str_charlen = 0; i < str_bytelen; str_charlen++) {
+ bytes = uni_utf8_char_bytes(str[i]);
+ char_lengths[i] = bytes;
+ i += bytes;
+ }
+ data = squat_data_normalize(trie, (const unsigned char *)str,
+ str_bytelen);
+
+ for (i = start = 0; i < str_bytelen && ret >= 0; i += char_lengths[i]) {
+ if (data[i] != '\0')
+ continue;
+
+ /* string has nonindexed characters.
+ search it in parts. */
+ if (i != start) {
+ ret = squat_trie_lookup_partial(&ctx, data + start,
+ char_lengths + start,
+ i - start);
+ searched = TRUE;
+ }
+ start = i + char_lengths[i];
+ }
+
+ if (start == 0) {
+ if (str_charlen <= trie->hdr.partial_len ||
+ trie->hdr.full_len > trie->hdr.partial_len) {
+ ret = squat_trie_lookup_data(trie, data, str_bytelen,
+ &ctx.tmp_uids);
+ if (ret > 0) {
+ squat_trie_filter_type(type, &ctx.tmp_uids,
+ definite_uids);
+ }
+ } else {
+ array_clear(definite_uids);
+ }
+
+ if (str_charlen <= trie->hdr.partial_len ||
+ trie->hdr.partial_len == 0) {
+ /* we have the result */
+ array_clear(maybe_uids);
+ } else {
+ ret = squat_trie_lookup_partial(&ctx, data + start,
+ char_lengths + start,
+ i - start);
+ }
+ } else if (str_bytelen > 0) {
+ /* string has nonindexed characters. finish the search. */
+ array_clear(definite_uids);
+ if (i != start && ret >= 0) {
+ ret = squat_trie_lookup_partial(&ctx, data + start,
+ char_lengths + start,
+ i - start);
+ } else if (!searched) {
+ /* string has only nonindexed chars,
+ list all root UIDs as maybes */
+ ret = squat_uidlist_get_seqrange(trie->uidlist,
+ trie->root.uid_list_idx,
+ &ctx.tmp_uids);
+ squat_trie_filter_type(type, &ctx.tmp_uids,
+ maybe_uids);
+ }
+ } else {
+ /* zero string length - list all root UIDs as definite
+ answers */
+#if 0 /* FIXME: this code is never actually reached now. */
+ ret = squat_uidlist_get_seqrange(trie->uidlist,
+ trie->root.uid_list_idx,
+ &ctx.tmp_uids);
+ squat_trie_filter_type(type, &ctx.tmp_uids,
+ definite_uids);
+#else
+ i_unreached();
+#endif
+ }
+ seq_range_array_remove_seq_range(maybe_uids, definite_uids);
+ squat_trie_add_unknown(trie, maybe_uids);
+ array_free(&ctx.tmp_uids);
+ array_free(&ctx.tmp_uids2);
+ return ret < 0 ? -1 : 0;
+}
+
+int squat_trie_lookup(struct squat_trie *trie, const char *str,
+ enum squat_index_type type,
+ ARRAY_TYPE(seq_range) *definite_uids,
+ ARRAY_TYPE(seq_range) *maybe_uids)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = squat_trie_lookup_real(trie, str, type,
+ definite_uids, maybe_uids);
+ } T_END;
+ return ret;
+}
+
+struct squat_uidlist *squat_trie_get_uidlist(struct squat_trie *trie)
+{
+ return trie->uidlist;
+}
+
+size_t squat_trie_mem_used(struct squat_trie *trie, unsigned int *count_r)
+{
+ *count_r = trie->hdr.node_count;
+ return trie->node_alloc_size;
+}
diff --git a/src/plugins/fts-squat/squat-trie.h b/src/plugins/fts-squat/squat-trie.h
new file mode 100644
index 0000000..91530b8
--- /dev/null
+++ b/src/plugins/fts-squat/squat-trie.h
@@ -0,0 +1,54 @@
+#ifndef SQUAT_TRIE_H
+#define SQUAT_TRIE_H
+
+#include "file-lock.h"
+#include "seq-range-array.h"
+
+enum squat_index_flags {
+ SQUAT_INDEX_FLAG_MMAP_DISABLE = 0x01,
+ SQUAT_INDEX_FLAG_NFS_FLUSH = 0x02,
+ SQUAT_INDEX_FLAG_DOTLOCK_USE_EXCL = 0x04
+};
+
+enum squat_index_type {
+ SQUAT_INDEX_TYPE_HEADER = 0x01,
+ SQUAT_INDEX_TYPE_BODY = 0x02
+};
+
+struct squat_trie_build_context;
+
+struct squat_trie *
+squat_trie_init(const char *path, uint32_t uidvalidity,
+ enum file_lock_method lock_method,
+ enum squat_index_flags flags, mode_t mode, gid_t gid);
+void squat_trie_deinit(struct squat_trie **trie);
+
+void squat_trie_set_partial_len(struct squat_trie *trie, unsigned int len);
+void squat_trie_set_full_len(struct squat_trie *trie, unsigned int len);
+
+int squat_trie_open(struct squat_trie *trie);
+int squat_trie_refresh(struct squat_trie *trie);
+
+int squat_trie_build_init(struct squat_trie *trie,
+ struct squat_trie_build_context **ctx_r);
+/* bodies must be added before headers */
+int squat_trie_build_more(struct squat_trie_build_context *ctx,
+ uint32_t uid, enum squat_index_type type,
+ const unsigned char *data, unsigned int size);
+/* if expunged_uids is non-NULL, they may be removed from the index if they
+ still exist. */
+int squat_trie_build_deinit(struct squat_trie_build_context **ctx,
+ const ARRAY_TYPE(seq_range) *expunged_uids)
+ ATTR_NULL(2);
+
+int squat_trie_get_last_uid(struct squat_trie *trie, uint32_t *last_uid_r);
+/* type specifies if we're looking at header, body or both */
+int squat_trie_lookup(struct squat_trie *trie, const char *str,
+ enum squat_index_type type,
+ ARRAY_TYPE(seq_range) *definite_uids,
+ ARRAY_TYPE(seq_range) *maybe_uids);
+
+struct squat_uidlist *squat_trie_get_uidlist(struct squat_trie *trie);
+size_t squat_trie_mem_used(struct squat_trie *trie, unsigned int *count_r);
+
+#endif
diff --git a/src/plugins/fts-squat/squat-uidlist.c b/src/plugins/fts-squat/squat-uidlist.c
new file mode 100644
index 0000000..facb8d0
--- /dev/null
+++ b/src/plugins/fts-squat/squat-uidlist.c
@@ -0,0 +1,1624 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "sort.h"
+#include "bsearch-insert-pos.h"
+#include "file-cache.h"
+#include "file-lock.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "ostream.h"
+#include "mmap-util.h"
+#include "squat-trie-private.h"
+#include "squat-uidlist.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#define UIDLIST_LIST_SIZE 31
+#define UIDLIST_BLOCK_LIST_COUNT 100
+#define UID_LIST_MASK_RANGE 0x80000000U
+
+/* set = points to uidlist index number, unset = points to uidlist offset */
+#define UID_LIST_POINTER_MASK_LIST_IDX 0x80000000U
+
+#define UIDLIST_PACKED_FLAG_BITMASK 1
+#define UIDLIST_PACKED_FLAG_BEGINS_WITH_POINTER 2
+
+struct uidlist_list {
+ unsigned int uid_count:31;
+ bool uid_begins_with_pointer:1;
+ uint32_t uid_list[UIDLIST_LIST_SIZE];
+};
+
+struct squat_uidlist {
+ struct squat_trie *trie;
+
+ char *path;
+ int fd;
+ struct file_cache *file_cache;
+
+ struct file_lock *file_lock;
+ struct dotlock *dotlock;
+ uoff_t locked_file_size;
+
+ void *mmap_base;
+ size_t mmap_size;
+ struct squat_uidlist_file_header hdr;
+
+ const void *data;
+ size_t data_size;
+
+ unsigned int cur_block_count;
+ const uint32_t *cur_block_offsets;
+ const uint32_t *cur_block_end_indexes;
+
+ size_t max_size;
+ bool corrupted:1;
+ bool building:1;
+};
+
+struct squat_uidlist_build_context {
+ struct squat_uidlist *uidlist;
+ struct ostream *output;
+
+ ARRAY_TYPE(uint32_t) block_offsets;
+ ARRAY_TYPE(uint32_t) block_end_indexes;
+
+ ARRAY(struct uidlist_list) lists;
+ uint32_t list_start_idx;
+
+ struct squat_uidlist_file_header build_hdr;
+ bool need_reopen:1;
+};
+
+struct squat_uidlist_rebuild_context {
+ struct squat_uidlist *uidlist;
+ struct squat_uidlist_build_context *build_ctx;
+
+ int fd;
+ struct ostream *output;
+
+ ARRAY_TYPE(uint32_t) new_block_offsets, new_block_end_indexes;
+ uoff_t cur_block_start_offset;
+
+ uint32_t list_sizes[UIDLIST_BLOCK_LIST_COUNT];
+ uint32_t next_uid_list_idx;
+ unsigned int list_idx;
+ unsigned int new_count;
+};
+
+static void squat_uidlist_close(struct squat_uidlist *uidlist);
+
+void squat_uidlist_delete(struct squat_uidlist *uidlist)
+{
+ i_unlink_if_exists(uidlist->path);
+}
+
+static void squat_uidlist_set_corrupted(struct squat_uidlist *uidlist,
+ const char *reason)
+{
+ if (uidlist->corrupted)
+ return;
+ uidlist->corrupted = TRUE;
+
+ i_error("Corrupted squat uidlist file %s: %s", uidlist->path, reason);
+ squat_trie_delete(uidlist->trie);
+}
+
+static int
+uidlist_write_array(struct ostream *output, const uint32_t *uid_list,
+ unsigned int uid_count, uint32_t packed_flags,
+ uint32_t offset, bool write_size, uint32_t *size_r)
+{
+ uint8_t *uidbuf, *bufp, sizebuf[SQUAT_PACK_MAX_SIZE], *sizebufp;
+ uint8_t listbuf[SQUAT_PACK_MAX_SIZE], *listbufp = listbuf;
+ uint32_t uid, uid2, prev, base_uid, size_value;
+ unsigned int i, bitmask_len, uid_list_len;
+ unsigned int idx, max_idx, mask;
+ bool datastack;
+ int num;
+
+ if ((packed_flags & UIDLIST_PACKED_FLAG_BEGINS_WITH_POINTER) != 0)
+ squat_pack_num(&listbufp, offset);
+
+ /* @UNSAFE */
+ base_uid = uid_list[0] & ~UID_LIST_MASK_RANGE;
+ datastack = uid_count < 1024*8/SQUAT_PACK_MAX_SIZE;
+ if (datastack)
+ uidbuf = t_malloc_no0(SQUAT_PACK_MAX_SIZE * uid_count);
+ else
+ uidbuf = i_malloc(SQUAT_PACK_MAX_SIZE * uid_count);
+ bufp = uidbuf;
+ squat_pack_num(&bufp, base_uid);
+
+ bitmask_len = (uid_list[uid_count-1] - base_uid + 7) / 8 +
+ (bufp - uidbuf);
+ if (bitmask_len < uid_count) {
+ bitmask_build:
+ i_assert(bitmask_len < SQUAT_PACK_MAX_SIZE*uid_count);
+
+ memset(bufp, 0, bitmask_len - (bufp - uidbuf));
+ if ((uid_list[0] & UID_LIST_MASK_RANGE) == 0) {
+ i = 1;
+ uid = i == uid_count ? 0 : uid_list[i];
+ } else {
+ i = 0;
+ uid = uid_list[0] + 1;
+ }
+ base_uid++;
+
+ for (; i < uid_count; i++) {
+ i_assert((uid & ~UID_LIST_MASK_RANGE) >= base_uid);
+ if ((uid & UID_LIST_MASK_RANGE) == 0) {
+ uid -= base_uid;
+ uid2 = uid;
+ } else {
+ uid &= ~UID_LIST_MASK_RANGE;
+ uid -= base_uid;
+ uid2 = uid_list[i+1] - base_uid;
+ i++;
+ }
+
+ if (uid2 - uid < 3*8) {
+ for (; uid <= uid2; uid++)
+ bufp[uid / 8] |= 1 << (uid % 8);
+ } else {
+ /* first byte */
+ idx = uid / 8;
+ num = uid % 8;
+ if (num != 0) {
+ uid += 8 - num;
+ for (mask = 0; num < 8; num++)
+ mask |= 1 << num;
+ bufp[idx++] |= mask;
+ }
+
+ /* middle bytes */
+ num = uid2 % 8;
+ max_idx = idx + (uid2 - num - uid)/8;
+ for (; idx < max_idx; idx++, uid += 8)
+ bufp[idx] = 0xff;
+
+ /* last byte */
+ for (mask = 0; num >= 0; num--)
+ mask |= 1 << num;
+ bufp[idx] |= mask;
+ }
+ uid = i+1 == uid_count ? 0 : uid_list[i+1];
+ }
+ uid_list_len = bitmask_len;
+ packed_flags |= UIDLIST_PACKED_FLAG_BITMASK;
+ } else {
+ bufp = uidbuf;
+ prev = 0;
+ for (i = 0; i < uid_count; i++) {
+ uid = uid_list[i];
+ if (unlikely((uid & ~UID_LIST_MASK_RANGE) < prev)) {
+ if (!datastack)
+ i_free(uidbuf);
+ return -1;
+ }
+ if ((uid & UID_LIST_MASK_RANGE) == 0) {
+ squat_pack_num(&bufp, (uid - prev) << 1);
+ prev = uid + 1;
+ } else {
+ uid &= ~UID_LIST_MASK_RANGE;
+ squat_pack_num(&bufp, 1 | (uid - prev) << 1);
+ squat_pack_num(&bufp, uid_list[i+1] - uid - 1);
+ prev = uid_list[i+1] + 1;
+ i++;
+ }
+ }
+ uid_list_len = bufp - uidbuf;
+ if (uid_list_len > bitmask_len) {
+ bufp = uidbuf;
+ squat_pack_num(&bufp, base_uid);
+ goto bitmask_build;
+ }
+ }
+
+ size_value = ((uid_list_len +
+ (listbufp - listbuf)) << 2) | packed_flags;
+ if (write_size) {
+ sizebufp = sizebuf;
+ squat_pack_num(&sizebufp, size_value);
+ o_stream_nsend(output, sizebuf, sizebufp - sizebuf);
+ }
+ o_stream_nsend(output, listbuf, listbufp - listbuf);
+ o_stream_nsend(output, uidbuf, uid_list_len);
+ if (!datastack)
+ i_free(uidbuf);
+
+ *size_r = size_value;
+ return 0;
+}
+
+static int
+uidlist_write(struct ostream *output, const struct uidlist_list *list,
+ bool write_size, uint32_t *size_r)
+{
+ const uint32_t *uid_list = list->uid_list;
+ uint8_t buf[SQUAT_PACK_MAX_SIZE], *bufp;
+ uint32_t uid_count = list->uid_count;
+ uint32_t packed_flags = 0;
+ uint32_t offset = 0;
+ int ret;
+
+ if (list->uid_begins_with_pointer) {
+ /* continued UID list */
+ packed_flags |= UIDLIST_PACKED_FLAG_BEGINS_WITH_POINTER;
+ if ((uid_list[0] & UID_LIST_POINTER_MASK_LIST_IDX) != 0) {
+ offset = ((uid_list[0] & ~UID_LIST_POINTER_MASK_LIST_IDX) << 1) | 1;
+ if (list->uid_count == 1) {
+ bufp = buf;
+ squat_pack_num(&bufp, offset);
+ o_stream_nsend(output, buf, bufp - buf);
+ *size_r = (bufp - buf) << 2 | packed_flags;
+ return 0;
+ }
+ } else if (unlikely(output->offset <= uid_list[0])) {
+ i_assert(output->closed);
+ return -1;
+ } else {
+ i_assert(list->uid_count > 1);
+ offset = (output->offset - uid_list[0]) << 1;
+ }
+ uid_list++;
+ uid_count--;
+ }
+
+ T_BEGIN {
+ ret = uidlist_write_array(output, uid_list, uid_count,
+ packed_flags, offset,
+ write_size, size_r);
+ } T_END;
+ return ret;
+}
+
+static void squat_uidlist_map_blocks_set_pointers(struct squat_uidlist *uidlist)
+{
+ const void *base;
+ size_t end_index_size, end_size;
+
+ base = CONST_PTR_OFFSET(uidlist->data, uidlist->hdr.block_list_offset +
+ sizeof(uint32_t));
+
+ end_index_size = uidlist->cur_block_count * sizeof(uint32_t);
+ end_size = end_index_size + uidlist->cur_block_count * sizeof(uint32_t);
+ if (end_size <= uidlist->data_size) {
+ uidlist->cur_block_end_indexes = base;
+ uidlist->cur_block_offsets =
+ CONST_PTR_OFFSET(base, end_index_size);
+ } else {
+ uidlist->cur_block_end_indexes = NULL;
+ uidlist->cur_block_offsets = NULL;
+ }
+}
+
+static int uidlist_file_cache_read(struct squat_uidlist *uidlist,
+ size_t offset, size_t size)
+{
+ if (uidlist->file_cache == NULL)
+ return 0;
+
+ if (file_cache_read(uidlist->file_cache, offset, size) < 0) {
+ i_error("read(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ uidlist->data = file_cache_get_map(uidlist->file_cache,
+ &uidlist->data_size);
+ squat_uidlist_map_blocks_set_pointers(uidlist);
+ return 0;
+}
+
+static int squat_uidlist_map_blocks(struct squat_uidlist *uidlist)
+{
+ const struct squat_uidlist_file_header *hdr = &uidlist->hdr;
+ const void *base;
+ uint32_t block_count, blocks_offset, blocks_size, i, verify_count;
+
+ if (hdr->block_list_offset == 0) {
+ /* empty file */
+ uidlist->cur_block_count = 0;
+ return 1;
+ }
+
+ /* get number of blocks */
+ if (uidlist_file_cache_read(uidlist, hdr->block_list_offset,
+ sizeof(block_count)) < 0)
+ return -1;
+ blocks_offset = hdr->block_list_offset + sizeof(block_count);
+ if (blocks_offset > uidlist->data_size) {
+ squat_uidlist_set_corrupted(uidlist, "block list outside file");
+ return 0;
+ }
+
+ i_assert(uidlist->data != NULL);
+ base = CONST_PTR_OFFSET(uidlist->data, hdr->block_list_offset);
+ memcpy(&block_count, base, sizeof(block_count));
+
+ /* map the blocks */
+ blocks_size = block_count * sizeof(uint32_t)*2;
+ if (uidlist_file_cache_read(uidlist, blocks_offset, blocks_size) < 0)
+ return -1;
+ if (blocks_offset + blocks_size > uidlist->data_size) {
+ squat_uidlist_set_corrupted(uidlist, "block list outside file");
+ return 0;
+ }
+
+ uidlist->cur_block_count = block_count;
+ squat_uidlist_map_blocks_set_pointers(uidlist);
+
+ i_assert(uidlist->cur_block_end_indexes != NULL);
+
+ /* verify just a couple of the end indexes to make sure they
+ look correct */
+ verify_count = I_MIN(block_count, 8);
+ for (i = 1; i < verify_count; i++) {
+ if (unlikely(uidlist->cur_block_end_indexes[i-1] >=
+ uidlist->cur_block_end_indexes[i])) {
+ squat_uidlist_set_corrupted(uidlist,
+ "block list corrupted");
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int squat_uidlist_map_header(struct squat_uidlist *uidlist)
+{
+ if (uidlist->hdr.indexid == 0) {
+ /* still being built */
+ return 1;
+ }
+ if (uidlist->hdr.indexid != uidlist->trie->hdr.indexid) {
+ /* see if trie was recreated */
+ (void)squat_trie_open(uidlist->trie);
+ }
+ if (uidlist->hdr.indexid != uidlist->trie->hdr.indexid) {
+ squat_uidlist_set_corrupted(uidlist, "wrong indexid");
+ return 0;
+ }
+ if (uidlist->hdr.used_file_size < sizeof(uidlist->hdr) ||
+ (uidlist->hdr.used_file_size > uidlist->mmap_size &&
+ uidlist->mmap_base != NULL)) {
+ squat_uidlist_set_corrupted(uidlist, "broken used_file_size");
+ return 0;
+ }
+ return squat_uidlist_map_blocks(uidlist);
+}
+
+static void squat_uidlist_unmap(struct squat_uidlist *uidlist)
+{
+ if (uidlist->mmap_size != 0) {
+ if (munmap(uidlist->mmap_base, uidlist->mmap_size) < 0)
+ i_error("munmap(%s) failed: %m", uidlist->path);
+ uidlist->mmap_base = NULL;
+ uidlist->mmap_size = 0;
+ }
+ uidlist->cur_block_count = 0;
+ uidlist->cur_block_end_indexes = NULL;
+ uidlist->cur_block_offsets = NULL;
+}
+
+static int squat_uidlist_mmap(struct squat_uidlist *uidlist)
+{
+ struct stat st;
+
+ if (fstat(uidlist->fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ if (st.st_size < (off_t)sizeof(uidlist->hdr)) {
+ squat_uidlist_set_corrupted(uidlist, "File too small");
+ return -1;
+ }
+
+ squat_uidlist_unmap(uidlist);
+ uidlist->mmap_size = st.st_size;
+ uidlist->mmap_base = mmap(NULL, uidlist->mmap_size,
+ PROT_READ | PROT_WRITE,
+ MAP_SHARED, uidlist->fd, 0);
+ if (uidlist->mmap_base == MAP_FAILED) {
+ uidlist->data = uidlist->mmap_base = NULL;
+ uidlist->data_size = uidlist->mmap_size = 0;
+ i_error("mmap(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ uidlist->data = uidlist->mmap_base;
+ uidlist->data_size = uidlist->mmap_size;
+ return 0;
+}
+
+static int squat_uidlist_map(struct squat_uidlist *uidlist)
+{
+ const struct squat_uidlist_file_header *mmap_hdr = uidlist->mmap_base;
+ int ret;
+
+ if (mmap_hdr != NULL && !uidlist->building &&
+ uidlist->hdr.block_list_offset == mmap_hdr->block_list_offset) {
+ /* file hasn't changed */
+ return 1;
+ }
+
+ if ((uidlist->trie->flags & SQUAT_INDEX_FLAG_MMAP_DISABLE) == 0) {
+ if (mmap_hdr == NULL || uidlist->building ||
+ uidlist->mmap_size < mmap_hdr->used_file_size) {
+ if (squat_uidlist_mmap(uidlist) < 0)
+ return -1;
+ }
+
+ if (!uidlist->building) {
+ memcpy(&uidlist->hdr, uidlist->mmap_base,
+ sizeof(uidlist->hdr));
+ }
+ } else if (uidlist->building) {
+ /* we want to update blocks mapping, but using the header
+ in memory */
+ } else {
+ ret = pread_full(uidlist->fd, &uidlist->hdr,
+ sizeof(uidlist->hdr), 0);
+ if (ret <= 0) {
+ if (ret < 0) {
+ i_error("pread(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ i_error("Corrupted %s: File too small", uidlist->path);
+ return 0;
+ }
+ uidlist->data = NULL;
+ uidlist->data_size = 0;
+ }
+ if (uidlist->file_cache == NULL &&
+ (uidlist->trie->flags & SQUAT_INDEX_FLAG_MMAP_DISABLE) != 0)
+ uidlist->file_cache = file_cache_new_path(uidlist->fd, uidlist->path);
+ return squat_uidlist_map_header(uidlist);
+}
+
+static int squat_uidlist_read_to_memory(struct squat_uidlist *uidlist)
+{
+ size_t i, page_size = mmap_get_page_size();
+
+ if (uidlist->file_cache != NULL) {
+ return uidlist_file_cache_read(uidlist, 0,
+ uidlist->hdr.used_file_size);
+ }
+ /* Tell the kernel we're going to use the uidlist data, so it loads
+ it into memory and keeps it there. */
+ (void)madvise(uidlist->mmap_base, uidlist->mmap_size, MADV_WILLNEED);
+ /* It also speeds up a bit for us to sequentially load everything
+ into memory, although at least Linux catches up quite fast even
+ without this code. Compiler can quite easily optimize away this
+ entire for loop, but volatile seems to help with gcc 4.2. */
+ for (i = 0; i < uidlist->mmap_size; i += page_size)
+ ((const volatile char *)uidlist->data)[i];
+ return 0;
+}
+
+static void squat_uidlist_free_from_memory(struct squat_uidlist *uidlist)
+{
+ size_t page_size = mmap_get_page_size();
+
+ if (uidlist->file_cache != NULL) {
+ file_cache_invalidate(uidlist->file_cache,
+ page_size, UOFF_T_MAX);
+ } else {
+ (void)madvise(uidlist->mmap_base, uidlist->mmap_size,
+ MADV_DONTNEED);
+ }
+}
+
+struct squat_uidlist *squat_uidlist_init(struct squat_trie *trie)
+{
+ struct squat_uidlist *uidlist;
+
+ uidlist = i_new(struct squat_uidlist, 1);
+ uidlist->trie = trie;
+ uidlist->path = i_strconcat(trie->path, ".uids", NULL);
+ uidlist->fd = -1;
+
+ return uidlist;
+}
+
+void squat_uidlist_deinit(struct squat_uidlist *uidlist)
+{
+ squat_uidlist_close(uidlist);
+
+ i_free(uidlist->path);
+ i_free(uidlist);
+}
+
+static int squat_uidlist_open(struct squat_uidlist *uidlist)
+{
+ squat_uidlist_close(uidlist);
+
+ uidlist->fd = open(uidlist->path, O_RDWR);
+ if (uidlist->fd == -1) {
+ if (errno == ENOENT) {
+ i_zero(&uidlist->hdr);
+ return 0;
+ }
+ i_error("open(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ return squat_uidlist_map(uidlist) <= 0 ? -1 : 0;
+}
+
+static void squat_uidlist_close(struct squat_uidlist *uidlist)
+{
+ i_assert(!uidlist->building);
+
+ squat_uidlist_unmap(uidlist);
+ if (uidlist->file_cache != NULL)
+ file_cache_free(&uidlist->file_cache);
+ file_lock_free(&uidlist->file_lock);
+ if (uidlist->dotlock != NULL)
+ file_dotlock_delete(&uidlist->dotlock);
+ i_close_fd_path(&uidlist->fd, uidlist->path);
+ uidlist->corrupted = FALSE;
+}
+
+int squat_uidlist_refresh(struct squat_uidlist *uidlist)
+{
+ /* we assume here that trie is locked, so that we don't need to worry
+ about it when reading the header */
+ if (uidlist->fd == -1 ||
+ uidlist->hdr.indexid != uidlist->trie->hdr.indexid) {
+ if (squat_uidlist_open(uidlist) < 0)
+ return -1;
+ } else {
+ if (squat_uidlist_map(uidlist) <= 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int squat_uidlist_is_file_stale(struct squat_uidlist *uidlist)
+{
+ struct stat st, st2;
+
+ i_assert(uidlist->fd != -1);
+
+ if (stat(uidlist->path, &st) < 0) {
+ if (errno == ENOENT)
+ return 1;
+
+ i_error("stat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ if (fstat(uidlist->fd, &st2) < 0) {
+ i_error("fstat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ uidlist->locked_file_size = st2.st_size;
+
+ return st.st_ino == st2.st_ino &&
+ CMP_DEV_T(st.st_dev, st2.st_dev) ? 0 : 1;
+}
+
+static int squat_uidlist_lock(struct squat_uidlist *uidlist)
+{
+ const char *error;
+ int ret;
+
+ for (;;) {
+ i_assert(uidlist->fd != -1);
+ i_assert(uidlist->file_lock == NULL);
+ i_assert(uidlist->dotlock == NULL);
+
+ if (uidlist->trie->lock_method != FILE_LOCK_METHOD_DOTLOCK) {
+ struct file_lock_settings lock_set = {
+ .lock_method = uidlist->trie->lock_method,
+ };
+ ret = file_wait_lock(uidlist->fd, uidlist->path,
+ F_WRLCK, &lock_set,
+ SQUAT_TRIE_LOCK_TIMEOUT,
+ &uidlist->file_lock, &error);
+ if (ret < 0) {
+ i_error("squat uidlist %s: %s",
+ uidlist->path, error);
+ }
+ } else {
+ ret = file_dotlock_create(&uidlist->trie->dotlock_set,
+ uidlist->path, 0,
+ &uidlist->dotlock);
+ }
+ if (ret == 0) {
+ i_error("squat uidlist %s: Locking timed out",
+ uidlist->path);
+ return 0;
+ }
+ if (ret < 0)
+ return -1;
+
+ ret = squat_uidlist_is_file_stale(uidlist);
+ if (ret == 0)
+ break;
+
+ if (uidlist->file_lock != NULL)
+ file_unlock(&uidlist->file_lock);
+ else
+ file_dotlock_delete(&uidlist->dotlock);
+ if (ret < 0)
+ return -1;
+
+ squat_uidlist_close(uidlist);
+ uidlist->fd = squat_trie_create_fd(uidlist->trie,
+ uidlist->path, 0);
+ if (uidlist->fd == -1)
+ return -1;
+ }
+ return 1;
+}
+
+static int squat_uidlist_open_or_create(struct squat_uidlist *uidlist)
+{
+ int ret;
+
+ if (uidlist->fd == -1) {
+ uidlist->fd = squat_trie_create_fd(uidlist->trie,
+ uidlist->path, 0);
+ if (uidlist->fd == -1)
+ return -1;
+ }
+ if (squat_uidlist_lock(uidlist) <= 0)
+ return -1;
+
+ if (uidlist->locked_file_size != 0) {
+ if ((ret = squat_uidlist_map(uidlist)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* broken file, truncate */
+ if (ftruncate(uidlist->fd, 0) < 0) {
+ i_error("ftruncate(%s) failed: %m",
+ uidlist->path);
+ return -1;
+ }
+ uidlist->locked_file_size = 0;
+ }
+ }
+ if (uidlist->locked_file_size == 0) {
+ /* write using 0 until we're finished */
+ i_zero(&uidlist->hdr);
+ if (write_full(uidlist->fd, &uidlist->hdr,
+ sizeof(uidlist->hdr)) < 0) {
+ i_error("write(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int squat_uidlist_build_init(struct squat_uidlist *uidlist,
+ struct squat_uidlist_build_context **ctx_r)
+{
+ struct squat_uidlist_build_context *ctx;
+ int ret;
+
+ i_assert(!uidlist->building);
+
+ ret = squat_uidlist_open_or_create(uidlist);
+ if (ret == 0 &&
+ lseek(uidlist->fd, uidlist->hdr.used_file_size, SEEK_SET) < 0) {
+ i_error("lseek(%s) failed: %m", uidlist->path);
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ if (uidlist->file_lock != NULL)
+ file_unlock(&uidlist->file_lock);
+ if (uidlist->dotlock != NULL)
+ file_dotlock_delete(&uidlist->dotlock);
+ return -1;
+ }
+
+ ctx = i_new(struct squat_uidlist_build_context, 1);
+ ctx->uidlist = uidlist;
+ ctx->output = o_stream_create_fd(uidlist->fd, 0);
+ if (ctx->output->offset == 0) {
+ struct squat_uidlist_file_header hdr;
+
+ i_zero(&hdr);
+ o_stream_nsend(ctx->output, &hdr, sizeof(hdr));
+ }
+ o_stream_cork(ctx->output);
+ i_array_init(&ctx->lists, 10240);
+ i_array_init(&ctx->block_offsets, 128);
+ i_array_init(&ctx->block_end_indexes, 128);
+ ctx->list_start_idx = uidlist->hdr.count;
+ ctx->build_hdr = uidlist->hdr;
+
+ uidlist->building = TRUE;
+ *ctx_r = ctx;
+ return 0;
+}
+
+static void
+uidlist_write_block_list_and_header(struct squat_uidlist_build_context *ctx,
+ struct ostream *output,
+ ARRAY_TYPE(uint32_t) *block_offsets,
+ ARRAY_TYPE(uint32_t) *block_end_indexes,
+ bool write_old_blocks)
+{
+ struct squat_uidlist *uidlist = ctx->uidlist;
+ unsigned int align, old_block_count, new_block_count;
+ uint32_t block_offset_count;
+ uoff_t block_list_offset;
+
+ i_assert(uidlist->trie->hdr.indexid != 0);
+ ctx->build_hdr.indexid = uidlist->trie->hdr.indexid;
+
+ if (array_count(block_end_indexes) == 0) {
+ ctx->build_hdr.used_file_size = output->offset;
+ ctx->build_hdr.block_list_offset = 0;
+ uidlist->hdr = ctx->build_hdr;
+ return;
+ }
+
+ align = output->offset % sizeof(uint32_t);
+ if (align != 0) {
+ static char null[sizeof(uint32_t)-1] = { 0, };
+
+ o_stream_nsend(output, null, sizeof(uint32_t) - align);
+ }
+ block_list_offset = output->offset;
+
+ new_block_count = array_count(block_offsets);
+ old_block_count = write_old_blocks ? uidlist->cur_block_count : 0;
+
+ block_offset_count = new_block_count + old_block_count;
+ o_stream_nsend(output, &block_offset_count, sizeof(block_offset_count));
+ /* write end indexes */
+ o_stream_nsend(output, uidlist->cur_block_end_indexes,
+ old_block_count * sizeof(uint32_t));
+ o_stream_nsend(output, array_front(block_end_indexes),
+ new_block_count * sizeof(uint32_t));
+ /* write offsets */
+ o_stream_nsend(output, uidlist->cur_block_offsets,
+ old_block_count * sizeof(uint32_t));
+ o_stream_nsend(output, array_front(block_offsets),
+ new_block_count * sizeof(uint32_t));
+ (void)o_stream_flush(output);
+
+ /* update header - it's written later when trie is locked */
+ ctx->build_hdr.block_list_offset = block_list_offset;
+ ctx->build_hdr.used_file_size = output->offset;
+ uidlist->hdr = ctx->build_hdr;
+}
+
+void squat_uidlist_build_flush(struct squat_uidlist_build_context *ctx)
+{
+ struct uidlist_list *lists;
+ uint8_t buf[SQUAT_PACK_MAX_SIZE], *bufp;
+ unsigned int i, j, count, max;
+ uint32_t block_offset, block_end_idx, start_offset;
+ uint32_t list_sizes[UIDLIST_BLOCK_LIST_COUNT];
+ size_t mem_size;
+
+ if (ctx->uidlist->corrupted)
+ return;
+
+ lists = array_get_modifiable(&ctx->lists, &count);
+ if (count == 0)
+ return;
+
+ /* write the lists and save the written sizes to uid_list[0] */
+ for (i = 0; i < count; i += UIDLIST_BLOCK_LIST_COUNT) {
+ start_offset = ctx->output->offset;
+ max = I_MIN(count - i, UIDLIST_BLOCK_LIST_COUNT);
+ for (j = 0; j < max; j++) {
+ if (uidlist_write(ctx->output, &lists[i+j],
+ FALSE, &list_sizes[j]) < 0) {
+ squat_uidlist_set_corrupted(ctx->uidlist,
+ "Broken uidlists");
+ return;
+ }
+ }
+
+ block_offset = ctx->output->offset;
+ block_end_idx = ctx->list_start_idx + i + max;
+ array_push_back(&ctx->block_offsets, &block_offset);
+ array_push_back(&ctx->block_end_indexes, &block_end_idx);
+
+ /* write the full size of the uidlists */
+ bufp = buf;
+ squat_pack_num(&bufp, block_offset - start_offset);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+
+ /* write the sizes/flags */
+ for (j = 0; j < max; j++) {
+ bufp = buf;
+ squat_pack_num(&bufp, list_sizes[j]);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+ }
+ }
+
+ mem_size = ctx->lists.arr.buffer->used +
+ ctx->block_offsets.arr.buffer->used +
+ ctx->block_end_indexes.arr.buffer->used;
+ if (ctx->uidlist->max_size < mem_size)
+ ctx->uidlist->max_size = mem_size;
+
+ ctx->list_start_idx += count;
+ array_clear(&ctx->lists);
+
+ uidlist_write_block_list_and_header(ctx, ctx->output,
+ &ctx->block_offsets,
+ &ctx->block_end_indexes, TRUE);
+
+ (void)squat_uidlist_map(ctx->uidlist);
+
+ array_clear(&ctx->block_offsets);
+ array_clear(&ctx->block_end_indexes);
+}
+
+int squat_uidlist_build_finish(struct squat_uidlist_build_context *ctx)
+{
+ if (ctx->uidlist->corrupted)
+ return -1;
+
+ if (!ctx->output->closed) {
+ (void)o_stream_seek(ctx->output, 0);
+ o_stream_nsend(ctx->output,
+ &ctx->build_hdr, sizeof(ctx->build_hdr));
+ (void)o_stream_seek(ctx->output, ctx->build_hdr.used_file_size);
+ }
+
+ if (o_stream_finish(ctx->output) < 0) {
+ i_error("write() to %s failed: %s", ctx->uidlist->path,
+ o_stream_get_error(ctx->output));
+ return -1;
+ }
+ return 0;
+}
+
+void squat_uidlist_build_deinit(struct squat_uidlist_build_context **_ctx)
+{
+ struct squat_uidlist_build_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ i_assert(array_count(&ctx->lists) == 0 || ctx->uidlist->corrupted);
+ i_assert(ctx->uidlist->building);
+ ctx->uidlist->building = FALSE;
+
+ if (ctx->uidlist->file_lock != NULL)
+ file_unlock(&ctx->uidlist->file_lock);
+ else
+ file_dotlock_delete(&ctx->uidlist->dotlock);
+
+ if (ctx->need_reopen)
+ (void)squat_uidlist_open(ctx->uidlist);
+
+ array_free(&ctx->block_offsets);
+ array_free(&ctx->block_end_indexes);
+ array_free(&ctx->lists);
+ o_stream_ignore_last_errors(ctx->output);
+ o_stream_unref(&ctx->output);
+ i_free(ctx);
+}
+
+int squat_uidlist_rebuild_init(struct squat_uidlist_build_context *build_ctx,
+ bool compress,
+ struct squat_uidlist_rebuild_context **ctx_r)
+{
+ struct squat_uidlist_rebuild_context *ctx;
+ struct squat_uidlist_file_header hdr;
+ const char *temp_path;
+ int fd;
+
+ if (build_ctx->build_hdr.link_count == 0)
+ return 0;
+
+ if (!compress) {
+ if (build_ctx->build_hdr.link_count <
+ build_ctx->build_hdr.count*2/3)
+ return 0;
+ }
+
+ /* make sure the entire uidlist is in memory before beginning,
+ otherwise the pages are faulted to memory in random order which
+ takes forever. */
+ if (squat_uidlist_read_to_memory(build_ctx->uidlist) < 0)
+ return -1;
+
+ temp_path = t_strconcat(build_ctx->uidlist->path, ".tmp", NULL);
+ fd = squat_trie_create_fd(build_ctx->uidlist->trie, temp_path, O_TRUNC);
+ if (fd == -1)
+ return -1;
+
+ ctx = i_new(struct squat_uidlist_rebuild_context, 1);
+ ctx->uidlist = build_ctx->uidlist;
+ ctx->build_ctx = build_ctx;
+ ctx->fd = fd;
+ ctx->output = o_stream_create_fd(ctx->fd, 0);
+ ctx->next_uid_list_idx = 0x100;
+ o_stream_cork(ctx->output);
+
+ i_zero(&hdr);
+ o_stream_nsend(ctx->output, &hdr, sizeof(hdr));
+
+ ctx->cur_block_start_offset = ctx->output->offset;
+ i_array_init(&ctx->new_block_offsets,
+ build_ctx->build_hdr.count / UIDLIST_BLOCK_LIST_COUNT);
+ i_array_init(&ctx->new_block_end_indexes,
+ build_ctx->build_hdr.count / UIDLIST_BLOCK_LIST_COUNT);
+ *ctx_r = ctx;
+ return 1;
+}
+
+static void
+uidlist_rebuild_flush_block(struct squat_uidlist_rebuild_context *ctx)
+{
+ uint8_t buf[SQUAT_PACK_MAX_SIZE], *bufp;
+ uint32_t block_offset, block_end_idx;
+ unsigned int i;
+
+ ctx->new_count += ctx->list_idx;
+
+ block_offset = ctx->output->offset;
+ block_end_idx = ctx->new_count;
+ array_push_back(&ctx->new_block_offsets, &block_offset);
+ array_push_back(&ctx->new_block_end_indexes, &block_end_idx);
+
+ /* this block's contents started from cur_block_start_offset and
+ ended to current offset. write the size of this area. */
+ bufp = buf;
+ squat_pack_num(&bufp, block_offset - ctx->cur_block_start_offset);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+
+ /* write the sizes/flags */
+ for (i = 0; i < ctx->list_idx; i++) {
+ bufp = buf;
+ squat_pack_num(&bufp, ctx->list_sizes[i]);
+ o_stream_nsend(ctx->output, buf, bufp - buf);
+ }
+ ctx->cur_block_start_offset = ctx->output->offset;
+}
+
+uint32_t squat_uidlist_rebuild_next(struct squat_uidlist_rebuild_context *ctx,
+ const ARRAY_TYPE(uint32_t) *uids)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = uidlist_write_array(ctx->output, array_front(uids),
+ array_count(uids), 0, 0, FALSE,
+ &ctx->list_sizes[ctx->list_idx]);
+ } T_END;
+ if (ret < 0)
+ squat_uidlist_set_corrupted(ctx->uidlist, "Broken uidlists");
+
+ if (++ctx->list_idx == UIDLIST_BLOCK_LIST_COUNT) {
+ uidlist_rebuild_flush_block(ctx);
+ ctx->list_idx = 0;
+ }
+ return ctx->next_uid_list_idx++ << 1;
+}
+
+uint32_t squat_uidlist_rebuild_nextu(struct squat_uidlist_rebuild_context *ctx,
+ const ARRAY_TYPE(seq_range) *uids)
+{
+ const struct seq_range *range;
+ ARRAY_TYPE(uint32_t) tmp_uids;
+ uint32_t seq, uid1, ret;
+ unsigned int i, count;
+
+ range = array_get(uids, &count);
+ if (count == 0)
+ return 0;
+
+ if (range[count-1].seq2 < 8) {
+ /* we can use a singleton bitmask */
+ ret = 0;
+ for (i = 0; i < count; i++) {
+ for (seq = range[i].seq1; seq <= range[i].seq2; seq++)
+ ret |= 1 << (seq+1);
+ }
+ return ret;
+ }
+ if (count == 1 && range[0].seq1 == range[0].seq2) {
+ /* single UID */
+ return (range[0].seq1 << 1) | 1;
+ }
+
+ /* convert seq range to our internal representation and use the
+ normal _rebuild_next() to write it */
+ i_array_init(&tmp_uids, 128);
+ for (i = 0; i < count; i++) {
+ if (range[i].seq1 == range[i].seq2)
+ array_push_back(&tmp_uids, &range[i].seq1);
+ else {
+ uid1 = range[i].seq1 | UID_LIST_MASK_RANGE;
+ array_push_back(&tmp_uids, &uid1);
+ array_push_back(&tmp_uids, &range[i].seq2);
+ }
+ }
+ ret = squat_uidlist_rebuild_next(ctx, &tmp_uids);
+ array_free(&tmp_uids);
+ return ret;
+}
+
+int squat_uidlist_rebuild_finish(struct squat_uidlist_rebuild_context *ctx,
+ bool cancel)
+{
+ const char *temp_path;
+ int ret = 1;
+
+ if (ctx->list_idx != 0)
+ uidlist_rebuild_flush_block(ctx);
+ if (cancel || ctx->uidlist->corrupted)
+ ret = 0;
+
+ temp_path = t_strconcat(ctx->uidlist->path, ".tmp", NULL);
+ if (ret > 0) {
+ ctx->build_ctx->build_hdr.indexid =
+ ctx->uidlist->trie->hdr.indexid;
+ ctx->build_ctx->build_hdr.count = ctx->new_count;
+ ctx->build_ctx->build_hdr.link_count = 0;
+ uidlist_write_block_list_and_header(ctx->build_ctx, ctx->output,
+ &ctx->new_block_offsets,
+ &ctx->new_block_end_indexes,
+ FALSE);
+ (void)o_stream_seek(ctx->output, 0);
+ o_stream_nsend(ctx->output, &ctx->build_ctx->build_hdr,
+ sizeof(ctx->build_ctx->build_hdr));
+ (void)o_stream_seek(ctx->output,
+ ctx->build_ctx->build_hdr.used_file_size);
+
+ if (ctx->uidlist->corrupted)
+ ret = -1;
+ else if (o_stream_finish(ctx->output) < 0) {
+ i_error("write(%s) failed: %s", temp_path,
+ o_stream_get_error(ctx->output));
+ ret = -1;
+ } else if (rename(temp_path, ctx->uidlist->path) < 0) {
+ i_error("rename(%s, %s) failed: %m",
+ temp_path, ctx->uidlist->path);
+ ret = -1;
+ }
+ ctx->build_ctx->need_reopen = TRUE;
+ } else {
+ o_stream_abort(ctx->output);
+ }
+
+ /* we no longer require the entire uidlist to be in memory,
+ let it be used for something more useful. */
+ squat_uidlist_free_from_memory(ctx->uidlist);
+
+ o_stream_unref(&ctx->output);
+ if (close(ctx->fd) < 0)
+ i_error("close(%s) failed: %m", temp_path);
+
+ if (ret <= 0)
+ i_unlink(temp_path);
+ array_free(&ctx->new_block_offsets);
+ array_free(&ctx->new_block_end_indexes);
+ i_free(ctx);
+ return ret < 0 ? -1 : 0;
+}
+
+static void
+uidlist_flush(struct squat_uidlist_build_context *ctx,
+ struct uidlist_list *list, uint32_t uid)
+{
+ uint32_t size, offset = ctx->output->offset;
+
+ ctx->build_hdr.link_count++;
+ if (uidlist_write(ctx->output, list, TRUE, &size) < 0)
+ squat_uidlist_set_corrupted(ctx->uidlist, "Broken uidlists");
+
+ list->uid_count = 2;
+ list->uid_begins_with_pointer = TRUE;
+
+ list->uid_list[0] = offset;
+ list->uid_list[1] = uid;
+}
+
+static struct uidlist_list *
+uidlist_add_new(struct squat_uidlist_build_context *ctx, unsigned int count,
+ uint32_t *uid_list_idx_r)
+{
+ struct uidlist_list *list;
+
+ i_assert(array_count(&ctx->lists) +
+ ctx->list_start_idx == ctx->build_hdr.count);
+ *uid_list_idx_r = (ctx->build_hdr.count + 0x100) << 1;
+ list = array_append_space(&ctx->lists);
+ ctx->build_hdr.count++;
+
+ list->uid_count = count;
+ return list;
+}
+
+uint32_t squat_uidlist_build_add_uid(struct squat_uidlist_build_context *ctx,
+ uint32_t uid_list_idx, uint32_t uid)
+{
+ struct uidlist_list *list;
+ unsigned int idx, mask;
+ uint32_t *p;
+
+ if ((uid_list_idx & 1) != 0) {
+ /* adding second UID */
+ uint32_t prev_uid = uid_list_idx >> 1;
+
+ i_assert(prev_uid != uid);
+ list = uidlist_add_new(ctx, 2, &uid_list_idx);
+ list->uid_list[0] = prev_uid;
+ if (prev_uid + 1 == uid)
+ list->uid_list[0] |= UID_LIST_MASK_RANGE;
+ list->uid_list[1] = uid;
+ return uid_list_idx;
+ } else if (uid_list_idx < (0x100 << 1)) {
+ uint32_t old_list_idx;
+
+ if (uid < 8) {
+ /* UID lists containing only UIDs 0-7 are saved as
+ uidlist values 2..511. think of it as a bitmask. */
+ uid_list_idx |= 1 << (uid + 1);
+ i_assert((uid_list_idx & 1) == 0);
+ return uid_list_idx;
+ }
+
+ if (uid_list_idx == 0) {
+ /* first UID */
+ return (uid << 1) | 1;
+ }
+
+ /* create a new list */
+ old_list_idx = uid_list_idx >> 1;
+ list = uidlist_add_new(ctx, 1, &uid_list_idx);
+ /* add the first UID ourself */
+ idx = 0;
+ i_assert((old_list_idx & 0xff) != 0);
+ for (mask = 1; mask <= 128; mask <<= 1, idx++) {
+ if ((old_list_idx & mask) != 0) {
+ list->uid_list[0] = idx;
+ idx++; mask <<= 1;
+ break;
+ }
+ }
+ for (; mask <= 128; mask <<= 1, idx++) {
+ if ((old_list_idx & mask) != 0) {
+ (void)squat_uidlist_build_add_uid(ctx,
+ uid_list_idx, idx);
+ }
+ }
+ }
+
+ /* add to existing list */
+ idx = (uid_list_idx >> 1) - 0x100;
+ if (idx < ctx->list_start_idx) {
+ list = uidlist_add_new(ctx, 2, &uid_list_idx);
+ list->uid_list[0] = UID_LIST_POINTER_MASK_LIST_IDX | idx;
+ list->uid_list[1] = uid;
+ list->uid_begins_with_pointer = TRUE;
+ ctx->build_hdr.link_count++;
+ return uid_list_idx;
+ }
+
+ idx -= ctx->list_start_idx;
+ if (idx >= array_count(&ctx->lists)) {
+ squat_uidlist_set_corrupted(ctx->uidlist,
+ "missing/broken uidlist");
+ return 0;
+ }
+ list = array_idx_modifiable(&ctx->lists, idx);
+ i_assert(list->uid_count > 0);
+
+ p = &list->uid_list[list->uid_count-1];
+ i_assert(uid != *p || ctx->uidlist->corrupted ||
+ (list->uid_count == 1 && list->uid_begins_with_pointer));
+ if (uid == *p + 1 &&
+ (list->uid_count > 1 || !list->uid_begins_with_pointer)) {
+ /* use a range */
+ if (list->uid_count > 1 && (p[-1] & UID_LIST_MASK_RANGE) != 0 &&
+ (list->uid_count > 2 || !list->uid_begins_with_pointer)) {
+ /* increase the existing range */
+ *p += 1;
+ return uid_list_idx;
+ }
+
+ if (list->uid_count == UIDLIST_LIST_SIZE) {
+ uidlist_flush(ctx, list, uid);
+ return uid_list_idx;
+ }
+ /* create a new range */
+ *p |= UID_LIST_MASK_RANGE;
+ } else {
+ if (list->uid_count == UIDLIST_LIST_SIZE) {
+ uidlist_flush(ctx, list, uid);
+ return uid_list_idx;
+ }
+ }
+
+ p++;
+ list->uid_count++;
+
+ *p = uid;
+ return uid_list_idx;
+}
+
+static void uidlist_array_append(ARRAY_TYPE(uint32_t) *uids, uint32_t uid)
+{
+ uint32_t *uidlist;
+ unsigned int count;
+
+ uidlist = array_get_modifiable(uids, &count);
+ if (count == 0) {
+ array_push_back(uids, &uid);
+ return;
+ }
+ if (uidlist[count-1] + 1 == uid) {
+ if (count > 1 && (uidlist[count-2] &
+ UID_LIST_MASK_RANGE) != 0) {
+ uidlist[count-1]++;
+ return;
+ }
+ uidlist[count-1] |= UID_LIST_MASK_RANGE;
+ }
+ array_push_back(uids, &uid);
+}
+
+static void uidlist_array_append_range(ARRAY_TYPE(uint32_t) *uids,
+ uint32_t uid1, uint32_t uid2)
+{
+ uint32_t *uidlist;
+ unsigned int count;
+
+ i_assert(uid1 < uid2);
+
+ uidlist = array_get_modifiable(uids, &count);
+ if (count == 0) {
+ uid1 |= UID_LIST_MASK_RANGE;
+ array_push_back(uids, &uid1);
+ array_push_back(uids, &uid2);
+ return;
+ }
+ if (uidlist[count-1] + 1 == uid1) {
+ if (count > 1 && (uidlist[count-2] &
+ UID_LIST_MASK_RANGE) != 0) {
+ uidlist[count-1] = uid2;
+ return;
+ }
+ uidlist[count-1] |= UID_LIST_MASK_RANGE;
+ } else {
+ uid1 |= UID_LIST_MASK_RANGE;
+ array_push_back(uids, &uid1);
+ }
+ array_push_back(uids, &uid2);
+}
+
+static int
+squat_uidlist_get_at_offset(struct squat_uidlist *uidlist, uoff_t offset,
+ uint32_t num, ARRAY_TYPE(uint32_t) *uids)
+{
+ const uint32_t *uid_list;
+ const uint8_t *p, *end;
+ uint32_t size, base_uid, next_uid, flags, prev;
+ uoff_t uidlist_data_offset;
+ unsigned int i, j, count;
+
+ if (num != 0)
+ uidlist_data_offset = offset;
+ else {
+ /* not given, read it */
+ if (uidlist_file_cache_read(uidlist, offset,
+ SQUAT_PACK_MAX_SIZE) < 0)
+ return -1;
+
+ p = CONST_PTR_OFFSET(uidlist->data, offset);
+ end = CONST_PTR_OFFSET(uidlist->data, uidlist->data_size);
+ num = squat_unpack_num(&p, end);
+ uidlist_data_offset = p - (const uint8_t *)uidlist->data;
+ }
+ size = num >> 2;
+
+ if (uidlist_file_cache_read(uidlist, uidlist_data_offset, size) < 0)
+ return -1;
+ if (uidlist_data_offset + size > uidlist->data_size) {
+ squat_uidlist_set_corrupted(uidlist,
+ "size points outside file");
+ return -1;
+ }
+
+ p = CONST_PTR_OFFSET(uidlist->data, uidlist_data_offset);
+ end = p + size;
+
+ flags = num;
+ if ((flags & UIDLIST_PACKED_FLAG_BEGINS_WITH_POINTER) != 0) {
+ /* link to the file */
+ prev = squat_unpack_num(&p, end);
+
+ if ((prev & 1) != 0) {
+ /* pointer to uidlist */
+ prev = ((prev >> 1) + 0x100) << 1;
+ if (squat_uidlist_get(uidlist, prev, uids) < 0)
+ return -1;
+ } else {
+ prev = offset - (prev >> 1);
+ if (squat_uidlist_get_at_offset(uidlist, prev,
+ 0, uids) < 0)
+ return -1;
+ }
+ uid_list = array_get(uids, &count);
+ next_uid = count == 0 ? 0 : uid_list[count-1] + 1;
+ } else {
+ next_uid = 0;
+ }
+
+ num = base_uid = squat_unpack_num(&p, end);
+ if ((flags & UIDLIST_PACKED_FLAG_BITMASK) == 0)
+ base_uid >>= 1;
+ if (base_uid < next_uid) {
+ squat_uidlist_set_corrupted(uidlist,
+ "broken continued uidlist");
+ return -1;
+ }
+
+ if ((flags & UIDLIST_PACKED_FLAG_BITMASK) != 0) {
+ /* bitmask */
+ size = end - p;
+
+ uidlist_array_append(uids, base_uid++);
+ for (i = 0; i < size; i++) {
+ for (j = 0; j < 8; j++, base_uid++) {
+ if ((p[i] & (1 << j)) != 0)
+ uidlist_array_append(uids, base_uid);
+ }
+ }
+ } else {
+ /* range */
+ for (;;) {
+ if ((num & 1) == 0) {
+ uidlist_array_append(uids, base_uid);
+ } else {
+ /* range */
+ uint32_t seq1 = base_uid;
+ base_uid += squat_unpack_num(&p, end) + 1;
+ uidlist_array_append_range(uids, seq1,
+ base_uid);
+ }
+ if (p == end)
+ break;
+
+ num = squat_unpack_num(&p, end);
+ base_uid += (num >> 1) + 1;
+ }
+ }
+ return 0;
+}
+
+static int
+squat_uidlist_get_offset(struct squat_uidlist *uidlist, uint32_t uid_list_idx,
+ uint32_t *offset_r, uint32_t *num_r)
+{
+ const uint8_t *p, *end;
+ unsigned int idx;
+ uint32_t num, skip_bytes, uidlists_offset;
+ size_t max_map_size;
+
+ if (uidlist->fd == -1) {
+ squat_uidlist_set_corrupted(uidlist, "no uidlists");
+ return -1;
+ }
+
+ if (bsearch_insert_pos(&uid_list_idx, uidlist->cur_block_end_indexes,
+ uidlist->cur_block_count,
+ sizeof(uint32_t), uint32_cmp, &idx))
+ idx++;
+ if (unlikely(idx == uidlist->cur_block_count)) {
+ squat_uidlist_set_corrupted(uidlist, "uidlist not found");
+ return -1;
+ }
+ i_assert(uidlist->cur_block_end_indexes != NULL);
+ if (unlikely(idx > 0 &&
+ uidlist->cur_block_end_indexes[idx-1] > uid_list_idx)) {
+ squat_uidlist_set_corrupted(uidlist, "broken block list");
+ return -1;
+ }
+
+ /* make sure everything is mapped */
+ uid_list_idx -= idx == 0 ? 0 : uidlist->cur_block_end_indexes[idx-1];
+ max_map_size = SQUAT_PACK_MAX_SIZE * (1+uid_list_idx);
+ if (uidlist_file_cache_read(uidlist, uidlist->cur_block_offsets[idx],
+ max_map_size) < 0)
+ return -1;
+
+ /* find the uidlist inside the block */
+ i_assert(uidlist->cur_block_offsets != NULL);
+ p = CONST_PTR_OFFSET(uidlist->data, uidlist->cur_block_offsets[idx]);
+ end = CONST_PTR_OFFSET(uidlist->data, uidlist->data_size);
+
+ uidlists_offset = uidlist->cur_block_offsets[idx] -
+ squat_unpack_num(&p, end);
+ for (skip_bytes = 0; uid_list_idx > 0; uid_list_idx--) {
+ num = squat_unpack_num(&p, end);
+ skip_bytes += num >> 2;
+ }
+ *offset_r = uidlists_offset + skip_bytes;
+ *num_r = squat_unpack_num(&p, end);
+
+ if (unlikely(p == end)) {
+ squat_uidlist_set_corrupted(uidlist, "broken file");
+ return -1;
+ }
+ if (unlikely(*offset_r > uidlist->mmap_size &&
+ uidlist->mmap_base != NULL)) {
+ squat_uidlist_set_corrupted(uidlist, "broken offset");
+ return -1;
+ }
+ return 0;
+}
+
+int squat_uidlist_get(struct squat_uidlist *uidlist, uint32_t uid_list_idx,
+ ARRAY_TYPE(uint32_t) *uids)
+{
+ unsigned int mask;
+ uint32_t uid, offset, num;
+
+ if ((uid_list_idx & 1) != 0) {
+ /* single UID */
+ uid = uid_list_idx >> 1;
+ uidlist_array_append(uids, uid);
+ return 0;
+ } else if (uid_list_idx < (0x100 << 1)) {
+ /* bitmask */
+ for (uid = 0, mask = 2; mask <= 256; mask <<= 1, uid++) {
+ if ((uid_list_idx & mask) != 0)
+ uidlist_array_append(uids, uid);
+ }
+ return 0;
+ }
+
+ uid_list_idx = (uid_list_idx >> 1) - 0x100;
+ if (squat_uidlist_get_offset(uidlist, uid_list_idx, &offset, &num) < 0)
+ return -1;
+ return squat_uidlist_get_at_offset(uidlist, offset, num, uids);
+}
+
+uint32_t squat_uidlist_singleton_last_uid(uint32_t uid_list_idx)
+{
+ unsigned int idx, mask;
+
+ if ((uid_list_idx & 1) != 0) {
+ /* single UID */
+ return uid_list_idx >> 1;
+ } else if (uid_list_idx < (0x100 << 1)) {
+ /* bitmask */
+ if (uid_list_idx == 2) {
+ /* just a quick optimization */
+ return 0;
+ }
+ for (idx = 7, mask = 256; mask > 2; mask >>= 1, idx--) {
+ if ((uid_list_idx & mask) != 0)
+ return idx;
+ }
+ }
+
+ i_unreached();
+ return 0;
+}
+
+int squat_uidlist_get_seqrange(struct squat_uidlist *uidlist,
+ uint32_t uid_list_idx,
+ ARRAY_TYPE(seq_range) *seq_range_arr)
+{
+ ARRAY_TYPE(uint32_t) tmp_uid_arr;
+ struct seq_range range;
+ const uint32_t *tmp_uids;
+ unsigned int i, count;
+ int ret;
+
+ i_array_init(&tmp_uid_arr, 128);
+ ret = squat_uidlist_get(uidlist, uid_list_idx, &tmp_uid_arr);
+ if (ret == 0) {
+ tmp_uids = array_get(&tmp_uid_arr, &count);
+ for (i = 0; i < count; i++) {
+ if ((tmp_uids[i] & UID_LIST_MASK_RANGE) == 0)
+ range.seq1 = range.seq2 = tmp_uids[i];
+ else {
+ range.seq1 = tmp_uids[i] & ~UID_LIST_MASK_RANGE;
+ range.seq2 = tmp_uids[++i];
+ }
+ array_push_back(seq_range_arr, &range);
+ }
+ }
+ array_free(&tmp_uid_arr);
+ return ret;
+}
+
+int squat_uidlist_filter(struct squat_uidlist *uidlist, uint32_t uid_list_idx,
+ ARRAY_TYPE(seq_range) *uids)
+{
+ const struct seq_range *parent_range;
+ ARRAY_TYPE(seq_range) dest_uids;
+ ARRAY_TYPE(uint32_t) relative_uids;
+ const uint32_t *rel_range;
+ unsigned int i, rel_count, parent_idx, parent_count, diff, parent_uid;
+ uint32_t prev_seq, seq1, seq2;
+ int ret = 0;
+
+ parent_range = array_get(uids, &parent_count);
+ if (parent_count == 0)
+ return 0;
+
+ i_array_init(&relative_uids, 128);
+ i_array_init(&dest_uids, 128);
+ if (squat_uidlist_get(uidlist, uid_list_idx, &relative_uids) < 0)
+ ret = -1;
+
+ parent_idx = 0;
+ rel_range = array_get(&relative_uids, &rel_count);
+ prev_seq = 0; parent_uid = parent_range[0].seq1;
+ for (i = 0; i < rel_count; i++) {
+ if (unlikely(parent_uid == (uint32_t)-1)) {
+ i_error("broken UID ranges");
+ ret = -1;
+ break;
+ }
+ if ((rel_range[i] & UID_LIST_MASK_RANGE) == 0)
+ seq1 = seq2 = rel_range[i];
+ else {
+ seq1 = (rel_range[i] & ~UID_LIST_MASK_RANGE);
+ seq2 = rel_range[++i];
+ }
+ i_assert(seq1 >= prev_seq);
+ diff = seq1 - prev_seq;
+ while (diff > 0) {
+ if (unlikely(parent_uid == (uint32_t)-1)) {
+ i_error("broken UID ranges");
+ ret = -1;
+ break;
+ }
+
+ for (; parent_idx < parent_count; parent_idx++) {
+ if (parent_range[parent_idx].seq2 <= parent_uid)
+ continue;
+ if (parent_uid < parent_range[parent_idx].seq1)
+ parent_uid = parent_range[parent_idx].seq1;
+ else
+ parent_uid++;
+ break;
+ }
+ diff--;
+ }
+ diff = seq2 - seq1 + 1;
+ while (diff > 0) {
+ if (unlikely(parent_uid == (uint32_t)-1)) {
+ i_error("broken UID ranges");
+ ret = -1;
+ break;
+ }
+ seq_range_array_add(&dest_uids, parent_uid);
+ for (; parent_idx < parent_count; parent_idx++) {
+ if (parent_range[parent_idx].seq2 <= parent_uid)
+ continue;
+ if (parent_uid < parent_range[parent_idx].seq1)
+ parent_uid = parent_range[parent_idx].seq1;
+ else
+ parent_uid++;
+ break;
+ }
+ diff--;
+ }
+
+ prev_seq = seq2 + 1;
+ }
+
+ buffer_set_used_size(uids->arr.buffer, 0);
+ array_append_array(uids, &dest_uids);
+
+ array_free(&relative_uids);
+ array_free(&dest_uids);
+ return ret;
+}
+
+size_t squat_uidlist_mem_used(struct squat_uidlist *uidlist,
+ unsigned int *count_r)
+{
+ *count_r = uidlist->hdr.count;
+ return uidlist->max_size;
+}
diff --git a/src/plugins/fts-squat/squat-uidlist.h b/src/plugins/fts-squat/squat-uidlist.h
new file mode 100644
index 0000000..79ed791
--- /dev/null
+++ b/src/plugins/fts-squat/squat-uidlist.h
@@ -0,0 +1,71 @@
+#ifndef SQUAT_UIDLIST_H
+#define SQUAT_UIDLIST_H
+
+struct squat_trie;
+struct squat_uidlist_build_context;
+struct squat_uidlist_rebuild_context;
+
+struct squat_uidlist_file_header {
+ uint32_t indexid;
+ uint32_t used_file_size;
+ uint32_t block_list_offset;
+ uint32_t count, link_count;
+};
+
+/*
+ uidlist file:
+
+ struct uidlist_header;
+
+ // size includes both prev_offset and uidlist
+ packed (size << 2) | packed_flags; // UIDLIST_PACKED_FLAG_*
+ [packed prev_offset;] // If UIDLIST_PACKED_FLAG_BEGINS_WITH_OFFSET is set
+ if (UIDLIST_PACKED_FLAG_BITMASK) {
+ packed base_uid; // first UID in uidlist
+ uint8_t bitmask[]; // first bit is base_uid+1
+ } else {
+ // FIXME: packed range
+ }
+*/
+
+#define UIDLIST_IS_SINGLETON(idx) \
+ (((idx) & 1) != 0 || (idx) < (0x100 << 1))
+
+struct squat_uidlist *squat_uidlist_init(struct squat_trie *trie);
+void squat_uidlist_deinit(struct squat_uidlist *uidlist);
+
+int squat_uidlist_refresh(struct squat_uidlist *uidlist);
+
+int squat_uidlist_build_init(struct squat_uidlist *uidlist,
+ struct squat_uidlist_build_context **ctx_r);
+uint32_t squat_uidlist_build_add_uid(struct squat_uidlist_build_context *ctx,
+ uint32_t uid_list_idx, uint32_t uid);
+void squat_uidlist_build_flush(struct squat_uidlist_build_context *ctx);
+int squat_uidlist_build_finish(struct squat_uidlist_build_context *ctx);
+void squat_uidlist_build_deinit(struct squat_uidlist_build_context **ctx);
+
+int squat_uidlist_rebuild_init(struct squat_uidlist_build_context *build_ctx,
+ bool compress,
+ struct squat_uidlist_rebuild_context **ctx_r);
+uint32_t squat_uidlist_rebuild_next(struct squat_uidlist_rebuild_context *ctx,
+ const ARRAY_TYPE(uint32_t) *uids);
+uint32_t squat_uidlist_rebuild_nextu(struct squat_uidlist_rebuild_context *ctx,
+ const ARRAY_TYPE(seq_range) *uids);
+int squat_uidlist_rebuild_finish(struct squat_uidlist_rebuild_context *ctx,
+ bool cancel);
+
+int squat_uidlist_get(struct squat_uidlist *uidlist, uint32_t uid_list_idx,
+ ARRAY_TYPE(uint32_t) *uids);
+uint32_t squat_uidlist_singleton_last_uid(uint32_t uid_list_idx);
+
+int squat_uidlist_get_seqrange(struct squat_uidlist *uidlist,
+ uint32_t uid_list_idx,
+ ARRAY_TYPE(seq_range) *seq_range_arr);
+int squat_uidlist_filter(struct squat_uidlist *uidlist, uint32_t uid_list_idx,
+ ARRAY_TYPE(seq_range) *uids);
+
+void squat_uidlist_delete(struct squat_uidlist *uidlist);
+size_t squat_uidlist_mem_used(struct squat_uidlist *uidlist,
+ unsigned int *count_r);
+
+#endif
diff --git a/src/plugins/fts/Makefile.am b/src/plugins/fts/Makefile.am
new file mode 100644
index 0000000..2e7753c
--- /dev/null
+++ b/src/plugins/fts/Makefile.am
@@ -0,0 +1,74 @@
+pkglibexecdir = $(libexecdir)/dovecot
+doveadm_moduledir = $(moduledir)/doveadm
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-fts \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-http \
+ -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/doveadm
+
+NOPLUGIN_LDFLAGS =
+lib20_doveadm_fts_plugin_la_LDFLAGS = -module -avoid-version
+lib20_fts_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_fts_plugin.la
+
+lib20_fts_plugin_la_LIBADD = ../../lib-fts/libfts.la
+
+lib20_fts_plugin_la_SOURCES = \
+ fts-api.c \
+ fts-build-mail.c \
+ fts-expunge-log.c \
+ fts-indexer.c \
+ fts-parser.c \
+ fts-parser-html.c \
+ fts-parser-script.c \
+ fts-parser-tika.c \
+ fts-plugin.c \
+ fts-search.c \
+ fts-search-args.c \
+ fts-search-serialize.c \
+ fts-storage.c \
+ fts-user.c
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ fts-api.h \
+ fts-api-private.h \
+ fts-expunge-log.h \
+ fts-indexer.h \
+ fts-parser.h \
+ fts-storage.h \
+ fts-user.h
+
+noinst_HEADERS = \
+ doveadm-fts.h \
+ fts-build-mail.h \
+ fts-plugin.h \
+ fts-search-args.h \
+ fts-search-serialize.h
+
+pkglibexec_PROGRAMS = xml2text
+
+xml2text_SOURCES = xml2text.c fts-parser-html.c
+xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS)
+
+pkglibexec_SCRIPTS = decode2text.sh
+EXTRA_DIST = $(pkglibexec_SCRIPTS)
+
+doveadm_module_LTLIBRARIES = \
+ lib20_doveadm_fts_plugin.la
+
+lib20_doveadm_fts_plugin_la_SOURCES = \
+ doveadm-fts.c \
+ doveadm-dump-fts-expunge-log.c
diff --git a/src/plugins/fts/Makefile.in b/src/plugins/fts/Makefile.in
new file mode 100644
index 0000000..50a11d0
--- /dev/null
+++ b/src/plugins/fts/Makefile.in
@@ -0,0 +1,1140 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = xml2text$(EXEEXT)
+subdir = src/plugins/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 $(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__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" \
+ "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(pkglibexec_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; }; \
+ }
+LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES)
+lib20_doveadm_fts_plugin_la_LIBADD =
+am_lib20_doveadm_fts_plugin_la_OBJECTS = doveadm-fts.lo \
+ doveadm-dump-fts-expunge-log.lo
+lib20_doveadm_fts_plugin_la_OBJECTS = \
+ $(am_lib20_doveadm_fts_plugin_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 =
+lib20_doveadm_fts_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_doveadm_fts_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+lib20_fts_plugin_la_DEPENDENCIES = ../../lib-fts/libfts.la
+am_lib20_fts_plugin_la_OBJECTS = fts-api.lo fts-build-mail.lo \
+ fts-expunge-log.lo fts-indexer.lo fts-parser.lo \
+ fts-parser-html.lo fts-parser-script.lo fts-parser-tika.lo \
+ fts-plugin.lo fts-search.lo fts-search-args.lo \
+ fts-search-serialize.lo fts-storage.lo fts-user.lo
+lib20_fts_plugin_la_OBJECTS = $(am_lib20_fts_plugin_la_OBJECTS)
+lib20_fts_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_fts_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_xml2text_OBJECTS = xml2text-xml2text.$(OBJEXT) \
+ xml2text-fts-parser-html.$(OBJEXT)
+xml2text_OBJECTS = $(am_xml2text_OBJECTS)
+am__DEPENDENCIES_1 =
+SCRIPTS = $(pkglibexec_SCRIPTS)
+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)/doveadm-dump-fts-expunge-log.Plo \
+ ./$(DEPDIR)/doveadm-fts.Plo ./$(DEPDIR)/fts-api.Plo \
+ ./$(DEPDIR)/fts-build-mail.Plo ./$(DEPDIR)/fts-expunge-log.Plo \
+ ./$(DEPDIR)/fts-indexer.Plo ./$(DEPDIR)/fts-parser-html.Plo \
+ ./$(DEPDIR)/fts-parser-script.Plo \
+ ./$(DEPDIR)/fts-parser-tika.Plo ./$(DEPDIR)/fts-parser.Plo \
+ ./$(DEPDIR)/fts-plugin.Plo ./$(DEPDIR)/fts-search-args.Plo \
+ ./$(DEPDIR)/fts-search-serialize.Plo \
+ ./$(DEPDIR)/fts-search.Plo ./$(DEPDIR)/fts-storage.Plo \
+ ./$(DEPDIR)/fts-user.Plo \
+ ./$(DEPDIR)/xml2text-fts-parser-html.Po \
+ ./$(DEPDIR)/xml2text-xml2text.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 = $(lib20_doveadm_fts_plugin_la_SOURCES) \
+ $(lib20_fts_plugin_la_SOURCES) $(xml2text_SOURCES)
+DIST_SOURCES = $(lib20_doveadm_fts_plugin_la_SOURCES) \
+ $(lib20_fts_plugin_la_SOURCES) $(xml2text_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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@
+doveadm_moduledir = $(moduledir)/doveadm
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-fts \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-http \
+ -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/doveadm
+
+lib20_doveadm_fts_plugin_la_LDFLAGS = -module -avoid-version
+lib20_fts_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_fts_plugin.la
+
+lib20_fts_plugin_la_LIBADD = ../../lib-fts/libfts.la
+lib20_fts_plugin_la_SOURCES = \
+ fts-api.c \
+ fts-build-mail.c \
+ fts-expunge-log.c \
+ fts-indexer.c \
+ fts-parser.c \
+ fts-parser-html.c \
+ fts-parser-script.c \
+ fts-parser-tika.c \
+ fts-plugin.c \
+ fts-search.c \
+ fts-search-args.c \
+ fts-search-serialize.c \
+ fts-storage.c \
+ fts-user.c
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ fts-api.h \
+ fts-api-private.h \
+ fts-expunge-log.h \
+ fts-indexer.h \
+ fts-parser.h \
+ fts-storage.h \
+ fts-user.h
+
+noinst_HEADERS = \
+ doveadm-fts.h \
+ fts-build-mail.h \
+ fts-plugin.h \
+ fts-search-args.h \
+ fts-search-serialize.h
+
+xml2text_SOURCES = xml2text.c fts-parser-html.c
+xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS)
+pkglibexec_SCRIPTS = decode2text.sh
+EXTRA_DIST = $(pkglibexec_SCRIPTS)
+doveadm_module_LTLIBRARIES = \
+ lib20_doveadm_fts_plugin.la
+
+lib20_doveadm_fts_plugin_la_SOURCES = \
+ doveadm-fts.c \
+ doveadm-dump-fts-expunge-log.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/plugins/fts/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/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):
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_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)$(doveadm_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \
+ }
+
+uninstall-doveadm_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \
+ done
+
+clean-doveadm_moduleLTLIBRARIES:
+ -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES)
+ @list='$(doveadm_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}; \
+ }
+
+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}; \
+ }
+
+lib20_doveadm_fts_plugin.la: $(lib20_doveadm_fts_plugin_la_OBJECTS) $(lib20_doveadm_fts_plugin_la_DEPENDENCIES) $(EXTRA_lib20_doveadm_fts_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_doveadm_fts_plugin_la_LINK) -rpath $(doveadm_moduledir) $(lib20_doveadm_fts_plugin_la_OBJECTS) $(lib20_doveadm_fts_plugin_la_LIBADD) $(LIBS)
+
+lib20_fts_plugin.la: $(lib20_fts_plugin_la_OBJECTS) $(lib20_fts_plugin_la_DEPENDENCIES) $(EXTRA_lib20_fts_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_fts_plugin_la_LINK) -rpath $(moduledir) $(lib20_fts_plugin_la_OBJECTS) $(lib20_fts_plugin_la_LIBADD) $(LIBS)
+
+xml2text$(EXEEXT): $(xml2text_OBJECTS) $(xml2text_DEPENDENCIES) $(EXTRA_xml2text_DEPENDENCIES)
+ @rm -f xml2text$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(xml2text_OBJECTS) $(xml2text_LDADD) $(LIBS)
+install-pkglibexecSCRIPTS: $(pkglibexec_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_SCRIPTS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_SCRIPTS)'; test -n "$(pkglibexecdir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(pkglibexecdir)'; $(am__uninstall_files_from_dir)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-dump-fts-expunge-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-fts.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-api.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-build-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-expunge-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-indexer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-parser-html.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-parser-script.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-parser-tika.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-search-args.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-search-serialize.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-user.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/xml2text-fts-parser-html.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/xml2text-xml2text.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 $@ $<
+
+xml2text-xml2text.o: xml2text.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT xml2text-xml2text.o -MD -MP -MF $(DEPDIR)/xml2text-xml2text.Tpo -c -o xml2text-xml2text.o `test -f 'xml2text.c' || echo '$(srcdir)/'`xml2text.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/xml2text-xml2text.Tpo $(DEPDIR)/xml2text-xml2text.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='xml2text.c' object='xml2text-xml2text.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) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o xml2text-xml2text.o `test -f 'xml2text.c' || echo '$(srcdir)/'`xml2text.c
+
+xml2text-xml2text.obj: xml2text.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT xml2text-xml2text.obj -MD -MP -MF $(DEPDIR)/xml2text-xml2text.Tpo -c -o xml2text-xml2text.obj `if test -f 'xml2text.c'; then $(CYGPATH_W) 'xml2text.c'; else $(CYGPATH_W) '$(srcdir)/xml2text.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/xml2text-xml2text.Tpo $(DEPDIR)/xml2text-xml2text.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='xml2text.c' object='xml2text-xml2text.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) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o xml2text-xml2text.obj `if test -f 'xml2text.c'; then $(CYGPATH_W) 'xml2text.c'; else $(CYGPATH_W) '$(srcdir)/xml2text.c'; fi`
+
+xml2text-fts-parser-html.o: fts-parser-html.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT xml2text-fts-parser-html.o -MD -MP -MF $(DEPDIR)/xml2text-fts-parser-html.Tpo -c -o xml2text-fts-parser-html.o `test -f 'fts-parser-html.c' || echo '$(srcdir)/'`fts-parser-html.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/xml2text-fts-parser-html.Tpo $(DEPDIR)/xml2text-fts-parser-html.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fts-parser-html.c' object='xml2text-fts-parser-html.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) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o xml2text-fts-parser-html.o `test -f 'fts-parser-html.c' || echo '$(srcdir)/'`fts-parser-html.c
+
+xml2text-fts-parser-html.obj: fts-parser-html.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT xml2text-fts-parser-html.obj -MD -MP -MF $(DEPDIR)/xml2text-fts-parser-html.Tpo -c -o xml2text-fts-parser-html.obj `if test -f 'fts-parser-html.c'; then $(CYGPATH_W) 'fts-parser-html.c'; else $(CYGPATH_W) '$(srcdir)/fts-parser-html.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/xml2text-fts-parser-html.Tpo $(DEPDIR)/xml2text-fts-parser-html.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fts-parser-html.c' object='xml2text-fts-parser-html.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) $(xml2text_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o xml2text-fts-parser-html.obj `if test -f 'fts-parser-html.c'; then $(CYGPATH_W) 'fts-parser-html.c'; else $(CYGPATH_W) '$(srcdir)/fts-parser-html.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(SCRIPTS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(pkglibexecdir)" "$(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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/doveadm-dump-fts-expunge-log.Plo
+ -rm -f ./$(DEPDIR)/doveadm-fts.Plo
+ -rm -f ./$(DEPDIR)/fts-api.Plo
+ -rm -f ./$(DEPDIR)/fts-build-mail.Plo
+ -rm -f ./$(DEPDIR)/fts-expunge-log.Plo
+ -rm -f ./$(DEPDIR)/fts-indexer.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-html.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-script.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-tika.Plo
+ -rm -f ./$(DEPDIR)/fts-parser.Plo
+ -rm -f ./$(DEPDIR)/fts-plugin.Plo
+ -rm -f ./$(DEPDIR)/fts-search-args.Plo
+ -rm -f ./$(DEPDIR)/fts-search-serialize.Plo
+ -rm -f ./$(DEPDIR)/fts-search.Plo
+ -rm -f ./$(DEPDIR)/fts-storage.Plo
+ -rm -f ./$(DEPDIR)/fts-user.Plo
+ -rm -f ./$(DEPDIR)/xml2text-fts-parser-html.Po
+ -rm -f ./$(DEPDIR)/xml2text-xml2text.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-doveadm_moduleLTLIBRARIES \
+ install-moduleLTLIBRARIES install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS install-pkglibexecSCRIPTS
+
+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)/doveadm-dump-fts-expunge-log.Plo
+ -rm -f ./$(DEPDIR)/doveadm-fts.Plo
+ -rm -f ./$(DEPDIR)/fts-api.Plo
+ -rm -f ./$(DEPDIR)/fts-build-mail.Plo
+ -rm -f ./$(DEPDIR)/fts-expunge-log.Plo
+ -rm -f ./$(DEPDIR)/fts-indexer.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-html.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-script.Plo
+ -rm -f ./$(DEPDIR)/fts-parser-tika.Plo
+ -rm -f ./$(DEPDIR)/fts-parser.Plo
+ -rm -f ./$(DEPDIR)/fts-plugin.Plo
+ -rm -f ./$(DEPDIR)/fts-search-args.Plo
+ -rm -f ./$(DEPDIR)/fts-search-serialize.Plo
+ -rm -f ./$(DEPDIR)/fts-search.Plo
+ -rm -f ./$(DEPDIR)/fts-storage.Plo
+ -rm -f ./$(DEPDIR)/fts-user.Plo
+ -rm -f ./$(DEPDIR)/xml2text-fts-parser-html.Po
+ -rm -f ./$(DEPDIR)/xml2text-xml2text.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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS uninstall-pkglibexecSCRIPTS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-pkglibexecPROGRAMS 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-doveadm_moduleLTLIBRARIES 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-pkglibexecPROGRAMS \
+ install-pkglibexecSCRIPTS 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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS uninstall-pkglibexecSCRIPTS
+
+.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/plugins/fts/decode2text.sh b/src/plugins/fts/decode2text.sh
new file mode 100755
index 0000000..1c881ff
--- /dev/null
+++ b/src/plugins/fts/decode2text.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+
+# Example attachment decoder script. The attachment comes from stdin, and
+# the script is expected to output UTF-8 data to stdout. (If the output isn't
+# UTF-8, everything except valid UTF-8 sequences are dropped from it.)
+
+# The attachment decoding is enabled by setting:
+#
+# plugin {
+# fts_decoder = decode2text
+# }
+# service decode2text {
+# executable = script /usr/local/libexec/dovecot/decode2text.sh
+# user = dovecot
+# unix_listener decode2text {
+# mode = 0666
+# }
+# }
+
+libexec_dir=`dirname $0`
+content_type=$1
+
+# The second parameter is the format's filename extension, which is used when
+# found from a filename of application/octet-stream. You can also add more
+# extensions by giving more parameters.
+formats='application/pdf pdf
+application/x-pdf pdf
+application/msword doc
+application/mspowerpoint ppt
+application/vnd.ms-powerpoint ppt
+application/ms-excel xls
+application/x-msexcel xls
+application/vnd.ms-excel xls
+application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
+application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
+application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
+application/vnd.oasis.opendocument.text odt
+application/vnd.oasis.opendocument.spreadsheet ods
+application/vnd.oasis.opendocument.presentation odp
+'
+
+if [ "$content_type" = "" ]; then
+ echo "$formats"
+ exit 0
+fi
+
+fmt=`echo "$formats" | grep -w "^$content_type" | cut -d ' ' -f 2`
+if [ "$fmt" = "" ]; then
+ echo "Content-Type: $content_type not supported" >&2
+ exit 1
+fi
+
+# most decoders can't handle stdin directly, so write the attachment
+# to a temp file
+path=`mktemp`
+trap "rm -f $path" 0 1 2 3 14 15
+cat > $path
+
+xmlunzip() {
+ name=$1
+
+ tempdir=`mktemp -d`
+ if [ "$tempdir" = "" ]; then
+ exit 1
+ fi
+ trap "rm -rf $path $tempdir" 0 1 2 3 14 15
+ cd $tempdir || exit 1
+ unzip -q "$path" 2>/dev/null || exit 0
+ find . -name "$name" -print0 | xargs -0 cat |
+ $libexec_dir/xml2text
+}
+
+wait_timeout() {
+ childpid=$!
+ trap "kill -9 $childpid; rm -f $path" 1 2 3 14 15
+ wait $childpid
+}
+
+LANG=en_US.UTF-8
+export LANG
+if [ $fmt = "pdf" ]; then
+ /usr/bin/pdftotext $path - 2>/dev/null&
+ wait_timeout 2>/dev/null
+elif [ $fmt = "doc" ]; then
+ (/usr/bin/catdoc $path; true) 2>/dev/null&
+ wait_timeout 2>/dev/null
+elif [ $fmt = "ppt" ]; then
+ (/usr/bin/catppt $path; true) 2>/dev/null&
+ wait_timeout 2>/dev/null
+elif [ $fmt = "xls" ]; then
+ (/usr/bin/xls2csv $path; true) 2>/dev/null&
+ wait_timeout 2>/dev/null
+elif [ $fmt = "odt" -o $fmt = "ods" -o $fmt = "odp" ]; then
+ xmlunzip "content.xml"
+elif [ $fmt = "docx" ]; then
+ xmlunzip "document.xml"
+elif [ $fmt = "xlsx" ]; then
+ xmlunzip "sharedStrings.xml"
+elif [ $fmt = "pptx" ]; then
+ xmlunzip "slide*.xml"
+else
+ echo "Buggy decoder script: $fmt not handled" >&2
+ exit 1
+fi
+exit 0
diff --git a/src/plugins/fts/doveadm-dump-fts-expunge-log.c b/src/plugins/fts/doveadm-dump-fts-expunge-log.c
new file mode 100644
index 0000000..7438bca
--- /dev/null
+++ b/src/plugins/fts/doveadm-dump-fts-expunge-log.c
@@ -0,0 +1,116 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "guid.h"
+#include "doveadm-dump.h"
+#include "doveadm-fts.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+struct fts_expunge_log_record {
+ uint32_t checksum;
+ uint32_t record_size;
+ guid_128_t guid;
+};
+
+static int dump_record(int fd, buffer_t *buf)
+{
+ struct fts_expunge_log_record rec;
+ off_t offset;
+ void *data;
+ const uint32_t *expunges, *uids;
+ ssize_t ret;
+ size_t data_size;
+ unsigned int i, uids_count;
+
+ offset = lseek(fd, 0, SEEK_CUR);
+
+ ret = read(fd, &rec, sizeof(rec));
+ if (ret == 0)
+ return 0;
+
+ if (ret != sizeof(rec))
+ i_fatal("rec read() %d != %d", (int)ret, (int)sizeof(rec));
+
+ if (rec.record_size < sizeof(rec) + sizeof(uint32_t) ||
+ rec.record_size > INT_MAX) {
+ i_fatal("Invalid record_size=%u at offset %"PRIuUOFF_T,
+ rec.record_size, offset);
+ }
+ data_size = rec.record_size - sizeof(rec);
+ buffer_set_used_size(buf, 0);
+ data = buffer_append_space_unsafe(buf, data_size);
+ ret = read(fd, data, data_size);
+ if (ret != (ssize_t)data_size)
+ i_fatal("rec read() %d != %d", (int)ret, (int)data_size);
+
+ printf("#%"PRIuUOFF_T":\n", offset);
+ printf(" checksum = %8x\n", rec.checksum);
+ printf(" size .... = %u\n", rec.record_size);
+ printf(" mailbox . = %s\n", guid_128_to_string(rec.guid));
+
+ expunges = CONST_PTR_OFFSET(data, data_size - sizeof(uint32_t));
+ printf(" expunges = %u\n", *expunges);
+
+ printf(" uids .... = ");
+
+ uids = data;
+ uids_count = (rec.record_size - sizeof(rec) - sizeof(uint32_t)) /
+ sizeof(uint32_t);
+ for (i = 0; i < uids_count; i += 2) {
+ if (i != 0)
+ printf(",");
+ if (uids[i] == uids[i+1])
+ printf("%u", uids[i]);
+ else
+ printf("%u-%u", uids[i], uids[i+1]);
+ }
+ printf("\n");
+ return 1;
+}
+
+static void
+cmd_dump_fts_expunge_log(const char *path, const char *const *args ATTR_UNUSED)
+{
+ buffer_t *buf;
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(%s) failed: %m", path);
+
+ buf = buffer_create_dynamic(default_pool, 1024);
+ do {
+ T_BEGIN {
+ ret = dump_record(fd, buf);
+ } T_END;
+ } while (ret > 0);
+ buffer_free(&buf);
+ i_close_fd(&fd);
+}
+
+static bool test_dump_fts_expunge_log(const char *path)
+{
+ const char *p;
+
+ if ((p = strrchr(path, '/')) != NULL)
+ p++;
+ else
+ p = path;
+ return strcmp(p, "dovecot-expunges.log") == 0;
+}
+
+static const struct doveadm_cmd_dump doveadm_cmd_dump_fts_expunge_log = {
+ "fts-expunge-log",
+ test_dump_fts_expunge_log,
+ cmd_dump_fts_expunge_log
+};
+
+void doveadm_dump_fts_expunge_log_init(void)
+{
+ doveadm_dump_register(&doveadm_cmd_dump_fts_expunge_log);
+}
diff --git a/src/plugins/fts/doveadm-fts.c b/src/plugins/fts/doveadm-fts.c
new file mode 100644
index 0000000..a96c739
--- /dev/null
+++ b/src/plugins/fts/doveadm-fts.c
@@ -0,0 +1,472 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-util.h"
+#include "mail-namespace.h"
+#include "mail-search.h"
+#include "mailbox-list-iter.h"
+#include "fts-tokenizer.h"
+#include "fts-filter.h"
+#include "fts-language.h"
+#include "fts-storage.h"
+#include "fts-search-args.h"
+#include "fts-user.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+#include "doveadm-mailbox-list-iter.h"
+#include "doveadm-fts.h"
+
+const char *doveadm_fts_plugin_version = DOVECOT_ABI_VERSION;
+
+struct fts_tokenize_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+ const char *language;
+ const char *tokens;
+};
+
+static int
+cmd_search_box(struct doveadm_mail_cmd_context *ctx,
+ const struct mailbox_info *info)
+{
+ struct mailbox *box;
+ struct fts_backend *backend;
+ struct fts_result result;
+ int ret = 0;
+
+ backend = fts_list_backend(info->ns->list);
+ if (backend == NULL) {
+ i_error("fts not enabled for %s", info->vname);
+ ctx->exit_code = EX_CONFIG;
+ return -1;
+ }
+
+ i_zero(&result);
+ result.pool = pool_alloconly_create("doveadm", 512);
+ i_array_init(&result.definite_uids, 16);
+ i_array_init(&result.maybe_uids, 16);
+ i_array_init(&result.scores, 16);
+
+ box = mailbox_alloc(info->ns->list, info->vname, 0);
+ if (fts_backend_lookup(backend, box, ctx->search_args->args,
+ FTS_LOOKUP_FLAG_AND_ARGS, &result) < 0) {
+ i_error("fts lookup failed");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ ret = -1;
+ } else {
+ printf("%s: ", info->vname);
+ if (array_count(&result.definite_uids) == 0)
+ printf("no results\n");
+ else T_BEGIN {
+ string_t *str = t_str_new(128);
+ imap_write_seq_range(str, &result.definite_uids);
+ printf("%s\n", str_c(str));
+ } T_END;
+ if (array_count(&result.maybe_uids) > 0) T_BEGIN {
+ string_t *str = t_str_new(128);
+ imap_write_seq_range(str, &result.maybe_uids);
+ printf(" - maybe: %s\n", str_c(str));
+ } T_END;
+ fts_backend_lookup_done(backend);
+ }
+ mailbox_free(&box);
+ array_free(&result.definite_uids);
+ array_free(&result.maybe_uids);
+ array_free(&result.scores);
+ pool_unref(&result.pool);
+ return ret;
+}
+
+static int
+cmd_fts_lookup_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct doveadm_mailbox_list_iter *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = doveadm_mailbox_list_iter_init(ctx, user, ctx->search_args,
+ iter_flags);
+ while ((info = doveadm_mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ if (cmd_search_box(ctx, info) < 0)
+ ret = -1;
+ } T_END;
+ if (doveadm_mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static void
+cmd_fts_lookup_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("fts lookup");
+
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_fts_lookup_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_fts_lookup_run;
+ ctx->v.init = cmd_fts_lookup_init;
+ return ctx;
+}
+
+static int
+cmd_fts_expand_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box;
+ struct fts_backend *backend;
+ string_t *str = t_str_new(128);
+
+ backend = fts_list_backend(ns->list);
+ if (backend == NULL) {
+ i_error("fts not enabled for INBOX");
+ ctx->exit_code = EX_CONFIG;
+ return -1;
+ }
+
+ box = mailbox_alloc(ns->list, "INBOX", 0);
+ mail_search_args_init(ctx->search_args, box, FALSE, NULL);
+
+ if (fts_search_args_expand(backend, ctx->search_args) < 0)
+ i_fatal("Couldn't expand search args");
+ mail_search_args_to_cmdline(str, ctx->search_args->args);
+ printf("%s\n", str_c(str));
+ mailbox_free(&box);
+ return 0;
+}
+
+static void
+cmd_fts_expand_init(struct doveadm_mail_cmd_context *ctx,
+ const char *const args[])
+{
+ if (args[0] == NULL)
+ doveadm_mail_help_name("fts expand");
+
+ ctx->search_args = doveadm_mail_build_search_args(args);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_fts_expand_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_fts_expand_run;
+ ctx->v.init = cmd_fts_expand_init;
+ return ctx;
+}
+
+static int
+cmd_fts_tokenize_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct fts_tokenize_cmd_context *ctx =
+ (struct fts_tokenize_cmd_context *)_ctx;
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct fts_backend *backend;
+ struct fts_user_language *user_lang;
+ const struct fts_language *lang = NULL;
+ int ret, ret2;
+ bool final = FALSE;
+
+ backend = fts_list_backend(ns->list);
+ if (backend == NULL) {
+ i_error("fts not enabled for INBOX");
+ _ctx->exit_code = EX_CONFIG;
+ return -1;
+ }
+
+ if (ctx->language == NULL) {
+ struct fts_language_list *lang_list =
+ fts_user_get_language_list(user);
+ enum fts_language_result result;
+ const char *error;
+
+ result = fts_language_detect(lang_list,
+ (const unsigned char *)ctx->tokens, strlen(ctx->tokens),
+ &lang, &error);
+ if (lang == NULL)
+ lang = fts_language_list_get_first(lang_list);
+ switch (result) {
+ case FTS_LANGUAGE_RESULT_SHORT:
+ i_warning("Text too short, can't detect its language - assuming %s", lang->name);
+ break;
+ case FTS_LANGUAGE_RESULT_UNKNOWN:
+ i_warning("Can't detect its language - assuming %s", lang->name);
+ break;
+ case FTS_LANGUAGE_RESULT_OK:
+ break;
+ case FTS_LANGUAGE_RESULT_ERROR:
+ i_error("Language detection library initialization failed: %s", error);
+ _ctx->exit_code = EX_CONFIG;
+ return -1;
+ default:
+ i_unreached();
+ }
+ } else {
+ lang = fts_language_find(ctx->language);
+ if (lang == NULL) {
+ i_error("Unknown language: %s", ctx->language);
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ }
+ user_lang = fts_user_language_find(user, lang);
+ if (user_lang == NULL) {
+ i_error("Language not enabled for user: %s", ctx->language);
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+
+ fts_tokenizer_reset(user_lang->index_tokenizer);
+ for (;;) {
+ const char *token, *error;
+
+ if (!final) {
+ ret = fts_tokenizer_next(user_lang->index_tokenizer,
+ (const unsigned char *)ctx->tokens, strlen(ctx->tokens),
+ &token, &error);
+ } else {
+ ret = fts_tokenizer_final(user_lang->index_tokenizer,
+ &token, &error);
+ }
+ if (ret < 0)
+ break;
+ if (ret > 0 && user_lang->filter != NULL) {
+ ret2 = fts_filter_filter(user_lang->filter, &token, &error);
+ if (ret2 > 0)
+ doveadm_print(token);
+ else if (ret2 < 0)
+ i_error("Couldn't create indexable tokens: %s", error);
+ }
+ if (ret == 0) {
+ if (final)
+ break;
+ final = TRUE;
+ }
+ }
+ return 0;
+}
+
+static void
+cmd_fts_tokenize_init(struct doveadm_mail_cmd_context *_ctx,
+ const char *const args[])
+{
+ struct fts_tokenize_cmd_context *ctx =
+ (struct fts_tokenize_cmd_context *)_ctx;
+
+ if (args[0] == NULL)
+ doveadm_mail_help_name("fts tokenize");
+
+ ctx->tokens = p_strdup(_ctx->pool, t_strarray_join(args, " "));
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FLOW);
+ doveadm_print_header("token", "token", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+}
+
+static bool
+cmd_fts_tokenize_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct fts_tokenize_cmd_context *ctx =
+ (struct fts_tokenize_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'l':
+ ctx->language = p_strdup(_ctx->pool, optarg);
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_fts_tokenize_alloc(void)
+{
+ struct fts_tokenize_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct fts_tokenize_cmd_context);
+ ctx->ctx.v.run = cmd_fts_tokenize_run;
+ ctx->ctx.v.init = cmd_fts_tokenize_init;
+ ctx->ctx.v.parse_arg = cmd_fts_tokenize_parse_arg;
+ ctx->ctx.getopt_args = "l";
+ return &ctx->ctx;
+}
+
+static int
+fts_namespace_find(struct mail_user *user, const char *ns_prefix,
+ struct mail_namespace **ns_r)
+{
+ struct mail_namespace *ns;
+
+ if (ns_prefix == NULL)
+ ns = mail_namespace_find_inbox(user->namespaces);
+ else {
+ ns = mail_namespace_find_prefix(user->namespaces, ns_prefix);
+ if (ns == NULL) {
+ i_error("Namespace prefix not found: %s", ns_prefix);
+ return -1;
+ }
+ }
+
+ if (fts_list_backend(ns->list) == NULL) {
+ i_error("fts not enabled for user's namespace %s",
+ ns_prefix != NULL ? ns_prefix : "INBOX");
+ return -1;
+ }
+ *ns_r = ns;
+ return 0;
+}
+
+static int
+cmd_fts_optimize_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ const char *ns_prefix = ctx->args[0];
+ struct mail_namespace *ns;
+ struct fts_backend *backend;
+
+ if (fts_namespace_find(user, ns_prefix, &ns) < 0) {
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+ backend = fts_list_backend(ns->list);
+ if (fts_backend_optimize(backend) < 0) {
+ i_error("fts optimize failed");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+cmd_fts_optimize_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (str_array_length(args) > 1)
+ doveadm_mail_help_name("fts optimize");
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_fts_optimize_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_fts_optimize_run;
+ ctx->v.init = cmd_fts_optimize_init;
+ return ctx;
+}
+
+static int
+cmd_fts_rescan_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
+{
+ const char *ns_prefix = ctx->args[0];
+ struct mail_namespace *ns;
+ struct fts_backend *backend;
+
+ if (fts_namespace_find(user, ns_prefix, &ns) < 0) {
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+ backend = fts_list_backend(ns->list);
+ if (fts_backend_rescan(backend) < 0) {
+ i_error("fts rescan failed");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+cmd_fts_rescan_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[])
+{
+ if (str_array_length(args) > 1)
+ doveadm_mail_help_name("fts rescan");
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_fts_rescan_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_fts_rescan_run;
+ ctx->v.init = cmd_fts_rescan_init;
+ return ctx;
+}
+
+static struct doveadm_cmd_ver2 fts_commands[] = {
+{
+ .name = "fts lookup",
+ .mail_cmd = cmd_fts_lookup_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fts expand",
+ .mail_cmd = cmd_fts_expand_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<search query>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "query", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fts tokenize",
+ .mail_cmd = cmd_fts_tokenize_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<text>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('l', "language", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "text", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fts optimize",
+ .mail_cmd = cmd_fts_optimize_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[<namespace>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "namespace", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fts rescan",
+ .mail_cmd = cmd_fts_rescan_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[<namespace>]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('\0', "namespace", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+};
+
+void doveadm_fts_plugin_init(struct module *module ATTR_UNUSED)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(fts_commands); i++)
+ doveadm_cmd_register_ver2(&fts_commands[i]);
+ doveadm_dump_fts_expunge_log_init();
+}
+
+void doveadm_fts_plugin_deinit(void)
+{
+}
diff --git a/src/plugins/fts/doveadm-fts.h b/src/plugins/fts/doveadm-fts.h
new file mode 100644
index 0000000..d4307fe
--- /dev/null
+++ b/src/plugins/fts/doveadm-fts.h
@@ -0,0 +1,11 @@
+#ifndef DOVEADM_FTS_H
+#define DOVEADM_FTS_H
+
+struct module;
+
+void doveadm_dump_fts_expunge_log_init(void);
+
+void doveadm_fts_plugin_init(struct module *module);
+void doveadm_fts_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fts/fts-api-private.h b/src/plugins/fts/fts-api-private.h
new file mode 100644
index 0000000..a070564
--- /dev/null
+++ b/src/plugins/fts/fts-api-private.h
@@ -0,0 +1,139 @@
+#ifndef FTS_API_PRIVATE_H
+#define FTS_API_PRIVATE_H
+
+#include "unichar.h"
+#include "fts-api.h"
+
+struct mail_user;
+struct mailbox_list;
+
+#define MAILBOX_GUID_HEX_LENGTH (GUID_128_SIZE*2)
+
+struct fts_backend_vfuncs {
+ struct fts_backend *(*alloc)(void);
+ int (*init)(struct fts_backend *backend, const char **error_r);
+ void (*deinit)(struct fts_backend *backend);
+
+ int (*get_last_uid)(struct fts_backend *backend, struct mailbox *box,
+ uint32_t *last_uid_r);
+
+ struct fts_backend_update_context *
+ (*update_init)(struct fts_backend *backend);
+ int (*update_deinit)(struct fts_backend_update_context *ctx);
+
+ void (*update_set_mailbox)(struct fts_backend_update_context *ctx,
+ struct mailbox *box);
+ void (*update_expunge)(struct fts_backend_update_context *ctx,
+ uint32_t uid);
+
+ /* Start a build for specified key */
+ bool (*update_set_build_key)(struct fts_backend_update_context *ctx,
+ const struct fts_backend_build_key *key);
+ /* Finish a build for specified key - guaranteed to be called */
+ void (*update_unset_build_key)(struct fts_backend_update_context *ctx);
+ /* Add data for current build key */
+ int (*update_build_more)(struct fts_backend_update_context *ctx,
+ const unsigned char *data, size_t size);
+
+ int (*refresh)(struct fts_backend *backend);
+ int (*rescan)(struct fts_backend *backend);
+ int (*optimize)(struct fts_backend *backend);
+
+ bool (*can_lookup)(struct fts_backend *backend,
+ const struct mail_search_arg *args);
+ int (*lookup)(struct fts_backend *backend, struct mailbox *box,
+ struct mail_search_arg *args, enum fts_lookup_flags flags,
+ struct fts_result *result);
+ int (*lookup_multi)(struct fts_backend *backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result);
+ void (*lookup_done)(struct fts_backend *backend);
+};
+
+enum fts_backend_flags {
+ /* Backend supports indexing binary MIME parts */
+ FTS_BACKEND_FLAG_BINARY_MIME_PARTS = 0x01,
+ /* Send built text to backend normalized rather than
+ preserving original case */
+ FTS_BACKEND_FLAG_NORMALIZE_INPUT = 0x02,
+ /* Send only fully indexable words rather than randomly sized blocks */
+ FTS_BACKEND_FLAG_BUILD_FULL_WORDS = 0x04,
+ /* Fuzzy search works */
+ FTS_BACKEND_FLAG_FUZZY_SEARCH = 0x08,
+ /* Tokenize all the input. update_build_more() will be called a single
+ directly indexable token at a time. Searching will modify the search
+ args so that lookup() sees only tokens that can be directly
+ searched. */
+ FTS_BACKEND_FLAG_TOKENIZED_INPUT = 0x10
+};
+
+struct fts_header_filters {
+ pool_t pool;
+ ARRAY_TYPE(const_string) includes;
+ ARRAY_TYPE(const_string) excludes;
+ bool loaded:1;
+ bool exclude_is_default:1;
+};
+
+struct fts_backend {
+ const char *name;
+ enum fts_backend_flags flags;
+
+ struct fts_backend_vfuncs v;
+ struct mail_namespace *ns;
+ struct fts_header_filters header_filters;
+
+ bool updating:1;
+};
+
+struct fts_backend_update_context {
+ struct fts_backend *backend;
+ normalizer_func_t *normalizer;
+
+ struct mailbox *cur_box, *backend_box;
+
+ bool build_key_open:1;
+ bool failed:1;
+};
+
+struct fts_index_header {
+ uint32_t last_indexed_uid;
+
+ /* Checksum of settings. If the settings change, the index should
+ be rebuilt. */
+ uint32_t settings_checksum;
+ uint32_t unused;
+};
+
+void fts_backend_register(const struct fts_backend *backend);
+void fts_backend_unregister(const char *name);
+
+bool fts_backend_default_can_lookup(struct fts_backend *backend,
+ const struct mail_search_arg *args);
+
+void fts_filter_uids(ARRAY_TYPE(seq_range) *definite_dest,
+ const ARRAY_TYPE(seq_range) *definite_filter,
+ ARRAY_TYPE(seq_range) *maybe_dest,
+ const ARRAY_TYPE(seq_range) *maybe_filter);
+
+/* Returns TRUE if ok, FALSE if no fts header */
+bool fts_index_get_header(struct mailbox *box, struct fts_index_header *hdr_r);
+int fts_index_set_header(struct mailbox *box,
+ const struct fts_index_header *hdr);
+int ATTR_NOWARN_UNUSED_RESULT
+fts_index_set_last_uid(struct mailbox *box, uint32_t last_uid);
+int fts_backend_reset_last_uids(struct fts_backend *backend);
+int fts_index_have_compatible_settings(struct mailbox_list *list,
+ uint32_t checksum);
+
+/* Returns TRUE if FTS backend should index the header for optimizing
+ separate lookups */
+bool fts_header_want_indexed(const char *hdr_name);
+/* Returns TRUE if header's values should be considered to have a language. */
+bool fts_header_has_language(const char *hdr_name);
+
+int fts_mailbox_get_guid(struct mailbox *box, const char **guid_r);
+
+#endif
diff --git a/src/plugins/fts/fts-api.c b/src/plugins/fts/fts-api.c
new file mode 100644
index 0000000..a6ea716
--- /dev/null
+++ b/src/plugins/fts/fts-api.c
@@ -0,0 +1,554 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hex-binary.h"
+#include "mail-index.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-iter.h"
+#include "mail-search.h"
+#include "fts-api-private.h"
+
+struct event_category event_category_fts = {
+ .name = "fts",
+};
+
+static ARRAY(const struct fts_backend *) backends;
+
+void fts_backend_register(const struct fts_backend *backend)
+{
+ if (!array_is_created(&backends))
+ i_array_init(&backends, 4);
+ array_push_back(&backends, &backend);
+}
+
+void fts_backend_unregister(const char *name)
+{
+ const struct fts_backend *const *be;
+ unsigned int i, count;
+
+ be = array_get(&backends, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(be[i]->name, name) == 0) {
+ array_delete(&backends, i, 1);
+ break;
+ }
+ }
+ if (i == count)
+ i_panic("fts_backend_unregister(%s): unknown backend", name);
+
+ if (count == 1)
+ array_free(&backends);
+}
+
+static const struct fts_backend *
+fts_backend_class_lookup(const char *backend_name)
+{
+ const struct fts_backend *const *be;
+ unsigned int i, count;
+
+ if (array_is_created(&backends)) {
+ be = array_get(&backends, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(be[i]->name, backend_name) == 0)
+ return be[i];
+ }
+ }
+ return NULL;
+}
+
+static void
+fts_header_filters_init(struct fts_backend *backend)
+{
+ struct fts_header_filters *filters = &backend->header_filters;
+ pool_t pool = filters->pool = pool_alloconly_create(
+ MEMPOOL_GROWING"fts_header_filters", 256);
+
+ p_array_init(&filters->includes, pool, 8);
+ p_array_init(&filters->excludes, pool, 8);
+}
+
+static void
+fts_header_filters_deinit(struct fts_backend *backend)
+{
+ pool_unref(&backend->header_filters.pool);
+}
+
+int fts_backend_init(const char *backend_name, struct mail_namespace *ns,
+ const char **error_r, struct fts_backend **backend_r)
+{
+ const struct fts_backend *be;
+ struct fts_backend *backend;
+
+ be = fts_backend_class_lookup(backend_name);
+ if (be == NULL) {
+ *error_r = "Unknown backend";
+ return -1;
+ }
+
+ backend = be->v.alloc();
+ backend->ns = ns;
+ if (backend->v.init(backend, error_r) < 0) {
+ i_free(backend);
+ return -1;
+ }
+
+ fts_header_filters_init(backend);
+ *backend_r = backend;
+ return 0;
+}
+
+void fts_backend_deinit(struct fts_backend **_backend)
+{
+ struct fts_backend *backend = *_backend;
+
+ fts_header_filters_deinit(backend);
+ *_backend = NULL;
+ backend->v.deinit(backend);
+}
+
+int fts_backend_get_last_uid(struct fts_backend *backend, struct mailbox *box,
+ uint32_t *last_uid_r)
+{
+ struct fts_index_header hdr;
+
+ if (box->virtual_vfuncs != NULL) {
+ /* virtual mailboxes themselves don't have any indexes,
+ so catch this call here */
+ if (!fts_index_get_header(box, &hdr))
+ *last_uid_r = 0;
+ else
+ *last_uid_r = hdr.last_indexed_uid;
+ return 0;
+ }
+
+ return backend->v.get_last_uid(backend, box, last_uid_r);
+}
+
+bool fts_backend_is_updating(struct fts_backend *backend)
+{
+ return backend->updating;
+}
+
+struct fts_backend_update_context *
+fts_backend_update_init(struct fts_backend *backend)
+{
+ struct fts_backend_update_context *ctx;
+
+ i_assert(!backend->updating);
+
+ backend->updating = TRUE;
+ ctx = backend->v.update_init(backend);
+ if ((backend->flags & FTS_BACKEND_FLAG_NORMALIZE_INPUT) != 0)
+ ctx->normalizer = backend->ns->user->default_normalizer;
+ return ctx;
+}
+
+static void fts_backend_set_cur_mailbox(struct fts_backend_update_context *ctx)
+{
+ fts_backend_update_unset_build_key(ctx);
+ if (ctx->backend_box != ctx->cur_box) {
+ ctx->backend->v.update_set_mailbox(ctx, ctx->cur_box);
+ ctx->backend_box = ctx->cur_box;
+ }
+}
+
+int fts_backend_update_deinit(struct fts_backend_update_context **_ctx)
+{
+ struct fts_backend_update_context *ctx = *_ctx;
+ struct fts_backend *backend = ctx->backend;
+ int ret;
+
+ *_ctx = NULL;
+
+ ctx->cur_box = NULL;
+ fts_backend_set_cur_mailbox(ctx);
+
+ ret = backend->v.update_deinit(ctx);
+ backend->updating = FALSE;
+ return ret;
+}
+
+void fts_backend_update_set_mailbox(struct fts_backend_update_context *ctx,
+ struct mailbox *box)
+{
+ if (ctx->backend_box != NULL && box != ctx->backend_box) {
+ /* make sure we don't reference the backend box anymore */
+ ctx->backend->v.update_set_mailbox(ctx, NULL);
+ ctx->backend_box = NULL;
+ }
+ ctx->cur_box = box;
+}
+
+void fts_backend_update_expunge(struct fts_backend_update_context *ctx,
+ uint32_t uid)
+{
+ fts_backend_set_cur_mailbox(ctx);
+ ctx->backend->v.update_expunge(ctx, uid);
+}
+
+bool fts_backend_update_set_build_key(struct fts_backend_update_context *ctx,
+ const struct fts_backend_build_key *key)
+{
+ fts_backend_set_cur_mailbox(ctx);
+
+ i_assert(ctx->cur_box != NULL);
+
+ if (!ctx->backend->v.update_set_build_key(ctx, key))
+ return FALSE;
+ ctx->build_key_open = TRUE;
+ return TRUE;
+}
+
+void fts_backend_update_unset_build_key(struct fts_backend_update_context *ctx)
+{
+ if (ctx->build_key_open) {
+ ctx->backend->v.update_unset_build_key(ctx);
+ ctx->build_key_open = FALSE;
+ }
+}
+
+int fts_backend_update_build_more(struct fts_backend_update_context *ctx,
+ const unsigned char *data, size_t size)
+{
+ i_assert(ctx->build_key_open);
+
+ return ctx->backend->v.update_build_more(ctx, data, size);
+}
+
+int fts_backend_refresh(struct fts_backend *backend)
+{
+ return backend->v.refresh(backend);
+}
+
+int fts_backend_reset_last_uids(struct fts_backend *backend)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ struct mailbox *box;
+ int ret = 0;
+
+ iter = mailbox_list_iter_init(backend->ns->list, "*",
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags &
+ (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0)
+ continue;
+
+ box = mailbox_alloc(info->ns->list, info->vname, 0);
+ if (mailbox_open(box) == 0) {
+ if (fts_index_set_last_uid(box, 0) < 0)
+ ret = -1;
+ }
+ mailbox_free(&box);
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+int fts_backend_rescan(struct fts_backend *backend)
+{
+ struct mailbox *box;
+ bool virtual_storage;
+
+ box = mailbox_alloc(backend->ns->list, "", 0);
+ virtual_storage = box->virtual_vfuncs != NULL;
+ mailbox_free(&box);
+
+ if (virtual_storage) {
+ /* just reset the last-uids for a virtual storage. */
+ return fts_backend_reset_last_uids(backend);
+ }
+
+ return backend->v.rescan == NULL ? 0 :
+ backend->v.rescan(backend);
+}
+
+int fts_backend_optimize(struct fts_backend *backend)
+{
+ return backend->v.optimize == NULL ? 0 :
+ backend->v.optimize(backend);
+}
+
+static void
+fts_merge_maybies(ARRAY_TYPE(seq_range) *dest_maybe,
+ const ARRAY_TYPE(seq_range) *dest_definite,
+ const ARRAY_TYPE(seq_range) *src_maybe,
+ const ARRAY_TYPE(seq_range) *src_definite)
+{
+ ARRAY_TYPE(seq_range) src_unwanted;
+ const struct seq_range *range;
+ struct seq_range new_range;
+ unsigned int i, count;
+ uint32_t seq;
+
+ /* add/leave to dest_maybe if at least one list has maybe,
+ and no lists have none */
+
+ /* create unwanted sequences list from both sources */
+ t_array_init(&src_unwanted, 128);
+ new_range.seq1 = 0; new_range.seq2 = (uint32_t)-1;
+ array_push_back(&src_unwanted, &new_range);
+ seq_range_array_remove_seq_range(&src_unwanted, src_maybe);
+ seq_range_array_remove_seq_range(&src_unwanted, src_definite);
+
+ /* drop unwanted uids */
+ seq_range_array_remove_seq_range(dest_maybe, &src_unwanted);
+
+ /* add uids that are in dest_definite and src_maybe lists */
+ range = array_get(dest_definite, &count);
+ for (i = 0; i < count; i++) {
+ for (seq = range[i].seq1; seq <= range[i].seq2; seq++) {
+ if (seq_range_exists(src_maybe, seq))
+ seq_range_array_add(dest_maybe, seq);
+ }
+ }
+}
+
+void fts_filter_uids(ARRAY_TYPE(seq_range) *definite_dest,
+ const ARRAY_TYPE(seq_range) *definite_filter,
+ ARRAY_TYPE(seq_range) *maybe_dest,
+ const ARRAY_TYPE(seq_range) *maybe_filter)
+{
+ T_BEGIN {
+ fts_merge_maybies(maybe_dest, definite_dest,
+ maybe_filter, definite_filter);
+ } T_END;
+ /* keep only what exists in both lists. the rest is in
+ maybies or not wanted */
+ seq_range_array_intersect(definite_dest, definite_filter);
+}
+
+bool fts_backend_default_can_lookup(struct fts_backend *backend,
+ const struct mail_search_arg *args)
+{
+ for (; args != NULL; args = args->next) {
+ switch (args->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (fts_backend_default_can_lookup(backend,
+ args->value.subargs))
+ return TRUE;
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ if (!args->no_fts)
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return FALSE;
+}
+
+bool fts_backend_can_lookup(struct fts_backend *backend,
+ const struct mail_search_arg *args)
+{
+ return backend->v.can_lookup(backend, args);
+}
+
+static int fts_score_map_sort(const struct fts_score_map *m1,
+ const struct fts_score_map *m2)
+{
+ if (m1->uid < m2->uid)
+ return -1;
+ if (m1->uid > m2->uid)
+ return 1;
+ return 0;
+}
+
+int fts_backend_lookup(struct fts_backend *backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result)
+{
+ array_clear(&result->definite_uids);
+ array_clear(&result->maybe_uids);
+ array_clear(&result->scores);
+
+ if (backend->v.lookup(backend, box, args, flags, result) < 0)
+ return -1;
+
+ if (!result->scores_sorted && array_is_created(&result->scores)) {
+ array_sort(&result->scores, fts_score_map_sort);
+ result->scores_sorted = TRUE;
+ }
+ return 0;
+}
+
+int fts_backend_lookup_multi(struct fts_backend *backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result)
+{
+ unsigned int i;
+
+ i_assert(boxes[0] != NULL);
+
+ if (backend->v.lookup_multi != NULL) {
+ if (backend->v.lookup_multi(backend, boxes, args,
+ flags, result) < 0)
+ return -1;
+ if (result->box_results == NULL) {
+ result->box_results = p_new(result->pool,
+ struct fts_result, 1);
+ }
+ return 0;
+ }
+
+ for (i = 0; boxes[i] != NULL; i++) ;
+ result->box_results = p_new(result->pool, struct fts_result, i+1);
+
+ for (i = 0; boxes[i] != NULL; i++) {
+ struct fts_result *box_result = &result->box_results[i];
+
+ p_array_init(&box_result->definite_uids, result->pool, 32);
+ p_array_init(&box_result->maybe_uids, result->pool, 32);
+ p_array_init(&box_result->scores, result->pool, 32);
+ if (backend->v.lookup(backend, boxes[i], args,
+ flags, box_result) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+void fts_backend_lookup_done(struct fts_backend *backend)
+{
+ if (backend->v.lookup_done != NULL)
+ backend->v.lookup_done(backend);
+}
+
+static uint32_t fts_index_get_ext_id(struct mailbox *box)
+{
+ return mail_index_ext_register(box->index, "fts",
+ sizeof(struct fts_index_header),
+ 0, 0);
+}
+
+bool fts_index_get_header(struct mailbox *box, struct fts_index_header *hdr_r)
+{
+ struct mail_index_view *view;
+ const void *data;
+ size_t data_size;
+ bool ret;
+
+ mail_index_refresh(box->index);
+ view = mail_index_view_open(box->index);
+ mail_index_get_header_ext(view, fts_index_get_ext_id(box),
+ &data, &data_size);
+ if (data_size < sizeof(*hdr_r)) {
+ i_zero(hdr_r);
+ ret = FALSE;
+ } else {
+ memcpy(hdr_r, data, sizeof(*hdr_r));
+ ret = TRUE;
+ }
+ mail_index_view_close(&view);
+ return ret;
+}
+
+int fts_index_set_header(struct mailbox *box,
+ const struct fts_index_header *hdr)
+{
+ struct mail_index_transaction *trans;
+ uint32_t ext_id = fts_index_get_ext_id(box);
+
+ trans = mail_index_transaction_begin(box->view, 0);
+ mail_index_update_header_ext(trans, ext_id, 0, hdr, sizeof(*hdr));
+ return mail_index_transaction_commit(&trans);
+}
+
+int fts_index_set_last_uid(struct mailbox *box, uint32_t last_uid)
+{
+ struct fts_index_header hdr;
+
+ (void)fts_index_get_header(box, &hdr);
+ hdr.last_indexed_uid = last_uid;
+ return fts_index_set_header(box, &hdr);
+}
+
+int fts_index_have_compatible_settings(struct mailbox_list *list,
+ uint32_t checksum)
+{
+ struct mail_namespace *ns = mailbox_list_get_namespace(list);
+ struct mailbox *box;
+ struct fts_index_header hdr;
+ const char *vname;
+ size_t len;
+ int ret;
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0)
+ vname = "INBOX";
+ else {
+ len = strlen(ns->prefix);
+ if (len > 0 && ns->prefix[len-1] == mail_namespace_get_sep(ns))
+ len--;
+ vname = t_strndup(ns->prefix, len);
+ }
+
+ box = mailbox_alloc(list, vname, 0);
+ if (mailbox_sync(box, (enum mailbox_sync_flags)0) < 0) {
+ i_error("fts: Failed to sync mailbox %s: %s", vname,
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else {
+ ret = fts_index_get_header(box, &hdr) &&
+ hdr.settings_checksum == checksum ? 1 : 0;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static const char *indexed_headers[] = {
+ "From", "To", "Cc", "Bcc", "Subject"
+};
+
+bool fts_header_want_indexed(const char *hdr_name)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(indexed_headers); i++) {
+ if (strcasecmp(hdr_name, indexed_headers[i]) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool fts_header_has_language(const char *hdr_name)
+{
+ /* FIXME: should email address headers be detected as different
+ languages? That mainly contains people's names.. */
+ /*if (message_header_is_address(hdr_name))
+ return TRUE;*/
+
+ /* Subject definitely contains language-specific data that can be
+ detected. Comment and Keywords headers also could contain, although
+ just about nobody uses those headers.
+
+ For now we assume that other headers contain non-language specific
+ data that we don't want to filter in special ways. For example
+ it is good to be able to search for Message-IDs. */
+ return strcasecmp(hdr_name, "Subject") == 0 ||
+ strcasecmp(hdr_name, "Comments") == 0 ||
+ strcasecmp(hdr_name, "Keywords") == 0;
+}
+
+int fts_mailbox_get_guid(struct mailbox *box, const char **guid_r)
+{
+ struct mailbox_metadata metadata;
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0)
+ return -1;
+
+ *guid_r = guid_128_to_string(metadata.guid);
+ return 0;
+}
diff --git a/src/plugins/fts/fts-api.h b/src/plugins/fts/fts-api.h
new file mode 100644
index 0000000..11a331f
--- /dev/null
+++ b/src/plugins/fts/fts-api.h
@@ -0,0 +1,173 @@
+#ifndef FTS_API_H
+#define FTS_API_H
+
+struct mail;
+struct mailbox;
+struct mail_namespace;
+struct mail_search_arg;
+
+struct fts_backend;
+
+#include "seq-range-array.h"
+
+enum fts_lookup_flags {
+ /* Specifies if the args should be ANDed or ORed together. */
+ FTS_LOOKUP_FLAG_AND_ARGS = 0x01,
+ /* Require exact matching for non-fuzzy search args by returning all
+ such matches as maybe_uids instead of definite_uids */
+ FTS_LOOKUP_FLAG_NO_AUTO_FUZZY = 0x02
+};
+
+enum fts_backend_build_key_type {
+ /* Header */
+ FTS_BACKEND_BUILD_KEY_HDR,
+ /* MIME part header */
+ FTS_BACKEND_BUILD_KEY_MIME_HDR,
+ /* MIME body part */
+ FTS_BACKEND_BUILD_KEY_BODY_PART,
+ /* Binary MIME body part, if backend supports binary data */
+ FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY
+};
+
+struct fts_backend_build_key {
+ uint32_t uid;
+ enum fts_backend_build_key_type type;
+ struct message_part *part;
+
+ /* for _KEY_HDR: */
+ const char *hdr_name;
+
+ /* for _KEY_BODY_PART and _KEY_BODY_PART_BINARY: */
+
+ /* Contains a valid parsed "type/subtype" string. For messages without
+ (valid) Content-Type: header, it's set to "text/plain". */
+ const char *body_content_type;
+ /* Content-Disposition: header without parsing/validation if it exists,
+ otherwise NULL. */
+ const char *body_content_disposition;
+};
+
+struct fts_score_map {
+ uint32_t uid;
+ float score;
+};
+ARRAY_DEFINE_TYPE(fts_score_map, struct fts_score_map);
+
+/* the structure is meant to be implemented by plugins that want to carry
+ some state over from a call to next ones within an fts_search_context
+ session.
+
+ The pointer to this structure is initially granted to be NULL and it
+ remains such unless the plugin itself activates it.
+
+ Any memory management for the pointer and its contents is expected to
+ be performed by the plugin itself, possibly but not necessarily using
+ the result pool propagated to plugin call by struct fts_result.pool and
+ struct fts_multi_result.pool. */
+
+struct fts_search_state;
+
+struct fts_result {
+ pool_t pool;
+ struct fts_search_state *search_state;
+
+ struct mailbox *box;
+
+ ARRAY_TYPE(seq_range) definite_uids;
+ /* The maybe_uids is useful with backends that can only filter out
+ messages, but can't definitively say if the search matched a
+ message. */
+ ARRAY_TYPE(seq_range) maybe_uids;
+ ARRAY_TYPE(fts_score_map) scores;
+ bool scores_sorted;
+};
+
+struct fts_multi_result {
+ pool_t pool;
+ struct fts_search_state *search_state;
+
+ /* box=NULL-terminated array of mailboxes and matching UIDs,
+ all allocated from the given pool. */
+ struct fts_result *box_results;
+};
+
+extern struct event_category event_category_fts;
+
+int fts_backend_init(const char *backend_name, struct mail_namespace *ns,
+ const char **error_r, struct fts_backend **backend_r);
+void fts_backend_deinit(struct fts_backend **backend);
+
+/* Get the last_uid for the mailbox. */
+int fts_backend_get_last_uid(struct fts_backend *backend, struct mailbox *box,
+ uint32_t *last_uid_r);
+
+/* Returns TRUE if there exists an update context. */
+bool fts_backend_is_updating(struct fts_backend *backend);
+
+/* Start an index update. */
+struct fts_backend_update_context *
+fts_backend_update_init(struct fts_backend *backend);
+/* Finish an index update. Returns 0 if ok, -1 if some updates failed.
+ If updates failed, the index is in unspecified state. */
+int fts_backend_update_deinit(struct fts_backend_update_context **ctx);
+
+/* Switch to updating the specified mailbox. box may also be set to NULL to
+ make sure the previous mailbox won't tried to be accessed anymore. */
+void fts_backend_update_set_mailbox(struct fts_backend_update_context *ctx,
+ struct mailbox *box);
+/* Expunge the specified mail. */
+void fts_backend_update_expunge(struct fts_backend_update_context *ctx,
+ uint32_t uid);
+
+/* Switch to building index for specified key. If backend doesn't want to
+ index this key, it can return FALSE and caller will skip to next key. */
+bool fts_backend_update_set_build_key(struct fts_backend_update_context *ctx,
+ const struct fts_backend_build_key *key);
+/* Make sure that if _build_more() is called, we'll assert-crash. */
+void fts_backend_update_unset_build_key(struct fts_backend_update_context *ctx);
+/* Add more content to the index for the currently specified build key.
+ Non-BODY_PART_BINARY data must contain only full valid UTF-8 characters,
+ but it doesn't need to be NUL-terminated. size contains the data size in
+ bytes, not characters. This function may be called many times and the data
+ block sizes may be small. Backend returns 0 if ok, -1 if build should be
+ aborted. */
+int fts_backend_update_build_more(struct fts_backend_update_context *ctx,
+ const unsigned char *data, size_t size);
+
+/* Refresh index to make sure we see latest changes from lookups.
+ Returns 0 if ok, -1 if error. */
+int fts_backend_refresh(struct fts_backend *backend);
+/* Go through the entire index and make sure all mails are indexed,
+ and delete any extra mails in the index. */
+int fts_backend_rescan(struct fts_backend *backend);
+/* Optimize the index. This can be a somewhat heavy operation. */
+int fts_backend_optimize(struct fts_backend *backend);
+
+/* Returns TRUE if fts_backend_lookup() should even be tried for the
+ given args. */
+bool fts_backend_can_lookup(struct fts_backend *backend,
+ const struct mail_search_arg *args);
+/* Do a FTS lookup for the given search args. Backends can support different
+ kinds of search arguments, so match_always=TRUE must be set to all search
+ args that were actually used to produce the search results. The other args
+ are handled by the regular search code. The backends MUST ignore all args
+ that have subargs (SEARCH_OR, SEARCH_SUB), since they are looked up
+ separately.
+
+ The arrays in result must be initialized by caller. */
+int fts_backend_lookup(struct fts_backend *backend, struct mailbox *box,
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_result *result);
+
+/* Search from multiple mailboxes. result->pool must be initialized. */
+int fts_backend_lookup_multi(struct fts_backend *backend,
+ struct mailbox *const boxes[],
+ struct mail_search_arg *args,
+ enum fts_lookup_flags flags,
+ struct fts_multi_result *result);
+/* Called after the lookups are done. The next lookup will be preceded by a
+ refresh. */
+void fts_backend_lookup_done(struct fts_backend *backend);
+
+#endif
diff --git a/src/plugins/fts/fts-build-mail.c b/src/plugins/fts/fts-build-mail.c
new file mode 100644
index 0000000..73d4f4b
--- /dev/null
+++ b/src/plugins/fts/fts-build-mail.c
@@ -0,0 +1,719 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "buffer.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "message-address.h"
+#include "message-parser.h"
+#include "message-decoder.h"
+#include "mail-storage.h"
+#include "index-mail.h"
+#include "fts-parser.h"
+#include "fts-user.h"
+#include "fts-language.h"
+#include "fts-tokenizer.h"
+#include "fts-filter.h"
+#include "fts-api-private.h"
+#include "fts-build-mail.h"
+
+/* there are other characters as well, but this doesn't have to be exact */
+#define IS_WORD_WHITESPACE(c) \
+ ((c) == ' ' || (c) == '\t' || (c) == '\n')
+/* if we see a word larger than this, just go ahead and split it from
+ wherever */
+#define MAX_WORD_SIZE 1024
+
+struct fts_mail_build_context {
+ struct mail *mail;
+ struct fts_backend_update_context *update_ctx;
+
+ char *content_type, *content_disposition;
+ struct fts_parser *body_parser;
+
+ buffer_t *word_buf, *pending_input;
+ struct fts_user_language *cur_user_lang;
+};
+
+static int fts_build_data(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size, bool last);
+
+static void fts_build_parse_content_type(struct fts_mail_build_context *ctx,
+ const struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *content_type;
+
+ if (ctx->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);
+ ctx->content_type = str_lcase(i_strdup(str_c(content_type)));
+ } T_END;
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+fts_build_parse_content_disposition(struct fts_mail_build_context *ctx,
+ const struct message_header_line *hdr)
+{
+ /* just pass it as-is to backend. */
+ i_free(ctx->content_disposition);
+ ctx->content_disposition =
+ i_strndup(hdr->full_value, hdr->full_value_len);
+}
+
+static void fts_parse_mail_header(struct fts_mail_build_context *ctx,
+ const struct message_block *raw_block)
+{
+ const struct message_header_line *hdr = raw_block->hdr;
+
+ if (strcasecmp(hdr->name, "Content-Type") == 0)
+ fts_build_parse_content_type(ctx, hdr);
+ else if (strcasecmp(hdr->name, "Content-Disposition") == 0)
+ fts_build_parse_content_disposition(ctx, hdr);
+}
+
+static int
+fts_build_unstructured_header(struct fts_mail_build_context *ctx,
+ const struct message_header_line *hdr)
+{
+ const unsigned char *data = hdr->full_value;
+ unsigned char *buf = NULL;
+ unsigned int i;
+ int ret;
+
+ /* @UNSAFE: if there are any NULs, replace them with spaces */
+ for (i = 0; i < hdr->full_value_len; i++) {
+ if (hdr->full_value[i] == '\0') {
+ if (buf == NULL) {
+ buf = i_memdup(hdr->full_value,
+ hdr->full_value_len);
+ data = buf;
+ }
+ buf[i] = ' ';
+ }
+ }
+ ret = fts_build_data(ctx, data, hdr->full_value_len, TRUE);
+ i_free(buf);
+ return ret;
+}
+
+static void fts_mail_build_ctx_set_lang(struct fts_mail_build_context *ctx,
+ struct fts_user_language *user_lang)
+{
+ i_assert(user_lang != NULL);
+
+ ctx->cur_user_lang = user_lang;
+ /* reset tokenizer between fields - just to be sure no state
+ leaks between fields (especially if previous indexing had
+ failed) */
+ fts_tokenizer_reset(user_lang->index_tokenizer);
+}
+
+static void
+fts_build_tokenized_hdr_update_lang(struct fts_mail_build_context *ctx,
+ const struct message_header_line *hdr)
+{
+ /* Headers that don't contain any human language will only be
+ translated to lowercase - no stemming or other filtering. There's
+ unfortunately no pefect way of detecting which headers contain
+ human languages, so we check with fts_header_has_language if the
+ header is something that's supposed to containing human text. */
+ if (fts_header_has_language(hdr->name))
+ ctx->cur_user_lang = NULL;
+ else {
+ fts_mail_build_ctx_set_lang(ctx,
+ fts_user_get_data_lang(ctx->update_ctx->backend->ns->user));
+ }
+}
+
+static int fts_build_mail_header(struct fts_mail_build_context *ctx,
+ const struct message_block *block)
+{
+ const struct message_header_line *hdr = block->hdr;
+ struct fts_backend_build_key key;
+ int ret;
+
+ if (hdr->eoh)
+ return 0;
+
+ /* hdr->full_value is always set because we get the block from
+ message_decoder */
+ i_zero(&key);
+ key.uid = ctx->mail->uid;
+ key.type = block->part->physical_pos == 0 ?
+ FTS_BACKEND_BUILD_KEY_HDR : FTS_BACKEND_BUILD_KEY_MIME_HDR;
+ key.part = block->part;
+ key.hdr_name = hdr->name;
+
+ if ((ctx->update_ctx->backend->flags &
+ FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0)
+ fts_build_tokenized_hdr_update_lang(ctx, hdr);
+
+ if (!fts_backend_update_set_build_key(ctx->update_ctx, &key))
+ return 0;
+
+ if (!message_header_is_address(hdr->name)) {
+ /* regular unstructured header */
+ ret = fts_build_unstructured_header(ctx, hdr);
+ } else T_BEGIN {
+ /* message address. normalize it to give better
+ search results. */
+ struct message_address *addr;
+ string_t *str;
+
+ addr = message_address_parse(pool_datastack_create(),
+ hdr->full_value,
+ hdr->full_value_len,
+ UINT_MAX, 0);
+ str = t_str_new(hdr->full_value_len);
+ message_address_write(str, addr);
+
+ ret = fts_build_data(ctx, str_data(str), str_len(str), TRUE);
+ } T_END;
+
+ if ((ctx->update_ctx->backend->flags &
+ FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0) {
+ /* index the header name itself using data-language. */
+ struct fts_user_language *prev_lang = ctx->cur_user_lang;
+
+ fts_mail_build_ctx_set_lang(ctx,
+ fts_user_get_data_lang(ctx->update_ctx->backend->ns->user));
+ key.hdr_name = "";
+ if (fts_backend_update_set_build_key(ctx->update_ctx, &key)) {
+ if (fts_build_data(ctx, (const void *)hdr->name,
+ strlen(hdr->name), TRUE) < 0)
+ ret = -1;
+ }
+ fts_mail_build_ctx_set_lang(ctx, prev_lang);
+ }
+ return ret;
+}
+
+static bool
+fts_build_body_begin(struct fts_mail_build_context *ctx,
+ struct message_part *part, bool *binary_body_r)
+{
+ struct mail_storage *storage;
+ struct fts_parser_context parser_context;
+ struct fts_backend_build_key key;
+
+ i_assert(ctx->body_parser == NULL);
+
+ *binary_body_r = FALSE;
+ i_zero(&key);
+ key.uid = ctx->mail->uid;
+ key.part = part;
+
+ i_zero(&parser_context);
+ parser_context.content_type = ctx->content_type != NULL ?
+ ctx->content_type : "text/plain";
+ if (str_begins(parser_context.content_type, "multipart/")) {
+ /* multiparts are never indexed, only their contents */
+ return FALSE;
+ }
+ storage = mailbox_get_storage(ctx->mail->box);
+ parser_context.user = mail_storage_get_user(storage);
+ parser_context.content_disposition = ctx->content_disposition;
+
+ if (fts_parser_init(&parser_context, &ctx->body_parser)) {
+ /* extract text using the the returned parser */
+ *binary_body_r = TRUE;
+ key.type = FTS_BACKEND_BUILD_KEY_BODY_PART;
+ } else if (str_begins(parser_context.content_type, "text/") ||
+ str_begins(parser_context.content_type, "message/")) {
+ /* text body parts */
+ key.type = FTS_BACKEND_BUILD_KEY_BODY_PART;
+ ctx->body_parser = fts_parser_text_init();
+ } else {
+ /* possibly binary */
+ if ((ctx->update_ctx->backend->flags &
+ FTS_BACKEND_FLAG_BINARY_MIME_PARTS) == 0)
+ return FALSE;
+ *binary_body_r = TRUE;
+ key.type = FTS_BACKEND_BUILD_KEY_BODY_PART_BINARY;
+ }
+ key.body_content_type = parser_context.content_type;
+ key.body_content_disposition = ctx->content_disposition;
+ ctx->cur_user_lang = NULL;
+ if (!fts_backend_update_set_build_key(ctx->update_ctx, &key)) {
+ if (ctx->body_parser != NULL)
+ (void)fts_parser_deinit(&ctx->body_parser, NULL);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+fts_build_add_tokens_with_filter(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size)
+{
+ struct fts_tokenizer *tokenizer = ctx->cur_user_lang->index_tokenizer;
+ struct fts_filter *filter = ctx->cur_user_lang->filter;
+ const char *token, *error;
+ int ret = 1, ret2;
+
+ while (ret > 0) T_BEGIN {
+ ret = ret2 = fts_tokenizer_next(tokenizer, data, size, &token, &error);
+ if (ret2 > 0 && filter != NULL)
+ ret2 = fts_filter_filter(filter, &token, &error);
+ if (ret2 < 0) {
+ mail_set_critical(ctx->mail,
+ "fts: Couldn't create indexable tokens: %s",
+ error);
+ }
+ if (ret2 > 0) {
+ if (fts_backend_update_build_more(ctx->update_ctx,
+ (const void *)token,
+ strlen(token)) < 0) {
+ mail_storage_set_internal_error(ctx->mail->box->storage);
+ ret = -1;
+ }
+ }
+ } T_END;
+ return ret;
+}
+
+static int
+fts_detect_language(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size, bool last,
+ const struct fts_language **lang_r)
+{
+ struct mail_user *user = ctx->update_ctx->backend->ns->user;
+ struct fts_language_list *lang_list = fts_user_get_language_list(user);
+ const struct fts_language *lang;
+ const char *error;
+
+ switch (fts_language_detect(lang_list, data, size, &lang, &error)) {
+ case FTS_LANGUAGE_RESULT_SHORT:
+ /* save the input so far and try again later */
+ buffer_append(ctx->pending_input, data, size);
+ if (last) {
+ /* we've run out of data. use the default language. */
+ *lang_r = fts_language_list_get_first(lang_list);
+ return 1;
+ }
+ return 0;
+ case FTS_LANGUAGE_RESULT_UNKNOWN:
+ /* use the default language */
+ *lang_r = fts_language_list_get_first(lang_list);
+ return 1;
+ case FTS_LANGUAGE_RESULT_OK:
+ *lang_r = lang;
+ return 1;
+ case FTS_LANGUAGE_RESULT_ERROR:
+ /* internal language detection library failure
+ (e.g. invalid config). don't index anything. */
+ mail_set_critical(ctx->mail,
+ "Language detection library initialization failed: %s",
+ error);
+ return -1;
+ default:
+ i_unreached();
+ }
+}
+
+static int
+fts_build_tokenized(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size, bool last)
+{
+ struct mail_user *user = ctx->update_ctx->backend->ns->user;
+ const struct fts_language *lang;
+ int ret;
+
+ if (ctx->cur_user_lang != NULL) {
+ /* we already have a language */
+ } else if ((ret = fts_detect_language(ctx, data, size, last, &lang)) < 0) {
+ return -1;
+ } else if (ret == 0) {
+ /* wait for more data */
+ return 0;
+ } else {
+ fts_mail_build_ctx_set_lang(ctx, fts_user_language_find(user, lang));
+
+ if (ctx->pending_input->used > 0) {
+ if (fts_build_add_tokens_with_filter(ctx,
+ ctx->pending_input->data,
+ ctx->pending_input->used) < 0)
+ return -1;
+ buffer_set_used_size(ctx->pending_input, 0);
+ }
+ }
+ if (fts_build_add_tokens_with_filter(ctx, data, size) < 0)
+ return -1;
+ if (last) {
+ if (fts_build_add_tokens_with_filter(ctx, NULL, 0) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+fts_build_full_words(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size, bool last)
+{
+ size_t i;
+
+ /* we'll need to send only full words to the backend */
+
+ if (ctx->word_buf != NULL && ctx->word_buf->used > 0) {
+ /* continuing previous word */
+ for (i = 0; i < size; i++) {
+ if (IS_WORD_WHITESPACE(data[i]))
+ break;
+ }
+ buffer_append(ctx->word_buf, data, i);
+ data += i;
+ size -= i;
+ if (size == 0 && ctx->word_buf->used < MAX_WORD_SIZE && !last) {
+ /* word is still not finished */
+ return 0;
+ }
+ /* we have a full word, index it */
+ if (fts_backend_update_build_more(ctx->update_ctx,
+ ctx->word_buf->data,
+ ctx->word_buf->used) < 0) {
+ mail_storage_set_internal_error(ctx->mail->box->storage);
+ return -1;
+ }
+ buffer_set_used_size(ctx->word_buf, 0);
+ }
+
+ /* find the boundary for last word */
+ if (last)
+ i = size;
+ else {
+ for (i = size; i > 0; i--) {
+ if (IS_WORD_WHITESPACE(data[i-1]))
+ break;
+ }
+ }
+
+ if (fts_backend_update_build_more(ctx->update_ctx, data, i) < 0) {
+ mail_storage_set_internal_error(ctx->mail->box->storage);
+ return -1;
+ }
+
+ if (i < size) {
+ if (ctx->word_buf == NULL) {
+ ctx->word_buf =
+ buffer_create_dynamic(default_pool, 128);
+ }
+ buffer_append(ctx->word_buf, data + i, size - i);
+ }
+ return 0;
+}
+
+static int fts_build_data(struct fts_mail_build_context *ctx,
+ const unsigned char *data, size_t size, bool last)
+{
+ if ((ctx->update_ctx->backend->flags &
+ FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0) {
+ return fts_build_tokenized(ctx, data, size, last);
+ } else if ((ctx->update_ctx->backend->flags &
+ FTS_BACKEND_FLAG_BUILD_FULL_WORDS) != 0) {
+ return fts_build_full_words(ctx, data, size, last);
+ } else {
+ if (fts_backend_update_build_more(ctx->update_ctx, data, size) < 0) {
+ mail_storage_set_internal_error(ctx->mail->box->storage);
+ return -1;
+ }
+ return 0;
+ }
+}
+
+static int fts_build_body_block(struct fts_mail_build_context *ctx,
+ const struct message_block *block, bool last)
+{
+ i_assert(block->hdr == NULL);
+
+ return fts_build_data(ctx, block->data, block->size, last);
+}
+
+static int fts_body_parser_finish(struct fts_mail_build_context *ctx,
+ const char **retriable_err_msg_r,
+ bool *may_need_retry_r)
+{
+ struct message_block block;
+ const char *retriable_error;
+ int ret = 0;
+ int deinit_ret;
+ *may_need_retry_r = FALSE;
+
+ do {
+ i_zero(&block);
+ fts_parser_more(ctx->body_parser, &block);
+ if (fts_build_body_block(ctx, &block, FALSE) < 0) {
+ ret = -1;
+ break;
+ }
+ } while (block.size > 0);
+
+ deinit_ret = fts_parser_deinit(&ctx->body_parser, &retriable_error);
+ if (ret < 0) {
+ /* indexing already failed - we don't want to retry
+ in any case */
+ return -1;
+ }
+
+ if (deinit_ret == 0) {
+ /* retry the parsing */
+ *may_need_retry_r = TRUE;
+ *retriable_err_msg_r = retriable_error;
+ return -1;
+ }
+ if (deinit_ret < 0) {
+ mail_storage_set_internal_error(ctx->mail->box->storage);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+load_header_filter(const char *key, struct fts_backend *backend,
+ ARRAY_TYPE(const_string) list, bool *matches_all_r)
+{
+ const char *str = mail_user_plugin_getenv(backend->ns->user, key);
+
+ *matches_all_r = FALSE;
+ if (str == NULL || *str == '\0')
+ return;
+
+ char **entries = p_strsplit_spaces(backend->header_filters.pool, str, " ");
+ for (char **entry = entries; *entry != NULL; ++entry) {
+ const char *value = str_lcase(*entry);
+ array_push_back(&list, &value);
+ if (*value == '*') {
+ *matches_all_r = TRUE;
+ break;
+ }
+ }
+ array_sort(&list, i_strcmp_p);
+}
+
+static struct fts_header_filters *
+load_header_filters(struct fts_backend *backend)
+{
+ struct fts_header_filters *filters = &backend->header_filters;
+ if (!filters->loaded) {
+ bool match_all;
+
+ /* match_all return ignored in includes */
+ load_header_filter("fts_header_includes", backend,
+ filters->includes, &match_all);
+
+ load_header_filter("fts_header_excludes", backend,
+ filters->excludes, &match_all);
+ filters->loaded = TRUE;
+ filters->exclude_is_default = match_all;
+ }
+ return filters;
+}
+
+/* This performs comparison between two strings, where the second one can end
+ * with the wildcard '*'. When the match reaches a '*' on the pitem side, zero
+ * (match) is returned regardles of the remaining characters.
+ *
+ * The function obeys the same lexicographic order as i_strcmp_p() and
+ * strcmp(), which is the reason for the casts to unsigned before comparing.
+ */
+static int ATTR_PURE
+header_prefix_cmp(const char *const *pkey, const char *const *pitem)
+{
+ const char *key = *pkey;
+ const char *item = *pitem;
+
+ while (*key == *item && *key != '\0') key++, item++;
+ return item[0] == '*' && item[1] == '\0' ? 0 :
+ (unsigned char)*key - (unsigned char)*item;
+}
+
+static bool
+is_header_indexable(const char *header_name, struct fts_backend *backend)
+{
+ bool indexable;
+ T_BEGIN {
+ struct fts_header_filters *filters = load_header_filters(backend);
+ const char *hdr = t_str_lcase(header_name);
+
+ if (array_bsearch(&filters->includes, &hdr, header_prefix_cmp) != NULL)
+ indexable = TRUE;
+ else if (filters->exclude_is_default ||
+ array_bsearch(&filters->excludes, &hdr, header_prefix_cmp) != NULL)
+ indexable = FALSE;
+ else
+ indexable = TRUE;
+ } T_END;
+ return indexable;
+}
+
+static int
+fts_build_mail_real(struct fts_backend_update_context *update_ctx,
+ struct mail *mail,
+ const char **retriable_err_msg_r,
+ bool *may_need_retry_r)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE,
+ };
+ struct fts_mail_build_context ctx;
+ struct istream *input;
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_block raw_block, block;
+ struct message_part *prev_part, *parts;
+ bool skip_body = FALSE, body_part = FALSE, body_added = FALSE;
+ bool binary_body;
+ const char *error;
+ int ret;
+
+ *may_need_retry_r = FALSE;
+ if (mail_get_stream_because(mail, NULL, NULL, "fts indexing", &input) < 0) {
+ if (mail->expunged)
+ return 0;
+ mail_set_critical(mail, "Failed to read stream: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ }
+
+ i_zero(&ctx);
+ ctx.update_ctx = update_ctx;
+ ctx.mail = mail;
+ if ((update_ctx->backend->flags & FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0)
+ ctx.pending_input = buffer_create_dynamic(default_pool, 128);
+
+ prev_part = NULL;
+ parser = message_parser_init(pool_datastack_create(), input, &parser_set);
+
+ decoder = message_decoder_init(update_ctx->normalizer, 0);
+ for (;;) {
+ ret = message_parser_parse_next_block(parser, &raw_block);
+ i_assert(ret != 0);
+ if (ret < 0) {
+ if (input->stream_errno == 0)
+ ret = 0;
+ else {
+ mail_set_critical(mail, "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ }
+ break;
+ }
+
+ if (raw_block.part != prev_part) {
+ /* body part changed. we're now parsing the end of
+ boundary, possibly followed by message epilogue */
+ if (ctx.body_parser != NULL) {
+ if (fts_body_parser_finish(&ctx, retriable_err_msg_r,
+ may_need_retry_r) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ message_decoder_set_return_binary(decoder, FALSE);
+ fts_backend_update_unset_build_key(update_ctx);
+ prev_part = raw_block.part;
+ i_free_and_null(ctx.content_type);
+ i_free_and_null(ctx.content_disposition);
+
+ if (raw_block.size != 0) {
+ /* multipart. skip until beginning of next
+ part's headers */
+ skip_body = TRUE;
+ }
+ }
+
+ if (raw_block.hdr != NULL) {
+ /* always handle headers */
+ } else if (raw_block.size == 0) {
+ /* end of headers */
+ skip_body = !fts_build_body_begin(&ctx, raw_block.part,
+ &binary_body);
+ if (binary_body)
+ message_decoder_set_return_binary(decoder, TRUE);
+ body_part = TRUE;
+ } else {
+ if (skip_body)
+ continue;
+ }
+
+ if (!message_decoder_decode_next_block(decoder, &raw_block,
+ &block))
+ continue;
+
+ if (block.hdr != NULL) {
+ fts_parse_mail_header(&ctx, &raw_block);
+ if (is_header_indexable(block.hdr->name, update_ctx->backend) &&
+ fts_build_mail_header(&ctx, &block) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if (block.size == 0) {
+ /* end of headers */
+ } else {
+ i_assert(body_part);
+ if (ctx.body_parser != NULL)
+ fts_parser_more(ctx.body_parser, &block);
+ if (fts_build_body_block(&ctx, &block, FALSE) < 0) {
+ ret = -1;
+ break;
+ }
+ body_added = TRUE;
+ }
+ }
+ if (ctx.body_parser != NULL) {
+ if (ret == 0)
+ ret = fts_body_parser_finish(&ctx, retriable_err_msg_r,
+ may_need_retry_r);
+ else
+ (void)fts_parser_deinit(&ctx.body_parser, NULL);
+ }
+ if (ret == 0 && body_part && !skip_body && !body_added) {
+ /* make sure body is added even when it doesn't exist */
+ block.data = NULL; block.size = 0;
+ ret = fts_build_body_block(&ctx, &block, TRUE);
+ }
+ if (message_parser_deinit_from_parts(&parser, &parts, &error) < 0)
+ index_mail_set_message_parts_corrupted(mail, error);
+ message_decoder_deinit(&decoder);
+ i_free(ctx.content_type);
+ i_free(ctx.content_disposition);
+ buffer_free(&ctx.word_buf);
+ buffer_free(&ctx.pending_input);
+ return ret < 0 ? -1 : 1;
+}
+
+int fts_build_mail(struct fts_backend_update_context *update_ctx,
+ struct mail *mail)
+{
+ int ret;
+ /* Number of attempts to be taken if retry is needed */
+ unsigned int attempts = 2;
+ const char *retriable_err_msg;
+ bool may_need_retry;
+
+ T_BEGIN {
+ while ((ret = fts_build_mail_real(update_ctx, mail,
+ &retriable_err_msg,
+ &may_need_retry)) < 0 &&
+ may_need_retry) {
+ if (--attempts == 0) {
+ /* Log this as info instead of as error,
+ because e.g. Tika doesn't differentiate
+ between temporary errors and invalid
+ document input. */
+ i_info("%s - ignoring", retriable_err_msg);
+ ret = 0;
+ break;
+ }
+ }
+ } T_END;
+ return ret;
+}
diff --git a/src/plugins/fts/fts-build-mail.h b/src/plugins/fts/fts-build-mail.h
new file mode 100644
index 0000000..aed4413
--- /dev/null
+++ b/src/plugins/fts/fts-build-mail.h
@@ -0,0 +1,9 @@
+#ifndef FTS_BUILD_MAIL_H
+#define FTS_BUILD_MAIL_H
+
+/* Build indexes for the given mail. Returns 0 on success, -1 on error.
+ The error is set to mail's storage. */
+int fts_build_mail(struct fts_backend_update_context *update_ctx,
+ struct mail *mail);
+
+#endif
diff --git a/src/plugins/fts/fts-expunge-log.c b/src/plugins/fts/fts-expunge-log.c
new file mode 100644
index 0000000..d39ceea
--- /dev/null
+++ b/src/plugins/fts/fts-expunge-log.c
@@ -0,0 +1,617 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "crc32.h"
+#include "hash.h"
+#include "istream.h"
+#include "write-full.h"
+#include "seq-range-array.h"
+#include "mail-storage.h"
+#include "fts-expunge-log.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+struct fts_expunge_log_record {
+ /* CRC32 of this entire record (except this checksum) */
+ uint32_t checksum;
+ /* Size of this entire record */
+ uint32_t record_size;
+
+ /* Mailbox GUID */
+ guid_128_t guid;
+ /* { uid1, uid2 } pairs */
+ /* uint32_t expunge_uid_ranges[]; */
+
+ /* Total number of messages expunged so far in this log */
+ /* uint32_t expunge_count; */
+};
+
+struct fts_expunge_log {
+ char *path;
+
+ int fd;
+ struct stat st;
+};
+
+struct fts_expunge_log_mailbox {
+ guid_128_t guid;
+ ARRAY_TYPE(seq_range) uids;
+ unsigned uids_count;
+};
+
+struct fts_expunge_log_append_ctx {
+ struct fts_expunge_log *log;
+ pool_t pool;
+
+ HASH_TABLE(uint8_t *, struct fts_expunge_log_mailbox *) mailboxes;
+ struct fts_expunge_log_mailbox *prev_mailbox;
+
+ bool failed;
+};
+
+struct fts_expunge_log_read_ctx {
+ struct fts_expunge_log *log;
+
+ struct istream *input;
+ buffer_t buffer;
+ struct fts_expunge_log_read_record read_rec;
+
+ bool failed;
+ bool corrupted;
+ bool unlink;
+};
+
+struct fts_expunge_log *fts_expunge_log_init(const char *path)
+{
+ struct fts_expunge_log *log;
+
+ log = i_new(struct fts_expunge_log, 1);
+ log->path = i_strdup(path);
+ log->fd = -1;
+ return log;
+}
+
+void fts_expunge_log_deinit(struct fts_expunge_log **_log)
+{
+ struct fts_expunge_log *log = *_log;
+
+ *_log = NULL;
+ i_close_fd(&log->fd);
+ i_free(log->path);
+ i_free(log);
+}
+
+static int fts_expunge_log_open(struct fts_expunge_log *log, bool create)
+{
+ int fd;
+
+ i_assert(log->fd == -1);
+
+ /* FIXME: use proper permissions */
+ fd = open(log->path, O_RDWR | O_APPEND | (create ? O_CREAT : 0), 0600);
+ if (fd == -1) {
+ if (errno == ENOENT && !create)
+ return 0;
+
+ i_error("open(%s) failed: %m", log->path);
+ return -1;
+ }
+ if (fstat(fd, &log->st) < 0) {
+ i_error("fstat(%s) failed: %m", log->path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ log->fd = fd;
+ return 1;
+}
+
+static int
+fts_expunge_log_reopen_if_needed(struct fts_expunge_log *log, bool create)
+{
+ struct stat st;
+
+ if (log->fd == -1)
+ return fts_expunge_log_open(log, create);
+
+ if (stat(log->path, &st) == 0) {
+ if (st.st_ino == log->st.st_ino &&
+ CMP_DEV_T(st.st_dev, log->st.st_dev)) {
+ /* same file */
+ return 0;
+ }
+ /* file changed */
+ } else if (errno == ENOENT) {
+ /* recreate the file */
+ } else {
+ i_error("stat(%s) failed: %m", log->path);
+ return -1;
+ }
+ if (close(log->fd) < 0)
+ i_error("close(%s) failed: %m", log->path);
+ log->fd = -1;
+ return fts_expunge_log_open(log, create);
+}
+
+static int
+fts_expunge_log_read_expunge_count(struct fts_expunge_log *log,
+ uint32_t *expunge_count_r)
+{
+ ssize_t ret;
+
+ i_assert(log->fd != -1);
+
+ if (fstat(log->fd, &log->st) < 0) {
+ i_error("fstat(%s) failed: %m", log->path);
+ return -1;
+ }
+ if ((uoff_t)log->st.st_size < sizeof(*expunge_count_r)) {
+ *expunge_count_r = 0;
+ return 0;
+ }
+ /* we'll assume that write()s atomically grow the file size, as
+ O_APPEND almost guarantees. even if not, having a race condition
+ isn't the end of the world. the expunge count is simply read wrong
+ and fts optimize is performed earlier or later than intended. */
+ ret = pread(log->fd, expunge_count_r, sizeof(*expunge_count_r),
+ log->st.st_size - 4);
+ if (ret < 0) {
+ i_error("pread(%s) failed: %m", log->path);
+ return -1;
+ }
+ if (ret != sizeof(*expunge_count_r)) {
+ i_error("pread(%s) read only %d of %d bytes", log->path,
+ (int)ret, (int)sizeof(*expunge_count_r));
+ return -1;
+ }
+ return 0;
+}
+
+struct fts_expunge_log_append_ctx *
+fts_expunge_log_append_begin(struct fts_expunge_log *log)
+{
+ struct fts_expunge_log_append_ctx *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fts expunge log append", 1024);
+ ctx = p_new(pool, struct fts_expunge_log_append_ctx, 1);
+ ctx->log = log;
+ ctx->pool = pool;
+ hash_table_create(&ctx->mailboxes, pool, 0, guid_128_hash, guid_128_cmp);
+
+ if (log != NULL && fts_expunge_log_reopen_if_needed(log, TRUE) < 0)
+ ctx->failed = TRUE;
+ return ctx;
+}
+
+static struct fts_expunge_log_mailbox *
+fts_expunge_log_mailbox_alloc(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid)
+{
+ uint8_t *guid_p;
+ struct fts_expunge_log_mailbox *mailbox;
+
+ mailbox = p_new(ctx->pool, struct fts_expunge_log_mailbox, 1);
+ guid_128_copy(mailbox->guid, mailbox_guid);
+ p_array_init(&mailbox->uids, ctx->pool, 16);
+
+ guid_p = mailbox->guid;
+ hash_table_insert(ctx->mailboxes, guid_p, mailbox);
+ return mailbox;
+}
+
+static struct fts_expunge_log_mailbox *
+fts_expunge_log_append_mailbox(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid)
+{
+ const uint8_t *guid_p = mailbox_guid;
+ struct fts_expunge_log_mailbox *mailbox;
+
+ if (ctx->prev_mailbox != NULL &&
+ guid_128_equals(mailbox_guid, ctx->prev_mailbox->guid))
+ mailbox = ctx->prev_mailbox;
+ else {
+ mailbox = hash_table_lookup(ctx->mailboxes, guid_p);
+ if (mailbox == NULL)
+ mailbox = fts_expunge_log_mailbox_alloc(ctx, mailbox_guid);
+ ctx->prev_mailbox = mailbox;
+ }
+ return mailbox;
+}
+void fts_expunge_log_append_next(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid,
+ uint32_t uid)
+{
+ struct fts_expunge_log_mailbox *mailbox;
+
+ mailbox = fts_expunge_log_append_mailbox(ctx, mailbox_guid);
+ if (!seq_range_array_add(&mailbox->uids, uid))
+ mailbox->uids_count++;
+}
+void fts_expunge_log_append_range(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid,
+ const struct seq_range *uids)
+{
+ struct fts_expunge_log_mailbox *mailbox;
+
+ mailbox = fts_expunge_log_append_mailbox(ctx, mailbox_guid);
+ mailbox->uids_count += seq_range_array_add_range_count(&mailbox->uids,
+ uids->seq1, uids->seq2);
+ /* To be honest, an unbacked log doesn't need to maintain the uids_count,
+ but we don't know here if we're supporting an unbacked log or not, so we
+ have to maintain the value, just in case.
+ At the moment, the only caller of this function is for unbacked logs. */
+}
+void fts_expunge_log_append_record(struct fts_expunge_log_append_ctx *ctx,
+ const struct fts_expunge_log_read_record *record)
+{
+ const struct seq_range *range;
+ /* FIXME: Optimise with a merge */
+ array_foreach(&record->uids, range)
+ fts_expunge_log_append_range(ctx, record->mailbox_guid, range);
+}
+static void fts_expunge_log_append_mailbox_record(struct fts_expunge_log_append_ctx *ctx,
+ struct fts_expunge_log_mailbox *mailbox)
+{
+ const struct seq_range *range;
+ /* FIXME: Optimise with a merge */
+ array_foreach(&mailbox->uids, range)
+ fts_expunge_log_append_range(ctx, mailbox->guid, range);
+}
+
+static void
+fts_expunge_log_export(struct fts_expunge_log_append_ctx *ctx,
+ uint32_t expunge_count, buffer_t *output)
+{
+ struct hash_iterate_context *iter;
+ uint8_t *guid_p;
+ struct fts_expunge_log_mailbox *mailbox;
+ struct fts_expunge_log_record *rec;
+ size_t rec_offset;
+
+ iter = hash_table_iterate_init(ctx->mailboxes);
+ while (hash_table_iterate(iter, ctx->mailboxes, &guid_p, &mailbox)) {
+ rec_offset = output->used;
+ rec = buffer_append_space_unsafe(output, sizeof(*rec));
+ memcpy(rec->guid, mailbox->guid, sizeof(rec->guid));
+
+ /* uint32_t expunge_uid_ranges[]; */
+ buffer_append(output, array_front(&mailbox->uids),
+ array_count(&mailbox->uids) *
+ sizeof(struct seq_range));
+ /* uint32_t expunge_count; */
+ expunge_count += mailbox->uids_count;
+ buffer_append(output, &expunge_count, sizeof(expunge_count));
+
+ /* update the header now that we know the record contents */
+ rec = buffer_get_space_unsafe(output, rec_offset,
+ output->used - rec_offset);
+ rec->record_size = output->used - rec_offset;
+ rec->checksum = crc32_data(&rec->record_size,
+ rec->record_size -
+ sizeof(rec->checksum));
+ }
+ hash_table_iterate_deinit(&iter);
+}
+
+static int
+fts_expunge_log_write(struct fts_expunge_log_append_ctx *ctx)
+{
+ struct fts_expunge_log *log = ctx->log;
+ buffer_t *buf;
+ uint32_t expunge_count, *e;
+ int ret;
+
+ /* Unbacked expunge logs cannot be written, by definition */
+ i_assert(log != NULL);
+
+ /* try to append to the latest file */
+ if (fts_expunge_log_reopen_if_needed(log, TRUE) < 0)
+ return -1;
+
+ if (fts_expunge_log_read_expunge_count(log, &expunge_count) < 0)
+ return -1;
+
+ buf = buffer_create_dynamic(default_pool, 1024);
+ fts_expunge_log_export(ctx, expunge_count, buf);
+ /* the file was opened with O_APPEND, so this write() should be
+ appended atomically without any need for locking. */
+ for (;;) {
+ if (write_full(log->fd, buf->data, buf->used) < 0) {
+ i_error("write(%s) failed: %m", log->path);
+ if (ftruncate(log->fd, log->st.st_size) < 0)
+ i_error("ftruncate(%s) failed: %m", log->path);
+ }
+ if ((ret = fts_expunge_log_reopen_if_needed(log, TRUE)) <= 0)
+ break;
+ /* the log was unlinked, so we'll need to write again to
+ the new file. the expunge_count needs to be reset to zero
+ from here. */
+ e = buffer_get_space_unsafe(buf, buf->used - sizeof(uint32_t),
+ sizeof(uint32_t));
+ i_assert(*e > expunge_count);
+ *e -= expunge_count;
+ expunge_count = 0;
+ }
+ buffer_free(&buf);
+
+ if (ret == 0) {
+ /* finish by closing the log. this forces NFS to flush the
+ changes to disk without our having to explicitly play with
+ fsync() */
+ if (close(log->fd) < 0) {
+ /* FIXME: we should ftruncate() in case there
+ were partial writes.. */
+ i_error("close(%s) failed: %m", log->path);
+ ret = -1;
+ }
+ log->fd = -1;
+ }
+ return ret;
+}
+
+static int fts_expunge_log_append_finalize(struct fts_expunge_log_append_ctx **_ctx,
+ bool commit)
+{
+ struct fts_expunge_log_append_ctx *ctx = *_ctx;
+ int ret = ctx->failed ? -1 : 0;
+
+ *_ctx = NULL;
+ if (commit && ret == 0)
+ ret = fts_expunge_log_write(ctx);
+
+ hash_table_destroy(&ctx->mailboxes);
+ pool_unref(&ctx->pool);
+ return ret;
+}
+
+int fts_expunge_log_uid_count(struct fts_expunge_log *log,
+ unsigned int *expunges_r)
+{
+ int ret;
+
+ if ((ret = fts_expunge_log_reopen_if_needed(log, FALSE)) <= 0) {
+ *expunges_r = 0;
+ return ret;
+ }
+
+ return fts_expunge_log_read_expunge_count(log, expunges_r);
+}
+
+int fts_expunge_log_append_commit(struct fts_expunge_log_append_ctx **_ctx)
+{
+ return fts_expunge_log_append_finalize(_ctx, TRUE);
+}
+
+int fts_expunge_log_append_abort(struct fts_expunge_log_append_ctx **_ctx)
+{
+ return fts_expunge_log_append_finalize(_ctx, FALSE);
+}
+
+struct fts_expunge_log_read_ctx *
+fts_expunge_log_read_begin(struct fts_expunge_log *log)
+{
+ struct fts_expunge_log_read_ctx *ctx;
+
+ ctx = i_new(struct fts_expunge_log_read_ctx, 1);
+ ctx->log = log;
+ if (fts_expunge_log_reopen_if_needed(log, FALSE) < 0)
+ ctx->failed = TRUE;
+ else if (log->fd != -1)
+ ctx->input = i_stream_create_fd(log->fd, SIZE_MAX);
+ ctx->unlink = TRUE;
+ return ctx;
+}
+
+static bool
+fts_expunge_log_record_size_is_valid(const struct fts_expunge_log_record *rec,
+ unsigned int *uids_size_r)
+{
+ if (rec->record_size < sizeof(*rec) + sizeof(uint32_t)*3)
+ return FALSE;
+ *uids_size_r = rec->record_size - sizeof(*rec) - sizeof(uint32_t);
+ return *uids_size_r % sizeof(uint32_t)*2 == 0;
+}
+
+static void
+fts_expunge_log_read_failure(struct fts_expunge_log_read_ctx *ctx,
+ unsigned int wanted_size)
+{
+ size_t size;
+
+ if (ctx->input->stream_errno != 0) {
+ ctx->failed = TRUE;
+ i_error("read(%s) failed: %s", ctx->log->path,
+ i_stream_get_error(ctx->input));
+ } else {
+ size = i_stream_get_data_size(ctx->input);
+ ctx->corrupted = TRUE;
+ i_error("Corrupted fts expunge log %s: "
+ "Unexpected EOF (read %zu / %u bytes)",
+ ctx->log->path, size, wanted_size);
+ }
+}
+
+const struct fts_expunge_log_read_record *
+fts_expunge_log_read_next(struct fts_expunge_log_read_ctx *ctx)
+{
+ const unsigned char *data;
+ const struct fts_expunge_log_record *rec;
+ unsigned int uids_size;
+ size_t size;
+ uint32_t checksum;
+
+ if (ctx->input == NULL)
+ return NULL;
+
+ /* initial read to try to get the record */
+ (void)i_stream_read_bytes(ctx->input, &data, &size, IO_BLOCK_SIZE);
+ if (size == 0 && ctx->input->stream_errno == 0) {
+ /* expected EOF - mark the file as read by unlinking it */
+ if (ctx->unlink)
+ i_unlink_if_exists(ctx->log->path);
+
+ /* try reading again, in case something new was written */
+ i_stream_sync(ctx->input);
+ (void)i_stream_read_bytes(ctx->input, &data, &size,
+ IO_BLOCK_SIZE);
+ }
+ if (size < sizeof(*rec)) {
+ if (size == 0 && ctx->input->stream_errno == 0) {
+ /* expected EOF */
+ return NULL;
+ }
+ fts_expunge_log_read_failure(ctx, sizeof(*rec));
+ return NULL;
+ }
+ rec = (const void *)data;
+
+ if (!fts_expunge_log_record_size_is_valid(rec, &uids_size)) {
+ ctx->corrupted = TRUE;
+ i_error("Corrupted fts expunge log %s: "
+ "Invalid record size: %u",
+ ctx->log->path, rec->record_size);
+ return NULL;
+ }
+
+ /* read the entire record */
+ while (size < rec->record_size) {
+ if (i_stream_read_bytes(ctx->input, &data, &size, rec->record_size) < 0) {
+ fts_expunge_log_read_failure(ctx, rec->record_size);
+ return NULL;
+ }
+ rec = (const void *)data;
+ }
+
+ /* verify that the record checksum is valid */
+ checksum = crc32_data(&rec->record_size,
+ rec->record_size - sizeof(rec->checksum));
+ if (checksum != rec->checksum) {
+ ctx->corrupted = TRUE;
+ i_error("Corrupted fts expunge log %s: "
+ "Record checksum mismatch: %u != %u",
+ ctx->log->path, checksum, rec->checksum);
+ return NULL;
+ }
+
+ memcpy(ctx->read_rec.mailbox_guid, rec->guid,
+ sizeof(ctx->read_rec.mailbox_guid));
+ /* create the UIDs array by pointing it directly into input
+ stream's buffer */
+ buffer_create_from_const_data(&ctx->buffer, rec + 1, uids_size);
+ array_create_from_buffer(&ctx->read_rec.uids, &ctx->buffer,
+ sizeof(struct seq_range));
+
+ i_stream_skip(ctx->input, rec->record_size);
+ return &ctx->read_rec;
+}
+
+int fts_expunge_log_read_end(struct fts_expunge_log_read_ctx **_ctx)
+{
+ struct fts_expunge_log_read_ctx *ctx = *_ctx;
+ int ret = ctx->failed ? -1 : (ctx->corrupted ? 0 : 1);
+
+ *_ctx = NULL;
+
+ if (ctx->corrupted) {
+ if (ctx->unlink)
+ i_unlink_if_exists(ctx->log->path);
+ }
+
+ i_stream_unref(&ctx->input);
+ i_free(ctx);
+ return ret;
+}
+
+int fts_expunge_log_flatten(const char *path,
+ struct fts_expunge_log_append_ctx **flattened_r)
+{
+ struct fts_expunge_log *read;
+ struct fts_expunge_log_read_ctx *read_ctx;
+ const struct fts_expunge_log_read_record *record;
+ struct fts_expunge_log_append_ctx *append;
+ int ret;
+
+ i_assert(path != NULL && flattened_r != NULL);
+ read = fts_expunge_log_init(path);
+
+ read_ctx = fts_expunge_log_read_begin(read);
+ read_ctx->unlink = FALSE;
+
+ append = fts_expunge_log_append_begin(NULL);
+ while((record = fts_expunge_log_read_next(read_ctx)) != NULL) {
+ fts_expunge_log_append_record(append, record);
+ }
+
+ if ((ret = fts_expunge_log_read_end(&read_ctx)) > 0)
+ *flattened_r = append;
+ fts_expunge_log_deinit(&read);
+
+ return ret;
+}
+bool fts_expunge_log_contains(const struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid, uint32_t uid)
+{
+ const struct fts_expunge_log_mailbox *mailbox;
+ const uint8_t *guid_p = mailbox_guid;
+
+ mailbox = hash_table_lookup(ctx->mailboxes, guid_p);
+ if (mailbox == NULL)
+ return FALSE;
+ return seq_range_exists(&mailbox->uids, uid);
+}
+int fts_expunge_log_append_remove(struct fts_expunge_log_append_ctx *from,
+ const struct fts_expunge_log_read_record *record)
+{
+ const uint8_t *guid_p = record->mailbox_guid;
+ struct fts_expunge_log_mailbox *mailbox = hash_table_lookup(from->mailboxes, guid_p);
+ if (mailbox == NULL)
+ return 0; /* may only remove things that exist */
+
+ mailbox->uids_count -= seq_range_array_remove_seq_range(&mailbox->uids, &record->uids);
+ return 1;
+}
+int fts_expunge_log_subtract(struct fts_expunge_log_append_ctx *from,
+ struct fts_expunge_log *subtract)
+{
+ unsigned int failures = 0;
+ struct fts_expunge_log_read_ctx *read_ctx = fts_expunge_log_read_begin(subtract);
+ read_ctx->unlink = FALSE;
+
+ const struct fts_expunge_log_read_record *record;
+ while ((record = fts_expunge_log_read_next(read_ctx)) != NULL) {
+ if (fts_expunge_log_append_remove(from, record) <= 0)
+ failures++;
+ }
+ if (failures > 0)
+ i_warning("fts: Expunge log subtract ignored %u nonexistent mailbox GUIDs",
+ failures);
+ return fts_expunge_log_read_end(&read_ctx);
+}
+/* It could be argued that somehow adding a log (file) to the append context
+ and then calling the _write() helper would be easier. But then there's the
+ _commit() vs. _abort() cleanup that would need to be addressed. Just creating
+ a copy is simpler. */
+int fts_expunge_log_flat_write(const struct fts_expunge_log_append_ctx *read_log,
+ const char *path)
+{
+ int ret;
+ struct fts_expunge_log *nlog = fts_expunge_log_init(path);
+ struct fts_expunge_log_append_ctx *nappend = fts_expunge_log_append_begin(nlog);
+
+ struct hash_iterate_context *iter;
+ uint8_t *guid_p;
+ struct fts_expunge_log_mailbox *mailbox;
+
+ iter = hash_table_iterate_init(read_log->mailboxes);
+ while (hash_table_iterate(iter, read_log->mailboxes, &guid_p, &mailbox))
+ fts_expunge_log_append_mailbox_record(nappend, mailbox);
+
+ hash_table_iterate_deinit(&iter);
+ ret = fts_expunge_log_append_commit(&nappend);
+ fts_expunge_log_deinit(&nlog);
+
+ return ret;
+}
diff --git a/src/plugins/fts/fts-expunge-log.h b/src/plugins/fts/fts-expunge-log.h
new file mode 100644
index 0000000..cc15f29
--- /dev/null
+++ b/src/plugins/fts/fts-expunge-log.h
@@ -0,0 +1,58 @@
+#ifndef FTS_EXPUNGE_LOG
+#define FTS_EXPUNGE_LOG
+
+#include "seq-range-array.h"
+#include "guid.h"
+
+struct fts_expunge_log_read_record {
+ guid_128_t mailbox_guid;
+ ARRAY_TYPE(seq_range) uids;
+};
+
+struct fts_expunge_log *fts_expunge_log_init(const char *path);
+void fts_expunge_log_deinit(struct fts_expunge_log **log);
+
+struct fts_expunge_log_append_ctx *
+fts_expunge_log_append_begin(struct fts_expunge_log *log);
+void fts_expunge_log_append_next(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid,
+ uint32_t uid);
+void fts_expunge_log_append_range(struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid,
+ const struct seq_range *uids);
+void fts_expunge_log_append_record(struct fts_expunge_log_append_ctx *ctx,
+ const struct fts_expunge_log_read_record *record);
+/* In-memory flattened structures may have records removed from them,
+ file-backed ones may not. Non-existence of UIDs is not an error,
+ non-existence of mailbox GUID causes an error return of 0. */
+int fts_expunge_log_append_remove(struct fts_expunge_log_append_ctx *ctx,
+ const struct fts_expunge_log_read_record *record);
+int fts_expunge_log_append_commit(struct fts_expunge_log_append_ctx **ctx);
+/* Do not commit non-backed structures, abort them after use. */
+int fts_expunge_log_append_abort(struct fts_expunge_log_append_ctx **ctx);
+
+int fts_expunge_log_uid_count(struct fts_expunge_log *log,
+ unsigned int *expunges_r);
+
+struct fts_expunge_log_read_ctx *
+fts_expunge_log_read_begin(struct fts_expunge_log *log);
+const struct fts_expunge_log_read_record *
+fts_expunge_log_read_next(struct fts_expunge_log_read_ctx *ctx);
+/* Returns 1 if all ok, 0 if there was corruption, -1 if I/O error.
+ If end() is called before reading all records, the log isn't unlinked. */
+int fts_expunge_log_read_end(struct fts_expunge_log_read_ctx **ctx);
+
+/* Read an entire log file, and flatten it into one hash of arrays.
+ The struct it returns cannot be written, as it has no backing store */
+int fts_expunge_log_flatten(const char *path,
+ struct fts_expunge_log_append_ctx **flattened_r);
+bool fts_expunge_log_contains(const struct fts_expunge_log_append_ctx *ctx,
+ const guid_128_t mailbox_guid, uint32_t uid);
+/* Modify in-place a flattened log. If non-existent mailbox GUIDs are
+ encountered, a warning will be logged. */
+int fts_expunge_log_subtract(struct fts_expunge_log_append_ctx *from,
+ struct fts_expunge_log *subtract);
+/* Write a modified flattened log as a new file. */
+int fts_expunge_log_flat_write(const struct fts_expunge_log_append_ctx *flattened,
+ const char *path);
+#endif
diff --git a/src/plugins/fts/fts-indexer.c b/src/plugins/fts/fts-indexer.c
new file mode 100644
index 0000000..aca23c9
--- /dev/null
+++ b/src/plugins/fts/fts-indexer.c
@@ -0,0 +1,300 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "connection.h"
+#include "write-full.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "settings-parser.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "fts-api.h"
+#include "fts-indexer.h"
+
+#define INDEXER_NOTIFY_INTERVAL_SECS 10
+#define INDEXER_SOCKET_NAME "indexer"
+#define INDEXER_WAIT_MSECS 250
+
+struct fts_indexer_context {
+ struct connection conn;
+
+ struct mailbox *box;
+ struct ioloop *ioloop;
+
+ struct timeval search_start_time, last_notify;
+ unsigned int percentage;
+ struct connection_list *connection_list;
+
+ bool notified:1;
+ bool failed:1;
+ bool completed:1;
+};
+
+static void fts_indexer_notify(struct fts_indexer_context *ctx)
+{
+ unsigned long long elapsed_msecs, est_total_msecs;
+ unsigned int eta_secs;
+
+ if (ioloop_time - ctx->last_notify.tv_sec < INDEXER_NOTIFY_INTERVAL_SECS)
+ return;
+ ctx->last_notify = ioloop_timeval;
+
+ if (ctx->box->storage->callbacks.notify_ok == NULL ||
+ ctx->percentage == 0)
+ return;
+
+ elapsed_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &ctx->search_start_time);
+ est_total_msecs = elapsed_msecs * 100 / ctx->percentage;
+ eta_secs = (est_total_msecs - elapsed_msecs) / 1000;
+
+ T_BEGIN {
+ const char *text;
+
+ text = t_strdup_printf("Indexed %d%% of the mailbox, "
+ "ETA %d:%02d", ctx->percentage,
+ eta_secs/60, eta_secs%60);
+ ctx->box->storage->callbacks.
+ notify_ok(ctx->box, text,
+ ctx->box->storage->callback_context);
+ ctx->notified = TRUE;
+ } T_END;
+}
+
+static int fts_indexer_more_int(struct fts_indexer_context *ctx)
+{
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct timeout *to;
+
+ if (ctx->failed)
+ return -1;
+ if (ctx->completed)
+ return 1;
+
+ /* wait for a while for the reply. FIXME: once search API supports
+ asynchronous waits, get rid of this wait and use the mail IO loop */
+ io_loop_set_current(ctx->ioloop);
+ to = timeout_add_short(INDEXER_WAIT_MSECS, io_loop_stop, ctx->ioloop);
+ io_loop_run(ctx->ioloop);
+ timeout_remove(&to);
+ io_loop_set_current(prev_ioloop);
+
+ if (ctx->failed)
+ return -1;
+ if (ctx->completed)
+ return 1;
+ return 0;
+}
+
+int fts_indexer_more(struct fts_indexer_context *ctx)
+{
+ int ret;
+
+ if ((ret = fts_indexer_more_int(ctx)) < 0) {
+ /* If failed is already set, the code has had a chance to
+ * set an internal error already, i.e. MAIL_ERROR_INUSE. */
+ if (!ctx->failed)
+ mail_storage_set_internal_error(ctx->box->storage);
+ ctx->failed = TRUE;
+ return -1;
+ }
+
+ if (ret == 0)
+ fts_indexer_notify(ctx);
+
+ return ret;
+}
+
+static void fts_indexer_destroy(struct connection *conn)
+{
+ struct fts_indexer_context *ctx =
+ container_of(conn, struct fts_indexer_context, conn);
+ connection_deinit(conn);
+ if (!ctx->completed)
+ ctx->failed = TRUE;
+ ctx->completed = TRUE;
+}
+
+int fts_indexer_deinit(struct fts_indexer_context **_ctx)
+{
+ struct fts_indexer_context *ctx = *_ctx;
+ i_assert(ctx != NULL);
+ *_ctx = NULL;
+ if (!ctx->completed)
+ ctx->failed = TRUE;
+ int ret = ctx->failed ? -1 : 0;
+ if (ctx->notified) {
+ /* we notified at least once */
+ ctx->box->storage->callbacks.
+ notify_ok(ctx->box, "Mailbox indexing finished",
+ ctx->box->storage->callback_context);
+ }
+ connection_list_deinit(&ctx->connection_list);
+ io_loop_set_current(ctx->ioloop);
+ io_loop_destroy(&ctx->ioloop);
+ i_free(ctx);
+ return ret;
+}
+
+static int
+fts_indexer_input_args(struct connection *conn, const char *const *args)
+{
+ struct fts_indexer_context *ctx =
+ container_of(conn, struct fts_indexer_context, conn);
+ int percentage;
+ if (args[1] == NULL) {
+ e_error(conn->event, "indexer sent invalid reply");
+ return -1;
+ }
+ if (strcmp(args[0], "1") != 0) {
+ e_error(conn->event, "indexer sent invalid reply");
+ return -1;
+ }
+ if (strcmp(args[1], "OK") == 0)
+ return 1;
+ if (str_to_int(args[1], &percentage) < 0) {
+ e_error(conn->event, "indexer sent invalid progress: %s", args[1]);
+ ctx->failed = TRUE;
+ return -1;
+ }
+ if (percentage < 0) {
+ e_error(ctx->box->event, "indexer failed to index mailbox");
+ ctx->failed = TRUE;
+ return -1;
+ }
+ ctx->percentage = percentage;
+ if (ctx->percentage == 100)
+ ctx->completed = TRUE;
+ return 1;
+}
+
+static void fts_indexer_client_connected(struct connection *conn, bool success)
+{
+ struct fts_indexer_context *ctx =
+ container_of(conn, struct fts_indexer_context, conn);
+ if (!success) {
+ ctx->completed = TRUE;
+ ctx->failed = TRUE;
+ return;
+ }
+ ctx->failed = ctx->completed = FALSE;
+ const char *cmd = t_strdup_printf("PREPEND\t1\t%s\t%s\t0\t%s\n",
+ str_tabescape(ctx->box->storage->user->username),
+ str_tabescape(ctx->box->vname),
+ str_tabescape(ctx->box->storage->user->session_id));
+ o_stream_nsend_str(conn->output, cmd);
+}
+
+static void fts_indexer_idle_timeout(struct connection *conn)
+{
+ struct fts_indexer_context *ctx =
+ container_of(conn, struct fts_indexer_context, conn);
+ mail_storage_set_error(ctx->box->storage, MAIL_ERROR_INUSE,
+ "Timeout while waiting for indexing to finish");
+ ctx->failed = TRUE;
+ connection_disconnect(conn);
+}
+
+static const struct connection_settings indexer_client_set =
+{
+ .service_name_in = "indexer",
+ .service_name_out = "indexer",
+ .major_version = 1,
+ .minor_version = 0,
+ .client_connect_timeout_msecs = 2000,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = IO_BLOCK_SIZE,
+ .client = TRUE,
+};
+
+static const struct connection_vfuncs indexer_client_vfuncs =
+{
+ .destroy = fts_indexer_destroy,
+ .client_connected = fts_indexer_client_connected,
+ .input_args = fts_indexer_input_args,
+ .idle_timeout = fts_indexer_idle_timeout,
+};
+
+int fts_indexer_init(struct fts_backend *backend, struct mailbox *box,
+ struct fts_indexer_context **ctx_r)
+{
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct fts_indexer_context *ctx;
+ struct mailbox_status status;
+ uint32_t last_uid, seq1, seq2;
+ const char *path, *value, *error;
+ unsigned int timeout_secs = 0;
+ int ret;
+
+ value = mail_user_plugin_getenv(box->storage->user, "fts_index_timeout");
+ if (value != NULL) {
+ if (settings_get_time(value, &timeout_secs, &error) < 0) {
+ e_error(box->storage->user->event,
+ "Invalid fts_index_timeout setting: %s",
+ error);
+ return -1;
+ }
+ }
+
+ if (fts_backend_get_last_uid(backend, box, &last_uid) < 0)
+ return -1;
+
+ mailbox_get_open_status(box, STATUS_UIDNEXT, &status);
+ if (status.uidnext == last_uid+1) {
+ /* everything is already indexed */
+ return 0;
+ }
+
+ mailbox_get_seq_range(box, last_uid+1, (uint32_t)-1, &seq1, &seq2);
+ if (seq1 == 0) {
+ /* no new messages (last messages in mailbox were expunged) */
+ return 0;
+ }
+
+ path = t_strconcat(box->storage->user->set->base_dir,
+ "/"INDEXER_SOCKET_NAME, NULL);
+
+ ctx = i_new(struct fts_indexer_context, 1);
+ ctx->box = box;
+ ctx->search_start_time = ioloop_timeval;
+ ctx->conn.event_parent = box->event;
+ ctx->ioloop = io_loop_create();
+ ctx->connection_list = connection_list_init(&indexer_client_set,
+ &indexer_client_vfuncs);
+ ctx->conn.input_idle_timeout_secs = timeout_secs;
+ connection_init_client_unix(ctx->connection_list, &ctx->conn,
+ path);
+ ret = connection_client_connect(&ctx->conn);
+ io_loop_set_current(prev_ioloop);
+ *ctx_r = ctx;
+ return ctx->failed || ret < 0 ? -1 : 1;
+}
+
+#define INDEXER_HANDSHAKE "1\t0\tindexer\tindexer\n"
+
+int fts_indexer_cmd(struct mail_user *user, const char *cmd,
+ const char **path_r)
+{
+ const char *path;
+ int fd;
+
+ path = t_strconcat(user->set->base_dir,
+ "/"INDEXER_SOCKET_NAME, NULL);
+ fd = net_connect_unix_with_retries(path, 1000);
+ if (fd == -1) {
+ i_error("net_connect_unix(%s) failed: %m", path);
+ return -1;
+ }
+
+ cmd = t_strconcat(INDEXER_HANDSHAKE, cmd, NULL);
+ if (write_full(fd, cmd, strlen(cmd)) < 0) {
+ i_error("write(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ *path_r = path;
+ return fd;
+}
diff --git a/src/plugins/fts/fts-indexer.h b/src/plugins/fts/fts-indexer.h
new file mode 100644
index 0000000..7ccbc7e
--- /dev/null
+++ b/src/plugins/fts/fts-indexer.h
@@ -0,0 +1,22 @@
+#ifndef FTS_BUILD_H
+#define FTS_BUILD_H
+
+struct fts_backend;
+struct fts_indexer_context;
+
+/* Initialize indexing the given mailbox via indexer service. Returns 1 if
+ indexing started, 0 if there was no need to index or -1 if error. */
+int fts_indexer_init(struct fts_backend *backend, struct mailbox *box,
+ struct fts_indexer_context **ctx_r);
+/* Returns 0 if ok, -1 if error. */
+int fts_indexer_deinit(struct fts_indexer_context **ctx);
+
+/* Build more. Returns 1 if finished, 0 if this function needs to be called
+ again, -1 if error. */
+int fts_indexer_more(struct fts_indexer_context *ctx);
+
+/* Returns fd, which you can either read from or close. */
+int fts_indexer_cmd(struct mail_user *user, const char *cmd,
+ const char **path_r);
+
+#endif
diff --git a/src/plugins/fts/fts-parser-html.c b/src/plugins/fts/fts-parser-html.c
new file mode 100644
index 0000000..aa2078d
--- /dev/null
+++ b/src/plugins/fts/fts-parser-html.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "message-parser.h"
+#include "mail-html2text.h"
+#include "fts-parser.h"
+
+struct html_fts_parser {
+ struct fts_parser parser;
+ struct mail_html2text *html2text;
+ buffer_t *output;
+};
+
+static struct fts_parser *
+fts_parser_html_try_init(struct fts_parser_context *parser_context)
+{
+ struct html_fts_parser *parser;
+
+ if (!mail_html2text_content_type_match(parser_context->content_type))
+ return NULL;
+
+ parser = i_new(struct html_fts_parser, 1);
+ parser->parser.v = fts_parser_html;
+ parser->html2text = mail_html2text_init(0);
+ parser->output = buffer_create_dynamic(default_pool, 4096);
+ return &parser->parser;
+}
+
+static void fts_parser_html_more(struct fts_parser *_parser,
+ struct message_block *block)
+{
+ struct html_fts_parser *parser = (struct html_fts_parser *)_parser;
+
+ if (block->size == 0) {
+ /* finished */
+ return;
+ }
+
+ buffer_set_used_size(parser->output, 0);
+ mail_html2text_more(parser->html2text, block->data, block->size,
+ parser->output);
+
+ block->data = parser->output->data;
+ block->size = parser->output->used;
+}
+
+static int fts_parser_html_deinit(struct fts_parser *_parser,
+ const char **retriable_err_msg_r ATTR_UNUSED)
+{
+ struct html_fts_parser *parser = (struct html_fts_parser *)_parser;
+
+ mail_html2text_deinit(&parser->html2text);
+ buffer_free(&parser->output);
+ i_free(parser);
+ return 1;
+}
+
+struct fts_parser_vfuncs fts_parser_html = {
+ fts_parser_html_try_init,
+ fts_parser_html_more,
+ fts_parser_html_deinit,
+ NULL
+};
diff --git a/src/plugins/fts/fts-parser-script.c b/src/plugins/fts/fts-parser-script.c
new file mode 100644
index 0000000..eefbe07
--- /dev/null
+++ b/src/plugins/fts/fts-parser-script.c
@@ -0,0 +1,277 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "net.h"
+#include "istream.h"
+#include "write-full.h"
+#include "module-context.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-parser.h"
+#include "mail-user.h"
+#include "fts-parser.h"
+
+#define SCRIPT_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_parser_script_user_module)
+
+#define SCRIPT_HANDSHAKE "VERSION\tscript\t4\t0\nalarm=10\nnoreply\n"
+
+struct content {
+ const char *content_type;
+ const char *const *extensions;
+};
+
+struct fts_parser_script_user {
+ union mail_user_module_context module_ctx;
+
+ ARRAY(struct content) content;
+};
+
+struct script_fts_parser {
+ struct fts_parser parser;
+
+ int fd;
+ char *path;
+
+ unsigned char outbuf[IO_BLOCK_SIZE];
+ bool failed;
+ bool shutdown;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(fts_parser_script_user_module,
+ &mail_user_module_register);
+
+static int script_connect(struct mail_user *user, const char **path_r)
+{
+ const char *path;
+ int fd;
+
+ path = mail_user_plugin_getenv(user, "fts_decoder");
+ if (path == NULL)
+ return -1;
+
+ if (*path != '/')
+ path = t_strconcat(user->set->base_dir, "/", path, NULL);
+ fd = net_connect_unix_with_retries(path, 1000);
+ if (fd == -1)
+ i_error("net_connect_unix(%s) failed: %m", path);
+ else
+ net_set_nonblock(fd, FALSE);
+ *path_r = path;
+ return fd;
+}
+
+static int script_contents_read(struct mail_user *user)
+{
+ struct fts_parser_script_user *suser = SCRIPT_USER_CONTEXT(user);
+ const char *path, *cmd, *line;
+ char **args;
+ struct istream *input;
+ struct content *content;
+ bool eof_seen = FALSE;
+ int fd, ret = 0;
+ i_assert(suser != NULL);
+
+ fd = script_connect(user, &path);
+ if (fd == -1)
+ return -1;
+
+ cmd = t_strdup_printf(SCRIPT_HANDSHAKE"\n");
+ if (write_full(fd, cmd, strlen(cmd)) < 0) {
+ i_error("write(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ input = i_stream_create_fd_autoclose(&fd, 1024);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ /* <content-type> <extension> [<extension> ...] */
+ args = p_strsplit_spaces(user->pool, line, " ");
+ if (args[0] == NULL) {
+ eof_seen = TRUE;
+ break;
+ }
+ if (args[0][0] == '\0' || args[1] == NULL) {
+ i_error("parser script sent invalid input: %s", line);
+ continue;
+ }
+
+ content = array_append_space(&suser->content);
+ content->content_type = str_lcase(args[0]);
+ content->extensions = (const void *)(args+1);
+ }
+ if (input->stream_errno != 0) {
+ i_error("parser script read(%s) failed: %s", path,
+ i_stream_get_error(input));
+ ret = -1;
+ } else if (!eof_seen) {
+ if (input->v_offset == 0)
+ i_error("parser script didn't send any data");
+ else
+ i_error("parser script didn't send empty EOF line");
+ }
+ i_stream_destroy(&input);
+ return ret;
+}
+
+static bool script_support_content(struct mail_user *user,
+ const char **content_type,
+ const char *filename)
+{
+ struct fts_parser_script_user *suser = SCRIPT_USER_CONTEXT(user);
+ const struct content *content;
+ const char *extension;
+
+ if (suser == NULL) {
+ suser = p_new(user->pool, struct fts_parser_script_user, 1);
+ p_array_init(&suser->content, user->pool, 32);
+ MODULE_CONTEXT_SET(user, fts_parser_script_user_module, suser);
+ }
+ if (array_count(&suser->content) == 0) {
+ if (script_contents_read(user) < 0)
+ return FALSE;
+ }
+
+ if (strcmp(*content_type, "application/octet-stream") == 0) {
+ if (filename == NULL)
+ return FALSE;
+ extension = strrchr(filename, '.');
+ if (extension == NULL)
+ return FALSE;
+ extension = extension + 1;
+
+ array_foreach(&suser->content, content) {
+ if (content->extensions != NULL &&
+ str_array_icase_find(content->extensions, extension)) {
+ *content_type = content->content_type;
+ return TRUE;
+ }
+ }
+ } else {
+ array_foreach(&suser->content, content) {
+ if (strcmp(content->content_type, *content_type) == 0)
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void parse_content_disposition(const char *content_disposition,
+ const char **filename_r)
+{
+ struct rfc822_parser_context parser;
+ const char *const *results, *filename2;
+ string_t *str;
+
+ *filename_r = NULL;
+
+ if (content_disposition == NULL)
+ return;
+
+ rfc822_parser_init(&parser, (const unsigned char *)content_disposition,
+ strlen(content_disposition), NULL);
+ rfc822_skip_lwsp(&parser);
+
+ /* type; param; param; .. */
+ str = t_str_new(32);
+ if (rfc822_parse_mime_token(&parser, str) < 0) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+
+ rfc2231_parse(&parser, &results);
+ filename2 = NULL;
+ for (; *results != NULL; results += 2) {
+ if (strcasecmp(results[0], "filename") == 0) {
+ *filename_r = results[1];
+ break;
+ }
+ if (strcasecmp(results[0], "filename*") == 0)
+ filename2 = results[1];
+ }
+ if (*filename_r == NULL) {
+ /* RFC 2231 style non-ascii filename. we don't really care
+ much about the filename actually, just about its extension */
+ *filename_r = filename2;
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static struct fts_parser *
+fts_parser_script_try_init(struct fts_parser_context *parser_context)
+{
+ struct script_fts_parser *parser;
+ const char *filename, *path, *cmd;
+ int fd;
+
+ parse_content_disposition(parser_context->content_disposition, &filename);
+ if (!script_support_content(parser_context->user, &parser_context->content_type, filename))
+ return NULL;
+
+ fd = script_connect(parser_context->user, &path);
+ if (fd == -1)
+ return NULL;
+ cmd = t_strdup_printf(SCRIPT_HANDSHAKE"%s\n\n", parser_context->content_type);
+ if (write_full(fd, cmd, strlen(cmd)) < 0) {
+ i_error("write(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return NULL;
+ }
+
+ parser = i_new(struct script_fts_parser, 1);
+ parser->parser.v = fts_parser_script;
+ parser->path = i_strdup(path);
+ parser->fd = fd;
+ return &parser->parser;
+}
+
+static void fts_parser_script_more(struct fts_parser *_parser,
+ struct message_block *block)
+{
+ struct script_fts_parser *parser = (struct script_fts_parser *)_parser;
+ ssize_t ret;
+
+ if (block->size > 0) {
+ /* first we'll send everything to the script */
+ if (!parser->failed &&
+ write_full(parser->fd, block->data, block->size) < 0) {
+ i_error("write(%s) failed: %m", parser->path);
+ parser->failed = TRUE;
+ }
+ block->size = 0;
+ } else {
+ if (!parser->shutdown) {
+ if (shutdown(parser->fd, SHUT_WR) < 0)
+ i_error("shutdown(%s) failed: %m", parser->path);
+ parser->shutdown = TRUE;
+ }
+ /* read the result from the script */
+ ret = read(parser->fd, parser->outbuf, sizeof(parser->outbuf));
+ if (ret < 0)
+ i_error("read(%s) failed: %m", parser->path);
+ else {
+ block->data = parser->outbuf;
+ block->size = ret;
+ }
+ }
+}
+
+static int fts_parser_script_deinit(struct fts_parser *_parser,
+ const char **retriable_err_msg_r ATTR_UNUSED)
+{
+ struct script_fts_parser *parser = (struct script_fts_parser *)_parser;
+ int ret = parser->failed ? -1 : 1;
+
+ if (close(parser->fd) < 0)
+ i_error("close(%s) failed: %m", parser->path);
+ i_free(parser->path);
+ i_free(parser);
+ return ret;
+}
+
+struct fts_parser_vfuncs fts_parser_script = {
+ fts_parser_script_try_init,
+ fts_parser_script_more,
+ fts_parser_script_deinit,
+ NULL
+};
diff --git a/src/plugins/fts/fts-parser-tika.c b/src/plugins/fts/fts-parser-tika.c
new file mode 100644
index 0000000..bb6379c
--- /dev/null
+++ b/src/plugins/fts/fts-parser-tika.c
@@ -0,0 +1,278 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "module-context.h"
+#include "iostream-ssl.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "message-parser.h"
+#include "mail-user.h"
+#include "fts-parser.h"
+
+#define TIKA_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_parser_tika_user_module)
+
+struct fts_parser_tika_user {
+ union mail_user_module_context module_ctx;
+ struct http_url *http_url;
+};
+
+struct tika_fts_parser {
+ struct fts_parser parser;
+ struct mail_user *user;
+ struct http_client_request *http_req;
+
+ struct ioloop *ioloop;
+ struct io *io;
+ struct istream *payload;
+
+ bool failed;
+};
+
+static struct http_client *tika_http_client = NULL;
+static MODULE_CONTEXT_DEFINE_INIT(fts_parser_tika_user_module,
+ &mail_user_module_register);
+
+static int
+tika_get_http_client_url(struct mail_user *user, struct http_url **http_url_r)
+{
+ struct fts_parser_tika_user *tuser = TIKA_USER_CONTEXT(user);
+ struct http_client_settings http_set;
+ struct ssl_iostream_settings ssl_set;
+ const char *url, *error;
+
+ url = mail_user_plugin_getenv(user, "fts_tika");
+ if (url == NULL) {
+ /* fts_tika disabled */
+ return -1;
+ }
+
+ if (tuser != NULL) {
+ *http_url_r = tuser->http_url;
+ return *http_url_r == NULL ? -1 : 0;
+ }
+
+ tuser = p_new(user->pool, struct fts_parser_tika_user, 1);
+ MODULE_CONTEXT_SET(user, fts_parser_tika_user_module, tuser);
+
+ if (http_url_parse(url, NULL, 0, user->pool,
+ &tuser->http_url, &error) < 0) {
+ i_error("fts_tika: Failed to parse HTTP url %s: %s", url, error);
+ return -1;
+ }
+
+ if (tika_http_client == NULL) {
+ mail_user_init_ssl_client_settings(user, &ssl_set);
+
+ i_zero(&http_set);
+ http_set.max_idle_time_msecs = 100;
+ http_set.max_parallel_connections = 1;
+ http_set.max_pipelined_requests = 1;
+ http_set.max_redirects = 1;
+ http_set.max_attempts = 3;
+ http_set.connect_timeout_msecs = 5*1000;
+ http_set.request_timeout_msecs = 60*1000;
+ http_set.ssl = &ssl_set;
+ http_set.debug = user->mail_debug;
+ http_set.event_parent = user->event;
+
+ /* FIXME: We should initialize a shared client instead. However,
+ this is currently not possible due to an obscure bug
+ in the blocking HTTP payload API, which causes
+ conflicts with other HTTP applications like FTS Solr.
+ Using a private client will provide a quick fix for
+ now. */
+ tika_http_client = http_client_init_private(&http_set);
+ }
+ *http_url_r = tuser->http_url;
+ return 0;
+}
+
+static void
+fts_tika_parser_response(const struct http_response *response,
+ struct tika_fts_parser *parser)
+{
+ i_assert(parser->payload == NULL);
+
+ switch (response->status) {
+ case 200:
+ /* read response */
+ if (response->payload == NULL)
+ parser->payload = i_stream_create_from_data("", 0);
+ else {
+ i_stream_ref(response->payload);
+ parser->payload = response->payload;
+ }
+ break;
+ case 204: /* empty response */
+ case 415: /* Unsupported Media Type */
+ case 422: /* Unprocessable Entity */
+ e_debug(parser->user->event, "fts_tika: PUT %s failed: %s",
+ mail_user_plugin_getenv(parser->user, "fts_tika"),
+ http_response_get_message(response));
+ parser->payload = i_stream_create_from_data("", 0);
+ break;
+ default:
+ if (response->status / 100 == 5) {
+ /* Server Error - the problem could be anything (in Tika or
+ HTTP server or proxy) and might be retriable, but Tika has
+ trouble processing some documents and throws up this error
+ every time for those documents. */
+ parser->parser.may_need_retry = TRUE;
+ i_free(parser->parser.retriable_error_msg);
+ parser->parser.retriable_error_msg =
+ i_strdup_printf("fts_tika: PUT %s failed: %s",
+ mail_user_plugin_getenv(parser->user, "fts_tika"),
+ http_response_get_message(response));
+ parser->payload = i_stream_create_from_data("", 0);
+ } else {
+ i_error("fts_tika: PUT %s failed: %s",
+ mail_user_plugin_getenv(parser->user, "fts_tika"),
+ http_response_get_message(response));
+ parser->failed = TRUE;
+ }
+ break;
+ }
+ parser->http_req = NULL;
+ io_loop_stop(current_ioloop);
+}
+
+static struct fts_parser *
+fts_parser_tika_try_init(struct fts_parser_context *parser_context)
+{
+ struct tika_fts_parser *parser;
+ struct http_url *http_url;
+ struct http_client_request *http_req;
+
+ if (tika_get_http_client_url(parser_context->user, &http_url) < 0)
+ return NULL;
+ if (http_url->path == NULL)
+ http_url->path = "/";
+
+ parser = i_new(struct tika_fts_parser, 1);
+ parser->parser.v = fts_parser_tika;
+ parser->user = parser_context->user;
+
+ http_req = http_client_request(tika_http_client, "PUT",
+ http_url->host.name,
+ t_strconcat(http_url->path, http_url->enc_query, NULL),
+ fts_tika_parser_response, parser);
+ http_client_request_set_port(http_req, http_url->port);
+ http_client_request_set_ssl(http_req, http_url->have_ssl);
+ if (parser_context->content_type != NULL)
+ http_client_request_add_header(http_req, "Content-Type",
+ parser_context->content_type);
+ if (parser_context->content_disposition != NULL)
+ http_client_request_add_header(http_req, "Content-Disposition",
+ parser_context->content_disposition);
+ http_client_request_add_header(http_req, "Accept", "text/plain");
+
+ parser->http_req = http_req;
+ return &parser->parser;
+}
+
+static void fts_parser_tika_more(struct fts_parser *_parser,
+ struct message_block *block)
+{
+ struct tika_fts_parser *parser = (struct tika_fts_parser *)_parser;
+ struct ioloop *prev_ioloop = current_ioloop;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ if (block->size > 0) {
+ /* first we'll send everything to Tika */
+ if (!parser->failed &&
+ http_client_request_send_payload(&parser->http_req,
+ block->data,
+ block->size) < 0)
+ parser->failed = TRUE;
+ block->size = 0;
+ return;
+ }
+
+ if (parser->payload == NULL) {
+ /* read the result from Tika */
+ if (!parser->failed &&
+ http_client_request_finish_payload(&parser->http_req) < 0)
+ parser->failed = TRUE;
+ if (!parser->failed && parser->payload == NULL)
+ http_client_wait(tika_http_client);
+ if (parser->failed)
+ return;
+ i_assert(parser->payload != NULL);
+ }
+ /* continue returning data from Tika. we'll create a new ioloop just
+ for reading this one payload. */
+ while ((ret = i_stream_read_more(parser->payload, &data, &size)) == 0) {
+ if (parser->failed)
+ break;
+ /* wait for more input from Tika */
+ if (parser->ioloop == NULL) {
+ parser->ioloop = io_loop_create();
+ parser->io = io_add_istream(parser->payload, io_loop_stop,
+ current_ioloop);
+ } else {
+ io_loop_set_current(parser->ioloop);
+ }
+ io_loop_run(current_ioloop);
+ }
+ /* switch back to original ioloop. */
+ io_loop_set_current(prev_ioloop);
+
+ if (parser->failed)
+ ;
+ else if (size > 0) {
+ i_assert(ret > 0);
+ block->data = data;
+ block->size = size;
+ i_stream_skip(parser->payload, size);
+ } else {
+ /* finished */
+ i_assert(ret == -1);
+ if (parser->payload->stream_errno != 0) {
+ i_error("read(%s) failed: %s",
+ i_stream_get_name(parser->payload),
+ i_stream_get_error(parser->payload));
+ parser->failed = TRUE;
+ }
+ }
+}
+
+static int fts_parser_tika_deinit(struct fts_parser *_parser, const char **retriable_err_msg_r)
+{
+ struct tika_fts_parser *parser = (struct tika_fts_parser *)_parser;
+ int ret = _parser->may_need_retry ? 0: (parser->failed ? -1 : 1);
+
+ i_assert(ret != 0 || _parser->retriable_error_msg != NULL);
+ if (retriable_err_msg_r != NULL)
+ *retriable_err_msg_r = t_strdup(_parser->retriable_error_msg);
+ i_free(_parser->retriable_error_msg);
+
+ /* remove io before unrefing payload - otherwise lib-http adds another
+ timeout to ioloop unnecessarily */
+ i_stream_unref(&parser->payload);
+ io_remove(&parser->io);
+ http_client_request_abort(&parser->http_req);
+ if (parser->ioloop != NULL) {
+ io_loop_set_current(parser->ioloop);
+ io_loop_destroy(&parser->ioloop);
+ }
+ i_free(parser);
+ return ret;
+}
+
+static void fts_parser_tika_unload(void)
+{
+ if (tika_http_client != NULL)
+ http_client_deinit(&tika_http_client);
+}
+
+struct fts_parser_vfuncs fts_parser_tika = {
+ fts_parser_tika_try_init,
+ fts_parser_tika_more,
+ fts_parser_tika_deinit,
+ fts_parser_tika_unload
+};
diff --git a/src/plugins/fts/fts-parser.c b/src/plugins/fts/fts-parser.c
new file mode 100644
index 0000000..c0eac80
--- /dev/null
+++ b/src/plugins/fts/fts-parser.c
@@ -0,0 +1,127 @@
+/* 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 "fts-parser.h"
+
+static const struct fts_parser_vfuncs *parsers[] = {
+ &fts_parser_html,
+ &fts_parser_script,
+ &fts_parser_tika
+};
+
+static const char *plaintext_content_types[] = {
+ "text/plain",
+ "message/delivery-status",
+ "message/disposition-notification",
+ "application/pgp-signature",
+ NULL
+};
+
+bool fts_parser_init(struct fts_parser_context *parser_context,
+ struct fts_parser **parser_r)
+{
+ unsigned int i;
+ i_assert(parser_context->user != NULL);
+ i_assert(parser_context->content_type != NULL);
+
+ if (str_array_find(plaintext_content_types, parser_context->content_type)) {
+ /* we probably don't want/need to allow parsers to handle
+ plaintext? */
+ return FALSE;
+ }
+
+ for (i = 0; i < N_ELEMENTS(parsers); i++) {
+ *parser_r = parsers[i]->try_init(parser_context);
+ if (*parser_r != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+struct fts_parser *fts_parser_text_init(void)
+{
+ return i_new(struct fts_parser, 1);
+}
+
+static bool data_has_nuls(const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\0')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void replace_nul_bytes(buffer_t *buf)
+{
+ unsigned char *data;
+ size_t i, size;
+
+ data = buffer_get_modifiable_data(buf, &size);
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\0')
+ data[i] = ' ';
+ }
+}
+
+void fts_parser_more(struct fts_parser *parser, struct message_block *block)
+{
+ if (parser->v.more != NULL)
+ parser->v.more(parser, block);
+
+ if (!uni_utf8_data_is_valid(block->data, block->size) ||
+ data_has_nuls(block->data, block->size)) {
+ /* output isn't valid UTF-8. make it. */
+ if (parser->utf8_output == NULL) {
+ parser->utf8_output =
+ buffer_create_dynamic(default_pool, 4096);
+ } else {
+ buffer_set_used_size(parser->utf8_output, 0);
+ }
+ if (uni_utf8_get_valid_data(block->data, block->size,
+ parser->utf8_output)) {
+ /* valid UTF-8, but there were NULs */
+ buffer_append(parser->utf8_output, block->data,
+ block->size);
+ }
+ replace_nul_bytes(parser->utf8_output);
+ block->data = parser->utf8_output->data;
+ block->size = parser->utf8_output->used;
+ }
+}
+
+int fts_parser_deinit(struct fts_parser **_parser, const char **retriable_err_msg_r)
+{
+ struct fts_parser *parser = *_parser;
+ int ret = 1;
+
+ *_parser = NULL;
+
+ buffer_free(&parser->utf8_output);
+ if (parser->v.deinit != NULL) {
+ const char *error = NULL;
+ ret = parser->v.deinit(parser, &error);
+ if (ret == 0) {
+ i_assert(error != NULL);
+ if (retriable_err_msg_r != NULL)
+ *retriable_err_msg_r = error;
+ }
+ } else
+ i_free(parser);
+ return ret;
+}
+
+void fts_parsers_unload(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(parsers); i++) {
+ if (parsers[i]->unload != NULL)
+ parsers[i]->unload();
+ }
+}
diff --git a/src/plugins/fts/fts-parser.h b/src/plugins/fts/fts-parser.h
new file mode 100644
index 0000000..0eb716e
--- /dev/null
+++ b/src/plugins/fts/fts-parser.h
@@ -0,0 +1,48 @@
+#ifndef FTS_PARSER_H
+#define FTS_PARSER_H
+
+struct message_block;
+struct mail_user;
+
+struct fts_parser_context {
+ /* Can't be NULL */
+ struct mail_user *user;
+ /* Can't be NULL */
+ const char *content_type;
+ const char *content_disposition;
+};
+
+struct fts_parser_vfuncs {
+ struct fts_parser *(*try_init)(struct fts_parser_context *parser_context);
+ void (*more)(struct fts_parser *parser, struct message_block *block);
+ int (*deinit)(struct fts_parser *parser, const char **retriable_err_msg_r);
+ void (*unload)(void);
+};
+
+struct fts_parser {
+ struct fts_parser_vfuncs v;
+ buffer_t *utf8_output;
+ bool may_need_retry;
+ char *retriable_error_msg;
+};
+
+extern struct fts_parser_vfuncs fts_parser_html;
+extern struct fts_parser_vfuncs fts_parser_script;
+extern struct fts_parser_vfuncs fts_parser_tika;
+
+bool fts_parser_init(struct fts_parser_context *parser_context,
+ struct fts_parser **parser_r);
+struct fts_parser *fts_parser_text_init(void);
+
+/* The parser is initially called with message body blocks. Once message is
+ finished, it's still called with incoming size=0 while the parser increases
+ it to non-zero. */
+void fts_parser_more(struct fts_parser *parser, struct message_block *block);
+/* Returns 1 if ok, 0 if the parsing should be retried, -1 if error.
+ If 0 is returned, the retriable_err_msg_r is set, which should be logged
+ as error if no retrying is performed. */
+int fts_parser_deinit(struct fts_parser **parser, const char **retriable_err_msg_r);
+
+void fts_parsers_unload(void);
+
+#endif
diff --git a/src/plugins/fts/fts-plugin.c b/src/plugins/fts/fts-plugin.c
new file mode 100644
index 0000000..1902cb6
--- /dev/null
+++ b/src/plugins/fts/fts-plugin.c
@@ -0,0 +1,33 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage-hooks.h"
+#include "fts-filter.h"
+#include "fts-tokenizer.h"
+#include "fts-parser.h"
+#include "fts-storage.h"
+#include "fts-user.h"
+#include "fts-plugin.h"
+#include "fts-library.h"
+
+const char *fts_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct mail_storage_hooks fts_mail_storage_hooks = {
+ .mail_namespaces_added = fts_mail_namespaces_added,
+ .mailbox_list_created = fts_mailbox_list_created,
+ .mailbox_allocated = fts_mailbox_allocated,
+ .mail_allocated = fts_mail_allocated
+};
+
+void fts_plugin_init(struct module *module)
+{
+ fts_library_init();
+ mail_storage_hooks_add(module, &fts_mail_storage_hooks);
+}
+
+void fts_plugin_deinit(void)
+{
+ fts_library_deinit();
+ fts_parsers_unload();
+ mail_storage_hooks_remove(&fts_mail_storage_hooks);
+}
diff --git a/src/plugins/fts/fts-plugin.h b/src/plugins/fts/fts-plugin.h
new file mode 100644
index 0000000..aeec68c
--- /dev/null
+++ b/src/plugins/fts/fts-plugin.h
@@ -0,0 +1,7 @@
+#ifndef FTS_PLUGIN_H
+#define FTS_PLUGIN_H
+
+void fts_plugin_init(struct module *module);
+void fts_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/fts/fts-search-args.c b/src/plugins/fts/fts-search-args.c
new file mode 100644
index 0000000..6b83573
--- /dev/null
+++ b/src/plugins/fts/fts-search-args.c
@@ -0,0 +1,426 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-namespace.h"
+#include "mail-search.h"
+#include "fts-api-private.h"
+#include "fts-tokenizer.h"
+#include "fts-filter-private.h"
+#include "fts-user.h"
+#include "fts-search-args.h"
+#include "fts-language.h"
+
+#define STOPWORDS_WORKAROUND_KEY "fts_stopwords_workaround"
+
+static void strings_deduplicate(ARRAY_TYPE(const_string) *arr)
+{
+ const char *const *strings;
+ unsigned int i, count;
+
+ strings = array_get(arr, &count);
+ for (i = 1; i < count; ) {
+ if (strcmp(strings[i-1], strings[i]) == 0) {
+ array_delete(arr, i, 1);
+ strings = array_get(arr, &count);
+ } else {
+ i++;
+ }
+ }
+}
+
+static struct mail_search_arg *
+fts_search_arg_create_or(const struct mail_search_arg *orig_arg, pool_t pool,
+ const ARRAY_TYPE(const_string) *tokens)
+{
+ struct mail_search_arg *arg, *or_arg, **argp;
+ const char *token;
+
+ /* create the OR arg first as the parent */
+ or_arg = p_new(pool, struct mail_search_arg, 1);
+ or_arg->type = SEARCH_OR;
+
+ /* now create all the child args for the OR */
+ argp = &or_arg->value.subargs;
+ array_foreach_elem(tokens, token) {
+ arg = p_new(pool, struct mail_search_arg, 1);
+ *arg = *orig_arg;
+ arg->match_not = FALSE; /* we copied this to the root OR */
+ arg->next = NULL;
+ arg->value.str = p_strdup(pool, token);
+
+ *argp = arg;
+ argp = &arg->next;
+ }
+ return or_arg;
+}
+
+static int
+fts_backend_dovecot_expand_tokens(struct fts_filter *filter,
+ pool_t pool,
+ struct mail_search_arg *parent_arg,
+ const struct mail_search_arg *orig_arg,
+ const char *orig_token, const char *token,
+ const char **error_r)
+{
+ struct mail_search_arg *arg;
+ ARRAY_TYPE(const_string) tokens;
+ const char *token2, *error;
+ int ret;
+
+ t_array_init(&tokens, 4);
+ /* first add the word exactly as it without any tokenization */
+ array_push_back(&tokens, &orig_token);
+ /* then add it tokenized, but without filtering */
+ array_push_back(&tokens, &token);
+
+ /* add the word filtered */
+ if (filter != NULL) {
+ token2 = t_strdup(token);
+ ret = fts_filter_filter(filter, &token2, &error);
+ if (ret > 0) {
+ token2 = t_strdup(token2);
+ array_push_back(&tokens, &token2);
+ } else if (ret < 0) {
+ *error_r = t_strdup_printf("Couldn't filter search token: %s", error);
+ return -1;
+ } else {
+ /* The filter dropped the token, which means it was
+ never even indexed. Ignore this word entirely in the
+ search query. */
+ return 0;
+ }
+ }
+ array_sort(&tokens, i_strcmp_p);
+ strings_deduplicate(&tokens);
+
+ arg = fts_search_arg_create_or(orig_arg, pool, &tokens);
+ arg->next = parent_arg->value.subargs;
+ parent_arg->value.subargs = arg;
+ return 0;
+}
+
+#define BOTTOM_LANGUAGE_EXPANSION TRUE
+#define TOP_LANGUAGE_EXPANSION FALSE
+
+static int
+fts_backend_dovecot_tokenize_lang(struct fts_user_language *user_lang,
+ pool_t pool, struct mail_search_arg *or_arg,
+ struct mail_search_arg *orig_arg,
+ const char *orig_token,
+ bool bottom_language_expansion,
+ const char **error_r)
+{
+ size_t orig_token_len = strlen(orig_token);
+ struct mail_search_arg *and_arg, *orig_or_args = or_arg->value.subargs;
+ const char *token, *error;
+ int ret;
+
+ /* we want all the tokens found from the string to be found, so create
+ a parent AND and place all the filtered token alternatives under
+ it */
+ and_arg = p_new(pool, struct mail_search_arg, 1);
+ and_arg->type = SEARCH_SUB;
+ and_arg->next = orig_or_args;
+ or_arg->value.subargs = and_arg;
+
+ /* reset tokenizer between search args in case there's any state left
+ from some previous failure */
+ fts_tokenizer_reset(user_lang->search_tokenizer);
+ while ((ret = fts_tokenizer_next(user_lang->search_tokenizer,
+ (const void *)orig_token,
+ orig_token_len, &token, &error)) > 0) {
+ if (fts_backend_dovecot_expand_tokens(user_lang->filter, pool,
+ and_arg, orig_arg, orig_token,
+ token, error_r) < 0)
+ return -1;
+ }
+ while (ret >= 0 &&
+ (ret = fts_tokenizer_final(user_lang->search_tokenizer, &token, &error)) > 0) {
+ if (fts_backend_dovecot_expand_tokens(user_lang->filter, pool,
+ and_arg, orig_arg, orig_token,
+ token, error_r) < 0)
+ return -1;
+ }
+ if (ret < 0) {
+ *error_r = t_strdup_printf("Couldn't tokenize search args: %s", error);
+ return -1;
+ }
+ if (and_arg->value.subargs == NULL) {
+ if (bottom_language_expansion) {
+ /* remove this empty term entirely */
+ or_arg->value.subargs = orig_or_args;
+ } else {
+ /* The simplifier will propagate the NIL to the
+ upper operators, if required, and remove it at
+ the appropriate level */
+ and_arg->type = SEARCH_NIL;
+ }
+ }
+ return 0;
+}
+
+static int fts_search_arg_expand(struct fts_backend *backend, pool_t pool,
+ struct fts_user_language *lang,
+ bool bottom_language_expansion,
+ struct mail_search_arg **argp)
+{
+ const ARRAY_TYPE(fts_user_language) *languages;
+ ARRAY_TYPE(fts_user_language) langs;
+ struct mail_search_arg *or_arg, *orig_arg = *argp;
+ const char *error, *orig_token = orig_arg->value.str;
+
+ /* If we are invoked with no lang (null), we are operating in a bottom
+ language expansion, which is iterated here. In this case we also expect
+ to be removing NILs terms.
+
+ Otherwise, if we are invoked with a specific lang, we are working in
+ a top level language expansion (done above us), and we do NOT want to
+ remove the NIL terms. */
+ i_assert(bottom_language_expansion == (lang == NULL));
+
+ if (((*argp)->type == SEARCH_HEADER ||
+ (*argp)->type == SEARCH_HEADER_ADDRESS ||
+ (*argp)->type == SEARCH_HEADER_COMPRESS_LWSP) &&
+ !fts_header_has_language((*argp)->hdr_field_name)) {
+ /* use only the data-language */
+ lang = fts_user_get_data_lang(backend->ns->user);
+ }
+ if (lang != NULL) {
+ /* getting here either in case of bottom language expansion OR
+ in case of language-less headers ... */
+ t_array_init(&langs, 1);
+ array_push_back(&langs, &lang);
+ languages = &langs;
+ } else {
+ /* ... otherwise getting here in case of top language expansion */
+ languages = fts_user_get_all_languages(backend->ns->user);
+ }
+
+ /* OR together all the different expansions for different languages.
+ it's enough for one of them to match. */
+ or_arg = p_new(pool, struct mail_search_arg, 1);
+ or_arg->type = SEARCH_OR;
+ or_arg->match_not = orig_arg->match_not;
+ or_arg->next = orig_arg->next;
+
+ /* this reduces to one single iteration on top language expansion or
+ languageless headers */
+ array_foreach_elem(languages, lang) {
+ if (fts_backend_dovecot_tokenize_lang(lang, pool, or_arg,
+ orig_arg, orig_token,
+ bottom_language_expansion,
+ &error) < 0) {
+ i_error("fts: %s", error);
+ return -1;
+ }
+ }
+
+ if (or_arg->value.subargs == NULL) {
+ /* We couldn't parse any tokens from the input.
+ This can happen only in bottom level expansion,
+ as in top level we grant that expansion always
+ produces at least a NIL term */
+ or_arg->type = SEARCH_ALL;
+ or_arg->match_not = !or_arg->match_not;
+ }
+ *argp = or_arg;
+ return 0;
+}
+
+static int
+fts_search_args_expand_tree(struct fts_backend *backend, pool_t pool,
+ struct fts_user_language *lang,
+ bool bottom_language_expansion,
+ struct mail_search_arg **argp)
+{
+ int ret;
+
+ for (; *argp != NULL; argp = &(*argp)->next) {
+ switch ((*argp)->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (fts_search_args_expand_tree(backend, pool, lang,
+ bottom_language_expansion,
+ &(*argp)->value.subargs) < 0)
+ return -1;
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if ((*argp)->value.str[0] == '\0') {
+ /* we're testing for the existence of
+ the header */
+ break;
+ }
+ /* fall through */
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ T_BEGIN {
+ ret = fts_search_arg_expand(backend, pool, lang,
+ bottom_language_expansion,
+ argp);
+ } T_END;
+ if (ret < 0)
+ return -1;
+ break;
+ default:
+ break;
+ }
+ }
+ return 0;
+}
+
+/* Takes in input the whole expression tree, as an implicit AND of argp-list terms.
+ Replaces the input expression tree with a single OR term, containing one AND
+ entry for each language, each AND entry containing a copy of the original
+ argp-list of terms, Then it expands each AND subargs-list according to the language.
+
+ Input: implicit-AND(argp-list)
+ Output: OR(lang1(AND(argp-list-copy), lang2(AND(argp-list-copy)) ...) */
+static int
+fts_search_args_expand_languages(struct fts_backend *backend, pool_t pool,
+ struct mail_search_arg **argp)
+{
+ if (*argp == NULL)
+ return 0;
+
+ /* ensure there is an explicit top node wich has onyl a single term,
+ be it either an AND or an OR node */
+ bool top_is_or = (*argp)->type == SEARCH_OR && (*argp)->next == NULL;
+ struct mail_search_arg *top_arg;
+ if (top_is_or) {
+ /* we already have a single top entry of type OR, reuse it */
+ top_arg = (*argp);
+ } else {
+ /* create a single top entry of type AND with the original args */
+ top_arg = p_new(pool, struct mail_search_arg, 1);
+ top_arg->value.subargs = (*argp);
+ }
+
+ /* the top node will be populated from scratch with the language expansions */
+ struct mail_search_arg *top_subargs = top_arg->value.subargs;
+ top_arg->value.subargs = NULL;
+
+ int direct = 0, negated = 0;
+ for (struct mail_search_arg *arg = top_subargs; arg != NULL; arg = arg->next, ++direct)
+ if (arg->match_not) ++negated, --direct;
+
+ #define XOR != /* '!=' is the boolean equivalent of bitwise xor '^' */
+
+ /* likely we might want a simplification that pushes all the negations
+ toward the root of the node before doing this, rather than the current
+ one that pushes them toward the leaves ? */
+
+ /* THIS CASE IS THE GREY ZONE ---------------------------------|______________| */
+ bool want_invert = negated == 0 ? FALSE : direct == 0 ? TRUE : negated > direct;
+ bool invert = want_invert XOR top_arg->match_not;
+
+ if (invert) {
+ top_arg->type = top_arg->type != SEARCH_OR ? SEARCH_OR : SEARCH_SUB;
+ for (struct mail_search_arg *arg = top_subargs; arg != NULL; arg = arg->next)
+ arg->match_not = !arg->match_not;
+ }
+
+ const ARRAY_TYPE(fts_user_language) *languages =
+ fts_user_get_all_languages(backend->ns->user);
+ struct fts_user_language *lang;
+ array_foreach_elem(languages, lang) {
+ struct mail_search_arg *lang_arg = p_new(pool, struct mail_search_arg, 1);
+ lang_arg->type = top_is_or XOR invert ? SEARCH_OR : SEARCH_SUB;
+ lang_arg->match_not = invert;
+ lang_arg->value.subargs = mail_search_arg_dup(pool, top_subargs);
+
+ if (fts_search_args_expand_tree(backend, pool, lang,
+ TOP_LANGUAGE_EXPANSION,
+ &lang_arg->value.subargs) < 0)
+ return -1;
+
+ lang_arg->next = top_arg->value.subargs;
+ top_arg->value.subargs = lang_arg;
+ }
+
+ *argp = top_arg;
+ return 0;
+}
+
+
+static bool fts_lang_has_stopwords(const struct fts_user_language *lang)
+{
+ struct fts_filter *filter;
+ for (filter = lang->filter; filter != NULL; filter = filter->parent)
+ if (strcmp(filter->class_name, "stopwords") == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static bool
+fts_search_args_expand_language_top_level(struct fts_backend *backend)
+{
+ const char *setting = mail_user_plugin_getenv(
+ backend->ns->user, STOPWORDS_WORKAROUND_KEY);
+
+ if (setting != NULL) {
+ if (strcasecmp(setting, "no") == 0) return FALSE;
+ if (strcasecmp(setting, "yes") == 0) return TRUE;
+ /* otherwise we imply auto */
+ }
+
+ struct fts_user_language *lang;
+ const ARRAY_TYPE(fts_user_language) *languages =
+ fts_user_get_all_languages(backend->ns->user);
+
+ unsigned int langs_count = 0;
+ bool stopwords = FALSE;
+ array_foreach_elem(languages, lang) {
+ if (strcmp(lang->lang->name, "data") == 0)
+ continue;
+
+ if (fts_lang_has_stopwords(lang))
+ stopwords = TRUE;
+
+ if (stopwords && ++langs_count > 1)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int fts_search_args_expand(struct fts_backend *backend,
+ struct mail_search_args *args)
+{
+ struct mail_search_arg *args_dup, *orig_args = args->args;
+
+ /* don't keep re-expanding every time the search args are used.
+ this is especially important to avoid an assert-crash in
+ index_search_result_update_flags(). */
+ if (args->fts_expanded)
+ return 0;
+ args->fts_expanded = TRUE;
+
+ /* duplicate the args, so if expansion fails we haven't changed
+ anything */
+ args_dup = mail_search_arg_dup(args->pool, args->args);
+
+ if (fts_search_args_expand_language_top_level(backend)) {
+ if (fts_search_args_expand_languages(
+ backend, args->pool, &args_dup) < 0)
+ return -1;
+ } else {
+ if (fts_search_args_expand_tree(
+ backend, args->pool, NULL, BOTTOM_LANGUAGE_EXPANSION,
+ &args_dup) < 0)
+ return -1;
+ }
+
+ /* we'll need to re-simplify the args if we changed anything */
+ args->simplified = FALSE;
+ args->args = args_dup;
+ mail_search_args_simplify(args);
+
+ /* duplicated args aren't initialized */
+ i_assert(args->init_refcount > 0);
+ mail_search_arg_init(args, args_dup);
+ mail_search_arg_deinit(orig_args);
+ return 0;
+}
diff --git a/src/plugins/fts/fts-search-args.h b/src/plugins/fts/fts-search-args.h
new file mode 100644
index 0000000..9fb8923
--- /dev/null
+++ b/src/plugins/fts/fts-search-args.h
@@ -0,0 +1,7 @@
+#ifndef FTS_SEARCH_ARGS_H
+#define FTS_SEARCH_ARGS_H
+
+int fts_search_args_expand(struct fts_backend *backend,
+ struct mail_search_args *args);
+
+#endif
diff --git a/src/plugins/fts/fts-search-serialize.c b/src/plugins/fts/fts-search-serialize.c
new file mode 100644
index 0000000..e30d4ce
--- /dev/null
+++ b/src/plugins/fts/fts-search-serialize.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "mail-search.h"
+#include "fts-search-serialize.h"
+
+#define HAVE_SUBARGS(arg) \
+ ((arg)->type == SEARCH_SUB || (arg)->type == SEARCH_OR)
+
+void fts_search_serialize(buffer_t *buf, const struct mail_search_arg *args)
+{
+ char chr;
+
+ for (; args != NULL; args = args->next) {
+ chr = (args->match_always ? 1 : 0) |
+ (args->nonmatch_always ? 2 : 0);
+ buffer_append_c(buf, chr);
+
+ if (HAVE_SUBARGS(args))
+ fts_search_serialize(buf, args->value.subargs);
+ }
+}
+
+static void fts_search_deserialize_idx(struct mail_search_arg *args,
+ const buffer_t *buf, unsigned int *idx)
+{
+ const char *data = buf->data;
+
+ for (; args != NULL; args = args->next) {
+ i_assert(*idx < buf->used);
+
+ args->match_always = (data[*idx] & 1) != 0;
+ args->nonmatch_always = (data[*idx] & 2) != 0;
+ args->result = args->match_always ? 1 :
+ (args->nonmatch_always ? 0 : -1);
+ *idx += 1;
+
+ if (HAVE_SUBARGS(args)) {
+ fts_search_deserialize_idx(args->value.subargs,
+ buf, idx);
+ }
+ }
+}
+
+void fts_search_deserialize(struct mail_search_arg *args,
+ const buffer_t *buf)
+{
+ unsigned int idx = 0;
+
+ fts_search_deserialize_idx(args, buf, &idx);
+ i_assert(idx == buf->used);
+}
+
+static void
+fts_search_deserialize_add_idx(struct mail_search_arg *args,
+ const buffer_t *buf, unsigned int *idx,
+ bool matches)
+{
+ const char *data = buf->data;
+
+ for (; args != NULL; args = args->next) {
+ i_assert(*idx < buf->used);
+
+ if (data[*idx] != 0) {
+ if (matches) {
+ args->match_always = TRUE;
+ args->result = 1;
+ } else {
+ args->nonmatch_always = TRUE;
+ args->result = 0;
+ }
+ }
+ *idx += 1;
+
+ if (HAVE_SUBARGS(args)) {
+ fts_search_deserialize_add_idx(args->value.subargs,
+ buf, idx, matches);
+ }
+ }
+}
+
+void fts_search_deserialize_add_matches(struct mail_search_arg *args,
+ const buffer_t *buf)
+{
+ unsigned int idx = 0;
+
+ fts_search_deserialize_add_idx(args, buf, &idx, TRUE);
+ i_assert(idx == buf->used);
+}
+
+void fts_search_deserialize_add_nonmatches(struct mail_search_arg *args,
+ const buffer_t *buf)
+{
+ unsigned int idx = 0;
+
+ fts_search_deserialize_add_idx(args, buf, &idx, FALSE);
+ i_assert(idx == buf->used);
+}
diff --git a/src/plugins/fts/fts-search-serialize.h b/src/plugins/fts/fts-search-serialize.h
new file mode 100644
index 0000000..c1a7d88
--- /dev/null
+++ b/src/plugins/fts/fts-search-serialize.h
@@ -0,0 +1,16 @@
+#ifndef FTS_SEARCH_SERIALIZE_H
+#define FTS_SEARCH_SERIALIZE_H
+
+/* serialize [non]match_always fields (clearing buffer) */
+void fts_search_serialize(buffer_t *buf, const struct mail_search_arg *args);
+/* add/remove [non]match_always fields in search args */
+void fts_search_deserialize(struct mail_search_arg *args,
+ const buffer_t *buf);
+/* add match_always=TRUE fields to search args */
+void fts_search_deserialize_add_matches(struct mail_search_arg *args,
+ const buffer_t *buf);
+/* add nonmatch_always=TRUE fields to search args */
+void fts_search_deserialize_add_nonmatches(struct mail_search_arg *args,
+ const buffer_t *buf);
+
+#endif
diff --git a/src/plugins/fts/fts-search.c b/src/plugins/fts/fts-search.c
new file mode 100644
index 0000000..895ea59
--- /dev/null
+++ b/src/plugins/fts/fts-search.c
@@ -0,0 +1,385 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "seq-range-array.h"
+#include "mail-search.h"
+#include "fts-api-private.h"
+#include "fts-search-args.h"
+#include "fts-search-serialize.h"
+#include "fts-storage.h"
+#include "hash.h"
+
+static void
+uid_range_to_seqs(struct fts_search_context *fctx,
+ const ARRAY_TYPE(seq_range) *uid_range,
+ ARRAY_TYPE(seq_range) *seq_range)
+{
+ const struct seq_range *range;
+ unsigned int i, count;
+ uint32_t seq1, seq2;
+
+ range = array_get(uid_range, &count);
+ if (!array_is_created(seq_range))
+ p_array_init(seq_range, fctx->result_pool, count);
+ for (i = 0; i < count; i++) {
+ if (range[i].seq1 > range[i].seq2)
+ continue;
+ mailbox_get_seq_range(fctx->box, range[i].seq1, range[i].seq2,
+ &seq1, &seq2);
+ if (seq1 != 0)
+ seq_range_array_add_range(seq_range, seq1, seq2);
+ }
+}
+
+static int fts_search_lookup_level_single(struct fts_search_context *fctx,
+ struct mail_search_arg *args,
+ bool and_args)
+{
+ enum fts_lookup_flags flags = fctx->flags |
+ (and_args ? FTS_LOOKUP_FLAG_AND_ARGS : 0);
+ struct fts_search_level *level;
+ struct fts_result result;
+
+ i_zero(&result);
+ result.search_state = fctx->search_state;
+ result.pool = fctx->result_pool;
+ p_array_init(&result.definite_uids, fctx->result_pool, 32);
+ p_array_init(&result.maybe_uids, fctx->result_pool, 32);
+ p_array_init(&result.scores, fctx->result_pool, 32);
+
+ mail_search_args_reset(args, TRUE);
+ if (fts_backend_lookup(fctx->backend, fctx->box, args, flags,
+ &result) < 0)
+ return -1;
+
+ fctx->search_state = result.search_state;
+ level = array_append_space(&fctx->levels);
+ level->args_matches = buffer_create_dynamic(fctx->result_pool, 16);
+ fts_search_serialize(level->args_matches, args);
+
+ uid_range_to_seqs(fctx, &result.definite_uids, &level->definite_seqs);
+ uid_range_to_seqs(fctx, &result.maybe_uids, &level->maybe_seqs);
+ level->score_map = result.scores;
+ return 0;
+}
+
+static void
+level_scores_add_vuids(struct mailbox *box,
+ struct fts_search_level *level, struct fts_result *br)
+{
+ const struct fts_score_map *scores;
+ unsigned int i, count;
+ ARRAY_TYPE(seq_range) backend_uids;
+ ARRAY_TYPE(uint32_t) vuids_arr;
+ const uint32_t *vuids;
+ struct fts_score_map *score;
+
+ scores = array_get(&br->scores, &count);
+ t_array_init(&vuids_arr, count);
+ t_array_init(&backend_uids, 64);
+ for (i = 0; i < count; i++)
+ seq_range_array_add(&backend_uids, scores[i].uid);
+ box->virtual_vfuncs->get_virtual_uid_map(box, br->box,
+ &backend_uids, &vuids_arr);
+
+ i_assert(array_count(&vuids_arr) == array_count(&br->scores));
+ vuids = array_get(&vuids_arr, &count);
+ for (i = 0; i < count; i++) {
+ score = array_append_space(&level->score_map);
+ score->uid = vuids[i];
+ score->score = scores[i].score;
+ }
+}
+
+static int
+mailbox_cmp_fts_backend(struct mailbox *const *m1, struct mailbox *const *m2)
+{
+ struct fts_backend *b1, *b2;
+
+ b1 = fts_mailbox_backend(*m1);
+ b2 = fts_mailbox_backend(*m2);
+ if (b1 < b2)
+ return -1;
+ if (b1 > b2)
+ return 1;
+ return 0;
+}
+
+static int
+multi_add_lookup_result(struct fts_search_context *fctx,
+ struct fts_search_level *level,
+ struct mail_search_arg *args,
+ struct fts_multi_result *result)
+{
+ ARRAY_TYPE(seq_range) vuids;
+ size_t orig_size;
+ unsigned int i;
+
+ orig_size = level->args_matches->used;
+ fts_search_serialize(level->args_matches, args);
+ if (orig_size > 0) {
+ if (level->args_matches->used != orig_size * 2 ||
+ memcmp(level->args_matches->data,
+ CONST_PTR_OFFSET(level->args_matches->data,
+ orig_size), orig_size) != 0)
+ i_panic("incompatible fts backends for namespaces");
+ buffer_set_used_size(level->args_matches, orig_size);
+ }
+
+ t_array_init(&vuids, 64);
+ for (i = 0; result->box_results[i].box != NULL; i++) {
+ struct fts_result *br = &result->box_results[i];
+
+ array_clear(&vuids);
+ if (array_is_created(&br->definite_uids)) {
+ fctx->box->virtual_vfuncs->get_virtual_uids(fctx->box,
+ br->box, &br->definite_uids, &vuids);
+ }
+ uid_range_to_seqs(fctx, &vuids, &level->definite_seqs);
+
+ array_clear(&vuids);
+ if (array_is_created(&br->maybe_uids)) {
+ fctx->box->virtual_vfuncs->get_virtual_uids(fctx->box,
+ br->box, &br->maybe_uids, &vuids);
+ }
+ uid_range_to_seqs(fctx, &vuids, &level->maybe_seqs);
+
+ if (array_is_created(&br->scores))
+ level_scores_add_vuids(fctx->box, level, br);
+ }
+ return 0;
+}
+
+static int fts_search_lookup_level_multi(struct fts_search_context *fctx,
+ struct mail_search_arg *args,
+ bool and_args)
+{
+ enum fts_lookup_flags flags = fctx->flags |
+ (and_args ? FTS_LOOKUP_FLAG_AND_ARGS : 0);
+ ARRAY_TYPE(mailboxes) mailboxes_arr, tmp_mailboxes;
+ struct mailbox *const *mailboxes;
+ struct fts_backend *backend;
+ struct fts_search_level *level;
+ struct fts_multi_result result;
+ unsigned int i, j, mailbox_count;
+
+ p_array_init(&mailboxes_arr, fctx->result_pool, 8);
+ fctx->box->virtual_vfuncs->get_virtual_backend_boxes(fctx->box,
+ &mailboxes_arr, TRUE);
+ array_sort(&mailboxes_arr, mailbox_cmp_fts_backend);
+
+ i_zero(&result);
+ result.search_state = fctx->search_state;
+ result.pool = fctx->result_pool;
+
+ level = array_append_space(&fctx->levels);
+ level->args_matches = buffer_create_dynamic(fctx->result_pool, 16);
+ p_array_init(&level->score_map, fctx->result_pool, 1);
+
+ mailboxes = array_get(&mailboxes_arr, &mailbox_count);
+ t_array_init(&tmp_mailboxes, mailbox_count);
+ for (i = 0; i < mailbox_count; i = j) {
+ array_clear(&tmp_mailboxes);
+ array_push_back(&tmp_mailboxes, &mailboxes[i]);
+
+ backend = fts_mailbox_backend(mailboxes[i]);
+ for (j = i + 1; j < mailbox_count; j++) {
+ if (fts_mailbox_backend(mailboxes[j]) != backend)
+ break;
+ array_push_back(&tmp_mailboxes, &mailboxes[j]);
+ }
+ array_append_zero(&tmp_mailboxes);
+
+ mail_search_args_reset(args, TRUE);
+ if (fts_backend_lookup_multi(backend,
+ array_front(&tmp_mailboxes),
+ args, flags, &result) < 0)
+ return -1;
+
+ if (multi_add_lookup_result(fctx, level, args, &result) < 0)
+ return -1;
+ }
+ fctx->search_state = result.search_state;
+ return 0;
+}
+
+static int fts_search_lookup_level(struct fts_search_context *fctx,
+ struct mail_search_arg *args,
+ bool and_args)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = !fctx->virtual_mailbox ?
+ fts_search_lookup_level_single(fctx, args, and_args) :
+ fts_search_lookup_level_multi(fctx, args, and_args);
+ } T_END;
+ if (ret < 0)
+ return -1;
+
+ for (; args != NULL; args = args->next) {
+ if (args->type != SEARCH_OR && args->type != SEARCH_SUB)
+ continue;
+
+ if (fts_search_lookup_level(fctx, args->value.subargs,
+ args->type == SEARCH_SUB) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void
+fts_search_merge_scores_and(ARRAY_TYPE(fts_score_map) *dest,
+ const ARRAY_TYPE(fts_score_map) *src)
+{
+ struct fts_score_map *dest_map;
+ const struct fts_score_map *src_map;
+ unsigned int desti, srci, dest_count, src_count;
+
+ dest_map = array_get_modifiable(dest, &dest_count);
+ src_map = array_get(src, &src_count);
+
+ /* arg_scores are summed to current scores. we could drop UIDs that
+ don't exist in both, but that's just extra work so don't bother */
+ for (desti = srci = 0; desti < dest_count && srci < src_count;) {
+ if (dest_map[desti].uid < src_map[srci].uid)
+ desti++;
+ else if (dest_map[desti].uid > src_map[srci].uid)
+ srci++;
+ else {
+ if (dest_map[desti].score < src_map[srci].score)
+ dest_map[desti].score = src_map[srci].score;
+ desti++; srci++;
+ }
+ }
+}
+
+static void
+fts_search_merge_scores_or(ARRAY_TYPE(fts_score_map) *dest,
+ const ARRAY_TYPE(fts_score_map) *src)
+{
+ ARRAY_TYPE(fts_score_map) src2;
+ const struct fts_score_map *src_map, *src2_map;
+ unsigned int srci, src2i, src_count, src2_count;
+
+ t_array_init(&src2, array_count(dest));
+ array_append_array(&src2, dest);
+ array_clear(dest);
+
+ src_map = array_get(src, &src_count);
+ src2_map = array_get(&src2, &src2_count);
+
+ /* add any missing UIDs to current scores. if any existing UIDs have
+ lower scores than in arg_scores, increase them. */
+ for (srci = src2i = 0; srci < src_count || src2i < src2_count;) {
+ if (src2i == src2_count ||
+ src_map[srci].uid < src2_map[src2i].uid) {
+ array_push_back(dest, &src_map[srci]);
+ srci++;
+ } else if (srci == src_count ||
+ src_map[srci].uid > src2_map[src2i].uid) {
+ array_push_back(dest, &src2_map[src2i]);
+ src2i++;
+ } else {
+ i_assert(src_map[srci].uid == src2_map[src2i].uid);
+ if (src_map[srci].score > src2_map[src2i].score)
+ array_push_back(dest, &src_map[srci]);
+ else
+ array_push_back(dest, &src2_map[src2i]);
+ srci++; src2i++;
+ }
+ }
+}
+
+static void
+fts_search_merge_scores_level(struct fts_search_context *fctx,
+ struct mail_search_arg *args, unsigned int *idx,
+ bool and_args, ARRAY_TYPE(fts_score_map) *scores)
+{
+ const struct fts_search_level *level;
+ ARRAY_TYPE(fts_score_map) arg_scores;
+
+ i_assert(array_count(scores) == 0);
+
+ /*
+ The (simplified) args can look like:
+
+ A and B and (C or D) and (E or F) and ...
+ A or B or (C and D) or (E and F) or ...
+
+ The A op B part's scores are in level->scores. The child args'
+ scores are in the sub levels' scores.
+ */
+
+ level = array_idx(&fctx->levels, *idx);
+ array_append_array(scores, &level->score_map);
+
+ t_array_init(&arg_scores, 64);
+ for (; args != NULL; args = args->next) {
+ if (args->type != SEARCH_OR && args->type != SEARCH_SUB)
+ continue;
+
+ *idx += 1;
+ array_clear(&arg_scores);
+ fts_search_merge_scores_level(fctx, args->value.subargs, idx,
+ args->type == SEARCH_OR,
+ &arg_scores);
+
+ if (and_args)
+ fts_search_merge_scores_and(scores, &arg_scores);
+ else
+ fts_search_merge_scores_or(scores, &arg_scores);
+ }
+}
+
+static void fts_search_merge_scores(struct fts_search_context *fctx)
+{
+ unsigned int idx = 0;
+
+ fts_search_merge_scores_level(fctx, fctx->args->args, &idx,
+ TRUE, &fctx->scores->score_map);
+}
+
+static void fts_search_try_lookup(struct fts_search_context *fctx)
+{
+ uint32_t last_uid, seq1, seq2;
+
+ i_assert(array_count(&fctx->levels) == 0);
+ i_assert(fctx->args->simplified);
+
+ if (fts_backend_refresh(fctx->backend) < 0)
+ return;
+ if (fts_backend_get_last_uid(fctx->backend, fctx->box, &last_uid) < 0)
+ return;
+ mailbox_get_seq_range(fctx->box, last_uid+1, (uint32_t)-1,
+ &seq1, &seq2);
+ fctx->first_unindexed_seq = seq1 != 0 ? seq1 : (uint32_t)-1;
+
+ if (fctx->virtual_mailbox) {
+ hash_table_clear(fctx->last_indexed_virtual_uids, TRUE);
+ fctx->next_unindexed_seq = fctx->first_unindexed_seq;
+ }
+
+ if ((fctx->backend->flags & FTS_BACKEND_FLAG_TOKENIZED_INPUT) != 0) {
+ if (fts_search_args_expand(fctx->backend, fctx->args) < 0)
+ return;
+ }
+ fts_search_serialize(fctx->orig_matches, fctx->args->args);
+
+ if (fts_search_lookup_level(fctx, fctx->args->args, TRUE) == 0) {
+ fctx->fts_lookup_success = TRUE;
+ fts_search_merge_scores(fctx);
+ }
+
+ fts_search_deserialize(fctx->args->args, fctx->orig_matches);
+ fts_backend_lookup_done(fctx->backend);
+}
+
+void fts_search_lookup(struct fts_search_context *fctx)
+{
+ struct event_reason *reason = event_reason_begin("fts:lookup");
+ fts_search_try_lookup(fctx);
+ event_reason_end(&reason);
+}
diff --git a/src/plugins/fts/fts-storage.c b/src/plugins/fts/fts-storage.c
new file mode 100644
index 0000000..101d52a
--- /dev/null
+++ b/src/plugins/fts/fts-storage.c
@@ -0,0 +1,981 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+#include "write-full.h"
+#include "mail-search-build.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "fts-api-private.h"
+#include "fts-tokenizer.h"
+#include "fts-indexer.h"
+#include "fts-build-mail.h"
+#include "fts-search-serialize.h"
+#include "fts-plugin.h"
+#include "fts-user.h"
+#include "fts-storage.h"
+#include "hash.h"
+
+
+#define FTS_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_storage_module)
+#define FTS_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, fts_storage_module)
+#define FTS_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, fts_mail_module)
+#define FTS_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_mailbox_list_module)
+#define FTS_LIST_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, fts_mailbox_list_module)
+
+#define INDEXER_SOCKET_NAME "indexer"
+#define INDEXER_HANDSHAKE "VERSION\tindexer\t1\t0\n"
+
+struct fts_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+ struct fts_backend *backend;
+
+ const char *backend_name;
+ struct fts_backend_update_context *update_ctx;
+ unsigned int update_ctx_refcount;
+
+ bool failed:1;
+};
+
+struct fts_mailbox {
+ union mailbox_module_context module_ctx;
+ struct fts_backend_update_context *sync_update_ctx;
+ bool fts_mailbox_excluded;
+};
+
+struct fts_transaction_context {
+ union mailbox_transaction_module_context module_ctx;
+
+ struct fts_scores *scores;
+ uint32_t next_index_seq;
+ uint32_t highest_virtual_uid;
+ unsigned int precache_extra_count;
+
+ bool indexing:1;
+ bool precached:1;
+ bool mails_saved:1;
+ const char *failure_reason;
+};
+
+struct fts_mail {
+ union mail_module_context module_ctx;
+ char score[30];
+
+ bool virtual_mail:1;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(fts_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(fts_mail_module, &mail_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(fts_mailbox_list_module,
+ &mailbox_list_module_register);
+
+static int fts_mailbox_get_last_cached_seq(struct mailbox *box, uint32_t *seq_r)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(box->list);
+ uint32_t seq1, seq2, last_uid;
+
+ if (fts_backend_get_last_uid(flist->backend, box, &last_uid) < 0) {
+ mail_storage_set_internal_error(box->storage);
+ return -1;
+ }
+
+ if (last_uid == 0)
+ *seq_r = 0;
+ else {
+ mailbox_get_seq_range(box, 1, last_uid, &seq1, &seq2);
+ *seq_r = seq2;
+ }
+ return 0;
+}
+
+static int
+fts_mailbox_get_status(struct mailbox *box, enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(box);
+ uint32_t seq;
+
+ if (fbox->module_ctx.super.get_status(box, items, status_r) < 0)
+ return -1;
+
+ if ((items & STATUS_LAST_CACHED_SEQ) != 0) {
+ if (fts_mailbox_get_last_cached_seq(box, &seq) < 0)
+ return -1;
+
+ /* Always use the FTS's last_cached_seq. This is because we
+ don't want to reindex all mails to FTS if .cache file is
+ deleted. */
+ status_r->last_cached_seq = seq;
+ }
+ return 0;
+}
+
+
+static void fts_scores_unref(struct fts_scores **_scores)
+{
+ struct fts_scores *scores = *_scores;
+
+ *_scores = NULL;
+ if (--scores->refcount == 0) {
+ array_free(&scores->score_map);
+ i_free(scores);
+ }
+}
+
+static void fts_try_build_init(struct mail_search_context *ctx,
+ struct fts_search_context *fctx)
+{
+ int ret;
+
+ i_assert(!fts_backend_is_updating(fctx->backend));
+
+ ret = fts_indexer_init(fctx->backend, ctx->transaction->box,
+ &fctx->indexer_ctx);
+ if (ret < 0)
+ return;
+
+ if (ret == 0) {
+ /* the index was up to date */
+ fts_search_lookup(fctx);
+ } else {
+ /* hide "searching" notifications while building index */
+ ctx->progress_hidden = TRUE;
+ }
+}
+
+static bool fts_want_build_args(const struct mail_search_arg *args)
+{
+ /* we want to update index only when searching from message body.
+ it's not worth the wait for searching only from headers, which
+ could be in cache file already */
+ for (; args != NULL; args = args->next) {
+ switch (args->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (fts_want_build_args(args->value.subargs))
+ return TRUE;
+ break;
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ if (!args->no_fts)
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return FALSE;
+}
+
+static bool fts_args_have_fuzzy(const struct mail_search_arg *args)
+{
+ for (; args != NULL; args = args->next) {
+ if (args->fuzzy)
+ return TRUE;
+ switch (args->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ if (fts_args_have_fuzzy(args->value.subargs))
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return FALSE;
+}
+
+static enum fts_enforced fts_enforced_parse(const char *str)
+{
+ if (str == NULL || strcmp(str, "no") == 0)
+ return FTS_ENFORCED_NO;
+ else if (strcmp(str, "body") == 0)
+ return FTS_ENFORCED_BODY;
+ else
+ return FTS_ENFORCED_YES;
+}
+
+static struct mail_search_context *
+fts_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)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(t);
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(t->box);
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(t->box->list);
+ struct mail_search_context *ctx;
+ struct fts_search_context *fctx;
+
+ ctx = fbox->module_ctx.super.search_init(t, args, sort_program,
+ wanted_fields, wanted_headers);
+
+ if (!fts_backend_can_lookup(flist->backend, args->args))
+ return ctx;
+
+ fctx = i_new(struct fts_search_context, 1);
+ fctx->box = t->box;
+ fctx->backend = flist->backend;
+ fctx->t = t;
+ fctx->args = args;
+ fctx->result_pool = pool_alloconly_create("fts results", 1024*64);
+ fctx->orig_matches = buffer_create_dynamic(default_pool, 64);
+ fctx->virtual_mailbox = t->box->virtual_vfuncs != NULL;
+ if (fctx->virtual_mailbox) {
+ hash_table_create(&fctx->last_indexed_virtual_uids,
+ default_pool, 0, str_hash, strcmp);
+ }
+ fctx->enforced = fts_enforced_parse(
+ mail_user_plugin_getenv(t->box->storage->user, "fts_enforced"));
+ i_array_init(&fctx->levels, 8);
+ fctx->scores = i_new(struct fts_scores, 1);
+ fctx->scores->refcount = 1;
+ i_array_init(&fctx->scores->score_map, 64);
+ MODULE_CONTEXT_SET(ctx, fts_storage_module, fctx);
+
+ /* FIXME: we'll assume that all the args are fuzzy. not good,
+ but would require much more work to fix it. */
+ if (!fts_args_have_fuzzy(args->args) &&
+ mail_user_plugin_getenv_bool(t->box->storage->user,
+ "fts_no_autofuzzy"))
+ fctx->flags |= FTS_LOOKUP_FLAG_NO_AUTO_FUZZY;
+ /* transaction contains the last search's scores. they can be
+ queried later with mail_get_special() */
+ if (ft->scores != NULL)
+ fts_scores_unref(&ft->scores);
+ ft->scores = fctx->scores;
+ ft->scores->refcount++;
+
+ if (fctx->enforced == FTS_ENFORCED_YES ||
+ fts_want_build_args(args->args))
+ fts_try_build_init(ctx, fctx);
+ else
+ fts_search_lookup(fctx);
+ return ctx;
+}
+
+static bool fts_mailbox_build_continue(struct mail_search_context *ctx)
+{
+ struct fts_search_context *fctx = FTS_CONTEXT_REQUIRE(ctx);
+ int ret;
+
+ ret = fts_indexer_more(fctx->indexer_ctx);
+ if (ret == 0)
+ return FALSE;
+
+ /* indexing finished */
+ ctx->progress_hidden = FALSE;
+ if (fts_indexer_deinit(&fctx->indexer_ctx) < 0)
+ ret = -1;
+ if (ret > 0)
+ fts_search_lookup(fctx);
+ if (ret < 0) {
+ /* if indexing timed out, it probably means that
+ the mailbox is still being indexed, but it's a large
+ mailbox and it takes a while. in this situation
+ we'll simply abort the search.
+
+ if indexing failed for any other reason, just
+ fallback to searching the slow way. */
+ fctx->indexing_timed_out =
+ mailbox_get_last_mail_error(fctx->box) == MAIL_ERROR_INUSE;
+ }
+ return TRUE;
+}
+
+static bool
+fts_mailbox_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct fts_search_context *fctx = FTS_CONTEXT(ctx);
+
+ if (fctx != NULL && fctx->indexer_ctx != NULL) {
+ /* this command is still building the indexes */
+ if (!fts_mailbox_build_continue(ctx)) {
+ *tryagain_r = TRUE;
+ return FALSE;
+ }
+ if (fctx->indexing_timed_out) {
+ *tryagain_r = FALSE;
+ return FALSE;
+ }
+ }
+ if (fctx != NULL && !fctx->fts_lookup_success &&
+ fctx->enforced != FTS_ENFORCED_NO)
+ return FALSE;
+
+ return fbox->module_ctx.super.
+ search_next_nonblock(ctx, mail_r, tryagain_r);
+}
+
+static void
+fts_search_apply_results_level(struct mail_search_context *ctx,
+ struct mail_search_arg *args, unsigned int *idx)
+{
+ struct fts_search_context *fctx = FTS_CONTEXT_REQUIRE(ctx);
+ const struct fts_search_level *level;
+
+ level = array_idx(&fctx->levels, *idx);
+
+ if (array_is_created(&level->definite_seqs) &&
+ seq_range_exists(&level->definite_seqs, ctx->seq))
+ fts_search_deserialize_add_matches(args, level->args_matches);
+ else if (!array_is_created(&level->maybe_seqs) ||
+ !seq_range_exists(&level->maybe_seqs, ctx->seq))
+ fts_search_deserialize_add_nonmatches(args, level->args_matches);
+
+ for (; args != NULL; args = args->next) {
+ if (args->type != SEARCH_OR && args->type != SEARCH_SUB)
+ continue;
+
+ *idx += 1;
+ fts_search_apply_results_level(ctx, args->value.subargs, idx);
+ }
+}
+
+static bool fts_mailbox_search_next_update_seq(struct mail_search_context *ctx)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct fts_search_context *fctx = FTS_CONTEXT(ctx);
+ unsigned int idx;
+
+ if (fctx == NULL || !fctx->fts_lookup_success) {
+ /* fts lookup not done for this search */
+ if (fctx != NULL && fctx->indexing_timed_out)
+ return FALSE;
+ return fbox->module_ctx.super.search_next_update_seq(ctx);
+ }
+
+ /* restore original [non]matches */
+ fts_search_deserialize(ctx->args->args, fctx->orig_matches);
+
+ if (!fbox->module_ctx.super.search_next_update_seq(ctx))
+ return FALSE;
+
+ if (ctx->seq >= fctx->first_unindexed_seq) {
+ /* we've not indexed this far */
+ return TRUE;
+ }
+
+ /* apply [non]matches based on the FTS lookup results */
+ idx = 0;
+ fts_search_apply_results_level(ctx, ctx->args->args, &idx);
+ return TRUE;
+}
+
+static int fts_mailbox_search_deinit(struct mail_search_context *ctx)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(ctx->transaction);
+ struct fts_search_context *fctx = FTS_CONTEXT(ctx);
+ int ret = 0;
+
+ if (fctx != NULL) {
+ if (fctx->virtual_mailbox)
+ hash_table_destroy(&fctx->last_indexed_virtual_uids);
+ if (fctx->indexer_ctx != NULL) {
+ if (fts_indexer_deinit(&fctx->indexer_ctx) < 0)
+ ft->failure_reason = "FTS indexing failed";
+ }
+ if (fctx->indexing_timed_out)
+ ret = -1;
+ else if (!fctx->fts_lookup_success &&
+ fctx->enforced != FTS_ENFORCED_NO) {
+ /* FTS lookup failed and we didn't want to fallback to
+ opening all the mails and searching manually */
+ mail_storage_set_internal_error(ctx->transaction->box->storage);
+ ret = -1;
+ }
+
+ buffer_free(&fctx->orig_matches);
+ array_free(&fctx->levels);
+ pool_unref(&fctx->result_pool);
+ fts_scores_unref(&fctx->scores);
+ i_free(fctx);
+ }
+ if (fbox->module_ctx.super.search_deinit(ctx) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int fts_score_cmp(const uint32_t *uid, const struct fts_score_map *score)
+{
+ return *uid < score->uid ? -1 :
+ (*uid > score->uid ? 1 : 0);
+}
+
+static int fts_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct fts_mail *fmail = FTS_MAIL_CONTEXT(mail);
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(_mail->transaction);
+ const struct fts_score_map *scores;
+
+ if (field != MAIL_FETCH_SEARCH_RELEVANCY || ft->scores == NULL)
+ scores = NULL;
+ else {
+ scores = array_bsearch(&ft->scores->score_map, &_mail->uid,
+ fts_score_cmp);
+ }
+ if (scores != NULL) {
+ i_assert(scores->uid == _mail->uid);
+ (void)i_snprintf(fmail->score, sizeof(fmail->score),
+ "%f", scores->score);
+
+ *value_r = fmail->score;
+ return 0;
+ }
+
+ return fmail->module_ctx.super.get_special(_mail, field, value_r);
+}
+
+static int
+fts_mail_precache_range(struct mailbox_transaction_context *trans,
+ struct fts_backend_update_context *update_ctx,
+ uint32_t seq1, uint32_t seq2, unsigned int *extra_count)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ int ret = 0;
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq1, seq2);
+ ctx = mailbox_search_init(trans, search_args, NULL,
+ MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail)) {
+ if (fts_build_mail(update_ctx, mail) < 0) {
+ ret = -1;
+ break;
+ }
+ if (mail_precache(mail) < 0) {
+ ret = -1;
+ break;
+ }
+ *extra_count += 1;
+ }
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int fts_mail_precache_init(struct mail *_mail)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(_mail->transaction);
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(_mail->box->list);
+ uint32_t last_seq;
+
+ if (fts_mailbox_get_last_cached_seq(_mail->box, &last_seq) < 0) {
+ ft->failure_reason = "Failed to lookup last indexed FTS mail";
+ return -1;
+ }
+
+ ft->precached = TRUE;
+ ft->next_index_seq = last_seq + 1;
+ if (flist->update_ctx == NULL)
+ flist->update_ctx = fts_backend_update_init(flist->backend);
+ flist->update_ctx_refcount++;
+ return 0;
+}
+
+static int fts_mail_index(struct mail *_mail)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(_mail->transaction);
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(_mail->box->list);
+ struct mail_private *pmail = (struct mail_private *)_mail;
+
+ if (ft->failure_reason != NULL)
+ return -1;
+
+ if (!ft->precached) {
+ if (fts_mail_precache_init(_mail) < 0)
+ return -1;
+ }
+ if (pmail->vmail != NULL) {
+ /* Indexing via virtual mailbox: Index all the mails in this
+ same real mailbox. */
+ uint32_t msgs_count =
+ mail_index_view_get_messages_count(_mail->box->view);
+
+ fts_backend_update_set_mailbox(flist->update_ctx, _mail->box);
+ if (ft->next_index_seq > msgs_count) {
+ /* everything indexed already */
+ return 0;
+ } else if (fts_mail_precache_range(_mail->transaction,
+ flist->update_ctx,
+ ft->next_index_seq,
+ msgs_count,
+ &ft->precache_extra_count) < 0) {
+ return -1;
+ } else {
+ ft->next_index_seq = msgs_count+1;
+ return 0;
+ }
+ }
+
+ if (ft->next_index_seq < _mail->seq) {
+ /* we'll first need to index all the missing mails up to the
+ current one. */
+ fts_backend_update_set_mailbox(flist->update_ctx, _mail->box);
+ if (fts_mail_precache_range(_mail->transaction,
+ flist->update_ctx,
+ ft->next_index_seq,
+ _mail->seq-1,
+ &ft->precache_extra_count) < 0)
+ return -1;
+ ft->next_index_seq = _mail->seq;
+ }
+
+ if (ft->next_index_seq == _mail->seq) {
+ fts_backend_update_set_mailbox(flist->update_ctx, _mail->box);
+ if (fts_build_mail(flist->update_ctx, _mail) < 0)
+ return -1;
+ ft->next_index_seq = _mail->seq + 1;
+ }
+ return 0;
+}
+
+static int fts_mail_precache(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct fts_mail *fmail = FTS_MAIL_CONTEXT(mail);
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(_mail->transaction);
+ int ret = 0;
+
+ fmail->module_ctx.super.precache(_mail);
+ if (fmail->virtual_mail) {
+ if (ft->highest_virtual_uid < _mail->uid)
+ ft->highest_virtual_uid = _mail->uid;
+ } else if (!ft->indexing) T_BEGIN {
+ /* avoid recursing here from fts_mail_precache_range() */
+ struct event_reason *reason =
+ event_reason_begin("fts:index");
+ ft->indexing = TRUE;
+ ret = fts_mail_index(_mail);
+ i_assert(ft->indexing);
+ ft->indexing = FALSE;
+ event_reason_end(&reason);
+ } T_END;
+ return ret;
+}
+
+void fts_mail_allocated(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ struct fts_mailbox *fbox = FTS_CONTEXT(_mail->box);
+ struct fts_mail *fmail;
+
+ if (fbox == NULL)
+ return;
+
+ fmail = p_new(mail->pool, struct fts_mail, 1);
+ fmail->module_ctx.super = *v;
+ mail->vlast = &fmail->module_ctx.super;
+ fmail->virtual_mail = _mail->box->virtual_vfuncs != NULL;
+
+ v->get_special = fts_mail_get_special;
+ v->precache = fts_mail_precache;
+ MODULE_CONTEXT_SET(mail, fts_mail_module, fmail);
+}
+
+static struct mailbox_transaction_context *
+fts_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(box);
+ struct mailbox_transaction_context *t;
+ struct fts_transaction_context *ft;
+
+ ft = i_new(struct fts_transaction_context, 1);
+
+ t = fbox->module_ctx.super.transaction_begin(box, flags, reason);
+ MODULE_CONTEXT_SET(t, fts_storage_module, ft);
+ return t;
+}
+
+static int fts_transaction_end(struct mailbox_transaction_context *t, const char **error_r)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(t);
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(t->box->list);
+ int ret = 0;
+
+ if (ft->failure_reason != NULL) {
+ *error_r = t_strdup(ft->failure_reason);
+ ret = -1;
+ }
+
+ struct event_reason *reason = event_reason_begin("fts:index");
+ if (ft->precached) {
+ i_assert(flist->update_ctx_refcount > 0);
+ if (--flist->update_ctx_refcount == 0) {
+ if (fts_backend_update_deinit(&flist->update_ctx) < 0) {
+ ret = -1;
+ *error_r = "backend deinit";
+ }
+ }
+ } else if (ft->highest_virtual_uid > 0) {
+ if (fts_index_set_last_uid(t->box, ft->highest_virtual_uid) < 0) {
+ ret = -1;
+ *error_r = "index last uid setting";
+ }
+ }
+ if (ft->scores != NULL)
+ fts_scores_unref(&ft->scores);
+ if (ft->precache_extra_count > 0) {
+ if (ret < 0) {
+ i_error("fts: Failed after indexing %u extra mails internally in %s: %s",
+ ft->precache_extra_count, t->box->vname, *error_r);
+ } else {
+ i_info("fts: Indexed %u extra mails internally in %s",
+ ft->precache_extra_count, t->box->vname);
+ }
+ }
+ event_reason_end(&reason);
+ i_free(ft);
+ return ret;
+}
+
+static void fts_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(t->box);
+ const char *error;
+
+ (void)fts_transaction_end(t, &error);
+ fbox->module_ctx.super.transaction_rollback(t);
+}
+
+static void fts_queue_index(struct mailbox *box)
+{
+ struct mail_user *user = box->storage->user;
+ string_t *str = t_str_new(256);
+ const char *path, *value;
+ unsigned int max_recent_msgs;
+ int fd;
+
+ path = t_strconcat(user->set->base_dir, "/"INDEXER_SOCKET_NAME, NULL);
+ fd = net_connect_unix(path);
+ if (fd == -1) {
+ i_error("net_connect_unix(%s) failed: %m", path);
+ return;
+ }
+
+ value = mail_user_plugin_getenv(user, "fts_autoindex_max_recent_msgs");
+ if (value == NULL || str_to_uint(value, &max_recent_msgs) < 0)
+ max_recent_msgs = 0;
+
+ str_append(str, INDEXER_HANDSHAKE);
+ str_append(str, "APPEND\t0\t");
+ str_append_tabescaped(str, user->username);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, box->vname);
+ str_printfa(str, "\t%u", max_recent_msgs);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, box->storage->user->session_id);
+ str_append_c(str, '\n');
+ if (write_full(fd, str_data(str), str_len(str)) < 0)
+ i_error("write(%s) failed: %m", path);
+ i_close_fd(&fd);
+}
+
+static int
+fts_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(t);
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(t->box);
+ struct mailbox *box = t->box;
+ bool autoindex;
+ int ret = 0;
+ const char *error;
+
+ autoindex = ft->mails_saved && !fbox->fts_mailbox_excluded &&
+ mail_user_plugin_getenv_bool(box->storage->user,
+ "fts_autoindex");
+
+ if (fts_transaction_end(t, &error) < 0) {
+ mail_storage_set_error(t->box->storage, MAIL_ERROR_TEMP,
+ t_strdup_printf("FTS transaction commit failed: %s",
+ error));
+ ret = -1;
+ }
+ if (fbox->module_ctx.super.transaction_commit(t, changes_r) < 0)
+ ret = -1;
+ if (ret < 0)
+ return -1;
+
+ if (autoindex)
+ fts_queue_index(box);
+ return 0;
+}
+
+static void fts_mailbox_sync_notify(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(box->list);
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(box);
+
+ if (fbox->module_ctx.super.sync_notify != NULL)
+ fbox->module_ctx.super.sync_notify(box, uid, sync_type);
+
+ if (sync_type != MAILBOX_SYNC_TYPE_EXPUNGE) {
+ if (uid == 0 && fbox->sync_update_ctx != NULL) {
+ /* this sync is finished */
+ (void)fts_backend_update_deinit(&fbox->sync_update_ctx);
+ }
+ return;
+ }
+
+ if (fbox->sync_update_ctx == NULL) {
+ if (fts_backend_is_updating(flist->backend)) {
+ /* FIXME: maildir workaround - we could get here
+ because we're building an index, which doesn't find
+ some mail and starts syncing the mailbox.. */
+ return;
+ }
+ fbox->sync_update_ctx = fts_backend_update_init(flist->backend);
+ fts_backend_update_set_mailbox(fbox->sync_update_ctx, box);
+ }
+ fts_backend_update_expunge(fbox->sync_update_ctx, uid);
+}
+
+static int fts_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct mailbox *box = ctx->box;
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(box);
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT(box->list);
+ bool optimize;
+ int ret = 0;
+
+ optimize = (ctx->flags & (MAILBOX_SYNC_FLAG_FORCE_RESYNC |
+ MAILBOX_SYNC_FLAG_OPTIMIZE)) != 0;
+ if (fbox->module_ctx.super.sync_deinit(ctx, status_r) < 0)
+ return -1;
+ ctx = NULL;
+
+ if (optimize) {
+ i_assert(flist != NULL);
+ if (fts_backend_optimize(flist->backend) < 0) {
+ mailbox_set_critical(box, "FTS optimize failed");
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int fts_save_finish(struct mail_save_context *ctx)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(ctx->transaction);
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+
+ if (fbox->module_ctx.super.save_finish(ctx) < 0)
+ return -1;
+ ft->mails_saved = TRUE;
+ return 0;
+}
+
+static int fts_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(ctx->transaction);
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+
+ if (fbox->module_ctx.super.copy(ctx, mail) < 0)
+ return -1;
+ ft->mails_saved = TRUE;
+ return 0;
+}
+
+static void fts_mailbox_virtual_match_mail(struct mail_search_context *ctx,
+ struct mail *mail)
+{
+ struct fts_search_context *fctx = FTS_CONTEXT(ctx);
+ unsigned int idx, be_last_uid;
+
+ if (fctx == NULL || !fctx->fts_lookup_success || !fctx->virtual_mailbox ||
+ ctx->seq < fctx->first_unindexed_seq)
+ return;
+ /* Table of last indexed UID per backend mailbox */
+ HASH_TABLE_TYPE(virtual_last_indexed) hash_tbl =
+ fctx->last_indexed_virtual_uids;
+
+ struct mail *backend_mail;
+ if (mail->box->mail_vfuncs->get_backend_mail(mail, &backend_mail) < 0)
+ return;
+ const char *box_name = backend_mail->box->vname;
+ /* Get the last indexed UID in the backend mailbox */
+ void *uid_value =
+ hash_table_lookup(fctx->last_indexed_virtual_uids, box_name);
+ if (uid_value == NULL) {
+ /* This backend's last indexed uid is not yet inserted to the table */
+ struct fts_mailbox_list *flist =
+ FTS_LIST_CONTEXT(backend_mail->box->list);
+ if (flist == NULL || flist->failed ||
+ mailbox_open(backend_mail->box) < 0 ||
+ fts_backend_get_last_uid(flist->backend, backend_mail->box,
+ &be_last_uid) < 0) {
+ be_last_uid = 0;
+ } else {
+ const char *vname_copy =
+ p_strdup(fctx->result_pool, backend_mail->box->vname);
+ hash_table_insert(hash_tbl, vname_copy,
+ POINTER_CAST(be_last_uid + 1));
+ }
+ } else {
+ be_last_uid = POINTER_CAST_TO(uid_value, uint32_t) - 1;
+ }
+ if (backend_mail->uid <= be_last_uid) {
+ /* Mail was already indexed in the backend mailbox.
+ Apply [non]matches based on the FTS lookup results */
+ struct fts_transaction_context *ft = FTS_CONTEXT_REQUIRE(ctx->transaction);
+
+ if (fctx->next_unindexed_seq == mail->seq) {
+ fctx->next_unindexed_seq++;
+ ft->highest_virtual_uid = mail->uid;
+ }
+ idx = 0;
+ fts_search_apply_results_level(ctx, ctx->args->args, &idx);
+ } else {
+ fctx->virtual_seen_unindexed_gaps = TRUE;
+ }
+}
+
+static int fts_mailbox_search_next_match_mail(struct mail_search_context *ctx,
+ struct mail *mail)
+{
+ struct fts_mailbox *fbox = FTS_CONTEXT_REQUIRE(ctx->transaction->box);
+
+ fts_mailbox_virtual_match_mail(ctx, mail);
+ return fbox->module_ctx.super.search_next_match_mail(ctx, mail);
+}
+
+void fts_mailbox_allocated(struct mailbox *box)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT(box->list);
+ struct mailbox_vfuncs *v = box->vlast;
+ struct fts_mailbox *fbox;
+
+ if (flist == NULL || flist->failed)
+ return;
+
+ fbox = p_new(box->pool, struct fts_mailbox, 1);
+ fbox->module_ctx.super = *v;
+ box->vlast = &fbox->module_ctx.super;
+ fbox->fts_mailbox_excluded = fts_user_autoindex_exclude(box);
+
+ v->get_status = fts_mailbox_get_status;
+ v->search_init = fts_mailbox_search_init;
+ v->search_next_nonblock = fts_mailbox_search_next_nonblock;
+ v->search_next_update_seq = fts_mailbox_search_next_update_seq;
+ v->search_deinit = fts_mailbox_search_deinit;
+ v->transaction_begin = fts_transaction_begin;
+ v->transaction_rollback = fts_transaction_rollback;
+ v->transaction_commit = fts_transaction_commit;
+ v->sync_notify = fts_mailbox_sync_notify;
+ v->sync_deinit = fts_sync_deinit;
+ v->save_finish = fts_save_finish;
+ v->copy = fts_copy;
+ v->search_next_match_mail = fts_mailbox_search_next_match_mail;
+
+ MODULE_CONTEXT_SET(box, fts_storage_module, fbox);
+}
+
+static void fts_mailbox_list_deinit(struct mailbox_list *list)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(list);
+
+ if (flist->backend != NULL)
+ fts_backend_deinit(&flist->backend);
+ flist->module_ctx.super.deinit(list);
+}
+
+static int
+fts_init_namespace(struct fts_mailbox_list *flist, struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct fts_backend *backend;
+ if (fts_backend_init(flist->backend_name, ns, error_r, &backend) < 0) {
+ flist->failed = TRUE;
+ return -1;
+ }
+ flist->backend = backend;
+ if ((flist->backend->flags & FTS_BACKEND_FLAG_FUZZY_SEARCH) != 0)
+ ns->user->fuzzy_search = TRUE;
+ return 0;
+}
+
+void fts_mail_namespaces_added(struct mail_namespace *ns)
+{
+ while(ns != NULL) {
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT(ns->list);
+ const char *error;
+
+ if (flist != NULL && !flist->failed && flist->backend == NULL &&
+ fts_init_namespace(flist, ns, &error) < 0) {
+ i_error("fts: Failed to initialize backend '%s': %s",
+ flist->backend_name, error);
+ }
+ ns = ns->next;
+ }
+}
+
+void
+fts_mailbox_list_created(struct mailbox_list *list)
+{
+ const char *name = mail_user_plugin_getenv(list->ns->user, "fts");
+ const char *path;
+
+ if (name == NULL || name[0] == '\0') {
+ e_debug(list->ns->user->event,
+ "fts: No fts setting - plugin disabled");
+ return;
+ }
+
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_INDEX, &path)) {
+ e_debug(list->ns->user->event,
+ "fts: Indexes disabled for namespace '%s'",
+ list->ns->prefix);
+ return;
+ }
+
+ struct fts_mailbox_list *flist;
+ struct mailbox_list_vfuncs *v = list->vlast;
+
+ flist = p_new(list->pool, struct fts_mailbox_list, 1);
+ flist->module_ctx.super = *v;
+ flist->backend_name = name;
+ list->vlast = &flist->module_ctx.super;
+ v->deinit = fts_mailbox_list_deinit;
+ MODULE_CONTEXT_SET(list, fts_mailbox_list_module, flist);
+}
+
+struct fts_backend *fts_mailbox_backend(struct mailbox *box)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT_REQUIRE(box->list);
+
+ return flist->backend;
+}
+
+struct fts_backend *fts_list_backend(struct mailbox_list *list)
+{
+ struct fts_mailbox_list *flist = FTS_LIST_CONTEXT(list);
+
+ return flist == NULL ? NULL : flist->backend;
+}
diff --git a/src/plugins/fts/fts-storage.h b/src/plugins/fts/fts-storage.h
new file mode 100644
index 0000000..ea28ed2
--- /dev/null
+++ b/src/plugins/fts/fts-storage.h
@@ -0,0 +1,70 @@
+#ifndef FTS_STORAGE_H
+#define FTS_STORAGE_H
+
+#include "mail-storage-private.h"
+#include "fts-api.h"
+
+enum fts_enforced {
+ FTS_ENFORCED_NO,
+ FTS_ENFORCED_YES,
+ FTS_ENFORCED_BODY,
+};
+
+struct fts_scores {
+ int refcount;
+ ARRAY_TYPE(fts_score_map) score_map;
+};
+
+struct fts_search_level {
+ ARRAY_TYPE(seq_range) definite_seqs, maybe_seqs;
+ buffer_t *args_matches;
+ ARRAY_TYPE(fts_score_map) score_map;
+};
+
+HASH_TABLE_DEFINE_TYPE(virtual_last_indexed, const char *, void *);
+
+struct fts_search_context {
+ union mail_search_module_context module_ctx;
+
+ struct fts_backend *backend;
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *args;
+ enum fts_lookup_flags flags;
+ enum fts_enforced enforced;
+
+ pool_t result_pool;
+ ARRAY(struct fts_search_level) levels;
+ buffer_t *orig_matches;
+
+ uint32_t first_unindexed_seq;
+ uint32_t next_unindexed_seq;
+ HASH_TABLE_TYPE(virtual_last_indexed) last_indexed_virtual_uids;
+
+ /* final scores, combined from all levels */
+ struct fts_scores *scores;
+
+ struct fts_indexer_context *indexer_ctx;
+ struct fts_search_state *search_state;
+
+ bool virtual_mailbox:1;
+ bool fts_lookup_success:1;
+ bool indexing_timed_out:1;
+ bool virtual_seen_unindexed_gaps:1;
+};
+
+/* Figure out if we want to use full text search indexes and update
+ backends in fctx accordingly. */
+void fts_search_analyze(struct fts_search_context *fctx);
+/* Perform the actual index lookup and update definite_uids and maybe_uids. */
+void fts_search_lookup(struct fts_search_context *fctx);
+/* Returns FTS backend for the given mailbox (assumes it has one). */
+struct fts_backend *fts_mailbox_backend(struct mailbox *box);
+/* Returns FTS backend for the given mailbox list, or NULL if it has none. */
+struct fts_backend *fts_list_backend(struct mailbox_list *list);
+
+void fts_mail_allocated(struct mail *mail);
+void fts_mail_namespaces_added(struct mail_namespace *ns);
+void fts_mailbox_allocated(struct mailbox *box);
+void fts_mailbox_list_created(struct mailbox_list *list);
+#endif
diff --git a/src/plugins/fts/fts-user.c b/src/plugins/fts/fts-user.c
new file mode 100644
index 0000000..d5ce916
--- /dev/null
+++ b/src/plugins/fts/fts-user.c
@@ -0,0 +1,412 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-context.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "mailbox-match-plugin.h"
+#include "fts-language.h"
+#include "fts-filter.h"
+#include "fts-tokenizer.h"
+#include "fts-user.h"
+
+#define FTS_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, fts_user_module)
+
+struct fts_user {
+ union mail_user_module_context module_ctx;
+ int refcount;
+
+ struct fts_language_list *lang_list;
+ struct fts_user_language *data_lang;
+ ARRAY_TYPE(fts_user_language) languages;
+
+ struct mailbox_match_plugin *autoindex_exclude;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(fts_user_module,
+ &mail_user_module_register);
+
+static const char *const *str_keyvalues_to_array(const char *str)
+{
+ const char *key, *value, *const *keyvalues;
+ ARRAY_TYPE(const_string) arr;
+ unsigned int i;
+
+ if (str == NULL)
+ return NULL;
+
+ t_array_init(&arr, 8);
+ keyvalues = t_strsplit_spaces(str, " ");
+ for (i = 0; keyvalues[i] != NULL; i++) {
+ value = strchr(keyvalues[i], '=');
+ if (value != NULL)
+ key = t_strdup_until(keyvalues[i], value++);
+ else {
+ key = keyvalues[i];
+ value = "";
+ }
+ array_push_back(&arr, &key);
+ array_push_back(&arr, &value);
+ }
+ array_append_zero(&arr);
+ return array_front(&arr);
+}
+
+static int
+fts_user_init_languages(struct mail_user *user, struct fts_user *fuser,
+ const char **error_r)
+{
+ const char *languages, *unknown;
+ const char *lang_config[3] = {NULL, NULL, NULL};
+
+ languages = mail_user_plugin_getenv(user, "fts_languages");
+ if (languages == NULL) {
+ *error_r = "fts_languages setting is missing";
+ return -1;
+ }
+
+ lang_config[1] = mail_user_plugin_getenv(user, "fts_language_config");
+ if (lang_config[1] != NULL)
+ lang_config[0] = "fts_language_config";
+ if (fts_language_list_init(lang_config, &fuser->lang_list, error_r) < 0)
+ return -1;
+
+ if (!fts_language_list_add_names(fuser->lang_list, languages, &unknown)) {
+ *error_r = t_strdup_printf(
+ "fts_languages: Unknown language '%s'", unknown);
+ return -1;
+ }
+ if (array_count(fts_language_list_get_all(fuser->lang_list)) == 0) {
+ *error_r = "fts_languages setting is empty";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+fts_user_create_filters(struct mail_user *user, const struct fts_language *lang,
+ struct fts_filter **filter_r, const char **error_r)
+{
+ const struct fts_filter *filter_class;
+ struct fts_filter *filter = NULL, *parent = NULL;
+ const char *filters_key, *const *filters, *filter_set_name;
+ const char *str, *error, *set_key;
+ unsigned int i;
+ int ret = 0;
+
+ /* try to get the language-specific filters first */
+ filters_key = t_strconcat("fts_filters_", lang->name, NULL);
+ str = mail_user_plugin_getenv(user, filters_key);
+ if (str == NULL) {
+ /* fallback to global filters */
+ filters_key = "fts_filters";
+ str = mail_user_plugin_getenv(user, filters_key);
+ if (str == NULL) {
+ /* No filters */
+ *filter_r = NULL;
+ return 0;
+ }
+ }
+
+ filters = t_strsplit_spaces(str, " ");
+ for (i = 0; filters[i] != NULL; i++) {
+ filter_class = fts_filter_find(filters[i]);
+ if (filter_class == NULL) {
+ *error_r = t_strdup_printf("%s: Unknown filter '%s'",
+ filters_key, filters[i]);
+ ret = -1;
+ break;
+ }
+
+ /* try the language-specific setting first */
+ filter_set_name = t_str_replace(filters[i], '-', '_');
+ set_key = t_strdup_printf("fts_filter_%s_%s",
+ lang->name, filter_set_name);
+ str = mail_user_plugin_getenv(user, set_key);
+ if (str == NULL) {
+ set_key = t_strdup_printf("fts_filter_%s", filter_set_name);
+ str = mail_user_plugin_getenv(user, set_key);
+ }
+
+ if (fts_filter_create(filter_class, parent, lang,
+ str_keyvalues_to_array(str),
+ &filter, &error) < 0) {
+ *error_r = t_strdup_printf("%s: %s", set_key, error);
+ ret = -1;
+ break;
+ }
+ if (parent != NULL)
+ fts_filter_unref(&parent);
+ parent = filter;
+ }
+ if (ret < 0) {
+ if (parent != NULL)
+ fts_filter_unref(&parent);
+ return -1;
+ }
+ *filter_r = filter;
+ return 0;
+}
+
+static int
+fts_user_create_tokenizer(struct mail_user *user,
+ const struct fts_language *lang,
+ struct fts_tokenizer **tokenizer_r, bool search,
+ const char **error_r)
+{
+ const struct fts_tokenizer *tokenizer_class;
+ struct fts_tokenizer *tokenizer = NULL, *parent = NULL;
+ const char *tokenizers_key, *const *tokenizers, *tokenizer_set_name;
+ const char *str, *error, *set_key;
+ unsigned int i;
+ int ret = 0;
+
+ tokenizers_key = t_strconcat("fts_tokenizers_", lang->name, NULL);
+ str = mail_user_plugin_getenv(user, tokenizers_key);
+ if (str == NULL) {
+ str = mail_user_plugin_getenv(user, "fts_tokenizers");
+ if (str == NULL) {
+ *error_r = t_strdup_printf("%s or fts_tokenizers setting must exist", tokenizers_key);
+ return -1;
+ }
+ tokenizers_key = "fts_tokenizers";
+ }
+
+ tokenizers = t_strsplit_spaces(str, " ");
+
+ for (i = 0; tokenizers[i] != NULL; i++) {
+ tokenizer_class = fts_tokenizer_find(tokenizers[i]);
+ if (tokenizer_class == NULL) {
+ *error_r = t_strdup_printf("%s: Unknown tokenizer '%s'",
+ tokenizers_key, tokenizers[i]);
+ ret = -1;
+ break;
+ }
+
+ tokenizer_set_name = t_str_replace(tokenizers[i], '-', '_');
+ set_key = t_strdup_printf("fts_tokenizer_%s_%s", tokenizer_set_name, lang->name);
+ str = mail_user_plugin_getenv(user, set_key);
+ if (str == NULL) {
+ set_key = t_strdup_printf("fts_tokenizer_%s", tokenizer_set_name);
+ str = mail_user_plugin_getenv(user, set_key);
+ }
+
+ /* tell the tokenizers that we're tokenizing a search string
+ (instead of tokenizing indexed data) */
+ if (search)
+ str = t_strconcat("search=yes ", str, NULL);
+
+ if (fts_tokenizer_create(tokenizer_class, parent,
+ str_keyvalues_to_array(str),
+ &tokenizer, &error) < 0) {
+ *error_r = t_strdup_printf("%s: %s", set_key, error);
+ ret = -1;
+ break;
+ }
+ if (parent != NULL)
+ fts_tokenizer_unref(&parent);
+ parent = tokenizer;
+ }
+ if (ret < 0) {
+ if (parent != NULL)
+ fts_tokenizer_unref(&parent);
+ return -1;
+ }
+ *tokenizer_r = tokenizer;
+ return 0;
+}
+
+static int
+fts_user_language_init_tokenizers(struct mail_user *user,
+ struct fts_user_language *user_lang,
+ const char **error_r)
+{
+ if (fts_user_create_tokenizer(user, user_lang->lang,
+ &user_lang->index_tokenizer, FALSE,
+ error_r) < 0)
+ return -1;
+
+ if (fts_user_create_tokenizer(user, user_lang->lang,
+ &user_lang->search_tokenizer, TRUE,
+ error_r) < 0)
+ return -1;
+ return 0;
+}
+
+struct fts_user_language *
+fts_user_language_find(struct mail_user *user,
+ const struct fts_language *lang)
+{
+ struct fts_user_language *user_lang;
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ i_assert(fuser != NULL);
+ array_foreach_elem(&fuser->languages, user_lang) {
+ if (strcmp(user_lang->lang->name, lang->name) == 0)
+ return user_lang;
+ }
+ return NULL;
+}
+
+static int fts_user_language_create(struct mail_user *user,
+ struct fts_user *fuser,
+ const struct fts_language *lang,
+ const char **error_r)
+{
+ struct fts_user_language *user_lang;
+
+ user_lang = p_new(user->pool, struct fts_user_language, 1);
+ user_lang->lang = lang;
+ array_push_back(&fuser->languages, &user_lang);
+
+ if (fts_user_language_init_tokenizers(user, user_lang, error_r) < 0)
+ return -1;
+ if (fts_user_create_filters(user, lang, &user_lang->filter, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+static int fts_user_languages_fill_all(struct mail_user *user,
+ struct fts_user *fuser,
+ const char **error_r)
+{
+ const struct fts_language *lang;
+
+ array_foreach_elem(fts_language_list_get_all(fuser->lang_list), lang) {
+ if (fts_user_language_create(user, fuser, lang, error_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+fts_user_init_data_language(struct mail_user *user, struct fts_user *fuser,
+ const char **error_r)
+{
+ struct fts_user_language *user_lang;
+ const char *error;
+
+ user_lang = p_new(user->pool, struct fts_user_language, 1);
+ user_lang->lang = &fts_language_data;
+
+ if (fts_user_language_init_tokenizers(user, user_lang, error_r) < 0)
+ return -1;
+
+ if (fts_filter_create(fts_filter_lowercase, NULL, user_lang->lang, NULL,
+ &user_lang->filter, &error) < 0)
+ i_unreached();
+ i_assert(user_lang->filter != NULL);
+
+ array_push_back(&fuser->languages, &user_lang);
+
+ fuser->data_lang = user_lang;
+ return 0;
+}
+
+struct fts_language_list *fts_user_get_language_list(struct mail_user *user)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ i_assert(fuser != NULL);
+ return fuser->lang_list;
+}
+
+const ARRAY_TYPE(fts_user_language) *
+fts_user_get_all_languages(struct mail_user *user)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ i_assert(fuser != NULL);
+ return &fuser->languages;
+}
+
+struct fts_user_language *fts_user_get_data_lang(struct mail_user *user)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ i_assert(fuser != NULL);
+ return fuser->data_lang;
+}
+
+bool fts_user_autoindex_exclude(struct mailbox *box)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(box->storage->user);
+
+ return mailbox_match_plugin_exclude(fuser->autoindex_exclude, box);
+}
+
+static void fts_user_language_free(struct fts_user_language *user_lang)
+{
+ if (user_lang->filter != NULL)
+ fts_filter_unref(&user_lang->filter);
+ if (user_lang->index_tokenizer != NULL)
+ fts_tokenizer_unref(&user_lang->index_tokenizer);
+ if (user_lang->search_tokenizer != NULL)
+ fts_tokenizer_unref(&user_lang->search_tokenizer);
+}
+
+static void fts_user_free(struct fts_user *fuser)
+{
+ struct fts_user_language *user_lang;
+
+ if (fuser->lang_list != NULL)
+ fts_language_list_deinit(&fuser->lang_list);
+
+ if (array_is_created(&fuser->languages)) {
+ array_foreach_elem(&fuser->languages, user_lang)
+ fts_user_language_free(user_lang);
+ }
+ mailbox_match_plugin_deinit(&fuser->autoindex_exclude);
+}
+
+static int
+fts_mail_user_init_libfts(struct mail_user *user, struct fts_user *fuser,
+ const char **error_r)
+{
+ p_array_init(&fuser->languages, user->pool, 4);
+
+ if (fts_user_init_languages(user, fuser, error_r) < 0 ||
+ fts_user_init_data_language(user, fuser, error_r) < 0)
+ return -1;
+ if (fts_user_languages_fill_all(user, fuser, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+int fts_mail_user_init(struct mail_user *user, bool initialize_libfts,
+ const char **error_r)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ if (fuser != NULL) {
+ /* multiple fts plugins are loaded */
+ fuser->refcount++;
+ return 0;
+ }
+
+ fuser = p_new(user->pool, struct fts_user, 1);
+ fuser->refcount = 1;
+ if (initialize_libfts) {
+ if (fts_mail_user_init_libfts(user, fuser, error_r) < 0) {
+ fts_user_free(fuser);
+ return -1;
+ }
+ }
+ fuser->autoindex_exclude =
+ mailbox_match_plugin_init(user, "fts_autoindex_exclude");
+
+ MODULE_CONTEXT_SET(user, fts_user_module, fuser);
+ return 0;
+}
+
+void fts_mail_user_deinit(struct mail_user *user)
+{
+ struct fts_user *fuser = FTS_USER_CONTEXT(user);
+
+ if (fuser != NULL) {
+ i_assert(fuser->refcount > 0);
+ if (--fuser->refcount == 0)
+ fts_user_free(fuser);
+ }
+}
diff --git a/src/plugins/fts/fts-user.h b/src/plugins/fts/fts-user.h
new file mode 100644
index 0000000..82084bb
--- /dev/null
+++ b/src/plugins/fts/fts-user.h
@@ -0,0 +1,25 @@
+#ifndef FTS_USER_H
+#define FTS_USER_H
+
+struct fts_user_language {
+ const struct fts_language *lang;
+ struct fts_filter *filter;
+ struct fts_tokenizer *index_tokenizer, *search_tokenizer;
+};
+ARRAY_DEFINE_TYPE(fts_user_language, struct fts_user_language *);
+
+struct fts_user_language *
+fts_user_language_find(struct mail_user *user,
+ const struct fts_language *lang);
+struct fts_language_list *fts_user_get_language_list(struct mail_user *user);
+const ARRAY_TYPE(fts_user_language) *
+fts_user_get_all_languages(struct mail_user *user);
+struct fts_user_language *fts_user_get_data_lang(struct mail_user *user);
+
+bool fts_user_autoindex_exclude(struct mailbox *box);
+
+int fts_mail_user_init(struct mail_user *user, bool initialize_libfts,
+ const char **error_r);
+void fts_mail_user_deinit(struct mail_user *user);
+
+#endif
diff --git a/src/plugins/fts/xml2text.c b/src/plugins/fts/xml2text.c
new file mode 100644
index 0000000..f3c573c
--- /dev/null
+++ b/src/plugins/fts/xml2text.c
@@ -0,0 +1,44 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-parser.h"
+#include "fts-parser.h"
+
+#include <unistd.h>
+
+int main(void)
+{
+ struct fts_parser *parser;
+ unsigned char buf[IO_BLOCK_SIZE];
+ struct message_block block;
+ ssize_t ret;
+ struct fts_parser_context parser_context = {.content_type = "text/html"};
+
+ lib_init();
+
+ parser = fts_parser_html.try_init(&parser_context);
+ i_assert(parser != NULL);
+
+ i_zero(&block);
+ while ((ret = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
+ block.data = buf;
+ block.size = ret;
+ parser->v.more(parser, &block);
+ if (write(STDOUT_FILENO, block.data, block.size) < 0)
+ i_fatal("write(stdout) failed: %m");
+ }
+ if (ret < 0)
+ i_fatal("read(stdin) failed: %m");
+
+ for (;;) {
+ block.size = 0;
+ parser->v.more(parser, &block);
+ if (block.size == 0)
+ break;
+ if (write(STDOUT_FILENO, block.data, block.size) < 0)
+ i_fatal("write(stdout) failed: %m");
+ }
+
+ lib_deinit();
+ return 0;
+}
diff --git a/src/plugins/imap-acl/Makefile.am b/src/plugins/imap-acl/Makefile.am
new file mode 100644
index 0000000..c683974
--- /dev/null
+++ b/src/plugins/imap-acl/Makefile.am
@@ -0,0 +1,31 @@
+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 \
+ -I$(top_srcdir)/src/lib-storage/index/imapc \
+ -I$(top_srcdir)/src/imap \
+ -I$(top_srcdir)/src/plugins/acl \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+imap_moduledir = $(moduledir)
+
+NOPLUGIN_LDFLAGS =
+lib02_imap_acl_plugin_la_LDFLAGS = -module -avoid-version
+
+imap_module_LTLIBRARIES = \
+ lib02_imap_acl_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib02_imap_acl_plugin_la_LIBADD = \
+ ../acl/lib01_acl_plugin.la
+endif
+
+lib02_imap_acl_plugin_la_SOURCES = \
+ imap-acl-plugin.c
+
+noinst_HEADERS = \
+ imap-acl-plugin.h
diff --git a/src/plugins/imap-acl/Makefile.in b/src/plugins/imap-acl/Makefile.in
new file mode 100644
index 0000000..649d092
--- /dev/null
+++ b/src/plugins/imap-acl/Makefile.in
@@ -0,0 +1,831 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/imap-acl
+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__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)$(imap_moduledir)"
+LTLIBRARIES = $(imap_module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib02_imap_acl_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../acl/lib01_acl_plugin.la
+am_lib02_imap_acl_plugin_la_OBJECTS = imap-acl-plugin.lo
+lib02_imap_acl_plugin_la_OBJECTS = \
+ $(am_lib02_imap_acl_plugin_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 =
+lib02_imap_acl_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib02_imap_acl_plugin_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)/imap-acl-plugin.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 = $(lib02_imap_acl_plugin_la_SOURCES)
+DIST_SOURCES = $(lib02_imap_acl_plugin_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 =
+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-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/imapc \
+ -I$(top_srcdir)/src/imap \
+ -I$(top_srcdir)/src/plugins/acl \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+imap_moduledir = $(moduledir)
+lib02_imap_acl_plugin_la_LDFLAGS = -module -avoid-version
+imap_module_LTLIBRARIES = \
+ lib02_imap_acl_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib02_imap_acl_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../acl/lib01_acl_plugin.la
+
+lib02_imap_acl_plugin_la_SOURCES = \
+ imap-acl-plugin.c
+
+noinst_HEADERS = \
+ imap-acl-plugin.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/plugins/imap-acl/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/imap-acl/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-imap_moduleLTLIBRARIES: $(imap_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_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)$(imap_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(imap_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(imap_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(imap_moduledir)"; \
+ }
+
+uninstall-imap_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(imap_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(imap_moduledir)/$$f"; \
+ done
+
+clean-imap_moduleLTLIBRARIES:
+ -test -z "$(imap_module_LTLIBRARIES)" || rm -f $(imap_module_LTLIBRARIES)
+ @list='$(imap_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}; \
+ }
+
+lib02_imap_acl_plugin.la: $(lib02_imap_acl_plugin_la_OBJECTS) $(lib02_imap_acl_plugin_la_DEPENDENCIES) $(EXTRA_lib02_imap_acl_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib02_imap_acl_plugin_la_LINK) -rpath $(imap_moduledir) $(lib02_imap_acl_plugin_la_OBJECTS) $(lib02_imap_acl_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-acl-plugin.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:
+ for dir in "$(DESTDIR)$(imap_moduledir)"; 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-imap_moduleLTLIBRARIES clean-libtool \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-acl-plugin.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-imap_moduleLTLIBRARIES
+
+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-acl-plugin.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-imap_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-imap_moduleLTLIBRARIES clean-libtool \
+ 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-imap_moduleLTLIBRARIES 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 uninstall-imap_moduleLTLIBRARIES
+
+.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/plugins/imap-acl/imap-acl-plugin.c b/src/plugins/imap-acl/imap-acl-plugin.c
new file mode 100644
index 0000000..5d1c3be
--- /dev/null
+++ b/src/plugins/imap-acl/imap-acl-plugin.c
@@ -0,0 +1,1128 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-quote.h"
+#include "imap-resp-code.h"
+#include "imap-commands.h"
+#include "imapc-client.h"
+#include "imapc-client-private.h"
+#include "imapc-settings.h"
+#include "imapc-storage.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "module-context.h"
+#include "acl-api.h"
+#include "acl-storage.h"
+#include "acl-plugin.h"
+#include "imap-acl-plugin.h"
+
+
+#define ERROR_NOT_ADMIN "["IMAP_RESP_CODE_NOPERM"] " \
+ "You lack administrator privileges on this mailbox."
+
+#define IMAP_ACL_ANYONE "anyone"
+#define IMAP_ACL_AUTHENTICATED "authenticated"
+#define IMAP_ACL_OWNER "owner"
+#define IMAP_ACL_GROUP_PREFIX "$"
+#define IMAP_ACL_GROUP_OVERRIDE_PREFIX "!$"
+#define IMAP_ACL_GLOBAL_PREFIX "#"
+
+#define IMAP_ACL_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, imap_acl_storage_module)
+#define IMAP_ACL_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, imap_acl_storage_module)
+
+struct imap_acl_letter_map {
+ char letter;
+ const char *name;
+};
+
+static const struct imap_acl_letter_map imap_acl_letter_map[] = {
+ { 'l', MAIL_ACL_LOOKUP },
+ { 'r', MAIL_ACL_READ },
+ { 'w', MAIL_ACL_WRITE },
+ { 's', MAIL_ACL_WRITE_SEEN },
+ { 't', MAIL_ACL_WRITE_DELETED },
+ { 'i', MAIL_ACL_INSERT },
+ { 'p', MAIL_ACL_POST },
+ { 'e', MAIL_ACL_EXPUNGE },
+ { 'k', MAIL_ACL_CREATE },
+ { 'x', MAIL_ACL_DELETE },
+ { 'a', MAIL_ACL_ADMIN },
+ { '\0', NULL }
+};
+
+struct imap_acl_storage {
+ union mail_storage_module_context module_ctx;
+ struct imapc_acl_context *iacl_ctx;
+};
+
+struct imap_acl_storage_module imap_acl_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+
+const char *imap_acl_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct module *imap_acl_module;
+static imap_client_created_func_t *next_hook_client_created;
+
+enum imap_acl_cmd {
+ IMAP_ACL_CMD_MYRIGHTS = 0,
+ IMAP_ACL_CMD_GETACL,
+ IMAP_ACL_CMD_SETACL,
+ IMAP_ACL_CMD_DELETEACL,
+};
+
+const char *imapc_acl_cmd_names[] = {
+ "MYRIGHTS", "GETACL", "SETACL", "DELETEACL"
+};
+
+struct imapc_acl_context {
+ struct imapc_client *client;
+ enum imap_acl_cmd proxy_cmd;
+ struct mail_storage *storage;
+ struct imapc_mailbox *expected_box;
+ string_t *reply;
+};
+
+static int
+acl_mailbox_open_as_admin(struct client_command_context *cmd,
+ struct mailbox *box, const char *name)
+{
+ enum mailbox_existence existence = MAILBOX_EXISTENCE_NONE;
+ int ret;
+
+ if (ACL_USER_CONTEXT(cmd->client->user) == NULL) {
+ client_send_command_error(cmd, "ACLs disabled.");
+ return 0;
+ }
+
+ if (mailbox_exists(box, TRUE, &existence) == 0 &&
+ existence == MAILBOX_EXISTENCE_SELECT) {
+ ret = acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_ADMIN);
+ if (ret > 0)
+ return ret;
+ }
+
+ /* mailbox doesn't exist / not an administrator. */
+ if (existence != MAILBOX_EXISTENCE_SELECT ||
+ acl_mailbox_right_lookup(box, ACL_STORAGE_RIGHT_LOOKUP) <= 0) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO ["IMAP_RESP_CODE_NONEXISTENT"] "
+ MAIL_ERRSTR_MAILBOX_NOT_FOUND, name));
+ } else {
+ client_send_tagline(cmd, "NO "ERROR_NOT_ADMIN);
+ }
+ return 0;
+}
+
+static const struct imap_acl_letter_map *
+imap_acl_letter_map_find(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; imap_acl_letter_map[i].name != NULL; i++) {
+ if (strcmp(imap_acl_letter_map[i].name, name) == 0)
+ return &imap_acl_letter_map[i];
+ }
+ return NULL;
+}
+
+static void
+imap_acl_write_rights_list(string_t *dest, const char *const *rights)
+{
+ const struct imap_acl_letter_map *map;
+ unsigned int i;
+ size_t orig_len = str_len(dest);
+ bool append_c = FALSE, append_d = FALSE;
+
+ for (i = 0; rights[i] != NULL; i++) {
+ /* write only letters */
+ map = imap_acl_letter_map_find(rights[i]);
+ if (map != NULL) {
+ str_append_c(dest, map->letter);
+ if (map->letter == 'k' || map->letter == 'x')
+ append_c = TRUE;
+ if (map->letter == 't' || map->letter == 'e')
+ append_d = TRUE;
+ }
+ }
+ if (append_c)
+ str_append_c(dest, 'c');
+ if (append_d)
+ str_append_c(dest, 'd');
+ if (orig_len == str_len(dest))
+ str_append(dest, "\"\"");
+}
+
+static void
+imap_acl_write_right(string_t *dest, string_t *tmp,
+ const struct acl_rights *right, bool neg)
+{
+ const char *const *rights = neg ? right->neg_rights : right->rights;
+
+ str_truncate(tmp, 0);
+ if (neg) str_append_c(tmp,'-');
+ if (right->global)
+ str_append(tmp, IMAP_ACL_GLOBAL_PREFIX);
+ switch (right->id_type) {
+ case ACL_ID_ANYONE:
+ str_append(tmp, IMAP_ACL_ANYONE);
+ break;
+ case ACL_ID_AUTHENTICATED:
+ str_append(tmp, IMAP_ACL_AUTHENTICATED);
+ break;
+ case ACL_ID_OWNER:
+ str_append(tmp, IMAP_ACL_OWNER);
+ break;
+ case ACL_ID_USER:
+ str_append(tmp, right->identifier);
+ break;
+ case ACL_ID_GROUP:
+ str_append(tmp, IMAP_ACL_GROUP_PREFIX);
+ str_append(tmp, right->identifier);
+ break;
+ case ACL_ID_GROUP_OVERRIDE:
+ str_append(tmp, IMAP_ACL_GROUP_OVERRIDE_PREFIX);
+ str_append(tmp, right->identifier);
+ break;
+ case ACL_ID_TYPE_COUNT:
+ i_unreached();
+ }
+
+ imap_append_astring(dest, str_c(tmp));
+ str_append_c(dest, ' ');
+ imap_acl_write_rights_list(dest, rights);
+}
+
+static bool
+acl_rights_is_owner(struct acl_backend *backend,
+ const struct acl_rights *rights)
+{
+ switch (rights->id_type) {
+ case ACL_ID_OWNER:
+ return TRUE;
+ case ACL_ID_USER:
+ return acl_backend_user_name_equals(backend,
+ rights->identifier);
+ default:
+ return FALSE;
+ }
+}
+
+static bool have_positive_owner_rights(struct acl_backend *backend,
+ struct acl_object *aclobj)
+{
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ bool ret = FALSE;
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (acl_rights_is_owner(backend, &rights)) {
+ if (rights.rights != NULL) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ (void)acl_object_list_deinit(&iter);
+ return ret;
+}
+
+static int
+imap_acl_write_aclobj(string_t *dest, struct acl_backend *backend,
+ struct acl_object *aclobj, bool convert_owner,
+ bool add_default)
+{
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ string_t *tmp;
+ const char *username;
+ size_t orig_len = str_len(dest);
+ bool seen_owner = FALSE, seen_positive_owner = FALSE;
+ int ret;
+
+ username = acl_backend_get_acl_username(backend);
+ if (username == NULL)
+ convert_owner = FALSE;
+
+ tmp = t_str_new(128);
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (acl_rights_is_owner(backend, &rights)) {
+ if (rights.id_type == ACL_ID_OWNER && convert_owner) {
+ rights.id_type = ACL_ID_USER;
+ rights.identifier = username;
+ }
+ if (seen_owner && convert_owner) {
+ /* oops, we have both owner and user=myself.
+ can't do the conversion, so try again. */
+ str_truncate(dest, orig_len);
+ return imap_acl_write_aclobj(dest, backend,
+ aclobj, FALSE,
+ add_default);
+ }
+ seen_owner = TRUE;
+ if (rights.rights != NULL)
+ seen_positive_owner = TRUE;
+ }
+
+ if (rights.rights != NULL) {
+ str_append_c(dest, ' ');
+ imap_acl_write_right(dest, tmp, &rights, FALSE);
+ }
+ if (rights.neg_rights != NULL) {
+ str_append_c(dest, ' ');
+ imap_acl_write_right(dest, tmp, &rights, TRUE);
+ }
+ }
+ ret = acl_object_list_deinit(&iter);
+
+ if (!seen_positive_owner && username != NULL && add_default) {
+ /* no positive owner rights returned, write default ACLs */
+ i_zero(&rights);
+ if (!convert_owner) {
+ rights.id_type = ACL_ID_OWNER;
+ } else {
+ rights.id_type = ACL_ID_USER;
+ rights.identifier = username;
+ }
+ rights.rights = acl_object_get_default_rights(aclobj);
+ if (rights.rights != NULL) {
+ str_append_c(dest, ' ');
+ imap_acl_write_right(dest, tmp, &rights, FALSE);
+ }
+ }
+ return ret;
+}
+
+static const char *
+imapc_acl_get_mailbox_error(struct imapc_mailbox *mbox)
+{
+ enum mail_error err;
+ const char *error = mailbox_get_last_error(&mbox->box, &err);
+ const char *resp_code;
+ string_t *str = t_str_new(128);
+
+ if (imapc_mail_error_to_resp_text_code(err, &resp_code))
+ str_printfa(str, "[%s] ", resp_code);
+ str_append(str, error);
+
+ return str_c(str);
+}
+
+static void
+imapc_acl_myrights_untagged_cb(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imap_acl_storage *iacl_storage =
+ IMAP_ACL_CONTEXT_REQUIRE(&client->_storage->storage);
+ struct imapc_acl_context *ctx = iacl_storage->iacl_ctx;
+ const char *value;
+
+ if (!imap_arg_get_astring(&reply->args[0], &value) ||
+ ctx->expected_box == NULL)
+ return;
+
+ /* Untagged reply was not meant for this mailbox */
+ if (!imapc_mailbox_name_equals(ctx->expected_box, value))
+ return;
+
+ /* copy rights from reply to string
+ <args[0](mailbox)> <args[1](rights)> */
+ if (imap_arg_get_astring(&reply->args[1], &value)) {
+ str_append(ctx->reply, value);
+ } else {
+ /* Rights could not been parsed mark this
+ failed and clear the prepared reply. */
+ str_truncate(ctx->reply, 0);
+ }
+ /* Just handle one untagged reply. */
+ ctx->expected_box = NULL;
+}
+
+static void
+imapc_acl_getacl_untagged_cb(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imap_acl_storage *iacl_storage =
+ IMAP_ACL_CONTEXT_REQUIRE(&client->_storage->storage);
+ struct imapc_acl_context *ctx = iacl_storage->iacl_ctx;
+ const char *key, *value;
+ unsigned int i;
+
+ if (!imap_arg_get_astring(&reply->args[0], &value) ||
+ ctx->expected_box == NULL)
+ return;
+
+ /* Untagged reply was not meant for this mailbox */
+ if (!imapc_mailbox_name_equals(ctx->expected_box, value))
+ return;
+
+ /* Parse key:value pairs of user:right and append them
+ to the prepared reply. */
+ for (i = 1; reply->args[i].type != IMAP_ARG_EOL; i += 2) {
+ if (imap_arg_get_astring(&reply->args[i], &key) &&
+ imap_arg_get_astring(&reply->args[i+1], &value)) {
+ str_append(iacl_storage->iacl_ctx->reply, key);
+ str_append_c(iacl_storage->iacl_ctx->reply, ' ');
+ str_append(iacl_storage->iacl_ctx->reply, value);
+ str_append_c(iacl_storage->iacl_ctx->reply, ' ');
+ } else {
+ /* Rights could not been parsed clear prepared reply. */
+ str_truncate(ctx->reply, 0);
+ break;
+ }
+ }
+ /* Just handle one untagged reply. */
+ ctx->expected_box = NULL;
+}
+
+static struct imapc_acl_context *
+imap_acl_cmd_context_alloc(struct imapc_mailbox *mbox)
+{
+ struct imapc_acl_context *iacl_ctx =
+ p_new(mbox->box.storage->pool, struct imapc_acl_context, 1);
+ iacl_ctx->reply = str_new(mbox->box.storage->pool, 128);
+ return iacl_ctx;
+}
+
+static void imap_acl_cmd_context_init(struct imapc_acl_context *iacl_ctx,
+ struct imapc_mailbox *mbox,
+ enum imap_acl_cmd proxy_cmd)
+{
+ iacl_ctx->client = mbox->storage->client->client;
+ iacl_ctx->proxy_cmd = proxy_cmd;
+ iacl_ctx->expected_box = mbox;
+ str_truncate(iacl_ctx->reply, 0);
+}
+
+static struct imapc_acl_context *
+imap_acl_cmd_context_register(struct imapc_mailbox *mbox, enum imap_acl_cmd proxy_cmd)
+{
+ struct mailbox *box = &mbox->box;
+ struct imap_acl_storage *iacl_storage = IMAP_ACL_CONTEXT(box->storage);
+
+ if (iacl_storage == NULL) {
+ iacl_storage = p_new(box->storage->pool, struct imap_acl_storage, 1);
+ MODULE_CONTEXT_SET(box->storage, imap_acl_storage_module, iacl_storage);
+ iacl_storage->iacl_ctx = imap_acl_cmd_context_alloc(mbox);
+ }
+
+ imap_acl_cmd_context_init(iacl_storage->iacl_ctx, mbox, proxy_cmd);
+
+ return iacl_storage->iacl_ctx;
+}
+
+static const char *imap_acl_get_mailbox_name(const struct mail_namespace *ns,
+ const char *mailbox)
+{
+ /* Strip namespace prefix from mailbox name or append "INBOX" if
+ mailbox is "" and mailbox is in shared namespace. */
+
+ if (ns->prefix_len == 0)
+ return mailbox;
+
+ i_assert(ns->prefix_len >= 1);
+
+ if ((mailbox[ns->prefix_len-1] == '\0' ||
+ mailbox[ns->prefix_len] == '\0') &&
+ strncmp(mailbox, ns->prefix, ns->prefix_len-1) == 0 &&
+ ns->type == MAIL_NAMESPACE_TYPE_SHARED) {
+ /* Given mailbox name does not contain an actual mailbox name
+ but just the namespace prefix so default to "INBOX". */
+ return "INBOX";
+ }
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ strcasecmp(mailbox, "INBOX") == 0) {
+ /* For user INBOX always use INBOX and ignore namespace
+ prefix. */
+ return "INBOX";
+ }
+
+ i_assert(strncmp(mailbox, ns->prefix, ns->prefix_len-1) == 0);
+ return mailbox+ns->prefix_len;
+}
+
+static const char *
+imapc_acl_prepare_cmd(string_t *reply_r, const char *mailbox,
+ const struct mail_namespace *ns, const char *cmd_args,
+ const enum imap_acl_cmd proxy_cmd)
+{
+ string_t *proxy_cmd_str = t_str_new(128);
+ /* Prepare proxy_cmd and untagged replies */
+ switch (proxy_cmd) {
+ case IMAP_ACL_CMD_MYRIGHTS:
+ /* Prepare client untagged reply. */
+ str_append(reply_r, "* MYRIGHTS ");
+ imap_append_astring(reply_r, mailbox);
+ str_append_c(reply_r, ' ');
+
+ str_append(proxy_cmd_str, "MYRIGHTS ");
+ /* Strip namespace prefix. */
+ imap_append_astring(proxy_cmd_str,
+ imap_acl_get_mailbox_name(ns, mailbox));
+ break;
+ case IMAP_ACL_CMD_GETACL:
+ /* Prepare client untagged reply. */
+ str_append(reply_r, "* ACL ");
+ imap_append_astring(reply_r, mailbox);
+ str_append_c(reply_r, ' ');
+
+ str_append(proxy_cmd_str, "GETACL ");
+ imap_append_astring(proxy_cmd_str,
+ imap_acl_get_mailbox_name(ns, mailbox));
+ break;
+ case IMAP_ACL_CMD_SETACL:
+ /* No contents in untagged replies for SETACL */
+ str_append(proxy_cmd_str, "SETACL ");
+ imap_append_astring(proxy_cmd_str,
+ imap_acl_get_mailbox_name(ns, mailbox));
+
+ str_append_c(proxy_cmd_str, ' ');
+ str_append(proxy_cmd_str, cmd_args);
+ break;
+ case IMAP_ACL_CMD_DELETEACL:
+ /* No contents in untagged replies for DELETEACL */
+ str_append(proxy_cmd_str, "DELETEACL ");
+ imap_append_astring(proxy_cmd_str,
+ imap_acl_get_mailbox_name(ns, mailbox));
+
+ str_append_c(proxy_cmd_str, ' ');
+ str_append(proxy_cmd_str, cmd_args);
+ break;
+ default:
+ i_unreached();
+ }
+ return str_c(proxy_cmd_str);
+}
+
+static struct imapc_command *
+imapc_acl_simple_context_init(struct imapc_simple_context *ctx,
+ struct imapc_mailbox *mbox)
+{
+ imapc_simple_context_init(ctx, mbox->storage->client);
+ return imapc_client_cmd(mbox->storage->client->client,
+ imapc_simple_callback, ctx);
+}
+
+static void imapc_acl_send_client_reply(struct imapc_acl_context *iacl_ctx,
+ struct client_command_context *orig_cmd,
+ const char *success_tagged_reply)
+{
+ if (str_len(iacl_ctx->reply) == 0)
+ client_send_tagline(orig_cmd, "NO "MAIL_ERRSTR_CRITICAL_MSG);
+ else {
+ client_send_line(orig_cmd->client, str_c(iacl_ctx->reply));
+ client_send_tagline(orig_cmd, success_tagged_reply);
+ }
+}
+
+static bool imap_acl_proxy_cmd(struct mailbox *box,
+ const char *mailbox,
+ const char *cmd_args,
+ const struct mail_namespace *ns,
+ struct client_command_context *orig_cmd,
+ const enum imap_acl_cmd proxy_cmd)
+{
+ struct imapc_acl_context *iacl_ctx;
+ struct imapc_simple_context ctx;
+ struct imapc_command *imapc_cmd;
+ const char *proxy_cmd_str;
+
+ if (strcmp(box->storage->name, "imapc") != 0) {
+ /* Storage is not "imapc". */
+ return FALSE;
+ }
+
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ if (!IMAPC_HAS_FEATURE(mbox->storage, IMAPC_FEATURE_ACL)) {
+ /* Storage is "imapc" but no proxying of ACL commands should
+ be done. */
+ return FALSE;
+ }
+
+ iacl_ctx = imap_acl_cmd_context_register(mbox, proxy_cmd);
+
+ /* Register callbacks for untagged replies */
+ imapc_storage_client_register_untagged(mbox->storage->client, "ACL",
+ imapc_acl_getacl_untagged_cb);
+ imapc_storage_client_register_untagged(mbox->storage->client, "MYRIGHTS",
+ imapc_acl_myrights_untagged_cb);
+
+ imapc_cmd = imapc_acl_simple_context_init(&ctx, mbox);
+
+ /* Prepare untagged replies and return proxy_cmd */
+ proxy_cmd_str = imapc_acl_prepare_cmd(iacl_ctx->reply, mailbox,
+ ns, cmd_args, proxy_cmd);
+
+ imapc_command_send(imapc_cmd, proxy_cmd_str);
+ imapc_simple_run(&ctx, &imapc_cmd);
+
+ if (ctx.ret != 0) {
+ /* If the remote replied BAD or NO send NO. */
+ client_send_tagline(orig_cmd,
+ t_strdup_printf("NO %s", imapc_acl_get_mailbox_error(mbox)));
+ } else {
+ /* Command was OK on remote backend, send untagged reply from
+ ctx.str and tagged reply. */
+ switch (iacl_ctx->proxy_cmd) {
+ case IMAP_ACL_CMD_DELETEACL:
+ client_send_tagline(orig_cmd, "OK Deleteacl complete.");
+ break;
+ case IMAP_ACL_CMD_GETACL:
+ imapc_acl_send_client_reply(iacl_ctx,
+ orig_cmd,
+ "OK Getacl complete.");
+ break;
+ case IMAP_ACL_CMD_MYRIGHTS:
+ imapc_acl_send_client_reply(iacl_ctx,
+ orig_cmd,
+ "OK Myrights complete.");
+ break;
+ case IMAP_ACL_CMD_SETACL:
+ client_send_tagline(orig_cmd, "OK Setacl complete.");
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ /* Unregister callbacks for untagged replies */
+ imapc_storage_client_unregister_untagged(mbox->storage->client, "MYRIGHTS");
+ imapc_storage_client_unregister_untagged(mbox->storage->client, "ACL");
+ return TRUE;
+}
+
+static void imap_acl_cmd_getacl(struct mailbox *box, struct mail_namespace *ns,
+ const char *mailbox,
+ struct client_command_context *cmd)
+{
+ struct acl_backend *backend;
+ string_t *str;
+ int ret;
+
+ if (acl_mailbox_open_as_admin(cmd, box, mailbox) <= 0)
+ return;
+
+ backend = acl_mailbox_list_get_backend(ns->list);
+
+ str = t_str_new(128);
+ str_append(str, "* ACL ");
+ imap_append_astring(str, mailbox);
+
+ ret = imap_acl_write_aclobj(str, backend,
+ acl_mailbox_get_aclobj(box), TRUE,
+ ns->type == MAIL_NAMESPACE_TYPE_PRIVATE);
+ if (ret > -1) {
+ client_send_line(cmd->client, str_c(str));
+ client_send_tagline(cmd, "OK Getacl completed.");
+ } else {
+ client_send_tagline(cmd, "NO "MAIL_ERRSTR_CRITICAL_MSG);
+ }
+}
+
+static bool cmd_getacl(struct client_command_context *cmd)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *mailbox, *orig_mailbox;
+
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ /* If the location is remote and imapc_feature acl is enabled, proxy the
+ command to the configured imapc location. */
+ if (!imap_acl_proxy_cmd(box, orig_mailbox, NULL, ns, cmd, IMAP_ACL_CMD_GETACL))
+ imap_acl_cmd_getacl(box, ns, orig_mailbox, cmd);
+ mailbox_free(&box);
+ return TRUE;
+}
+
+static void imap_acl_cmd_myrights(struct mailbox *box, const char *mailbox,
+ struct client_command_context *cmd)
+{
+ const char *const *rights;
+ string_t *str = t_str_new(128);
+
+ if (acl_object_get_my_rights(acl_mailbox_get_aclobj(box),
+ pool_datastack_create(), &rights) < 0) {
+ client_send_tagline(cmd, "NO "MAIL_ERRSTR_CRITICAL_MSG);
+ return;
+ }
+
+ /* Post right alone doesn't give permissions to see if the mailbox
+ exists or not. Only mail deliveries care about that. */
+ if (*rights == NULL ||
+ (strcmp(*rights, MAIL_ACL_POST) == 0 && rights[1] == NULL)) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO ["IMAP_RESP_CODE_NONEXISTENT"] "
+ MAIL_ERRSTR_MAILBOX_NOT_FOUND, mailbox));
+ return;
+ }
+
+ str_append(str, "* MYRIGHTS ");
+ imap_append_astring(str, mailbox);
+ str_append_c(str, ' ');
+ imap_acl_write_rights_list(str, rights);
+
+ client_send_line(cmd->client, str_c(str));
+ client_send_tagline(cmd, "OK Myrights completed.");
+}
+
+static bool cmd_myrights(struct client_command_context *cmd)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *mailbox, *orig_mailbox;
+
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ if (ACL_USER_CONTEXT(cmd->client->user) == NULL) {
+ client_send_command_error(cmd, "ACLs disabled.");
+ return TRUE;
+ }
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+
+ /* If the location is remote and imapc_feature acl is enabled, proxy the
+ command to the configured imapc location. */
+ if (!imap_acl_proxy_cmd(box, orig_mailbox, NULL, ns,
+ cmd, IMAP_ACL_CMD_MYRIGHTS))
+ imap_acl_cmd_myrights(box, orig_mailbox, cmd);
+ mailbox_free(&box);
+ return TRUE;
+}
+
+static bool cmd_listrights(struct client_command_context *cmd)
+{
+ struct mailbox *box;
+ struct mail_namespace *ns;
+ const char *mailbox, *orig_mailbox, *identifier;
+ string_t *str;
+
+ if (!client_read_string_args(cmd, 2, &mailbox, &identifier))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+
+ str = t_str_new(128);
+ str_append(str, "* LISTRIGHTS ");
+ imap_append_astring(str, orig_mailbox);
+ str_append_c(str, ' ');
+ imap_append_astring(str, identifier);
+ str_append_c(str, ' ');
+ str_append(str, "\"\" l r w s t p i e k x a c d");
+
+ client_send_line(cmd->client, str_c(str));
+ client_send_tagline(cmd, "OK Listrights completed.");
+ mailbox_free(&box);
+ return TRUE;
+}
+
+static int
+imap_acl_letters_parse(const char *letters, const char *const **rights_r,
+ const char **client_error_r)
+{
+ static const char *acl_k = MAIL_ACL_CREATE;
+ static const char *acl_x = MAIL_ACL_DELETE;
+ static const char *acl_e = MAIL_ACL_EXPUNGE;
+ static const char *acl_t = MAIL_ACL_WRITE_DELETED;
+ ARRAY_TYPE(const_string) rights;
+ unsigned int i;
+
+ t_array_init(&rights, 64);
+ for (; *letters != '\0'; letters++) {
+ for (i = 0; imap_acl_letter_map[i].name != NULL; i++) {
+ if (imap_acl_letter_map[i].letter == *letters) {
+ array_push_back(&rights,
+ &imap_acl_letter_map[i].name);
+ break;
+ }
+ }
+ if (imap_acl_letter_map[i].name == NULL) {
+ /* Handling of obsolete rights as virtual
+ rights according to RFC 4314 */
+ switch (*letters) {
+ case 'c':
+ array_push_back(&rights, &acl_k);
+ array_push_back(&rights, &acl_x);
+ break;
+ case 'd':
+ array_push_back(&rights, &acl_e);
+ array_push_back(&rights, &acl_t);
+ break;
+ default:
+ *client_error_r = t_strdup_printf(
+ "Invalid ACL right: %c", *letters);
+ return -1;
+ }
+ }
+ }
+ array_append_zero(&rights);
+ *rights_r = array_front(&rights);
+ return 0;
+}
+
+static bool acl_anyone_allow(struct mail_user *user)
+{
+ const char *env;
+
+ env = mail_user_plugin_getenv(user, "acl_anyone");
+ return env != NULL && strcmp(env, "allow") == 0;
+}
+
+static int
+imap_acl_identifier_parse(struct client_command_context *cmd,
+ const char *id, struct acl_rights *rights,
+ bool check_anyone, const char **client_error_r)
+{
+ struct mail_user *user = cmd->client->user;
+
+ if (str_begins(id, IMAP_ACL_GLOBAL_PREFIX)) {
+ *client_error_r = t_strdup_printf("Global ACLs can't be modified: %s",
+ id);
+ return -1;
+ }
+
+ if (strcmp(id, IMAP_ACL_ANYONE) == 0) {
+ if (check_anyone && !acl_anyone_allow(user)) {
+ *client_error_r = "'anyone' identifier is disallowed";
+ return -1;
+ }
+ rights->id_type = ACL_ID_ANYONE;
+ } else if (strcmp(id, IMAP_ACL_AUTHENTICATED) == 0) {
+ if (check_anyone && !acl_anyone_allow(user)) {
+ *client_error_r = "'authenticated' identifier is disallowed";
+ return -1;
+ }
+ rights->id_type = ACL_ID_AUTHENTICATED;
+ } else if (strcmp(id, IMAP_ACL_OWNER) == 0)
+ rights->id_type = ACL_ID_OWNER;
+ else if (str_begins(id, IMAP_ACL_GROUP_PREFIX)) {
+ rights->id_type = ACL_ID_GROUP;
+ rights->identifier = id + strlen(IMAP_ACL_GROUP_PREFIX);
+ } else if (str_begins(id, IMAP_ACL_GROUP_OVERRIDE_PREFIX)) {
+ rights->id_type = ACL_ID_GROUP_OVERRIDE;
+ rights->identifier = id +
+ strlen(IMAP_ACL_GROUP_OVERRIDE_PREFIX);
+ } else {
+ rights->id_type = ACL_ID_USER;
+ rights->identifier = id;
+ }
+ return 0;
+}
+
+static void imap_acl_update_ensure_keep_admins(struct acl_backend *backend,
+ struct acl_object *aclobj,
+ struct acl_rights_update *update)
+{
+ static const char *acl_admin = MAIL_ACL_ADMIN;
+ const char *const *rights = update->rights.rights;
+ const char *const *default_rights;
+ ARRAY_TYPE(const_string) new_rights;
+ unsigned int i;
+
+ t_array_init(&new_rights, 64);
+ for (i = 0; rights[i] != NULL; i++) {
+ if (strcmp(rights[i], MAIL_ACL_ADMIN) == 0)
+ break;
+ array_push_back(&new_rights, &rights[i]);
+ }
+
+ switch (update->modify_mode) {
+ case ACL_MODIFY_MODE_ADD:
+ if (have_positive_owner_rights(backend, aclobj))
+ return;
+
+ /* adding initial rights for a user. we need to add
+ the defaults also. don't worry about duplicates. */
+ for (; rights[i] != NULL; i++)
+ array_push_back(&new_rights, &rights[i]);
+ default_rights = acl_object_get_default_rights(aclobj);
+ for (i = 0; default_rights[i] != NULL; i++)
+ array_push_back(&new_rights, &default_rights[i]);
+ break;
+ case ACL_MODIFY_MODE_REMOVE:
+ if (rights[i] == NULL)
+ return;
+
+ /* skip over the ADMIN removal and add the rest */
+ for (i++; rights[i] != NULL; i++)
+ array_push_back(&new_rights, &rights[i]);
+ break;
+ case ACL_MODIFY_MODE_REPLACE:
+ if (rights[i] != NULL)
+ return;
+
+ /* add the missing ADMIN right */
+ array_push_back(&new_rights, &acl_admin);
+ break;
+ default:
+ return;
+ }
+ array_append_zero(&new_rights);
+ update->rights.rights = array_front(&new_rights);
+}
+
+static int
+cmd_acl_mailbox_update(struct mailbox *box,
+ const struct acl_rights_update *update,
+ const char **client_error_r)
+{
+ struct mailbox_transaction_context *t;
+ int ret;
+
+ if (mailbox_open(box) < 0) {
+ *client_error_r = mailbox_get_last_error(box, NULL);
+ return -1;
+ }
+
+ t = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_EXTERNAL,
+ __func__);
+ ret = acl_mailbox_update_acl(t, update);
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+ *client_error_r = MAIL_ERRSTR_CRITICAL_MSG;
+ return ret;
+}
+
+static void imap_acl_cmd_setacl(struct mailbox *box, struct mail_namespace *ns,
+ const char *mailbox, const char *identifier,
+ const char *rights,
+ struct client_command_context *cmd)
+{
+ struct acl_backend *backend;
+ struct acl_object *aclobj;
+ struct acl_rights_update update;
+ struct acl_rights *r;
+ const char *client_error;
+ bool negative = FALSE;
+
+ i_zero(&update);
+ if (*identifier == '-') {
+ negative = TRUE;
+ identifier++;
+ }
+
+ switch (*rights) {
+ case '-':
+ update.modify_mode = ACL_MODIFY_MODE_REMOVE;
+ rights++;
+ break;
+ case '+':
+ update.modify_mode = ACL_MODIFY_MODE_ADD;
+ rights++;
+ break;
+ default:
+ update.modify_mode = ACL_MODIFY_MODE_REPLACE;
+ break;
+ }
+
+ if (imap_acl_identifier_parse(cmd, identifier, &update.rights,
+ TRUE, &client_error) < 0) {
+ client_send_command_error(cmd, client_error);
+ return;
+ }
+ if (imap_acl_letters_parse(rights, &update.rights.rights, &client_error) < 0) {
+ client_send_command_error(cmd, client_error);
+ return;
+ }
+ r = &update.rights;
+
+ if (acl_mailbox_open_as_admin(cmd, box, mailbox) <= 0)
+ return;
+
+ backend = acl_mailbox_list_get_backend(ns->list);
+ if (ns->type == MAIL_NAMESPACE_TYPE_PUBLIC &&
+ r->id_type == ACL_ID_OWNER) {
+ client_send_tagline(cmd, "NO Public namespaces have no owner");
+ return;
+ }
+
+ aclobj = acl_mailbox_get_aclobj(box);
+ if (negative) {
+ update.neg_modify_mode = update.modify_mode;
+ update.modify_mode = ACL_MODIFY_MODE_REMOVE;
+ update.rights.neg_rights = update.rights.rights;
+ update.rights.rights = NULL;
+ } else if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE &&
+ r->rights != NULL &&
+ ((r->id_type == ACL_ID_USER &&
+ acl_backend_user_name_equals(backend, r->identifier)) ||
+ (r->id_type == ACL_ID_OWNER &&
+ strcmp(acl_backend_get_acl_username(backend),
+ ns->user->username) == 0))) {
+ /* make sure client doesn't (accidentally) remove admin
+ privileges from its own mailboxes */
+ imap_acl_update_ensure_keep_admins(backend, aclobj, &update);
+ }
+
+ if (cmd_acl_mailbox_update(box, &update, &client_error) < 0)
+ client_send_tagline(cmd, t_strdup_printf("NO %s", client_error));
+ else
+ client_send_tagline(cmd, "OK Setacl complete.");
+}
+
+static bool cmd_setacl(struct client_command_context *cmd)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *mailbox, *orig_mailbox, *identifier, *rights;
+ string_t *proxy_cmd_args = t_str_new(64);
+
+ if (!client_read_string_args(cmd, 3, &mailbox, &identifier, &rights))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ if (*identifier == '\0') {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ /* Keep original identifer for proxy_cmd_args */
+ imap_append_astring(proxy_cmd_args, identifier);
+ str_append_c(proxy_cmd_args, ' ');
+ /* Append original rights for proxy_cmd_args */
+ imap_append_astring(proxy_cmd_args, rights);
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ /* If the location is remote and imapc_feature acl is enabled, proxy the
+ command to the configured imapc location. */
+ if (!imap_acl_proxy_cmd(box, orig_mailbox, str_c(proxy_cmd_args),
+ ns, cmd, IMAP_ACL_CMD_SETACL))
+ imap_acl_cmd_setacl(box, ns, orig_mailbox, identifier, rights, cmd);
+ mailbox_free(&box);
+ return TRUE;
+}
+
+static void imap_acl_cmd_deleteacl(struct mailbox *box, const char *mailbox,
+ const char *identifier,
+ struct client_command_context *cmd)
+{
+ struct acl_rights_update update;
+ const char *client_error;
+
+ i_zero(&update);
+ if (*identifier != '-')
+ update.modify_mode = ACL_MODIFY_MODE_CLEAR;
+ else {
+ update.neg_modify_mode = ACL_MODIFY_MODE_CLEAR;
+ identifier++;
+ }
+
+ if (imap_acl_identifier_parse(cmd, identifier, &update.rights,
+ FALSE, &client_error) < 0) {
+ client_send_command_error(cmd, client_error);
+ return;
+ }
+
+ if (acl_mailbox_open_as_admin(cmd, box, mailbox) <= 0)
+ return;
+
+ if (cmd_acl_mailbox_update(box, &update, &client_error) < 0)
+ client_send_tagline(cmd, t_strdup_printf("NO %s", client_error));
+ else
+ client_send_tagline(cmd, "OK Deleteacl complete.");
+}
+
+static bool cmd_deleteacl(struct client_command_context *cmd)
+{
+ struct mailbox *box;
+ struct mail_namespace *ns;
+ const char *mailbox, *orig_mailbox, *identifier;
+ string_t *proxy_cmd_args = t_str_new(64);
+
+ if (!client_read_string_args(cmd, 2, &mailbox, &identifier))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ if (*identifier == '\0') {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ /* Escaped identifer for proxy_cmd_args */
+ imap_append_astring(proxy_cmd_args, identifier);
+
+ box = mailbox_alloc(ns->list, mailbox,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS);
+
+ /* If the location is remote and imapc_feature acl is enabled, proxy the
+ command to the configured imapc location. */
+ if (!imap_acl_proxy_cmd(box, orig_mailbox, str_c(proxy_cmd_args),
+ ns, cmd, IMAP_ACL_CMD_DELETEACL))
+ imap_acl_cmd_deleteacl(box, orig_mailbox, identifier, cmd);
+ mailbox_free(&box);
+ return TRUE;
+}
+
+static void imap_acl_client_created(struct client **client)
+{
+ if (mail_user_is_plugin_loaded((*client)->user, imap_acl_module)) {
+ client_add_capability(*client, "ACL");
+ client_add_capability(*client, "RIGHTS=texk");
+ }
+
+ if (next_hook_client_created != NULL)
+ next_hook_client_created(client);
+}
+
+void imap_acl_plugin_init(struct module *module)
+{
+ command_register("LISTRIGHTS", cmd_listrights, 0);
+ command_register("GETACL", cmd_getacl, 0);
+ command_register("MYRIGHTS", cmd_myrights, 0);
+ command_register("SETACL", cmd_setacl, 0);
+ command_register("DELETEACL", cmd_deleteacl, 0);
+
+ imap_acl_module = module;
+ next_hook_client_created =
+ imap_client_created_hook_set(imap_acl_client_created);
+}
+
+void imap_acl_plugin_deinit(void)
+{
+ command_unregister("GETACL");
+ command_unregister("MYRIGHTS");
+ command_unregister("SETACL");
+ command_unregister("DELETEACL");
+ command_unregister("LISTRIGHTS");
+
+ imap_client_created_hook_set(next_hook_client_created);
+}
+
+const char *imap_acl_plugin_dependencies[] = { "acl", NULL };
+const char imap_acl_plugin_binary_dependency[] = "imap";
diff --git a/src/plugins/imap-acl/imap-acl-plugin.h b/src/plugins/imap-acl/imap-acl-plugin.h
new file mode 100644
index 0000000..117e8fb
--- /dev/null
+++ b/src/plugins/imap-acl/imap-acl-plugin.h
@@ -0,0 +1,12 @@
+#ifndef IMAP_ACL_PLUGIN_H
+#define IMAP_ACL_PLUGIN_H
+
+extern const char *imap_acl_plugin_dependencies[];
+extern const char imap_acl_plugin_binary_dependency[];
+
+extern MODULE_CONTEXT_DEFINE(imap_acl_storage_module, &mail_storage_module_register);
+
+void imap_acl_plugin_init(struct module *module);
+void imap_acl_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/imap-old-stats/Makefile.am b/src/plugins/imap-old-stats/Makefile.am
new file mode 100644
index 0000000..ed07ed5
--- /dev/null
+++ b/src/plugins/imap-old-stats/Makefile.am
@@ -0,0 +1,28 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-old-stats \
+ -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/imap \
+ -I$(top_srcdir)/src/plugins/old-stats
+
+imap_moduledir = $(moduledir)
+
+NOPLUGIN_LDFLAGS =
+lib95_imap_old_stats_plugin_la_LDFLAGS = -module -avoid-version
+
+imap_module_LTLIBRARIES = \
+ lib95_imap_old_stats_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib95_imap_old_stats_plugin_la_LIBADD = \
+ ../old-stats/lib90_old_stats_plugin.la
+endif
+
+lib95_imap_old_stats_plugin_la_SOURCES = \
+ imap-stats-plugin.c
+
+noinst_HEADERS = \
+ imap-stats-plugin.h
diff --git a/src/plugins/imap-old-stats/Makefile.in b/src/plugins/imap-old-stats/Makefile.in
new file mode 100644
index 0000000..6044d7f
--- /dev/null
+++ b/src/plugins/imap-old-stats/Makefile.in
@@ -0,0 +1,827 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/imap-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 $(noinst_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)$(imap_moduledir)"
+LTLIBRARIES = $(imap_module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib95_imap_old_stats_plugin_la_DEPENDENCIES = ../old-stats/lib90_old_stats_plugin.la
+am_lib95_imap_old_stats_plugin_la_OBJECTS = imap-stats-plugin.lo
+lib95_imap_old_stats_plugin_la_OBJECTS = \
+ $(am_lib95_imap_old_stats_plugin_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 =
+lib95_imap_old_stats_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib95_imap_old_stats_plugin_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)/imap-stats-plugin.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 = $(lib95_imap_old_stats_plugin_la_SOURCES)
+DIST_SOURCES = $(lib95_imap_old_stats_plugin_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 =
+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-old-stats \
+ -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/imap \
+ -I$(top_srcdir)/src/plugins/old-stats
+
+imap_moduledir = $(moduledir)
+lib95_imap_old_stats_plugin_la_LDFLAGS = -module -avoid-version
+imap_module_LTLIBRARIES = \
+ lib95_imap_old_stats_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib95_imap_old_stats_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../old-stats/lib90_old_stats_plugin.la
+
+lib95_imap_old_stats_plugin_la_SOURCES = \
+ imap-stats-plugin.c
+
+noinst_HEADERS = \
+ imap-stats-plugin.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/plugins/imap-old-stats/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/imap-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):
+
+install-imap_moduleLTLIBRARIES: $(imap_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_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)$(imap_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(imap_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(imap_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(imap_moduledir)"; \
+ }
+
+uninstall-imap_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(imap_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(imap_moduledir)/$$f"; \
+ done
+
+clean-imap_moduleLTLIBRARIES:
+ -test -z "$(imap_module_LTLIBRARIES)" || rm -f $(imap_module_LTLIBRARIES)
+ @list='$(imap_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}; \
+ }
+
+lib95_imap_old_stats_plugin.la: $(lib95_imap_old_stats_plugin_la_OBJECTS) $(lib95_imap_old_stats_plugin_la_DEPENDENCIES) $(EXTRA_lib95_imap_old_stats_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib95_imap_old_stats_plugin_la_LINK) -rpath $(imap_moduledir) $(lib95_imap_old_stats_plugin_la_OBJECTS) $(lib95_imap_old_stats_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-stats-plugin.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:
+ for dir in "$(DESTDIR)$(imap_moduledir)"; 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-imap_moduleLTLIBRARIES clean-libtool \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-stats-plugin.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-imap_moduleLTLIBRARIES
+
+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-stats-plugin.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-imap_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-imap_moduleLTLIBRARIES clean-libtool \
+ 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-imap_moduleLTLIBRARIES 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 uninstall-imap_moduleLTLIBRARIES
+
+.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/plugins/imap-old-stats/imap-stats-plugin.c b/src/plugins/imap-old-stats/imap-stats-plugin.c
new file mode 100644
index 0000000..6c91aa7
--- /dev/null
+++ b/src/plugins/imap-old-stats/imap-stats-plugin.c
@@ -0,0 +1,128 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "base64.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "stats.h"
+#include "stats-plugin.h"
+#include "stats-connection.h"
+#include "imap-stats-plugin.h"
+
+#define IMAP_STATS_IMAP_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, imap_stats_imap_module)
+
+struct stats_client_command {
+ union imap_module_context module_ctx;
+
+ unsigned int id;
+ bool continued;
+ struct stats *stats, *pre_stats;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(imap_stats_imap_module,
+ &imap_module_register);
+
+const char *imap_stats_plugin_version = DOVECOT_ABI_VERSION;
+
+static void stats_command_pre(struct client_command_context *cmd)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT(cmd->client->user);
+ struct stats_client_command *scmd;
+ static unsigned int stats_cmd_id_counter = 0;
+
+ if (suser == NULL || !suser->track_commands)
+ return;
+
+ if (strcasecmp(cmd->name, "IDLE") == 0) {
+ /* IDLE can run forever and waste stats process's memory while
+ waiting for it to timeout. don't send them. */
+ return;
+ }
+
+ scmd = IMAP_STATS_IMAP_CONTEXT(cmd);
+ if (scmd == NULL) {
+ scmd = p_new(cmd->pool, struct stats_client_command, 1);
+ scmd->id = ++stats_cmd_id_counter;
+ scmd->stats = stats_alloc(cmd->pool);
+ scmd->pre_stats = stats_alloc(cmd->pool);
+ MODULE_CONTEXT_SET(cmd, imap_stats_imap_module, scmd);
+ }
+
+ mail_user_stats_fill(cmd->client->user, scmd->pre_stats);
+}
+
+static void stats_command_post(struct client_command_context *cmd)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT(cmd->client->user);
+ struct stats_client_command *scmd = IMAP_STATS_IMAP_CONTEXT(cmd);
+ struct stats *new_stats, *diff_stats;
+ const char *error;
+ size_t args_pos = 0, args_len = 0;
+ string_t *str;
+ buffer_t *buf;
+
+ if (suser == NULL || scmd == NULL)
+ return;
+
+ new_stats = stats_alloc(pool_datastack_create());
+ diff_stats = stats_alloc(pool_datastack_create());
+
+ mail_user_stats_fill(cmd->client->user, new_stats);
+ if (!stats_diff(scmd->pre_stats, new_stats, diff_stats, &error))
+ i_error("stats: command stats shrank: %s", error);
+ stats_add(scmd->stats, diff_stats);
+
+ str = t_str_new(128);
+ str_append(str, "UPDATE-CMD\t");
+ str_append(str, suser->stats_session_id);
+
+ str_printfa(str, "\t%u\t", scmd->id);
+ if (cmd->state == CLIENT_COMMAND_STATE_DONE)
+ str_append_c(str, 'd');
+ if (scmd->continued)
+ str_append_c(str, 'c');
+ else {
+ str_append_c(str, '\t');
+ str_append(str, cmd->name);
+ str_append_c(str, '\t');
+ args_pos = str_len(str);
+ if (cmd->args != NULL)
+ str_append(str, cmd->args);
+ args_len = str_len(str) - args_pos;
+ scmd->continued = TRUE;
+ }
+
+ buf = t_buffer_create(128);
+ stats_export(buf, scmd->stats);
+ str_append_c(str, '\t');
+ base64_encode(buf->data, buf->used, str);
+
+ str_append_c(str, '\n');
+
+ if (str_len(str) > PIPE_BUF) {
+ /* truncate the args so it fits */
+ size_t delete_count = str_len(str) - PIPE_BUF;
+
+ i_assert(args_pos != 0);
+ if (delete_count > args_len)
+ delete_count = args_len;
+ str_delete(str, args_pos + args_len - delete_count,
+ delete_count);
+ }
+
+ stats_connection_send(suser->stats_conn, str);
+}
+
+void imap_old_stats_plugin_init(struct module *module ATTR_UNUSED)
+{
+ command_hook_register(stats_command_pre, stats_command_post);
+}
+
+void imap_old_stats_plugin_deinit(void)
+{
+ command_hook_unregister(stats_command_pre, stats_command_post);
+}
+
+const char *imap_old_stats_plugin_dependencies[] = { "old_stats", NULL };
+const char imap_old_stats_plugin_binary_dependency[] = "imap";
diff --git a/src/plugins/imap-old-stats/imap-stats-plugin.h b/src/plugins/imap-old-stats/imap-stats-plugin.h
new file mode 100644
index 0000000..2a95b53
--- /dev/null
+++ b/src/plugins/imap-old-stats/imap-stats-plugin.h
@@ -0,0 +1,12 @@
+#ifndef IMAP_STATS_PLUGIN_H
+#define IMAP_STATS_PLUGIN_H
+
+struct module;
+
+extern const char *imap_stats_plugin_dependencies[];
+extern const char imap_stats_plugin_binary_dependency[];
+
+void imap_old_stats_plugin_init(struct module *module);
+void imap_old_stats_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/imap-quota/Makefile.am b/src/plugins/imap-quota/Makefile.am
new file mode 100644
index 0000000..7c19c02
--- /dev/null
+++ b/src/plugins/imap-quota/Makefile.am
@@ -0,0 +1,27 @@
+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/imap \
+ -I$(top_srcdir)/src/plugins/quota
+
+imap_moduledir = $(moduledir)
+
+NOPLUGIN_LDFLAGS =
+lib11_imap_quota_plugin_la_LDFLAGS = -module -avoid-version
+
+imap_module_LTLIBRARIES = \
+ lib11_imap_quota_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib11_imap_quota_plugin_la_LIBADD = \
+ ../quota/lib10_quota_plugin.la
+endif
+
+lib11_imap_quota_plugin_la_SOURCES = \
+ imap-quota-plugin.c
+
+noinst_HEADERS = \
+ imap-quota-plugin.h
diff --git a/src/plugins/imap-quota/Makefile.in b/src/plugins/imap-quota/Makefile.in
new file mode 100644
index 0000000..3232d5e
--- /dev/null
+++ b/src/plugins/imap-quota/Makefile.in
@@ -0,0 +1,827 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/imap-quota
+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__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)$(imap_moduledir)"
+LTLIBRARIES = $(imap_module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib11_imap_quota_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../quota/lib10_quota_plugin.la
+am_lib11_imap_quota_plugin_la_OBJECTS = imap-quota-plugin.lo
+lib11_imap_quota_plugin_la_OBJECTS = \
+ $(am_lib11_imap_quota_plugin_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 =
+lib11_imap_quota_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib11_imap_quota_plugin_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)/imap-quota-plugin.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 = $(lib11_imap_quota_plugin_la_SOURCES)
+DIST_SOURCES = $(lib11_imap_quota_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/imap \
+ -I$(top_srcdir)/src/plugins/quota
+
+imap_moduledir = $(moduledir)
+lib11_imap_quota_plugin_la_LDFLAGS = -module -avoid-version
+imap_module_LTLIBRARIES = \
+ lib11_imap_quota_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib11_imap_quota_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../quota/lib10_quota_plugin.la
+
+lib11_imap_quota_plugin_la_SOURCES = \
+ imap-quota-plugin.c
+
+noinst_HEADERS = \
+ imap-quota-plugin.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/plugins/imap-quota/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/imap-quota/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-imap_moduleLTLIBRARIES: $(imap_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_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)$(imap_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(imap_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(imap_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(imap_moduledir)"; \
+ }
+
+uninstall-imap_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(imap_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(imap_moduledir)/$$f"; \
+ done
+
+clean-imap_moduleLTLIBRARIES:
+ -test -z "$(imap_module_LTLIBRARIES)" || rm -f $(imap_module_LTLIBRARIES)
+ @list='$(imap_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}; \
+ }
+
+lib11_imap_quota_plugin.la: $(lib11_imap_quota_plugin_la_OBJECTS) $(lib11_imap_quota_plugin_la_DEPENDENCIES) $(EXTRA_lib11_imap_quota_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib11_imap_quota_plugin_la_LINK) -rpath $(imap_moduledir) $(lib11_imap_quota_plugin_la_OBJECTS) $(lib11_imap_quota_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-quota-plugin.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:
+ for dir in "$(DESTDIR)$(imap_moduledir)"; 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-imap_moduleLTLIBRARIES clean-libtool \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-quota-plugin.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-imap_moduleLTLIBRARIES
+
+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-quota-plugin.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-imap_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-imap_moduleLTLIBRARIES clean-libtool \
+ 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-imap_moduleLTLIBRARIES 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 uninstall-imap_moduleLTLIBRARIES
+
+.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/plugins/imap-quota/imap-quota-plugin.c b/src/plugins/imap-quota/imap-quota-plugin.c
new file mode 100644
index 0000000..7863419
--- /dev/null
+++ b/src/plugins/imap-quota/imap-quota-plugin.c
@@ -0,0 +1,266 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "ostream.h"
+#include "imap-quote.h"
+#include "mail-namespace.h"
+#include "imap-commands.h"
+#include "quota.h"
+#include "quota-plugin.h"
+#include "imap-quota-plugin.h"
+
+
+#define QUOTA_USER_SEPARATOR ':'
+
+const char *imap_quota_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct module *imap_quota_module;
+static imap_client_created_func_t *next_hook_client_created;
+
+static const char *
+imap_quota_root_get_name(struct mail_user *user, struct mail_user *owner,
+ struct quota_root *root)
+{
+ const char *name;
+
+ name = quota_root_get_name(root);
+ if (user == owner || owner == NULL)
+ return name;
+ return t_strdup_printf("%s%c%s", owner->username,
+ QUOTA_USER_SEPARATOR, name);
+}
+
+static int
+quota_reply_write(string_t *str, struct mail_user *user,
+ struct mail_user *owner, struct quota_root *root)
+{
+ const char *name, *const *list, *error;
+ unsigned int i;
+ uint64_t value, limit;
+ size_t prefix_len, orig_len = str_len(str);
+ enum quota_get_result ret = QUOTA_GET_RESULT_UNLIMITED;
+
+ str_append(str, "* QUOTA ");
+ name = imap_quota_root_get_name(user, owner, root);
+ imap_append_astring(str, name);
+
+ str_append(str, " (");
+ prefix_len = str_len(str);
+ list = quota_root_get_resources(root);
+ for (i = 0; *list != NULL; list++) {
+ ret = quota_get_resource(root, "", *list, &value, &limit, &error);
+ if (ret == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ i_error("Failed to get quota resource %s: %s",
+ *list, error);
+ break;
+ }
+ if (ret == QUOTA_GET_RESULT_LIMITED) {
+ if (i > 0)
+ str_append_c(str, ' ');
+ str_printfa(str, "%s %"PRIu64" %"PRIu64, *list,
+ value, limit);
+ i++;
+ }
+ }
+ if (str_len(str) == prefix_len) {
+ /* this quota root doesn't have any quota actually enabled. */
+ str_truncate(str, orig_len);
+ } else {
+ str_append(str, ")\r\n");
+ }
+ return ret == QUOTA_GET_RESULT_INTERNAL_ERROR ? -1 : 0;
+}
+
+static bool cmd_getquotaroot(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct quota_user *quser = QUOTA_USER_CONTEXT(client->user);
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct quota_root_iter *iter;
+ struct quota_root *root;
+ const char *mailbox, *orig_mailbox, *name;
+ string_t *quotaroot_reply, *quota_reply;
+ int ret;
+
+ /* <mailbox> */
+ if (!client_read_string_args(cmd, 1, &mailbox))
+ return FALSE;
+ orig_mailbox = mailbox;
+
+ ns = client_find_namespace(cmd, &mailbox);
+ if (ns == NULL)
+ return TRUE;
+
+ if (quser == NULL) {
+ client_send_tagline(cmd, "OK No quota.");
+ return TRUE;
+ }
+ if (ns->owner != NULL && ns->owner != client->user) {
+ client_send_tagline(cmd, "NO Not showing other users' quota.");
+ return TRUE;
+ }
+
+ box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY);
+
+ /* build QUOTAROOT reply and QUOTA reply for all quota roots */
+ quotaroot_reply = t_str_new(128);
+ quota_reply = t_str_new(256);
+ str_append(quotaroot_reply, "* QUOTAROOT ");
+ imap_append_astring(quotaroot_reply, orig_mailbox);
+
+ ret = 0;
+ iter = quota_root_iter_init(box);
+ while ((root = quota_root_iter_next(iter)) != NULL) {
+ if (quota_root_is_hidden(root))
+ continue;
+ str_append_c(quotaroot_reply, ' ');
+ name = imap_quota_root_get_name(client->user, ns->owner, root);
+ imap_append_astring(quotaroot_reply, name);
+
+ if (quota_reply_write(quota_reply, client->user, ns->owner, root) < 0)
+ ret = -1;
+ }
+ quota_root_iter_deinit(&iter);
+ mailbox_free(&box);
+
+ /* send replies */
+ if (ret < 0)
+ client_send_tagline(cmd, "NO Internal quota calculation error.");
+ else if (str_len(quota_reply) == 0)
+ client_send_tagline(cmd, "OK No quota.");
+ else {
+ client_send_line(client, str_c(quotaroot_reply));
+ o_stream_nsend(client->output, str_data(quota_reply),
+ str_len(quota_reply));
+ client_send_tagline(cmd, "OK Getquotaroot completed.");
+ }
+ return TRUE;
+}
+
+static bool
+parse_quota_root(struct mail_user *user, const char *root_name,
+ struct mail_user **owner_r, struct quota_root **root_r)
+{
+ const char *p;
+
+ *owner_r = user;
+ *root_r = quota_root_lookup(user, root_name);
+ if (*root_r != NULL || !user->admin)
+ return *root_r != NULL;
+
+ /* we're an admin. see if there's a quota root for another user. */
+ p = strchr(root_name, QUOTA_USER_SEPARATOR);
+ if (p != NULL) {
+ *owner_r = mail_user_find(user, t_strdup_until(root_name, p));
+ *root_r = *owner_r == NULL ? NULL :
+ quota_root_lookup(*owner_r, p + 1);
+ }
+ return *root_r != NULL;
+}
+
+static bool cmd_getquota(struct client_command_context *cmd)
+{
+ struct mail_user *owner;
+ struct quota_root *root;
+ const char *root_name;
+ string_t *quota_reply;
+
+ /* <quota root> */
+ if (!client_read_string_args(cmd, 1, &root_name))
+ return FALSE;
+
+ if (!parse_quota_root(cmd->client->user, root_name, &owner, &root)) {
+ client_send_tagline(cmd, "NO Quota root doesn't exist.");
+ return TRUE;
+ }
+
+ quota_reply = t_str_new(128);
+ if (quota_reply_write(quota_reply, cmd->client->user, owner, root) < 0)
+ client_send_tagline(cmd, "NO Internal quota calculation error.");
+ else {
+ o_stream_nsend(cmd->client->output, str_data(quota_reply),
+ str_len(quota_reply));
+ client_send_tagline(cmd, "OK Getquota completed.");
+ }
+ return TRUE;
+}
+
+static bool cmd_setquota(struct client_command_context *cmd)
+{
+ struct quota_root *root;
+ struct mail_user *owner;
+ const struct imap_arg *args, *list_args;
+ const char *root_name, *name, *value_str, *client_error;
+ uint64_t value;
+
+ /* <quota root> <resource limits> */
+ if (!client_read_args(cmd, 2, 0, &args))
+ return FALSE;
+
+ if (!imap_arg_get_astring(&args[0], &root_name) ||
+ !imap_arg_get_list(&args[1], &list_args)) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ if (!cmd->client->user->admin) {
+ client_send_tagline(cmd, "NO Quota can be changed only by admin.");
+ return TRUE;
+ }
+
+ if (!parse_quota_root(cmd->client->user, root_name, &owner, &root)) {
+ client_send_tagline(cmd, "NO Quota root doesn't exist.");
+ return TRUE;
+ }
+
+ for (; !IMAP_ARG_IS_EOL(list_args); list_args += 2) {
+ if (!imap_arg_get_atom(&list_args[0], &name) ||
+ !imap_arg_get_atom(&list_args[1], &value_str) ||
+ str_to_uint64(value_str, &value) < 0) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+
+ if (quota_set_resource(root, name, value, &client_error) < 0) {
+ client_send_command_error(cmd, client_error);
+ return TRUE;
+ }
+ }
+
+ client_send_tagline(cmd, "OK Setquota completed.");
+ return TRUE;
+}
+
+static void imap_quota_client_created(struct client **client)
+{
+ if (mail_user_is_plugin_loaded((*client)->user, imap_quota_module))
+ client_add_capability(*client, "QUOTA");
+
+ if (next_hook_client_created != NULL)
+ next_hook_client_created(client);
+}
+
+void imap_quota_plugin_init(struct module *module)
+{
+ command_register("GETQUOTAROOT", cmd_getquotaroot, 0);
+ command_register("GETQUOTA", cmd_getquota, 0);
+ command_register("SETQUOTA", cmd_setquota, 0);
+
+ imap_quota_module = module;
+ next_hook_client_created =
+ imap_client_created_hook_set(imap_quota_client_created);
+}
+
+void imap_quota_plugin_deinit(void)
+{
+ command_unregister("GETQUOTAROOT");
+ command_unregister("GETQUOTA");
+ command_unregister("SETQUOTA");
+
+ imap_client_created_hook_set(next_hook_client_created);
+}
+
+const char *imap_quota_plugin_dependencies[] = { "quota", NULL };
+const char imap_quota_plugin_binary_dependency[] = "imap";
diff --git a/src/plugins/imap-quota/imap-quota-plugin.h b/src/plugins/imap-quota/imap-quota-plugin.h
new file mode 100644
index 0000000..1e55d10
--- /dev/null
+++ b/src/plugins/imap-quota/imap-quota-plugin.h
@@ -0,0 +1,12 @@
+#ifndef IMAP_QUOTA_PLUGIN_H
+#define IMAP_QUOTA_PLUGIN_H
+
+struct module;
+
+extern const char *imap_quota_plugin_dependencies[];
+extern const char imap_quota_plugin_binary_dependency[];
+
+void imap_quota_plugin_init(struct module *module);
+void imap_quota_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/imap-zlib/Makefile.am b/src/plugins/imap-zlib/Makefile.am
new file mode 100644
index 0000000..fa4f2f1
--- /dev/null
+++ b/src/plugins/imap-zlib/Makefile.am
@@ -0,0 +1,25 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-compression \
+ -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/imap
+
+imap_moduledir = $(moduledir)
+
+NOPLUGIN_LDFLAGS =
+lib30_imap_zlib_plugin_la_LDFLAGS = -module -avoid-version
+
+imap_module_LTLIBRARIES = \
+ lib30_imap_zlib_plugin.la
+
+lib30_imap_zlib_plugin_la_LIBADD = \
+ ../../lib-compression/libcompression.la
+
+lib30_imap_zlib_plugin_la_SOURCES = \
+ imap-zlib-plugin.c
+
+noinst_HEADERS = \
+ imap-zlib-plugin.h
diff --git a/src/plugins/imap-zlib/Makefile.in b/src/plugins/imap-zlib/Makefile.in
new file mode 100644
index 0000000..4613864
--- /dev/null
+++ b/src/plugins/imap-zlib/Makefile.in
@@ -0,0 +1,827 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/imap-zlib
+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__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)$(imap_moduledir)"
+LTLIBRARIES = $(imap_module_LTLIBRARIES)
+lib30_imap_zlib_plugin_la_DEPENDENCIES = \
+ ../../lib-compression/libcompression.la
+am_lib30_imap_zlib_plugin_la_OBJECTS = imap-zlib-plugin.lo
+lib30_imap_zlib_plugin_la_OBJECTS = \
+ $(am_lib30_imap_zlib_plugin_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 =
+lib30_imap_zlib_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib30_imap_zlib_plugin_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)/imap-zlib-plugin.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 = $(lib30_imap_zlib_plugin_la_SOURCES)
+DIST_SOURCES = $(lib30_imap_zlib_plugin_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 =
+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-compression \
+ -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/imap
+
+imap_moduledir = $(moduledir)
+lib30_imap_zlib_plugin_la_LDFLAGS = -module -avoid-version
+imap_module_LTLIBRARIES = \
+ lib30_imap_zlib_plugin.la
+
+lib30_imap_zlib_plugin_la_LIBADD = \
+ ../../lib-compression/libcompression.la
+
+lib30_imap_zlib_plugin_la_SOURCES = \
+ imap-zlib-plugin.c
+
+noinst_HEADERS = \
+ imap-zlib-plugin.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/plugins/imap-zlib/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/imap-zlib/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-imap_moduleLTLIBRARIES: $(imap_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_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)$(imap_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(imap_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(imap_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(imap_moduledir)"; \
+ }
+
+uninstall-imap_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(imap_module_LTLIBRARIES)'; test -n "$(imap_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(imap_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(imap_moduledir)/$$f"; \
+ done
+
+clean-imap_moduleLTLIBRARIES:
+ -test -z "$(imap_module_LTLIBRARIES)" || rm -f $(imap_module_LTLIBRARIES)
+ @list='$(imap_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}; \
+ }
+
+lib30_imap_zlib_plugin.la: $(lib30_imap_zlib_plugin_la_OBJECTS) $(lib30_imap_zlib_plugin_la_DEPENDENCIES) $(EXTRA_lib30_imap_zlib_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib30_imap_zlib_plugin_la_LINK) -rpath $(imap_moduledir) $(lib30_imap_zlib_plugin_la_OBJECTS) $(lib30_imap_zlib_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-zlib-plugin.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:
+ for dir in "$(DESTDIR)$(imap_moduledir)"; 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-imap_moduleLTLIBRARIES clean-libtool \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-zlib-plugin.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-imap_moduleLTLIBRARIES
+
+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-zlib-plugin.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-imap_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-imap_moduleLTLIBRARIES clean-libtool \
+ 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-imap_moduleLTLIBRARIES 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 uninstall-imap_moduleLTLIBRARIES
+
+.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/plugins/imap-zlib/imap-zlib-plugin.c b/src/plugins/imap-zlib/imap-zlib-plugin.c
new file mode 100644
index 0000000..df5508b
--- /dev/null
+++ b/src/plugins/imap-zlib/imap-zlib-plugin.c
@@ -0,0 +1,184 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "module-context.h"
+#include "imap-commands.h"
+#include "compression.h"
+#include "imap-zlib-plugin.h"
+
+
+#define IMAP_COMPRESS_DEFAULT_LEVEL 6
+
+#define IMAP_ZLIB_IMAP_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, imap_zlib_imap_module)
+
+struct zlib_client {
+ union imap_module_context module_ctx;
+
+ int (*next_state_export)(struct client *client, bool internal,
+ buffer_t *dest, const char **error_r);
+ const struct compression_handler *handler;
+};
+
+const char *imap_zlib_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct module *imap_zlib_module;
+static imap_client_created_func_t *next_hook_client_created;
+
+static MODULE_CONTEXT_DEFINE_INIT(imap_zlib_imap_module,
+ &imap_module_register);
+
+static void client_skip_line(struct client *client)
+{
+ const unsigned char *data;
+ size_t data_size;
+
+ data = i_stream_get_data(client->input, &data_size);
+ i_assert(data_size > 0);
+ if (data[0] == '\n')
+ i_stream_skip(client->input, 1);
+ else if (data[0] == '\r' && data_size > 1 && data[1] == '\n')
+ i_stream_skip(client->input, 2);
+ else
+ i_unreached();
+ client->input_skip_line = FALSE;
+}
+
+static void client_update_imap_parser_streams(struct client *client)
+{
+ struct client_command_context *cmd;
+
+ if (client->free_parser != NULL) {
+ imap_parser_set_streams(client->free_parser,
+ client->input, client->output);
+ }
+
+ for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) {
+ imap_parser_set_streams(cmd->parser,
+ client->input, client->output);
+ }
+}
+
+static bool cmd_compress(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ struct zlib_client *zclient = IMAP_ZLIB_IMAP_CONTEXT(client);
+ const struct compression_handler *handler;
+ const struct imap_arg *args;
+ struct istream *old_input;
+ struct ostream *old_output;
+ const char *mechanism, *value;
+ int level;
+ int ret;
+
+ /* <mechanism> */
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ if (!imap_arg_get_atom(args, &mechanism) ||
+ !IMAP_ARG_IS_EOL(&args[1])) {
+ client_send_command_error(cmd, "Invalid arguments.");
+ return TRUE;
+ }
+ if (zclient->handler != NULL) {
+ client_send_tagline(cmd, t_strdup_printf(
+ "NO [COMPRESSIONACTIVE] COMPRESSION=%s already enabled.",
+ t_str_ucase(zclient->handler->name)));
+ return TRUE;
+ }
+
+ ret = compression_lookup_handler(t_str_lcase(mechanism), &handler);
+ if (ret <= 0) {
+ const char * tagline =
+ t_strdup_printf("NO %s compression mechanism",
+ ret == 0 ? "Unsupported" : "Unknown");
+ client_send_tagline(cmd, tagline);
+ return TRUE;
+ }
+
+ client_skip_line(client);
+ client_send_tagline(cmd, "OK Begin compression.");
+
+ const char *setting = t_strdup_printf("imap_compress_%s_level",
+ handler->name);
+ value = mail_user_plugin_getenv(client->user, setting);
+ if (value == NULL) {
+ level = handler->get_default_level();
+ } else if (str_to_int(value, &level) < 0 ||
+ level < handler->get_min_level() ||
+ level > handler->get_max_level()) {
+ i_error("%s: Level must be between %d..%d",
+ setting,
+ handler->get_min_level(),
+ handler->get_max_level());
+ level = handler->get_default_level();
+ }
+ old_input = client->input;
+ old_output = client->output;
+ client->input = handler->create_istream(old_input);
+ client->output = handler->create_ostream(old_output, level);
+ /* preserve output offset so that the bytes out counter in logout
+ message doesn't get reset here */
+ client->output->offset = old_output->offset;
+ i_stream_unref(&old_input);
+ o_stream_unref(&old_output);
+
+ client_update_imap_parser_streams(client);
+ zclient->handler = handler;
+ return TRUE;
+}
+
+static int
+imap_zlib_state_export(struct client *client, bool internal,
+ buffer_t *dest, const char **error_r)
+{
+ struct zlib_client *zclient = IMAP_ZLIB_IMAP_CONTEXT(client);
+
+ if (zclient->handler != NULL && internal) {
+ *error_r = "COMPRESS enabled";
+ return 0;
+ }
+ return zclient->next_state_export(client, internal, dest, error_r);
+}
+
+static void imap_zlib_client_created(struct client **clientp)
+{
+ struct client *client = *clientp;
+ struct zlib_client *zclient;
+ const struct compression_handler *handler;
+
+ if (mail_user_is_plugin_loaded(client->user, imap_zlib_module) &&
+ compression_lookup_handler("deflate", &handler) > 0) {
+ zclient = p_new(client->pool, struct zlib_client, 1);
+ MODULE_CONTEXT_SET(client, imap_zlib_imap_module, zclient);
+
+ zclient->next_state_export = (*clientp)->v.state_export;
+ (*clientp)->v.state_export = imap_zlib_state_export;
+
+ client_add_capability(*clientp, "COMPRESS=DEFLATE");
+ }
+
+ if (next_hook_client_created != NULL)
+ next_hook_client_created(clientp);
+}
+
+void imap_zlib_plugin_init(struct module *module)
+{
+ command_register("COMPRESS", cmd_compress, 0);
+
+ imap_zlib_module = module;
+ next_hook_client_created =
+ imap_client_created_hook_set(imap_zlib_client_created);
+}
+
+void imap_zlib_plugin_deinit(void)
+{
+ command_unregister("COMPRESS");
+
+ imap_client_created_hook_set(next_hook_client_created);
+}
+
+const char imap_zlib_plugin_binary_dependency[] = "imap";
diff --git a/src/plugins/imap-zlib/imap-zlib-plugin.h b/src/plugins/imap-zlib/imap-zlib-plugin.h
new file mode 100644
index 0000000..251bb23
--- /dev/null
+++ b/src/plugins/imap-zlib/imap-zlib-plugin.h
@@ -0,0 +1,12 @@
+#ifndef IMAP_ZLIB_PLUGIN_H
+#define IMAP_ZLIB_PLUGIN_H
+
+struct module;
+
+extern const char *imap_zlib_plugin_dependencies[];
+extern const char imap_zlib_plugin_binary_dependency[];
+
+void imap_zlib_plugin_init(struct module *module);
+void imap_zlib_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/last-login/Makefile.am b/src/plugins/last-login/Makefile.am
new file mode 100644
index 0000000..cdedf43
--- /dev/null
+++ b/src/plugins/last-login/Makefile.am
@@ -0,0 +1,19 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib10_last_login_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib10_last_login_plugin.la
+
+lib10_last_login_plugin_la_SOURCES = \
+ last-login-plugin.c
+
+noinst_HEADERS = \
+ last-login-plugin.h
diff --git a/src/plugins/last-login/Makefile.in b/src/plugins/last-login/Makefile.in
new file mode 100644
index 0000000..d7cb8e4
--- /dev/null
+++ b/src/plugins/last-login/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/last-login
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib10_last_login_plugin_la_LIBADD =
+am_lib10_last_login_plugin_la_OBJECTS = last-login-plugin.lo
+lib10_last_login_plugin_la_OBJECTS = \
+ $(am_lib10_last_login_plugin_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 =
+lib10_last_login_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib10_last_login_plugin_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)/last-login-plugin.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 = $(lib10_last_login_plugin_la_SOURCES)
+DIST_SOURCES = $(lib10_last_login_plugin_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 =
+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-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib10_last_login_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib10_last_login_plugin.la
+
+lib10_last_login_plugin_la_SOURCES = \
+ last-login-plugin.c
+
+noinst_HEADERS = \
+ last-login-plugin.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/plugins/last-login/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/last-login/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-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}; \
+ }
+
+lib10_last_login_plugin.la: $(lib10_last_login_plugin_la_OBJECTS) $(lib10_last_login_plugin_la_DEPENDENCIES) $(EXTRA_lib10_last_login_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib10_last_login_plugin_la_LINK) -rpath $(moduledir) $(lib10_last_login_plugin_la_OBJECTS) $(lib10_last_login_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/last-login-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/last-login-plugin.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-moduleLTLIBRARIES
+
+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)/last-login-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/last-login/last-login-plugin.c b/src/plugins/last-login/last-login-plugin.c
new file mode 100644
index 0000000..1f580b9
--- /dev/null
+++ b/src/plugins/last-login/last-login-plugin.c
@@ -0,0 +1,157 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dict.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mail-storage-hooks.h"
+#include "last-login-plugin.h"
+
+#define LAST_LOGIN_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, last_login_user_module)
+
+#define LAST_LOGIN_DEFAULT_KEY_PREFIX "last-login/"
+
+struct last_login_user {
+ union mail_user_module_context module_ctx;
+ struct dict *dict;
+ struct timeout *to;
+};
+
+const char *last_login_plugin_version = DOVECOT_ABI_VERSION;
+
+static MODULE_CONTEXT_DEFINE_INIT(last_login_user_module,
+ &mail_user_module_register);
+
+static void last_login_dict_deinit(struct mail_user *user)
+{
+ struct last_login_user *luser = LAST_LOGIN_USER_CONTEXT(user);
+
+ if (luser->dict != NULL) {
+ dict_wait(luser->dict);
+ dict_deinit(&luser->dict);
+ }
+ /* remove timeout after dict_wait(), which may trigger
+ last_login_dict_commit() */
+ timeout_remove(&luser->to);
+}
+
+static void last_login_user_deinit(struct mail_user *user)
+{
+ struct last_login_user *luser = LAST_LOGIN_USER_CONTEXT(user);
+
+ last_login_dict_deinit(user);
+ luser->module_ctx.super.deinit(user);
+}
+
+static void
+last_login_dict_commit(const struct dict_commit_result *result,
+ struct mail_user *user)
+{
+ struct last_login_user *luser = LAST_LOGIN_USER_CONTEXT(user);
+
+ switch(result->ret) {
+ case DICT_COMMIT_RET_OK:
+ case DICT_COMMIT_RET_NOTFOUND:
+ break;
+ case DICT_COMMIT_RET_FAILED:
+ i_error("last_login_dict: Failed to write value: %s",
+ result->error);
+ break;
+ case DICT_COMMIT_RET_WRITE_UNCERTAIN:
+ i_error("last_login_dict: Write was unconfirmed (timeout or disconnect): %s",
+ result->error);
+ break;
+ }
+
+ /* don't deinit the dict immediately here, lib-dict will just crash */
+ luser->to = timeout_add(0, last_login_dict_deinit, user);
+}
+
+static void last_login_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct last_login_user *luser;
+ struct dict *dict;
+ struct dict_settings set;
+ struct dict_transaction_context *trans;
+ const char *dict_value, *key_name, *precision, *error;
+
+ if (user->autocreated) {
+ /* we want to handle only logged in users,
+ not lda's raw user or accessed shared users */
+ return;
+ }
+ if (user->session_restored) {
+ /* This is IMAP unhibernation, not a real login. */
+ return;
+ }
+
+ dict_value = mail_user_plugin_getenv(user, "last_login_dict");
+ if (dict_value == NULL || dict_value[0] == '\0')
+ return;
+
+ i_zero(&set);
+ set.base_dir = user->set->base_dir;
+ set.event_parent = user->event;
+ if (dict_init(dict_value, &set, &dict, &error) < 0) {
+ i_error("last_login_dict: dict_init(%s) failed: %s",
+ dict_value, error);
+ return;
+ }
+
+ luser = p_new(user->pool, struct last_login_user, 1);
+ luser->module_ctx.super = *v;
+ user->vlast = &luser->module_ctx.super;
+ v->deinit = last_login_user_deinit;
+
+ luser->dict = dict;
+ MODULE_CONTEXT_SET(user, last_login_user_module, luser);
+
+ key_name = mail_user_plugin_getenv(user, "last_login_key");
+ if (key_name == NULL) {
+ key_name = t_strdup_printf(LAST_LOGIN_DEFAULT_KEY_PREFIX"%s",
+ user->username);
+ }
+ key_name = t_strconcat(DICT_PATH_SHARED, key_name, NULL);
+
+ precision = mail_user_plugin_getenv(user, "last_login_precision");
+
+ const struct dict_op_settings *dset = mail_user_get_dict_op_settings(user);
+ trans = dict_transaction_begin(dict, dset);
+ if (precision == NULL || strcmp(precision, "s") == 0)
+ dict_set(trans, key_name, dec2str(ioloop_time));
+ else if (strcmp(precision, "ms") == 0) {
+ dict_set(trans, key_name, t_strdup_printf(
+ "%ld%03u", (long)ioloop_timeval.tv_sec,
+ (unsigned int)(ioloop_timeval.tv_usec/1000)));
+ } else if (strcmp(precision, "us") == 0) {
+ dict_set(trans, key_name, t_strdup_printf(
+ "%ld%06u", (long)ioloop_timeval.tv_sec,
+ (unsigned int)ioloop_timeval.tv_usec));
+ } else if (strcmp(precision, "ns") == 0) {
+ dict_set(trans, key_name, t_strdup_printf(
+ "%ld%06u000", (long)ioloop_timeval.tv_sec,
+ (unsigned int)ioloop_timeval.tv_usec));
+ } else {
+ i_error("last_login_dict: Invalid last_login_precision '%s'", precision);
+ }
+ dict_transaction_no_slowness_warning(trans);
+ dict_transaction_commit_async(&trans, last_login_dict_commit, user);
+}
+
+static struct mail_storage_hooks last_login_mail_storage_hooks = {
+ .mail_user_created = last_login_mail_user_created
+};
+
+void last_login_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &last_login_mail_storage_hooks);
+}
+
+void last_login_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&last_login_mail_storage_hooks);
+}
diff --git a/src/plugins/last-login/last-login-plugin.h b/src/plugins/last-login/last-login-plugin.h
new file mode 100644
index 0000000..481a148
--- /dev/null
+++ b/src/plugins/last-login/last-login-plugin.h
@@ -0,0 +1,7 @@
+#ifndef LAST_LOGIN_PLUGIN_H
+#define LAST_LOGIN_PLUGIN_H
+
+void last_login_plugin_init(struct module *module);
+void last_login_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/lazy-expunge/Makefile.am b/src/plugins/lazy-expunge/Makefile.am
new file mode 100644
index 0000000..8147b64
--- /dev/null
+++ b/src/plugins/lazy-expunge/Makefile.am
@@ -0,0 +1,21 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -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/maildir \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/plugins/quota
+
+NOPLUGIN_LDFLAGS =
+lib02_lazy_expunge_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib02_lazy_expunge_plugin.la
+
+lib02_lazy_expunge_plugin_la_SOURCES = \
+ lazy-expunge-plugin.c
+
+noinst_HEADERS = \
+ lazy-expunge-plugin.h
diff --git a/src/plugins/lazy-expunge/Makefile.in b/src/plugins/lazy-expunge/Makefile.in
new file mode 100644
index 0000000..9184174
--- /dev/null
+++ b/src/plugins/lazy-expunge/Makefile.in
@@ -0,0 +1,824 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/lazy-expunge
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib02_lazy_expunge_plugin_la_LIBADD =
+am_lib02_lazy_expunge_plugin_la_OBJECTS = lazy-expunge-plugin.lo
+lib02_lazy_expunge_plugin_la_OBJECTS = \
+ $(am_lib02_lazy_expunge_plugin_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 =
+lib02_lazy_expunge_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib02_lazy_expunge_plugin_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)/lazy-expunge-plugin.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 = $(lib02_lazy_expunge_plugin_la_SOURCES)
+DIST_SOURCES = $(lib02_lazy_expunge_plugin_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 =
+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-mail \
+ -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/maildir \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/plugins/quota
+
+lib02_lazy_expunge_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib02_lazy_expunge_plugin.la
+
+lib02_lazy_expunge_plugin_la_SOURCES = \
+ lazy-expunge-plugin.c
+
+noinst_HEADERS = \
+ lazy-expunge-plugin.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/plugins/lazy-expunge/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/lazy-expunge/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-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}; \
+ }
+
+lib02_lazy_expunge_plugin.la: $(lib02_lazy_expunge_plugin_la_OBJECTS) $(lib02_lazy_expunge_plugin_la_DEPENDENCIES) $(EXTRA_lib02_lazy_expunge_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib02_lazy_expunge_plugin_la_LINK) -rpath $(moduledir) $(lib02_lazy_expunge_plugin_la_OBJECTS) $(lib02_lazy_expunge_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lazy-expunge-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/lazy-expunge-plugin.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-moduleLTLIBRARIES
+
+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)/lazy-expunge-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/lazy-expunge/lazy-expunge-plugin.c b/src/plugins/lazy-expunge/lazy-expunge-plugin.c
new file mode 100644
index 0000000..ca10839
--- /dev/null
+++ b/src/plugins/lazy-expunge/lazy-expunge-plugin.c
@@ -0,0 +1,654 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "seq-range-array.h"
+#include "mkdir-parents.h"
+#include "mail-storage-private.h"
+#include "mail-search-build.h"
+#include "mailbox-list-private.h"
+#include "mailbox-match-plugin.h"
+#include "mail-namespace.h"
+#include "lazy-expunge-plugin.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <time.h>
+
+#define LAZY_EXPUNGE_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, lazy_expunge_mail_storage_module)
+#define LAZY_EXPUNGE_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, lazy_expunge_mail_storage_module)
+#define LAZY_EXPUNGE_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, lazy_expunge_mailbox_list_module)
+#define LAZY_EXPUNGE_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, lazy_expunge_mail_user_module)
+#define LAZY_EXPUNGE_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, lazy_expunge_mail_user_module)
+#define LAZY_EXPUNGE_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, lazy_expunge_mail_module)
+#define LAZY_EXPUNGE_MAIL_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, lazy_expunge_mail_module)
+
+struct lazy_expunge_mail {
+ union mail_module_context module_ctx;
+ bool moving;
+ bool recursing;
+};
+
+struct lazy_expunge_mail_user {
+ union mail_user_module_context module_ctx;
+
+ struct mail_namespace *lazy_ns;
+ struct mailbox_match_plugin *excludes;
+ const char *lazy_mailbox_vname;
+ const char *env;
+ bool copy_only_last_instance;
+};
+
+struct lazy_expunge_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+
+ bool allow_rename:1;
+ bool internal_namespace:1;
+};
+
+struct lazy_expunge_transaction {
+ union mailbox_transaction_module_context module_ctx;
+
+ struct mailbox *dest_box;
+ struct mailbox_transaction_context *dest_trans;
+
+ pool_t pool;
+ HASH_TABLE(const char *, void *) guids;
+
+ char *delayed_errstr;
+ char *delayed_internal_errstr;
+ enum mail_error delayed_error;
+
+ bool copy_only_last_instance;
+};
+
+const char *lazy_expunge_plugin_version = DOVECOT_ABI_VERSION;
+
+static MODULE_CONTEXT_DEFINE_INIT(lazy_expunge_mail_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(lazy_expunge_mail_module,
+ &mail_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(lazy_expunge_mailbox_list_module,
+ &mailbox_list_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(lazy_expunge_mail_user_module,
+ &mail_user_module_register);
+
+static const char *
+get_dest_vname(struct mailbox_list *list, struct mailbox *src_box)
+{
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT_REQUIRE(list->ns->user);
+ const char *name;
+ char src_sep, dest_sep;
+
+ if (luser->lazy_mailbox_vname != NULL)
+ return luser->lazy_mailbox_vname;
+
+ /* use the (canonical / unaliased) storage name */
+ name = src_box->name;
+ /* replace hierarchy separators with destination virtual separator */
+ src_sep = mailbox_list_get_hierarchy_sep(src_box->list);
+ dest_sep = mail_namespace_get_sep(list->ns);
+ if (src_sep != dest_sep) {
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ for (i = 0; name[i] != '\0'; i++) {
+ if (name[i] == src_sep)
+ str_append_c(str, dest_sep);
+ else
+ str_append_c(str, name[i]);
+ }
+ name = str_c(str);
+ }
+ /* add expunge namespace prefix. the name is now a proper vname */
+ return t_strconcat(list->ns->prefix, name, NULL);
+}
+
+static struct mailbox *
+mailbox_open_or_create(struct mailbox_list *list, struct mailbox *src_box,
+ const char **error_r)
+{
+ struct mailbox *box;
+ enum mail_error error;
+ const char *name;
+
+ name = get_dest_vname(list, src_box);
+
+ box = mailbox_alloc(list, name, MAILBOX_FLAG_NO_INDEX_FILES |
+ MAILBOX_FLAG_SAVEONLY | MAILBOX_FLAG_IGNORE_ACLS);
+ if (mailbox_open(box) == 0) {
+ *error_r = NULL;
+ return box;
+ }
+
+ *error_r = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_NOTFOUND) {
+ *error_r = t_strdup_printf("Failed to open mailbox %s: %s",
+ name, *error_r);
+ mailbox_free(&box);
+ return NULL;
+ }
+
+ /* try creating and re-opening it. */
+ if (mailbox_create(box, NULL, FALSE) < 0 &&
+ mailbox_get_last_mail_error(box) != MAIL_ERROR_EXISTS) {
+ *error_r = t_strdup_printf("Failed to create mailbox %s: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ return NULL;
+ }
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("Failed to open created mailbox %s: %s", name,
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ return NULL;
+ }
+ return box;
+}
+
+static unsigned int
+lazy_expunge_count_in_transaction(struct lazy_expunge_transaction *lt,
+ const char *guid)
+{
+ void *refcountp;
+ unsigned int refcount;
+
+ if (lt->pool == NULL) {
+ lt->pool = pool_alloconly_create("lazy expunge transaction",
+ 1024);
+ hash_table_create(&lt->guids, lt->pool, 0, str_hash, strcmp);
+ }
+
+ refcountp = hash_table_lookup(lt->guids, guid);
+ refcount = POINTER_CAST_TO(refcountp, unsigned int) + 1;
+ refcountp = POINTER_CAST(refcount);
+ if (refcount == 1) {
+ guid = p_strdup(lt->pool, guid);
+ hash_table_insert(lt->guids, guid, refcountp);
+ } else {
+ hash_table_update(lt->guids, guid, refcountp);
+ }
+ return refcount-1;
+}
+
+static int lazy_expunge_mail_is_last_instance(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct lazy_expunge_transaction *lt =
+ LAZY_EXPUNGE_CONTEXT_REQUIRE(_mail->transaction);
+ const char *value, *errstr;
+ unsigned long refcount;
+ enum mail_error error;
+
+ /* mail is reused by the search query, so the next mail_prefetch() on
+ it will try to prefetch the refcount */
+ mail->wanted_fields |= MAIL_FETCH_REFCOUNT;
+
+ if (mail_get_special(_mail, MAIL_FETCH_REFCOUNT, &value) < 0) {
+ errstr = mailbox_get_last_internal_error(_mail->box, &error);
+ if (error == MAIL_ERROR_EXPUNGED) {
+ /* already expunged - just ignore it */
+ return 0;
+ }
+ mail_set_critical(_mail,
+ "lazy_expunge: Couldn't lookup message's refcount: %s",
+ errstr);
+ return -1;
+ }
+ if (*value == '\0') {
+ /* refcounts not supported by backend. assume all mails are
+ the last instance. */
+ return 1;
+ }
+ if (str_to_ulong(value, &refcount) < 0)
+ i_panic("Invalid mail refcount number: %s", value);
+ if (refcount > 1) {
+ /* this probably isn't the last instance of the mail, but
+ it's possible that the same mail was copied to this mailbox
+ multiple times and we're deleting more than one instance
+ within this transaction. in those cases each expunge will
+ see the same refcount, so we need to adjust the refcount
+ by tracking the expunged message's refcount IDs. */
+ if (mail_get_special(_mail, MAIL_FETCH_REFCOUNT_ID, &value) < 0) {
+ errstr = mailbox_get_last_internal_error(_mail->box, &error);
+ if (error == MAIL_ERROR_EXPUNGED) {
+ /* already expunged - just ignore it */
+ return 0;
+ }
+ mail_set_critical(_mail,
+ "lazy_expunge: Couldn't lookup message's refcount ID: %s", errstr);
+ return -1;
+ }
+ if (*value == '\0') {
+ /* refcount IDs not supported by backend, but refcounts
+ are? not with our current backends. */
+ mail_set_critical(_mail,
+ "lazy_expunge: Message unexpectedly has no refcount ID");
+ return -1;
+ }
+ refcount -= lazy_expunge_count_in_transaction(lt, value);
+ }
+ return refcount <= 1 ? 1 : 0;
+}
+
+static bool lazy_expunge_is_internal_mailbox(struct mailbox *box)
+{
+ struct mail_namespace *ns = box->list->ns;
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT(ns->user);
+ struct lazy_expunge_mailbox_list *llist =
+ LAZY_EXPUNGE_LIST_CONTEXT(box->list);
+
+ if (luser == NULL || llist == NULL) {
+ /* lazy_expunge not enabled at all */
+ return FALSE;
+ }
+ if (llist->internal_namespace) {
+ /* lazy-expunge namespace */
+ return TRUE;
+ }
+ if (luser->lazy_mailbox_vname != NULL &&
+ strcmp(luser->lazy_mailbox_vname, box->vname) == 0) {
+ /* lazy-expunge mailbox */
+ return TRUE;
+ }
+ if (mailbox_match_plugin_exclude(luser->excludes, box)) {
+ /* Mailbox explicitly excluded by configuration */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void lazy_expunge_set_error(struct lazy_expunge_transaction *lt,
+ struct mail_storage *storage)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mail_storage_get_last_error(storage, &error);
+ if (error == MAIL_ERROR_EXPUNGED) {
+ /* expunging failed because the mail was already expunged.
+ we don't want to fail because of that. */
+ return;
+ }
+
+ if (lt->delayed_error != MAIL_ERROR_NONE)
+ return;
+ lt->delayed_error = error;
+ lt->delayed_errstr = i_strdup(errstr);
+ lt->delayed_internal_errstr =
+ i_strdup(mail_storage_get_last_internal_error(storage, NULL));
+}
+
+static void lazy_expunge_mail_expunge_move(struct mail *_mail)
+{
+ struct mail_namespace *ns = _mail->box->list->ns;
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT_REQUIRE(ns->user);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct lazy_expunge_mail *mmail =
+ LAZY_EXPUNGE_MAIL_CONTEXT_REQUIRE(mail);
+ struct lazy_expunge_transaction *lt =
+ LAZY_EXPUNGE_CONTEXT_REQUIRE(_mail->transaction);
+ struct mail_save_context *save_ctx;
+ const char *error;
+
+ if (lt->dest_box == NULL) {
+ lt->dest_box = mailbox_open_or_create(luser->lazy_ns->list,
+ _mail->box, &error);
+ if (lt->dest_box == NULL) {
+ mail_set_critical(_mail,
+ "lazy_expunge: Couldn't open expunge mailbox: "
+ "%s", error);
+ lazy_expunge_set_error(lt, _mail->box->storage);
+ return;
+ }
+ if (mailbox_sync(lt->dest_box, 0) < 0) {
+ mail_set_critical(_mail,
+ "lazy_expunge: Couldn't sync expunge mailbox");
+ lazy_expunge_set_error(lt, lt->dest_box->storage);
+ mailbox_free(&lt->dest_box);
+ return;
+ }
+
+ lt->dest_trans = mailbox_transaction_begin(lt->dest_box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL,
+ __func__);
+ }
+
+ save_ctx = mailbox_save_alloc(lt->dest_trans);
+ mailbox_save_copy_flags(save_ctx, _mail);
+ save_ctx->data.flags &= ENUM_NEGATE(MAIL_DELETED);
+
+ mmail->recursing = TRUE;
+ if (mailbox_move(&save_ctx, _mail) < 0 && !_mail->expunged)
+ lazy_expunge_set_error(lt, lt->dest_box->storage);
+ mmail->recursing = FALSE;
+}
+
+static void lazy_expunge_mail_expunge(struct mail *_mail)
+{
+ struct lazy_expunge_transaction *lt =
+ LAZY_EXPUNGE_CONTEXT_REQUIRE(_mail->transaction);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct lazy_expunge_mail *mmail =
+ LAZY_EXPUNGE_MAIL_CONTEXT_REQUIRE(mail);
+ struct mail *real_mail;
+ bool moving = mmail->moving;
+ int ret;
+
+ if (lt->delayed_error != MAIL_ERROR_NONE)
+ return;
+ if (mmail->recursing) {
+ mmail->module_ctx.super.expunge(_mail);
+ return;
+ }
+
+ /* Clear this in case the mail is used for non-move later on. */
+ mmail->moving = FALSE;
+
+ /* don't copy the mail if we're expunging from lazy_expunge
+ namespace (even if it's via a virtual mailbox) */
+ if (mail_get_backend_mail(_mail, &real_mail) < 0) {
+ lazy_expunge_set_error(lt, _mail->box->storage);
+ return;
+ }
+ if (lazy_expunge_is_internal_mailbox(real_mail->box)) {
+ mmail->module_ctx.super.expunge(_mail);
+ return;
+ }
+
+ struct event_reason *reason =
+ event_reason_begin("lazy_expunge:expunge");
+ if (!lt->copy_only_last_instance)
+ ret = 1;
+ else {
+ /* we want to copy only the last instance of the mail to
+ lazy_expunge namespace. other instances will be expunged
+ immediately. */
+ if (moving)
+ ret = 0;
+ else {
+ ret = lazy_expunge_mail_is_last_instance(_mail);
+ if (ret < 0)
+ lazy_expunge_set_error(lt, _mail->box->storage);
+ }
+ }
+ if (ret > 0)
+ lazy_expunge_mail_expunge_move(_mail);
+ event_reason_end(&reason);
+
+ if (ret == 0) {
+ /* Not the last instance of the mail - expunge it normally.
+ Since this is a normal expunge, do it without the
+ reason_code. */
+ mmail->module_ctx.super.expunge(_mail);
+ }
+}
+
+static int lazy_expunge_copy(struct mail_save_context *ctx, struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mailbox_module_context *mbox =
+ LAZY_EXPUNGE_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct lazy_expunge_mail *mmail = LAZY_EXPUNGE_MAIL_CONTEXT(mail);
+
+ if (mmail != NULL)
+ mmail->moving = ctx->moving;
+ return mbox->super.copy(ctx, _mail);
+}
+
+static struct mailbox_transaction_context *
+lazy_expunge_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT_REQUIRE(box->list->ns->user);
+ union mailbox_module_context *mbox = LAZY_EXPUNGE_CONTEXT_REQUIRE(box);
+ struct mailbox_transaction_context *t;
+ struct lazy_expunge_transaction *lt;
+
+ t = mbox->super.transaction_begin(box, flags, reason);
+ lt = i_new(struct lazy_expunge_transaction, 1);
+ lt->copy_only_last_instance = luser->copy_only_last_instance;
+
+ MODULE_CONTEXT_SET(t, lazy_expunge_mail_storage_module, lt);
+ return t;
+}
+
+static void lazy_expunge_transaction_free(struct lazy_expunge_transaction *lt)
+{
+ if (lt->dest_trans != NULL)
+ mailbox_transaction_rollback(&lt->dest_trans);
+ if (lt->dest_box != NULL)
+ mailbox_free(&lt->dest_box);
+ hash_table_destroy(&lt->guids);
+ pool_unref(&lt->pool);
+ i_free(lt->delayed_errstr);
+ i_free(lt->delayed_internal_errstr);
+ i_free(lt);
+}
+
+static int
+lazy_expunge_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ union mailbox_module_context *mbox = LAZY_EXPUNGE_CONTEXT_REQUIRE(ctx->box);
+ struct lazy_expunge_transaction *lt = LAZY_EXPUNGE_CONTEXT_REQUIRE(ctx);
+ int ret;
+
+ if (lt->dest_trans != NULL && lt->delayed_error == MAIL_ERROR_NONE) {
+ if (mailbox_transaction_commit(&lt->dest_trans) < 0) {
+ lazy_expunge_set_error(lt, ctx->box->storage);
+ }
+ }
+
+ if (lt->delayed_error == MAIL_ERROR_NONE)
+ ret = mbox->super.transaction_commit(ctx, changes_r);
+ else if (lt->delayed_error != MAIL_ERROR_TEMP) {
+ mail_storage_set_error(ctx->box->storage, lt->delayed_error,
+ lt->delayed_errstr);
+ mbox->super.transaction_rollback(ctx);
+ ret = -1;
+ } else {
+ mailbox_set_critical(ctx->box,
+ "Lazy-expunge transaction failed: %s",
+ lt->delayed_internal_errstr);
+ mbox->super.transaction_rollback(ctx);
+ ret = -1;
+ }
+ lazy_expunge_transaction_free(lt);
+ return ret;
+}
+
+static void
+lazy_expunge_transaction_rollback(struct mailbox_transaction_context *ctx)
+{
+ union mailbox_module_context *mbox = LAZY_EXPUNGE_CONTEXT_REQUIRE(ctx->box);
+ struct lazy_expunge_transaction *lt = LAZY_EXPUNGE_CONTEXT_REQUIRE(ctx);
+
+ mbox->super.transaction_rollback(ctx);
+ lazy_expunge_transaction_free(lt);
+}
+
+static void lazy_expunge_mail_allocated(struct mail *_mail)
+{
+ struct lazy_expunge_transaction *lt =
+ LAZY_EXPUNGE_CONTEXT(_mail->transaction);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ struct lazy_expunge_mail *mmail;
+
+ if (lt == NULL)
+ return;
+
+ mmail = p_new(mail->pool, struct lazy_expunge_mail, 1);
+ mmail->module_ctx.super = *v;
+ mail->vlast = &mmail->module_ctx.super;
+
+ v->expunge = lazy_expunge_mail_expunge;
+ MODULE_CONTEXT_SET(mail, lazy_expunge_mail_module, mmail);
+}
+
+static int
+lazy_expunge_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ union mailbox_module_context *lbox = LAZY_EXPUNGE_CONTEXT_REQUIRE(src);
+ struct lazy_expunge_mailbox_list *src_llist =
+ LAZY_EXPUNGE_LIST_CONTEXT(src->list);
+ struct lazy_expunge_mailbox_list *dest_llist =
+ LAZY_EXPUNGE_LIST_CONTEXT(dest->list);
+
+ i_assert(src_llist != NULL && dest_llist != NULL);
+
+ if (!src_llist->allow_rename &&
+ (src_llist->internal_namespace ||
+ dest_llist->internal_namespace)) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename mailboxes to/from expunge namespace.");
+ return -1;
+ }
+ return lbox->super.rename_box(src, dest);
+}
+
+static void lazy_expunge_mailbox_allocated(struct mailbox *box)
+{
+ struct lazy_expunge_mailbox_list *llist =
+ LAZY_EXPUNGE_LIST_CONTEXT(box->list);
+ union mailbox_module_context *mbox;
+ struct mailbox_vfuncs *v = box->vlast;
+
+ if (llist == NULL || (box->flags & MAILBOX_FLAG_DELETE_UNSAFE) != 0)
+ return;
+
+ mbox = p_new(box->pool, union mailbox_module_context, 1);
+ mbox->super = *v;
+ box->vlast = &mbox->super;
+ MODULE_CONTEXT_SET_SELF(box, lazy_expunge_mail_storage_module, mbox);
+
+ if (!lazy_expunge_is_internal_mailbox(box)) {
+ v->copy = lazy_expunge_copy;
+ v->transaction_begin = lazy_expunge_transaction_begin;
+ v->transaction_commit = lazy_expunge_transaction_commit;
+ v->transaction_rollback = lazy_expunge_transaction_rollback;
+ v->rename_box = lazy_expunge_mailbox_rename;
+ } else if (llist->internal_namespace) {
+ v->rename_box = lazy_expunge_mailbox_rename;
+ } else {
+ /* internal mailbox in a non-internal namespace -
+ don't add any unnecessary restrictions to it. if it's not
+ wanted, just use the ACL plugin. */
+ }
+}
+
+static void lazy_expunge_mailbox_list_created(struct mailbox_list *list)
+{
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT(list->ns->user);
+ struct lazy_expunge_mailbox_list *llist;
+
+ if (luser == NULL)
+ return;
+
+ /* if this is one of our internal namespaces, mark it as such before
+ quota plugin sees it */
+ if (strcmp(list->ns->prefix, luser->env) == 0)
+ list->ns->flags |= NAMESPACE_FLAG_NOQUOTA;
+
+ if (list->ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) {
+ llist = p_new(list->pool, struct lazy_expunge_mailbox_list, 1);
+ MODULE_CONTEXT_SET(list, lazy_expunge_mailbox_list_module,
+ llist);
+ }
+}
+
+static void
+lazy_expunge_mail_namespaces_created(struct mail_namespace *namespaces)
+{
+ struct lazy_expunge_mail_user *luser =
+ LAZY_EXPUNGE_USER_CONTEXT(namespaces->user);
+ struct lazy_expunge_mailbox_list *llist;
+
+ if (luser == NULL)
+ return;
+
+ luser->lazy_ns = mail_namespace_find_prefix(namespaces, luser->env);
+ if (luser->lazy_ns != NULL) {
+ /* we don't want to override this namespace's expunge operation. */
+ llist = LAZY_EXPUNGE_LIST_CONTEXT(luser->lazy_ns->list);
+ i_assert(llist != NULL);
+ llist->internal_namespace = TRUE;
+ } else {
+ /* store the the expunged mails to the specified mailbox. */
+ luser->lazy_ns = mail_namespace_find(namespaces, luser->env);
+ luser->lazy_mailbox_vname = luser->env;
+ }
+ mail_namespace_ref(luser->lazy_ns);
+}
+
+static void lazy_expunge_user_deinit(struct mail_user *user)
+{
+ struct lazy_expunge_mail_user *luser = LAZY_EXPUNGE_USER_CONTEXT_REQUIRE(user);
+
+ /* mail_namespaces_created hook isn't necessarily ever called */
+ if (luser->lazy_ns != NULL)
+ mail_namespace_unref(&luser->lazy_ns);
+ mailbox_match_plugin_deinit(&luser->excludes);
+
+ luser->module_ctx.super.deinit(user);
+}
+
+static void lazy_expunge_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct lazy_expunge_mail_user *luser;
+ const char *env;
+
+ env = mail_user_plugin_getenv(user, "lazy_expunge");
+ if (env != NULL && env[0] != '\0') {
+ luser = p_new(user->pool, struct lazy_expunge_mail_user, 1);
+ luser->module_ctx.super = *v;
+ user->vlast = &luser->module_ctx.super;
+ v->deinit = lazy_expunge_user_deinit;
+ luser->env = env;
+ luser->copy_only_last_instance =
+ mail_user_plugin_getenv_bool(user, "lazy_expunge_only_last_instance");
+ luser->excludes = mailbox_match_plugin_init(user, "lazy_expunge_exclude");
+
+ MODULE_CONTEXT_SET(user, lazy_expunge_mail_user_module, luser);
+ } else {
+ e_debug(user->event, "lazy_expunge: No lazy_expunge setting - "
+ "plugin disabled");
+ }
+}
+
+static struct mail_storage_hooks lazy_expunge_mail_storage_hooks = {
+ .mail_user_created = lazy_expunge_mail_user_created,
+ .mail_namespaces_created = lazy_expunge_mail_namespaces_created,
+ .mailbox_list_created = lazy_expunge_mailbox_list_created,
+ .mailbox_allocated = lazy_expunge_mailbox_allocated,
+ .mail_allocated = lazy_expunge_mail_allocated
+};
+
+void lazy_expunge_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &lazy_expunge_mail_storage_hooks);
+}
+
+void lazy_expunge_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&lazy_expunge_mail_storage_hooks);
+}
diff --git a/src/plugins/lazy-expunge/lazy-expunge-plugin.h b/src/plugins/lazy-expunge/lazy-expunge-plugin.h
new file mode 100644
index 0000000..88ee53d
--- /dev/null
+++ b/src/plugins/lazy-expunge/lazy-expunge-plugin.h
@@ -0,0 +1,7 @@
+#ifndef LAZY_EXPUNGE_PLUGIN_H
+#define LAZY_EXPUNGE_PLUGIN_H
+
+void lazy_expunge_plugin_init(struct module *module);
+void lazy_expunge_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/listescape/Makefile.am b/src/plugins/listescape/Makefile.am
new file mode 100644
index 0000000..fa614a3
--- /dev/null
+++ b/src/plugins/listescape/Makefile.am
@@ -0,0 +1,18 @@
+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
+
+NOPLUGIN_LDFLAGS =
+lib20_listescape_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_listescape_plugin.la
+
+lib20_listescape_plugin_la_SOURCES = \
+ listescape-plugin.c
+
+noinst_HEADERS = \
+ listescape-plugin.h
diff --git a/src/plugins/listescape/Makefile.in b/src/plugins/listescape/Makefile.in
new file mode 100644
index 0000000..47dd48b
--- /dev/null
+++ b/src/plugins/listescape/Makefile.in
@@ -0,0 +1,821 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/listescape
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_listescape_plugin_la_LIBADD =
+am_lib20_listescape_plugin_la_OBJECTS = listescape-plugin.lo
+lib20_listescape_plugin_la_OBJECTS = \
+ $(am_lib20_listescape_plugin_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 =
+lib20_listescape_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_listescape_plugin_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)/listescape-plugin.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 = $(lib20_listescape_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_listescape_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib20_listescape_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_listescape_plugin.la
+
+lib20_listescape_plugin_la_SOURCES = \
+ listescape-plugin.c
+
+noinst_HEADERS = \
+ listescape-plugin.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/plugins/listescape/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/listescape/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-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}; \
+ }
+
+lib20_listescape_plugin.la: $(lib20_listescape_plugin_la_OBJECTS) $(lib20_listescape_plugin_la_DEPENDENCIES) $(EXTRA_lib20_listescape_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_listescape_plugin_la_LINK) -rpath $(moduledir) $(lib20_listescape_plugin_la_OBJECTS) $(lib20_listescape_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/listescape-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/listescape-plugin.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-moduleLTLIBRARIES
+
+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)/listescape-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/listescape/listescape-plugin.c b/src/plugins/listescape/listescape-plugin.c
new file mode 100644
index 0000000..89fb1f9
--- /dev/null
+++ b/src/plugins/listescape/listescape-plugin.c
@@ -0,0 +1,36 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage-hooks.h"
+#include "mailbox-list-private.h"
+#include "listescape-plugin.h"
+
+#define DEFAULT_ESCAPE_CHAR '\\'
+
+const char *listescape_plugin_version = DOVECOT_ABI_VERSION;
+
+static void listescape_mailbox_list_created(struct mailbox_list *list)
+{
+ const char *env;
+
+ if (list->set.storage_name_escape_char == '\0') {
+ env = mail_user_plugin_getenv(list->ns->user, "listescape_char");
+ list->set.storage_name_escape_char =
+ env != NULL && *env != '\0' ?
+ env[0] : DEFAULT_ESCAPE_CHAR;
+ }
+}
+
+static struct mail_storage_hooks listescape_mail_storage_hooks = {
+ .mailbox_list_created = listescape_mailbox_list_created
+};
+
+void listescape_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &listescape_mail_storage_hooks);
+}
+
+void listescape_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&listescape_mail_storage_hooks);
+}
diff --git a/src/plugins/listescape/listescape-plugin.h b/src/plugins/listescape/listescape-plugin.h
new file mode 100644
index 0000000..f8ff4fc
--- /dev/null
+++ b/src/plugins/listescape/listescape-plugin.h
@@ -0,0 +1,7 @@
+#ifndef LISTESCAPE_PLUGIN_H
+#define LISTESCAPE_PLUGIN_H
+
+void listescape_plugin_init(struct module *module);
+void listescape_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/mail-crypt/Makefile.am b/src/plugins/mail-crypt/Makefile.am
new file mode 100644
index 0000000..942dc87
--- /dev/null
+++ b/src/plugins/mail-crypt/Makefile.am
@@ -0,0 +1,116 @@
+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-mail \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/doveadm \
+ -I$(top_srcdir)/src/plugins/acl
+
+if SSL_VERSION_GE_102
+test_options =
+else !SSL_VERSION_GE_102
+test_options = NOUNDEF=1
+endif !SSL_VERSION_GE_102
+
+doveadm_moduledir = $(moduledir)/doveadm
+
+NOPLUGIN_LDFLAGS =
+
+module_LTLIBRARIES = \
+ lib10_mail_crypt_plugin.la \
+ lib05_mail_crypt_acl_plugin.la \
+ libfs_crypt.la \
+ libfs_mail_crypt.la
+
+doveadm_module_LTLIBRARIES = \
+ libdoveadm_mail_crypt_plugin.la
+
+lib10_mail_crypt_plugin_la_LDFLAGS = -module -avoid-version
+lib10_mail_crypt_plugin_la_LIBADD = \
+ $(LIBDCRYPT_LIBS) \
+ $(LIBDOVECOT)
+
+lib05_mail_crypt_acl_plugin_la_LDFLAGS = -module -avoid-version
+if DOVECOT_PLUGIN_DEPS
+lib05_mail_crypt_acl_plugin_la_LIBADD = \
+ $(LIBDCRYPT_LIBS) \
+ lib10_mail_crypt_plugin.la
+endif
+
+lib10_mail_crypt_plugin_la_SOURCES = \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c \
+ mail-crypt-key.c \
+ mail-crypt-plugin.c
+
+lib05_mail_crypt_acl_plugin_la_SOURCES = \
+ mail-crypt-acl-plugin.c
+
+libfs_crypt_la_SOURCES = fs-crypt.c \
+ mail-crypt-global-key.c \
+ mail-crypt-pluginenv.c \
+ fs-crypt-settings.c
+
+libfs_crypt_la_LIBADD = $(LIBDOVECOT)
+libfs_crypt_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libfs_crypt_la_LDFLAGS = -module -avoid-version
+
+libfs_mail_crypt_la_SOURCES = fs-mail-crypt.c \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c
+libfs_mail_crypt_la_LIBADD = $(LIBDOVECOT)
+libfs_mail_crypt_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libfs_mail_crypt_la_LDFLAGS = -module -avoid-version
+
+libdoveadm_mail_crypt_plugin_la_SOURCES = \
+ doveadm-mail-crypt.c
+libdoveadm_mail_crypt_plugin_la_LIBADD = $(LIBDOVECOT)
+libdoveadm_mail_crypt_plugin_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libdoveadm_mail_crypt_plugin_la_LDFLAGS = -module -avoid-version
+
+test_programs = \
+ test-mail-global-key \
+ test-mail-key
+
+test_mail_global_key_SOURCES = \
+ test-mail-global-key.c \
+ fs-crypt-settings.c \
+ mail-crypt-global-key.c
+test_mail_global_key_LDADD = $(LIBDOVECOT)
+test_mail_global_key_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+test_mail_global_key_LDFLAGS = $(DOVECOT_BINARY_LDFLAGS)
+test_mail_global_key_CFLAGS = $(AM_CPPFLAGS) $(DOVECOT_BINARY_CFLAGS) -Dtop_builddir=\"$(top_builddir)\"
+
+test_mail_key_SOURCES = \
+ test-mail-key.c \
+ mail-crypt-key.c \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c
+
+test_mail_key_LDADD = $(LIBDOVECOT_STORAGE) $(LIBDOVECOT)
+test_mail_key_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(LIBDOVECOT_STORAGE_DEPS)
+test_mail_key_LDFLAGS = $(DOVECOT_BINARY_LDFLAGS)
+test_mail_key_CFLAGS = $(AM_CPPFLAGS) $(DOVECOT_BINARY_CFLAGS) -Dtop_builddir=\"$(top_builddir)\"
+
+EXTRA_DIST = fs-crypt-common.c
+
+noinst_HEADERS = \
+ mail-crypt-plugin.h \
+ mail-crypt-common.h \
+ mail-crypt-global-key.h \
+ mail-crypt-key.h \
+ fs-crypt-settings.h
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+noinst_PROGRAMS = $(test_programs)
diff --git a/src/plugins/mail-crypt/Makefile.in b/src/plugins/mail-crypt/Makefile.in
new file mode 100644
index 0000000..bf96636
--- /dev/null
+++ b/src/plugins/mail-crypt/Makefile.in
@@ -0,0 +1,1200 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/mail-crypt
+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-mail-global-key$(EXEEXT) test-mail-key$(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)$(doveadm_moduledir)" \
+ "$(DESTDIR)$(moduledir)"
+LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib05_mail_crypt_acl_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ lib10_mail_crypt_plugin.la
+am_lib05_mail_crypt_acl_plugin_la_OBJECTS = mail-crypt-acl-plugin.lo
+lib05_mail_crypt_acl_plugin_la_OBJECTS = \
+ $(am_lib05_mail_crypt_acl_plugin_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 =
+lib05_mail_crypt_acl_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib05_mail_crypt_acl_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+lib10_mail_crypt_plugin_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_lib10_mail_crypt_plugin_la_OBJECTS = mail-crypt-global-key.lo \
+ mail-crypt-userenv.lo mail-crypt-key.lo mail-crypt-plugin.lo
+lib10_mail_crypt_plugin_la_OBJECTS = \
+ $(am_lib10_mail_crypt_plugin_la_OBJECTS)
+lib10_mail_crypt_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib10_mail_crypt_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_libdoveadm_mail_crypt_plugin_la_OBJECTS = doveadm-mail-crypt.lo
+libdoveadm_mail_crypt_plugin_la_OBJECTS = \
+ $(am_libdoveadm_mail_crypt_plugin_la_OBJECTS)
+libdoveadm_mail_crypt_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(libdoveadm_mail_crypt_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am_libfs_crypt_la_OBJECTS = fs-crypt.lo mail-crypt-global-key.lo \
+ mail-crypt-pluginenv.lo fs-crypt-settings.lo
+libfs_crypt_la_OBJECTS = $(am_libfs_crypt_la_OBJECTS)
+libfs_crypt_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libfs_crypt_la_LDFLAGS) $(LDFLAGS) -o \
+ $@
+am_libfs_mail_crypt_la_OBJECTS = fs-mail-crypt.lo \
+ mail-crypt-global-key.lo mail-crypt-userenv.lo
+libfs_mail_crypt_la_OBJECTS = $(am_libfs_mail_crypt_la_OBJECTS)
+libfs_mail_crypt_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libfs_mail_crypt_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_mail_global_key_OBJECTS = \
+ test_mail_global_key-test-mail-global-key.$(OBJEXT) \
+ test_mail_global_key-fs-crypt-settings.$(OBJEXT) \
+ test_mail_global_key-mail-crypt-global-key.$(OBJEXT)
+test_mail_global_key_OBJECTS = $(am_test_mail_global_key_OBJECTS)
+test_mail_global_key_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(test_mail_global_key_CFLAGS) $(CFLAGS) \
+ $(test_mail_global_key_LDFLAGS) $(LDFLAGS) -o $@
+am_test_mail_key_OBJECTS = test_mail_key-test-mail-key.$(OBJEXT) \
+ test_mail_key-mail-crypt-key.$(OBJEXT) \
+ test_mail_key-mail-crypt-global-key.$(OBJEXT) \
+ test_mail_key-mail-crypt-userenv.$(OBJEXT)
+test_mail_key_OBJECTS = $(am_test_mail_key_OBJECTS)
+test_mail_key_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_mail_key_CFLAGS) \
+ $(CFLAGS) $(test_mail_key_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)/doveadm-mail-crypt.Plo \
+ ./$(DEPDIR)/fs-crypt-settings.Plo ./$(DEPDIR)/fs-crypt.Plo \
+ ./$(DEPDIR)/fs-mail-crypt.Plo \
+ ./$(DEPDIR)/mail-crypt-acl-plugin.Plo \
+ ./$(DEPDIR)/mail-crypt-global-key.Plo \
+ ./$(DEPDIR)/mail-crypt-key.Plo \
+ ./$(DEPDIR)/mail-crypt-plugin.Plo \
+ ./$(DEPDIR)/mail-crypt-pluginenv.Plo \
+ ./$(DEPDIR)/mail-crypt-userenv.Plo \
+ ./$(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po \
+ ./$(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po \
+ ./$(DEPDIR)/test_mail_global_key-test-mail-global-key.Po \
+ ./$(DEPDIR)/test_mail_key-mail-crypt-global-key.Po \
+ ./$(DEPDIR)/test_mail_key-mail-crypt-key.Po \
+ ./$(DEPDIR)/test_mail_key-mail-crypt-userenv.Po \
+ ./$(DEPDIR)/test_mail_key-test-mail-key.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 = $(lib05_mail_crypt_acl_plugin_la_SOURCES) \
+ $(lib10_mail_crypt_plugin_la_SOURCES) \
+ $(libdoveadm_mail_crypt_plugin_la_SOURCES) \
+ $(libfs_crypt_la_SOURCES) $(libfs_mail_crypt_la_SOURCES) \
+ $(test_mail_global_key_SOURCES) $(test_mail_key_SOURCES)
+DIST_SOURCES = $(lib05_mail_crypt_acl_plugin_la_SOURCES) \
+ $(lib10_mail_crypt_plugin_la_SOURCES) \
+ $(libdoveadm_mail_crypt_plugin_la_SOURCES) \
+ $(libfs_crypt_la_SOURCES) $(libfs_mail_crypt_la_SOURCES) \
+ $(test_mail_global_key_SOURCES) $(test_mail_key_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@
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/doveadm \
+ -I$(top_srcdir)/src/plugins/acl
+
+@SSL_VERSION_GE_102_FALSE@test_options = NOUNDEF=1
+@SSL_VERSION_GE_102_TRUE@test_options =
+doveadm_moduledir = $(moduledir)/doveadm
+module_LTLIBRARIES = \
+ lib10_mail_crypt_plugin.la \
+ lib05_mail_crypt_acl_plugin.la \
+ libfs_crypt.la \
+ libfs_mail_crypt.la
+
+doveadm_module_LTLIBRARIES = \
+ libdoveadm_mail_crypt_plugin.la
+
+lib10_mail_crypt_plugin_la_LDFLAGS = -module -avoid-version
+lib10_mail_crypt_plugin_la_LIBADD = \
+ $(LIBDCRYPT_LIBS) \
+ $(LIBDOVECOT)
+
+lib05_mail_crypt_acl_plugin_la_LDFLAGS = -module -avoid-version
+@DOVECOT_PLUGIN_DEPS_TRUE@lib05_mail_crypt_acl_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ $(LIBDCRYPT_LIBS) \
+@DOVECOT_PLUGIN_DEPS_TRUE@ lib10_mail_crypt_plugin.la
+
+lib10_mail_crypt_plugin_la_SOURCES = \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c \
+ mail-crypt-key.c \
+ mail-crypt-plugin.c
+
+lib05_mail_crypt_acl_plugin_la_SOURCES = \
+ mail-crypt-acl-plugin.c
+
+libfs_crypt_la_SOURCES = fs-crypt.c \
+ mail-crypt-global-key.c \
+ mail-crypt-pluginenv.c \
+ fs-crypt-settings.c
+
+libfs_crypt_la_LIBADD = $(LIBDOVECOT)
+libfs_crypt_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libfs_crypt_la_LDFLAGS = -module -avoid-version
+libfs_mail_crypt_la_SOURCES = fs-mail-crypt.c \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c
+
+libfs_mail_crypt_la_LIBADD = $(LIBDOVECOT)
+libfs_mail_crypt_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libfs_mail_crypt_la_LDFLAGS = -module -avoid-version
+libdoveadm_mail_crypt_plugin_la_SOURCES = \
+ doveadm-mail-crypt.c
+
+libdoveadm_mail_crypt_plugin_la_LIBADD = $(LIBDOVECOT)
+libdoveadm_mail_crypt_plugin_la_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+libdoveadm_mail_crypt_plugin_la_LDFLAGS = -module -avoid-version
+test_programs = \
+ test-mail-global-key \
+ test-mail-key
+
+test_mail_global_key_SOURCES = \
+ test-mail-global-key.c \
+ fs-crypt-settings.c \
+ mail-crypt-global-key.c
+
+test_mail_global_key_LDADD = $(LIBDOVECOT)
+test_mail_global_key_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+test_mail_global_key_LDFLAGS = $(DOVECOT_BINARY_LDFLAGS)
+test_mail_global_key_CFLAGS = $(AM_CPPFLAGS) $(DOVECOT_BINARY_CFLAGS) -Dtop_builddir=\"$(top_builddir)\"
+test_mail_key_SOURCES = \
+ test-mail-key.c \
+ mail-crypt-key.c \
+ mail-crypt-global-key.c \
+ mail-crypt-userenv.c
+
+test_mail_key_LDADD = $(LIBDOVECOT_STORAGE) $(LIBDOVECOT)
+test_mail_key_DEPENDENCIES = $(LIBDOVECOT_DEPS) $(LIBDOVECOT_STORAGE_DEPS)
+test_mail_key_LDFLAGS = $(DOVECOT_BINARY_LDFLAGS)
+test_mail_key_CFLAGS = $(AM_CPPFLAGS) $(DOVECOT_BINARY_CFLAGS) -Dtop_builddir=\"$(top_builddir)\"
+EXTRA_DIST = fs-crypt-common.c
+noinst_HEADERS = \
+ mail-crypt-plugin.h \
+ mail-crypt-common.h \
+ mail-crypt-global-key.h \
+ mail-crypt-key.h \
+ fs-crypt-settings.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/plugins/mail-crypt/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/mail-crypt/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-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_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)$(doveadm_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \
+ }
+
+uninstall-doveadm_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \
+ done
+
+clean-doveadm_moduleLTLIBRARIES:
+ -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES)
+ @list='$(doveadm_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}; \
+ }
+
+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}; \
+ }
+
+lib05_mail_crypt_acl_plugin.la: $(lib05_mail_crypt_acl_plugin_la_OBJECTS) $(lib05_mail_crypt_acl_plugin_la_DEPENDENCIES) $(EXTRA_lib05_mail_crypt_acl_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib05_mail_crypt_acl_plugin_la_LINK) -rpath $(moduledir) $(lib05_mail_crypt_acl_plugin_la_OBJECTS) $(lib05_mail_crypt_acl_plugin_la_LIBADD) $(LIBS)
+
+lib10_mail_crypt_plugin.la: $(lib10_mail_crypt_plugin_la_OBJECTS) $(lib10_mail_crypt_plugin_la_DEPENDENCIES) $(EXTRA_lib10_mail_crypt_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib10_mail_crypt_plugin_la_LINK) -rpath $(moduledir) $(lib10_mail_crypt_plugin_la_OBJECTS) $(lib10_mail_crypt_plugin_la_LIBADD) $(LIBS)
+
+libdoveadm_mail_crypt_plugin.la: $(libdoveadm_mail_crypt_plugin_la_OBJECTS) $(libdoveadm_mail_crypt_plugin_la_DEPENDENCIES) $(EXTRA_libdoveadm_mail_crypt_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdoveadm_mail_crypt_plugin_la_LINK) -rpath $(doveadm_moduledir) $(libdoveadm_mail_crypt_plugin_la_OBJECTS) $(libdoveadm_mail_crypt_plugin_la_LIBADD) $(LIBS)
+
+libfs_crypt.la: $(libfs_crypt_la_OBJECTS) $(libfs_crypt_la_DEPENDENCIES) $(EXTRA_libfs_crypt_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libfs_crypt_la_LINK) -rpath $(moduledir) $(libfs_crypt_la_OBJECTS) $(libfs_crypt_la_LIBADD) $(LIBS)
+
+libfs_mail_crypt.la: $(libfs_mail_crypt_la_OBJECTS) $(libfs_mail_crypt_la_DEPENDENCIES) $(EXTRA_libfs_mail_crypt_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libfs_mail_crypt_la_LINK) -rpath $(moduledir) $(libfs_mail_crypt_la_OBJECTS) $(libfs_mail_crypt_la_LIBADD) $(LIBS)
+
+test-mail-global-key$(EXEEXT): $(test_mail_global_key_OBJECTS) $(test_mail_global_key_DEPENDENCIES) $(EXTRA_test_mail_global_key_DEPENDENCIES)
+ @rm -f test-mail-global-key$(EXEEXT)
+ $(AM_V_CCLD)$(test_mail_global_key_LINK) $(test_mail_global_key_OBJECTS) $(test_mail_global_key_LDADD) $(LIBS)
+
+test-mail-key$(EXEEXT): $(test_mail_key_OBJECTS) $(test_mail_key_DEPENDENCIES) $(EXTRA_test_mail_key_DEPENDENCIES)
+ @rm -f test-mail-key$(EXEEXT)
+ $(AM_V_CCLD)$(test_mail_key_LINK) $(test_mail_key_OBJECTS) $(test_mail_key_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-mail-crypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-crypt-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-crypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-mail-crypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-acl-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-global-key.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-key.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-pluginenv.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-crypt-userenv.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_global_key-test-mail-global-key.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_key-mail-crypt-global-key.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_key-mail-crypt-key.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_key-mail-crypt-userenv.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_mail_key-test-mail-key.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_mail_global_key-test-mail-global-key.o: test-mail-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-test-mail-global-key.o -MD -MP -MF $(DEPDIR)/test_mail_global_key-test-mail-global-key.Tpo -c -o test_mail_global_key-test-mail-global-key.o `test -f 'test-mail-global-key.c' || echo '$(srcdir)/'`test-mail-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-test-mail-global-key.Tpo $(DEPDIR)/test_mail_global_key-test-mail-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mail-global-key.c' object='test_mail_global_key-test-mail-global-key.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-test-mail-global-key.o `test -f 'test-mail-global-key.c' || echo '$(srcdir)/'`test-mail-global-key.c
+
+test_mail_global_key-test-mail-global-key.obj: test-mail-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-test-mail-global-key.obj -MD -MP -MF $(DEPDIR)/test_mail_global_key-test-mail-global-key.Tpo -c -o test_mail_global_key-test-mail-global-key.obj `if test -f 'test-mail-global-key.c'; then $(CYGPATH_W) 'test-mail-global-key.c'; else $(CYGPATH_W) '$(srcdir)/test-mail-global-key.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-test-mail-global-key.Tpo $(DEPDIR)/test_mail_global_key-test-mail-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mail-global-key.c' object='test_mail_global_key-test-mail-global-key.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-test-mail-global-key.obj `if test -f 'test-mail-global-key.c'; then $(CYGPATH_W) 'test-mail-global-key.c'; else $(CYGPATH_W) '$(srcdir)/test-mail-global-key.c'; fi`
+
+test_mail_global_key-fs-crypt-settings.o: fs-crypt-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-fs-crypt-settings.o -MD -MP -MF $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Tpo -c -o test_mail_global_key-fs-crypt-settings.o `test -f 'fs-crypt-settings.c' || echo '$(srcdir)/'`fs-crypt-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Tpo $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fs-crypt-settings.c' object='test_mail_global_key-fs-crypt-settings.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-fs-crypt-settings.o `test -f 'fs-crypt-settings.c' || echo '$(srcdir)/'`fs-crypt-settings.c
+
+test_mail_global_key-fs-crypt-settings.obj: fs-crypt-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-fs-crypt-settings.obj -MD -MP -MF $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Tpo -c -o test_mail_global_key-fs-crypt-settings.obj `if test -f 'fs-crypt-settings.c'; then $(CYGPATH_W) 'fs-crypt-settings.c'; else $(CYGPATH_W) '$(srcdir)/fs-crypt-settings.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Tpo $(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fs-crypt-settings.c' object='test_mail_global_key-fs-crypt-settings.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-fs-crypt-settings.obj `if test -f 'fs-crypt-settings.c'; then $(CYGPATH_W) 'fs-crypt-settings.c'; else $(CYGPATH_W) '$(srcdir)/fs-crypt-settings.c'; fi`
+
+test_mail_global_key-mail-crypt-global-key.o: mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-mail-crypt-global-key.o -MD -MP -MF $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Tpo -c -o test_mail_global_key-mail-crypt-global-key.o `test -f 'mail-crypt-global-key.c' || echo '$(srcdir)/'`mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Tpo $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-global-key.c' object='test_mail_global_key-mail-crypt-global-key.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-mail-crypt-global-key.o `test -f 'mail-crypt-global-key.c' || echo '$(srcdir)/'`mail-crypt-global-key.c
+
+test_mail_global_key-mail-crypt-global-key.obj: mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_global_key_CFLAGS) $(CFLAGS) -MT test_mail_global_key-mail-crypt-global-key.obj -MD -MP -MF $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Tpo -c -o test_mail_global_key-mail-crypt-global-key.obj `if test -f 'mail-crypt-global-key.c'; then $(CYGPATH_W) 'mail-crypt-global-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-global-key.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Tpo $(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-global-key.c' object='test_mail_global_key-mail-crypt-global-key.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_mail_global_key_CFLAGS) $(CFLAGS) -c -o test_mail_global_key-mail-crypt-global-key.obj `if test -f 'mail-crypt-global-key.c'; then $(CYGPATH_W) 'mail-crypt-global-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-global-key.c'; fi`
+
+test_mail_key-test-mail-key.o: test-mail-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-test-mail-key.o -MD -MP -MF $(DEPDIR)/test_mail_key-test-mail-key.Tpo -c -o test_mail_key-test-mail-key.o `test -f 'test-mail-key.c' || echo '$(srcdir)/'`test-mail-key.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-test-mail-key.Tpo $(DEPDIR)/test_mail_key-test-mail-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mail-key.c' object='test_mail_key-test-mail-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-test-mail-key.o `test -f 'test-mail-key.c' || echo '$(srcdir)/'`test-mail-key.c
+
+test_mail_key-test-mail-key.obj: test-mail-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-test-mail-key.obj -MD -MP -MF $(DEPDIR)/test_mail_key-test-mail-key.Tpo -c -o test_mail_key-test-mail-key.obj `if test -f 'test-mail-key.c'; then $(CYGPATH_W) 'test-mail-key.c'; else $(CYGPATH_W) '$(srcdir)/test-mail-key.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-test-mail-key.Tpo $(DEPDIR)/test_mail_key-test-mail-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mail-key.c' object='test_mail_key-test-mail-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-test-mail-key.obj `if test -f 'test-mail-key.c'; then $(CYGPATH_W) 'test-mail-key.c'; else $(CYGPATH_W) '$(srcdir)/test-mail-key.c'; fi`
+
+test_mail_key-mail-crypt-key.o: mail-crypt-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-key.o -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-key.Tpo -c -o test_mail_key-mail-crypt-key.o `test -f 'mail-crypt-key.c' || echo '$(srcdir)/'`mail-crypt-key.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-key.Tpo $(DEPDIR)/test_mail_key-mail-crypt-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-key.c' object='test_mail_key-mail-crypt-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-key.o `test -f 'mail-crypt-key.c' || echo '$(srcdir)/'`mail-crypt-key.c
+
+test_mail_key-mail-crypt-key.obj: mail-crypt-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-key.obj -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-key.Tpo -c -o test_mail_key-mail-crypt-key.obj `if test -f 'mail-crypt-key.c'; then $(CYGPATH_W) 'mail-crypt-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-key.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-key.Tpo $(DEPDIR)/test_mail_key-mail-crypt-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-key.c' object='test_mail_key-mail-crypt-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-key.obj `if test -f 'mail-crypt-key.c'; then $(CYGPATH_W) 'mail-crypt-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-key.c'; fi`
+
+test_mail_key-mail-crypt-global-key.o: mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-global-key.o -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-global-key.Tpo -c -o test_mail_key-mail-crypt-global-key.o `test -f 'mail-crypt-global-key.c' || echo '$(srcdir)/'`mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-global-key.Tpo $(DEPDIR)/test_mail_key-mail-crypt-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-global-key.c' object='test_mail_key-mail-crypt-global-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-global-key.o `test -f 'mail-crypt-global-key.c' || echo '$(srcdir)/'`mail-crypt-global-key.c
+
+test_mail_key-mail-crypt-global-key.obj: mail-crypt-global-key.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-global-key.obj -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-global-key.Tpo -c -o test_mail_key-mail-crypt-global-key.obj `if test -f 'mail-crypt-global-key.c'; then $(CYGPATH_W) 'mail-crypt-global-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-global-key.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-global-key.Tpo $(DEPDIR)/test_mail_key-mail-crypt-global-key.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-global-key.c' object='test_mail_key-mail-crypt-global-key.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-global-key.obj `if test -f 'mail-crypt-global-key.c'; then $(CYGPATH_W) 'mail-crypt-global-key.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-global-key.c'; fi`
+
+test_mail_key-mail-crypt-userenv.o: mail-crypt-userenv.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-userenv.o -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-userenv.Tpo -c -o test_mail_key-mail-crypt-userenv.o `test -f 'mail-crypt-userenv.c' || echo '$(srcdir)/'`mail-crypt-userenv.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-userenv.Tpo $(DEPDIR)/test_mail_key-mail-crypt-userenv.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-userenv.c' object='test_mail_key-mail-crypt-userenv.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-userenv.o `test -f 'mail-crypt-userenv.c' || echo '$(srcdir)/'`mail-crypt-userenv.c
+
+test_mail_key-mail-crypt-userenv.obj: mail-crypt-userenv.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_mail_key_CFLAGS) $(CFLAGS) -MT test_mail_key-mail-crypt-userenv.obj -MD -MP -MF $(DEPDIR)/test_mail_key-mail-crypt-userenv.Tpo -c -o test_mail_key-mail-crypt-userenv.obj `if test -f 'mail-crypt-userenv.c'; then $(CYGPATH_W) 'mail-crypt-userenv.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-userenv.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_mail_key-mail-crypt-userenv.Tpo $(DEPDIR)/test_mail_key-mail-crypt-userenv.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-crypt-userenv.c' object='test_mail_key-mail-crypt-userenv.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_mail_key_CFLAGS) $(CFLAGS) -c -o test_mail_key-mail-crypt-userenv.obj `if test -f 'mail-crypt-userenv.c'; then $(CYGPATH_W) 'mail-crypt-userenv.c'; else $(CYGPATH_W) '$(srcdir)/mail-crypt-userenv.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)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)"; 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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/doveadm-mail-crypt.Plo
+ -rm -f ./$(DEPDIR)/fs-crypt-settings.Plo
+ -rm -f ./$(DEPDIR)/fs-crypt.Plo
+ -rm -f ./$(DEPDIR)/fs-mail-crypt.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-acl-plugin.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-global-key.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-key.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-plugin.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-pluginenv.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-userenv.Plo
+ -rm -f ./$(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po
+ -rm -f ./$(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_global_key-test-mail-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-userenv.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-test-mail-key.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-doveadm_moduleLTLIBRARIES \
+ install-moduleLTLIBRARIES
+
+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)/doveadm-mail-crypt.Plo
+ -rm -f ./$(DEPDIR)/fs-crypt-settings.Plo
+ -rm -f ./$(DEPDIR)/fs-crypt.Plo
+ -rm -f ./$(DEPDIR)/fs-mail-crypt.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-acl-plugin.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-global-key.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-key.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-plugin.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-pluginenv.Plo
+ -rm -f ./$(DEPDIR)/mail-crypt-userenv.Plo
+ -rm -f ./$(DEPDIR)/test_mail_global_key-fs-crypt-settings.Po
+ -rm -f ./$(DEPDIR)/test_mail_global_key-mail-crypt-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_global_key-test-mail-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-global-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-key.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-mail-crypt-userenv.Po
+ -rm -f ./$(DEPDIR)/test_mail_key-test-mail-key.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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-doveadm_moduleLTLIBRARIES \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ 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-doveadm_moduleLTLIBRARIES 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-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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(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/plugins/mail-crypt/doveadm-mail-crypt.c b/src/plugins/mail-crypt/doveadm-mail-crypt.c
new file mode 100644
index 0000000..a4322ed
--- /dev/null
+++ b/src/plugins/mail-crypt/doveadm-mail-crypt.c
@@ -0,0 +1,1048 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "askpass.h"
+#include "doveadm-mail.h"
+#include "getopt.h"
+#include "array.h"
+#include "str.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "ioloop-private.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "mailbox-attribute.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mailbox-list-iter.h"
+#include "doveadm-print.h"
+#include "hex-binary.h"
+
+#define DOVEADM_MCP_SUCCESS "\xE2\x9C\x93" /* emits a utf-8 CHECK MARK (U+2713) */
+#define DOVEADM_MCP_FAIL "x"
+#define DOVEADM_MCP_USERKEY "<userkey>"
+
+struct generated_key {
+ const char *name;
+ const char *id;
+ const char *error;
+ struct mailbox *box;
+ bool success:1;
+ bool active:1;
+};
+
+ARRAY_DEFINE_TYPE(generated_keys, struct generated_key);
+
+struct mcp_cmd_context {
+ struct doveadm_mail_cmd_context ctx;
+
+ const char *old_password;
+ const char *new_password;
+
+ unsigned int matched_keys;
+
+ bool userkey_only:1;
+ bool recrypt_box_keys:1;
+ bool force:1;
+ bool ask_old_password:1;
+ bool ask_new_password:1;
+ bool clear_password:1;
+};
+
+struct mcp_key_iter_ctx {
+ pool_t pool;
+ ARRAY_TYPE(generated_keys) keys;
+};
+
+void doveadm_mail_crypt_plugin_init(struct module *mod ATTR_UNUSED);
+void doveadm_mail_crypt_plugin_deinit(void);
+
+static int
+mcp_user_create(struct mail_user *user, const char *dest_username,
+ struct mail_user **dest_user_r,
+ struct mail_storage_service_user **dest_service_user_r,
+ const char **error_r)
+{
+ const struct mail_storage_service_input *old_input;
+ struct mail_storage_service_input input;
+ struct mail_storage_service_ctx *service_ctx;
+ struct ioloop_context *cur_ioloop_ctx;
+
+ int ret;
+
+ i_assert(user->_service_user != NULL);
+ service_ctx = mail_storage_service_user_get_service_ctx(user->_service_user);
+ old_input = mail_storage_service_user_get_input(user->_service_user);
+
+ if ((cur_ioloop_ctx = io_loop_get_current_context(current_ioloop)) != NULL)
+ io_loop_context_deactivate(cur_ioloop_ctx);
+
+ i_zero(&input);
+ input.module = old_input->module;
+ input.service = old_input->service;
+ input.username = dest_username;
+ input.session_id_prefix = user->session_id;
+ input.flags_override_add = MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
+
+ ret = mail_storage_service_lookup_next(service_ctx, &input,
+ dest_service_user_r,
+ dest_user_r, error_r);
+
+ if (ret == 0)
+ *error_r = "User not found";
+
+ return ret;
+}
+
+static int
+mcp_update_shared_key(struct mailbox_transaction_context *t,
+ struct mail_user *user, const char *target_uid,
+ struct dcrypt_private_key *key, const char **error_r)
+{
+ const char *error;
+ struct mail_user *dest_user;
+ struct mail_storage_service_user *dest_service_user;
+ struct ioloop_context *cur_ioloop_ctx;
+ struct dcrypt_public_key *pkey;
+ const char *dest_username;
+ int ret = 0;
+
+ bool disallow_insecure = mail_crypt_acl_secure_sharing_enabled(user);
+
+ ret = mcp_user_create(user, target_uid, &dest_user,
+ &dest_service_user, &error);
+
+ /* to make sure we get correct logging context */
+ if (ret > 0)
+ mail_storage_service_io_deactivate_user(dest_service_user);
+ mail_storage_service_io_activate_user(user->_service_user);
+
+ if (ret <= 0) {
+ i_error("Cannot initialize destination user %s: %s",
+ target_uid, error);
+ return ret;
+ } else {
+ i_assert(dest_user != NULL);
+ dest_username = dest_user->username;
+
+ /* get public key from target user */
+ if ((ret = mail_crypt_user_get_public_key(dest_user,
+ &pkey, error_r)) <= 0) {
+ if (ret == 0 && disallow_insecure) {
+ *error_r = t_strdup_printf("User %s has no active public key",
+ dest_user->username);
+ ret = -1;
+ } else if (ret == 0) {
+ /* perform insecure sharing */
+ dest_username = NULL;
+ pkey = NULL;
+ ret = 1;
+ }
+ }
+
+ if (ret == 1) {
+ ARRAY_TYPE(dcrypt_private_key) keys;
+ t_array_init(&keys, 1);
+ array_push_back(&keys, &key);
+ ret = mail_crypt_box_share_private_keys(t, pkey,
+ dest_username,
+ &keys, error_r);
+ }
+
+ }
+
+ /* logging context swap again */
+ mail_storage_service_io_deactivate_user(user->_service_user);
+ mail_storage_service_io_activate_user(dest_service_user);
+
+ mail_user_deinit(&dest_user);
+ mail_storage_service_user_unref(&dest_service_user);
+
+ if ((cur_ioloop_ctx = io_loop_get_current_context(current_ioloop)) != NULL)
+ io_loop_context_deactivate(cur_ioloop_ctx);
+
+ mail_storage_service_io_activate_user(user->_service_user);
+
+ return ret;
+}
+
+static int mcp_update_shared_keys(struct doveadm_mail_cmd_context *ctx,
+ struct mailbox *box, struct mail_user *user,
+ const char *pubid, struct dcrypt_private_key *key)
+{
+ const char *error;
+ int ret;
+
+ ARRAY_TYPE(const_string) ids;
+ t_array_init(&ids, 8);
+
+ /* figure out who needs the key */
+ if (mail_crypt_box_get_pvt_digests(box, pool_datastack_create(),
+ MAIL_ATTRIBUTE_TYPE_SHARED,
+ &ids, &error) < 0) {
+ i_error("mail_crypt_box_get_pvt_digests(%s, /shared) failed: %s",
+ mailbox_get_vname(box),
+ error);
+ return -1;
+ }
+
+ const char *id;
+ bool found = FALSE;
+ string_t *uid = t_str_new(64);
+
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box, ctx->transaction_flags, __func__);
+
+ ret = 0;
+
+ /* then perform sharing */
+ array_foreach_elem(&ids, id) {
+ if (strchr(id, '/') != NULL) {
+ str_truncate(uid, 0);
+ const char *hexuid = t_strcut(id, '/');
+ hex_to_binary(hexuid, uid);
+ if (mcp_update_shared_key(t, user, str_c(uid), key,
+ &error) < 0) {
+ i_error("mcp_update_shared_key(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ str_c(uid),
+ error);
+ ret = -1;
+ break;
+ }
+ } else if (!found) {
+ found = TRUE;
+ if (mail_crypt_box_set_shared_key(t, pubid, key,
+ NULL, NULL,
+ &error) < 0) {
+ i_error("mail_crypt_box_set_shared_key(%s) failed: %s",
+ mailbox_get_vname(box),
+ error);
+ ret = -1;
+ break;
+ }
+ }
+ }
+
+ if (ret < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if (mailbox_transaction_commit(&t) < 0) {
+ i_error("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ error);
+ ret = -1;
+ }
+
+ return ret;
+}
+
+static int mcp_keypair_generate(struct mcp_cmd_context *ctx,
+ struct dcrypt_public_key *user_key,
+ struct mailbox *box, struct dcrypt_keypair *pair_r,
+ const char **pubid_r, const char **error_r)
+{
+ struct dcrypt_keypair pair = {NULL, NULL};
+
+ int ret;
+
+ if ((ret = mail_crypt_box_get_public_key(box, &pair.pub, error_r)) < 0) {
+ ret = -1;
+ } else if (ret == 1 && !ctx->force) {
+ i_info("Folder key exists. Use -f to generate a new one");
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ const char *error;
+ if (!dcrypt_key_id_public(pair.pub,
+ MAIL_CRYPT_KEY_ID_ALGORITHM,
+ key_id, &error)) {
+ i_error("dcrypt_key_id_public() failed: %s",
+ error);
+ return -1;
+ }
+ *pubid_r = p_strdup(ctx->ctx.pool, binary_to_hex(key_id->data,
+ key_id->used));
+ *pair_r = pair;
+ return 1;
+ } else if (ret == 1 && ctx->recrypt_box_keys) {
+ /* do nothing, because force isn't being used *OR*
+ we are recrypting box keys and force refers to
+ user keypair.
+
+ FIXME: this could be less confusing altogether */
+ ret = 0;
+ } else {
+ if (mail_crypt_box_generate_keypair(box, &pair, user_key,
+ pubid_r, error_r) < 0) {
+ ret = -1;
+ } else {
+ *pubid_r = p_strdup(ctx->ctx.pool, *pubid_r);
+ *pair_r = pair;
+ return 1;
+ }
+ }
+
+ if (pair.pub != NULL)
+ dcrypt_key_unref_public(&pair.pub);
+ if (pair.priv != NULL)
+ dcrypt_key_unref_private(&pair.priv);
+
+ return ret;
+}
+
+static int mcp_keypair_generate_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user,
+ ARRAY_TYPE(generated_keys) *result)
+{
+ const char *error;
+ int ret;
+ struct dcrypt_public_key *user_key;
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+ const char *pubid;
+ bool user_key_generated = FALSE;
+ struct generated_key *res;
+
+ if ((ret = mail_crypt_user_get_public_key(user, &user_key,
+ &error)) <= 0) {
+ struct dcrypt_keypair pair;
+ if (ret < 0) {
+ i_error("mail_crypt_user_get_public_key(%s) failed: %s",
+ user->username,
+ error);
+ } else if (mail_crypt_user_generate_keypair(user, &pair,
+ &pubid, &error) < 0) {
+ ret = -1;
+ i_error("mail_crypt_user_generate_keypair(%s) failed: %s",
+ user->username,
+ error);
+ res = array_append_space(result);
+ res->name = "";
+ res->error = p_strdup(_ctx->pool, error);
+ res->success = FALSE;
+ } else {
+ res = array_append_space(result);
+ res->name = DOVEADM_MCP_USERKEY;
+ res->id = p_strdup(_ctx->pool, pubid);
+ res->success = TRUE;
+ /* don't do it again later on */
+ user_key_generated = TRUE;
+ ret = 1;
+ user_key = pair.pub;
+ dcrypt_key_unref_private(&pair.priv);
+ }
+ if (ret < 0) return ret;
+ ctx->matched_keys++;
+ }
+ if (ret == 1 && ctx->userkey_only && !user_key_generated) {
+ if (!ctx->force) {
+ i_info("userkey exists. Use -f to generate a new one");
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ if (!dcrypt_key_id_public(user_key,
+ MAIL_CRYPT_KEY_ID_ALGORITHM,
+ key_id, &error)) {
+ i_error("dcrypt_key_id_public() failed: %s",
+ error);
+ dcrypt_key_unref_public(&user_key);
+ return -1;
+ }
+ const char *hash = binary_to_hex(key_id->data,
+ key_id->used);
+ res = array_append_space(result);
+ res->name = DOVEADM_MCP_USERKEY;
+ res->id = p_strdup(_ctx->pool, hash);
+ res->success = TRUE;
+ ctx->matched_keys++;
+ dcrypt_key_unref_public(&user_key);
+ return 1;
+ }
+ struct dcrypt_keypair pair;
+ dcrypt_key_unref_public(&user_key);
+ /* regen user key */
+ res = array_append_space(result);
+ res->name = DOVEADM_MCP_USERKEY;
+ if (mail_crypt_user_generate_keypair(user, &pair, &pubid,
+ &error) < 0) {
+ res->success = FALSE;
+ res->error = p_strdup(_ctx->pool, error);
+ return -1;
+ }
+ res->success = TRUE;
+ res->id = p_strdup(_ctx->pool, pubid);
+ user_key = pair.pub;
+ dcrypt_key_unref_private(&pair.priv);
+ ctx->matched_keys++;
+ }
+
+ if (ctx->userkey_only) {
+ dcrypt_key_unref_public(&user_key);
+ return 0;
+ }
+
+ const char *const *patterns = (const char *const[]){ "*", NULL };
+
+ /* only re-encrypt all folder keys if wanted */
+ if (!ctx->recrypt_box_keys) {
+ patterns = ctx->ctx.args;
+ }
+
+ const struct mailbox_info *info;
+ struct mailbox_list_iterate_context *iter =
+ mailbox_list_iter_init_namespaces(user->namespaces,
+ patterns,
+ MAIL_NAMESPACE_TYPE_PRIVATE,
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags & MAILBOX_NOSELECT) != 0 ||
+ (info->flags & MAILBOX_NONEXISTENT) != 0) continue;
+ struct dcrypt_keypair pair;
+
+ struct mailbox *box =
+ mailbox_alloc(info->ns->list,
+ info->vname, 0);
+ if (mailbox_open(box) < 0) {
+ res = array_append_space(result);
+ res->name = p_strdup(_ctx->pool, info->vname);
+ res->success = FALSE;
+ res->error = p_strdup(_ctx->pool,
+ mailbox_get_last_internal_error(box, NULL));
+ } else if ((ret = mcp_keypair_generate(ctx, user_key, box,
+ &pair, &pubid,
+ &error)) < 0) {
+ res = array_append_space(result);
+ res->name = p_strdup(_ctx->pool, info->vname);
+ res->success = FALSE;
+ res->error = p_strdup(_ctx->pool, error);
+ } else if (ret == 0) {
+ /* nothing happened because key already existed and
+ force wasn't used, skip */
+ } else if (ret > 0) {
+ res = array_append_space(result);
+ res->name = p_strdup(_ctx->pool, info->vname);
+ res->success = TRUE;
+ res->id = pubid;
+ T_BEGIN {
+ mcp_update_shared_keys(&ctx->ctx, box, user,
+ pubid, pair.priv);
+ } T_END;
+ if (pair.pub != NULL)
+ dcrypt_key_unref_public(&pair.pub);
+ if (pair.priv != NULL)
+ dcrypt_key_unref_private(&pair.priv);
+ ctx->matched_keys++;
+ }
+ mailbox_free(&box);
+ }
+
+ (void)mailbox_list_iter_deinit(&iter);
+
+ dcrypt_key_unref_public(&user_key);
+ return 0;
+}
+
+static int cmd_mcp_keypair_generate_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+
+ int ret = 0;
+
+ ARRAY_TYPE(generated_keys) result;
+ p_array_init(&result, _ctx->pool, 8);
+
+ if (mcp_keypair_generate_run(_ctx, user, &result) < 0)
+ _ctx->exit_code = EX_DATAERR;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("success", " ", 0);
+ doveadm_print_header("box", "Folder", 0);
+ doveadm_print_header("pubid", "Public ID", 0);
+
+ const struct generated_key *res;
+
+ array_foreach(&result, res) {
+ if (res->success)
+ doveadm_print(DOVEADM_MCP_SUCCESS);
+ else {
+ _ctx->exit_code = EX_DATAERR;
+ ret = -1;
+ doveadm_print(DOVEADM_MCP_FAIL);
+ }
+ doveadm_print(res->name);
+ if (!res->success)
+ doveadm_print(t_strdup_printf("ERROR: %s", res->error));
+ else
+ doveadm_print(res->id);
+ }
+
+ if (ctx->matched_keys == 0)
+ i_warning("mailbox cryptokey generate: Nothing was matched. "
+ "Use -U or specify mask?");
+ return ret;
+}
+
+static void mcp_key_list(struct mcp_cmd_context *ctx,
+ struct mail_user *user,
+ void(*callback)(const struct generated_key *, void *),
+ void *context)
+{
+ const char *error;
+
+ /* we need to use the mailbox attribute API here, as we
+ are not necessarily able to decrypt any of these keys
+ */
+
+ ARRAY_TYPE(const_string) ids;
+ t_array_init(&ids, 8);
+
+ if (ctx->userkey_only) {
+ struct mailbox_attribute_iter *iter;
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box =
+ mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_READONLY);
+ struct mail_attribute_value value;
+ i_zero(&value);
+
+ if (mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value) < 0) {
+ i_error("mailbox_get_attribute(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+
+ iter = mailbox_attribute_iter_init(box,
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ USER_CRYPT_PREFIX
+ PRIVKEYS_PREFIX);
+ const char *key_id;
+ if (value.value == NULL)
+ value.value = "<NO ACTIVE KEY>";
+ while ((key_id = mailbox_attribute_iter_next(iter)) != NULL) {
+ struct generated_key key;
+ key.id = key_id;
+ key.active = strcmp(value.value, key_id) == 0;
+ key.name = "";
+ key.box = box;
+ callback(&key, context);
+ ctx->matched_keys++;
+ }
+ if (mailbox_attribute_iter_deinit(&iter) < 0)
+ i_error("mailbox_attribute_iter_deinit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ return;
+ }
+
+ const struct mailbox_info *info;
+ struct mailbox_list_iterate_context *iter =
+ mailbox_list_iter_init_namespaces(user->namespaces,
+ ctx->ctx.args,
+ MAIL_NAMESPACE_TYPE_PRIVATE,
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+
+ while((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags & MAILBOX_NOSELECT) != 0 ||
+ (info->flags & MAILBOX_NONEXISTENT) != 0) continue;
+
+ struct mailbox *box =
+ mailbox_alloc(info->ns->list,
+ info->vname, MAILBOX_FLAG_READONLY);
+ struct mail_attribute_value value;
+ i_zero(&value);
+ array_clear(&ids);
+
+ /* get active ID */
+ if (mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value) < 0) {
+ i_error("mailbox_get_attribute(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ } else if (mail_crypt_box_get_pvt_digests(box, pool_datastack_create(),
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ &ids, &error) < 0) {
+ i_error("mail_crypt_box_get_pvt_digests(%s) failed: %s",
+ mailbox_get_vname(box),
+ error);
+ } else {
+ const char *id;
+ const char *boxname = mailbox_get_vname(box);
+ if (value.value == NULL)
+ value.value = "<NO ACTIVE KEY>";
+ array_foreach_elem(&ids, id) {
+ struct generated_key key;
+ key.name = boxname;
+ key.id = id;
+ if (value.value != NULL)
+ key.active = strcmp(id, value.value) == 0;
+ else
+ key.active = FALSE;
+ key.box = box;
+ callback(&key, context);
+ ctx->matched_keys++;
+ }
+ }
+ mailbox_free(&box);
+ }
+
+ (void)mailbox_list_iter_deinit(&iter);
+}
+
+static void cmd_mcp_key_list_cb(const struct generated_key *_key, void *context)
+{
+ struct mcp_key_iter_ctx *ctx = context;
+ struct generated_key *key = array_append_space(&ctx->keys);
+ key->name = p_strdup(ctx->pool, _key->name);
+ key->id = p_strdup(ctx->pool, _key->id);
+ key->active = _key->active;
+}
+
+static int cmd_mcp_key_list_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+ struct mcp_key_iter_ctx iter_ctx;
+ i_zero(&iter_ctx);
+ iter_ctx.pool = _ctx->pool;
+ p_array_init(&iter_ctx.keys, _ctx->pool, 8);
+
+ mcp_key_list(ctx, user, cmd_mcp_key_list_cb, &iter_ctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ doveadm_print_header("box", "Folder", 0);
+ doveadm_print_header("active", "Active", 0);
+ doveadm_print_header("pubid", "Public ID", 0);
+
+ const struct generated_key *key;
+ array_foreach(&iter_ctx.keys, key) {
+ doveadm_print(key->name);
+ doveadm_print(key->active ? "yes" : "no");
+ doveadm_print(key->id);
+ }
+
+ if (ctx->matched_keys == 0)
+ i_warning("mailbox cryptokey list: Nothing was matched. "
+ "Use -U or specify mask?");
+
+ return 0;
+}
+
+static void cmd_mcp_key_export_cb(const struct generated_key *key,
+ void *context ATTR_UNUSED)
+{
+ struct dcrypt_private_key *pkey;
+ bool user_key = FALSE;
+ const char *error = NULL;
+ int ret;
+
+ if (*key->name == '\0')
+ user_key = TRUE;
+
+ doveadm_print(key->name);
+ doveadm_print(key->id);
+
+ if ((ret = mail_crypt_get_private_key(key->box, key->id, user_key, FALSE,
+ &pkey, &error)) <= 0) {
+ if (ret == 0)
+ error = "key not found";
+ doveadm_print(t_strdup_printf("ERROR: %s", error));
+ doveadm_print("");
+ } else {
+ string_t *out = t_str_new(64);
+ if (!dcrypt_key_store_private(pkey, DCRYPT_FORMAT_PEM, NULL, out,
+ NULL, NULL, &error)) {
+ doveadm_print(t_strdup_printf("ERROR: %s", error));
+ doveadm_print("");
+ } else {
+ /* this is to make it more compatible with openssl cli
+ as it expects BEGIN on it's own line */
+ doveadm_print(t_strdup_printf("\n%s", str_c(out)));
+ }
+ dcrypt_key_unref_private(&pkey);
+ }
+}
+
+static int cmd_mcp_key_export_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+ doveadm_print_header("box", "Folder", 0);
+ doveadm_print_header("name", "Public ID", 0);
+ doveadm_print_header("error", "Error", 0);
+ doveadm_print_header("key", "Key", 0);
+
+ mcp_key_list(ctx, user, cmd_mcp_key_export_cb, NULL);
+
+ return 0;
+}
+
+static int cmd_mcp_key_password_run(struct doveadm_mail_cmd_context *_ctx,
+ struct mail_user *user)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+ bool cli = (_ctx->cctx->conn_type == DOVEADM_CONNECTION_TYPE_CLI);
+
+ struct raw_key {
+ const char *attr;
+ const char *id;
+ const char *data;
+ };
+
+ ARRAY(struct raw_key) raw_keys;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+
+ doveadm_print_header_simple("result");
+
+ if (ctx->ask_old_password) {
+ if (ctx->old_password != NULL) {
+ doveadm_print("old password specified, cannot ask for it");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ if (!cli) {
+ doveadm_print("No cli - cannot ask for password");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ ctx->old_password =
+ p_strdup(_ctx->pool, t_askpass("Old password: "));
+ }
+
+ if (ctx->ask_new_password) {
+ if (ctx->new_password != NULL) {
+ doveadm_print("new password specified, cannot ask for it");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ if (!cli) {
+ doveadm_print("No cli - cannot ask for password");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ const char *passw;
+ passw = t_askpass("New password: ");
+ if (strcmp(passw, t_askpass("Confirm new password: ")) != 0) {
+ doveadm_print("Passwords don't match, aborting");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+ ctx->new_password = p_strdup(_ctx->pool, passw);
+ }
+
+ if (ctx->clear_password &&
+ (ctx->new_password != NULL ||
+ mail_user_plugin_getenv(user, MAIL_CRYPT_USERENV_PASSWORD) != NULL)) {
+ doveadm_print("clear password and new password specified");
+ _ctx->exit_code = EX_USAGE;
+ return -1;
+ }
+
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX", 0);
+ if (mailbox_open(box) < 0) {
+ doveadm_print(t_strdup_printf("mailbox_open(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL)));
+ _ctx->exit_code = EX_TEMPFAIL;
+ return -1;
+ }
+
+ t_array_init(&raw_keys, 8);
+
+ /* then get the current user keys, all of them */
+ struct mailbox_attribute_iter *iter =
+ mailbox_attribute_iter_init(box,
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ USER_CRYPT_PREFIX
+ PRIVKEYS_PREFIX);
+ const char *error;
+ const char *key_id;
+ int ret = 1;
+ unsigned int count = 0;
+
+ while ((key_id = mailbox_attribute_iter_next(iter)) != NULL) {
+ const char *attr =
+ t_strdup_printf(USER_CRYPT_PREFIX PRIVKEYS_PREFIX "%s",
+ key_id);
+
+ struct mail_attribute_value value;
+ if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ attr, &value)) < 0) {
+ doveadm_print(t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
+ mailbox_get_vname(box), attr,
+ mailbox_get_last_internal_error(box, NULL)));
+ _ctx->exit_code = EX_TEMPFAIL;
+ break;
+ } else if (ret > 0) {
+ struct raw_key *raw_key = array_append_space(&raw_keys);
+ raw_key->attr = p_strdup(_ctx->pool, attr);
+ raw_key->id = p_strdup(_ctx->pool, key_id);
+ raw_key->data = p_strdup(_ctx->pool, value.value);
+ }
+ }
+
+ if (ret == 1) {
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box, _ctx->transaction_flags,
+ __func__);
+ struct dcrypt_private_key *key;
+ const struct raw_key *raw_key;
+ const char *algo = ctx->new_password != NULL ?
+ MAIL_CRYPT_PW_CIPHER :
+ NULL;
+ string_t *newkey = t_str_new(256);
+
+ array_foreach(&raw_keys, raw_key) {
+ struct mail_attribute_value value;
+
+ if (!dcrypt_key_load_private(&key, raw_key->data,
+ ctx->old_password, NULL,
+ &error)) {
+ doveadm_print(t_strdup_printf("dcrypt_key_load_private(%s) failed: %s",
+ raw_key->id,
+ error));
+ _ctx->exit_code = EX_DATAERR;
+ ret = -1;
+ break;
+ }
+
+ /* save it */
+ str_truncate(newkey, 0);
+
+ if (!dcrypt_key_store_private(key, DCRYPT_FORMAT_DOVECOT,
+ algo, newkey,
+ ctx->new_password,
+ NULL, &error)) {
+ doveadm_print(t_strdup_printf("dcrypt_key_store_private(%s) failed: %s",
+ raw_key->id,
+ error));
+ _ctx->exit_code = EX_DATAERR;
+ ret = -1;
+ }
+
+ dcrypt_key_unref_private(&key);
+ if (ret == -1) break;
+
+ i_zero(&value);
+ value.value = str_c(newkey);
+
+ /* and store it */
+ if (mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ raw_key->attr, &value) < 0) {
+ doveadm_print(t_strdup_printf("mailbox_attribute_set(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ raw_key->attr,
+ mailbox_get_last_internal_error(box, NULL)));
+ _ctx->exit_code = EX_TEMPFAIL;
+ ret = -1;
+ break;
+ }
+ count++;
+ }
+
+ if (ret < 1) {
+ mailbox_transaction_rollback(&t);
+ } else {
+ if (mailbox_transaction_commit(&t) < 0) {
+ doveadm_print(t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL)));
+ } else {
+ doveadm_print(t_strdup_printf("Changed password for %u key(s)",
+ count));
+ }
+ }
+ }
+
+ (void)mailbox_attribute_iter_deinit(&iter);
+ mailbox_free(&box);
+
+ return ret;
+}
+
+
+static bool cmd_mcp_keypair_generate_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'U':
+ ctx->userkey_only = TRUE;
+ break;
+ case 'R':
+ ctx->recrypt_box_keys = TRUE;
+ break;
+ case 'f':
+ ctx->force = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+
+}
+
+static bool cmd_mcp_key_password_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'N':
+ ctx->ask_new_password = TRUE;
+ break;
+ case 'O':
+ ctx->ask_old_password = TRUE;
+ break;
+ case 'C':
+ ctx->clear_password = TRUE;
+ break;
+ case 'o':
+ ctx->old_password = p_strdup(_ctx->pool, optarg);
+ break;
+ case 'n':
+ ctx->new_password = p_strdup(_ctx->pool, optarg);
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool cmd_mcp_key_parse_arg(struct doveadm_mail_cmd_context *_ctx, int c)
+{
+ struct mcp_cmd_context *ctx =
+ (struct mcp_cmd_context *)_ctx;
+
+ switch (c) {
+ case 'U':
+ ctx->userkey_only = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+
+}
+
+static struct doveadm_mail_cmd_context *cmd_mcp_keypair_generate_alloc(void)
+{
+ struct mcp_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct mcp_cmd_context);
+ ctx->ctx.getopt_args = "URf";
+ ctx->ctx.v.parse_arg = cmd_mcp_keypair_generate_parse_arg;
+ ctx->ctx.v.run = cmd_mcp_keypair_generate_run;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mcp_key_list_alloc(void)
+{
+ struct mcp_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct mcp_cmd_context);
+ ctx->ctx.getopt_args = "U";
+ ctx->ctx.v.parse_arg = cmd_mcp_key_parse_arg;
+ ctx->ctx.v.run = cmd_mcp_key_list_run;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mcp_key_export_alloc(void)
+{
+ struct mcp_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct mcp_cmd_context);
+ ctx->ctx.getopt_args = "U";
+ ctx->ctx.v.parse_arg = cmd_mcp_key_parse_arg;
+ ctx->ctx.v.run = cmd_mcp_key_export_run;
+ return &ctx->ctx;
+}
+
+static struct doveadm_mail_cmd_context *cmd_mcp_key_password_alloc(void)
+{
+ struct mcp_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct mcp_cmd_context);
+ ctx->ctx.getopt_args = "NOCo:n:";
+ ctx->ctx.v.parse_arg = cmd_mcp_key_password_parse_arg;
+ ctx->ctx.v.run = cmd_mcp_key_password_run;
+ return &ctx->ctx;
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_mcp_keypair_generate = {
+ .name = "mailbox cryptokey generate",
+ .mail_cmd = cmd_mcp_keypair_generate_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-URf] mailbox [ mailbox .. ]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('U', "user-key-only", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('R', "re-encrypt-box-keys", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('f', "force", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mcp_key_list = {
+ .name = "mailbox cryptokey list",
+ .mail_cmd = cmd_mcp_key_list_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "-U | mailbox [ mailbox .. ]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('U', "user-key", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mcp_key_export = {
+ .name = "mailbox cryptokey export",
+ .mail_cmd = cmd_mcp_key_export_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "-U | mailbox [ mailbox .. ]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('U', "user-key", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "mailbox", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+};
+
+struct doveadm_cmd_ver2 doveadm_cmd_mcp_key_password = {
+ .name = "mailbox cryptokey password",
+ .mail_cmd = cmd_mcp_key_password_alloc,
+ .usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "[-NOC] [-opassword] [-npassword]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAM('C', "clear-password", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('N', "ask-new-password", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('n', "new-password", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('O', "ask-old-password", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('o', "old-password", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAMS_END
+};
+
+void doveadm_mail_crypt_plugin_init(struct module *mod ATTR_UNUSED)
+{
+ doveadm_cmd_register_ver2(&doveadm_cmd_mcp_keypair_generate);
+ doveadm_cmd_register_ver2(&doveadm_cmd_mcp_key_list);
+ doveadm_cmd_register_ver2(&doveadm_cmd_mcp_key_export);
+ doveadm_cmd_register_ver2(&doveadm_cmd_mcp_key_password);
+}
+
+void doveadm_mail_crypt_plugin_deinit(void)
+{
+}
diff --git a/src/plugins/mail-crypt/fs-crypt-common.c b/src/plugins/mail-crypt/fs-crypt-common.c
new file mode 100644
index 0000000..7432fa6
--- /dev/null
+++ b/src/plugins/mail-crypt/fs-crypt-common.c
@@ -0,0 +1,368 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "randgen.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-decrypt.h"
+#include "ostream-encrypt.h"
+#include "iostream-temp.h"
+#include "mailbox-list.h"
+#include "mail-namespace.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "dcrypt-iostream.h"
+#include "fs-api-private.h"
+
+struct crypt_fs {
+ struct fs fs;
+ struct mail_crypt_global_keys keys;
+ bool keys_loaded;
+
+ char *enc_algo;
+ char *set_prefix;
+ char *public_key_path;
+ char *private_key_path;
+ char *password;
+};
+
+struct crypt_fs_file {
+ struct fs_file file;
+ struct crypt_fs *fs;
+ struct fs_file *super_read;
+ enum fs_open_mode open_mode;
+ struct istream *input;
+
+ struct ostream *super_output;
+ struct ostream *temp_output;
+};
+
+#define CRYPT_FS(ptr) container_of((ptr), struct crypt_fs, fs)
+#define CRYPT_FILE(ptr) container_of((ptr), struct crypt_fs_file, file)
+
+/* defined outside this file */
+extern const struct fs FS_CLASS_CRYPT;
+
+static
+int fs_crypt_load_keys(struct crypt_fs *fs, const char **error_r);
+
+static struct fs *fs_crypt_alloc(void)
+{
+ struct crypt_fs *fs;
+
+ fs = i_new(struct crypt_fs, 1);
+ fs->fs = FS_CLASS_CRYPT;
+
+ return &fs->fs;
+}
+
+static int
+fs_crypt_init(struct fs *_fs, const char *args, const struct fs_settings *set,
+ const char **error_r)
+{
+ struct crypt_fs *fs = CRYPT_FS(_fs);
+ const char *enc_algo, *set_prefix;
+ const char *p, *arg, *value, *error, *parent_name, *parent_args;
+ const char *public_key_path = "", *private_key_path = "", *password = "";
+
+ if (!dcrypt_initialize("openssl", NULL, &error))
+ i_fatal("dcrypt_initialize(): %s", error);
+
+ /* [algo=<s>:][set_prefix=<n>:][public_key_path=<s>:]
+ [private_key_path=<s>:[password=<s>:]]<parent fs> */
+ set_prefix = "mail_crypt_global";
+ enc_algo = "aes-256-gcm-sha256";
+ for (;;) {
+ p = strchr(args, ':');
+ if (p == NULL) {
+ *error_r = "Missing parameters";
+ return -1;
+ }
+ arg = t_strdup_until(args, p);
+ if ((value = strchr(arg, '=')) == NULL)
+ break;
+ arg = t_strdup_until(arg, value++);
+ args = p+1;
+
+ if (strcmp(arg, "algo") == 0)
+ enc_algo = value;
+ else if (strcmp(arg, "set_prefix") == 0)
+ set_prefix = value;
+ else if (strcmp(arg, "public_key_path") == 0)
+ public_key_path = value;
+ else if (strcmp(arg, "private_key_path") == 0)
+ private_key_path = value;
+ else if (strcmp(arg, "password") == 0)
+ password = value;
+ else {
+ *error_r = t_strdup_printf(
+ "Invalid parameter '%s'", arg);
+ 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;
+ fs->enc_algo = i_strdup(enc_algo);
+ fs->set_prefix = i_strdup(set_prefix);
+ fs->public_key_path = i_strdup_empty(public_key_path);
+ fs->private_key_path = i_strdup_empty(private_key_path);
+ fs->password = i_strdup_empty(password);
+ return 0;
+}
+
+static void fs_crypt_free(struct fs *_fs)
+{
+ struct crypt_fs *fs = CRYPT_FS(_fs);
+
+ mail_crypt_global_keys_free(&fs->keys);
+ i_free(fs->enc_algo);
+ i_free(fs->set_prefix);
+ i_free(fs->public_key_path);
+ i_free(fs->private_key_path);
+ i_free(fs->password);
+ i_free(fs);
+}
+
+static struct fs_file *fs_crypt_file_alloc(void)
+{
+ struct crypt_fs_file *file = i_new(struct crypt_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_crypt_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct crypt_fs *fs = CRYPT_FS(_file->fs);
+ struct crypt_fs_file *file = CRYPT_FILE(_file);
+
+ 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 (mode == FS_OPEN_MODE_READONLY &&
+ (flags & FS_OPEN_FLAG_ASYNC) == 0) {
+ /* use async stream for super, 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;
+ }
+}
+
+static void fs_crypt_file_deinit(struct fs_file *_file)
+{
+ struct crypt_fs_file *file = CRYPT_FILE(_file);
+
+ if (file->super_read != _file->parent)
+ fs_file_deinit(&file->super_read);
+ fs_file_free(_file);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_crypt_file_close(struct fs_file *_file)
+{
+ struct crypt_fs_file *file = CRYPT_FILE(_file);
+
+ i_stream_unref(&file->input);
+ fs_file_close(file->super_read);
+ fs_file_close(_file->parent);
+}
+
+static int fs_crypt_read_file(const char *set_name, const char *path,
+ char **key_data_r, const char **error_r)
+{
+ struct istream *input;
+ int ret;
+
+ input = i_stream_create_file(path, SIZE_MAX);
+ while (i_stream_read(input) > 0) ;
+ if (input->stream_errno != 0) {
+ *error_r = t_strdup_printf("%s: read(%s) failed: %s",
+ set_name, path, i_stream_get_error(input));
+ ret = -1;
+ } else {
+ size_t size;
+ const unsigned char *data = i_stream_get_data(input, &size);
+ *key_data_r = i_strndup(data, size);
+ ret = 0;
+ }
+ i_stream_unref(&input);
+ return ret;
+}
+
+static int
+fs_crypt_load_keys_from_path(struct crypt_fs *fs, const char **error_r)
+{
+ char *key_data;
+
+ mail_crypt_global_keys_init(&fs->keys);
+ if (fs->public_key_path != NULL) {
+ if (fs_crypt_read_file("crypt:public_key_path",
+ fs->public_key_path,
+ &key_data, error_r) < 0)
+ return -1;
+ if (mail_crypt_load_global_public_key("crypt:public_key_path",
+ key_data, &fs->keys,
+ error_r) < 0) {
+ i_free(key_data);
+ return -1;
+ }
+ i_free(key_data);
+ }
+ if (fs->private_key_path != NULL) {
+ if (fs_crypt_read_file("crypt:private_key_path",
+ fs->private_key_path,
+ &key_data, error_r) < 0)
+ return -1;
+ if (mail_crypt_load_global_private_key("crypt:private_key_path",
+ key_data, "crypt:password",
+ fs->password, &fs->keys,
+ error_r) < 0) {
+ i_free(key_data);
+ return -1;
+ }
+ i_free(key_data);
+ }
+ return 0;
+}
+
+static int
+fs_crypt_istream_get_key(const char *pubkey_digest,
+ struct dcrypt_private_key **priv_key_r,
+ const char **error_r, void *context)
+{
+ struct crypt_fs_file *file = context;
+
+ if (fs_crypt_load_keys(file->fs, error_r) < 0)
+ return -1;
+
+ *priv_key_r = mail_crypt_global_key_find(&file->fs->keys, pubkey_digest);
+ if (*priv_key_r == NULL)
+ return 0;
+ dcrypt_key_ref_private(*priv_key_r);
+ return 1;
+}
+
+static struct istream *
+fs_crypt_read_stream(struct fs_file *_file, size_t max_buffer_size)
+{
+ struct crypt_fs_file *file = CRYPT_FILE(_file);
+ struct istream *input;
+
+ 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_decrypt_callback(input,
+ fs_crypt_istream_get_key, file);
+ i_stream_unref(&input);
+ i_stream_ref(file->input);
+ return file->input;
+}
+
+static void fs_crypt_write_stream(struct fs_file *_file)
+{
+ struct crypt_fs_file *file = CRYPT_FILE(_file);
+ const char *error;
+
+ i_assert(_file->output == NULL);
+
+ if (fs_crypt_load_keys(file->fs, &error) < 0) {
+ _file->output = o_stream_create_error_str(EIO,
+ "Couldn't read settings: %s", error);
+ return;
+ }
+
+ if (file->fs->keys.public_key == NULL) {
+ if (_file->fs->set.debug)
+ i_debug("No public key provided, "
+ "NOT encrypting stream %s",
+ fs_file_path(_file));
+ file->super_output = fs_write_stream(_file->parent);
+ _file->output = file->super_output;
+ return;
+ }
+
+ enum io_stream_encrypt_flags flags;
+ if (strstr(file->fs->enc_algo, "gcm") != NULL ||
+ strstr(file->fs->enc_algo, "ccm") != NULL) {
+ flags = IO_STREAM_ENC_INTEGRITY_AEAD;
+ } else {
+ flags = IO_STREAM_ENC_INTEGRITY_HMAC;
+ }
+
+ 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_encrypt(file->temp_output,
+ file->fs->enc_algo, file->fs->keys.public_key,
+ flags);
+}
+
+static int fs_crypt_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct crypt_fs_file *file = CRYPT_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 encryption */
+ i_assert(file->temp_output == NULL);
+ fs_write_stream_abort_error(_file->parent, &file->super_output,
+ "write(%s) failed: %s",
+ o_stream_get_name(file->super_output),
+ o_stream_get_error(file->super_output));
+ } else {
+ o_stream_destroy(&file->temp_output);
+ }
+ return -1;
+ }
+
+ if (file->super_output != NULL) {
+ /* no encrypt */
+ 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 */
+ input = iostream_temp_finish(&file->temp_output, IO_BLOCK_SIZE);
+ file->super_output = fs_write_stream(_file->parent);
+ o_stream_nsend_istream(file->super_output, input);
+ ret = fs_write_stream_finish(_file->parent, &file->super_output);
+ i_stream_unref(&input);
+ return ret;
+}
diff --git a/src/plugins/mail-crypt/fs-crypt-settings.c b/src/plugins/mail-crypt/fs-crypt-settings.c
new file mode 100644
index 0000000..ba70f8a
--- /dev/null
+++ b/src/plugins/mail-crypt/fs-crypt-settings.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "fs-crypt-settings.h"
+
+static const struct setting_define fs_crypt_setting_defines[] = {
+ { .type = SET_STRLIST, .key = "plugin",
+ .offset = offsetof(struct fs_crypt_settings, plugin_envs) },
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct fs_crypt_settings fs_crypt_default_settings = {
+ .plugin_envs = ARRAY_INIT
+};
+
+static const struct setting_parser_info *fs_crypt_setting_dependencies[] = {
+ NULL
+};
+
+const struct setting_parser_info fs_crypt_setting_parser_info = {
+ .module_name = "fs-crypt",
+ .defines = fs_crypt_setting_defines,
+ .defaults = &fs_crypt_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct fs_crypt_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = fs_crypt_setting_dependencies
+};
diff --git a/src/plugins/mail-crypt/fs-crypt-settings.h b/src/plugins/mail-crypt/fs-crypt-settings.h
new file mode 100644
index 0000000..df1a7b1
--- /dev/null
+++ b/src/plugins/mail-crypt/fs-crypt-settings.h
@@ -0,0 +1,11 @@
+#ifndef FS_CRYPT_SETTINGS_H
+#define FS_CRYPT_SETTINGS_H
+
+struct fs_crypt_settings {
+ ARRAY(const char *) plugin_envs;
+};
+
+extern const struct setting_parser_info fs_crypt_setting_parser_info;
+
+#endif
+
diff --git a/src/plugins/mail-crypt/fs-crypt.c b/src/plugins/mail-crypt/fs-crypt.c
new file mode 100644
index 0000000..0a81c69
--- /dev/null
+++ b/src/plugins/mail-crypt/fs-crypt.c
@@ -0,0 +1,65 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+#define FS_CLASS_CRYPT fs_class_crypt
+#include "fs-crypt-common.c"
+
+static
+int fs_crypt_load_keys(struct crypt_fs *fs, const char **error_r)
+{
+ const char *error;
+
+ if (fs->keys_loaded)
+ return 0;
+ if (fs->public_key_path != NULL || fs->private_key_path != NULL) {
+ /* overrides using settings */
+ if (fs_crypt_load_keys_from_path(fs, error_r) < 0)
+ return -1;
+ fs->keys_loaded = TRUE;
+ return 0;
+ }
+ if (mail_crypt_global_keys_load_pluginenv(fs->set_prefix, &fs->keys,
+ &error) < 0) {
+ *error_r = t_strdup_printf("%s: %s", fs->set_prefix, error);
+ return -1;
+ }
+ fs->keys_loaded = TRUE;
+ return 0;
+}
+
+const struct fs fs_class_crypt = {
+ .name = "crypt",
+ .v = {
+ fs_crypt_alloc,
+ fs_crypt_init,
+ NULL,
+ fs_crypt_free,
+ fs_wrapper_get_properties,
+ fs_crypt_file_alloc,
+ fs_crypt_file_init,
+ fs_crypt_file_deinit,
+ fs_crypt_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_read_via_stream,
+ fs_crypt_read_stream,
+ fs_write_via_stream,
+ fs_crypt_write_stream,
+ fs_crypt_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_wrapper_stat,
+ fs_wrapper_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/plugins/mail-crypt/fs-mail-crypt.c b/src/plugins/mail-crypt/fs-mail-crypt.c
new file mode 100644
index 0000000..b017d3f
--- /dev/null
+++ b/src/plugins/mail-crypt/fs-mail-crypt.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+#define FS_CLASS_CRYPT fs_class_mail_crypt
+#include "fs-crypt-common.c"
+
+static
+int fs_crypt_load_keys(struct crypt_fs *fs, const char **error_r)
+{
+ struct mailbox_list *list = mailbox_list_fs_get_list(&fs->fs);
+ const char *error;
+
+ if (fs->keys_loaded)
+ return 0;
+ if (fs->public_key_path != NULL || fs->private_key_path != NULL) {
+ /* overrides using settings */
+ if (fs_crypt_load_keys_from_path(fs, error_r) < 0)
+ return -1;
+ fs->keys_loaded = TRUE;
+ return 0;
+ }
+ if (list == NULL) {
+ *error_r = "fs-mail-crypt can be used only via lib-storage";
+ return -1;
+ }
+
+ if (mail_crypt_global_keys_load(mailbox_list_get_namespace(list)->user,
+ fs->set_prefix, &fs->keys, FALSE,
+ &error) < 0) {
+ *error_r = t_strdup_printf("%s: %s", fs->set_prefix, error);
+ return -1;
+ }
+ fs->keys_loaded = TRUE;
+ return 0;
+}
+
+const struct fs fs_class_mail_crypt = {
+ .name = "mail-crypt",
+ .v = {
+ fs_crypt_alloc,
+ fs_crypt_init,
+ NULL,
+ fs_crypt_free,
+ fs_wrapper_get_properties,
+ fs_crypt_file_alloc,
+ fs_crypt_file_init,
+ fs_crypt_file_deinit,
+ fs_crypt_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_read_via_stream,
+ fs_crypt_read_stream,
+ fs_write_via_stream,
+ fs_crypt_write_stream,
+ fs_crypt_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_wrapper_stat,
+ fs_wrapper_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/plugins/mail-crypt/mail-crypt-acl-plugin.c b/src/plugins/mail-crypt/mail-crypt-acl-plugin.c
new file mode 100644
index 0000000..71ab1e7
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-acl-plugin.c
@@ -0,0 +1,431 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop-private.h"
+#include "str.h"
+#include "sha2.h"
+#include "module-dir.h"
+#include "var-expand.h"
+#include "hex-binary.h"
+#include "mail-namespace.h"
+#include "mail-storage-hooks.h"
+#include "mail-storage-service.h"
+#include "acl-plugin.h"
+#include "acl-api-private.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mail-crypt-plugin.h"
+
+#define MAIL_CRYPT_ACL_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_crypt_acl_mailbox_list_module)
+
+struct mail_crypt_acl_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+ struct acl_backend_vfuncs acl_vprev;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_acl_mailbox_list_module,
+ &mailbox_list_module_register);
+
+void mail_crypt_acl_plugin_init(struct module *module);
+void mail_crypt_acl_plugin_deinit(void);
+
+static int
+mail_crypt_acl_has_user_read_right(struct acl_object *aclobj,
+ const char *username,
+ const char **error_r)
+{
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ int ret = 0;
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (rights.id_type == ACL_ID_USER &&
+ strcmp(rights.identifier, username) == 0) {
+ ret = str_array_find(rights.rights, MAIL_ACL_READ) ? 1 : 0;
+ break;
+ }
+ }
+ if (acl_object_list_deinit(&iter) < 0) {
+ *error_r = "Failed to iterate ACL objects";
+ return -1;
+ }
+
+ return ret;
+}
+
+static int mail_crypt_acl_has_nonuser_read_right(struct acl_object *aclobj,
+ const char **error_r)
+{
+ struct acl_object_list_iter *iter;
+ struct acl_rights rights;
+ int ret = 0;
+
+ iter = acl_object_list_init(aclobj);
+ while (acl_object_list_next(iter, &rights)) {
+ if (rights.id_type != ACL_ID_USER &&
+ rights.id_type != ACL_ID_OWNER &&
+ rights.rights != NULL &&
+ str_array_find(rights.rights, MAIL_ACL_READ)) {
+ ret = 1;
+ break;
+ }
+ }
+ if (acl_object_list_deinit(&iter) < 0) {
+ *error_r = "Failed to iterate ACL objects";
+ return -1;
+ }
+ return ret;
+}
+
+static int
+mail_crypt_acl_unset_private_keys(struct mailbox *src_box,
+ const char *dest_user,
+ enum mail_attribute_type type,
+ const char **error_r)
+{
+ ARRAY_TYPE(const_string) digests;
+ const char *error;
+ int ret = 1;
+
+ if (mailbox_open(src_box) < 0) {
+ *error_r = t_strdup_printf("mail-crypt-acl-plugin: "
+ "mailbox_open(%s) failed: %s",
+ mailbox_get_vname(src_box),
+ mailbox_get_last_internal_error(src_box, NULL));
+ return -1;
+ }
+
+ t_array_init(&digests, 4);
+ if (mail_crypt_box_get_pvt_digests(src_box, pool_datastack_create(),
+ type, &digests, &error) < 0) {
+ *error_r = t_strdup_printf("mail-crypt-acl-plugin: "
+ "Failed to lookup public key digests: %s",
+ error);
+ mailbox_free(&src_box);
+ return -1;
+ }
+
+ struct mailbox_transaction_context *t;
+ t = mailbox_transaction_begin(src_box, 0, __func__);
+
+ const char *hash;
+ array_foreach_elem(&digests, hash) {
+ const char *ptr;
+ /* if the id contains username part, skip to key public id */
+ if ((ptr = strchr(hash, '/')) != NULL)
+ ptr++;
+ else
+ ptr = hash;
+ if ((ret = mail_crypt_box_unset_shared_key(t, ptr, dest_user,
+ error_r)) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (ret < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if (mailbox_transaction_commit(&t) < 0) {
+ *error_r = t_strdup_printf("mail-crypt-acl-plugin: "
+ "mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(src_box),
+ mailbox_get_last_internal_error(src_box, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mail_crypt_acl_user_create(struct mail_user *user, const char *dest_username,
+ struct mail_user **dest_user_r,
+ struct mail_storage_service_user **dest_service_user_r,
+ const char **error_r)
+{
+ const struct mail_storage_service_input *old_input;
+ struct mail_storage_service_input input;
+ struct mail_storage_service_ctx *service_ctx;
+ struct ioloop_context *cur_ioloop_ctx;
+
+ int ret;
+
+ i_assert(user->_service_user != NULL);
+ service_ctx = mail_storage_service_user_get_service_ctx(user->_service_user);
+ old_input = mail_storage_service_user_get_input(user->_service_user);
+
+ if ((cur_ioloop_ctx = io_loop_get_current_context(current_ioloop)) != NULL)
+ io_loop_context_deactivate(cur_ioloop_ctx);
+
+ i_zero(&input);
+ input.module = old_input->module;
+ input.service = old_input->service;
+ input.username = dest_username;
+ input.session_id_prefix = user->session_id;
+ input.flags_override_add = MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
+ input.flags_override_remove = MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES;
+
+ ret = mail_storage_service_lookup_next(service_ctx, &input,
+ dest_service_user_r,
+ dest_user_r, error_r);
+
+ return ret;
+}
+
+static int
+mail_crypt_acl_update_private_key(struct mailbox *src_box,
+ struct mail_user *dest_user, bool set,
+ bool disallow_insecure,
+ const char **error_r)
+{
+ struct dcrypt_public_key *key = NULL;
+ struct dcrypt_private_key *priv_key;
+ int ret = 0;
+
+ if (!set) {
+ return mail_crypt_acl_unset_private_keys(src_box,
+ dest_user->username,
+ MAIL_ATTRIBUTE_TYPE_SHARED,
+ error_r);
+ }
+
+ if (dest_user != NULL) {
+ /* get public key from target user */
+ if ((ret = mail_crypt_user_get_public_key(dest_user,
+ &key, error_r)) <= 0) {
+ if (ret == 0 && disallow_insecure) {
+ *error_r = t_strdup_printf("User %s has no active public key",
+ dest_user->username);
+ return -1;
+ } else if (ret < 0) {
+ return -1;
+ } else if (ret == 0) {
+ /* perform insecure sharing */
+ dest_user = NULL;
+ key = NULL;
+ }
+ }
+ }
+
+ ARRAY_TYPE(dcrypt_private_key) keys;
+ t_array_init(&keys, 8);
+
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(src_box, 0, __func__);
+
+ /* get private keys from box */
+ if (mail_crypt_box_get_private_keys(src_box, &keys, error_r) < 0 ||
+ mail_crypt_box_share_private_keys(t, key,
+ dest_user == NULL ? NULL :
+ dest_user->username,
+ &keys, error_r) < 0)
+ ret = -1;
+ if (key != NULL)
+ dcrypt_key_unref_public(&key);
+
+ if (ret >= 0) {
+ array_foreach_elem(&keys, priv_key)
+ dcrypt_key_unref_private(&priv_key);
+ }
+
+ if (mailbox_transaction_commit(&t) < 0) {
+ *error_r = mailbox_get_last_internal_error(src_box, NULL);
+ ret = -1;
+ }
+
+ return ret;
+}
+
+static int mail_crypt_acl_object_update(struct acl_object *aclobj,
+ const struct acl_rights_update *update)
+{
+ const char *error;
+ struct mail_crypt_acl_mailbox_list *mlist =
+ MAIL_CRYPT_ACL_LIST_CONTEXT(aclobj->backend->list);
+ const char *username;
+ struct mail_user *dest_user;
+ struct mail_storage_service_user *dest_service_user;
+ struct ioloop_context *cur_ioloop_ctx;
+ bool have_rights;
+ int ret = 0;
+
+ if (mlist->acl_vprev.object_update(aclobj, update) < 0)
+ return -1;
+
+ bool disallow_insecure =
+ mail_crypt_acl_secure_sharing_enabled(aclobj->backend->list->ns->user);
+
+ const char *box_name = mailbox_list_get_vname(aclobj->backend->list,
+ aclobj->name);
+ struct mailbox *box = mailbox_alloc(aclobj->backend->list, box_name, 0);
+
+ switch (update->rights.id_type) {
+ case ACL_ID_USER:
+ /* setting rights for specific user: we can encrypt the
+ mailbox key for the user. */
+ username = update->rights.identifier;
+ ret = mail_crypt_acl_has_user_read_right(aclobj, username, &error);
+
+ if (ret < 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "mail_crypt_acl_has_user_read_right(%s) failed: %s",
+ username,
+ error);
+ break;
+ }
+
+ have_rights = ret > 0;
+
+ ret = mail_crypt_acl_user_create(aclobj->backend->list->ns->user,
+ username, &dest_user,
+ &dest_service_user, &error);
+
+ /* to make sure we get correct logging context */
+ if (ret > 0)
+ mail_storage_service_io_deactivate_user(dest_service_user);
+ mail_storage_service_io_activate_user(
+ aclobj->backend->list->ns->user->_service_user
+ );
+
+ if (ret <= 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "Cannot initialize destination user %s: %s",
+ username, error);
+ break;
+ } else {
+ i_assert(dest_user != NULL);
+ if ((ret = mailbox_open(box)) < 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "mailbox_open(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ } else if ((ret = mail_crypt_acl_update_private_key(box, dest_user,
+ have_rights,
+ disallow_insecure,
+ &error)) < 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "acl_update_private_key(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ username,
+ error);
+ }
+ }
+
+ /* logging context swap again */
+ mail_storage_service_io_deactivate_user(
+ aclobj->backend->list->ns->user->_service_user
+ );
+ mail_storage_service_io_activate_user(dest_service_user);
+
+ mail_user_deinit(&dest_user);
+ mail_storage_service_user_unref(&dest_service_user);
+
+ if ((cur_ioloop_ctx = io_loop_get_current_context(current_ioloop)) != NULL)
+ io_loop_context_deactivate(cur_ioloop_ctx);
+ mail_storage_service_io_activate_user(
+ aclobj->backend->list->ns->user->_service_user
+ );
+ break;
+ case ACL_ID_OWNER:
+ /* we should be the one doing this? ignore */
+ break;
+ case ACL_ID_ANYONE:
+ case ACL_ID_AUTHENTICATED:
+ case ACL_ID_GROUP:
+ case ACL_ID_GROUP_OVERRIDE:
+ if (disallow_insecure) {
+ i_error("mail-crypt-acl-plugin: "
+ "Secure key sharing is enabled -"
+ "Remove or set plugin { %s = no }",
+ MAIL_CRYPT_ACL_SECURE_SHARE_SETTING);
+ ret = -1;
+ break;
+ }
+ /* the mailbox key needs to be stored unencrypted. for groups
+ we could in theory use per-group encrypted keys, which the
+ users belonging to the group would able to decrypt with
+ their private key, but that becomes quite complicated. */
+ if ((ret = mail_crypt_acl_has_nonuser_read_right(aclobj, &error)) < 0) {
+ i_error("mail-crypt-acl-plugin: %s", error);
+ } else if ((ret = mailbox_open(box)) < 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "mailbox_open(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ } else if ((ret = mail_crypt_acl_update_private_key(box,
+ NULL,
+ TRUE,
+ disallow_insecure,
+ &error)) < 0) {
+ i_error("mail-crypt-acl-plugin: "
+ "acl_update_private_key(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ "",
+ error);
+ }
+ break;
+ case ACL_ID_TYPE_COUNT:
+ i_unreached();
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+static void
+mail_crypt_acl_mail_namespace_storage_added(struct mail_namespace *ns)
+{
+ struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(ns->list);
+ struct mail_crypt_acl_mailbox_list *mlist =
+ MAIL_CRYPT_ACL_LIST_CONTEXT(ns->list);
+ struct acl_backend *backend;
+
+ if (alist == NULL)
+ return;
+
+ /* FIXME: this method works only if there's a single plugin doing it.
+ if there are ever multiple plugins hooking into ACL commands the
+ ACL core code would need some changing to make it work correctly. */
+ backend = alist->rights.backend;
+ mlist->acl_vprev = backend->v;
+ backend->v.object_update = mail_crypt_acl_object_update;
+}
+
+static void mail_crypt_acl_mailbox_list_deinit(struct mailbox_list *list)
+{
+ struct mail_crypt_acl_mailbox_list *mlist =
+ MAIL_CRYPT_ACL_LIST_CONTEXT(list);
+
+ mlist->module_ctx.super.deinit(list);
+}
+
+static void mail_crypt_acl_mailbox_list_created(struct mailbox_list *list)
+{
+ struct mailbox_list_vfuncs *v = list->vlast;
+ struct mail_crypt_acl_mailbox_list *mlist;
+
+ mlist = p_new(list->pool, struct mail_crypt_acl_mailbox_list, 1);
+ mlist->module_ctx.super = *v;
+ list->vlast = &mlist->module_ctx.super;
+ v->deinit = mail_crypt_acl_mailbox_list_deinit;
+
+ MODULE_CONTEXT_SET(list, mail_crypt_acl_mailbox_list_module, mlist);
+}
+
+static struct mail_storage_hooks mail_crypt_acl_mail_storage_hooks = {
+ .mailbox_list_created = mail_crypt_acl_mailbox_list_created,
+ .mail_namespace_storage_added = mail_crypt_acl_mail_namespace_storage_added
+};
+
+void mail_crypt_acl_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &mail_crypt_acl_mail_storage_hooks);
+}
+
+void mail_crypt_acl_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&mail_crypt_acl_mail_storage_hooks);
+}
+
+const char *mail_crypt_acl_plugin_dependencies[] = { "acl", NULL };
diff --git a/src/plugins/mail-crypt/mail-crypt-common.h b/src/plugins/mail-crypt/mail-crypt-common.h
new file mode 100644
index 0000000..57e5e2f
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-common.h
@@ -0,0 +1,30 @@
+#ifndef MAIL_CRYPT_COMMON_H
+#define MAIL_CRYPT_COMMON_H
+
+#include "dcrypt.h"
+
+#define MAIL_CRYPT_PW_CIPHER "aes-256-ctr"
+#define MAIL_CRYPT_KEY_CIPHER "ecdh-aes-256-ctr"
+#define MAIL_CRYPT_ENC_ALGORITHM "aes-256-gcm-sha256"
+#define MAIL_CRYPT_KEY_ID_ALGORITHM "sha256"
+#define MAIL_CRYPT_KEY_ATTRIBUTE_FORMAT DCRYPT_FORMAT_DOVECOT
+#define MAIL_CRYPT_ACL_SECURE_SHARE_SETTING "mail_crypt_acl_require_secure_key_sharing"
+#define MAIL_CRYPT_REQUIRE_ENCRYPTED_USER_KEY "mail_crypt_require_encrypted_user_key"
+#define MAIL_CRYPT_HASH_BUF_SIZE 128
+#define MAIL_CRYPT_KEY_BUF_SIZE 1024
+#define ACTIVE_KEY_NAME "active"
+#define PUBKEYS_PREFIX "pubkeys/"
+#define PRIVKEYS_PREFIX "privkeys/"
+
+#define BOX_CRYPT_PREFIX MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"crypt/"
+#define USER_CRYPT_PREFIX \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"crypt/"
+
+#define MAIL_CRYPT_USERENV_PASSWORD "mail_crypt_private_password"
+#define MAIL_CRYPT_USERENV_KEY "mail_crypt_private_key"
+#define MAIL_CRYPT_USERENV_CURVE "mail_crypt_curve"
+
+ARRAY_DEFINE_TYPE(dcrypt_private_key, struct dcrypt_private_key*);
+
+#endif
diff --git a/src/plugins/mail-crypt/mail-crypt-global-key.c b/src/plugins/mail-crypt/mail-crypt-global-key.c
new file mode 100644
index 0000000..01cb937
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-global-key.c
@@ -0,0 +1,172 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "base64.h"
+#include "mail-user.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mail-crypt-plugin.h"
+
+int mail_crypt_load_global_public_key(const char *set_key, const char *key_data,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r)
+{
+ const char *error;
+ enum dcrypt_key_format format;
+ enum dcrypt_key_kind kind;
+ if (!dcrypt_key_string_get_info(key_data, &format, NULL,
+ &kind, NULL, NULL, NULL, &error)) {
+ key_data = str_c(t_base64_decode_str(key_data));
+ if (!dcrypt_key_string_get_info(key_data, &format, NULL,
+ &kind, NULL, NULL, NULL, &error)) {
+ *error_r = t_strdup_printf("%s: Couldn't parse public key: %s",
+ set_key, error);
+ return -1;
+ }
+ }
+ if (kind != DCRYPT_KEY_KIND_PUBLIC) {
+ *error_r = t_strdup_printf("%s: key is not public", set_key);
+ return -1;
+ }
+ if (!dcrypt_key_load_public(&global_keys->public_key, key_data, &error)) {
+ *error_r = t_strdup_printf("%s: Couldn't load public key: %s",
+ set_key, error);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mail_crypt_key_get_ids(struct dcrypt_private_key *key,
+ const char **key_id_r, const char **key_id_old_r,
+ const char **error_r)
+{
+ const char *error;
+ buffer_t *key_id;
+
+ *key_id_r = NULL;
+ *key_id_old_r = NULL;
+
+ /* new key ID */
+ key_id = t_buffer_create(MAIL_CRYPT_HASH_BUF_SIZE);
+ if (!dcrypt_key_id_private(key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id, &error)) {
+ *error_r = t_strdup_printf("Failed to get private key ID: %s", error);
+ return -1;
+ }
+ *key_id_r = binary_to_hex(key_id->data, key_id->used);
+
+ buffer_set_used_size(key_id, 0);
+
+ /* old key ID */
+ if (dcrypt_key_type_private(key) != DCRYPT_KEY_EC)
+ return 0;
+
+ if (!dcrypt_key_id_private_old(key, key_id, &error)) {
+ *error_r = t_strdup_printf("Failed to get private key old ID: %s",
+ error);
+ return -1;
+ }
+ *key_id_old_r = binary_to_hex(key_id->data, key_id->used);
+ return 0;
+}
+
+int mail_crypt_load_global_private_key(const char *set_key, const char *key_data,
+ const char *set_pw, const char *key_password,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r)
+{
+ enum dcrypt_key_format format;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type enc_type;
+ const char *error;
+
+ if (!dcrypt_key_string_get_info(key_data, &format, NULL, &kind,
+ &enc_type, NULL, NULL, &error)) {
+ key_data = str_c(t_base64_decode_str(key_data));
+ if (!dcrypt_key_string_get_info(key_data, &format, NULL, &kind,
+ &enc_type, NULL, NULL, &error)) {
+ *error_r = t_strdup_printf("%s: Couldn't parse private"
+ " key: %s", set_key, error);
+ return -1;
+ }
+ }
+ if (kind != DCRYPT_KEY_KIND_PRIVATE) {
+ *error_r = t_strdup_printf("%s: key is not private", set_key);
+ return -1;
+ }
+
+ if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD) {
+ /* Fail here if password is not set since openssl will prompt
+ * for it otherwise */
+ if (key_password == NULL) {
+ *error_r = t_strdup_printf("%s: %s unset, no password to decrypt the key",
+ set_key, set_pw);
+ return -1;
+ }
+ }
+
+ struct dcrypt_private_key *key = NULL;
+ if (!dcrypt_key_load_private(&key, key_data, key_password, NULL, &error)) {
+ *error_r = t_strdup_printf("%s: Couldn't load private key: %s",
+ set_key, error);
+ return -1;
+ }
+
+ const char *key_id, *key_id_old;
+ if (mail_crypt_key_get_ids(key, &key_id, &key_id_old, error_r) < 0) {
+ dcrypt_key_unref_private(&key);
+ return -1;
+ }
+
+ struct mail_crypt_global_private_key *priv_key =
+ array_append_space(&global_keys->private_keys);
+ priv_key->key = key;
+ priv_key->key_id = i_strdup(key_id);
+ priv_key->key_id_old = i_strdup(key_id_old);
+ return 0;
+}
+
+void mail_crypt_global_keys_init(struct mail_crypt_global_keys *global_keys_r)
+{
+ i_zero(global_keys_r);
+ i_array_init(&global_keys_r->private_keys, 4);
+}
+
+void mail_crypt_global_keys_free(struct mail_crypt_global_keys *global_keys)
+{
+ struct mail_crypt_global_private_key *priv_key;
+
+ if (global_keys->public_key != NULL)
+ dcrypt_key_unref_public(&global_keys->public_key);
+
+ if (!array_is_created(&global_keys->private_keys))
+ return;
+ array_foreach_modifiable(&global_keys->private_keys, priv_key) {
+ dcrypt_key_unref_private(&priv_key->key);
+ i_free(priv_key->key_id);
+ i_free(priv_key->key_id_old);
+ }
+ array_free(&global_keys->private_keys);
+}
+
+struct dcrypt_private_key *
+mail_crypt_global_key_find(struct mail_crypt_global_keys *global_keys,
+ const char *pubkey_digest)
+{
+ const struct mail_crypt_global_private_key *priv_key;
+
+ if (!array_is_created(&global_keys->private_keys))
+ return NULL;
+
+ array_foreach(&global_keys->private_keys, priv_key) {
+ if (strcmp(priv_key->key_id, pubkey_digest) == 0)
+ return priv_key->key;
+ if (priv_key->key_id_old != NULL &&
+ strcmp(priv_key->key_id_old, pubkey_digest) == 0)
+ return priv_key->key;
+ }
+ return NULL;
+}
diff --git a/src/plugins/mail-crypt/mail-crypt-global-key.h b/src/plugins/mail-crypt/mail-crypt-global-key.h
new file mode 100644
index 0000000..6f4679a
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-global-key.h
@@ -0,0 +1,38 @@
+#ifndef MAIL_CRYPT_GLOBAL_KEY_H
+#define MAIL_CRYPT_GLOBAL_KEY_H
+
+struct mail_crypt_global_private_key {
+ struct dcrypt_private_key *key;
+ char *key_id, *key_id_old;
+};
+
+struct mail_crypt_global_keys {
+ struct dcrypt_public_key *public_key;
+ ARRAY(struct mail_crypt_global_private_key) private_keys;
+};
+
+struct mail_user;
+
+int mail_crypt_global_keys_load(struct mail_user *user, const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys_r,
+ bool ignore_privkey_errors,
+ const char **error_r);
+int mail_crypt_global_keys_load_pluginenv(const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys_r,
+ const char **error_r);
+void mail_crypt_global_keys_init(struct mail_crypt_global_keys *global_keys_r);
+void mail_crypt_global_keys_free(struct mail_crypt_global_keys *global_keys);
+
+int mail_crypt_load_global_public_key(const char *set_key, const char *key_data,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r);
+int mail_crypt_load_global_private_key(const char *set_key, const char *key_data,
+ const char *set_pw, const char *key_password,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r);
+
+struct dcrypt_private_key *
+mail_crypt_global_key_find(struct mail_crypt_global_keys *global_keys,
+ const char *pubkey_digest);
+
+#endif
diff --git a/src/plugins/mail-crypt/mail-crypt-key.c b/src/plugins/mail-crypt/mail-crypt-key.c
new file mode 100644
index 0000000..22c86e3
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-key.c
@@ -0,0 +1,1242 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "dict.h"
+#include "array.h"
+#include "var-expand.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mail-crypt-plugin.h"
+#include "mail-user.h"
+#include "hex-binary.h"
+#include "safe-memset.h"
+#include "base64.h"
+#include "sha2.h"
+
+struct mail_crypt_key_cache_entry {
+ struct mail_crypt_key_cache_entry *next;
+
+ char *pubid;
+ /* this is lazily initialized */
+ struct dcrypt_keypair pair;
+};
+
+static
+int mail_crypt_get_key_cache(struct mail_crypt_key_cache_entry *cache,
+ const char *pubid,
+ struct dcrypt_private_key **privkey_r,
+ struct dcrypt_public_key **pubkey_r)
+{
+ for(struct mail_crypt_key_cache_entry *ent = cache;
+ ent != NULL; ent = ent->next)
+ {
+ if (strcmp(pubid, ent->pubid) == 0) {
+ if (privkey_r != NULL && ent->pair.priv != NULL) {
+ dcrypt_key_ref_private(ent->pair.priv);
+ *privkey_r = ent->pair.priv;
+ return 1;
+ } else if (pubkey_r != NULL && ent->pair.pub != NULL) {
+ dcrypt_key_ref_public(ent->pair.pub);
+ *pubkey_r = ent->pair.pub;
+ return 1;
+ } else if ((privkey_r == NULL && pubkey_r == NULL) ||
+ (ent->pair.priv == NULL &&
+ ent->pair.pub == NULL)) {
+ i_unreached();
+ }
+ }
+ }
+ return 0;
+}
+
+static
+void mail_crypt_put_key_cache(struct mail_crypt_key_cache_entry **cache,
+ const char *pubid,
+ struct dcrypt_private_key *privkey,
+ struct dcrypt_public_key *pubkey)
+{
+ for(struct mail_crypt_key_cache_entry *ent = *cache;
+ ent != NULL; ent = ent->next)
+ {
+ if (strcmp(pubid, ent->pubid) == 0) {
+ if (privkey != NULL) {
+ if (ent->pair.priv == NULL) {
+ ent->pair.priv = privkey;
+ dcrypt_key_ref_private(ent->pair.priv);
+ }
+ } else if (pubkey != NULL) {
+ if (ent->pair.pub == NULL) {
+ ent->pair.pub = pubkey;
+ dcrypt_key_ref_public(ent->pair.pub);
+ }
+ } else
+ i_unreached();
+ return;
+ }
+ }
+
+ /* not found */
+ struct mail_crypt_key_cache_entry *ent =
+ i_new(struct mail_crypt_key_cache_entry, 1);
+ ent->pubid = i_strdup(pubid);
+ ent->pair.priv = privkey;
+ ent->pair.pub = pubkey;
+ if (ent->pair.priv != NULL)
+ dcrypt_key_ref_private(ent->pair.priv);
+ if (ent->pair.pub != NULL)
+ dcrypt_key_ref_public(ent->pair.pub);
+
+ if (*cache == NULL) {
+ *cache = ent;
+ } else {
+ ent->next = *cache;
+ *cache = ent;
+ }
+}
+
+void mail_crypt_key_cache_destroy(struct mail_crypt_key_cache_entry **cache)
+{
+ struct mail_crypt_key_cache_entry *next, *cur = *cache;
+
+ *cache = NULL;
+
+ while(cur != NULL) {
+ next = cur->next;
+ i_free(cur->pubid);
+ if (cur->pair.priv != NULL)
+ dcrypt_key_unref_private(&cur->pair.priv);
+ if (cur->pair.pub != NULL)
+ dcrypt_key_unref_public(&cur->pair.pub);
+ i_free(cur);
+ cur = next;
+ }
+}
+
+int mail_crypt_private_key_id_match(struct dcrypt_private_key *key,
+ const char *pubid, const char **error_r)
+{
+ i_assert(key != NULL);
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ if (!dcrypt_key_id_private(key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
+ error_r))
+ return -1;
+ const char *hash = binary_to_hex(key_id->data, key_id->used);
+ if (strcmp(pubid, hash) == 0) return 1;
+
+ buffer_set_used_size(key_id, 0);
+ if (!dcrypt_key_id_private_old(key, key_id, error_r)) {
+ return -1;
+ }
+ hash = binary_to_hex(key_id->data, key_id->used);
+
+ if (strcmp(pubid, hash) != 0) {
+ *error_r = t_strdup_printf("Key %s does not match given ID %s",
+ hash, pubid);
+ return 0;
+ }
+ return 1;
+}
+
+int mail_crypt_public_key_id_match(struct dcrypt_public_key *key,
+ const char *pubid, const char **error_r)
+{
+ i_assert(key != NULL);
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ if (!dcrypt_key_id_public(key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
+ error_r))
+ return -1;
+ const char *hash = binary_to_hex(key_id->data, key_id->used);
+ if (strcmp(pubid, hash) == 0) return 1;
+
+ buffer_set_used_size(key_id, 0);
+ if (!dcrypt_key_id_public_old(key, key_id, error_r)) {
+ return -1;
+ }
+ hash = binary_to_hex(key_id->data, key_id->used);
+
+ if (strcmp(pubid, hash) != 0) {
+ *error_r = t_strdup_printf("Key %s does not match given ID %s",
+ hash, pubid);
+ return 0;
+ }
+ return 1;
+}
+
+static
+int mail_crypt_env_get_private_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ struct mail_crypt_global_keys global_keys;
+ int ret = 0;
+ if (mail_crypt_global_keys_load(user, "mail_crypt", &global_keys,
+ TRUE, error_r) < 0) {
+ mail_crypt_global_keys_free(&global_keys);
+ return -1;
+ }
+
+ /* see if we got a key */
+ struct dcrypt_private_key *key =
+ mail_crypt_global_key_find(&global_keys, pubid);
+
+ if (key != NULL) {
+ dcrypt_key_ref_private(key);
+ *key_r = key;
+ ret = 1;
+ }
+
+ mail_crypt_global_keys_free(&global_keys);
+
+ return ret;
+}
+
+static
+const char *mail_crypt_get_key_path(bool user_key, bool public, const char *pubid)
+{
+ const char *ret = t_strdup_printf("%s%s%s",
+ user_key ? USER_CRYPT_PREFIX :
+ BOX_CRYPT_PREFIX,
+ public ? PUBKEYS_PREFIX :
+ PRIVKEYS_PREFIX,
+ pubid);
+ return ret;
+}
+
+static
+int mail_crypt_decrypt_private_key(struct mailbox *box, const char *pubid,
+ const char *data,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ enum dcrypt_key_kind key_kind;
+ enum dcrypt_key_encryption_type enc_type;
+ const char *enc_hash = NULL, *key_hash = NULL, *pw = NULL;
+ struct dcrypt_private_key *key = NULL, *dec_key = NULL;
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ int ret = 0;
+
+ i_assert(pubid != NULL);
+ i_assert(data != NULL);
+
+ /* see what the key needs for decrypting */
+ if (!dcrypt_key_string_get_info(data, NULL, NULL, &key_kind,
+ &enc_type, &enc_hash, &key_hash, error_r)) {
+ return -1;
+ }
+
+ if (key_kind != DCRYPT_KEY_KIND_PRIVATE) {
+ *error_r = t_strdup_printf("Cannot use key %s: "
+ "Expected private key, got public key",
+ pubid);
+ return -1;
+ }
+
+ if (key_hash != NULL && strcmp(key_hash, pubid) != 0) {
+ *error_r = t_strdup_printf("Cannot use key %s: "
+ "Incorrect key hash %s stored",
+ pubid,
+ key_hash);
+ return -1;
+ }
+
+ /* see if it needs decrypting */
+ if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE) {
+ /* no key or password */
+ } else if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD) {
+ pw = mail_user_plugin_getenv(user, MAIL_CRYPT_USERENV_PASSWORD);
+ if (pw == NULL) {
+ *error_r = t_strdup_printf("Cannot decrypt key %s: "
+ "Password not available",
+ pubid);
+ return -1;
+ }
+ } else if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_KEY) {
+ if ((ret = mail_crypt_user_get_private_key(user, enc_hash,
+ &dec_key, error_r)) <= 0) {
+ /* last resort, look at environment */
+ if (ret == 0 && (ret = mail_crypt_env_get_private_key(user, enc_hash,
+ &dec_key, error_r)) == 0) {
+ *error_r = t_strdup_printf("Cannot decrypt key %s: "
+ "Private key %s not available:",
+ pubid, enc_hash);
+ return -1;
+ } else if (ret < 0) {
+ *error_r = t_strdup_printf("Cannot decrypt key %s: %s",
+ pubid, *error_r);
+ return ret;
+ }
+ }
+ }
+
+ bool res = dcrypt_key_load_private(&key, data, pw, dec_key, error_r);
+
+ if (dec_key != NULL)
+ dcrypt_key_unref_private(&dec_key);
+
+ if (!res)
+ return -1;
+
+ if (mail_crypt_private_key_id_match(key, pubid, error_r) <= 0) {
+ if (key != NULL)
+ dcrypt_key_unref_private(&key);
+ return -1;
+ }
+
+ i_assert(key != NULL);
+
+ *key_r = key;
+
+ return 1;
+}
+
+int mail_crypt_get_private_key(struct mailbox *box, const char *pubid,
+ bool user_key, bool shared,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
+
+ /* check cache */
+ if (mail_crypt_get_key_cache(muser->key_cache, pubid, key_r, NULL) > 0) {
+ return 1;
+ }
+
+ struct mail_attribute_value value;
+ struct dcrypt_private_key *key;
+ int ret;
+ const char *attr_name = mail_crypt_get_key_path(user_key, FALSE, pubid);
+
+ if ((ret = mailbox_attribute_get(box,
+ shared ? MAIL_ATTRIBUTE_TYPE_SHARED :
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ attr_name, &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, %s%s) failed: %s",
+ mailbox_get_vname(box),
+ shared ? "/shared/" :
+ "/priv/",
+ attr_name,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return ret;
+ }
+
+ if ((ret = mail_crypt_decrypt_private_key(box, pubid, value.value,
+ &key, error_r)) <= 0)
+ return ret;
+
+ i_assert(key != NULL);
+
+ mail_crypt_put_key_cache(&muser->key_cache, pubid, key, NULL);
+
+ *key_r = key;
+
+ return 1;
+}
+
+int mail_crypt_user_get_private_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ struct mail_attribute_value value;
+ int ret;
+
+ /* try retrieve currently active user key */
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
+ "INBOX",
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+
+ if (pubid == NULL) {
+ if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ } else {
+ pubid = value.value;
+ ret = 1;
+ }
+ } else
+ ret = 1;
+
+ /* try to open key */
+ if (ret > 0)
+ ret = mail_crypt_get_private_key(box, pubid, TRUE, FALSE,
+ key_r, error_r);
+ mailbox_free(&box);
+ return ret;
+}
+
+int mail_crypt_box_get_private_key(struct mailbox *box,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ struct mail_attribute_value value;
+ int ret;
+ /* get active key */
+ if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME, &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return ret;
+ }
+
+ return mail_crypt_get_private_key(box, value.value,
+ FALSE, FALSE,
+ key_r, error_r);
+}
+
+static
+int mail_crypt_set_private_key(struct mailbox_transaction_context *t,
+ bool user_key, bool shared, const char *pubid,
+ struct dcrypt_public_key *enc_key,
+ struct dcrypt_private_key *key,
+ const char **error_r)
+{
+ /* folder keys must be encrypted with some other key,
+ unless they are shared keys */
+ i_assert(user_key || shared || enc_key != NULL);
+
+ buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
+ const char *pw = NULL;
+ const char *algo = NULL;
+ struct mail_user *user = mail_storage_get_user(
+ mailbox_get_storage(
+ mailbox_transaction_get_mailbox(t)));
+ const char *attr_name = mail_crypt_get_key_path(user_key, FALSE, pubid);
+ struct mail_attribute_value value;
+ int ret;
+
+ if (enc_key != NULL) {
+ algo = MAIL_CRYPT_KEY_CIPHER;
+ } else if (user_key &&
+ (pw = mail_user_plugin_getenv(user,MAIL_CRYPT_USERENV_PASSWORD))
+ != NULL) {
+ algo = MAIL_CRYPT_PW_CIPHER;
+ }
+
+ /* export key */
+ if (!dcrypt_key_store_private(key, DCRYPT_FORMAT_DOVECOT, algo, data,
+ pw, enc_key, error_r)) {
+ return -1;
+ }
+
+ /* store it */
+ value.value_stream = NULL;
+ value.value = str_c(data);
+ value.last_change = 0;
+
+ if ((ret = mailbox_attribute_set(t,
+ shared ? MAIL_ATTRIBUTE_TYPE_SHARED :
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ attr_name,
+ &value)) < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, %s/%s) failed: %s",
+ mailbox_get_vname(mailbox_transaction_get_mailbox(t)),
+ shared ? "/shared" : "/priv",
+ attr_name,
+ mailbox_get_last_internal_error(
+ mailbox_transaction_get_mailbox(t), NULL));
+ }
+
+ safe_memset(buffer_get_modifiable_data(data, NULL), 0, data->used);
+
+ return ret;
+}
+
+int mail_crypt_user_set_private_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_private_key *key,
+ const char **error_r)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ struct dcrypt_private_key *env_key = NULL;
+ struct dcrypt_public_key *enc_key = NULL;
+ struct mailbox_transaction_context *t;
+ int ret;
+
+ if ((ret = mail_crypt_env_get_private_key(user, NULL, &env_key,
+ error_r)) < 0) {
+ return -1;
+ } else if (ret > 0) {
+ dcrypt_key_convert_private_to_public(env_key, &enc_key);
+ dcrypt_key_unref_private(&env_key);
+ }
+
+ if (mail_user_plugin_getenv(user, MAIL_CRYPT_REQUIRE_ENCRYPTED_USER_KEY) != NULL &&
+ mail_user_plugin_getenv(user, MAIL_CRYPT_USERENV_PASSWORD) == NULL &&
+ mail_user_plugin_getenv(user, MAIL_CRYPT_USERENV_KEY) == NULL)
+ {
+ *error_r = MAIL_CRYPT_REQUIRE_ENCRYPTED_USER_KEY " set, cannot "
+ "generate user keypair without password or key";
+ return -1;
+ }
+
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
+ "INBOX",
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+
+ if ((ret = mail_crypt_set_private_key(t, TRUE, FALSE, pubid, enc_key, key,
+ error_r)) < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if ((ret = mailbox_transaction_commit(&t)) < 0) {
+ *error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ }
+
+ mailbox_free(&box);
+
+ return ret;
+}
+
+int mail_crypt_box_set_private_key(struct mailbox *box, const char *pubid,
+ struct dcrypt_private_key *key,
+ struct dcrypt_public_key *user_key,
+ const char **error_r)
+{
+ int ret;
+ struct mailbox_transaction_context *t;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ if ((ret = mail_crypt_set_private_key(t, FALSE, FALSE, pubid, user_key,
+ key, error_r)) < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if ((ret = mailbox_transaction_commit(&t)) < 0) {
+ *error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ }
+
+ return ret;
+}
+
+static
+int mail_crypt_get_public_key(struct mailbox *box, const char *pubid,
+ bool user_key, struct dcrypt_public_key **key_r,
+ const char **error_r)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
+
+ /* check cache */
+ if (mail_crypt_get_key_cache(muser->key_cache, pubid, NULL, key_r) > 0) {
+ return 1;
+ }
+
+ enum dcrypt_key_kind key_kind;
+ const char *key_hash = NULL;
+ struct dcrypt_public_key *key;
+ struct mail_attribute_value value;
+ int ret;
+ const char *attr_name = mail_crypt_get_key_path(user_key, TRUE, pubid);
+
+ if ((ret = mailbox_attribute_get(box,
+ MAIL_ATTRIBUTE_TYPE_SHARED,
+ attr_name, &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ attr_name,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return ret;
+ }
+
+ if (!dcrypt_key_string_get_info(value.value, NULL, NULL, &key_kind,
+ NULL, NULL, &key_hash, error_r)) {
+ return -1;
+ }
+
+ if (key_kind != DCRYPT_KEY_KIND_PUBLIC) {
+ *error_r = t_strdup_printf("Cannot use key %s: "
+ "Expected public key, got private key",
+ pubid);
+ return -1;
+ }
+
+ if (key_hash != NULL && strcmp(key_hash, pubid) != 0) {
+ *error_r = t_strdup_printf("Cannot use key %s: "
+ "Incorrect key hash %s stored",
+ pubid, key_hash);
+ return -1;
+ }
+
+ /* load the key */
+ if (!dcrypt_key_load_public(&key, value.value, error_r)) {
+ return -1;
+ }
+
+ if (pubid != NULL &&
+ mail_crypt_public_key_id_match(key, pubid, error_r) <= 0) {
+ dcrypt_key_unref_public(&key);
+ return -1;
+ }
+
+ mail_crypt_put_key_cache(&muser->key_cache, pubid, NULL, key);
+
+ *key_r = key;
+
+ return 1;
+}
+
+int mail_crypt_user_get_public_key(struct mail_user *user,
+ struct dcrypt_public_key **key_r,
+ const char **error_r)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ struct mail_attribute_value value;
+ int ret;
+
+ /* try retrieve currently active user key */
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
+ "INBOX",
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+
+ if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ } else {
+ ret = mail_crypt_get_public_key(box, value.value, TRUE, key_r, error_r);
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+int mail_crypt_box_get_public_key(struct mailbox *box,
+ struct dcrypt_public_key **key_r,
+ const char **error_r)
+{
+ struct mail_attribute_value value;
+ int ret;
+
+ if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return ret;
+ }
+ return mail_crypt_get_public_key(box, value.value, FALSE, key_r, error_r);
+}
+
+static
+int mail_crypt_set_public_key(struct mailbox_transaction_context *t,
+ bool user_key, const char *pubid,
+ struct dcrypt_public_key *key,
+ const char **error_r)
+{
+ buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
+ const char *attr_name = mail_crypt_get_key_path(user_key, TRUE, pubid);
+ struct mail_attribute_value value;
+
+ /* export key */
+ if (!dcrypt_key_store_public(key, DCRYPT_FORMAT_DOVECOT, data,
+ error_r)) {
+ return -1;
+ }
+
+ /* store it */
+ value.value_stream = NULL;
+ value.value = str_c(data);
+ value.last_change = 0;
+
+ if (mailbox_attribute_set(t,
+ MAIL_ATTRIBUTE_TYPE_SHARED,
+ attr_name,
+ &value) < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, %s/%s) failed: %s",
+ mailbox_get_vname(mailbox_transaction_get_mailbox(t)),
+ "/shared",
+ attr_name,
+ mailbox_get_last_internal_error(
+ mailbox_transaction_get_mailbox(t), NULL));
+ return -1;
+ }
+
+ return 0;
+}
+
+int mail_crypt_user_set_public_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_public_key *key,
+ const char **error_r)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ struct mailbox_transaction_context *t;
+ struct mail_attribute_value value;
+ int ret;
+
+ /* try retrieve currently active user key */
+ if (mailbox_open(box) < 0) {
+ *error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
+ "INBOX",
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+
+ if ((ret = mail_crypt_set_public_key(t, TRUE, pubid, key,
+ error_r)) == 0) {
+ value.value_stream = NULL;
+ value.value = pubid;
+ value.last_change = 0;
+
+ if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value)) < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ }
+
+ if (ret < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if (mailbox_transaction_commit(&t) < 0) {
+ *error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+
+ mailbox_free(&box);
+
+ return ret;
+}
+
+int mail_crypt_box_set_public_key(struct mailbox *box, const char *pubid,
+ struct dcrypt_public_key *key,
+ const char **error_r)
+{
+ int ret;
+ struct mailbox_transaction_context *t;
+ struct mail_attribute_value value;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ if ((ret = mail_crypt_set_public_key(t, FALSE, pubid, key,
+ error_r)) == 0) {
+ value.value_stream = NULL;
+ value.value = pubid;
+ value.last_change = 0;
+
+ if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ &value)) < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(box),
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ }
+
+ if (ret < 0) {
+ mailbox_transaction_rollback(&t);
+ } else if (mailbox_transaction_commit(&t) < 0) {
+ *error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+
+ return ret;
+
+}
+
+static
+int mail_crypt_user_set_keys(struct mail_user *user,
+ const char *pubid,
+ struct dcrypt_private_key *privkey,
+ struct dcrypt_public_key *pubkey,
+ const char **error_r)
+{
+ if (mail_crypt_user_set_private_key(user, pubid, privkey, error_r) < 0)
+ return -1;
+ if (mail_crypt_user_set_public_key(user, pubid, pubkey, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+static
+int mail_crypt_box_set_keys(struct mailbox *box,
+ const char *pubid,
+ struct dcrypt_private_key *privkey,
+ struct dcrypt_public_key *user_key,
+ struct dcrypt_public_key *pubkey,
+ const char **error_r)
+{
+ if (mail_crypt_box_set_private_key(box, pubid, privkey, user_key,
+ error_r) < 0)
+ return -1;
+ if (mail_crypt_box_set_public_key(box, pubid, pubkey, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+int mail_crypt_box_get_shared_key(struct mailbox *box,
+ const char *pubid,
+ struct dcrypt_private_key **key_r,
+ const char **error_r)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
+
+ struct dcrypt_private_key *key = NULL;
+ struct mail_attribute_value value;
+ int ret;
+
+ /* check cache */
+ if (mail_crypt_get_key_cache(muser->key_cache, pubid, key_r, NULL) > 0) {
+ return 1;
+ }
+
+ const char *hexname =
+ binary_to_hex((const unsigned char*)user->username,
+ strlen(user->username));
+
+ const char *attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
+ PRIVKEYS_PREFIX"%s/%s",
+ hexname,
+ pubid);
+
+ if ((ret = mailbox_attribute_get(box,
+ MAIL_ATTRIBUTE_TYPE_SHARED,
+ attr_name, &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ attr_name,
+ mailbox_get_last_internal_error(box, NULL));
+ return ret;
+ }
+ return mail_crypt_get_private_key(box, pubid, FALSE, TRUE, key_r,
+ error_r);
+ } else {
+ if ((ret = mail_crypt_decrypt_private_key(box, pubid, value.value,
+ &key, error_r)) <= 0)
+ return ret;
+ }
+
+ mail_crypt_put_key_cache(&muser->key_cache, pubid, key, NULL);
+
+ *key_r = key;
+
+ return 1;
+}
+
+int mail_crypt_box_set_shared_key(struct mailbox_transaction_context *t,
+ const char *pubid,
+ struct dcrypt_private_key *privkey,
+ const char *target_uid,
+ struct dcrypt_public_key *user_key,
+ const char **error_r)
+{
+ struct mail_attribute_value value;
+ buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
+ int ret;
+ const char *attr_name;
+ const char *algo = NULL;
+
+ i_assert(target_uid == NULL || user_key != NULL);
+
+ if (target_uid != NULL) {
+ /* hash target UID */
+ algo = MAIL_CRYPT_KEY_CIPHER;
+ const char *hexname =
+ binary_to_hex((const unsigned char*)target_uid,
+ strlen(target_uid));
+ attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
+ PRIVKEYS_PREFIX"%s/%s",
+ hexname,
+ pubid);
+ } else {
+ attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
+ PRIVKEYS_PREFIX"%s",
+ pubid);
+ }
+
+ if (!dcrypt_key_store_private(privkey, DCRYPT_FORMAT_DOVECOT,
+ algo, data,
+ NULL, user_key, error_r)) {
+ return -1;
+ }
+
+ value.value_stream = NULL;
+ value.value = str_c(data);
+ value.last_change = 0;
+
+ if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
+ attr_name, &value)) < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
+ mailbox_get_vname(
+ mailbox_transaction_get_mailbox(t)),
+ attr_name,
+ mailbox_get_last_internal_error(
+ mailbox_transaction_get_mailbox(t),
+ NULL));
+ }
+
+ safe_memset(buffer_get_modifiable_data(data, NULL), 0, data->used);
+
+ return ret;
+}
+
+int mail_crypt_box_unset_shared_key(struct mailbox_transaction_context *t,
+ const char *pubid,
+ const char *target_uid,
+ const char **error_r)
+{
+ int ret;
+
+ const char *hexname =
+ binary_to_hex((const unsigned char*)target_uid,
+ strlen(target_uid));
+
+ const char *attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
+ PRIVKEYS_PREFIX"%s/%s",
+ hexname,
+ pubid);
+
+ if ((ret = mailbox_attribute_unset(t, MAIL_ATTRIBUTE_TYPE_SHARED,
+ attr_name)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_unset(%s, "
+ " /shared/%s): failed: %s",
+ mailbox_get_vname(
+ mailbox_transaction_get_mailbox(t)),
+ attr_name,
+ mailbox_get_last_internal_error(
+ mailbox_transaction_get_mailbox(t),
+ NULL));
+ }
+ }
+
+ return ret;
+}
+
+static
+int mail_crypt_generate_keypair(const char *curve,
+ struct dcrypt_keypair *pair_r,
+ const char **pubid_r,
+ const char **error_r)
+{
+ if (curve == NULL) {
+ *error_r = MAIL_CRYPT_USERENV_CURVE " not set, cannot generate EC key";
+ return -1;
+ }
+
+ if (!dcrypt_keypair_generate(pair_r, DCRYPT_KEY_EC, 0, curve, error_r)) {
+ return -1;
+ }
+
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ if (!dcrypt_key_id_public(pair_r->pub, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
+ error_r)) {
+ dcrypt_keypair_unref(pair_r);
+ return -1;
+ }
+
+ *pubid_r = binary_to_hex(key_id->data, key_id->used);
+
+ return 0;
+}
+
+int mail_crypt_user_generate_keypair(struct mail_user *user,
+ struct dcrypt_keypair *pair,
+ const char **pubid_r,
+ const char **error_r)
+{
+ struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
+ const char *curve = mail_user_plugin_getenv(user, MAIL_CRYPT_USERENV_CURVE);
+
+ if (mail_crypt_generate_keypair(curve, pair, pubid_r, error_r) < 0) {
+ return -1;
+ }
+
+ if (mail_crypt_user_set_keys(user, *pubid_r,
+ pair->priv, pair->pub, error_r) < 0) {
+ dcrypt_keypair_unref(pair);
+ return -1;
+ }
+
+ mail_crypt_put_key_cache(&muser->key_cache, *pubid_r, pair->priv, pair->pub);
+
+ return 0;
+}
+
+int mail_crypt_box_generate_keypair(struct mailbox *box,
+ struct dcrypt_keypair *pair,
+ struct dcrypt_public_key *user_key,
+ const char **pubid_r,
+ const char **error_r)
+{
+ int ret;
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
+ const char *curve = mail_user_plugin_getenv(user,
+ MAIL_CRYPT_USERENV_CURVE);
+
+ if (user_key == NULL) {
+ if ((ret = mail_crypt_user_get_public_key(user,
+ &user_key,
+ error_r)) <= 0) {
+ if (ret < 0)
+ return ret;
+ /* generate keypair */
+ struct dcrypt_keypair user_pair;
+ const char *user_pubid;
+ if (mail_crypt_user_generate_keypair(user, &user_pair,
+ &user_pubid,
+ error_r) < 0) {
+ return -1;
+ }
+
+ mail_crypt_put_key_cache(&muser->key_cache, user_pubid,
+ user_pair.priv, user_pair.pub);
+
+ user_key = user_pair.pub;
+ dcrypt_key_unref_private(&user_pair.priv);
+ }
+ } else {
+ dcrypt_key_ref_public(user_key);
+ }
+
+ if ((ret = mail_crypt_generate_keypair(curve, pair, pubid_r, error_r)) < 0) {
+ /* failed */
+ } else if ((ret = mail_crypt_box_set_keys(box, *pubid_r,
+ pair->priv, user_key, pair->pub,
+ error_r)) < 0) {
+ dcrypt_keypair_unref(pair);
+ } else {
+ mail_crypt_put_key_cache(&muser->key_cache, *pubid_r, pair->priv,
+ pair->pub);
+ }
+
+ dcrypt_key_unref_public(&user_key);
+
+ return ret;
+}
+
+int mail_crypt_box_get_pvt_digests(struct mailbox *box, pool_t pool,
+ enum mail_attribute_type type,
+ ARRAY_TYPE(const_string) *digests,
+ const char **error_r)
+{
+ struct mailbox_attribute_iter *iter;
+ const char *key;
+ int ret;
+
+ iter = mailbox_attribute_iter_init(box, type,
+ BOX_CRYPT_PREFIX PRIVKEYS_PREFIX);
+ while ((key = mailbox_attribute_iter_next(iter)) != NULL) {
+ key = p_strdup(pool, key);
+ array_push_back(digests, &key);
+ }
+ ret = mailbox_attribute_iter_deinit(&iter);
+ if (ret < 0)
+ *error_r = mailbox_get_last_internal_error(box, NULL);
+ return ret;
+}
+
+int mail_crypt_box_get_private_keys(struct mailbox *box,
+ ARRAY_TYPE(dcrypt_private_key) *keys_r,
+ const char **error_r)
+{
+ struct mailbox_attribute_iter *iter;
+ iter = mailbox_attribute_iter_init(box, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ BOX_CRYPT_PREFIX PRIVKEYS_PREFIX);
+ const char *id;
+ int ret;
+
+ while ((id = mailbox_attribute_iter_next(iter)) != NULL) {
+ struct dcrypt_private_key *key = NULL;
+ if ((ret = mail_crypt_get_private_key(box, id, FALSE, FALSE,
+ &key, error_r)) < 0) {
+ (void)mailbox_attribute_iter_deinit(&iter);
+ return -1;
+ } else if (ret > 0)
+ array_push_back(keys_r, &key);
+ }
+
+ ret = mailbox_attribute_iter_deinit(&iter);
+ if (ret < 0)
+ *error_r = mailbox_get_last_internal_error(box, NULL);
+ return ret;
+}
+
+int mail_crypt_box_share_private_keys(struct mailbox_transaction_context *t,
+ struct dcrypt_public_key *dest_pub_key,
+ const char *dest_user,
+ const ARRAY_TYPE(dcrypt_private_key) *priv_keys,
+ const char **error_r)
+{
+ i_assert(dest_user == NULL || dest_pub_key != NULL);
+
+ struct dcrypt_private_key *priv_key;
+ buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
+ int ret = 0;
+
+ array_foreach_elem(priv_keys, priv_key) {
+ ret = -1;
+ if (!dcrypt_key_id_private(priv_key, MAIL_CRYPT_KEY_ID_ALGORITHM,
+ key_id, error_r) ||
+ (ret = mail_crypt_box_set_shared_key(t,
+ binary_to_hex(key_id->data,
+ key_id->used),
+ priv_key, dest_user,
+ dest_pub_key, error_r)) < 0)
+ break;
+ }
+
+ return ret;
+}
+
+int
+mail_crypt_user_get_or_gen_public_key(struct mail_user *user,
+ struct dcrypt_public_key **pub_r,
+ const char **error_r)
+{
+ i_assert(user != NULL);
+ i_assert(pub_r != NULL);
+ i_assert(error_r != NULL);
+
+ int ret;
+ if ((ret = mail_crypt_user_get_public_key(user, pub_r, error_r)) == 0) {
+ struct dcrypt_keypair pair;
+ const char *pubid = NULL;
+ if (mail_crypt_user_generate_keypair(user, &pair,
+ &pubid, error_r) < 0) {
+ return -1;
+ }
+ *pub_r = pair.pub;
+ dcrypt_key_unref_private(&pair.priv);
+ } else
+ return ret;
+ return 0;
+}
+
+int
+mail_crypt_box_get_or_gen_public_key(struct mailbox *box,
+ struct dcrypt_public_key **pub_r,
+ const char **error_r)
+{
+ i_assert(box != NULL);
+ i_assert(pub_r != NULL);
+ i_assert(error_r != NULL);
+
+ struct mail_user *user =
+ mail_storage_get_user(mailbox_get_storage(box));
+ int ret;
+ if ((ret = mail_crypt_box_get_public_key(box, pub_r, error_r)) == 0) {
+ struct dcrypt_public_key *user_key;
+ if (mail_crypt_user_get_or_gen_public_key(user, &user_key,
+ error_r) < 0) {
+ return -1;
+ }
+
+ struct dcrypt_keypair pair;
+ const char *pubid = NULL;
+ if (mail_crypt_box_generate_keypair(box, &pair, user_key,
+ &pubid, error_r) < 0) {
+ return -1;
+ }
+ *pub_r = pair.pub;
+ dcrypt_key_unref_public(&user_key);
+ dcrypt_key_unref_private(&pair.priv);
+ } else
+ return ret;
+ return 0;
+}
+
+bool mail_crypt_acl_secure_sharing_enabled(struct mail_user *user)
+{
+ const char *env =
+ mail_user_plugin_getenv(user, MAIL_CRYPT_ACL_SECURE_SHARE_SETTING);
+
+ /* disabled by default */
+ bool ret = FALSE;
+
+ if (env != NULL) {
+ /* enable unless specifically
+ requested not to */
+ ret = TRUE;
+ switch (env[0]) {
+ case 'n':
+ case 'N':
+ case '0':
+ case 'f':
+ case 'F':
+ ret = FALSE;
+ }
+ }
+
+ return ret;
+}
+
+static const struct mailbox_attribute_internal mailbox_internal_attributes[] = {
+ { .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ .key = BOX_CRYPT_PREFIX,
+ .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
+ },
+ { .type = MAIL_ATTRIBUTE_TYPE_SHARED,
+ .key = BOX_CRYPT_PREFIX,
+ .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
+ },
+ { .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ .key = USER_CRYPT_PREFIX,
+ .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
+ },
+ { .type = MAIL_ATTRIBUTE_TYPE_SHARED,
+ .key = USER_CRYPT_PREFIX,
+ .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
+ }
+};
+
+void mail_crypt_key_register_mailbox_internal_attributes(void)
+{
+ mailbox_attribute_register_internals(mailbox_internal_attributes,
+ N_ELEMENTS(mailbox_internal_attributes));
+}
diff --git a/src/plugins/mail-crypt/mail-crypt-key.h b/src/plugins/mail-crypt/mail-crypt-key.h
new file mode 100644
index 0000000..f4a724a
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-key.h
@@ -0,0 +1,119 @@
+#ifndef MAIL_CRYPT_KEY
+#define MAIL_CRYPT_KEY
+
+#include "mail-crypt-common.h"
+#include "mail-crypt-global-key.h"
+#include "mail-storage.h"
+
+/*
+ For mailboxes:
+
+ shared/<mailbox GUID>/.../crypt/active = digest for the active public key
+ that is used for encrypting new emails
+ shared/<mailbox GUID>/.../crypt/pubkeys/<digest> = <key>
+ private/<mailbox GUID>/.../crypt/privkeys/<digest> = <key>
+
+ Similarly for users:
+
+ shared/<INBOX GUID>/.../crypt/active = digest for the active public key that
+ is used for encrypting new folder keys
+ shared/<INBOX GUID>/.../crypt/pubkeys/<digest> = <key>
+ private/<INBOX GUID>/.../crypt/privkeys/<digest> = <key>
+*/
+
+struct mail_crypt_key_cache_entry;
+
+/**
+ * key cache management functions
+ */
+void mail_crypt_key_cache_destroy(struct mail_crypt_key_cache_entry **cache);
+void mail_crypt_key_register_mailbox_internal_attributes(void);
+
+/* returns -1 on error, 0 not found, 1 = found */
+int mail_crypt_get_private_key(struct mailbox *box, const char *pubid,
+ bool user_key, bool shared,
+ struct dcrypt_private_key **key_r,
+ const char **error_r);
+int mail_crypt_user_get_private_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_private_key **key_r,
+ const char **error_r);
+int mail_crypt_box_get_private_key(struct mailbox *box,
+ struct dcrypt_private_key **key_r,
+ const char **error_r);
+int mail_crypt_box_get_private_keys(struct mailbox *box,
+ ARRAY_TYPE(dcrypt_private_key) *keys_r,
+ const char **error_r);
+int mail_crypt_user_get_public_key(struct mail_user *user,
+ struct dcrypt_public_key **key_r,
+ const char **error_r);
+int mail_crypt_box_get_public_key(struct mailbox *box,
+ struct dcrypt_public_key **key_r,
+ const char **error_r);
+int mail_crypt_box_get_shared_key(struct mailbox *box,
+ const char *pubid,
+ struct dcrypt_private_key **key_r,
+ const char **error_r);
+/* returns -1 on error, 0 no match , 1 = match */
+int mail_crypt_private_key_id_match(struct dcrypt_private_key *key,
+ const char *pubid, const char **error_r);
+int mail_crypt_public_key_id_match(struct dcrypt_public_key *key,
+ const char *pubid, const char **error_r);
+/* returns -1 on error, 0 = ok */
+int mail_crypt_user_set_private_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_private_key *key,
+ const char **error_r);
+int mail_crypt_box_set_private_key(struct mailbox *box, const char *pubid,
+ struct dcrypt_private_key *key,
+ struct dcrypt_public_key *user_key,
+ const char **error_r);
+int mail_crypt_user_set_public_key(struct mail_user *user, const char *pubid,
+ struct dcrypt_public_key *key,
+ const char **error_r);
+int mail_crypt_box_set_public_key(struct mailbox *box, const char *pubid,
+ struct dcrypt_public_key *key,
+ const char **error_r);
+int mail_crypt_user_generate_keypair(struct mail_user *user,
+ struct dcrypt_keypair *pair,
+ const char **pubid_r,
+ const char **error_r);
+int mail_crypt_box_generate_keypair(struct mailbox *box,
+ struct dcrypt_keypair *pair,
+ struct dcrypt_public_key *user_key,
+ const char **pubid_r,
+ const char **error_r);
+/* returns -1 on error, 0 = ok */
+int mail_crypt_box_set_shared_key(struct mailbox_transaction_context *t,
+ const char *pubid,
+ struct dcrypt_private_key *privkey,
+ const char *target_uid,
+ struct dcrypt_public_key *user_key,
+ const char **error_r);
+int mail_crypt_box_unset_shared_key(struct mailbox_transaction_context *t,
+ const char *pubid,
+ const char *target_uid,
+ const char **error_r);
+int mail_crypt_box_share_private_keys(struct mailbox_transaction_context *t,
+ struct dcrypt_public_key *dest_pub_key,
+ const char *dest_user,
+ const ARRAY_TYPE(dcrypt_private_key) *priv_keys,
+ const char **error_r);
+/* returns -1 on error, 0 = ok
+ these will also attempt to generate a keypair
+*/
+int mail_crypt_user_get_or_gen_public_key(struct mail_user *user,
+ struct dcrypt_public_key **pub_key_r,
+ const char **error_r);
+int mail_crypt_box_get_or_gen_public_key(struct mailbox *box,
+ struct dcrypt_public_key **pub_key_r,
+ const char **error_r);
+
+/* Lookup all private keys' digests. Returns 0 if ok, -1 on error. */
+int mail_crypt_box_get_pvt_digests(struct mailbox *box, pool_t pool,
+ enum mail_attribute_type type,
+ ARRAY_TYPE(const_string) *digests,
+ const char **error_r);
+
+/* is secure sharing enabled */
+bool mail_crypt_acl_secure_sharing_enabled(struct mail_user *user);
+
+#endif
diff --git a/src/plugins/mail-crypt/mail-crypt-plugin.c b/src/plugins/mail-crypt/mail-crypt-plugin.c
new file mode 100644
index 0000000..69c3b59
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-plugin.c
@@ -0,0 +1,501 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+/* FIXME: cache handling could be useful to move to Dovecot core, so that if
+ we're using this plugin together with zlib plugin there would be just one
+ cache. */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "randgen.h"
+#include "module-dir.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "istream.h"
+#include "istream-decrypt.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+#include "ostream-encrypt.h"
+#include "mail-user.h"
+#include "mail-copy.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mail-crypt-plugin.h"
+#include "sha2.h"
+#include "dcrypt-iostream.h"
+#include "hex-binary.h"
+
+struct mail_crypt_mailbox {
+ union mailbox_module_context module_ctx;
+ struct dcrypt_public_key *pub_key;
+};
+
+const char *mail_crypt_plugin_version = DOVECOT_ABI_VERSION;
+
+#define MAIL_CRYPT_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_crypt_mail_module)
+#define MAIL_CRYPT_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_crypt_storage_module)
+#define MAIL_CRYPT_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mail_crypt_user_module)
+#define MAIL_CRYPT_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_crypt_user_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_user_module,
+ &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_mail_module,
+ &mail_module_register);
+
+struct mail_crypt_user *mail_crypt_get_mail_crypt_user(struct mail_user *user)
+{
+ return MAIL_CRYPT_USER_CONTEXT(user);
+}
+
+static bool mail_crypt_is_stream_encrypted(struct istream *input)
+{
+ const unsigned char *data = NULL;
+ size_t size;
+
+ if (i_stream_read_data(input, &data, &size,
+ sizeof(IOSTREAM_CRYPT_MAGIC)) <= 0) {
+ return FALSE;
+ }
+
+ if (memcmp(data, IOSTREAM_CRYPT_MAGIC,
+ sizeof(IOSTREAM_CRYPT_MAGIC)) != 0) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void mail_crypt_cache_close(struct mail_crypt_user *muser)
+{
+ struct mail_crypt_cache *cache = &muser->cache;
+
+ timeout_remove(&cache->to);
+ i_stream_unref(&cache->input);
+ i_zero(cache);
+}
+
+static struct istream *
+mail_crypt_cache_open(struct mail_crypt_user *muser, struct mail *mail,
+ struct istream *input)
+{
+ struct mail_crypt_cache *cache = &muser->cache;
+ struct istream *inputs[2];
+ string_t *temp_prefix = t_str_new(128);
+
+ mail_crypt_cache_close(muser);
+
+ input->seekable = FALSE;
+ inputs[0] = input;
+ inputs[1] = NULL;
+ mail_user_set_get_temp_prefix(temp_prefix, mail->box->storage->user->set);
+ input = i_stream_create_seekable_path(inputs,
+ i_stream_get_max_buffer_size(inputs[0]),
+ str_c(temp_prefix));
+ i_stream_unref(&inputs[0]);
+
+ if (mail->uid > 0) {
+ cache->to = timeout_add(MAIL_CRYPT_MAIL_CACHE_EXPIRE_MSECS,
+ mail_crypt_cache_close, muser);
+ cache->box = mail->box;
+ cache->uid = mail->uid;
+ cache->input = input;
+ /* index-mail wants the stream to be destroyed at close, so create
+ a new stream instead of just increasing reference. */
+ return i_stream_create_limit(cache->input, UOFF_T_MAX);
+ }
+
+ return input;
+}
+
+static int mail_crypt_istream_get_private_key(const char *pubkey_digest,
+ struct dcrypt_private_key **priv_key_r,
+ const char **error_r,
+ void *context)
+{
+ /* mailbox_crypt_search_all_private_keys requires error_r != NULL */
+ i_assert(error_r != NULL);
+ int ret;
+ struct mail *_mail = context;
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT_REQUIRE(_mail->box->storage->user);
+
+ *priv_key_r = mail_crypt_global_key_find(&muser->global_keys,
+ pubkey_digest);
+ if (*priv_key_r != NULL) {
+ dcrypt_key_ref_private(*priv_key_r);
+ return 1;
+ }
+
+ struct mail_namespace *ns = mailbox_get_namespace(_mail->box);
+
+ if (ns->type == MAIL_NAMESPACE_TYPE_SHARED) {
+ ret = mail_crypt_box_get_shared_key(_mail->box, pubkey_digest,
+ priv_key_r, error_r);
+ /* priv_key_r is already referenced */
+ } else if (ns->type != MAIL_NAMESPACE_TYPE_PUBLIC) {
+ ret = mail_crypt_get_private_key(_mail->box, pubkey_digest,
+ FALSE, FALSE, priv_key_r,
+ error_r);
+ /* priv_key_r is already referenced */
+ } else {
+ *error_r = "Public emails cannot have keys";
+ ret = -1;
+ }
+
+ i_assert(ret <= 0 || *priv_key_r != NULL);
+
+ return ret;
+}
+
+static int
+mail_crypt_istream_opened(struct mail *_mail, struct istream **stream)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_user *user = _mail->box->storage->user;
+ struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT_REQUIRE(user);
+ struct mail_crypt_cache *cache = &muser->cache;
+ union mail_module_context *mmail = MAIL_CRYPT_MAIL_CONTEXT(mail);
+ struct istream *input;
+
+ if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) {
+ /* use the cached stream. when doing partial reads it should
+ already be seeked into the wanted offset. */
+ i_stream_unref(stream);
+ i_stream_seek(cache->input, 0);
+ *stream = i_stream_create_limit(cache->input, UOFF_T_MAX);
+ return mmail->super.istream_opened(_mail, stream);
+ }
+
+ /* decryption is the outmost stream, so add it before others
+ (e.g. zlib) */
+ if (!mail_crypt_is_stream_encrypted(*stream))
+ return mmail->super.istream_opened(_mail, stream);
+
+ input = *stream;
+ *stream = i_stream_create_decrypt_callback(input,
+ mail_crypt_istream_get_private_key, _mail);
+ i_stream_unref(&input);
+
+ *stream = mail_crypt_cache_open(muser, _mail, *stream);
+ return mmail->super.istream_opened(_mail, stream);
+}
+
+static void mail_crypt_close(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *mmail = MAIL_CRYPT_MAIL_CONTEXT(mail);
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT_REQUIRE(_mail->box->storage->user);
+ struct mail_crypt_cache *cache = &muser->cache;
+ uoff_t size;
+
+ if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) {
+ /* make sure we have read the entire email into the seekable
+ stream (which causes the original input stream to be
+ unrefed). we can't safely keep the original input stream
+ open after the mail is closed. */
+ if (i_stream_get_size(cache->input, TRUE, &size) < 0)
+ mail_crypt_cache_close(muser);
+ }
+ mmail->super.close(_mail);
+}
+
+static void mail_crypt_mail_allocated(struct mail *_mail)
+{
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT(_mail->box->storage->user);
+ if (muser == NULL) return;
+
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ union mail_module_context *mmail;
+
+ mmail = p_new(mail->pool, union mail_module_context, 1);
+ mmail->super = *v;
+ mail->vlast = &mmail->super;
+
+ v->istream_opened = mail_crypt_istream_opened;
+ v->close = mail_crypt_close;
+ MODULE_CONTEXT_SET_SELF(mail, mail_crypt_mail_module, mmail);
+}
+
+static int mail_crypt_mail_save_finish(struct mail_save_context *ctx)
+{
+ struct mailbox *box = ctx->transaction->box;
+ union mailbox_module_context *zbox = MAIL_CRYPT_CONTEXT(box);
+ struct istream *input;
+
+ if (zbox->super.save_finish(ctx) < 0)
+ return -1;
+
+ /* we're here only if mail-crypt plugin is disabled. we want to make
+ sure that even though we're saving an unencrypted mail, the mail
+ can't be faked to look like an encrypted mail. */
+ if (mail_get_stream(ctx->dest_mail, NULL, NULL, &input) < 0)
+ return -1;
+
+ if (mail_crypt_is_stream_encrypted(input)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Saving mails encrypted by client isn't supported");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mail_crypt_mail_save_begin(struct mail_save_context *ctx,
+ struct istream *input)
+{
+ const char *pubid;
+ struct mailbox *box = ctx->transaction->box;
+ struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(box);
+ struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT(box->storage->user);
+
+ enum io_stream_encrypt_flags enc_flags = 0;
+ if (muser != NULL) {
+ if (muser->save_version == 1) {
+ enc_flags = IO_STREAM_ENC_VERSION_1;
+ } else if (muser->save_version == 2) {
+ enc_flags = IO_STREAM_ENC_INTEGRITY_AEAD;
+ } else {
+ i_assert(muser->save_version == 0);
+ }
+ }
+
+ if (mbox->module_ctx.super.save_begin(ctx, input) < 0)
+ return -1;
+
+ if (enc_flags == 0)
+ return 0;
+
+ struct dcrypt_public_key *pub_key;
+ if (muser->global_keys.public_key != NULL)
+ pub_key = muser->global_keys.public_key;
+ else if (mbox->pub_key != NULL)
+ pub_key = mbox->pub_key;
+ else {
+ const char *error;
+ int ret;
+
+ if ((ret = mail_crypt_box_get_public_key(box, &pub_key,
+ &error)) <= 0)
+ {
+ struct dcrypt_keypair pair;
+
+ if (ret < 0) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_PARAMS,
+ t_strdup_printf("get_public_key(%s) failed: %s",
+ mailbox_get_vname(box),
+ error));
+ return ret;
+ }
+
+ if (muser->save_version < 2) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_PARAMS,
+ t_strdup_printf("generate_keypair(%s) failed: "
+ "unsupported save_version=%d",
+ mailbox_get_vname(box),
+ muser->save_version));
+ return -1;
+ }
+
+ if (mail_crypt_box_generate_keypair(box, &pair, NULL,
+ &pubid, &error) < 0) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_PARAMS,
+ t_strdup_printf("generate_keypair(%s) failed: %s",
+ mailbox_get_vname(box),
+ error));
+ return -1;
+ }
+ pub_key = pair.pub;
+ dcrypt_key_unref_private(&pair.priv);
+
+ }
+ mbox->pub_key = pub_key;
+ }
+
+ /* encryption is the outermost layer (zlib etc. are inside) */
+ struct ostream *output = o_stream_create_encrypt(ctx->data.output,
+ MAIL_CRYPT_ENC_ALGORITHM, pub_key, enc_flags);
+
+ o_stream_unref(&ctx->data.output);
+ ctx->data.output = output;
+ o_stream_cork(ctx->data.output);
+ return 0;
+}
+
+static int
+mail_crypt_mailbox_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox *dest_box = ctx->transaction->box;
+ struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(dest_box);
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT(dest_box->storage->user);
+
+ bool raw_copy;
+ if (mailbox_backends_equal(dest_box, mail->box)) {
+ /* Copy to same box always have identical crypt profile */
+ raw_copy = TRUE;
+ } else if (strcmp(dest_box->storage->user->username,
+ mail->box->storage->user->username) != 0) {
+ /* Always consider copies between different users unsafe
+ as they may have different encryption level and/or
+ different global keys */
+ raw_copy = FALSE;
+ } else {
+ /* Within same user, consider safe only the case where
+ encryption is enabled and keys are global. */
+ raw_copy = muser != NULL &&
+ muser->save_version != 0 &&
+ muser->global_keys.public_key != NULL;
+ }
+
+ return raw_copy ?
+ mbox->module_ctx.super.copy(ctx, mail) :
+ mail_storage_copy(ctx, mail);
+}
+
+static void mail_crypt_mailbox_close(struct mailbox *box)
+{
+ struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(box);
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT(box->storage->user);
+
+ if (mbox->pub_key != NULL)
+ dcrypt_key_unref_public(&mbox->pub_key);
+ if (muser != NULL && muser->cache.box == box)
+ mail_crypt_cache_close(muser);
+ mbox->module_ctx.super.close(box);
+}
+
+static void mail_crypt_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct mail_crypt_user *muser =
+ MAIL_CRYPT_USER_CONTEXT(box->storage->user);
+ struct mail_crypt_mailbox *mbox;
+ enum mail_storage_class_flags class_flags = box->storage->class_flags;
+
+ mbox = p_new(box->pool, struct mail_crypt_mailbox, 1);
+ mbox->module_ctx.super = *v;
+ box->vlast = &mbox->module_ctx.super;
+ v->close = mail_crypt_mailbox_close;
+
+ MODULE_CONTEXT_SET(box, mail_crypt_storage_module, mbox);
+
+ if ((class_flags & MAIL_STORAGE_CLASS_FLAG_BINARY_DATA) != 0) {
+ v->save_begin = mail_crypt_mail_save_begin;
+ v->copy = mail_crypt_mailbox_copy;
+
+ if (muser == NULL || muser->save_version == 0)
+ v->save_finish = mail_crypt_mail_save_finish;
+ }
+}
+
+static void mail_crypt_mail_user_deinit(struct mail_user *user)
+{
+ struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT_REQUIRE(user);
+
+ mail_crypt_key_cache_destroy(&muser->key_cache);
+ mail_crypt_global_keys_free(&muser->global_keys);
+ mail_crypt_cache_close(muser);
+ muser->module_ctx.super.deinit(user);
+}
+
+static void mail_crypt_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct mail_crypt_user *muser;
+ const char *error = NULL;
+
+ muser = p_new(user->pool, struct mail_crypt_user, 1);
+ muser->module_ctx.super = *v;
+ user->vlast = &muser->module_ctx.super;
+
+ const char *curve = mail_user_plugin_getenv(user, "mail_crypt_curve");
+ buffer_t *tmp = t_str_new(64);
+ if (curve == NULL || *curve == '\0') {
+ e_debug(user->event, "mail_crypt_plugin: mail_crypt_curve setting "
+ "missing - generating EC keys disabled");
+ } else if (!dcrypt_name2oid(curve, tmp, &error)) {
+ user->error = p_strdup_printf(user->pool,
+ "mail_crypt_plugin: "
+ "invalid mail_crypt_curve setting %s: %s",
+ curve, error);
+ } else {
+ muser->curve = p_strdup(user->pool, curve);
+ }
+
+ const char *version = mail_user_plugin_getenv(user,
+ "mail_crypt_save_version");
+
+ if (version == NULL || *version == '\0') {
+ user->error = p_strdup_printf(user->pool,
+ "mail_crypt_plugin: "
+ "mail_crypt_save_version setting missing");
+ } else if (version[0] == '0') {
+ muser->save_version = 0;
+ } else if (version[0] == '1') {
+ muser->save_version = 1;
+ } else if (version[0] == '2') {
+ muser->save_version = 2;
+ } else {
+ user->error = p_strdup_printf(user->pool,
+ "mail_crypt_plugin: Invalid "
+ "mail_crypt_save_version %s: use 0, 1, or 2 ",
+ version);
+ }
+
+ if (mail_crypt_global_keys_load(user, "mail_crypt_global",
+ &muser->global_keys, FALSE, &error) < 0) {
+ user->error = p_strdup_printf(user->pool,
+ "mail_crypt_plugin: %s", error);
+ }
+
+ v->deinit = mail_crypt_mail_user_deinit;
+ MODULE_CONTEXT_SET(user, mail_crypt_user_module, muser);
+}
+
+static struct mail_storage_hooks mail_crypt_mail_storage_hooks = {
+ .mail_user_created = mail_crypt_mail_user_created,
+ .mail_allocated = mail_crypt_mail_allocated
+};
+
+static struct mail_storage_hooks mail_crypt_mail_storage_hooks_post = {
+ .mailbox_allocated = mail_crypt_mailbox_allocated
+};
+
+static struct module crypto_post_module = {
+ .path = "lib95_mail_crypt_plugin.so"
+};
+
+void mail_crypt_plugin_init(struct module *module)
+{
+ const char* error;
+ if (!dcrypt_initialize("openssl", NULL, &error))
+ i_fatal("dcrypt_initialize(): %s", error);
+ mail_storage_hooks_add(module, &mail_crypt_mail_storage_hooks);
+ /* when this plugin is loaded, there's the potential chance for
+ mixed delivery between encrypted and non-encrypted recipients.
+ The mail_crypt_mailbox_allocated() hook ensures encrypted
+ content isn't copied as such into cleartext recipients
+ (and the other way around) */
+ mail_storage_hooks_add_forced(&crypto_post_module,
+ &mail_crypt_mail_storage_hooks_post);
+ mail_crypt_key_register_mailbox_internal_attributes();
+}
+
+void mail_crypt_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&mail_crypt_mail_storage_hooks);
+ mail_storage_hooks_remove(&mail_crypt_mail_storage_hooks_post);
+}
diff --git a/src/plugins/mail-crypt/mail-crypt-plugin.h b/src/plugins/mail-crypt/mail-crypt-plugin.h
new file mode 100644
index 0000000..4556a92
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-plugin.h
@@ -0,0 +1,32 @@
+#ifndef MAIL_CRYPT_PLUGIN_H
+#define MAIL_CRYPT_PLUGIN_H
+
+struct mailbox;
+struct module;
+
+struct mail_crypt_cache {
+ struct timeout *to;
+ struct mailbox *box;
+ uint32_t uid;
+
+ struct istream *input;
+};
+
+struct mail_crypt_user {
+ union mail_user_module_context module_ctx;
+
+ struct mail_crypt_global_keys global_keys;
+ struct mail_crypt_cache cache;
+ struct mail_crypt_key_cache_entry *key_cache;
+ const char *curve;
+ int save_version;
+};
+
+void mail_crypt_plugin_init(struct module *module);
+void mail_crypt_plugin_deinit(void);
+
+#define MAIL_CRYPT_MAIL_CACHE_EXPIRE_MSECS (60*1000)
+
+struct mail_crypt_user *mail_crypt_get_mail_crypt_user(struct mail_user *user);
+
+#endif
diff --git a/src/plugins/mail-crypt/mail-crypt-pluginenv.c b/src/plugins/mail-crypt/mail-crypt-pluginenv.c
new file mode 100644
index 0000000..68cf94f
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-pluginenv.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "fs-crypt-settings.h"
+
+static const struct fs_crypt_settings *
+fs_crypt_load_settings(void)
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &fs_crypt_setting_parser_info,
+ NULL
+ };
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+ const char *error;
+
+ i_zero(&input);
+ input.roots = set_roots;
+ input.module = "fs-crypt";
+ input.service = "fs-crypt";
+ if (master_service_settings_read(master_service, &input,
+ &output, &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ return master_service_settings_get_others(master_service)[0];
+}
+
+static
+const char *mail_crypt_plugin_getenv(const struct fs_crypt_settings *set,
+ const char *name)
+{
+ const char *const *envs;
+ unsigned int i, count;
+
+ if (set == NULL)
+ return NULL;
+
+ 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;
+}
+
+static int
+mail_crypt_load_global_private_keys(const struct fs_crypt_settings *set,
+ const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r)
+{
+ string_t *set_key = t_str_new(64);
+ str_append(set_key, set_prefix);
+ str_append(set_key, "_private_key");
+ size_t prefix_len = str_len(set_key);
+
+ unsigned int i = 1;
+ const char *key_data;
+ while ((key_data = mail_crypt_plugin_getenv(set, str_c(set_key))) != NULL) {
+ const char *set_pw = t_strconcat(str_c(set_key), "_password", NULL);
+ const char *password = mail_crypt_plugin_getenv(set, set_pw);
+ if (mail_crypt_load_global_private_key(str_c(set_key), key_data,
+ set_pw, password,
+ global_keys, error_r) < 0)
+ return -1;
+ str_truncate(set_key, prefix_len);
+ str_printfa(set_key, "%u", ++i);
+ }
+ return 0;
+}
+
+int mail_crypt_global_keys_load_pluginenv(const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys_r,
+ const char **error_r)
+{
+ const struct fs_crypt_settings *set = fs_crypt_load_settings();
+
+ const char *set_key = t_strconcat(set_prefix, "_public_key", NULL);
+ const char *key_data = mail_crypt_plugin_getenv(set, set_key);
+ int ret = 0;
+
+ mail_crypt_global_keys_init(global_keys_r);
+ if (key_data != NULL) {
+ if (mail_crypt_load_global_public_key(set_key, key_data,
+ global_keys_r, error_r) < 0)
+ ret = -1;
+ }
+
+ if (ret == 0 &&
+ mail_crypt_load_global_private_keys(set, set_prefix, global_keys_r,
+ error_r) < 0)
+ ret = -1;
+
+ if (ret != 0)
+ mail_crypt_global_keys_free(global_keys_r);
+ return ret;
+}
diff --git a/src/plugins/mail-crypt/mail-crypt-userenv.c b/src/plugins/mail-crypt/mail-crypt-userenv.c
new file mode 100644
index 0000000..b152a7f
--- /dev/null
+++ b/src/plugins/mail-crypt/mail-crypt-userenv.c
@@ -0,0 +1,66 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+#include "lib.h"
+#include "str.h"
+#include "mail-user.h"
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+
+static int
+mail_crypt_load_global_private_keys(struct mail_user *user,
+ const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys,
+ bool ignore_errors,
+ const char **error_r)
+{
+ string_t *set_key = t_str_new(64);
+ str_append(set_key, set_prefix);
+ str_append(set_key, "_private_key");
+ size_t prefix_len = str_len(set_key);
+
+ unsigned int i = 1;
+ const char *key_data;
+ while ((key_data = mail_user_plugin_getenv(user, str_c(set_key))) != NULL) {
+ const char *set_pw = t_strconcat(str_c(set_key), "_password", NULL);
+ const char *password = mail_user_plugin_getenv(user, set_pw);
+ if (mail_crypt_load_global_private_key(str_c(set_key), key_data,
+ set_pw, password,
+ global_keys,
+ error_r) < 0) {
+ /* skip this key */
+ if (ignore_errors) {
+ e_debug(user->event, "mail-crypt-plugin: "
+ "mail_crypt_load_global_private_key failed: %s",
+ *error_r);
+ *error_r = NULL;
+ continue;
+ }
+ return -1;
+ }
+ str_truncate(set_key, prefix_len);
+ str_printfa(set_key, "%u", ++i);
+ }
+ return 0;
+}
+
+int mail_crypt_global_keys_load(struct mail_user *user, const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys_r,
+ bool ignore_privkey_errors,
+ const char **error_r)
+{
+ const char *set_key = t_strconcat(set_prefix, "_public_key", NULL);
+ const char *key_data = mail_user_plugin_getenv(user, set_key);
+
+ mail_crypt_global_keys_init(global_keys_r);
+ if (key_data != NULL) {
+ if (mail_crypt_load_global_public_key(set_key,
+ key_data,
+ global_keys_r,
+ error_r) < 0)
+ return -1;
+ }
+ if (mail_crypt_load_global_private_keys(user, set_prefix, global_keys_r,
+ ignore_privkey_errors,
+ error_r) < 0)
+ return -1;
+ return 0;
+}
diff --git a/src/plugins/mail-crypt/test-mail-global-key.c b/src/plugins/mail-crypt/test-mail-global-key.c
new file mode 100644
index 0000000..4c5ed38
--- /dev/null
+++ b/src/plugins/mail-crypt/test-mail-global-key.c
@@ -0,0 +1,130 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "randgen.h"
+#include "array.h"
+#include "dcrypt.h"
+#include "hex-binary.h"
+
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "fs-crypt-settings.h"
+
+#include "mail-crypt-pluginenv.c"
+
+static struct fs_crypt_settings fs_set;
+
+static const char *settings[] = {
+ "mail_crypt_global_private_key",
+ "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ1lJdWZKWlplMlk2aUZ6NXgKa29Jb3lzYjNkWkxaV3N5ZWtqT2MvR2pzTGQyaFJBTkNBQVNuSVdnUXVoRThqcUFMY21maXVuUnlFazd2a3EveQphOXZZSzUwYjNjRmhDc0xVNHRmVlRMa0IxWS82VmxaajYzUUtNelhOdms1RzVPRDFvZkVsY3B5agotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==",
+ "mail_crypt_global_public_key",
+ "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFcHlGb0VMb1JQSTZnQzNKbjRycDBjaEpPNzVLdgo4bXZiMkN1ZEc5M0JZUXJDMU9MWDFVeTVBZFdQK2xaV1krdDBDak0xemI1T1J1VGc5YUh4SlhLY293PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==",
+ "mail_crypt_global_private_key2",
+ "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLQpNSUhlTUVrR0NTcUdTSWIzRFFFRkRUQThNQnNHQ1NxR1NJYjNEUUVGRERBT0JBaXA2cUpja1FET3F3SUNDQUF3CkhRWUpZSVpJQVdVREJBRXFCQkFXN09oUFRlU0xSOExLcGYwZjZHa3ZCSUdRZk5rYUpodnM2VWVWS2RkN2NzdFMKMURSNXJYTWtON09FbVNjTTljRlk2UDVrMzdnY1VJUFZudTQrOTFYZUE1MTU2cnBpUEpycEdkZnprcjhPNVFqZApsMWRycmR6Z0hqZHE4T2VmbUR1MEEzMjRZd25SS3hGRExUcjlHMkxVMkhoYmV6a0xjV1FwMVJISDZsNXRRcUtwCjZid05iMnc3OXhCb01YSjN6MVZqcElOZk9wRnJ6M3lucVlqUXhseTIrQjg2Ci0tLS0tRU5EIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCg==",
+ "mail_crypt_global_private_key2_password",
+ "password",
+};
+
+int
+mail_crypt_load_global_private_keys(const struct fs_crypt_settings *set,
+ const char *set_prefix,
+ struct mail_crypt_global_keys *global_keys,
+ const char **error_r);
+
+static void test_setup(void)
+{
+ struct dcrypt_settings set = {
+ .module_dir = top_builddir "/src/lib-dcrypt/.libs"
+ };
+ if (!dcrypt_initialize(NULL, &set, NULL)) {
+ i_info("No functional dcrypt backend found - skipping tests");
+ test_exit(0);
+ }
+ i_array_init(&fs_set.plugin_envs, 8);
+ array_append(&fs_set.plugin_envs, settings, N_ELEMENTS(settings));
+}
+
+static void test_try_load_keys(void)
+{
+ const char *pubid1 = "c79e262924842de291a8bcd413f4122a570abd033adeff7c1cdfdc9d05998c75";
+ const char *pubid2 = "aaf927444bff8b63425e852c6b3f769e8221b952b42cf886fae7d326c5be098e";
+ buffer_t *key_id = t_buffer_create(128);
+
+ const char *error = NULL;
+ test_begin("try_load_keys");
+
+ struct mail_crypt_global_keys keys;
+ i_zero(&keys);
+ mail_crypt_global_keys_init(&keys);
+
+ const char *set_prefix = "mail_crypt_global";
+ const char *set_key = t_strconcat(set_prefix, "_public_key", NULL);
+ const char *key_data = mail_crypt_plugin_getenv(&fs_set, set_key);
+
+ test_assert(key_data != NULL);
+
+ if (key_data != NULL) {
+ test_assert(mail_crypt_load_global_public_key(set_key, key_data,
+ &keys, &error) == 0);
+ test_assert(mail_crypt_load_global_private_keys(&fs_set, set_prefix,
+ &keys, &error) == 0);
+ /* did we get two private keys? */
+ test_assert(array_count(&keys.private_keys) == 2);
+
+ /* public key id checks */
+
+ buffer_set_used_size(key_id, 0);
+ test_assert(dcrypt_key_id_public(keys.public_key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id, &error) == TRUE);
+ test_assert(strcmp(binary_to_hex(key_id->data, key_id->used), pubid1) == 0);
+
+ const struct mail_crypt_global_private_key *key =
+ array_front(&keys.private_keys);
+
+ buffer_set_used_size(key_id, 0);
+ test_assert(dcrypt_key_id_private(key->key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id, &error) == TRUE);
+ test_assert(strcmp(binary_to_hex(key_id->data, key_id->used), pubid1) == 0);
+
+ key = array_idx(&keys.private_keys, 1);
+ buffer_set_used_size(key_id, 0);
+ test_assert(dcrypt_key_id_private(key->key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id, &error) == TRUE);
+ test_assert(strcmp(binary_to_hex(key_id->data, key_id->used), pubid2) == 0);
+
+ }
+
+ mail_crypt_global_keys_free(&keys);
+
+ test_end();
+}
+
+static void test_empty_keyset(void)
+{
+ test_begin("test_empty_keyset");
+
+ /* this should not crash */
+ struct mail_crypt_global_keys keys;
+ i_zero(&keys);
+ test_assert(mail_crypt_global_key_find(&keys, "423423423423") == NULL);
+
+ test_end();
+}
+
+static void test_teardown(void)
+{
+ array_free(&fs_set.plugin_envs);
+ dcrypt_deinitialize();
+}
+
+int main(void)
+{
+ void (*tests[])(void) = {
+ test_setup,
+ test_try_load_keys,
+ test_empty_keyset,
+ test_teardown,
+ NULL
+ };
+
+ int ret = test_run(tests);
+ return ret;
+}
diff --git a/src/plugins/mail-crypt/test-mail-key.c b/src/plugins/mail-crypt/test-mail-key.c
new file mode 100644
index 0000000..ac89835
--- /dev/null
+++ b/src/plugins/mail-crypt/test-mail-key.c
@@ -0,0 +1,424 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "hex-binary.h"
+#include "master-service.h"
+#include "test-mail-storage-common.h"
+#include "dcrypt.h"
+
+#include "mail-crypt-common.h"
+#include "mail-crypt-key.h"
+#include "mail-crypt-plugin.h"
+
+static const char *mcp_old_user_key = "1\t716\t0\t048FD04FD3612B22D32790C592CF21CEF417EFD2EA34AE5F688FA5B51BED29E05A308B68DA78E16E90B47A11E133BD9A208A2894FD01B0BEE865CE339EA3FB17AC\td0cfaca5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd433615e77a0";
+static const char *mcp_old_user_key_id = "d0cfaca5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd433615e77a0";
+static const char *mcp_old_box_key = "1\t716\t1\t0567e6bf9579813ae967314423b0fceb14bda24749303923de9a9bb9370e0026f995901a57e63113eeb2baf0c940e978d00686cbb52bd5014bc318563375876255\t0300E46DA2125427BE968EB3B649910CDC4C405E5FFDE18D433A97CABFEE28CEEFAE9EE356C792004FFB80981D67E741B8CC036A34235A8D2E1F98D1658CFC963D07EB\td0cfaca5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd433615e77a0\t7c9a1039ea2e4fed73e81dd3ffc3fa22ea4a28352939adde7bf8ea858b00fa4f";
+static const char *mcp_old_box_key_id = "7c9a1039ea2e4fed73e81dd3ffc3fa22ea4a28352939adde7bf8ea858b00fa4f";
+
+static struct test_mail_storage_ctx *test_ctx;
+static const char *test_user_key_id;
+static const char *test_box_key_id;
+
+static struct mail_crypt_user mail_crypt_user;
+
+struct mail_crypt_user *mail_crypt_get_mail_crypt_user(struct mail_user *user ATTR_UNUSED)
+{
+ return &mail_crypt_user;
+}
+
+static
+int test_mail_attribute_get(struct mailbox *box, bool user_key, bool shared,
+ const char *pubid, const char **value_r, const char **error_r)
+{
+ const char *attr_name;
+ enum mail_attribute_type attr_type;
+
+ if (strcmp(pubid, ACTIVE_KEY_NAME) == 0) {
+ attr_name = user_key ? USER_CRYPT_PREFIX ACTIVE_KEY_NAME :
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME;
+ attr_type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ } else {
+ attr_name = t_strdup_printf("%s%s%s",
+ user_key ? USER_CRYPT_PREFIX :
+ BOX_CRYPT_PREFIX,
+ shared ? PUBKEYS_PREFIX :
+ PRIVKEYS_PREFIX,
+ pubid);
+ attr_type = shared ? MAIL_ATTRIBUTE_TYPE_SHARED : MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ }
+ struct mail_attribute_value value;
+
+ int ret;
+
+ if ((ret = mailbox_attribute_get(box, attr_type,
+ attr_name, &value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
+ mailbox_get_vname(box),
+ attr_name,
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ } else {
+ *value_r = t_strdup(value.value);
+ }
+ return ret;
+}
+
+static int
+test_mail_attribute_set(struct mailbox_transaction_context *t,
+ bool user_key, bool shared, const char *pubid,
+ const char *value, const char **error_r)
+{
+ const char *attr_name;
+ enum mail_attribute_type attr_type;
+
+ if (strcmp(pubid, ACTIVE_KEY_NAME) == 0) {
+ attr_name = user_key ? USER_CRYPT_PREFIX ACTIVE_KEY_NAME :
+ BOX_CRYPT_PREFIX ACTIVE_KEY_NAME;
+ attr_type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ } else {
+ attr_name = t_strdup_printf("%s%s%s",
+ user_key ? USER_CRYPT_PREFIX :
+ BOX_CRYPT_PREFIX,
+ shared ? PUBKEYS_PREFIX :
+ PRIVKEYS_PREFIX,
+ pubid);
+ attr_type = shared ? MAIL_ATTRIBUTE_TYPE_SHARED : MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ }
+
+ struct mail_attribute_value attr_value;
+
+ int ret;
+
+ i_zero(&attr_value);
+ attr_value.value = value;
+
+ if ((ret = mailbox_attribute_set(t, attr_type,
+ attr_name, &attr_value)) <= 0) {
+ if (ret < 0) {
+ *error_r = t_strdup_printf("mailbox_attribute_set(%s, %s) failed: %s",
+ mailbox_get_vname(mailbox_transaction_get_mailbox(t)),
+ attr_name,
+ mailbox_get_last_internal_error(mailbox_transaction_get_mailbox(t), NULL));
+ }
+ }
+
+ return ret;
+}
+
+
+static void test_generate_user_key(void)
+{
+ struct dcrypt_keypair pair;
+ const char *pubid;
+ const char *error = NULL;
+
+ test_begin("generate user key");
+
+ /* try to generate a keypair for user */
+ if (mail_crypt_user_generate_keypair(test_ctx->user, &pair,
+ &pubid, &error) < 0) {
+ i_error("generate_keypair failed: %s", error);
+ test_exit(1);
+ }
+
+ test_assert(pubid != NULL);
+
+ test_user_key_id = p_strdup(test_ctx->pool, pubid);
+
+ dcrypt_keypair_unref(&pair);
+ error = NULL;
+
+ /* keys ought to be in cache or somewhere...*/
+ if (mail_crypt_user_get_private_key(test_ctx->user, NULL, &pair.priv, &error) <= 0)
+ {
+ i_error("Cannot get user private key: %s", error);
+ }
+
+ test_assert(pair.priv != NULL);
+
+ if (pair.priv != NULL)
+ dcrypt_key_unref_private(&pair.priv);
+
+ test_end();
+}
+
+static void test_generate_inbox_key(void)
+{
+ struct dcrypt_public_key *user_key;
+ struct dcrypt_keypair pair;
+ const char *error = NULL, *pubid = NULL;
+
+ test_begin("generate inbox key");
+
+ if (mail_crypt_user_get_public_key(test_ctx->user, &user_key,
+ &error) <= 0) {
+ i_error("Cannot get user private key: %s", error);
+ }
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(test_ctx->user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ if (mailbox_open(box) < 0)
+ i_fatal("mailbox_open(INBOX) failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ if (mail_crypt_box_generate_keypair(box, &pair, user_key, &pubid,
+ &error) < 0) {
+ i_error("generate_keypair failed: %s", error);
+ test_exit(1);
+ }
+
+ i_assert(pubid != NULL);
+
+ dcrypt_keypair_unref(&pair);
+ dcrypt_key_unref_public(&user_key);
+ mailbox_free(&box);
+
+ test_box_key_id = p_strdup(test_ctx->pool, pubid);
+
+ test_end();
+}
+
+static void test_cache_reset(void)
+{
+ struct dcrypt_keypair pair;
+ const char *error = NULL;
+
+ test_begin("cache reset");
+
+ struct mail_crypt_user *muser =
+ mail_crypt_get_mail_crypt_user(test_ctx->user);
+ mail_crypt_key_cache_destroy(&muser->key_cache);
+
+ test_assert(mail_crypt_user_get_private_key(test_ctx->user, NULL,
+ &pair.priv, &error) > 0);
+ if (error != NULL)
+ i_error("mail_crypt_user_get_private_key() failed: %s", error);
+ error = NULL;
+ test_assert(mail_crypt_user_get_public_key(test_ctx->user,
+ &pair.pub, &error) > 0);
+ if (error != NULL)
+ i_error("mail_crypt_user_get_public_key() failed: %s", error);
+
+ dcrypt_keypair_unref(&pair);
+
+ test_end();
+}
+
+static void test_verify_keys(void)
+{
+ const char *value = "", *error = NULL;
+
+ const char *enc_id;
+ enum dcrypt_key_encryption_type enc_type;
+
+ test_begin("verify keys");
+
+ struct dcrypt_private_key *privkey = NULL, *user_key = NULL;
+ struct dcrypt_public_key *pubkey = NULL;
+
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(test_ctx->user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ if (mailbox_open(box) < 0)
+ i_fatal("mailbox_open(INBOX) failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ /* verify links */
+
+ /* user's public key */
+ test_assert(test_mail_attribute_get(box, TRUE, TRUE, ACTIVE_KEY_NAME,
+ &value, &error) > 0);
+ test_assert(strcmp(value, test_user_key_id) == 0);
+
+ test_assert(test_mail_attribute_get(box, TRUE, TRUE, value, &value,
+ &error) > 0);
+
+ /* load key */
+ test_assert(dcrypt_key_load_public(&pubkey, value, &error) == TRUE);
+
+ /* see if it matches */
+ test_assert(mail_crypt_public_key_id_match(pubkey, test_user_key_id,
+ &error) > 0);
+ dcrypt_key_unref_public(&pubkey);
+
+ /* user's private key */
+ test_assert(test_mail_attribute_get(box, TRUE, FALSE, ACTIVE_KEY_NAME,
+ &value, &error) > 0);
+ test_assert(strcmp(value, test_user_key_id) == 0);
+
+ test_assert(test_mail_attribute_get(box, TRUE, FALSE, value, &value,
+ &error) > 0);
+
+ /* load key */
+ test_assert(dcrypt_key_load_private(&user_key, value, NULL, NULL,
+ &error) == TRUE);
+
+ /* see if it matches */
+ test_assert(mail_crypt_private_key_id_match(user_key, test_user_key_id,
+ &error) > 0);
+
+
+
+
+ /* inbox's public key */
+ test_assert(test_mail_attribute_get(box, FALSE, TRUE, ACTIVE_KEY_NAME,
+ &value, &error) > 0);
+ test_assert(strcmp(value, test_box_key_id) == 0);
+
+ test_assert(test_mail_attribute_get(box, FALSE, TRUE, value, &value,
+ &error) > 0);
+
+ /* load key */
+ test_assert(dcrypt_key_load_public(&pubkey, value, &error) == TRUE);
+
+ /* see if it matches */
+ test_assert(mail_crypt_public_key_id_match(pubkey, test_box_key_id,
+ &error) > 0);
+ dcrypt_key_unref_public(&pubkey);
+
+ /* user's private key */
+ test_assert(test_mail_attribute_get(box, FALSE, FALSE, ACTIVE_KEY_NAME,
+ &value, &error) > 0);
+ test_assert(strcmp(value, test_box_key_id) == 0);
+
+ test_assert(test_mail_attribute_get(box, FALSE, FALSE, value, &value,
+ &error) > 0);
+
+ test_assert(dcrypt_key_string_get_info(value, NULL, NULL, NULL,
+ &enc_type, &enc_id, NULL,
+ &error) == TRUE);
+
+ test_assert(enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_KEY);
+ test_assert(strcmp(enc_id, test_user_key_id) == 0);
+
+ /* load key */
+ test_assert(dcrypt_key_load_private(&privkey, value, NULL, user_key,
+ &error) == TRUE);
+
+ /* see if it matches */
+ test_assert(mail_crypt_private_key_id_match(privkey, test_box_key_id,
+ &error) > 0);
+ dcrypt_key_unref_private(&privkey);
+ dcrypt_key_unref_private(&user_key);
+
+ mailbox_free(&box);
+
+ test_end();
+}
+
+static void test_old_key(void)
+{
+ test_begin("old keys");
+
+ const char *error = NULL;
+ struct dcrypt_private_key *privkey = NULL;
+
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(test_ctx->user->namespaces);
+ struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ if (mailbox_open(box) < 0)
+ i_fatal("mailbox_open(INBOX) failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box, 0, __func__);
+
+ test_mail_attribute_set(t, TRUE, FALSE, mcp_old_user_key_id,
+ mcp_old_user_key, &error);
+ test_mail_attribute_set(t, FALSE, FALSE, mcp_old_box_key_id,
+ mcp_old_box_key, &error);
+
+ (void)mailbox_transaction_commit(&t);
+
+ error = NULL;
+
+ /* try to load old key */
+ test_assert(mail_crypt_get_private_key(box, mcp_old_box_key_id, FALSE, FALSE,
+ &privkey, &error) > 0);
+
+ if (error != NULL)
+ i_error("mail_crypt_get_private_key(%s) failed: %s",
+ mcp_old_box_key_id,
+ error);
+
+ test_assert(privkey != NULL);
+
+ if (privkey != NULL) {
+ buffer_t *key_id = t_buffer_create(32);
+ test_assert(dcrypt_key_id_private_old(privkey, key_id, &error));
+ test_assert(strcmp(binary_to_hex(key_id->data, key_id->used), mcp_old_box_key_id) == 0);
+ dcrypt_key_unref_private(&privkey);
+ }
+
+ mailbox_free(&box);
+
+ test_end();
+}
+
+static void test_setup(void)
+{
+ struct dcrypt_settings set = {
+ .module_dir = top_builddir "/src/lib-dcrypt/.libs"
+ };
+ if (!dcrypt_initialize(NULL, &set, NULL)) {
+ i_info("No functional dcrypt backend found - skipping tests");
+ test_exit(0);
+ }
+ test_ctx = test_mail_storage_init();
+ const char *username = "mcp_test@example.com";
+ const char *const extra_input[] = {
+ t_strdup_printf("mail_crypt_curve=prime256v1"),
+ t_strdup_printf("mail_attribute_dict=file:%s/%s/dovecot-attributes",
+ test_ctx->home_root, username),
+ NULL
+ };
+ struct test_mail_storage_settings storage_set = {
+ .username = username,
+ .driver = "maildir",
+ .hierarchy_sep = "/",
+ .extra_input = extra_input,
+ };
+ test_mail_storage_init_user(test_ctx, &storage_set);
+
+ mail_crypt_key_register_mailbox_internal_attributes();
+}
+
+static void test_teardown(void)
+{
+ struct mail_crypt_user *muser =
+ mail_crypt_get_mail_crypt_user(test_ctx->user);
+ mail_crypt_key_cache_destroy(&muser->key_cache);
+
+ test_mail_storage_deinit_user(test_ctx);
+ test_mail_storage_deinit(&test_ctx);
+ dcrypt_deinitialize();
+}
+
+int main(int argc, char **argv)
+{
+ void (*tests[])(void) = {
+ test_setup,
+ test_generate_user_key,
+ test_generate_inbox_key,
+ test_cache_reset,
+ test_verify_keys,
+ test_old_key,
+ test_teardown,
+ NULL
+ };
+
+
+ master_service = master_service_init("test-mail-key",
+ 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, "");
+ int ret = test_run(tests);
+ master_service_deinit(&master_service);
+ return ret;
+}
diff --git a/src/plugins/mail-log/Makefile.am b/src/plugins/mail-log/Makefile.am
new file mode 100644
index 0000000..2f3fbc6
--- /dev/null
+++ b/src/plugins/mail-log/Makefile.am
@@ -0,0 +1,24 @@
+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/plugins/notify
+
+NOPLUGIN_LDFLAGS =
+lib20_mail_log_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_mail_log_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib20_mail_log_plugin_la_LIBADD = \
+ ../notify/lib15_notify_plugin.la
+endif
+
+lib20_mail_log_plugin_la_SOURCES = \
+ mail-log-plugin.c
+
+noinst_HEADERS = \
+ mail-log-plugin.h
diff --git a/src/plugins/mail-log/Makefile.in b/src/plugins/mail-log/Makefile.in
new file mode 100644
index 0000000..ee39d2d
--- /dev/null
+++ b/src/plugins/mail-log/Makefile.in
@@ -0,0 +1,826 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/mail-log
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib20_mail_log_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../notify/lib15_notify_plugin.la
+am_lib20_mail_log_plugin_la_OBJECTS = mail-log-plugin.lo
+lib20_mail_log_plugin_la_OBJECTS = \
+ $(am_lib20_mail_log_plugin_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 =
+lib20_mail_log_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_mail_log_plugin_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)/mail-log-plugin.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 = $(lib20_mail_log_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_mail_log_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/notify
+
+lib20_mail_log_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_mail_log_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib20_mail_log_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../notify/lib15_notify_plugin.la
+
+lib20_mail_log_plugin_la_SOURCES = \
+ mail-log-plugin.c
+
+noinst_HEADERS = \
+ mail-log-plugin.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/plugins/mail-log/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/mail-log/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-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}; \
+ }
+
+lib20_mail_log_plugin.la: $(lib20_mail_log_plugin_la_OBJECTS) $(lib20_mail_log_plugin_la_DEPENDENCIES) $(EXTRA_lib20_mail_log_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_mail_log_plugin_la_LINK) -rpath $(moduledir) $(lib20_mail_log_plugin_la_OBJECTS) $(lib20_mail_log_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-log-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mail-log-plugin.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-moduleLTLIBRARIES
+
+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-log-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/mail-log/mail-log-plugin.c b/src/plugins/mail-log/mail-log-plugin.c
new file mode 100644
index 0000000..f01cd0d
--- /dev/null
+++ b/src/plugins/mail-log/mail-log-plugin.c
@@ -0,0 +1,546 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "imap-util.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "notify-plugin.h"
+#include "mail-log-plugin.h"
+
+
+#define MAILBOX_NAME_LOG_LEN 64
+#define HEADER_LOG_LEN 80
+
+#define MAIL_LOG_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_log_user_module)
+
+enum mail_log_field {
+ MAIL_LOG_FIELD_UID = 0x01,
+ MAIL_LOG_FIELD_BOX = 0x02,
+ MAIL_LOG_FIELD_MSGID = 0x04,
+ MAIL_LOG_FIELD_PSIZE = 0x08,
+ MAIL_LOG_FIELD_VSIZE = 0x10,
+ MAIL_LOG_FIELD_FLAGS = 0x20,
+ MAIL_LOG_FIELD_FROM = 0x40,
+ MAIL_LOG_FIELD_SUBJECT = 0x80
+};
+#define MAIL_LOG_DEFAULT_FIELDS \
+ (MAIL_LOG_FIELD_UID | MAIL_LOG_FIELD_BOX | \
+ MAIL_LOG_FIELD_MSGID | MAIL_LOG_FIELD_PSIZE)
+
+enum mail_log_event {
+ MAIL_LOG_EVENT_DELETE = 0x01,
+ MAIL_LOG_EVENT_UNDELETE = 0x02,
+ MAIL_LOG_EVENT_EXPUNGE = 0x04,
+ MAIL_LOG_EVENT_SAVE = 0x08,
+ MAIL_LOG_EVENT_COPY = 0x10,
+ MAIL_LOG_EVENT_MAILBOX_CREATE = 0x20,
+ MAIL_LOG_EVENT_MAILBOX_DELETE = 0x40,
+ MAIL_LOG_EVENT_MAILBOX_RENAME = 0x80,
+ MAIL_LOG_EVENT_FLAG_CHANGE = 0x100
+};
+#define MAIL_LOG_DEFAULT_EVENTS \
+ (MAIL_LOG_EVENT_DELETE | MAIL_LOG_EVENT_UNDELETE | \
+ MAIL_LOG_EVENT_EXPUNGE | MAIL_LOG_EVENT_SAVE | MAIL_LOG_EVENT_COPY | \
+ MAIL_LOG_EVENT_MAILBOX_DELETE | MAIL_LOG_EVENT_MAILBOX_RENAME)
+
+static const char *field_names[] = {
+ "uid",
+ "box",
+ "msgid",
+ "size",
+ "vsize",
+ "flags",
+ "from",
+ "subject",
+ NULL
+};
+
+static const char *event_names[] = {
+ "delete",
+ "undelete",
+ "expunge",
+ "save",
+ "copy",
+ "mailbox_create",
+ "mailbox_delete",
+ "mailbox_rename",
+ "flag_change",
+ NULL
+};
+
+struct mail_log_user {
+ union mail_user_module_context module_ctx;
+
+ enum mail_log_field fields;
+ enum mail_log_event events;
+ bool cached_only;
+};
+
+struct mail_log_message {
+ struct mail_log_message *prev, *next;
+
+ enum mail_log_event event;
+ bool ignore;
+ const char *pretext, *text;
+};
+
+struct mail_log_mail_txn_context {
+ pool_t pool;
+ struct mail_log_message *messages, *messages_tail;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_log_user_module,
+ &mail_user_module_register);
+
+static enum mail_log_field mail_log_field_find(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; field_names[i] != NULL; i++) {
+ if (strcmp(name, field_names[i]) == 0)
+ return 1 << i;
+ }
+ return 0;
+}
+
+static enum mail_log_event mail_log_event_find(const char *name)
+{
+ unsigned int i;
+
+ if (strcmp(name, "append") == 0) {
+ /* v1.x backwards compatibility */
+ name = "save";
+ }
+ for (i = 0; event_names[i] != NULL; i++) {
+ if (strcmp(name, event_names[i]) == 0)
+ return 1 << i;
+ }
+ return 0;
+}
+
+static enum mail_log_field mail_log_parse_fields(const char *str)
+{
+ const char *const *tmp;
+ static enum mail_log_field field, fields = 0;
+
+ for (tmp = t_strsplit_spaces(str, ", "); *tmp != NULL; tmp++) {
+ field = mail_log_field_find(*tmp);
+ if (field == 0)
+ i_fatal("Unknown field in mail_log_fields: '%s'", *tmp);
+ fields |= field;
+ }
+ return fields;
+}
+
+static enum mail_log_event mail_log_parse_events(const char *str)
+{
+ const char *const *tmp;
+ static enum mail_log_event event, events = 0;
+
+ for (tmp = t_strsplit_spaces(str, ", "); *tmp != NULL; tmp++) {
+ event = mail_log_event_find(*tmp);
+ if (event == 0)
+ i_fatal("Unknown event in mail_log_events: '%s'", *tmp);
+ events |= event;
+ }
+ return events;
+}
+
+static void mail_log_mail_user_created(struct mail_user *user)
+{
+ struct mail_log_user *muser;
+ const char *str;
+
+ muser = p_new(user->pool, struct mail_log_user, 1);
+ MODULE_CONTEXT_SET(user, mail_log_user_module, muser);
+
+ str = mail_user_plugin_getenv(user, "mail_log_fields");
+ muser->fields = str == NULL ? MAIL_LOG_DEFAULT_FIELDS :
+ mail_log_parse_fields(str);
+
+ str = mail_user_plugin_getenv(user, "mail_log_events");
+ muser->events = str == NULL ? MAIL_LOG_DEFAULT_EVENTS :
+ mail_log_parse_events(str);
+
+ muser->cached_only =
+ mail_user_plugin_getenv_bool(user, "mail_log_cached_only");
+}
+
+static void mail_log_append_mailbox_name(string_t *str, struct mail *mail)
+{
+ const char *mailbox_str;
+
+ mailbox_str = mailbox_get_vname(mail->box);
+ str_printfa(str, "box=%s",
+ str_sanitize(mailbox_str, MAILBOX_NAME_LOG_LEN));
+}
+
+static void
+mail_log_append_mail_header(string_t *str, struct mail *mail,
+ const char *name, const char *header)
+{
+ const char *value;
+
+ if (mail_get_first_header_utf8(mail, header, &value) <= 0)
+ value = "";
+ str_printfa(str, "%s=%s", name, str_sanitize(value, HEADER_LOG_LEN));
+}
+
+static void
+mail_log_append_uid(struct mail_log_mail_txn_context *ctx,
+ struct mail_log_message *msg, string_t *str, uint32_t uid)
+{
+ if (uid != 0)
+ str_printfa(str, "uid=%u", uid);
+ else {
+ /* we don't know the uid yet, assign it later */
+ str_printfa(str, "uid=");
+ msg->pretext = p_strdup(ctx->pool, str_c(str));
+ str_truncate(str, 0);
+ }
+}
+
+static void
+mail_log_update_wanted_fields(struct mail *mail, enum mail_log_field fields)
+{
+ enum mail_fetch_field wanted_fields = 0;
+ struct mailbox_header_lookup_ctx *wanted_headers = NULL;
+ const char *headers[4];
+ unsigned int hdr_idx = 0;
+
+ if ((fields & MAIL_LOG_FIELD_MSGID) != 0)
+ headers[hdr_idx++] = "Message-ID";
+ if ((fields & MAIL_LOG_FIELD_FROM) != 0)
+ headers[hdr_idx++] = "From";
+ if ((fields & MAIL_LOG_FIELD_SUBJECT) != 0)
+ headers[hdr_idx++] = "Subject";
+ if (hdr_idx > 0) {
+ i_assert(hdr_idx < N_ELEMENTS(headers));
+ headers[hdr_idx] = NULL;
+ wanted_headers = mailbox_header_lookup_init(mail->box, headers);
+ }
+
+ if ((fields & MAIL_LOG_FIELD_PSIZE) != 0)
+ wanted_fields |= MAIL_FETCH_PHYSICAL_SIZE;
+ if ((fields & MAIL_LOG_FIELD_VSIZE) != 0)
+ wanted_fields |= MAIL_FETCH_VIRTUAL_SIZE;
+
+ mail_add_temp_wanted_fields(mail, wanted_fields, wanted_headers);
+ mailbox_header_lookup_unref(&wanted_headers);
+}
+
+static void
+mail_log_append_mail_message_real(struct mail_log_mail_txn_context *ctx,
+ struct mail *mail, enum mail_log_event event,
+ const char *desc)
+{
+ struct mail_log_user *muser =
+ MAIL_LOG_USER_CONTEXT(mail->box->storage->user);
+ struct mail_log_message *msg;
+ string_t *text;
+ uoff_t size;
+
+ msg = p_new(ctx->pool, struct mail_log_message, 1);
+
+ /* avoid parsing through the message multiple times */
+ mail_log_update_wanted_fields(mail, muser->fields);
+
+ text = t_str_new(128);
+ str_append(text, desc);
+ str_append(text, ": ");
+ if ((muser->fields & MAIL_LOG_FIELD_BOX) != 0) {
+ mail_log_append_mailbox_name(text, mail);
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_UID) != 0) {
+ if (event != MAIL_LOG_EVENT_SAVE &&
+ event != MAIL_LOG_EVENT_COPY)
+ mail_log_append_uid(ctx, msg, text, mail->uid);
+ else {
+ /* with mbox mail->uid contains the uid, but handle
+ this consistently with all mailbox formats */
+ mail_log_append_uid(ctx, msg, text, 0);
+ }
+ /* make sure UID is assigned to this mail */
+ mail->transaction->flags |= MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS;
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_MSGID) != 0) {
+ mail_log_append_mail_header(text, mail, "msgid", "Message-ID");
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_PSIZE) != 0) {
+ if (mail_get_physical_size(mail, &size) == 0)
+ str_printfa(text, "size=%"PRIuUOFF_T, size);
+ else
+ str_printfa(text, "size=error");
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_VSIZE) != 0) {
+ if (mail_get_virtual_size(mail, &size) == 0)
+ str_printfa(text, "vsize=%"PRIuUOFF_T, size);
+ else
+ str_printfa(text, "vsize=error");
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_FROM) != 0) {
+ mail_log_append_mail_header(text, mail, "from", "From");
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_SUBJECT) != 0) {
+ mail_log_append_mail_header(text, mail, "subject", "Subject");
+ str_append(text, ", ");
+ }
+ if ((muser->fields & MAIL_LOG_FIELD_FLAGS) != 0) {
+ str_printfa(text, "flags=(");
+ imap_write_flags(text, mail_get_flags(mail),
+ mail_get_keywords(mail));
+ str_append(text, "), ");
+ }
+ str_truncate(text, str_len(text)-2);
+
+ msg->event = event;
+ msg->text = p_strdup(ctx->pool, str_c(text));
+ DLLIST2_APPEND(&ctx->messages, &ctx->messages_tail, msg);
+}
+
+static void mail_log_add_dummy_msg(struct mail_log_mail_txn_context *ctx,
+ enum mail_log_event event)
+{
+ struct mail_log_message *msg;
+
+ msg = p_new(ctx->pool, struct mail_log_message, 1);
+ msg->event = event;
+ msg->ignore = TRUE;
+ DLLIST2_APPEND(&ctx->messages, &ctx->messages_tail, msg);
+}
+
+static void
+mail_log_append_mail_message(struct mail_log_mail_txn_context *ctx,
+ struct mail *mail, enum mail_log_event event,
+ const char *desc)
+{
+ struct mail_log_user *muser =
+ MAIL_LOG_USER_CONTEXT(mail->box->storage->user);
+
+ if ((muser->events & event) == 0) {
+ if (event == MAIL_LOG_EVENT_SAVE ||
+ event == MAIL_LOG_EVENT_COPY)
+ mail_log_add_dummy_msg(ctx, event);
+ return;
+ }
+
+ T_BEGIN {
+ enum mail_lookup_abort orig_lookup_abort = mail->lookup_abort;
+
+ if (event != MAIL_LOG_EVENT_SAVE && muser->cached_only)
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ mail_log_append_mail_message_real(ctx, mail, event, desc);
+ mail->lookup_abort = orig_lookup_abort;
+ } T_END;
+}
+
+static void *
+mail_log_mail_transaction_begin(struct mailbox_transaction_context *t ATTR_UNUSED)
+{
+ pool_t pool;
+ struct mail_log_mail_txn_context *ctx;
+
+ pool = pool_alloconly_create("mail-log", 2048);
+ ctx = p_new(pool, struct mail_log_mail_txn_context, 1);
+ ctx->pool = pool;
+ return ctx;
+}
+
+static void mail_log_mail_save(void *txn, struct mail *mail)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+
+ mail_log_append_mail_message(ctx, mail, MAIL_LOG_EVENT_SAVE, "save");
+}
+
+static void mail_log_mail_copy(void *txn, struct mail *src, struct mail *dst)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+ struct mail_private *src_pmail = (struct mail_private *)src;
+ struct mailbox *src_box = src->box;
+ const char *desc;
+
+ if (src_pmail->vmail != NULL) {
+ /* copying a mail from virtual storage. src points to the
+ backend mail, but we want to log the virtual mailbox name. */
+ src_box = src_pmail->vmail->box;
+ }
+ desc = t_strdup_printf("copy from %s",
+ str_sanitize(mailbox_get_vname(src_box),
+ MAILBOX_NAME_LOG_LEN));
+ mail_log_append_mail_message(ctx, dst,
+ MAIL_LOG_EVENT_COPY, desc);
+}
+
+static void mail_log_mail_expunge(void *txn, struct mail *mail)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+ struct mail_private *p = (struct mail_private*)mail;
+
+ mail_log_append_mail_message(ctx, mail, MAIL_LOG_EVENT_EXPUNGE,
+ p->autoexpunged ? "autoexpunge" : "expunge");
+}
+
+static void mail_log_mail_update_flags(void *txn, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+ enum mail_flags new_flags = mail_get_flags(mail);
+
+ if (((old_flags ^ new_flags) & MAIL_DELETED) == 0) {
+ mail_log_append_mail_message(ctx, mail,
+ MAIL_LOG_EVENT_FLAG_CHANGE,
+ "flag_change");
+ } else if ((old_flags & MAIL_DELETED) == 0) {
+ mail_log_append_mail_message(ctx, mail, MAIL_LOG_EVENT_DELETE,
+ "delete");
+ } else {
+ mail_log_append_mail_message(ctx, mail, MAIL_LOG_EVENT_UNDELETE,
+ "undelete");
+ }
+}
+
+static void
+mail_log_mail_update_keywords(void *txn, struct mail *mail,
+ const char *const *old_keywords ATTR_UNUSED)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+
+ mail_log_append_mail_message(ctx, mail, MAIL_LOG_EVENT_FLAG_CHANGE,
+ "flag_change");
+}
+
+static void mail_log_save(const struct mail_log_message *msg, uint32_t uid)
+{
+ if (msg->ignore) {
+ /* not logging this save/copy */
+ } else if (msg->pretext == NULL)
+ i_info("%s", msg->text);
+ else if (uid != 0)
+ i_info("%s%u%s", msg->pretext, uid, msg->text);
+ else
+ i_info("%serror%s", msg->pretext, msg->text);
+}
+
+static void
+mail_log_mail_transaction_commit(void *txn,
+ struct mail_transaction_commit_changes *changes)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+ struct mail_log_message *msg;
+ struct seq_range_iter iter;
+ unsigned int n = 0;
+ uint32_t uid;
+
+ seq_range_array_iter_init(&iter, &changes->saved_uids);
+ for (msg = ctx->messages; msg != NULL; msg = msg->next) {
+ if (msg->event == MAIL_LOG_EVENT_SAVE ||
+ msg->event == MAIL_LOG_EVENT_COPY) {
+ if (!seq_range_array_iter_nth(&iter, n++, &uid))
+ uid = 0;
+ mail_log_save(msg, uid);
+ } else {
+ i_assert(msg->pretext == NULL);
+ i_info("%s", msg->text);
+ }
+ }
+ i_assert(!seq_range_array_iter_nth(&iter, n, &uid));
+
+ pool_unref(&ctx->pool);
+}
+
+static void mail_log_mail_transaction_rollback(void *txn)
+{
+ struct mail_log_mail_txn_context *ctx =
+ (struct mail_log_mail_txn_context *)txn;
+
+ pool_unref(&ctx->pool);
+}
+
+static void
+mail_log_mailbox_create(struct mailbox *box)
+{
+ struct mail_log_user *muser = MAIL_LOG_USER_CONTEXT(box->storage->user);
+
+ if ((muser->events & MAIL_LOG_EVENT_MAILBOX_CREATE) == 0)
+ return;
+
+ i_info("Mailbox created: %s",
+ str_sanitize(mailbox_get_vname(box), MAILBOX_NAME_LOG_LEN));
+}
+
+static void
+mail_log_mailbox_delete_commit(void *txn ATTR_UNUSED, struct mailbox *box)
+{
+ struct mail_log_user *muser = MAIL_LOG_USER_CONTEXT(box->storage->user);
+
+ if ((muser->events & MAIL_LOG_EVENT_MAILBOX_DELETE) == 0)
+ return;
+
+ i_info("Mailbox deleted: %s",
+ str_sanitize(mailbox_get_vname(box), MAILBOX_NAME_LOG_LEN));
+}
+
+static void
+mail_log_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ struct mail_log_user *muser = MAIL_LOG_USER_CONTEXT(src->storage->user);
+
+ if ((muser->events & MAIL_LOG_EVENT_MAILBOX_RENAME) == 0)
+ return;
+
+ i_info("Mailbox renamed: %s -> %s",
+ str_sanitize(mailbox_get_vname(src), MAILBOX_NAME_LOG_LEN),
+ str_sanitize(mailbox_get_vname(dest), MAILBOX_NAME_LOG_LEN));
+}
+
+static const struct notify_vfuncs mail_log_vfuncs = {
+ .mail_transaction_begin = mail_log_mail_transaction_begin,
+ .mail_save = mail_log_mail_save,
+ .mail_copy = mail_log_mail_copy,
+ .mail_expunge = mail_log_mail_expunge,
+ .mail_update_flags = mail_log_mail_update_flags,
+ .mail_update_keywords = mail_log_mail_update_keywords,
+ .mail_transaction_commit = mail_log_mail_transaction_commit,
+ .mail_transaction_rollback = mail_log_mail_transaction_rollback,
+ .mailbox_create = mail_log_mailbox_create,
+ .mailbox_delete_commit = mail_log_mailbox_delete_commit,
+ .mailbox_rename = mail_log_mailbox_rename
+};
+
+static struct notify_context *mail_log_ctx;
+
+static struct mail_storage_hooks mail_log_mail_storage_hooks = {
+ .mail_user_created = mail_log_mail_user_created
+};
+
+void mail_log_plugin_init(struct module *module)
+{
+ mail_log_ctx = notify_register(&mail_log_vfuncs);
+ mail_storage_hooks_add(module, &mail_log_mail_storage_hooks);
+}
+
+void mail_log_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&mail_log_mail_storage_hooks);
+ notify_unregister(mail_log_ctx);
+}
+
+const char *mail_log_plugin_dependencies[] = { "notify", NULL };
diff --git a/src/plugins/mail-log/mail-log-plugin.h b/src/plugins/mail-log/mail-log-plugin.h
new file mode 100644
index 0000000..0af47ac
--- /dev/null
+++ b/src/plugins/mail-log/mail-log-plugin.h
@@ -0,0 +1,9 @@
+#ifndef MAIL_LOG_PLUGIN_H
+#define MAIL_LOG_PLUGIN_H
+
+extern const char *mail_log_plugin_dependencies[];
+
+void mail_log_plugin_init(struct module *module);
+void mail_log_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/mail-lua/Makefile.am b/src/plugins/mail-lua/Makefile.am
new file mode 100644
index 0000000..1698cb4
--- /dev/null
+++ b/src/plugins/mail-lua/Makefile.am
@@ -0,0 +1,32 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-lua \
+ $(LUA_CFLAGS)
+
+NOPLUGIN_LDFLAGS =
+
+module_LTLIBRARIES = \
+ lib01_mail_lua_plugin.la
+
+lib01_mail_lua_plugin_la_LDFLAGS = -module -avoid-version $(LUA_LIBS)
+lib01_mail_lua_plugin_la_LIBADD = \
+ ../../lib-storage/libdovecot-storage-lua.la \
+ $(LIBDOVECOT)
+lib01_mail_lua_plugin_la_SOURCES = \
+ mail-lua-plugin.c
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ mail-lua-plugin.h
+
+test_programs =
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+noinst_PROGRAMS = $(test_programs)
diff --git a/src/plugins/mail-lua/Makefile.in b/src/plugins/mail-lua/Makefile.in
new file mode 100644
index 0000000..33bb878
--- /dev/null
+++ b/src/plugins/mail-lua/Makefile.in
@@ -0,0 +1,873 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/mail-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 =
+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)$(moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+lib01_mail_lua_plugin_la_DEPENDENCIES = \
+ ../../lib-storage/libdovecot-storage-lua.la \
+ $(am__DEPENDENCIES_1)
+am_lib01_mail_lua_plugin_la_OBJECTS = mail-lua-plugin.lo
+lib01_mail_lua_plugin_la_OBJECTS = \
+ $(am_lib01_mail_lua_plugin_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 =
+lib01_mail_lua_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib01_mail_lua_plugin_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)/mail-lua-plugin.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 = $(lib01_mail_lua_plugin_la_SOURCES)
+DIST_SOURCES = $(lib01_mail_lua_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-lua \
+ $(LUA_CFLAGS)
+
+module_LTLIBRARIES = \
+ lib01_mail_lua_plugin.la
+
+lib01_mail_lua_plugin_la_LDFLAGS = -module -avoid-version $(LUA_LIBS)
+lib01_mail_lua_plugin_la_LIBADD = \
+ ../../lib-storage/libdovecot-storage-lua.la \
+ $(LIBDOVECOT)
+
+lib01_mail_lua_plugin_la_SOURCES = \
+ mail-lua-plugin.c
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ mail-lua-plugin.h
+
+test_programs =
+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/plugins/mail-lua/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/mail-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-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}; \
+ }
+
+lib01_mail_lua_plugin.la: $(lib01_mail_lua_plugin_la_OBJECTS) $(lib01_mail_lua_plugin_la_DEPENDENCIES) $(EXTRA_lib01_mail_lua_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib01_mail_lua_plugin_la_LINK) -rpath $(moduledir) $(lib01_mail_lua_plugin_la_OBJECTS) $(lib01_mail_lua_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-lua-plugin.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 $(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-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mail-lua-plugin.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-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)/mail-lua-plugin.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-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-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
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(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/plugins/mail-lua/mail-lua-plugin.c b/src/plugins/mail-lua/mail-lua-plugin.c
new file mode 100644
index 0000000..146d95b
--- /dev/null
+++ b/src/plugins/mail-lua/mail-lua-plugin.c
@@ -0,0 +1,167 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "mail-lua-plugin.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-private.h"
+#include "mail-storage-hooks.h"
+#include "dlua-script-private.h"
+
+#define MAIL_LUA_SCRIPT "mail_lua_script"
+#define MAIL_LUA_USER_CREATED_FN "mail_user_created"
+#define MAIL_LUA_USER_DEINIT_FN "mail_user_deinit"
+#define MAIL_LUA_USER_DEINIT_PRE_FN "mail_user_deinit_pre"
+#define MAIL_LUA_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mail_lua_user_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_lua_user_module,
+ &mail_user_module_register);
+
+struct mail_lua_user_context {
+ union mail_user_module_context module_ctx;
+ struct dlua_script *script;
+};
+
+static int mail_lua_call_hook(struct dlua_script *script,
+ struct mail_user *user,
+ const char *hook,
+ const char **error_r)
+{
+ const char *error;
+
+ if (!dlua_script_has_function(script, hook))
+ return 0;
+
+ if (user->mail_debug)
+ e_debug(user->event, "mail-lua: Calling %s(user)", hook);
+
+ dlua_push_mail_user(script->L, user);
+
+ if (dlua_pcall(script->L, hook, 1, 2, &error) < 0) {
+ *error_r = t_strdup_printf("%s(user) failed: %s", hook, error);
+ return -1;
+ }
+
+ int ret = lua_tonumber(script->L, -2);
+ const char *errmsg = lua_tostring(script->L, -1);
+
+ if (ret < 0) {
+ *error_r = t_strdup_printf("%s(user) failed: %s",
+ hook, errmsg);
+ }
+
+ lua_pop(script->L, 2);
+ (void)lua_gc(script->L, LUA_GCCOLLECT, 0);
+
+ return ret < 0 ? -1 : 1;
+}
+
+static void mail_lua_user_deinit_pre(struct mail_user *user)
+{
+ struct mail_lua_user_context *luser = MAIL_LUA_USER_CONTEXT(user);
+ const char *error;
+
+ if (luser == NULL)
+ return;
+
+ if (mail_lua_call_hook(luser->script, user, MAIL_LUA_USER_DEINIT_PRE_FN,
+ &error) < 0) {
+ e_error(user->event, "mail-lua: %s", error);
+ }
+
+ luser->module_ctx.super.deinit_pre(user);
+}
+
+static void mail_lua_user_deinit(struct mail_user *user)
+{
+ struct mail_lua_user_context *luser = MAIL_LUA_USER_CONTEXT(user);
+ const char *error;
+
+ if (luser == NULL)
+ return;
+
+ luser->module_ctx.super.deinit(user);
+
+ if (mail_lua_call_hook(luser->script, user, MAIL_LUA_USER_DEINIT_FN,
+ &error) < 0) {
+ e_error(user->event, "mail-lua: %s", error);
+ }
+
+ dlua_script_unref(&luser->script);
+}
+
+static void mail_lua_user_created(struct mail_user *user)
+{
+ struct mail_lua_user_context *luser;
+ struct mail_user_vfuncs *v = user->vlast;
+ struct dlua_script *script;
+ const char *error;
+ const char *script_fn = mail_user_plugin_getenv(user, MAIL_LUA_SCRIPT);
+ int ret;
+
+ if (script_fn == NULL)
+ return;
+
+ if (dlua_script_create_file(script_fn, &script, user->event, &error) < 0) {
+ user->error = p_strdup_printf(user->pool, "dlua_script_create_file(%s) failed: %s",
+ script_fn, error);
+ return;
+ }
+
+ dlua_dovecot_register(script);
+ dlua_register_mail_storage(script);
+
+ /* init */
+ if (dlua_script_init(script, &error) < 0) {
+ user->error = p_strdup_printf(user->pool, "dlua_script_init(%s) failed: %s",
+ script_fn, error);
+ dlua_script_unref(&script);
+ return;
+ }
+
+ /* call postlogin hook */
+ if ((ret = mail_lua_call_hook(script, user, MAIL_LUA_USER_CREATED_FN,
+ &error)) <= 0) {
+ if (ret < 0)
+ user->error = p_strdup(user->pool, error);
+ dlua_script_unref(&script);
+ return;
+ }
+
+ luser = p_new(user->pool, struct mail_lua_user_context, 1);
+ luser->module_ctx.super = *v;
+ v->deinit_pre = mail_lua_user_deinit_pre;
+ v->deinit = mail_lua_user_deinit;
+ luser->script = script;
+ user->vlast = &luser->module_ctx.super;
+
+ MODULE_CONTEXT_SET(user, mail_lua_user_module, luser);
+}
+
+bool mail_lua_plugin_get_script(struct mail_user *user,
+ struct dlua_script **script_r)
+{
+ struct mail_lua_user_context *luser = MAIL_LUA_USER_CONTEXT(user);
+ if (luser != NULL) {
+ *script_r = luser->script;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static const struct mail_storage_hooks mail_lua_hooks = {
+ .mail_user_created = mail_lua_user_created,
+};
+
+void mail_lua_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &mail_lua_hooks);
+}
+
+void mail_lua_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&mail_lua_hooks);
+}
+
+const char *mail_lua_plugin_dependencies[] = { NULL };
diff --git a/src/plugins/mail-lua/mail-lua-plugin.h b/src/plugins/mail-lua/mail-lua-plugin.h
new file mode 100644
index 0000000..e3bfb98
--- /dev/null
+++ b/src/plugins/mail-lua/mail-lua-plugin.h
@@ -0,0 +1,14 @@
+#ifndef MAIL_LUA_PLUGIN_H
+#define MAIL_LUA_PLUGIN_H 1
+
+struct dlua_script;
+struct mail_user;
+struct module;
+
+void mail_lua_plugin_init(struct module *module);
+void mail_lua_plugin_deinit(void);
+
+bool mail_lua_plugin_get_script(struct mail_user *user,
+ struct dlua_script **script_r);
+
+#endif
diff --git a/src/plugins/mailbox-alias/Makefile.am b/src/plugins/mailbox-alias/Makefile.am
new file mode 100644
index 0000000..fa5f785
--- /dev/null
+++ b/src/plugins/mailbox-alias/Makefile.am
@@ -0,0 +1,18 @@
+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
+
+NOPLUGIN_LDFLAGS =
+lib20_mailbox_alias_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_mailbox_alias_plugin.la
+
+lib20_mailbox_alias_plugin_la_SOURCES = \
+ mailbox-alias-plugin.c
+
+noinst_HEADERS = \
+ mailbox-alias-plugin.h
diff --git a/src/plugins/mailbox-alias/Makefile.in b/src/plugins/mailbox-alias/Makefile.in
new file mode 100644
index 0000000..fafe3be
--- /dev/null
+++ b/src/plugins/mailbox-alias/Makefile.in
@@ -0,0 +1,821 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/mailbox-alias
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_mailbox_alias_plugin_la_LIBADD =
+am_lib20_mailbox_alias_plugin_la_OBJECTS = mailbox-alias-plugin.lo
+lib20_mailbox_alias_plugin_la_OBJECTS = \
+ $(am_lib20_mailbox_alias_plugin_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 =
+lib20_mailbox_alias_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_mailbox_alias_plugin_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)/mailbox-alias-plugin.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 = $(lib20_mailbox_alias_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_mailbox_alias_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib20_mailbox_alias_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_mailbox_alias_plugin.la
+
+lib20_mailbox_alias_plugin_la_SOURCES = \
+ mailbox-alias-plugin.c
+
+noinst_HEADERS = \
+ mailbox-alias-plugin.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/plugins/mailbox-alias/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/mailbox-alias/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-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}; \
+ }
+
+lib20_mailbox_alias_plugin.la: $(lib20_mailbox_alias_plugin_la_OBJECTS) $(lib20_mailbox_alias_plugin_la_DEPENDENCIES) $(EXTRA_lib20_mailbox_alias_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_mailbox_alias_plugin_la_LINK) -rpath $(moduledir) $(lib20_mailbox_alias_plugin_la_OBJECTS) $(lib20_mailbox_alias_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-alias-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mailbox-alias-plugin.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-moduleLTLIBRARIES
+
+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)/mailbox-alias-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/mailbox-alias/mailbox-alias-plugin.c b/src/plugins/mailbox-alias/mailbox-alias-plugin.c
new file mode 100644
index 0000000..ebd433c
--- /dev/null
+++ b/src/plugins/mailbox-alias/mailbox-alias-plugin.c
@@ -0,0 +1,356 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-storage-hooks.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "mailbox-alias-plugin.h"
+
+#define MAILBOX_ALIAS_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mailbox_alias_user_module)
+#define MAILBOX_ALIAS_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mailbox_alias_storage_module)
+#define MAILBOX_ALIAS_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mailbox_alias_mailbox_list_module)
+
+struct mailbox_alias {
+ const char *old_vname, *new_vname;
+};
+
+struct mailbox_alias_user {
+ union mail_user_module_context module_ctx;
+
+ ARRAY(struct mailbox_alias) aliases;
+};
+
+struct mailbox_alias_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+};
+
+struct mailbox_alias_mailbox {
+ union mailbox_module_context module_ctx;
+};
+
+enum mailbox_symlink_existence {
+ MAILBOX_SYMLINK_EXISTENCE_NONEXISTENT,
+ MAILBOX_SYMLINK_EXISTENCE_SYMLINK,
+ MAILBOX_SYMLINK_EXISTENCE_NOT_SYMLINK
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mailbox_alias_user_module,
+ &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(mailbox_alias_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(mailbox_alias_mailbox_list_module,
+ &mailbox_list_module_register);
+
+const char *mailbox_alias_plugin_version = DOVECOT_ABI_VERSION;
+
+static const char *
+mailbox_alias_find_new(struct mail_user *user, const char *new_vname)
+{
+ struct mailbox_alias_user *auser = MAILBOX_ALIAS_USER_CONTEXT(user);
+ const struct mailbox_alias *alias;
+
+ array_foreach(&auser->aliases, alias) {
+ if (strcmp(alias->new_vname, new_vname) == 0)
+ return alias->old_vname;
+ }
+ return NULL;
+}
+
+static int mailbox_symlink_exists(struct mailbox_list *list, const char *vname,
+ enum mailbox_symlink_existence *existence_r)
+{
+ struct mailbox_alias_mailbox_list *alist =
+ MAILBOX_ALIAS_LIST_CONTEXT(list);
+ struct stat st;
+ const char *symlink_name, *symlink_path;
+ int ret;
+
+ symlink_name = alist->module_ctx.super.get_storage_name(list, vname);
+ ret = mailbox_list_get_path(list, symlink_name,
+ MAILBOX_LIST_PATH_TYPE_DIR, &symlink_path);
+ if (ret < 0)
+ return -1;
+ i_assert(ret > 0);
+
+ if (lstat(symlink_path, &st) < 0) {
+ if (errno == ENOENT) {
+ *existence_r = MAILBOX_SYMLINK_EXISTENCE_NONEXISTENT;
+ return 0;
+ }
+ mailbox_list_set_critical(list,
+ "lstat(%s) failed: %m", symlink_path);
+ return -1;
+ }
+ if (S_ISLNK(st.st_mode))
+ *existence_r = MAILBOX_SYMLINK_EXISTENCE_SYMLINK;
+ else
+ *existence_r = MAILBOX_SYMLINK_EXISTENCE_NOT_SYMLINK;
+ return 0;
+}
+
+static int mailbox_is_alias_symlink(struct mailbox *box)
+{
+ enum mailbox_symlink_existence existence;
+
+ if (mailbox_alias_find_new(box->storage->user, box->vname) == NULL)
+ return 0;
+ if (mailbox_symlink_exists(box->list, box->vname, &existence) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ return existence == MAILBOX_SYMLINK_EXISTENCE_SYMLINK ? 1 : 0;
+}
+
+static int
+mailbox_has_aliases(struct mailbox_list *list, const char *old_vname)
+{
+ struct mailbox_alias_user *auser =
+ MAILBOX_ALIAS_USER_CONTEXT(list->ns->user);
+ const struct mailbox_alias *alias;
+ enum mailbox_symlink_existence existence;
+ int ret = 0;
+
+ array_foreach(&auser->aliases, alias) {
+ if (strcmp(alias->old_vname, old_vname) == 0) {
+ if (mailbox_symlink_exists(list, alias->new_vname,
+ &existence) < 0)
+ ret = -1;
+ else if (existence == MAILBOX_SYMLINK_EXISTENCE_SYMLINK)
+ return 1;
+ }
+ }
+ return ret;
+}
+
+static int
+mailbox_alias_create_symlink(struct mailbox *box,
+ const char *old_name, const char *new_name)
+{
+ const char *old_path, *new_path, *fname;
+ int ret;
+
+ ret = mailbox_list_get_path(box->list, old_name,
+ MAILBOX_LIST_PATH_TYPE_DIR, &old_path);
+ if (ret > 0) {
+ ret = mailbox_list_get_path(box->list, new_name,
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ &new_path);
+ }
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox aliases not supported by storage");
+ return -1;
+ }
+ fname = strrchr(old_path, '/');
+ i_assert(fname != NULL);
+ fname++;
+ i_assert(strncmp(new_path, old_path, fname-old_path) == 0);
+
+ if (symlink(fname, new_path) < 0) {
+ if (errno == EEXIST) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ }
+ mailbox_set_critical(box,
+ "symlink(%s, %s) failed: %m", fname, new_path);
+ return -1;
+ }
+ return 0;
+}
+
+static const char *
+mailbox_alias_get_storage_name(struct mailbox_list *list, const char *vname)
+{
+ struct mailbox_alias_mailbox_list *alist =
+ MAILBOX_ALIAS_LIST_CONTEXT(list);
+ const char *old_vname;
+ enum mailbox_symlink_existence existence;
+
+ /* access the old mailbox so that e.g. full text search won't
+ index the mailbox twice. this also means that deletion must be
+ careful to delete the symlink, box->name. */
+ old_vname = mailbox_alias_find_new(list->ns->user, vname);
+ if (old_vname != NULL &&
+ mailbox_symlink_exists(list, vname, &existence) == 0 &&
+ existence != MAILBOX_SYMLINK_EXISTENCE_NOT_SYMLINK)
+ vname = old_vname;
+
+ return alist->module_ctx.super.get_storage_name(list, vname);
+}
+
+static int
+mailbox_alias_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ struct mailbox_alias_mailbox *abox = MAILBOX_ALIAS_CONTEXT(box);
+ struct mailbox_alias_mailbox_list *alist =
+ MAILBOX_ALIAS_LIST_CONTEXT(box->list);
+ const char *symlink_name;
+ int ret;
+
+ ret = abox->module_ctx.super.create_box(box, update, directory);
+ if (mailbox_alias_find_new(box->storage->user, box->vname) == NULL)
+ return ret;
+ if (ret < 0 && mailbox_get_last_mail_error(box) != MAIL_ERROR_EXISTS)
+ return ret;
+
+ /* all the code so far has actually only created the original
+ mailbox. now we'll create the symlink if it's missing. */
+ symlink_name = alist->module_ctx.super.
+ get_storage_name(box->list, box->vname);
+ return mailbox_alias_create_symlink(box, box->name, symlink_name);
+}
+
+static int mailbox_alias_delete(struct mailbox *box)
+{
+ struct mailbox_alias_mailbox *abox = MAILBOX_ALIAS_CONTEXT(box);
+ struct mailbox_alias_mailbox_list *alist =
+ MAILBOX_ALIAS_LIST_CONTEXT(box->list);
+ const char *symlink_name;
+ int ret;
+
+ ret = mailbox_has_aliases(box->list, box->vname);
+ if (ret < 0)
+ return -1;
+ if (ret > 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't delete mailbox while it has aliases");
+ return -1;
+ }
+
+ if ((ret = mailbox_is_alias_symlink(box)) < 0)
+ return -1;
+ if (ret > 0) {
+ /* we're deleting an alias mailbox. we'll need to handle this
+ explicitly since box->name points to the original mailbox */
+ symlink_name = alist->module_ctx.super.
+ get_storage_name(box->list, box->vname);
+ if (mailbox_list_delete_symlink(box->list, symlink_name) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ return 0;
+ }
+
+ return abox->module_ctx.super.delete_box(box);
+}
+
+static int mailbox_alias_rename(struct mailbox *src, struct mailbox *dest)
+{
+ struct mailbox_alias_mailbox *abox = MAILBOX_ALIAS_CONTEXT(src);
+ int ret;
+
+ if ((ret = mailbox_is_alias_symlink(src)) < 0)
+ return -1;
+ else if (ret > 0) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename alias mailboxes");
+ return -1;
+ }
+ if ((ret = mailbox_is_alias_symlink(dest)) < 0)
+ return -1;
+ else if (ret > 0) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename to mailbox alias");
+ return -1;
+ }
+ ret = mailbox_has_aliases(src->list, src->vname);
+ if (ret < 0)
+ return -1;
+ if (ret > 0) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename mailbox while it has aliases");
+ return -1;
+ }
+
+ return abox->module_ctx.super.rename_box(src, dest);
+}
+
+static void mailbox_alias_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct mailbox_alias_user *auser;
+ struct mailbox_alias *alias;
+ string_t *oldkey, *newkey;
+ const char *old_vname, *new_vname;
+ unsigned int i;
+
+ auser = p_new(user->pool, struct mailbox_alias_user, 1);
+ auser->module_ctx.super = *v;
+ user->vlast = &auser->module_ctx.super;
+
+ p_array_init(&auser->aliases, user->pool, 8);
+
+ oldkey = t_str_new(32);
+ newkey = t_str_new(32);
+ str_append(oldkey, "mailbox_alias_old");
+ str_append(newkey, "mailbox_alias_new");
+ for (i = 2;; i++) {
+ old_vname = mail_user_plugin_getenv(user, str_c(oldkey));
+ new_vname = mail_user_plugin_getenv(user, str_c(newkey));
+ if (old_vname == NULL || new_vname == NULL)
+ break;
+
+ alias = array_append_space(&auser->aliases);
+ alias->old_vname = old_vname;
+ alias->new_vname = new_vname;
+
+ str_truncate(oldkey, 0);
+ str_truncate(newkey, 0);
+ str_printfa(oldkey, "mailbox_alias_old%u", i);
+ str_printfa(newkey, "mailbox_alias_new%u", i);
+ }
+
+ MODULE_CONTEXT_SET(user, mailbox_alias_user_module, auser);
+}
+
+static void mailbox_alias_mailbox_list_created(struct mailbox_list *list)
+{
+ struct mailbox_list_vfuncs *v = list->vlast;
+ struct mailbox_alias_mailbox_list *alist;
+
+ alist = p_new(list->pool, struct mailbox_alias_mailbox_list, 1);
+ alist->module_ctx.super = *v;
+ list->vlast = &alist->module_ctx.super;
+
+ v->get_storage_name = mailbox_alias_get_storage_name;
+ MODULE_CONTEXT_SET(list, mailbox_alias_mailbox_list_module, alist);
+}
+
+static void mailbox_alias_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct mailbox_alias_mailbox *abox;
+
+ abox = p_new(box->pool, struct mailbox_alias_mailbox, 1);
+ abox->module_ctx.super = *v;
+ box->vlast = &abox->module_ctx.super;
+
+ v->create_box = mailbox_alias_create;
+ v->delete_box = mailbox_alias_delete;
+ v->rename_box = mailbox_alias_rename;
+ MODULE_CONTEXT_SET(box, mailbox_alias_storage_module, abox);
+}
+
+static struct mail_storage_hooks mailbox_alias_mail_storage_hooks = {
+ .mail_user_created = mailbox_alias_mail_user_created,
+ .mailbox_list_created = mailbox_alias_mailbox_list_created,
+ .mailbox_allocated = mailbox_alias_mailbox_allocated
+};
+
+void mailbox_alias_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &mailbox_alias_mail_storage_hooks);
+}
+
+void mailbox_alias_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&mailbox_alias_mail_storage_hooks);
+}
diff --git a/src/plugins/mailbox-alias/mailbox-alias-plugin.h b/src/plugins/mailbox-alias/mailbox-alias-plugin.h
new file mode 100644
index 0000000..bd439b9
--- /dev/null
+++ b/src/plugins/mailbox-alias/mailbox-alias-plugin.h
@@ -0,0 +1,7 @@
+#ifndef MAILBOX_ALIAS_PLUGIN_H
+#define MAILBOX_ALIAS_PLUGIN_H
+
+void mailbox_alias_plugin_init(struct module *module);
+void mailbox_alias_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/notify-status/Makefile.am b/src/plugins/notify-status/Makefile.am
new file mode 100644
index 0000000..241e88d
--- /dev/null
+++ b/src/plugins/notify-status/Makefile.am
@@ -0,0 +1,17 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/plugins/notify
+
+NOPLUGIN_LDFLAGS =
+lib20_notify_status_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_notify_status_plugin.la
+
+lib20_notify_status_plugin_la_SOURCES = \
+ notify-status-plugin.c
diff --git a/src/plugins/notify-status/Makefile.in b/src/plugins/notify-status/Makefile.in
new file mode 100644
index 0000000..01da241
--- /dev/null
+++ b/src/plugins/notify-status/Makefile.in
@@ -0,0 +1,817 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/notify-status
+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)$(moduledir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_notify_status_plugin_la_LIBADD =
+am_lib20_notify_status_plugin_la_OBJECTS = notify-status-plugin.lo
+lib20_notify_status_plugin_la_OBJECTS = \
+ $(am_lib20_notify_status_plugin_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 =
+lib20_notify_status_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_notify_status_plugin_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)/notify-status-plugin.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 = $(lib20_notify_status_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_notify_status_plugin_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)
+# 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@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/plugins/notify
+
+lib20_notify_status_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_notify_status_plugin.la
+
+lib20_notify_status_plugin_la_SOURCES = \
+ notify-status-plugin.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/plugins/notify-status/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/notify-status/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-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}; \
+ }
+
+lib20_notify_status_plugin.la: $(lib20_notify_status_plugin_la_OBJECTS) $(lib20_notify_status_plugin_la_DEPENDENCIES) $(EXTRA_lib20_notify_status_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_notify_status_plugin_la_LINK) -rpath $(moduledir) $(lib20_notify_status_plugin_la_OBJECTS) $(lib20_notify_status_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-status-plugin.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)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/notify-status-plugin.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-moduleLTLIBRARIES
+
+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)/notify-status-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/notify-status/notify-status-plugin.c b/src/plugins/notify-status/notify-status-plugin.c
new file mode 100644
index 0000000..6d0f2a6
--- /dev/null
+++ b/src/plugins/notify-status/notify-status-plugin.c
@@ -0,0 +1,363 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "json-parser.h"
+#include "str.h"
+#include "var-expand.h"
+#include "mail-user.h"
+#include "mail-storage.h"
+#include "mail-storage-private.h"
+#include "mail-namespace.h"
+#include "mail-storage-hooks.h"
+#include "imap-match.h"
+#include "dict.h"
+#include "notify-plugin.h"
+
+#define NOTIFY_STATUS_SETTING_DICT_URI "notify_status_dict"
+#define NOTIFY_STATUS_SETTING_MAILBOX_PREFIX "notify_status_mailbox"
+#define NOTIFY_STATUS_SETTING_VALUE_TEMPLATE "notify_status_value"
+#define NOTIFY_STATUS_SETTING_VALUE_TEMPLATE_DEFAULT "{\"messages\":%{messages},\"unseen\":%{unseen}}"
+#define NOTIFY_STATUS_KEY "priv/status/%s"
+
+#define NOTIFY_STATUS_USER_CONTEXT(obj) \
+ (struct notify_status_user*)MODULE_CONTEXT(obj, notify_status_user_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(notify_status_user_module,
+ &mail_user_module_register);
+
+void notify_status_plugin_init(struct module *module);
+void notify_status_plugin_deinit(void);
+
+const char *notify_status_plugin_version = DOVECOT_ABI_VERSION;
+const char *notify_status_plugin_dependencies[] = { "notify", NULL };
+
+ARRAY_DEFINE_TYPE(imap_match_glob, struct imap_match_glob*);
+
+struct notify_status_mail_txn {
+ struct mailbox *box;
+ bool changed:1;
+};
+
+struct notify_status_user {
+ union mail_user_module_context module_ctx;
+
+ ARRAY_TYPE(imap_match_glob) patterns;
+ struct dict *dict;
+ const char *value_template;
+ struct notify_context *context;
+};
+
+static int notify_status_dict_init(struct mail_user *user, const char *uri,
+ struct dict **dict_r, const char **error_r)
+{
+ struct dict_settings set = {
+ .base_dir = user->set->base_dir,
+ .event_parent = user->event,
+ };
+ if (dict_init(uri, &set, dict_r, error_r) < 0) {
+ *error_r = t_strdup_printf("dict_init(%s) failed: %s",
+ uri, *error_r);
+ return -1;
+ }
+ return 0;
+}
+
+static void notify_status_mailbox_patterns_init(struct mail_user *user,
+ ARRAY_TYPE(imap_match_glob) *patterns)
+{
+ const char *value;
+ unsigned int i;
+
+ p_array_init(patterns, user->pool, 2);
+
+ for(i=1;;i++) {
+ struct imap_match_glob **glob;
+ const char *key = NOTIFY_STATUS_SETTING_MAILBOX_PREFIX;
+ if (i > 1)
+ key = t_strdup_printf("%s%u", key, i);
+ value = mail_user_plugin_getenv(user, key);
+ if (value == NULL)
+ return;
+ char sep = mail_namespace_get_sep(user->namespaces);
+ glob = array_append_space(patterns);
+ *glob = imap_match_init(user->pool, value, TRUE, sep);
+ }
+}
+
+static bool notify_status_mailbox_enabled(struct mailbox *box)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct notify_status_user *nuser = NOTIFY_STATUS_USER_CONTEXT(user);
+ struct imap_match_glob *glob;
+ /* not enabled */
+ if (nuser == NULL)
+ return FALSE;
+
+ /* if no patterns defined, anything goes */
+ if (array_count(&nuser->patterns) == 0)
+ return TRUE;
+
+ array_foreach_elem(&nuser->patterns, glob) {
+ if ((imap_match(glob, mailbox_get_vname(box)) & IMAP_MATCH_YES) != 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void notify_update_callback(const struct dict_commit_result *result,
+ void *context ATTR_UNUSED)
+{
+ if (result->ret == DICT_COMMIT_RET_OK ||
+ result->ret == DICT_COMMIT_RET_NOTFOUND)
+ return;
+
+ i_error("notify-status: dict_transaction_commit failed: %s",
+ result->error == NULL ? "" : result->error);
+}
+
+#define MAILBOX_STATUS_NOTIFY (STATUS_MESSAGES|STATUS_UNSEEN|\
+ STATUS_RECENT|STATUS_UIDNEXT|\
+ STATUS_UIDVALIDITY|\
+ STATUS_HIGHESTMODSEQ|STATUS_FIRST_RECENT_UID|\
+ STATUS_HIGHESTPVTMODSEQ)
+static void notify_update_mailbox_status(struct mailbox *box)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct notify_status_user *nuser = NOTIFY_STATUS_USER_CONTEXT(user);
+ i_assert(nuser != NULL);
+ struct dict_transaction_context *t;
+ struct mailbox_status status;
+
+ e_debug(box->event, "notify-status: Updating mailbox status");
+
+ box = mailbox_alloc(mailbox_get_namespace(box)->list,
+ mailbox_get_vname(box), MAILBOX_FLAG_READONLY);
+
+ if (mailbox_open(box) < 0) {
+ i_error("notify-status: mailbox_open(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_error(box, NULL));
+ } else if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ i_error("notify-status: mailbox_sync(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_error(box, NULL));
+ } else if (mailbox_get_status(box, MAILBOX_STATUS_NOTIFY,
+ &status) < 0) {
+ i_error("notify-status: mailbox_get_status(%s) failed: %s",
+ mailbox_get_vname(box),
+ mailbox_get_last_error(box, NULL));
+ } else {
+ string_t *username = t_str_new(strlen(user->username));
+ string_t *mboxname = t_str_new(64);
+
+ json_append_escaped(username, user->username);
+ json_append_escaped(mboxname, mailbox_get_vname(box));
+
+ const struct var_expand_table values[] = {
+ { '\0', str_c(username), "username" },
+ { '\0', str_c(mboxname), "mailbox" },
+ { '\0', dec2str(status.messages), "messages" },
+ { '\0', dec2str(status.unseen), "unseen" },
+ { '\0', dec2str(status.recent), "recent" },
+ { '\0', dec2str(status.uidvalidity), "uidvalidity" },
+ { '\0', dec2str(status.uidnext), "uidnext" },
+ { '\0', dec2str(status.first_recent_uid), "first_recent_uid" },
+ { '\0', dec2str(status.highest_modseq), "highest_modseq" },
+ { '\0', dec2str(status.highest_pvt_modseq), "highest_pvt_modseq" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+ const char *key =
+ t_strdup_printf(NOTIFY_STATUS_KEY, mailbox_get_vname(box));
+ string_t *dest = t_str_new(64);
+ if (var_expand(dest, nuser->value_template, values, &error)<0) {
+ i_error("notify-status: var_expand(%s) failed: %s",
+ nuser->value_template, error);
+ } else {
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ t = dict_transaction_begin(nuser->dict, set);
+ dict_set(t, key, str_c(dest));
+ dict_transaction_commit_async(&t, notify_update_callback, NULL) ;
+ }
+ }
+
+ mailbox_free(&box);
+}
+
+static void notify_remove_mailbox_status(struct mailbox *box)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct notify_status_user *nuser = NOTIFY_STATUS_USER_CONTEXT(user);
+ i_assert(nuser != NULL);
+ struct dict_transaction_context *t;
+
+ e_debug(box->event, "notify-status: Removing mailbox status");
+
+ const char *key =
+ t_strdup_printf(NOTIFY_STATUS_KEY, mailbox_get_vname(box));
+
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ t = dict_transaction_begin(nuser->dict, set);
+ dict_unset(t, key);
+ dict_transaction_commit_async(&t, notify_update_callback, NULL) ;
+}
+
+static void *notify_status_mail_transaction_begin(struct mailbox_transaction_context *t)
+{
+ struct notify_status_mail_txn *txn = i_new(struct notify_status_mail_txn, 1);
+ txn->box = mailbox_transaction_get_mailbox(t);
+ return txn;
+}
+
+static void
+notify_status_mail_transaction_commit(void *t,
+ struct mail_transaction_commit_changes *changes ATTR_UNUSED)
+{
+ struct notify_status_mail_txn *txn = (struct notify_status_mail_txn *)t;
+ if (txn->changed && notify_status_mailbox_enabled(txn->box))
+ notify_update_mailbox_status(txn->box);
+ i_free(txn);
+}
+
+static void notify_status_mail_transaction_rollback(void *t)
+{
+ i_free(t);
+}
+
+static void notify_status_mailbox_create(struct mailbox *box)
+{
+ if (notify_status_mailbox_enabled(box))
+ notify_update_mailbox_status(box);
+}
+
+static void notify_status_mailbox_delete_commit(void *txn ATTR_UNUSED,
+ struct mailbox *box)
+{
+ if (notify_status_mailbox_enabled(box))
+ notify_remove_mailbox_status(box);
+}
+
+static void notify_status_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ if (notify_status_mailbox_enabled(src))
+ notify_remove_mailbox_status(src);
+ if (notify_status_mailbox_enabled(dest))
+ notify_update_mailbox_status(dest);
+}
+
+static void notify_status_mail_save(void *t, struct mail *mail ATTR_UNUSED)
+{
+ struct notify_status_mail_txn *txn = (struct notify_status_mail_txn *)t;
+ txn->changed = TRUE;
+}
+
+static void notify_status_mail_copy(void *t, struct mail *src ATTR_UNUSED,
+ struct mail *dst ATTR_UNUSED)
+{
+ struct notify_status_mail_txn *txn = (struct notify_status_mail_txn *)t;
+ txn->changed = TRUE;
+}
+
+static void notify_status_mail_expunge(void *t, struct mail *mail ATTR_UNUSED)
+{
+ struct notify_status_mail_txn *txn = (struct notify_status_mail_txn *)t;
+ txn->changed = TRUE;
+}
+static void notify_status_mail_update_flags(void *t, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct notify_status_mail_txn *txn = (struct notify_status_mail_txn *)t;
+ if ((old_flags & MAIL_SEEN) != (mail_get_flags(mail) & MAIL_SEEN))
+ txn->changed = TRUE;
+}
+
+static const struct notify_vfuncs notify_vfuncs =
+{
+ .mail_transaction_begin = notify_status_mail_transaction_begin,
+ .mail_save = notify_status_mail_save,
+ .mail_copy = notify_status_mail_copy,
+ .mail_expunge = notify_status_mail_expunge,
+ .mail_update_flags = notify_status_mail_update_flags,
+ .mail_transaction_commit = notify_status_mail_transaction_commit,
+ .mail_transaction_rollback = notify_status_mail_transaction_rollback,
+ .mailbox_create = notify_status_mailbox_create,
+ .mailbox_delete_commit = notify_status_mailbox_delete_commit,
+ .mailbox_rename = notify_status_mailbox_rename,
+};
+
+static void notify_status_mail_user_deinit(struct mail_user *user)
+{
+ struct notify_status_user *nuser = NOTIFY_STATUS_USER_CONTEXT(user);
+ i_assert(nuser != NULL);
+
+ dict_wait(nuser->dict);
+ dict_deinit(&nuser->dict);
+ if (nuser->context != NULL)
+ notify_unregister(nuser->context);
+ nuser->module_ctx.super.deinit(user);
+}
+
+static void notify_status_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct notify_status_user *nuser;
+ struct dict *dict;
+ const char *error;
+ const char *template = mail_user_plugin_getenv(user, NOTIFY_STATUS_SETTING_VALUE_TEMPLATE);
+ const char *uri = mail_user_plugin_getenv(user, NOTIFY_STATUS_SETTING_DICT_URI);
+
+ if (user->autocreated)
+ return;
+
+ if (uri == NULL || *uri == '\0') {
+ e_debug(user->event, "notify-status: Disabled - Missing plugin/"
+ NOTIFY_STATUS_SETTING_DICT_URI" setting");
+ return;
+ }
+
+ if (template == NULL || *template == '\0')
+ template = NOTIFY_STATUS_SETTING_VALUE_TEMPLATE_DEFAULT;
+
+ if (notify_status_dict_init(user, uri, &dict, &error) < 0) {
+ i_error("notify-status: %s", error);
+ return;
+ }
+
+ nuser = p_new(user->pool, struct notify_status_user, 1);
+ nuser->module_ctx.super = *v;
+ nuser->dict = dict;
+ user->vlast = &nuser->module_ctx.super;
+ v->deinit = notify_status_mail_user_deinit;
+ /* either static value or lifetime is user object's lifetime */
+ nuser->value_template = template;
+
+ MODULE_CONTEXT_SET(user, notify_status_user_module, nuser);
+}
+
+static void
+notify_status_mail_namespaces_created(struct mail_namespace *namespaces)
+{
+ struct mail_user *user = namespaces->user;
+ struct notify_status_user *nuser = NOTIFY_STATUS_USER_CONTEXT(user);
+ if (nuser == NULL)
+ return;
+ notify_status_mailbox_patterns_init(user, &nuser->patterns);
+ nuser->context = notify_register(&notify_vfuncs);
+}
+
+static const struct mail_storage_hooks notify_storage_hooks =
+{
+ .mail_user_created = notify_status_mail_user_created,
+ .mail_namespaces_created = notify_status_mail_namespaces_created,
+};
+
+void notify_status_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &notify_storage_hooks);
+}
+
+void notify_status_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&notify_storage_hooks);
+}
diff --git a/src/plugins/notify/Makefile.am b/src/plugins/notify/Makefile.am
new file mode 100644
index 0000000..dfa286e
--- /dev/null
+++ b/src/plugins/notify/Makefile.am
@@ -0,0 +1,25 @@
+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 \
+ -I$(top_srcdir)/src/lib-storage/index/maildir
+
+NOPLUGIN_LDFLAGS =
+lib15_notify_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib15_notify_plugin.la
+
+lib15_notify_plugin_la_SOURCES = \
+ notify-plugin.c \
+ notify-storage.c
+
+headers = \
+ notify-plugin.h \
+ notify-plugin-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/plugins/notify/Makefile.in b/src/plugins/notify/Makefile.in
new file mode 100644
index 0000000..221876b
--- /dev/null
+++ b/src/plugins/notify/Makefile.in
@@ -0,0 +1,852 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/notify
+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)$(moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib15_notify_plugin_la_LIBADD =
+am_lib15_notify_plugin_la_OBJECTS = notify-plugin.lo notify-storage.lo
+lib15_notify_plugin_la_OBJECTS = $(am_lib15_notify_plugin_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 =
+lib15_notify_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib15_notify_plugin_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)/notify-plugin.Plo \
+ ./$(DEPDIR)/notify-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 = $(lib15_notify_plugin_la_SOURCES)
+DIST_SOURCES = $(lib15_notify_plugin_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 =
+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-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/maildir
+
+lib15_notify_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib15_notify_plugin.la
+
+lib15_notify_plugin_la_SOURCES = \
+ notify-plugin.c \
+ notify-storage.c
+
+headers = \
+ notify-plugin.h \
+ notify-plugin-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/plugins/notify/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/notify/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-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}; \
+ }
+
+lib15_notify_plugin.la: $(lib15_notify_plugin_la_OBJECTS) $(lib15_notify_plugin_la_DEPENDENCIES) $(EXTRA_lib15_notify_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib15_notify_plugin_la_LINK) -rpath $(moduledir) $(lib15_notify_plugin_la_OBJECTS) $(lib15_notify_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-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)$(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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/notify-plugin.Plo
+ -rm -f ./$(DEPDIR)/notify-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-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)/notify-plugin.Plo
+ -rm -f ./$(DEPDIR)/notify-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-moduleLTLIBRARIES 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-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-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
+
+
+# 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/plugins/notify/notify-plugin-private.h b/src/plugins/notify/notify-plugin-private.h
new file mode 100644
index 0000000..cef0771
--- /dev/null
+++ b/src/plugins/notify/notify-plugin-private.h
@@ -0,0 +1,29 @@
+#ifndef NOTIFY_PLUGIN_PRIVATE_H
+#define NOTIFY_PLUGIN_PRIVATE_H
+
+#include "notify-plugin.h"
+
+void notify_contexts_mail_transaction_begin(struct mailbox_transaction_context *t);
+void notify_contexts_mail_save(struct mail *mail);
+void notify_contexts_mail_copy(struct mail *src, struct mail *dst);
+void notify_contexts_mail_expunge(struct mail *mail);
+void notify_contexts_mail_update_flags(struct mail *mail,
+ enum mail_flags old_flags);
+void notify_contexts_mail_update_keywords(struct mail *mail,
+ const char *const *old_keywords);
+void notify_contexts_mail_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes);
+void notify_contexts_mail_transaction_rollback(struct mailbox_transaction_context *t);
+void notify_contexts_mailbox_create(struct mailbox *box);
+void notify_contexts_mailbox_update(struct mailbox *box);
+void notify_contexts_mailbox_delete_begin(struct mailbox *box);
+void notify_contexts_mailbox_delete_commit(struct mailbox *box);
+void notify_contexts_mailbox_delete_rollback(void);
+void notify_contexts_mailbox_rename(struct mailbox *src, struct mailbox *dest);
+void notify_contexts_mailbox_set_subscribed(struct mailbox *box,
+ bool subscribed);
+
+void notify_plugin_init_storage(struct module *module);
+void notify_plugin_deinit_storage(void);
+
+#endif
diff --git a/src/plugins/notify/notify-plugin.c b/src/plugins/notify/notify-plugin.c
new file mode 100644
index 0000000..e9ff9bb
--- /dev/null
+++ b/src/plugins/notify/notify-plugin.c
@@ -0,0 +1,265 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "mail-storage.h"
+#include "notify-plugin-private.h"
+
+
+struct notify_mail_txn {
+ struct notify_mail_txn *prev, *next;
+ struct mailbox_transaction_context *parent_mailbox_txn;
+ struct mail *tmp_mail;
+ void *txn;
+};
+
+struct notify_context {
+ struct notify_context *prev, *next;
+ struct notify_vfuncs v;
+ struct notify_mail_txn *mail_txn_list;
+ void *mailbox_delete_txn;
+};
+
+const char *notify_plugin_version = DOVECOT_ABI_VERSION;
+static struct notify_context *ctx_list = NULL;
+
+static struct notify_mail_txn *
+notify_context_find_mail_txn(struct notify_context *ctx,
+ struct mailbox_transaction_context *t)
+{
+ struct notify_mail_txn *mail_txn = ctx->mail_txn_list;
+
+ for (; mail_txn != NULL; mail_txn = mail_txn->next) {
+ if (mail_txn->parent_mailbox_txn == t)
+ return mail_txn;
+ }
+ i_panic("no notify_mail_txn found");
+}
+
+void notify_contexts_mail_transaction_begin(struct mailbox_transaction_context *t)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ mail_txn = i_new(struct notify_mail_txn, 1);
+ mail_txn->parent_mailbox_txn = t;
+ mail_txn->txn = ctx->v.mail_transaction_begin == NULL ? NULL :
+ ctx->v.mail_transaction_begin(t);
+ DLLIST_PREPEND(&ctx->mail_txn_list, mail_txn);
+ }
+}
+
+void notify_contexts_mail_save(struct mail *mail)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mail_save == NULL)
+ continue;
+ mail_txn = notify_context_find_mail_txn(ctx, mail->transaction);
+ ctx->v.mail_save(mail_txn->txn, mail);
+ }
+}
+
+void notify_contexts_mail_copy(struct mail *src, struct mail *dst)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mail_copy == NULL)
+ continue;
+ mail_txn = notify_context_find_mail_txn(ctx, dst->transaction);
+ ctx->v.mail_copy(mail_txn->txn, src, dst);
+ }
+}
+
+void notify_contexts_mail_expunge(struct mail *mail)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mail_expunge == NULL)
+ continue;
+ mail_txn = notify_context_find_mail_txn(ctx, mail->transaction);
+ ctx->v.mail_expunge(mail_txn->txn, mail);
+ }
+}
+
+void notify_contexts_mail_update_flags(struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ if (mail->saving)
+ return;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mail_update_flags == NULL)
+ continue;
+ mail_txn = notify_context_find_mail_txn(ctx, mail->transaction);
+ ctx->v.mail_update_flags(mail_txn->txn, mail, old_flags);
+ }
+}
+
+void notify_contexts_mail_update_keywords(struct mail *mail,
+ const char *const *old_keywords)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ if (mail->saving)
+ return;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mail_update_keywords == NULL)
+ continue;
+ mail_txn = notify_context_find_mail_txn(ctx, mail->transaction);
+ ctx->v.mail_update_keywords(mail_txn->txn, mail, old_keywords);
+ }
+}
+
+void notify_contexts_mail_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ mail_txn = notify_context_find_mail_txn(ctx, t);
+ if (ctx->v.mail_transaction_commit != NULL)
+ ctx->v.mail_transaction_commit(mail_txn->txn, changes);
+ DLLIST_REMOVE(&ctx->mail_txn_list, mail_txn);
+ i_free(mail_txn);
+ }
+}
+
+void notify_contexts_mail_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ struct notify_context *ctx;
+ struct notify_mail_txn *mail_txn;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ mail_txn = notify_context_find_mail_txn(ctx, t);
+ if (ctx->v.mail_transaction_rollback != NULL)
+ ctx->v.mail_transaction_rollback(mail_txn->txn);
+ DLLIST_REMOVE(&ctx->mail_txn_list, mail_txn);
+ i_free(mail_txn);
+ }
+}
+
+void notify_contexts_mailbox_create(struct mailbox *box)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_create != NULL)
+ ctx->v.mailbox_create(box);
+ }
+}
+
+void notify_contexts_mailbox_update(struct mailbox *box)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_update != NULL)
+ ctx->v.mailbox_update(box);
+ }
+}
+
+void notify_contexts_mailbox_delete_begin(struct mailbox *box)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ ctx->mailbox_delete_txn =
+ ctx->v.mailbox_delete_begin == NULL ? NULL :
+ ctx->v.mailbox_delete_begin(box);
+ }
+}
+
+void notify_contexts_mailbox_delete_commit(struct mailbox *box)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_delete_commit != NULL) {
+ ctx->v.mailbox_delete_commit(ctx->mailbox_delete_txn,
+ box);
+ }
+ ctx->mailbox_delete_txn = NULL;
+ }
+}
+
+void notify_contexts_mailbox_delete_rollback(void)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_delete_rollback != NULL)
+ ctx->v.mailbox_delete_rollback(ctx->mailbox_delete_txn);
+ ctx->mailbox_delete_txn = NULL;
+ }
+}
+
+void notify_contexts_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_rename != NULL)
+ ctx->v.mailbox_rename(src, dest);
+ }
+}
+
+void notify_contexts_mailbox_set_subscribed(struct mailbox *box,
+ bool subscribed)
+{
+ struct notify_context *ctx;
+
+ for (ctx = ctx_list; ctx != NULL; ctx = ctx->next) {
+ if (ctx->v.mailbox_set_subscribed != NULL)
+ ctx->v.mailbox_set_subscribed(box, subscribed);
+ }
+}
+
+struct notify_context *
+notify_register(const struct notify_vfuncs *v)
+{
+ struct notify_context *ctx;
+
+ ctx = i_new(struct notify_context, 1);
+ ctx->v = *v;
+ DLLIST_PREPEND(&ctx_list, ctx);
+ return ctx;
+}
+
+void notify_unregister(struct notify_context *ctx)
+{
+ struct notify_mail_txn *mail_txn = ctx->mail_txn_list;
+
+ for (; mail_txn != NULL; mail_txn = mail_txn->next) {
+ if (ctx->v.mail_transaction_rollback != NULL)
+ ctx->v.mail_transaction_rollback(mail_txn->txn);
+ }
+ if (ctx->mailbox_delete_txn != NULL &&
+ ctx->v.mailbox_delete_rollback != NULL)
+ ctx->v.mailbox_delete_rollback(ctx->mailbox_delete_txn);
+ DLLIST_REMOVE(&ctx_list, ctx);
+ i_free(ctx);
+}
+
+void notify_plugin_init(struct module *module)
+{
+ notify_plugin_init_storage(module);
+}
+
+void notify_plugin_deinit(void)
+{
+ notify_plugin_deinit_storage();
+}
diff --git a/src/plugins/notify/notify-plugin.h b/src/plugins/notify/notify-plugin.h
new file mode 100644
index 0000000..3c95071
--- /dev/null
+++ b/src/plugins/notify/notify-plugin.h
@@ -0,0 +1,43 @@
+#ifndef NOTIFY_PLUGIN_H
+#define NOTIFY_PLUGIN_H
+
+#include "mail-types.h"
+
+struct mail;
+struct mail_transaction_commit_changes;
+struct mail_storage;
+struct mailbox_transaction_context;
+struct mailbox_list;
+struct mailbox;
+struct notify_context;
+struct module;
+
+struct notify_vfuncs {
+ void *(*mail_transaction_begin)(struct mailbox_transaction_context *t);
+ void (*mail_save)(void *txn, struct mail *mail);
+ void (*mail_copy)(void *txn, struct mail *src, struct mail *dst);
+ void (*mail_expunge)(void *txn, struct mail *mail);
+ void (*mail_update_flags)(void *txn, struct mail *mail,
+ enum mail_flags old_flags);
+ void (*mail_update_keywords)(void *txn, struct mail *mail,
+ const char *const *old_keywords);
+ void (*mail_transaction_commit)(void *txn,
+ struct mail_transaction_commit_changes *changes);
+ void (*mail_transaction_rollback)(void *txn);
+ void (*mailbox_create)(struct mailbox *box);
+ void (*mailbox_update)(struct mailbox *box);
+ void *(*mailbox_delete_begin)(struct mailbox *box);
+ void (*mailbox_delete_commit)(void *txn, struct mailbox *box);
+ void (*mailbox_delete_rollback)(void *txn);
+ void (*mailbox_rename)(struct mailbox *src, struct mailbox *dest);
+ void (*mailbox_set_subscribed)(struct mailbox *box, bool subscribed);
+};
+
+struct notify_context *
+notify_register(const struct notify_vfuncs *vfuncs);
+void notify_unregister(struct notify_context *ctx);
+
+void notify_plugin_init(struct module *module);
+void notify_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/notify/notify-storage.c b/src/plugins/notify/notify-storage.c
new file mode 100644
index 0000000..b7a6a3a
--- /dev/null
+++ b/src/plugins/notify/notify-storage.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "notify-plugin-private.h"
+
+#define NOTIFY_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, notify_storage_module)
+#define NOTIFY_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, notify_mail_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(notify_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(notify_mail_module,
+ &mail_module_register);
+
+static void
+notify_mail_expunge(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *lmail = NOTIFY_MAIL_CONTEXT(mail);
+
+ notify_contexts_mail_expunge(_mail);
+ lmail->super.expunge(_mail);
+}
+
+static void
+notify_mail_update_flags(struct mail *_mail, enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *lmail = NOTIFY_MAIL_CONTEXT(mail);
+ enum mail_flags old_flags, new_flags;
+
+ old_flags = mail_get_flags(_mail);
+ lmail->super.update_flags(_mail, modify_type, flags);
+ new_flags = mail_get_flags(_mail);
+
+ if ((old_flags ^ new_flags) == 0)
+ return;
+
+ notify_contexts_mail_update_flags(_mail, old_flags);
+}
+
+static void
+notify_mail_update_keywords(struct mail *_mail, enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *lmail = NOTIFY_MAIL_CONTEXT(mail);
+ const char *const *old_keywords, *const *new_keywords;
+ unsigned int i;
+
+ old_keywords = mail_get_keywords(_mail);
+ lmail->super.update_keywords(_mail, modify_type, keywords);
+ new_keywords = mail_get_keywords(_mail);
+
+ for (i = 0; old_keywords[i] != NULL && new_keywords[i] != NULL; i++) {
+ if (strcmp(old_keywords[i], new_keywords[i]) != 0)
+ break;
+ }
+
+ if (old_keywords[i] == NULL && new_keywords[i] == NULL)
+ return;
+
+ notify_contexts_mail_update_keywords(_mail, old_keywords);
+}
+
+static void notify_mail_allocated(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ union mail_module_context *lmail;
+
+ if ((_mail->transaction->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) != 0)
+ return;
+
+ lmail = p_new(mail->pool, union mail_module_context, 1);
+ lmail->super = *v;
+ mail->vlast = &lmail->super;
+
+ v->expunge = notify_mail_expunge;
+ v->update_flags = notify_mail_update_flags;
+ v->update_keywords = notify_mail_update_keywords;
+ MODULE_CONTEXT_SET_SELF(mail, notify_mail_module, lmail);
+}
+
+static int
+notify_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ union mailbox_module_context *lbox =
+ NOTIFY_CONTEXT(ctx->transaction->box);
+ int ret;
+
+ if ((ret = lbox->super.copy(ctx, mail)) < 0)
+ return -1;
+
+ if ((ctx->transaction->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) != 0) {
+ /* no notifications */
+ } else if (ctx->saving) {
+ /* we came from mailbox_save_using_mail() */
+ notify_contexts_mail_save(ctx->dest_mail);
+ } else {
+ notify_contexts_mail_copy(mail, ctx->dest_mail);
+ }
+ return ret;
+}
+
+static int
+notify_save_finish(struct mail_save_context *ctx)
+{
+ union mailbox_module_context *lbox =
+ NOTIFY_CONTEXT(ctx->transaction->box);
+ struct mail *dest_mail = ctx->copying_via_save ? NULL : ctx->dest_mail;
+
+ if (lbox->super.save_finish(ctx) < 0)
+ return -1;
+ if (dest_mail != NULL &&
+ (ctx->transaction->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) == 0)
+ notify_contexts_mail_save(dest_mail);
+ return 0;
+}
+
+static struct mailbox_transaction_context *
+notify_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(box);
+ struct mailbox_transaction_context *t;
+
+ t = lbox->super.transaction_begin(box, flags, reason);
+
+ if ((t->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) == 0)
+ notify_contexts_mail_transaction_begin(t);
+ return t;
+}
+
+static int
+notify_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(t->box);
+ bool no_notify = (t->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) != 0;
+
+ if ((lbox->super.transaction_commit(t, changes_r)) < 0) {
+ if (!no_notify)
+ notify_contexts_mail_transaction_rollback(t);
+ return -1;
+ }
+
+ /* FIXME: note that t is already freed at this stage. it's not actually
+ being dereferenced anymore though. still, a bit unsafe.. */
+ if (!no_notify)
+ notify_contexts_mail_transaction_commit(t, changes_r);
+ return 0;
+}
+
+static void
+notify_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(t->box);
+
+ if ((t->flags & MAILBOX_TRANSACTION_FLAG_NO_NOTIFY) == 0)
+ notify_contexts_mail_transaction_rollback(t);
+ lbox->super.transaction_rollback(t);
+}
+
+static int
+notify_mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(box);
+
+ if (lbox->super.create_box(box, update, directory) < 0)
+ return -1;
+
+ notify_contexts_mailbox_create(box);
+ return 0;
+}
+
+static int
+notify_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(box);
+
+ if (lbox->super.update_box(box, update) < 0)
+ return -1;
+
+ notify_contexts_mailbox_update(box);
+ return 0;
+}
+
+static int
+notify_mailbox_delete(struct mailbox *box)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(box);
+
+ notify_contexts_mailbox_delete_begin(box);
+ if (lbox->super.delete_box(box) < 0) {
+ notify_contexts_mailbox_delete_rollback();
+ return -1;
+ }
+ notify_contexts_mailbox_delete_commit(box);
+ return 0;
+}
+
+static int
+notify_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(src);
+
+ if (lbox->super.rename_box(src, dest) < 0)
+ return -1;
+
+ notify_contexts_mailbox_rename(src, dest);
+ return 0;
+}
+
+static int
+notify_mailbox_set_subscribed(struct mailbox *box, bool set)
+{
+ union mailbox_module_context *lbox = NOTIFY_CONTEXT(box);
+
+ if (lbox->super.set_subscribed(box, set) < 0)
+ return -1;
+
+ notify_contexts_mailbox_set_subscribed(box, set);
+ return 0;
+}
+
+static void notify_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ union mailbox_module_context *lbox;
+
+ lbox = p_new(box->pool, union mailbox_module_context, 1);
+ lbox->super = *v;
+ box->vlast = &lbox->super;
+
+ v->copy = notify_copy;
+ v->save_finish = notify_save_finish;
+ v->transaction_begin = notify_transaction_begin;
+ v->transaction_commit = notify_transaction_commit;
+ v->transaction_rollback = notify_transaction_rollback;
+ v->create_box = notify_mailbox_create;
+ v->update_box = notify_mailbox_update;
+ v->delete_box = notify_mailbox_delete;
+ v->rename_box = notify_mailbox_rename;
+ v->set_subscribed = notify_mailbox_set_subscribed;
+ MODULE_CONTEXT_SET_SELF(box, notify_storage_module, lbox);
+}
+
+static struct mail_storage_hooks notify_mail_storage_hooks = {
+ .mailbox_allocated = notify_mailbox_allocated,
+ .mail_allocated = notify_mail_allocated
+};
+
+void notify_plugin_init_storage(struct module *module)
+{
+ mail_storage_hooks_add(module, &notify_mail_storage_hooks);
+}
+
+void notify_plugin_deinit_storage(void)
+{
+ mail_storage_hooks_remove(&notify_mail_storage_hooks);
+}
diff --git a/src/plugins/old-stats/Makefile.am b/src/plugins/old-stats/Makefile.am
new file mode 100644
index 0000000..8a95363
--- /dev/null
+++ b/src/plugins/old-stats/Makefile.am
@@ -0,0 +1,33 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-old-stats \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib90_old_stats_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib90_old_stats_plugin.la
+
+lib90_old_stats_plugin_la_SOURCES = \
+ mail-stats.c \
+ mail-stats-fill.c \
+ mail-stats-connection.c \
+ stats-plugin.c
+
+noinst_HEADERS = \
+ mail-stats.h \
+ mail-stats-connection.h \
+ stats-plugin.h
+
+old_stats_moduledir = $(moduledir)/old-stats
+old_stats_module_LTLIBRARIES = libold_stats_mail.la
+
+libold_stats_mail_la_LDFLAGS = -module -avoid-version
+libold_stats_mail_la_LIBADD = mail-stats.lo $(LIBDOVECOT)
+libold_stats_mail_la_DEPENDENCIES = mail-stats.lo
+libold_stats_mail_la_SOURCES =
diff --git a/src/plugins/old-stats/Makefile.in b/src/plugins/old-stats/Makefile.in
new file mode 100644
index 0000000..7e375a6
--- /dev/null
+++ b/src/plugins/old-stats/Makefile.in
@@ -0,0 +1,898 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/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 $(noinst_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)$(moduledir)" \
+ "$(DESTDIR)$(old_stats_moduledir)"
+LTLIBRARIES = $(module_LTLIBRARIES) $(old_stats_module_LTLIBRARIES)
+lib90_old_stats_plugin_la_LIBADD =
+am_lib90_old_stats_plugin_la_OBJECTS = mail-stats.lo \
+ mail-stats-fill.lo mail-stats-connection.lo stats-plugin.lo
+lib90_old_stats_plugin_la_OBJECTS = \
+ $(am_lib90_old_stats_plugin_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 =
+lib90_old_stats_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib90_old_stats_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+am_libold_stats_mail_la_OBJECTS =
+libold_stats_mail_la_OBJECTS = $(am_libold_stats_mail_la_OBJECTS)
+libold_stats_mail_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libold_stats_mail_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)/mail-stats-connection.Plo \
+ ./$(DEPDIR)/mail-stats-fill.Plo ./$(DEPDIR)/mail-stats.Plo \
+ ./$(DEPDIR)/stats-plugin.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 = $(lib90_old_stats_plugin_la_SOURCES) \
+ $(libold_stats_mail_la_SOURCES)
+DIST_SOURCES = $(lib90_old_stats_plugin_la_SOURCES) \
+ $(libold_stats_mail_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 =
+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-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-old-stats \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib90_old_stats_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib90_old_stats_plugin.la
+
+lib90_old_stats_plugin_la_SOURCES = \
+ mail-stats.c \
+ mail-stats-fill.c \
+ mail-stats-connection.c \
+ stats-plugin.c
+
+noinst_HEADERS = \
+ mail-stats.h \
+ mail-stats-connection.h \
+ stats-plugin.h
+
+old_stats_moduledir = $(moduledir)/old-stats
+old_stats_module_LTLIBRARIES = libold_stats_mail.la
+libold_stats_mail_la_LDFLAGS = -module -avoid-version
+libold_stats_mail_la_LIBADD = mail-stats.lo $(LIBDOVECOT)
+libold_stats_mail_la_DEPENDENCIES = mail-stats.lo
+libold_stats_mail_la_SOURCES =
+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/plugins/old-stats/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/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):
+
+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}; \
+ }
+
+install-old_stats_moduleLTLIBRARIES: $(old_stats_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(old_stats_module_LTLIBRARIES)'; test -n "$(old_stats_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)$(old_stats_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(old_stats_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(old_stats_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(old_stats_moduledir)"; \
+ }
+
+uninstall-old_stats_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(old_stats_module_LTLIBRARIES)'; test -n "$(old_stats_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(old_stats_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(old_stats_moduledir)/$$f"; \
+ done
+
+clean-old_stats_moduleLTLIBRARIES:
+ -test -z "$(old_stats_module_LTLIBRARIES)" || rm -f $(old_stats_module_LTLIBRARIES)
+ @list='$(old_stats_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}; \
+ }
+
+lib90_old_stats_plugin.la: $(lib90_old_stats_plugin_la_OBJECTS) $(lib90_old_stats_plugin_la_DEPENDENCIES) $(EXTRA_lib90_old_stats_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib90_old_stats_plugin_la_LINK) -rpath $(moduledir) $(lib90_old_stats_plugin_la_OBJECTS) $(lib90_old_stats_plugin_la_LIBADD) $(LIBS)
+
+libold_stats_mail.la: $(libold_stats_mail_la_OBJECTS) $(libold_stats_mail_la_DEPENDENCIES) $(EXTRA_libold_stats_mail_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libold_stats_mail_la_LINK) -rpath $(old_stats_moduledir) $(libold_stats_mail_la_OBJECTS) $(libold_stats_mail_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-stats-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-stats-fill.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-stats.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(old_stats_moduledir)"; 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-old_stats_moduleLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mail-stats-connection.Plo
+ -rm -f ./$(DEPDIR)/mail-stats-fill.Plo
+ -rm -f ./$(DEPDIR)/mail-stats.Plo
+ -rm -f ./$(DEPDIR)/stats-plugin.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-moduleLTLIBRARIES \
+ install-old_stats_moduleLTLIBRARIES
+
+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-stats-connection.Plo
+ -rm -f ./$(DEPDIR)/mail-stats-fill.Plo
+ -rm -f ./$(DEPDIR)/mail-stats.Plo
+ -rm -f ./$(DEPDIR)/stats-plugin.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-moduleLTLIBRARIES \
+ uninstall-old_stats_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ clean-old_stats_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-html install-html-am install-info \
+ install-info-am install-man install-moduleLTLIBRARIES \
+ install-old_stats_moduleLTLIBRARIES 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-moduleLTLIBRARIES \
+ uninstall-old_stats_moduleLTLIBRARIES
+
+.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/plugins/old-stats/mail-stats-connection.c b/src/plugins/old-stats/mail-stats-connection.c
new file mode 100644
index 0000000..ca20fcb
--- /dev/null
+++ b/src/plugins/old-stats/mail-stats-connection.c
@@ -0,0 +1,75 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+#include "mail-storage.h"
+#include "stats.h"
+#include "stats-plugin.h"
+#include "mail-stats-connection.h"
+
+int mail_stats_connection_connect(struct stats_connection *conn,
+ struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ string_t *str = t_str_new(128);
+
+ str_append(str, "CONNECT\t");
+ /* required fields */
+ str_append(str, suser->stats_session_id);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, user->username);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, user->service);
+ str_printfa(str, "\t%s", my_pid);
+
+ /* optional fields */
+ if (user->conn.local_ip != NULL) {
+ str_append(str, "\tlip=");
+ str_append(str, net_ip2addr(user->conn.local_ip));
+ }
+ if (user->conn.remote_ip != NULL) {
+ str_append(str, "\trip=");
+ str_append(str, net_ip2addr(user->conn.remote_ip));
+ }
+ str_append_c(str, '\n');
+ return stats_connection_send(conn, str);
+}
+
+void mail_stats_connection_disconnect(struct stats_connection *conn,
+ struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ string_t *str = t_str_new(128);
+
+ str_append(str, "DISCONNECT\t");
+ str_append(str, suser->stats_session_id);
+ str_append_c(str, '\n');
+ if (stats_connection_send(conn, str) < 0) {
+ /* we could retry this later, but stats process will forget it
+ anyway after 15 minutes. */
+ }
+}
+
+void mail_stats_connection_send_session(struct stats_connection *conn,
+ struct mail_user *user,
+ const struct stats *stats)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ string_t *str = t_str_new(256);
+ buffer_t *buf;
+
+ buf = t_buffer_create(128);
+ stats_export(buf, stats);
+
+ str_append(str, "UPDATE-SESSION\t");
+ str_append(str, suser->stats_session_id);
+ str_append_c(str, '\t');
+ base64_encode(buf->data, buf->used, str);
+
+ str_append_c(str, '\n');
+ (void)stats_connection_send(conn, str);
+}
diff --git a/src/plugins/old-stats/mail-stats-connection.h b/src/plugins/old-stats/mail-stats-connection.h
new file mode 100644
index 0000000..f98b691
--- /dev/null
+++ b/src/plugins/old-stats/mail-stats-connection.h
@@ -0,0 +1,19 @@
+#ifndef MAIL_STATS_CONNECTION_H
+#define MAIL_STATS_CONNECTION_H
+
+#include "stats-connection.h"
+
+struct mail_stats;
+struct mail_user;
+
+int mail_stats_connection_connect(struct stats_connection *conn,
+ struct mail_user *user);
+void mail_stats_connection_disconnect(struct stats_connection *conn,
+ struct mail_user *user);
+
+void mail_stats_connection_send_session(struct stats_connection *conn,
+ struct mail_user *user,
+ const struct stats *stats);
+void mail_stats_connection_send(struct stats_connection *conn, const string_t *str);
+
+#endif
diff --git a/src/plugins/old-stats/mail-stats-fill.c b/src/plugins/old-stats/mail-stats-fill.c
new file mode 100644
index 0000000..10d8c39
--- /dev/null
+++ b/src/plugins/old-stats/mail-stats-fill.c
@@ -0,0 +1,156 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "time-util.h"
+#include "stats-plugin.h"
+#include "mail-stats.h"
+
+#include <sys/resource.h>
+
+#define PROC_IO_PATH "/proc/self/io"
+
+static bool proc_io_disabled = FALSE;
+static int proc_io_fd = -1;
+
+static int
+process_io_buffer_parse(const char *buf, struct mail_stats *stats)
+{
+ const char *const *tmp;
+
+ tmp = t_strsplit(buf, "\n");
+ for (; *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "rchar: ")) {
+ if (str_to_uint64(*tmp + 7, &stats->read_bytes) < 0)
+ return -1;
+ } else if (str_begins(*tmp, "wchar: ")) {
+ if (str_to_uint64(*tmp + 7, &stats->write_bytes) < 0)
+ return -1;
+ } else if (str_begins(*tmp, "syscr: ")) {
+ if (str_to_uint32(*tmp + 7, &stats->read_count) < 0)
+ return -1;
+ } else if (str_begins(*tmp, "syscw: ")) {
+ if (str_to_uint32(*tmp + 7, &stats->write_count) < 0)
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int process_io_open(void)
+{
+ uid_t uid;
+
+ if (proc_io_fd != -1)
+ return proc_io_fd;
+
+ if (proc_io_disabled)
+ return -1;
+
+ proc_io_fd = open(PROC_IO_PATH, O_RDONLY);
+ if (proc_io_fd == -1 && errno == EACCES) {
+ /* kludge: if we're running with permissions temporarily
+ dropped, get them temporarily back so we can open
+ /proc/self/io. */
+ uid = geteuid();
+ if (seteuid(0) == 0) {
+ proc_io_fd = open(PROC_IO_PATH, O_RDONLY);
+ if (seteuid(uid) < 0) {
+ /* oops, this is bad */
+ i_fatal("stats: seteuid(%s) failed", dec2str(uid));
+ }
+ }
+ errno = EACCES;
+ }
+ if (proc_io_fd == -1) {
+ /* ignore access errors too, certain security options can
+ prevent root access to this file when not owned by root */
+ if (errno != ENOENT && errno != EACCES)
+ i_error("open(%s) failed: %m", PROC_IO_PATH);
+ proc_io_disabled = TRUE;
+ return -1;
+ }
+ return proc_io_fd;
+}
+
+static void process_read_io_stats(struct mail_stats *stats)
+{
+ char buf[1024];
+ int fd, ret;
+
+ if ((fd = process_io_open()) == -1)
+ return;
+
+ ret = pread(fd, buf, sizeof(buf), 0);
+ if (ret <= 0) {
+ if (ret == -1)
+ i_error("read(%s) failed: %m", PROC_IO_PATH);
+ else
+ i_error("read(%s) returned EOF", PROC_IO_PATH);
+ } else if (ret == sizeof(buf)) {
+ /* just shouldn't happen.. */
+ i_error("%s is larger than expected", PROC_IO_PATH);
+ proc_io_disabled = TRUE;
+ } else {
+ buf[ret] = '\0';
+ T_BEGIN {
+ if (process_io_buffer_parse(buf, stats) < 0) {
+ i_error("Invalid input in file %s",
+ PROC_IO_PATH);
+ proc_io_disabled = TRUE;
+ }
+ } T_END;
+ }
+}
+
+static void
+user_trans_stats_get(struct stats_user *suser, struct mail_stats *dest_r)
+{
+ struct stats_transaction_context *strans;
+
+ mail_stats_add_transaction(dest_r, &suser->finished_transaction_stats);
+ for (strans = suser->transactions; strans != NULL; strans = strans->next)
+ mail_stats_add_transaction(dest_r, &strans->trans->stats);
+}
+
+void mail_stats_fill(struct stats_user *suser, struct mail_stats *stats_r)
+{
+ static bool getrusage_broken = FALSE;
+ static struct rusage prev_usage;
+ struct rusage usage;
+
+ i_zero(stats_r);
+ /* cputime */
+ if (getrusage(RUSAGE_SELF, &usage) < 0) {
+ if (!getrusage_broken) {
+ i_error("getrusage() failed: %m");
+ getrusage_broken = TRUE;
+ }
+ usage = prev_usage;
+ } else if (timeval_diff_usecs(&usage.ru_stime, &prev_usage.ru_stime) < 0) {
+ /* This seems to be a Linux bug. */
+ usage.ru_stime = prev_usage.ru_stime;
+ }
+ prev_usage = usage;
+
+ stats_r->user_cpu = usage.ru_utime;
+ stats_r->sys_cpu = usage.ru_stime;
+ stats_r->min_faults = usage.ru_minflt;
+ stats_r->maj_faults = usage.ru_majflt;
+ stats_r->vol_cs = usage.ru_nvcsw;
+ stats_r->invol_cs = usage.ru_nivcsw;
+ stats_r->disk_input = (unsigned long long)usage.ru_inblock * 512ULL;
+ stats_r->disk_output = (unsigned long long)usage.ru_oublock * 512ULL;
+ i_gettimeofday(&stats_r->clock_time);
+ process_read_io_stats(stats_r);
+ user_trans_stats_get(suser, stats_r);
+}
+
+void mail_stats_global_preinit(void)
+{
+ (void)process_io_open();
+}
+
+void mail_stats_fill_global_deinit(void)
+{
+ i_close_fd(&proc_io_fd);
+}
diff --git a/src/plugins/old-stats/mail-stats.c b/src/plugins/old-stats/mail-stats.c
new file mode 100644
index 0000000..3662d23
--- /dev/null
+++ b/src/plugins/old-stats/mail-stats.c
@@ -0,0 +1,168 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "time-util.h"
+#include "stats.h"
+#include "stats-parser.h"
+#include "mail-stats.h"
+
+static struct stats_parser_field mail_stats_fields[] = {
+#define E(parsename, name, type) { parsename, offsetof(struct mail_stats, name), sizeof(((struct mail_stats *)0)->name), type }
+#define EN(parsename, name) E(parsename, name, STATS_PARSER_TYPE_UINT)
+ E("user_cpu", user_cpu, STATS_PARSER_TYPE_TIMEVAL),
+ E("sys_cpu", sys_cpu, STATS_PARSER_TYPE_TIMEVAL),
+ E("clock_time", clock_time, STATS_PARSER_TYPE_TIMEVAL),
+ EN("min_faults", min_faults),
+ EN("maj_faults", maj_faults),
+ EN("vol_cs", vol_cs),
+ EN("invol_cs", invol_cs),
+ EN("disk_input", disk_input),
+ EN("disk_output", disk_output),
+
+ EN("read_count", read_count),
+ EN("read_bytes", read_bytes),
+ EN("write_count", write_count),
+ EN("write_bytes", write_bytes),
+
+ /*EN("mopen", trans_stats.open_lookup_count),
+ EN("mstat", trans_stats.stat_lookup_count),
+ EN("mfstat", trans_stats.fstat_lookup_count),*/
+ EN("mail_lookup_path", trans_lookup_path),
+ EN("mail_lookup_attr", trans_lookup_attr),
+ EN("mail_read_count", trans_files_read_count),
+ EN("mail_read_bytes", trans_files_read_bytes),
+ EN("mail_cache_hits", trans_cache_hit_count)
+};
+
+static size_t mail_stats_alloc_size(void)
+{
+ return sizeof(struct mail_stats);
+}
+
+static unsigned int mail_stats_field_count(void)
+{
+ return N_ELEMENTS(mail_stats_fields);
+}
+
+static const char *mail_stats_field_name(unsigned int n)
+{
+ i_assert(n < N_ELEMENTS(mail_stats_fields));
+
+ return mail_stats_fields[n].name;
+}
+
+static void
+mail_stats_field_value(string_t *str, const struct stats *stats,
+ unsigned int n)
+{
+ i_assert(n < N_ELEMENTS(mail_stats_fields));
+
+ stats_parser_value(str, &mail_stats_fields[n], stats);
+}
+
+static bool
+mail_stats_diff(const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r)
+{
+ return stats_parser_diff(mail_stats_fields, N_ELEMENTS(mail_stats_fields),
+ stats1, stats2, diff_stats_r, error_r);
+}
+
+static void mail_stats_add(struct stats *dest, const struct stats *src)
+{
+ stats_parser_add(mail_stats_fields, N_ELEMENTS(mail_stats_fields),
+ dest, src);
+}
+
+static bool
+mail_stats_have_changed(const struct stats *_prev, const struct stats *_cur)
+{
+ const struct mail_stats *prev = (const struct mail_stats *)_prev;
+ const struct mail_stats *cur = (const struct mail_stats *)_cur;
+
+ if (cur->disk_input != prev->disk_input ||
+ cur->disk_output != prev->disk_output ||
+ cur->trans_lookup_path != prev->trans_lookup_path ||
+ cur->trans_lookup_attr != prev->trans_lookup_attr ||
+ cur->trans_files_read_count != prev->trans_files_read_count ||
+ cur->trans_files_read_bytes != prev->trans_files_read_bytes ||
+ cur->trans_cache_hit_count != prev->trans_cache_hit_count)
+ return TRUE;
+
+ /* allow a tiny bit of changes that are caused by this
+ timeout handling */
+ if (timeval_diff_msecs(&cur->user_cpu, &prev->user_cpu) != 0)
+ return TRUE;
+ if (timeval_diff_msecs(&cur->sys_cpu, &prev->sys_cpu) != 0)
+ return TRUE;
+
+ if (cur->maj_faults > prev->maj_faults+10)
+ return TRUE;
+ if (cur->invol_cs > prev->invol_cs+10)
+ return TRUE;
+ /* don't check for read/write count/bytes changes, since they get
+ changed by stats checking itself */
+ return FALSE;
+}
+
+static void mail_stats_export(buffer_t *buf, const struct stats *_stats)
+{
+ const struct mail_stats *stats = (const struct mail_stats *)_stats;
+
+ buffer_append(buf, stats, sizeof(*stats));
+}
+
+static bool
+mail_stats_import(const unsigned char *data, size_t size, size_t *pos_r,
+ struct stats *_stats, const char **error_r)
+{
+ struct mail_stats *stats = (struct mail_stats *)_stats;
+
+ if (size < sizeof(*stats)) {
+ *error_r = "mail_stats too small";
+ return FALSE;
+ }
+ memcpy(stats, data, sizeof(*stats));
+ *pos_r = sizeof(*stats);
+ return TRUE;
+}
+
+void mail_stats_add_transaction(struct mail_stats *stats,
+ const struct mailbox_transaction_stats *trans_stats)
+{
+ stats->trans_lookup_path += trans_stats->open_lookup_count;
+ stats->trans_lookup_attr += trans_stats->stat_lookup_count +
+ trans_stats->fstat_lookup_count;
+ stats->trans_files_read_count += trans_stats->files_read_count;
+ stats->trans_files_read_bytes += trans_stats->files_read_bytes;
+ stats->trans_cache_hit_count += trans_stats->cache_hit_count;
+}
+
+const struct stats_vfuncs mail_stats_vfuncs = {
+ "mail",
+ mail_stats_alloc_size,
+ mail_stats_field_count,
+ mail_stats_field_name,
+ mail_stats_field_value,
+ mail_stats_diff,
+ mail_stats_add,
+ mail_stats_have_changed,
+ mail_stats_export,
+ mail_stats_import
+};
+
+/* for the stats_mail plugin: */
+void old_stats_mail_init(void);
+void old_stats_mail_deinit(void);
+
+static struct stats_item *mail_stats_item;
+
+void old_stats_mail_init(void)
+{
+ mail_stats_item = stats_register(&mail_stats_vfuncs);
+}
+
+void old_stats_mail_deinit(void)
+{
+ stats_unregister(&mail_stats_item);
+}
diff --git a/src/plugins/old-stats/mail-stats.h b/src/plugins/old-stats/mail-stats.h
new file mode 100644
index 0000000..7254b69
--- /dev/null
+++ b/src/plugins/old-stats/mail-stats.h
@@ -0,0 +1,41 @@
+#ifndef MAIL_STATS_H
+#define MAIL_STATS_H
+
+#include <sys/time.h>
+#include "mail-storage-private.h"
+
+struct stats_user;
+
+struct mail_stats {
+ /* user/system CPU time used */
+ struct timeval user_cpu, sys_cpu;
+ /* clock time used (not counting the time in ioloop wait) */
+ struct timeval clock_time;
+ /* minor / major page faults */
+ uint32_t min_faults, maj_faults;
+ /* voluntary / involuntary context switches */
+ uint32_t vol_cs, invol_cs;
+ /* disk input/output bytes */
+ uint64_t disk_input, disk_output;
+ /* read()/write() syscall count and number of bytes */
+ uint32_t read_count, write_count;
+ uint64_t read_bytes, write_bytes;
+
+ /* based on struct mailbox_transaction_stats: */
+ uint32_t trans_lookup_path;
+ uint32_t trans_lookup_attr;
+ uint32_t trans_files_read_count;
+ uint64_t trans_files_read_bytes;
+ uint64_t trans_cache_hit_count;
+};
+
+extern const struct stats_vfuncs mail_stats_vfuncs;
+
+void mail_stats_fill(struct stats_user *suser, struct mail_stats *mail_stats);
+void mail_stats_add_transaction(struct mail_stats *stats,
+ const struct mailbox_transaction_stats *trans_stats);
+
+void mail_stats_global_preinit(void);
+void mail_stats_fill_global_deinit(void);
+
+#endif
diff --git a/src/plugins/old-stats/stats-plugin.c b/src/plugins/old-stats/stats-plugin.c
new file mode 100644
index 0000000..9d33016
--- /dev/null
+++ b/src/plugins/old-stats/stats-plugin.c
@@ -0,0 +1,481 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "llist.h"
+#include "str.h"
+#include "time-util.h"
+#include "settings-parser.h"
+#include "mail-stats.h"
+#include "stats.h"
+#include "mail-stats-connection.h"
+#include "stats-plugin.h"
+
+#define STATS_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, stats_storage_module)
+#define STATS_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, stats_storage_module)
+
+/* If session isn't refreshed every 15 minutes, it's dropped.
+ Must be smaller than MAIL_SESSION_IDLE_TIMEOUT_MSECS in stats server */
+#define SESSION_STATS_FORCE_REFRESH_SECS (5*60)
+#define REFRESH_CHECK_INTERVAL 100
+#define MAIL_STATS_FIFO_NAME "old-stats-mail"
+
+struct stats_storage {
+ union mail_storage_module_context module_ctx;
+
+ struct mail_storage_callbacks old_callbacks;
+ void *old_context;
+};
+
+struct stats_mailbox {
+ union mailbox_module_context module_ctx;
+};
+
+const char *stats_plugin_version = DOVECOT_ABI_VERSION;
+
+struct stats_user_module stats_user_module =
+ MODULE_CONTEXT_INIT(&mail_user_module_register);
+struct stats_storage_module stats_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+
+static struct stats_item *mail_stats_item;
+static struct stats_connection *global_stats_conn = NULL;
+static struct mail_user *stats_global_user = NULL;
+static unsigned int stats_user_count = 0;
+
+static void session_stats_refresh_timeout(struct mail_user *user);
+
+static void stats_io_activate(struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ struct mail_stats *mail_stats;
+
+ if (stats_user_count == 1) {
+ /* the first user sets the global user. the second user sets
+ it to NULL. when we get back to one user we'll need to set
+ the global user again somewhere. do it here. */
+ stats_global_user = user;
+ /* skip time spent waiting in ioloop */
+ mail_stats = stats_fill_ptr(suser->pre_io_stats, mail_stats_item);
+ mail_stats->clock_time = ioloop_timeval;
+ } else {
+ i_assert(stats_global_user == NULL);
+
+ mail_user_stats_fill(user, suser->pre_io_stats);
+ }
+}
+
+static void stats_add_session(struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ struct stats *new_stats, *diff_stats;
+ const char *error;
+
+ new_stats = stats_alloc(pool_datastack_create());
+ diff_stats = stats_alloc(pool_datastack_create());
+
+ mail_user_stats_fill(user, new_stats);
+ /* we'll count new_stats-pre_io_stats and add the changes to
+ session_stats. the new_stats can't be directly copied to
+ session_stats because there are some fields that don't start from
+ zero, like clock_time. (actually with stats_global_user code we're
+ requiring that clock_time is the only such field..) */
+ if (!stats_diff(suser->pre_io_stats, new_stats, diff_stats, &error))
+ i_error("stats: session stats shrank: %s", error);
+ stats_add(suser->session_stats, diff_stats);
+ /* copying is only needed if stats_global_user=NULL */
+ stats_copy(suser->pre_io_stats, new_stats);
+}
+
+static bool
+session_stats_need_send(struct stats_user *suser, time_t now,
+ bool *changed_r, unsigned int *to_next_secs_r)
+{
+ unsigned int diff;
+
+ *to_next_secs_r = SESSION_STATS_FORCE_REFRESH_SECS;
+
+ if (stats_have_changed(suser->last_sent_session_stats,
+ suser->session_stats)) {
+ *to_next_secs_r = suser->refresh_secs;
+ *changed_r = TRUE;
+ return TRUE;
+ }
+ *changed_r = FALSE;
+
+ diff = now - suser->last_session_update;
+ if (diff >= SESSION_STATS_FORCE_REFRESH_SECS)
+ return TRUE;
+ *to_next_secs_r = SESSION_STATS_FORCE_REFRESH_SECS - diff;
+
+ if (!suser->session_sent_duplicate) {
+ if (suser->last_session_update != now) {
+ /* send one duplicate notification so stats reader
+ knows that this session is idle now */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void session_stats_refresh(struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ unsigned int to_next_secs;
+ time_t now = time(NULL);
+ bool changed;
+
+ if (!suser->stats_connected) {
+ if (mail_stats_connection_connect(suser->stats_conn, user) == 0)
+ suser->stats_connected = TRUE;
+ }
+ if (session_stats_need_send(suser, now, &changed, &to_next_secs) &&
+ suser->stats_connected) {
+ suser->session_sent_duplicate = !changed;
+ suser->last_session_update = now;
+ stats_copy(suser->last_sent_session_stats, suser->session_stats);
+ mail_stats_connection_send_session(suser->stats_conn, user,
+ suser->session_stats);
+ }
+
+ timeout_remove(&suser->to_stats_timeout);
+ suser->to_stats_timeout =
+ timeout_add(to_next_secs*1000,
+ session_stats_refresh_timeout, user);
+}
+
+static struct mailbox_transaction_context *
+stats_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(box->storage->user);
+ struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(box);
+ struct mailbox_transaction_context *trans;
+ struct stats_transaction_context *strans;
+
+ trans = sbox->module_ctx.super.transaction_begin(box, flags, reason);
+ trans->stats_track = TRUE;
+
+ strans = i_new(struct stats_transaction_context, 1);
+ strans->trans = trans;
+ DLLIST_PREPEND(&suser->transactions, strans);
+
+ MODULE_CONTEXT_SET(trans, stats_storage_module, strans);
+ return trans;
+}
+
+static void stats_transaction_free(struct stats_user *suser,
+ struct stats_transaction_context *strans)
+{
+ const struct mailbox_transaction_stats *src = &strans->trans->stats;
+ struct mailbox_transaction_stats *dest =
+ &suser->finished_transaction_stats;
+
+ DLLIST_REMOVE(&suser->transactions, strans);
+
+ dest->open_lookup_count += src->open_lookup_count;
+ dest->stat_lookup_count += src->stat_lookup_count;
+ dest->fstat_lookup_count += src->fstat_lookup_count;
+ dest->files_read_count += src->files_read_count;
+ dest->files_read_bytes += src->files_read_bytes;
+ dest->cache_hit_count += src->cache_hit_count;
+ i_free(strans);
+}
+
+static int
+stats_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct stats_transaction_context *strans = STATS_CONTEXT_REQUIRE(ctx);
+ struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->box);
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(ctx->box->storage->user);
+
+ stats_transaction_free(suser, strans);
+ return sbox->module_ctx.super.transaction_commit(ctx, changes_r);
+}
+
+static void
+stats_transaction_rollback(struct mailbox_transaction_context *ctx)
+{
+ struct stats_transaction_context *strans = STATS_CONTEXT_REQUIRE(ctx);
+ struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->box);
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(ctx->box->storage->user);
+
+ stats_transaction_free(suser, strans);
+ sbox->module_ctx.super.transaction_rollback(ctx);
+}
+
+static bool stats_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct mail_user *user = ctx->transaction->box->storage->user;
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ bool ret;
+
+ ret = sbox->module_ctx.super.
+ search_next_nonblock(ctx, mail_r, tryagain_r);
+ if (!ret && !*tryagain_r) {
+ /* end of search */
+ return FALSE;
+ }
+
+ if (*tryagain_r ||
+ ++suser->refresh_check_counter % REFRESH_CHECK_INTERVAL == 0) {
+ /* a) retrying, so this is a long running search.
+ b) we've returned enough matches */
+ if (time(NULL) != suser->last_session_update)
+ session_stats_refresh(user);
+ }
+ return ret;
+}
+
+static void
+stats_notify_ok(struct mailbox *box, const char *text, void *context)
+{
+ struct stats_storage *sstorage = STATS_CONTEXT_REQUIRE(box->storage);
+
+ /* most importantly we want to refresh stats for very long running
+ mailbox syncs */
+ session_stats_refresh(box->storage->user);
+
+ if (sstorage->old_callbacks.notify_ok != NULL)
+ sstorage->old_callbacks.notify_ok(box, text, context);
+}
+
+static void stats_register_notify_callbacks(struct mail_storage *storage)
+{
+ struct stats_storage *sstorage = STATS_CONTEXT(storage);
+
+ if (sstorage != NULL)
+ return;
+
+ sstorage = p_new(storage->pool, struct stats_storage, 1);
+ sstorage->old_callbacks = storage->callbacks;
+ storage->callbacks.notify_ok = stats_notify_ok;
+
+ MODULE_CONTEXT_SET(storage, stats_storage_module, sstorage);
+}
+
+static void stats_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct stats_mailbox *sbox;
+ struct stats_user *suser = STATS_USER_CONTEXT(box->storage->user);
+
+ if (suser == NULL)
+ return;
+
+ stats_register_notify_callbacks(box->storage);
+
+ sbox = p_new(box->pool, struct stats_mailbox, 1);
+ sbox->module_ctx.super = *v;
+ box->vlast = &sbox->module_ctx.super;
+
+ v->transaction_begin = stats_transaction_begin;
+ v->transaction_commit = stats_transaction_commit;
+ v->transaction_rollback = stats_transaction_rollback;
+ v->search_next_nonblock = stats_search_next_nonblock;
+ MODULE_CONTEXT_SET(box, stats_storage_module, sbox);
+}
+
+static void session_stats_refresh_timeout(struct mail_user *user)
+{
+ if (stats_global_user != NULL)
+ stats_add_session(user);
+ session_stats_refresh(user);
+}
+
+static void stats_io_deactivate(struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ unsigned int last_update_secs;
+
+ if (stats_global_user == NULL)
+ stats_add_session(user);
+
+ last_update_secs = time(NULL) - suser->last_session_update;
+ if (last_update_secs >= suser->refresh_secs) {
+ if (stats_global_user != NULL)
+ stats_add_session(user);
+ session_stats_refresh(user);
+ } else if (suser->to_stats_timeout == NULL) {
+ suser->to_stats_timeout =
+ timeout_add(suser->refresh_secs*1000,
+ session_stats_refresh_timeout, user);
+ }
+}
+
+static void stats_user_stats_fill(struct mail_user *user, struct stats *stats)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ struct mail_stats *mail_stats;
+
+ mail_stats = stats_fill_ptr(stats, mail_stats_item);
+ mail_stats_fill(suser, mail_stats);
+
+ suser->module_ctx.super.stats_fill(user, stats);
+}
+
+static void stats_user_deinit(struct mail_user *user)
+{
+ struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user);
+ struct stats_connection *stats_conn = suser->stats_conn;
+
+ i_assert(stats_user_count > 0);
+
+ stats_user_count--;
+ if (stats_global_user != NULL) {
+ /* we were updating the session lazily. do one final update. */
+ i_assert(stats_global_user == user);
+ stats_add_session(user);
+ stats_global_user = NULL;
+ }
+
+ io_loop_context_remove_callbacks(suser->ioloop_ctx,
+ stats_io_activate,
+ stats_io_deactivate, user);
+ /* send final stats before disconnection */
+ session_stats_refresh(user);
+ if (suser->stats_connected)
+ mail_stats_connection_disconnect(stats_conn, user);
+
+ timeout_remove(&suser->to_stats_timeout);
+ suser->module_ctx.super.deinit(user);
+
+ stats_connection_unref(&stats_conn);
+}
+
+static void stats_user_created(struct mail_user *user)
+{
+ struct ioloop_context *ioloop_ctx =
+ io_loop_get_current_context(current_ioloop);
+ struct stats_user *suser;
+ struct mail_user_vfuncs *v = user->vlast;
+ const char *path, *str, *error;
+ unsigned int refresh_secs;
+
+ if (ioloop_ctx == NULL) {
+ /* we're probably running some test program, or at least
+ mail-storage-service wasn't used to create this user.
+ disable stats tracking. */
+ return;
+ }
+ if (user->autocreated) {
+ /* lda / shared user. we're not tracking this one. */
+ return;
+ }
+
+ /* get refresh time */
+ str = mail_user_plugin_getenv(user, "old_stats_refresh");
+ if (str == NULL)
+ return;
+ if (settings_get_time(str, &refresh_secs, &error) < 0) {
+ i_error("stats: Invalid old_stats_refresh setting: %s", error);
+ return;
+ }
+ if (refresh_secs == 0)
+ return;
+ if (refresh_secs > SESSION_STATS_FORCE_REFRESH_SECS) {
+ i_warning("stats: stats_refresh too large, changing to %u",
+ SESSION_STATS_FORCE_REFRESH_SECS);
+ refresh_secs = SESSION_STATS_FORCE_REFRESH_SECS;
+ }
+
+ if (global_stats_conn == NULL) {
+ path = mail_user_plugin_getenv(user, "old_stats_notify_path");
+ if (path == NULL)
+ path = MAIL_STATS_FIFO_NAME;
+ if (path[0] != '/')
+ path = t_strconcat(user->set->base_dir, "/", path, NULL);
+ global_stats_conn = stats_connection_create(path);
+ }
+ stats_connection_ref(global_stats_conn);
+
+ if (stats_user_count == 0) {
+ /* first user connection */
+ stats_global_user = user;
+ } else if (stats_user_count == 1) {
+ /* second user connection. we'll need to start doing
+ per-io callback tracking now. (we might have been doing it
+ also previously but just temporarily quickly dropped to
+ having 1 user, in which case stats_global_user=NULL) */
+ if (stats_global_user != NULL) {
+ stats_add_session(stats_global_user);
+ stats_global_user = NULL;
+ }
+ }
+ stats_user_count++;
+
+ suser = p_new(user->pool, struct stats_user, 1);
+ suser->module_ctx.super = *v;
+ user->vlast = &suser->module_ctx.super;
+ v->deinit = stats_user_deinit;
+ v->stats_fill = stats_user_stats_fill;
+
+ suser->refresh_secs = refresh_secs;
+ if (mail_user_plugin_getenv_bool(user, "old_stats_track_cmds"))
+ suser->track_commands = TRUE;
+
+ suser->stats_conn = global_stats_conn;
+ if (user->session_id != NULL && user->session_id[0] != '\0')
+ suser->stats_session_id = user->session_id;
+ else {
+ guid_128_t guid;
+
+ guid_128_generate(guid);
+ suser->stats_session_id =
+ p_strdup(user->pool, guid_128_to_string(guid));
+ }
+ suser->last_session_update = time(NULL);
+ user->stats_enabled = TRUE;
+
+ suser->ioloop_ctx = ioloop_ctx;
+ io_loop_context_add_callbacks(ioloop_ctx,
+ stats_io_activate,
+ stats_io_deactivate, user);
+
+ suser->pre_io_stats = stats_alloc(user->pool);
+ suser->session_stats = stats_alloc(user->pool);
+ suser->last_sent_session_stats = stats_alloc(user->pool);
+
+ MODULE_CONTEXT_SET(user, stats_user_module, suser);
+ if (mail_stats_connection_connect(suser->stats_conn, user) == 0)
+ suser->stats_connected = TRUE;
+ suser->to_stats_timeout =
+ timeout_add(suser->refresh_secs*1000,
+ session_stats_refresh_timeout, user);
+ /* fill the initial values. this is necessary for the process-global
+ values (e.g. getrusage()) if the process is reused for multiple
+ users. otherwise the next user will start with the previous one's
+ last values. */
+ mail_user_stats_fill(user, suser->pre_io_stats);
+}
+
+static struct mail_storage_hooks stats_mail_storage_hooks = {
+ .mailbox_allocated = stats_mailbox_allocated,
+ .mail_user_created = stats_user_created
+};
+
+void old_stats_plugin_init(struct module *module)
+{
+ mail_stats_item = stats_register(&mail_stats_vfuncs);
+ mail_storage_hooks_add(module, &stats_mail_storage_hooks);
+}
+
+void old_stats_plugin_preinit(void)
+{
+ mail_stats_global_preinit();
+}
+
+void old_stats_plugin_deinit(void)
+{
+ if (global_stats_conn != NULL)
+ stats_connection_unref(&global_stats_conn);
+ mail_stats_fill_global_deinit();
+ mail_storage_hooks_remove(&stats_mail_storage_hooks);
+ stats_unregister(&mail_stats_item);
+}
diff --git a/src/plugins/old-stats/stats-plugin.h b/src/plugins/old-stats/stats-plugin.h
new file mode 100644
index 0000000..dedcfbd
--- /dev/null
+++ b/src/plugins/old-stats/stats-plugin.h
@@ -0,0 +1,60 @@
+#ifndef STATS_PLUGIN_H
+#define STATS_PLUGIN_H
+
+#include "module-context.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+
+#define STATS_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, stats_user_module)
+
+#define STATS_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, stats_user_module)
+
+struct stats_user {
+ union mail_user_module_context module_ctx;
+
+ struct ioloop_context *ioloop_ctx;
+ struct stats_connection *stats_conn;
+ const char *stats_session_id;
+ bool stats_connected;
+
+ unsigned int refresh_secs;
+ bool track_commands;
+ unsigned int refresh_check_counter;
+
+ /* current session statistics */
+ struct stats *session_stats;
+ /* cumulative trans_stats for all already freed transactions. */
+ struct mailbox_transaction_stats finished_transaction_stats;
+ /* stats before calling IO callback. after IO callback this value is
+ compared to current stats to see the difference */
+ struct stats *pre_io_stats;
+
+ time_t last_session_update;
+ struct timeout *to_stats_timeout;
+ /* stats that were last sent to stats server */
+ struct stats *last_sent_session_stats;
+ bool session_sent_duplicate;
+
+ /* list of all currently existing transactions for this user */
+ struct stats_transaction_context *transactions;
+};
+
+struct stats_transaction_context {
+ union mailbox_transaction_module_context module_ctx;
+
+ struct stats_transaction_context *prev, *next;
+ struct mailbox_transaction_context *trans;
+
+ struct mailbox_transaction_stats prev_stats;
+};
+
+extern MODULE_CONTEXT_DEFINE(stats_user_module, &mail_user_module_register);
+extern MODULE_CONTEXT_DEFINE(stats_storage_module, &mail_storage_module_register);
+
+void old_stats_plugin_init(struct module *module);
+void old_stats_plugin_preinit(void);
+void old_stats_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/pop3-migration/Makefile.am b/src/plugins/pop3-migration/Makefile.am
new file mode 100644
index 0000000..f58d065
--- /dev/null
+++ b/src/plugins/pop3-migration/Makefile.am
@@ -0,0 +1,41 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+NOPLUGIN_LDFLAGS =
+lib05_pop3_migration_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib05_pop3_migration_plugin.la
+
+lib05_pop3_migration_plugin_la_SOURCES = \
+ pop3-migration-plugin.c
+
+noinst_HEADERS = \
+ pop3-migration-plugin.h
+
+noinst_PROGRAMS = $(test_programs)
+
+test_programs = \
+ test-pop3-migration-plugin
+
+test_libs = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+test_deps = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+test_pop3_migration_plugin_SOURCES = test-pop3-migration-plugin.c
+test_pop3_migration_plugin_LDADD = pop3-migration-plugin.lo $(test_libs)
+test_pop3_migration_plugin_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/plugins/pop3-migration/Makefile.in b/src/plugins/pop3-migration/Makefile.in
new file mode 100644
index 0000000..7e34282
--- /dev/null
+++ b/src/plugins/pop3-migration/Makefile.in
@@ -0,0 +1,872 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/pop3-migration
+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-pop3-migration-plugin$(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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib05_pop3_migration_plugin_la_LIBADD =
+am_lib05_pop3_migration_plugin_la_OBJECTS = pop3-migration-plugin.lo
+lib05_pop3_migration_plugin_la_OBJECTS = \
+ $(am_lib05_pop3_migration_plugin_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 =
+lib05_pop3_migration_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib05_pop3_migration_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am_test_pop3_migration_plugin_OBJECTS = \
+ test-pop3-migration-plugin.$(OBJEXT)
+test_pop3_migration_plugin_OBJECTS = \
+ $(am_test_pop3_migration_plugin_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(am__DEPENDENCIES_1) $(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)/pop3-migration-plugin.Plo \
+ ./$(DEPDIR)/test-pop3-migration-plugin.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 = $(lib05_pop3_migration_plugin_la_SOURCES) \
+ $(test_pop3_migration_plugin_SOURCES)
+DIST_SOURCES = $(lib05_pop3_migration_plugin_la_SOURCES) \
+ $(test_pop3_migration_plugin_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@
+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-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+lib05_pop3_migration_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib05_pop3_migration_plugin.la
+
+lib05_pop3_migration_plugin_la_SOURCES = \
+ pop3-migration-plugin.c
+
+noinst_HEADERS = \
+ pop3-migration-plugin.h
+
+test_programs = \
+ test-pop3-migration-plugin
+
+test_libs = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+test_deps = \
+ $(module_LTLIBRARIES) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+test_pop3_migration_plugin_SOURCES = test-pop3-migration-plugin.c
+test_pop3_migration_plugin_LDADD = pop3-migration-plugin.lo $(test_libs)
+test_pop3_migration_plugin_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/plugins/pop3-migration/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/pop3-migration/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}; \
+ }
+
+lib05_pop3_migration_plugin.la: $(lib05_pop3_migration_plugin_la_OBJECTS) $(lib05_pop3_migration_plugin_la_DEPENDENCIES) $(EXTRA_lib05_pop3_migration_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib05_pop3_migration_plugin_la_LINK) -rpath $(moduledir) $(lib05_pop3_migration_plugin_la_OBJECTS) $(lib05_pop3_migration_plugin_la_LIBADD) $(LIBS)
+
+test-pop3-migration-plugin$(EXEEXT): $(test_pop3_migration_plugin_OBJECTS) $(test_pop3_migration_plugin_DEPENDENCIES) $(EXTRA_test_pop3_migration_plugin_DEPENDENCIES)
+ @rm -f test-pop3-migration-plugin$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_pop3_migration_plugin_OBJECTS) $(test_pop3_migration_plugin_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-migration-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-pop3-migration-plugin.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)"; 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-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/pop3-migration-plugin.Plo
+ -rm -f ./$(DEPDIR)/test-pop3-migration-plugin.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-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)/pop3-migration-plugin.Plo
+ -rm -f ./$(DEPDIR)/test-pop3-migration-plugin.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
+
+.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-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-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
+
+.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/plugins/pop3-migration/pop3-migration-plugin.c b/src/plugins/pop3-migration/pop3-migration-plugin.c
new file mode 100644
index 0000000..362597a
--- /dev/null
+++ b/src/plugins/pop3-migration/pop3-migration-plugin.c
@@ -0,0 +1,1058 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-header-filter.h"
+#include "str.h"
+#include "sha1.h"
+#include "message-size.h"
+#include "message-header-hash.h"
+#include "message-header-parser.h"
+#include "mail-cache.h"
+#include "mail-namespace.h"
+#include "mail-search-build.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "pop3-migration-plugin.h"
+
+#define POP3_MIGRATION_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, pop3_migration_storage_module)
+#define POP3_MIGRATION_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, pop3_migration_storage_module)
+#define POP3_MIGRATION_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, pop3_migration_mail_module)
+
+struct msg_map_common {
+ /* sha1(header) - set only when needed */
+ unsigned char hdr_sha1[SHA1_RESULTLEN];
+ bool hdr_sha1_set:1;
+};
+
+struct pop3_uidl_map {
+ struct msg_map_common common;
+
+ uint32_t pop3_seq;
+ uint32_t imap_uid;
+
+ /* UIDL */
+ const char *pop3_uidl;
+ /* LIST size */
+ uoff_t size;
+};
+
+struct imap_msg_map {
+ struct msg_map_common common;
+
+ uint32_t uid, pop3_seq;
+ uoff_t psize;
+ const char *pop3_uidl;
+};
+
+struct pop3_migration_mail_storage {
+ union mail_storage_module_context module_ctx;
+
+ const char *pop3_box_vname;
+ ARRAY(struct pop3_uidl_map) pop3_uidl_map;
+
+ bool all_mailboxes:1;
+ bool pop3_all_hdr_sha1_set:1;
+ bool ignore_missing_uidls:1;
+ bool ignore_extra_uidls:1;
+ bool skip_size_check:1;
+ bool skip_uidl_cache:1;
+};
+
+struct pop3_migration_mailbox {
+ union mailbox_module_context module_ctx;
+
+ ARRAY(struct imap_msg_map) imap_msg_map;
+ unsigned int first_unfound_idx;
+
+ struct mail_cache_field cache_field;
+
+ bool cache_field_registered:1;
+ bool uidl_synced:1;
+ bool uidl_sync_failed:1;
+};
+
+/* NOTE: these headers must be sorted */
+static const char *hdr_hash_skip_headers[] = {
+ "Content-Length",
+ "Return-Path", /* Yahoo IMAP has Return-Path, Yahoo POP3 doesn't */
+ "Status",
+ "X-IMAP",
+ "X-IMAPbase",
+ "X-Keywords",
+ "X-Message-Flag",
+ "X-Status",
+ "X-UID",
+ "X-UIDL",
+ "X-Yahoo-Newman-Property"
+};
+const char *pop3_migration_plugin_version = DOVECOT_ABI_VERSION;
+
+static MODULE_CONTEXT_DEFINE_INIT(pop3_migration_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(pop3_migration_mail_module,
+ &mail_module_register);
+
+static int imap_msg_map_uid_cmp(const struct imap_msg_map *map1,
+ const struct imap_msg_map *map2)
+{
+ if (map1->uid < map2->uid)
+ return -1;
+ if (map1->uid > map2->uid)
+ return 1;
+ return 0;
+}
+
+static int pop3_uidl_map_pop3_seq_cmp(const struct pop3_uidl_map *map1,
+ const struct pop3_uidl_map *map2)
+{
+ if (map1->pop3_seq < map2->pop3_seq)
+ return -1;
+ if (map1->pop3_seq > map2->pop3_seq)
+ return 1;
+ return 0;
+}
+
+static int pop3_uidl_map_uidl_cmp(const struct pop3_uidl_map *map1,
+ const struct pop3_uidl_map *map2)
+{
+ return strcmp(map1->pop3_uidl, map2->pop3_uidl);
+}
+
+static int imap_msg_map_uidl_cmp(const struct imap_msg_map *map1,
+ const struct imap_msg_map *map2)
+{
+ return null_strcmp(map1->pop3_uidl, map2->pop3_uidl);
+}
+
+static int pop3_uidl_map_hdr_cmp(const struct pop3_uidl_map *map1,
+ const struct pop3_uidl_map *map2)
+{
+ return memcmp(map1->common.hdr_sha1, map2->common.hdr_sha1,
+ sizeof(map1->common.hdr_sha1));
+}
+
+static int imap_msg_map_hdr_cmp(const struct imap_msg_map *map1,
+ const struct imap_msg_map *map2)
+{
+ return memcmp(map1->common.hdr_sha1, map2->common.hdr_sha1,
+ sizeof(map1->common.hdr_sha1));
+}
+
+struct pop3_hdr_context {
+ bool have_eoh;
+ bool stop;
+};
+
+static bool header_name_is_valid(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; name[i] != '\0'; i++) {
+ if ((uint8_t)name[i] <= 0x20 || name[i] >= 0x7f)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool header_value_want_skip(const struct message_header_line *hdr)
+{
+ for (size_t i = 0; i < hdr->value_len; i++) {
+ if (hdr->value[i] != ' ' && hdr->value[i] != '\t')
+ return FALSE;
+ }
+ /* "header: \r\n \r\n" - Zimbra's BODY[HEADER] strips this line away. */
+ return TRUE;
+}
+
+static void
+pop3_header_filter_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, struct pop3_hdr_context *ctx)
+{
+ if (hdr == NULL)
+ return;
+ if (hdr->eoh) {
+ ctx->have_eoh = TRUE;
+ if (ctx->stop)
+ *matched = TRUE;
+ } else {
+ if (hdr->value_len > 0 && hdr->middle_len == 0 && hdr->name_len == 0 &&
+ i_memspn(hdr->value, hdr->value_len, "\r", 1U) == hdr->value_len) {
+ /* CR+CR+LF - some servers stop the header processing
+ here while others don't. To make sure they can be
+ matched correctly we want to stop here entirely. */
+ ctx->stop = TRUE;
+ } else if (!hdr->continued && hdr->middle_len == 0) {
+ /* not a valid "key: value" header -
+ Zimbra's BODY[HEADER] strips this line away. */
+ *matched = TRUE;
+ } else if (hdr->continued && header_value_want_skip(hdr)) {
+ *matched = TRUE;
+ }
+ if (ctx->stop)
+ *matched = TRUE;
+ else if (!header_name_is_valid(hdr->name)) {
+ /* Yahoo IMAP drops headers with invalid names, while
+ Yahoo POP3 preserves them. Drop them all. */
+ *matched = TRUE;
+ }
+ }
+}
+
+int pop3_migration_get_hdr_sha1(uint32_t mail_seq, struct istream *input,
+ unsigned char sha1_r[STATIC_ARRAY SHA1_RESULTLEN],
+ bool *have_eoh_r)
+{
+ const unsigned char *data;
+ size_t size;
+ struct message_header_hash_context hash_ctx;
+ struct sha1_ctxt sha1_ctx;
+ struct pop3_hdr_context hdr_ctx;
+
+ i_zero(&hdr_ctx);
+ /* hide headers that might change or be different in IMAP vs. POP3 */
+ input = i_stream_create_header_filter(input, HEADER_FILTER_HIDE_BODY |
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ hdr_hash_skip_headers,
+ N_ELEMENTS(hdr_hash_skip_headers),
+ pop3_header_filter_callback, &hdr_ctx);
+
+ sha1_init(&sha1_ctx);
+ i_zero(&hash_ctx);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ message_header_hash_more(&hash_ctx, &hash_method_sha1, &sha1_ctx,
+ MESSAGE_HEADER_HASH_MAX_VERSION,
+ data, size);
+ i_stream_skip(input, size);
+ }
+ if (input->stream_errno != 0) {
+ i_error("pop3_migration: Failed to read header for msg %u: %s",
+ mail_seq, i_stream_get_error(input));
+ i_stream_unref(&input);
+ return -1;
+ }
+ sha1_result(&sha1_ctx, sha1_r);
+ i_stream_unref(&input);
+
+ *have_eoh_r = hdr_ctx.have_eoh;
+ return 0;
+}
+
+static unsigned int get_cache_idx(struct mail *mail)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(mail->box);
+
+ if (mbox->cache_field_registered)
+ return mbox->cache_field.idx;
+
+ mbox->cache_field.name = "pop3-migration.hdr";
+ mbox->cache_field.type = MAIL_CACHE_FIELD_FIXED_SIZE;
+ mbox->cache_field.field_size = SHA1_RESULTLEN;
+ mail_cache_register_fields(mail->box->cache, &mbox->cache_field, 1);
+ mbox->cache_field_registered = TRUE;
+ return mbox->cache_field.idx;
+}
+
+static int
+get_hdr_sha1(struct mail *mail, unsigned char sha1_r[STATIC_ARRAY SHA1_RESULTLEN])
+{
+ struct istream *input;
+ const char *errstr;
+ enum mail_error error;
+ bool have_eoh;
+ int ret;
+
+ if (mail_get_hdr_stream(mail, NULL, &input) < 0) {
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ i_error("pop3_migration: Failed to get header for msg %u: %s",
+ mail->seq, errstr);
+ return error == MAIL_ERROR_EXPUNGED ? 0 : -1;
+ }
+ if (pop3_migration_get_hdr_sha1(mail->seq, input, sha1_r, &have_eoh) < 0)
+ return -1;
+ if (have_eoh) {
+ struct index_mail *imail = (struct index_mail *)mail;
+
+ index_mail_cache_add_idx(imail, get_cache_idx(mail),
+ sha1_r, SHA1_RESULTLEN);
+ return 1;
+ }
+
+ /* The empty "end of headers" line is missing. Either this means that
+ the headers ended unexpectedly (which is ok) or that the remote
+ server is buggy. Some servers have problems with
+
+ 1) header line continuations that contain only whitespace and
+ 2) headers that have no ":". The header gets truncated when such
+ line is reached.
+
+ At least Oracle IMS IMAP FETCH BODY[HEADER] handles 1) by not
+ returning the whitespace line and 2) by returning the line but
+ truncating the rest. POP3 TOP instead returns the entire header.
+ This causes the IMAP and POP3 hashes not to match.
+
+ If there's LF+CR+CR+LF in the middle of headers, Courier IMAP's
+ FETCH BODY[HEADER] stops after that, but Courier POP3's TOP doesn't.
+
+ So we'll try to avoid this by falling back to full FETCH BODY[]
+ (and/or RETR) and we'll parse the header ourself from it. This
+ should work around any similar bugs in all IMAP/POP3 servers. */
+ if (mail_get_stream_because(mail, NULL, NULL, "pop3-migration", &input) < 0) {
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ i_error("pop3_migration: Failed to get body for msg %u: %s",
+ mail->seq, errstr);
+ return error == MAIL_ERROR_EXPUNGED ? 0 : -1;
+ }
+ ret = pop3_migration_get_hdr_sha1(mail->seq, input, sha1_r, &have_eoh);
+ if (ret == 0) {
+ if (!have_eoh)
+ i_warning("pop3_migration: Truncated email with UID %u stored as truncated", mail->uid);
+ struct index_mail *imail = (struct index_mail *)mail;
+ index_mail_cache_add_idx(imail, get_cache_idx(mail),
+ sha1_r, SHA1_RESULTLEN);
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+static bool
+get_cached_hdr_sha1(struct mail *mail, buffer_t *cache_buf,
+ unsigned char sha1_r[STATIC_ARRAY SHA1_RESULTLEN])
+{
+ struct index_mail *imail = (struct index_mail *)mail;
+
+ buffer_set_used_size(cache_buf, 0);
+ if (index_mail_cache_lookup_field(imail, cache_buf,
+ get_cache_idx(mail)) > 0 &&
+ cache_buf->used == SHA1_RESULTLEN) {
+ memcpy(sha1_r, cache_buf->data, cache_buf->used);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct mailbox *pop3_mailbox_alloc(struct mail_storage *storage)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(storage);
+ struct mail_namespace *ns;
+ struct mailbox *box;
+
+ ns = mail_namespace_find(storage->user->namespaces,
+ mstorage->pop3_box_vname);
+ i_assert(ns != NULL);
+ box = mailbox_alloc(ns->list, mstorage->pop3_box_vname,
+ MAILBOX_FLAG_READONLY | MAILBOX_FLAG_POP3_SESSION);
+ return box;
+}
+
+static int pop3_map_read(struct mail_storage *storage, struct mailbox *pop3_box)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(storage);
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct pop3_uidl_map *map;
+ const char *uidl;
+ uoff_t size = UOFF_T_MAX;
+ int ret = 0;
+
+ if (array_is_created(&mstorage->pop3_uidl_map)) {
+ /* already read these, just reset the imap_uids */
+ array_foreach_modifiable(&mstorage->pop3_uidl_map, map)
+ map->imap_uid = 0;
+ return 0;
+ }
+ i_array_init(&mstorage->pop3_uidl_map, 128);
+
+ if (mailbox_sync(pop3_box, 0) < 0) {
+ i_error("pop3_migration: Couldn't sync mailbox %s: %s",
+ pop3_box->vname, mailbox_get_last_internal_error(pop3_box, NULL));
+ return -1;
+ }
+
+ t = mailbox_transaction_begin(pop3_box, 0, __func__);
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+ ctx = mailbox_search_init(t, search_args, NULL,
+ mstorage->skip_size_check ? 0 :
+ MAIL_FETCH_PHYSICAL_SIZE, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail)) {
+ /* get the size with LIST instead of RETR */
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+
+ if (mstorage->skip_size_check)
+ ;
+ else if (mail_get_physical_size(mail, &size) < 0) {
+ i_error("pop3_migration: Failed to get size for msg %u: %s",
+ mail->seq,
+ mailbox_get_last_internal_error(pop3_box, NULL));
+ ret = -1;
+ break;
+ }
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+
+ if (mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &uidl) < 0) {
+ i_error("pop3_migration: Failed to get UIDL for msg %u: %s",
+ mail->seq,
+ mailbox_get_last_internal_error(pop3_box, NULL));
+ ret = -1;
+ break;
+ }
+ if (*uidl == '\0') {
+ i_warning("pop3_migration: UIDL for msg %u is empty",
+ mail->seq);
+ continue;
+ }
+
+ map = array_append_space(&mstorage->pop3_uidl_map);
+ map->pop3_seq = mail->seq;
+ map->pop3_uidl = p_strdup(storage->pool, uidl);
+ map->size = size;
+ }
+
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_error("pop3_migration: Failed to search all POP3 mails: %s",
+ mailbox_get_last_internal_error(pop3_box, NULL));
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&t);
+ return ret;
+}
+
+static void
+pop3_map_read_cached_hdr_hashes(struct mailbox_transaction_context *t,
+ struct mail_search_args *search_args,
+ struct array *msg_map)
+{
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct msg_map_common *map;
+ buffer_t *cache_buf;
+
+ ctx = mailbox_search_init(t, search_args, NULL, 0, NULL);
+ cache_buf = t_buffer_create(SHA1_RESULTLEN);
+
+ while (mailbox_search_next(ctx, &mail)) {
+ map = array_idx_modifiable_i(msg_map, mail->seq-1);
+
+ if (get_cached_hdr_sha1(mail, cache_buf, map->hdr_sha1))
+ map->hdr_sha1_set = TRUE;
+ }
+
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_warning("pop3_migration: Failed to search all cached POP3 header hashes: %s - ignoring",
+ mailbox_get_last_internal_error(t->box, NULL));
+ }
+}
+
+static void map_remove_found_seqs(struct mail_search_arg *search_arg,
+ struct array *msg_map, uint32_t seq1)
+{
+ const struct msg_map_common *map;
+ uint32_t seq, count = array_count_i(msg_map);
+
+ i_assert(search_arg->type == SEARCH_SEQSET);
+
+ for (seq = seq1; seq <= count; seq++) {
+ map = array_idx_i(msg_map, seq-1);
+ if (map->hdr_sha1_set)
+ seq_range_array_remove(&search_arg->value.seqset, seq);
+ }
+}
+
+static int
+map_read_hdr_hashes(struct mailbox *box, struct array *msg_map, uint32_t seq1)
+{
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct msg_map_common *map;
+ int ret = 0;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ /* get all the cached hashes */
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq1, array_count_i(msg_map));
+ pop3_map_read_cached_hdr_hashes(t, search_args, msg_map);
+ /* read all the non-cached hashes. doing this in two passes allows
+ us to set wanted_fields=MAIL_FETCH_STREAM_HEADER, which allows
+ prefetching to work without downloading all the headers even
+ for mails that already are cached. */
+ map_remove_found_seqs(search_args->args, msg_map, seq1);
+ ctx = mailbox_search_init(t, search_args, NULL,
+ MAIL_FETCH_STREAM_HEADER, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail)) {
+ map = array_idx_modifiable_i(msg_map, mail->seq-1);
+
+ if ((ret = get_hdr_sha1(mail, map->hdr_sha1)) < 0) {
+ ret = -1;
+ break;
+ }
+ if (ret > 0)
+ map->hdr_sha1_set = TRUE;
+ }
+
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_error("pop3_migration: Failed to search all mail headers: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&t);
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+pop3_map_read_hdr_hashes(struct mail_storage *storage, struct mailbox *pop3_box,
+ unsigned first_seq)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(storage);
+
+ if (mstorage->pop3_all_hdr_sha1_set)
+ return 0;
+ if (mstorage->all_mailboxes) {
+ /* we may be matching against multiple mailboxes.
+ read all the hashes only once. */
+ first_seq = 1;
+ }
+
+ if (map_read_hdr_hashes(pop3_box, &mstorage->pop3_uidl_map.arr,
+ first_seq) < 0)
+ return -1;
+
+ if (first_seq == 1)
+ mstorage->pop3_all_hdr_sha1_set = TRUE;
+ return 0;
+}
+
+static int imap_map_read(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(box->storage);
+ const unsigned int uidl_cache_idx =
+ ibox->cache_fields[MAIL_CACHE_POP3_UIDL].idx;
+ struct mailbox_status status;
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct imap_msg_map *map;
+ uoff_t psize = UOFF_T_MAX;
+ string_t *uidl;
+ int ret = 0;
+
+ mailbox_get_open_status(box, STATUS_MESSAGES, &status);
+
+ i_assert(!array_is_created(&mbox->imap_msg_map));
+ p_array_init(&mbox->imap_msg_map, box->pool, status.messages);
+
+ 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,
+ mstorage->skip_size_check ? 0 :
+ MAIL_FETCH_PHYSICAL_SIZE, NULL);
+ mail_search_args_unref(&search_args);
+
+ uidl = t_str_new(64);
+ while (mailbox_search_next(ctx, &mail)) {
+ if (mstorage->skip_size_check)
+ ;
+ else if (mail_get_physical_size(mail, &psize) < 0) {
+ i_error("pop3_migration: Failed to get psize for imap uid %u: %s",
+ mail->uid,
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ break;
+ }
+
+ if (!mstorage->skip_uidl_cache) {
+ str_truncate(uidl, 0);
+ (void)mail_cache_lookup_field(mail->transaction->cache_view,
+ uidl, mail->seq, uidl_cache_idx);
+ }
+
+ map = array_append_space(&mbox->imap_msg_map);
+ map->uid = mail->uid;
+ map->psize = psize;
+ map->pop3_uidl = p_strdup_empty(box->pool, str_c(uidl));
+ }
+
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_error("pop3_migration: Failed to search all IMAP mails: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&t);
+ return ret;
+}
+
+static int imap_map_read_hdr_hashes(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+
+ return map_read_hdr_hashes(box, &mbox->imap_msg_map.arr,
+ mbox->first_unfound_idx+1);
+}
+
+static void pop3_uidl_assign_cached(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(box->storage);
+ struct pop3_uidl_map *pop3_map;
+ struct imap_msg_map *imap_map;
+ unsigned int imap_idx, pop3_idx, pop3_count, imap_count;
+ int ret;
+
+ if (mstorage->skip_uidl_cache)
+ return;
+
+ array_sort(&mstorage->pop3_uidl_map, pop3_uidl_map_uidl_cmp);
+ array_sort(&mbox->imap_msg_map, imap_msg_map_uidl_cmp);
+
+ pop3_map = array_get_modifiable(&mstorage->pop3_uidl_map, &pop3_count);
+ imap_map = array_get_modifiable(&mbox->imap_msg_map, &imap_count);
+
+ /* see if we can match the messages using sizes */
+ for (imap_idx = pop3_idx = 0; imap_idx < imap_count; imap_idx++) {
+ if (imap_map[imap_idx].pop3_uidl == NULL)
+ continue;
+
+ ret = 1;
+ for (; pop3_idx < pop3_count; pop3_idx++) {
+ ret = strcmp(imap_map[imap_idx].pop3_uidl,
+ pop3_map[pop3_idx].pop3_uidl);
+ if (ret >= 0)
+ break;
+ }
+ if (ret == 0) {
+ imap_map[imap_idx].pop3_seq =
+ pop3_map[pop3_idx].pop3_seq;
+ pop3_map[pop3_idx].imap_uid = imap_map[imap_idx].uid;
+ }
+ }
+}
+
+static bool pop3_uidl_assign_by_size(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(box->storage);
+ struct pop3_uidl_map *pop3_map;
+ struct imap_msg_map *imap_map;
+ unsigned int i, pop3_count, imap_count, count;
+ unsigned int size_match = 0, uidl_match = 0;
+
+ pop3_map = array_get_modifiable(&mstorage->pop3_uidl_map, &pop3_count);
+ imap_map = array_get_modifiable(&mbox->imap_msg_map, &imap_count);
+ count = I_MIN(pop3_count, imap_count);
+
+ /* see if we can match the messages using sizes */
+ for (i = 0; i < count; i++) {
+ if (imap_map[i].pop3_uidl != NULL) {
+ /* some of the UIDLs were already found cached. */
+ if (strcmp(pop3_map[i].pop3_uidl, imap_map[i].pop3_uidl) == 0) {
+ uidl_match++;
+ continue;
+ }
+ /* mismatch - can't trust the sizes */
+ break;
+ }
+
+ if (pop3_map[i].size != imap_map[i].psize ||
+ mstorage->skip_size_check)
+ break;
+ if (i+1 < count && pop3_map[i].size == pop3_map[i+1].size) {
+ /* two messages with same size, don't trust them */
+ break;
+ }
+
+ size_match++;
+ pop3_map[i].imap_uid = imap_map[i].uid;
+ imap_map[i].pop3_uidl = pop3_map[i].pop3_uidl;
+ imap_map[i].pop3_seq = pop3_map[i].pop3_seq;
+ }
+ mbox->first_unfound_idx = i;
+ e_debug(box->event, "pop3_migration: cached uidls=%u, size matches=%u, total=%u",
+ uidl_match, size_match, count);
+ return i == count && imap_count == pop3_count;
+}
+
+static int
+pop3_uidl_assign_by_hdr_hash(struct mailbox *box, struct mailbox *pop3_box)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(box->storage);
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct pop3_uidl_map *pop3_map;
+ struct imap_msg_map *imap_map;
+ unsigned int pop3_idx, imap_idx, pop3_count, imap_count;
+ unsigned int first_seq, missing_uids_count;
+ uint32_t first_missing_idx = 0, first_missing_seq = (uint32_t)-1;
+ int ret;
+
+ first_seq = mbox->first_unfound_idx+1;
+ if (pop3_map_read_hdr_hashes(box->storage, pop3_box, first_seq) < 0 ||
+ imap_map_read_hdr_hashes(box) < 0)
+ return -1;
+
+ array_sort(&mstorage->pop3_uidl_map, pop3_uidl_map_hdr_cmp);
+ array_sort(&mbox->imap_msg_map, imap_msg_map_hdr_cmp);
+
+ pop3_map = array_get_modifiable(&mstorage->pop3_uidl_map, &pop3_count);
+ imap_map = array_get_modifiable(&mbox->imap_msg_map, &imap_count);
+
+ pop3_idx = imap_idx = 0;
+ while (pop3_idx < pop3_count && imap_idx < imap_count) {
+ if (!pop3_map[pop3_idx].common.hdr_sha1_set ||
+ pop3_map[pop3_idx].imap_uid != 0) {
+ pop3_idx++;
+ continue;
+ }
+ if (!imap_map[imap_idx].common.hdr_sha1_set ||
+ imap_map[imap_idx].pop3_uidl != NULL) {
+ imap_idx++;
+ continue;
+ }
+ ret = memcmp(pop3_map[pop3_idx].common.hdr_sha1,
+ imap_map[imap_idx].common.hdr_sha1,
+ sizeof(pop3_map[pop3_idx].common.hdr_sha1));
+ if (ret < 0)
+ pop3_idx++;
+ else if (ret > 0)
+ imap_idx++;
+ else {
+ pop3_map[pop3_idx].imap_uid = imap_map[imap_idx].uid;
+ imap_map[imap_idx].pop3_uidl =
+ pop3_map[pop3_idx].pop3_uidl;
+ imap_map[imap_idx].pop3_seq =
+ pop3_map[pop3_idx].pop3_seq;
+ }
+ }
+ missing_uids_count = 0;
+ for (pop3_idx = 0; pop3_idx < pop3_count; pop3_idx++) {
+ if (pop3_map[pop3_idx].imap_uid != 0) {
+ /* matched */
+ } else if (!pop3_map[pop3_idx].common.hdr_sha1_set) {
+ /* we treated this mail as expunged - ignore */
+ } else {
+ uint32_t seq = pop3_map[pop3_idx].pop3_seq;
+ if (first_missing_seq > seq) {
+ first_missing_seq = seq;
+ first_missing_idx = pop3_idx;
+ }
+ missing_uids_count++;
+ }
+ }
+ if (missing_uids_count > 0 && !mstorage->all_mailboxes) {
+ string_t *str = t_str_new(128);
+ bool all_imap_mails_found = FALSE;
+
+ str_printfa(str, "pop3_migration: %u POP3 messages have no "
+ "matching IMAP messages (first POP3 msg %u UIDL %s)",
+ missing_uids_count, first_missing_seq,
+ pop3_map[first_missing_idx].pop3_uidl);
+ if (imap_count + missing_uids_count == pop3_count) {
+ str_append(str, " - all IMAP messages were found "
+ "(POP3 contains more than IMAP INBOX - you may want to set pop3_migration_all_mailboxes=yes)");
+ all_imap_mails_found = TRUE;
+ }
+ if (all_imap_mails_found && mstorage->ignore_extra_uidls) {
+ /* pop3 had more mails than imap. maybe it was just
+ that a new mail was just delivered. */
+ } else if (!mstorage->ignore_missing_uidls) {
+ str_append(str, " - set pop3_migration_ignore_missing_uidls=yes");
+ if (all_imap_mails_found)
+ str_append(str, " or pop3_migration_ignore_extra_uidls=yes");
+ i_error("%s to continue anyway", str_c(str));
+ return -1;
+ }
+ i_warning("%s", str_c(str));
+ } else
+ e_debug(box->event, "pop3_migration: %u mails matched by headers", pop3_count);
+ array_sort(&mstorage->pop3_uidl_map, pop3_uidl_map_pop3_seq_cmp);
+ array_sort(&mbox->imap_msg_map, imap_msg_map_uid_cmp);
+ return 0;
+}
+
+static void imap_uidls_add_to_cache(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ struct index_mail *imail;
+ struct imap_msg_map *imap_map;
+ unsigned int i, count;
+ unsigned int field_idx;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(t, 0, NULL);
+ imail = INDEX_MAIL(mail);
+ field_idx = imail->ibox->cache_fields[MAIL_CACHE_POP3_UIDL].idx;
+
+ imap_map = array_get_modifiable(&mbox->imap_msg_map, &count);
+ for (i = 0; i < count; i++) {
+ if (imap_map[i].pop3_uidl == NULL)
+ continue;
+
+ if (!mail_set_uid(mail, imap_map[i].uid))
+ i_unreached();
+ if (mail_cache_field_can_add(t->cache_trans, mail->seq, field_idx)) {
+ index_mail_cache_add_idx(imail, field_idx,
+ imap_map[i].pop3_uidl, strlen(imap_map[i].pop3_uidl)+1);
+ }
+ }
+ mail_free(&mail);
+ (void)mailbox_transaction_commit(&t);
+}
+
+static int pop3_migration_uidl_sync(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(box->storage);
+ struct mailbox *pop3_box;
+
+ pop3_box = pop3_mailbox_alloc(box->storage);
+ /* the POP3 server isn't connected to yet. handle all IMAP traffic
+ first before connecting, so POP3 server won't disconnect us due to
+ idling. */
+ if (imap_map_read(box) < 0 ||
+ pop3_map_read(box->storage, pop3_box) < 0) {
+ mailbox_free(&pop3_box);
+ return -1;
+ }
+
+ pop3_uidl_assign_cached(box);
+
+ array_sort(&mstorage->pop3_uidl_map, pop3_uidl_map_pop3_seq_cmp);
+ array_sort(&mbox->imap_msg_map, imap_msg_map_uid_cmp);
+
+ if (!pop3_uidl_assign_by_size(box)) {
+ /* everything wasn't assigned, figure out the rest with
+ header hashes */
+ if (pop3_uidl_assign_by_hdr_hash(box, pop3_box) < 0) {
+ mailbox_free(&pop3_box);
+ return -1;
+ }
+ }
+
+ if (!mstorage->skip_uidl_cache)
+ imap_uidls_add_to_cache(box);
+
+ mbox->uidl_synced = TRUE;
+ mailbox_free(&pop3_box);
+ return 0;
+}
+
+static int pop3_migration_uidl_sync_if_needed(struct mailbox *box)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(box);
+ int ret = 0;
+
+ if (mbox->uidl_synced)
+ return 0;
+
+ if (mbox->uidl_sync_failed)
+ ret = -1;
+ else {
+ struct event_reason *reason =
+ event_reason_begin("pop3_migration:uidl_sync");
+ ret = pop3_migration_uidl_sync(box);
+ event_reason_end(&reason);
+ }
+ if (ret < 0) {
+ mbox->uidl_sync_failed = TRUE;
+ mail_storage_set_error(box->storage, MAIL_ERROR_TEMP,
+ "POP3 UIDLs couldn't be synced");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+pop3_migration_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ union mail_module_context *mmail = POP3_MIGRATION_MAIL_CONTEXT(mail);
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(_mail->box);
+ struct imap_msg_map map_key, *map;
+
+ if (field == MAIL_FETCH_UIDL_BACKEND ||
+ field == MAIL_FETCH_POP3_ORDER) {
+ if (pop3_migration_uidl_sync_if_needed(_mail->box) < 0)
+ return -1;
+
+ i_zero(&map_key);
+ map_key.uid = _mail->uid;
+ map = array_bsearch(&mbox->imap_msg_map, &map_key,
+ imap_msg_map_uid_cmp);
+ if (map != NULL && map->pop3_uidl != NULL) {
+ if (field == MAIL_FETCH_UIDL_BACKEND)
+ *value_r = map->pop3_uidl;
+ else
+ *value_r = t_strdup_printf("%u", map->pop3_seq);
+ return 0;
+ }
+ /* not found from POP3 server, fallback to default */
+ }
+ return mmail->super.get_special(_mail, field, value_r);
+}
+
+static void pop3_migration_mail_allocated(struct mail *_mail)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT(_mail->box->storage);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ union mail_module_context *mmail;
+ struct mail_namespace *ns;
+
+ if (mstorage == NULL ||
+ (!mstorage->all_mailboxes && !_mail->box->inbox_user)) {
+ /* assigns UIDLs only for INBOX */
+ return;
+ }
+
+ ns = mail_namespace_find(_mail->box->storage->user->namespaces,
+ mstorage->pop3_box_vname);
+ if (ns == mailbox_get_namespace(_mail->box)) {
+ /* we're accessing the pop3-migration namespace itself */
+ return;
+ }
+
+ mmail = p_new(mail->pool, union mail_module_context, 1);
+ mmail->super = *v;
+ mail->vlast = &mmail->super;
+
+ v->get_special = pop3_migration_get_special;
+ MODULE_CONTEXT_SET_SELF(mail, pop3_migration_mail_module, mmail);
+}
+
+static struct mail_search_context *
+pop3_migration_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)
+{
+ struct pop3_migration_mailbox *mbox = POP3_MIGRATION_CONTEXT_REQUIRE(t->box);
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(t->box->storage);
+
+ if ((wanted_fields & (MAIL_FETCH_UIDL_BACKEND |
+ MAIL_FETCH_POP3_ORDER)) != 0 &&
+ (mstorage->all_mailboxes || t->box->inbox_user)) {
+ /* Start POP3 UIDL syncing before the search, so we'll do it
+ before we start sending any FETCH BODY[]s to IMAP. It
+ shouldn't matter much, except this works around a bug in
+ Yahoo IMAP where it sometimes breaks its state when doing
+ a FETCH BODY[] followed by FETCH BODY[HEADER].. */
+ (void)pop3_migration_uidl_sync_if_needed(t->box);
+ }
+
+ return mbox->module_ctx.super.search_init(t, args, sort_program,
+ wanted_fields, wanted_headers);
+}
+
+static void pop3_migration_mailbox_allocated(struct mailbox *box)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT(box->storage);
+ struct mailbox_vfuncs *v = box->vlast;
+ struct pop3_migration_mailbox *mbox;
+
+ if (mstorage == NULL)
+ return;
+
+ mbox = p_new(box->pool, struct pop3_migration_mailbox, 1);
+ mbox->module_ctx.super = *v;
+ box->vlast = &mbox->module_ctx.super;
+
+ v->search_init = pop3_migration_mailbox_search_init;
+
+ MODULE_CONTEXT_SET(box, pop3_migration_storage_module, mbox);
+}
+
+static void pop3_migration_mail_storage_destroy(struct mail_storage *storage)
+{
+ struct pop3_migration_mail_storage *mstorage =
+ POP3_MIGRATION_CONTEXT_REQUIRE(storage);
+
+ if (array_is_created(&mstorage->pop3_uidl_map))
+ array_free(&mstorage->pop3_uidl_map);
+
+ mstorage->module_ctx.super.destroy(storage);
+}
+
+static void pop3_migration_mail_storage_created(struct mail_storage *storage)
+{
+ struct pop3_migration_mail_storage *mstorage;
+ struct mail_storage_vfuncs *v = storage->vlast;
+ const char *pop3_box_vname;
+
+ pop3_box_vname = mail_user_plugin_getenv(storage->user,
+ "pop3_migration_mailbox");
+ if (pop3_box_vname == NULL) {
+ e_debug(storage->user->event, "pop3_migration: No pop3_migration_mailbox setting - disabled");
+ return;
+ }
+
+ mstorage = p_new(storage->pool, struct pop3_migration_mail_storage, 1);
+ mstorage->module_ctx.super = *v;
+ storage->vlast = &mstorage->module_ctx.super;
+ v->destroy = pop3_migration_mail_storage_destroy;
+
+ mstorage->pop3_box_vname = p_strdup(storage->pool, pop3_box_vname);
+ mstorage->all_mailboxes =
+ mail_user_plugin_getenv_bool(storage->user,
+ "pop3_migration_all_mailboxes");
+ mstorage->ignore_missing_uidls =
+ mail_user_plugin_getenv_bool(storage->user,
+ "pop3_migration_ignore_missing_uidls");
+ mstorage->ignore_extra_uidls =
+ mail_user_plugin_getenv_bool(storage->user,
+ "pop3_migration_ignore_extra_uidls");
+ mstorage->skip_size_check =
+ mail_user_plugin_getenv_bool(storage->user,
+ "pop3_migration_skip_size_check");
+ mstorage->skip_uidl_cache =
+ mail_user_plugin_getenv_bool(storage->user,
+ "pop3_migration_skip_uidl_cache");
+
+ MODULE_CONTEXT_SET(storage, pop3_migration_storage_module, mstorage);
+}
+
+static struct mail_storage_hooks pop3_migration_mail_storage_hooks = {
+ .mail_allocated = pop3_migration_mail_allocated,
+ .mailbox_allocated = pop3_migration_mailbox_allocated,
+ .mail_storage_created = pop3_migration_mail_storage_created
+};
+
+void pop3_migration_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &pop3_migration_mail_storage_hooks);
+}
+
+void pop3_migration_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&pop3_migration_mail_storage_hooks);
+}
diff --git a/src/plugins/pop3-migration/pop3-migration-plugin.h b/src/plugins/pop3-migration/pop3-migration-plugin.h
new file mode 100644
index 0000000..1dc84e0
--- /dev/null
+++ b/src/plugins/pop3-migration/pop3-migration-plugin.h
@@ -0,0 +1,13 @@
+#ifndef POP3_MIGRATION_PLUGIN_H
+#define POP3_MIGRATION_PLUGIN_H
+
+struct module;
+
+void pop3_migration_plugin_init(struct module *module);
+void pop3_migration_plugin_deinit(void);
+
+int pop3_migration_get_hdr_sha1(uint32_t mail_seq, struct istream *input,
+ unsigned char sha1_r[STATIC_ARRAY SHA1_RESULTLEN],
+ bool *have_eoh_r);
+
+#endif
diff --git a/src/plugins/pop3-migration/test-pop3-migration-plugin.c b/src/plugins/pop3-migration/test-pop3-migration-plugin.c
new file mode 100644
index 0000000..f8e52d8
--- /dev/null
+++ b/src/plugins/pop3-migration/test-pop3-migration-plugin.c
@@ -0,0 +1,63 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sha1.h"
+#include "hex-binary.h"
+#include "istream.h"
+#include "test-common.h"
+#include "pop3-migration-plugin.h"
+
+static void test_pop3_migration_get_hdr_sha1(void)
+{
+ static const struct {
+ const char *input;
+ const char *sha1;
+ bool have_eoh;
+ } tests[] = {
+ { "", "da39a3ee5e6b4b0d3255bfef95601890afd80709", FALSE },
+ { "\n", "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc", TRUE },
+ { "a: \r\n", "a3871371f2d468493005286282ae10549dab2c57", FALSE },
+ { "a: b\r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b \r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b \r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b \r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\r\n\r\n", "938b96404495cced816e3a4f6031734eab4e71b3", TRUE },
+ { "a: b\r\n\r\r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\r\n\r\r\nc: d\r\n\r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", TRUE },
+ { "a: b\r\n \r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\r\n \r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\r\n\t\r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\t\t\t\t\r\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+ { "a: b\nfoo\n", "44ef6a20971148dd54a161f79814e22e2d098ddb", FALSE },
+
+ { "a: b\nc: d\n", "4dbea2c1bdd1323e15931382c1835200d9286230", FALSE },
+ { "a:b\nc:d\n", "4dbea2c1bdd1323e15931382c1835200d9286230", FALSE },
+ { "a: b\nfoo\nc: d\n", "4dbea2c1bdd1323e15931382c1835200d9286230", FALSE },
+ };
+ struct istream *input;
+ unsigned char digest[SHA1_RESULTLEN];
+ unsigned int i;
+ bool have_eoh;
+
+ test_begin("pop3 migration get hdr sha1");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ input = i_stream_create_from_data(tests[i].input,
+ strlen(tests[i].input));
+ test_assert_idx(pop3_migration_get_hdr_sha1(1, input, digest, &have_eoh) == 0, i);
+ test_assert_idx(strcasecmp(binary_to_hex(digest, sizeof(digest)), tests[i].sha1) == 0, i);
+ test_assert_idx(tests[i].have_eoh == have_eoh, i);
+ i_stream_unref(&input);
+ }
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_pop3_migration_get_hdr_sha1,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/plugins/push-notification/Makefile.am b/src/plugins/push-notification/Makefile.am
new file mode 100644
index 0000000..c065e38
--- /dev/null
+++ b/src/plugins/push-notification/Makefile.am
@@ -0,0 +1,83 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/plugins/notify
+
+NOPLUGIN_LDFLAGS =
+lib20_push_notification_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = lib20_push_notification_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+notify_deps = ../notify/lib15_notify_plugin.la
+endif
+
+lib20_push_notification_plugin_la_LIBADD = \
+ $(notify_deps)
+
+lib20_push_notification_plugin_la_SOURCES = \
+ push-notification-driver-dlog.c \
+ push-notification-driver-ox.c \
+ push-notification-drivers.c \
+ push-notification-event-flagsclear.c \
+ push-notification-event-flagsset.c \
+ push-notification-event-mailboxcreate.c \
+ push-notification-event-mailboxdelete.c \
+ push-notification-event-mailboxrename.c \
+ push-notification-event-mailboxsubscribe.c \
+ push-notification-event-mailboxunsubscribe.c \
+ push-notification-event-messageappend.c \
+ push-notification-event-messageexpunge.c \
+ push-notification-event-messagenew.c \
+ push-notification-event-messageread.c \
+ push-notification-event-messagetrash.c \
+ push-notification-event-message-common.c \
+ push-notification-events.c \
+ push-notification-events-rfc5423.c \
+ push-notification-plugin.c \
+ push-notification-triggers.c \
+ push-notification-txn-mbox.c \
+ push-notification-txn-msg.c
+
+headers = \
+ push-notification-drivers.h \
+ push-notification-event-flagsclear.h \
+ push-notification-event-flagsset.h \
+ push-notification-event-mailboxcreate.h \
+ push-notification-event-mailboxdelete.h \
+ push-notification-event-mailboxrename.h \
+ push-notification-event-mailboxsubscribe.h \
+ push-notification-event-mailboxunsubscribe.h \
+ push-notification-event-message-common.h \
+ push-notification-event-messageappend.h \
+ push-notification-event-messageexpunge.h \
+ push-notification-event-messagenew.h \
+ push-notification-event-messageread.h \
+ push-notification-event-messagetrash.h \
+ push-notification-events.h \
+ push-notification-events-rfc5423.h \
+ push-notification-plugin.h \
+ push-notification-triggers.h \
+ push-notification-txn-mbox.h \
+ push-notification-txn-msg.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+if HAVE_LUA
+lib22_push_notification_lua_plugin_la_CFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-lua \
+ -I$(top_srcdir)/src/plugins/mail-lua \
+ $(LUA_CFLAGS)
+lib22_push_notification_lua_plugin_la_LDFLAGS = module -avoid-version
+module_LTLIBRARIES += \
+ lib22_push_notification_lua_plugin.la
+lib22_push_notification_lua_plugin_la_LIBADD = $(notify_deps) $(LUA_LIBS)
+lib22_push_notification_lua_plugin_la_SOURCES = \
+ push-notification-driver-lua.c
+endif
diff --git a/src/plugins/push-notification/Makefile.in b/src/plugins/push-notification/Makefile.in
new file mode 100644
index 0000000..4547f90
--- /dev/null
+++ b/src/plugins/push-notification/Makefile.in
@@ -0,0 +1,1037 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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 = \
+@HAVE_LUA_TRUE@ lib22_push_notification_lua_plugin.la
+
+subdir = src/plugins/push-notification
+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)$(moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_push_notification_plugin_la_DEPENDENCIES = $(notify_deps)
+am_lib20_push_notification_plugin_la_OBJECTS = \
+ push-notification-driver-dlog.lo \
+ push-notification-driver-ox.lo push-notification-drivers.lo \
+ push-notification-event-flagsclear.lo \
+ push-notification-event-flagsset.lo \
+ push-notification-event-mailboxcreate.lo \
+ push-notification-event-mailboxdelete.lo \
+ push-notification-event-mailboxrename.lo \
+ push-notification-event-mailboxsubscribe.lo \
+ push-notification-event-mailboxunsubscribe.lo \
+ push-notification-event-messageappend.lo \
+ push-notification-event-messageexpunge.lo \
+ push-notification-event-messagenew.lo \
+ push-notification-event-messageread.lo \
+ push-notification-event-messagetrash.lo \
+ push-notification-event-message-common.lo \
+ push-notification-events.lo \
+ push-notification-events-rfc5423.lo \
+ push-notification-plugin.lo push-notification-triggers.lo \
+ push-notification-txn-mbox.lo push-notification-txn-msg.lo
+lib20_push_notification_plugin_la_OBJECTS = \
+ $(am_lib20_push_notification_plugin_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 =
+lib20_push_notification_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) \
+ --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link \
+ $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_push_notification_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+@HAVE_LUA_TRUE@lib22_push_notification_lua_plugin_la_DEPENDENCIES = \
+@HAVE_LUA_TRUE@ $(notify_deps) $(am__DEPENDENCIES_1)
+am__lib22_push_notification_lua_plugin_la_SOURCES_DIST = \
+ push-notification-driver-lua.c
+@HAVE_LUA_TRUE@am_lib22_push_notification_lua_plugin_la_OBJECTS = lib22_push_notification_lua_plugin_la-push-notification-driver-lua.lo
+lib22_push_notification_lua_plugin_la_OBJECTS = \
+ $(am_lib22_push_notification_lua_plugin_la_OBJECTS)
+lib22_push_notification_lua_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) \
+ --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link \
+ $(CCLD) $(lib22_push_notification_lua_plugin_la_CFLAGS) \
+ $(CFLAGS) $(lib22_push_notification_lua_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@HAVE_LUA_TRUE@am_lib22_push_notification_lua_plugin_la_rpath = \
+@HAVE_LUA_TRUE@ -rpath $(moduledir)
+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)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Plo \
+ ./$(DEPDIR)/push-notification-driver-dlog.Plo \
+ ./$(DEPDIR)/push-notification-driver-ox.Plo \
+ ./$(DEPDIR)/push-notification-drivers.Plo \
+ ./$(DEPDIR)/push-notification-event-flagsclear.Plo \
+ ./$(DEPDIR)/push-notification-event-flagsset.Plo \
+ ./$(DEPDIR)/push-notification-event-mailboxcreate.Plo \
+ ./$(DEPDIR)/push-notification-event-mailboxdelete.Plo \
+ ./$(DEPDIR)/push-notification-event-mailboxrename.Plo \
+ ./$(DEPDIR)/push-notification-event-mailboxsubscribe.Plo \
+ ./$(DEPDIR)/push-notification-event-mailboxunsubscribe.Plo \
+ ./$(DEPDIR)/push-notification-event-message-common.Plo \
+ ./$(DEPDIR)/push-notification-event-messageappend.Plo \
+ ./$(DEPDIR)/push-notification-event-messageexpunge.Plo \
+ ./$(DEPDIR)/push-notification-event-messagenew.Plo \
+ ./$(DEPDIR)/push-notification-event-messageread.Plo \
+ ./$(DEPDIR)/push-notification-event-messagetrash.Plo \
+ ./$(DEPDIR)/push-notification-events-rfc5423.Plo \
+ ./$(DEPDIR)/push-notification-events.Plo \
+ ./$(DEPDIR)/push-notification-plugin.Plo \
+ ./$(DEPDIR)/push-notification-triggers.Plo \
+ ./$(DEPDIR)/push-notification-txn-mbox.Plo \
+ ./$(DEPDIR)/push-notification-txn-msg.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 = $(lib20_push_notification_plugin_la_SOURCES) \
+ $(lib22_push_notification_lua_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_push_notification_plugin_la_SOURCES) \
+ $(am__lib22_push_notification_lua_plugin_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 = $(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@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/plugins/notify
+
+lib20_push_notification_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = lib20_push_notification_plugin.la $(am__append_1)
+@DOVECOT_PLUGIN_DEPS_TRUE@notify_deps = ../notify/lib15_notify_plugin.la
+lib20_push_notification_plugin_la_LIBADD = \
+ $(notify_deps)
+
+lib20_push_notification_plugin_la_SOURCES = \
+ push-notification-driver-dlog.c \
+ push-notification-driver-ox.c \
+ push-notification-drivers.c \
+ push-notification-event-flagsclear.c \
+ push-notification-event-flagsset.c \
+ push-notification-event-mailboxcreate.c \
+ push-notification-event-mailboxdelete.c \
+ push-notification-event-mailboxrename.c \
+ push-notification-event-mailboxsubscribe.c \
+ push-notification-event-mailboxunsubscribe.c \
+ push-notification-event-messageappend.c \
+ push-notification-event-messageexpunge.c \
+ push-notification-event-messagenew.c \
+ push-notification-event-messageread.c \
+ push-notification-event-messagetrash.c \
+ push-notification-event-message-common.c \
+ push-notification-events.c \
+ push-notification-events-rfc5423.c \
+ push-notification-plugin.c \
+ push-notification-triggers.c \
+ push-notification-txn-mbox.c \
+ push-notification-txn-msg.c
+
+headers = \
+ push-notification-drivers.h \
+ push-notification-event-flagsclear.h \
+ push-notification-event-flagsset.h \
+ push-notification-event-mailboxcreate.h \
+ push-notification-event-mailboxdelete.h \
+ push-notification-event-mailboxrename.h \
+ push-notification-event-mailboxsubscribe.h \
+ push-notification-event-mailboxunsubscribe.h \
+ push-notification-event-message-common.h \
+ push-notification-event-messageappend.h \
+ push-notification-event-messageexpunge.h \
+ push-notification-event-messagenew.h \
+ push-notification-event-messageread.h \
+ push-notification-event-messagetrash.h \
+ push-notification-events.h \
+ push-notification-events-rfc5423.h \
+ push-notification-plugin.h \
+ push-notification-triggers.h \
+ push-notification-txn-mbox.h \
+ push-notification-txn-msg.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+@HAVE_LUA_TRUE@lib22_push_notification_lua_plugin_la_CFLAGS = $(AM_CPPFLAGS) \
+@HAVE_LUA_TRUE@ -I$(top_srcdir)/src/lib-lua \
+@HAVE_LUA_TRUE@ -I$(top_srcdir)/src/plugins/mail-lua \
+@HAVE_LUA_TRUE@ $(LUA_CFLAGS)
+
+@HAVE_LUA_TRUE@lib22_push_notification_lua_plugin_la_LDFLAGS = module -avoid-version
+@HAVE_LUA_TRUE@lib22_push_notification_lua_plugin_la_LIBADD = $(notify_deps) $(LUA_LIBS)
+@HAVE_LUA_TRUE@lib22_push_notification_lua_plugin_la_SOURCES = \
+@HAVE_LUA_TRUE@ push-notification-driver-lua.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/plugins/push-notification/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/push-notification/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-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}; \
+ }
+
+lib20_push_notification_plugin.la: $(lib20_push_notification_plugin_la_OBJECTS) $(lib20_push_notification_plugin_la_DEPENDENCIES) $(EXTRA_lib20_push_notification_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_push_notification_plugin_la_LINK) -rpath $(moduledir) $(lib20_push_notification_plugin_la_OBJECTS) $(lib20_push_notification_plugin_la_LIBADD) $(LIBS)
+
+lib22_push_notification_lua_plugin.la: $(lib22_push_notification_lua_plugin_la_OBJECTS) $(lib22_push_notification_lua_plugin_la_DEPENDENCIES) $(EXTRA_lib22_push_notification_lua_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib22_push_notification_lua_plugin_la_LINK) $(am_lib22_push_notification_lua_plugin_la_rpath) $(lib22_push_notification_lua_plugin_la_OBJECTS) $(lib22_push_notification_lua_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-driver-dlog.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-driver-ox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-drivers.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-flagsclear.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-flagsset.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-mailboxcreate.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-mailboxdelete.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-mailboxrename.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-mailboxsubscribe.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-mailboxunsubscribe.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-message-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-messageappend.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-messageexpunge.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-messagenew.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-messageread.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-event-messagetrash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-events-rfc5423.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-events.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-triggers.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-txn-mbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/push-notification-txn-msg.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 $@ $<
+
+lib22_push_notification_lua_plugin_la-push-notification-driver-lua.lo: push-notification-driver-lua.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) $(lib22_push_notification_lua_plugin_la_CFLAGS) $(CFLAGS) -MT lib22_push_notification_lua_plugin_la-push-notification-driver-lua.lo -MD -MP -MF $(DEPDIR)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Tpo -c -o lib22_push_notification_lua_plugin_la-push-notification-driver-lua.lo `test -f 'push-notification-driver-lua.c' || echo '$(srcdir)/'`push-notification-driver-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Tpo $(DEPDIR)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='push-notification-driver-lua.c' object='lib22_push_notification_lua_plugin_la-push-notification-driver-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) $(AM_CPPFLAGS) $(CPPFLAGS) $(lib22_push_notification_lua_plugin_la_CFLAGS) $(CFLAGS) -c -o lib22_push_notification_lua_plugin_la-push-notification-driver-lua.lo `test -f 'push-notification-driver-lua.c' || echo '$(srcdir)/'`push-notification-driver-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
+check: check-am
+all-am: Makefile $(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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Plo
+ -rm -f ./$(DEPDIR)/push-notification-driver-dlog.Plo
+ -rm -f ./$(DEPDIR)/push-notification-driver-ox.Plo
+ -rm -f ./$(DEPDIR)/push-notification-drivers.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-flagsclear.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-flagsset.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxcreate.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxdelete.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxrename.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxsubscribe.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxunsubscribe.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-message-common.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageappend.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageexpunge.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messagenew.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageread.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messagetrash.Plo
+ -rm -f ./$(DEPDIR)/push-notification-events-rfc5423.Plo
+ -rm -f ./$(DEPDIR)/push-notification-events.Plo
+ -rm -f ./$(DEPDIR)/push-notification-plugin.Plo
+ -rm -f ./$(DEPDIR)/push-notification-triggers.Plo
+ -rm -f ./$(DEPDIR)/push-notification-txn-mbox.Plo
+ -rm -f ./$(DEPDIR)/push-notification-txn-msg.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-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)/lib22_push_notification_lua_plugin_la-push-notification-driver-lua.Plo
+ -rm -f ./$(DEPDIR)/push-notification-driver-dlog.Plo
+ -rm -f ./$(DEPDIR)/push-notification-driver-ox.Plo
+ -rm -f ./$(DEPDIR)/push-notification-drivers.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-flagsclear.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-flagsset.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxcreate.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxdelete.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxrename.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxsubscribe.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-mailboxunsubscribe.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-message-common.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageappend.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageexpunge.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messagenew.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messageread.Plo
+ -rm -f ./$(DEPDIR)/push-notification-event-messagetrash.Plo
+ -rm -f ./$(DEPDIR)/push-notification-events-rfc5423.Plo
+ -rm -f ./$(DEPDIR)/push-notification-events.Plo
+ -rm -f ./$(DEPDIR)/push-notification-plugin.Plo
+ -rm -f ./$(DEPDIR)/push-notification-triggers.Plo
+ -rm -f ./$(DEPDIR)/push-notification-txn-mbox.Plo
+ -rm -f ./$(DEPDIR)/push-notification-txn-msg.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-moduleLTLIBRARIES 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-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-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
+
+
+# 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/plugins/push-notification/push-notification-driver-dlog.c b/src/plugins/push-notification/push-notification-driver-dlog.c
new file mode 100644
index 0000000..e0cf790
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-driver-dlog.c
@@ -0,0 +1,114 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-mbox.h"
+#include "push-notification-txn-msg.h"
+
+static int
+push_notification_driver_dlog_init(
+ struct push_notification_driver_config *config,
+ struct mail_user *user ATTR_UNUSED, pool_t pool ATTR_UNUSED,
+ void **context ATTR_UNUSED, const char **error_r ATTR_UNUSED)
+{
+ i_debug("Called init push_notification plugin hook.");
+
+ if (config->raw_config != NULL) {
+ i_debug("Config string for dlog push_notification driver: %s",
+ config->raw_config);
+ }
+
+ return 0;
+}
+
+static bool
+push_notification_driver_dlog_begin_txn(
+ struct push_notification_driver_txn *dtxn)
+{
+ const struct push_notification_event *event;
+
+ i_debug("Called begin_txn push_notification plugin hook.");
+
+ array_foreach_elem(&push_notification_events, event)
+ push_notification_event_init(dtxn, event->name, NULL);
+ return TRUE;
+}
+
+static void
+push_notification_driver_dlog_process_mbox(
+ struct push_notification_driver_txn *dtxn ATTR_UNUSED,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_txn_event *event;
+
+ i_debug("Called process_mbox push_notification plugin hook.");
+
+ i_debug("Mailbox data: Mailbox [%s]", mbox->mailbox);
+
+ if (array_is_created(&mbox->eventdata)) {
+ array_foreach_elem(&mbox->eventdata, event) {
+ if (event->event->event->mbox.debug_mbox != NULL)
+ event->event->event->mbox.debug_mbox(event);
+ }
+ }
+}
+
+static void
+push_notification_driver_dlog_process_msg(
+ struct push_notification_driver_txn *dtxn ATTR_UNUSED,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_txn_event *event;
+
+ i_debug("Called process_msg push_notification plugin hook.");
+
+ i_debug("Message data: Mailbox [%s], UID [%u], UIDVALIDITY [%u]",
+ msg->mailbox, msg->uid, msg->uid_validity);
+
+ if (array_is_created(&msg->eventdata)) {
+ array_foreach_elem(&msg->eventdata, event) {
+ if (event->event->event->msg.debug_msg != NULL)
+ event->event->event->msg.debug_msg(event);
+ }
+ }
+}
+
+static void
+push_notification_driver_dlog_end_txn(
+ struct push_notification_driver_txn *dtxn ATTR_UNUSED,
+ bool success ATTR_UNUSED)
+{
+ i_debug("Called end_txn push_notification plugin hook.");
+}
+
+static void
+push_notification_driver_dlog_deinit(
+ struct push_notification_driver_user *duser ATTR_UNUSED)
+{
+ i_debug("Called deinit push_notification plugin hook.");
+}
+
+static void push_notification_driver_dlog_cleanup(void)
+{
+ i_debug("Called cleanup push_notification plugin hook.");
+}
+
+/* Driver definition */
+
+extern struct push_notification_driver push_notification_driver_dlog;
+
+struct push_notification_driver push_notification_driver_dlog = {
+ .name = "dlog",
+ .v = {
+ .init = push_notification_driver_dlog_init,
+ .begin_txn = push_notification_driver_dlog_begin_txn,
+ .process_mbox = push_notification_driver_dlog_process_mbox,
+ .process_msg = push_notification_driver_dlog_process_msg,
+ .end_txn = push_notification_driver_dlog_end_txn,
+ .deinit = push_notification_driver_dlog_deinit,
+ .cleanup = push_notification_driver_dlog_cleanup
+ }
+};
diff --git a/src/plugins/push-notification/push-notification-driver-lua.c b/src/plugins/push-notification/push-notification-driver-lua.c
new file mode 100644
index 0000000..e1178fa
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-driver-lua.c
@@ -0,0 +1,663 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hash.h"
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+
+#include "mail-storage.h"
+#include "mail-user.h"
+#include "mail-lua-plugin.h"
+#include "mail-storage-lua.h"
+
+#include "push-notification-plugin.h"
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-message-common.h"
+#include "push-notification-txn-mbox.h"
+#include "push-notification-txn-msg.h"
+
+#include "push-notification-event-flagsclear.h"
+#include "push-notification-event-flagsset.h"
+#include "push-notification-event-mailboxcreate.h"
+#include "push-notification-event-mailboxdelete.h"
+#include "push-notification-event-mailboxrename.h"
+#include "push-notification-event-mailboxsubscribe.h"
+#include "push-notification-event-mailboxunsubscribe.h"
+#include "push-notification-event-messageappend.h"
+#include "push-notification-event-message-common.h"
+#include "push-notification-event-messageexpunge.h"
+#include "push-notification-event-messagenew.h"
+#include "push-notification-event-messageread.h"
+#include "push-notification-event-messagetrash.h"
+
+#define DLUA_LOG_USERENV_KEY "push_notification_lua_script_file"
+
+#define DLUA_FN_BEGIN_TXN "dovecot_lua_notify_begin_txn"
+#define DLUA_FN_EVENT_PREFIX "dovecot_lua_notify_event"
+#define DLUA_FN_END_TXN "dovecot_lua_notify_end_txn"
+
+#define DLUA_CALL_FINISHED "push_notification_lua_call_finished"
+
+struct dlua_push_notification_context {
+ struct dlua_script *script;
+ struct event *event;
+ bool debug;
+
+ struct push_notification_event_messagenew_config config_mn;
+ struct push_notification_event_messageappend_config config_ma;
+ struct push_notification_event_flagsclear_config config_fc;
+ struct push_notification_event_flagsset_config config_fs;
+};
+
+struct dlua_push_notification_txn_context {
+ int tx_ref;
+};
+
+#define DLUA_DEFAULT_EVENTS (\
+ PUSH_NOTIFICATION_MESSAGE_HDR_FROM | \
+ PUSH_NOTIFICATION_MESSAGE_HDR_TO | \
+ PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT | \
+ PUSH_NOTIFICATION_MESSAGE_HDR_DATE | \
+ PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET | \
+ PUSH_NOTIFICATION_MESSAGE_FLAGS | \
+ PUSH_NOTIFICATION_MESSAGE_KEYWORDS | \
+ PUSH_NOTIFICATION_MESSAGE_HDR_MESSAGE_ID)
+
+static const char *push_notification_driver_lua_to_fn(const char *evname);
+
+static int
+push_notification_driver_lua_init(
+ struct push_notification_driver_config *config, struct mail_user *user,
+ pool_t pool, void **context, const char **error_r)
+{
+ struct dlua_push_notification_context *ctx;
+ const char *tmp, *file;
+ struct event *event = event_create(user->event);
+ event_add_category(event, push_notification_get_event_category());
+ event_set_append_log_prefix(event, "lua: ");
+
+ if ((tmp = mail_user_plugin_getenv(user, DLUA_LOG_USERENV_KEY)) == NULL)
+ tmp = hash_table_lookup(config->config, (const char *)"file");
+
+ if (tmp == NULL) {
+ struct dlua_script *script;
+ /* If there is a script loaded, use the same context */
+ if (mail_lua_plugin_get_script(user, &script)) {
+ dlua_script_ref(script);
+ ctx = p_new(
+ pool, struct dlua_push_notification_context, 1);
+ ctx->script = script;
+ ctx->event = event;
+ *context = ctx;
+ return 0;
+ }
+
+ event_unref(&event);
+ *error_r = "No file in config and no "
+ DLUA_LOG_USERENV_KEY " set";
+ return -1;
+ }
+ file = tmp;
+
+ ctx = p_new(pool, struct dlua_push_notification_context, 1);
+ ctx->event = event;
+
+ e_debug(ctx->event, "Loading %s", file);
+
+ if (dlua_script_create_file(file, &ctx->script, event, error_r) < 0) {
+ /* There is a T_POP after this, which will break errors */
+ event_unref(&event);
+ *error_r = p_strdup(pool, *error_r);
+ return -1;
+ }
+
+ /* Register dovecot helpers */
+ dlua_dovecot_register(ctx->script);
+ dlua_register_mail_storage(ctx->script);
+
+ e_debug(ctx->event, "Calling script_init");
+
+ /* Initialize script */
+ if (dlua_script_init(ctx->script, error_r) < 0) {
+ *error_r = p_strdup(pool, *error_r);
+ event_unref(&event);
+ dlua_script_unref(&ctx->script);
+ return -1;
+ }
+
+ *context = ctx;
+ return 0;
+}
+
+static bool
+push_notification_driver_lua_init_events(
+ struct push_notification_driver_txn *dtxn)
+{
+ struct dlua_push_notification_context *ctx = dtxn->duser->context;
+ const struct push_notification_event *event;
+ ctx->config_mn.flags = DLUA_DEFAULT_EVENTS;
+ ctx->config_ma.flags = DLUA_DEFAULT_EVENTS;
+ ctx->config_fc.store_old = TRUE;
+ bool found_one = FALSE;
+
+ /* Register *all* events that are present in Lua */
+ array_foreach_elem(push_notification_get_events(), event) {
+ const char *name = event->name;
+ const char *fn = push_notification_driver_lua_to_fn(name);
+ if (!dlua_script_has_function(ctx->script, fn))
+ continue;
+
+ found_one = TRUE;
+
+ e_debug(ctx->event, "Found %s, handling %s event", fn, name);
+
+ if (strcmp(name, "MessageNew") == 0) {
+ push_notification_event_init(dtxn, name,
+ &ctx->config_mn);
+ } else if (strcmp(name, "MessageAppend") == 0) {
+ push_notification_event_init(dtxn, name,
+ &ctx->config_ma);
+ } else if (strcmp(name, "FlagsSet") == 0) {
+ push_notification_event_init(dtxn, name,
+ &ctx->config_fs);
+ } else if (strcmp(name, "FlagsClear") == 0) {
+ push_notification_event_init(dtxn, name,
+ &ctx->config_fc);
+ } else if (event->init.default_config != NULL) {
+ void *config = event->init.default_config();
+ push_notification_event_init(dtxn, name, config);
+ } else {
+ push_notification_event_init(dtxn, name, NULL);
+ }
+ }
+
+ return found_one;
+}
+
+static bool
+push_notification_driver_lua_begin_txn(
+ struct push_notification_driver_txn *dtxn)
+{
+ struct mail_user *user = dtxn->ptxn->muser;
+ struct dlua_push_notification_context *ctx = dtxn->duser->context;
+ struct event *event = event_create(ctx->event);
+ const char *error;
+
+ event_set_name(event, DLUA_CALL_FINISHED);
+ event_add_str(event, "function_name", DLUA_FN_BEGIN_TXN);
+
+ if (!dlua_script_has_function(ctx->script, DLUA_FN_BEGIN_TXN)) {
+ event_add_str(event, "error",
+ "Missing function " DLUA_FN_BEGIN_TXN);
+ e_error(event, "Missing function " DLUA_FN_BEGIN_TXN);
+ event_unref(&event);
+ return FALSE;
+ }
+
+ if (!push_notification_driver_lua_init_events(dtxn)) {
+ e_debug(event, "No event handlers found in script");
+ event_unref(&event);
+ return FALSE;
+ }
+
+ e_debug(ctx->event, "Calling " DLUA_FN_BEGIN_TXN "(%s)",
+ user->username);
+
+ /* Push mail user as argument */
+ dlua_push_mail_user(ctx->script->L, user);
+ if (dlua_pcall(ctx->script->L, DLUA_FN_BEGIN_TXN, 1, 1, &error) < 0) {
+ event_add_str(event, "error", error);
+ e_error(event, "%s", error);
+ return FALSE;
+ }
+
+ e_debug(event, "Called " DLUA_FN_BEGIN_TXN);
+ event_unref(&event);
+
+ /* Store the result */
+ struct dlua_push_notification_txn_context *tctx =
+ p_new(dtxn->ptxn->pool,
+ struct dlua_push_notification_txn_context, 1);
+
+ tctx->tx_ref = luaL_ref(ctx->script->L, LUA_REGISTRYINDEX);
+ dtxn->context = tctx;
+ mail_user_ref(user);
+
+ return TRUE;
+}
+
+/* This function only works here, it converts MessageType to event_message_type
+ */
+static const char *push_notification_driver_lua_to_fn(const char *evname)
+{
+ /* Camelcase to event_event_name (most events have two underscores) */
+ string_t *fn = t_str_new(strlen(evname) +
+ strlen(DLUA_FN_EVENT_PREFIX) + 2);
+ str_append(fn, DLUA_FN_EVENT_PREFIX);
+
+ for(; *evname != '\0'; evname++) {
+ if (*evname >= 'A' && *evname <= 'Z') {
+ str_append_c(fn, '_');
+ str_append_c(fn, (*evname) - 'A' + 'a');
+ } else {
+ str_append_c(fn, *evname);
+ }
+ }
+
+ return str_c(fn);
+}
+
+/* Pushes lua list of flags */
+static void dlua_push_flags(struct dlua_script *script, enum mail_flags flags)
+{
+ lua_newtable(script->L);
+ int idx = 1;
+
+ if ((flags & MAIL_ANSWERED) != 0) {
+ lua_pushliteral(script->L, "\\Answered");
+ lua_rawseti(script->L, -2, idx++);
+ }
+ if ((flags & MAIL_FLAGGED) != 0) {
+ lua_pushliteral(script->L, "\\Flagged");
+ lua_rawseti(script->L, -2, idx++);
+ }
+ if ((flags & MAIL_DELETED) != 0) {
+ lua_pushliteral(script->L, "\\Deleted");
+ lua_rawseti(script->L, -2, idx++);
+ }
+ if ((flags & MAIL_SEEN) != 0) {
+ lua_pushliteral(script->L, "\\Seen");
+ lua_rawseti(script->L, -2, idx++);
+ }
+ if ((flags & MAIL_DRAFT) != 0) {
+ lua_pushliteral(script->L, "\\Draft");
+ lua_rawseti(script->L, -2, idx++);
+ }
+ if ((flags & MAIL_RECENT) != 0) {
+ lua_pushliteral(script->L, "\\Recent");
+ lua_rawseti(script->L, -2, idx++);
+ }
+}
+
+static void
+dlua_push_keywords(struct dlua_script *script, const char *const *keywords,
+ unsigned int count)
+{
+ lua_newtable(script->L);
+ if (keywords == NULL)
+ return;
+ for (unsigned int idx = 0; idx < count; idx++) {
+ lua_pushstring(script->L, keywords[idx]);
+ lua_rawseti(script->L, -2, idx+1);
+ }
+}
+
+static void
+push_notification_lua_push_flagsclear(
+ const struct push_notification_txn_event *event,
+ struct dlua_script *script)
+{
+ /* Push cleared flags */
+ unsigned int size = 0;
+ struct push_notification_event_flagsclear_data *data = event->data;
+
+ dlua_push_flags(script, data->flags_clear);
+ lua_setfield(script->L, -2, "flags");
+ dlua_push_flags(script, data->flags_old);
+ lua_setfield(script->L, -2, "flags_old");
+
+ if (array_is_created(&data->keywords_clear)) {
+ const char *const *kw = array_get(&data->keywords_clear, &size);
+ dlua_push_keywords(script, kw, size);
+ lua_setfield(script->L, -2, "keywords");
+ }
+
+ if (array_is_created(&data->keywords_old)) {
+ const char *const *kw = array_get(&data->keywords_old, &size);
+ dlua_push_keywords(script, kw, size);
+ lua_setfield(script->L, -2, "keywords_old");
+ }
+}
+
+static void
+push_notification_lua_push_flagsset(
+ const struct push_notification_txn_event *event,
+ struct dlua_script *script)
+{
+ /* push set flags */
+ unsigned int size = 0;
+ struct push_notification_event_flagsset_data *data = event->data;
+
+ dlua_push_flags(script, data->flags_set);
+ lua_setfield(script->L, -2, "flags");
+
+ if (array_is_created(&data->keywords_set)) {
+ const char *const *kw = array_get(&data->keywords_set, &size);
+ dlua_push_keywords(script, kw, size);
+ lua_setfield(script->L, -2, "keywords");
+ }
+}
+
+static void
+push_notification_lua_push_mailboxrename(
+ const struct push_notification_txn_event *event,
+ struct dlua_script *script)
+{
+ struct push_notification_event_mailboxrename_data *data = event->data;
+
+ lua_pushstring(script->L, data->old_mbox);
+ lua_setfield(script->L, -2, "mailbox_old");
+}
+
+#define push_notification_lua_push_string(L, value) \
+ lua_pushstring((L), (value) == NULL ? "" : (value))
+
+static void
+push_notification_lua_push_message_ext(
+ const struct push_notification_message_ext *ext,
+ struct dlua_script *script)
+{
+ push_notification_lua_push_string(script->L, ext->from_address);
+ lua_setfield(script->L, -2, "from_address");
+ push_notification_lua_push_string(script->L, ext->from_display_name_utf8);
+ lua_setfield(script->L, -2, "from_display_name");
+
+ push_notification_lua_push_string(script->L, ext->to_address);
+ lua_setfield(script->L, -2, "to_address");
+ push_notification_lua_push_string(script->L, ext->to_display_name_utf8);
+ lua_setfield(script->L, -2, "to_display_name");
+
+ lua_pushstring(script->L, ext->subject_utf8);
+ lua_setfield(script->L, -2, "subject");
+}
+
+static void
+push_notification_lua_push_messageappend(
+ const struct push_notification_txn_event *event,
+ struct dlua_script *script)
+{
+ struct push_notification_event_messageappend_data *data = event->data;
+
+ lua_pushnumber(script->L, data->date);
+ lua_setfield(script->L, -2, "date");
+
+ lua_pushnumber(script->L, data->date_tz);
+ lua_setfield(script->L, -2, "tz");
+
+ push_notification_lua_push_string(script->L, data->from);
+ lua_setfield(script->L, -2, "from");
+
+ push_notification_lua_push_string(script->L, data->to);
+ lua_setfield(script->L, -2, "to");
+
+ lua_pushstring(script->L, data->snippet);
+ lua_setfield(script->L, -2, "snippet");
+
+ dlua_push_flags(script, data->flags);
+ lua_setfield(script->L, -2, "flags");
+
+ dlua_push_keywords(script, data->keywords,
+ str_array_length(data->keywords));
+ lua_setfield(script->L, -2, "keywords");
+
+ lua_pushstring(script->L, data->message_id);
+ lua_setfield(script->L, -2, "message_id");
+
+ push_notification_lua_push_message_ext(&data->ext, script);
+}
+
+static void
+push_notification_lua_push_messagenew(
+ const struct push_notification_txn_event *event,
+ struct dlua_script *script)
+{
+ struct push_notification_event_messagenew_data *data = event->data;
+
+ lua_pushnumber(script->L, data->date);
+ lua_setfield(script->L, -2, "date");
+
+ lua_pushnumber(script->L, data->date_tz);
+ lua_setfield(script->L, -2, "tz");
+
+ push_notification_lua_push_string(script->L, data->from);
+ lua_setfield(script->L, -2, "from");
+
+ push_notification_lua_push_string(script->L, data->to);
+ lua_setfield(script->L, -2, "to");
+
+ lua_pushstring(script->L, data->snippet);
+ lua_setfield(script->L, -2, "snippet");
+
+ dlua_push_flags(script, data->flags);
+ lua_setfield(script->L, -2, "flags");
+
+ dlua_push_keywords(script, data->keywords,
+ str_array_length(data->keywords));
+ lua_setfield(script->L, -2, "keywords");
+
+ lua_pushstring(script->L, data->message_id);
+ lua_setfield(script->L, -2, "message_id");
+
+ push_notification_lua_push_message_ext(&data->ext, script);
+}
+
+/* Events that need special treatment */
+static struct push_notification_event_to_lua {
+ const char *event_name;
+ void (*push)(const struct push_notification_txn_event *event,
+ struct dlua_script *script);
+} event_to_push_table[] = {
+ {
+ .event_name = "FlagsClear",
+ .push = push_notification_lua_push_flagsclear
+ },
+ {
+ .event_name = "FlagsSet",
+ .push = push_notification_lua_push_flagsset
+ },
+ {
+ .event_name = "MailboxRename",
+ .push = push_notification_lua_push_mailboxrename
+ },
+ {
+ .event_name = "MessageAppend",
+ .push = push_notification_lua_push_messageappend
+ },
+ {
+ .event_name = "MessageNew",
+ .push = push_notification_lua_push_messagenew
+ },
+};
+
+static void
+push_notification_driver_lua_push_event(
+ const struct push_notification_txn_event *event,
+ struct dlua_push_notification_context *ctx)
+{
+ struct dlua_script *script = ctx->script;
+ const char *name = event->event->event->name;
+
+ /* Create a table */
+ lua_newtable(script->L);
+
+ /* Event name */
+ lua_pushstring(script->L, name);
+ lua_setfield(script->L, -2, "name");
+
+ for(size_t i = 0; i < N_ELEMENTS(event_to_push_table); i++)
+ if (strcmp(event_to_push_table[i].event_name, name) == 0)
+ event_to_push_table[i].push(event, script);
+}
+
+static void
+push_notification_driver_lua_call(
+ struct dlua_push_notification_context *ctx,
+ struct dlua_push_notification_txn_context *tctx,
+ const struct push_notification_txn_event *event,
+ const struct push_notification_txn_mbox *mbox,
+ struct push_notification_txn_msg *msg)
+{
+ const char *error;
+ const char *fn =
+ push_notification_driver_lua_to_fn(event->event->event->name);
+ struct event *e = event_create(ctx->event);
+ event_set_name(e, DLUA_CALL_FINISHED);
+ event_add_str(e, "event_name", event->event->event->name);
+ event_add_str(e, "function_name", fn);
+
+ /* Push context */
+ lua_rawgeti(ctx->script->L, LUA_REGISTRYINDEX, tctx->tx_ref);
+
+ /* Push event + common fields */
+ push_notification_driver_lua_push_event(event, ctx);
+
+ if (mbox != NULL) {
+ lua_pushstring(ctx->script->L, mbox->mailbox);
+ lua_setfield(ctx->script->L, -2, "mailbox");
+ e_debug(ctx->event,
+ "Calling %s(ctx, event[name=%s,mailbox=%s])",
+ fn, event->event->event->name, mbox->mailbox);
+ event_add_str(e, "mailbox", mbox->mailbox);
+ } else if (msg != NULL) {
+ lua_pushstring(ctx->script->L, msg->mailbox);
+ lua_setfield(ctx->script->L, -2, "mailbox");
+ lua_pushnumber(ctx->script->L, msg->uid);
+ lua_setfield(ctx->script->L, -2, "uid");
+ lua_pushnumber(ctx->script->L, msg->uid_validity);
+ lua_setfield(ctx->script->L, -2, "uid_validity");
+ e_debug(ctx->event,
+ "Calling %s(ctx, event[name=%s,mailbox=%s,uid=%u])",
+ fn, event->event->event->name, msg->mailbox, msg->uid);
+ event_add_str(e, "mailbox", msg->mailbox);
+ event_add_int(e, "uid", msg->uid);
+ } else
+ i_unreached();
+
+ /* Perform call */
+ if (dlua_pcall(ctx->script->L, fn, 2, 0, &error) < 0) {
+ event_add_str(e, "error", error);
+ e_error(e, "%s", error);
+ } else {
+ e_debug(e, "Called %s", fn);
+ }
+ event_unref(&e);
+}
+
+static void
+push_notification_driver_lua_process_mbox(
+ struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_txn_event *event;
+ struct dlua_push_notification_context *ctx = dtxn->duser->context;
+ struct dlua_push_notification_txn_context *tctx = dtxn->context;
+
+ if (array_is_created(&mbox->eventdata)) {
+ array_foreach_elem(&mbox->eventdata, event) {
+ push_notification_driver_lua_call(ctx, tctx,
+ event, mbox, NULL);
+ }
+ }
+}
+
+static void
+push_notification_driver_lua_process_msg(
+ struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_txn_event *event;
+ struct dlua_push_notification_context *ctx = dtxn->duser->context;
+ struct dlua_push_notification_txn_context *tctx = dtxn->context;
+
+ if (array_is_created(&msg->eventdata)) {
+ array_foreach_elem(&msg->eventdata, event) {
+ push_notification_driver_lua_call(ctx, tctx,
+ event, NULL, msg);
+ }
+ }
+}
+
+static void
+push_notification_driver_lua_end_txn(struct push_notification_driver_txn *dtxn,
+ bool success)
+{
+ /* Call end txn */
+ const char *error;
+ struct dlua_push_notification_context *ctx = dtxn->duser->context;
+ struct dlua_push_notification_txn_context *tctx = dtxn->context;
+ struct mail_user *user = dtxn->ptxn->muser;
+ struct event *event = event_create(ctx->event);
+ event_set_name(event, DLUA_CALL_FINISHED);
+ event_add_str(event, "function_name", DLUA_FN_END_TXN);
+
+ if (!dlua_script_has_function(ctx->script, DLUA_FN_END_TXN)) {
+ e_error(event, "Missing function " DLUA_FN_END_TXN);
+ } else {
+ e_debug(ctx->event, "Calling " DLUA_FN_END_TXN);
+ lua_rawgeti(ctx->script->L, LUA_REGISTRYINDEX, tctx->tx_ref);
+ lua_pushboolean(ctx->script->L, success);
+ if (dlua_pcall(ctx->script->L, DLUA_FN_END_TXN, 2, 0, &error) < 0) {
+ event_add_str(event, "error", error);
+ e_error(event, "%s", error);
+ } else {
+ e_debug(event, "Called " DLUA_FN_END_TXN);
+ }
+ }
+
+ event_unref(&event);
+ /* Release context */
+ luaL_unref(ctx->script->L, LUA_REGISTRYINDEX, tctx->tx_ref);
+ /* Call gc here */
+ (void)lua_gc(ctx->script->L, LUA_GCCOLLECT, 1);
+ mail_user_unref(&user);
+}
+
+static void
+push_notification_driver_lua_deinit(struct push_notification_driver_user *duser)
+{
+ /* Call lua deinit */
+ struct dlua_push_notification_context *ctx = duser->context;
+ dlua_script_unref(&ctx->script);
+ event_unref(&ctx->event);
+}
+
+static void push_notification_driver_lua_cleanup(void)
+{
+ /* noop */
+}
+
+/* Driver definition */
+
+struct push_notification_driver push_notification_driver_lua = {
+ .name = "lua",
+ .v = {
+ .init = push_notification_driver_lua_init,
+ .begin_txn = push_notification_driver_lua_begin_txn,
+ .process_mbox = push_notification_driver_lua_process_mbox,
+ .process_msg = push_notification_driver_lua_process_msg,
+ .end_txn = push_notification_driver_lua_end_txn,
+ .deinit = push_notification_driver_lua_deinit,
+ .cleanup = push_notification_driver_lua_cleanup,
+ },
+};
+
+void push_notification_lua_plugin_init(struct module *module);
+void push_notification_lua_plugin_deinit(void);
+
+void push_notification_lua_plugin_init(struct module *module ATTR_UNUSED)
+{
+ push_notification_driver_register(&push_notification_driver_lua);
+}
+
+void push_notification_lua_plugin_deinit(void)
+{
+ push_notification_driver_unregister(&push_notification_driver_lua);
+}
+
+const char *push_notification_lua_plugin_version = DOVECOT_ABI_VERSION;
+const char *push_notification_lua_plugin_dependencies[] =
+ { "push_notification", "mail_lua", NULL};
diff --git a/src/plugins/push-notification/push-notification-driver-ox.c b/src/plugins/push-notification/push-notification-driver-ox.c
new file mode 100644
index 0000000..728cce9
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-driver-ox.c
@@ -0,0 +1,470 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "settings-parser.h"
+#include "json-parser.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-private.h"
+#include "str.h"
+#include "strescape.h"
+#include "iostream-ssl.h"
+
+#include "push-notification-plugin.h"
+#include "push-notification-drivers.h"
+#include "push-notification-event-messagenew.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-msg.h"
+
+#define OX_METADATA_KEY \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER \
+ "vendor/vendor.dovecot/http-notify"
+
+/* Default values. */
+static const char *const default_events[] = { "MessageNew", NULL };
+static const char *const default_mboxes[] = { "INBOX", NULL };
+#define DEFAULT_CACHE_LIFETIME_SECS 60
+#define DEFAULT_TIMEOUT_MSECS 2000
+#define DEFAULT_RETRY_COUNT 1
+
+/* This is data that is shared by all plugin users. */
+struct push_notification_driver_ox_global {
+ struct http_client *http_client;
+ int refcount;
+};
+static struct push_notification_driver_ox_global *ox_global = NULL;
+
+/* This is data specific to an OX driver. */
+struct push_notification_driver_ox_config {
+ struct http_url *http_url;
+ struct event *event;
+ unsigned int cached_ox_metadata_lifetime_secs;
+ bool use_unsafe_username;
+ unsigned int http_max_retries;
+ unsigned int http_timeout_msecs;
+
+ char *cached_ox_metadata;
+ time_t cached_ox_metadata_timestamp;
+};
+
+/* This is data specific to an OX driver transaction. */
+struct push_notification_driver_ox_txn {
+ const char *unsafe_user;
+};
+
+static void
+push_notification_driver_ox_init_global(
+ struct mail_user *user,
+ struct push_notification_driver_ox_config *config)
+{
+ struct http_client_settings http_set;
+ struct ssl_iostream_settings ssl_set;
+
+ if (ox_global->http_client == NULL) {
+ /* This is going to use the first user's settings, but these are
+ unlikely to change between users so it shouldn't matter much.
+ */
+ i_zero(&http_set);
+ http_set.debug = user->mail_debug;
+ http_set.max_attempts = config->http_max_retries+1;
+ http_set.request_timeout_msecs = config->http_timeout_msecs;
+ http_set.event_parent = user->event;
+ mail_user_init_ssl_client_settings(user, &ssl_set);
+ http_set.ssl = &ssl_set;
+
+ ox_global->http_client = http_client_init(&http_set);
+ }
+}
+
+static int
+push_notification_driver_ox_init(struct push_notification_driver_config *config,
+ struct mail_user *user, pool_t pool,
+ void **context, const char **error_r)
+{
+ struct push_notification_driver_ox_config *dconfig;
+ const char *error, *tmp;
+
+ /* Valid config keys: cache_lifetime, url */
+ tmp = hash_table_lookup(config->config, (const char *)"url");
+ if (tmp == NULL) {
+ *error_r = "Driver requires the url parameter";
+ return -1;
+ }
+
+ dconfig = p_new(pool, struct push_notification_driver_ox_config, 1);
+ dconfig->event = event_create(user->event);
+ event_add_category(dconfig->event, &event_category_push_notification);
+ event_set_append_log_prefix(dconfig->event, "push-notification-ox: ");
+
+ if (http_url_parse(tmp, NULL, HTTP_URL_ALLOW_USERINFO_PART, pool,
+ &dconfig->http_url, &error) < 0) {
+ event_unref(&dconfig->event);
+ *error_r = t_strdup_printf("Failed to parse OX REST URL %s: %s",
+ tmp, error);
+ return -1;
+ }
+ dconfig->use_unsafe_username =
+ hash_table_lookup(config->config,
+ (const char *)"user_from_metadata") != NULL;
+
+ e_debug(dconfig->event, "Using URL %s", tmp);
+
+ tmp = hash_table_lookup(config->config, (const char *)"cache_lifetime");
+ if (tmp == NULL) {
+ dconfig->cached_ox_metadata_lifetime_secs =
+ DEFAULT_CACHE_LIFETIME_SECS;
+ } else if (settings_get_time(
+ tmp, &dconfig->cached_ox_metadata_lifetime_secs, &error) < 0) {
+ event_unref(&dconfig->event);
+ *error_r = t_strdup_printf(
+ "Failed to parse OX cache_lifetime %s: %s", tmp, error);
+ return -1;
+ }
+
+ tmp = hash_table_lookup(config->config, (const char *)"max_retries");
+ if ((tmp == NULL) ||
+ (str_to_uint(tmp, &dconfig->http_max_retries) < 0)) {
+ dconfig->http_max_retries = DEFAULT_RETRY_COUNT;
+ }
+ tmp = hash_table_lookup(config->config, (const char *)"timeout_msecs");
+ if ((tmp == NULL) ||
+ (str_to_uint(tmp, &dconfig->http_timeout_msecs) < 0)) {
+ dconfig->http_timeout_msecs = DEFAULT_TIMEOUT_MSECS;
+ }
+
+ e_debug(dconfig->event, "Using cache lifetime: %u",
+ dconfig->cached_ox_metadata_lifetime_secs);
+
+ if (ox_global == NULL) {
+ ox_global = i_new(struct push_notification_driver_ox_global, 1);
+ ox_global->refcount = 0;
+ }
+
+ ++ox_global->refcount;
+ *context = dconfig;
+
+ return 0;
+}
+
+static const char *
+push_notification_driver_ox_get_metadata(
+ struct push_notification_driver_txn *dtxn)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+ struct mail_attribute_value attr;
+ struct mailbox *inbox;
+ struct mail_namespace *ns;
+ bool success = FALSE, use_existing_txn = FALSE;
+ int ret;
+
+ if ((dconfig->cached_ox_metadata != NULL) &&
+ ((dconfig->cached_ox_metadata_timestamp +
+ (time_t)dconfig->cached_ox_metadata_lifetime_secs) >
+ ioloop_time)) {
+ return dconfig->cached_ox_metadata;
+ }
+
+ /* Get canonical INBOX, where private server-level metadata is stored.
+ * See imap/cmd-getmetadata.c */
+ if ((dtxn->ptxn->t != NULL) && dtxn->ptxn->mbox->inbox_user) {
+ inbox = dtxn->ptxn->mbox;
+ use_existing_txn = TRUE;
+ } else {
+ ns = mail_namespace_find_inbox(dtxn->ptxn->muser->namespaces);
+ inbox = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_READONLY);
+ }
+
+ ret = mailbox_attribute_get(inbox, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ OX_METADATA_KEY, &attr);
+ if (ret < 0) {
+ e_error(dconfig->event,
+ "Skipped because unable to get attribute: %s",
+ mailbox_get_last_internal_error(inbox, NULL));
+ } else if (ret == 0) {
+ e_debug(dconfig->event,
+ "Skipped because not active "
+ "(/private/"OX_METADATA_KEY" METADATA not set)");
+ } else {
+ success = TRUE;
+ }
+
+ if (!use_existing_txn)
+ mailbox_free(&inbox);
+ if (!success)
+ return NULL;
+
+ i_free(dconfig->cached_ox_metadata);
+ dconfig->cached_ox_metadata = i_strdup(attr.value);
+ dconfig->cached_ox_metadata_timestamp = ioloop_time;
+
+ return dconfig->cached_ox_metadata;
+}
+
+static bool
+push_notification_driver_ox_begin_txn(struct push_notification_driver_txn *dtxn)
+{
+ const char *const *args;
+ struct push_notification_event_messagenew_config *config;
+ const char *key, *mbox_curr, *md_value, *value;
+ bool mbox_found = FALSE;
+ struct push_notification_driver_ox_txn *txn;
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+
+ md_value = push_notification_driver_ox_get_metadata(dtxn);
+ if (md_value == NULL)
+ return FALSE;
+
+ /* Unused keys: events, expire, folder */
+ /* TODO: To be implemented later(?) */
+ const char *const *events = default_events;
+ time_t expire = INT_MAX;
+ const char *const *mboxes = default_mboxes;
+
+ if (expire < ioloop_time) {
+ e_debug(dconfig->event, "Skipped due to expiration (%ld < %ld)",
+ (long)expire, (long)ioloop_time);
+ return FALSE;
+ }
+
+ mbox_curr = mailbox_get_vname(dtxn->ptxn->mbox);
+ for (; *mboxes != NULL; mboxes++) {
+ if (strcmp(mbox_curr, *mboxes) == 0) {
+ mbox_found = TRUE;
+ break;
+ }
+ }
+
+ if (mbox_found == FALSE) {
+ e_debug(dconfig->event,
+ "Skipped because %s is not a watched mailbox",
+ mbox_curr);
+ return FALSE;
+ }
+
+ txn = p_new(dtxn->ptxn->pool,
+ struct push_notification_driver_ox_txn, 1);
+
+ /* Valid keys: user */
+ args = t_strsplit_tabescaped(md_value);
+ for (; *args != NULL; args++) {
+ key = *args;
+
+ value = strchr(key, '=');
+ if (value != NULL) {
+ key = t_strdup_until(key, value++);
+ if (strcmp(key, "user") == 0) {
+ txn->unsafe_user =
+ p_strdup(dtxn->ptxn->pool, value);
+ }
+ }
+ }
+
+ if (txn->unsafe_user == NULL) {
+ e_error(dconfig->event, "No user provided in config");
+ return FALSE;
+ }
+
+ e_debug(dconfig->event, "User (%s)", txn->unsafe_user);
+
+ for (; *events != NULL; events++) {
+ if (strcmp(*events, "MessageNew") == 0) {
+ config = p_new(
+ dtxn->ptxn->pool,
+ struct push_notification_event_messagenew_config, 1);
+ config->flags = PUSH_NOTIFICATION_MESSAGE_HDR_FROM |
+ PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT |
+ PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET;
+ push_notification_event_init(
+ dtxn, "MessageNew", config);
+ e_debug(dconfig->event, "Handling MessageNew event");
+ }
+ }
+
+ dtxn->context = txn;
+
+ return TRUE;
+}
+
+static void
+push_notification_driver_ox_http_callback(
+ const struct http_response *response,
+ struct push_notification_driver_ox_config *dconfig)
+{
+ switch (response->status / 100) {
+ case 2:
+ // Success.
+ e_debug(dconfig->event, "Notification sent successfully: %s",
+ http_response_get_message(response));
+ break;
+
+ default:
+ // Error.
+ e_error(dconfig->event, "Error when sending notification: %s",
+ http_response_get_message(response));
+ break;
+ }
+}
+
+/* Callback needed for i_stream_add_destroy_callback() in
+ push_notification_driver_ox_process_msg. */
+static void str_free_i(string_t *str)
+{
+ str_free(&str);
+}
+
+static int
+push_notification_driver_ox_get_mailbox_status(
+ struct push_notification_driver_txn *dtxn,
+ struct mailbox_status *r_box_status)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+ /* The already opened mailbox. We cannot use or sync it, because we are
+ within a save transaction. */
+ struct mailbox *mbox = dtxn->ptxn->mbox;
+ struct mailbox *box;
+ int ret;
+
+ /* Open and sync new instance of the same mailbox to get most recent
+ status */
+ box = mailbox_alloc(mailbox_get_namespace(mbox)->list,
+ mailbox_get_name(mbox), MAILBOX_FLAG_READONLY);
+ if (mailbox_sync(box, 0) < 0) {
+ e_error(dconfig->event, "mailbox_sync(%s) failed: %s",
+ mailbox_get_vname(mbox),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else {
+ /* only 'unseen' is needed at the moment */
+ mailbox_get_open_status(box, STATUS_UNSEEN, r_box_status);
+ e_debug(dconfig->event,
+ "Got status of mailbox '%s': (unseen: %u)",
+ mailbox_get_vname(box), r_box_status->unseen);
+ ret = 0;
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+
+static void
+push_notification_driver_ox_process_msg(
+ struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ (struct push_notification_driver_ox_config *)
+ dtxn->duser->context;
+ struct http_client_request *http_req;
+ struct push_notification_event_messagenew_data *messagenew;
+ struct istream *payload;
+ string_t *str;
+ struct push_notification_driver_ox_txn *txn =
+ (struct push_notification_driver_ox_txn *)dtxn->context;
+ struct mail_user *user = dtxn->ptxn->muser;
+ struct mailbox_status box_status;
+ bool status_success = TRUE;
+
+ if (push_notification_driver_ox_get_mailbox_status(
+ dtxn, &box_status) < 0) {
+ status_success = FALSE;
+ }
+
+ messagenew = push_notification_txn_msg_get_eventdata(msg, "MessageNew");
+ if (messagenew == NULL)
+ return;
+
+ push_notification_driver_ox_init_global(user, dconfig);
+
+ http_req = http_client_request_url(
+ ox_global->http_client, "PUT", dconfig->http_url,
+ push_notification_driver_ox_http_callback, dconfig);
+ http_client_request_set_event(http_req, dtxn->ptxn->event);
+ http_client_request_add_header(http_req, "Content-Type",
+ "application/json; charset=utf-8");
+
+ str = str_new(default_pool, 256);
+ str_append(str, "{\"user\":\"");
+ json_append_escaped(str, dconfig->use_unsafe_username ?
+ txn->unsafe_user : user->username);
+ str_append(str, "\",\"event\":\"messageNew\",\"folder\":\"");
+ json_append_escaped(str, msg->mailbox);
+ str_printfa(str, "\",\"imap-uidvalidity\":%u,\"imap-uid\":%u",
+ msg->uid_validity, msg->uid);
+ if (messagenew->from != NULL) {
+ str_append(str, ",\"from\":\"");
+ json_append_escaped(str, messagenew->from);
+ str_append(str, "\"");
+ }
+ if (messagenew->subject != NULL) {
+ str_append(str, ",\"subject\":\"");
+ json_append_escaped(str, messagenew->subject);
+ str_append(str, "\"");
+ }
+ if (messagenew->snippet != NULL) {
+ str_append(str, ",\"snippet\":\"");
+ json_append_escaped(str, messagenew->snippet);
+ str_append(str, "\"");
+ }
+ if (status_success) {
+ str_printfa(str, ",\"unseen\":%u", box_status.unseen);
+ }
+ str_append(str, "}");
+
+ e_debug(dconfig->event, "Sending notification: %s", str_c(str));
+
+ payload = i_stream_create_from_data(str_data(str), str_len(str));
+ i_stream_add_destroy_callback(payload, str_free_i, str);
+ http_client_request_set_payload(http_req, payload, FALSE);
+
+ http_client_request_submit(http_req);
+ i_stream_unref(&payload);
+}
+
+static void
+push_notification_driver_ox_deinit(
+ struct push_notification_driver_user *duser ATTR_UNUSED)
+{
+ struct push_notification_driver_ox_config *dconfig = duser->context;
+
+ i_free(dconfig->cached_ox_metadata);
+ if (ox_global != NULL) {
+ if (ox_global->http_client != NULL)
+ http_client_wait(ox_global->http_client);
+ i_assert(ox_global->refcount > 0);
+ --ox_global->refcount;
+ }
+ event_unref(&dconfig->event);
+}
+
+static void push_notification_driver_ox_cleanup(void)
+{
+ if ((ox_global != NULL) && (ox_global->refcount <= 0)) {
+ if (ox_global->http_client != NULL) {
+ http_client_deinit(&ox_global->http_client);
+ }
+ i_free_and_null(ox_global);
+ }
+}
+
+/* Driver definition */
+
+extern struct push_notification_driver push_notification_driver_ox;
+
+struct push_notification_driver push_notification_driver_ox = {
+ .name = "ox",
+ .v = {
+ .init = push_notification_driver_ox_init,
+ .begin_txn = push_notification_driver_ox_begin_txn,
+ .process_msg = push_notification_driver_ox_process_msg,
+ .deinit = push_notification_driver_ox_deinit,
+ .cleanup = push_notification_driver_ox_cleanup,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-drivers.c b/src/plugins/push-notification/push-notification-drivers.c
new file mode 100644
index 0000000..67295d1
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-drivers.c
@@ -0,0 +1,181 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "mail-user.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+
+static ARRAY(const struct push_notification_driver *) push_notification_drivers;
+
+static bool
+push_notification_driver_find(const char *name, unsigned int *idx_r)
+{
+ unsigned int count, i;
+ const struct push_notification_driver *const *drivers;
+
+ drivers = array_get(&push_notification_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(drivers[i]->name, name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static const struct push_notification_driver *
+push_notification_driver_find_class(const char *driver)
+{
+ unsigned int idx;
+
+ if (!push_notification_driver_find(driver, &idx))
+ return NULL;
+
+ return array_idx_elem(&push_notification_drivers, idx);
+}
+
+static struct push_notification_driver_config *
+push_notification_driver_parse_config(const char *p)
+{
+ const char **args, *key, *p2, *value;
+ struct push_notification_driver_config *config;
+
+ config = t_new(struct push_notification_driver_config, 1);
+ config->raw_config = p;
+
+ hash_table_create(&config->config, unsafe_data_stack_pool, 0,
+ str_hash, strcmp);
+
+ if (p == NULL)
+ return config;
+
+ args = t_strsplit_spaces(p, " ");
+
+ for (; *args != NULL; args++) {
+ p2 = strchr(*args, '=');
+ if (p2 != NULL) {
+ key = t_strdup_until(*args, p2);
+ value = t_strdup(p2 + 1);
+ } else {
+ key = *args;
+ value = "";
+ }
+ hash_table_update(config->config, key, value);
+ }
+
+ return config;
+}
+
+int push_notification_driver_init(
+ struct mail_user *user, const char *config_in, pool_t pool,
+ struct push_notification_driver_user **duser_r)
+{
+ void *context = NULL;
+ const struct push_notification_driver *driver;
+ const char *driver_name, *error_r, *p;
+ struct push_notification_driver_user *duser;
+ int ret;
+
+ /* <driver>[:<driver config>] */
+ p = strchr(config_in, ':');
+ if (p == NULL)
+ driver_name = config_in;
+ else
+ driver_name = t_strdup_until(config_in, p);
+
+ driver = push_notification_driver_find_class(driver_name);
+ if (driver == NULL) {
+ i_error("Unknown push notification driver: %s", driver_name);
+ return -1;
+ }
+
+ if (driver->v.init != NULL) {
+ T_BEGIN {
+ struct push_notification_driver_config *config;
+
+ config = push_notification_driver_parse_config(
+ (p == NULL) ? p : p + 1);
+ ret = driver->v.init(config, user, pool,
+ &context, &error_r);
+ if (ret < 0)
+ i_error("%s: %s", driver_name, error_r);
+ hash_table_destroy(&config->config);
+ } T_END;
+
+ if (ret < 0)
+ return -1;
+ }
+
+ duser = p_new(pool, struct push_notification_driver_user, 1);
+ duser->context = context;
+ duser->driver = driver;
+
+ *duser_r = duser;
+
+ return 0;
+}
+
+void push_notification_driver_cleanup_all(void)
+{
+ const struct push_notification_driver *driver;
+
+ /* Loop through driver list and perform global cleanup tasks. We may not
+ have used all drivers in this plugin/worker, but the cleanup hooks
+ are designed to ignore these unused drivers. */
+ array_foreach_elem(&push_notification_drivers, driver) {
+ if (driver->v.cleanup != NULL)
+ driver->v.cleanup();
+ }
+}
+
+void ATTR_FORMAT(3, 4)
+push_notification_driver_debug(const char *label, struct mail_user *user,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ T_BEGIN {
+ va_start(args, fmt);
+ e_debug(user->event, "%s%s", label,
+ t_strdup_vprintf(fmt, args));
+ va_end(args);
+ } T_END;
+}
+
+void push_notification_driver_register(
+ const struct push_notification_driver *driver)
+{
+ unsigned int idx;
+
+ if (!array_is_created(&push_notification_drivers))
+ i_array_init(&push_notification_drivers, 4);
+
+ if (push_notification_driver_find(driver->name, &idx)) {
+ i_panic("push_notification_driver_register(%s): "
+ "duplicate driver", driver->name);
+ }
+
+ array_push_back(&push_notification_drivers, &driver);
+}
+
+void push_notification_driver_unregister(
+ const struct push_notification_driver *driver)
+{
+ unsigned int idx;
+
+ if (!push_notification_driver_find(driver->name, &idx)) {
+ i_panic("push_notification_driver_register(%s): "
+ "unknown driver", driver->name);
+ }
+
+ if (array_is_created(&push_notification_drivers)) {
+ array_delete(&push_notification_drivers, idx, 1);
+
+ if (array_is_empty(&push_notification_drivers))
+ array_free(&push_notification_drivers);
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-drivers.h b/src/plugins/push-notification/push-notification-drivers.h
new file mode 100644
index 0000000..07b570f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-drivers.h
@@ -0,0 +1,123 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_DRIVERS_H
+#define PUSH_NOTIFICATION_DRIVERS_H
+
+#include "mail-user.h"
+#include "push-notification-triggers.h"
+
+struct mail_user;
+struct push_notification_driver_config;
+struct push_notification_driver_txn;
+struct push_notification_driver_user;
+struct push_notification_txn_mbox;
+struct push_notification_txn_msg;
+
+HASH_TABLE_DEFINE_TYPE(push_notification_config, const char *, const char *);
+HASH_TABLE_DEFINE_TYPE(push_notification_msgs, void *,
+ struct push_notification_txn_msg *);
+
+struct push_notification_driver_vfuncs {
+ /* Init driver. Config (from plugin configuration) is parsed once (no
+ user variable substitutions). Return 0 on success, or -1 if this
+ driver should be disabled (or on error). */
+ int (*init)(struct push_notification_driver_config *config,
+ struct mail_user *user, pool_t pool, void **context,
+ const char **error_r);
+ /* Called at the beginning of a notification transaction. Return TRUE on
+ success, or FALSE if this driver should be ignored for this
+ transaction. */
+ bool (*begin_txn)(struct push_notification_driver_txn *dtxn);
+ /* Called once for every mailbox processed. */
+ void (*process_mbox)(struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_mbox *mbox);
+ /* Called once for every message processed. */
+ void (*process_msg)(struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_msg *msg);
+ /* Called at the end of a successful notification transaction. */
+ void (*end_txn)(struct push_notification_driver_txn *dtxn,
+ bool success);
+ /* Called when plugin is deinitialized. */
+ void (*deinit)(struct push_notification_driver_user *duser);
+ /* Called to cleanup any global resources used in plugin. */
+ void (*cleanup)(void);
+};
+
+struct push_notification_driver {
+ const char *name;
+ struct push_notification_driver_vfuncs v;
+};
+
+struct push_notification_driver_config {
+ HASH_TABLE_TYPE(push_notification_config) config;
+ const char *raw_config;
+};
+
+struct push_notification_driver_user {
+ const struct push_notification_driver *driver;
+ void *context;
+};
+
+struct push_notification_driver_txn {
+ const struct push_notification_driver_user *duser;
+ struct push_notification_txn *ptxn;
+
+ /* Transaction context. */
+ void *context;
+};
+
+struct push_notification_driver_list {
+ ARRAY(struct push_notification_driver_user *) drivers;
+};
+
+struct push_notification_user {
+ union mail_user_module_context module_ctx;
+ struct push_notification_driver_list *driverlist;
+};
+
+struct push_notification_trigger_ctx {
+ const char *name;
+ void *context;
+};
+
+struct push_notification_txn {
+ pool_t pool;
+
+ struct mailbox *mbox;
+ struct mail_user *muser;
+ struct push_notification_user *puser;
+ bool initialized;
+
+ enum push_notification_event_trigger trigger;
+ struct push_notification_trigger_ctx *trigger_ctx;
+ ARRAY(struct push_notification_driver_txn *) drivers;
+ ARRAY(struct push_notification_event_config *) events;
+
+ struct event *event;
+
+ /* Used with mailbox events. */
+ struct push_notification_txn_mbox *mbox_txn;
+
+ /* Used with mailbox events. */
+ HASH_TABLE_TYPE(push_notification_msgs) messages;
+
+ /* Private (used with message events). */
+ struct mailbox_transaction_context *t;
+};
+
+
+int push_notification_driver_init(
+ struct mail_user *user, const char *config_in, pool_t pool,
+ struct push_notification_driver_user **duser_r);
+void push_notification_driver_cleanup_all(void);
+
+void ATTR_FORMAT(3, 4)
+push_notification_driver_debug(const char *label, struct mail_user *user,
+ const char *fmt, ...);
+
+void push_notification_driver_register(
+ const struct push_notification_driver *driver);
+void push_notification_driver_unregister(
+ const struct push_notification_driver *driver);
+
+#endif
diff --git a/src/plugins/push-notification/push-notification-event-flagsclear.c b/src/plugins/push-notification/push-notification-event-flagsclear.c
new file mode 100644
index 0000000..2ce0c3f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-flagsclear.c
@@ -0,0 +1,170 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage.h"
+#include "mail-types.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-flagsclear.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "FlagsClear"
+
+static struct push_notification_event_flagsclear_config default_config;
+
+
+static void *push_notification_event_flagsclear_default_config(void)
+{
+ i_zero(&default_config);
+
+ return &default_config;
+}
+
+static void
+push_notification_event_flagsclear_debug_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_flagsclear_data *data = event->data;
+ const char *keyword;
+
+ if ((data->flags_clear & MAIL_ANSWERED) != 0)
+ i_debug("%s: Answered flag cleared", EVENT_NAME);
+ if ((data->flags_clear & MAIL_FLAGGED) != 0)
+ i_debug("%s: Flagged flag cleared", EVENT_NAME);
+ if ((data->flags_clear & MAIL_DELETED) != 0)
+ i_debug("%s: Deleted flag cleared", EVENT_NAME);
+ if ((data->flags_clear & MAIL_SEEN) != 0)
+ i_debug("%s: Seen flag cleared", EVENT_NAME);
+ if ((data->flags_clear & MAIL_DRAFT) != 0)
+ i_debug("%s: Draft flag cleared", EVENT_NAME);
+
+ array_foreach_elem(&data->keywords_clear, keyword)
+ i_debug("%s: Keyword clear [%s]", EVENT_NAME, keyword);
+}
+
+static struct push_notification_event_flagsclear_data *
+push_notification_event_flagsclear_get_data(
+ struct push_notification_txn *ptxn,
+ struct push_notification_txn_msg *msg,
+ struct push_notification_event_config *ec)
+{
+ struct push_notification_event_flagsclear_config *config =
+ (struct push_notification_event_flagsclear_config *)ec->config;
+ struct push_notification_event_flagsclear_data *data;
+
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if (data == NULL) {
+ data = p_new(ptxn->pool,
+ struct push_notification_event_flagsclear_data, 1);
+ data->flags_clear = 0;
+ data->flags_old = 0;
+ p_array_init(&data->keywords_clear, ptxn->pool, 4);
+ if (config->store_old == TRUE)
+ p_array_init(&data->keywords_old, ptxn->pool, 4);
+
+ push_notification_txn_msg_set_eventdata(ptxn, msg, ec, data);
+ }
+
+ return data;
+}
+
+static void
+push_notification_event_flagsclear_flags_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct push_notification_event_flagsclear_config *config =
+ (struct push_notification_event_flagsclear_config *)ec->config;
+ struct push_notification_event_flagsclear_data *data;
+ enum mail_flags flag_check_always[] = {
+ MAIL_ANSWERED,
+ MAIL_DELETED,
+ MAIL_DRAFT,
+ MAIL_FLAGGED,
+ MAIL_SEEN
+ };
+ enum mail_flags flags;
+ unsigned int i;
+
+ data = push_notification_event_flagsclear_get_data(ptxn, msg, ec);
+ flags = mail_get_flags(mail);
+
+ for (i = 0; i < N_ELEMENTS(flag_check_always); i++) {
+ if ((flags & flag_check_always[i]) == 0 &&
+ (old_flags & flag_check_always[i]) != 0)
+ data->flags_clear |= flag_check_always[i];
+ }
+
+ if (config->store_old == TRUE)
+ data->flags_old = old_flags;
+}
+
+static void
+push_notification_event_flagsclear_keywords_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ const char *const *old_keywords)
+{
+ struct push_notification_event_flagsclear_config *config =
+ (struct push_notification_event_flagsclear_config *)ec->config;
+ struct push_notification_event_flagsclear_data *data;
+ const char *const *keywords, *const *kp, *ok;
+
+ data = push_notification_event_flagsclear_get_data(ptxn, msg, ec);
+ keywords = mail_get_keywords(mail);
+
+ for (; *old_keywords != NULL; old_keywords++) {
+ for (kp = keywords; *kp != NULL; kp++) {
+ if (strcmp(*old_keywords, *kp) == 0)
+ break;
+ }
+
+ if (*kp == NULL) {
+ ok = p_strdup(ptxn->pool, *old_keywords);
+ array_push_back(&data->keywords_clear, &ok);
+ }
+
+ if (config->store_old == TRUE) {
+ ok = p_strdup(ptxn->pool, *old_keywords);
+ array_push_back(&data->keywords_old, &ok);
+ }
+ }
+}
+
+static void
+push_notification_event_flagsclear_free_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_flagsclear_data *data = event->data;
+
+ if (array_is_created(&data->keywords_clear))
+ array_free(&data->keywords_clear);
+ if (array_is_created(&data->keywords_old))
+ array_free(&data->keywords_old);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_flagsclear;
+
+struct push_notification_event push_notification_event_flagsclear = {
+ .name = EVENT_NAME,
+ .init = {
+ .default_config =
+ push_notification_event_flagsclear_default_config,
+ },
+ .msg = {
+ .debug_msg = push_notification_event_flagsclear_debug_msg,
+ .free_msg = push_notification_event_flagsclear_free_msg,
+ },
+ .msg_triggers = {
+ .flagchange = push_notification_event_flagsclear_flags_event,
+ .keywordchange =
+ push_notification_event_flagsclear_keywords_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-flagsclear.h b/src/plugins/push-notification/push-notification-event-flagsclear.h
new file mode 100644
index 0000000..5412335
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-flagsclear.h
@@ -0,0 +1,22 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_FLAGSCLEAR_H
+#define PUSH_NOTIFICATION_EVENT_FLAGSCLEAR_H
+
+#include "mail-types.h"
+
+struct push_notification_event_flagsclear_config {
+ /* Store the old flags/keywords? */
+ bool store_old;
+};
+
+struct push_notification_event_flagsclear_data {
+ enum mail_flags flags_clear;
+ ARRAY_TYPE(keywords) keywords_clear;
+
+ enum mail_flags flags_old;
+ ARRAY_TYPE(keywords) keywords_old;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-flagsset.c b/src/plugins/push-notification/push-notification-event-flagsset.c
new file mode 100644
index 0000000..c596ced
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-flagsset.c
@@ -0,0 +1,167 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage.h"
+#include "mail-types.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-flagsset.h"
+#include "push-notification-txn-msg.h"
+
+
+#define EVENT_NAME "FlagsSet"
+
+static struct push_notification_event_flagsset_config default_config;
+
+
+static void *push_notification_event_flagsset_default_config(void)
+{
+ i_zero(&default_config);
+
+ return &default_config;
+}
+
+static void
+push_notification_event_flagsset_debug_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_flagsset_data *data = event->data;
+ const char *keyword;
+
+ if ((data->flags_set & MAIL_ANSWERED) != 0)
+ i_debug("%s: Answered flag set", EVENT_NAME);
+ if ((data->flags_set & MAIL_FLAGGED) != 0)
+ i_debug("%s: Flagged flag set", EVENT_NAME);
+ if ((data->flags_set & MAIL_DELETED) != 0)
+ i_debug("%s: Deleted flag set", EVENT_NAME);
+ if ((data->flags_set & MAIL_SEEN) != 0)
+ i_debug("%s: Seen flag set", EVENT_NAME);
+ if ((data->flags_set & MAIL_DRAFT) != 0)
+ i_debug("%s: Draft flag set", EVENT_NAME);
+
+ array_foreach_elem(&data->keywords_set, keyword)
+ i_debug("%s: Keyword set [%s]", EVENT_NAME, keyword);
+}
+
+static struct push_notification_event_flagsset_data *
+push_notification_event_flagsset_get_data(
+ struct push_notification_txn *ptxn,
+ struct push_notification_txn_msg *msg,
+ struct push_notification_event_config *ec)
+{
+ struct push_notification_event_flagsset_data *data;
+
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if (data == NULL) {
+ data = p_new(ptxn->pool,
+ struct push_notification_event_flagsset_data, 1);
+ data->flags_set = 0;
+ p_array_init(&data->keywords_set, ptxn->pool, 4);
+
+ push_notification_txn_msg_set_eventdata(ptxn, msg, ec, data);
+ }
+
+ return data;
+}
+
+static void
+push_notification_event_flagsset_flags_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct push_notification_event_flagsset_config *config =
+ (struct push_notification_event_flagsset_config *)ec->config;
+ struct push_notification_event_flagsset_data *data;
+ enum mail_flags flag_check_always[] = {
+ MAIL_ANSWERED,
+ MAIL_DRAFT,
+ MAIL_FLAGGED
+ };
+ enum mail_flags flags, flags_set = 0;
+ unsigned int i;
+
+ flags = mail_get_flags(mail);
+
+ for (i = 0; i < N_ELEMENTS(flag_check_always); i++) {
+ if ((flags & flag_check_always[i]) != 0 &&
+ (old_flags & flag_check_always[i]) == 0)
+ flags_set |= flag_check_always[i];
+ }
+
+ if (!config->hide_deleted && (flags & MAIL_DELETED) != 0 &&
+ (old_flags & MAIL_DELETED) == 0) {
+ flags_set |= MAIL_DELETED;
+ }
+
+ if (!config->hide_seen && (flags & MAIL_SEEN) != 0 &&
+ (old_flags & MAIL_SEEN) == 0) {
+ flags_set |= MAIL_SEEN;
+ }
+
+ /* Only create data element if at least one flag was set. */
+ if (flags_set != 0) {
+ data = push_notification_event_flagsset_get_data(ptxn, msg, ec);
+ data->flags_set |= flags_set;
+ }
+}
+
+static void
+push_notification_event_flagsset_keywords_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ const char *const *old_keywords)
+{
+ struct push_notification_event_flagsset_data *data;
+ const char *k, *const *keywords, *const *op;
+
+ data = push_notification_event_flagsset_get_data(ptxn, msg, ec);
+ keywords = mail_get_keywords(mail);
+
+ for (; *keywords != NULL; keywords++) {
+ for (op = old_keywords; *op != NULL; op++) {
+ if (strcmp(*keywords, *op) == 0)
+ break;
+ }
+
+ if (*op == NULL) {
+ k = p_strdup(ptxn->pool, *keywords);
+ array_push_back(&data->keywords_set, &k);
+ }
+ }
+}
+
+static void
+push_notification_event_flagsset_free_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_flagsset_data *data = event->data;
+
+ if (array_is_created(&data->keywords_set))
+ array_free(&data->keywords_set);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_flagsset;
+
+struct push_notification_event push_notification_event_flagsset = {
+ .name = EVENT_NAME,
+ .init = {
+ .default_config =
+ push_notification_event_flagsset_default_config,
+ },
+ .msg = {
+ .debug_msg = push_notification_event_flagsset_debug_msg,
+ .free_msg = push_notification_event_flagsset_free_msg,
+ },
+ .msg_triggers = {
+ .flagchange = push_notification_event_flagsset_flags_event,
+ .keywordchange =
+ push_notification_event_flagsset_keywords_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-flagsset.h b/src/plugins/push-notification/push-notification-event-flagsset.h
new file mode 100644
index 0000000..4830260
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-flagsset.h
@@ -0,0 +1,22 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_FLAGSSET_H
+#define PUSH_NOTIFICATION_EVENT_FLAGSSET_H
+
+#include "mail-types.h"
+
+struct push_notification_event_flagsset_config {
+ /* RFC 5423[4.2] - allow configuration whether FlagsSet event returns
+ Deleted and/or Seen flags, since these flags are also settable via
+ MessageRead/MessageTrash events. By default, include them here. */
+ bool hide_deleted;
+ bool hide_seen;
+};
+
+struct push_notification_event_flagsset_data {
+ enum mail_flags flags_set;
+ ARRAY_TYPE(keywords) keywords_set;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-mailboxcreate.c b/src/plugins/push-notification/push-notification-event-mailboxcreate.c
new file mode 100644
index 0000000..87e46c1
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxcreate.c
@@ -0,0 +1,56 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-mailboxcreate.h"
+#include "push-notification-txn-mbox.h"
+
+#define EVENT_NAME "MailboxCreate"
+
+static void
+push_notification_event_mailboxcreate_debug_mbox(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Mailbox was created", EVENT_NAME);
+}
+
+static void
+push_notification_event_mailboxcreate_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_mailboxcreate_data *data;
+ struct mailbox_status status;
+
+ if (mailbox_get_status(ptxn->mbox, STATUS_UIDVALIDITY, &status) < 0) {
+ i_error(EVENT_NAME
+ "Failed to get created mailbox '%s' uidvalidity: %s",
+ mailbox_get_vname(ptxn->mbox),
+ mailbox_get_last_internal_error(ptxn->mbox, NULL));
+ status.uidvalidity = 0;
+ }
+
+ data = p_new(ptxn->pool,
+ struct push_notification_event_mailboxcreate_data, 1);
+ data->uid_validity = status.uidvalidity;
+
+ push_notification_txn_mbox_set_eventdata(ptxn, mbox, ec, data);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_mailboxcreate;
+
+struct push_notification_event push_notification_event_mailboxcreate = {
+ .name = EVENT_NAME,
+ .mbox = {
+ .debug_mbox = push_notification_event_mailboxcreate_debug_mbox,
+ },
+ .mbox_triggers = {
+ .create = push_notification_event_mailboxcreate_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-mailboxcreate.h b/src/plugins/push-notification/push-notification-event-mailboxcreate.h
new file mode 100644
index 0000000..9a5e1bf
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxcreate.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MAILBOXCREATE_H
+#define PUSH_NOTIFICATION_EVENT_MAILBOXCREATE_H
+
+struct push_notification_event_mailboxcreate_data {
+ /* RFC 5423 [4.4]: UIDVALIDITY required for create event. */
+ uint32_t uid_validity;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-mailboxdelete.c b/src/plugins/push-notification/push-notification-event-mailboxdelete.c
new file mode 100644
index 0000000..8e65f3c
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxdelete.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-mailboxdelete.h"
+#include "push-notification-txn-mbox.h"
+
+#define EVENT_NAME "MailboxDelete"
+
+static void
+push_notification_event_mailboxdelete_debug_mbox(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Mailbox was deleted", EVENT_NAME);
+}
+
+static void
+push_notification_event_mailboxdelete_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_mailboxdelete_data *data;
+
+ data = p_new(ptxn->pool,
+ struct push_notification_event_mailboxdelete_data, 1);
+ data->deleted = TRUE;
+
+ push_notification_txn_mbox_set_eventdata(ptxn, mbox, ec, data);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_mailboxdelete;
+
+struct push_notification_event push_notification_event_mailboxdelete = {
+ .name = EVENT_NAME,
+ .mbox = {
+ .debug_mbox = push_notification_event_mailboxdelete_debug_mbox,
+ },
+ .mbox_triggers = {
+ .delete = push_notification_event_mailboxdelete_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-mailboxdelete.h b/src/plugins/push-notification/push-notification-event-mailboxdelete.h
new file mode 100644
index 0000000..ac36747
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxdelete.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MAILBOXDELETE_H
+#define PUSH_NOTIFICATION_EVENT_MAILBOXDELETE_H
+
+struct push_notification_event_mailboxdelete_data {
+ /* Can only be TRUE. */
+ bool deleted;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-mailboxrename.c b/src/plugins/push-notification/push-notification-event-mailboxrename.c
new file mode 100644
index 0000000..e4c2cae
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxrename.c
@@ -0,0 +1,50 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-mailboxrename.h"
+#include "push-notification-txn-mbox.h"
+
+#define EVENT_NAME "MailboxRename"
+
+static void
+push_notification_event_mailboxrename_debug_mbox(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_mailboxrename_data *data = event->data;
+
+ i_debug("%s: Mailbox was renamed (old name: %s)",
+ EVENT_NAME, data->old_mbox);
+}
+
+static void
+push_notification_event_mailboxrename_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox, struct mailbox *old)
+{
+ struct push_notification_event_mailboxrename_data *data;
+
+ data = p_new(ptxn->pool,
+ struct push_notification_event_mailboxrename_data, 1);
+ data->old_mbox = mailbox_get_vname(old);
+
+ push_notification_txn_mbox_set_eventdata(ptxn, mbox, ec, data);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_mailboxrename;
+
+struct push_notification_event push_notification_event_mailboxrename = {
+ .name = EVENT_NAME,
+ .mbox = {
+ .debug_mbox = push_notification_event_mailboxrename_debug_mbox,
+ },
+ .mbox_triggers = {
+ .rename = push_notification_event_mailboxrename_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-mailboxrename.h b/src/plugins/push-notification/push-notification-event-mailboxrename.h
new file mode 100644
index 0000000..66e21f8
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxrename.h
@@ -0,0 +1,11 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MAILBOXRENAME_H
+#define PUSH_NOTIFICATION_EVENT_MAILBOXRENAME_H
+
+struct push_notification_event_mailboxrename_data {
+ const char *old_mbox;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-mailboxsubscribe.c b/src/plugins/push-notification/push-notification-event-mailboxsubscribe.c
new file mode 100644
index 0000000..0bcd101
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxsubscribe.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-mailboxsubscribe.h"
+#include "push-notification-txn-mbox.h"
+
+#define EVENT_NAME "MailboxSubscribe"
+
+static void
+push_notification_event_mailboxsubscribe_debug_mbox(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Mailbox was subscribed to", EVENT_NAME);
+}
+
+static void
+push_notification_event_mailboxsubscribe_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_mailboxsubscribe_data *data;
+
+ data = p_new(ptxn->pool,
+ struct push_notification_event_mailboxsubscribe_data, 1);
+ data->subscribe = TRUE;
+
+ push_notification_txn_mbox_set_eventdata(ptxn, mbox, ec, data);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_mailboxsubscribe;
+
+struct push_notification_event push_notification_event_mailboxsubscribe = {
+ .name = EVENT_NAME,
+ .mbox = {
+ .debug_mbox =
+ push_notification_event_mailboxsubscribe_debug_mbox,
+ },
+ .mbox_triggers = {
+ .subscribe = push_notification_event_mailboxsubscribe_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-mailboxsubscribe.h b/src/plugins/push-notification/push-notification-event-mailboxsubscribe.h
new file mode 100644
index 0000000..d278482
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxsubscribe.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MAILBOXSUBSCRIBE_H
+#define PUSH_NOTIFICATION_EVENT_MAILBOXSUBSCRIBE_H
+
+struct push_notification_event_mailboxsubscribe_data {
+ /* Can only be TRUE. */
+ bool subscribe;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.c b/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.c
new file mode 100644
index 0000000..b8d078f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-mailboxunsubscribe.h"
+#include "push-notification-txn-mbox.h"
+
+#define EVENT_NAME "MailboxUnsubscribe"
+
+static void
+push_notification_event_mailboxunsubscribe_debug_mbox(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Mailbox was subscribed to", EVENT_NAME);
+}
+
+static void
+push_notification_event_mailboxunsubscribe_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_mailboxunsubscribe_data *data;
+
+ data = p_new(ptxn->pool,
+ struct push_notification_event_mailboxunsubscribe_data, 1);
+ data->subscribe = FALSE;
+
+ push_notification_txn_mbox_set_eventdata(ptxn, mbox, ec, data);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_mailboxunsubscribe;
+
+struct push_notification_event push_notification_event_mailboxunsubscribe = {
+ .name = EVENT_NAME,
+ .mbox = {
+ .debug_mbox =
+ push_notification_event_mailboxunsubscribe_debug_mbox,
+ },
+ .mbox_triggers = {
+ .unsubscribe = push_notification_event_mailboxunsubscribe_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.h b/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.h
new file mode 100644
index 0000000..c519da2
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-mailboxunsubscribe.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MAILBOXUNSUBSCRIBE_H
+#define PUSH_NOTIFICATION_EVENT_MAILBOXUNSUBSCRIBE_H
+
+struct push_notification_event_mailboxunsubscribe_data {
+ /* Can only be FALSE. */
+ bool subscribe;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-message-common.c b/src/plugins/push-notification/push-notification-event-message-common.c
new file mode 100644
index 0000000..7965039
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-message-common.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2015-2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "message-address.h"
+#include "message-header-decode.h"
+#include "mail-storage.h"
+#include "push-notification-event-message-common.h"
+
+static void
+decode_address_header(pool_t pool, const char *hdr,
+ const char **address_r, const char **name_r)
+{
+ struct message_address *addr;
+ const char *display_name;
+
+ if (hdr == NULL)
+ return;
+
+ addr = message_address_parse(pool_datastack_create(),
+ (const unsigned char *)hdr, strlen(hdr), 1, 0);
+ if (addr == NULL)
+ return;
+
+ display_name = addr->name;
+ if (addr->domain == NULL) {
+ /* group */
+ display_name = addr->mailbox;
+ } else if (addr->domain[0] != '\0')
+ *address_r = p_strdup_printf(pool, "%s@%s", addr->mailbox,
+ addr->domain);
+ else if (addr->mailbox != NULL && addr->mailbox[0] != '\0')
+ *address_r = p_strdup(pool, addr->mailbox);
+
+ if (display_name != NULL && display_name[0] != '\0') {
+ string_t *name_utf8 = t_str_new(128);
+
+ message_header_decode_utf8((const unsigned char *)display_name,
+ strlen(display_name), name_utf8, NULL);
+ *name_r = p_strdup(pool, str_c(name_utf8));
+ }
+}
+
+void push_notification_message_fill(
+ struct mail *mail, pool_t pool,
+ enum push_notification_event_message_flags event_flags,
+ const char **from, const char **to, const char **subject, time_t *date,
+ int *date_tz, const char **message_id, enum mail_flags *flags,
+ bool *flags_set, const char *const **keywords, const char **snippet,
+ struct push_notification_message_ext *ext)
+{
+ const char *value;
+ time_t tmp_date;
+ int tmp_tz;
+
+ if ((*from == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_HDR_FROM) != 0 &&
+ (mail_get_first_header(mail, "From", &value) >= 0)) {
+ *from = p_strdup(pool, value);
+ decode_address_header(pool, value, &ext->from_address,
+ &ext->from_display_name_utf8);
+ }
+
+ if ((*to == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_HDR_TO) != 0 &&
+ (mail_get_first_header(mail, "To", &value) >= 0)) {
+ *to = p_strdup(pool, value);
+ decode_address_header(pool, value, &ext->to_address,
+ &ext->to_display_name_utf8);
+ }
+
+ if ((*subject == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT) != 0 &&
+ (mail_get_first_header(mail, "Subject", &value) >= 0)) {
+ string_t *subject_utf8 = t_str_new(128);
+
+ *subject = p_strdup(pool, value);
+ if (value != NULL) {
+ message_header_decode_utf8(
+ (const unsigned char *)value,
+ strlen(value), subject_utf8, NULL);
+ ext->subject_utf8 = p_strdup(pool, str_c(subject_utf8));
+ }
+ }
+
+ if ((*date == -1) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_HDR_DATE) != 0 &&
+ (mail_get_date(mail, &tmp_date, &tmp_tz) >= 0)) {
+ *date = tmp_date;
+ *date_tz = tmp_tz;
+ }
+
+ if ((*message_id == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_HDR_MESSAGE_ID) != 0 &&
+ (mail_get_first_header(mail, "Message-ID", &value) >= 0)) {
+ *message_id = p_strdup(pool, value);
+ }
+
+ if (!*flags_set &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_FLAGS) != 0) {
+ *flags = mail_get_flags(mail);
+ *flags_set = TRUE;
+ }
+
+ if ((*keywords == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_KEYWORDS) != 0) {
+ *keywords = p_strarray_dup(pool, mail_get_keywords(mail));
+ }
+
+ if ((*snippet == NULL) &&
+ (event_flags & PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET) != 0 &&
+ (mail_get_special(mail, MAIL_FETCH_BODY_SNIPPET, &value) >= 0)) {
+ /* [0] contains the snippet algorithm, skip over it */
+ i_assert(value[0] != '\0');
+ *snippet = p_strdup(pool, value + 1);
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-event-message-common.h b/src/plugins/push-notification/push-notification-event-message-common.h
new file mode 100644
index 0000000..48c37de
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-message-common.h
@@ -0,0 +1,40 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGE_COMMON_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGE_COMMON_H
+
+enum push_notification_event_message_flags {
+ /* Header: From */
+ PUSH_NOTIFICATION_MESSAGE_HDR_FROM = 0x01,
+ /* Header: To */
+ PUSH_NOTIFICATION_MESSAGE_HDR_TO = 0x02,
+ /* Header: Subject */
+ PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT = 0x04,
+ /* Header: Date */
+ PUSH_NOTIFICATION_MESSAGE_HDR_DATE = 0x08,
+ /* Body: Snippet */
+ PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET = 0x10,
+ /* Meta: Flags */
+ PUSH_NOTIFICATION_MESSAGE_FLAGS = 0x20,
+ /* Meta: Keywords */
+ PUSH_NOTIFICATION_MESSAGE_KEYWORDS = 0x40,
+ /* Header: Message-ID */
+ PUSH_NOTIFICATION_MESSAGE_HDR_MESSAGE_ID = 0x80,
+};
+
+struct push_notification_message_ext {
+ const char *from_address, *from_display_name_utf8;
+ const char *to_address, *to_display_name_utf8;
+ const char *subject_utf8;
+};
+
+void push_notification_message_fill(
+ struct mail *mail, pool_t pool,
+ enum push_notification_event_message_flags event_flags,
+ const char **from, const char **to, const char **subject, time_t *date,
+ int *date_tz, const char **message_id, enum mail_flags *flags,
+ bool *flags_set, const char *const **keywords, const char **snippet,
+ struct push_notification_message_ext *ext);
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-messageappend.c b/src/plugins/push-notification/push-notification-event-messageappend.c
new file mode 100644
index 0000000..b6e0318
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageappend.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "istream.h"
+#include "iso8601-date.h"
+#include "mail-storage.h"
+
+#include <time.h>
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-message-common.h"
+#include "push-notification-event-messageappend.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "MessageAppend"
+
+static struct push_notification_event_messageappend_config default_config;
+
+static void *push_notification_event_messageappend_default_config(void)
+{
+ i_zero(&default_config);
+
+ return &default_config;
+}
+
+static void
+push_notification_event_messageappend_debug_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_messageappend_data *data = event->data;
+ struct tm *tm;
+
+ if (data->date != -1) {
+ tm = gmtime(&data->date);
+ i_debug("%s: Date [%s]", EVENT_NAME,
+ iso8601_date_create_tm(tm, data->date_tz));
+ }
+
+ if (data->from != NULL)
+ i_debug("%s: From [%s]", EVENT_NAME, data->from);
+ if (data->snippet != NULL)
+ i_debug("%s: Snippet [%s]", EVENT_NAME, data->snippet);
+ if (data->subject != NULL)
+ i_debug("%s: Subject [%s]", EVENT_NAME, data->subject);
+ if (data->to != NULL)
+ i_debug("%s: To [%s]", EVENT_NAME, data->to);
+}
+
+static void
+push_notification_event_messageappend_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail)
+{
+ struct push_notification_event_messageappend_config *config =
+ (struct push_notification_event_messageappend_config *)
+ ec->config;
+ struct push_notification_event_messageappend_data *data;
+
+ if (config->flags == 0)
+ return;
+
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if (data == NULL) {
+ data = p_new(ptxn->pool,
+ struct push_notification_event_messageappend_data, 1);
+ data->date = -1;
+ push_notification_txn_msg_set_eventdata(ptxn, msg, ec, data);
+ }
+
+ push_notification_message_fill(mail, ptxn->pool, config->flags,
+ &data->from, &data->to, &data->subject,
+ &data->date, &data->date_tz,
+ &data->message_id,
+ &data->flags, &data->flags_set,
+ &data->keywords,
+ &data->snippet, &data->ext);
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_messageappend;
+
+struct push_notification_event push_notification_event_messageappend = {
+ .name = EVENT_NAME,
+ .init = {
+ .default_config =
+ push_notification_event_messageappend_default_config,
+ },
+ .msg = {
+ .debug_msg = push_notification_event_messageappend_debug_msg,
+ },
+ .msg_triggers = {
+ .append = push_notification_event_messageappend_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-messageappend.h b/src/plugins/push-notification/push-notification-event-messageappend.h
new file mode 100644
index 0000000..511fc6b
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageappend.h
@@ -0,0 +1,30 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGEAPPEND_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGEAPPEND_H
+
+struct push_notification_event_messageappend_config {
+ enum push_notification_event_message_flags flags;
+};
+
+struct push_notification_event_messageappend_data {
+ const char *from;
+ const char *to;
+ const char *subject;
+ const char *snippet;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_DATE */
+ time_t date;
+ int date_tz;
+ /* PUSH_NOTIFICATION_MESSAGE_FLAGS */
+ bool flags_set;
+ enum mail_flags flags;
+ /* PUSH_NOTIFICATION_MESSAGE_KEYWORDS */
+ const char *const *keywords;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_MESSAGE_ID */
+ const char *message_id;
+
+ struct push_notification_message_ext ext;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-messageexpunge.c b/src/plugins/push-notification/push-notification-event-messageexpunge.c
new file mode 100644
index 0000000..cfa301e
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageexpunge.c
@@ -0,0 +1,53 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "mail-types.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-event-messageexpunge.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "MessageExpunge"
+
+static void
+push_notification_event_messageexpunge_debug_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_messageexpunge_data *data = event->data;
+
+ if (data != NULL)
+ i_debug("%s: Message was expunged", EVENT_NAME);
+}
+
+static void
+push_notification_event_messageexpunge_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_event_messageexpunge_data *data;
+
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if (data == NULL) {
+ data = p_new(ptxn->pool,
+ struct push_notification_event_messageexpunge_data, 1);
+ data->expunge = TRUE;
+ push_notification_txn_msg_set_eventdata(ptxn, msg, ec, data);
+ }
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_messageexpunge;
+
+struct push_notification_event push_notification_event_messageexpunge = {
+ .name = EVENT_NAME,
+ .msg = {
+ .debug_msg = push_notification_event_messageexpunge_debug_msg,
+ },
+ .msg_triggers = {
+ .expunge = push_notification_event_messageexpunge_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-messageexpunge.h b/src/plugins/push-notification/push-notification-event-messageexpunge.h
new file mode 100644
index 0000000..bc20f73
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageexpunge.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGEEXPUNGE_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGEEXPUNGE_H
+
+struct push_notification_event_messageexpunge_data {
+ /* Can only be TRUE. */
+ bool expunge;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-messagenew.c b/src/plugins/push-notification/push-notification-event-messagenew.c
new file mode 100644
index 0000000..930c0ee
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messagenew.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "iso8601-date.h"
+#include "istream.h"
+#include "mail-storage.h"
+
+#include <time.h>
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-message-common.h"
+#include "push-notification-event-messagenew.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "MessageNew"
+
+static struct push_notification_event_messagenew_config default_config;
+
+static void *push_notification_event_messagenew_default_config(void)
+{
+ i_zero(&default_config);
+
+ return &default_config;
+}
+
+static void
+push_notification_event_messagenew_debug_msg(
+ struct push_notification_txn_event *event)
+{
+ struct push_notification_event_messagenew_data *data = event->data;
+ struct tm *tm;
+
+ if (data->date != -1) {
+ tm = gmtime(&data->date);
+ i_debug("%s: Date [%s]", EVENT_NAME,
+ iso8601_date_create_tm(tm, data->date_tz));
+ }
+
+ if (data->from != NULL)
+ i_debug("%s: From [%s]", EVENT_NAME, data->from);
+ if (data->snippet != NULL)
+ i_debug("%s: Snippet [%s]", EVENT_NAME, data->snippet);
+ if (data->subject != NULL)
+ i_debug("%s: Subject [%s]", EVENT_NAME, data->subject);
+ if (data->to != NULL)
+ i_debug("%s: To [%s]", EVENT_NAME, data->to);
+}
+
+static void
+push_notification_event_messagenew_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail)
+{
+ struct push_notification_event_messagenew_config *config =
+ (struct push_notification_event_messagenew_config *)ec->config;
+ struct push_notification_event_messagenew_data *data;
+
+ if (config->flags == 0)
+ return;
+
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if (data == NULL) {
+ data = p_new(ptxn->pool,
+ struct push_notification_event_messagenew_data, 1);
+ data->date = -1;
+
+ push_notification_txn_msg_set_eventdata(ptxn, msg, ec, data);
+ }
+
+ push_notification_message_fill(mail, ptxn->pool, config->flags,
+ &data->from, &data->to, &data->subject,
+ &data->date, &data->date_tz,
+ &data->message_id,
+ &data->flags, &data->flags_set,
+ &data->keywords,
+ &data->snippet, &data->ext);
+}
+
+/* Event definition */
+
+struct push_notification_event push_notification_event_messagenew = {
+ .name = EVENT_NAME,
+ .init = {
+ .default_config =
+ push_notification_event_messagenew_default_config,
+ },
+ .msg = {
+ .debug_msg = push_notification_event_messagenew_debug_msg,
+ },
+ .msg_triggers = {
+ .save = push_notification_event_messagenew_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-messagenew.h b/src/plugins/push-notification/push-notification-event-messagenew.h
new file mode 100644
index 0000000..a939668
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messagenew.h
@@ -0,0 +1,36 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGENEW_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGENEW_H
+
+#include "push-notification-event-message-common.h"
+
+struct push_notification_event_messagenew_config {
+ enum push_notification_event_message_flags flags;
+};
+
+struct push_notification_event_messagenew_data {
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_FROM */
+ const char *from;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_TO */
+ const char *to;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT */
+ const char *subject;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_DATE */
+ time_t date;
+ int date_tz;
+ /* PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET */
+ const char *snippet;
+ /* PUSH_NOTIFICATION_MESSAGE_FLAGS */
+ bool flags_set;
+ enum mail_flags flags;
+ /* PUSH_NOTIFICATION_MESSAGE_KEYWORDS */
+ const char *const *keywords;
+ /* PUSH_NOTIFICATION_MESSAGE_HDR_MESSAGE_ID */
+ const char *message_id;
+
+ struct push_notification_message_ext ext;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-event-messageread.c b/src/plugins/push-notification/push-notification-event-messageread.c
new file mode 100644
index 0000000..8f0d709
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageread.c
@@ -0,0 +1,58 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "mail-types.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-messageread.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "MessageRead"
+
+static void
+push_notification_event_messageread_debug_msg(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Message was flagged as seen", EVENT_NAME);
+}
+
+static void
+push_notification_event_messageread_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct push_notification_event_messageread_data *data;
+ enum mail_flags flags;
+
+ /* If data struct exists, that means the read flag was changed. */
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if ((data == NULL) && (old_flags & MAIL_SEEN) == 0) {
+ flags = mail_get_flags(mail);
+ if ((flags & MAIL_SEEN) != 0) {
+ data = p_new(
+ ptxn->pool,
+ struct push_notification_event_messageread_data, 1);
+ data->read = TRUE;
+ push_notification_txn_msg_set_eventdata(
+ ptxn, msg, ec, data);
+ }
+ }
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_messageread;
+
+struct push_notification_event push_notification_event_messageread = {
+ .name = EVENT_NAME,
+ .msg = {
+ .debug_msg = push_notification_event_messageread_debug_msg,
+ },
+ .msg_triggers = {
+ .flagchange = push_notification_event_messageread_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-messageread.h b/src/plugins/push-notification/push-notification-event-messageread.h
new file mode 100644
index 0000000..6eb5c18
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messageread.h
@@ -0,0 +1,11 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGEREAD_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGEREAD_H
+
+struct push_notification_event_messageread_data {
+ /* Can only be TRUE. */
+ bool read;
+};
+
+#endif
diff --git a/src/plugins/push-notification/push-notification-event-messagetrash.c b/src/plugins/push-notification/push-notification-event-messagetrash.c
new file mode 100644
index 0000000..2284b6f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messagetrash.c
@@ -0,0 +1,58 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "mail-types.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-event-messagetrash.h"
+#include "push-notification-txn-msg.h"
+
+#define EVENT_NAME "MessageTrash"
+
+static void
+push_notification_event_messagetrash_debug_msg(
+ struct push_notification_txn_event *event ATTR_UNUSED)
+{
+ i_debug("%s: Message was marked as deleted", EVENT_NAME);
+}
+
+static void
+push_notification_event_messagetrash_event(
+ struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct push_notification_event_messagetrash_data *data;
+ enum mail_flags flags;
+
+ /* If data struct exists, that means the deleted flag was changed. */
+ data = push_notification_txn_msg_get_eventdata(msg, EVENT_NAME);
+ if ((data == NULL) && (old_flags & MAIL_DELETED) == 0) {
+ flags = mail_get_flags(mail);
+ if ((flags & MAIL_DELETED) != 0) {
+ data = p_new(
+ ptxn->pool,
+ struct push_notification_event_messagetrash_data, 1);
+ data->trash = TRUE;
+ push_notification_txn_msg_set_eventdata(
+ ptxn, msg, ec, data);
+ }
+ }
+}
+
+/* Event definition */
+
+extern struct push_notification_event push_notification_event_messagetrash;
+
+struct push_notification_event push_notification_event_messagetrash = {
+ .name = EVENT_NAME,
+ .msg = {
+ .debug_msg = push_notification_event_messagetrash_debug_msg,
+ },
+ .msg_triggers = {
+ .flagchange = push_notification_event_messagetrash_event,
+ },
+};
diff --git a/src/plugins/push-notification/push-notification-event-messagetrash.h b/src/plugins/push-notification/push-notification-event-messagetrash.h
new file mode 100644
index 0000000..712732f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-event-messagetrash.h
@@ -0,0 +1,12 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENT_MESSAGETRASH_H
+#define PUSH_NOTIFICATION_EVENT_MESSAGETRASH_H
+
+struct push_notification_event_messagetrash_data {
+ /* Can only be TRUE. */
+ bool trash;
+};
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-events-rfc5423.c b/src/plugins/push-notification/push-notification-events-rfc5423.c
new file mode 100644
index 0000000..10dd0c7
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-events-rfc5423.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "push-notification-events.h"
+#include "push-notification-events-rfc5423.h"
+
+/* These are the RFC 5423 Mail Store Events currently handled within the core
+ push-notification code.
+
+ @todo: These events are not currently handled:
+ - Login
+ - Logout
+ - QuotaExceed
+ - Quota Within
+ */
+extern struct push_notification_event push_notification_event_flagsclear;
+extern struct push_notification_event push_notification_event_flagsset;
+extern struct push_notification_event push_notification_event_mailboxcreate;
+extern struct push_notification_event push_notification_event_mailboxdelete;
+extern struct push_notification_event push_notification_event_mailboxrename;
+extern struct push_notification_event push_notification_event_mailboxsubscribe;
+extern struct push_notification_event push_notification_event_mailboxunsubscribe;
+extern struct push_notification_event push_notification_event_messageappend;
+extern struct push_notification_event push_notification_event_messageexpunge;
+extern struct push_notification_event push_notification_event_messagenew;
+extern struct push_notification_event push_notification_event_messageread;
+extern struct push_notification_event push_notification_event_messagetrash;
+
+static struct push_notification_event *rfc5423_events[] = {
+ &push_notification_event_flagsclear,
+ &push_notification_event_flagsset,
+ &push_notification_event_mailboxcreate,
+ &push_notification_event_mailboxdelete,
+ &push_notification_event_mailboxrename,
+ &push_notification_event_mailboxsubscribe,
+ &push_notification_event_mailboxunsubscribe,
+ &push_notification_event_messageappend,
+ &push_notification_event_messageexpunge,
+ &push_notification_event_messagenew,
+ &push_notification_event_messageread,
+ &push_notification_event_messagetrash
+};
+
+void push_notification_event_register_rfc5423_events(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(rfc5423_events); i++)
+ push_notification_event_register(rfc5423_events[i]);
+}
+
+void push_notification_event_unregister_rfc5423_events(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(rfc5423_events); i++)
+ push_notification_event_unregister(rfc5423_events[i]);
+}
diff --git a/src/plugins/push-notification/push-notification-events-rfc5423.h b/src/plugins/push-notification/push-notification-events-rfc5423.h
new file mode 100644
index 0000000..71f7009
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-events-rfc5423.h
@@ -0,0 +1,10 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENTS_RFC5423_H
+#define PUSH_NOTIFICATION_EVENTS_RFC5423_H
+
+void push_notification_event_register_rfc5423_events(void);
+void push_notification_event_unregister_rfc5423_events(void);
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-events.c b/src/plugins/push-notification/push-notification-events.c
new file mode 100644
index 0000000..a71e8a6
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-events.c
@@ -0,0 +1,100 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+
+ARRAY_TYPE(push_notification_event) push_notification_events;
+
+ARRAY_TYPE(push_notification_event) *push_notification_get_events(void)
+{
+ return &push_notification_events;
+}
+
+static bool
+push_notification_event_find(const char *name, unsigned int *idx_r)
+{
+ unsigned int count, i;
+ const struct push_notification_event *const *events;
+
+ events = array_get(&push_notification_events, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(events[i]->name, name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static const struct push_notification_event *
+push_notification_event_find_class(const char *driver)
+{
+ unsigned int idx;
+
+ if (!push_notification_event_find(driver, &idx))
+ return NULL;
+
+ return array_idx_elem(&push_notification_events, idx);
+}
+
+void push_notification_event_init(struct push_notification_driver_txn *dtxn,
+ const char *event_name, void *config)
+{
+ const struct push_notification_event *event;
+ struct push_notification_event_config *ec;
+
+ if (!array_is_created(&dtxn->ptxn->events))
+ p_array_init(&dtxn->ptxn->events, dtxn->ptxn->pool, 4);
+
+ event = push_notification_event_find_class(event_name);
+ if (event != NULL) {
+ if ((config == NULL) && (event->init.default_config != NULL)) {
+ config = event->init.default_config();
+ }
+
+ ec = p_new(dtxn->ptxn->pool,
+ struct push_notification_event_config, 1);
+ ec->config = config;
+ ec->event = event;
+
+ array_push_back(&dtxn->ptxn->events, &ec);
+ }
+}
+
+void push_notification_event_register(
+ const struct push_notification_event *event)
+{
+ unsigned int idx;
+
+ if (!array_is_created(&push_notification_events))
+ i_array_init(&push_notification_events, 16);
+
+ if (push_notification_event_find(event->name, &idx)) {
+ i_panic("push_notification_event_register(%s): duplicate event",
+ event->name);
+ }
+
+ array_push_back(&push_notification_events, &event);
+}
+
+void push_notification_event_unregister(
+ const struct push_notification_event *event)
+{
+ unsigned int idx;
+
+ if (!push_notification_event_find(event->name, &idx)) {
+ i_panic("push_notification_event_register(%s): unknown event",
+ event->name);
+ }
+
+ if (array_is_created(&push_notification_events)) {
+ array_delete(&push_notification_events, idx, 1);
+
+ if (array_is_empty(&push_notification_events))
+ array_free(&push_notification_events);
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-events.h b/src/plugins/push-notification/push-notification-events.h
new file mode 100644
index 0000000..f20075f
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-events.h
@@ -0,0 +1,124 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_EVENTS_H
+#define PUSH_NOTIFICATION_EVENTS_H
+
+#include "mail-types.h"
+
+struct mail;
+struct mailbox;
+struct push_notification_event_config;
+struct push_notification_driver_txn;
+struct push_notification_txn;
+struct push_notification_txn_event;
+struct push_notification_txn_mbox;
+struct push_notification_txn_msg;
+
+struct push_notification_event_vfuncs_init {
+ /* Return the default config for an event (or NULL if config is
+ required). */
+ void *(*default_config)(void);
+};
+
+struct push_notification_event_vfuncs_mbox {
+ /* Output debug information about a message event. */
+ void (*debug_mbox)(struct push_notification_txn_event *event);
+ /* Called when message data is about to be free'd. */
+ void (*free_mbox)(struct push_notification_txn_event *event);
+};
+
+struct push_notification_event_vfuncs_mbox_triggers {
+ /* Mailbox event: create mailbox. */
+ void (*create)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox);
+ /* Mailbox event: delete mailbox. */
+ void (*delete)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox);
+ /* Mailbox event: rename mailbox. */
+ void (*rename)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox,
+ struct mailbox *old);
+ /* Mailbox event: subscribe mailbox. */
+ void (*subscribe)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox);
+ /* Mailbox event: unsubscribe mailbox. */
+ void (*unsubscribe)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_mbox *mbox);
+};
+
+struct push_notification_event_vfuncs_msg {
+ /* Output debug information about a message event. */
+ void (*debug_msg)(struct push_notification_txn_event *event);
+ /* Called when message data is about to be free'd. */
+ void (*free_msg)(struct push_notification_txn_event *event);
+};
+
+struct push_notification_event_vfuncs_msg_triggers {
+ /* Message event: save message (from MTA). */
+ void (*save)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg,
+ struct mail *mail);
+ /* Message event: append message (from MUA). */
+ void (*append)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg,
+ struct mail *mail);
+ /* Message event: expunge message. */
+ void (*expunge)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg);
+ /* Message event: flag change. */
+ void (*flagchange)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg,
+ struct mail *mail, enum mail_flags old_flags);
+ /* Message event: keyword change. */
+ void (*keywordchange)(struct push_notification_txn *ptxn,
+ struct push_notification_event_config *ec,
+ struct push_notification_txn_msg *msg,
+ struct mail *mail,
+ const char *const *old_keywords);
+};
+
+struct push_notification_event_config {
+ const struct push_notification_event *event;
+ void *config;
+};
+
+struct push_notification_event {
+ const char *name;
+ struct push_notification_event_vfuncs_init init;
+ struct push_notification_event_vfuncs_mbox mbox;
+ struct push_notification_event_vfuncs_mbox_triggers mbox_triggers;
+ struct push_notification_event_vfuncs_msg msg;
+ struct push_notification_event_vfuncs_msg_triggers msg_triggers;
+};
+
+struct push_notification_txn_event {
+ struct push_notification_event_config *event;
+ void *data;
+};
+
+ARRAY_DEFINE_TYPE(push_notification_event,
+ const struct push_notification_event *);
+extern ARRAY_TYPE(push_notification_event) push_notification_events;
+
+ARRAY_TYPE(push_notification_event) *push_notification_get_events(void);
+
+
+void push_notification_event_init(struct push_notification_driver_txn *dtxn,
+ const char *event_name, void *config);
+
+void push_notification_event_register(
+ const struct push_notification_event *event);
+void push_notification_event_unregister(
+ const struct push_notification_event *event);
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-plugin.c b/src/plugins/push-notification/push-notification-plugin.c
new file mode 100644
index 0000000..22c2fb0
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-plugin.c
@@ -0,0 +1,390 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-private.h"
+#include "notify-plugin.h"
+#include "str.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-events-rfc5423.h"
+#include "push-notification-plugin.h"
+#include "push-notification-triggers.h"
+#include "push-notification-txn-mbox.h"
+#include "push-notification-txn-msg.h"
+
+#define PUSH_NOTIFICATION_CONFIG "push_notification_driver"
+#define PUSH_NOTIFICATION_CONFIG_OLD "push_notification_backend"
+#define PUSH_NOTIFICATION_EVENT_FINISHED "push_notification_finished"
+
+#define PUSH_NOTIFICATION_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, push_notification_user_module)
+static MODULE_CONTEXT_DEFINE_INIT(push_notification_user_module,
+ &mail_user_module_register);
+static struct ioloop *main_ioloop;
+
+struct event_category event_category_push_notification = {
+ .name = "push-notification",
+};
+
+struct event_category *push_notification_get_event_category(void)
+{
+ return &event_category_push_notification;
+}
+
+struct push_notification_event *push_notification_get_event_messagenew(void)
+{
+ return &push_notification_event_messagenew;
+}
+
+static void
+push_notification_transaction_init(struct push_notification_txn *ptxn)
+{
+ struct push_notification_driver_txn *dtxn;
+ struct push_notification_driver_user *duser;
+ struct mail_storage *storage;
+
+ if (ptxn->initialized)
+ return;
+
+ ptxn->initialized = TRUE;
+
+ storage = mailbox_get_storage(ptxn->mbox);
+ if (storage->user->autocreated &&
+ (strcmp(storage->name, "raw") == 0)) {
+ /* No notifications for autocreated raw users */
+ return;
+ }
+
+ array_foreach_elem(&ptxn->puser->driverlist->drivers, duser) {
+ dtxn = p_new(ptxn->pool, struct push_notification_driver_txn, 1);
+ dtxn->duser = duser;
+ dtxn->ptxn = ptxn;
+
+ if ((dtxn->duser->driver->v.begin_txn == NULL) ||
+ dtxn->duser->driver->v.begin_txn(dtxn)) {
+ array_push_back(&ptxn->drivers, &dtxn);
+ }
+ }
+}
+
+static struct push_notification_txn *
+push_notification_transaction_create(struct mailbox *box,
+ struct mailbox_transaction_context *t)
+{
+ pool_t pool;
+ struct push_notification_txn *ptxn;
+ struct mail_storage *storage;
+
+ pool = pool_alloconly_create("push notification transaction", 2048);
+
+ ptxn = p_new(pool, struct push_notification_txn, 1);
+ ptxn->mbox = box;
+ storage = mailbox_get_storage(box);
+ ptxn->muser = mail_storage_get_user(storage);
+ ptxn->pool = pool;
+ ptxn->puser = PUSH_NOTIFICATION_USER_CONTEXT(ptxn->muser);
+ ptxn->t = t;
+ ptxn->trigger = PUSH_NOTIFICATION_EVENT_TRIGGER_NONE;
+ ptxn->event = event_create(ptxn->muser->event);
+ event_add_category(ptxn->event, &event_category_push_notification);
+ event_set_append_log_prefix(ptxn->event, "push-notification: ");
+ p_array_init(&ptxn->drivers, pool, 4);
+
+ return ptxn;
+}
+
+static void
+push_notification_transaction_end(struct push_notification_txn *ptxn,
+ bool success)
+{
+ struct push_notification_driver_txn *dtxn;
+
+ if (ptxn->initialized) {
+ array_foreach_elem(&ptxn->drivers, dtxn) {
+ if (dtxn->duser->driver->v.end_txn != NULL)
+ dtxn->duser->driver->v.end_txn(dtxn, success);
+ }
+ }
+
+ if (success && ptxn->trigger != 0) {
+ struct event_passthrough *e = event_create_passthrough(ptxn->event)->
+ set_name(PUSH_NOTIFICATION_EVENT_FINISHED);
+ /* Emit event */
+ e_debug(e->event(), "Push notification transaction completed");
+ }
+
+ event_unref(&ptxn->event);
+ pool_unref(&ptxn->pool);
+}
+
+static void
+push_notification_transaction_commit(
+ void *txn, struct mail_transaction_commit_changes *changes)
+{
+ struct push_notification_txn *ptxn = (struct push_notification_txn *)txn;
+ struct ioloop *prev_ioloop = current_ioloop;
+
+ /* Make sure we're not in just any random ioloop, which could get
+ destroyed soon. This way the push-notification drivers can do async
+ operations that finish in the main ioloop. */
+ io_loop_set_current(main_ioloop);
+ if (changes == NULL)
+ push_notification_txn_mbox_end(ptxn);
+ else
+ push_notification_txn_msg_end(ptxn, changes);
+
+ push_notification_transaction_end(ptxn, TRUE);
+ io_loop_set_current(prev_ioloop);
+}
+
+static void push_notification_mailbox_create(struct mailbox *box)
+{
+ struct push_notification_txn *ptxn;
+
+ ptxn = push_notification_transaction_create(box, NULL);
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_mbox_create(ptxn, box, NULL);
+ push_notification_transaction_commit(ptxn, NULL);
+}
+
+static void
+push_notification_mailbox_delete(void *txn ATTR_UNUSED, struct mailbox *box)
+{
+ struct push_notification_txn *ptxn;
+
+ ptxn = push_notification_transaction_create(box, NULL);
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_mbox_delete(ptxn, box, NULL);
+ push_notification_transaction_commit(ptxn, NULL);
+}
+
+static void
+push_notification_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ struct push_notification_txn *ptxn;
+
+ ptxn = push_notification_transaction_create(dest, NULL);
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_mbox_rename(ptxn, src, dest, NULL);
+ push_notification_transaction_commit(ptxn, NULL);
+}
+
+static void
+push_notification_mailbox_subscribe(struct mailbox *box, bool subscribed)
+{
+ struct push_notification_txn *ptxn;
+
+ ptxn = push_notification_transaction_create(box, NULL);
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_mbox_subscribe(ptxn, box, subscribed, NULL);
+ push_notification_transaction_commit(ptxn, NULL);
+}
+
+static void push_notification_mail_save(void *txn, struct mail *mail)
+{
+ struct push_notification_txn *ptxn = txn;
+
+ push_notification_transaction_init(ptxn);
+
+ /* POST_SESSION means MTA delivery. */
+ if ((mail->box->flags & MAILBOX_FLAG_POST_SESSION) != 0)
+ push_notification_trigger_msg_save_new(ptxn, mail, NULL);
+ else
+ push_notification_trigger_msg_save_append(ptxn, mail, NULL);
+}
+
+static void
+push_notification_mail_copy(void *txn, struct mail *src ATTR_UNUSED,
+ struct mail *dest)
+{
+ push_notification_mail_save(txn, dest);
+}
+
+static void push_notification_mail_expunge(void *txn, struct mail *mail)
+{
+ struct push_notification_txn *ptxn = txn;
+
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_msg_save_expunge(txn, mail, NULL);
+}
+
+static void
+push_notification_mail_update_flags(void *txn, struct mail *mail,
+ enum mail_flags old_flags)
+{
+ struct push_notification_txn *ptxn = txn;
+
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_msg_flag_change(txn, mail, NULL, old_flags);
+}
+
+static void
+push_notification_mail_update_keywords(void *txn, struct mail *mail,
+ const char *const *old_keywords)
+{
+ struct push_notification_txn *ptxn = txn;
+
+ push_notification_transaction_init(ptxn);
+ push_notification_trigger_msg_keyword_change(
+ txn, mail, NULL, old_keywords);
+}
+
+static void *
+push_notification_transaction_begin(struct mailbox_transaction_context *t)
+{
+ return push_notification_transaction_create(
+ mailbox_transaction_get_mailbox(t), t);
+}
+
+static void push_notification_transaction_rollback(void *txn)
+{
+ struct push_notification_txn *ptxn = txn;
+
+ push_notification_transaction_end(ptxn, FALSE);
+}
+
+static void
+push_notification_config_init(const char *config_name, struct mail_user *user,
+ struct push_notification_driver_list *dlist)
+{
+ struct push_notification_driver_user *duser;
+ const char *env;
+ unsigned int i;
+ string_t *root_name;
+
+ root_name = t_str_new(32);
+ str_append(root_name, config_name);
+
+ for (i = 2;; i++) {
+ env = mail_user_plugin_getenv(user, str_c(root_name));
+ if ((env == NULL) || (*env == '\0'))
+ break;
+
+ if (push_notification_driver_init(
+ user, env, user->pool, &duser) < 0)
+ break;
+
+ /* Add driver. */
+ array_push_back(&dlist->drivers, &duser);
+
+ str_truncate(root_name, strlen(config_name));
+ str_printfa(root_name, "%d", i);
+ }
+}
+
+static struct push_notification_driver_list *
+push_notification_driver_list_init(struct mail_user *user)
+{
+ struct push_notification_driver_list *dlist;
+
+ dlist = p_new(user->pool, struct push_notification_driver_list, 1);
+ p_array_init(&dlist->drivers, user->pool, 4);
+
+ push_notification_config_init(PUSH_NOTIFICATION_CONFIG, user, dlist);
+
+ if (array_is_empty(&dlist->drivers)) {
+ /* Support old configuration (it was available at time initial
+ OX driver was first released). */
+ push_notification_config_init(PUSH_NOTIFICATION_CONFIG_OLD,
+ user, dlist);
+ }
+ return dlist;
+}
+
+static void push_notification_user_deinit(struct mail_user *user)
+{
+ struct push_notification_user *puser =
+ PUSH_NOTIFICATION_USER_CONTEXT(user);
+ struct push_notification_driver_list *dlist = puser->driverlist;
+ struct push_notification_driver_user *duser;
+ struct ioloop *prev_ioloop = current_ioloop;
+
+ /* Make sure we're in the main ioloop, so if the deinit/cleanup moves
+ any I/Os or timeouts they won't get moved to some temporary ioloop.
+ */
+ io_loop_set_current(main_ioloop);
+
+ array_foreach_elem(&dlist->drivers, duser) {
+ if (duser->driver->v.deinit != NULL)
+ duser->driver->v.deinit(duser);
+ if (duser->driver->v.cleanup != NULL)
+ duser->driver->v.cleanup();
+ }
+ io_loop_set_current(prev_ioloop);
+
+ puser->module_ctx.super.deinit(user);
+}
+
+static void push_notification_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct push_notification_user *puser;
+
+ puser = p_new(user->pool, struct push_notification_user, 1);
+ puser->module_ctx.super = *v;
+ user->vlast = &puser->module_ctx.super;
+ v->deinit = push_notification_user_deinit;
+ puser->driverlist = push_notification_driver_list_init(user);
+
+ MODULE_CONTEXT_SET(user, push_notification_user_module, puser);
+}
+
+/* Plugin interface. */
+
+const char *push_notification_plugin_version = DOVECOT_ABI_VERSION;
+const char *push_notification_plugin_dependencies[] = { "notify", NULL };
+
+extern struct push_notification_driver push_notification_driver_dlog;
+extern struct push_notification_driver push_notification_driver_ox;
+
+static struct notify_context *push_notification_ctx;
+
+static const struct notify_vfuncs push_notification_vfuncs = {
+ /* Mailbox Events */
+ .mailbox_create = push_notification_mailbox_create,
+ .mailbox_delete_commit = push_notification_mailbox_delete,
+ .mailbox_rename = push_notification_mailbox_rename,
+ .mailbox_set_subscribed = push_notification_mailbox_subscribe,
+
+ /* Mail Events */
+ .mail_copy = push_notification_mail_copy,
+ .mail_save = push_notification_mail_save,
+ .mail_expunge = push_notification_mail_expunge,
+ .mail_update_flags = push_notification_mail_update_flags,
+ .mail_update_keywords = push_notification_mail_update_keywords,
+ .mail_transaction_begin = push_notification_transaction_begin,
+ .mail_transaction_commit = push_notification_transaction_commit,
+ .mail_transaction_rollback = push_notification_transaction_rollback,
+};
+
+static struct mail_storage_hooks push_notification_storage_hooks = {
+ .mail_user_created = push_notification_user_created,
+};
+
+void push_notification_plugin_init(struct module *module)
+{
+ push_notification_ctx = notify_register(&push_notification_vfuncs);
+ mail_storage_hooks_add(module, &push_notification_storage_hooks);
+
+ push_notification_driver_register(&push_notification_driver_dlog);
+ push_notification_driver_register(&push_notification_driver_ox);
+
+ push_notification_event_register_rfc5423_events();
+ main_ioloop = current_ioloop;
+ i_assert(main_ioloop != NULL);
+}
+
+void push_notification_plugin_deinit(void)
+{
+ push_notification_driver_unregister(&push_notification_driver_dlog);
+ push_notification_driver_unregister(&push_notification_driver_ox);
+
+ push_notification_event_unregister_rfc5423_events();
+ mail_storage_hooks_remove(&push_notification_storage_hooks);
+ notify_unregister(push_notification_ctx);
+}
diff --git a/src/plugins/push-notification/push-notification-plugin.h b/src/plugins/push-notification/push-notification-plugin.h
new file mode 100644
index 0000000..7db5397
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-plugin.h
@@ -0,0 +1,18 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_PLUGIN_H
+#define PUSH_NOTIFICATION_PLUGIN_H
+
+extern const char *push_notification_plugin_dependencies[];
+extern struct event_category event_category_push_notification;
+extern struct push_notification_event push_notification_event_messagenew;
+
+struct module;
+
+struct event_category *push_notification_get_event_category(void);
+struct push_notification_event *push_notification_get_event_messagenew(void);
+
+void push_notification_plugin_init(struct module *module);
+void push_notification_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/push-notification/push-notification-triggers.c b/src/plugins/push-notification/push-notification-triggers.c
new file mode 100644
index 0000000..3882982
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-triggers.c
@@ -0,0 +1,215 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+
+#include "mail-storage.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-triggers.h"
+#include "push-notification-txn-mbox.h"
+#include "push-notification-txn-msg.h"
+
+static void
+push_notification_trigger_mbox_common(
+ struct push_notification_txn *txn, struct mailbox *box,
+ struct push_notification_txn_mbox **mbox,
+ enum push_notification_event_trigger trigger)
+{
+ if (*mbox == NULL) {
+ *mbox = push_notification_txn_mbox_create(txn, box);
+ }
+
+ txn->trigger |= trigger;
+ event_add_str(txn->event, "mailbox", mailbox_get_vname(box));
+}
+
+void push_notification_trigger_mbox_create(
+ struct push_notification_txn *txn, struct mailbox *box,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_mbox_common(
+ txn, box, &mbox, PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_CREATE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->mbox_triggers.create != NULL)
+ ec->event->mbox_triggers.create(txn, ec, mbox);
+ }
+ }
+}
+
+void push_notification_trigger_mbox_delete(
+ struct push_notification_txn *txn, struct mailbox *box,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_mbox_common(
+ txn, box, &mbox, PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_DELETE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->mbox_triggers.delete != NULL)
+ ec->event->mbox_triggers.delete(txn, ec, mbox);
+ }
+ }
+}
+
+void push_notification_trigger_mbox_rename(
+ struct push_notification_txn *txn,
+ struct mailbox *src, struct mailbox *dest,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_mbox_common(
+ txn, dest, &mbox, PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_RENAME);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->mbox_triggers.rename != NULL) {
+ ec->event->mbox_triggers.rename(
+ txn, ec, mbox, src);
+ }
+ }
+ }
+}
+
+void push_notification_trigger_mbox_subscribe(
+ struct push_notification_txn *txn, struct mailbox *box, bool subscribed,
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_mbox_common(
+ txn, box, &mbox,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_SUBSCRIBE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (subscribed == TRUE) {
+ if (ec->event->mbox_triggers.subscribe != NULL) {
+ ec->event->mbox_triggers.subscribe(
+ txn, ec, mbox);
+ }
+ } else {
+ if (ec->event->mbox_triggers.unsubscribe != NULL) {
+ ec->event->mbox_triggers.unsubscribe(
+ txn, ec, mbox);
+ }
+ }
+ }
+ }
+}
+
+static void
+push_notification_trigger_msg_common(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg **msg,
+ enum push_notification_event_trigger trigger)
+{
+ if (*msg == NULL)
+ *msg = push_notification_txn_msg_create(txn, mail);
+
+ txn->trigger |= trigger;
+}
+
+void push_notification_trigger_msg_save_new(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_msg_common(
+ txn, mail, &msg, PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_SAVE_NEW);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->msg_triggers.save != NULL) {
+ ec->event->msg_triggers.save(
+ txn, ec, msg, mail);
+ }
+ }
+ }
+}
+
+void push_notification_trigger_msg_save_append(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_msg_common(
+ txn, mail, &msg,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_SAVE_APPEND);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->msg_triggers.append != NULL) {
+ ec->event->msg_triggers.append(
+ txn, ec, msg, mail);
+ }
+ }
+ }
+}
+
+void push_notification_trigger_msg_save_expunge(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_msg_common(
+ txn, mail, &msg, PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_EXPUNGE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->msg_triggers.expunge != NULL)
+ ec->event->msg_triggers.expunge(txn, ec, msg);
+ }
+ }
+}
+
+void push_notification_trigger_msg_flag_change(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg, enum mail_flags old_flags)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_msg_common(
+ txn, mail, &msg,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_FLAGCHANGE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->msg_triggers.flagchange != NULL) {
+ ec->event->msg_triggers.flagchange(
+ txn, ec, msg, mail, old_flags);
+ }
+ }
+ }
+}
+
+void push_notification_trigger_msg_keyword_change(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg, const char *const *old_keywords)
+{
+ struct push_notification_event_config *ec;
+
+ push_notification_trigger_msg_common(
+ txn, mail, &msg,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_KEYWORDCHANGE);
+
+ if (array_is_created(&txn->events)) {
+ array_foreach_elem(&txn->events, ec) {
+ if (ec->event->msg_triggers.keywordchange != NULL) {
+ ec->event->msg_triggers.keywordchange(
+ txn, ec, msg, mail, old_keywords);
+ }
+ }
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-triggers.h b/src/plugins/push-notification/push-notification-triggers.h
new file mode 100644
index 0000000..b29bc47
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-triggers.h
@@ -0,0 +1,64 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_TRIGGERS_H
+#define PUSH_NOTIFICATION_TRIGGERS_H
+
+#include "mail-types.h"
+
+struct mail;
+struct mailbox;
+struct push_notification_txn;
+struct push_notification_txn_mbox;
+struct push_notification_txn_msg;
+
+enum push_notification_event_trigger {
+ PUSH_NOTIFICATION_EVENT_TRIGGER_NONE,
+
+ /* Mailbox actions */
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_CREATE = 0x001,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_DELETE = 0x002,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_RENAME = 0x004,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MBOX_SUBSCRIBE = 0x008,
+
+ /* Message actions */
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_SAVE_NEW = 0x010,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_SAVE_APPEND = 0x020,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_EXPUNGE = 0x040,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_FLAGCHANGE = 0x080,
+ PUSH_NOTIFICATION_EVENT_TRIGGER_MSG_KEYWORDCHANGE = 0x100,
+};
+
+/* Mailbox actions. */
+void push_notification_trigger_mbox_create(
+ struct push_notification_txn *txn, struct mailbox *box,
+ struct push_notification_txn_mbox *mbox);
+void push_notification_trigger_mbox_delete(
+ struct push_notification_txn *txn, struct mailbox *box,
+ struct push_notification_txn_mbox *mbox);
+void push_notification_trigger_mbox_rename(
+ struct push_notification_txn *txn,
+ struct mailbox *src, struct mailbox *dest,
+ struct push_notification_txn_mbox *mbox);
+void push_notification_trigger_mbox_subscribe(
+ struct push_notification_txn *txn, struct mailbox *box, bool subscribed,
+ struct push_notification_txn_mbox *mbox);
+
+/* Message actions. */
+void push_notification_trigger_msg_save_new(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg);
+void push_notification_trigger_msg_save_append(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg);
+void push_notification_trigger_msg_save_expunge(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg);
+void push_notification_trigger_msg_flag_change(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg, enum mail_flags old_flags);
+void push_notification_trigger_msg_keyword_change(
+ struct push_notification_txn *txn, struct mail *mail,
+ struct push_notification_txn_msg *msg, const char *const *old_keywords);
+
+#endif
+
diff --git a/src/plugins/push-notification/push-notification-txn-mbox.c b/src/plugins/push-notification/push-notification-txn-mbox.c
new file mode 100644
index 0000000..4401819
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-txn-mbox.c
@@ -0,0 +1,90 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "mail-storage-private.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-mbox.h"
+
+struct push_notification_txn_mbox *
+push_notification_txn_mbox_create(struct push_notification_txn *txn,
+ struct mailbox *box)
+{
+ if (txn->mbox_txn == NULL) {
+ txn->mbox_txn = p_new(txn->pool,
+ struct push_notification_txn_mbox, 1);
+ txn->mbox_txn->mailbox = mailbox_get_vname(box);
+ }
+
+ return txn->mbox_txn;
+}
+
+void push_notification_txn_mbox_end(struct push_notification_txn *ptxn)
+{
+ struct push_notification_driver_txn **dtxn;
+
+ if (ptxn->mbox_txn != NULL) {
+ array_foreach_modifiable(&ptxn->drivers, dtxn) {
+ if ((*dtxn)->duser->driver->v.process_mbox != NULL) {
+ (*dtxn)->duser->driver->v.process_mbox(
+ *dtxn, ptxn->mbox_txn);
+ }
+ }
+
+ push_notification_txn_mbox_deinit_eventdata(ptxn->mbox_txn);
+ }
+}
+
+void *
+push_notification_txn_mbox_get_eventdata(
+ struct push_notification_txn_mbox *mbox, const char *event_name)
+{
+ struct push_notification_txn_event **mevent;
+
+ if (array_is_created(&mbox->eventdata)) {
+ array_foreach_modifiable(&mbox->eventdata, mevent) {
+ if (strcmp((*mevent)->event->event->name,
+ event_name) == 0) {
+ return (*mevent)->data;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+void push_notification_txn_mbox_set_eventdata(
+ struct push_notification_txn *txn,
+ struct push_notification_txn_mbox *mbox,
+ struct push_notification_event_config *event, void *data)
+{
+ struct push_notification_txn_event *mevent;
+
+ if (!array_is_created(&mbox->eventdata)) {
+ p_array_init(&mbox->eventdata, txn->pool, 4);
+ }
+
+ mevent = p_new(txn->pool, struct push_notification_txn_event, 1);
+ mevent->data = data;
+ mevent->event = event;
+
+ array_push_back(&mbox->eventdata, &mevent);
+}
+
+void push_notification_txn_mbox_deinit_eventdata(
+ struct push_notification_txn_mbox *mbox)
+{
+ struct push_notification_txn_event **mevent;
+
+ if (array_is_created(&mbox->eventdata)) {
+ array_foreach_modifiable(&mbox->eventdata, mevent) {
+ if (((*mevent)->data != NULL) &&
+ ((*mevent)->event->event->mbox.free_mbox != NULL)) {
+ (*mevent)->event->event->mbox.free_mbox(
+ *mevent);
+ }
+ }
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-txn-mbox.h b/src/plugins/push-notification/push-notification-txn-mbox.h
new file mode 100644
index 0000000..c7a7e55
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-txn-mbox.h
@@ -0,0 +1,29 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_TXN_MBOX_H
+#define PUSH_NOTIFICATION_TXN_MBOX_H
+
+struct push_notification_txn_event;
+
+struct push_notification_txn_mbox {
+ const char *mailbox;
+
+ ARRAY(struct push_notification_txn_event *) eventdata;
+};
+
+struct push_notification_txn_mbox *
+push_notification_txn_mbox_create(struct push_notification_txn *txn,
+ struct mailbox *box);
+void push_notification_txn_mbox_end(struct push_notification_txn *ptxn);
+
+void *
+push_notification_txn_mbox_get_eventdata(
+ struct push_notification_txn_mbox *mbox, const char *event_name);
+void push_notification_txn_mbox_set_eventdata(
+ struct push_notification_txn *txn,
+ struct push_notification_txn_mbox *mbox,
+ struct push_notification_event_config *event, void *data);
+void push_notification_txn_mbox_deinit_eventdata(
+ struct push_notification_txn_mbox *mbox);
+
+#endif
diff --git a/src/plugins/push-notification/push-notification-txn-msg.c b/src/plugins/push-notification/push-notification-txn-msg.c
new file mode 100644
index 0000000..ff37bff
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-txn-msg.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "mail-storage-private.h"
+
+#include "push-notification-drivers.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-msg.h"
+
+
+struct push_notification_txn_msg *
+push_notification_txn_msg_create(struct push_notification_txn *txn,
+ struct mail *mail)
+{
+ struct push_notification_txn_msg *msg = NULL;
+
+ if (hash_table_is_created(txn->messages)) {
+ msg = hash_table_lookup(txn->messages,
+ POINTER_CAST(mail->seq));
+ } else {
+ hash_table_create_direct(&txn->messages, txn->pool, 4);
+ }
+
+ if (msg == NULL) {
+ msg = p_new(txn->pool, struct push_notification_txn_msg, 1);
+ msg->mailbox = mailbox_get_vname(mail->box);
+ /* Save sequence number - used to determine UID later. */
+ if (mail->uid == 0)
+ msg->save_idx = txn->t->save_count;
+ else
+ msg->save_idx = UINT_MAX;
+ msg->uid = mail->uid;
+
+ hash_table_insert(txn->messages, POINTER_CAST(mail->seq),
+ msg);
+ }
+
+ return msg;
+}
+
+void push_notification_txn_msg_end(
+ struct push_notification_txn *ptxn,
+ struct mail_transaction_commit_changes *changes)
+{
+ struct hash_iterate_context *hiter;
+ void *key;
+ struct push_notification_driver_txn **dtxn;
+ struct seq_range_iter siter;
+ struct mailbox_status status;
+ uint32_t uid, uid_validity;
+ struct push_notification_txn_msg *value;
+
+ if (!hash_table_is_created(ptxn->messages)) {
+ return;
+ }
+
+ hiter = hash_table_iterate_init(ptxn->messages);
+ seq_range_array_iter_init(&siter, &changes->saved_uids);
+
+ /* uid_validity is only set in changes if message is new. */
+ if (changes->uid_validity == 0) {
+ mailbox_get_open_status(ptxn->mbox, STATUS_UIDVALIDITY, &status);
+ uid_validity = status.uidvalidity;
+ } else {
+ uid_validity = changes->uid_validity;
+ }
+
+ while (hash_table_iterate(hiter, ptxn->messages, &key, &value)) {
+ if (value->uid == 0) {
+ if (seq_range_array_iter_nth(&siter, value->save_idx, &uid)) {
+ value->uid = uid;
+ }
+ } else
+ i_assert(value->save_idx == UINT_MAX);
+ value->uid_validity = uid_validity;
+
+ array_foreach_modifiable(&ptxn->drivers, dtxn) {
+ if ((*dtxn)->duser->driver->v.process_msg != NULL) {
+ (*dtxn)->duser->driver->v.process_msg(*dtxn, value);
+ }
+ }
+
+ push_notification_txn_msg_deinit_eventdata(value);
+ }
+
+ hash_table_iterate_deinit(&hiter);
+ hash_table_destroy(&ptxn->messages);
+}
+
+void *
+push_notification_txn_msg_get_eventdata(struct push_notification_txn_msg *msg,
+ const char *event_name)
+{
+ struct push_notification_txn_event **mevent;
+
+ if (array_is_created(&msg->eventdata)) {
+ array_foreach_modifiable(&msg->eventdata, mevent) {
+ if (strcmp((*mevent)->event->event->name, event_name) == 0) {
+ return (*mevent)->data;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+void push_notification_txn_msg_set_eventdata(
+ struct push_notification_txn *txn,
+ struct push_notification_txn_msg *msg,
+ struct push_notification_event_config *event, void *data)
+{
+ struct push_notification_txn_event *mevent;
+
+ if (!array_is_created(&msg->eventdata)) {
+ p_array_init(&msg->eventdata, txn->pool, 4);
+ }
+
+ mevent = p_new(txn->pool, struct push_notification_txn_event, 1);
+ mevent->data = data;
+ mevent->event = event;
+
+ array_push_back(&msg->eventdata, &mevent);
+}
+
+void push_notification_txn_msg_deinit_eventdata(
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_txn_event **mevent;
+
+ if (array_is_created(&msg->eventdata)) {
+ array_foreach_modifiable(&msg->eventdata, mevent) {
+ if (((*mevent)->data != NULL) &&
+ ((*mevent)->event->event->msg.free_msg != NULL)) {
+ (*mevent)->event->event->msg.free_msg(*mevent);
+ }
+ }
+ }
+}
diff --git a/src/plugins/push-notification/push-notification-txn-msg.h b/src/plugins/push-notification/push-notification-txn-msg.h
new file mode 100644
index 0000000..777f115
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-txn-msg.h
@@ -0,0 +1,39 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#ifndef PUSH_NOTIFICATION_TXN_MSG_H
+#define PUSH_NOTIFICATION_TXN_MSG_H
+
+struct mail_transaction_commit_changes;
+struct push_notification_event_config;
+struct push_notification_txn;
+struct push_notification_txn_event;
+
+struct push_notification_txn_msg {
+ const char *mailbox;
+ uint32_t uid;
+ uint32_t uid_validity;
+
+ ARRAY(struct push_notification_txn_event *) eventdata;
+
+ /* Private */
+ unsigned int save_idx;
+};
+
+struct push_notification_txn_msg *
+push_notification_txn_msg_create(struct push_notification_txn *txn,
+ struct mail *mail);
+void push_notification_txn_msg_end(
+ struct push_notification_txn *ptxn,
+ struct mail_transaction_commit_changes *changes);
+
+void *
+push_notification_txn_msg_get_eventdata(struct push_notification_txn_msg *msg,
+ const char *event_name);
+void push_notification_txn_msg_set_eventdata(
+ struct push_notification_txn *txn,
+ struct push_notification_txn_msg *msg,
+ struct push_notification_event_config *event, void *data);
+void push_notification_txn_msg_deinit_eventdata(
+ struct push_notification_txn_msg *msg);
+
+#endif
diff --git a/src/plugins/quota-clone/Makefile.am b/src/plugins/quota-clone/Makefile.am
new file mode 100644
index 0000000..ed4ba68
--- /dev/null
+++ b/src/plugins/quota-clone/Makefile.am
@@ -0,0 +1,19 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -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/plugins/quota
+
+NOPLUGIN_LDFLAGS =
+lib20_quota_clone_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_quota_clone_plugin.la
+
+lib20_quota_clone_plugin_la_SOURCES = \
+ quota-clone-plugin.c
+
+noinst_HEADERS = \
+ quota-clone-plugin.h
diff --git a/src/plugins/quota-clone/Makefile.in b/src/plugins/quota-clone/Makefile.in
new file mode 100644
index 0000000..c5e76ad
--- /dev/null
+++ b/src/plugins/quota-clone/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/quota-clone
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_quota_clone_plugin_la_LIBADD =
+am_lib20_quota_clone_plugin_la_OBJECTS = quota-clone-plugin.lo
+lib20_quota_clone_plugin_la_OBJECTS = \
+ $(am_lib20_quota_clone_plugin_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 =
+lib20_quota_clone_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_quota_clone_plugin_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)/quota-clone-plugin.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 = $(lib20_quota_clone_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_quota_clone_plugin_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 =
+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-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/quota
+
+lib20_quota_clone_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_quota_clone_plugin.la
+
+lib20_quota_clone_plugin_la_SOURCES = \
+ quota-clone-plugin.c
+
+noinst_HEADERS = \
+ quota-clone-plugin.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/plugins/quota-clone/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/quota-clone/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-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}; \
+ }
+
+lib20_quota_clone_plugin.la: $(lib20_quota_clone_plugin_la_OBJECTS) $(lib20_quota_clone_plugin_la_DEPENDENCIES) $(EXTRA_lib20_quota_clone_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_quota_clone_plugin_la_LINK) -rpath $(moduledir) $(lib20_quota_clone_plugin_la_OBJECTS) $(lib20_quota_clone_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-clone-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/quota-clone-plugin.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-moduleLTLIBRARIES
+
+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)/quota-clone-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/quota-clone/quota-clone-plugin.c b/src/plugins/quota-clone/quota-clone-plugin.c
new file mode 100644
index 0000000..5f5efa4
--- /dev/null
+++ b/src/plugins/quota-clone/quota-clone-plugin.c
@@ -0,0 +1,308 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-context.h"
+#include "ioloop.h"
+#include "dict.h"
+#include "mail-storage-private.h"
+#include "quota.h"
+#include "quota-clone-plugin.h"
+
+/* If mailbox is kept open for this many milliseconds after quota update,
+ flush quota-clone. */
+#define QUOTA_CLONE_FLUSH_DELAY_MSECS (10*1000)
+
+#define DICT_QUOTA_CLONE_PATH DICT_PATH_PRIVATE"quota/"
+#define DICT_QUOTA_CLONE_BYTES_PATH DICT_QUOTA_CLONE_PATH"storage"
+#define DICT_QUOTA_CLONE_COUNT_PATH DICT_QUOTA_CLONE_PATH"messages"
+
+#define QUOTA_CLONE_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, quota_clone_user_module)
+#define QUOTA_CLONE_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, quota_clone_user_module)
+#define QUOTA_CLONE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, quota_clone_storage_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(quota_clone_user_module,
+ &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(quota_clone_storage_module,
+ &mail_storage_module_register);
+
+struct quota_clone_user {
+ union mail_user_module_context module_ctx;
+ struct dict *dict;
+ struct timeout *to_quota_flush;
+ bool quota_changed;
+ bool quota_flushing;
+};
+
+static void
+quota_clone_dict_commit(const struct dict_commit_result *result,
+ struct quota_clone_user *quser)
+{
+ switch (result->ret) {
+ case DICT_COMMIT_RET_OK:
+ case DICT_COMMIT_RET_NOTFOUND:
+ if (!quser->quota_changed)
+ timeout_remove(&quser->to_quota_flush);
+ break;
+ case DICT_COMMIT_RET_FAILED:
+ quser->quota_changed = TRUE;
+ i_error("quota_clone_dict: Failed to write value: %s",
+ result->error);
+ break;
+ case DICT_COMMIT_RET_WRITE_UNCERTAIN:
+ quser->quota_changed = TRUE;
+ i_error("quota_clone_dict: Write was unconfirmed (timeout or disconnect): %s",
+ result->error);
+ break;
+ }
+
+ quser->quota_flushing = FALSE;
+}
+
+static bool quota_clone_flush_real(struct mail_user *user)
+{
+ struct quota_clone_user *quser =
+ QUOTA_CLONE_USER_CONTEXT_REQUIRE(user);
+ struct dict_transaction_context *trans;
+ struct quota_root_iter *iter;
+ struct quota_root *root;
+ uint64_t bytes_value, count_value, limit;
+ const char *error;
+ enum quota_get_result bytes_res, count_res;
+
+ /* we'll clone the first quota root */
+ iter = quota_root_iter_init_user(user);
+ root = quota_root_iter_next(iter);
+ quota_root_iter_deinit(&iter);
+ if (root == NULL) {
+ /* no quota roots defined - ignore */
+ quser->quota_changed = FALSE;
+ return TRUE;
+ }
+
+ /* get new values first */
+ bytes_res = quota_get_resource(root, "", QUOTA_NAME_STORAGE_BYTES,
+ &bytes_value, &limit, &error);
+ if (bytes_res == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ i_error("quota_clone_plugin: "
+ "Failed to get quota resource "QUOTA_NAME_STORAGE_BYTES": %s",
+ error);
+ return TRUE;
+ }
+ count_res = quota_get_resource(root, "", QUOTA_NAME_MESSAGES,
+ &count_value, &limit, &error);
+ if (count_res == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ i_error("quota_clone_plugin: "
+ "Failed to get quota resource "QUOTA_NAME_MESSAGES": %s",
+ error);
+ return TRUE;
+ }
+ if (bytes_res == QUOTA_GET_RESULT_UNKNOWN_RESOURCE &&
+ count_res == QUOTA_GET_RESULT_UNKNOWN_RESOURCE) {
+ /* quota resources don't exist - no point in updating it */
+ return TRUE;
+ }
+ if (bytes_res == QUOTA_GET_RESULT_BACKGROUND_CALC &&
+ count_res == QUOTA_GET_RESULT_BACKGROUND_CALC) {
+ /* Blocked by an ongoing quota calculation - try again later */
+ quser->quota_flushing = FALSE;
+ return FALSE;
+ }
+
+ /* Then update the resources that exist. The resources' existence can't
+ change unless the quota backend is changed, so we don't worry about
+ the special case of lookup changing from
+ RESULT_LIMITED/RESULT_UNLIMITED to RESULT_UNKNOWN_RESOURCE, which
+ leaves the old value unchanged. */
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ trans = dict_transaction_begin(quser->dict, set);
+ if (bytes_res == QUOTA_GET_RESULT_LIMITED ||
+ bytes_res == QUOTA_GET_RESULT_UNLIMITED) {
+ dict_set(trans, DICT_QUOTA_CLONE_BYTES_PATH,
+ t_strdup_printf("%"PRIu64, bytes_value));
+ }
+ if (count_res == QUOTA_GET_RESULT_LIMITED ||
+ count_res == QUOTA_GET_RESULT_UNLIMITED) {
+ dict_set(trans, DICT_QUOTA_CLONE_COUNT_PATH,
+ t_strdup_printf("%"PRIu64, count_value));
+ }
+ quser->quota_changed = FALSE;
+ dict_transaction_commit_async(&trans, quota_clone_dict_commit, quser);
+ return FALSE;
+}
+
+static void quota_clone_flush(struct mail_user *user)
+{
+ struct quota_clone_user *quser =
+ QUOTA_CLONE_USER_CONTEXT_REQUIRE(user);
+
+ if (quser->quota_changed) {
+ i_assert(quser->to_quota_flush != NULL);
+ if (quser->quota_flushing) {
+ /* async quota commit is running in background. timeout is still
+ active, so another update will be done later. */
+ } else {
+ quser->quota_flushing = TRUE;
+ /* Returns TRUE if flushing action is complete. */
+ if (quota_clone_flush_real(user)) {
+ quser->quota_flushing = FALSE;
+ timeout_remove(&quser->to_quota_flush);
+ }
+ }
+ } else {
+ timeout_remove(&quser->to_quota_flush);
+ }
+}
+
+static struct mail_user *quota_mailbox_get_user(struct mailbox *box)
+{
+ struct mail_namespace *ns = mailbox_list_get_namespace(box->list);
+ return ns->owner != NULL ? ns->owner : ns->user;
+}
+
+static void quota_clone_changed(struct mailbox *box)
+{
+ struct mail_user *user = quota_mailbox_get_user(box);
+ struct quota_clone_user *quser =
+ QUOTA_CLONE_USER_CONTEXT_REQUIRE(user);
+
+ quser->quota_changed = TRUE;
+ if (quser->to_quota_flush == NULL) {
+ quser->to_quota_flush = timeout_add(QUOTA_CLONE_FLUSH_DELAY_MSECS,
+ quota_clone_flush, user);
+ }
+}
+
+static int quota_clone_save_finish(struct mail_save_context *ctx)
+{
+ union mailbox_module_context *qbox =
+ QUOTA_CLONE_CONTEXT(ctx->transaction->box);
+
+ quota_clone_changed(ctx->transaction->box);
+ return qbox->super.save_finish(ctx);
+}
+
+static int
+quota_clone_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ union mailbox_module_context *qbox =
+ QUOTA_CLONE_CONTEXT(ctx->transaction->box);
+
+ quota_clone_changed(ctx->transaction->box);
+ return qbox->super.copy(ctx, mail);
+}
+
+static void
+quota_clone_mailbox_sync_notify(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type)
+{
+ union mailbox_module_context *qbox = QUOTA_CLONE_CONTEXT(box);
+
+ if (qbox->super.sync_notify != NULL)
+ qbox->super.sync_notify(box, uid, sync_type);
+
+ if (sync_type == MAILBOX_SYNC_TYPE_EXPUNGE)
+ quota_clone_changed(box);
+}
+
+static void quota_clone_mailbox_allocated(struct mailbox *box)
+{
+ struct quota_clone_user *quser =
+ QUOTA_CLONE_USER_CONTEXT(box->storage->user);
+ struct mailbox_vfuncs *v = box->vlast;
+ union mailbox_module_context *qbox;
+
+ if (quser == NULL)
+ return;
+
+ qbox = p_new(box->pool, union mailbox_module_context, 1);
+ qbox->super = *v;
+ box->vlast = &qbox->super;
+
+ v->save_finish = quota_clone_save_finish;
+ v->copy = quota_clone_copy;
+ v->sync_notify = quota_clone_mailbox_sync_notify;
+ MODULE_CONTEXT_SET_SELF(box, quota_clone_storage_module, qbox);
+}
+
+static void quota_clone_mail_user_deinit_pre(struct mail_user *user)
+{
+ struct quota_clone_user *quser = QUOTA_CLONE_USER_CONTEXT_REQUIRE(user);
+
+ dict_wait(quser->dict);
+ /* Check once more if quota needs to be updated. This needs to be done
+ in deinit_pre(), because at deinit() the quota is already
+ deinitialized. */
+ if (quser->to_quota_flush != NULL) {
+ i_assert(!quser->quota_flushing);
+ quota_clone_flush(user);
+ dict_wait(quser->dict);
+ /* If dict update fails or background calculation is running,
+ the timeout is still set. Just forget about it. */
+ timeout_remove(&quser->to_quota_flush);
+ }
+ quser->module_ctx.super.deinit_pre(user);
+}
+
+static void quota_clone_mail_user_deinit(struct mail_user *user)
+{
+ struct quota_clone_user *quser = QUOTA_CLONE_USER_CONTEXT_REQUIRE(user);
+
+ /* wait once more, just in case something changed quota during
+ deinit_pre() */
+ dict_wait(quser->dict);
+ i_assert(quser->to_quota_flush == NULL);
+ dict_deinit(&quser->dict);
+ quser->module_ctx.super.deinit(user);
+}
+
+static void quota_clone_mail_user_created(struct mail_user *user)
+{
+ struct quota_clone_user *quser;
+ struct mail_user_vfuncs *v = user->vlast;
+ struct dict_settings dict_set;
+ struct dict *dict;
+ const char *uri, *error;
+
+ uri = mail_user_plugin_getenv(user, "quota_clone_dict");
+ if (uri == NULL || uri[0] == '\0') {
+ e_debug(user->event, "The quota_clone_dict setting is missing from configuration");
+ return;
+ }
+
+ i_zero(&dict_set);
+ dict_set.base_dir = user->set->base_dir;
+ dict_set.event_parent = user->event;
+ if (dict_init(uri, &dict_set, &dict, &error) < 0) {
+ i_error("quota_clone_dict: Failed to initialize '%s': %s",
+ uri, error);
+ return;
+ }
+
+ quser = p_new(user->pool, struct quota_clone_user, 1);
+ quser->module_ctx.super = *v;
+ user->vlast = &quser->module_ctx.super;
+ v->deinit_pre = quota_clone_mail_user_deinit_pre;
+ v->deinit = quota_clone_mail_user_deinit;
+ quser->dict = dict;
+ MODULE_CONTEXT_SET(user, quota_clone_user_module, quser);
+}
+
+static struct mail_storage_hooks quota_clone_mail_storage_hooks = {
+ .mailbox_allocated = quota_clone_mailbox_allocated,
+ .mail_user_created = quota_clone_mail_user_created
+};
+
+void quota_clone_plugin_init(struct module *module ATTR_UNUSED)
+{
+ mail_storage_hooks_add(module, &quota_clone_mail_storage_hooks);
+}
+
+void quota_clone_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&quota_clone_mail_storage_hooks);
+}
+
+const char *quota_clone_plugin_dependencies[] = { "quota", NULL };
diff --git a/src/plugins/quota-clone/quota-clone-plugin.h b/src/plugins/quota-clone/quota-clone-plugin.h
new file mode 100644
index 0000000..9ae1d8f
--- /dev/null
+++ b/src/plugins/quota-clone/quota-clone-plugin.h
@@ -0,0 +1,7 @@
+#ifndef QUOTA_CLONE_PLUGIN_H
+#define QUOTA_CLONE_PLUGIN_H
+
+void quota_clone_plugin_init(struct module *module);
+void quota_clone_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/quota/Makefile.am b/src/plugins/quota/Makefile.am
new file mode 100644
index 0000000..ad08d50
--- /dev/null
+++ b/src/plugins/quota/Makefile.am
@@ -0,0 +1,148 @@
+doveadm_moduledir = $(moduledir)/doveadm
+
+pkglibexecdir = $(libexecdir)/dovecot
+pkglibexec_PROGRAMS = quota-status
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/imapc \
+ -I$(top_srcdir)/src/lib-storage/index/maildir \
+ -I$(top_srcdir)/src/lib-program-client \
+ -I$(top_srcdir)/src/doveadm \
+ $(LIBTIRPC_CFLAGS)
+
+NOPLUGIN_LDFLAGS =
+lib10_doveadm_quota_plugin_la_LDFLAGS = -module -avoid-version
+lib10_quota_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib10_quota_plugin.la
+
+quota_dist_sources = \
+ quota.c \
+ quota-count.c \
+ quota-fs.c \
+ quota-dict.c \
+ quota-dirsize.c \
+ quota-imapc.c \
+ quota-maildir.c \
+ quota-plugin.c \
+ quota-storage.c \
+ quota-util.c
+
+quota_common_objects = \
+ quota.lo \
+ quota-count.lo \
+ quota-fs.lo \
+ quota-dict.lo \
+ quota-dirsize.lo \
+ quota-imapc.lo \
+ quota-maildir.lo \
+ quota-plugin.lo \
+ quota-storage.lo \
+ quota-util.lo \
+ $(RQUOTA_XDR_LO)
+
+lib10_quota_plugin_la_SOURCES = $(quota_dist_sources)
+nodist_lib10_quota_plugin_la_SOURCES = $(RQUOTA_XDR)
+lib10_quota_plugin_la_LIBADD = $(QUOTA_LIBS)
+
+doveadm_module_LTLIBRARIES = \
+ lib10_doveadm_quota_plugin.la
+
+lib10_doveadm_quota_plugin_la_SOURCES = \
+ doveadm-quota.c
+
+quota_status_SOURCES = \
+ quota-status.c \
+ quota-status-settings.c
+
+quota_status_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+quota_status_LDADD = \
+ $(quota_common_objects) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(QUOTA_LIBS) \
+ $(BINARY_LDFLAGS)
+quota_status_DEPENDENCIES = \
+ $(quota_common_objects) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+if HAVE_RQUOTA
+RQUOTA_XDR = rquota_xdr.c
+RQUOTA_XDR_LO = rquota_xdr.lo
+RQUOTA_X = $(srcdir)/rquota.x
+RQUOTA_PRAGMAS_H = $(srcdir)/rquota-pragmas.h
+rquota_xdr.c: Makefile rquota.h
+ if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \
+ cp $(RQUOTA_X) $(top_builddir)/src/plugins/quota/; \
+ fi; \
+ (echo '#include "lib.h"'; \
+ echo '#undef FALSE'; \
+ echo '#undef TRUE'; \
+ echo '#include <rpc/rpc.h>'; \
+ $(RPCGEN) -c $(top_builddir)/src/plugins/quota/rquota.x | \
+ sed \
+ -e 's/IXDR_PUT/(void)IXDR_PUT/g' \
+ -e 's,!xdr_,0 == xdr_,' \
+ -e 's,/usr/include/rpcsvc/rquota.h,rquota.h,' \
+ -e 's/int32_t \*buf/int32_t *buf ATTR_UNUSED/' \
+ -e 's/^static char rcsid.*//' ) > rquota_xdr.c.tmp
+ cat $(RQUOTA_PRAGMAS_H) rquota_xdr.c.tmp > rquota_xdr.c
+
+rquota.h: Makefile $(RQUOTA_X)
+ $(RPCGEN) -h $(RQUOTA_X) > rquota.h.tmp
+ cat $(RQUOTA_PRAGMAS_H) rquota.h.tmp > rquota.h
+
+quota-fs.lo: rquota.h
+
+endif
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ quota.h \
+ quota-fs.h \
+ quota-plugin.h \
+ quota-private.h
+noinst_HEADERS = \
+ quota-status-settings.h \
+ rquota-pragmas.h
+
+EXTRA_DIST = rquota.x
+
+clean-generic:
+ if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \
+ rm -f $(top_builddir)/src/plugins/quota/rquota.x; \
+ fi; \
+ rm -f rquota_xdr.c rquota.h
+
+test_programs = \
+ test-quota-util
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_quota_util_SOURCES = test-quota-util.c
+test_quota_util_LDADD = quota-util.lo $(test_libs)
+test_quota_util_DEPENDENCIES = quota-util.lo $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/plugins/quota/Makefile.in b/src/plugins/quota/Makefile.in
new file mode 100644
index 0000000..5f7a045
--- /dev/null
+++ b/src/plugins/quota/Makefile.in
@@ -0,0 +1,1183 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = quota-status$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/plugins/quota
+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-quota-util$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_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; }; \
+ }
+LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES)
+lib10_doveadm_quota_plugin_la_LIBADD =
+am_lib10_doveadm_quota_plugin_la_OBJECTS = doveadm-quota.lo
+lib10_doveadm_quota_plugin_la_OBJECTS = \
+ $(am_lib10_doveadm_quota_plugin_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 =
+lib10_doveadm_quota_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib10_doveadm_quota_plugin_la_LDFLAGS) $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+lib10_quota_plugin_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am__objects_1 = quota.lo quota-count.lo quota-fs.lo quota-dict.lo \
+ quota-dirsize.lo quota-imapc.lo quota-maildir.lo \
+ quota-plugin.lo quota-storage.lo quota-util.lo
+am_lib10_quota_plugin_la_OBJECTS = $(am__objects_1)
+@HAVE_RQUOTA_TRUE@am__objects_2 = rquota_xdr.lo
+nodist_lib10_quota_plugin_la_OBJECTS = $(am__objects_2)
+lib10_quota_plugin_la_OBJECTS = $(am_lib10_quota_plugin_la_OBJECTS) \
+ $(nodist_lib10_quota_plugin_la_OBJECTS)
+lib10_quota_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib10_quota_plugin_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_quota_status_OBJECTS = quota_status-quota-status.$(OBJEXT) \
+ quota_status-quota-status-settings.$(OBJEXT)
+quota_status_OBJECTS = $(am_quota_status_OBJECTS)
+am_test_quota_util_OBJECTS = test-quota-util.$(OBJEXT)
+test_quota_util_OBJECTS = $(am_test_quota_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)/doveadm-quota.Plo \
+ ./$(DEPDIR)/quota-count.Plo ./$(DEPDIR)/quota-dict.Plo \
+ ./$(DEPDIR)/quota-dirsize.Plo ./$(DEPDIR)/quota-fs.Plo \
+ ./$(DEPDIR)/quota-imapc.Plo ./$(DEPDIR)/quota-maildir.Plo \
+ ./$(DEPDIR)/quota-plugin.Plo ./$(DEPDIR)/quota-storage.Plo \
+ ./$(DEPDIR)/quota-util.Plo ./$(DEPDIR)/quota.Plo \
+ ./$(DEPDIR)/quota_status-quota-status-settings.Po \
+ ./$(DEPDIR)/quota_status-quota-status.Po \
+ ./$(DEPDIR)/rquota_xdr.Plo ./$(DEPDIR)/test-quota-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 = $(lib10_doveadm_quota_plugin_la_SOURCES) \
+ $(lib10_quota_plugin_la_SOURCES) \
+ $(nodist_lib10_quota_plugin_la_SOURCES) \
+ $(quota_status_SOURCES) $(test_quota_util_SOURCES)
+DIST_SOURCES = $(lib10_doveadm_quota_plugin_la_SOURCES) \
+ $(lib10_quota_plugin_la_SOURCES) $(quota_status_SOURCES) \
+ $(test_quota_util_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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@
+doveadm_moduledir = $(moduledir)/doveadm
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/imapc \
+ -I$(top_srcdir)/src/lib-storage/index/maildir \
+ -I$(top_srcdir)/src/lib-program-client \
+ -I$(top_srcdir)/src/doveadm \
+ $(LIBTIRPC_CFLAGS)
+
+lib10_doveadm_quota_plugin_la_LDFLAGS = -module -avoid-version
+lib10_quota_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib10_quota_plugin.la
+
+quota_dist_sources = \
+ quota.c \
+ quota-count.c \
+ quota-fs.c \
+ quota-dict.c \
+ quota-dirsize.c \
+ quota-imapc.c \
+ quota-maildir.c \
+ quota-plugin.c \
+ quota-storage.c \
+ quota-util.c
+
+quota_common_objects = \
+ quota.lo \
+ quota-count.lo \
+ quota-fs.lo \
+ quota-dict.lo \
+ quota-dirsize.lo \
+ quota-imapc.lo \
+ quota-maildir.lo \
+ quota-plugin.lo \
+ quota-storage.lo \
+ quota-util.lo \
+ $(RQUOTA_XDR_LO)
+
+lib10_quota_plugin_la_SOURCES = $(quota_dist_sources)
+nodist_lib10_quota_plugin_la_SOURCES = $(RQUOTA_XDR)
+lib10_quota_plugin_la_LIBADD = $(QUOTA_LIBS)
+doveadm_module_LTLIBRARIES = \
+ lib10_doveadm_quota_plugin.la
+
+lib10_doveadm_quota_plugin_la_SOURCES = \
+ doveadm-quota.c
+
+quota_status_SOURCES = \
+ quota-status.c \
+ quota-status-settings.c
+
+quota_status_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+quota_status_LDADD = \
+ $(quota_common_objects) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(QUOTA_LIBS) \
+ $(BINARY_LDFLAGS)
+
+quota_status_DEPENDENCIES = \
+ $(quota_common_objects) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+@HAVE_RQUOTA_TRUE@RQUOTA_XDR = rquota_xdr.c
+@HAVE_RQUOTA_TRUE@RQUOTA_XDR_LO = rquota_xdr.lo
+@HAVE_RQUOTA_TRUE@RQUOTA_X = $(srcdir)/rquota.x
+@HAVE_RQUOTA_TRUE@RQUOTA_PRAGMAS_H = $(srcdir)/rquota-pragmas.h
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ quota.h \
+ quota-fs.h \
+ quota-plugin.h \
+ quota-private.h
+
+noinst_HEADERS = \
+ quota-status-settings.h \
+ rquota-pragmas.h
+
+EXTRA_DIST = rquota.x
+test_programs = \
+ test-quota-util
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_quota_util_SOURCES = test-quota-util.c
+test_quota_util_LDADD = quota-util.lo $(test_libs)
+test_quota_util_DEPENDENCIES = quota-util.lo $(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/plugins/quota/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/quota/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_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)$(doveadm_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \
+ }
+
+uninstall-doveadm_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \
+ done
+
+clean-doveadm_moduleLTLIBRARIES:
+ -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES)
+ @list='$(doveadm_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}; \
+ }
+
+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}; \
+ }
+
+lib10_doveadm_quota_plugin.la: $(lib10_doveadm_quota_plugin_la_OBJECTS) $(lib10_doveadm_quota_plugin_la_DEPENDENCIES) $(EXTRA_lib10_doveadm_quota_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib10_doveadm_quota_plugin_la_LINK) -rpath $(doveadm_moduledir) $(lib10_doveadm_quota_plugin_la_OBJECTS) $(lib10_doveadm_quota_plugin_la_LIBADD) $(LIBS)
+
+lib10_quota_plugin.la: $(lib10_quota_plugin_la_OBJECTS) $(lib10_quota_plugin_la_DEPENDENCIES) $(EXTRA_lib10_quota_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib10_quota_plugin_la_LINK) -rpath $(moduledir) $(lib10_quota_plugin_la_OBJECTS) $(lib10_quota_plugin_la_LIBADD) $(LIBS)
+
+quota-status$(EXEEXT): $(quota_status_OBJECTS) $(quota_status_DEPENDENCIES) $(EXTRA_quota_status_DEPENDENCIES)
+ @rm -f quota-status$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(quota_status_OBJECTS) $(quota_status_LDADD) $(LIBS)
+
+test-quota-util$(EXEEXT): $(test_quota_util_OBJECTS) $(test_quota_util_DEPENDENCIES) $(EXTRA_test_quota_util_DEPENDENCIES)
+ @rm -f test-quota-util$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_quota_util_OBJECTS) $(test_quota_util_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-quota.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-count.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-dirsize.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-fs.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-imapc.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-maildir.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota_status-quota-status-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota_status-quota-status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rquota_xdr.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-quota-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 $@ $<
+
+quota_status-quota-status.o: quota-status.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status.o -MD -MP -MF $(DEPDIR)/quota_status-quota-status.Tpo -c -o quota_status-quota-status.o `test -f 'quota-status.c' || echo '$(srcdir)/'`quota-status.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status.Tpo $(DEPDIR)/quota_status-quota-status.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status.c' object='quota_status-quota-status.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) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status.o `test -f 'quota-status.c' || echo '$(srcdir)/'`quota-status.c
+
+quota_status-quota-status.obj: quota-status.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status.obj -MD -MP -MF $(DEPDIR)/quota_status-quota-status.Tpo -c -o quota_status-quota-status.obj `if test -f 'quota-status.c'; then $(CYGPATH_W) 'quota-status.c'; else $(CYGPATH_W) '$(srcdir)/quota-status.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status.Tpo $(DEPDIR)/quota_status-quota-status.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status.c' object='quota_status-quota-status.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) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status.obj `if test -f 'quota-status.c'; then $(CYGPATH_W) 'quota-status.c'; else $(CYGPATH_W) '$(srcdir)/quota-status.c'; fi`
+
+quota_status-quota-status-settings.o: quota-status-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status-settings.o -MD -MP -MF $(DEPDIR)/quota_status-quota-status-settings.Tpo -c -o quota_status-quota-status-settings.o `test -f 'quota-status-settings.c' || echo '$(srcdir)/'`quota-status-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status-settings.Tpo $(DEPDIR)/quota_status-quota-status-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status-settings.c' object='quota_status-quota-status-settings.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) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status-settings.o `test -f 'quota-status-settings.c' || echo '$(srcdir)/'`quota-status-settings.c
+
+quota_status-quota-status-settings.obj: quota-status-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status-settings.obj -MD -MP -MF $(DEPDIR)/quota_status-quota-status-settings.Tpo -c -o quota_status-quota-status-settings.obj `if test -f 'quota-status-settings.c'; then $(CYGPATH_W) 'quota-status-settings.c'; else $(CYGPATH_W) '$(srcdir)/quota-status-settings.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status-settings.Tpo $(DEPDIR)/quota_status-quota-status-settings.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status-settings.c' object='quota_status-quota-status-settings.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) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status-settings.obj `if test -f 'quota-status-settings.c'; then $(CYGPATH_W) 'quota-status-settings.c'; else $(CYGPATH_W) '$(srcdir)/quota-status-settings.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)$(pkglibexecdir)" "$(DESTDIR)$(doveadm_moduledir)" "$(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:
+
+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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/doveadm-quota.Plo
+ -rm -f ./$(DEPDIR)/quota-count.Plo
+ -rm -f ./$(DEPDIR)/quota-dict.Plo
+ -rm -f ./$(DEPDIR)/quota-dirsize.Plo
+ -rm -f ./$(DEPDIR)/quota-fs.Plo
+ -rm -f ./$(DEPDIR)/quota-imapc.Plo
+ -rm -f ./$(DEPDIR)/quota-maildir.Plo
+ -rm -f ./$(DEPDIR)/quota-plugin.Plo
+ -rm -f ./$(DEPDIR)/quota-storage.Plo
+ -rm -f ./$(DEPDIR)/quota-util.Plo
+ -rm -f ./$(DEPDIR)/quota.Plo
+ -rm -f ./$(DEPDIR)/quota_status-quota-status-settings.Po
+ -rm -f ./$(DEPDIR)/quota_status-quota-status.Po
+ -rm -f ./$(DEPDIR)/rquota_xdr.Plo
+ -rm -f ./$(DEPDIR)/test-quota-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-doveadm_moduleLTLIBRARIES \
+ install-moduleLTLIBRARIES install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS
+
+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)/doveadm-quota.Plo
+ -rm -f ./$(DEPDIR)/quota-count.Plo
+ -rm -f ./$(DEPDIR)/quota-dict.Plo
+ -rm -f ./$(DEPDIR)/quota-dirsize.Plo
+ -rm -f ./$(DEPDIR)/quota-fs.Plo
+ -rm -f ./$(DEPDIR)/quota-imapc.Plo
+ -rm -f ./$(DEPDIR)/quota-maildir.Plo
+ -rm -f ./$(DEPDIR)/quota-plugin.Plo
+ -rm -f ./$(DEPDIR)/quota-storage.Plo
+ -rm -f ./$(DEPDIR)/quota-util.Plo
+ -rm -f ./$(DEPDIR)/quota.Plo
+ -rm -f ./$(DEPDIR)/quota_status-quota-status-settings.Po
+ -rm -f ./$(DEPDIR)/quota_status-quota-status.Po
+ -rm -f ./$(DEPDIR)/rquota_xdr.Plo
+ -rm -f ./$(DEPDIR)/test-quota-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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-doveadm_moduleLTLIBRARIES \
+ clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ clean-noinstPROGRAMS clean-pkglibexecPROGRAMS 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-doveadm_moduleLTLIBRARIES 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-pkglibexecPROGRAMS \
+ 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-doveadm_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS \
+ uninstall-pkglibexecPROGRAMS
+
+.PRECIOUS: Makefile
+
+@HAVE_RQUOTA_TRUE@rquota_xdr.c: Makefile rquota.h
+@HAVE_RQUOTA_TRUE@ if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \
+@HAVE_RQUOTA_TRUE@ cp $(RQUOTA_X) $(top_builddir)/src/plugins/quota/; \
+@HAVE_RQUOTA_TRUE@ fi; \
+@HAVE_RQUOTA_TRUE@ (echo '#include "lib.h"'; \
+@HAVE_RQUOTA_TRUE@ echo '#undef FALSE'; \
+@HAVE_RQUOTA_TRUE@ echo '#undef TRUE'; \
+@HAVE_RQUOTA_TRUE@ echo '#include <rpc/rpc.h>'; \
+@HAVE_RQUOTA_TRUE@ $(RPCGEN) -c $(top_builddir)/src/plugins/quota/rquota.x | \
+@HAVE_RQUOTA_TRUE@ sed \
+@HAVE_RQUOTA_TRUE@ -e 's/IXDR_PUT/(void)IXDR_PUT/g' \
+@HAVE_RQUOTA_TRUE@ -e 's,!xdr_,0 == xdr_,' \
+@HAVE_RQUOTA_TRUE@ -e 's,/usr/include/rpcsvc/rquota.h,rquota.h,' \
+@HAVE_RQUOTA_TRUE@ -e 's/int32_t \*buf/int32_t *buf ATTR_UNUSED/' \
+@HAVE_RQUOTA_TRUE@ -e 's/^static char rcsid.*//' ) > rquota_xdr.c.tmp
+@HAVE_RQUOTA_TRUE@ cat $(RQUOTA_PRAGMAS_H) rquota_xdr.c.tmp > rquota_xdr.c
+
+@HAVE_RQUOTA_TRUE@rquota.h: Makefile $(RQUOTA_X)
+@HAVE_RQUOTA_TRUE@ $(RPCGEN) -h $(RQUOTA_X) > rquota.h.tmp
+@HAVE_RQUOTA_TRUE@ cat $(RQUOTA_PRAGMAS_H) rquota.h.tmp > rquota.h
+
+@HAVE_RQUOTA_TRUE@quota-fs.lo: rquota.h
+
+clean-generic:
+ if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \
+ rm -f $(top_builddir)/src/plugins/quota/rquota.x; \
+ fi; \
+ rm -f rquota_xdr.c rquota.h
+
+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/plugins/quota/doveadm-quota.c b/src/plugins/quota/doveadm-quota.c
new file mode 100644
index 0000000..8a42b22
--- /dev/null
+++ b/src/plugins/quota/doveadm-quota.c
@@ -0,0 +1,165 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "quota-plugin.h"
+#include "quota-private.h"
+#include "doveadm-print.h"
+#include "doveadm-mail.h"
+
+const char *doveadm_quota_plugin_version = DOVECOT_ABI_VERSION;
+
+void doveadm_quota_plugin_init(struct module *module);
+void doveadm_quota_plugin_deinit(void);
+
+static int cmd_quota_get_root(struct quota_root *root)
+{
+ const char *const *res;
+ const char *error;
+ uint64_t value, limit;
+ enum quota_get_result qret;
+ int ret = 0;
+
+ res = quota_root_get_resources(root);
+ for (; *res != NULL; res++) {
+ qret = quota_get_resource(root, "", *res, &value, &limit, &error);
+ doveadm_print(root->set->name);
+ doveadm_print(*res);
+ if (qret == QUOTA_GET_RESULT_LIMITED) {
+ doveadm_print_num(value);
+ doveadm_print_num(limit);
+ if (limit > 0)
+ doveadm_print_num(value*100 / limit);
+ else
+ doveadm_print("0");
+ } else if (qret == QUOTA_GET_RESULT_UNLIMITED) {
+ doveadm_print_num(value);
+ doveadm_print("-");
+ doveadm_print("0");
+ } else {
+ i_error("Failed to get quota resource %s: %s",
+ *res, error);
+ doveadm_print("error");
+ doveadm_print("error");
+ doveadm_print("error");
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int
+cmd_quota_get_run(struct doveadm_mail_cmd_context *ctx,
+ struct mail_user *user)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+ struct quota_root *const *root;
+
+ if (quser == NULL) {
+ i_error("Quota not enabled");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+
+ int ret = 0;
+ array_foreach(&quser->quota->roots, root)
+ if (cmd_quota_get_root(*root) < 0)
+ ret = -1;
+ if (ret < 0)
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP);
+ return ret;
+}
+
+static void cmd_quota_get_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ const char *const args[] ATTR_UNUSED)
+{
+ doveadm_print_header("root", "Quota name", 0);
+ doveadm_print_header("type", "Type", 0);
+ doveadm_print_header("value", "Value",
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY);
+ doveadm_print_header("limit", "Limit",
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY);
+ doveadm_print_header("percent", "%",
+ DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY);
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_quota_get_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_quota_get_run;
+ ctx->v.init = cmd_quota_get_init;
+ doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
+ return ctx;
+}
+
+static int
+cmd_quota_recalc_run(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
+ struct mail_user *user)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+ struct quota_root *const *root;
+ struct quota_transaction_context trans;
+
+ if (quser == NULL) {
+ i_error("Quota not enabled");
+ doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND);
+ return -1;
+ }
+
+ i_zero(&trans);
+ trans.quota = quser->quota;
+ trans.recalculate = QUOTA_RECALCULATE_FORCED;
+
+ array_foreach(&quser->quota->roots, root) {
+ const char *error;
+ if ((*root)->backend.v.update(*root, &trans, &error) < 0)
+ i_error("Recalculating quota failed: %s", error);
+ if ((*root)->backend.v.flush != NULL)
+ (*root)->backend.v.flush(*root);
+ }
+ return 0;
+}
+
+static struct doveadm_mail_cmd_context *
+cmd_quota_recalc_alloc(void)
+{
+ struct doveadm_mail_cmd_context *ctx;
+
+ ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
+ ctx->v.run = cmd_quota_recalc_run;
+ return ctx;
+}
+
+static struct doveadm_cmd_ver2 quota_commands[] = {
+ {
+ .name = "quota get",
+ .usage = "",
+ .mail_cmd = cmd_quota_get_alloc,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAMS_END
+ },
+ {
+ .name = "quota recalc",
+ .usage = "",
+ .mail_cmd = cmd_quota_recalc_alloc,
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_MAIL_COMMON
+DOVEADM_CMD_PARAMS_END
+ }
+};
+
+void doveadm_quota_plugin_init(struct module *module ATTR_UNUSED)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(quota_commands); i++)
+ doveadm_cmd_register_ver2(&quota_commands[i]);
+}
+
+void doveadm_quota_plugin_deinit(void)
+{
+}
diff --git a/src/plugins/quota/quota-count.c b/src/plugins/quota/quota-count.c
new file mode 100644
index 0000000..00e25e6
--- /dev/null
+++ b/src/plugins/quota/quota-count.c
@@ -0,0 +1,400 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mailbox-list-iter.h"
+#include "quota-private.h"
+
+struct count_quota_root {
+ struct quota_root root;
+
+ struct timeval cache_timeval;
+ uint64_t cached_bytes, cached_count;
+};
+
+struct quota_mailbox_iter {
+ struct quota_root *root;
+ struct mail_namespace *ns;
+ unsigned int ns_idx;
+ struct mailbox_list_iterate_context *iter;
+ struct mailbox_info info;
+ const char *error;
+};
+
+extern struct quota_backend quota_backend_count;
+
+static int
+quota_count_mailbox(struct quota_root *root, struct mail_namespace *ns,
+ const char *vname, uint64_t *bytes, uint64_t *count,
+ enum quota_get_result *error_result_r,
+ const char **error_r)
+{
+ struct quota_rule *rule;
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+ struct mailbox_status status;
+ enum mail_error error;
+ const char *errstr;
+ int ret;
+
+ rule = quota_root_rule_find(root->set, vname);
+ if (rule != NULL && rule->ignore) {
+ /* mailbox not included in quota */
+ return 0;
+ }
+
+ box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY);
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0) {
+ /* quota doesn't exist for this mailbox/storage */
+ ret = 0;
+ } else if (mailbox_get_metadata(box, root->quota->set->vsizes ?
+ MAILBOX_METADATA_VIRTUAL_SIZE :
+ MAILBOX_METADATA_PHYSICAL_SIZE,
+ &metadata) < 0 ||
+ mailbox_get_status(box, STATUS_MESSAGES, &status) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_TEMP) {
+ *error_r = t_strdup_printf(
+ "Couldn't get size of mailbox %s: %s",
+ vname, errstr);
+ *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR;
+ ret = -1;
+ } else if (error == MAIL_ERROR_INUSE) {
+ /* started on background. don't log an error. */
+ *error_r = t_strdup_printf(
+ "Ongoing quota calculation blocked getting size of %s: %s",
+ vname, errstr);
+ *error_result_r = QUOTA_GET_RESULT_BACKGROUND_CALC;
+ ret = -1;
+ } else {
+ /* non-temporary error, e.g. ACLs denied access. */
+ ret = 0;
+ }
+ } else {
+ ret = 0;
+ *bytes += root->quota->set->vsizes ?
+ metadata.virtual_size : metadata.physical_size;
+ *count += status.messages;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static struct quota_mailbox_iter *
+quota_mailbox_iter_begin(struct quota_root *root)
+{
+ struct quota_mailbox_iter *iter;
+
+ iter = i_new(struct quota_mailbox_iter, 1);
+ iter->root = root;
+ iter->error = "";
+ return iter;
+}
+
+static int
+quota_mailbox_iter_deinit(struct quota_mailbox_iter **_iter,
+ const char **error_r)
+{
+ struct quota_mailbox_iter *iter = *_iter;
+ int ret = *iter->error != '\0' ? -1 : 0;
+
+ *_iter = NULL;
+
+ const char *error2 = "";
+ if (iter->iter != NULL) {
+ if (mailbox_list_iter_deinit(&iter->iter) < 0) {
+ error2 = t_strdup_printf(
+ "Listing namespace '%s' failed: %s",
+ iter->ns->prefix,
+ mailbox_list_get_last_internal_error(iter->ns->list, NULL));
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ const char *separator =
+ *iter->error != '\0' && *error2 != '\0' ? " and " : "";
+ *error_r = t_strdup_printf("%s%s%s",
+ iter->error, separator, error2);
+ }
+ i_free(iter);
+ return ret;
+}
+
+static const struct mailbox_info *
+quota_mailbox_iter_next(struct quota_mailbox_iter *iter)
+{
+ struct mail_namespace *const *namespaces;
+ const struct mailbox_info *info;
+ unsigned int count;
+
+ if (iter->iter == NULL) {
+ namespaces = array_get(&iter->root->quota->namespaces, &count);
+ do {
+ if (iter->ns_idx >= count)
+ return NULL;
+
+ iter->ns = namespaces[iter->ns_idx++];
+ } while (!quota_root_is_namespace_visible(iter->root, iter->ns));
+ iter->iter = mailbox_list_iter_init(iter->ns->list, "*",
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES);
+ }
+ while ((info = mailbox_list_iter_next(iter->iter)) != NULL) {
+ if ((info->flags & (MAILBOX_NONEXISTENT |
+ MAILBOX_NOSELECT)) == 0)
+ return info;
+ }
+ if (mailbox_list_iter_deinit(&iter->iter) < 0) {
+ iter->error = t_strdup_printf(
+ "Listing namespace '%s' failed: %s",
+ iter->ns->prefix,
+ mailbox_list_get_last_internal_error(iter->ns->list, NULL));
+ }
+ if (iter->ns->prefix_len > 0 &&
+ (iter->ns->prefix_len != 6 ||
+ strncasecmp(iter->ns->prefix, "INBOX", 5) != 0)) {
+ /* if the namespace prefix itself exists, count it also */
+ iter->info.ns = iter->ns;
+ iter->info.vname = t_strndup(iter->ns->prefix,
+ iter->ns->prefix_len-1);
+ return &iter->info;
+ }
+ /* try the next namespace */
+ return quota_mailbox_iter_next(iter);
+}
+
+int quota_count(struct quota_root *root, uint64_t *bytes_r, uint64_t *count_r,
+ enum quota_get_result *error_result_r, const char **error_r)
+{
+ struct quota_mailbox_iter *iter;
+ const struct mailbox_info *info;
+ const char *error1 = "", *error2 = "";
+ int ret = 1;
+
+ *bytes_r = *count_r = 0;
+ if (root->recounting)
+ return 0;
+ root->recounting = TRUE;
+
+ struct event_reason *reason = event_reason_begin("quota:count");
+
+ iter = quota_mailbox_iter_begin(root);
+ while ((info = quota_mailbox_iter_next(iter)) != NULL) {
+ if (quota_count_mailbox(root, info->ns, info->vname,
+ bytes_r, count_r, error_result_r,
+ &error1) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ if (quota_mailbox_iter_deinit(&iter, &error2) < 0) {
+ *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR;
+ ret = -1;
+ }
+ if (ret < 0) {
+ const char *separator =
+ *error1 != '\0' && *error2 != '\0' ? " and " : "";
+ *error_r = t_strconcat(error1, separator, error2, NULL);
+ }
+ event_reason_end(&reason);
+ root->recounting = FALSE;
+ return ret;
+}
+
+static enum quota_get_result
+quota_count_cached(struct count_quota_root *root,
+ uint64_t *bytes_r, uint64_t *count_r,
+ const char **error_r)
+{
+ int ret;
+
+ if (root->cache_timeval.tv_usec == ioloop_timeval.tv_usec &&
+ root->cache_timeval.tv_sec == ioloop_timeval.tv_sec &&
+ ioloop_timeval.tv_sec != 0) {
+ *bytes_r = root->cached_bytes;
+ *count_r = root->cached_count;
+ return QUOTA_GET_RESULT_LIMITED;
+ }
+
+ enum quota_get_result error_res;
+ ret = quota_count(&root->root, bytes_r, count_r, &error_res, error_r);
+ if (ret < 0) {
+ return error_res;
+ } else if (ret > 0) {
+ root->cache_timeval = ioloop_timeval;
+ root->cached_bytes = *bytes_r;
+ root->cached_count = *count_r;
+ }
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static struct quota_root *count_quota_alloc(void)
+{
+ struct count_quota_root *root;
+
+ root = i_new(struct count_quota_root, 1);
+ return &root->root;
+}
+
+static int count_quota_init(struct quota_root *root, const char *args,
+ const char **error_r)
+{
+ if (!root->quota->set->vsizes) {
+ *error_r = "quota count backend requires quota_vsizes=yes";
+ return -1;
+ }
+ event_set_append_log_prefix(root->backend.event, "quota-count: ");
+
+ root->auto_updating = TRUE;
+ return quota_root_default_init(root, args, error_r);
+}
+
+static void count_quota_deinit(struct quota_root *_root)
+{
+ i_free(_root);
+}
+
+static const char *const *
+count_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+ static const char *resources[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES, QUOTA_NAME_MESSAGES, NULL
+ };
+ return resources;
+}
+
+static enum quota_get_result
+count_quota_get_resource(struct quota_root *_root,
+ const char *name, uint64_t *value_r,
+ const char **error_r)
+{
+ struct count_quota_root *root = (struct count_quota_root *)_root;
+ uint64_t bytes, count;
+ enum quota_get_result ret;
+
+ ret = quota_count_cached(root, &bytes, &count, error_r);
+ if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR)
+ return ret;
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ *value_r = bytes;
+ else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0)
+ *value_r = count;
+ else {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static int quota_count_recalculate_box(struct mailbox *box,
+ const char **error_r)
+{
+ struct mail_index_transaction *trans;
+ struct mailbox_metadata metadata;
+ struct mailbox_index_vsize vsize_hdr;
+ const char *errstr;
+ enum mail_error error;
+
+ if (mailbox_open(box) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_TEMP) {
+ /* non-temporary error, e.g. ACLs denied access. */
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "Couldn't open mailbox %s: %s", box->vname, errstr);
+ return -1;
+ }
+
+ /* reset the vsize header first */
+ trans = mail_index_transaction_begin(box->view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ i_zero(&vsize_hdr);
+ mail_index_update_header_ext(trans, box->vsize_hdr_ext_id,
+ 0, &vsize_hdr, sizeof(vsize_hdr));
+ if (mail_index_transaction_commit(&trans) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't commit mail index transaction for %s: %s",
+ box->vname,
+ mail_index_get_error_message(box->view->index));
+ return -1;
+ }
+ /* getting the vsize now forces its recalculation */
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_VIRTUAL_SIZE,
+ &metadata) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't get mailbox %s vsize: %s", box->vname,
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+ /* call sync to write the change to mailbox list index */
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't sync mailbox %s: %s", box->vname,
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int quota_count_recalculate(struct quota_root *root,
+ const char **error_r)
+{
+ struct event_reason *reason;
+ struct quota_mailbox_iter *iter;
+ const struct mailbox_info *info;
+ struct mailbox *box;
+ int ret = 0;
+ const char *error1 = "", *error2 = "";
+
+ reason = event_reason_begin("quota:recalculate");
+
+ iter = quota_mailbox_iter_begin(root);
+ while ((info = quota_mailbox_iter_next(iter)) != NULL) {
+ box = mailbox_alloc(info->ns->list, info->vname, 0);
+ if (quota_count_recalculate_box(box, &error1) < 0)
+ ret = -1;
+ mailbox_free(&box);
+ }
+ if (quota_mailbox_iter_deinit(&iter, &error2) < 0)
+ ret = -1;
+ if (ret < 0) {
+ const char *separator =
+ *error1 != '\0' && *error2 != '\0' ? " and " : "";
+ *error_r = t_strdup_printf(
+ "quota-count: recalculate failed: %s%s%s",
+ error1, separator, error2);
+ }
+ event_reason_end(&reason);
+ return ret;
+}
+
+static int
+count_quota_update(struct quota_root *root,
+ struct quota_transaction_context *ctx,
+ const char **error_r)
+{
+ struct count_quota_root *croot = (struct count_quota_root *)root;
+
+ croot->cache_timeval.tv_sec = 0;
+ if (ctx->recalculate == QUOTA_RECALCULATE_FORCED) {
+ if (quota_count_recalculate(root, error_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+struct quota_backend quota_backend_count = {
+ .name = "count",
+
+ .v = {
+ .alloc = count_quota_alloc,
+ .init = count_quota_init,
+ .deinit = count_quota_deinit,
+ .get_resources = count_quota_root_get_resources,
+ .get_resource = count_quota_get_resource,
+ .update = count_quota_update,
+ }
+};
diff --git a/src/plugins/quota/quota-dict.c b/src/plugins/quota/quota-dict.c
new file mode 100644
index 0000000..02e444a
--- /dev/null
+++ b/src/plugins/quota/quota-dict.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "dict.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "quota-private.h"
+
+
+#define DICT_QUOTA_CURRENT_PATH DICT_PATH_PRIVATE"quota/"
+#define DICT_QUOTA_CURRENT_BYTES_PATH DICT_QUOTA_CURRENT_PATH"storage"
+#define DICT_QUOTA_CURRENT_COUNT_PATH DICT_QUOTA_CURRENT_PATH"messages"
+
+struct dict_quota_root {
+ struct quota_root root;
+ struct dict *dict;
+ struct timeout *to_update;
+ bool disable_unset;
+};
+
+extern struct quota_backend quota_backend_dict;
+
+static struct quota_root *dict_quota_alloc(void)
+{
+ struct dict_quota_root *root;
+
+ root = i_new(struct dict_quota_root, 1);
+ return &root->root;
+}
+
+static void handle_nounset_param(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ ((struct dict_quota_root *)_root)->disable_unset = TRUE;
+}
+
+static int dict_quota_init(struct quota_root *_root, const char *args,
+ const char **error_r)
+{
+ struct dict_quota_root *root = (struct dict_quota_root *)_root;
+ struct dict_settings set;
+ const char *username, *p, *error;
+
+ event_set_append_log_prefix(_root->backend.event, "quota-dict: ");
+
+ const struct quota_param_parser dict_params[] = {
+ {.param_name = "no-unset", .param_handler = handle_nounset_param},
+ quota_param_hidden, quota_param_ignoreunlimited, quota_param_noenforcing, quota_param_ns,
+ {.param_name = NULL}
+ };
+
+ p = args == NULL ? NULL : strchr(args, ':');
+ if (p == NULL) {
+ *error_r = "URI missing from parameters";
+ return -1;
+ }
+
+ username = t_strdup_until(args, p);
+ args = p+1;
+
+ if (quota_parse_parameters(_root, &args, error_r, dict_params, FALSE) < 0)
+ i_unreached();
+
+ if (*username == '\0')
+ username = _root->quota->user->username;
+
+ e_debug(_root->backend.event, "user=%s, uri=%s, noenforcing=%d",
+ username, args, _root->no_enforcing ? 1 : 0);
+
+ /* FIXME: we should use 64bit integer as datatype instead but before
+ it can actually be used don't bother */
+ i_zero(&set);
+ set.base_dir = _root->quota->user->set->base_dir;
+ set.event_parent = _root->quota->user->event;
+ if (dict_init(args, &set, &root->dict, &error) < 0) {
+ *error_r = t_strdup_printf("dict_init(%s) failed: %s", args, error);
+ return -1;
+ }
+ return 0;
+}
+
+static void dict_quota_deinit(struct quota_root *_root)
+{
+ struct dict_quota_root *root = (struct dict_quota_root *)_root;
+
+ i_assert(root->to_update == NULL);
+
+ if (root->dict != NULL) {
+ dict_wait(root->dict);
+ dict_deinit(&root->dict);
+ }
+ i_free(root);
+}
+
+static const char *const *
+dict_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+ static const char *resources[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES, QUOTA_NAME_MESSAGES, NULL
+ };
+
+ return resources;
+}
+
+static enum quota_get_result
+dict_quota_count(struct dict_quota_root *root,
+ bool want_bytes, uint64_t *value_r,
+ const char **error_r)
+{
+ struct dict_transaction_context *dt;
+ struct event_reason *reason;
+ uint64_t bytes, count;
+ enum quota_get_result error_res;
+ const struct dict_op_settings *set;
+
+ reason = event_reason_begin("quota:recalculate");
+ int ret = quota_count(&root->root, &bytes, &count, &error_res, error_r);
+ event_reason_end(&reason);
+ if (ret < 0)
+ return error_res;
+
+ set = mail_user_get_dict_op_settings(root->root.quota->user);
+ dt = dict_transaction_begin(root->dict, set);
+ /* these unsets are mainly necessary for pgsql, because its
+ trigger otherwise increases quota without deleting it.
+ but some people with other databases want to store the
+ quota usage among other data in the same row, which
+ shouldn't be deleted. */
+ if (!root->disable_unset) {
+ dict_unset(dt, DICT_QUOTA_CURRENT_BYTES_PATH);
+ dict_unset(dt, DICT_QUOTA_CURRENT_COUNT_PATH);
+ }
+ dict_set(dt, DICT_QUOTA_CURRENT_BYTES_PATH, dec2str(bytes));
+ dict_set(dt, DICT_QUOTA_CURRENT_COUNT_PATH, dec2str(count));
+
+ e_debug(root->root.backend.event, "Quota recalculated: "
+ "count=%"PRIu64" bytes=%"PRIu64, count, bytes);
+
+ dict_transaction_commit_async_nocallback(&dt);
+ *value_r = want_bytes ? bytes : count;
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static enum quota_get_result
+dict_quota_get_resource(struct quota_root *_root,
+ const char *name, uint64_t *value_r,
+ const char **error_r)
+{
+ struct dict_quota_root *root = (struct dict_quota_root *)_root;
+ bool want_bytes;
+ int ret;
+ const struct dict_op_settings *set;
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ want_bytes = TRUE;
+ else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0)
+ want_bytes = FALSE;
+ else {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+
+ set = mail_user_get_dict_op_settings(root->root.quota->user);
+ const char *key, *value, *error;
+ key = want_bytes ? DICT_QUOTA_CURRENT_BYTES_PATH :
+ DICT_QUOTA_CURRENT_COUNT_PATH;
+ ret = dict_lookup(root->dict, set, unsafe_data_stack_pool,
+ key, &value, &error);
+ if (ret < 0) {
+ *error_r = t_strdup_printf(
+ "dict_lookup(%s) failed: %s", key, error);
+ *value_r = 0;
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+ }
+
+ intmax_t tmp;
+ /* recalculate quota if it's negative or if it wasn't found */
+ if (ret == 0 || str_to_intmax(value, &tmp) < 0)
+ tmp = -1;
+ if (tmp >= 0)
+ *value_r = tmp;
+ else
+ return dict_quota_count(root, want_bytes, value_r, error_r);
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static void dict_quota_recalc_timeout(struct dict_quota_root *root)
+{
+ uint64_t value;
+ const char *error;
+
+ timeout_remove(&root->to_update);
+ if (dict_quota_count(root, TRUE, &value, &error)
+ <= QUOTA_GET_RESULT_INTERNAL_ERROR)
+ e_error(root->root.backend.event,
+ "Recalculation failed: %s", error);
+}
+
+static void dict_quota_update_callback(const struct dict_commit_result *result,
+ struct dict_quota_root *root)
+{
+ if (result->ret == 0) {
+ /* row doesn't exist, need to recalculate it */
+ if (root->to_update == NULL)
+ root->to_update = timeout_add_short(0, dict_quota_recalc_timeout, root);
+ } else if (result->ret < 0) {
+ e_error(root->root.backend.event,
+ "Quota update failed: %s "
+ "- Quota is now desynced", result->error);
+ }
+}
+
+static int
+dict_quota_update(struct quota_root *_root,
+ struct quota_transaction_context *ctx,
+ const char **error_r)
+{
+ struct dict_quota_root *root = (struct dict_quota_root *) _root;
+ struct dict_transaction_context *dt;
+ uint64_t value;
+ const struct dict_op_settings *set;
+
+ if (ctx->recalculate != QUOTA_RECALCULATE_DONT) {
+ if (dict_quota_count(root, TRUE, &value, error_r)
+ <= QUOTA_GET_RESULT_INTERNAL_ERROR)
+ return -1;
+ } else {
+ set = mail_user_get_dict_op_settings(root->root.quota->user);
+ dt = dict_transaction_begin(root->dict, set);
+ if (ctx->bytes_used != 0) {
+ dict_atomic_inc(dt, DICT_QUOTA_CURRENT_BYTES_PATH,
+ ctx->bytes_used);
+ }
+ if (ctx->count_used != 0) {
+ dict_atomic_inc(dt, DICT_QUOTA_CURRENT_COUNT_PATH,
+ ctx->count_used);
+ }
+ dict_transaction_no_slowness_warning(dt);
+ dict_transaction_commit_async(&dt, dict_quota_update_callback,
+ root);
+ }
+ return 0;
+}
+
+static void dict_quota_flush(struct quota_root *_root)
+{
+ struct dict_quota_root *root = (struct dict_quota_root *)_root;
+
+ dict_wait(root->dict);
+ if (root->to_update != NULL) {
+ dict_quota_recalc_timeout(root);
+ dict_wait(root->dict);
+ }
+}
+
+struct quota_backend quota_backend_dict = {
+ .name = "dict",
+
+ .v = {
+ .alloc = dict_quota_alloc,
+ .init = dict_quota_init,
+ .deinit = dict_quota_deinit,
+ .get_resources = dict_quota_root_get_resources,
+ .get_resource = dict_quota_get_resource,
+ .update = dict_quota_update,
+ .flush = dict_quota_flush,
+ }
+};
diff --git a/src/plugins/quota/quota-dirsize.c b/src/plugins/quota/quota-dirsize.c
new file mode 100644
index 0000000..a8305d8
--- /dev/null
+++ b/src/plugins/quota/quota-dirsize.c
@@ -0,0 +1,232 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/* Quota reporting based on simply summing sizes of all files in mailbox
+ together. */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "quota-private.h"
+
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+struct quota_count_path {
+ const char *path;
+ bool is_file;
+};
+ARRAY_DEFINE_TYPE(quota_count_path, struct quota_count_path);
+
+extern struct quota_backend quota_backend_dirsize;
+
+static struct quota_root *dirsize_quota_alloc(void)
+{
+ return i_new(struct quota_root, 1);
+}
+
+static int dirsize_quota_init(struct quota_root *root, const char *args,
+ const char **error_r)
+{
+ root->auto_updating = TRUE;
+ event_set_append_log_prefix(root->backend.event, "quota-dirsize: ");
+ return quota_root_default_init(root, args, error_r);
+}
+
+static void dirsize_quota_deinit(struct quota_root *_root)
+{
+ i_free(_root);
+}
+
+static const char *const *
+dirsize_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+ static const char *resources[] = { QUOTA_NAME_STORAGE_KILOBYTES, NULL };
+
+ return resources;
+}
+
+static int get_dir_usage(const char *dir, uint64_t *value,
+ const char **error_r)
+{
+ DIR *dirp;
+ string_t *path;
+ struct dirent *d;
+ struct stat st;
+ unsigned int path_pos;
+ int ret;
+
+ dirp = opendir(dir);
+ if (dirp == NULL) {
+ if (errno == ENOENT)
+ return 0;
+
+ *error_r = t_strdup_printf("opendir(%s) failed: %m", dir);
+ return -1;
+ }
+
+ path = t_str_new(128);
+ str_append(path, dir);
+ str_append_c(path, '/');
+ path_pos = str_len(path);
+
+ ret = 0;
+ 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;
+ }
+
+ str_truncate(path, path_pos);
+ str_append(path, d->d_name);
+
+ if (lstat(str_c(path), &st) < 0) {
+ if (errno == ENOENT)
+ continue;
+
+ *error_r = t_strdup_printf("lstat(%s) failed: %m", dir);
+ ret = -1;
+ break;
+ } else if (S_ISDIR(st.st_mode)) {
+ if (get_dir_usage(str_c(path), value, error_r) < 0) {
+ ret = -1;
+ break;
+ }
+ } else {
+ *value += st.st_size;
+ }
+ }
+
+ (void)closedir(dirp);
+ return ret;
+}
+
+static int get_usage(const char *path, bool is_file, uint64_t *value_r,
+ const char **error_r)
+{
+ struct stat st;
+
+ if (is_file) {
+ if (lstat(path, &st) < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ *error_r = t_strdup_printf("lstat(%s) failed: %m", path);
+ return -1;
+ }
+ *value_r += st.st_size;
+ } else {
+ if (get_dir_usage(path, value_r, error_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void quota_count_path_add(ARRAY_TYPE(quota_count_path) *paths,
+ const char *path, bool is_file)
+{
+ struct quota_count_path *count_path;
+ unsigned int i, count;
+ size_t path_len;
+
+ path_len = strlen(path);
+ count_path = array_get_modifiable(paths, &count);
+ for (i = 0; i < count; ) {
+ if (strncmp(count_path[i].path, path,
+ strlen(count_path[i].path)) == 0) {
+ /* this path has already been counted */
+ return;
+ }
+ if (strncmp(count_path[i].path, path, path_len) == 0 &&
+ count_path[i].path[path_len] == '/') {
+ /* the new path contains the existing path.
+ drop it and see if there are more to drop. */
+ array_delete(paths, i, 1);
+ count_path = array_get_modifiable(paths, &count);
+ } else {
+ i++;
+ }
+ }
+
+ count_path = array_append_space(paths);
+ count_path->path = t_strdup(path);
+ count_path->is_file = is_file;
+}
+
+static int
+get_quota_root_usage(struct quota_root *root, uint64_t *value_r,
+ const char **error_r)
+{
+ struct mail_namespace *const *namespaces;
+ ARRAY_TYPE(quota_count_path) paths;
+ const struct quota_count_path *count_paths;
+ unsigned int i, count;
+ const char *path;
+ bool is_file;
+
+ t_array_init(&paths, 8);
+ namespaces = array_get(&root->quota->namespaces, &count);
+ for (i = 0; i < count; i++) {
+ if (!quota_root_is_namespace_visible(root, namespaces[i]))
+ continue;
+
+ is_file = mail_storage_is_mailbox_file(namespaces[i]->storage);
+ if (mailbox_list_get_root_path(namespaces[i]->list,
+ MAILBOX_LIST_PATH_TYPE_DIR, &path))
+ quota_count_path_add(&paths, path, FALSE);
+
+ /* INBOX may be in different path. */
+ if (mailbox_list_get_path(namespaces[i]->list, "INBOX",
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &path) > 0)
+ quota_count_path_add(&paths, path, is_file);
+ }
+
+ /* now sum up the found paths */
+ *value_r = 0;
+ count_paths = array_get(&paths, &count);
+ for (i = 0; i < count; i++) {
+ if (get_usage(count_paths[i].path, count_paths[i].is_file,
+ value_r, error_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static enum quota_get_result
+dirsize_quota_get_resource(struct quota_root *_root, const char *name,
+ uint64_t *value_r, const char **error_r)
+{
+ int ret;
+
+ if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) != 0) {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+
+ ret = get_quota_root_usage(_root, value_r, error_r);
+
+ return ret < 0 ? QUOTA_GET_RESULT_INTERNAL_ERROR : QUOTA_GET_RESULT_LIMITED;
+}
+
+static int
+dirsize_quota_update(struct quota_root *root ATTR_UNUSED,
+ struct quota_transaction_context *ctx ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return 0;
+}
+
+struct quota_backend quota_backend_dirsize = {
+ .name = "dirsize",
+
+ .v = {
+ .alloc = dirsize_quota_alloc,
+ .init = dirsize_quota_init,
+ .deinit = dirsize_quota_deinit,
+ .get_resources = dirsize_quota_root_get_resources,
+ .get_resource = dirsize_quota_get_resource,
+ .update = dirsize_quota_update,
+ }
+};
diff --git a/src/plugins/quota/quota-fs.c b/src/plugins/quota/quota-fs.c
new file mode 100644
index 0000000..57620c1
--- /dev/null
+++ b/src/plugins/quota/quota-fs.c
@@ -0,0 +1,970 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/* Only for reporting filesystem quota */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hostpid.h"
+#include "mountpoint.h"
+#include "quota-private.h"
+#include "quota-fs.h"
+
+#ifdef HAVE_FS_QUOTA
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#ifdef HAVE_LINUX_DQBLK_XFS_H
+# include <linux/dqblk_xfs.h>
+# define HAVE_XFS_QUOTA
+#elif defined (HAVE_XFS_XQM_H)
+# include <xfs/xqm.h> /* CentOS 4.x at least uses this */
+# define HAVE_XFS_QUOTA
+#endif
+
+#ifdef HAVE_RQUOTA
+# include "rquota.h"
+# define RQUOTA_GETQUOTA_TIMEOUT_SECS 10
+#endif
+
+#ifndef DEV_BSIZE
+# ifdef DQBSIZE
+# define DEV_BSIZE DQBSIZE /* AIX */
+# else
+# define DEV_BSIZE 512
+# endif
+#endif
+
+#ifdef HAVE_STRUCT_DQBLK_CURSPACE
+# define dqb_curblocks dqb_curspace
+#endif
+
+/* Very old sys/quota.h doesn't define _LINUX_QUOTA_VERSION at all, which means
+ it supports only v1 quota. However, new sys/quota.h (glibc 2.25) removes
+ support for v1 entirely and again it doesn't define it. I guess we can just
+ assume v2 now, and if someone still wants v1 support they can add
+ -D_LINUX_QUOTA_VERSION=1 to CFLAGS. */
+#ifndef _LINUX_QUOTA_VERSION
+# define _LINUX_QUOTA_VERSION 2
+#endif
+
+#define mount_type_is_nfs(mount) \
+ (strcmp((mount)->type, "nfs") == 0 || \
+ strcmp((mount)->type, "nfs4") == 0)
+
+struct fs_quota_mountpoint {
+ int refcount;
+
+ char *mount_path;
+ char *device_path;
+ char *type;
+ unsigned int block_size;
+
+#ifdef FS_QUOTA_SOLARIS
+ int fd;
+ char *path;
+#endif
+};
+
+struct fs_quota_root {
+ struct quota_root root;
+ char *storage_mount_path;
+
+ uid_t uid;
+ gid_t gid;
+ struct fs_quota_mountpoint *mount;
+
+ bool inode_per_mail:1;
+ bool user_disabled:1;
+ bool group_disabled:1;
+#ifdef FS_QUOTA_NETBSD
+ struct quotahandle *qh;
+#endif
+};
+
+extern struct quota_backend quota_backend_fs;
+
+static struct quota_root *fs_quota_alloc(void)
+{
+ struct fs_quota_root *root;
+
+ root = i_new(struct fs_quota_root, 1);
+ root->uid = geteuid();
+ root->gid = getegid();
+
+ return &root->root;
+}
+
+static void handle_user_param(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ ((struct fs_quota_root *)_root)->group_disabled = TRUE;
+}
+
+static void handle_group_param(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ ((struct fs_quota_root *)_root)->user_disabled = TRUE;
+}
+
+static void handle_inode_param(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ ((struct fs_quota_root *)_root)->inode_per_mail = TRUE;
+}
+
+static void handle_mount_param(struct quota_root *_root, const char *param_value)
+{
+ struct fs_quota_root *root = (struct fs_quota_root *)_root;
+ i_free(root->storage_mount_path);
+ root->storage_mount_path = i_strdup(param_value);
+}
+
+static int fs_quota_init(struct quota_root *_root, const char *args,
+ const char **error_r)
+{
+ const struct quota_param_parser fs_params[] = {
+ {.param_name = "user", .param_handler = handle_user_param},
+ {.param_name = "group", .param_handler = handle_group_param},
+ {.param_name = "mount=", .param_handler = handle_mount_param},
+ {.param_name = "inode_per_mail", .param_handler = handle_inode_param},
+ quota_param_hidden, quota_param_noenforcing, quota_param_ns,
+ {.param_name = NULL}
+ };
+
+ event_set_append_log_prefix(_root->backend.event, "quota-fs: ");
+
+ if (quota_parse_parameters(_root, &args, error_r, fs_params, TRUE) < 0)
+ return -1;
+ _root->auto_updating = TRUE;
+ return 0;
+}
+
+static void fs_quota_mountpoint_free(struct fs_quota_mountpoint *mount)
+{
+ if (--mount->refcount > 0)
+ return;
+
+#ifdef FS_QUOTA_SOLARIS
+ i_close_fd_path(&mount->fd, mount->path);
+ i_free(mount->path);
+#endif
+
+ i_free(mount->device_path);
+ i_free(mount->mount_path);
+ i_free(mount->type);
+ i_free(mount);
+}
+
+static void fs_quota_deinit(struct quota_root *_root)
+{
+ struct fs_quota_root *root = (struct fs_quota_root *)_root;
+
+ if (root->mount != NULL)
+ fs_quota_mountpoint_free(root->mount);
+ i_free(root->storage_mount_path);
+ i_free(root);
+}
+
+static struct fs_quota_mountpoint *fs_quota_mountpoint_get(const char *dir)
+{
+ struct fs_quota_mountpoint *mount;
+ struct mountpoint point;
+ int ret;
+
+ ret = mountpoint_get(dir, default_pool, &point);
+ if (ret <= 0)
+ return NULL;
+
+ mount = i_new(struct fs_quota_mountpoint, 1);
+ mount->refcount = 1;
+ mount->device_path = point.device_path;
+ mount->mount_path = point.mount_path;
+ mount->type = point.type;
+ mount->block_size = point.block_size;
+#ifdef FS_QUOTA_SOLARIS
+ mount->fd = -1;
+#endif
+
+ if (mount_type_is_nfs(mount)) {
+ if (strchr(mount->device_path, ':') == NULL) {
+ e_error(quota_backend_fs.event,
+ "%s is not a valid NFS device path",
+ mount->device_path);
+ fs_quota_mountpoint_free(mount);
+ return NULL;
+ }
+ }
+ return mount;
+}
+
+#define QUOTA_ROOT_MATCH(root, mount) \
+ ((root)->root.backend.name == quota_backend_fs.name && \
+ ((root)->storage_mount_path == NULL || \
+ strcmp((root)->storage_mount_path, (mount)->mount_path) == 0))
+
+static struct fs_quota_root *
+fs_quota_root_find_mountpoint(struct quota *quota,
+ const struct fs_quota_mountpoint *mount)
+{
+ struct quota_root *const *roots;
+ struct fs_quota_root *empty = NULL;
+ unsigned int i, count;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ struct fs_quota_root *root = (struct fs_quota_root *)roots[i];
+ if (QUOTA_ROOT_MATCH(root, mount)) {
+ if (root->mount == NULL)
+ empty = root;
+ else if (strcmp(root->mount->mount_path,
+ mount->mount_path) == 0)
+ return root;
+ }
+ }
+ return empty;
+}
+
+static void
+fs_quota_mount_init(struct fs_quota_root *root,
+ struct fs_quota_mountpoint *mount, const char *dir)
+{
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+#ifdef FS_QUOTA_SOLARIS
+#ifdef HAVE_RQUOTA
+ if (mount_type_is_nfs(mount)) {
+ /* using rquota for this mount */
+ } else
+#endif
+ if (mount->path == NULL) {
+ mount->path = i_strconcat(mount->mount_path, "/quotas", NULL);
+ mount->fd = open(mount->path, O_RDONLY);
+ if (mount->fd == -1 && errno != ENOENT)
+ e_error(root->root.backend.event,
+ "open(%s) failed: %m", mount->path);
+ }
+#endif
+ root->mount = mount;
+
+ e_debug(root->root.backend.event, "fs quota add mailbox dir = %s", dir);
+ e_debug(root->root.backend.event, "fs quota block device = %s", mount->device_path);
+ e_debug(root->root.backend.event, "fs quota mount point = %s", mount->mount_path);
+ e_debug(root->root.backend.event, "fs quota mount type = %s", mount->type);
+
+ /* if there are more unused quota roots, copy this mount to them */
+ roots = array_get(&root->root.quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ root = (struct fs_quota_root *)roots[i];
+ if (QUOTA_ROOT_MATCH(root, mount) && root->mount == NULL) {
+ mount->refcount++;
+ root->mount = mount;
+ }
+ }
+}
+
+static void fs_quota_add_missing_mounts(struct quota *quota)
+{
+ struct fs_quota_mountpoint *mount;
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ struct fs_quota_root *root = (struct fs_quota_root *)roots[i];
+
+ if (root->root.backend.name != quota_backend_fs.name ||
+ root->storage_mount_path == NULL || root->mount != NULL)
+ continue;
+
+ mount = fs_quota_mountpoint_get(root->storage_mount_path);
+ if (mount != NULL) {
+ fs_quota_mount_init(root, mount,
+ root->storage_mount_path);
+ }
+ }
+}
+
+static void fs_quota_namespace_added(struct quota *quota,
+ struct mail_namespace *ns)
+{
+ struct fs_quota_mountpoint *mount;
+ struct fs_quota_root *root;
+ const char *dir;
+
+ if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &dir))
+ mount = NULL;
+ else
+ mount = fs_quota_mountpoint_get(dir);
+ if (mount != NULL) {
+ root = fs_quota_root_find_mountpoint(quota, mount);
+ if (root != NULL && root->mount == NULL)
+ fs_quota_mount_init(root, mount, dir);
+ else
+ fs_quota_mountpoint_free(mount);
+ }
+
+ /* we would actually want to do this only once after all quota roots
+ are created, but there's no way to do this right now */
+ fs_quota_add_missing_mounts(quota);
+}
+
+static const char *const *
+fs_quota_root_get_resources(struct quota_root *_root)
+{
+ struct fs_quota_root *root = (struct fs_quota_root *)_root;
+ static const char *resources_kb[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES,
+ NULL
+ };
+ static const char *resources_kb_messages[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES,
+ QUOTA_NAME_MESSAGES,
+ NULL
+ };
+
+ return root->inode_per_mail ? resources_kb_messages : resources_kb;
+}
+
+#if defined(FS_QUOTA_LINUX) || defined(FS_QUOTA_BSDAIX) || \
+ defined(FS_QUOTA_NETBSD) || defined(HAVE_RQUOTA)
+static void fs_quota_root_disable(struct fs_quota_root *root, bool group)
+{
+ if (group)
+ root->group_disabled = TRUE;
+ else
+ root->user_disabled = TRUE;
+}
+#endif
+
+#ifdef HAVE_RQUOTA
+static void
+rquota_get_result(const rquota *rq,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r)
+{
+ /* use soft limits if they exist, fallback to hard limits */
+
+ /* convert the results from blocks to bytes */
+ *bytes_value_r = (uint64_t)rq->rq_curblocks *
+ (uint64_t)rq->rq_bsize;
+ if (rq->rq_bsoftlimit != 0) {
+ *bytes_limit_r = (uint64_t)rq->rq_bsoftlimit *
+ (uint64_t)rq->rq_bsize;
+ } else {
+ *bytes_limit_r = (uint64_t)rq->rq_bhardlimit *
+ (uint64_t)rq->rq_bsize;
+ }
+
+ *count_value_r = rq->rq_curfiles;
+ if (rq->rq_fsoftlimit != 0)
+ *count_limit_r = rq->rq_fsoftlimit;
+ else
+ *count_limit_r = rq->rq_fhardlimit;
+}
+
+static int
+do_rquota_user(struct fs_quota_root *root,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct getquota_rslt result;
+ struct getquota_args args;
+ struct timeval timeout;
+ enum clnt_stat call_status;
+ CLIENT *cl;
+ struct fs_quota_mountpoint *mount = root->mount;
+ const char *host;
+ char *path;
+
+ path = strchr(mount->device_path, ':');
+ i_assert(path != NULL);
+
+ host = t_strdup_until(mount->device_path, path);
+ path++;
+
+ /* For NFSv4, we send the filesystem path without initial /. Server
+ prepends proper NFS pseudoroot automatically and uses this for
+ detection of NFSv4 mounts. */
+ if (strcmp(root->mount->type, "nfs4") == 0) {
+ while (*path == '/')
+ path++;
+ }
+
+ e_debug(root->root.backend.event, "host=%s, path=%s, uid=%s",
+ host, path, dec2str(root->uid));
+
+ /* clnt_create() polls for a while to establish a connection */
+ cl = clnt_create(host, RQUOTAPROG, RQUOTAVERS, "udp");
+ if (cl == NULL) {
+ *error_r = t_strdup_printf(
+ "could not contact RPC service on %s", host);
+ return -1;
+ }
+
+ /* Establish some RPC credentials */
+ auth_destroy(cl->cl_auth);
+ cl->cl_auth = authunix_create_default();
+
+ /* make the rquota call on the remote host */
+ args.gqa_pathp = path;
+ args.gqa_uid = root->uid;
+
+ timeout.tv_sec = RQUOTA_GETQUOTA_TIMEOUT_SECS;
+ timeout.tv_usec = 0;
+ call_status = clnt_call(cl, RQUOTAPROC_GETQUOTA,
+ (xdrproc_t)xdr_getquota_args, (char *)&args,
+ (xdrproc_t)xdr_getquota_rslt, (char *)&result,
+ timeout);
+
+ /* the result has been deserialized, let the client go */
+ auth_destroy(cl->cl_auth);
+ clnt_destroy(cl);
+
+ if (call_status != RPC_SUCCESS) {
+ const char *rpc_error_msg = clnt_sperrno(call_status);
+
+ *error_r = t_strdup_printf(
+ "remote rquota call failed: %s",
+ rpc_error_msg);
+ return -1;
+ }
+
+ switch (result.status) {
+ case Q_OK: {
+ rquota_get_result(&result.getquota_rslt_u.gqr_rquota,
+ bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r);
+ e_debug(root->root.backend.event, "uid=%s, bytes=%"PRIu64"/%"PRIu64" "
+ "files=%"PRIu64"/%"PRIu64,
+ dec2str(root->uid),
+ *bytes_value_r, *bytes_limit_r,
+ *count_value_r, *count_limit_r);
+ return 1;
+ }
+ case Q_NOQUOTA:
+ e_debug(root->root.backend.event, "uid=%s, limit=unlimited",
+ dec2str(root->uid));
+ fs_quota_root_disable(root, FALSE);
+ return 0;
+ case Q_EPERM:
+ *error_r = "permission denied to rquota service";
+ return -1;
+ default:
+ *error_r = t_strdup_printf(
+ "unrecognized status code (%d) from rquota service",
+ result.status);
+ return -1;
+ }
+}
+
+static int
+do_rquota_group(struct fs_quota_root *root ATTR_UNUSED,
+ uint64_t *bytes_value_r ATTR_UNUSED,
+ uint64_t *bytes_limit_r ATTR_UNUSED,
+ uint64_t *count_value_r ATTR_UNUSED,
+ uint64_t *count_limit_r ATTR_UNUSED,
+ const char **error_r)
+{
+#if defined(EXT_RQUOTAVERS) && defined(GRPQUOTA)
+ struct getquota_rslt result;
+ ext_getquota_args args;
+ struct timeval timeout;
+ enum clnt_stat call_status;
+ CLIENT *cl;
+ struct fs_quota_mountpoint *mount = root->mount;
+ const char *host;
+ char *path;
+
+ path = strchr(mount->device_path, ':');
+ i_assert(path != NULL);
+
+ host = t_strdup_until(mount->device_path, path);
+ path++;
+
+ e_debug(root->root.backend.event, "host=%s, path=%s, gid=%s",
+ host, path, dec2str(root->gid));
+
+ /* clnt_create() polls for a while to establish a connection */
+ cl = clnt_create(host, RQUOTAPROG, EXT_RQUOTAVERS, "udp");
+ if (cl == NULL) {
+ *error_r = t_strdup_printf(
+ "could not contact RPC service on %s (group)", host);
+ return -1;
+ }
+
+ /* Establish some RPC credentials */
+ auth_destroy(cl->cl_auth);
+ cl->cl_auth = authunix_create_default();
+
+ /* make the rquota call on the remote host */
+ args.gqa_pathp = path;
+ args.gqa_id = root->gid;
+ args.gqa_type = GRPQUOTA;
+ timeout.tv_sec = RQUOTA_GETQUOTA_TIMEOUT_SECS;
+ timeout.tv_usec = 0;
+
+ call_status = clnt_call(cl, RQUOTAPROC_GETQUOTA,
+ (xdrproc_t)xdr_ext_getquota_args, (char *)&args,
+ (xdrproc_t)xdr_getquota_rslt, (char *)&result,
+ timeout);
+
+ /* the result has been deserialized, let the client go */
+ auth_destroy(cl->cl_auth);
+ clnt_destroy(cl);
+
+ if (call_status != RPC_SUCCESS) {
+ const char *rpc_error_msg = clnt_sperrno(call_status);
+
+ *error_r = t_strdup_printf(
+ "remote ext rquota call failed: %s", rpc_error_msg);
+ return -1;
+ }
+
+ switch (result.status) {
+ case Q_OK: {
+ rquota_get_result(&result.getquota_rslt_u.gqr_rquota,
+ bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r);
+ e_debug(root->root.backend.event, "gid=%s, bytes=%"PRIu64"/%"PRIu64" "
+ "files=%"PRIu64"/%"PRIu64,
+ dec2str(root->gid),
+ *bytes_value_r, *bytes_limit_r,
+ *count_value_r, *count_limit_r);
+ return 1;
+ }
+ case Q_NOQUOTA:
+ e_debug(root->root.backend.event, "gid=%s, limit=unlimited",
+ dec2str(root->gid));
+ fs_quota_root_disable(root, TRUE);
+ return 0;
+ case Q_EPERM:
+ *error_r = "permission denied to ext rquota service";
+ return -1;
+ default:
+ *error_r = t_strdup_printf(
+ "unrecognized status code (%d) from ext rquota service",
+ result.status);
+ return -1;
+ }
+#else
+ *error_r = "rquota not compiled with group support";
+ return -1;
+#endif
+}
+#endif
+
+#ifdef FS_QUOTA_LINUX
+static int
+fs_quota_get_linux(struct fs_quota_root *root, bool group,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct dqblk dqblk;
+ int type, id;
+
+ type = group ? GRPQUOTA : USRQUOTA;
+ id = group ? root->gid : root->uid;
+
+#ifdef HAVE_XFS_QUOTA
+ if (strcmp(root->mount->type, "xfs") == 0) {
+ struct fs_disk_quota xdqblk;
+
+ if (quotactl(QCMD(Q_XGETQUOTA, type),
+ root->mount->device_path,
+ id, (caddr_t)&xdqblk) < 0) {
+ if (errno == ESRCH) {
+ fs_quota_root_disable(root, group);
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "errno=%d, quotactl(Q_XGETQUOTA, %s) failed: %m",
+ errno, root->mount->device_path);
+ return -1;
+ }
+
+ /* values always returned in 512 byte blocks */
+ *bytes_value_r = xdqblk.d_bcount * 512ULL;
+ *bytes_limit_r = xdqblk.d_blk_softlimit * 512ULL;
+ if (*bytes_limit_r == 0) {
+ *bytes_limit_r = xdqblk.d_blk_hardlimit * 512ULL;
+ }
+ *count_value_r = xdqblk.d_icount;
+ *count_limit_r = xdqblk.d_ino_softlimit;
+ if (*count_limit_r == 0) {
+ *count_limit_r = xdqblk.d_ino_hardlimit;
+ }
+ } else
+#endif
+ {
+ /* ext2, ext3 */
+ if (quotactl(QCMD(Q_GETQUOTA, type),
+ root->mount->device_path,
+ id, (caddr_t)&dqblk) < 0) {
+ if (errno == ESRCH) {
+ fs_quota_root_disable(root, group);
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "quotactl(Q_GETQUOTA, %s) failed: %m",
+ root->mount->device_path);
+ if (errno == EINVAL) {
+ *error_r = t_strdup_printf("%s, "
+ "Dovecot was compiled with Linux quota "
+ "v%d support, try changing it "
+ "(CPPFLAGS=-D_LINUX_QUOTA_VERSION=%d configure)",
+ *error_r,
+ _LINUX_QUOTA_VERSION,
+ _LINUX_QUOTA_VERSION == 1 ? 2 : 1);
+ }
+ return -1;
+ }
+
+#if _LINUX_QUOTA_VERSION == 1
+ *bytes_value_r = dqblk.dqb_curblocks * 1024ULL;
+#else
+ *bytes_value_r = dqblk.dqb_curblocks;
+#endif
+ *bytes_limit_r = dqblk.dqb_bsoftlimit * 1024ULL;
+ if (*bytes_limit_r == 0) {
+ *bytes_limit_r = dqblk.dqb_bhardlimit * 1024ULL;
+ }
+ *count_value_r = dqblk.dqb_curinodes;
+ *count_limit_r = dqblk.dqb_isoftlimit;
+ if (*count_limit_r == 0) {
+ *count_limit_r = dqblk.dqb_ihardlimit;
+ }
+ }
+ return 1;
+}
+#endif
+
+#ifdef FS_QUOTA_BSDAIX
+static int
+fs_quota_get_bsdaix(struct fs_quota_root *root, bool group,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct dqblk dqblk;
+ int type, id;
+
+ type = group ? GRPQUOTA : USRQUOTA;
+ id = group ? root->gid : root->uid;
+
+ if (quotactl(root->mount->mount_path, QCMD(Q_GETQUOTA, type),
+ id, (void *)&dqblk) < 0) {
+ if (errno == ESRCH) {
+ fs_quota_root_disable(root, group);
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "quotactl(Q_GETQUOTA, %s) failed: %m",
+ root->mount->mount_path);
+ return -1;
+ }
+ *bytes_value_r = (uint64_t)dqblk.dqb_curblocks * DEV_BSIZE;
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit * DEV_BSIZE;
+ if (*bytes_limit_r == 0) {
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit * DEV_BSIZE;
+ }
+ *count_value_r = dqblk.dqb_curinodes;
+ *count_limit_r = dqblk.dqb_isoftlimit;
+ if (*count_limit_r == 0) {
+ *count_limit_r = dqblk.dqb_ihardlimit;
+ }
+ return 1;
+}
+#endif
+
+#ifdef FS_QUOTA_NETBSD
+static int
+fs_quota_get_netbsd(struct fs_quota_root *root, bool group,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct quotakey qk;
+ struct quotaval qv;
+ struct quotahandle *qh;
+ int ret;
+
+ if ((qh = quota_open(root->mount->mount_path)) == NULL) {
+ *error_r = t_strdup_printf("cannot open quota for %s: %m",
+ root->mount->mount_path);
+ fs_quota_root_disable(root, group);
+ return 0;
+ }
+
+ qk.qk_idtype = group ? QUOTA_IDTYPE_GROUP : QUOTA_IDTYPE_USER;
+ qk.qk_id = group ? root->gid : root->uid;
+
+ for (int i = 0; i < 2; i++) {
+ qk.qk_objtype = i == 0 ? QUOTA_OBJTYPE_BLOCKS : QUOTA_OBJTYPE_FILES;
+
+ if (quota_get(qh, &qk, &qv) != 0) {
+ if (errno == ESRCH) {
+ fs_quota_root_disable(root, group);
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "quotactl(Q_GETQUOTA, %s) failed: %m",
+ root->mount->mount_path);
+ ret = -1;
+ break;
+ }
+ if (i == 0) {
+ *bytes_value_r = qv.qv_usage * DEV_BSIZE;
+ *bytes_limit_r = qv.qv_softlimit * DEV_BSIZE;
+ } else {
+ *count_value_r = qv.qv_usage;
+ *count_limit_r = qv.qv_softlimit;
+ }
+ ret = 1;
+ }
+ quota_close(qh);
+ return ret;
+}
+#endif
+
+#ifdef FS_QUOTA_HPUX
+static int
+fs_quota_get_hpux(struct fs_quota_root *root,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct dqblk dqblk;
+
+ if (quotactl(Q_GETQUOTA, root->mount->device_path,
+ root->uid, &dqblk) < 0) {
+ if (errno == ESRCH) {
+ root->user_disabled = TRUE;
+ return 0;
+ }
+ *error_r = t_strdup_printf(
+ "quotactl(Q_GETQUOTA, %s) failed: %m",
+ root->mount->device_path);
+ return -1;
+ }
+
+ *bytes_value_r = (uint64_t)dqblk.dqb_curblocks *
+ root->mount->block_size;
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit *
+ root->mount->block_size;
+ if (*bytes_limit_r == 0) {
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit *
+ root->mount->block_size;
+ }
+ *count_value_r = dqblk.dqb_curfiles;
+ *count_limit_r = dqblk.dqb_fsoftlimit;
+ if (*count_limit_r == 0) {
+ *count_limit_r = dqblk.dqb_fhardlimit;
+ }
+ return 1;
+}
+#endif
+
+#ifdef FS_QUOTA_SOLARIS
+static int
+fs_quota_get_solaris(struct fs_quota_root *root,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ struct dqblk dqblk;
+ struct quotctl ctl;
+
+ if (root->mount->fd == -1)
+ return 0;
+
+ ctl.op = Q_GETQUOTA;
+ ctl.uid = root->uid;
+ ctl.addr = (caddr_t)&dqblk;
+ if (ioctl(root->mount->fd, Q_QUOTACTL, &ctl) < 0) {
+ *error_r = t_strdup_printf(
+ "ioctl(%s, Q_QUOTACTL) failed: %m",
+ root->mount->path);
+ return -1;
+ }
+ *bytes_value_r = (uint64_t)dqblk.dqb_curblocks * DEV_BSIZE;
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit * DEV_BSIZE;
+ if (*bytes_limit_r == 0) {
+ *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit * DEV_BSIZE;
+ }
+ *count_value_r = dqblk.dqb_curfiles;
+ *count_limit_r = dqblk.dqb_fsoftlimit;
+ if (*count_limit_r == 0) {
+ *count_limit_r = dqblk.dqb_fhardlimit;
+ }
+ return 1;
+}
+#endif
+
+static int
+fs_quota_get_resources(struct fs_quota_root *root, bool group,
+ uint64_t *bytes_value_r, uint64_t *bytes_limit_r,
+ uint64_t *count_value_r, uint64_t *count_limit_r,
+ const char **error_r)
+{
+ if (group) {
+ if (root->group_disabled)
+ return 0;
+ } else {
+ if (root->user_disabled)
+ return 0;
+ }
+#ifdef FS_QUOTA_LINUX
+ return fs_quota_get_linux(root, group, bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r, error_r);
+#elif defined (FS_QUOTA_NETBSD)
+ return fs_quota_get_netbsd(root, group, bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r, error_r);
+#elif defined (FS_QUOTA_BSDAIX)
+ return fs_quota_get_bsdaix(root, group, bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r, error_r);
+#else
+ if (group) {
+ /* not supported */
+ return 0;
+ }
+#ifdef FS_QUOTA_HPUX
+ return fs_quota_get_hpux(root, bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r, error_r);
+#else
+ return fs_quota_get_solaris(root, bytes_value_r, bytes_limit_r,
+ count_value_r, count_limit_r, error_r);
+#endif
+#endif
+}
+
+static bool fs_quota_match_box(struct quota_root *_root, struct mailbox *box)
+{
+ struct fs_quota_root *root = (struct fs_quota_root *)_root;
+ struct stat mst, rst;
+ const char *mailbox_path;
+ bool match;
+
+ if (root->storage_mount_path == NULL)
+ return TRUE;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &mailbox_path) <= 0)
+ return FALSE;
+ if (stat(mailbox_path, &mst) < 0) {
+ if (errno != ENOENT)
+ e_error(_root->backend.event,
+ "stat(%s) failed: %m", mailbox_path);
+ return FALSE;
+ }
+ if (stat(root->storage_mount_path, &rst) < 0) {
+ e_debug(_root->backend.event, "stat(%s) failed: %m",
+ root->storage_mount_path);
+ return FALSE;
+ }
+ match = CMP_DEV_T(mst.st_dev, rst.st_dev);
+ e_debug(_root->backend.event, "box=%s mount=%s match=%s", mailbox_path,
+ root->storage_mount_path, match ? "yes" : "no");
+ return match;
+}
+
+static enum quota_get_result
+fs_quota_get_resource(struct quota_root *_root, const char *name,
+ uint64_t *value_r, const char **error_r)
+{
+ struct fs_quota_root *root = (struct fs_quota_root *)_root;
+ uint64_t bytes_value, count_value;
+ uint64_t bytes_limit = 0, count_limit = 0;
+ int ret;
+
+ *value_r = 0;
+
+ if (root->mount == NULL) {
+ if (root->storage_mount_path != NULL)
+ *error_r = t_strdup_printf(
+ "Mount point unknown for path %s",
+ root->storage_mount_path);
+ else
+ *error_r = "Mount point unknown";
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+ }
+ if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) != 0 &&
+ strcasecmp(name, QUOTA_NAME_MESSAGES) != 0) {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+
+#ifdef HAVE_RQUOTA
+ if (mount_type_is_nfs(root->mount)) {
+ ret = root->user_disabled ? 0 :
+ do_rquota_user(root, &bytes_value, &bytes_limit,
+ &count_value, &count_limit, error_r);
+ if (ret == 0 && !root->group_disabled)
+ ret = do_rquota_group(root, &bytes_value,
+ &bytes_limit, &count_value,
+ &count_limit, error_r);
+ } else
+#endif
+ {
+ ret = fs_quota_get_resources(root, FALSE, &bytes_value,
+ &bytes_limit, &count_value,
+ &count_limit, error_r);
+ if (ret == 0) {
+ /* fallback to group quota */
+ ret = fs_quota_get_resources(root, TRUE, &bytes_value,
+ &bytes_limit, &count_value,
+ &count_limit, error_r);
+ }
+ }
+ if (ret < 0)
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+ else if (ret == 0)
+ return QUOTA_GET_RESULT_LIMITED;
+
+ if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ *value_r = bytes_value;
+ else
+ *value_r = count_value;
+ if (_root->bytes_limit != (int64_t)bytes_limit ||
+ _root->count_limit != (int64_t)count_limit) {
+ /* update limit */
+ _root->bytes_limit = bytes_limit;
+ _root->count_limit = count_limit;
+
+ /* limits have changed, so we'll need to recalculate
+ relative quota rules */
+ quota_root_recalculate_relative_rules(_root->set, bytes_limit, count_limit);
+ }
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static int
+fs_quota_update(struct quota_root *root ATTR_UNUSED,
+ struct quota_transaction_context *ctx ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return 0;
+}
+
+struct quota_backend quota_backend_fs = {
+ .name = "fs",
+
+ .v = {
+ .alloc = fs_quota_alloc,
+ .init = fs_quota_init,
+ .deinit = fs_quota_deinit,
+
+ .namespace_added = fs_quota_namespace_added,
+
+ .get_resources = fs_quota_root_get_resources,
+ .get_resource = fs_quota_get_resource,
+ .update = fs_quota_update,
+
+ .match_box = fs_quota_match_box,
+ }
+};
+
+#endif
diff --git a/src/plugins/quota/quota-fs.h b/src/plugins/quota/quota-fs.h
new file mode 100644
index 0000000..8508cdf
--- /dev/null
+++ b/src/plugins/quota/quota-fs.h
@@ -0,0 +1,51 @@
+#ifndef QUOTA_FS_H
+#define QUOTA_FS_H
+
+#if defined (HAVE_STRUCT_DQBLK_CURBLOCKS) || \
+ defined (HAVE_STRUCT_DQBLK_CURSPACE)
+# define HAVE_FS_QUOTA
+#endif
+
+#ifdef HAVE_QUOTA_OPEN
+/* absolute path to avoid confusion with ./quota.h */
+# include "/usr/include/quota.h" /* NetBSD with libquota */
+#endif
+
+#ifdef HAVE_SYS_QUOTA_H
+# include <sys/quota.h> /* Linux, HP-UX */
+#elif defined(HAVE_SYS_FS_UFS_QUOTA_H)
+# include <sys/fs/ufs_quota.h> /* Solaris */
+#elif defined(HAVE_UFS_UFS_QUOTA_H)
+# include <ufs/ufs/quota.h> /* BSDs */
+#elif defined(HAVE_JFS_QUOTA_H)
+# include <jfs/quota.h> /* AIX */
+# ifdef HAVE_SYS_FS_QUOTA_COMMON_H
+# include <sys/fs/quota_common.h> /* quotactl() */
+# endif
+#else
+# undef HAVE_FS_QUOTA
+#endif
+
+#ifdef HAVE_QUOTACTL
+# ifdef HAVE_SYS_QUOTA_H
+# ifndef _HPUX_SOURCE
+# define FS_QUOTA_LINUX
+# else
+# define FS_QUOTA_HPUX
+# endif
+# else
+# define FS_QUOTA_BSDAIX
+# endif
+#elif defined (HAVE_Q_QUOTACTL)
+# define FS_QUOTA_SOLARIS
+#else
+# undef HAVE_FS_QUOTA
+#endif
+
+#ifdef HAVE_QUOTA_OPEN /* NetBSD with libquota */
+# define FS_QUOTA_NETBSD
+# define HAVE_FS_QUOTA
+# undef FS_QUOTA_LINUX /* obtained because we also have <sys/quota.h> */
+#endif
+
+#endif
diff --git a/src/plugins/quota/quota-imapc.c b/src/plugins/quota/quota-imapc.c
new file mode 100644
index 0000000..d5931db
--- /dev/null
+++ b/src/plugins/quota/quota-imapc.c
@@ -0,0 +1,494 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "imap-arg.h"
+#include "imapc-storage.h"
+#include "mailbox-list-private.h"
+#include "quota-private.h"
+
+struct imapc_quota_refresh_root {
+ const char *name;
+ unsigned int order;
+
+ uint64_t bytes_cur, count_cur;
+ uint64_t bytes_limit, count_limit;
+};
+
+struct imapc_quota_refresh {
+ pool_t pool;
+ const char *box_name;
+ ARRAY(struct imapc_quota_refresh_root) roots;
+};
+
+struct imapc_quota_root {
+ struct quota_root root;
+ const char *box_name, *root_name;
+
+ struct mail_namespace *imapc_ns;
+ struct imapc_storage_client *client;
+ bool initialized;
+
+ uint64_t bytes_last, count_last;
+
+ struct timeval last_refresh;
+ struct imapc_quota_refresh refresh;
+};
+
+extern struct quota_backend quota_backend_imapc;
+
+static struct quota_root *imapc_quota_alloc(void)
+{
+ struct imapc_quota_root *root;
+
+ root = i_new(struct imapc_quota_root, 1);
+ return &root->root;
+}
+
+static void handle_box_param(struct quota_root *_root, const char *param_value)
+{
+ ((struct imapc_quota_root *)_root)->box_name = p_strdup(_root->pool, param_value);
+}
+
+static void handle_root_param(struct quota_root *_root, const char *param_value)
+{
+ ((struct imapc_quota_root *)_root)->root_name = p_strdup(_root->pool, param_value);
+}
+
+static int imapc_quota_init(struct quota_root *_root, const char *args,
+ const char **error_r)
+{
+ struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+ const struct quota_param_parser imapc_params[] = {
+ {.param_name = "box=", .param_handler = handle_box_param},
+ {.param_name = "root=", .param_handler = handle_root_param},
+ quota_param_ns,
+ {.param_name = NULL}
+ };
+
+ _root->auto_updating = TRUE;
+ event_set_append_log_prefix(root->root.backend.event, "quota-imapc: ");
+
+ if (quota_parse_parameters(_root, &args, error_r, imapc_params, TRUE) < 0)
+ return -1;
+
+ if (root->box_name == NULL && root->root_name == NULL)
+ root->box_name = "INBOX";
+ /* we'll never try to enforce the quota - it's just a lot of
+ unnecessary remote GETQUOTA calls. */
+ _root->no_enforcing = TRUE;
+ return 0;
+}
+
+static void imapc_quota_deinit(struct quota_root *_root)
+{
+ i_free(_root);
+}
+
+static void
+imapc_quota_root_namespace_added(struct quota_root *_root,
+ struct mail_namespace *ns)
+{
+ struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+ if (root->imapc_ns == NULL)
+ root->imapc_ns = ns;
+}
+
+static struct imapc_quota_refresh *
+imapc_quota_root_refresh_find(struct imapc_storage_client *client)
+{
+ struct imapc_storage *storage = client->_storage;
+ struct quota *quota;
+ struct quota_root *const *rootp;
+
+ i_assert(storage != NULL);
+ quota = quota_get_mail_user_quota(storage->storage.user);
+ i_assert(quota != NULL);
+
+ /* find the quota root that is being refreshed */
+ array_foreach(&quota->roots, rootp) {
+ if ((*rootp)->backend.name == quota_backend_imapc.name) {
+ struct imapc_quota_root *root =
+ (struct imapc_quota_root *)*rootp;
+
+ if (root->refresh.pool != NULL)
+ return &root->refresh;
+ }
+ }
+ return NULL;
+}
+
+static struct imapc_quota_refresh_root *
+imapc_quota_refresh_root_get(struct imapc_quota_refresh *refresh,
+ const char *root_name)
+{
+ struct imapc_quota_refresh_root *refresh_root;
+
+ array_foreach_modifiable(&refresh->roots, refresh_root) {
+ if (strcmp(refresh_root->name, root_name) == 0)
+ return refresh_root;
+ }
+
+ refresh_root = array_append_space(&refresh->roots);
+ refresh_root->order = UINT_MAX;
+ refresh_root->name = p_strdup(refresh->pool, root_name);
+ refresh_root->bytes_limit = (uint64_t)-1;
+ refresh_root->count_limit = (uint64_t)-1;
+ return refresh_root;
+}
+
+static void imapc_untagged_quotaroot(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imapc_quota_refresh *refresh;
+ struct imapc_quota_refresh_root *refresh_root;
+ const char *mailbox_name, *root_name;
+ unsigned int i;
+
+ if (!imap_arg_get_astring(&reply->args[0], &mailbox_name))
+ return;
+
+ if ((refresh = imapc_quota_root_refresh_find(client)) == NULL ||
+ refresh->box_name == NULL ||
+ strcmp(refresh->box_name, mailbox_name) != 0) {
+ /* unsolicited QUOTAROOT reply - ignore */
+ return;
+ }
+ if (array_count(&refresh->roots) > 0) {
+ /* duplicate QUOTAROOT reply - ignore */
+ return;
+ }
+
+ i = 1;
+ while (imap_arg_get_astring(&reply->args[i], &root_name)) {
+ refresh_root = imapc_quota_refresh_root_get(refresh, root_name);
+ refresh_root->order = i;
+ i++;
+ }
+}
+
+static void imapc_untagged_quota(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ const struct imap_arg *list;
+ struct imapc_quota_refresh *refresh;
+ struct imapc_quota_refresh_root *refresh_root;
+ const char *root_name, *resource, *value_str, *limit_str;
+ uint64_t value, limit;
+ unsigned int i;
+
+ if (!imap_arg_get_astring(&reply->args[0], &root_name) ||
+ !imap_arg_get_list(&reply->args[1], &list))
+ return;
+
+ if ((refresh = imapc_quota_root_refresh_find(client)) == NULL) {
+ /* unsolicited QUOTA reply - ignore */
+ return;
+ }
+ refresh_root = imapc_quota_refresh_root_get(refresh, root_name);
+
+ for (i = 0; list[i].type != IMAP_ARG_EOL; i += 3) {
+ if (!imap_arg_get_atom(&list[i], &resource) ||
+ !imap_arg_get_atom(&list[i+1], &value_str) ||
+ !imap_arg_get_atom(&list[i+2], &limit_str) ||
+ /* RFC2087 uses 32bit number, but be ready for future */
+ str_to_uint64(value_str, &value) < 0 ||
+ str_to_uint64(limit_str, &limit) < 0)
+ return;
+
+ if (strcasecmp(resource, QUOTA_NAME_STORAGE_KILOBYTES) == 0) {
+ refresh_root->bytes_cur = value * 1024;
+ refresh_root->bytes_limit = limit * 1024;
+ } else if (strcasecmp(resource, QUOTA_NAME_MESSAGES) == 0) {
+ refresh_root->count_cur = value;
+ refresh_root->count_limit = limit;
+ }
+ }
+}
+
+static bool imapc_quota_client_init(struct imapc_quota_root *root)
+{
+ struct mailbox_list *list;
+ struct mail_storage *storage;
+
+ if (root->initialized)
+ return root->client != NULL;
+ root->initialized = TRUE;
+
+ list = root->imapc_ns->list;
+ if (mailbox_list_get_storage(&list, "", &storage) == 0 &&
+ strcmp(storage->name, IMAPC_STORAGE_NAME) != 0) {
+ /* non-imapc namespace, skip */
+ if ((storage->class_flags &
+ MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) {
+ e_warning(root->root.backend.event,
+ "Namespace '%s' is not imapc, "
+ "skipping for imapc quota",
+ root->imapc_ns->prefix);
+ }
+ return FALSE;
+ }
+ root->client = ((struct imapc_storage *)storage)->client;
+
+ imapc_storage_client_register_untagged(root->client, "QUOTAROOT",
+ imapc_untagged_quotaroot);
+ imapc_storage_client_register_untagged(root->client, "QUOTA",
+ imapc_untagged_quota);
+ return TRUE;
+}
+
+static void imapc_quota_refresh_init(struct imapc_quota_refresh *refresh)
+{
+ i_assert(refresh->pool == NULL);
+
+ refresh->pool = pool_alloconly_create("imapc quota refresh", 256);
+ p_array_init(&refresh->roots, refresh->pool, 4);
+}
+
+static void
+imapc_quota_refresh_update(struct quota *quota,
+ struct imapc_quota_refresh *refresh)
+{
+ struct quota_root *const *rootp;
+ const struct imapc_quota_refresh_root *refresh_root;
+
+ if (array_count(&refresh->roots) == 0) {
+ e_error(quota_backend_imapc.event,
+ "imapc didn't return any QUOTA results");
+ return;
+ }
+ /* use the first quota root for everything */
+ refresh_root = array_front(&refresh->roots);
+
+ array_foreach(&quota->roots, rootp) {
+ if ((*rootp)->backend.name == quota_backend_imapc.name) {
+ struct imapc_quota_root *root =
+ (struct imapc_quota_root *)*rootp;
+
+ root->bytes_last = refresh_root->bytes_cur;
+ root->count_last = refresh_root->count_cur;
+
+ /* If limits are higher than what dovecot can handle
+ consider them unlimited. */
+ if (refresh_root->bytes_limit > INT64_MAX)
+ root->root.bytes_limit = 0;
+ else
+ root->root.bytes_limit = refresh_root->bytes_limit;
+ if (refresh_root->count_limit > INT64_MAX)
+ root->root.count_limit = 0;
+ else
+ root->root.count_limit = refresh_root->count_limit;
+ }
+ }
+}
+
+static void
+imapc_quota_refresh_deinit(struct quota *quota,
+ struct imapc_quota_refresh *refresh, bool success)
+{
+ if (success)
+ imapc_quota_refresh_update(quota, refresh);
+ pool_unref(&refresh->pool);
+ i_zero(refresh);
+}
+
+static int
+imapc_quota_refresh_root_order_cmp(const struct imapc_quota_refresh_root *root1,
+ const struct imapc_quota_refresh_root *root2)
+{
+ if (root1->order < root2->order)
+ return -1;
+ else if (root1->order > root2->order)
+ return 1;
+ else
+ return 0;
+}
+
+static int imapc_quota_refresh_mailbox(struct imapc_quota_root *root,
+ const char **error_r)
+{
+ struct imapc_simple_context sctx;
+ struct imapc_command *cmd;
+
+ i_assert(root->box_name != NULL);
+
+ /* ask quotas for the configured mailbox */
+ imapc_quota_refresh_init(&root->refresh);
+ root->refresh.box_name = root->box_name;
+
+ imapc_simple_context_init(&sctx, root->client);
+ cmd = imapc_client_cmd(root->client->client,
+ imapc_simple_callback, &sctx);
+ imapc_command_sendf(cmd, "GETQUOTAROOT %s", root->box_name);
+ imapc_simple_run(&sctx, &cmd);
+
+ /* if there are multiple quota roots, use the first one returned by
+ the QUOTAROOT */
+ array_sort(&root->refresh.roots, imapc_quota_refresh_root_order_cmp);
+ imapc_quota_refresh_deinit(root->root.quota, &root->refresh,
+ sctx.ret == 0);
+ if (sctx.ret < 0)
+ *error_r = t_strdup_printf(
+ "GETQUOTAROOT %s failed: %s",
+ root->box_name,
+ mail_storage_get_last_internal_error(
+ &root->client->_storage->storage, NULL));
+
+ return sctx.ret;
+}
+
+static int imapc_quota_refresh_root(struct imapc_quota_root *root,
+ const char **error_r)
+{
+ struct imapc_simple_context sctx;
+ struct imapc_command *cmd;
+
+ i_assert(root->root_name != NULL);
+
+ /* ask quotas for the configured quota root */
+ imapc_quota_refresh_init(&root->refresh);
+
+ imapc_simple_context_init(&sctx, root->client);
+ cmd = imapc_client_cmd(root->client->client,
+ imapc_simple_callback, &sctx);
+ imapc_command_sendf(cmd, "GETQUOTA %s", root->root_name);
+ imapc_simple_run(&sctx, &cmd);
+
+ /* there shouldn't be more than one QUOTA reply, but ignore anyway
+ anything we didn't expect. */
+ while (array_count(&root->refresh.roots) > 0) {
+ const struct imapc_quota_refresh_root *refresh_root =
+ array_front(&root->refresh.roots);
+ if (strcmp(refresh_root->name, root->root_name) == 0)
+ break;
+ array_pop_front(&root->refresh.roots);
+ }
+ imapc_quota_refresh_deinit(root->root.quota, &root->refresh,
+ sctx.ret == 0);
+ if (sctx.ret < 0)
+ *error_r = t_strdup_printf(
+ "GETQUOTA %s failed: %s",
+ root->root_name,
+ mail_storage_get_last_internal_error(
+ &root->client->_storage->storage, NULL));
+ return sctx.ret;
+}
+
+static int imapc_quota_refresh(struct imapc_quota_root *root,
+ const char **error_r)
+{
+ enum imapc_capability capa;
+ int ret;
+
+ if (root->imapc_ns == NULL) {
+ /* imapc namespace is missing - disable this quota backend */
+ return 0;
+ }
+ if (root->last_refresh.tv_sec == ioloop_timeval.tv_sec &&
+ root->last_refresh.tv_usec == ioloop_timeval.tv_usec)
+ return 0;
+ if (!imapc_quota_client_init(root))
+ return 0;
+
+ if (imapc_client_get_capabilities(root->client->client, &capa) < 0) {
+ *error_r = "Failed to get server capabilities";
+ return -1;
+ }
+ if ((capa & IMAPC_CAPABILITY_QUOTA) == 0) {
+ /* no QUOTA capability - disable quota */
+ e_warning(root->root.backend.event,
+ "Remote IMAP server doesn't support QUOTA - disabling");
+ root->client = NULL;
+ return 0;
+ }
+
+ if (root->root_name == NULL)
+ ret = imapc_quota_refresh_mailbox(root, error_r);
+ else
+ ret = imapc_quota_refresh_root(root, error_r);
+
+ /* set the last_refresh only after the refresh, because it changes
+ ioloop_timeval. */
+ root->last_refresh = ioloop_timeval;
+ return ret;
+}
+
+static int imapc_quota_init_limits(struct quota_root *_root,
+ const char **error_r)
+{
+ struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+ return imapc_quota_refresh(root, error_r);
+}
+
+static void
+imapc_quota_namespace_added(struct quota *quota, struct mail_namespace *ns)
+{
+ struct quota_root **roots;
+ unsigned int i, count;
+
+ roots = array_get_modifiable(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i]->backend.name == quota_backend_imapc.name &&
+ ((roots[i]->ns_prefix == NULL &&
+ ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) ||
+ roots[i]->ns == ns))
+ imapc_quota_root_namespace_added(roots[i], ns);
+ }
+}
+
+static const char *const *
+imapc_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+ static const char *resources_both[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES,
+ QUOTA_NAME_MESSAGES,
+ NULL
+ };
+ return resources_both;
+}
+
+static enum quota_get_result
+imapc_quota_get_resource(struct quota_root *_root, const char *name,
+ uint64_t *value_r, const char **error_r)
+{
+ struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+ if (imapc_quota_refresh(root, error_r) < 0)
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ *value_r = root->bytes_last;
+ else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0)
+ *value_r = root->count_last;
+ else {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static int
+imapc_quota_update(struct quota_root *root ATTR_UNUSED,
+ struct quota_transaction_context *ctx ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return 0;
+}
+
+struct quota_backend quota_backend_imapc = {
+ .name = "imapc",
+
+ .v = {
+ .alloc = imapc_quota_alloc,
+ .init = imapc_quota_init,
+ .deinit = imapc_quota_deinit,
+ .init_limits = imapc_quota_init_limits,
+ .namespace_added = imapc_quota_namespace_added,
+ .get_resources = imapc_quota_root_get_resources,
+ .get_resource = imapc_quota_get_resource,
+ .update = imapc_quota_update,
+ }
+};
diff --git a/src/plugins/quota/quota-maildir.c b/src/plugins/quota/quota-maildir.c
new file mode 100644
index 0000000..f4fd3a7
--- /dev/null
+++ b/src/plugins/quota/quota-maildir.c
@@ -0,0 +1,953 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "nfs-workarounds.h"
+#include "safe-mkstemp.h"
+#include "mkdir-parents.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "str.h"
+#include "maildir-storage.h"
+#include "mailbox-list-private.h"
+#include "quota-private.h"
+
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define MAILDIRSIZE_FILENAME "maildirsize"
+#define MAILDIRSIZE_STALE_SECS (60*15)
+
+struct maildir_quota_root {
+ struct quota_root root;
+
+ struct mail_namespace *maildirsize_ns;
+ const char *maildirsize_path;
+
+ uint64_t total_bytes;
+ uint64_t total_count;
+
+ int fd;
+ time_t recalc_last_stamp;
+ off_t last_size;
+
+ bool limits_initialized:1;
+};
+
+struct maildir_list_context {
+ struct mailbox_list *list;
+ struct maildir_quota_root *root;
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+
+ string_t *path;
+ int state;
+};
+
+extern struct quota_backend quota_backend_maildir;
+
+static struct dotlock_settings dotlock_settings = {
+ .timeout = 0,
+ .stale_timeout = 30
+};
+
+static int maildir_sum_dir(const char *dir, uint64_t *total_bytes,
+ uint64_t *total_count, const char **error_r)
+{
+ DIR *dirp;
+ struct dirent *dp;
+ string_t *path;
+ const char *p;
+ size_t len;
+ uoff_t num;
+ int ret = 0;
+
+ dirp = opendir(dir);
+ if (dirp == NULL) {
+ if (errno == ENOENT || errno == ESTALE)
+ return 0;
+ *error_r = t_strdup_printf("opendir(%s) failed: %m", dir);
+ return -1;
+ }
+
+ path = t_str_new(256);
+ str_append(path, dir);
+ str_append_c(path, '/');
+
+ len = str_len(path);
+ while ((dp = readdir(dirp)) != NULL) {
+ if (dp->d_name[0] == '.' &&
+ (dp->d_name[1] == '\0' || dp->d_name[1] == '.'))
+ continue;
+
+ p = strstr(dp->d_name, ",S=");
+ num = UOFF_T_MAX;
+ if (p != NULL) {
+ /* ,S=nnnn[:,] */
+ p += 3;
+ for (num = 0; *p >= '0' && *p <= '9'; p++)
+ num = num * 10 + (*p - '0');
+
+ if (*p != ':' && *p != '\0' && *p != ',') {
+ /* not in expected format, fallback to stat() */
+ num = UOFF_T_MAX;
+ } else {
+ *total_bytes += num;
+ *total_count += 1;
+ }
+ }
+ if (num == UOFF_T_MAX) {
+ struct stat st;
+
+ str_truncate(path, len);
+ str_append(path, dp->d_name);
+ if (stat(str_c(path), &st) == 0) {
+ *total_bytes += st.st_size;
+ *total_count += 1;
+ } else if (errno != ENOENT && errno != ESTALE) {
+ *error_r = t_strdup_printf(
+ "stat(%s) failed: %m", str_c(path));
+ ret = -1;
+ }
+ }
+ }
+
+ if (closedir(dirp) < 0) {
+ *error_r = t_strdup_printf("closedir(%s) failed: %m", dir);
+ return -1;
+ }
+ return ret;
+}
+
+static struct maildir_list_context *
+maildir_list_init(struct maildir_quota_root *root, struct mailbox_list *list)
+{
+ struct maildir_list_context *ctx;
+
+ ctx = i_new(struct maildir_list_context, 1);
+ ctx->root = root;
+ ctx->path = str_new(default_pool, 512);
+ ctx->list = list;
+ ctx->iter = mailbox_list_iter_init(list, "*",
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ return ctx;
+}
+
+static bool maildir_set_next_path(struct maildir_list_context *ctx)
+{
+ const char *path, *storage_name;
+
+ str_truncate(ctx->path, 0);
+
+ storage_name = mailbox_list_get_storage_name(
+ ctx->info->ns->list, ctx->info->vname);
+ if (mailbox_list_get_path(ctx->list, storage_name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) > 0) {
+ str_append(ctx->path, path);
+ str_append(ctx->path, ctx->state == 0 ?
+ "/new" : "/cur");
+ }
+
+ return str_len(ctx->path) > 0;
+}
+
+static const char *
+maildir_list_next(struct maildir_list_context *ctx, time_t *mtime_r)
+{
+ struct quota_rule *rule;
+ struct stat st;
+
+ for (;;) {
+ if (ctx->state == 0) {
+ ctx->info = mailbox_list_iter_next(ctx->iter);
+ if (ctx->info == NULL)
+ return NULL;
+
+ rule = quota_root_rule_find(ctx->root->root.set,
+ ctx->info->vname);
+ if (rule != NULL && rule->ignore) {
+ /* mailbox not included in quota */
+ continue;
+ }
+ }
+
+ if (!maildir_set_next_path(ctx)) {
+ ctx->state = 0;
+ continue;
+ }
+
+ if (++ctx->state == 2)
+ ctx->state = 0;
+
+ if (stat(str_c(ctx->path), &st) == 0)
+ break;
+ /* ignore if the directory got lost, stale or if it was
+ actually a file and not a directory */
+ if (errno != ENOENT && errno != ESTALE && errno != ENOTDIR) {
+ e_error(ctx->root->root.backend.event,
+ "stat(%s) failed: %m", str_c(ctx->path));
+ ctx->state = 0;
+ }
+ }
+
+ *mtime_r = st.st_mtime;
+ return str_c(ctx->path);
+}
+
+static int maildir_list_deinit(struct maildir_list_context *ctx,
+ const char **error_r)
+{
+ int ret = mailbox_list_iter_deinit(&ctx->iter);
+ if (ret < 0)
+ *error_r = t_strdup_printf(
+ "Listing mailboxes failed: %s",
+ mailbox_list_get_last_internal_error(ctx->list, NULL));
+
+ str_free(&ctx->path);
+ i_free(ctx);
+ return ret;
+}
+
+static int
+maildirs_check_have_changed(struct maildir_quota_root *root,
+ struct mail_namespace *ns, time_t latest_mtime,
+ const char **error_r)
+{
+ struct maildir_list_context *ctx;
+ time_t mtime;
+ int ret = 0;
+
+ ctx = maildir_list_init(root, ns->list);
+ while (maildir_list_next(ctx, &mtime) != NULL) {
+ if (mtime > latest_mtime) {
+ ret = 1;
+ break;
+ }
+ }
+ if (maildir_list_deinit(ctx, error_r) < 0)
+ return -1;
+ return ret;
+}
+
+static int maildirsize_write(struct maildir_quota_root *root, const char *path)
+{
+ const struct mail_storage_settings *set =
+ root->maildirsize_ns->mail_set;
+ struct quota_root *_root = &root->root;
+ struct mail_namespace *const *namespaces;
+ unsigned int i, count;
+ struct mailbox_permissions perm;
+ const char *p, *dir;
+ string_t *str, *temp_path;
+ int fd;
+
+ i_assert(root->fd == -1);
+
+ /* figure out what permissions we should use for maildirsize.
+ use the inbox namespace's permissions if possible. */
+ perm.file_create_mode = 0600; perm.dir_create_mode = 0700;
+ perm.file_create_gid = (gid_t)-1;
+ perm.file_create_gid_origin = "default";
+ namespaces = array_get(&root->root.quota->namespaces, &count);
+ i_assert(count > 0);
+ for (i = 0; i < count; i++) {
+ if ((namespaces[i]->flags & NAMESPACE_FLAG_INBOX_USER) == 0)
+ continue;
+
+ mailbox_list_get_root_permissions(namespaces[i]->list,
+ &perm);
+ break;
+ }
+
+ dotlock_settings.use_excl_lock = set->dotlock_use_excl;
+ dotlock_settings.nfs_flush = set->mail_nfs_storage;
+
+ temp_path = t_str_new(128);
+ str_append(temp_path, path);
+ fd = safe_mkstemp_hostpid_group(temp_path, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ if (fd == -1 && errno == ENOENT) {
+ /* the control directory doesn't exist yet? create it */
+ p = strrchr(path, '/');
+ 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) {
+ e_error(root->root.backend.event,
+ "mkdir_parents(%s) failed: %m", dir);
+ return -1;
+ }
+ fd = safe_mkstemp_hostpid_group(temp_path,
+ perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ }
+ if (fd == -1) {
+ e_error(root->root.backend.event,
+ "safe_mkstemp(%s) failed: %m", path);
+ return -1;
+ }
+
+ str = t_str_new(128);
+ /* if we have no limits, write 0S instead of an empty line */
+ if (_root->bytes_limit != 0 || _root->count_limit == 0) {
+ str_printfa(str, "%"PRId64"S", _root->bytes_limit);
+ }
+ if (_root->count_limit != 0) {
+ if (str_len(str) > 0)
+ str_append_c(str, ',');
+ str_printfa(str, "%"PRIu64"C", _root->count_limit);
+ }
+ str_printfa(str, "\n%"PRIu64" %"PRIu64"\n",
+ root->total_bytes, root->total_count);
+ if (write_full(fd, str_data(str), str_len(str)) < 0) {
+ e_error(root->root.backend.event,
+ "write_full(%s) failed: %m", str_c(temp_path));
+ i_close_fd(&fd);
+ i_unlink(str_c(temp_path));
+ return -1;
+ }
+ i_close_fd(&fd);
+
+ if (rename(str_c(temp_path), path) < 0) {
+ e_error(root->root.backend.event,
+ "rename(%s, %s) failed: %m", str_c(temp_path), path);
+ i_unlink_if_exists(str_c(temp_path));
+ return -1;
+ }
+ return 0;
+}
+
+static void maildirsize_recalculate_init(struct maildir_quota_root *root)
+{
+ root->total_bytes = root->total_count = 0;
+ root->recalc_last_stamp = 0;
+}
+
+static int maildirsize_recalculate_namespace(struct maildir_quota_root *root,
+ struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct maildir_list_context *ctx;
+ const char *dir;
+ time_t mtime;
+ int ret = 0;
+
+ ctx = maildir_list_init(root, ns->list);
+ while ((dir = maildir_list_next(ctx, &mtime)) != NULL) {
+ if (mtime > root->recalc_last_stamp)
+ root->recalc_last_stamp = mtime;
+
+ if (maildir_sum_dir(dir, &root->total_bytes,
+ &root->total_count, error_r) < 0)
+ ret = -1;
+ }
+ if (maildir_list_deinit(ctx, error_r) < 0)
+ ret = -1;
+
+ return ret;
+}
+
+static void maildirsize_rebuild_later(struct maildir_quota_root *root)
+{
+ if (!root->root.set->force_default_rule) {
+ /* FIXME: can't unlink(), because the limits would be lost. */
+ return;
+ }
+
+ if (unlink(root->maildirsize_path) < 0 &&
+ errno != ENOENT && errno != ESTALE)
+ e_error(root->root.backend.event,
+ "unlink(%s) failed: %m", root->maildirsize_path);
+}
+
+static int maildirsize_recalculate_finish(struct maildir_quota_root *root,
+ int ret, const char **error_r)
+{
+ if (ret == 0) {
+ /* maildir didn't change, we can write the maildirsize file */
+ if ((ret = maildirsize_write(root, root->maildirsize_path)) < 0)
+ *error_r = "failed to write maildirsize";
+ }
+ if (ret != 0)
+ maildirsize_rebuild_later(root);
+
+ return ret;
+}
+
+static int maildirsize_recalculate(struct maildir_quota_root *root,
+ const char **error_r)
+{
+ struct mail_namespace *const *namespaces;
+ struct event_reason *reason;
+ unsigned int i, count;
+ int ret = 0;
+
+ reason = event_reason_begin("quota:recalculate");
+ maildirsize_recalculate_init(root);
+
+ /* count mails from all namespaces */
+ namespaces = array_get(&root->root.quota->namespaces, &count);
+ for (i = 0; i < count; i++) {
+ if (!quota_root_is_namespace_visible(&root->root, namespaces[i]))
+ continue;
+
+ if (maildirsize_recalculate_namespace(root, namespaces[i], error_r) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (ret == 0) {
+ /* check if any of the directories have changed */
+ for (i = 0; i < count; i++) {
+ if (!quota_root_is_namespace_visible(&root->root,
+ namespaces[i]))
+ continue;
+
+ ret = maildirs_check_have_changed(root, namespaces[i],
+ root->recalc_last_stamp,
+ error_r);
+ if (ret != 0)
+ break;
+ }
+ }
+
+ ret = maildirsize_recalculate_finish(root, ret, error_r);
+ event_reason_end(&reason);
+ return ret;
+}
+
+static bool
+maildir_parse_limit(const char *str, uint64_t *bytes_r, uint64_t *count_r)
+{
+ const char *const *limit;
+ unsigned long long value;
+ const char *pos;
+ bool ret = TRUE;
+
+ *bytes_r = 0;
+ *count_r = 0;
+
+ /* 0 values mean unlimited */
+ for (limit = t_strsplit(str, ","); *limit != NULL; limit++) {
+ if (str_parse_ullong(*limit, &value, &pos) < 0) {
+ ret = FALSE;
+ continue;
+ }
+ if (pos[0] != '\0' && pos[1] == '\0') {
+ switch (pos[0]) {
+ case 'C':
+ if (value != 0)
+ *count_r = value;
+ break;
+ case 'S':
+ if (value != 0)
+ *bytes_r = value;
+ break;
+ default:
+ ret = FALSE;
+ break;
+ }
+ } else {
+ ret = FALSE;
+ }
+ }
+ return ret;
+}
+
+static int maildirsize_parse(struct maildir_quota_root *root,
+ int fd, const char *const *lines)
+{
+ struct quota_root *_root = &root->root;
+ uint64_t message_bytes_limit, message_count_limit;
+ long long bytes_diff, total_bytes;
+ int count_diff, total_count;
+ unsigned int line_count = 0;
+
+ if (*lines == NULL)
+ return -1;
+
+ /* first line contains the limits */
+ (void)maildir_parse_limit(lines[0], &message_bytes_limit,
+ &message_count_limit);
+
+ /* truncate too high limits to signed 64bit int range */
+ if (message_bytes_limit >= (1ULL << 63))
+ message_bytes_limit = (1ULL << 63) - 1;
+ if (message_count_limit >= (1ULL << 63))
+ message_count_limit = (1ULL << 63) - 1;
+
+ if (root->root.bytes_limit == (int64_t)message_bytes_limit &&
+ root->root.count_limit == (int64_t)message_count_limit) {
+ /* limits haven't changed */
+ } else if (root->root.set->force_default_rule) {
+ /* we know the limits and they've changed.
+ the file must be rewritten. */
+ return 0;
+ } else {
+ /* we're using limits from the file. */
+ root->root.bytes_limit = message_bytes_limit;
+ root->root.count_limit = message_count_limit;
+ quota_root_recalculate_relative_rules(root->root.set,
+ message_bytes_limit,
+ message_count_limit);
+ }
+
+ if (*lines == NULL) {
+ /* no quota lines. rebuild it. */
+ return 0;
+ }
+
+ /* rest of the lines contains <bytes> <count> diffs */
+ total_bytes = 0; total_count = 0;
+ for (lines++; *lines != NULL; lines++, line_count++) {
+ if (sscanf(*lines, "%lld %d", &bytes_diff, &count_diff) != 2)
+ return -1;
+
+ total_bytes += bytes_diff;
+ total_count += count_diff;
+ }
+
+ if (total_bytes < 0 || total_count < 0) {
+ /* corrupted */
+ return -1;
+ }
+
+ if ((total_bytes > _root->bytes_limit && _root->bytes_limit != 0) ||
+ (total_count > _root->count_limit && _root->count_limit != 0)) {
+ /* we're over quota. don't trust these values if the file
+ contains more than the initial summary line, or if the file
+ is older than 15 minutes. */
+ struct stat st;
+
+ if (line_count > 1)
+ return 0;
+
+ if (fstat(fd, &st) < 0 ||
+ st.st_mtime < ioloop_time - MAILDIRSIZE_STALE_SECS)
+ return 0;
+ }
+ root->total_bytes = (uint64_t)total_bytes;
+ root->total_count = (uint64_t)total_count;
+ return 1;
+}
+
+static int maildirsize_open(struct maildir_quota_root *root,
+ const char **error_r)
+{
+ i_close_fd_path(&root->fd, root->maildirsize_path);
+
+ root->fd = nfs_safe_open(root->maildirsize_path, O_RDWR | O_APPEND);
+ if (root->fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+ *error_r = t_strdup_printf(
+ "open(%s) failed: %m", root->maildirsize_path);
+ return -1;
+ }
+ return 1;
+}
+
+static bool maildirsize_has_changed(struct maildir_quota_root *root)
+{
+ struct stat st1, st2;
+
+ if (dotlock_settings.nfs_flush) {
+ nfs_flush_file_handle_cache(root->maildirsize_path);
+ nfs_flush_attr_cache_unlocked(root->maildirsize_path);
+ }
+
+ if (root->fd == -1)
+ return TRUE;
+
+ if (stat(root->maildirsize_path, &st1) < 0)
+ return TRUE;
+ if (fstat(root->fd, &st2) < 0)
+ return TRUE;
+
+ return root->last_size != st2.st_size || st1.st_ino != st2.st_ino ||
+ !CMP_DEV_T(st1.st_dev, st2.st_dev);
+}
+
+static int maildirsize_read(struct maildir_quota_root *root, bool *retry,
+ const char **error_r)
+{
+ char buf[5120+1];
+ unsigned int i, size;
+ bool retry_estale = *retry;
+ int ret;
+
+ *retry = FALSE;
+
+ if (!maildirsize_has_changed(root))
+ return 1;
+
+ if ((ret = maildirsize_open(root, error_r)) <= 0)
+ return ret;
+
+ /* @UNSAFE */
+ size = 0;
+ while ((ret = read(root->fd, buf + size, sizeof(buf)-1 - size)) != 0) {
+ if (ret < 0) {
+ if (errno == ESTALE && retry_estale) {
+ *retry = TRUE;
+ break;
+ }
+ *error_r = t_strdup_printf(
+ "read(%s) failed: %m", root->maildirsize_path);
+ break;
+ }
+ size += ret;
+ if (size >= sizeof(buf)-1) {
+ /* we'll need to recalculate the quota */
+ break;
+ }
+ }
+
+ /* try to use the file even if we ran into some error. if we don't have
+ forced limits, we'll need to read the header to get them */
+ root->total_bytes = root->total_count = 0;
+ root->last_size = size;
+
+ /* skip the last line if there's no LF at the end. Remove the last LF
+ so we don't get one empty line in the strsplit. */
+ while (size > 0 && buf[size-1] != '\n') size--;
+ if (size > 0) size--;
+ buf[size] = '\0';
+
+ if (ret < 0 && size == 0) {
+ /* the read failed and there's no usable header, fail. */
+ i_close_fd(&root->fd);
+ return -1;
+ }
+
+ /* If there are any NUL bytes, the file is broken. */
+ for (i = 0; i < size; i++) {
+ if (buf[i] == '\0')
+ break;
+ }
+
+ if (i == size &&
+ maildirsize_parse(root, root->fd, t_strsplit(buf, "\n")) > 0 &&
+ ret == 0)
+ ret = 1;
+ else {
+ /* broken file / need recalculation */
+ i_close_fd(&root->fd);
+ ret = 0;
+ }
+ return ret;
+}
+
+static bool maildirquota_limits_init(struct maildir_quota_root *root)
+{
+ struct mailbox_list *list;
+ struct mail_storage *storage;
+ const char *control_dir;
+
+ if (root->limits_initialized)
+ return root->maildirsize_path != NULL;
+ root->limits_initialized = TRUE;
+
+ if (root->maildirsize_ns == NULL) {
+ i_assert(root->maildirsize_path == NULL);
+ return FALSE;
+ }
+
+ list = root->maildirsize_ns->list;
+ if (mailbox_list_get_storage(&list, "", &storage) == 0 &&
+ strcmp(storage->name, MAILDIR_STORAGE_NAME) != 0) {
+ /* non-maildir namespace, skip */
+ if ((storage->class_flags &
+ MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) {
+ e_warning(root->root.backend.event,
+ "Namespace '%s' is not Maildir, "
+ "skipping for Maildir++ quota",
+ root->maildirsize_ns->prefix);
+ }
+ root->maildirsize_path = NULL;
+ return FALSE;
+ }
+ if (root->maildirsize_path == NULL) {
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_CONTROL,
+ &control_dir))
+ i_unreached();
+ root->maildirsize_path =
+ p_strconcat(root->root.pool, control_dir,
+ "/"MAILDIRSIZE_FILENAME, NULL);
+ }
+ return TRUE;
+}
+
+static int maildirquota_read_limits(struct maildir_quota_root *root,
+ const char **error_r)
+{
+ bool retry = TRUE;
+ int ret, n = 0;
+
+ if (!maildirquota_limits_init(root))
+ return 1;
+
+ do {
+ if (n == NFS_ESTALE_RETRY_COUNT)
+ retry = FALSE;
+ ret = maildirsize_read(root, &retry, error_r);
+ n++;
+ } while (ret == -1 && retry);
+ return ret;
+}
+
+static int
+maildirquota_refresh(struct maildir_quota_root *root, bool *recalculated_r,
+ const char **error_r)
+{
+ int ret;
+
+ *recalculated_r = FALSE;
+
+ ret = maildirquota_read_limits(root, error_r);
+ if (ret == 0) {
+ if (root->root.bytes_limit == 0 &&
+ root->root.count_limit == 0 &&
+ root->root.set->default_rule.bytes_limit == 0 &&
+ root->root.set->default_rule.count_limit == 0) {
+ /* no quota */
+ if (!root->root.set->force_default_rule)
+ return 0;
+ /* explicitly specified 0 as quota. keep the quota
+ updated even if it's not enforced. */
+ }
+
+ ret = maildirsize_recalculate(root, error_r);
+ if (ret == 0)
+ *recalculated_r = TRUE;
+ }
+ return ret < 0 ? -1 : 0;
+}
+
+static int maildirsize_update(struct maildir_quota_root *root,
+ int count_diff, int64_t bytes_diff)
+{
+ char str[MAX_INT_STRLEN * 2 + 2];
+ int ret = 0;
+
+ if (count_diff == 0 && bytes_diff == 0)
+ return 0;
+
+ /* We rely on O_APPEND working in here. That isn't NFS-safe, but it
+ isn't necessarily that bad because the file is recreated once in
+ a while, and sooner if corruption causes calculations to go
+ over quota. This is also how Maildir++ spec specifies it should be
+ done.. */
+ if (i_snprintf(str, sizeof(str), "%lld %d\n",
+ (long long)bytes_diff, count_diff) < 0)
+ i_unreached();
+ if (write_full(root->fd, str, strlen(str)) < 0) {
+ ret = -1;
+ if (errno == ESTALE) {
+ /* deleted/replaced already, ignore */
+ } else {
+ e_error(root->root.backend.event,
+ "write_full(%s) failed: %m",
+ root->maildirsize_path);
+ }
+ } else {
+ /* close the file to force a flush with NFS */
+ if (close(root->fd) < 0) {
+ ret = -1;
+ if (errno != ESTALE)
+ e_error(root->root.backend.event,
+ "close(%s) failed: %m", root->maildirsize_path);
+ }
+ root->fd = -1;
+ }
+ return ret;
+}
+
+static struct quota_root *maildir_quota_alloc(void)
+{
+ struct maildir_quota_root *root;
+
+ root = i_new(struct maildir_quota_root, 1);
+ root->fd = -1;
+ return &root->root;
+}
+
+static int maildir_quota_init(struct quota_root *_root, const char *args,
+ const char **error_r)
+{
+ event_set_append_log_prefix(_root->backend.event, "quota-maildir: ");
+ return quota_root_default_init(_root, args, error_r);
+}
+
+static void maildir_quota_deinit(struct quota_root *_root)
+{
+ struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
+
+ i_close_fd(&root->fd);
+ i_free(root);
+}
+
+static bool
+maildir_quota_parse_rule(struct quota_root_settings *root_set ATTR_UNUSED,
+ struct quota_rule *rule,
+ const char *str, const char **error_r)
+{
+ uint64_t bytes, count;
+
+ if (strcmp(str, "NOQUOTA") == 0) {
+ bytes = 0;
+ count = 0;
+ } else if (!maildir_parse_limit(str, &bytes, &count)) {
+ *error_r = t_strdup_printf(
+ "quota-maildir: Invalid Maildir++ quota rule \"%s\"",
+ str);
+ return FALSE;
+ }
+
+ rule->bytes_limit = bytes;
+ rule->count_limit = count;
+ return TRUE;
+}
+
+static int maildir_quota_init_limits(struct quota_root *_root,
+ const char **error_r)
+{
+ struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
+ const char *error;
+
+ if (maildirquota_read_limits(root, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "quota-maildir: Failed to read limits: %s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+maildir_quota_root_namespace_added(struct quota_root *_root,
+ struct mail_namespace *ns)
+{
+ struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
+
+ if (root->maildirsize_ns == NULL)
+ root->maildirsize_ns = ns;
+}
+
+static void
+maildir_quota_namespace_added(struct quota *quota, struct mail_namespace *ns)
+{
+ struct quota_root **roots;
+ unsigned int i, count;
+
+ roots = array_get_modifiable(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i]->backend.name == quota_backend_maildir.name &&
+ ((roots[i]->ns_prefix == NULL &&
+ ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) ||
+ roots[i]->ns == ns))
+ maildir_quota_root_namespace_added(roots[i], ns);
+ }
+}
+
+static const char *const *
+maildir_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+ static const char *resources_both[] = {
+ QUOTA_NAME_STORAGE_KILOBYTES,
+ QUOTA_NAME_MESSAGES,
+ NULL
+ };
+
+ return resources_both;
+}
+
+static enum quota_get_result
+maildir_quota_get_resource(struct quota_root *_root, const char *name,
+ uint64_t *value_r, const char **error_r)
+{
+ struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
+ bool recalculated;
+ const char *error;
+
+ if (maildirquota_refresh(root, &recalculated, &error) < 0) {
+ *error_r = t_strdup_printf("Failed to get %s: %s", name, error);
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+ }
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) {
+ *value_r = root->total_bytes;
+ } else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) {
+ *value_r = root->total_count;
+ } else {
+ *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING;
+ return QUOTA_GET_RESULT_UNKNOWN_RESOURCE;
+ }
+ return QUOTA_GET_RESULT_LIMITED;
+}
+
+static int
+maildir_quota_update(struct quota_root *_root,
+ struct quota_transaction_context *ctx,
+ const char **error_r)
+{
+ struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
+ bool recalculated;
+ const char *error;
+
+ if (!maildirquota_limits_init(root)) {
+ /* no limits */
+ return 0;
+ }
+
+ /* even though we don't really care about the limits in here ourself,
+ we do want to make sure the header gets updated if the limits have
+ changed. also this makes sure the maildirsize file is created if
+ it doesn't exist. */
+ if (maildirquota_refresh(root, &recalculated, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Could not update storage usage data: %s",
+ error);
+ return -1;
+ }
+
+ if (recalculated) {
+ /* quota was just recalculated and it already contains the changes
+ we wanted to do. */
+ } else if (root->fd == -1) {
+ if (maildirsize_recalculate(root, &error) < 0)
+ e_error(root->root.backend.event, "%s", error);
+ } else if (ctx->recalculate != QUOTA_RECALCULATE_DONT) {
+ i_close_fd(&root->fd);
+ if (maildirsize_recalculate(root, &error) < 0)
+ e_error(root->root.backend.event, "%s", error);
+ } else if (maildirsize_update(root, ctx->count_used, ctx->bytes_used) < 0) {
+ i_close_fd(&root->fd);
+ maildirsize_rebuild_later(root);
+ }
+
+ return 0;
+}
+
+struct quota_backend quota_backend_maildir = {
+ .name = "maildir",
+
+ .v = {
+ .alloc = maildir_quota_alloc,
+ .init = maildir_quota_init,
+ .deinit = maildir_quota_deinit,
+ .parse_rule = maildir_quota_parse_rule,
+ .init_limits = maildir_quota_init_limits,
+ .namespace_added = maildir_quota_namespace_added,
+ .get_resources = maildir_quota_root_get_resources,
+ .get_resource = maildir_quota_get_resource,
+ .update = maildir_quota_update,
+ }
+};
diff --git a/src/plugins/quota/quota-plugin.c b/src/plugins/quota/quota-plugin.c
new file mode 100644
index 0000000..507a329
--- /dev/null
+++ b/src/plugins/quota/quota-plugin.c
@@ -0,0 +1,31 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-user.h"
+#include "mail-storage-hooks.h"
+#include "quota-plugin.h"
+
+void quota_backends_register(void);
+void quota_backends_unregister(void);
+
+const char *quota_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct mail_storage_hooks quota_mail_storage_hooks = {
+ .mail_user_created = quota_mail_user_created,
+ .mail_namespaces_created = quota_mail_namespaces_created,
+ .mailbox_list_created = quota_mailbox_list_created,
+ .mailbox_allocated = quota_mailbox_allocated,
+ .mail_allocated = quota_mail_allocated
+};
+
+void quota_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &quota_mail_storage_hooks);
+ quota_backends_register();
+}
+
+void quota_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&quota_mail_storage_hooks);
+ quota_backends_unregister();
+}
diff --git a/src/plugins/quota/quota-plugin.h b/src/plugins/quota/quota-plugin.h
new file mode 100644
index 0000000..927d428
--- /dev/null
+++ b/src/plugins/quota/quota-plugin.h
@@ -0,0 +1,36 @@
+#ifndef QUOTA_PLUGIN_H
+#define QUOTA_PLUGIN_H
+
+#include "module-context.h"
+#include "mail-user.h"
+
+struct module;
+struct mailbox;
+struct mailbox_list;
+struct mail;
+
+#define QUOTA_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, quota_user_module)
+#define QUOTA_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, quota_user_module)
+
+struct quota_user {
+ union mail_user_module_context module_ctx;
+
+ struct quota *quota;
+};
+
+struct mail_storage;
+
+extern MODULE_CONTEXT_DEFINE(quota_user_module, &mail_user_module_register);
+
+void quota_mail_user_created(struct mail_user *user);
+void quota_mailbox_list_created(struct mailbox_list *list);
+void quota_mail_namespaces_created(struct mail_namespace *namespaces);
+void quota_mailbox_allocated(struct mailbox *box);
+void quota_mail_allocated(struct mail *mail);
+
+void quota_plugin_init(struct module *module);
+void quota_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/quota/quota-private.h b/src/plugins/quota/quota-private.h
new file mode 100644
index 0000000..ac091d3
--- /dev/null
+++ b/src/plugins/quota/quota-private.h
@@ -0,0 +1,230 @@
+#ifndef QUOTA_PRIVATE_H
+#define QUOTA_PRIVATE_H
+
+#include "mail-storage-private.h"
+#include "mail-namespace.h"
+#include "quota.h"
+
+/* Modules should use do "my_id = quota_module_id++" and
+ use quota_module_contexts[id] for their own purposes. */
+extern unsigned int quota_module_id;
+
+struct quota {
+ struct mail_user *user;
+ struct quota_settings *set;
+ struct event *event;
+
+ ARRAY(struct quota_root *) roots;
+ ARRAY(struct mail_namespace *) namespaces;
+ struct mail_namespace *unwanted_ns;
+};
+
+struct quota_settings {
+ pool_t pool;
+
+ ARRAY(struct quota_root_settings *) root_sets;
+ struct event *event;
+ enum quota_alloc_result (*test_alloc)(
+ struct quota_transaction_context *ctx, uoff_t size,
+ const char **error_r);
+
+ uoff_t max_mail_size;
+ const char *quota_exceeded_msg;
+ bool debug:1;
+ bool initialized:1;
+ bool vsizes:1;
+};
+
+struct quota_rule {
+ const char *mailbox_mask;
+
+ int64_t bytes_limit, count_limit;
+ /* relative to default_rule */
+ int bytes_percent, count_percent;
+
+ /* Don't include this mailbox in quota */
+ bool ignore:1;
+};
+
+struct quota_warning_rule {
+ struct quota_rule rule;
+ const char *command;
+ bool reverse:1;
+};
+
+struct quota_backend_vfuncs {
+ struct quota_root *(*alloc)(void);
+ int (*init)(struct quota_root *root, const char *args,
+ const char **error_r);
+ void (*deinit)(struct quota_root *root);
+
+ bool (*parse_rule)(struct quota_root_settings *root_set,
+ struct quota_rule *rule,
+ const char *str, const char **error_r);
+ int (*init_limits)(struct quota_root *root, const char **error_r);
+
+ /* called once for each namespace */
+ void (*namespace_added)(struct quota *quota,
+ struct mail_namespace *ns);
+
+ const char *const *(*get_resources)(struct quota_root *root);
+ /* Backends return success as QUOTA_GET_RESULT_LIMITED, and returning
+ QUOTA_GET_RESULT_UNLIMITED is prohibited by quota_get_resource(),
+ which is the only caller of this vfunc. */
+ enum quota_get_result (*get_resource)(struct quota_root *root,
+ const char *name,
+ uint64_t *value_r,
+ const char **error_r);
+
+ int (*update)(struct quota_root *root,
+ struct quota_transaction_context *ctx,
+ const char **error_r);
+ bool (*match_box)(struct quota_root *root, struct mailbox *box);
+ void (*flush)(struct quota_root *root);
+};
+
+struct quota_backend {
+ /* quota backends equal if backend1.name == backend2.name */
+ const char *name;
+ struct event *event;
+ struct quota_backend_vfuncs v;
+};
+
+struct quota_root_settings {
+ /* Unique quota root name. */
+ const char *name;
+ /* Name in settings, e.g. "quota", "quota2", .. */
+ const char *set_name;
+
+ struct quota_settings *set;
+ const char *args;
+
+ const struct quota_backend *backend;
+ struct quota_rule default_rule;
+ ARRAY(struct quota_rule) rules;
+ ARRAY(struct quota_warning_rule) warning_rules;
+ const char *limit_set;
+
+ /* If user is under quota before saving a mail, allow the last mail to
+ bring the user over quota by this many bytes. */
+ uint64_t last_mail_max_extra_bytes;
+ struct quota_rule grace_rule;
+
+ /* Limits in default_rule override backend's quota limits */
+ bool force_default_rule:1;
+ /* TRUE if any of the warning_rules have reverse==TRUE */
+ bool have_reverse_warnings:1;
+};
+
+struct quota_root {
+ pool_t pool;
+
+ struct quota_root_settings *set;
+ struct quota *quota;
+ struct quota_backend backend;
+ struct dict *limit_set_dict;
+
+ /* this quota root applies only to this namespace. it may also be
+ a public namespace without an owner. */
+ struct mail_namespace *ns;
+ /* this is set in quota init(), because namespaces aren't known yet.
+ when accessing shared users the ns_prefix may be non-NULL but
+ ns=NULL, so when checking if quota root applies only to a specific
+ namespace use the ns_prefix!=NULL check. */
+ const char *ns_prefix;
+
+ /* initially the same as set->default_rule.*_limit, but some backends
+ may change these by reading the limits elsewhere (e.g. Maildir++,
+ FS quota) */
+ int64_t bytes_limit, count_limit;
+
+ /* Module-specific contexts. See quota_module_id. */
+ ARRAY(void) quota_module_contexts;
+
+ /* don't enforce quota when saving */
+ bool no_enforcing:1;
+ /* quota is automatically updated. update() should be called but the
+ bytes won't be changed. count is still changed, because it's cheap
+ to do and it's internally used to figure out whether there have
+ been some changes and that quota_warnings should be checked. */
+ bool auto_updating:1;
+ /* If user has unlimited quota, disable quota tracking */
+ bool disable_unlimited_tracking:1;
+ /* Set while quota is being recalculated to avoid recursion. */
+ bool recounting:1;
+ /* Quota root is hidden (to e.g. IMAP GETQUOTAROOT) */
+ bool hidden:1;
+ /* Did we already check quota_over_flag correctness? */
+ bool quota_over_flag_checked:1;
+};
+
+struct quota_transaction_context {
+ union mailbox_transaction_module_context module_ctx;
+
+ struct quota *quota;
+ struct mailbox *box;
+
+ int64_t bytes_used, count_used;
+ /* how many bytes/mails can be saved until limit is reached.
+ (set once, not updated by bytes_used/count_used).
+
+ if last_mail_max_extra_bytes>0, the bytes_ceil is initially
+ increased by that much, while bytes_ceil2 contains the real ceiling.
+ after the first allocation is done, bytes_ceil is set to
+ bytes_ceil2. */
+ uint64_t bytes_ceil, bytes_ceil2, count_ceil;
+ /* How many bytes/mails we are over quota. Like *_ceil, these are set
+ only once and not updated by bytes_used/count_used. (Either *_ceil
+ or *_over is always zero.) */
+ uint64_t bytes_over, count_over;
+
+ struct mail *tmp_mail;
+ enum quota_recalculate recalculate;
+
+ bool limits_set:1;
+ bool failed:1;
+ bool sync_transaction:1;
+ /* TRUE if all roots have auto_updating=TRUE */
+ bool auto_updating:1;
+ /* Quota doesn't need to be updated within this transaction. */
+ bool no_quota_updates:1;
+};
+
+/* Register storage to all user's quota roots. */
+void quota_add_user_namespace(struct quota *quota, struct mail_namespace *ns);
+void quota_remove_user_namespace(struct mail_namespace *ns);
+
+int quota_root_default_init(struct quota_root *root, const char *args,
+ const char **error_r);
+struct quota *quota_get_mail_user_quota(struct mail_user *user);
+
+bool quota_root_is_namespace_visible(struct quota_root *root,
+ struct mail_namespace *ns);
+struct quota_rule *
+quota_root_rule_find(struct quota_root_settings *root_set, const char *name);
+
+void quota_root_recalculate_relative_rules(struct quota_root_settings *root_set,
+ int64_t bytes_limit,
+ int64_t count_limit);
+/* Returns 1 if values were returned successfully, 0 if we're recursing into
+ the same function, -1 if error. */
+int quota_count(struct quota_root *root, uint64_t *bytes_r, uint64_t *count_r,
+ enum quota_get_result *error_result_r, const char **error_r);
+
+int quota_root_parse_grace(struct quota_root_settings *root_set,
+ const char *value, const char **error_r);
+bool quota_warning_match(const struct quota_warning_rule *w,
+ uint64_t bytes_before, uint64_t bytes_current,
+ uint64_t count_before, uint64_t count_current,
+ const char **reason_r);
+bool quota_transaction_is_over(struct quota_transaction_context *ctx, uoff_t size);
+int quota_transaction_set_limits(struct quota_transaction_context *ctx,
+ enum quota_get_result *error_result_r,
+ const char **error_r);
+
+void quota_backend_register(const struct quota_backend *backend);
+void quota_backend_unregister(const struct quota_backend *backend);
+
+#define QUOTA_UNKNOWN_RESOURCE_ERROR_STRING "Unknown quota resource"
+
+#endif
diff --git a/src/plugins/quota/quota-status-settings.c b/src/plugins/quota/quota-status-settings.c
new file mode 100644
index 0000000..e98cc09
--- /dev/null
+++ b/src/plugins/quota/quota-status-settings.c
@@ -0,0 +1,37 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "quota-status-settings.h"
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct quota_status_settings)
+
+static const struct setting_define quota_status_setting_defines[] = {
+ DEF(STR, recipient_delimiter),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct quota_status_settings quota_status_default_settings = {
+ .recipient_delimiter = "+",
+};
+
+static const struct setting_parser_info *quota_status_setting_dependencies[] = {
+ NULL
+};
+
+const struct setting_parser_info quota_status_setting_parser_info = {
+ .module_name = "mail",
+ .defines = quota_status_setting_defines,
+ .defaults = &quota_status_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct quota_status_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = quota_status_setting_dependencies
+};
diff --git a/src/plugins/quota/quota-status-settings.h b/src/plugins/quota/quota-status-settings.h
new file mode 100644
index 0000000..e69a0aa
--- /dev/null
+++ b/src/plugins/quota/quota-status-settings.h
@@ -0,0 +1,10 @@
+#ifndef QUOTA_STATUS_SETTINGS_H
+#define QUOTA_STATUS_SETTINGS_H 1
+
+struct quota_status_settings {
+ const char *recipient_delimiter;
+};
+
+extern const struct setting_parser_info quota_status_setting_parser_info;
+
+#endif
diff --git a/src/plugins/quota/quota-status.c b/src/plugins/quota/quota-status.c
new file mode 100644
index 0000000..7db0cb4
--- /dev/null
+++ b/src/plugins/quota/quota-status.c
@@ -0,0 +1,360 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "ostream.h"
+#include "connection.h"
+#include "restrict-access.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "mail-storage-service.h"
+#include "smtp-address.h"
+#include "quota-private.h"
+#include "quota-plugin.h"
+#include "quota-status-settings.h"
+
+enum quota_protocol {
+ QUOTA_PROTOCOL_UNKNOWN = 0,
+ QUOTA_PROTOCOL_POSTFIX
+};
+
+struct quota_client {
+ struct connection conn;
+
+ struct event *event;
+
+ char *state;
+ char *recipient;
+ uoff_t size;
+
+ bool warned_bad_state:1;
+};
+
+static struct event_category event_category_quota_status = {
+ .name = "quota-status"
+};
+
+static struct quota_status_settings *quota_status_settings;
+static pool_t quota_status_pool;
+static enum quota_protocol protocol;
+static struct mail_storage_service_ctx *storage_service;
+static struct connection_list *clients;
+static char *nouser_reply;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct quota_client *client;
+
+ client = i_new(struct quota_client, 1);
+
+ client->event = event_create(NULL);
+ client->conn.event_parent = client->event;
+ event_add_category(client->event, &event_category_quota_status);
+ connection_init_server(clients, &client->conn,
+ "quota-client", conn->fd, conn->fd);
+ master_service_client_connection_accept(conn);
+
+ e_debug(client->event, "Client connected");
+}
+
+static void client_reset(struct quota_client *client)
+{
+ i_free(client->state);
+ i_free(client->recipient);
+}
+
+static enum quota_alloc_result
+quota_check(struct mail_user *user, uoff_t mail_size, const char **error_r)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct quota_transaction_context *ctx;
+ enum quota_alloc_result ret;
+
+ if (quser == NULL) {
+ /* no quota for user */
+ e_debug(user->event, "User has no quota");
+ return QUOTA_ALLOC_RESULT_OK;
+ }
+
+ ns = mail_namespace_find_inbox(user->namespaces);
+ box = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_POST_SESSION);
+
+ ctx = quota_transaction_begin(box);
+ const char *internal_error;
+ ret = quota_test_alloc(ctx, I_MAX(1, mail_size), &internal_error);
+ if (ret == QUOTA_ALLOC_RESULT_TEMPFAIL)
+ e_error(user->event, "quota check failed: %s", internal_error);
+ *error_r = quota_alloc_result_errstr(ret, ctx);
+ quota_transaction_rollback(&ctx);
+
+ mailbox_free(&box);
+ return ret;
+}
+
+static int client_check_mta_state(struct quota_client *client)
+{
+ if (client->state == NULL || strcasecmp(client->state, "RCPT") == 0)
+ return 0;
+
+ if (!client->warned_bad_state) {
+ e_warning(client->event,
+ "Received policy query from MTA in unexpected state %s "
+ "(service can only be used for recipient restrictions)",
+ client->state);
+ }
+ client->warned_bad_state = TRUE;
+ return -1;
+}
+
+static void client_handle_request(struct quota_client *client)
+{
+ struct mail_storage_service_input input;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ struct smtp_address *rcpt;
+ const char *value = NULL, *error;
+ const char *detail ATTR_UNUSED;
+ char delim ATTR_UNUSED;
+ string_t *resp;
+ int ret;
+
+ if (client_check_mta_state(client) < 0 || client->recipient == NULL) {
+ e_debug(client->event, "Response: action=DUNNO");
+ o_stream_nsend_str(client->conn.output, "action=DUNNO\n\n");
+ return;
+ }
+
+ if (smtp_address_parse_path(pool_datastack_create(), client->recipient,
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART,
+ &rcpt, &error) < 0) {
+ e_error(client->event,
+ "Client sent invalid recipient address `%s': "
+ "%s", str_sanitize(client->recipient, 256), error);
+ e_debug(client->event, "Response: action=DUNNO");
+ o_stream_nsend_str(client->conn.output, "action=DUNNO\n\n");
+ return;
+ }
+
+ i_zero(&input);
+ input.event_parent = client->event;
+ smtp_address_detail_parse_temp(quota_status_settings->recipient_delimiter,
+ rcpt, &input.username, &delim,
+ &detail);
+ ret = mail_storage_service_lookup_next(storage_service, &input,
+ &service_user, &user, &error);
+ restrict_access_allow_coredumps(TRUE);
+ if (ret == 0) {
+ e_debug(client->event, "User `%s' not found", input.username);
+ value = nouser_reply;
+ } else if (ret > 0) {
+ enum quota_alloc_result qret = quota_check(user, client->size,
+ &error);
+ if (qret == QUOTA_ALLOC_RESULT_OK) {
+ e_debug(client->event,
+ "Message is acceptable");
+ } else {
+ e_debug(client->event,
+ "Quota check failed: %s", error);
+ }
+
+ switch (qret) {
+ case QUOTA_ALLOC_RESULT_OK: /* under quota */
+ value = mail_user_plugin_getenv(user,
+ "quota_status_success");
+ if (value == NULL)
+ value = "OK";
+ break;
+ case QUOTA_ALLOC_RESULT_OVER_MAXSIZE:
+ /* even over maximum quota */
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT:
+ value = mail_user_plugin_getenv(user,
+ "quota_status_toolarge");
+ /* fall through */
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA:
+ if (value == NULL)
+ value = mail_user_plugin_getenv(user,
+ "quota_status_overquota");
+ if (value == NULL)
+ value = t_strdup_printf("554 5.2.2 %s", error);
+ break;
+ case QUOTA_ALLOC_RESULT_TEMPFAIL:
+ case QUOTA_ALLOC_RESULT_BACKGROUND_CALC:
+ ret = -1;
+ break;
+ }
+ value = t_strdup(value); /* user's pool is being freed */
+ mail_user_deinit(&user);
+ mail_storage_service_user_unref(&service_user);
+ } else {
+ e_error(client->event,
+ "Failed to lookup user %s: %s", input.username, error);
+ error = "Temporary internal error";
+ }
+
+ resp = t_str_new(256);
+ if (ret < 0) {
+ /* temporary failure */
+ str_append(resp, "action=DEFER_IF_PERMIT ");
+ str_append(resp, error);
+ } else {
+ str_append(resp, "action=");
+ str_append(resp, value);
+ }
+
+ e_debug(client->event, "Response: %s", str_c(resp));
+ str_append(resp, "\n\n");
+ o_stream_nsend_str(client->conn.output, str_c(resp));
+}
+
+static int client_input_line(struct connection *conn, const char *line)
+{
+ struct quota_client *client = (struct quota_client *)conn;
+
+ e_debug(client->event, "Request: %s", str_sanitize(line, 1024));
+
+ if (*line == '\0') {
+ o_stream_cork(conn->output);
+ client_handle_request(client);
+ o_stream_uncork(conn->output);
+ client_reset(client);
+ return 1;
+ }
+ if (str_begins(line, "recipient=")) {
+ if (client->recipient == NULL)
+ client->recipient = i_strdup(line + 10);
+ } else if (str_begins(line, "size=")) {
+ if (str_to_uoff(line+5, &client->size) < 0)
+ client->size = 0;
+ } else if (str_begins(line, "protocol_state=")) {
+ if (client->state == NULL)
+ client->state = i_strdup(line + 15);
+ }
+ return 1;
+}
+
+static void client_destroy(struct connection *conn)
+{
+ struct quota_client *client = (struct quota_client *)conn;
+
+ e_debug(client->event, "Client disconnected");
+
+ connection_deinit(&client->conn);
+ client_reset(client);
+ event_unref(&client->event);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static struct connection_settings client_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = client_destroy,
+ .input_line = client_input_line
+};
+
+static void main_preinit(void)
+{
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_init(void)
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &quota_status_setting_parser_info,
+ NULL
+ };
+ struct mail_storage_service_input input;
+ const struct setting_parser_info *user_info;
+ const struct setting_parser_context *set_parser;
+ const struct mail_user_settings *user_set;
+ const struct quota_status_settings *set;
+ const char *value, *error;
+ pool_t pool;
+
+ clients = connection_list_init(&client_set, &client_vfuncs);
+ storage_service = mail_storage_service_init(master_service, set_roots,
+ MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT |
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP |
+ MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP |
+ MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR);
+
+ i_zero(&input);
+ input.service = "quota-status";
+ input.module = "mail";
+ input.username = "";
+
+ quota_status_pool = pool_alloconly_create("quota status settings", 512);
+ pool = pool_alloconly_create("service all settings", 4096);
+ if (mail_storage_service_read_settings(storage_service, &input, pool,
+ &user_info, &set_parser,
+ &error) < 0)
+ i_fatal("%s", error);
+ user_set = master_service_settings_parser_get_others(master_service,
+ set_parser)[0];
+ set = master_service_settings_get_others(master_service)[1];
+
+ quota_status_settings = settings_dup(&quota_status_setting_parser_info, set,
+ quota_status_pool);
+ value = mail_user_set_plugin_getenv(user_set, "quota_status_nouser");
+ nouser_reply = p_strdup(quota_status_pool,
+ value != NULL ? value : "REJECT Unknown user");
+ pool_unref(&pool);
+}
+
+static void main_deinit(void)
+{
+ pool_unref(&quota_status_pool);
+ connection_list_deinit(&clients);
+ mail_storage_service_deinit(&storage_service);
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ int c;
+
+ protocol = QUOTA_PROTOCOL_UNKNOWN;
+ master_service = master_service_init("quota-status", service_flags,
+ &argc, &argv, "p:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'p':
+ if (strcmp(optarg, "postfix") == 0)
+ protocol = QUOTA_PROTOCOL_POSTFIX;
+ else
+ i_fatal("Unknown -p parameter: '%s'", optarg);
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ if (protocol == QUOTA_PROTOCOL_UNKNOWN)
+ i_fatal("Missing -p parameter");
+
+ master_service_init_log(master_service);
+ main_preinit();
+
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/plugins/quota/quota-storage.c b/src/plugins/quota/quota-storage.c
new file mode 100644
index 0000000..a1d08ee
--- /dev/null
+++ b/src/plugins/quota/quota-storage.c
@@ -0,0 +1,780 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "mail-search-build.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "maildir-storage.h"
+#include "index-mailbox-size.h"
+#include "quota-private.h"
+#include "quota-plugin.h"
+
+#include <sys/stat.h>
+
+#define QUOTA_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, quota_storage_module)
+#define QUOTA_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, quota_storage_module)
+#define QUOTA_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, quota_mail_module)
+#define QUOTA_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, quota_mailbox_list_module)
+
+struct quota_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+};
+
+struct quota_mailbox {
+ union mailbox_module_context module_ctx;
+
+ struct mailbox_transaction_context *expunge_trans;
+ struct quota_transaction_context *expunge_qt;
+ ARRAY(uint32_t) expunge_uids;
+ ARRAY(uoff_t) expunge_sizes;
+ unsigned int prev_idx;
+
+ bool recalculate:1;
+ bool sync_transaction_expunge:1;
+};
+
+struct quota_user_module quota_user_module =
+ MODULE_CONTEXT_INIT(&mail_user_module_register);
+
+static MODULE_CONTEXT_DEFINE_INIT(quota_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(quota_mail_module, &mail_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(quota_mailbox_list_module,
+ &mailbox_list_module_register);
+
+static void quota_set_storage_error(struct quota_transaction_context *qt,
+ struct mailbox *box,
+ enum quota_alloc_result res,
+ const char *internal_err)
+{
+ const char *errstr = quota_alloc_result_errstr(res, qt);
+ struct mail_storage *storage = box->storage;
+ switch (res) {
+ case QUOTA_ALLOC_RESULT_OVER_MAXSIZE:
+ mail_storage_set_error(storage, MAIL_ERROR_LIMIT, errstr);
+ break;
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT:
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA:
+ mail_storage_set_error(storage, MAIL_ERROR_NOQUOTA, errstr);
+ break;
+ case QUOTA_ALLOC_RESULT_TEMPFAIL:
+ case QUOTA_ALLOC_RESULT_BACKGROUND_CALC:
+ mailbox_set_critical(box, "quota: %s", internal_err);
+ break;
+ case QUOTA_ALLOC_RESULT_OK:
+ i_unreached();
+ }
+}
+
+static void quota_mail_expunge(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(_mail->box);
+ struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(_mail->box->storage->user);
+ union mail_module_context *qmail = QUOTA_MAIL_CONTEXT(mail);
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(_mail->transaction);
+ uoff_t size;
+ int ret;
+
+ if (qt->auto_updating) {
+ qmail->super.expunge(_mail);
+ return;
+ }
+
+ /* We need to handle the situation where multiple transactions expunged
+ the mail at the same time. In here we'll just save the message's
+ physical size and do the quota freeing later when the message was
+ known to be expunged. */
+ if (quser->quota->set->vsizes)
+ ret = mail_get_virtual_size(_mail, &size);
+ else
+ ret = mail_get_physical_size(_mail, &size);
+ if (ret == 0) {
+ if (!array_is_created(&qbox->expunge_uids)) {
+ i_array_init(&qbox->expunge_uids, 64);
+ i_array_init(&qbox->expunge_sizes, 64);
+ }
+ array_push_back(&qbox->expunge_uids, &_mail->uid);
+ array_push_back(&qbox->expunge_sizes, &size);
+ if ((_mail->transaction->flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0) {
+ /* we're running dsync. if this brings the quota below
+ a negative quota warning, don't execute it, because
+ it probably was already executed by the replica. */
+ qbox->sync_transaction_expunge = TRUE;
+ } else {
+ qbox->sync_transaction_expunge = FALSE;
+ }
+ }
+
+ qmail->super.expunge(_mail);
+}
+
+static int
+quota_get_status(struct mailbox *box, enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box);
+ struct quota_transaction_context *qt;
+ int ret = 0;
+
+ if ((items & STATUS_CHECK_OVER_QUOTA) != 0) {
+ qt = quota_transaction_begin(box);
+ const char *error;
+ enum quota_alloc_result qret = quota_test_alloc(qt, 0, &error);
+ if (qret != QUOTA_ALLOC_RESULT_OK) {
+ quota_set_storage_error(qt, box, qret, error);
+ ret = -1;
+ }
+ quota_transaction_rollback(&qt);
+
+ if ((items & ENUM_NEGATE(STATUS_CHECK_OVER_QUOTA)) == 0) {
+ /* don't bother calling parent, it may unnecessarily
+ try to open the mailbox */
+ return ret < 0 ? -1 : 0;
+ }
+ }
+
+ if (qbox->module_ctx.super.get_status(box, items, status_r) < 0)
+ ret = -1;
+ return ret < 0 ? -1 : 0;
+}
+
+static struct mailbox_transaction_context *
+quota_mailbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box);
+ struct mailbox_transaction_context *t;
+ struct quota_transaction_context *qt;
+
+ t = qbox->module_ctx.super.transaction_begin(box, flags, reason);
+ qt = quota_transaction_begin(box);
+ qt->sync_transaction = (flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0;
+
+ MODULE_CONTEXT_SET(t, quota_storage_module, qt);
+ return t;
+}
+
+static int
+quota_mailbox_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box);
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(ctx);
+
+ i_assert(qt->tmp_mail == NULL);
+
+ if (qbox->module_ctx.super.transaction_commit(ctx, changes_r) < 0) {
+ quota_transaction_rollback(&qt);
+ return -1;
+ } else {
+ (void)quota_transaction_commit(&qt);
+ return 0;
+ }
+}
+
+static void
+quota_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box);
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(ctx);
+
+ i_assert(qt->tmp_mail == NULL);
+
+ qbox->module_ctx.super.transaction_rollback(ctx);
+ quota_transaction_rollback(&qt);
+}
+
+void quota_mail_allocated(struct mail *_mail)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT(_mail->box);
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ union mail_module_context *qmail;
+
+ if (qbox == NULL)
+ return;
+
+ qmail = p_new(mail->pool, union mail_module_context, 1);
+ qmail->super = *v;
+ mail->vlast = &qmail->super;
+
+ v->expunge = quota_mail_expunge;
+ MODULE_CONTEXT_SET_SELF(mail, quota_mail_module, qmail);
+}
+
+static bool
+quota_move_requires_check(struct mailbox *dest_box, struct mailbox *src_box)
+{
+ struct mail_namespace *src_ns = src_box->list->ns;
+ struct mail_namespace *dest_ns = dest_box->list->ns;
+ struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(src_ns->user);
+ struct quota_root *const *rootp;
+
+ array_foreach(&quser->quota->roots, rootp) {
+ bool have_src_quota, have_dest_quota;
+
+ have_src_quota = quota_root_is_namespace_visible(*rootp, src_ns);
+ have_dest_quota = quota_root_is_namespace_visible(*rootp, dest_ns);
+ if (have_src_quota == have_dest_quota) {
+ /* Both/neither have this quota */
+ } else if (have_dest_quota) {
+ /* Destination mailbox has a quota that doesn't exist
+ in source. We'll need to check if it's being
+ exceeded. */
+ return TRUE;
+ } else {
+ /* Source mailbox has a quota root that doesn't exist
+ in destination. We're not increasing the source
+ quota, so ignore it. */
+ }
+ }
+ return FALSE;
+}
+
+static int quota_check(struct mail_save_context *ctx, struct mailbox *src_box)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t);
+ enum quota_alloc_result ret;
+
+ i_assert(!ctx->moving || src_box != NULL);
+
+ if (ctx->moving &&
+ !quota_move_requires_check(ctx->transaction->box, src_box)) {
+ /* the mail is being moved. the quota won't increase (after
+ the following expunge), so allow this even if user is
+ currently over quota */
+ quota_alloc(qt, ctx->dest_mail);
+ return 0;
+ } else if (qt->failed) {
+ return 0;
+ }
+
+ const char *error;
+ ret = quota_try_alloc(qt, ctx->dest_mail, &error);
+ switch (ret) {
+ case QUOTA_ALLOC_RESULT_OK:
+ return 0;
+ case QUOTA_ALLOC_RESULT_TEMPFAIL:
+ /* Log the error, but allow saving anyway. */
+ e_error(qt->quota->event,
+ "Failed to check if user is under quota: %s - "
+ "saving mail anyway", error);
+ return 0;
+ case QUOTA_ALLOC_RESULT_BACKGROUND_CALC:
+ /* Could not determine if there is enough space due to ongoing
+ background quota calculation, allow saving anyway. */
+ e_warning(qt->quota->event,
+ "Failed to check if user is under quota: %s - "
+ "saving mail anyway", error);
+ return 0;
+ default:
+ quota_set_storage_error(qt, t->box, ret, error);
+ return -1;
+ }
+}
+
+static int
+quota_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t);
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(t->box);
+
+ /* we always want to know the mail size */
+ mail_add_temp_wanted_fields(ctx->dest_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL);
+
+ /* get quota before copying any mails. this avoids dovecot-vsize.lock
+ deadlocks with backends that lock mails for expunging/copying. */
+ enum quota_get_result error_res;
+ const char *error;
+ if (quota_transaction_set_limits(qt, &error_res, &error) < 0) {
+ if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) {
+ e_warning(qt->quota->event,
+ "%s - copying mail anyway", error);
+ } else {
+ e_error(qt->quota->event,
+ "%s - copying mail anyway", error);
+ }
+ }
+
+ if (qbox->module_ctx.super.copy(ctx, mail) < 0)
+ return -1;
+
+ if (ctx->copying_via_save) {
+ /* copying used saving internally, we already checked the
+ quota */
+ return 0;
+ }
+ return quota_check(ctx, mail->box);
+}
+
+static int
+quota_save_begin(struct mail_save_context *ctx, struct istream *input)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t);
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(t->box);
+ const char *error;
+ uoff_t size;
+
+ if (!ctx->moving && i_stream_get_size(input, TRUE, &size) > 0 &&
+ !qt->failed) {
+ /* Input size is known, check for quota immediately. This
+ check isn't perfect, especially because input stream's
+ linefeeds may contain CR+LFs while physical message would
+ only contain LFs. With mbox some headers might be skipped
+ entirely.
+
+ I think these don't really matter though compared to the
+ benefit of giving "out of quota" error before sending the
+ full mail. */
+
+ enum quota_alloc_result qret = quota_test_alloc(qt, size, &error);
+ switch (qret) {
+ case QUOTA_ALLOC_RESULT_OK:
+ /* Great, there is space. */
+ break;
+ case QUOTA_ALLOC_RESULT_TEMPFAIL:
+ /* Log the error, but allow saving anyway. */
+ e_error(qt->quota->event,
+ "Failed to check if user is under quota: %s - "
+ "saving mail anyway", error);
+ break;
+ case QUOTA_ALLOC_RESULT_BACKGROUND_CALC:
+ /* Could not determine if there is enough space due to
+ * ongoing background quota calculation, allow saving
+ * anyway. */
+ e_warning(qt->quota->event,
+ "Failed to check if user is under quota: %s - "
+ "saving mail anyway", error);
+ break;
+ default:
+ quota_set_storage_error(qt, t->box, qret, error);
+ return -1;
+ }
+ }
+
+ /* we always want to know the mail size */
+ mail_add_temp_wanted_fields(ctx->dest_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL);
+
+ /* get quota before copying any mails. this avoids dovecot-vsize.lock
+ deadlocks with backends that lock mails for expunging/copying. */
+ enum quota_get_result error_res;
+ if (quota_transaction_set_limits(qt, &error_res, &error) < 0) {
+ if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC)
+ e_warning(qt->quota->event,
+ "%s - saving mail anyway", error);
+ else
+ e_error(qt->quota->event,
+ "%s - saving mail anyway", error);
+ }
+
+ return qbox->module_ctx.super.save_begin(ctx, input);
+}
+
+static int quota_save_finish(struct mail_save_context *ctx)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->transaction->box);
+ struct mailbox *src_box;
+
+ if (qbox->module_ctx.super.save_finish(ctx) < 0)
+ return -1;
+
+ src_box = ctx->copy_src_mail == NULL ? NULL : ctx->copy_src_mail->box;
+ return quota_check(ctx, src_box);
+}
+
+static void quota_mailbox_sync_cleanup(struct quota_mailbox *qbox)
+{
+ if (array_is_created(&qbox->expunge_uids)) {
+ array_clear(&qbox->expunge_uids);
+ array_clear(&qbox->expunge_sizes);
+ }
+
+ if (qbox->expunge_qt != NULL && qbox->expunge_qt->tmp_mail != NULL) {
+ mail_free(&qbox->expunge_qt->tmp_mail);
+ (void)mailbox_transaction_commit(&qbox->expunge_trans);
+ }
+ qbox->sync_transaction_expunge = FALSE;
+}
+
+static void quota_mailbox_sync_commit(struct quota_mailbox *qbox)
+{
+ quota_mailbox_sync_cleanup(qbox);
+ if (qbox->expunge_qt != NULL)
+ (void)quota_transaction_commit(&qbox->expunge_qt);
+ qbox->recalculate = FALSE;
+}
+
+static void quota_mailbox_sync_notify(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box);
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(box->storage->user);
+ const uint32_t *uids;
+ const uoff_t *sizep;
+ unsigned int i, count;
+ uoff_t size;
+
+ if (qbox->module_ctx.super.sync_notify != NULL)
+ qbox->module_ctx.super.sync_notify(box, uid, sync_type);
+
+ if (sync_type != MAILBOX_SYNC_TYPE_EXPUNGE || qbox->recalculate ||
+ (box->flags & MAILBOX_FLAG_DELETE_UNSAFE) != 0) {
+ if (uid == 0) {
+ /* free the transaction before view syncing begins,
+ otherwise it'll crash. */
+ quota_mailbox_sync_cleanup(qbox);
+ }
+ return;
+ }
+
+ if (qbox->expunge_qt == NULL) {
+ qbox->expunge_qt = quota_transaction_begin(box);
+ qbox->expunge_qt->sync_transaction =
+ qbox->sync_transaction_expunge;
+ }
+ if (qbox->expunge_qt->auto_updating) {
+ /* even though backend doesn't care about size/count changes,
+ make sure count_used changes so quota_warnings are
+ executed */
+ quota_free_bytes(qbox->expunge_qt, 0);
+ return;
+ }
+
+ /* we're in the middle of syncing the mailbox, so it's a bad idea to
+ try and get the message sizes at this point. Rely on sizes that
+ we saved earlier, or recalculate the whole quota if we don't know
+ the size. */
+ if (!array_is_created(&qbox->expunge_uids) ||
+ array_is_empty(&qbox->expunge_uids)) {
+ i = count = 0;
+ } else {
+ uids = array_get(&qbox->expunge_uids, &count);
+ for (i = qbox->prev_idx; i < count; i++) {
+ if (uids[i] == uid)
+ break;
+ }
+ if (i >= count) {
+ for (i = 0; i < qbox->prev_idx; i++) {
+ if (uids[i] == uid)
+ break;
+ }
+ if (i == qbox->prev_idx)
+ i = count;
+ }
+ qbox->prev_idx = i;
+ }
+
+ if (i != count) {
+ /* we already know the size */
+ sizep = array_idx(&qbox->expunge_sizes, i);
+ quota_free_bytes(qbox->expunge_qt, *sizep);
+ /* FIXME: it's not ideal that we do the vsize update here, but
+ this is the easiest place for it for now.. maybe the mail
+ size checking code could be moved to lib-storage */
+ if (ibox->vsize_update != NULL && quser->quota->set->vsizes)
+ index_mailbox_vsize_hdr_expunge(ibox->vsize_update, uid, *sizep);
+ return;
+ }
+
+ /* try to look up the size. this works only if it's cached. */
+ if (qbox->expunge_qt->tmp_mail == NULL) {
+ /* FIXME: ugly kludge to open the transaction for sync_view.
+ box->view may not have all the new messages that
+ sync_notify() notifies about, and those messages would
+ cause a quota recalculation. */
+ struct mail_index_view *box_view = box->view;
+ if (box->tmp_sync_view != NULL)
+ box->view = box->tmp_sync_view;
+ qbox->expunge_trans = mailbox_transaction_begin(box, 0, "quota");
+ box->view = box_view;
+ qbox->expunge_qt->tmp_mail =
+ mail_alloc(qbox->expunge_trans,
+ MAIL_FETCH_PHYSICAL_SIZE, NULL);
+ }
+ if (!mail_set_uid(qbox->expunge_qt->tmp_mail, uid))
+ ;
+ else if (!quser->quota->set->vsizes) {
+ if (mail_get_physical_size(qbox->expunge_qt->tmp_mail, &size) == 0) {
+ quota_free_bytes(qbox->expunge_qt, size);
+ return;
+ }
+ } else if (mail_get_virtual_size(qbox->expunge_qt->tmp_mail, &size) == 0) {
+ quota_free_bytes(qbox->expunge_qt, size);
+ if (ibox->vsize_update != NULL)
+ index_mailbox_vsize_hdr_expunge(ibox->vsize_update, uid, size);
+ } else {
+ /* there's no way to get the size. recalculate the quota. */
+ quota_recalculate(qbox->expunge_qt, QUOTA_RECALCULATE_MISSING_FREES);
+ qbox->recalculate = TRUE;
+ }
+}
+
+static int quota_mailbox_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box);
+ int ret;
+
+ ret = qbox->module_ctx.super.sync_deinit(ctx, status_r);
+ /* update quota only after syncing is finished. the quota commit may
+ recalculate the quota and cause all mailboxes to be synced,
+ including the one we're already syncing. */
+ quota_mailbox_sync_commit(qbox);
+ return ret;
+}
+
+static void quota_roots_flush(struct quota *quota)
+{
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i]->backend.v.flush != NULL)
+ roots[i]->backend.v.flush(roots[i]);
+ }
+}
+
+static void quota_mailbox_close(struct mailbox *box)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box);
+ struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(box->storage->user);
+
+ /* sync_notify() may be called outside sync_begin()..sync_deinit().
+ make sure we apply changes at close time at latest. */
+ quota_mailbox_sync_commit(qbox);
+
+ /* make sure quota backend flushes all data. this could also be done
+ somewhat later, but user.deinit() is too late, since the flushing
+ can trigger quota recalculation which isn't safe to do anymore
+ at user.deinit() when most of the loaded plugins have already been
+ deinitialized. */
+ quota_roots_flush(quser->quota);
+
+ qbox->module_ctx.super.close(box);
+}
+
+static void quota_mailbox_free(struct mailbox *box)
+{
+ struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box);
+
+ if (array_is_created(&qbox->expunge_uids)) {
+ array_free(&qbox->expunge_uids);
+ array_free(&qbox->expunge_sizes);
+ }
+ i_assert(qbox->expunge_qt == NULL ||
+ qbox->expunge_qt->tmp_mail == NULL);
+
+ qbox->module_ctx.super.free(box);
+}
+
+void quota_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct quota_mailbox *qbox;
+
+ if (QUOTA_LIST_CONTEXT(box->list) == NULL)
+ return;
+
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0)
+ return;
+
+ qbox = p_new(box->pool, struct quota_mailbox, 1);
+ qbox->module_ctx.super = *v;
+ box->vlast = &qbox->module_ctx.super;
+
+ v->get_status = quota_get_status;
+ v->transaction_begin = quota_mailbox_transaction_begin;
+ v->transaction_commit = quota_mailbox_transaction_commit;
+ v->transaction_rollback = quota_mailbox_transaction_rollback;
+ v->save_begin = quota_save_begin;
+ v->save_finish = quota_save_finish;
+ v->copy = quota_copy;
+ v->sync_notify = quota_mailbox_sync_notify;
+ v->sync_deinit = quota_mailbox_sync_deinit;
+ v->close = quota_mailbox_close;
+ v->free = quota_mailbox_free;
+ MODULE_CONTEXT_SET(box, quota_storage_module, qbox);
+}
+
+static void quota_mailbox_list_deinit(struct mailbox_list *list)
+{
+ struct quota_mailbox_list *qlist = QUOTA_LIST_CONTEXT(list);
+
+ i_assert(qlist != NULL);
+ quota_remove_user_namespace(list->ns);
+ qlist->module_ctx.super.deinit(list);
+}
+
+struct quota *quota_get_mail_user_quota(struct mail_user *user)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+
+ return quser == NULL ? NULL : quser->quota;
+}
+
+static void quota_user_deinit(struct mail_user *user)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(user);
+ struct quota_settings *quota_set = quser->quota->set;
+
+ quota_deinit(&quser->quota);
+ quser->module_ctx.super.deinit(user);
+
+ quota_settings_deinit(&quota_set);
+}
+
+void quota_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct quota_user *quser;
+ struct quota_settings *set;
+ struct quota *quota;
+ const char *error;
+ int ret;
+
+ if ((ret = quota_user_read_settings(user, &set, &error)) > 0) {
+ if (quota_init(set, user, &quota, &error) < 0) {
+ quota_settings_deinit(&set);
+ ret = -1;
+ }
+ }
+
+ if (ret < 0) {
+ user->error = p_strdup_printf(user->pool,
+ "Failed to initialize quota: %s", error);
+ return;
+ }
+ if (ret > 0) {
+ quser = p_new(user->pool, struct quota_user, 1);
+ quser->module_ctx.super = *v;
+ user->vlast = &quser->module_ctx.super;
+ v->deinit = quota_user_deinit;
+ quser->quota = quota;
+
+ MODULE_CONTEXT_SET(user, quota_user_module, quser);
+ } else {
+ e_debug(user->event, "quota: No quota setting - plugin disabled");
+ }
+}
+
+static struct quota_root *
+quota_find_root_for_ns(struct quota *quota, struct mail_namespace *ns)
+{
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i]->ns_prefix != NULL &&
+ strcmp(roots[i]->ns_prefix, ns->prefix) == 0)
+ return roots[i];
+ }
+ return NULL;
+}
+
+void quota_mailbox_list_created(struct mailbox_list *list)
+{
+ struct quota_mailbox_list *qlist;
+ struct quota *quota = NULL;
+ struct quota_root *root;
+ struct mail_user *quota_user;
+ bool add;
+
+ /* see if we have a quota explicitly defined for this namespace */
+ quota = quota_get_mail_user_quota(list->ns->user);
+ if (quota == NULL)
+ return;
+ root = quota_find_root_for_ns(quota, list->ns);
+ if (root != NULL) {
+ /* explicit quota root */
+ root->ns = list->ns;
+ quota_user = list->ns->user;
+ } else {
+ quota_user = list->ns->owner != NULL ?
+ list->ns->owner : list->ns->user;
+ }
+
+ if ((list->ns->flags & NAMESPACE_FLAG_NOQUOTA) != 0)
+ add = FALSE;
+ else if (list->ns->owner == NULL) {
+ /* public namespace - add quota only if namespace is
+ explicitly defined for it */
+ add = root != NULL;
+ } else {
+ /* for shared namespaces add only if the owner has quota
+ enabled */
+ add = QUOTA_USER_CONTEXT(quota_user) != NULL;
+ }
+
+ if (add) {
+ struct mailbox_list_vfuncs *v = list->vlast;
+
+ qlist = p_new(list->pool, struct quota_mailbox_list, 1);
+ qlist->module_ctx.super = *v;
+ list->vlast = &qlist->module_ctx.super;
+ v->deinit = quota_mailbox_list_deinit;
+ MODULE_CONTEXT_SET(list, quota_mailbox_list_module, qlist);
+
+ quota = quota_get_mail_user_quota(quota_user);
+ i_assert(quota != NULL);
+ quota_add_user_namespace(quota, list->ns);
+ }
+}
+
+static void quota_root_set_namespace(struct quota_root *root,
+ struct mail_namespace *namespaces)
+{
+ const struct quota_rule *rule;
+ const char *name;
+ struct mail_namespace *ns;
+ /* silence errors for autocreated (shared) users */
+ bool silent_errors = namespaces->user->autocreated;
+
+ if (root->ns_prefix != NULL && root->ns == NULL) {
+ root->ns = mail_namespace_find_prefix(namespaces,
+ root->ns_prefix);
+ if (root->ns == NULL && !silent_errors) {
+ e_error(root->quota->event,
+ "Unknown namespace: %s",
+ root->ns_prefix);
+ }
+ }
+
+ array_foreach(&root->set->rules, rule) {
+ name = rule->mailbox_mask;
+ ns = mail_namespace_find(namespaces, name);
+ if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0 &&
+ !silent_errors)
+ e_error(root->quota->event,
+ "Unknown namespace: %s", name);
+ }
+}
+
+void quota_mail_namespaces_created(struct mail_namespace *namespaces)
+{
+ struct quota *quota;
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ quota = quota_get_mail_user_quota(namespaces->user);
+ if (quota == NULL)
+ return;
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++)
+ quota_root_set_namespace(roots[i], namespaces);
+
+ quota_over_flag_check_startup(quota);
+}
diff --git a/src/plugins/quota/quota-util.c b/src/plugins/quota/quota-util.c
new file mode 100644
index 0000000..95ce369
--- /dev/null
+++ b/src/plugins/quota/quota-util.c
@@ -0,0 +1,465 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "wildcard-match.h"
+#include "quota-private.h"
+
+#include <ctype.h>
+
+#define QUOTA_DEFAULT_GRACE "10%"
+
+#define RULE_NAME_DEFAULT_FORCE "*"
+#define RULE_NAME_DEFAULT_NONFORCE "?"
+
+struct quota_rule *
+quota_root_rule_find(struct quota_root_settings *root_set, const char *name)
+{
+ struct quota_rule *rule;
+
+ array_foreach_modifiable(&root_set->rules, rule) {
+ if (wildcard_match(name, rule->mailbox_mask))
+ return rule;
+ }
+ return NULL;
+}
+
+static struct quota_rule *
+quota_root_rule_find_exact(struct quota_root_settings *root_set,
+ const char *name)
+{
+ struct quota_rule *rule;
+
+ array_foreach_modifiable(&root_set->rules, rule) {
+ if (strcmp(rule->mailbox_mask, name) == 0)
+ return rule;
+ }
+ return NULL;
+}
+
+static int
+quota_rule_parse_percentage(struct quota_root_settings *root_set,
+ struct quota_rule *rule,
+ int64_t *limit, const char **error_r)
+{
+ int64_t percentage = *limit;
+
+ if (percentage <= -100 || percentage >= UINT_MAX) {
+ *error_r = "Invalid percentage";
+ return -1;
+ }
+
+ if (rule == &root_set->default_rule) {
+ *error_r = "Default rule can't be a percentage";
+ return -1;
+ }
+
+ if (limit == &rule->bytes_limit)
+ rule->bytes_percent = percentage;
+ else if (limit == &rule->count_limit)
+ rule->count_percent = percentage;
+ else
+ i_unreached();
+ return 0;
+}
+
+static int quota_limit_parse(struct quota_root_settings *root_set,
+ struct quota_rule *rule, const char *unit,
+ uint64_t multiply, int64_t *limit,
+ const char **error_r)
+{
+ switch (i_toupper(*unit)) {
+ case '\0':
+ /* default */
+ break;
+ case 'B':
+ multiply = 1;
+ break;
+ case 'K':
+ multiply = 1024;
+ break;
+ case 'M':
+ multiply = 1024*1024;
+ break;
+ case 'G':
+ multiply = 1024*1024*1024;
+ break;
+ case 'T':
+ multiply = 1024ULL*1024*1024*1024;
+ break;
+ case '%':
+ multiply = 0;
+ if (quota_rule_parse_percentage(root_set, rule, limit,
+ error_r) < 0)
+ return -1;
+ break;
+ default:
+ *error_r = t_strdup_printf("Unknown unit: %s", unit);
+ return -1;
+ }
+ *limit *= multiply;
+ return 0;
+}
+
+static void
+quota_rule_recalculate_relative_rules(struct quota_rule *rule,
+ int64_t bytes_limit, int64_t count_limit)
+{
+ if (rule->bytes_percent != 0)
+ rule->bytes_limit = bytes_limit * rule->bytes_percent / 100;
+ if (rule->count_percent != 0)
+ rule->count_limit = count_limit * rule->count_percent / 100;
+}
+
+void quota_root_recalculate_relative_rules(struct quota_root_settings *root_set,
+ int64_t bytes_limit,
+ int64_t count_limit)
+{
+ struct quota_rule *rule;
+ struct quota_warning_rule *warning_rule;
+
+ array_foreach_modifiable(&root_set->rules, rule) {
+ quota_rule_recalculate_relative_rules(rule, bytes_limit,
+ count_limit);
+ }
+
+ array_foreach_modifiable(&root_set->warning_rules, warning_rule) {
+ quota_rule_recalculate_relative_rules(&warning_rule->rule,
+ bytes_limit, count_limit);
+ }
+ quota_rule_recalculate_relative_rules(&root_set->grace_rule,
+ bytes_limit, 0);
+ root_set->last_mail_max_extra_bytes = root_set->grace_rule.bytes_limit;
+
+ if (root_set->set->initialized) {
+ e_debug(root_set->set->event,
+ "Quota root %s: Recalculated relative rules with "
+ "bytes=%lld count=%lld. Now grace=%"PRIu64, root_set->name,
+ (long long)bytes_limit, (long long)count_limit,
+ root_set->last_mail_max_extra_bytes);
+ }
+}
+
+static int
+quota_rule_parse_limits(struct quota_root_settings *root_set,
+ struct quota_rule *rule, const char *limits,
+ const char *full_rule_def,
+ bool relative_rule, const char **error_r)
+{
+ const char **args, *key, *value, *error, *p;
+ uint64_t multiply;
+ int64_t *limit;
+
+ args = t_strsplit(limits, ":");
+ for (; *args != NULL; args++) {
+ multiply = 1;
+ limit = NULL;
+
+ key = *args;
+ value = strchr(key, '=');
+ if (value == NULL)
+ value = "";
+ else
+ key = t_strdup_until(key, value++);
+
+ if (*value == '+') {
+ if (!relative_rule) {
+ *error_r = "Rule limit cannot have '+'";
+ return -1;
+ }
+ value++;
+ } else if (*value != '-' && relative_rule) {
+ e_warning(root_set->set->event, "quota root %s rule %s: "
+ "obsolete configuration for rule '%s' "
+ "should be changed to '%s=+%s'",
+ root_set->name, full_rule_def,
+ *args, key, value);
+ }
+
+ if (strcmp(key, "storage") == 0) {
+ multiply = 1024;
+ limit = &rule->bytes_limit;
+ if (str_parse_int64(value, limit, &p) < 0) {
+ *error_r = p_strdup_printf(root_set->set->pool,
+ "Invalid storage limit: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "bytes") == 0) {
+ limit = &rule->bytes_limit;
+ if (str_parse_int64(value, limit, &p) < 0) {
+ *error_r = p_strdup_printf(root_set->set->pool,
+ "Invalid bytes limit: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "messages") == 0) {
+ limit = &rule->count_limit;
+ if (str_parse_int64(value, limit, &p) < 0) {
+ *error_r = p_strdup_printf(root_set->set->pool,
+ "Invalid bytes messages: %s", value);
+ return -1;
+ }
+ } else {
+ *error_r = p_strdup_printf(root_set->set->pool,
+ "Unknown rule limit name: %s", key);
+ return -1;
+ }
+
+ if (quota_limit_parse(root_set, rule, p, multiply,
+ limit, &error) < 0) {
+ *error_r = p_strdup_printf(root_set->set->pool,
+ "Invalid rule limit value '%s': %s",
+ *args, error);
+ return -1;
+ }
+ }
+ if (!relative_rule) {
+ if (rule->bytes_limit < 0) {
+ *error_r = "Bytes limit can't be negative";
+ return -1;
+ }
+ if (rule->count_limit < 0) {
+ *error_r = "Count limit can't be negative";
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int quota_root_add_rule(struct quota_root_settings *root_set,
+ const char *rule_def, const char **error_r)
+{
+ struct quota_rule *rule;
+ const char *p, *mailbox_mask;
+ int ret = 0;
+
+ p = strchr(rule_def, ':');
+ if (p == NULL) {
+ *error_r = "Invalid rule";
+ return -1;
+ }
+
+ /* <mailbox mask>:<quota limits> */
+ mailbox_mask = t_strdup_until(rule_def, p++);
+
+ rule = quota_root_rule_find_exact(root_set, mailbox_mask);
+ if (rule == NULL) {
+ if (strcmp(mailbox_mask, RULE_NAME_DEFAULT_NONFORCE) == 0)
+ rule = &root_set->default_rule;
+ else if (strcmp(mailbox_mask, RULE_NAME_DEFAULT_FORCE) == 0) {
+ rule = &root_set->default_rule;
+ root_set->force_default_rule = TRUE;
+ } else {
+ rule = array_append_space(&root_set->rules);
+ rule->mailbox_mask = strcasecmp(mailbox_mask, "INBOX") == 0 ? "INBOX" :
+ p_strdup(root_set->set->pool, mailbox_mask);
+ }
+ }
+
+ if (strcmp(p, "ignore") == 0) {
+ rule->ignore = TRUE;
+ e_debug(root_set->set->event,
+ "Quota rule: root=%s mailbox=%s ignored",
+ root_set->name, mailbox_mask);
+ return 0;
+ }
+
+ if (str_begins(p, "backend=")) {
+ if (root_set->backend->v.parse_rule == NULL) {
+ *error_r = "backend rule not supported";
+ ret = -1;
+ } else if (!root_set->backend->v.parse_rule(root_set, rule,
+ p + 8, error_r))
+ ret = -1;
+ } else {
+ bool relative_rule = rule != &root_set->default_rule;
+
+ if (quota_rule_parse_limits(root_set, rule, p, rule_def,
+ relative_rule, error_r) < 0)
+ ret = -1;
+ }
+
+ quota_root_recalculate_relative_rules(root_set,
+ root_set->default_rule.bytes_limit,
+ root_set->default_rule.count_limit);
+ const char *rule_plus =
+ rule == &root_set->default_rule ? "" : "+";
+
+ e_debug(root_set->set->event, "Quota rule: root=%s mailbox=%s "
+ "bytes=%s%lld%s messages=%s%lld%s",
+ root_set->name, mailbox_mask,
+ rule->bytes_limit > 0 ? rule_plus : "",
+ (long long)rule->bytes_limit,
+ rule->bytes_percent == 0 ? "" :
+ t_strdup_printf(" (%u%%)", rule->bytes_percent),
+ rule->count_limit > 0 ? rule_plus : "",
+ (long long)rule->count_limit,
+ rule->count_percent == 0 ? "" :
+ t_strdup_printf(" (%u%%)", rule->count_percent));
+ return ret;
+}
+
+int quota_root_add_warning_rule(struct quota_root_settings *root_set,
+ const char *rule_def, const char **error_r)
+{
+ struct quota_warning_rule *warning;
+ struct quota_rule rule;
+ const char *p, *q;
+ int ret;
+ bool reverse = FALSE;
+
+ p = strchr(rule_def, ' ');
+ if (p == NULL || p[1] == '\0') {
+ *error_r = "No command specified";
+ return -1;
+ }
+
+ if (*rule_def == '+') {
+ /* warn when exceeding quota */
+ q = rule_def+1;
+ } else if (*rule_def == '-') {
+ /* warn when going below quota */
+ q = rule_def+1;
+ reverse = TRUE;
+ } else {
+ /* default: same as '+' */
+ q = rule_def;
+ }
+
+ i_zero(&rule);
+ ret = quota_rule_parse_limits(root_set, &rule, t_strdup_until(q, p),
+ rule_def, FALSE, error_r);
+ if (ret < 0)
+ return -1;
+
+ warning = array_append_space(&root_set->warning_rules);
+ warning->command = p_strdup(root_set->set->pool, p+1);
+ warning->rule = rule;
+ warning->reverse = reverse;
+ if (reverse)
+ root_set->have_reverse_warnings = TRUE;
+
+ quota_root_recalculate_relative_rules(root_set,
+ root_set->default_rule.bytes_limit,
+ root_set->default_rule.count_limit);
+ e_debug(root_set->set->event, "Quota warning: bytes=%"PRId64"%s "
+ "messages=%"PRId64"%s reverse=%s command=%s",
+ warning->rule.bytes_limit,
+ warning->rule.bytes_percent == 0 ? "" :
+ t_strdup_printf(" (%u%%)", warning->rule.bytes_percent),
+ warning->rule.count_limit,
+ warning->rule.count_percent == 0 ? "" :
+ t_strdup_printf(" (%u%%)", warning->rule.count_percent),
+ warning->reverse ? "yes" : "no",
+ warning->command);
+ return 0;
+}
+
+int quota_root_parse_grace(struct quota_root_settings *root_set,
+ const char *value, const char **error_r)
+{
+ const char *p;
+
+ if (value == NULL) {
+ /* default */
+ value = QUOTA_DEFAULT_GRACE;
+ }
+
+ if (str_parse_int64(value, &root_set->grace_rule.bytes_limit, &p) < 0)
+ return -1;
+ if (quota_limit_parse(root_set, &root_set->grace_rule, p, 1,
+ &root_set->grace_rule.bytes_limit, error_r) < 0)
+ return -1;
+ quota_rule_recalculate_relative_rules(&root_set->grace_rule,
+ root_set->default_rule.bytes_limit, 0);
+ root_set->last_mail_max_extra_bytes = root_set->grace_rule.bytes_limit;
+ e_debug(root_set->set->event, "Quota grace: root=%s bytes=%lld%s",
+ root_set->name, (long long)root_set->grace_rule.bytes_limit,
+ root_set->grace_rule.bytes_percent == 0 ? "" :
+ t_strdup_printf(" (%u%%)", root_set->grace_rule.bytes_percent));
+ return 0;
+}
+
+bool quota_warning_match(const struct quota_warning_rule *w,
+ uint64_t bytes_before, uint64_t bytes_current,
+ uint64_t count_before, uint64_t count_current,
+ const char **reason_r)
+{
+#define QUOTA_EXCEEDED(before, current, limit) \
+ ((before) < (uint64_t)(limit) && (current) >= (uint64_t)(limit))
+ if (!w->reverse) {
+ /* over quota (default) */
+ if (QUOTA_EXCEEDED(bytes_before, bytes_current, w->rule.bytes_limit)) {
+ *reason_r = t_strdup_printf("bytes=%"PRIu64" -> %"PRIu64" over limit %"PRId64,
+ bytes_before, bytes_current, w->rule.bytes_limit);
+ return TRUE;
+ }
+ if (QUOTA_EXCEEDED(count_before, count_current, w->rule.count_limit)) {
+ *reason_r = t_strdup_printf("count=%"PRIu64" -> %"PRIu64" over limit %"PRId64,
+ count_before, count_current, w->rule.count_limit);
+ return TRUE;
+ }
+ } else {
+ if (QUOTA_EXCEEDED(bytes_current, bytes_before, w->rule.bytes_limit)) {
+ *reason_r = t_strdup_printf("bytes=%"PRIu64" -> %"PRIu64" below limit %"PRId64,
+ bytes_before, bytes_current, w->rule.bytes_limit);
+ return TRUE;
+ }
+ if (QUOTA_EXCEEDED(count_current, count_before, w->rule.count_limit)) {
+ *reason_r = t_strdup_printf("count=%"PRIu64" -> %"PRIu64" below limit %"PRId64,
+ count_before, count_current, w->rule.count_limit);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool quota_transaction_is_over(struct quota_transaction_context *ctx,
+ uoff_t size)
+{
+ if (ctx->count_used < 0) {
+ /* we've deleted some messages. we should be ok, unless we
+ were already over quota and still are after these
+ deletions. */
+ const uint64_t count_deleted = (uint64_t)-ctx->count_used;
+
+ if (ctx->count_over > 0) {
+ if (count_deleted - 1 < ctx->count_over)
+ return TRUE;
+ }
+ } else {
+ if (ctx->count_ceil < 1 ||
+ ctx->count_ceil - 1 < (uint64_t)ctx->count_used) {
+ /* count limit reached */
+ return TRUE;
+ }
+ }
+
+ if (ctx->bytes_used < 0) {
+ const uint64_t bytes_deleted = (uint64_t)-ctx->bytes_used;
+
+ /* we've deleted some messages. same logic as above. */
+ if (ctx->bytes_over > 0) {
+ if (ctx->bytes_over > bytes_deleted) {
+ /* even after deletions we're over quota */
+ return TRUE;
+ }
+ if (size > bytes_deleted - ctx->bytes_over)
+ return TRUE;
+ } else {
+ if (size > bytes_deleted &&
+ size - bytes_deleted < ctx->bytes_ceil)
+ return TRUE;
+ }
+ } else if (size == 0) {
+ /* we need to explicitly test this case, since the generic
+ check would fail if user is already over quota */
+ if (ctx->bytes_over > 0)
+ return TRUE;
+ } else {
+ if (ctx->bytes_ceil < size ||
+ ctx->bytes_ceil - size < (uint64_t)ctx->bytes_used) {
+ /* bytes limit reached */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
diff --git a/src/plugins/quota/quota.c b/src/plugins/quota/quota.c
new file mode 100644
index 0000000..3d6d8e5
--- /dev/null
+++ b/src/plugins/quota/quota.c
@@ -0,0 +1,1543 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "ioloop.h"
+#include "net.h"
+#include "write-full.h"
+#include "eacces-error.h"
+#include "wildcard-match.h"
+#include "dict.h"
+#include "mailbox-list-private.h"
+#include "quota-private.h"
+#include "quota-fs.h"
+#include "llist.h"
+#include "program-client.h"
+#include "settings-parser.h"
+
+#include <sys/wait.h>
+
+#define DEFAULT_QUOTA_EXCEEDED_MSG \
+ "Quota exceeded (mailbox for user is full)"
+#define QUOTA_LIMIT_SET_PATH DICT_PATH_PRIVATE"quota/limit/"
+
+/* How many seconds after the userdb lookup do we still want to execute the
+ quota_over_script. This applies to quota_over_flag_lazy_check=yes and also
+ after unhibernating IMAP connections. */
+#define QUOTA_OVER_FLAG_MAX_DELAY_SECS 10
+
+struct quota_root_iter {
+ struct quota *quota;
+ struct mailbox *box;
+
+ unsigned int i;
+};
+
+unsigned int quota_module_id = 0;
+
+extern struct quota_backend quota_backend_count;
+extern struct quota_backend quota_backend_dict;
+extern struct quota_backend quota_backend_dirsize;
+extern struct quota_backend quota_backend_fs;
+extern struct quota_backend quota_backend_imapc;
+extern struct quota_backend quota_backend_maildir;
+
+static const struct quota_backend *quota_internal_backends[] = {
+#ifdef HAVE_FS_QUOTA
+ &quota_backend_fs,
+#endif
+ &quota_backend_count,
+ &quota_backend_dict,
+ &quota_backend_dirsize,
+ &quota_backend_imapc,
+ &quota_backend_maildir
+};
+
+static ARRAY(const struct quota_backend*) quota_backends;
+
+static void hidden_param_handler(struct quota_root *_root, const char *param_value);
+static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value);
+static void noenforcing_param_handler(struct quota_root *_root, const char *param_value);
+static void ns_param_handler(struct quota_root *_root, const char *param_value);
+
+struct quota_param_parser quota_param_hidden = {.param_name = "hidden", .param_handler = hidden_param_handler};
+struct quota_param_parser quota_param_ignoreunlimited = {.param_name = "ignoreunlimited", .param_handler = ignoreunlim_param_handler};
+struct quota_param_parser quota_param_noenforcing = {.param_name = "noenforcing", .param_handler = noenforcing_param_handler};
+struct quota_param_parser quota_param_ns = {.param_name = "ns=", .param_handler = ns_param_handler};
+
+static enum quota_alloc_result quota_default_test_alloc(
+ struct quota_transaction_context *ctx, uoff_t size,
+ const char **error_r);
+static void quota_over_flag_check_root(struct quota_root *root);
+
+static const struct quota_backend *quota_backend_find(const char *name)
+{
+ const struct quota_backend *const *backend;
+
+ array_foreach(&quota_backends, backend) {
+ if (strcmp((*backend)->name, name) == 0)
+ return *backend;
+ }
+
+ return NULL;
+}
+
+void quota_backend_register(const struct quota_backend *backend)
+{
+ i_assert(quota_backend_find(backend->name) == NULL);
+ array_push_back(&quota_backends, &backend);
+}
+
+void quota_backend_unregister(const struct quota_backend *backend)
+{
+ for(unsigned int i = 0; i < array_count(&quota_backends); i++) {
+ const struct quota_backend *be =
+ array_idx_elem(&quota_backends, i);
+ if (strcmp(be->name, backend->name) == 0) {
+ array_delete(&quota_backends, i, 1);
+ return;
+ }
+ }
+
+ i_unreached();
+}
+
+void quota_backends_register(void);
+void quota_backends_unregister(void);
+
+void quota_backends_register(void)
+{
+ i_array_init(&quota_backends, 8);
+ array_append(&quota_backends, quota_internal_backends,
+ N_ELEMENTS(quota_internal_backends));
+}
+
+void quota_backends_unregister(void)
+{
+ for(size_t i = 0; i < N_ELEMENTS(quota_internal_backends); i++) {
+ quota_backend_unregister(quota_internal_backends[i]);
+ }
+
+ i_assert(array_count(&quota_backends) == 0);
+ array_free(&quota_backends);
+
+}
+
+static int quota_root_add_rules(struct mail_user *user, const char *root_name,
+ struct quota_root_settings *root_set,
+ const char **error_r)
+{
+ const char *rule_name, *rule, *error;
+ unsigned int i;
+
+ rule_name = t_strconcat(root_name, "_rule", NULL);
+ for (i = 2;; i++) {
+ rule = mail_user_plugin_getenv(user, rule_name);
+ if (rule == NULL)
+ break;
+
+ if (quota_root_add_rule(root_set, rule, &error) < 0) {
+ *error_r = t_strdup_printf("Invalid rule %s: %s",
+ rule, error);
+ return -1;
+ }
+ rule_name = t_strdup_printf("%s_rule%d", root_name, i);
+ }
+ return 0;
+}
+
+static int
+quota_root_add_warning_rules(struct mail_user *user, const char *root_name,
+ struct quota_root_settings *root_set,
+ const char **error_r)
+{
+ const char *rule_name, *rule, *error;
+ unsigned int i;
+
+ rule_name = t_strconcat(root_name, "_warning", NULL);
+ for (i = 2;; i++) {
+ rule = mail_user_plugin_getenv(user, rule_name);
+ if (rule == NULL)
+ break;
+
+ if (quota_root_add_warning_rule(root_set, rule, &error) < 0) {
+ *error_r = t_strdup_printf("Invalid warning rule: %s",
+ rule);
+ return -1;
+ }
+ rule_name = t_strdup_printf("%s_warning%d", root_name, i);
+ }
+ return 0;
+}
+
+static int
+quota_root_parse_set(struct mail_user *user, const char *root_name,
+ struct quota_root_settings *root_set,
+ const char **error_r)
+{
+ const char *name, *value;
+
+ name = t_strconcat(root_name, "_set", NULL);
+ value = mail_user_plugin_getenv(user, name);
+ if (value == NULL)
+ return 0;
+
+ if (!str_begins(value, "dict:")) {
+ *error_r = t_strdup_printf("%s supports only dict backend", name);
+ return -1;
+ }
+ root_set->limit_set = p_strdup(root_set->set->pool, value+5);
+ return 0;
+}
+
+static int
+quota_root_settings_init(struct quota_settings *quota_set, const char *root_def,
+ struct quota_root_settings **set_r,
+ const char **error_r)
+{
+ struct quota_root_settings *root_set;
+ const struct quota_backend *backend;
+ const char *p, *args, *backend_name;
+
+ /* <backend>[:<quota root name>[:<backend args>]] */
+ p = strchr(root_def, ':');
+ if (p == NULL) {
+ backend_name = root_def;
+ args = NULL;
+ } else {
+ backend_name = t_strdup_until(root_def, p);
+ args = p + 1;
+ }
+
+ backend = quota_backend_find(backend_name);
+ if (backend == NULL) {
+ *error_r = t_strdup_printf("Unknown quota backend: %s",
+ backend_name);
+ return -1;
+ }
+
+ root_set = p_new(quota_set->pool, struct quota_root_settings, 1);
+ root_set->set = quota_set;
+ root_set->backend = backend;
+
+ if (args != NULL) {
+ /* save root's name */
+ p = strchr(args, ':');
+ if (p == NULL) {
+ root_set->name = p_strdup(quota_set->pool, args);
+ args = NULL;
+ } else {
+ root_set->name =
+ p_strdup_until(quota_set->pool, args, p);
+ args = p + 1;
+ }
+ } else {
+ root_set->name = "";
+ }
+ root_set->args = p_strdup(quota_set->pool, args);
+
+ e_debug(quota_set->event, "Quota root: name=%s backend=%s args=%s",
+ root_set->name, backend_name, args == NULL ? "" : args);
+
+ p_array_init(&root_set->rules, quota_set->pool, 4);
+ p_array_init(&root_set->warning_rules, quota_set->pool, 4);
+ array_push_back(&quota_set->root_sets, &root_set);
+ *set_r = root_set;
+ return 0;
+}
+
+static int
+quota_root_add(struct quota_settings *quota_set, struct mail_user *user,
+ const char *env, const char *root_name, const char **error_r)
+{
+ struct quota_root_settings *root_set;
+ const char *set_name, *value;
+
+ if (quota_root_settings_init(quota_set, env, &root_set, error_r) < 0)
+ return -1;
+ root_set->set_name = p_strdup(quota_set->pool, root_name);
+ if (quota_root_add_rules(user, root_name, root_set, error_r) < 0)
+ return -1;
+ if (quota_root_add_warning_rules(user, root_name, root_set, error_r) < 0)
+ return -1;
+ if (quota_root_parse_set(user, root_name, root_set, error_r) < 0)
+ return -1;
+
+ set_name = t_strconcat(root_name, "_grace", NULL);
+ value = mail_user_plugin_getenv(user, set_name);
+ if (quota_root_parse_grace(root_set, value, error_r) < 0) {
+ *error_r = t_strdup_printf("Invalid %s value '%s': %s",
+ set_name, value, *error_r);
+ return -1;
+ }
+ return 0;
+}
+
+const char *quota_alloc_result_errstr(enum quota_alloc_result res,
+ struct quota_transaction_context *qt)
+{
+ switch (res) {
+ case QUOTA_ALLOC_RESULT_OK:
+ return "OK";
+ case QUOTA_ALLOC_RESULT_BACKGROUND_CALC:
+ return "Blocked by an ongoing background quota calculation";
+ case QUOTA_ALLOC_RESULT_TEMPFAIL:
+ return "Internal quota calculation error";
+ case QUOTA_ALLOC_RESULT_OVER_MAXSIZE:
+ return "Mail size is larger than the maximum size allowed by "
+ "server configuration";
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT:
+ case QUOTA_ALLOC_RESULT_OVER_QUOTA:
+ return qt->quota->set->quota_exceeded_msg;
+ }
+ i_unreached();
+}
+
+int quota_user_read_settings(struct mail_user *user,
+ struct quota_settings **set_r,
+ const char **error_r)
+{
+ struct quota_settings *quota_set;
+ char root_name[5 + MAX_INT_STRLEN];
+ const char *env, *error;
+ unsigned int i;
+ pool_t pool;
+
+ pool = pool_alloconly_create("quota settings", 2048);
+ quota_set = p_new(pool, struct quota_settings, 1);
+ quota_set->pool = pool;
+ quota_set->event = event_create(user->event);
+ quota_set->test_alloc = quota_default_test_alloc;
+ quota_set->debug = user->mail_debug;
+ quota_set->quota_exceeded_msg =
+ mail_user_plugin_getenv(user, "quota_exceeded_message");
+ if (quota_set->quota_exceeded_msg == NULL)
+ quota_set->quota_exceeded_msg = DEFAULT_QUOTA_EXCEEDED_MSG;
+ quota_set->vsizes = mail_user_plugin_getenv_bool(user, "quota_vsizes");
+
+ const char *max_size = mail_user_plugin_getenv(user,
+ "quota_max_mail_size");
+ if (max_size != NULL) {
+ const char *error = NULL;
+ if (settings_get_size(max_size, &quota_set->max_mail_size,
+ &error) < 0) {
+ *error_r = t_strdup_printf("quota_max_mail_size: %s",
+ error);
+ return -1;
+ }
+ }
+
+ p_array_init(&quota_set->root_sets, pool, 4);
+ if (i_strocpy(root_name, "quota", sizeof(root_name)) < 0)
+ i_unreached();
+ for (i = 2;; i++) {
+ env = mail_user_plugin_getenv(user, root_name);
+ if (env == NULL || *env == '\0')
+ break;
+
+ if (quota_root_add(quota_set, user, env, root_name,
+ &error) < 0) {
+ *error_r = t_strdup_printf("Invalid quota root %s: %s",
+ root_name, error);
+ event_unref(&quota_set->event);
+ pool_unref(&pool);
+ return -1;
+ }
+ if (i_snprintf(root_name, sizeof(root_name), "quota%d", i) < 0)
+ i_unreached();
+ }
+ if (quota_set->max_mail_size == 0 &&
+ array_count(&quota_set->root_sets) == 0) {
+ event_unref(&quota_set->event);
+ pool_unref(&pool);
+ return 0;
+ }
+
+ quota_set->initialized = TRUE;
+ *set_r = quota_set;
+ return 1;
+}
+
+void quota_settings_deinit(struct quota_settings **_quota_set)
+{
+ struct quota_settings *quota_set = *_quota_set;
+
+ *_quota_set = NULL;
+
+ event_unref(&quota_set->event);
+ pool_unref(&quota_set->pool);
+}
+
+static void quota_root_deinit(struct quota_root *root)
+{
+ pool_t pool = root->pool;
+
+ if (root->limit_set_dict != NULL)
+ dict_deinit(&root->limit_set_dict);
+ event_unref(&root->backend.event);
+ root->backend.v.deinit(root);
+ pool_unref(&pool);
+}
+
+int quota_root_default_init(struct quota_root *root, const char *args,
+ const char **error_r)
+{
+ const struct quota_param_parser default_params[] = {
+ quota_param_hidden,
+ quota_param_ignoreunlimited,
+ quota_param_noenforcing,
+ quota_param_ns,
+ {.param_name = NULL}
+ };
+ return quota_parse_parameters(root, &args, error_r, default_params, TRUE);
+}
+
+static int
+quota_root_init(struct quota_root_settings *root_set, struct quota *quota,
+ struct quota_root **root_r, const char **error_r)
+{
+ struct quota_root *root;
+
+ root = root_set->backend->v.alloc();
+ root->pool = pool_alloconly_create("quota root", 512);
+ root->set = root_set;
+ root->quota = quota;
+ root->backend = *root_set->backend;
+ root->bytes_limit = root_set->default_rule.bytes_limit;
+ root->count_limit = root_set->default_rule.count_limit;
+
+ array_create(&root->quota_module_contexts, root->pool,
+ sizeof(void *), 10);
+
+ if (root->backend.v.init != NULL) {
+ root->backend.event = event_create(quota->event);
+ event_drop_parent_log_prefixes(root->backend.event, 1);
+ event_set_forced_debug(root->backend.event, root->quota->set->debug);
+
+ if (root->backend.v.init(root, root_set->args, error_r) < 0) {
+ *error_r = t_strdup_printf("%s quota init failed: %s",
+ root->backend.name, *error_r);
+
+ event_unref(&root->backend.event);
+ return -1;
+ }
+ } else {
+ if (quota_root_default_init(root, root_set->args, error_r) < 0)
+ return -1;
+ }
+ if (root_set->default_rule.bytes_limit == 0 &&
+ root_set->default_rule.count_limit == 0 &&
+ root->disable_unlimited_tracking) {
+ quota_root_deinit(root);
+ return 0;
+ }
+ *root_r = root;
+ return 1;
+}
+
+int quota_init(struct quota_settings *quota_set, struct mail_user *user,
+ struct quota **quota_r, const char **error_r)
+{
+ struct quota *quota;
+ struct quota_root *root;
+ struct quota_root_settings *const *root_sets;
+ unsigned int i, count;
+ const char *error;
+ int ret;
+
+ quota = i_new(struct quota, 1);
+ quota->event = event_create(user->event);
+ event_set_forced_debug(quota->event, quota_set->debug);
+ event_set_append_log_prefix(quota->event, "quota: ");
+ quota->user = user;
+ quota->set = quota_set;
+ i_array_init(&quota->roots, 8);
+
+ root_sets = array_get(&quota_set->root_sets, &count);
+ i_array_init(&quota->namespaces, count);
+ for (i = 0; i < count; i++) {
+ ret = quota_root_init(root_sets[i], quota, &root, &error);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("Quota root %s: %s",
+ root_sets[i]->name, error);
+ quota_deinit(&quota);
+ return -1;
+ }
+ if (ret > 0)
+ array_push_back(&quota->roots, &root);
+ }
+ *quota_r = quota;
+ return 0;
+}
+
+void quota_deinit(struct quota **_quota)
+{
+ struct quota *quota = *_quota;
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++)
+ quota_root_deinit(roots[i]);
+
+ /* deinit quota roots before setting quser->quota=NULL */
+ *_quota = NULL;
+
+ array_free(&quota->roots);
+ array_free(&quota->namespaces);
+ event_unref(&quota->event);
+ i_free(quota);
+}
+
+static int quota_root_get_rule_limits(struct quota_root *root,
+ const char *mailbox_name,
+ uint64_t *bytes_limit_r,
+ uint64_t *count_limit_r,
+ bool *ignored_r,
+ const char **error_r)
+{
+ struct quota_rule *rule;
+ int64_t bytes_limit, count_limit;
+ int ret;
+
+ *ignored_r = FALSE;
+
+ if (!root->set->force_default_rule) {
+ if (root->backend.v.init_limits != NULL) {
+ const char *error;
+ if (root->backend.v.init_limits(root, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Initializing limits failed for quota backend: %s",
+ error);
+ return -1;
+ }
+ }
+ }
+
+ bytes_limit = root->bytes_limit;
+ count_limit = root->count_limit;
+
+ /* if default rule limits are 0, user has unlimited quota.
+ ignore any specific quota rules */
+ if (bytes_limit != 0 || count_limit != 0) {
+ (void)mail_namespace_find_unalias(root->quota->user->namespaces,
+ &mailbox_name);
+ rule = quota_root_rule_find(root->set, mailbox_name);
+ ret = 1;
+ } else {
+ rule = NULL;
+ ret = 0;
+ }
+
+ if (rule != NULL) {
+ if (!rule->ignore) {
+ bytes_limit += rule->bytes_limit;
+ count_limit += rule->count_limit;
+ } else {
+ bytes_limit = 0;
+ count_limit = 0;
+ *ignored_r = TRUE;
+ }
+ }
+
+ *bytes_limit_r = bytes_limit <= 0 ? 0 : bytes_limit;
+ *count_limit_r = count_limit <= 0 ? 0 : count_limit;
+ return ret;
+}
+
+static bool
+quota_is_duplicate_namespace(struct quota *quota, struct mail_namespace *ns)
+{
+ struct mail_namespace *const *namespaces;
+ unsigned int i, count;
+ const char *path, *path2;
+
+ if (!mailbox_list_get_root_path(ns->list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &path))
+ path = NULL;
+
+ namespaces = array_get(&quota->namespaces, &count);
+ for (i = 0; i < count; i++) {
+ /* count namespace aliases only once. don't rely only on
+ alias_for != NULL, because the alias might have been
+ explicitly added as the wanted quota namespace. */
+ if (ns->alias_for == namespaces[i] ||
+ namespaces[i]->alias_for == ns)
+ continue;
+
+ if (path != NULL &&
+ mailbox_list_get_root_path(namespaces[i]->list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &path2) &&
+ strcmp(path, path2) == 0) {
+ /* duplicate path */
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) == 0)
+ return TRUE;
+
+ /* this is inbox=yes namespace, but the earlier one
+ that had the same location was inbox=no. we need to
+ include the INBOX also in quota calculations, so we
+ can't just ignore this namespace. but since we've
+ already called backend's namespace_added(), we can't
+ just remove it either. so just mark the old one as
+ unwanted namespace.
+
+ an alternative would be to do a bit larger change so
+ namespaces wouldn't be added until
+ mail_namespaces_created() hook is called */
+ i_assert(quota->unwanted_ns == NULL);
+ quota->unwanted_ns = namespaces[i];
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+void quota_add_user_namespace(struct quota *quota, struct mail_namespace *ns)
+{
+ struct quota_root *const *roots;
+ struct quota_backend **backends;
+ unsigned int i, j, count;
+
+ /* first check if there already exists a namespace with the exact same
+ path. we don't want to count them twice. */
+ if (quota_is_duplicate_namespace(quota, ns))
+ return;
+
+ array_push_back(&quota->namespaces, &ns);
+
+ roots = array_get(&quota->roots, &count);
+ /* @UNSAFE: get different backends into one array */
+ backends = t_new(struct quota_backend *, count + 1);
+ for (i = 0; i < count; i++) {
+ for (j = 0; backends[j] != NULL; j++) {
+ if (backends[j]->name == roots[i]->backend.name)
+ break;
+ }
+ if (backends[j] == NULL)
+ backends[j] = &roots[i]->backend;
+ }
+
+ for (i = 0; backends[i] != NULL; i++) {
+ if (backends[i]->v.namespace_added != NULL)
+ backends[i]->v.namespace_added(quota, ns);
+ }
+}
+
+void quota_remove_user_namespace(struct mail_namespace *ns)
+{
+ struct quota *quota;
+ struct mail_namespace *const *namespaces;
+ unsigned int i, count;
+
+ quota = ns->owner != NULL ?
+ quota_get_mail_user_quota(ns->owner) :
+ quota_get_mail_user_quota(ns->user);
+ if (quota == NULL) {
+ /* no quota for this namespace */
+ return;
+ }
+
+ namespaces = array_get(&quota->namespaces, &count);
+ for (i = 0; i < count; i++) {
+ if (namespaces[i] == ns) {
+ array_delete(&quota->namespaces, i, 1);
+ break;
+ }
+ }
+}
+
+struct quota_root_iter *
+quota_root_iter_init_user(struct mail_user *user)
+{
+ struct quota_root_iter *iter;
+
+ iter = i_new(struct quota_root_iter, 1);
+ iter->quota = quota_get_mail_user_quota(user);
+ return iter;
+}
+
+struct quota_root_iter *
+quota_root_iter_init(struct mailbox *box)
+{
+ struct quota_root_iter *iter;
+ struct mail_user *user;
+
+ user = box->list->ns->owner != NULL ?
+ box->list->ns->owner : box->list->ns->user;
+ iter = quota_root_iter_init_user(user);
+ iter->box = box;
+ return iter;
+}
+
+bool quota_root_is_namespace_visible(struct quota_root *root,
+ struct mail_namespace *ns)
+{
+ struct mailbox_list *list = ns->list;
+ struct mail_storage *storage;
+
+ /* this check works as long as there is only one storage per list */
+ if (mailbox_list_get_storage(&list, "", &storage) == 0 &&
+ (storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0)
+ return FALSE;
+ if (root->quota->unwanted_ns == ns)
+ return FALSE;
+
+ if (root->ns_prefix != NULL) {
+ if (root->ns != ns)
+ return FALSE;
+ } else {
+ if (ns->owner == NULL)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+quota_root_is_visible(struct quota_root *root, struct mailbox *box)
+{
+ if (!quota_root_is_namespace_visible(root, box->list->ns))
+ return FALSE;
+ if (array_count(&root->quota->roots) == 1) {
+ /* a single quota root: don't bother checking further */
+ return TRUE;
+ }
+ return root->backend.v.match_box == NULL ? TRUE :
+ root->backend.v.match_box(root, box);
+}
+
+struct quota_root *quota_root_iter_next(struct quota_root_iter *iter)
+{
+ struct quota_root *const *roots, *root = NULL;
+ unsigned int count;
+
+ if (iter->quota == NULL)
+ return NULL;
+
+ roots = array_get(&iter->quota->roots, &count);
+ if (iter->i >= count)
+ return NULL;
+
+ for (; iter->i < count; iter->i++) {
+ if (iter->box != NULL &&
+ !quota_root_is_visible(roots[iter->i], iter->box))
+ continue;
+
+ root = roots[iter->i];
+ break;
+ }
+
+ iter->i++;
+ return root;
+}
+
+void quota_root_iter_deinit(struct quota_root_iter **_iter)
+{
+ struct quota_root_iter *iter = *_iter;
+
+ *_iter = NULL;
+ i_free(iter);
+}
+
+struct quota_root *quota_root_lookup(struct mail_user *user, const char *name)
+{
+ struct quota *quota;
+ struct quota_root *const *roots;
+ unsigned int i, count;
+
+ quota = quota_get_mail_user_quota(user);
+ if (quota == NULL)
+ return NULL;
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(roots[i]->set->name, name) == 0)
+ return roots[i];
+ }
+ return NULL;
+}
+
+const char *quota_root_get_name(struct quota_root *root)
+{
+ return root->set->name;
+}
+
+const char *const *quota_root_get_resources(struct quota_root *root)
+{
+ /* if we haven't checked the quota_over_flag yet, do it now */
+ quota_over_flag_check_root(root);
+
+ return root->backend.v.get_resources(root);
+}
+
+bool quota_root_is_hidden(struct quota_root *root)
+{
+ return root->hidden;
+}
+
+enum quota_get_result
+quota_get_resource(struct quota_root *root, const char *mailbox_name,
+ const char *name, uint64_t *value_r, uint64_t *limit_r,
+ const char **error_r)
+{
+ const char *error;
+ uint64_t bytes_limit, count_limit;
+ bool ignored, kilobytes = FALSE;
+ enum quota_get_result ret;
+
+ *value_r = *limit_r = 0;
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0) {
+ name = QUOTA_NAME_STORAGE_BYTES;
+ kilobytes = TRUE;
+ }
+
+ /* Get the value first. This call may also update quota limits if
+ they're defined externally. */
+ ret = root->backend.v.get_resource(root, name, value_r, &error);
+ if (ret == QUOTA_GET_RESULT_UNLIMITED)
+ i_panic("Quota backend %s returned QUOTA_GET_RESULT_UNLIMITED "
+ "while getting resource %s from box %s",
+ root->backend.name, name, mailbox_name);
+ else if (ret != QUOTA_GET_RESULT_LIMITED) {
+ *error_r = t_strdup_printf(
+ "quota-%s: %s", root->set->backend->name, error);
+ return ret;
+ }
+
+ if (quota_root_get_rule_limits(root, mailbox_name,
+ &bytes_limit, &count_limit,
+ &ignored, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Failed to get quota root rule limits for mailbox %s: %s",
+ mailbox_name, error);
+ return QUOTA_GET_RESULT_INTERNAL_ERROR;
+ }
+
+ if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ *limit_r = bytes_limit;
+ else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0)
+ *limit_r = count_limit;
+ else
+ *limit_r = 0;
+
+ if (kilobytes) {
+ *value_r = (*value_r + 1023) / 1024;
+ *limit_r = (*limit_r + 1023) / 1024;
+ }
+ return *limit_r == 0 ? QUOTA_GET_RESULT_UNLIMITED : QUOTA_GET_RESULT_LIMITED;
+}
+
+int quota_set_resource(struct quota_root *root, const char *name,
+ uint64_t value, const char **client_error_r)
+{
+ struct dict_transaction_context *trans;
+ const char *key, *error;
+ const struct dict_op_settings *set;
+
+ if (root->set->limit_set == NULL) {
+ *client_error_r = MAIL_ERRSTR_NO_PERMISSION;
+ return -1;
+ }
+ if (strcasecmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0)
+ key = "storage";
+ else if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+ key = "bytes";
+ else if (strcasecmp(name, QUOTA_NAME_MESSAGES) == 0)
+ key = "messages";
+ else {
+ *client_error_r = t_strdup_printf(
+ "Unsupported resource name: %s", name);
+ return -1;
+ }
+
+ if (root->limit_set_dict == NULL) {
+ struct dict_settings set;
+
+ i_zero(&set);
+ set.base_dir = root->quota->user->set->base_dir;
+ set.event_parent = root->quota->user->event;
+ if (dict_init(root->set->limit_set, &set,
+ &root->limit_set_dict, &error) < 0) {
+ e_error(root->quota->event,
+ "dict_init() failed: %s", error);
+ *client_error_r = "Internal quota limit update error";
+ return -1;
+ }
+ }
+
+ set = mail_user_get_dict_op_settings(root->ns->user);
+ trans = dict_transaction_begin(root->limit_set_dict, set);
+ key = t_strdup_printf(QUOTA_LIMIT_SET_PATH"%s", key);
+ dict_set(trans, key, dec2str(value));
+ if (dict_transaction_commit(&trans, &error) < 0) {
+ e_error(root->quota->event,
+ "dict_transaction_commit() failed: %s", error);
+ *client_error_r = "Internal quota limit update error";
+ return -1;
+ }
+ return 0;
+}
+
+struct quota_transaction_context *quota_transaction_begin(struct mailbox *box)
+{
+ struct quota_transaction_context *ctx;
+ struct quota_root *const *rootp;
+ const struct quota_rule *rule;
+ const char *mailbox_name;
+
+ ctx = i_new(struct quota_transaction_context, 1);
+ ctx->quota = box->list->ns->owner != NULL ?
+ quota_get_mail_user_quota(box->list->ns->owner) :
+ quota_get_mail_user_quota(box->list->ns->user);
+ i_assert(ctx->quota != NULL);
+
+ ctx->box = box;
+ ctx->bytes_ceil = (uint64_t)-1;
+ ctx->bytes_ceil2 = (uint64_t)-1;
+ ctx->count_ceil = (uint64_t)-1;
+
+ mailbox_name = mailbox_get_vname(box);
+ (void)mail_namespace_find_unalias(box->storage->user->namespaces,
+ &mailbox_name);
+
+ ctx->auto_updating = TRUE;
+ array_foreach(&ctx->quota->roots, rootp) {
+ if (!quota_root_is_visible(*rootp, ctx->box))
+ continue;
+
+ rule = quota_root_rule_find((*rootp)->set, mailbox_name);
+ if (rule != NULL && rule->ignore) {
+ /* This mailbox isn't included in quota. This means
+ it's also not included in quota_warnings, so make
+ sure it's fully ignored. */
+ continue;
+ }
+
+ /* If there are reverse quota_warnings, we'll need to track
+ how many bytes were expunged even with auto_updating roots.
+ (An alternative could be to get the current quota usage
+ before and after the expunges, but that's more complicated
+ and probably isn't any better.) */
+ if (!(*rootp)->auto_updating ||
+ (*rootp)->set->have_reverse_warnings)
+ ctx->auto_updating = FALSE;
+ }
+
+ if (box->storage->user->dsyncing) {
+ /* ignore quota for dsync */
+ ctx->limits_set = TRUE;
+ }
+ return ctx;
+}
+
+int quota_transaction_set_limits(struct quota_transaction_context *ctx,
+ enum quota_get_result *error_result_r,
+ const char **error_r)
+{
+ struct quota_root *const *roots;
+ const char *mailbox_name, *error;
+ unsigned int i, count;
+ uint64_t bytes_limit, count_limit, current, limit, diff;
+ bool use_grace, ignored;
+ enum quota_get_result ret;
+
+ if (ctx->limits_set)
+ return 0;
+ ctx->limits_set = TRUE;
+ mailbox_name = mailbox_get_vname(ctx->box);
+ /* use quota_grace only for LDA/LMTP */
+ use_grace = (ctx->box->flags & MAILBOX_FLAG_POST_SESSION) != 0;
+ ctx->no_quota_updates = TRUE;
+
+ /* find the lowest quota limits from all roots and use them */
+ roots = array_get(&ctx->quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ /* make sure variables get initialized */
+ bytes_limit = count_limit = 0;
+ if (!quota_root_is_visible(roots[i], ctx->box))
+ continue;
+ else if (roots[i]->no_enforcing) {
+ ignored = FALSE;
+ } else if (quota_root_get_rule_limits(roots[i], mailbox_name,
+ &bytes_limit, &count_limit,
+ &ignored, &error) < 0) {
+ ctx->failed = TRUE;
+ *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR;
+ *error_r = t_strdup_printf(
+ "Failed to get quota root rule limits for %s: %s",
+ mailbox_name, error);
+ return -1;
+ }
+ if (!ignored)
+ ctx->no_quota_updates = FALSE;
+
+ if (bytes_limit > 0) {
+ ret = quota_get_resource(roots[i], mailbox_name,
+ QUOTA_NAME_STORAGE_BYTES,
+ &current, &limit, &error);
+ if (ret == QUOTA_GET_RESULT_LIMITED) {
+ if (limit <= current) {
+ /* over quota */
+ ctx->bytes_ceil = 0;
+ ctx->bytes_ceil2 = 0;
+ diff = current - limit;
+ if (ctx->bytes_over < diff)
+ ctx->bytes_over = diff;
+ } else {
+ diff = limit - current;
+ if (ctx->bytes_ceil2 > diff)
+ ctx->bytes_ceil2 = diff;
+ diff += !use_grace ? 0 :
+ roots[i]->set->last_mail_max_extra_bytes;
+ if (ctx->bytes_ceil > diff)
+ ctx->bytes_ceil = diff;
+ }
+ } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ ctx->failed = TRUE;
+ *error_result_r = ret;
+ *error_r = t_strdup_printf(
+ "Failed to get quota resource "
+ QUOTA_NAME_STORAGE_BYTES" for %s: %s",
+ mailbox_name, error);
+ return -1;
+ }
+ }
+
+ if (count_limit > 0) {
+ ret = quota_get_resource(roots[i], mailbox_name,
+ QUOTA_NAME_MESSAGES,
+ &current, &limit, &error);
+ if (ret == QUOTA_GET_RESULT_LIMITED) {
+ if (limit <= current) {
+ /* over quota */
+ ctx->count_ceil = 0;
+ diff = current - limit;
+ if (ctx->count_over < diff)
+ ctx->count_over = diff;
+ } else {
+ diff = limit - current;
+ if (ctx->count_ceil > diff)
+ ctx->count_ceil = diff;
+ }
+ } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ ctx->failed = TRUE;
+ *error_result_r = ret;
+ *error_r = t_strdup_printf(
+ "Failed to get quota resource "
+ QUOTA_NAME_MESSAGES" for %s: %s",
+ mailbox_name, error);
+ return -1;
+ }
+ }
+ }
+ return 0;
+}
+
+static void quota_warning_execute(struct quota_root *root, const char *cmd,
+ const char *last_arg, const char *reason)
+{
+ const char *socket_path, *const *args, *error, *scheme, *ptr;
+
+ struct program_client_settings set = {
+ .client_connect_timeout_msecs = 1000,
+ .debug = root->quota->user->mail_debug,
+ };
+ struct program_client *pc;
+
+ restrict_access_init(&set.restrict_set);
+
+ e_debug(root->quota->event, "Executing warning: %s (because %s)", cmd, reason);
+
+ args = t_strsplit_spaces(cmd, " ");
+ if (last_arg != NULL) {
+ unsigned int count = str_array_length(args);
+ const char **new_args = t_new(const char *, count + 2);
+
+ memcpy(new_args, args, sizeof(const char *) * count);
+ new_args[count] = last_arg;
+ args = new_args;
+ }
+ socket_path = args[0];
+
+ if ((ptr = strchr(socket_path, ':')) != NULL) {
+ scheme = t_strcut(socket_path, ':');
+ socket_path = ptr+1;
+ } else {
+ scheme = "unix";
+ }
+
+ if (*socket_path != '/' &&
+ strcmp(scheme, "unix") == 0)
+ socket_path =
+ t_strconcat(root->quota->user->set->base_dir,
+ "/", socket_path, NULL);
+
+ socket_path = t_strdup_printf("%s:%s", scheme, socket_path);
+
+ args++;
+
+ if (program_client_create(socket_path, args, &set, TRUE,
+ &pc, &error) < 0) {
+ e_error(root->quota->event,
+ "program_client_create(%s) failed: %s", socket_path,
+ error);
+ return;
+ }
+
+ (void)program_client_run(pc);
+
+ program_client_destroy(&pc);
+}
+
+static void quota_warnings_execute(struct quota_transaction_context *ctx,
+ struct quota_root *root)
+{
+ struct quota_warning_rule *warnings;
+ unsigned int i, count;
+ uint64_t bytes_current, bytes_before, bytes_limit;
+ uint64_t count_current, count_before, count_limit;
+ const char *reason, *error;
+
+ warnings = array_get_modifiable(&root->set->warning_rules, &count);
+ if (count == 0)
+ return;
+
+ if (quota_get_resource(root, "", QUOTA_NAME_STORAGE_BYTES,
+ &bytes_current, &bytes_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ e_error(root->quota->event,
+ "Failed to get quota resource "QUOTA_NAME_STORAGE_BYTES
+ ": %s", error);
+ return;
+ }
+ if (quota_get_resource(root, "", QUOTA_NAME_MESSAGES,
+ &count_current, &count_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ e_error(root->quota->event,
+ "Failed to get quota resource "QUOTA_NAME_MESSAGES
+ ": %s", error);
+ return;
+ }
+
+ if (ctx->bytes_used > 0 && bytes_current < (uint64_t)ctx->bytes_used)
+ bytes_before = 0;
+ else
+ bytes_before = (int64_t)bytes_current - ctx->bytes_used;
+
+ if (ctx->count_used > 0 && count_current < (uint64_t)ctx->count_used)
+ count_before = 0;
+ else
+ count_before = (int64_t)count_current - ctx->count_used;
+ for (i = 0; i < count; i++) {
+ if (quota_warning_match(&warnings[i],
+ bytes_before, bytes_current,
+ count_before, count_current,
+ &reason)) {
+ quota_warning_execute(root, warnings[i].command,
+ NULL, reason);
+ break;
+ }
+ }
+}
+
+int quota_transaction_commit(struct quota_transaction_context **_ctx)
+{
+ struct quota_transaction_context *ctx = *_ctx;
+ struct quota_rule *rule;
+ struct quota_root *const *roots;
+ unsigned int i, count;
+ const char *mailbox_name;
+ int ret = 0;
+
+ *_ctx = NULL;
+
+ if (ctx->failed)
+ ret = -1;
+ else if (ctx->bytes_used != 0 || ctx->count_used != 0 ||
+ ctx->recalculate != QUOTA_RECALCULATE_DONT) T_BEGIN {
+ ARRAY(struct quota_root *) warn_roots;
+
+ mailbox_name = mailbox_get_vname(ctx->box);
+ (void)mail_namespace_find_unalias(
+ ctx->box->storage->user->namespaces, &mailbox_name);
+
+ roots = array_get(&ctx->quota->roots, &count);
+ t_array_init(&warn_roots, count);
+ for (i = 0; i < count; i++) {
+ if (!quota_root_is_visible(roots[i], ctx->box))
+ continue;
+
+ rule = quota_root_rule_find(roots[i]->set,
+ mailbox_name);
+ if (rule != NULL && rule->ignore) {
+ /* mailbox not included in quota */
+ continue;
+ }
+
+ const char *error;
+ if (roots[i]->backend.v.update(roots[i], ctx, &error) < 0) {
+ e_error(ctx->quota->event,
+ "Failed to update quota for %s: %s",
+ mailbox_name, error);
+ ret = -1;
+ }
+ else if (!ctx->sync_transaction)
+ array_push_back(&warn_roots, &roots[i]);
+ }
+ /* execute quota warnings after all updates. this makes it
+ work correctly regardless of whether backend.get_resource()
+ returns updated values before backend.update() or not.
+ warnings aren't executed when dsync bring the user over,
+ because the user probably already got the warning on the
+ other replica. */
+ array_foreach(&warn_roots, roots)
+ quota_warnings_execute(ctx, *roots);
+ } T_END;
+
+ i_free(ctx);
+ return ret;
+}
+
+static bool quota_over_flag_init_root(struct quota_root *root,
+ const char **quota_over_script_r,
+ const char **quota_over_flag_r,
+ bool *status_r)
+{
+ const char *name, *flag_mask;
+
+ *quota_over_flag_r = NULL;
+ *status_r = FALSE;
+
+ name = t_strconcat(root->set->set_name, "_over_script", NULL);
+ *quota_over_script_r = mail_user_plugin_getenv(root->quota->user, name);
+ if (*quota_over_script_r == NULL) {
+ e_debug(root->quota->event, "quota_over_flag check: "
+ "%s unset - skipping", name);
+ return FALSE;
+ }
+
+ /* e.g.: quota_over_flag_value=TRUE or quota_over_flag_value=* */
+ name = t_strconcat(root->set->set_name, "_over_flag_value", NULL);
+ flag_mask = mail_user_plugin_getenv(root->quota->user, name);
+ if (flag_mask == NULL) {
+ e_debug(root->quota->event, "quota_over_flag check: "
+ "%s unset - skipping", name);
+ return FALSE;
+ }
+
+ /* compare quota_over_flag's value (that comes from userdb) to
+ quota_over_flag_value and save the result. */
+ name = t_strconcat(root->set->set_name, "_over_flag", NULL);
+ *quota_over_flag_r = mail_user_plugin_getenv(root->quota->user, name);
+ *status_r = *quota_over_flag_r != NULL &&
+ wildcard_match_icase(*quota_over_flag_r, flag_mask);
+ return TRUE;
+}
+
+static void quota_over_flag_check_root(struct quota_root *root)
+{
+ const char *quota_over_script, *quota_over_flag, *error;
+ const char *const *resources;
+ unsigned int i;
+ uint64_t value, limit;
+ bool cur_overquota = FALSE;
+ bool quota_over_status;
+ enum quota_get_result ret;
+
+ if (root->quota_over_flag_checked)
+ return;
+ if (root->quota->user->session_create_time +
+ QUOTA_OVER_FLAG_MAX_DELAY_SECS < ioloop_time) {
+ /* userdb's quota_over_flag lookup is too old. */
+ e_debug(root->quota->event, "quota_over_flag check: "
+ "Flag lookup time is too old - skipping");
+ return;
+ }
+ if (root->quota->user->session_restored) {
+ /* we don't know whether the quota_over_script was executed
+ before hibernation. just assume that it was, so we don't
+ unnecessarily call it too often. */
+ e_debug(root->quota->event, "quota_over_flag check: "
+ "Session was already hibernated - skipping");
+ return;
+ }
+ root->quota_over_flag_checked = TRUE;
+ if (!quota_over_flag_init_root(root, &quota_over_script,
+ &quota_over_flag, &quota_over_status))
+ return;
+
+ resources = quota_root_get_resources(root);
+ for (i = 0; resources[i] != NULL; i++) {
+ ret = quota_get_resource(root, "", resources[i], &value,
+ &limit, &error);
+ if (ret == QUOTA_GET_RESULT_INTERNAL_ERROR) {
+ /* can't reliably verify this */
+ e_error(root->quota->event, "Quota %s lookup failed -"
+ "can't verify quota_over_flag: %s",
+ resources[i], error);
+ return;
+ }
+ e_debug(root->quota->event, "quota_over_flag check: %s ret=%d"
+ "value=%"PRIu64" limit=%"PRIu64, resources[i], ret,
+ value, limit);
+ if (ret == QUOTA_GET_RESULT_LIMITED && value >= limit)
+ cur_overquota = TRUE;
+ }
+ e_debug(root->quota->event, "quota_over_flag=%d(%s) vs currently overquota=%d",
+ quota_over_status ? 1 : 0,
+ quota_over_flag == NULL ? "(null)" : quota_over_flag,
+ cur_overquota ? 1 : 0);
+ if (cur_overquota != quota_over_status) {
+ quota_warning_execute(root, quota_over_script, quota_over_flag,
+ "quota_over_flag mismatch");
+ }
+}
+
+void quota_over_flag_check_startup(struct quota *quota)
+{
+ struct quota_root *const *roots;
+ unsigned int i, count;
+ const char *name;
+
+ roots = array_get(&quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ name = t_strconcat(roots[i]->set->set_name, "_over_flag_lazy_check", NULL);
+ if (!mail_user_plugin_getenv_bool(roots[i]->quota->user, name))
+ quota_over_flag_check_root(roots[i]);
+ }
+}
+
+void quota_transaction_rollback(struct quota_transaction_context **_ctx)
+{
+ struct quota_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_free(ctx);
+}
+
+static int quota_get_mail_size(struct quota_transaction_context *ctx,
+ struct mail *mail, uoff_t *size_r)
+{
+ if (ctx->quota->set->vsizes)
+ return mail_get_virtual_size(mail, size_r);
+ else
+ return mail_get_physical_size(mail, size_r);
+}
+
+enum quota_alloc_result quota_try_alloc(struct quota_transaction_context *ctx,
+ struct mail *mail, const char **error_r)
+{
+ uoff_t size;
+ const char *error;
+ enum quota_get_result error_res;
+
+ if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) {
+ if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC)
+ return QUOTA_ALLOC_RESULT_BACKGROUND_CALC;
+ return QUOTA_ALLOC_RESULT_TEMPFAIL;
+ }
+
+ if (ctx->no_quota_updates)
+ return QUOTA_ALLOC_RESULT_OK;
+
+ if (quota_get_mail_size(ctx, mail, &size) < 0) {
+ enum mail_error err;
+ error = mailbox_get_last_internal_error(mail->box, &err);
+
+ if (err == MAIL_ERROR_EXPUNGED) {
+ /* mail being copied was already expunged. it'll fail,
+ so just return success for the quota allocated. */
+ return QUOTA_ALLOC_RESULT_OK;
+ }
+ *error_r = t_strdup_printf(
+ "Failed to get mail size (box=%s, uid=%u): %s",
+ mail->box->vname, mail->uid, error);
+ return QUOTA_ALLOC_RESULT_TEMPFAIL;
+ }
+
+ enum quota_alloc_result ret = quota_test_alloc(ctx, size, error_r);
+ if (ret != QUOTA_ALLOC_RESULT_OK)
+ return ret;
+ /* with quota_try_alloc() we want to keep track of how many bytes
+ we've been adding/removing, so disable auto_updating=TRUE
+ optimization. this of course doesn't work perfectly if
+ quota_alloc() or quota_free_bytes() was already used within the same
+ transaction, but that doesn't normally happen. */
+ ctx->auto_updating = FALSE;
+ quota_alloc(ctx, mail);
+ return QUOTA_ALLOC_RESULT_OK;
+}
+
+enum quota_alloc_result quota_test_alloc(struct quota_transaction_context *ctx,
+ uoff_t size, const char **error_r)
+{
+ if (ctx->failed) {
+ *error_r = "Quota transaction has failed earlier";
+ return QUOTA_ALLOC_RESULT_TEMPFAIL;
+ }
+
+ enum quota_get_result error_res;
+ if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) {
+ if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC)
+ return QUOTA_ALLOC_RESULT_BACKGROUND_CALC;
+ return QUOTA_ALLOC_RESULT_TEMPFAIL;
+ }
+
+ uoff_t max_size = ctx->quota->set->max_mail_size;
+ if (max_size > 0 && size > max_size) {
+ *error_r = t_strdup_printf(
+ "Requested allocation size %"PRIuUOFF_T" exceeds max "
+ "mail size %"PRIuUOFF_T, size, max_size);
+ return QUOTA_ALLOC_RESULT_OVER_MAXSIZE;
+ }
+
+ if (ctx->no_quota_updates)
+ return QUOTA_ALLOC_RESULT_OK;
+ /* this is a virtual function mainly for trash plugin and similar,
+ which may automatically delete mails to stay under quota. */
+ return ctx->quota->set->test_alloc(ctx, size, error_r);
+}
+
+static enum quota_alloc_result quota_default_test_alloc(
+ struct quota_transaction_context *ctx, uoff_t size,
+ const char **error_r)
+{
+ struct quota_root *const *roots;
+ unsigned int i, count;
+ bool ignore;
+ int ret;
+
+ if (!quota_transaction_is_over(ctx, size))
+ return QUOTA_ALLOC_RESULT_OK;
+
+ /* limit reached. */
+ roots = array_get(&ctx->quota->roots, &count);
+ for (i = 0; i < count; i++) {
+ uint64_t bytes_limit, count_limit;
+
+ if (!quota_root_is_visible(roots[i], ctx->box) ||
+ roots[i]->no_enforcing)
+ continue;
+
+ const char *error;
+ ret = quota_root_get_rule_limits(roots[i],
+ mailbox_get_vname(ctx->box),
+ &bytes_limit, &count_limit,
+ &ignore, &error);
+ if (ret < 0) {
+ *error_r = t_strdup_printf(
+ "Failed to get quota root rule limits: %s",
+ error);
+ return QUOTA_ALLOC_RESULT_TEMPFAIL;
+ }
+
+ /* if size is bigger than any limit, then
+ it is bigger than the lowest limit */
+ if (bytes_limit > 0 && size > bytes_limit) {
+ *error_r = t_strdup_printf(
+ "Allocating %"PRIuUOFF_T" bytes would exceed quota limit",
+ size);
+ return QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT;
+ }
+ }
+ *error_r = t_strdup_printf(
+ "Allocating %"PRIuUOFF_T" bytes would exceed quota", size);
+ return QUOTA_ALLOC_RESULT_OVER_QUOTA;
+}
+
+void quota_alloc(struct quota_transaction_context *ctx, struct mail *mail)
+{
+ uoff_t size;
+
+ if (!ctx->auto_updating) {
+ if (quota_get_mail_size(ctx, mail, &size) == 0)
+ ctx->bytes_used += size;
+ }
+
+ ctx->bytes_ceil = ctx->bytes_ceil2;
+ ctx->count_used++;
+}
+
+void quota_free_bytes(struct quota_transaction_context *ctx,
+ uoff_t physical_size)
+{
+ i_assert(physical_size <= INT64_MAX);
+ ctx->bytes_used -= (int64_t)physical_size;
+ ctx->count_used--;
+}
+
+void quota_recalculate(struct quota_transaction_context *ctx,
+ enum quota_recalculate recalculate)
+{
+ ctx->recalculate = recalculate;
+}
+
+static void hidden_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ _root->hidden = TRUE;
+}
+
+static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ _root->disable_unlimited_tracking = TRUE;
+}
+
+static void noenforcing_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED)
+{
+ _root->no_enforcing = TRUE;
+}
+
+static void ns_param_handler(struct quota_root *_root, const char *param_value)
+{
+ _root->ns_prefix = p_strdup(_root->pool, param_value);
+}
+
+int quota_parse_parameters(struct quota_root *root, const char **args, const char **error_r,
+ const struct quota_param_parser *valid_params, bool fail_on_unknown)
+{
+ const char *tmp_param_name, *tmp_param_val;
+ size_t tmp_param_len;
+
+ while (*args != NULL && (*args)[0] != '\0') {
+ for (; valid_params->param_name != NULL; ++valid_params) {
+ tmp_param_name = valid_params->param_name;
+ tmp_param_len = strlen(valid_params->param_name);
+ i_assert(*args != NULL);
+ if (strncmp(*args, tmp_param_name, tmp_param_len) == 0) {
+ tmp_param_val = NULL;
+ *args += tmp_param_len;
+ if (tmp_param_name[tmp_param_len - 1] == '=') {
+ const char *next_colon = strchr(*args, ':');
+ tmp_param_val = (next_colon == NULL)?
+ t_strdup(*args):
+ t_strdup_until(*args, next_colon);
+ *args = (next_colon == NULL) ? NULL : next_colon + 1;
+ }
+ else if ((*args)[0] == '\0' ||
+ (*args)[0] == ':') {
+ *args = ((*args)[0] == ':') ? *args + 1 : NULL;
+ /* in case parameter is a boolean second parameter
+ * string parameter value will be ignored by param_handler
+ * we just need some non-NULL value
+ * to indicate that argument is to be processed */
+ tmp_param_val = "";
+ }
+ if (tmp_param_val != NULL) {
+ valid_params->param_handler(root, tmp_param_val);
+ break;
+ }
+ }
+ }
+ if (valid_params->param_name == NULL) {
+ if (fail_on_unknown) {
+ *error_r = t_strdup_printf(
+ "Unknown parameter for backend %s: %s",
+ root->backend.name, *args);
+ return -1;
+ }
+ else {
+ break;
+ }
+ }
+ }
+ return 0;
+}
diff --git a/src/plugins/quota/quota.h b/src/plugins/quota/quota.h
new file mode 100644
index 0000000..8de2d8e
--- /dev/null
+++ b/src/plugins/quota/quota.h
@@ -0,0 +1,147 @@
+#ifndef QUOTA_H
+#define QUOTA_H
+
+struct mail;
+struct mailbox;
+struct mail_user;
+
+/* Message storage size kilobytes. */
+#define QUOTA_NAME_STORAGE_KILOBYTES "STORAGE"
+/* Message storage size bytes. This is used only internally. */
+#define QUOTA_NAME_STORAGE_BYTES "STORAGE_BYTES"
+/* Number of messages. */
+#define QUOTA_NAME_MESSAGES "MESSAGE"
+
+struct quota;
+struct quota_settings;
+struct quota_root_settings;
+struct quota_root;
+struct quota_root_iter;
+struct quota_transaction_context;
+
+struct quota_param_parser {
+ char *param_name;
+ void (* param_handler)(struct quota_root *_root, const char *param_value);
+};
+
+extern struct quota_param_parser quota_param_hidden;
+extern struct quota_param_parser quota_param_ignoreunlimited;
+extern struct quota_param_parser quota_param_noenforcing;
+extern struct quota_param_parser quota_param_ns;
+
+enum quota_recalculate {
+ QUOTA_RECALCULATE_DONT = 0,
+ /* We may want to recalculate quota because we weren't able to call
+ quota_free*() correctly for all mails. Quota needs to be
+ recalculated unless the backend does the quota tracking
+ internally. */
+ QUOTA_RECALCULATE_MISSING_FREES,
+ /* doveadm quota recalc called - make sure the quota is correct */
+ QUOTA_RECALCULATE_FORCED
+};
+
+enum quota_alloc_result {
+ QUOTA_ALLOC_RESULT_OK,
+ QUOTA_ALLOC_RESULT_TEMPFAIL,
+ QUOTA_ALLOC_RESULT_OVER_MAXSIZE,
+ QUOTA_ALLOC_RESULT_OVER_QUOTA,
+ /* Mail size is larger than even the maximum allowed quota. */
+ QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT,
+ /* Blocked by ongoing background quota calculation. */
+ QUOTA_ALLOC_RESULT_BACKGROUND_CALC,
+};
+
+/* Anything <= QUOTA_GET_RESULT_INTERNAL_ERROR is an error. */
+enum quota_get_result {
+ /* Ongoing background quota calculation */
+ QUOTA_GET_RESULT_BACKGROUND_CALC,
+ /* Quota resource name doesn't exist */
+ QUOTA_GET_RESULT_UNKNOWN_RESOURCE,
+ /* Internal error */
+ QUOTA_GET_RESULT_INTERNAL_ERROR,
+
+ /* Quota limit exists and was returned successfully */
+ QUOTA_GET_RESULT_LIMITED,
+ /* Quota is unlimited, but its value was returned */
+ QUOTA_GET_RESULT_UNLIMITED,
+};
+
+const char *quota_alloc_result_errstr(enum quota_alloc_result res,
+ struct quota_transaction_context *qt);
+
+int quota_user_read_settings(struct mail_user *user,
+ struct quota_settings **set_r,
+ const char **error_r);
+void quota_settings_deinit(struct quota_settings **quota_set);
+
+/* Add a new rule too the quota root. Returns 0 if ok, -1 if rule is invalid. */
+int quota_root_add_rule(struct quota_root_settings *root_set,
+ const char *rule_def, const char **error_r);
+/* Add a new warning rule for the quota root. Returns 0 if ok, -1 if rule is
+ invalid. */
+int quota_root_add_warning_rule(struct quota_root_settings *root_set,
+ const char *rule_def, const char **error_r);
+
+/* Initialize quota for the given user. Returns 0 and quota_r on success,
+ -1 and error_r on failure. */
+int quota_init(struct quota_settings *quota_set, struct mail_user *user,
+ struct quota **quota_r, const char **error_r);
+void quota_deinit(struct quota **quota);
+
+/* List all visible quota roots. They don't need to be freed. */
+struct quota_root_iter *quota_root_iter_init_user(struct mail_user *user);
+struct quota_root_iter *quota_root_iter_init(struct mailbox *box);
+struct quota_root *quota_root_iter_next(struct quota_root_iter *iter);
+void quota_root_iter_deinit(struct quota_root_iter **iter);
+
+/* Return quota root or NULL. */
+struct quota_root *quota_root_lookup(struct mail_user *user, const char *name);
+
+/* Returns name of the quota root. */
+const char *quota_root_get_name(struct quota_root *root);
+/* Return a list of all resources set for the quota root. */
+const char *const *quota_root_get_resources(struct quota_root *root);
+/* Returns TRUE if quota root is marked as hidden (so it shouldn't be visible
+ to users via IMAP GETQUOTAROOT command). */
+bool quota_root_is_hidden(struct quota_root *root);
+
+/* Returns 1 if values were successfully returned, 0 if resource name doesn't
+ exist or isn't enabled, -1 if error. */
+enum quota_get_result
+quota_get_resource(struct quota_root *root, const char *mailbox_name,
+ const char *name, uint64_t *value_r, uint64_t *limit_r,
+ const char **error_r);
+/* Returns 0 if OK, -1 if error (eg. permission denied, invalid name). */
+int quota_set_resource(struct quota_root *root, const char *name,
+ uint64_t value, const char **client_error_r);
+
+/* Start a new quota transaction. */
+struct quota_transaction_context *quota_transaction_begin(struct mailbox *box);
+/* Commit quota transaction. Returns 0 if ok, -1 if failed. */
+int quota_transaction_commit(struct quota_transaction_context **ctx);
+/* Rollback quota transaction changes. */
+void quota_transaction_rollback(struct quota_transaction_context **ctx);
+
+/* Allocate from quota if there's space. error_r is set when result is not
+ * QUOTA_ALLOC_RESULT_OK. */
+enum quota_alloc_result quota_try_alloc(struct quota_transaction_context *ctx,
+ struct mail *mail, const char **error_r);
+/* Like quota_try_alloc(), but don't actually allocate anything. */
+enum quota_alloc_result quota_test_alloc(struct quota_transaction_context *ctx,
+ uoff_t size, const char **error_r);
+/* Update quota by allocating/freeing space used by mail. */
+void quota_alloc(struct quota_transaction_context *ctx, struct mail *mail);
+void quota_free_bytes(struct quota_transaction_context *ctx,
+ uoff_t physical_size);
+/* Mark the quota to be recalculated */
+void quota_recalculate(struct quota_transaction_context *ctx,
+ enum quota_recalculate recalculate);
+
+/* Execute quota_over_scripts if needed. */
+void quota_over_flag_check_startup(struct quota *quota);
+
+/* Common quota parameters parsing loop */
+int quota_parse_parameters(struct quota_root *root, const char **args, const char **error_r,
+ const struct quota_param_parser *valid_params, bool fail_on_unknown);
+
+#endif
diff --git a/src/plugins/quota/rquota-pragmas.h b/src/plugins/quota/rquota-pragmas.h
new file mode 100644
index 0000000..3cf2a1f
--- /dev/null
+++ b/src/plugins/quota/rquota-pragmas.h
@@ -0,0 +1,4 @@
+#include "config.h"
+#ifdef HAVE_STRICT_BOOL
+# pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
diff --git a/src/plugins/quota/rquota.x b/src/plugins/quota/rquota.x
new file mode 100644
index 0000000..3cd5c10
--- /dev/null
+++ b/src/plugins/quota/rquota.x
@@ -0,0 +1,139 @@
+/* @(#)rquota.x 2.1 88/08/01 4.0 RPCSRC */
+/* @(#)rquota.x 1.2 87/09/20 Copyr 1987 Sun Micro */
+
+/*
+ * Remote quota protocol
+ * Requires unix authentication
+ */
+
+const RQ_PATHLEN = 1024;
+
+struct sq_dqblk {
+ unsigned int rq_bhardlimit; /* absolute limit on disk blks alloc */
+ unsigned int rq_bsoftlimit; /* preferred limit on disk blks */
+ unsigned int rq_curblocks; /* current block count */
+ unsigned int rq_fhardlimit; /* absolute limit on allocated files */
+ unsigned int rq_fsoftlimit; /* preferred file limit */
+ unsigned int rq_curfiles; /* current # allocated files */
+ unsigned int rq_btimeleft; /* time left for excessive disk use */
+ unsigned int rq_ftimeleft; /* time left for excessive files */
+};
+
+struct getquota_args {
+ string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
+ int gqa_uid; /* Inquire about quota for uid */
+};
+
+struct setquota_args {
+ int sqa_qcmd;
+ string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
+ int sqa_id; /* Set quota for uid */
+ sq_dqblk sqa_dqblk;
+};
+
+struct ext_getquota_args {
+ string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
+ int gqa_type; /* Type of quota info is needed about */
+ int gqa_id; /* Inquire about quota for id */
+};
+
+struct ext_setquota_args {
+ int sqa_qcmd;
+ string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
+ int sqa_id; /* Set quota for id */
+ int sqa_type; /* Type of quota to set */
+ sq_dqblk sqa_dqblk;
+};
+
+/*
+ * remote quota structure
+ */
+struct rquota {
+ int rq_bsize; /* block size for block counts */
+ bool rq_active; /* indicates whether quota is active */
+ unsigned int rq_bhardlimit; /* absolute limit on disk blks alloc */
+ unsigned int rq_bsoftlimit; /* preferred limit on disk blks */
+ unsigned int rq_curblocks; /* current block count */
+ unsigned int rq_fhardlimit; /* absolute limit on allocated files */
+ unsigned int rq_fsoftlimit; /* preferred file limit */
+ unsigned int rq_curfiles; /* current # allocated files */
+ unsigned int rq_btimeleft; /* time left for excessive disk use */
+ unsigned int rq_ftimeleft; /* time left for excessive files */
+};
+
+enum qr_status {
+ Q_OK = 1, /* quota returned */
+ Q_NOQUOTA = 2, /* noquota for uid */
+ Q_EPERM = 3 /* no permission to access quota */
+};
+
+union getquota_rslt switch (qr_status status) {
+case Q_OK:
+ rquota gqr_rquota; /* valid if status == Q_OK */
+case Q_NOQUOTA:
+ void;
+case Q_EPERM:
+ void;
+};
+
+union setquota_rslt switch (qr_status status) {
+case Q_OK:
+ rquota sqr_rquota; /* valid if status == Q_OK */
+case Q_NOQUOTA:
+ void;
+case Q_EPERM:
+ void;
+};
+
+program RQUOTAPROG {
+ version RQUOTAVERS {
+ /*
+ * Get all quotas
+ */
+ getquota_rslt
+ RQUOTAPROC_GETQUOTA(getquota_args) = 1;
+
+ /*
+ * Get active quotas only
+ */
+ getquota_rslt
+ RQUOTAPROC_GETACTIVEQUOTA(getquota_args) = 2;
+
+ /*
+ * Set all quotas
+ */
+ setquota_rslt
+ RQUOTAPROC_SETQUOTA(setquota_args) = 3;
+
+ /*
+ * Get active quotas only
+ */
+ setquota_rslt
+ RQUOTAPROC_SETACTIVEQUOTA(setquota_args) = 4;
+ } = 1;
+ version EXT_RQUOTAVERS {
+ /*
+ * Get all quotas
+ */
+ getquota_rslt
+ RQUOTAPROC_GETQUOTA(ext_getquota_args) = 1;
+
+ /*
+ * Get active quotas only
+ */
+ getquota_rslt
+ RQUOTAPROC_GETACTIVEQUOTA(ext_getquota_args) = 2;
+
+ /*
+ * Set all quotas
+ */
+ setquota_rslt
+ RQUOTAPROC_SETQUOTA(ext_setquota_args) = 3;
+
+ /*
+ * Set active quotas only
+ */
+ setquota_rslt
+ RQUOTAPROC_SETACTIVEQUOTA(ext_setquota_args) = 4;
+ } = 2;
+} = 100011;
diff --git a/src/plugins/quota/test-quota-util.c b/src/plugins/quota/test-quota-util.c
new file mode 100644
index 0000000..682cffe
--- /dev/null
+++ b/src/plugins/quota/test-quota-util.c
@@ -0,0 +1,96 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "quota-private.h"
+#include "test-common.h"
+
+struct test {
+ uint64_t limit, initial_size;
+ int64_t transaction_diff;
+ uint64_t new_size;
+ bool is_over;
+};
+
+static void test_quota_transaction_is_over(void)
+{
+#define MAXU64 (uint64_t)-1
+#define MAXS64 9223372036854775807LL
+#define MINS64 (-MAXS64 - 1LL)
+ static const struct test tests[] = {
+ /* first test only with new_size=1. these are used for both
+ count and bytes tests: */
+
+ /* limit, init, diff, new */
+ { 1, 0, 0, 1, FALSE },
+ { MAXU64, MAXU64, 0, 1, TRUE },
+ { MAXU64, MAXU64-1, 0, 1, FALSE },
+ { MAXU64, MAXU64-1, 1, 1, TRUE },
+ { MAXU64-1, MAXU64-1, 0, 1, TRUE },
+ { MAXU64-1, MAXU64-1, -1, 1, FALSE },
+ { MAXU64-2, MAXU64-1, -1, 1, TRUE },
+ { MAXU64-2, MAXU64-1, -2, 1, FALSE },
+
+ /* these are for bytes tests: */
+
+ /* limit, init, diff, new */
+ { MAXU64, MAXU64, 0, 0, FALSE },
+ { MAXU64, MAXU64-1, 1, 0, FALSE },
+ { MAXU64-1, MAXU64, 1, 0, TRUE },
+ { MAXU64-1, MAXU64, 0, 0, TRUE },
+ { MAXU64-1, MAXU64, -1, 0, FALSE },
+ { MAXU64, MAXU64, 0, 1, TRUE },
+ { MAXU64, 0, 0, MAXU64, FALSE },
+ { MAXU64, 1, 0, MAXU64, TRUE },
+ { MAXU64, 0, 1, MAXU64, TRUE },
+ { MAXU64-1, 0, 0, MAXU64, TRUE },
+ { MAXU64-1, 0, 0, MAXU64-1, FALSE },
+ { MAXU64-1, 1, 0, MAXU64-1, TRUE },
+ { MAXU64-1, 1, -1, MAXU64-1, FALSE },
+ { MAXU64, MAXU64, 0, MAXU64, TRUE },
+ };
+ struct quota_transaction_context ctx;
+ unsigned int i;
+
+ test_begin("quota transaction is over (count)");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ if (tests[i].new_size != 1)
+ continue;
+
+ i_zero(&ctx);
+ ctx.count_used = tests[i].transaction_diff;
+ if (tests[i].initial_size > tests[i].limit)
+ ctx.count_over = tests[i].initial_size - tests[i].limit;
+ else {
+ ctx.count_ceil = tests[i].limit - tests[i].initial_size;
+ i_assert(ctx.count_used < 0 ||
+ (uint64_t)ctx.count_used <= ctx.count_ceil); /* test is broken otherwise */
+ }
+ test_assert_idx(quota_transaction_is_over(&ctx, 0) == tests[i].is_over, i);
+ }
+ test_end();
+
+ test_begin("quota transaction is over (bytes)");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ i_zero(&ctx);
+ ctx.count_ceil = 1;
+ ctx.bytes_used = tests[i].transaction_diff;
+ if (tests[i].initial_size > tests[i].limit)
+ ctx.bytes_over = tests[i].initial_size - tests[i].limit;
+ else {
+ ctx.bytes_ceil = tests[i].limit - tests[i].initial_size;
+ i_assert(ctx.bytes_used < 0 ||
+ (uint64_t)ctx.bytes_used <= ctx.bytes_ceil); /* test is broken otherwise */
+ }
+ test_assert_idx(quota_transaction_is_over(&ctx, tests[i].new_size) == tests[i].is_over, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_quota_transaction_is_over,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/plugins/replication/Makefile.am b/src/plugins/replication/Makefile.am
new file mode 100644
index 0000000..461935c
--- /dev/null
+++ b/src/plugins/replication/Makefile.am
@@ -0,0 +1,25 @@
+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/replication \
+ -I$(top_srcdir)/src/plugins/notify
+
+NOPLUGIN_LDFLAGS =
+lib20_replication_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_replication_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib20_replication_plugin_la_LIBADD = \
+ ../notify/lib15_notify_plugin.la
+endif
+
+lib20_replication_plugin_la_SOURCES = \
+ replication-plugin.c
+
+noinst_HEADERS = \
+ replication-plugin.h
diff --git a/src/plugins/replication/Makefile.in b/src/plugins/replication/Makefile.in
new file mode 100644
index 0000000..71cb9de
--- /dev/null
+++ b/src/plugins/replication/Makefile.in
@@ -0,0 +1,827 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/replication
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib20_replication_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../notify/lib15_notify_plugin.la
+am_lib20_replication_plugin_la_OBJECTS = replication-plugin.lo
+lib20_replication_plugin_la_OBJECTS = \
+ $(am_lib20_replication_plugin_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 =
+lib20_replication_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_replication_plugin_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)/replication-plugin.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 = $(lib20_replication_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_replication_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/replication \
+ -I$(top_srcdir)/src/plugins/notify
+
+lib20_replication_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_replication_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib20_replication_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../notify/lib15_notify_plugin.la
+
+lib20_replication_plugin_la_SOURCES = \
+ replication-plugin.c
+
+noinst_HEADERS = \
+ replication-plugin.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/plugins/replication/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/replication/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-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}; \
+ }
+
+lib20_replication_plugin.la: $(lib20_replication_plugin_la_OBJECTS) $(lib20_replication_plugin_la_DEPENDENCIES) $(EXTRA_lib20_replication_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_replication_plugin_la_LINK) -rpath $(moduledir) $(lib20_replication_plugin_la_OBJECTS) $(lib20_replication_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replication-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/replication-plugin.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-moduleLTLIBRARIES
+
+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)/replication-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/replication/replication-plugin.c b/src/plugins/replication/replication-plugin.c
new file mode 100644
index 0000000..9b4bb08
--- /dev/null
+++ b/src/plugins/replication/replication-plugin.c
@@ -0,0 +1,404 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ioloop.h"
+#include "net.h"
+#include "write-full.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "notify-plugin.h"
+#include "replication-common.h"
+#include "replication-plugin.h"
+
+
+#define REPLICATION_SOCKET_NAME "replication-notify"
+#define REPLICATION_FIFO_NAME "replication-notify-fifo"
+#define REPLICATION_NOTIFY_DELAY_MSECS 500
+#define REPLICATION_SYNC_TIMEOUT_SECS 10
+
+#define REPLICATION_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, replication_user_module)
+
+struct replication_user {
+ union mail_user_module_context module_ctx;
+
+ const char *socket_path;
+
+ struct timeout *to;
+ enum replication_priority priority;
+ unsigned int sync_secs;
+};
+
+struct replication_mail_txn_context {
+ struct mail_namespace *ns;
+ bool new_messages;
+ bool sync_trans;
+ char *reason;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(replication_user_module,
+ &mail_user_module_register);
+static int fifo_fd;
+static bool fifo_failed;
+static char *fifo_path;
+
+static int
+replication_fifo_notify(struct mail_user *user,
+ enum replication_priority priority)
+{
+ string_t *str;
+ ssize_t ret;
+
+ if (fifo_failed)
+ return -1;
+ if (fifo_fd == -1) {
+ fifo_fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
+ if (fifo_fd == -1) {
+ i_error("open(%s) failed: %m", fifo_path);
+ fifo_failed = TRUE;
+ return -1;
+ }
+ }
+ /* <username> \t <priority> */
+ str = t_str_new(256);
+ str_append_tabescaped(str, user->username);
+ str_append_c(str, '\t');
+ switch (priority) {
+ case REPLICATION_PRIORITY_NONE:
+ case REPLICATION_PRIORITY_SYNC:
+ i_unreached();
+ case REPLICATION_PRIORITY_LOW:
+ str_append(str, "low");
+ break;
+ case REPLICATION_PRIORITY_HIGH:
+ str_append(str, "high");
+ break;
+ }
+ str_append_c(str, '\n');
+ ret = write(fifo_fd, str_data(str), str_len(str));
+ i_assert(ret != 0);
+ if (ret != (ssize_t)str_len(str)) {
+ if (ret > 0)
+ i_error("write(%s) wrote partial data", fifo_path);
+ else if (errno == EAGAIN) {
+ /* busy, try again later */
+ return 0;
+ } else if (errno != EPIPE) {
+ i_error("write(%s) failed: %m", fifo_path);
+ } else {
+ /* server was probably restarted, don't bother logging
+ this. */
+ }
+ if (close(fifo_fd) < 0)
+ i_error("close(%s) failed: %m", fifo_path);
+ fifo_fd = -1;
+ return -1;
+ }
+ return 1;
+}
+
+static void replication_notify_now(struct mail_user *user)
+{
+ struct replication_user *ruser = REPLICATION_USER_CONTEXT(user);
+ int ret;
+
+ i_assert(ruser != NULL);
+ i_assert(ruser->priority != REPLICATION_PRIORITY_NONE);
+ i_assert(ruser->priority != REPLICATION_PRIORITY_SYNC);
+
+ if ((ret = replication_fifo_notify(user, ruser->priority)) < 0 &&
+ !fifo_failed) {
+ /* retry once, in case replication server was restarted */
+ ret = replication_fifo_notify(user, ruser->priority);
+ }
+ if (ret != 0) {
+ timeout_remove(&ruser->to);
+ ruser->priority = REPLICATION_PRIORITY_NONE;
+ }
+}
+
+static int replication_notify_sync(struct mail_user *user)
+{
+ struct replication_user *ruser = REPLICATION_USER_CONTEXT(user);
+ string_t *str;
+ char buf[1024];
+ int fd;
+ ssize_t ret;
+ bool success = FALSE;
+
+ i_assert(ruser != NULL);
+
+ fd = net_connect_unix(ruser->socket_path);
+ if (fd == -1) {
+ i_error("net_connect_unix(%s) failed: %m", ruser->socket_path);
+ return -1;
+ }
+ net_set_nonblock(fd, FALSE);
+
+ /* <username> \t "sync" */
+ str = t_str_new(256);
+ str_append_tabescaped(str, user->username);
+ str_append(str, "\tsync\n");
+ alarm(ruser->sync_secs);
+ if (write_full(fd, str_data(str), str_len(str)) < 0) {
+ i_error("write(%s) failed: %m", ruser->socket_path);
+ } else {
+ /* + | - */
+ ret = read(fd, buf, sizeof(buf));
+ if (ret < 0) {
+ if (errno != EINTR) {
+ i_error("read(%s) failed: %m",
+ ruser->socket_path);
+ } else {
+ i_warning("replication(%s): Sync failure: "
+ "Timeout in %u secs",
+ user->username, ruser->sync_secs);
+ }
+ } else if (ret == 0) {
+ i_error("read(%s) failed: EOF", ruser->socket_path);
+ } else if (buf[0] == '+') {
+ /* success */
+ success = TRUE;
+ } else if (buf[0] == '-') {
+ /* failure */
+ if (buf[ret-1] == '\n') ret--;
+ i_warning("replication(%s): Sync failure: %s",
+ user->username, t_strndup(buf+1, ret-1));
+ i_warning("replication(%s): "
+ "Remote sent invalid input: %s",
+ user->username, t_strndup(buf, ret));
+ }
+ }
+ alarm(0);
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", ruser->socket_path);
+ return success ? 0 : -1;
+}
+
+static void replication_notify(struct mail_namespace *ns,
+ enum replication_priority priority,
+ const char *event)
+{
+ struct replication_user *ruser;
+
+ ruser = REPLICATION_USER_CONTEXT(ns->user);
+ if (ruser == NULL)
+ return;
+
+ e_debug(ns->user->event,
+ "replication: Replication requested by '%s', priority=%d",
+ event, priority);
+
+ if (priority == REPLICATION_PRIORITY_SYNC) {
+ if (replication_notify_sync(ns->user) == 0) {
+ timeout_remove(&ruser->to);
+ ruser->priority = REPLICATION_PRIORITY_NONE;
+ return;
+ }
+ /* sync replication failed, try as "high" via fifo */
+ priority = REPLICATION_PRIORITY_HIGH;
+ }
+
+ if (ruser->priority < priority)
+ ruser->priority = priority;
+ if (ruser->to == NULL) {
+ ruser->to = timeout_add_short(REPLICATION_NOTIFY_DELAY_MSECS,
+ replication_notify_now, ns->user);
+ }
+}
+
+static void *
+replication_mail_transaction_begin(struct mailbox_transaction_context *t)
+{
+ struct replication_mail_txn_context *ctx;
+
+ ctx = i_new(struct replication_mail_txn_context, 1);
+ ctx->ns = mailbox_get_namespace(t->box);
+ ctx->reason = i_strdup(t->reason);
+ if ((t->flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0) {
+ /* Transaction is from dsync. Don't trigger replication back. */
+ ctx->sync_trans = TRUE;
+ }
+ return ctx;
+}
+
+static void replication_mail_save(void *txn, struct mail *mail ATTR_UNUSED)
+{
+ struct replication_mail_txn_context *ctx =
+ (struct replication_mail_txn_context *)txn;
+
+ ctx->new_messages = TRUE;
+}
+
+static void replication_mail_copy(void *txn, struct mail *src,
+ struct mail *dst)
+{
+ struct replication_mail_txn_context *ctx =
+ (struct replication_mail_txn_context *)txn;
+
+ if (src->box->storage != dst->box->storage) {
+ /* copy between storages, e.g. new mail delivery */
+ ctx->new_messages = TRUE;
+ } else {
+ /* copy within storage, which isn't as high priority since the
+ mail already exists. and especially copies to Trash or to
+ lazy-expunge namespace is pretty low priority. */
+ }
+}
+
+static bool
+replication_want_sync_changes(const struct mail_transaction_commit_changes *changes)
+{
+ /* Replication needs to be triggered on all the user-visible changes,
+ but not e.g. due to writes to cache file. */
+ return (changes->changes_mask &
+ ENUM_NEGATE(MAIL_INDEX_TRANSACTION_CHANGE_OTHERS)) != 0;
+}
+
+static void
+replication_mail_transaction_commit(void *txn,
+ struct mail_transaction_commit_changes *changes)
+{
+ struct replication_mail_txn_context *ctx =
+ (struct replication_mail_txn_context *)txn;
+ struct replication_user *ruser =
+ REPLICATION_USER_CONTEXT(ctx->ns->user);
+ enum replication_priority priority;
+
+ if (ruser != NULL && !ctx->sync_trans &&
+ (ctx->new_messages || replication_want_sync_changes(changes))) {
+ priority = !ctx->new_messages ? REPLICATION_PRIORITY_LOW :
+ ruser->sync_secs == 0 ? REPLICATION_PRIORITY_HIGH :
+ REPLICATION_PRIORITY_SYNC;
+ replication_notify(ctx->ns, priority, ctx->reason);
+ }
+ i_free(ctx->reason);
+ i_free(ctx);
+}
+
+static void replication_mailbox_create(struct mailbox *box)
+{
+ replication_notify(mailbox_get_namespace(box),
+ REPLICATION_PRIORITY_LOW, "mailbox create");
+}
+
+static void
+replication_mailbox_delete_commit(void *txn ATTR_UNUSED,
+ struct mailbox *box)
+{
+ replication_notify(mailbox_get_namespace(box),
+ REPLICATION_PRIORITY_LOW, "mailbox delete");
+}
+
+static void
+replication_mailbox_rename(struct mailbox *src ATTR_UNUSED,
+ struct mailbox *dest)
+{
+ replication_notify(mailbox_get_namespace(dest),
+ REPLICATION_PRIORITY_LOW, "mailbox rename");
+}
+
+static void replication_mailbox_set_subscribed(struct mailbox *box,
+ bool subscribed ATTR_UNUSED)
+{
+ replication_notify(mailbox_get_namespace(box),
+ REPLICATION_PRIORITY_LOW, "mailbox subscribe");
+}
+
+static void replication_user_deinit(struct mail_user *user)
+{
+ struct replication_user *ruser = REPLICATION_USER_CONTEXT(user);
+
+ i_assert(ruser != NULL);
+
+ if (ruser->to != NULL) {
+ replication_notify_now(user);
+ if (ruser->to != NULL) {
+ i_warning("%s: Couldn't send final notification "
+ "due to fifo being busy", fifo_path);
+ timeout_remove(&ruser->to);
+ }
+ }
+
+ ruser->module_ctx.super.deinit(user);
+}
+
+static void replication_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct replication_user *ruser;
+ const char *value;
+
+ value = mail_user_plugin_getenv(user, "mail_replica");
+ if (value == NULL || value[0] == '\0') {
+ e_debug(user->event, "replication: No mail_replica setting - replication disabled");
+ return;
+ }
+
+ if (user->dsyncing) {
+ /* we're running dsync, which means that the remote is telling
+ us about a change. don't trigger a replication back to it */
+ e_debug(user->event, "replication: We're running dsync - replication disabled");
+ return;
+ }
+
+ ruser = p_new(user->pool, struct replication_user, 1);
+ ruser->module_ctx.super = *v;
+ user->vlast = &ruser->module_ctx.super;
+ v->deinit = replication_user_deinit;
+ MODULE_CONTEXT_SET(user, replication_user_module, ruser);
+
+ if (fifo_path == NULL) {
+ /* we'll assume that all users have the same base_dir.
+ they really should. */
+ fifo_path = i_strconcat(user->set->base_dir,
+ "/"REPLICATION_FIFO_NAME, NULL);
+ }
+ ruser->socket_path = p_strconcat(user->pool, user->set->base_dir,
+ "/"REPLICATION_SOCKET_NAME, NULL);
+ value = mail_user_plugin_getenv(user, "replication_sync_timeout");
+ if (value != NULL && str_to_uint(value, &ruser->sync_secs) < 0) {
+ i_error("replication(%s): "
+ "Invalid replication_sync_timeout value: %s",
+ user->username, value);
+ }
+}
+
+static const struct notify_vfuncs replication_vfuncs = {
+ .mail_transaction_begin = replication_mail_transaction_begin,
+ .mail_save = replication_mail_save,
+ .mail_copy = replication_mail_copy,
+ .mail_transaction_commit = replication_mail_transaction_commit,
+ .mailbox_create = replication_mailbox_create,
+ .mailbox_delete_commit = replication_mailbox_delete_commit,
+ .mailbox_rename = replication_mailbox_rename,
+ .mailbox_set_subscribed = replication_mailbox_set_subscribed
+};
+
+static struct notify_context *replication_ctx;
+
+static struct mail_storage_hooks replication_mail_storage_hooks = {
+ .mail_user_created = replication_user_created
+};
+
+void replication_plugin_init(struct module *module)
+{
+ fifo_fd = -1;
+ replication_ctx = notify_register(&replication_vfuncs);
+ mail_storage_hooks_add(module, &replication_mail_storage_hooks);
+}
+
+void replication_plugin_deinit(void)
+{
+ i_close_fd_path(&fifo_fd, fifo_path);
+ i_free_and_null(fifo_path);
+
+ mail_storage_hooks_remove(&replication_mail_storage_hooks);
+ notify_unregister(replication_ctx);
+}
+
+const char *replication_plugin_dependencies[] = { "notify", NULL };
diff --git a/src/plugins/replication/replication-plugin.h b/src/plugins/replication/replication-plugin.h
new file mode 100644
index 0000000..7fa344f
--- /dev/null
+++ b/src/plugins/replication/replication-plugin.h
@@ -0,0 +1,9 @@
+#ifndef REPLICATION_PLUGIN_H
+#define REPLICATION_PLUGIN_H
+
+extern const char *replication_plugin_dependencies[];
+
+void replication_plugin_init(struct module *module);
+void replication_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/trash/Makefile.am b/src/plugins/trash/Makefile.am
new file mode 100644
index 0000000..af15d13
--- /dev/null
+++ b/src/plugins/trash/Makefile.am
@@ -0,0 +1,23 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/quota
+
+NOPLUGIN_LDFLAGS =
+lib11_trash_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib11_trash_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib11_trash_plugin_la_LIBADD = \
+ ../quota/lib10_quota_plugin.la
+endif
+
+lib11_trash_plugin_la_SOURCES = \
+ trash-plugin.c
+
+noinst_HEADERS = \
+ trash-plugin.h
diff --git a/src/plugins/trash/Makefile.in b/src/plugins/trash/Makefile.in
new file mode 100644
index 0000000..62364d8
--- /dev/null
+++ b/src/plugins/trash/Makefile.in
@@ -0,0 +1,824 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/trash
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+@DOVECOT_PLUGIN_DEPS_TRUE@lib11_trash_plugin_la_DEPENDENCIES = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../quota/lib10_quota_plugin.la
+am_lib11_trash_plugin_la_OBJECTS = trash-plugin.lo
+lib11_trash_plugin_la_OBJECTS = $(am_lib11_trash_plugin_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 =
+lib11_trash_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib11_trash_plugin_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)/trash-plugin.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 = $(lib11_trash_plugin_la_SOURCES)
+DIST_SOURCES = $(lib11_trash_plugin_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 =
+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-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/plugins/quota
+
+lib11_trash_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib11_trash_plugin.la
+
+@DOVECOT_PLUGIN_DEPS_TRUE@lib11_trash_plugin_la_LIBADD = \
+@DOVECOT_PLUGIN_DEPS_TRUE@ ../quota/lib10_quota_plugin.la
+
+lib11_trash_plugin_la_SOURCES = \
+ trash-plugin.c
+
+noinst_HEADERS = \
+ trash-plugin.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/plugins/trash/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/trash/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-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}; \
+ }
+
+lib11_trash_plugin.la: $(lib11_trash_plugin_la_OBJECTS) $(lib11_trash_plugin_la_DEPENDENCIES) $(EXTRA_lib11_trash_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib11_trash_plugin_la_LINK) -rpath $(moduledir) $(lib11_trash_plugin_la_OBJECTS) $(lib11_trash_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/trash-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/trash-plugin.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-moduleLTLIBRARIES
+
+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)/trash-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/trash/trash-plugin.c b/src/plugins/trash/trash-plugin.c
new file mode 100644
index 0000000..d918484
--- /dev/null
+++ b/src/plugins/trash/trash-plugin.c
@@ -0,0 +1,392 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "unichar.h"
+#include "istream.h"
+#include "mail-namespace.h"
+#include "mail-search-build.h"
+#include "quota-private.h"
+#include "quota-plugin.h"
+#include "trash-plugin.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define INIT_TRASH_MAILBOX_COUNT 4
+#define MAX_RETRY_COUNT 3
+
+#define TRASH_USER_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, trash_user_module)
+#define TRASH_USER_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, trash_user_module)
+
+struct trash_mailbox {
+ const char *name;
+ int priority; /* lower number = higher priority */
+
+ struct mail_namespace *ns;
+
+ /* temporarily set while cleaning: */
+ struct mailbox *box;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+};
+
+struct trash_user {
+ union mail_user_module_context module_ctx;
+
+ const char *config_file;
+ /* ordered by priority, highest first */
+ ARRAY(struct trash_mailbox) trash_boxes;
+};
+
+const char *trash_plugin_version = DOVECOT_ABI_VERSION;
+
+static MODULE_CONTEXT_DEFINE_INIT(trash_user_module,
+ &mail_user_module_register);
+static enum quota_alloc_result (*trash_next_quota_test_alloc)(
+ struct quota_transaction_context *, uoff_t,
+ const char **error_r);
+
+static int trash_clean_mailbox_open(struct trash_mailbox *trash)
+{
+ struct mail_search_args *search_args;
+
+ trash->box = mailbox_alloc(trash->ns->list, trash->name, 0);
+ if (mailbox_open(trash->box) < 0) {
+ mailbox_free(&trash->box);
+ return 0;
+ }
+
+ if (mailbox_sync(trash->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0)
+ return -1;
+
+ trash->trans = mailbox_transaction_begin(trash->box, 0, __func__);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+ trash->search_ctx = mailbox_search_init(trash->trans,
+ search_args, NULL,
+ MAIL_FETCH_PHYSICAL_SIZE |
+ MAIL_FETCH_RECEIVED_DATE, NULL);
+ mail_search_args_unref(&search_args);
+
+ return mailbox_search_next(trash->search_ctx, &trash->mail) ? 1 : 0;
+}
+
+static int trash_clean_mailbox_get_next(struct trash_mailbox *trash,
+ time_t *received_time_r)
+{
+ int ret;
+
+ if (trash->mail == NULL) {
+ if (trash->box == NULL)
+ ret = trash_clean_mailbox_open(trash);
+ else {
+ ret = mailbox_search_next(trash->search_ctx,
+ &trash->mail) ? 1 : 0;
+ }
+ if (ret <= 0) {
+ *received_time_r = 0;
+ return ret;
+ }
+ }
+
+ if (mail_get_received_date(trash->mail, received_time_r) < 0)
+ return -1;
+ return 1;
+}
+
+static int trash_try_clean_mails(struct quota_transaction_context *ctx,
+ uint64_t size_needed,
+ unsigned int count_needed)
+{
+ struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(ctx->quota->user);
+ struct trash_mailbox *trashes;
+ struct event_reason *reason;
+ unsigned int i, j, count, oldest_idx;
+ time_t oldest, received = 0;
+ uint64_t size, size_expunged = 0;
+ unsigned int expunged_count = 0;
+ int ret = 0;
+
+ reason = event_reason_begin("trash:clean");
+
+ trashes = array_get_modifiable(&tuser->trash_boxes, &count);
+ for (i = 0; i < count; ) {
+ /* expunge oldest mails first in all trash boxes with
+ same priority */
+ oldest_idx = count;
+ oldest = (time_t)-1;
+ for (j = i; j < count; j++) {
+ if (trashes[j].priority != trashes[i].priority)
+ break;
+
+ ret = trash_clean_mailbox_get_next(&trashes[j],
+ &received);
+ if (ret < 0)
+ goto err;
+ if (ret > 0) {
+ if (oldest == (time_t)-1 || received < oldest) {
+ oldest = received;
+ oldest_idx = j;
+ }
+ }
+ }
+
+ if (oldest_idx < count) {
+ if (mail_get_physical_size(trashes[oldest_idx].mail,
+ &size) < 0) {
+ /* maybe expunged already? */
+ trashes[oldest_idx].mail = NULL;
+ continue;
+ }
+
+ mail_expunge(trashes[oldest_idx].mail);
+ expunged_count++;
+ size_expunged += size;
+ if (size_expunged >= size_needed &&
+ expunged_count >= count_needed)
+ break;
+ trashes[oldest_idx].mail = NULL;
+ } else {
+ /* find more mails from next priority's mailbox */
+ i = j;
+ }
+ }
+
+err:
+ for (i = 0; i < count; i++) {
+ struct trash_mailbox *trash = &trashes[i];
+
+ if (trash->box == NULL)
+ continue;
+
+ trash->mail = NULL;
+ (void)mailbox_search_deinit(&trash->search_ctx);
+
+ if (size_expunged >= size_needed &&
+ expunged_count >= count_needed) {
+ (void)mailbox_transaction_commit(&trash->trans);
+ (void)mailbox_sync(trash->box, 0);
+ } else {
+ /* couldn't get enough space, don't expunge anything */
+ mailbox_transaction_rollback(&trash->trans);
+ }
+
+ mailbox_free(&trash->box);
+ }
+ event_reason_end(&reason);
+
+ if (size_expunged < size_needed) {
+ e_debug(ctx->quota->user->event,
+ "trash plugin: Failed to remove enough messages "
+ "(needed %"PRIu64" bytes, expunged only %"PRIu64" bytes)",
+ size_needed, size_expunged);
+ return 0;
+ }
+ if (expunged_count < count_needed) {
+ e_debug(ctx->quota->user->event,
+ "trash plugin: Failed to remove enough messages "
+ "(needed %u messages, expunged only %u messages)",
+ count_needed, expunged_count);
+ return 0;
+ }
+
+ if (ctx->bytes_over > 0) {
+ /* user is over quota. drop the over-bytes first. */
+ i_assert(ctx->bytes_over <= size_expunged);
+ size_expunged -= ctx->bytes_over;
+ ctx->bytes_over = 0;
+ }
+ if (ctx->count_over > 0) {
+ /* user is over quota. drop the over-count first. */
+ i_assert(ctx->count_over <= expunged_count);
+ expunged_count -= ctx->count_over;
+ ctx->count_over = 0;
+ }
+
+ if (ctx->bytes_ceil > ((uint64_t)-1 - size_expunged)) {
+ ctx->bytes_ceil = (uint64_t)-1;
+ } else {
+ ctx->bytes_ceil += size_expunged;
+ }
+ if (ctx->count_ceil < ((uint64_t)-1 - expunged_count)) {
+ ctx->count_ceil = (uint64_t)-1;
+ } else {
+ ctx->count_ceil += expunged_count;
+ }
+ return 1;
+}
+
+static enum quota_alloc_result
+trash_quota_test_alloc(struct quota_transaction_context *ctx,
+ uoff_t size, const char **error_r)
+{
+ int i;
+ uint64_t size_needed = 0;
+ unsigned int count_needed = 0;
+
+ for (i = 0; ; i++) {
+ enum quota_alloc_result ret;
+ ret = trash_next_quota_test_alloc(ctx, size, error_r);
+ if (ret != QUOTA_ALLOC_RESULT_OVER_QUOTA) {
+ if (ret == QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT &&
+ ctx->quota->user->mail_debug)
+ i_debug("trash plugin: Mail is larger than "
+ "quota, won't even try to handle");
+ return ret;
+ }
+
+ if (i == MAX_RETRY_COUNT) {
+ /* trash_try_clean_mails() should have returned 0 if
+ it couldn't get enough space, but allow retrying
+ it a couple of times if there was some extra space
+ that was needed.. */
+ break;
+ }
+
+ if (ctx->bytes_ceil != (uint64_t)-1 &&
+ ctx->bytes_ceil < size + ctx->bytes_over)
+ size_needed = size + ctx->bytes_over - ctx->bytes_ceil;
+ if (ctx->count_ceil != (uint64_t)-1 &&
+ ctx->count_ceil < 1 + ctx->count_over)
+ count_needed = 1 + ctx->count_over - ctx->count_ceil;
+
+ /* not enough space. try deleting some from mailbox. */
+ if (trash_try_clean_mails(ctx, size_needed, count_needed) <= 0) {
+ *error_r = t_strdup_printf(
+ "Allocating %"PRIuUOFF_T" bytes would exceed quota", size);
+ return QUOTA_ALLOC_RESULT_OVER_QUOTA;
+ }
+ }
+ *error_r = t_strdup_printf(
+ "Allocating %"PRIuUOFF_T" bytes would exceed quota", size);
+ return QUOTA_ALLOC_RESULT_OVER_QUOTA;
+}
+
+static bool trash_find_storage(struct mail_user *user,
+ struct trash_mailbox *trash)
+{
+ struct mail_namespace *ns;
+
+ ns = mail_namespace_find(user->namespaces, trash->name);
+ if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0)
+ return FALSE;
+
+ trash->ns = ns;
+ return TRUE;
+}
+
+static int trash_mailbox_priority_cmp(const struct trash_mailbox *t1,
+ const struct trash_mailbox *t2)
+{
+ return t1->priority - t2->priority;
+}
+
+static int read_configuration(struct mail_user *user, const char *path)
+{
+ struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(user);
+ struct istream *input;
+ const char *line, *name;
+ struct trash_mailbox *trash;
+ int fd, ret = 0;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ i_error("trash plugin: open(%s) failed: %m", path);
+ return -1;
+ }
+
+ p_array_init(&tuser->trash_boxes, user->pool, INIT_TRASH_MAILBOX_COUNT);
+
+ input = i_stream_create_fd(fd, SIZE_MAX);
+ i_stream_set_return_partial_line(input, TRUE);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ /* <priority> <mailbox name> */
+ name = strchr(line, ' ');
+ if (name == NULL || name[1] == '\0' || *line == '#')
+ continue;
+
+ trash = array_append_space(&tuser->trash_boxes);
+ trash->name = p_strdup(user->pool, name+1);
+ if (str_to_int(t_strdup_until(line, name),
+ &trash->priority) < 0) {
+ i_error("trash: Invalid priority for mailbox '%s'",
+ trash->name);
+ ret = -1;
+ }
+
+ if (!uni_utf8_str_is_valid(trash->name)) {
+ i_error("trash: Mailbox name not UTF-8: %s",
+ trash->name);
+ ret = -1;
+ }
+ if (!trash_find_storage(user, trash)) {
+ i_error("trash: Namespace not found for mailbox '%s'",
+ trash->name);
+ ret = -1;
+ }
+
+ e_debug(user->event, "trash plugin: Added '%s' with priority %d",
+ trash->name, trash->priority);
+ }
+ i_stream_destroy(&input);
+ i_close_fd(&fd);
+
+ array_sort(&tuser->trash_boxes, trash_mailbox_priority_cmp);
+ return ret;
+}
+
+static void
+trash_mail_user_created(struct mail_user *user)
+{
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+ struct trash_user *tuser;
+ const char *env;
+
+ env = mail_user_plugin_getenv(user, "trash");
+ if (env == NULL) {
+ e_debug(user->event, "trash: No trash setting - plugin disabled");
+ } else if (quser == NULL) {
+ i_error("trash plugin: quota plugin not initialized");
+ } else {
+ tuser = p_new(user->pool, struct trash_user, 1);
+ tuser->config_file = env;
+ MODULE_CONTEXT_SET(user, trash_user_module, tuser);
+ }
+}
+
+static void
+trash_mail_namespaces_created(struct mail_namespace *namespaces)
+{
+ struct mail_user *user = namespaces->user;
+ struct trash_user *tuser = TRASH_USER_CONTEXT(user);
+ struct quota_user *quser = QUOTA_USER_CONTEXT(user);
+
+ if (tuser != NULL && read_configuration(user, tuser->config_file) == 0) {
+ i_assert(quser != NULL);
+ trash_next_quota_test_alloc =
+ quser->quota->set->test_alloc;
+ quser->quota->set->test_alloc = trash_quota_test_alloc;
+ }
+}
+
+static struct mail_storage_hooks trash_mail_storage_hooks = {
+ .mail_user_created = trash_mail_user_created,
+ .mail_namespaces_created = trash_mail_namespaces_created,
+};
+
+void trash_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &trash_mail_storage_hooks);
+}
+
+void trash_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&trash_mail_storage_hooks);
+}
+
+const char *trash_plugin_dependencies[] = { "quota", NULL };
diff --git a/src/plugins/trash/trash-plugin.h b/src/plugins/trash/trash-plugin.h
new file mode 100644
index 0000000..bd6db6b
--- /dev/null
+++ b/src/plugins/trash/trash-plugin.h
@@ -0,0 +1,9 @@
+#ifndef TRASH_PLUGIN_H
+#define TRASH_PLUGIN_H
+
+extern const char *trash_plugin_dependencies[];
+
+void trash_plugin_init(struct module *module);
+void trash_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/var-expand-crypt/Makefile.am b/src/plugins/var-expand-crypt/Makefile.am
new file mode 100644
index 0000000..6feb1a7
--- /dev/null
+++ b/src/plugins/var-expand-crypt/Makefile.am
@@ -0,0 +1,39 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dcrypt
+
+NOPLUGIN_LDFLAGS =
+lib20_var_expand_crypt_la_LDFLAGS = -module -avoid-version
+lib20_auth_var_expand_crypt_la_LDFLAGS = -module -avoid-version
+
+auth_moduledir = $(moduledir)/auth
+
+module_LTLIBRARIES = \
+ lib20_var_expand_crypt.la
+
+auth_module_LTLIBRARIES = \
+ lib20_auth_var_expand_crypt.la
+
+lib20_auth_var_expand_crypt_la_SOURCES = \
+ var-expand-crypt-plugin.c
+
+lib20_var_expand_crypt_la_SOURCES = \
+ var-expand-crypt-plugin.c
+
+test_programs = test-var-expand-crypt
+
+test_var_expand_crypt_CFLAGS = \
+ -DDCRYPT_BUILD_DIR=\"$(top_builddir)/src/lib-dcrypt\"
+test_var_expand_crypt_SOURCES = \
+ test-var-expand-crypt.c
+test_var_expand_crypt_LDADD = \
+ ../../lib-dovecot/libdovecot.la \
+ lib20_auth_var_expand_crypt.la
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+noinst_PROGRAMS = $(test_programs)
diff --git a/src/plugins/var-expand-crypt/Makefile.in b/src/plugins/var-expand-crypt/Makefile.in
new file mode 100644
index 0000000..faa70a2
--- /dev/null
+++ b/src/plugins/var-expand-crypt/Makefile.in
@@ -0,0 +1,938 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/var-expand-crypt
+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-var-expand-crypt$(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)$(auth_moduledir)" \
+ "$(DESTDIR)$(moduledir)"
+LTLIBRARIES = $(auth_module_LTLIBRARIES) $(module_LTLIBRARIES)
+lib20_auth_var_expand_crypt_la_LIBADD =
+am_lib20_auth_var_expand_crypt_la_OBJECTS = \
+ var-expand-crypt-plugin.lo
+lib20_auth_var_expand_crypt_la_OBJECTS = \
+ $(am_lib20_auth_var_expand_crypt_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 =
+lib20_auth_var_expand_crypt_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) \
+ $(lib20_auth_var_expand_crypt_la_LDFLAGS) $(LDFLAGS) -o $@
+lib20_var_expand_crypt_la_LIBADD =
+am_lib20_var_expand_crypt_la_OBJECTS = var-expand-crypt-plugin.lo
+lib20_var_expand_crypt_la_OBJECTS = \
+ $(am_lib20_var_expand_crypt_la_OBJECTS)
+lib20_var_expand_crypt_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_var_expand_crypt_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_var_expand_crypt_OBJECTS = \
+ test_var_expand_crypt-test-var-expand-crypt.$(OBJEXT)
+test_var_expand_crypt_OBJECTS = $(am_test_var_expand_crypt_OBJECTS)
+test_var_expand_crypt_DEPENDENCIES = ../../lib-dovecot/libdovecot.la \
+ lib20_auth_var_expand_crypt.la
+test_var_expand_crypt_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(test_var_expand_crypt_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)/test_var_expand_crypt-test-var-expand-crypt.Po \
+ ./$(DEPDIR)/var-expand-crypt-plugin.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 = $(lib20_auth_var_expand_crypt_la_SOURCES) \
+ $(lib20_var_expand_crypt_la_SOURCES) \
+ $(test_var_expand_crypt_SOURCES)
+DIST_SOURCES = $(lib20_auth_var_expand_crypt_la_SOURCES) \
+ $(lib20_var_expand_crypt_la_SOURCES) \
+ $(test_var_expand_crypt_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@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dcrypt
+
+lib20_var_expand_crypt_la_LDFLAGS = -module -avoid-version
+lib20_auth_var_expand_crypt_la_LDFLAGS = -module -avoid-version
+auth_moduledir = $(moduledir)/auth
+module_LTLIBRARIES = \
+ lib20_var_expand_crypt.la
+
+auth_module_LTLIBRARIES = \
+ lib20_auth_var_expand_crypt.la
+
+lib20_auth_var_expand_crypt_la_SOURCES = \
+ var-expand-crypt-plugin.c
+
+lib20_var_expand_crypt_la_SOURCES = \
+ var-expand-crypt-plugin.c
+
+test_programs = test-var-expand-crypt
+test_var_expand_crypt_CFLAGS = \
+ -DDCRYPT_BUILD_DIR=\"$(top_builddir)/src/lib-dcrypt\"
+
+test_var_expand_crypt_SOURCES = \
+ test-var-expand-crypt.c
+
+test_var_expand_crypt_LDADD = \
+ ../../lib-dovecot/libdovecot.la \
+ lib20_auth_var_expand_crypt.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/plugins/var-expand-crypt/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/var-expand-crypt/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-auth_moduleLTLIBRARIES: $(auth_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_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)$(auth_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(auth_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(auth_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(auth_moduledir)"; \
+ }
+
+uninstall-auth_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(auth_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(auth_moduledir)/$$f"; \
+ done
+
+clean-auth_moduleLTLIBRARIES:
+ -test -z "$(auth_module_LTLIBRARIES)" || rm -f $(auth_module_LTLIBRARIES)
+ @list='$(auth_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}; \
+ }
+
+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}; \
+ }
+
+lib20_auth_var_expand_crypt.la: $(lib20_auth_var_expand_crypt_la_OBJECTS) $(lib20_auth_var_expand_crypt_la_DEPENDENCIES) $(EXTRA_lib20_auth_var_expand_crypt_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_auth_var_expand_crypt_la_LINK) -rpath $(auth_moduledir) $(lib20_auth_var_expand_crypt_la_OBJECTS) $(lib20_auth_var_expand_crypt_la_LIBADD) $(LIBS)
+
+lib20_var_expand_crypt.la: $(lib20_var_expand_crypt_la_OBJECTS) $(lib20_var_expand_crypt_la_DEPENDENCIES) $(EXTRA_lib20_var_expand_crypt_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_var_expand_crypt_la_LINK) -rpath $(moduledir) $(lib20_var_expand_crypt_la_OBJECTS) $(lib20_var_expand_crypt_la_LIBADD) $(LIBS)
+
+test-var-expand-crypt$(EXEEXT): $(test_var_expand_crypt_OBJECTS) $(test_var_expand_crypt_DEPENDENCIES) $(EXTRA_test_var_expand_crypt_DEPENDENCIES)
+ @rm -f test-var-expand-crypt$(EXEEXT)
+ $(AM_V_CCLD)$(test_var_expand_crypt_LINK) $(test_var_expand_crypt_OBJECTS) $(test_var_expand_crypt_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/var-expand-crypt-plugin.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_var_expand_crypt-test-var-expand-crypt.o: test-var-expand-crypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_var_expand_crypt_CFLAGS) $(CFLAGS) -MT test_var_expand_crypt-test-var-expand-crypt.o -MD -MP -MF $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Tpo -c -o test_var_expand_crypt-test-var-expand-crypt.o `test -f 'test-var-expand-crypt.c' || echo '$(srcdir)/'`test-var-expand-crypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Tpo $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-var-expand-crypt.c' object='test_var_expand_crypt-test-var-expand-crypt.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_var_expand_crypt_CFLAGS) $(CFLAGS) -c -o test_var_expand_crypt-test-var-expand-crypt.o `test -f 'test-var-expand-crypt.c' || echo '$(srcdir)/'`test-var-expand-crypt.c
+
+test_var_expand_crypt-test-var-expand-crypt.obj: test-var-expand-crypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_var_expand_crypt_CFLAGS) $(CFLAGS) -MT test_var_expand_crypt-test-var-expand-crypt.obj -MD -MP -MF $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Tpo -c -o test_var_expand_crypt-test-var-expand-crypt.obj `if test -f 'test-var-expand-crypt.c'; then $(CYGPATH_W) 'test-var-expand-crypt.c'; else $(CYGPATH_W) '$(srcdir)/test-var-expand-crypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Tpo $(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-var-expand-crypt.c' object='test_var_expand_crypt-test-var-expand-crypt.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_var_expand_crypt_CFLAGS) $(CFLAGS) -c -o test_var_expand_crypt-test-var-expand-crypt.obj `if test -f 'test-var-expand-crypt.c'; then $(CYGPATH_W) 'test-var-expand-crypt.c'; else $(CYGPATH_W) '$(srcdir)/test-var-expand-crypt.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)
+installdirs:
+ for dir in "$(DESTDIR)$(auth_moduledir)" "$(DESTDIR)$(moduledir)"; 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-auth_moduleLTLIBRARIES clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/test_var_expand_crypt-test-var-expand-crypt.Po
+ -rm -f ./$(DEPDIR)/var-expand-crypt-plugin.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-auth_moduleLTLIBRARIES \
+ install-moduleLTLIBRARIES
+
+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)/test_var_expand_crypt-test-var-expand-crypt.Po
+ -rm -f ./$(DEPDIR)/var-expand-crypt-plugin.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-auth_moduleLTLIBRARIES \
+ uninstall-moduleLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-auth_moduleLTLIBRARIES clean-generic \
+ clean-libtool clean-moduleLTLIBRARIES 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-auth_moduleLTLIBRARIES 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-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-auth_moduleLTLIBRARIES uninstall-moduleLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! env $(test_options) $(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/plugins/var-expand-crypt/test-var-expand-crypt.c b/src/plugins/var-expand-crypt/test-var-expand-crypt.c
new file mode 100644
index 0000000..e12a040
--- /dev/null
+++ b/src/plugins/var-expand-crypt/test-var-expand-crypt.c
@@ -0,0 +1,102 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "str.h"
+#include "var-expand.h"
+#include "randgen.h"
+#include "dcrypt.h"
+
+struct module;
+
+extern void var_expand_crypt_init(struct module *module);
+extern void var_expand_crypt_deinit(void);
+
+static void test_var_expand_crypt(void)
+{
+ struct var_expand_table table[] = {
+ { '\0', "98b3b40a48ca40f998b3b40a48ca40f9", "iv" },
+ { '\0', "cc2981c8f38aea59cc2981c8f38aea59", "key" },
+ { '\0', "46b58741763fe22598014be26331a082", "encrypted_noiv" },
+ { '\0', "98b3b40a48ca40f998b3b40a48ca40f9$46b58741763fe22598014be26331a082$", "encrypted" },
+ { '\0', "hello, world", "decrypted" },
+ { '\0', NULL, "encrypted2" },
+ { '\0', NULL, NULL }
+ };
+
+ static struct {
+ const char *input;
+ const char *output;
+ int expect_ret;
+ } test_cases[] = {
+ { "%{encrypt;algo=null:decrypted}", "", -1 },
+ { "%{encrypt;algo=aes-128-cbc,iv=98b3b40a48ca40f998b3b40a48ca40f9,key=cc2981c8f38aea59cc2981c8f38aea59:decrypted}", "98b3b40a48ca40f998b3b40a48ca40f9$46b58741763fe22598014be26331a082$", 1 },
+ { "%{encrypt;noiv=yes,algo=aes-128-cbc,iv=98b3b40a48ca40f998b3b40a48ca40f9,key=cc2981c8f38aea59cc2981c8f38aea59:decrypted}", "46b58741763fe22598014be26331a082", 1 },
+ { "%{encrypt;algo=aes-128-cbc,iv=%{iv},key=%{key}:decrypted}", "98b3b40a48ca40f998b3b40a48ca40f9$46b58741763fe22598014be26331a082$", 1 },
+ { "%{decrypt;algo=null:encrypted}", "", -1 },
+ { "%{decrypt;algo=aes-128-cbc,key=%{key}:encrypted}", "hello, world", 1 },
+ { "%{decrypt;algo=aes-128-cbc,iv=%{iv},key=%{key}:encrypted_noiv}", "hello, world", 1 },
+ { "%{decrypt;algo=aes-128-cbc,iv=98b3b40a48ca40f998b3b40a48ca40f9,key=cc2981c8f38aea59cc2981c8f38aea59:encrypted_noiv}", "hello, world", 1 },
+ };
+
+ unsigned int i;
+
+ test_begin("var_expand_crypt");
+ var_expand_crypt_init(NULL);
+
+ for(i=0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ const char *error;
+ string_t *dest = t_str_new(32);
+ int ret = var_expand(dest, test_cases[i].input, table, &error);
+ if (ret < 0) {
+ if (test_cases[i].expect_ret == -1)
+ i_info("Expected: var_expand(%s): %s", test_cases[i].input, error);
+ else
+ i_error("var_expand(%s): %s", test_cases[i].input, error);
+ }
+ test_assert_idx(strcmp(str_c(dest), test_cases[i].output)==0, i);
+ test_assert_idx(ret == test_cases[i].expect_ret, i);
+ } T_END;
+
+ test_end();
+
+ test_begin("var_expand_crypt_random");
+
+ string_t *input = t_str_new(32);
+ string_t *output = t_str_new(32);
+
+ for(i=0;i<1000;i++) {
+ const char *error;
+ str_truncate(input, 0);
+ str_truncate(output, 0);
+
+ test_assert_idx(var_expand(input, "%{encrypt;algo=aes-128-cbc,key=%{key}:decrypted}", table, &error) == 1, i);
+ table[5].value = str_c(input);
+ test_assert_idx(var_expand(output, "%{decrypt;algo=aes-128-cbc,key=%{key}:encrypted2}", table, &error) == 1, i);
+ test_assert_idx(strcmp(str_c(output), table[4].value)==0, i);
+ };
+
+ var_expand_crypt_deinit();
+ test_end();
+}
+
+int main(void)
+{
+ int ret = 0;
+ static void (*const test_functions[])(void) = {
+ test_var_expand_crypt,
+ NULL
+ };
+ struct dcrypt_settings set = {
+ .module_dir = DCRYPT_BUILD_DIR"/.libs"
+ };
+
+ if (!dcrypt_initialize(NULL, &set, NULL))
+ return 0;
+
+ ret = test_run(test_functions);
+
+ dcrypt_deinitialize();
+
+ return ret;
+}
diff --git a/src/plugins/var-expand-crypt/var-expand-crypt-plugin.c b/src/plugins/var-expand-crypt/var-expand-crypt-plugin.c
new file mode 100644
index 0000000..1f6cce7
--- /dev/null
+++ b/src/plugins/var-expand-crypt/var-expand-crypt-plugin.c
@@ -0,0 +1,335 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hex-binary.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "var-expand-private.h"
+#include "dcrypt.h"
+
+#define VAR_EXPAND_CRYPT_DEFAULT_ALGO "AES-256-CBC"
+
+struct module;
+
+enum crypt_field_format {
+ FORMAT_HEX,
+ FORMAT_BASE64
+};
+
+struct var_expand_crypt_context {
+ struct var_expand_context *ctx;
+ const char *algo;
+ string_t *iv;
+ string_t *enckey;
+ enum crypt_field_format format;
+ bool enc_result_only:1;
+};
+
+static bool var_expand_crypt_initialize(const char **error_r);
+
+void var_expand_crypt_init(struct module *module);
+void var_expand_crypt_deinit(void);
+void auth_var_expand_crypt_init(struct module *module);
+void auth_var_expand_crypt_deinit(void);
+
+static int
+var_expand_crypt_settings(struct var_expand_crypt_context *ctx,
+ const char *const *args, const char **error_r)
+{
+ 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, "iv") == 0) {
+ str_truncate(ctx->iv, 0);
+ if (var_expand_with_funcs(ctx->iv, value, ctx->ctx->table,
+ ctx->ctx->func_table,
+ ctx->ctx->context, error_r) < 0) {
+ return -1;
+ }
+ const char *hexiv = t_strdup(str_c(ctx->iv));
+ /* try to decode IV */
+ str_truncate(ctx->iv, 0);
+ hex_to_binary(hexiv, ctx->iv);
+ } if (strcmp(k, "noiv") == 0) {
+ ctx->enc_result_only = strcasecmp(value, "yes")==0;
+ } if (strcmp(k, "algo") == 0) {
+ ctx->algo = value;
+ } else if (strcmp(k, "key") == 0) {
+ str_truncate(ctx->enckey, 0);
+ if (var_expand_with_funcs(ctx->enckey, value,
+ ctx->ctx->table,
+ ctx->ctx->func_table,
+ ctx->ctx->context,
+ error_r) < 0) {
+ return -1;
+ }
+ const char *hexkey = t_strdup(str_c(ctx->enckey));
+ str_truncate(ctx->enckey, 0);
+ hex_to_binary(hexkey, ctx->enckey);
+ } else if (strcmp(k, "format") == 0) {
+ if (strcmp(value, "hex") == 0) {
+ ctx->format = FORMAT_HEX;
+ } else if (strcmp(value, "base64") == 0) {
+ ctx->format = FORMAT_BASE64;
+ } else {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not supported format",
+ value);
+ return -1;
+ }
+ }
+ args++;
+ }
+
+ if (ctx->algo == NULL) {
+ ctx->algo = "AES-256-CBC";
+ }
+
+ return 0;
+}
+
+static int
+var_expand_crypt(struct dcrypt_context_symmetric *dctx, buffer_t *key, buffer_t *iv,
+ const buffer_t *input, buffer_t *output, const char **error_r)
+{
+ /* make sure IV is correct */
+ if (iv->used == 0) {
+ dcrypt_ctx_sym_set_key_iv_random(dctx);
+ /* acquire IV */
+ dcrypt_ctx_sym_get_iv(dctx, iv);
+ } else if (dcrypt_ctx_sym_get_iv_length(dctx) != iv->used) {
+ *error_r = t_strdup_printf("crypt: IV length invalid (%zu != %u)",
+ iv->used,
+ dcrypt_ctx_sym_get_iv_length(dctx));
+ return -1;
+ } else {
+ dcrypt_ctx_sym_set_iv(dctx, iv->data, iv->used);
+ }
+
+ if (dcrypt_ctx_sym_get_key_length(dctx) != key->used) {
+ *error_r = t_strdup_printf("crypt: Key length invalid (%zu != %u)",
+ key->used,
+ dcrypt_ctx_sym_get_key_length(dctx));
+ return -1;
+ } else {
+ dcrypt_ctx_sym_set_key(dctx, key->data, key->used);
+ }
+
+ if (!dcrypt_ctx_sym_init(dctx, error_r) ||
+ !dcrypt_ctx_sym_update(dctx, input->data,
+ input->used, output, error_r) ||
+ !dcrypt_ctx_sym_final(dctx, output, error_r))
+ return -1;
+ return 0;
+}
+
+static int
+var_expand_encrypt(struct var_expand_context *_ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r)
+{
+ if (!var_expand_crypt_initialize(error_r))
+ return -1;
+
+ const char *p = strchr(key, ';');
+ const char *const *args = NULL;
+ const char *value;
+ struct var_expand_crypt_context ctx;
+ string_t *dest;
+ int ret = 0;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.ctx = _ctx;
+ ctx.format = FORMAT_HEX;
+
+ if (p != NULL) {
+ args = t_strsplit(p+1, ",");
+ }
+
+ string_t *field_value = t_str_new(64);
+ ctx.iv = t_str_new(64);
+ ctx.enckey = t_str_new(64);
+ string_t *tmp = t_str_new(128);
+
+ if ((ret = var_expand_long(_ctx, field, strlen(field),
+ &value, error_r)) < 1) {
+ return ret;
+ }
+
+ if (*value == '\0') {
+ *result_r = value;
+ return ret;
+ }
+
+ if (var_expand_crypt_settings(&ctx, args, error_r) < 0)
+ return -1;
+
+ str_append(field_value, value);
+
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create(ctx.algo, DCRYPT_MODE_ENCRYPT, &dctx, error_r))
+ return -1;
+
+ ret = var_expand_crypt(dctx, ctx.enckey, ctx.iv, field_value, tmp, error_r);
+ dcrypt_ctx_sym_destroy(&dctx);
+
+ if (ret == 0) {
+ /* makes compiler happy */
+ const char *enciv = "";
+ const char *res = "";
+
+ switch(ctx.format) {
+ case FORMAT_HEX:
+ enciv = binary_to_hex(ctx.iv->data, ctx.iv->used);
+ res = binary_to_hex(tmp->data, tmp->used);
+ break;
+ case FORMAT_BASE64:
+ dest = t_str_new(32);
+ base64_encode(ctx.iv->data, ctx.iv->used, dest);
+ enciv = str_c(dest);
+ dest = t_str_new(32);
+ base64_encode(tmp->data, tmp->used, dest);
+ res = str_c(dest);
+ break;
+ default:
+ i_unreached();
+ }
+ if (ctx.enc_result_only)
+ *result_r = t_strdup(res);
+ else
+ *result_r = t_strdup_printf("%s$%s$", enciv, res);
+ ret = 1;
+ }
+
+ return ret;
+}
+
+static int
+var_expand_decrypt(struct var_expand_context *_ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r)
+{
+ if (!var_expand_crypt_initialize(error_r))
+ return -1;
+
+ const char *p = strchr(key, ';');
+ const char *const *args = NULL;
+ const char *value;
+ struct var_expand_crypt_context ctx;
+ int ret = 0;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.ctx = _ctx;
+ ctx.format = FORMAT_HEX;
+
+ if (p != NULL) {
+ args = t_strsplit(p+1, ",");
+ }
+
+ string_t *field_value = t_str_new(64);
+ ctx.iv = t_str_new(64);
+ ctx.enckey = t_str_new(64);
+ string_t *tmp = t_str_new(128);
+
+ if ((ret = var_expand_long(_ctx, field, strlen(field),
+ &value, error_r)) < 1) {
+ return ret;
+ }
+
+ if (*value == '\0') {
+ *result_r = value;
+ return ret;
+ }
+
+ if (var_expand_crypt_settings(&ctx, args, error_r) < 0)
+ return -1;
+
+ const char *encdata = value;
+ const char *enciv = "";
+
+ /* make sure IV is correct */
+ if (ctx.iv->used == 0 && (p = strchr(encdata, '$')) != NULL) {
+ /* see if IV can be taken from data */
+ enciv = t_strcut(encdata, '$');
+ encdata = t_strcut(p+1,'$');
+ }
+
+ str_truncate(field_value, 0);
+
+ /* try to decode iv and encdata */
+ switch(ctx.format) {
+ case FORMAT_HEX:
+ if (ctx.iv->used == 0)
+ hex_to_binary(enciv, ctx.iv);
+ hex_to_binary(encdata, field_value);
+ break;
+ case FORMAT_BASE64:
+ if (ctx.iv->used == 0)
+ str_append_str(ctx.iv, t_base64_decode_str(enciv));
+ str_append_str(field_value, t_base64_decode_str(encdata));
+ break;
+ }
+
+ if (ctx.iv->used == 0) {
+ *error_r = t_strdup_printf("decrypt: IV missing");
+ return -1;
+ }
+
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create(ctx.algo, DCRYPT_MODE_DECRYPT, &dctx, error_r))
+ return -1;
+ ret = var_expand_crypt(dctx, ctx.enckey, ctx.iv, field_value, tmp, error_r);
+ dcrypt_ctx_sym_destroy(&dctx);
+
+ if (ret == 0) {
+ *result_r = str_c(tmp);
+ ret = 1;
+ }
+
+ return ret;
+}
+
+static const struct var_expand_extension_func_table funcs[] = {
+ { "encrypt", var_expand_encrypt },
+ { "decrypt", var_expand_decrypt },
+ { NULL, NULL, }
+};
+
+static bool var_expand_crypt_initialize(const char **error_r)
+{
+ return dcrypt_initialize(NULL, NULL, error_r);
+}
+
+void var_expand_crypt_init(struct module *module ATTR_UNUSED)
+{
+ var_expand_register_func_array(funcs);
+ /* do not initialize dcrypt here - saves alot of memory
+ to not load openssl every time. Only load it if
+ needed */
+}
+
+void var_expand_crypt_deinit(void)
+{
+ var_expand_unregister_func_array(funcs);
+}
+
+void auth_var_expand_crypt_init(struct module *module)
+{
+ var_expand_crypt_init(module);
+}
+
+void auth_var_expand_crypt_deinit(void)
+{
+ var_expand_crypt_deinit();
+}
diff --git a/src/plugins/virtual/Makefile.am b/src/plugins/virtual/Makefile.am
new file mode 100644
index 0000000..9ba873d
--- /dev/null
+++ b/src/plugins/virtual/Makefile.am
@@ -0,0 +1,29 @@
+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 \
+ -I$(top_srcdir)/src/lib-imap-storage
+
+NOPLUGIN_LDFLAGS =
+lib20_virtual_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_virtual_plugin.la
+
+lib20_virtual_plugin_la_SOURCES = \
+ virtual-config.c \
+ virtual-mail.c \
+ virtual-plugin.c \
+ virtual-search.c \
+ virtual-storage.c \
+ virtual-save.c \
+ virtual-sync.c \
+ virtual-transaction.c
+
+noinst_HEADERS = \
+ virtual-plugin.h \
+ virtual-storage.h \
+ virtual-transaction.h
diff --git a/src/plugins/virtual/Makefile.in b/src/plugins/virtual/Makefile.in
new file mode 100644
index 0000000..b085495
--- /dev/null
+++ b/src/plugins/virtual/Makefile.in
@@ -0,0 +1,859 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/virtual
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_virtual_plugin_la_LIBADD =
+am_lib20_virtual_plugin_la_OBJECTS = virtual-config.lo virtual-mail.lo \
+ virtual-plugin.lo virtual-search.lo virtual-storage.lo \
+ virtual-save.lo virtual-sync.lo virtual-transaction.lo
+lib20_virtual_plugin_la_OBJECTS = \
+ $(am_lib20_virtual_plugin_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 =
+lib20_virtual_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_virtual_plugin_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)/virtual-config.Plo \
+ ./$(DEPDIR)/virtual-mail.Plo ./$(DEPDIR)/virtual-plugin.Plo \
+ ./$(DEPDIR)/virtual-save.Plo ./$(DEPDIR)/virtual-search.Plo \
+ ./$(DEPDIR)/virtual-storage.Plo ./$(DEPDIR)/virtual-sync.Plo \
+ ./$(DEPDIR)/virtual-transaction.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 = $(lib20_virtual_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_virtual_plugin_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 =
+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-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-imap-storage
+
+lib20_virtual_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_virtual_plugin.la
+
+lib20_virtual_plugin_la_SOURCES = \
+ virtual-config.c \
+ virtual-mail.c \
+ virtual-plugin.c \
+ virtual-search.c \
+ virtual-storage.c \
+ virtual-save.c \
+ virtual-sync.c \
+ virtual-transaction.c
+
+noinst_HEADERS = \
+ virtual-plugin.h \
+ virtual-storage.h \
+ virtual-transaction.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/plugins/virtual/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/virtual/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-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}; \
+ }
+
+lib20_virtual_plugin.la: $(lib20_virtual_plugin_la_OBJECTS) $(lib20_virtual_plugin_la_DEPENDENCIES) $(EXTRA_lib20_virtual_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_virtual_plugin_la_LINK) -rpath $(moduledir) $(lib20_virtual_plugin_la_OBJECTS) $(lib20_virtual_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-config.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/virtual-transaction.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/virtual-config.Plo
+ -rm -f ./$(DEPDIR)/virtual-mail.Plo
+ -rm -f ./$(DEPDIR)/virtual-plugin.Plo
+ -rm -f ./$(DEPDIR)/virtual-save.Plo
+ -rm -f ./$(DEPDIR)/virtual-search.Plo
+ -rm -f ./$(DEPDIR)/virtual-storage.Plo
+ -rm -f ./$(DEPDIR)/virtual-sync.Plo
+ -rm -f ./$(DEPDIR)/virtual-transaction.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-moduleLTLIBRARIES
+
+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)/virtual-config.Plo
+ -rm -f ./$(DEPDIR)/virtual-mail.Plo
+ -rm -f ./$(DEPDIR)/virtual-plugin.Plo
+ -rm -f ./$(DEPDIR)/virtual-save.Plo
+ -rm -f ./$(DEPDIR)/virtual-search.Plo
+ -rm -f ./$(DEPDIR)/virtual-storage.Plo
+ -rm -f ./$(DEPDIR)/virtual-sync.Plo
+ -rm -f ./$(DEPDIR)/virtual-transaction.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/virtual/virtual-config.c b/src/plugins/virtual/virtual-config.c
new file mode 100644
index 0000000..05a6305
--- /dev/null
+++ b/src/plugins/virtual/virtual-config.c
@@ -0,0 +1,567 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "crc32.h"
+#include "istream.h"
+#include "str.h"
+#include "unichar.h"
+#include "wildcard-match.h"
+#include "imap-parser.h"
+#include "imap-match.h"
+#include "mail-namespace.h"
+#include "mail-search-build.h"
+#include "mail-search-parser.h"
+#include "mailbox-attribute.h"
+#include "mailbox-list-iter.h"
+#include "imap-metadata.h"
+#include "virtual-storage.h"
+#include "virtual-plugin.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+struct virtual_parse_context {
+ struct virtual_mailbox *mbox;
+ struct istream *input;
+
+ pool_t pool;
+ string_t *rule;
+ unsigned int rule_idx;
+
+ char sep;
+ bool have_wildcards;
+ bool have_mailbox_defines;
+};
+
+static struct mail_search_args *
+virtual_search_args_parse(const string_t *rule, const char **error_r)
+{
+ struct istream *input;
+ struct imap_parser *imap_parser;
+ const struct imap_arg *args;
+ struct mail_search_parser *parser;
+ struct mail_search_args *sargs;
+ const char *charset = "UTF-8";
+ int ret;
+
+ if (str_len(rule) == 0) {
+ sargs = mail_search_build_init();
+ mail_search_build_add_all(sargs);
+ return sargs;
+ }
+
+ input = i_stream_create_from_data(str_data(rule), str_len(rule));
+ (void)i_stream_read(input);
+
+ imap_parser = imap_parser_create(input, NULL, SIZE_MAX);
+ ret = imap_parser_finish_line(imap_parser, 0, 0, &args);
+ if (ret < 0) {
+ sargs = NULL;
+ *error_r = t_strdup(imap_parser_get_error(imap_parser, NULL));
+ } else {
+ parser = mail_search_parser_init_imap(args);
+ if (mail_search_build(mail_search_register_get_imap(),
+ parser, &charset, &sargs, error_r) < 0)
+ sargs = NULL;
+ mail_search_parser_deinit(&parser);
+ }
+
+ imap_parser_unref(&imap_parser);
+ i_stream_destroy(&input);
+ return sargs;
+}
+
+static int
+virtual_config_add_rule(struct virtual_parse_context *ctx, const char **error_r)
+{
+ struct virtual_backend_box *const *bboxes;
+ struct mail_search_args *search_args;
+ unsigned int i, count;
+
+ *error_r = NULL;
+
+ if (ctx->rule_idx == array_count(&ctx->mbox->backend_boxes)) {
+ i_assert(str_len(ctx->rule) == 0);
+ return 0;
+ }
+
+ ctx->mbox->search_args_crc32 =
+ crc32_str_more(ctx->mbox->search_args_crc32, str_c(ctx->rule));
+ search_args = virtual_search_args_parse(ctx->rule, error_r);
+ str_truncate(ctx->rule, 0);
+ if (search_args == NULL) {
+ i_assert(*error_r != NULL);
+ *error_r = t_strconcat("Previous search rule is invalid: ",
+ *error_r, NULL);
+ return -1;
+ }
+
+ /* update at all the mailboxes that were introduced since the previous
+ rule. */
+ bboxes = array_get(&ctx->mbox->backend_boxes, &count);
+ i_assert(ctx->rule_idx < count);
+ for (i = ctx->rule_idx; i < count; i++) {
+ i_assert(bboxes[i]->search_args == NULL);
+ mail_search_args_ref(search_args);
+ bboxes[i]->search_args = search_args;
+ }
+ mail_search_args_unref(&search_args);
+
+ ctx->rule_idx = array_count(&ctx->mbox->backend_boxes);
+ return 0;
+}
+
+static int
+virtual_config_parse_line(struct virtual_parse_context *ctx, const char *line,
+ const char **error_r)
+{
+ struct mail_user *user = ctx->mbox->storage->storage.user;
+ struct virtual_backend_box *bbox;
+ const char *p;
+ bool no_wildcards = FALSE;
+
+ if (*line == ' ' || *line == '\t') {
+ /* continues the previous search rule */
+ if (ctx->rule_idx == array_count(&ctx->mbox->backend_boxes)) {
+ *error_r = "Search rule without a mailbox";
+ return -1;
+ }
+ while (*line == ' ' || *line == '\t') line++;
+ str_append_c(ctx->rule, ' ');
+ str_append(ctx->rule, line);
+ return 0;
+ }
+ /* if there is no rule yet, it means we want the previous mailboxes
+ to use the rule that comes later */
+ if (str_len(ctx->rule) > 0) {
+ if (virtual_config_add_rule(ctx, error_r) < 0)
+ return -1;
+ }
+ if (!uni_utf8_str_is_valid(line)) {
+ *error_r = t_strdup_printf("Mailbox name not UTF-8: %s",
+ line);
+ return -1;
+ }
+
+ /* new mailbox. the search args are added to it later. */
+ bbox = p_new(ctx->pool, struct virtual_backend_box, 1);
+ bbox->virtual_mbox = ctx->mbox;
+ if (strcasecmp(line, "INBOX") == 0)
+ line = "INBOX";
+ bbox->name = p_strdup(ctx->pool, line);
+ switch (bbox->name[0]) {
+ case '+':
+ bbox->name++;
+ bbox->clear_recent = TRUE;
+ break;
+ case '-':
+ bbox->name++;
+ bbox->negative_match = TRUE;
+ break;
+ case '!':
+ /* save messages here */
+ if (ctx->mbox->save_bbox != NULL) {
+ *error_r = "Multiple save mailboxes defined";
+ return -1;
+ }
+ bbox->name++;
+ ctx->mbox->save_bbox = bbox;
+ no_wildcards = TRUE;
+ break;
+ }
+ if (bbox->name[0] == '/') {
+ /* [+-!]/metadata entry:value */
+ if ((p = strchr(bbox->name, ':')) == NULL) {
+ *error_r = "':' separator missing between metadata entry name and value";
+ return -1;
+ }
+ bbox->metadata_entry = p_strdup_until(ctx->pool, bbox->name, p++);
+ bbox->metadata_value = p;
+ if (!imap_metadata_verify_entry_name(bbox->metadata_entry, error_r))
+ return -1;
+ no_wildcards = TRUE;
+ }
+
+ if (!no_wildcards &&
+ (strchr(bbox->name, '*') != NULL ||
+ strchr(bbox->name, '%') != NULL)) {
+ bbox->glob = imap_match_init(ctx->pool, bbox->name, TRUE, ctx->sep);
+ ctx->have_wildcards = TRUE;
+ }
+ if (bbox->metadata_entry == NULL) {
+ /* now that the prefix characters have been processed,
+ find the namespace */
+ bbox->ns = strcasecmp(bbox->name, "INBOX") == 0 ?
+ mail_namespace_find_inbox(user->namespaces) :
+ mail_namespace_find(user->namespaces, bbox->name);
+ if (bbox->ns == NULL) {
+ *error_r = t_strdup_printf("Namespace not found for %s",
+ bbox->name);
+ return -1;
+ }
+ if (strcmp(bbox->name, ctx->mbox->box.vname) == 0) {
+ *error_r = "Virtual mailbox can't point to itself";
+ return -1;
+ }
+ ctx->have_mailbox_defines = TRUE;
+ }
+
+ array_push_back(&ctx->mbox->backend_boxes, &bbox);
+ return 0;
+}
+
+static void
+virtual_mailbox_get_list_patterns(struct virtual_parse_context *ctx)
+{
+ struct virtual_mailbox *mbox = ctx->mbox;
+ ARRAY_TYPE(mailbox_virtual_patterns) *dest;
+ struct mailbox_virtual_pattern pattern;
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ i_zero(&pattern);
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ p_array_init(&mbox->list_include_patterns, ctx->pool, count);
+ p_array_init(&mbox->list_exclude_patterns, ctx->pool, count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->metadata_entry == NULL)
+ continue;
+ pattern.ns = bboxes[i]->ns;
+ pattern.pattern = bboxes[i]->name;
+ if (bboxes[i]->negative_match)
+ dest = &mbox->list_include_patterns;
+ else {
+ dest = &mbox->list_exclude_patterns;
+ pattern.pattern++;
+ }
+ array_push_back(dest, &pattern);
+ }
+}
+
+static void
+separate_wildcard_mailboxes(struct virtual_mailbox *mbox,
+ ARRAY_TYPE(virtual_backend_box) *wildcard_boxes,
+ ARRAY_TYPE(virtual_backend_box) *neg_boxes,
+ ARRAY_TYPE(virtual_backend_box) *metadata_boxes)
+{
+ struct virtual_backend_box *const *bboxes;
+ ARRAY_TYPE(virtual_backend_box) *dest;
+ unsigned int i, count;
+
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ t_array_init(wildcard_boxes, I_MIN(16, count));
+ t_array_init(neg_boxes, 4);
+ t_array_init(metadata_boxes, 4);
+ for (i = 0; i < count;) {
+ if (bboxes[i]->metadata_entry != NULL)
+ dest = metadata_boxes;
+ else if (bboxes[i]->negative_match)
+ dest = neg_boxes;
+ else if (bboxes[i]->glob != NULL)
+ dest = wildcard_boxes;
+ else {
+ dest = NULL;
+ i++;
+ }
+
+ if (dest != NULL) {
+ array_push_back(dest, &bboxes[i]);
+ array_delete(&mbox->backend_boxes, i, 1);
+ bboxes = array_get_modifiable(&mbox->backend_boxes,
+ &count);
+ }
+ }
+}
+
+static void virtual_config_copy_expanded(struct virtual_parse_context *ctx,
+ struct virtual_backend_box *wbox,
+ const char *name)
+{
+ struct virtual_backend_box *bbox;
+
+ bbox = p_new(ctx->pool, struct virtual_backend_box, 1);
+ *bbox = *wbox;
+ bbox->name = p_strdup(ctx->pool, name);
+ bbox->glob = NULL;
+ bbox->wildcard = TRUE;
+ mail_search_args_ref(bbox->search_args);
+ array_push_back(&ctx->mbox->backend_boxes, &bbox);
+}
+
+static bool virtual_ns_match(struct mail_namespace *config_ns,
+ struct mail_namespace *iter_ns)
+{
+ /* we match only one namespace for each pattern, except with shared
+ namespaces match also autocreated children */
+ if (config_ns == iter_ns)
+ return TRUE;
+ if (config_ns->type == iter_ns->type &&
+ (config_ns->flags & NAMESPACE_FLAG_AUTOCREATED) == 0 &&
+ (iter_ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0)
+ return TRUE;
+ if ((iter_ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ (config_ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0 &&
+ config_ns->prefix_len == 0) {
+ /* prefix="" namespace was autocreated, so e.g. "*" would match
+ only that empty namespace. but we want "*" to also match
+ the inbox=yes namespace, so check it here separately. */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool virtual_config_match(const struct mailbox_info *info,
+ ARRAY_TYPE(virtual_backend_box) *boxes_arr,
+ unsigned int *idx_r)
+{
+ struct virtual_backend_box *const *boxes;
+ unsigned int i, count;
+
+ boxes = array_get_modifiable(boxes_arr, &count);
+ for (i = 0; i < count; i++) {
+ if (boxes[i]->glob != NULL) {
+ if (virtual_ns_match(boxes[i]->ns, info->ns) &&
+ imap_match(boxes[i]->glob,
+ info->vname) == IMAP_MATCH_YES) {
+ *idx_r = i;
+ return TRUE;
+ }
+ } else {
+ if (strcmp(boxes[i]->name, info->vname) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static int virtual_config_box_metadata_match(struct mailbox *box,
+ struct virtual_backend_box *bbox,
+ const char **error_r)
+{
+ struct imap_metadata_transaction *imtrans;
+ struct mail_attribute_value value;
+ int ret;
+
+ imtrans = imap_metadata_transaction_begin(box);
+ ret = imap_metadata_get(imtrans, bbox->metadata_entry, &value);
+ if (ret < 0)
+ *error_r = t_strdup(imap_metadata_transaction_get_last_error(imtrans, NULL));
+ if (ret > 0)
+ ret = wildcard_match(value.value, bbox->metadata_value) ? 1 : 0;
+ if (ret >= 0 && bbox->negative_match)
+ ret = ret > 0 ? 0 : 1;
+ (void)imap_metadata_transaction_commit(&imtrans, NULL, NULL);
+ return ret;
+}
+
+static int
+virtual_config_metadata_match(const struct mailbox_info *info,
+ ARRAY_TYPE(virtual_backend_box) *boxes_arr,
+ const char **error_r)
+{
+ struct virtual_backend_box *const *boxes;
+ struct mailbox *box;
+ unsigned int i, count;
+ int ret = 1;
+
+ boxes = array_get_modifiable(boxes_arr, &count);
+ if (count == 0)
+ return 1;
+
+ box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY);
+ for (i = 0; i < count; i++) {
+ /* break on error or match */
+ if ((ret = virtual_config_box_metadata_match(box, boxes[i], error_r)) < 0 || ret > 0)
+ break;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int virtual_config_expand_wildcards(struct virtual_parse_context *ctx,
+ const char **error_r)
+{
+ const enum mail_namespace_type iter_ns_types =
+ MAIL_NAMESPACE_TYPE_MASK_ALL;
+ const enum mailbox_list_iter_flags iter_flags =
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct mail_user *user = ctx->mbox->storage->storage.user;
+ ARRAY_TYPE(virtual_backend_box) wildcard_boxes, neg_boxes, metadata_boxes;
+ struct mailbox_list_iterate_context *iter;
+ struct virtual_backend_box *const *wboxes, *const *boxp;
+ const char **patterns;
+ const struct mailbox_info *info;
+ unsigned int i, j, count;
+ int ret = 0;
+
+ separate_wildcard_mailboxes(ctx->mbox, &wildcard_boxes,
+ &neg_boxes, &metadata_boxes);
+
+ /* get patterns we want to list */
+ wboxes = array_get_modifiable(&wildcard_boxes, &count);
+ if (count == 0) {
+ /* only negative wildcards - doesn't really make sense.
+ just ignore. */
+ return 0;
+ }
+ patterns = t_new(const char *, count + 1);
+ for (i = 0; i < count; i++)
+ patterns[i] = wboxes[i]->name;
+
+ /* match listed mailboxes to wildcards */
+ iter = mailbox_list_iter_init_namespaces(user->namespaces, patterns,
+ iter_ns_types, iter_flags);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ /* skip non-selectable mailboxes (especially mbox
+ directories) */
+ if ((info->flags & MAILBOX_NOSELECT) != 0)
+ continue;
+ if (strcmp(info->vname, ctx->mbox->box.vname) == 0) {
+ /* don't allow virtual folder to point to itself */
+ continue;
+ }
+
+ if (virtual_config_match(info, &wildcard_boxes, &i) &&
+ !virtual_config_match(info, &neg_boxes, &j) &&
+ virtual_backend_box_lookup_name(ctx->mbox,
+ info->vname) == NULL) {
+ ret = virtual_config_metadata_match(info, &metadata_boxes, error_r);
+ if (ret < 0)
+ break;
+ if (ret > 0) {
+ virtual_config_copy_expanded(ctx, wboxes[i],
+ info->vname);
+ }
+ }
+ }
+ for (i = 0; i < count; i++)
+ mail_search_args_unref(&wboxes[i]->search_args);
+ array_foreach(&neg_boxes, boxp)
+ mail_search_args_unref(&(*boxp)->search_args);
+ array_foreach(&metadata_boxes, boxp)
+ mail_search_args_unref(&(*boxp)->search_args);
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ *error_r = mailbox_list_get_last_internal_error(user->namespaces->list, NULL);
+ return -1;
+ }
+ return ret < 0 ? -1 : 0;
+}
+
+static void virtual_config_search_args_dup(struct virtual_mailbox *mbox)
+{
+ struct virtual_backend_box *const *bboxes;
+ struct mail_search_args *old_args;
+ unsigned int i, count;
+
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ old_args = bboxes[i]->search_args;
+ bboxes[i]->search_args = mail_search_args_dup(old_args);
+ mail_search_args_unref(&old_args);
+ }
+}
+
+int virtual_config_read(struct virtual_mailbox *mbox)
+{
+ struct mail_storage *storage = mbox->box.storage;
+ struct virtual_parse_context ctx;
+ const char *box_path, *path, *line, *error;
+ unsigned int linenum = 0;
+ int fd, ret = 0;
+
+ i_array_init(&mbox->backend_boxes, 8);
+ mbox->search_args_crc32 = (uint32_t)-1;
+
+ box_path = mailbox_get_path(&mbox->box);
+ path = t_strconcat(box_path, "/"VIRTUAL_CONFIG_FNAME, NULL);
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == EACCES) {
+ mailbox_set_critical(&mbox->box, "%s",
+ mail_error_eacces_msg("open", path));
+ } else if (errno != ENOENT) {
+ mailbox_set_critical(&mbox->box,
+ "open(%s) failed: %m", path);
+ } else if (errno == ENOENT) {
+ mail_storage_set_error(storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(mbox->box.vname));
+ } else {
+ mailbox_set_critical(&mbox->box,
+ "stat(%s) failed: %m", box_path);
+ }
+ return -1;
+ }
+
+ i_zero(&ctx);
+ ctx.sep = mail_namespaces_get_root_sep(storage->user->namespaces);
+ ctx.mbox = mbox;
+ ctx.pool = mbox->box.pool;
+ ctx.rule = t_str_new(256);
+ ctx.input = i_stream_create_fd(fd, SIZE_MAX);
+ i_stream_set_return_partial_line(ctx.input, TRUE);
+ while ((line = i_stream_read_next_line(ctx.input)) != NULL) {
+ linenum++;
+ if (*line == '#')
+ continue;
+ if (*line == '\0')
+ ret = virtual_config_add_rule(&ctx, &error);
+ else
+ ret = virtual_config_parse_line(&ctx, line, &error);
+ if (ret < 0) {
+ mailbox_set_critical(&mbox->box,
+ "%s: Error at line %u: %s",
+ path, linenum, error);
+ break;
+ }
+ }
+ if (ret == 0) {
+ ret = virtual_config_add_rule(&ctx, &error);
+ if (ret < 0) {
+ mailbox_set_critical(&mbox->box,
+ "%s: Error at line %u: %s",
+ path, linenum, error);
+ }
+ }
+
+ virtual_mailbox_get_list_patterns(&ctx);
+ if (ret == 0 && ctx.have_wildcards) {
+ struct event_reason *reason =
+ event_reason_begin("virtual:config_read");
+ ret = virtual_config_expand_wildcards(&ctx, &error);
+ if (ret < 0)
+ mailbox_set_critical(&mbox->box, "%s: %s", path, error);
+ event_reason_end(&reason);
+ }
+
+ if (ret == 0 && !ctx.have_mailbox_defines) {
+ mailbox_set_critical(&mbox->box,
+ "%s: No mailboxes defined", path);
+ ret = -1;
+ }
+ if (ret == 0)
+ virtual_config_search_args_dup(mbox);
+ i_stream_unref(&ctx.input);
+ i_close_fd(&fd);
+ return ret;
+}
+
+void virtual_config_free(struct virtual_mailbox *mbox)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ if (!array_is_created(&mbox->backend_boxes)) {
+ /* mailbox wasn't opened */
+ return;
+ }
+
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->search_args != NULL)
+ mail_search_args_unref(&bboxes[i]->search_args);
+ }
+ array_free(&mbox->backend_boxes);
+}
diff --git a/src/plugins/virtual/virtual-mail.c b/src/plugins/virtual/virtual-mail.c
new file mode 100644
index 0000000..fd8303f
--- /dev/null
+++ b/src/plugins/virtual/virtual-mail.c
@@ -0,0 +1,583 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "index-mail.h"
+#include "virtual-storage.h"
+#include "virtual-transaction.h"
+
+struct virtual_mail {
+ struct index_mail imail;
+
+ enum mail_fetch_field wanted_fields;
+ struct mailbox_header_lookup_ctx *wanted_headers;
+
+ /* temp_wanted_fields for this mail. Used only when mail doesn't have
+ a backend mail yet. */
+ enum mail_fetch_field delayed_temp_fields;
+ struct mailbox_header_lookup_ctx *delayed_temp_headers;
+
+ /* currently active mail */
+ struct mail *cur_backend_mail;
+ struct virtual_mail_index_record cur_vrec;
+
+ /* all allocated mails */
+ ARRAY(struct mail *) backend_mails;
+
+ /* mail is lost if backend_mail doesn't point to correct mail */
+ bool cur_lost:1;
+};
+
+struct mail *
+virtual_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)t->box;
+ struct virtual_mail *vmail;
+ pool_t mail_pool, data_pool;
+
+ mail_pool = pool_alloconly_create("vmail", 1024);
+ data_pool = pool_alloconly_create("virtual index_mail", 512);
+ vmail = p_new(mail_pool, struct virtual_mail, 1);
+ vmail->wanted_fields = wanted_fields;
+ vmail->wanted_headers = wanted_headers;
+ if (vmail->wanted_headers != NULL)
+ mailbox_header_lookup_ref(vmail->wanted_headers);
+ /* Do not pass wanted_fields or wanted_headers to index_mail_init.
+ It will just cause unwanted behaviour, as we only want these
+ to be passed to backend mails. */
+ index_mail_init(&vmail->imail, t, 0, NULL, mail_pool, data_pool);
+ vmail->imail.mail.v = virtual_mail_vfuncs;
+ i_array_init(&vmail->backend_mails, array_count(&mbox->backend_boxes));
+ return &vmail->imail.mail.mail;
+}
+
+static void virtual_mail_close(struct mail *mail)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail **mails;
+ unsigned int i, count;
+
+ if (mail->seq != 0) {
+ mailbox_header_lookup_unref(&vmail->delayed_temp_headers);
+ vmail->delayed_temp_fields = 0;
+ }
+
+ mails = array_get_modifiable(&vmail->backend_mails, &count);
+ for (i = 0; i < count; i++) {
+ struct mail_private *p = (struct mail_private *)mails[i];
+
+ if (vmail->imail.freeing)
+ mail_free(&mails[i]);
+ else
+ p->v.close(mails[i]);
+ }
+ if (vmail->imail.freeing) {
+ array_free(&vmail->backend_mails);
+ mailbox_header_lookup_unref(&vmail->wanted_headers);
+ }
+ index_mail_close(mail);
+}
+
+static struct mail *
+backend_mail_find(struct virtual_mail *vmail, struct mailbox *box)
+{
+ struct mail *const *mails;
+ unsigned int i, count;
+
+ mails = array_get(&vmail->backend_mails, &count);
+ for (i = 0; i < count; i++) {
+ if (mails[i]->box == box)
+ return mails[i];
+ }
+ return NULL;
+}
+
+static int backend_mail_get(struct virtual_mail *vmail,
+ struct mail **backend_mail_r)
+{
+ struct mail *mail = &vmail->imail.mail.mail;
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)mail->box;
+ struct virtual_backend_box *bbox;
+
+ *backend_mail_r = NULL;
+
+ if (vmail->cur_backend_mail != NULL) {
+ if (vmail->cur_lost) {
+ mail_set_expunged(&vmail->imail.mail.mail);
+ return -1;
+ }
+ *backend_mail_r = vmail->cur_backend_mail;
+ return 0;
+ }
+
+ if (!virtual_backend_box_lookup(mbox, vmail->cur_vrec.mailbox_id, &bbox))
+ i_unreached();
+
+ vmail->cur_backend_mail = backend_mail_find(vmail, bbox->box);
+ if (vmail->cur_backend_mail == NULL) {
+ if (!bbox->box->opened &&
+ virtual_backend_box_open(mbox, bbox) < 0) {
+ virtual_box_copy_error(mail->box, bbox->box);
+ return -1;
+ }
+ (void)virtual_mail_set_backend_mail(mail, bbox);
+ i_assert(vmail->cur_backend_mail != NULL);
+ }
+ virtual_backend_box_accessed(mbox, bbox);
+ vmail->cur_lost = !mail_set_uid(vmail->cur_backend_mail,
+ vmail->cur_vrec.real_uid);
+ mail->expunged = vmail->cur_lost || vmail->cur_backend_mail->expunged;
+ if (vmail->cur_lost) {
+ mail_set_expunged(&vmail->imail.mail.mail);
+ return -1;
+ }
+ /* headers need to be converted to backend-headers, so go through
+ the virtual add_temp_wanted_fields() again. */
+ mail_add_temp_wanted_fields(mail, vmail->delayed_temp_fields,
+ vmail->delayed_temp_headers);
+ *backend_mail_r = vmail->cur_backend_mail;
+ return 0;
+}
+
+struct mail *
+virtual_mail_set_backend_mail(struct mail *mail,
+ struct virtual_backend_box *bbox)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail_private *backend_pmail;
+ struct mailbox_transaction_context *backend_trans;
+ struct mailbox_header_lookup_ctx *backend_headers;
+
+ i_assert(bbox->box->opened);
+
+ backend_trans = virtual_transaction_get(mail->transaction, bbox->box);
+
+ backend_headers = vmail->wanted_headers == NULL ? NULL :
+ mailbox_header_lookup_init(bbox->box,
+ vmail->wanted_headers->name);
+ vmail->cur_backend_mail =
+ mail_alloc(backend_trans, vmail->wanted_fields, backend_headers);
+ mailbox_header_lookup_unref(&backend_headers);
+
+ backend_pmail = (struct mail_private *)vmail->cur_backend_mail;
+ backend_pmail->vmail = mail;
+ array_push_back(&vmail->backend_mails, &vmail->cur_backend_mail);
+ return vmail->cur_backend_mail;
+}
+
+void virtual_mail_set_unattached_backend_mail(struct mail *mail,
+ struct mail *backend_mail)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail_private *backend_pmail;
+
+ vmail->cur_backend_mail = backend_mail;
+
+ backend_pmail = (struct mail_private *)backend_mail;
+ backend_pmail->vmail = mail;
+}
+
+static void virtual_mail_set_seq(struct mail *mail, uint32_t seq, bool saving)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)mail->box;
+ const void *data;
+
+ i_assert(!saving);
+
+ mail_index_lookup_ext(mail->transaction->view, seq,
+ mbox->virtual_ext_id, &data, NULL);
+ memcpy(&vmail->cur_vrec, data, sizeof(vmail->cur_vrec));
+
+ index_mail_set_seq(mail, seq, saving);
+
+ vmail->cur_backend_mail = NULL;
+}
+
+static bool virtual_mail_set_uid(struct mail *mail, uint32_t uid)
+{
+ uint32_t seq;
+
+ if (!mail_index_lookup_seq(mail->transaction->view, uid, &seq))
+ return FALSE;
+
+ virtual_mail_set_seq(mail, seq, FALSE);
+ return TRUE;
+}
+
+static void virtual_mail_set_uid_cache_updates(struct mail *mail, bool set)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mail_private *p;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return;
+ p = (struct mail_private *)backend_mail;
+ p->v.set_uid_cache_updates(backend_mail, set);
+}
+
+static bool virtual_mail_prefetch(struct mail *mail)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mail_private *p;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return TRUE;
+ p = (struct mail_private *)backend_mail;
+ return p->v.prefetch(backend_mail);
+}
+
+static int virtual_mail_precache(struct mail *mail)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mail_private *p;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ p = (struct mail_private *)backend_mail;
+ return p->v.precache(backend_mail);
+}
+
+static void
+virtual_mail_add_temp_wanted_fields(struct mail *mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mailbox_header_lookup_ctx *backend_headers, *new_headers;
+
+ if (mail->seq == 0) {
+ /* No mail set yet. Delay until it is set. */
+ vmail->delayed_temp_fields |= fields;
+ if (vmail->delayed_temp_headers == NULL)
+ vmail->delayed_temp_headers = headers;
+ else {
+ new_headers = mailbox_header_lookup_merge(
+ vmail->delayed_temp_headers, headers);
+ mailbox_header_lookup_unref(&vmail->delayed_temp_headers);
+ vmail->delayed_temp_headers = new_headers;
+ }
+ return;
+ }
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return;
+ /* convert header indexes to backend mailbox's header indexes */
+ backend_headers = headers == NULL ? NULL :
+ mailbox_header_lookup_init(backend_mail->box, headers->name);
+ mail_add_temp_wanted_fields(backend_mail, fields, backend_headers);
+ mailbox_header_lookup_unref(&backend_headers);
+}
+
+static int
+virtual_mail_get_parts(struct mail *mail, struct message_part **parts_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_parts(backend_mail, parts_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_mail_get_date(struct mail *mail, time_t *date_r, int *timezone_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ int tz;
+
+ if (timezone_r == NULL)
+ timezone_r = &tz;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_date(backend_mail, date_r, timezone_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int virtual_mail_get_received_date(struct mail *mail, time_t *date_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_received_date(backend_mail, date_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int virtual_mail_get_save_date(struct mail *mail, time_t *date_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ int ret;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ ret = mail_get_save_date(backend_mail, date_r);
+ if (ret < 0)
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return ret;
+}
+
+static int virtual_mail_get_virtual_mail_size(struct mail *mail, uoff_t *size_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_virtual_size(backend_mail, size_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int virtual_mail_get_physical_size(struct mail *mail, uoff_t *size_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_physical_size(backend_mail, size_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_mail_get_first_header(struct mail *mail, const char *field,
+ bool decode_to_utf8, const char **value_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mail_private *p;
+ int ret;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ p = (struct mail_private *)backend_mail;
+ ret = p->v.get_first_header(backend_mail, field,
+ decode_to_utf8, value_r);
+ if (ret < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return ret;
+}
+
+static int
+virtual_mail_get_headers(struct mail *mail, const char *field,
+ bool decode_to_utf8, const char *const **value_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mail_private *p;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ p = (struct mail_private *)backend_mail;
+ if (p->v.get_headers(backend_mail, field, decode_to_utf8, value_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_mail_get_header_stream(struct mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+ struct mailbox_header_lookup_ctx *backend_headers;
+ int ret;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+
+ backend_headers = mailbox_header_lookup_init(backend_mail->box,
+ headers->name);
+ ret = mail_get_header_stream(backend_mail, backend_headers, stream_r);
+ mailbox_header_lookup_unref(&backend_headers);
+ if (ret < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_mail_get_stream(struct mail *mail, bool get_body,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail_private *vp = (struct mail_private *)mail;
+ struct mail *backend_mail;
+ const char *reason = t_strdup_printf("virtual mailbox %s: Opened mail UID=%u: %s",
+ mailbox_get_vname(mail->box), mail->uid, vp->get_stream_reason);
+ int ret;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+
+ if (get_body) {
+ ret = mail_get_stream_because(backend_mail, hdr_size, body_size,
+ reason, stream_r);
+ } else {
+ ret = mail_get_hdr_stream_because(backend_mail, hdr_size,
+ reason, stream_r);
+ }
+
+ if (ret < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_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 virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+
+ struct mail_private *p = (struct mail_private *)backend_mail;
+ if (p->v.get_binary_stream(backend_mail, part, include_hdr,
+ size_r, lines_r, binary_r, stream_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+virtual_mail_get_special(struct mail *mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+ if (mail_get_special(backend_mail, field, value_r) < 0) {
+ virtual_box_copy_error(mail->box, backend_mail->box);
+ return -1;
+ }
+ return 0;
+}
+
+static int virtual_mail_get_backend_mail(struct mail *mail,
+ struct mail **real_mail_r)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return -1;
+
+ if (mail_get_backend_mail(backend_mail, real_mail_r) < 0)
+ return -1;
+ return 0;
+}
+
+static void virtual_mail_update_pop3_uidl(struct mail *mail, const char *uidl)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return;
+ mail_update_pop3_uidl(backend_mail, uidl);
+}
+
+static void virtual_mail_expunge(struct mail *mail)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return;
+ mail_expunge(backend_mail);
+}
+
+static void
+virtual_mail_set_cache_corrupted(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason)
+{
+ struct virtual_mail *vmail = (struct virtual_mail *)mail;
+ struct mail *backend_mail;
+
+ if (backend_mail_get(vmail, &backend_mail) < 0)
+ return;
+ mail_set_cache_corrupted(backend_mail, field, reason);
+}
+
+struct mail_vfuncs virtual_mail_vfuncs = {
+ virtual_mail_close,
+ index_mail_free,
+ virtual_mail_set_seq,
+ virtual_mail_set_uid,
+ virtual_mail_set_uid_cache_updates,
+ virtual_mail_prefetch,
+ virtual_mail_precache,
+ virtual_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,
+ virtual_mail_get_parts,
+ virtual_mail_get_date,
+ virtual_mail_get_received_date,
+ virtual_mail_get_save_date,
+ virtual_mail_get_virtual_mail_size,
+ virtual_mail_get_physical_size,
+ virtual_mail_get_first_header,
+ virtual_mail_get_headers,
+ virtual_mail_get_header_stream,
+ virtual_mail_get_stream,
+ virtual_mail_get_binary_stream,
+ virtual_mail_get_special,
+ virtual_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ virtual_mail_update_pop3_uidl,
+ virtual_mail_expunge,
+ virtual_mail_set_cache_corrupted,
+ NULL,
+};
diff --git a/src/plugins/virtual/virtual-plugin.c b/src/plugins/virtual/virtual-plugin.c
new file mode 100644
index 0000000..43db7cb
--- /dev/null
+++ b/src/plugins/virtual/virtual-plugin.c
@@ -0,0 +1,25 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-namespace.h"
+#include "virtual-storage.h"
+#include "virtual-plugin.h"
+
+const char *virtual_plugin_version = DOVECOT_ABI_VERSION;
+
+static struct mail_storage_hooks acl_mail_storage_hooks = {
+ .mailbox_allocated = virtual_backend_mailbox_allocated,
+ .mailbox_opened = virtual_backend_mailbox_opened
+};
+
+void virtual_plugin_init(struct module *module ATTR_UNUSED)
+{
+ mail_storage_class_register(&virtual_storage);
+ mail_storage_hooks_add(module, &acl_mail_storage_hooks);
+}
+
+void virtual_plugin_deinit(void)
+{
+ mail_storage_class_unregister(&virtual_storage);
+ mail_storage_hooks_remove(&acl_mail_storage_hooks);
+}
diff --git a/src/plugins/virtual/virtual-plugin.h b/src/plugins/virtual/virtual-plugin.h
new file mode 100644
index 0000000..39f04bb
--- /dev/null
+++ b/src/plugins/virtual/virtual-plugin.h
@@ -0,0 +1,7 @@
+#ifndef VIRTUAL_PLUGIN_H
+#define VIRTUAL_PLUGIN_H
+
+void virtual_plugin_init(struct module *module);
+void virtual_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/virtual/virtual-save.c b/src/plugins/virtual/virtual-save.c
new file mode 100644
index 0000000..9499cd3
--- /dev/null
+++ b/src/plugins/virtual/virtual-save.c
@@ -0,0 +1,153 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "virtual-transaction.h"
+#include "virtual-storage.h"
+
+struct virtual_save_context {
+ struct mail_save_context ctx;
+ struct mail_save_context *backend_save_ctx;
+ struct mailbox *backend_box;
+ char *open_errstr;
+ enum mail_error open_error;
+};
+
+struct mail_save_context *
+virtual_save_alloc(struct mailbox_transaction_context *_t)
+{
+ struct virtual_transaction_context *t =
+ (struct virtual_transaction_context *)_t;
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)_t->box;
+ struct mailbox_transaction_context *backend_trans;
+ struct virtual_save_context *ctx;
+ const char *errstr;
+
+ if (_t->save_ctx == NULL) {
+ ctx = i_new(struct virtual_save_context, 1);
+ ctx->ctx.transaction = &t->t;
+ _t->save_ctx = &ctx->ctx;
+ } else {
+ ctx = (struct virtual_save_context *)_t->save_ctx;
+ }
+
+ if (mbox->save_bbox != NULL) {
+ i_assert(ctx->backend_save_ctx == NULL);
+ i_assert(ctx->open_errstr == NULL);
+
+ if (!mbox->save_bbox->box->opened &&
+ virtual_backend_box_open(mbox, mbox->save_bbox) < 0) {
+ errstr = mailbox_get_last_error(mbox->save_bbox->box,
+ &ctx->open_error);
+ ctx->open_errstr = i_strdup(errstr);
+ } else {
+ backend_trans =
+ virtual_transaction_get(_t, mbox->save_bbox->box);
+ ctx->backend_save_ctx = mailbox_save_alloc(backend_trans);
+ }
+ virtual_backend_box_accessed(mbox, mbox->save_bbox);
+ }
+ return _t->save_ctx;
+}
+
+static struct mail_keywords *
+virtual_copy_keywords(struct mailbox *src_box,
+ const struct mail_keywords *src_keywords,
+ struct mailbox *dest_box)
+{
+ struct mailbox_status status;
+ ARRAY_TYPE(keywords) kw_strings;
+ const char *kw;
+ unsigned int i;
+
+ if (src_keywords == NULL || src_keywords->count == 0)
+ return NULL;
+
+ t_array_init(&kw_strings, src_keywords->count + 1);
+ mailbox_get_open_status(src_box, STATUS_KEYWORDS, &status);
+
+ for (i = 0; i < src_keywords->count; i++) {
+ kw = array_idx_elem(status.keywords, src_keywords->idx[i]);
+ array_push_back(&kw_strings, &kw);
+ }
+ array_append_zero(&kw_strings);
+ return mailbox_keywords_create_valid(dest_box,
+ array_front(&kw_strings));
+}
+
+int virtual_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct virtual_save_context *ctx = (struct virtual_save_context *)_ctx;
+ struct mail_save_data *mdata = &_ctx->data;
+ struct mail_keywords *keywords;
+
+ if (ctx->backend_save_ctx == NULL) {
+ if (ctx->open_errstr != NULL) {
+ /* mailbox_open() failed */
+ mail_storage_set_error(_ctx->transaction->box->storage,
+ ctx->open_error, ctx->open_errstr);
+ } else {
+ mail_storage_set_error(_ctx->transaction->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Can't save messages to this virtual mailbox");
+ }
+ return -1;
+ }
+
+ ctx->backend_box = ctx->backend_save_ctx->transaction->box;
+ keywords = virtual_copy_keywords(_ctx->transaction->box, mdata->keywords,
+ ctx->backend_box);
+ mailbox_save_set_flags(ctx->backend_save_ctx,
+ mdata->flags | mdata->pvt_flags,
+ keywords);
+ if (keywords != NULL)
+ mail_index_keywords_unref(&keywords);
+
+ mailbox_save_set_received_date(ctx->backend_save_ctx,
+ mdata->received_date,
+ mdata->received_tz_offset);
+ mailbox_save_set_from_envelope(ctx->backend_save_ctx,
+ mdata->from_envelope);
+ mailbox_save_set_guid(ctx->backend_save_ctx, mdata->guid);
+ mailbox_save_set_min_modseq(ctx->backend_save_ctx, mdata->min_modseq);
+
+ virtual_mail_set_unattached_backend_mail(_ctx->dest_mail,
+ ctx->backend_save_ctx->dest_mail);
+ return mailbox_save_begin(&ctx->backend_save_ctx, input);
+}
+
+int virtual_save_continue(struct mail_save_context *_ctx)
+{
+ struct virtual_save_context *ctx = (struct virtual_save_context *)_ctx;
+
+ return mailbox_save_continue(ctx->backend_save_ctx);
+}
+
+int virtual_save_finish(struct mail_save_context *_ctx)
+{
+ struct virtual_save_context *ctx = (struct virtual_save_context *)_ctx;
+ int ret;
+ ret = mailbox_save_finish(&ctx->backend_save_ctx);
+ index_save_context_free(_ctx);
+
+ return ret;
+}
+
+void virtual_save_cancel(struct mail_save_context *_ctx)
+{
+ struct virtual_save_context *ctx = (struct virtual_save_context *)_ctx;
+
+ if (ctx->backend_save_ctx != NULL)
+ mailbox_save_cancel(&ctx->backend_save_ctx);
+ i_free_and_null(ctx->open_errstr);
+ _ctx->unfinished = FALSE;
+}
+
+void virtual_save_free(struct mail_save_context *_ctx)
+{
+ struct virtual_save_context *ctx = (struct virtual_save_context *)_ctx;
+
+ virtual_save_cancel(_ctx);
+ mailbox_save_context_deinit(_ctx);
+ i_free(ctx);
+}
diff --git a/src/plugins/virtual/virtual-search.c b/src/plugins/virtual/virtual-search.c
new file mode 100644
index 0000000..ee04a74
--- /dev/null
+++ b/src/plugins/virtual/virtual-search.c
@@ -0,0 +1,206 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-search.h"
+#include "index-search-private.h"
+#include "virtual-storage.h"
+
+
+enum virtual_search_state {
+ VIRTUAL_SEARCH_STATE_BUILD,
+ VIRTUAL_SEARCH_STATE_RETURN,
+ VIRTUAL_SEARCH_STATE_SORT,
+ VIRTUAL_SEARCH_STATE_SORT_DONE
+};
+
+struct virtual_search_record {
+ uint32_t mailbox_id;
+ uint32_t real_uid;
+ uint32_t virtual_seq;
+};
+
+struct virtual_search_context {
+ union mail_search_module_context module_ctx;
+
+ ARRAY_TYPE(seq_range) result;
+ struct seq_range_iter result_iter;
+ ARRAY(struct virtual_search_record) records;
+
+ enum virtual_search_state search_state;
+ unsigned int next_result_n;
+ unsigned int next_record_idx;
+};
+
+static int virtual_search_record_cmp(const struct virtual_search_record *r1,
+ const struct virtual_search_record *r2)
+{
+ if (r1->mailbox_id < r2->mailbox_id)
+ return -1;
+ if (r1->mailbox_id > r2->mailbox_id)
+ return 1;
+
+ if (r1->real_uid < r2->real_uid)
+ return -1;
+ if (r1->real_uid > r2->real_uid)
+ return 1;
+ return 0;
+}
+
+static int mail_search_get_result(struct mail_search_context *ctx)
+{
+ const struct mail_search_arg *arg;
+ int ret = 1;
+
+ for (arg = ctx->args->args; arg != NULL; arg = arg->next) {
+ if (arg->result < 0)
+ return -1;
+ if (arg->result == 0)
+ ret = 0;
+ }
+ return ret;
+}
+
+static void virtual_search_get_records(struct mail_search_context *ctx,
+ struct virtual_search_context *vctx)
+{
+ struct virtual_mailbox *mbox =
+ (struct virtual_mailbox *)ctx->transaction->box;
+ const struct virtual_mail_index_record *vrec;
+ struct virtual_search_record srec;
+ const void *data;
+ int result;
+
+ i_zero(&srec);
+ while (index_storage_search_next_update_seq(ctx)) {
+ result = mail_search_get_result(ctx);
+ i_assert(result != 0);
+ if (result > 0) {
+ /* full match, no need to check this any further */
+ seq_range_array_add(&vctx->result, ctx->seq);
+ } else {
+ /* possible match, save and check later */
+ mail_index_lookup_ext(mbox->box.view, ctx->seq,
+ mbox->virtual_ext_id,
+ &data, NULL);
+ vrec = data;
+
+ srec.mailbox_id = vrec->mailbox_id;
+ srec.real_uid = vrec->real_uid;
+ srec.virtual_seq = ctx->seq;
+ array_push_back(&vctx->records, &srec);
+ }
+ mail_search_args_reset(ctx->args->args, FALSE);
+ }
+ array_sort(&vctx->records, virtual_search_record_cmp);
+
+ ctx->progress_max = array_count(&vctx->records);
+}
+
+struct mail_search_context *
+virtual_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 mail_search_context *ctx;
+ struct virtual_search_context *vctx;
+
+ ctx = index_storage_search_init(t, args, sort_program,
+ wanted_fields, wanted_headers);
+
+ vctx = i_new(struct virtual_search_context, 1);
+ vctx->search_state = VIRTUAL_SEARCH_STATE_BUILD;
+ i_array_init(&vctx->result, 64);
+ i_array_init(&vctx->records, 64);
+ MODULE_CONTEXT_SET(ctx, virtual_storage_module, vctx);
+
+ virtual_search_get_records(ctx, vctx);
+ seq_range_array_iter_init(&vctx->result_iter, &vctx->result);
+ return ctx;
+}
+
+int virtual_search_deinit(struct mail_search_context *ctx)
+{
+ struct virtual_search_context *vctx = VIRTUAL_CONTEXT_REQUIRE(ctx);
+
+ array_free(&vctx->result);
+ array_free(&vctx->records);
+ i_free(vctx);
+ return index_storage_search_deinit(ctx);
+}
+
+bool virtual_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ struct virtual_search_context *vctx = VIRTUAL_CONTEXT_REQUIRE(ctx);
+ struct index_search_context *ictx = (struct index_search_context *)ctx;
+ uint32_t seq;
+
+ switch (vctx->search_state) {
+ case VIRTUAL_SEARCH_STATE_BUILD:
+ if (ctx->sort_program == NULL)
+ vctx->search_state = VIRTUAL_SEARCH_STATE_SORT;
+ else
+ vctx->search_state = VIRTUAL_SEARCH_STATE_RETURN;
+ return virtual_search_next_nonblock(ctx, mail_r, tryagain_r);
+ case VIRTUAL_SEARCH_STATE_RETURN:
+ return index_storage_search_next_nonblock(ctx, mail_r, tryagain_r);
+ case VIRTUAL_SEARCH_STATE_SORT:
+ /* the messages won't be returned sorted, so we'll have to
+ do it ourself */
+ while (index_storage_search_next_nonblock(ctx, mail_r, tryagain_r))
+ seq_range_array_add(&vctx->result, (*mail_r)->seq);
+ if (*tryagain_r)
+ return FALSE;
+
+ vctx->next_result_n = 0;
+ vctx->search_state = VIRTUAL_SEARCH_STATE_SORT_DONE;
+ /* fall through */
+ case VIRTUAL_SEARCH_STATE_SORT_DONE:
+ *tryagain_r = FALSE;
+ if (!seq_range_array_iter_nth(&vctx->result_iter,
+ vctx->next_result_n, &seq))
+ return FALSE;
+ vctx->next_result_n++;
+ *mail_r = index_search_get_mail(ictx);
+ i_assert(*mail_r != NULL);
+ mail_set_seq(*mail_r, seq);
+ return TRUE;
+ }
+ i_unreached();
+}
+
+static void search_args_set_full_match(struct mail_search_arg *args)
+{
+ for (; args != NULL; args = args->next)
+ args->result = 1;
+}
+
+bool virtual_search_next_update_seq(struct mail_search_context *ctx)
+{
+ struct virtual_search_context *vctx = VIRTUAL_CONTEXT_REQUIRE(ctx);
+ const struct virtual_search_record *recs;
+ unsigned int count;
+
+ recs = array_get(&vctx->records, &count);
+ if (vctx->next_record_idx < count) {
+ /* go through potential results first */
+ ctx->seq = recs[vctx->next_record_idx++].virtual_seq - 1;
+ if (!index_storage_search_next_update_seq(ctx))
+ i_unreached();
+ ctx->progress_cur = vctx->next_record_idx;
+ return TRUE;
+ }
+
+ if (ctx->sort_program != NULL &&
+ seq_range_array_iter_nth(&vctx->result_iter,
+ vctx->next_result_n, &ctx->seq)) {
+ /* this is known to match fully */
+ search_args_set_full_match(ctx->args->args);
+ vctx->next_result_n++;
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/plugins/virtual/virtual-storage.c b/src/plugins/virtual/virtual-storage.c
new file mode 100644
index 0000000..6d7dd98
--- /dev/null
+++ b/src/plugins/virtual/virtual-storage.c
@@ -0,0 +1,953 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "llist.h"
+#include "mkdir-parents.h"
+#include "unlink-directory.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+#include "mail-search.h"
+#include "mailbox-list-private.h"
+#include "virtual-plugin.h"
+#include "virtual-transaction.h"
+#include "virtual-storage.h"
+#include "mailbox-list-notify.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define VIRTUAL_DEFAULT_MAX_OPEN_MAILBOXES 64
+
+#define VIRTUAL_BACKEND_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, virtual_backend_storage_module)
+
+struct virtual_backend_mailbox {
+ union mailbox_module_context module_ctx;
+};
+
+extern struct mail_storage virtual_storage;
+extern struct mailbox virtual_mailbox;
+extern struct virtual_mailbox_vfuncs virtual_mailbox_vfuncs;
+
+struct virtual_storage_module virtual_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(virtual_backend_storage_module,
+ &mail_storage_module_register);
+
+static bool ns_is_visible(struct mail_namespace *ns)
+{
+ return (ns->flags & NAMESPACE_FLAG_LIST_PREFIX) != 0 ||
+ (ns->flags & NAMESPACE_FLAG_LIST_CHILDREN) != 0 ||
+ (ns->flags & NAMESPACE_FLAG_HIDDEN) == 0;
+}
+
+static const char *get_user_visible_mailbox_name(struct mailbox *box)
+{
+ if (ns_is_visible(box->list->ns))
+ return box->vname;
+ else {
+ return t_strdup_printf("<hidden>%c%s",
+ mail_namespace_get_sep(box->list->ns),
+ box->vname);
+ }
+}
+
+void virtual_box_copy_error(struct mailbox *dest, struct mailbox *src)
+{
+ const char *name, *str;
+ enum mail_error error;
+
+ name = get_user_visible_mailbox_name(src);
+ str = mailbox_get_last_error(src, &error);
+
+ str = t_strdup_printf("%s (for backend mailbox %s)", str, name);
+ mail_storage_set_error(dest->storage, error, str);
+}
+
+static struct mail_storage *virtual_storage_alloc(void)
+{
+ struct virtual_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("virtual storage", 1024);
+ storage = p_new(pool, struct virtual_storage, 1);
+ storage->storage = virtual_storage;
+ storage->storage.pool = pool;
+ p_array_init(&storage->open_stack, pool, 8);
+ return &storage->storage;
+}
+
+static int
+virtual_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns ATTR_UNUSED,
+ const char **error_r)
+{
+ struct virtual_storage *storage = (struct virtual_storage *)_storage;
+ const char *value;
+
+ value = mail_user_plugin_getenv(_storage->user, "virtual_max_open_mailboxes");
+ if (value == NULL)
+ storage->max_open_mailboxes = VIRTUAL_DEFAULT_MAX_OPEN_MAILBOXES;
+ else if (str_to_uint(value, &storage->max_open_mailboxes) < 0) {
+ *error_r = "Invalid virtual_max_open_mailboxes setting";
+ return -1;
+ }
+ return 0;
+}
+
+static void
+virtual_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 = VIRTUAL_SUBSCRIPTION_FILE_NAME;
+}
+
+struct virtual_backend_box *
+virtual_backend_box_lookup_name(struct virtual_mailbox *mbox, const char *name)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(bboxes[i]->name, name) == 0)
+ return bboxes[i];
+ }
+ return NULL;
+}
+
+bool virtual_backend_box_lookup(struct virtual_mailbox *mbox, uint32_t mailbox_id,
+ struct virtual_backend_box **bbox_r)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ *bbox_r = NULL;
+ if (mailbox_id == 0)
+ return FALSE;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->mailbox_id == mailbox_id) {
+ *bbox_r = bboxes[i];
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool virtual_mailbox_is_in_open_stack(struct virtual_storage *storage,
+ const char *name)
+{
+ const char *const *names;
+ unsigned int i, count;
+
+ names = array_get(&storage->open_stack, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(names[i], name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int virtual_backend_box_open_failed(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox)
+{
+ enum mail_error error;
+ const char *str;
+
+ str = t_strdup_printf(
+ "Virtual mailbox open failed because of mailbox %s: %s",
+ get_user_visible_mailbox_name(bbox->box),
+ mailbox_get_last_error(bbox->box, &error));
+ mail_storage_set_error(mbox->box.storage, error, str);
+ mailbox_free(&bbox->box);
+
+ if (error == MAIL_ERROR_PERM && bbox->wildcard) {
+ /* this mailbox wasn't explicitly specified. just skip it. */
+ return 0;
+ }
+ return -1;
+}
+
+static int virtual_backend_box_alloc(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox,
+ enum mailbox_flags flags)
+{
+ struct mail_user *user = mbox->storage->storage.user;
+ struct mail_namespace *ns;
+ const char *mailbox;
+ enum mailbox_existence existence;
+
+ i_assert(bbox->box == NULL);
+
+ if (!bbox->clear_recent)
+ flags &= ENUM_NEGATE(MAILBOX_FLAG_DROP_RECENT);
+
+ mailbox = bbox->name;
+ ns = mail_namespace_find(user->namespaces, mailbox);
+ bbox->box = mailbox_alloc(ns->list, mailbox, flags);
+ MODULE_CONTEXT_SET(bbox->box, virtual_storage_module, bbox);
+
+ if (bbox == mbox->save_bbox) {
+ /* Assume that the save_bbox exists, whether or not it truly
+ does. This at least gives a better error message than crash
+ later on. */
+ existence = MAILBOX_EXISTENCE_SELECT;
+ } else {
+ if (mailbox_exists(bbox->box, TRUE, &existence) < 0)
+ return virtual_backend_box_open_failed(mbox, bbox);
+ }
+ if (existence != MAILBOX_EXISTENCE_SELECT) {
+ /* ignore this. it could be intentional. */
+ e_debug(mbox->box.event,
+ "Skipping non-existing mailbox %s",
+ bbox->box->vname);
+ mailbox_free(&bbox->box);
+ return 0;
+ }
+
+ i_array_init(&bbox->uids, 64);
+ i_array_init(&bbox->sync_pending_removes, 64);
+ /* we use modseqs for being able to check quickly if backend mailboxes
+ have changed. make sure the backend has them enabled. */
+ (void)mailbox_enable(bbox->box, MAILBOX_FEATURE_CONDSTORE);
+ return 1;
+}
+
+static int virtual_mailboxes_open(struct virtual_mailbox *mbox,
+ enum mailbox_flags flags)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+ int ret;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; ) {
+ ret = virtual_backend_box_alloc(mbox, bboxes[i], flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ break;
+ array_delete(&mbox->backend_boxes, i, 1);
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ } else {
+ i++;
+ }
+ }
+ if (i == count)
+ return 0;
+ else {
+ /* failed */
+ for (; i > 0; i--) {
+ mailbox_free(&bboxes[i-1]->box);
+ array_free(&bboxes[i-1]->uids);
+ }
+ return -1;
+ }
+}
+
+static struct mailbox *
+virtual_mailbox_alloc(struct mail_storage *_storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct virtual_storage *storage = (struct virtual_storage *)_storage;
+ struct virtual_mailbox *mbox;
+ pool_t pool;
+
+ pool = pool_alloconly_create("virtual mailbox", 2048);
+ mbox = p_new(pool, struct virtual_mailbox, 1);
+ mbox->box = virtual_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = _storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &virtual_mail_vfuncs;
+ mbox->box.virtual_vfuncs = &virtual_mailbox_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ mbox->storage = storage;
+ mbox->virtual_ext_id = (uint32_t)-1;
+ mbox->virtual_ext2_id = (uint32_t)-1;
+ mbox->virtual_guid_ext_id = (uint32_t)-1;
+ return &mbox->box;
+}
+
+void virtual_backend_box_sync_mail_unset(struct virtual_backend_box *bbox)
+{
+ struct mailbox_transaction_context *trans;
+
+ if (bbox->sync_mail != NULL) {
+ trans = bbox->sync_mail->transaction;
+ mail_free(&bbox->sync_mail);
+ (void)mailbox_transaction_commit(&trans);
+ }
+}
+
+static bool virtual_backend_box_can_close(struct virtual_backend_box *bbox)
+{
+ if (bbox->box->notify_callback != NULL) {
+ /* we can close it if notify is set
+ because we have no need to keep it open
+ for tracking changes */
+ return bbox->notify != NULL;
+ }
+ if (array_count(&bbox->sync_pending_removes) > 0) {
+ /* FIXME: we could probably close this by making
+ syncing support it? */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+virtual_backend_box_close_any_except(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *except_bbox)
+{
+ struct virtual_backend_box *bbox;
+
+ /* first try to close a mailbox without any transactions.
+ we'll also skip any mailbox that has notifications enabled (ideally
+ these would be handled by mailbox list index) */
+ for (bbox = mbox->open_backend_boxes_head; bbox != NULL; bbox = bbox->next_open) {
+ i_assert(bbox->box->opened);
+
+ if (bbox != except_bbox &&
+ bbox->box->transaction_count == 0 &&
+ virtual_backend_box_can_close(bbox)) {
+ i_assert(bbox->sync_mail == NULL);
+ virtual_backend_box_close(mbox, bbox);
+ return TRUE;
+ }
+ }
+
+ /* next try to close a mailbox that has sync_mail, but no
+ other transactions */
+ for (bbox = mbox->open_backend_boxes_head; bbox != NULL; bbox = bbox->next_open) {
+ if (bbox != except_bbox &&
+ bbox->sync_mail != NULL &&
+ bbox->box->transaction_count == 1 &&
+ virtual_backend_box_can_close(bbox)) {
+ virtual_backend_box_sync_mail_unset(bbox);
+ i_assert(bbox->box->transaction_count == 0);
+ virtual_backend_box_close(mbox, bbox);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void virtual_backend_mailbox_close(struct mailbox *box)
+{
+ struct virtual_backend_box *bbox = VIRTUAL_CONTEXT(box);
+ struct virtual_backend_mailbox *vbox = VIRTUAL_BACKEND_CONTEXT(box);
+
+ if (bbox != NULL && bbox->open_tracked) {
+ /* we could have gotten here from e.g. mailbox_autocreate()
+ without going through virtual_mailbox_close() */
+ virtual_backend_box_close(bbox->virtual_mbox, bbox);
+ }
+ vbox->module_ctx.super.close(box);
+}
+
+void virtual_backend_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct virtual_backend_mailbox *vbox;
+
+ vbox = p_new(box->pool, struct virtual_backend_mailbox, 1);
+ vbox->module_ctx.super = *v;
+ box->vlast = &vbox->module_ctx.super;
+ v->close = virtual_backend_mailbox_close;
+ MODULE_CONTEXT_SET(box, virtual_backend_storage_module, vbox);
+}
+
+void virtual_backend_mailbox_opened(struct mailbox *box)
+{
+ struct virtual_backend_box *bbox = VIRTUAL_CONTEXT(box);
+ struct virtual_mailbox *mbox;
+
+ if (bbox == NULL) {
+ /* not a backend for a virtual mailbox */
+ return;
+ }
+ i_assert(!bbox->open_tracked);
+ mbox = bbox->virtual_mbox;
+
+ /* the backend mailbox was already opened. if we didn't get here
+ from virtual_backend_box_open() we may need to close a mailbox */
+ while (mbox->backends_open_count >= mbox->storage->max_open_mailboxes &&
+ virtual_backend_box_close_any_except(mbox, bbox))
+ ;
+
+ bbox->open_tracked = TRUE;
+ mbox->backends_open_count++;
+ DLLIST2_APPEND_FULL(&mbox->open_backend_boxes_head,
+ &mbox->open_backend_boxes_tail, bbox,
+ prev_open, next_open);
+}
+
+int virtual_backend_box_open(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox)
+{
+ i_assert(!bbox->box->opened);
+
+ /* try to keep the number of open mailboxes below the threshold
+ before opening the mailbox */
+ while (mbox->backends_open_count >= mbox->storage->max_open_mailboxes &&
+ virtual_backend_box_close_any_except(mbox, bbox))
+ ;
+
+ return mailbox_open(bbox->box);
+}
+
+void virtual_backend_box_close(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox)
+{
+ i_assert(bbox->box->opened);
+ i_assert(bbox->open_tracked);
+
+ if (bbox->search_result != NULL)
+ mailbox_search_result_free(&bbox->search_result);
+
+ if (bbox->search_args != NULL &&
+ bbox->search_args_initialized) {
+ mail_search_args_deinit(bbox->search_args);
+ bbox->search_args_initialized = FALSE;
+ }
+ i_assert(mbox->backends_open_count > 0);
+ mbox->backends_open_count--;
+ bbox->open_tracked = FALSE;
+
+ DLLIST2_REMOVE_FULL(&mbox->open_backend_boxes_head,
+ &mbox->open_backend_boxes_tail, bbox,
+ prev_open, next_open);
+
+ /* stop receiving notifications */
+ if (bbox->notify_changes_started)
+ mailbox_notify_changes_stop(bbox->box);
+ bbox->notify_changes_started = FALSE;
+
+ mailbox_close(bbox->box);
+}
+
+void virtual_backend_box_accessed(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox)
+{
+ DLLIST2_REMOVE_FULL(&mbox->open_backend_boxes_head,
+ &mbox->open_backend_boxes_tail, bbox,
+ prev_open, next_open);
+ DLLIST2_APPEND_FULL(&mbox->open_backend_boxes_head,
+ &mbox->open_backend_boxes_tail, bbox,
+ prev_open, next_open);
+}
+
+static void virtual_mailbox_close_internal(struct virtual_mailbox *mbox)
+{
+ struct virtual_backend_box **bboxes;
+ unsigned int i, count;
+
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->box == NULL)
+ continue;
+ if (bboxes[i]->notify != NULL)
+ mailbox_list_notify_deinit(&bboxes[i]->notify);
+ if (bboxes[i]->box->opened)
+ virtual_backend_box_close(mbox, bboxes[i]);
+ mailbox_free(&bboxes[i]->box);
+ if (array_is_created(&bboxes[i]->sync_outside_expunges))
+ array_free(&bboxes[i]->sync_outside_expunges);
+ array_free(&bboxes[i]->sync_pending_removes);
+ array_free(&bboxes[i]->uids);
+ }
+ i_assert(mbox->backends_open_count == 0);
+}
+
+static int
+virtual_mailbox_exists(struct mailbox *box, bool auto_boxes ATTR_UNUSED,
+ enum mailbox_existence *existence_r)
+{
+ return index_storage_mailbox_exists_full(box, VIRTUAL_CONFIG_FNAME,
+ existence_r);
+}
+
+static int virtual_mailbox_open(struct mailbox *box)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ bool broken;
+ int ret = 0;
+
+ if (virtual_mailbox_is_in_open_stack(mbox->storage, box->name)) {
+ mailbox_set_critical(box,
+ "Virtual mailbox loops: %s", box->name);
+ return -1;
+ }
+
+ if (!array_is_created(&mbox->backend_boxes))
+ ret = virtual_config_read(mbox);
+ if (ret == 0) {
+ array_push_back(&mbox->storage->open_stack, &box->name);
+ ret = virtual_mailboxes_open(mbox, box->flags);
+ array_pop_back(&mbox->storage->open_stack);
+ }
+ if (ret == 0)
+ ret = index_storage_mailbox_open(box, FALSE);
+ if (ret < 0) {
+ virtual_mailbox_close_internal(mbox);
+ return -1;
+ }
+
+ mbox->virtual_ext_id =
+ mail_index_ext_register(mbox->box.index, "virtual", 0,
+ sizeof(struct virtual_mail_index_record),
+ sizeof(uint32_t));
+ mbox->virtual_ext2_id =
+ mail_index_ext_register(mbox->box.index, "virtual2", 0, 0, 0);
+
+ mbox->virtual_guid_ext_id =
+ mail_index_ext_register(mbox->box.index, "virtual-guid", GUID_128_SIZE,
+ 0, 0);
+
+ if (virtual_mailbox_ext_header_read(mbox, box->view, &broken) < 0) {
+ virtual_mailbox_close_internal(mbox);
+ index_storage_mailbox_close(box);
+ return -1;
+ }
+
+ /* if GUID is missing write it here */
+ if (guid_128_is_empty(mbox->guid)) {
+ guid_128_generate(mbox->guid);
+ struct mail_index_transaction *t =
+ mail_index_transaction_begin(box->view, 0);
+ mail_index_update_header_ext(t, mbox->virtual_guid_ext_id,
+ 0, mbox->guid, GUID_128_SIZE);
+ if (mail_index_transaction_commit(&t) < 0) {
+ mailbox_set_critical(box,
+ "Cannot write GUID for virtual mailbox to index");
+ virtual_mailbox_close_internal(mbox);
+ index_storage_mailbox_close(box);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void virtual_mailbox_close(struct mailbox *box)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+
+ virtual_mailbox_close_internal(mbox);
+ index_storage_mailbox_close(box);
+}
+
+static void virtual_mailbox_free(struct mailbox *box)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+
+ virtual_config_free(mbox);
+ index_storage_mailbox_free(box);
+}
+
+static int
+virtual_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,
+ "Can't create virtual mailboxes");
+ return -1;
+}
+
+static int
+virtual_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't update virtual mailboxes");
+ return -1;
+}
+
+static int virtual_storage_set_have_guid_flags(struct virtual_mailbox *mbox)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+ struct mailbox_status status;
+
+ if (!mbox->box.opened) {
+ if (mailbox_open(&mbox->box) < 0)
+ return -1;
+ }
+
+ mbox->have_guids = TRUE;
+ mbox->have_save_guids = TRUE;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (mailbox_get_status(bboxes[i]->box, 0, &status) < 0) {
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_error(bboxes[i]->box, &error);
+ if (error == MAIL_ERROR_NOTFOUND) {
+ /* backend mailbox was just lost - skip it */
+ continue;
+ }
+ /* Not expected to happen, but we can't return failure
+ since this could be called from
+ mailbox_get_open_status() and it would panic.
+ So just log the error and skip the mailbox. */
+ mailbox_set_critical(&mbox->box,
+ "Virtual mailbox: Failed to get have_guid existence for backend mailbox %s: %s",
+ mailbox_get_vname(bboxes[i]->box), errstr);
+ continue;
+ }
+ if (!status.have_guids)
+ mbox->have_guids = FALSE;
+ if (!status.have_save_guids)
+ mbox->have_save_guids = FALSE;
+ }
+ return 0;
+}
+
+static int
+virtual_storage_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+
+ if ((items & STATUS_LAST_CACHED_SEQ) != 0)
+ items |= STATUS_MESSAGES;
+
+ if (index_storage_get_status(box, items, status_r) < 0)
+ return -1;
+
+ if ((items & STATUS_LAST_CACHED_SEQ) != 0) {
+ /* Virtual mailboxes have no cached data of their own, so the
+ current value is always 0. The most important use for this
+ functionality is for "doveadm index" to do FTS indexing and
+ it doesn't really matter there if we set this value
+ correctly or not. So for now just assume that everything is
+ indexed. */
+ status_r->last_cached_seq = status_r->messages;
+ }
+ if (!mbox->have_guid_flags_set) {
+ if (virtual_storage_set_have_guid_flags(mbox) < 0)
+ return -1;
+ mbox->have_guid_flags_set = TRUE;
+ }
+
+ if (mbox->have_guids)
+ status_r->have_guids = TRUE;
+ if (mbox->have_save_guids)
+ status_r->have_save_guids = TRUE;
+ return 0;
+}
+
+static int
+virtual_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ i_assert(box->opened);
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ if (guid_128_is_empty(mbox->guid)) {
+ mailbox_set_critical(box, "GUID missing for virtual folder");
+ return -1;
+ }
+ guid_128_copy(metadata_r->guid, mbox->guid);
+ }
+ return 0;
+}
+
+static void
+virtual_notify_callback(struct mailbox *bbox ATTR_UNUSED, struct mailbox *box)
+{
+ box->notify_callback(box, box->notify_context);
+}
+
+static void virtual_backend_box_changed(struct virtual_backend_box *bbox)
+{
+ virtual_notify_callback(bbox->box, &bbox->virtual_mbox->box);
+}
+
+static int virtual_notify_start(struct virtual_backend_box *bbox)
+{
+ i_assert(bbox->notify == NULL);
+ if (mailbox_list_notify_init(bbox->box->list, MAILBOX_LIST_NOTIFY_STATUS,
+ &bbox->notify) < 0)
+ /* did not support notifications */
+ return -1;
+ mailbox_list_notify_wait(bbox->notify, virtual_backend_box_changed, bbox);
+ return 0;
+}
+
+static void virtual_notify_changes(struct mailbox *box)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct virtual_backend_box **bboxp;
+
+ if (box->notify_callback == NULL) {
+ array_foreach_modifiable(&mbox->backend_boxes, bboxp) {
+ if ((*bboxp)->notify_changes_started) {
+ mailbox_notify_changes_stop((*bboxp)->box);
+ (*bboxp)->notify_changes_started = FALSE;
+ }
+ if ((*bboxp)->notify != NULL)
+ mailbox_list_notify_deinit(&(*bboxp)->notify);
+ }
+ return;
+ }
+
+ array_foreach_modifiable(&mbox->backend_boxes, bboxp) {
+ if (array_count(&mbox->backend_boxes) == 1 &&
+ (*bboxp)->box->opened) {
+ /* There's only a single backend mailbox and its
+ indexes are already opened. Might as well use the
+ backend directly for notifications. */
+ } else {
+ /* we are already waiting for notifications */
+ if ((*bboxp)->notify != NULL)
+ continue;
+ /* wait for notifications */
+ if (virtual_notify_start(*bboxp) == 0)
+ continue;
+ /* it did not work, so open the mailbox and use
+ alternative method */
+ }
+
+ if (!(*bboxp)->box->opened &&
+ virtual_backend_box_open(mbox, *bboxp) < 0) {
+ /* we can't report error in here, so do it later */
+ (*bboxp)->open_failed = TRUE;
+ continue;
+ }
+ mailbox_notify_changes((*bboxp)->box,
+ virtual_notify_callback, box);
+ (*bboxp)->notify_changes_started = TRUE;
+ }
+}
+
+static void
+virtual_uidmap_to_uid_array(struct virtual_backend_box *bbox,
+ ARRAY_TYPE(seq_range) *uids_r)
+{
+ const struct virtual_backend_uidmap *uid;
+ array_foreach(&bbox->uids, uid) {
+ seq_range_array_add(uids_r, uid->real_uid);
+ }
+}
+
+static void
+virtual_get_virtual_uids(struct mailbox *box,
+ struct mailbox *backend_mailbox,
+ const ARRAY_TYPE(seq_range) *backend_uids,
+ ARRAY_TYPE(seq_range) *virtual_uids_r)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct virtual_backend_box *bbox;
+ const struct virtual_backend_uidmap *uids;
+ ARRAY_TYPE(seq_range) uid_range;
+ struct seq_range_iter iter;
+ unsigned int n, i, count;
+ uint32_t uid;
+
+ if (mbox->lookup_prev_bbox != NULL &&
+ strcmp(mbox->lookup_prev_bbox->box->vname, backend_mailbox->vname) == 0)
+ bbox = mbox->lookup_prev_bbox;
+ else {
+ bbox = virtual_backend_box_lookup_name(mbox, backend_mailbox->vname);
+ mbox->lookup_prev_bbox = bbox;
+ }
+ if (bbox == NULL)
+ return;
+
+ uids = array_get(&bbox->uids, &count); i = 0;
+
+ t_array_init(&uid_range, 8);
+ virtual_uidmap_to_uid_array(bbox, &uid_range);
+ seq_range_array_intersect(&uid_range, backend_uids);
+
+ seq_range_array_iter_init(&iter, &uid_range); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ while (i < count && uids[i].real_uid < uid) i++;
+ if (i < count && uids[i].real_uid == uid) {
+ i_assert(uids[i].virtual_uid > 0);
+ seq_range_array_add(virtual_uids_r,
+ uids[i].virtual_uid);
+ i++;
+ }
+ }
+}
+
+static void
+virtual_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)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct virtual_backend_box *bbox;
+ const struct virtual_backend_uidmap *uids;
+ struct seq_range_iter iter;
+ unsigned int n, i, count;
+ uint32_t uid;
+
+ if (mbox->lookup_prev_bbox != NULL &&
+ strcmp(mbox->lookup_prev_bbox->box->vname, backend_mailbox->vname) == 0)
+ bbox = mbox->lookup_prev_bbox;
+ else {
+ bbox = virtual_backend_box_lookup_name(mbox, backend_mailbox->vname);
+ mbox->lookup_prev_bbox = bbox;
+ }
+ if (bbox == NULL)
+ return;
+
+ uids = array_get(&bbox->uids, &count); i = 0;
+ seq_range_array_iter_init(&iter, backend_uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ while (i < count && uids[i].real_uid < uid) i++;
+ if (i == count || uids[i].real_uid > uid) {
+ uint32_t zero = 0;
+
+ array_push_back(virtual_uids_r, &zero);
+ } else {
+ i_assert(uids[i].virtual_uid > 0);
+ array_push_back(virtual_uids_r, &uids[i].virtual_uid);
+ i++;
+ }
+ }
+}
+
+static void
+virtual_get_virtual_backend_boxes(struct mailbox *box,
+ ARRAY_TYPE(mailboxes) *mailboxes,
+ bool only_with_msgs)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (!only_with_msgs || array_count(&bboxes[i]->uids) > 0)
+ array_push_back(mailboxes, &bboxes[i]->box);
+ }
+}
+
+static bool virtual_is_inconsistent(struct mailbox *box)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+
+ if (mbox->inconsistent)
+ return TRUE;
+
+ return index_storage_is_inconsistent(box);
+}
+
+static int
+virtual_list_index_has_changed(struct mailbox *box ATTR_UNUSED,
+ struct mail_index_view *list_view ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED, bool quick ATTR_UNUSED,
+ const char **reason_r)
+{
+ /* we don't have any quick and easy optimizations for tracking
+ virtual folders. ideally we'd completely disable mailbox list
+ indexes for them, but this is the easiest way to do it for now. */
+ *reason_r = "Virtual indexes always change";
+ return 1;
+}
+
+static void
+virtual_list_index_update_sync(struct mailbox *box ATTR_UNUSED,
+ struct mail_index_transaction *trans ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED)
+{
+}
+
+struct mail_storage virtual_storage = {
+ .name = VIRTUAL_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_NOQUOTA |
+ MAIL_STORAGE_CLASS_FLAG_SECONDARY_INDEX,
+
+ .v = {
+ NULL,
+ virtual_storage_alloc,
+ virtual_storage_create,
+ index_storage_destroy,
+ NULL,
+ virtual_storage_get_list_settings,
+ NULL,
+ virtual_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mailbox virtual_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ virtual_mailbox_exists,
+ virtual_mailbox_open,
+ virtual_mailbox_close,
+ virtual_mailbox_free,
+ virtual_mailbox_create,
+ virtual_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ virtual_storage_get_status,
+ virtual_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,
+ virtual_list_index_has_changed,
+ virtual_list_index_update_sync,
+ virtual_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ virtual_notify_changes,
+ virtual_transaction_begin,
+ virtual_transaction_commit,
+ virtual_transaction_rollback,
+ NULL,
+ virtual_mail_alloc,
+ virtual_search_init,
+ virtual_search_deinit,
+ virtual_search_next_nonblock,
+ virtual_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ virtual_save_alloc,
+ virtual_save_begin,
+ virtual_save_continue,
+ virtual_save_finish,
+ virtual_save_cancel,
+ mail_storage_copy,
+ NULL,
+ NULL,
+ NULL,
+ virtual_is_inconsistent
+ }
+};
+
+struct virtual_mailbox_vfuncs virtual_mailbox_vfuncs = {
+ virtual_get_virtual_uids,
+ virtual_get_virtual_uid_map,
+ virtual_get_virtual_backend_boxes
+};
diff --git a/src/plugins/virtual/virtual-storage.h b/src/plugins/virtual/virtual-storage.h
new file mode 100644
index 0000000..4bb794f
--- /dev/null
+++ b/src/plugins/virtual/virtual-storage.h
@@ -0,0 +1,251 @@
+#ifndef VIRTUAL_STORAGE_H
+#define VIRTUAL_STORAGE_H
+
+#include "seq-range-array.h"
+#include "index-storage.h"
+
+#define VIRTUAL_STORAGE_NAME "virtual"
+#define VIRTUAL_SUBSCRIPTION_FILE_NAME ".virtual-subscriptions"
+#define VIRTUAL_CONFIG_FNAME "dovecot-virtual"
+
+#define VIRTUAL_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, virtual_storage_module)
+#define VIRTUAL_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, virtual_storage_module)
+
+#define VIRTUAL_MAIL_INDEX_EXT2_HEADER_VERSION 1
+
+struct virtual_save_context;
+
+struct virtual_mail_index_header {
+ /* Increased by one each time the header is modified */
+ uint32_t change_counter;
+ /* Number of mailbox records following this header. Mailbox names
+ follow the mailbox records - they have neither NUL terminator nor
+ padding. */
+ uint32_t mailbox_count;
+ /* Highest used mailbox ID. IDs are never reused. */
+ uint32_t highest_mailbox_id;
+ /* CRC32 of all the search parameters. If it changes, the mailbox is
+ rebuilt. */
+ uint32_t search_args_crc32;
+};
+
+struct virtual_mail_index_mailbox_record {
+ /* Unique mailbox ID used as mailbox_id in records. */
+ uint32_t id;
+ /* Length of this mailbox's name. */
+ uint32_t name_len;
+ /* Synced UID validity value */
+ uint32_t uid_validity;
+ /* Next unseen UID */
+ uint32_t next_uid;
+ /* Synced highest modseq value */
+ uint64_t highest_modseq;
+};
+
+struct virtual_mail_index_ext2_header {
+ /* Version compatibility number:
+ VIRTUAL_MAIL_INDEX_EXT2_HEADER_VERSION */
+ uint8_t version;
+ /* Set to sizeof(struct virtual_mail_index_mailbox_ext2_record) when
+ writing. */
+ uint8_t ext_record_size;
+ /* Set to sizeof(struct virtual_mail_index_ext2_header) when writing. */
+ uint16_t hdr_size;
+ /* Must be the same as virtual_mail_index_header.change_counter.
+ If not, it means the header was modified by an older Dovecot version
+ and this extension header should be ignored/rewritten. */
+ uint32_t change_counter;
+};
+
+struct virtual_mail_index_mailbox_ext2_record {
+ /* Synced GUID value */
+ uint8_t guid[16];
+};
+
+struct virtual_mail_index_record {
+ uint32_t mailbox_id;
+ uint32_t real_uid;
+};
+
+struct virtual_storage {
+ struct mail_storage storage;
+
+ /* List of mailboxes while a virtual mailbox is being opened.
+ Used to track loops. */
+ ARRAY_TYPE(const_string) open_stack;
+
+ unsigned int max_open_mailboxes;
+};
+
+struct virtual_backend_uidmap {
+ uint32_t real_uid;
+ /* can be 0 temporarily while syncing before the UID is assigned */
+ uint32_t virtual_uid;
+};
+
+struct virtual_backend_box {
+ union mailbox_module_context module_ctx;
+ struct virtual_mailbox *virtual_mbox;
+
+ /* linked list for virtual_mailbox->open_backend_boxes_{head,tail} */
+ struct virtual_backend_box *prev_open, *next_open;
+
+ /* Initially zero, updated by syncing */
+ uint32_t mailbox_id;
+ const char *name;
+
+ unsigned int sync_mailbox_idx1;
+ uint32_t sync_uid_validity;
+ uint32_t sync_next_uid;
+ uint64_t sync_highest_modseq;
+ guid_128_t sync_guid;
+ /* this value is either 0 or same as sync_highest_modseq. it's kept 0
+ when there are pending removes that have yet to be expunged */
+ uint64_t ondisk_highest_modseq;
+
+ struct mail_search_args *search_args;
+ struct mail_search_result *search_result;
+
+ struct mailbox *box;
+ /* Messages currently included in the virtual mailbox,
+ sorted by real_uid */
+ ARRAY(struct virtual_backend_uidmap) uids;
+
+ /* temporary mail used while syncing */
+ struct mail *sync_mail;
+ /* pending removed UIDs */
+ ARRAY_TYPE(seq_range) sync_pending_removes;
+ /* another process expunged these UIDs. they need to be removed on
+ next sync. */
+ ARRAY_TYPE(seq_range) sync_outside_expunges;
+
+ /* name contains a wildcard, this is a glob for it */
+ struct imap_match_glob *glob;
+ struct mail_namespace *ns;
+ /* mailbox metadata matching */
+ const char *metadata_entry, *metadata_value;
+
+ /* notify context */
+ struct mailbox_list_notify *notify;
+
+ bool open_tracked:1;
+ bool open_failed:1;
+ bool sync_seen:1;
+ bool wildcard:1;
+ bool clear_recent:1;
+ bool negative_match:1;
+ bool uids_nonsorted:1;
+ bool search_args_initialized:1;
+ bool deleted:1;
+ bool notify_changes_started:1; /* if the box was opened for notify_changes */
+ bool first_sync:1; /* if this is the first sync after bbox was (re-)created */
+};
+ARRAY_DEFINE_TYPE(virtual_backend_box, struct virtual_backend_box *);
+
+struct virtual_mailbox {
+ struct mailbox box;
+ struct virtual_storage *storage;
+
+ uint32_t virtual_ext_id;
+ uint32_t virtual_ext2_id;
+ uint32_t virtual_guid_ext_id;
+
+ uint32_t prev_uid_validity;
+ uint32_t prev_change_counter;
+ uint32_t highest_mailbox_id;
+ uint32_t search_args_crc32;
+ guid_128_t guid;
+
+ struct virtual_backend_box *lookup_prev_bbox;
+ uint32_t sync_virtual_next_uid;
+
+ /* Mailboxes this virtual mailbox consists of, sorted by mailbox_id */
+ ARRAY_TYPE(virtual_backend_box) backend_boxes;
+ /* backend mailbox where to save messages when saving to this mailbox */
+ struct virtual_backend_box *save_bbox;
+
+ /* linked list of open backend mailboxes. head will contain the oldest
+ accessed mailbox, tail will contain the newest. */
+ struct virtual_backend_box *open_backend_boxes_head;
+ struct virtual_backend_box *open_backend_boxes_tail;
+ /* number of backend mailboxes that are open currently. */
+ unsigned int backends_open_count;
+
+ ARRAY_TYPE(mailbox_virtual_patterns) list_include_patterns;
+ ARRAY_TYPE(mailbox_virtual_patterns) list_exclude_patterns;
+
+ bool uids_mapped:1;
+ bool sync_initialized:1;
+ bool inconsistent:1;
+ bool have_guid_flags_set:1;
+ bool have_guids:1;
+ bool have_save_guids:1;
+ bool ext_header_rewrite:1;
+};
+
+extern MODULE_CONTEXT_DEFINE(virtual_storage_module,
+ &mail_storage_module_register);
+
+extern struct mail_storage virtual_storage;
+extern struct mail_vfuncs virtual_mail_vfuncs;
+
+int virtual_config_read(struct virtual_mailbox *mbox);
+void virtual_config_free(struct virtual_mailbox *mbox);
+
+int virtual_mailbox_ext_header_read(struct virtual_mailbox *mbox,
+ struct mail_index_view *view,
+ bool *broken_r);
+
+struct virtual_backend_box *
+virtual_backend_box_lookup_name(struct virtual_mailbox *mbox, const char *name);
+bool virtual_backend_box_lookup(struct virtual_mailbox *mbox, uint32_t mailbox_id,
+ struct virtual_backend_box **vbox_r);
+
+int virtual_backend_box_open(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox);
+void virtual_backend_box_close(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox);
+void virtual_backend_box_accessed(struct virtual_mailbox *mbox,
+ struct virtual_backend_box *bbox);
+void virtual_backend_box_sync_mail_unset(struct virtual_backend_box *bbox);
+
+struct mail_search_context *
+virtual_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 virtual_search_deinit(struct mail_search_context *ctx);
+bool virtual_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r);
+bool virtual_search_next_update_seq(struct mail_search_context *ctx);
+
+struct mail *
+virtual_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+struct mail *
+virtual_mail_set_backend_mail(struct mail *mail,
+ struct virtual_backend_box *bbox);
+void virtual_mail_set_unattached_backend_mail(struct mail *mail,
+ struct mail *backend_mail);
+
+struct mailbox_sync_context *
+virtual_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+
+struct mail_save_context *
+virtual_save_alloc(struct mailbox_transaction_context *t);
+int virtual_save_begin(struct mail_save_context *ctx, struct istream *input);
+int virtual_save_continue(struct mail_save_context *ctx);
+int virtual_save_finish(struct mail_save_context *ctx);
+void virtual_save_cancel(struct mail_save_context *ctx);
+void virtual_save_free(struct mail_save_context *ctx);
+
+void virtual_box_copy_error(struct mailbox *dest, struct mailbox *src);
+
+void virtual_backend_mailbox_allocated(struct mailbox *box);
+void virtual_backend_mailbox_opened(struct mailbox *box);
+
+#endif
diff --git a/src/plugins/virtual/virtual-sync.c b/src/plugins/virtual/virtual-sync.c
new file mode 100644
index 0000000..df3f9e6
--- /dev/null
+++ b/src/plugins/virtual/virtual-sync.c
@@ -0,0 +1,1973 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mail-index-modseq.h"
+#include "mail-search-build.h"
+#include "mailbox-search-result-private.h"
+#include "mailbox-recent-flags.h"
+#include "index-sync-private.h"
+#include "index-search-result.h"
+#include "virtual-storage.h"
+
+
+struct virtual_add_record {
+ struct virtual_mail_index_record rec;
+ time_t received_date;
+};
+
+struct virtual_sync_mail {
+ uint32_t vseq;
+ struct virtual_mail_index_record vrec;
+};
+
+struct virtual_sync_context {
+ struct virtual_mailbox *mbox;
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index *index;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+ const char *const *kw_all;
+
+ /* messages expunged within this sync */
+ ARRAY_TYPE(seq_range) sync_expunges;
+
+ ARRAY(struct virtual_add_record) all_adds;
+
+ /* all messages in this sync, sorted by mailbox_id
+ (but unsorted inside it for now, since it doesn't matter) */
+ ARRAY(struct virtual_sync_mail) all_mails;
+ uint32_t all_mails_idx, all_mails_prev_mailbox_id;
+
+ enum mailbox_sync_flags flags;
+ uint32_t uid_validity;
+
+ bool ext_header_changed:1;
+ bool expunge_removed:1;
+ bool index_broken:1;
+};
+
+static void virtual_sync_backend_box_deleted(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox);
+
+static void virtual_sync_set_uidvalidity(struct virtual_sync_context *ctx)
+{
+ 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);
+ ctx->uid_validity = uid_validity;
+}
+
+static void virtual_sync_external_flags(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ uint32_t vseq, uint32_t real_uid)
+{
+ enum mail_flags flags;
+ const char *const *kw_names;
+ struct mail_keywords *keywords;
+
+ if (!mail_set_uid(bbox->sync_mail, real_uid)) {
+ /* we may have reopened the mailbox, which could have
+ caused the mail to be expunged already. */
+ return;
+ }
+
+ /* copy flags */
+ flags = mail_get_flags(bbox->sync_mail);
+
+ /* we don't need to keep recent flags here */
+ mail_index_update_flags(ctx->trans, vseq, MODIFY_REPLACE,
+ flags & ENUM_NEGATE(MAIL_RECENT));
+
+ /* copy keywords */
+ kw_names = mail_get_keywords(bbox->sync_mail);
+ keywords = mail_index_keywords_create(ctx->index, kw_names);
+ mail_index_update_keywords(ctx->trans, vseq, MODIFY_REPLACE, keywords);
+ mail_index_keywords_unref(&keywords);
+}
+
+static int virtual_sync_mail_uid_cmp(const void *p1, const void *p2)
+{
+ const struct virtual_sync_mail *m1 = p1, *m2 = p2;
+
+ if (m1->vrec.mailbox_id < m2->vrec.mailbox_id)
+ return -1;
+ if (m1->vrec.mailbox_id > m2->vrec.mailbox_id)
+ return 1;
+
+ if (m1->vrec.real_uid < m2->vrec.real_uid)
+ return -1;
+ if (m1->vrec.real_uid > m2->vrec.real_uid)
+ return 1;
+ /* broken */
+ return 0;
+}
+
+static void
+virtual_backend_box_sync_mail_set(struct virtual_backend_box *bbox)
+{
+ struct mailbox_transaction_context *trans;
+
+ if (bbox->sync_mail == NULL) {
+ trans = mailbox_transaction_begin(bbox->box, 0, __func__);
+ bbox->sync_mail = mail_alloc(trans, 0, NULL);
+ }
+}
+
+static int bbox_mailbox_id_cmp(struct virtual_backend_box *const *b1,
+ struct virtual_backend_box *const *b2)
+{
+ if ((*b1)->mailbox_id < (*b2)->mailbox_id)
+ return -1;
+ if ((*b1)->mailbox_id > (*b2)->mailbox_id)
+ return 1;
+ return 0;
+}
+
+static int
+virtual_sync_get_backend_box(struct virtual_mailbox *mbox, const char *name,
+ struct virtual_backend_box **bbox_r)
+{
+ *bbox_r = virtual_backend_box_lookup_name(mbox, name);
+ if (*bbox_r != NULL || !mbox->sync_initialized)
+ return 0;
+
+ /* another process just added a new mailbox.
+ we can't handle this currently. */
+ mbox->inconsistent = TRUE;
+ mail_storage_set_error(mbox->box.storage, MAIL_ERROR_TEMP, t_strdup_printf(
+ "Backend mailbox '%s' added by another session. "
+ "Reopen the virtual mailbox.", name));
+ return -1;
+}
+
+static bool
+virtual_mailbox_ext2_header_read(struct virtual_mailbox *mbox,
+ struct mail_index_view *view,
+ const struct virtual_mail_index_header *ext_hdr)
+{
+ const char *box_path = mailbox_get_path(&mbox->box);
+ const struct virtual_mail_index_ext2_header *ext2_hdr;
+ const struct virtual_mail_index_mailbox_ext2_record *ext2_rec;
+ const void *ext2_data;
+ size_t ext2_size;
+ struct virtual_backend_box *bbox;
+
+ mail_index_get_header_ext(view, mbox->virtual_ext2_id,
+ &ext2_data, &ext2_size);
+ ext2_hdr = ext2_data;
+ if (ext2_size == 0) {
+ /* ext2 is missing - silently add it */
+ return FALSE;
+ }
+ if (ext2_size < sizeof(*ext2_hdr)) {
+ i_error("virtual index %s: Invalid ext2 header size: %zu",
+ box_path, ext2_size);
+ return FALSE;
+ }
+ if (ext2_hdr->hdr_size > ext2_size) {
+ i_error("virtual index %s: ext2 header size too large: %u > %zu",
+ box_path, ext2_hdr->hdr_size, ext2_size);
+ return FALSE;
+ }
+ if (ext2_hdr->ext_record_size < sizeof(*ext2_rec)) {
+ i_error("virtual index %s: Invalid ext2 record size: %u",
+ box_path, ext2_hdr->ext_record_size);
+ return FALSE;
+ }
+
+ if (ext_hdr->change_counter != ext2_hdr->change_counter) {
+ i_warning("virtual index %s: "
+ "Extension header change_counter mismatch (%u != %u) - "
+ "Index was modified by an older version?",
+ box_path, ext_hdr->change_counter,
+ ext2_hdr->change_counter);
+ return FALSE;
+ }
+ size_t mailboxes_size = ext2_size - ext2_hdr->hdr_size;
+ if (mailboxes_size % ext2_hdr->ext_record_size != 0 ||
+ mailboxes_size / ext2_hdr->ext_record_size != ext_hdr->mailbox_count) {
+ i_error("virtual index %s: Invalid ext2 size: "
+ "hdr_size=%u record_size=%u total_size=%zu mailbox_count=%u",
+ box_path, ext2_hdr->hdr_size, ext2_hdr->ext_record_size,
+ ext2_size, ext_hdr->mailbox_count);
+ return FALSE;
+ }
+
+ ext2_rec = CONST_PTR_OFFSET(ext2_data, ext2_hdr->hdr_size);
+ array_foreach_elem(&mbox->backend_boxes, bbox) {
+ if (bbox->sync_mailbox_idx1 == 0)
+ continue;
+
+ guid_128_copy(bbox->sync_guid,
+ ext2_rec[bbox->sync_mailbox_idx1-1].guid);
+ }
+ return TRUE;
+}
+
+int virtual_mailbox_ext_header_read(struct virtual_mailbox *mbox,
+ struct mail_index_view *view,
+ bool *broken_r)
+{
+ const char *box_path = mailbox_get_path(&mbox->box);
+ const struct virtual_mail_index_header *ext_hdr;
+ const struct mail_index_header *hdr;
+ const struct virtual_mail_index_mailbox_record *mailboxes;
+ struct virtual_backend_box *bbox, **bboxes;
+ const void *ext_data;
+ size_t ext_size;
+ unsigned int i, count, ext_name_offset, ext_mailbox_count;
+ uint32_t prev_mailbox_id;
+ int ret = 1;
+
+ *broken_r = FALSE;
+
+ hdr = mail_index_get_header(view);
+ mail_index_get_header_ext(view, mbox->virtual_ext_id,
+ &ext_data, &ext_size);
+ ext_hdr = ext_data;
+ if (mbox->sync_initialized &&
+ mbox->prev_uid_validity == hdr->uid_validity &&
+ ext_size >= sizeof(*ext_hdr) &&
+ mbox->prev_change_counter == ext_hdr->change_counter) {
+ /* fully refreshed */
+ return 1;
+ }
+
+ mbox->prev_uid_validity = hdr->uid_validity;
+ if (ext_hdr == NULL ||
+ mbox->search_args_crc32 != ext_hdr->search_args_crc32) {
+ mailboxes = NULL;
+ ext_name_offset = 0;
+ ext_mailbox_count = 0;
+ ret = 0;
+ } else {
+ const void *guid_data;
+ size_t guid_size;
+ mail_index_get_header_ext(view, mbox->virtual_guid_ext_id,
+ &guid_data, &guid_size);
+ if (guid_size >= GUID_128_SIZE)
+ guid_128_copy(mbox->guid, guid_data);
+
+ mbox->prev_change_counter = ext_hdr->change_counter;
+ mailboxes = (const void *)(ext_hdr + 1);
+ ext_name_offset = sizeof(*ext_hdr) +
+ ext_hdr->mailbox_count * sizeof(*mailboxes);
+ if (ext_name_offset >= ext_size ||
+ ext_hdr->mailbox_count > INT_MAX/sizeof(*mailboxes)) {
+ i_error("virtual index %s: Broken mailbox_count header",
+ box_path);
+ *broken_r = TRUE;
+ ext_mailbox_count = 0;
+ ret = 0;
+ } else {
+ ext_mailbox_count = ext_hdr->mailbox_count;
+ }
+ }
+
+ /* update mailbox backends */
+ prev_mailbox_id = 0;
+ for (i = 0; i < ext_mailbox_count; i++) {
+ if (mailboxes[i].id > ext_hdr->highest_mailbox_id ||
+ mailboxes[i].id <= prev_mailbox_id) {
+ i_error("virtual index %s: Broken mailbox id",
+ box_path);
+ break;
+ }
+ if (mailboxes[i].name_len == 0 ||
+ mailboxes[i].name_len > ext_size) {
+ i_error("virtual index %s: Broken mailbox name_len",
+ box_path);
+ break;
+ }
+ if (ext_name_offset + mailboxes[i].name_len > ext_size) {
+ i_error("virtual index %s: Broken mailbox list",
+ box_path);
+ break;
+ }
+ T_BEGIN {
+ const unsigned char *nameptr;
+ const char *name;
+
+ nameptr = CONST_PTR_OFFSET(ext_data, ext_name_offset);
+ name = t_strndup(nameptr, mailboxes[i].name_len);
+ if (virtual_sync_get_backend_box(mbox, name, &bbox) < 0)
+ ret = -1;
+ } T_END;
+
+ if (bbox == NULL) {
+ if (ret < 0)
+ return -1;
+ /* mailbox no longer exists. */
+ ret = 0;
+ } else {
+ bbox->mailbox_id = mailboxes[i].id;
+ bbox->sync_uid_validity = mailboxes[i].uid_validity;
+ bbox->ondisk_highest_modseq =
+ bbox->sync_highest_modseq =
+ mailboxes[i].highest_modseq;
+ bbox->sync_next_uid = mailboxes[i].next_uid;
+ bbox->sync_mailbox_idx1 = i+1;
+ }
+ ext_name_offset += mailboxes[i].name_len;
+ prev_mailbox_id = mailboxes[i].id;
+ }
+ if (i < ext_mailbox_count) {
+ *broken_r = TRUE;
+ mbox->ext_header_rewrite = TRUE;
+ ret = 0;
+ }
+ if (!*broken_r && ext_mailbox_count > 0) {
+ if (!virtual_mailbox_ext2_header_read(mbox, view, ext_hdr))
+ mbox->ext_header_rewrite = TRUE;
+ }
+
+ mbox->highest_mailbox_id = ext_hdr == NULL ? 0 :
+ ext_hdr->highest_mailbox_id;
+ /* do not mark it initialized if it's broken */
+ mbox->sync_initialized = !*broken_r;
+
+ /* assign new mailbox IDs if any are missing */
+ bboxes = array_get_modifiable(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->mailbox_id == 0) {
+ bboxes[i]->mailbox_id = ++mbox->highest_mailbox_id;
+ ret = 0;
+ }
+ }
+ /* sort the backend mailboxes by mailbox_id. */
+ array_sort(&mbox->backend_boxes, bbox_mailbox_id_cmp);
+ if (ret == 0)
+ mbox->ext_header_rewrite = TRUE;
+ return ret;
+}
+
+static void virtual_sync_ext_header_rewrite(struct virtual_sync_context *ctx)
+{
+ struct virtual_mail_index_header ext_hdr;
+ struct virtual_mail_index_mailbox_record mailbox;
+ struct virtual_mail_index_mailbox_ext2_record ext2_rec;
+ struct virtual_backend_box **bboxes;
+ buffer_t *buf, *buf2;
+ const void *ext_data;
+ size_t ext_size;
+ unsigned int i, mailbox_pos, name_pos, count;
+
+ bboxes = array_get_modifiable(&ctx->mbox->backend_boxes, &count);
+ mailbox_pos = sizeof(ext_hdr);
+ name_pos = mailbox_pos + sizeof(mailbox) * count;
+
+ i_zero(&ext_hdr);
+ i_zero(&mailbox);
+ i_zero(&ext2_rec);
+
+ ext_hdr.change_counter = ++ctx->mbox->prev_change_counter;
+ ext_hdr.mailbox_count = count;
+ ext_hdr.highest_mailbox_id = ctx->mbox->highest_mailbox_id;
+ ext_hdr.search_args_crc32 = ctx->mbox->search_args_crc32;
+
+ buf = buffer_create_dynamic(default_pool, name_pos + 256);
+ buffer_append(buf, &ext_hdr, sizeof(ext_hdr));
+
+ struct virtual_mail_index_ext2_header ext2_hdr = {
+ .version = VIRTUAL_MAIL_INDEX_EXT2_HEADER_VERSION,
+ .ext_record_size = sizeof(struct virtual_mail_index_mailbox_ext2_record),
+ .hdr_size = sizeof(struct virtual_mail_index_ext2_header),
+ .change_counter = ext_hdr.change_counter,
+ };
+ buf2 = buffer_create_dynamic(default_pool, sizeof(ext2_hdr) +
+ sizeof(ext2_rec) * count);
+ buffer_append(buf2, &ext2_hdr, sizeof(ext2_hdr));
+
+ for (i = 0; i < count; i++) {
+ i_assert(i == 0 ||
+ bboxes[i]->mailbox_id > bboxes[i-1]->mailbox_id);
+
+ bboxes[i]->sync_mailbox_idx1 = i+1;
+ mailbox.id = bboxes[i]->mailbox_id;
+ mailbox.name_len = strlen(bboxes[i]->name);
+ mailbox.uid_validity = bboxes[i]->sync_uid_validity;
+ mailbox.highest_modseq = bboxes[i]->ondisk_highest_modseq;
+ mailbox.next_uid = bboxes[i]->sync_next_uid;
+ buffer_write(buf, mailbox_pos, &mailbox, sizeof(mailbox));
+ buffer_write(buf, name_pos, bboxes[i]->name, mailbox.name_len);
+
+ guid_128_copy(ext2_rec.guid, bboxes[i]->sync_guid);
+ buffer_append(buf2, &ext2_rec, sizeof(ext2_rec));
+
+ mailbox_pos += sizeof(mailbox);
+ name_pos += mailbox.name_len;
+
+ /* repair the value */
+ if (ctx->mbox->highest_mailbox_id < mailbox.id)
+ ctx->mbox->highest_mailbox_id = mailbox.id;
+ }
+ if (ctx->mbox->highest_mailbox_id != ext_hdr.highest_mailbox_id) {
+ ext_hdr.highest_mailbox_id = ctx->mbox->highest_mailbox_id;
+ buffer_write(buf, 0, &ext_hdr, sizeof(ext_hdr));
+ }
+ i_assert(buf->used == name_pos);
+
+ /* update base extension */
+ mail_index_get_header_ext(ctx->sync_view, ctx->mbox->virtual_ext_id,
+ &ext_data, &ext_size);
+ if (ext_size < name_pos) {
+ mail_index_ext_resize(ctx->trans, ctx->mbox->virtual_ext_id,
+ name_pos,
+ sizeof(struct virtual_mail_index_record),
+ sizeof(uint32_t));
+ }
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext_id,
+ 0, buf->data, name_pos);
+
+ /* update ext2 */
+ mail_index_get_header_ext(ctx->sync_view, ctx->mbox->virtual_ext2_id,
+ &ext_data, &ext_size);
+ if (ext_size != buf2->used) {
+ mail_index_ext_resize(ctx->trans, ctx->mbox->virtual_ext2_id,
+ buf2->used, 0, 0);
+ }
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext2_id,
+ 0, buf2->data, buf2->used);
+ buffer_free(&buf);
+ buffer_free(&buf2);
+}
+
+static void virtual_sync_ext_header_update(struct virtual_sync_context *ctx)
+{
+ struct virtual_mail_index_header ext_hdr;
+
+ if (!ctx->ext_header_changed)
+ return;
+
+ /* we changed something - update the change counter in header */
+ ext_hdr.change_counter = ++ctx->mbox->prev_change_counter;
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext_id,
+ offsetof(struct virtual_mail_index_header, change_counter),
+ &ext_hdr.change_counter, sizeof(ext_hdr.change_counter));
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext2_id,
+ offsetof(struct virtual_mail_index_ext2_header, change_counter),
+ &ext_hdr.change_counter, sizeof(ext_hdr.change_counter));
+}
+
+static int virtual_sync_index_rec(struct virtual_sync_context *ctx,
+ const struct mail_index_sync_rec *sync_rec)
+{
+ uint32_t virtual_ext_id = ctx->mbox->virtual_ext_id;
+ struct virtual_backend_box *bbox;
+ const struct virtual_mail_index_record *vrec;
+ const void *data;
+ enum mail_flags flags;
+ struct mail_keywords *keywords;
+ enum modify_type modify_type;
+ const char *kw_names[2];
+ uint32_t vseq, seq1, seq2;
+
+ switch (sync_rec->type) {
+ case MAIL_INDEX_SYNC_TYPE_EXPUNGE:
+ case MAIL_INDEX_SYNC_TYPE_FLAGS:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ break;
+ }
+ if (!mail_index_lookup_seq_range(ctx->sync_view,
+ sync_rec->uid1, sync_rec->uid2,
+ &seq1, &seq2)) {
+ /* already expunged, nothing to do. */
+ return 0;
+ }
+
+ for (vseq = seq1; vseq <= seq2; vseq++) {
+ mail_index_lookup_ext(ctx->sync_view, vseq, virtual_ext_id,
+ &data, NULL);
+ vrec = data;
+
+ if (!virtual_backend_box_lookup(ctx->mbox, vrec->mailbox_id,
+ &bbox))
+ continue;
+ if (!bbox->box->opened) {
+ if (virtual_backend_box_open(ctx->mbox, bbox) < 0) {
+ virtual_box_copy_error(&ctx->mbox->box,
+ bbox->box);
+ return -1;
+ }
+ } else {
+ virtual_backend_box_accessed(ctx->mbox, bbox);
+ }
+
+ virtual_backend_box_sync_mail_set(bbox);
+ if (!mail_set_uid(bbox->sync_mail, vrec->real_uid)) {
+ /* message is already expunged from backend mailbox. */
+ continue;
+ }
+
+ switch (sync_rec->type) {
+ case MAIL_INDEX_SYNC_TYPE_EXPUNGE:
+ mail_expunge(bbox->sync_mail);
+ break;
+ case MAIL_INDEX_SYNC_TYPE_FLAGS:
+ flags = sync_rec->add_flags & MAIL_FLAGS_NONRECENT;
+ if (flags != 0) {
+ mail_update_flags(bbox->sync_mail,
+ MODIFY_ADD, flags);
+ }
+ flags = sync_rec->remove_flags & MAIL_FLAGS_NONRECENT;
+ if (flags != 0) {
+ mail_update_flags(bbox->sync_mail,
+ MODIFY_REMOVE, flags);
+ }
+ break;
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ kw_names[0] = ctx->kw_all[sync_rec->keyword_idx];
+ kw_names[1] = NULL;
+ keywords = mailbox_keywords_create_valid(bbox->box,
+ kw_names);
+
+ modify_type = sync_rec->type ==
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD ?
+ MODIFY_ADD : MODIFY_REMOVE;
+ mail_update_keywords(bbox->sync_mail,
+ modify_type, keywords);
+ mailbox_keywords_unref(&keywords);
+ break;
+ }
+ }
+ return 0;
+}
+
+static int virtual_sync_index_changes(struct virtual_sync_context *ctx)
+{
+ const ARRAY_TYPE(keywords) *keywords;
+ struct mail_index_sync_rec sync_rec;
+
+ keywords = mail_index_get_keywords(ctx->index);
+ ctx->kw_all = array_count(keywords) == 0 ? NULL :
+ array_front(keywords);
+ while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec)) {
+ if (virtual_sync_index_rec(ctx, &sync_rec) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void virtual_sync_index_finish(struct virtual_sync_context *ctx)
+{
+ struct mailbox *box = &ctx->mbox->box;
+ const struct mail_index_header *hdr;
+ struct mail_index_view *view;
+ uint32_t seq1, seq2;
+
+ view = mail_index_transaction_open_updated_view(ctx->trans);
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ if (hdr->uid_validity != 0)
+ ctx->uid_validity = hdr->uid_validity;
+ else
+ virtual_sync_set_uidvalidity(ctx);
+
+ /* mark the newly seen messages as recent */
+ if (mail_index_lookup_seq_range(view, hdr->first_recent_uid,
+ (uint32_t)-1, &seq1, &seq2)) {
+ mailbox_recent_flags_set_seqs(&ctx->mbox->box, view,
+ seq1, seq2);
+ }
+
+ mail_index_view_close(&view);
+
+ if (ctx->mbox->ext_header_rewrite) {
+ /* entire mailbox list needs to be rewritten */
+ virtual_sync_ext_header_rewrite(ctx);
+ } else {
+ /* update only changed parts in the header */
+ virtual_sync_ext_header_update(ctx);
+ }
+
+ mailbox_sync_notify(box, 0, 0);
+}
+
+static int virtual_sync_backend_box_init(struct virtual_backend_box *bbox)
+{
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ struct virtual_backend_uidmap uidmap;
+ enum mailbox_search_result_flags result_flags;
+ int ret;
+
+ trans = mailbox_transaction_begin(bbox->box, 0, __func__);
+
+ if (!bbox->search_args_initialized) {
+ mail_search_args_init(bbox->search_args, bbox->box, FALSE, NULL);
+ bbox->search_args_initialized = TRUE;
+ }
+ search_ctx = mailbox_search_init(trans, bbox->search_args, NULL,
+ 0, NULL);
+
+ /* save the result and keep it updated */
+ result_flags = MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC;
+ bbox->search_result =
+ mailbox_search_result_save(search_ctx, result_flags);
+
+ /* add the found UIDs to uidmap. virtual_uid gets assigned later. */
+ i_zero(&uidmap);
+ array_clear(&bbox->uids);
+ while (mailbox_search_next(search_ctx, &mail)) {
+ uidmap.real_uid = mail->uid;
+ array_push_back(&bbox->uids, &uidmap);
+ }
+ ret = mailbox_search_deinit(&search_ctx);
+
+ (void)mailbox_transaction_commit(&trans);
+ return ret;
+}
+
+static int
+virtual_backend_uidmap_bsearch_cmp(const uint32_t *uidp,
+ const struct virtual_backend_uidmap *uidmap)
+{
+ return *uidp < uidmap->real_uid ? -1 :
+ (*uidp > uidmap->real_uid ? 1 : 0);
+}
+
+static void
+virtual_sync_mailbox_box_remove(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ const ARRAY_TYPE(seq_range) *removed_uids)
+{
+ const struct seq_range *uids;
+ struct virtual_backend_uidmap *uidmap;
+ unsigned int i, src, dest, uid_count, rec_count;
+ uint32_t uid, vseq;
+
+ uids = array_get(removed_uids, &uid_count);
+ if (uid_count == 0)
+ return;
+
+ /* everything in removed_uids should exist in bbox->uids */
+ uidmap = array_get_modifiable(&bbox->uids, &rec_count);
+ i_assert(rec_count >= uid_count);
+
+ /* find the first uidmap record to be removed */
+ if (!array_bsearch_insert_pos(&bbox->uids, &uids[0].seq1,
+ virtual_backend_uidmap_bsearch_cmp, &src))
+ i_unreached();
+
+ /* remove the unwanted messages */
+ dest = src;
+ for (i = 0; i < uid_count; i++) {
+ uid = uids[i].seq1;
+ while (uidmap[src].real_uid != uid) {
+ uidmap[dest++] = uidmap[src++];
+ i_assert(src < rec_count);
+ }
+
+ for (; uid <= uids[i].seq2; uid++, src++) {
+ i_assert(src < rec_count);
+ i_assert(uidmap[src].real_uid == uid);
+ if (uidmap[src].virtual_uid == 0) {
+ /* has not been assigned yet */
+ continue;
+ }
+ if (mail_index_lookup_seq(ctx->sync_view,
+ uidmap[src].virtual_uid,
+ &vseq))
+ mail_index_expunge(ctx->trans, vseq);
+ }
+ }
+ array_delete(&bbox->uids, dest, src - dest);
+}
+
+static void
+virtual_sync_mailbox_box_add(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ const ARRAY_TYPE(seq_range) *added_uids_arr)
+{
+ const struct seq_range *added_uids;
+ struct virtual_backend_uidmap *uidmap;
+ struct virtual_add_record rec;
+ unsigned int i, src, dest, uid_count, add_count, rec_count;
+ uint32_t add_uid;
+
+ added_uids = array_get(added_uids_arr, &uid_count);
+ if (uid_count == 0)
+ return;
+ add_count = seq_range_count(added_uids_arr);
+
+ /* none of added_uids should exist in bbox->uids. find the position
+ of the first inserted index. */
+ uidmap = array_get_modifiable(&bbox->uids, &rec_count);
+ if (rec_count == 0 ||
+ added_uids[0].seq1 > uidmap[rec_count-1].real_uid) {
+ /* fast path: usually messages are appended */
+ dest = rec_count;
+ } else if (array_bsearch_insert_pos(&bbox->uids, &added_uids[0].seq1,
+ virtual_backend_uidmap_bsearch_cmp,
+ &dest))
+ i_unreached();
+
+ /* make space for all added UIDs. */
+ if (rec_count == dest)
+ array_idx_clear(&bbox->uids, dest + add_count-1);
+ else {
+ array_copy(&bbox->uids.arr, dest + add_count,
+ &bbox->uids.arr, dest, rec_count - dest);
+ }
+ uidmap = array_get_modifiable(&bbox->uids, &rec_count);
+ src = dest + add_count;
+
+ /* add/move the UIDs to their correct positions */
+ i_zero(&rec);
+ rec.rec.mailbox_id = bbox->mailbox_id;
+ for (i = 0; i < uid_count; i++) {
+ add_uid = added_uids[i].seq1;
+ while (src < rec_count && uidmap[src].real_uid < add_uid)
+ uidmap[dest++] = uidmap[src++];
+
+ for (; add_uid <= added_uids[i].seq2; add_uid++, dest++) {
+ i_assert(dest < rec_count);
+
+ uidmap[dest].real_uid = add_uid;
+ uidmap[dest].virtual_uid = 0;
+
+ if (ctx->mbox->uids_mapped) {
+ rec.rec.real_uid = add_uid;
+ array_push_back(&ctx->all_adds, &rec);
+ }
+ }
+ }
+}
+
+static void
+virtual_sync_mailbox_box_update_flags(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ const ARRAY_TYPE(seq_range) *uids_arr)
+{
+ unsigned int i, uid, vseq;
+ struct virtual_backend_uidmap *vuid;
+ struct seq_range_iter iter;
+
+ i = 0;
+ seq_range_array_iter_init(&iter, uids_arr);
+ while(seq_range_array_iter_nth(&iter, i++, &uid)) {
+ vuid = array_bsearch(&bbox->uids, &uid,
+ virtual_backend_uidmap_bsearch_cmp);
+ if (vuid == NULL ||
+ vuid->virtual_uid == 0 ||
+ !mail_index_lookup_seq(ctx->sync_view,
+ vuid->virtual_uid, &vseq)) {
+ /* the entry has been already removed either by
+ us or some other session. doesn't matter,
+ we don't need to update the flags.
+
+ it might also have not yet been assigned a uid
+ so we don't want to update the flags then either.
+ */
+ continue;
+ }
+ virtual_sync_external_flags(ctx, bbox, vseq,
+ vuid->real_uid);
+ }
+}
+
+static int virtual_backend_uidmap_cmp(const struct virtual_backend_uidmap *u1,
+ const struct virtual_backend_uidmap *u2)
+{
+ if (u1->real_uid < u2->real_uid)
+ return -1;
+ if (u1->real_uid > u2->real_uid)
+ return 1;
+ return 0;
+}
+
+static void virtual_sync_bbox_uids_sort(struct virtual_backend_box *bbox)
+{
+ /* the uidmap must be sorted by real_uids */
+ array_sort(&bbox->uids, virtual_backend_uidmap_cmp);
+ bbox->uids_nonsorted = FALSE;
+}
+
+static void virtual_sync_backend_boxes_sort_uids(struct virtual_mailbox *mbox)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ bboxes = array_get(&mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (bboxes[i]->uids_nonsorted)
+ virtual_sync_bbox_uids_sort(bboxes[i]);
+ }
+}
+
+static void
+virtual_sync_backend_add_vmsgs_results(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ uint32_t real_uid,
+ struct mail_search_result *result,
+ const uint32_t vseq)
+{
+ struct virtual_backend_uidmap uidmap;
+ uint32_t vuid, seq;
+
+ mail_index_lookup_uid(ctx->sync_view, vseq, &vuid);
+
+ i_zero(&uidmap);
+ uidmap.real_uid = real_uid;
+ uidmap.virtual_uid = vuid;
+ array_push_back(&bbox->uids, &uidmap);
+
+ if (result == NULL)
+ ;
+ else if (mail_index_lookup_seq(bbox->box->view, real_uid, &seq))
+ seq_range_array_add(&result->uids, real_uid);
+ else
+ seq_range_array_add(&result->removed_uids, real_uid);
+}
+
+static void
+virtual_sync_backend_handle_old_vmsgs(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ struct mail_search_result *result)
+{
+ const struct virtual_mail_index_record *vrec;
+ const struct virtual_sync_mail *sync_mail, *sync_mails;
+ const void *data;
+ uint32_t i, vseq, messages;
+
+ /* find the messages that currently exist in virtual index and add them
+ to the backend mailbox's list of uids. */
+ array_clear(&bbox->uids);
+
+ if (array_is_created(&ctx->all_mails)) {
+ i_assert(ctx->all_mails_prev_mailbox_id < bbox->mailbox_id);
+ sync_mails = array_get(&ctx->all_mails, &messages);
+ for (i = ctx->all_mails_idx; i < messages; i++) {
+ sync_mail = &sync_mails[i];
+ if (sync_mail->vrec.mailbox_id != bbox->mailbox_id) {
+ if (sync_mail->vrec.mailbox_id < bbox->mailbox_id) {
+ /* stale mailbox_id, ignore */
+ continue;
+ }
+ /* Should be in mailbox_id order,
+ so skip to next box */
+ break;
+ }
+
+ virtual_sync_backend_add_vmsgs_results(ctx, bbox,
+ sync_mail->vrec.real_uid, result, sync_mail->vseq);
+ }
+ ctx->all_mails_idx = i;
+ ctx->all_mails_prev_mailbox_id = bbox->mailbox_id;
+ } else {
+ /* there should be only a single backend mailbox, but in the
+ existing index there may be stale mailbox_ids that we'll
+ just skip over. */
+ messages = mail_index_view_get_messages_count(ctx->sync_view);
+ for (vseq = 1; vseq <= messages; vseq++) {
+ mail_index_lookup_ext(ctx->sync_view, vseq,
+ ctx->mbox->virtual_ext_id, &data, NULL);
+ vrec = data;
+ if (vrec->mailbox_id == bbox->mailbox_id) {
+ virtual_sync_backend_add_vmsgs_results(ctx,
+ bbox, vrec->real_uid, result, vseq);
+ }
+ }
+ }
+ virtual_sync_bbox_uids_sort(bbox);
+}
+
+static int virtual_sync_backend_box_continue(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox)
+{
+ const enum mailbox_search_result_flags result_flags =
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC;
+ struct mail_index_view *view = bbox->box->view;
+ struct mail_search_result *result;
+ ARRAY_TYPE(seq_range) expunged_uids = ARRAY_INIT, removed_uids;
+ ARRAY_TYPE(seq_range) added_uids, flag_update_uids;
+ uint64_t modseq, old_highest_modseq;
+ uint32_t seq, uid, old_msg_count;
+
+ /* initialize the search result from all the existing messages in
+ virtual index. */
+ if (!bbox->search_args_initialized) {
+ mail_search_args_init(bbox->search_args, bbox->box, FALSE, NULL);
+ bbox->search_args_initialized = TRUE;
+ }
+ result = mailbox_search_result_alloc(bbox->box, bbox->search_args,
+ result_flags);
+ mailbox_search_result_initial_done(result);
+ i_assert(array_count(&result->removed_uids) == 0);
+ virtual_sync_backend_handle_old_vmsgs(ctx, bbox, result);
+ if (array_count(&result->removed_uids) > 0) {
+ /* these are all expunged messages. treat them separately from
+ "no longer matching messages" (=removed_uids) */
+ t_array_init(&expunged_uids, array_count(&result->removed_uids));
+ array_append_array(&expunged_uids, &result->removed_uids);
+ array_clear(&result->removed_uids);
+ }
+
+ /* get list of changed old messages (messages already once seen by
+ virtual index), based on modseq changes. (we'll assume all modseq
+ changes are due to flag changes, which may not be true in future) */
+ if (bbox->sync_next_uid <= 1 ||
+ !mail_index_lookup_seq_range(view, 1, bbox->sync_next_uid-1,
+ &seq, &old_msg_count))
+ old_msg_count = 0;
+ old_highest_modseq = mail_index_modseq_get_highest(view);
+
+ t_array_init(&flag_update_uids, I_MIN(128, old_msg_count));
+ if (bbox->sync_highest_modseq < old_highest_modseq) {
+ for (seq = 1; seq <= old_msg_count; seq++) {
+ modseq = mail_index_modseq_lookup(view, seq);
+ if (modseq > bbox->sync_highest_modseq) {
+ mail_index_lookup_uid(view, seq, &uid);
+ seq_range_array_add(&flag_update_uids, uid);
+ }
+ }
+ }
+
+ /* update the search result based on the flag changes and
+ new messages */
+ if (index_search_result_update_flags(result, &flag_update_uids) < 0 ||
+ index_search_result_update_appends(result, old_msg_count) < 0) {
+ mailbox_search_result_free(&result);
+ return -1;
+ }
+
+ t_array_init(&removed_uids, 128);
+ t_array_init(&added_uids, 128);
+ mailbox_search_result_sync(result, &removed_uids, &added_uids);
+ if (array_is_created(&expunged_uids)) {
+ seq_range_array_remove_seq_range(&removed_uids, &expunged_uids);
+ virtual_sync_mailbox_box_remove(ctx, bbox, &expunged_uids);
+ }
+ if (ctx->expunge_removed)
+ virtual_sync_mailbox_box_remove(ctx, bbox, &removed_uids);
+ else {
+ /* delayed remove */
+ seq_range_array_merge(&bbox->sync_pending_removes,
+ &removed_uids);
+ }
+ virtual_sync_mailbox_box_add(ctx, bbox, &added_uids);
+ virtual_sync_mailbox_box_update_flags(ctx, bbox, &flag_update_uids);
+
+ bbox->search_result = result;
+ return 0;
+}
+
+static void virtual_sync_drop_existing(struct virtual_backend_box *bbox,
+ ARRAY_TYPE(seq_range) *added_uids)
+{
+ ARRAY_TYPE(seq_range) drop_uids;
+ const struct virtual_backend_uidmap *uidmap;
+ struct seq_range_iter iter;
+ unsigned int i, n = 0, count;
+ uint32_t add_uid;
+
+ seq_range_array_iter_init(&iter, added_uids);
+ if (!seq_range_array_iter_nth(&iter, n++, &add_uid))
+ return;
+
+ (void)array_bsearch_insert_pos(&bbox->uids, &add_uid,
+ virtual_backend_uidmap_bsearch_cmp, &i);
+
+ uidmap = array_get_modifiable(&bbox->uids, &count);
+ if (i == count)
+ return;
+
+ t_array_init(&drop_uids, array_count(added_uids));
+ for (; i < count; ) {
+ if (uidmap[i].real_uid < add_uid) {
+ i++;
+ continue;
+ }
+ if (uidmap[i].real_uid == add_uid) {
+ seq_range_array_add(&drop_uids, add_uid);
+ i++;
+ }
+ if (!seq_range_array_iter_nth(&iter, n++, &add_uid))
+ break;
+ }
+ seq_range_array_remove_seq_range(added_uids, &drop_uids);
+}
+
+static void virtual_sync_drop_nonexistent(struct virtual_backend_box *bbox,
+ ARRAY_TYPE(seq_range) *removed_uids)
+{
+ ARRAY_TYPE(seq_range) drop_uids;
+ const struct virtual_backend_uidmap *uidmap;
+ struct seq_range_iter iter;
+ unsigned int i, n = 0, count;
+ uint32_t remove_uid;
+ bool iter_done = FALSE;
+
+ seq_range_array_iter_init(&iter, removed_uids);
+ if (!seq_range_array_iter_nth(&iter, n++, &remove_uid))
+ return;
+
+ (void)array_bsearch_insert_pos(&bbox->uids, &remove_uid,
+ virtual_backend_uidmap_bsearch_cmp, &i);
+
+ t_array_init(&drop_uids, array_count(removed_uids)); iter_done = FALSE;
+ uidmap = array_get_modifiable(&bbox->uids, &count);
+ for (; i < count; ) {
+ if (uidmap[i].real_uid < remove_uid) {
+ i++;
+ continue;
+ }
+ if (uidmap[i].real_uid != remove_uid)
+ seq_range_array_add(&drop_uids, remove_uid);
+ else
+ i++;
+ if (!seq_range_array_iter_nth(&iter, n++, &remove_uid)) {
+ iter_done = TRUE;
+ break;
+ }
+ }
+ if (!iter_done) {
+ do {
+ seq_range_array_add(&drop_uids, remove_uid);
+ } while (seq_range_array_iter_nth(&iter, n++, &remove_uid));
+ }
+ seq_range_array_remove_seq_range(removed_uids, &drop_uids);
+}
+
+static void virtual_sync_mailbox_box_update(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox)
+{
+ ARRAY_TYPE(seq_range) removed_uids, added_uids, temp_uids;
+ unsigned int count1, count2;
+
+ t_array_init(&removed_uids, 128);
+ t_array_init(&added_uids, 128);
+
+ mailbox_search_result_sync(bbox->search_result,
+ &removed_uids, &added_uids);
+ if (array_is_created(&bbox->sync_outside_expunges)) {
+ seq_range_array_remove_seq_range(&bbox->sync_outside_expunges,
+ &added_uids);
+ seq_range_array_merge(&removed_uids,
+ &bbox->sync_outside_expunges);
+ array_clear(&bbox->sync_outside_expunges);
+ }
+
+ virtual_sync_drop_existing(bbox, &added_uids);
+ virtual_sync_drop_nonexistent(bbox, &removed_uids);
+
+ /* if any of the pending removes came back, we don't want to expunge
+ them anymore. also since they already exist, remove them from
+ added_uids. */
+ count1 = array_count(&bbox->sync_pending_removes);
+ count2 = array_count(&added_uids);
+ if (count1 > 0 && count2 > 0) {
+ t_array_init(&temp_uids, count1);
+ array_append_array(&temp_uids, &bbox->sync_pending_removes);
+ if (seq_range_array_remove_seq_range(
+ &bbox->sync_pending_removes, &added_uids) > 0) {
+ seq_range_array_remove_seq_range(&added_uids,
+ &temp_uids);
+ }
+ }
+
+ if (!ctx->expunge_removed) {
+ /* delay removing messages that don't match the search
+ criteria, but don't delay removing expunged messages */
+ if (array_count(&ctx->sync_expunges) > 0) {
+ seq_range_array_remove_seq_range(&bbox->sync_pending_removes,
+ &ctx->sync_expunges);
+ seq_range_array_remove_seq_range(&removed_uids,
+ &ctx->sync_expunges);
+ virtual_sync_mailbox_box_remove(ctx, bbox,
+ &ctx->sync_expunges);
+ }
+ seq_range_array_merge(&bbox->sync_pending_removes,
+ &removed_uids);
+ } else if (array_count(&bbox->sync_pending_removes) > 0) {
+ /* remove all current and old */
+ seq_range_array_merge(&bbox->sync_pending_removes,
+ &removed_uids);
+ virtual_sync_mailbox_box_remove(ctx, bbox,
+ &bbox->sync_pending_removes);
+ array_clear(&bbox->sync_pending_removes);
+ } else {
+ virtual_sync_mailbox_box_remove(ctx, bbox, &removed_uids);
+ }
+ virtual_sync_mailbox_box_add(ctx, bbox, &added_uids);
+}
+
+static bool virtual_sync_find_seqs(struct virtual_backend_box *bbox,
+ const struct mailbox_sync_rec *sync_rec,
+ unsigned int *idx1_r,
+ unsigned int *idx2_r)
+{
+ const struct virtual_backend_uidmap *uidmap;
+ unsigned int idx, count;
+ uint32_t uid1, uid2;
+
+ mail_index_lookup_uid(bbox->box->view, sync_rec->seq1, &uid1);
+ mail_index_lookup_uid(bbox->box->view, sync_rec->seq2, &uid2);
+ (void)array_bsearch_insert_pos(&bbox->uids, &uid1,
+ virtual_backend_uidmap_bsearch_cmp,
+ &idx);
+
+ uidmap = array_get_modifiable(&bbox->uids, &count);
+ if (idx == count || uidmap[idx].real_uid > uid2)
+ return FALSE;
+
+ *idx1_r = idx;
+ while (idx < count && uidmap[idx].real_uid <= uid2) idx++;
+ *idx2_r = idx - 1;
+ return TRUE;
+}
+
+static void virtual_sync_expunge_add(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ const struct mailbox_sync_rec *sync_rec)
+{
+ struct virtual_backend_uidmap *uidmap;
+ uint32_t uid1, uid2;
+ unsigned int i, idx1, count;
+
+ mail_index_lookup_uid(bbox->box->view, sync_rec->seq1, &uid1);
+ mail_index_lookup_uid(bbox->box->view, sync_rec->seq2, &uid2);
+
+ /* remember only the expunges for messages that
+ already exist for this mailbox */
+ (void)array_bsearch_insert_pos(&bbox->uids, &uid1,
+ virtual_backend_uidmap_bsearch_cmp,
+ &idx1);
+ uidmap = array_get_modifiable(&bbox->uids, &count);
+ for (i = idx1; i < count; i++) {
+ if (uidmap[i].real_uid > uid2)
+ break;
+ seq_range_array_add(&ctx->sync_expunges, uidmap[i].real_uid);
+ }
+}
+
+static int virtual_sync_backend_box_sync(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ enum mailbox_sync_flags sync_flags)
+{
+ struct mailbox_sync_context *sync_ctx;
+ const struct virtual_backend_uidmap *uidmap;
+ struct mailbox_sync_rec sync_rec;
+ struct mailbox_sync_status sync_status;
+ unsigned int idx1, idx2;
+ uint32_t vseq, vuid;
+
+ sync_ctx = mailbox_sync_init(bbox->box, sync_flags);
+ virtual_backend_box_sync_mail_set(bbox);
+ while (mailbox_sync_next(sync_ctx, &sync_rec)) {
+ switch (sync_rec.type) {
+ case MAILBOX_SYNC_TYPE_EXPUNGE:
+ if (ctx->expunge_removed) {
+ /* no need to keep track of expunges */
+ break;
+ }
+ virtual_sync_expunge_add(ctx, bbox, &sync_rec);
+ break;
+ case MAILBOX_SYNC_TYPE_FLAGS:
+ if (!virtual_sync_find_seqs(bbox, &sync_rec,
+ &idx1, &idx2))
+ break;
+ uidmap = array_front(&bbox->uids);
+ for (; idx1 <= idx2; idx1++) {
+ vuid = uidmap[idx1].virtual_uid;
+ if (vuid == 0) {
+ /* has not been even assigned yet */
+ continue;
+ }
+ if (!mail_index_lookup_seq(ctx->sync_view,
+ vuid, &vseq)) {
+ /* expunged by another session,
+ but we haven't yet updated
+ bbox->uids. */
+ continue;
+ }
+ virtual_sync_external_flags(ctx, bbox, vseq,
+ uidmap[idx1].real_uid);
+ }
+ break;
+ case MAILBOX_SYNC_TYPE_MODSEQ:
+ break;
+ }
+ }
+ if (mailbox_sync_deinit(&sync_ctx, &sync_status) < 0) {
+ if (mailbox_get_last_mail_error(bbox->box) != MAIL_ERROR_NOTFOUND)
+ return -1;
+ /* mailbox was deleted */
+ virtual_sync_backend_box_deleted(ctx, bbox);
+ return 0;
+ }
+ return 0;
+}
+
+static bool
+virtual_bbox_mailbox_equals(struct virtual_backend_box *bbox,
+ const struct mailbox_status *status,
+ struct mailbox_metadata *metadata,
+ const char **reason_r)
+{
+ if (!guid_128_equals(bbox->sync_guid, metadata->guid)) {
+ *reason_r = t_strdup_printf("GUID changed: %s -> %s",
+ guid_128_to_string(bbox->sync_guid),
+ guid_128_to_string(metadata->guid));
+ return FALSE;
+ }
+ if (bbox->sync_uid_validity != status->uidvalidity) {
+ *reason_r = t_strdup_printf("UIDVALIDITY changed: %u -> %u",
+ bbox->sync_uid_validity, status->uidvalidity);
+ return FALSE;
+ }
+ if (bbox->sync_next_uid != status->uidnext) {
+ *reason_r = t_strdup_printf("UIDNEXT changed: %u -> %u",
+ bbox->sync_next_uid, status->uidnext);
+ return FALSE;
+ }
+ if (bbox->sync_highest_modseq != status->highest_modseq) {
+ *reason_r = t_strdup_printf("HIGHESTMODSEQ changed: "
+ "%"PRIu64" -> %"PRIu64,
+ bbox->sync_highest_modseq, status->highest_modseq);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void virtual_sync_backend_ext_header(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox)
+{
+ const unsigned int uidval_pos =
+ offsetof(struct virtual_mail_index_mailbox_record,
+ uid_validity);
+ struct mailbox_status status;
+ struct virtual_mail_index_mailbox_record mailbox;
+ struct virtual_mail_index_mailbox_ext2_record ext2;
+ unsigned int mailbox_offset, ext2_offset;
+ uint64_t wanted_ondisk_highest_modseq;
+ struct mailbox_metadata metadata;
+ const char *reason;
+
+ mailbox_get_open_status(bbox->box, STATUS_UIDVALIDITY |
+ STATUS_HIGHESTMODSEQ, &status);
+ wanted_ondisk_highest_modseq =
+ array_count(&bbox->sync_pending_removes) > 0 ? 0 :
+ status.highest_modseq;
+
+ if (mailbox_get_metadata(bbox->box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ /* Either a temporary failure or the mailbox was already
+ deleted. Either way, it doesn't really matter at this point.
+ We'll just leave the error handling until the next sync. */
+ return;
+ }
+
+ if (virtual_bbox_mailbox_equals(bbox, &status, &metadata, &reason) &&
+ bbox->ondisk_highest_modseq == wanted_ondisk_highest_modseq)
+ return;
+
+ /* mailbox changed - update extension header */
+ bbox->sync_uid_validity = status.uidvalidity;
+ bbox->sync_highest_modseq = status.highest_modseq;
+ bbox->ondisk_highest_modseq = wanted_ondisk_highest_modseq;
+ bbox->sync_next_uid = status.uidnext;
+ guid_128_copy(bbox->sync_guid, metadata.guid);
+
+ if (ctx->mbox->ext_header_rewrite) {
+ /* we'll rewrite the entire header later */
+ return;
+ }
+
+ i_zero(&mailbox);
+ mailbox.uid_validity = bbox->sync_uid_validity;
+ mailbox.highest_modseq = bbox->ondisk_highest_modseq;
+ mailbox.next_uid = bbox->sync_next_uid;
+
+ i_zero(&ext2);
+ guid_128_copy(ext2.guid, bbox->sync_guid);
+
+ i_assert(bbox->sync_mailbox_idx1 > 0);
+ mailbox_offset = sizeof(struct virtual_mail_index_header) +
+ (bbox->sync_mailbox_idx1-1) * sizeof(mailbox);
+ ext2_offset = sizeof(struct virtual_mail_index_ext2_header) +
+ (bbox->sync_mailbox_idx1-1) * sizeof(ext2);
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext_id,
+ mailbox_offset + uidval_pos,
+ CONST_PTR_OFFSET(&mailbox, uidval_pos),
+ sizeof(mailbox) - uidval_pos);
+ mail_index_update_header_ext(ctx->trans, ctx->mbox->virtual_ext2_id,
+ ext2_offset, &ext2, sizeof(ext2));
+ ctx->ext_header_changed = TRUE;
+}
+
+static void virtual_sync_backend_box_deleted(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox)
+{
+ ARRAY_TYPE(seq_range) removed_uids;
+ const struct virtual_backend_uidmap *uidmap;
+
+ /* delay its full removal until the next time we open the virtual
+ mailbox. for now just treat it as if it was empty. */
+
+ t_array_init(&removed_uids, 128);
+ array_foreach(&bbox->uids, uidmap)
+ seq_range_array_add(&removed_uids, uidmap->real_uid);
+ virtual_sync_mailbox_box_remove(ctx, bbox, &removed_uids);
+
+ bbox->deleted = TRUE;
+}
+
+static int
+virtual_try_open_and_sync_backend_box(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox,
+ enum mailbox_sync_flags sync_flags)
+{
+ int ret = 0;
+
+ if (!bbox->box->opened)
+ ret = virtual_backend_box_open(ctx->mbox, bbox);
+ if (ret == 0)
+ ret = mailbox_sync(bbox->box, sync_flags);
+ if (ret < 0) {
+ if (mailbox_get_last_mail_error(bbox->box) != MAIL_ERROR_NOTFOUND)
+ return -1;
+ /* mailbox was deleted */
+ virtual_sync_backend_box_deleted(ctx, bbox);
+ return 0;
+ }
+ return 1;
+}
+
+static int virtual_sync_backend_box(struct virtual_sync_context *ctx,
+ struct virtual_backend_box *bbox)
+{
+ enum mailbox_sync_flags sync_flags;
+ struct mailbox_status status;
+ const char *reason;
+ int ret;
+
+ if (bbox->deleted)
+ return 0;
+
+ /* if we already did some changes to index, commit them before
+ syncing starts. */
+ virtual_backend_box_sync_mail_unset(bbox);
+
+ sync_flags = ctx->flags & (MAILBOX_SYNC_FLAG_FULL_READ |
+ MAILBOX_SYNC_FLAG_FULL_WRITE |
+ MAILBOX_SYNC_FLAG_FAST);
+
+ if (bbox->search_result == NULL) {
+ struct mailbox_metadata metadata;
+
+ /* a) first sync in this process.
+ b) we had auto-closed this backend mailbox.
+
+ first try to quickly check if the mailbox has changed.
+ if we can do that check from mailbox list index, we don't
+ even need to open the mailbox. */
+ i_assert(array_count(&bbox->sync_pending_removes) == 0);
+ if (bbox->box->opened || bbox->open_failed) {
+ /* a) index already opened, refresh it
+ b) delayed error handling for mailbox_open()
+ that failed in virtual_notify_changes() */
+ if ((ret = virtual_try_open_and_sync_backend_box(ctx, bbox, sync_flags)) <= 0)
+ return ret;
+ bbox->open_failed = FALSE;
+ }
+
+ if ((mailbox_get_status(bbox->box, STATUS_UIDVALIDITY |
+ STATUS_UIDNEXT | STATUS_HIGHESTMODSEQ,
+ &status) < 0) ||
+ (mailbox_get_metadata(bbox->box, MAILBOX_METADATA_GUID,
+ &metadata) < 0)) {
+ if (mailbox_get_last_mail_error(bbox->box) != MAIL_ERROR_NOTFOUND)
+ return -1;
+ /* mailbox was deleted */
+ virtual_sync_backend_box_deleted(ctx, bbox);
+ return 0;
+ }
+ if (guid_128_is_empty(bbox->sync_guid)) {
+ /* upgrading from old virtual index */
+ guid_128_copy(bbox->sync_guid, metadata.guid);
+ ctx->mbox->ext_header_rewrite = TRUE;
+ }
+ if (virtual_bbox_mailbox_equals(bbox, &status, &metadata, &reason)) {
+ /* mailbox hasn't changed since we last opened it,
+ skip it for now.
+
+ we'll still need to create the bbox->uids mapping
+ using the current index. */
+ if (array_count(&bbox->uids) == 0)
+ virtual_sync_backend_handle_old_vmsgs(ctx, bbox, NULL);
+ return 0;
+ }
+ e_debug(ctx->mbox->box.event, "Backend mailbox %s changed: %s",
+ bbox->box->vname, reason);
+ if (!bbox->box->opened) {
+ /* first time we're opening the index */
+ if ((ret = virtual_try_open_and_sync_backend_box(ctx, bbox, sync_flags)) <= 0)
+ return ret;
+ }
+
+ virtual_backend_box_sync_mail_set(bbox);
+ if ((status.uidvalidity != bbox->sync_uid_validity) ||
+ !guid_128_equals(metadata.guid, bbox->sync_guid)) {
+ /* UID validity or GUID changed since last sync (or
+ this is the first sync), do a full search */
+ bbox->first_sync = TRUE;
+ ret = virtual_sync_backend_box_init(bbox);
+ } else {
+ /* build the initial search using the saved modseq. */
+ ret = virtual_sync_backend_box_continue(ctx, bbox);
+ }
+ i_assert(bbox->search_result != NULL || ret < 0);
+ } else {
+ /* sync using the existing search result */
+ i_assert(bbox->box->opened);
+ i_array_init(&ctx->sync_expunges, 32);
+ ret = virtual_sync_backend_box_sync(ctx, bbox, sync_flags);
+ if (ret == 0) T_BEGIN {
+ virtual_sync_mailbox_box_update(ctx, bbox);
+ } T_END;
+ array_free(&ctx->sync_expunges);
+ }
+
+ virtual_sync_backend_ext_header(ctx, bbox);
+ return ret;
+}
+
+static void virtual_sync_backend_map_uids(struct virtual_sync_context *ctx)
+{
+ uint32_t virtual_ext_id = ctx->mbox->virtual_ext_id;
+ struct virtual_sync_mail *vmails;
+ struct virtual_backend_box *bbox;
+ struct virtual_backend_uidmap *uidmap = NULL;
+ struct virtual_add_record add_rec;
+ const struct virtual_mail_index_record *vrec;
+ const void *data;
+ uint32_t i, vseq, vuid, messages;
+ unsigned int j = 0, uidmap_count = 0;
+
+ messages = mail_index_view_get_messages_count(ctx->sync_view);
+ if (messages == 0)
+ return;
+
+ /* sort the messages in current view by their backend mailbox and
+ real UID */
+ vmails = i_new(struct virtual_sync_mail, messages);
+ for (vseq = 1; vseq <= messages; vseq++) {
+ mail_index_lookup_ext(ctx->sync_view, vseq, virtual_ext_id,
+ &data, NULL);
+ vrec = data;
+ vmails[vseq-1].vseq = vseq;
+ vmails[vseq-1].vrec = *vrec;
+ }
+ qsort(vmails, messages, sizeof(*vmails), virtual_sync_mail_uid_cmp);
+
+ /* create real mailbox uid -> virtual uid mapping and expunge
+ messages no longer matching the search rule */
+ i_zero(&add_rec);
+ bbox = NULL;
+ for (i = 0; i < messages; i++) {
+ vseq = vmails[i].vseq;
+ vrec = &vmails[i].vrec;
+
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ /* add the rest of the newly seen messages */
+ for (; j < uidmap_count; j++) {
+ add_rec.rec.real_uid = uidmap[j].real_uid;
+ array_push_back(&ctx->all_adds, &add_rec);
+ }
+ if (!virtual_backend_box_lookup(ctx->mbox,
+ vrec->mailbox_id,
+ &bbox) ||
+ bbox->first_sync) {
+ /* the entire mailbox is lost */
+ mail_index_expunge(ctx->trans, vseq);
+ continue;
+ }
+ uidmap = array_get_modifiable(&bbox->uids,
+ &uidmap_count);
+ j = 0;
+ add_rec.rec.mailbox_id = bbox->mailbox_id;
+ bbox->sync_seen = TRUE;
+ }
+ mail_index_lookup_uid(ctx->sync_view, vseq, &vuid);
+
+ /* if virtual record doesn't exist in uidmap, it's expunged */
+ for (; j < uidmap_count; j++) {
+ if (uidmap[j].real_uid >= vrec->real_uid)
+ break;
+
+ /* newly seen message */
+ add_rec.rec.real_uid = uidmap[j].real_uid;
+ array_push_back(&ctx->all_adds, &add_rec);
+ }
+ if (j == uidmap_count || uidmap[j].real_uid != vrec->real_uid)
+ mail_index_expunge(ctx->trans, vseq);
+ else {
+ /* exists - update uidmap and flags */
+ uidmap[j++].virtual_uid = vuid;
+ if (bbox->search_result == NULL) {
+ /* mailbox is completely unchanged since last
+ sync - no need to sync flags */
+ } else {
+ virtual_sync_external_flags(ctx, bbox, vseq,
+ vrec->real_uid);
+ }
+ }
+ }
+ i_free(vmails);
+
+ /* finish adding messages to the last mailbox */
+ for (; j < uidmap_count; j++) {
+ add_rec.rec.real_uid = uidmap[j].real_uid;
+ array_push_back(&ctx->all_adds, &add_rec);
+ }
+}
+
+static void virtual_sync_new_backend_boxes(struct virtual_sync_context *ctx)
+{
+ struct virtual_backend_box *const *bboxes;
+ struct virtual_add_record add_rec;
+ struct virtual_backend_uidmap *uidmap;
+ unsigned int i, j, count, uidmap_count;
+
+ /* if there are any mailboxes we didn't yet sync, add new messages in
+ them */
+ i_zero(&add_rec);
+ bboxes = array_get(&ctx->mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++) {
+ bboxes[i]->first_sync = FALSE; /* this is the end of the sync */
+
+ if (bboxes[i]->sync_seen)
+ continue;
+
+ add_rec.rec.mailbox_id = bboxes[i]->mailbox_id;
+ uidmap = array_get_modifiable(&bboxes[i]->uids, &uidmap_count);
+ for (j = 0; j < uidmap_count; j++) {
+ add_rec.rec.real_uid = uidmap[j].real_uid;
+ array_push_back(&ctx->all_adds, &add_rec);
+ }
+ }
+}
+
+static int virtual_add_record_cmp(const struct virtual_add_record *add1,
+ const struct virtual_add_record *add2)
+{
+ if (add1->received_date < add2->received_date)
+ return -1;
+ if (add1->received_date > add2->received_date)
+ return 1;
+
+ /* if they're in same mailbox, we can order them correctly by the UID.
+ if they're in different mailboxes, ordering by UID doesn't really
+ help but it doesn't really harm either. */
+ if (add1->rec.real_uid < add2->rec.real_uid)
+ return -1;
+ if (add1->rec.real_uid > add2->rec.real_uid)
+ return 1;
+
+ /* two messages in different mailboxes have the same received date
+ and UID. */
+ return 0;
+}
+
+static int virtual_sync_backend_sort_new(struct virtual_sync_context *ctx)
+{
+ struct virtual_backend_box *bbox;
+ struct virtual_add_record *adds;
+ const struct virtual_mail_index_record *vrec;
+ unsigned int i, count;
+
+ /* get all messages' received dates */
+ adds = array_get_modifiable(&ctx->all_adds, &count);
+ for (bbox = NULL, i = 0; i < count; i++) {
+ vrec = &adds[i].rec;
+
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ /* The mailbox_id comes from ctx->all_adds, which is
+ filled earlier in this sync. The mailbox_ids in it
+ come via mbox->backend_boxes, so the backend box is
+ guaranteed to have existed already in this sync.
+ Since backend_boxes are never removed during the sync,
+ the lookup is guaranteed to find it here.
+ (backend_boxes removal is done only while opening
+ the virtual mailbox in virtual_mailbox_open() ) */
+ if (!virtual_backend_box_lookup(ctx->mbox,
+ vrec->mailbox_id, &bbox))
+ i_unreached();
+ if (!bbox->box->opened &&
+ virtual_backend_box_open(ctx->mbox, bbox) < 0)
+ return -1;
+ virtual_backend_box_sync_mail_set(bbox);
+ }
+ if (!mail_set_uid(bbox->sync_mail, vrec->real_uid)) {
+ /* we may have reopened the mailbox, which could have
+ caused the mail to be expunged already. */
+ adds[i].received_date = 0;
+ } else if (mail_get_received_date(bbox->sync_mail,
+ &adds[i].received_date) < 0) {
+ if (!bbox->sync_mail->expunged)
+ return -1;
+ /* expunged already, just add it somewhere */
+ adds[i].received_date = 0;
+ }
+ }
+
+ array_sort(&ctx->all_adds, virtual_add_record_cmp);
+ return 0;
+}
+
+static int virtual_sync_backend_add_new(struct virtual_sync_context *ctx)
+{
+ uint32_t virtual_ext_id = ctx->mbox->virtual_ext_id;
+ struct virtual_add_record *adds;
+ struct virtual_backend_box *bbox;
+ struct virtual_backend_uidmap *uidmap;
+ const struct mail_index_header *hdr;
+ const struct virtual_mail_index_record *vrec;
+ unsigned int i, count, idx;
+ ARRAY_TYPE(seq_range) saved_uids;
+ uint32_t vseq, first_uid;
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ adds = array_get_modifiable(&ctx->all_adds, &count);
+ if (count == 0) {
+ ctx->mbox->sync_virtual_next_uid = hdr->next_uid;
+ return 0;
+ }
+
+ if (adds[0].rec.mailbox_id == adds[count-1].rec.mailbox_id) {
+ /* all messages are from a single mailbox. add them in
+ the same order. */
+ } else {
+ /* sort new messages by received date to get the add order */
+ if (virtual_sync_backend_sort_new(ctx) < 0)
+ return -1;
+ }
+
+ for (bbox = NULL, i = 0; i < count; i++) {
+ vrec = &adds[i].rec;
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ if (!virtual_backend_box_lookup(ctx->mbox,
+ vrec->mailbox_id, &bbox)) {
+ /* See virtual_sync_backend_sort_new() */
+ i_unreached();
+ }
+ if (!bbox->box->opened &&
+ virtual_backend_box_open(ctx->mbox, bbox) < 0)
+ return -1;
+ virtual_backend_box_sync_mail_set(bbox);
+ }
+
+ mail_index_append(ctx->trans, 0, &vseq);
+ mail_index_update_ext(ctx->trans, vseq, virtual_ext_id,
+ vrec, NULL);
+ virtual_sync_external_flags(ctx, bbox, vseq, vrec->real_uid);
+ }
+
+ /* assign UIDs to new messages */
+ first_uid = hdr->next_uid;
+ t_array_init(&saved_uids, 1);
+ mail_index_append_finish_uids(ctx->trans, first_uid, &saved_uids);
+ i_assert(seq_range_count(&saved_uids) == count);
+
+ /* update virtual UIDs in uidmap */
+ for (bbox = NULL, i = 0; i < count; i++) {
+ vrec = &adds[i].rec;
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ if (!virtual_backend_box_lookup(ctx->mbox,
+ vrec->mailbox_id, &bbox)) {
+ /* See virtual_sync_backend_sort_new() */
+ i_unreached();
+ }
+
+ }
+
+ if (!array_bsearch_insert_pos(&bbox->uids, &vrec->real_uid,
+ virtual_backend_uidmap_bsearch_cmp,
+ &idx))
+ i_unreached();
+ uidmap = array_idx_modifiable(&bbox->uids, idx);
+ i_assert(uidmap->virtual_uid == 0);
+ uidmap->virtual_uid = first_uid + i;
+ }
+ ctx->mbox->sync_virtual_next_uid = first_uid + i;
+ return 0;
+}
+
+static int
+virtual_sync_apply_existing_appends(struct virtual_sync_context *ctx)
+{
+ uint32_t virtual_ext_id = ctx->mbox->virtual_ext_id;
+ struct virtual_backend_box *bbox = NULL;
+ const struct mail_index_header *hdr;
+ const struct virtual_mail_index_record *vrec;
+ struct virtual_backend_uidmap uidmap;
+ const void *data;
+ uint32_t seq, seq2;
+
+ if (!ctx->mbox->uids_mapped)
+ return 0;
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ if (ctx->mbox->sync_virtual_next_uid >= hdr->next_uid)
+ return 0;
+
+ /* another process added messages to virtual index. get backend boxes'
+ uid lists up-to-date by adding the new messages there. */
+ if (!mail_index_lookup_seq_range(ctx->sync_view,
+ ctx->mbox->sync_virtual_next_uid,
+ (uint32_t)-1, &seq, &seq2))
+ return 0;
+
+ i_zero(&uidmap);
+ for (; seq <= seq2; seq++) {
+ mail_index_lookup_ext(ctx->sync_view, seq, virtual_ext_id,
+ &data, NULL);
+ vrec = data;
+ uidmap.real_uid = vrec->real_uid;
+ mail_index_lookup_uid(ctx->sync_view, seq, &uidmap.virtual_uid);
+
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ if (!virtual_backend_box_lookup(ctx->mbox,
+ vrec->mailbox_id, &bbox)) {
+ mail_index_expunge(ctx->trans, seq);
+ continue;
+ }
+ }
+ array_push_back(&bbox->uids, &uidmap);
+ bbox->uids_nonsorted = TRUE;
+ }
+
+ virtual_sync_backend_boxes_sort_uids(ctx->mbox);
+ return 0;
+}
+
+static void
+virtual_sync_apply_existing_expunges(struct virtual_mailbox *mbox,
+ struct mailbox_sync_context *sync_ctx)
+{
+ struct index_mailbox_sync_context *isync_ctx =
+ (struct index_mailbox_sync_context *)sync_ctx;
+ struct virtual_backend_box *bbox = NULL;
+ struct seq_range_iter iter;
+ const struct virtual_mail_index_record *vrec;
+ const void *data;
+ unsigned int n = 0;
+ uint32_t seq;
+
+ if (isync_ctx->expunges == NULL)
+ return;
+
+ seq_range_array_iter_init(&iter, isync_ctx->expunges);
+ while (seq_range_array_iter_nth(&iter, n++, &seq)) {
+ mail_index_lookup_ext(mbox->box.view, seq,
+ mbox->virtual_ext_id, &data, NULL);
+ vrec = data;
+
+ if (bbox == NULL || bbox->mailbox_id != vrec->mailbox_id) {
+ if (!virtual_backend_box_lookup(mbox,
+ vrec->mailbox_id, &bbox))
+ continue;
+ if (!array_is_created(&bbox->sync_outside_expunges))
+ i_array_init(&bbox->sync_outside_expunges, 32);
+ }
+ seq_range_array_add(&bbox->sync_outside_expunges,
+ vrec->real_uid);
+ }
+}
+
+static int virtual_sync_mail_mailbox_cmp(const struct virtual_sync_mail *m1,
+ const struct virtual_sync_mail *m2)
+{
+ if (m1->vrec.mailbox_id < m2->vrec.mailbox_id)
+ return -1;
+ if (m1->vrec.mailbox_id > m2->vrec.mailbox_id)
+ return 1;
+ return 0;
+}
+
+static void virtual_sync_bboxes_get_mails(struct virtual_sync_context *ctx)
+{
+ uint32_t messages, vseq;
+ const void *mail_data;
+ const struct virtual_mail_index_record *vrec;
+ struct virtual_sync_mail *sync_mail;
+
+ messages = mail_index_view_get_messages_count(ctx->sync_view);
+ i_array_init(&ctx->all_mails, messages);
+ for (vseq = 1; vseq <= messages; vseq++) {
+ mail_index_lookup_ext(ctx->sync_view, vseq,
+ ctx->mbox->virtual_ext_id, &mail_data, NULL);
+ vrec = mail_data;
+ sync_mail = array_append_space(&ctx->all_mails);
+ sync_mail->vseq = vseq;
+ sync_mail->vrec = *vrec;
+ }
+ array_sort(&ctx->all_mails, virtual_sync_mail_mailbox_cmp);
+}
+
+static int virtual_sync_backend_boxes(struct virtual_sync_context *ctx)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+ int ret;
+
+ if (virtual_sync_apply_existing_appends(ctx) < 0)
+ return -1;
+
+ i_array_init(&ctx->all_adds, 128);
+ bboxes = array_get(&ctx->mbox->backend_boxes, &count);
+
+ /* we have different optimizations depending on whether the virtual
+ mailbox consists of multiple backend boxes or just one */
+ if (count > 1)
+ virtual_sync_bboxes_get_mails(ctx);
+
+ for (i = 0; i < count; i++) {
+ if (virtual_sync_backend_box(ctx, bboxes[i]) < 0) {
+ /* backend failed, copy the error */
+ virtual_box_copy_error(&ctx->mbox->box,
+ bboxes[i]->box);
+ return -1;
+ }
+ }
+
+ if (!ctx->mbox->uids_mapped) {
+ /* initial sync: assign virtual UIDs to existing messages and
+ sync all flags */
+ ctx->mbox->uids_mapped = TRUE;
+ virtual_sync_backend_map_uids(ctx);
+ virtual_sync_new_backend_boxes(ctx);
+ }
+ ret = virtual_sync_backend_add_new(ctx);
+#ifdef DEBUG
+ for (i = 0; i < count; i++) {
+ const struct virtual_backend_uidmap *uidmap;
+
+ array_foreach(&bboxes[i]->uids, uidmap)
+ i_assert(uidmap->virtual_uid > 0);
+ }
+#endif
+ array_free(&ctx->all_adds);
+ if (array_is_created(&ctx->all_mails))
+ array_free(&ctx->all_mails);
+ return ret;
+}
+
+static void virtual_sync_backend_boxes_finish(struct virtual_sync_context *ctx)
+{
+ struct virtual_backend_box *const *bboxes;
+ unsigned int i, count;
+
+ bboxes = array_get(&ctx->mbox->backend_boxes, &count);
+ for (i = 0; i < count; i++)
+ virtual_backend_box_sync_mail_unset(bboxes[i]);
+}
+
+static int virtual_sync_finish(struct virtual_sync_context *ctx, bool success)
+{
+ int ret = success ? 0 : -1;
+
+ virtual_sync_backend_boxes_finish(ctx);
+ if (success) {
+ if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) {
+ mailbox_set_index_error(&ctx->mbox->box);
+ ret = -1;
+ }
+ ctx->mbox->ext_header_rewrite = FALSE;
+ } else {
+ if (ctx->index_broken) {
+ /* make sure we don't complain about the same errors
+ over and over again. */
+ if (mail_index_unlink(ctx->index) < 0) {
+ i_error("virtual index %s: Failed to unlink() "
+ "broken indexes: %m",
+ mailbox_get_path(&ctx->mbox->box));
+ }
+ }
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ }
+ i_free(ctx);
+ return ret;
+}
+
+static int virtual_sync(struct virtual_mailbox *mbox,
+ enum mailbox_sync_flags flags)
+{
+ struct virtual_sync_context *ctx;
+ enum mail_index_sync_flags index_sync_flags;
+ bool broken;
+ int ret;
+
+ ctx = i_new(struct virtual_sync_context, 1);
+ ctx->mbox = mbox;
+ ctx->flags = flags;
+ ctx->index = mbox->box.index;
+ /* Removed messages are expunged when
+ a) EXPUNGE is used
+ b) Mailbox is being opened (FIX_INCONSISTENT is set) */
+ ctx->expunge_removed =
+ (ctx->flags & (MAILBOX_SYNC_FLAG_EXPUNGE |
+ MAILBOX_SYNC_FLAG_FIX_INCONSISTENT)) != 0;
+
+ index_sync_flags = MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY |
+ MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES;
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0)
+ index_sync_flags |= MAIL_INDEX_SYNC_FLAG_DROP_RECENT;
+
+ ret = mail_index_sync_begin(ctx->index, &ctx->index_sync_ctx,
+ &ctx->sync_view, &ctx->trans,
+ index_sync_flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_set_index_error(&mbox->box);
+ i_free(ctx);
+ return ret;
+ }
+
+ ret = virtual_mailbox_ext_header_read(mbox, ctx->sync_view, &broken);
+ if (ret < 0)
+ return virtual_sync_finish(ctx, FALSE);
+ if (broken)
+ ctx->index_broken = TRUE;
+ /* apply changes from virtual index to backend mailboxes */
+ if (virtual_sync_index_changes(ctx) < 0)
+ return virtual_sync_finish(ctx, FALSE);
+ /* update list of UIDs in backend mailboxes */
+ if (virtual_sync_backend_boxes(ctx) < 0)
+ return virtual_sync_finish(ctx, FALSE);
+
+ virtual_sync_index_finish(ctx);
+ return virtual_sync_finish(ctx, TRUE);
+}
+
+struct mailbox_sync_context *
+virtual_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct mailbox_sync_context *sync_ctx;
+ int ret = 0;
+
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ ret = -1;
+ }
+
+ if (index_mailbox_want_full_sync(&mbox->box, flags) && ret == 0)
+ ret = virtual_sync(mbox, flags);
+
+ sync_ctx = index_mailbox_sync_init(box, flags, ret < 0);
+ virtual_sync_apply_existing_expunges(mbox, sync_ctx);
+ return sync_ctx;
+}
diff --git a/src/plugins/virtual/virtual-transaction.c b/src/plugins/virtual/virtual-transaction.c
new file mode 100644
index 0000000..53ac0a8
--- /dev/null
+++ b/src/plugins/virtual/virtual-transaction.c
@@ -0,0 +1,87 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "virtual-storage.h"
+#include "virtual-transaction.h"
+
+struct mailbox_transaction_context *
+virtual_transaction_get(struct mailbox_transaction_context *trans,
+ struct mailbox *backend_box)
+{
+ struct virtual_transaction_context *vt =
+ (struct virtual_transaction_context *)trans;
+ struct mailbox_transaction_context *const *bt, *new_bt;
+ unsigned int i, count;
+
+ bt = array_get(&vt->backend_transactions, &count);
+ for (i = 0; i < count; i++) {
+ if (bt[i]->box == backend_box)
+ return bt[i];
+ }
+
+ new_bt = mailbox_transaction_begin(backend_box, trans->flags, __func__);
+ array_push_back(&vt->backend_transactions, &new_bt);
+ return new_bt;
+}
+
+struct mailbox_transaction_context *
+virtual_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct virtual_mailbox *mbox = (struct virtual_mailbox *)box;
+ struct virtual_transaction_context *vt;
+
+ vt = i_new(struct virtual_transaction_context, 1);
+ i_array_init(&vt->backend_transactions,
+ array_count(&mbox->backend_boxes));
+ index_transaction_init(&vt->t, box, flags, reason);
+ return &vt->t;
+}
+
+int virtual_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct virtual_transaction_context *vt =
+ (struct virtual_transaction_context *)t;
+ struct mailbox_transaction_context **bt;
+ unsigned int i, count;
+ int ret = 0;
+
+ if (t->save_ctx != NULL) {
+ virtual_save_free(t->save_ctx);
+ t->save_ctx = NULL;
+ }
+
+ bt = array_get_modifiable(&vt->backend_transactions, &count);
+ for (i = 0; i < count; i++) {
+ if (mailbox_transaction_commit(&bt[i]) < 0)
+ ret = -1;
+ }
+ array_free(&vt->backend_transactions);
+
+ if (index_transaction_commit(t, changes_r) < 0)
+ ret = -1;
+ return ret;
+}
+
+void virtual_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ struct virtual_transaction_context *vt =
+ (struct virtual_transaction_context *)t;
+ struct mailbox_transaction_context **bt;
+ unsigned int i, count;
+
+ if (t->save_ctx != NULL) {
+ virtual_save_free(t->save_ctx);
+ t->save_ctx = NULL;
+ }
+
+ bt = array_get_modifiable(&vt->backend_transactions, &count);
+ for (i = 0; i < count; i++)
+ mailbox_transaction_rollback(&bt[i]);
+ array_free(&vt->backend_transactions);
+
+ index_transaction_rollback(t);
+}
diff --git a/src/plugins/virtual/virtual-transaction.h b/src/plugins/virtual/virtual-transaction.h
new file mode 100644
index 0000000..a950fcb
--- /dev/null
+++ b/src/plugins/virtual/virtual-transaction.h
@@ -0,0 +1,24 @@
+#ifndef VIRTUAL_TRANSACTION_H
+#define VIRTUAL_TRANSACTION_H
+
+#include "index-storage.h"
+
+struct virtual_transaction_context {
+ struct mailbox_transaction_context t;
+
+ ARRAY(struct mailbox_transaction_context *) backend_transactions;
+};
+
+struct mailbox_transaction_context *
+virtual_transaction_get(struct mailbox_transaction_context *trans,
+ struct mailbox *backend_box);
+
+struct mailbox_transaction_context *
+virtual_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason);
+int virtual_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r);
+void virtual_transaction_rollback(struct mailbox_transaction_context *t);
+
+#endif
diff --git a/src/plugins/welcome/Makefile.am b/src/plugins/welcome/Makefile.am
new file mode 100644
index 0000000..98968f5
--- /dev/null
+++ b/src/plugins/welcome/Makefile.am
@@ -0,0 +1,14 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib99_welcome_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib99_welcome_plugin.la
+
+lib99_welcome_plugin_la_SOURCES = \
+ welcome-plugin.c
diff --git a/src/plugins/welcome/Makefile.in b/src/plugins/welcome/Makefile.in
new file mode 100644
index 0000000..339449c
--- /dev/null
+++ b/src/plugins/welcome/Makefile.in
@@ -0,0 +1,814 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/welcome
+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)$(moduledir)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib99_welcome_plugin_la_LIBADD =
+am_lib99_welcome_plugin_la_OBJECTS = welcome-plugin.lo
+lib99_welcome_plugin_la_OBJECTS = \
+ $(am_lib99_welcome_plugin_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 =
+lib99_welcome_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib99_welcome_plugin_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)/welcome-plugin.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 = $(lib99_welcome_plugin_la_SOURCES)
+DIST_SOURCES = $(lib99_welcome_plugin_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)
+# 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@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+lib99_welcome_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib99_welcome_plugin.la
+
+lib99_welcome_plugin_la_SOURCES = \
+ welcome-plugin.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/plugins/welcome/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/welcome/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-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}; \
+ }
+
+lib99_welcome_plugin.la: $(lib99_welcome_plugin_la_OBJECTS) $(lib99_welcome_plugin_la_DEPENDENCIES) $(EXTRA_lib99_welcome_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib99_welcome_plugin_la_LINK) -rpath $(moduledir) $(lib99_welcome_plugin_la_OBJECTS) $(lib99_welcome_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/welcome-plugin.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)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/welcome-plugin.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-moduleLTLIBRARIES
+
+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)/welcome-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/welcome/welcome-plugin.c b/src/plugins/welcome/welcome-plugin.c
new file mode 100644
index 0000000..8c78f54
--- /dev/null
+++ b/src/plugins/welcome/welcome-plugin.c
@@ -0,0 +1,135 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+#include "eacces-error.h"
+#include "write-full.h"
+#include "module-context.h"
+#include "mail-storage-private.h"
+
+#define WELCOME_SOCKET_TIMEOUT_SECS 30
+
+#define WELCOME_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, welcome_storage_module)
+
+struct welcome_mailbox {
+ union mailbox_module_context module_ctx;
+ bool created;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(welcome_storage_module,
+ &mail_storage_module_register);
+
+static void script_execute(struct mail_user *user, const char *cmd, bool wait)
+{
+ const char *socket_path, *const *args;
+ string_t *str;
+ char buf[1024];
+ int fd, ret;
+
+ e_debug(user->event, "welcome: Executing %s (wait=%d)", cmd, wait ? 1 : 0);
+
+ args = t_strsplit_spaces(cmd, " ");
+ socket_path = args[0];
+ args++;
+
+ if (*socket_path != '/') {
+ socket_path = t_strconcat(user->set->base_dir, "/",
+ socket_path, NULL);
+ }
+ if ((fd = net_connect_unix_with_retries(socket_path, 1000)) < 0) {
+ if (errno == EACCES) {
+ i_error("welcome: %s",
+ eacces_error_get("net_connect_unix",
+ socket_path));
+ } else {
+ i_error("welcome: net_connect_unix(%s) failed: %m",
+ socket_path);
+ }
+ return;
+ }
+
+ str = t_str_new(1024);
+ str_append(str, "VERSION\tscript\t4\t0\n");
+ if (!wait)
+ str_append(str, "noreply\n");
+ else
+ str_append(str, "-\n");
+ for (; *args != NULL; args++) {
+ str_append_tabescaped(str, *args);
+ str_append_c(str, '\n');
+ }
+ str_append_c(str, '\n');
+
+ alarm(WELCOME_SOCKET_TIMEOUT_SECS);
+ net_set_nonblock(fd, FALSE);
+ if (write_full(fd, str_data(str), str_len(str)) < 0)
+ i_error("write(%s) failed: %m", socket_path);
+ else if (wait) {
+ ret = read(fd, buf, sizeof(buf));
+ if (ret < 0)
+ i_error("welcome: read(%s) failed: %m", socket_path);
+ else if (ret < 2)
+ i_error("welcome: %s failed: Only %d bytes read", socket_path, ret);
+ else if (buf[0] != '+')
+ i_error("welcome: %s failed: Script returned error", socket_path);
+ }
+ if (close(fd) < 0)
+ i_error("close(%s) failed: %m", socket_path);
+}
+
+static int
+welcome_create_box(struct mailbox *box,
+ const struct mailbox_update *update, bool directory)
+{
+ struct welcome_mailbox *wbox = WELCOME_CONTEXT(box);
+ const char *cmd;
+
+ if (wbox->module_ctx.super.create_box(box, update, directory) < 0)
+ return -1;
+ cmd = mail_user_plugin_getenv(box->storage->user, "welcome_script");
+ if (cmd != NULL) {
+ bool wait = mail_user_plugin_getenv_bool(box->storage->user,
+ "welcome_wait");
+ script_execute(box->storage->user, cmd, wait);
+ }
+
+ return 0;
+}
+
+static void welcome_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct welcome_mailbox *wbox;
+
+ if (!box->inbox_user)
+ return;
+
+ wbox = p_new(box->pool, struct welcome_mailbox, 1);
+ wbox->module_ctx.super = *v;
+ box->vlast = &wbox->module_ctx.super;
+
+ v->create_box = welcome_create_box;
+ MODULE_CONTEXT_SET(box, welcome_storage_module, wbox);
+}
+
+static struct mail_storage_hooks welcome_mail_storage_hooks = {
+ .mailbox_allocated = welcome_mailbox_allocated
+};
+
+void welcome_plugin_init(struct module *module);
+void welcome_plugin_deinit(void);
+
+void welcome_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &welcome_mail_storage_hooks);
+}
+
+void welcome_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&welcome_mail_storage_hooks);
+}
+
+const char *welcome_plugin_version = DOVECOT_ABI_VERSION;
diff --git a/src/plugins/zlib/Makefile.am b/src/plugins/zlib/Makefile.am
new file mode 100644
index 0000000..45c3d76
--- /dev/null
+++ b/src/plugins/zlib/Makefile.am
@@ -0,0 +1,24 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-compression \
+ -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
+
+NOPLUGIN_LDFLAGS =
+lib20_zlib_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+ lib20_zlib_plugin.la
+
+lib20_zlib_plugin_la_LIBADD = \
+ ../../lib-compression/libcompression.la
+
+lib20_zlib_plugin_la_SOURCES = \
+ zlib-plugin.c
+
+noinst_HEADERS = \
+ zlib-plugin.h
diff --git a/src/plugins/zlib/Makefile.in b/src/plugins/zlib/Makefile.in
new file mode 100644
index 0000000..23722eb
--- /dev/null
+++ b/src/plugins/zlib/Makefile.in
@@ -0,0 +1,827 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/plugins/zlib
+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__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)"
+LTLIBRARIES = $(module_LTLIBRARIES)
+lib20_zlib_plugin_la_DEPENDENCIES = \
+ ../../lib-compression/libcompression.la
+am_lib20_zlib_plugin_la_OBJECTS = zlib-plugin.lo
+lib20_zlib_plugin_la_OBJECTS = $(am_lib20_zlib_plugin_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 =
+lib20_zlib_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(lib20_zlib_plugin_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)/zlib-plugin.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 = $(lib20_zlib_plugin_la_SOURCES)
+DIST_SOURCES = $(lib20_zlib_plugin_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 =
+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-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-compression \
+ -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
+
+lib20_zlib_plugin_la_LDFLAGS = -module -avoid-version
+module_LTLIBRARIES = \
+ lib20_zlib_plugin.la
+
+lib20_zlib_plugin_la_LIBADD = \
+ ../../lib-compression/libcompression.la
+
+lib20_zlib_plugin_la_SOURCES = \
+ zlib-plugin.c
+
+noinst_HEADERS = \
+ zlib-plugin.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/plugins/zlib/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/plugins/zlib/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-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}; \
+ }
+
+lib20_zlib_plugin.la: $(lib20_zlib_plugin_la_OBJECTS) $(lib20_zlib_plugin_la_DEPENDENCIES) $(EXTRA_lib20_zlib_plugin_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(lib20_zlib_plugin_la_LINK) -rpath $(moduledir) $(lib20_zlib_plugin_la_OBJECTS) $(lib20_zlib_plugin_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/zlib-plugin.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:
+ for dir in "$(DESTDIR)$(moduledir)"; 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 \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/zlib-plugin.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-moduleLTLIBRARIES
+
+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)/zlib-plugin.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-moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-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-html install-html-am \
+ install-info install-info-am install-man \
+ install-moduleLTLIBRARIES 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-moduleLTLIBRARIES
+
+.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/plugins/zlib/zlib-plugin.c b/src/plugins/zlib/zlib-plugin.c
new file mode 100644
index 0000000..f716058
--- /dev/null
+++ b/src/plugins/zlib/zlib-plugin.c
@@ -0,0 +1,388 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+#include "str.h"
+#include "mail-user.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "compression.h"
+#include "zlib-plugin.h"
+
+#include <fcntl.h>
+
+#define ZLIB_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, zlib_storage_module)
+#define ZLIB_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, zlib_mail_module)
+#define ZLIB_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, zlib_user_module)
+
+#define MAX_INBUF_SIZE (1024*1024)
+#define ZLIB_MAIL_CACHE_EXPIRE_MSECS (60*1000)
+
+struct zlib_mail {
+ union mail_module_context module_ctx;
+ bool verifying_save;
+};
+
+struct zlib_mail_cache {
+ struct timeout *to;
+ struct mailbox *box;
+ uint32_t uid;
+
+ struct istream *input;
+};
+
+struct zlib_user {
+ union mail_user_module_context module_ctx;
+
+ struct zlib_mail_cache cache;
+
+ const struct compression_handler *save_handler;
+ int save_level;
+};
+
+const char *zlib_plugin_version = DOVECOT_ABI_VERSION;
+
+static MODULE_CONTEXT_DEFINE_INIT(zlib_user_module,
+ &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(zlib_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(zlib_mail_module, &mail_module_register);
+
+static bool zlib_mailbox_is_permail(struct mailbox *box)
+{
+ enum mail_storage_class_flags class_flags = box->storage->class_flags;
+
+ return (class_flags & MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS) == 0 &&
+ (class_flags & MAIL_STORAGE_CLASS_FLAG_BINARY_DATA) != 0;
+}
+
+static void zlib_mail_cache_close(struct zlib_user *zuser)
+{
+ struct zlib_mail_cache *cache = &zuser->cache;
+
+ timeout_remove(&cache->to);
+ i_stream_unref(&cache->input);
+ i_zero(cache);
+}
+
+static struct istream *
+zlib_mail_cache_open(struct zlib_user *zuser, struct mail *mail,
+ struct istream *input, bool do_cache)
+{
+ struct zlib_mail_cache *cache = &zuser->cache;
+ struct istream *inputs[2];
+ string_t *temp_prefix = t_str_new(128);
+
+ if (do_cache)
+ zlib_mail_cache_close(zuser);
+
+ /* zlib istream is seekable, but very slow. create a seekable istream
+ which we can use to quickly seek around in the stream that's been
+ read so far. usually the partial IMAP FETCHes continue from where
+ the previous left off, so this isn't strictly necessary, but with
+ the way lib-imap-storage's CRLF-cache works it has to seek backwards
+ somewhat, which causes a zlib stream reset. And the CRLF-cache isn't
+ easy to fix.. */
+ input->seekable = FALSE;
+ inputs[0] = input;
+ inputs[1] = NULL;
+ mail_user_set_get_temp_prefix(temp_prefix, mail->box->storage->user->set);
+ input = i_stream_create_seekable_path(inputs,
+ i_stream_get_max_buffer_size(inputs[0]),
+ str_c(temp_prefix));
+ i_stream_set_name(input, t_strdup_printf("compress(%s)",
+ i_stream_get_name(inputs[0])));
+ i_stream_unref(&inputs[0]);
+
+ if (do_cache) {
+ cache->to = timeout_add(ZLIB_MAIL_CACHE_EXPIRE_MSECS,
+ zlib_mail_cache_close, zuser);
+ cache->box = mail->box;
+ cache->uid = mail->uid;
+ cache->input = input;
+ /* index-mail wants the stream to be destroyed at close, so create
+ a new stream instead of just increasing reference. */
+ return i_stream_create_limit(cache->input, UOFF_T_MAX);
+ } else {
+ return input;
+ }
+}
+
+static int zlib_istream_opened(struct mail *_mail, struct istream **stream)
+{
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(_mail->box->storage->user);
+ struct zlib_mail_cache *cache = &zuser->cache;
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct zlib_mail *zmail = ZLIB_MAIL_CONTEXT(mail);
+ struct istream *input;
+ const struct compression_handler *handler;
+
+ if (zmail->verifying_save) {
+ /* zlib_mail_save_finish() is verifying that the user-given
+ input doesn't look compressed. */
+ return zmail->module_ctx.super.istream_opened(_mail, stream);
+ }
+
+ if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) {
+ /* use the cached stream. when doing partial reads it should
+ already be seeked into the wanted offset. */
+ i_stream_unref(stream);
+ i_stream_seek(cache->input, 0);
+ *stream = i_stream_create_limit(cache->input, UOFF_T_MAX);
+ return zmail->module_ctx.super.istream_opened(_mail, stream);
+ }
+
+ handler = compression_detect_handler(*stream);
+ if (handler != NULL) {
+ if (handler->create_istream == NULL) {
+ mail_set_critical(_mail,
+ "zlib plugin: Detected %s compression "
+ "but support not compiled in", handler->ext);
+ return -1;
+ }
+
+ input = *stream;
+ *stream = handler->create_istream(input);
+ i_stream_unref(&input);
+ /* dont cache the stream if _mail->uid is 0 */
+ *stream = zlib_mail_cache_open(zuser, _mail, *stream, (_mail->uid > 0));
+ }
+ return zmail->module_ctx.super.istream_opened(_mail, stream);
+}
+
+static void zlib_mail_close(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct zlib_mail *zmail = ZLIB_MAIL_CONTEXT(mail);
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(_mail->box->storage->user);
+ struct zlib_mail_cache *cache = &zuser->cache;
+ uoff_t size;
+
+ if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) {
+ /* make sure we have read the entire email into the seekable
+ stream (which causes the original input stream to be
+ unrefed). we can't safely keep the original input stream
+ open after the mail is closed. */
+ if (i_stream_get_size(cache->input, TRUE, &size) < 0)
+ zlib_mail_cache_close(zuser);
+ }
+ zmail->module_ctx.super.close(_mail);
+}
+
+static void zlib_mail_allocated(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ struct mail_vfuncs *v = mail->vlast;
+ struct zlib_mail *zmail;
+
+ if (!zlib_mailbox_is_permail(_mail->box))
+ return;
+
+ zmail = p_new(mail->pool, struct zlib_mail, 1);
+ zmail->module_ctx.super = *v;
+ mail->vlast = &zmail->module_ctx.super;
+
+ v->istream_opened = zlib_istream_opened;
+ v->close = zlib_mail_close;
+ MODULE_CONTEXT_SET(mail, zlib_mail_module, zmail);
+}
+
+static int zlib_mail_save_finish(struct mail_save_context *ctx)
+{
+ struct mailbox *box = ctx->transaction->box;
+ union mailbox_module_context *zbox = ZLIB_CONTEXT(box);
+ struct mail_private *mail = (struct mail_private *)ctx->dest_mail;
+ struct zlib_mail *zmail = ZLIB_MAIL_CONTEXT(mail);
+ struct istream *input;
+ int ret;
+
+ if (zbox->super.save_finish(ctx) < 0)
+ return -1;
+
+ zmail->verifying_save = TRUE;
+ ret = mail_get_stream(ctx->dest_mail, NULL, NULL, &input);
+ zmail->verifying_save = FALSE;
+ if (ret < 0)
+ return -1;
+
+ if (compression_detect_handler(input) != NULL) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Saving mails compressed by client isn't supported");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+zlib_mail_save_compress_begin(struct mail_save_context *ctx,
+ struct istream *input)
+{
+ struct mailbox *box = ctx->transaction->box;
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(box->storage->user);
+ union mailbox_module_context *zbox = ZLIB_CONTEXT(box);
+ struct ostream *output;
+
+ if (zbox->super.save_begin(ctx, input) < 0)
+ return -1;
+
+ output = zuser->save_handler->create_ostream(ctx->data.output,
+ zuser->save_level);
+ o_stream_unref(&ctx->data.output);
+ ctx->data.output = output;
+ o_stream_cork(ctx->data.output);
+ return 0;
+}
+
+static void
+zlib_permail_alloc_init(struct mailbox *box, struct mailbox_vfuncs *v)
+{
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(box->storage->user);
+
+ if (zuser->save_handler == NULL) {
+ v->save_finish = zlib_mail_save_finish;
+ } else {
+ v->save_begin = zlib_mail_save_compress_begin;
+ }
+}
+
+static void zlib_mailbox_open_input(struct mailbox *box)
+{
+ const struct compression_handler *handler;
+ struct istream *input;
+ struct stat st;
+ int fd;
+
+ if (compression_lookup_handler_from_ext(box->name, &handler) <= 0)
+ return;
+
+ if (mail_storage_is_mailbox_file(box->storage)) {
+ /* looks like a compressed single file mailbox. we should be
+ able to handle this. */
+ const char *box_path = mailbox_get_path(box);
+
+ fd = open(box_path, O_RDONLY);
+ if (fd == -1) {
+ /* let the standard handler figure out what to do
+ with the failure */
+ return;
+ }
+ if (fstat(fd, &st) == 0 && S_ISDIR(st.st_mode)) {
+ i_close_fd(&fd);
+ return;
+ }
+ input = i_stream_create_fd_autoclose(&fd, MAX_INBUF_SIZE);
+ i_stream_set_name(input, box_path);
+ box->input = handler->create_istream(input);
+ i_stream_unref(&input);
+ box->flags |= MAILBOX_FLAG_READONLY;
+ }
+}
+
+static int zlib_mailbox_open(struct mailbox *box)
+{
+ union mailbox_module_context *zbox = ZLIB_CONTEXT(box);
+
+ if (box->input == NULL &&
+ (box->storage->class_flags &
+ MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS) != 0)
+ zlib_mailbox_open_input(box);
+
+ return zbox->super.open(box);
+}
+
+static void zlib_mailbox_close(struct mailbox *box)
+{
+ union mailbox_module_context *zbox = ZLIB_CONTEXT(box);
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(box->storage->user);
+
+ if (zuser->cache.box == box)
+ zlib_mail_cache_close(zuser);
+ zbox->super.close(box);
+}
+
+static void zlib_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ union mailbox_module_context *zbox;
+
+ zbox = p_new(box->pool, union mailbox_module_context, 1);
+ zbox->super = *v;
+ box->vlast = &zbox->super;
+ v->open = zlib_mailbox_open;
+ v->close = zlib_mailbox_close;
+
+ MODULE_CONTEXT_SET_SELF(box, zlib_storage_module, zbox);
+
+ if (zlib_mailbox_is_permail(box))
+ zlib_permail_alloc_init(box, v);
+}
+
+static void zlib_mail_user_deinit(struct mail_user *user)
+{
+ struct zlib_user *zuser = ZLIB_USER_CONTEXT(user);
+
+ zlib_mail_cache_close(zuser);
+ zuser->module_ctx.super.deinit(user);
+}
+
+static void zlib_mail_user_created(struct mail_user *user)
+{
+ struct mail_user_vfuncs *v = user->vlast;
+ struct zlib_user *zuser;
+ const char *name;
+ int ret;
+
+ zuser = p_new(user->pool, struct zlib_user, 1);
+ zuser->module_ctx.super = *v;
+ user->vlast = &zuser->module_ctx.super;
+ v->deinit = zlib_mail_user_deinit;
+
+ name = mail_user_plugin_getenv(user, "zlib_save");
+ if (name != NULL && *name != '\0') {
+ ret = compression_lookup_handler(name, &zuser->save_handler);
+ if (ret <= 0) {
+ i_error("zlib_save: %s: %s", ret == 0 ?
+ "Support not compiled in for handler" :
+ "Unknown handler", name);
+ zuser->save_handler = NULL;
+ }
+ }
+ name = zuser->save_handler == NULL ? NULL :
+ mail_user_plugin_getenv(user, "zlib_save_level");
+ if (name != NULL && name[0] != '\0') {
+ if (str_to_int(name, &zuser->save_level) < 0 ||
+ zuser->save_level < zuser->save_handler->get_min_level() ||
+ zuser->save_level > zuser->save_handler->get_max_level()) {
+ i_error("zlib_save_level: Level must be between %d..%d",
+ zuser->save_handler->get_min_level(),
+ zuser->save_handler->get_max_level());
+ zuser->save_level =
+ zuser->save_handler->get_default_level();
+ }
+ } else if (zuser->save_handler != NULL) {
+ zuser->save_level = zuser->save_handler->get_default_level();
+ }
+ MODULE_CONTEXT_SET(user, zlib_user_module, zuser);
+}
+
+static struct mail_storage_hooks zlib_mail_storage_hooks = {
+ .mail_user_created = zlib_mail_user_created,
+ .mailbox_allocated = zlib_mailbox_allocated,
+ .mail_allocated = zlib_mail_allocated
+};
+
+void zlib_plugin_init(struct module *module)
+{
+ mail_storage_hooks_add(module, &zlib_mail_storage_hooks);
+}
+
+void zlib_plugin_deinit(void)
+{
+ mail_storage_hooks_remove(&zlib_mail_storage_hooks);
+}
diff --git a/src/plugins/zlib/zlib-plugin.h b/src/plugins/zlib/zlib-plugin.h
new file mode 100644
index 0000000..98f520d
--- /dev/null
+++ b/src/plugins/zlib/zlib-plugin.h
@@ -0,0 +1,7 @@
+#ifndef ZLIB_PLUGIN_H
+#define ZLIB_PLUGIN_H
+
+void zlib_plugin_init(struct module *module);
+void zlib_plugin_deinit(void);
+
+#endif
diff --git a/src/pop3-login/Makefile.am b/src/pop3-login/Makefile.am
new file mode 100644
index 0000000..0cba57c
--- /dev/null
+++ b/src/pop3-login/Makefile.am
@@ -0,0 +1,34 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = pop3-login
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/login-common \
+ $(BINARY_CFLAGS)
+
+pop3_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+pop3_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+pop3_login_SOURCES = \
+ client.c \
+ client-authenticate.c \
+ pop3-login-settings.c \
+ pop3-proxy.c
+
+noinst_HEADERS = \
+ client.h \
+ client-authenticate.h \
+ pop3-login-settings.h \
+ pop3-proxy.h
diff --git a/src/pop3-login/Makefile.in b/src/pop3-login/Makefile.in
new file mode 100644
index 0000000..8ecc998
--- /dev/null
+++ b/src/pop3-login/Makefile.in
@@ -0,0 +1,830 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = pop3-login$(EXEEXT)
+subdir = src/pop3-login
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_pop3_login_OBJECTS = client.$(OBJEXT) client-authenticate.$(OBJEXT) \
+ pop3-login-settings.$(OBJEXT) pop3-proxy.$(OBJEXT)
+pop3_login_OBJECTS = $(am_pop3_login_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/client-authenticate.Po \
+ ./$(DEPDIR)/client.Po ./$(DEPDIR)/pop3-login-settings.Po \
+ ./$(DEPDIR)/pop3-proxy.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 = $(pop3_login_SOURCES)
+DIST_SOURCES = $(pop3_login_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/login-common \
+ $(BINARY_CFLAGS)
+
+pop3_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS) \
+ $(BINARY_LDFLAGS)
+
+pop3_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+pop3_login_SOURCES = \
+ client.c \
+ client-authenticate.c \
+ pop3-login-settings.c \
+ pop3-proxy.c
+
+noinst_HEADERS = \
+ client.h \
+ client-authenticate.h \
+ pop3-login-settings.h \
+ pop3-proxy.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/pop3-login/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/pop3-login/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+pop3-login$(EXEEXT): $(pop3_login_OBJECTS) $(pop3_login_DEPENDENCIES) $(EXTRA_pop3_login_DEPENDENCIES)
+ @rm -f pop3-login$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(pop3_login_OBJECTS) $(pop3_login_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-authenticate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-login-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-proxy.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/pop3-login-settings.Po
+ -rm -f ./$(DEPDIR)/pop3-proxy.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-pkglibexecPROGRAMS
+
+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)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/pop3-login-settings.Po
+ -rm -f ./$(DEPDIR)/pop3-proxy.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/pop3-login/client-authenticate.c b/src/pop3-login/client-authenticate.c
new file mode 100644
index 0000000..e215833
--- /dev/null
+++ b/src/pop3-login/client-authenticate.c
@@ -0,0 +1,247 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "auth-client.h"
+#include "../pop3/pop3-capability.h"
+#include "client.h"
+#include "client-authenticate.h"
+#include "pop3-proxy.h"
+
+
+static const char *capability_string = POP3_CAPABILITY_REPLY;
+
+bool cmd_capa(struct pop3_client *client, const char *args ATTR_UNUSED)
+{
+ const struct auth_mech_desc *mech;
+ unsigned int i, count;
+ string_t *str;
+
+ str = t_str_new(128);
+ str_append(str, "+OK\r\n");
+ str_append(str, capability_string);
+
+ if (client_is_tls_enabled(&client->common) && !client->common.tls)
+ str_append(str, "STLS\r\n");
+ if (!client->common.set->disable_plaintext_auth ||
+ client->common.secured)
+ str_append(str, "USER\r\n");
+
+ str_append(str, "SASL");
+ mech = sasl_server_get_advertised_mechs(&client->common, &count);
+ for (i = 0; i < count; i++) {
+ str_append_c(str, ' ');
+ str_append(str, mech[i].name);
+ }
+ str_append(str, "\r\n.\r\n");
+
+ client_send_raw(&client->common, str_c(str));
+ return TRUE;
+}
+
+void pop3_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply ATTR_UNUSED,
+ const char *text)
+{
+ switch (result) {
+ case CLIENT_AUTH_RESULT_SUCCESS:
+ /* nothing to be done for POP3 */
+ break;
+ case CLIENT_AUTH_RESULT_TEMPFAIL:
+ client_send_reply(client, POP3_CMD_REPLY_TEMPFAIL, text);
+ break;
+ case CLIENT_AUTH_RESULT_AUTHFAILED:
+ case CLIENT_AUTH_RESULT_AUTHFAILED_REASON:
+ case CLIENT_AUTH_RESULT_AUTHZFAILED:
+ case CLIENT_AUTH_RESULT_PASS_EXPIRED:
+ case CLIENT_AUTH_RESULT_SSL_REQUIRED:
+ case CLIENT_AUTH_RESULT_LOGIN_DISABLED:
+ case CLIENT_AUTH_RESULT_MECH_INVALID:
+ case CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED:
+ case CLIENT_AUTH_RESULT_INVALID_BASE64:
+ client_send_reply(client, POP3_CMD_REPLY_AUTH_ERROR, text);
+ break;
+ default:
+ client_send_reply(client, POP3_CMD_REPLY_ERROR, text);
+ break;
+ }
+}
+
+int cmd_auth(struct pop3_client *pop3_client)
+{
+ /* NOTE: This command's input is handled specially because the
+ SASL-IR can be large. */
+ struct client *client = &pop3_client->common;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ /* <auth mechanism name> [<initial SASL response>] */
+ if (!pop3_client->auth_mech_name_parsed) {
+ data = i_stream_get_data(client->input, &size);
+ for (i = 0; i < size; i++) {
+ if (data[i] == ' ' ||
+ data[i] == '\r' || data[i] == '\n')
+ break;
+ }
+ if (i == size)
+ return 0;
+ if (i == 0) {
+ /* Old-style SASL discovery, used by MS Outlook */
+ unsigned int i, count;
+ const struct auth_mech_desc *mech;
+
+ client_send_raw(client, "+OK\r\n");
+ mech = sasl_server_get_advertised_mechs(client, &count);
+ for (i = 0; i < count; i++) {
+ client_send_raw(client, mech[i].name);
+ client_send_raw(client, "\r\n");
+ }
+ client_send_raw(client, ".\r\n");
+ (void)i_stream_read_next_line(client->input);
+ return 1;
+ }
+ i_free(client->auth_mech_name);
+ client->auth_mech_name = i_strndup(data, i);
+ pop3_client->auth_mech_name_parsed = TRUE;
+ if (data[i] == ' ')
+ i++;
+ i_stream_skip(client->input, i);
+ }
+
+ /* get SASL-IR, if any */
+ if ((ret = client_auth_read_line(client)) <= 0)
+ return ret;
+
+ const char *ir = NULL;
+ if (client->auth_response->used > 0)
+ ir = t_strdup(str_c(client->auth_response));
+
+ pop3_client->auth_mech_name_parsed = FALSE;
+ /* The whole AUTH line command is parsed now. The rest of the SASL
+ protocol exchange happens in login-common code. We can free the
+ current command here already, because no pop3-login code is called
+ until the authentication is finished. Also, there's currently no
+ single location that is called in pop3-login code after the
+ authentication is finished. For example it could be an auth failure
+ or it could be a successful authentication with a proxying
+ failure. */
+ i_free(pop3_client->current_cmd);
+ return client_auth_begin(client, t_strdup(client->auth_mech_name), ir);
+}
+
+bool cmd_user(struct pop3_client *pop3_client, const char *args)
+{
+ if (!client_check_plaintext_auth(&pop3_client->common, FALSE)) {
+ if (pop3_client->common.virtual_user == NULL)
+ pop3_client->common.virtual_user = i_strdup(args);
+ return TRUE;
+ }
+
+ i_free(pop3_client->last_user);
+ pop3_client->last_user = i_strdup(args);
+
+ client_send_raw(&pop3_client->common, "+OK\r\n");
+ return TRUE;
+}
+
+bool cmd_pass(struct pop3_client *pop3_client, const char *args)
+{
+ struct client *client = &pop3_client->common;
+ string_t *plain_login, *base64;
+
+ if (pop3_client->last_user == NULL) {
+ /* client may ignore the USER reply and only display the error
+ message from PASS */
+ if (!client_check_plaintext_auth(client, TRUE))
+ return TRUE;
+
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ "No username given.");
+ return TRUE;
+ }
+
+ /* authorization ID \0 authentication ID \0 pass */
+ plain_login = t_str_new(128);
+ str_append_c(plain_login, '\0');
+ str_append(plain_login, pop3_client->last_user);
+ str_append_c(plain_login, '\0');
+ str_append(plain_login, args);
+
+ i_free_and_null(pop3_client->last_user);
+
+ base64 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(plain_login->used));
+ base64_encode(plain_login->data, plain_login->used, base64);
+
+ (void)client_auth_begin(client, "PLAIN", str_c(base64));
+ return TRUE;
+}
+
+bool cmd_apop(struct pop3_client *pop3_client, const char *args)
+{
+ struct client *client = &pop3_client->common;
+ buffer_t *apop_data, *base64;
+ const char *p;
+ unsigned int server_pid, connect_uid;
+
+ if (pop3_client->apop_challenge == NULL) {
+ if (client->set->auth_verbose)
+ e_info(client->event, "APOP failed: APOP not enabled");
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ "APOP not enabled.");
+ return TRUE;
+ }
+
+ /* <username> <md5 sum in hex> */
+ p = strchr(args, ' ');
+ if (p == NULL || strlen(p+1) != 32) {
+ if (client->set->auth_verbose)
+ e_info(client->event, "APOP failed: Invalid parameters");
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ "Invalid parameters.");
+ return TRUE;
+ }
+
+ /* APOP challenge \0 username \0 APOP response */
+ apop_data = t_buffer_create(128);
+ buffer_append(apop_data, pop3_client->apop_challenge,
+ strlen(pop3_client->apop_challenge)+1);
+ buffer_append(apop_data, args, (size_t)(p-args));
+ buffer_append_c(apop_data, '\0');
+
+ if (hex_to_binary(p+1, apop_data) < 0) {
+ if (client->set->auth_verbose) {
+ e_info(client->event, "APOP failed: "
+ "Invalid characters in MD5 response");
+ }
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ "Invalid characters in MD5 response.");
+ return TRUE;
+ }
+
+ base64 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(apop_data->used));
+ base64_encode(apop_data->data, apop_data->used, base64);
+
+ auth_client_get_connect_id(auth_client, &server_pid, &connect_uid);
+ if (pop3_client->apop_server_pid != server_pid ||
+ pop3_client->apop_connect_uid != connect_uid) {
+ /* we reconnected to auth server and can't authenticate
+ with APOP in this session anymore. disconnecting the user
+ is probably the best solution now. */
+ client_destroy(client,
+ "Reconnected to auth server, can't do APOP");
+ return TRUE;
+ }
+
+ (void)client_auth_begin_private(client, "APOP", str_c(base64));
+ return TRUE;
+}
diff --git a/src/pop3-login/client-authenticate.h b/src/pop3-login/client-authenticate.h
new file mode 100644
index 0000000..f9b6fa6
--- /dev/null
+++ b/src/pop3-login/client-authenticate.h
@@ -0,0 +1,15 @@
+#ifndef CLIENT_AUTHENTICATE_H
+#define CLIENT_AUTHENTICATE_H
+
+void pop3_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply,
+ const char *text);
+
+bool cmd_capa(struct pop3_client *client, const char *args);
+bool cmd_user(struct pop3_client *client, const char *args);
+bool cmd_pass(struct pop3_client *client, const char *args);
+int cmd_auth(struct pop3_client *client);
+bool cmd_apop(struct pop3_client *client, const char *args);
+
+#endif
diff --git a/src/pop3-login/client.c b/src/pop3-login/client.c
new file mode 100644
index 0000000..d57d284
--- /dev/null
+++ b/src/pop3-login/client.c
@@ -0,0 +1,395 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "randgen.h"
+#include "hostpid.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "client.h"
+#include "client-authenticate.h"
+#include "auth-client.h"
+#include "pop3-proxy.h"
+#include "pop3-login-settings.h"
+
+#include <ctype.h>
+
+/* Disconnect client when it sends too many bad commands */
+#define CLIENT_MAX_BAD_COMMANDS 3
+#define CLIENT_MAX_CMD_LEN 8
+
+static bool cmd_stls(struct pop3_client *client)
+{
+ client_cmd_starttls(&client->common);
+ return TRUE;
+}
+
+static bool cmd_quit(struct pop3_client *client)
+{
+ client_send_reply(&client->common, POP3_CMD_REPLY_OK, "Logging out");
+ client_destroy(&client->common, CLIENT_UNAUTHENTICATED_LOGOUT_MSG);
+ return TRUE;
+}
+
+static bool cmd_xclient(struct pop3_client *client, const char *args)
+{
+ const char *const *tmp;
+ in_port_t remote_port;
+ bool args_ok = TRUE;
+
+ if (!client->common.trusted) {
+ client_send_reply(&client->common, POP3_CMD_REPLY_OK,
+ "You are not from trusted IP - ignoring");
+ return TRUE;
+ }
+ for (tmp = t_strsplit(args, " "); *tmp != NULL; tmp++) {
+ if (strncasecmp(*tmp, "ADDR=", 5) == 0) {
+ if (net_addr2ip(*tmp + 5, &client->common.ip) < 0)
+ args_ok = FALSE;
+ } else if (strncasecmp(*tmp, "PORT=", 5) == 0) {
+ if (net_str2port(*tmp + 5, &remote_port) < 0)
+ args_ok = FALSE;
+ else
+ client->common.remote_port = remote_port;
+ } else if (strncasecmp(*tmp, "SESSION=", 8) == 0) {
+ const char *value = *tmp + 8;
+
+ if (strlen(value) <= LOGIN_MAX_SESSION_ID_LEN) {
+ client->common.session_id =
+ p_strdup(client->common.pool, value);
+ }
+ } else if (strncasecmp(*tmp, "TTL=", 4) == 0) {
+ if (str_to_uint(*tmp + 4, &client->common.proxy_ttl) < 0)
+ args_ok = FALSE;
+ } else if (strncasecmp(*tmp, "FORWARD=", 8) == 0) {
+ size_t value_len = strlen((*tmp)+8);
+ client->common.forward_fields =
+ str_new(client->common.preproxy_pool,
+ MAX_BASE64_DECODED_SIZE(value_len));
+ if (base64_decode((*tmp)+8, value_len, NULL,
+ client->common.forward_fields) < 0)
+ args_ok = FALSE;
+ }
+ }
+ if (!args_ok) {
+ client_send_reply(&client->common, POP3_CMD_REPLY_ERROR,
+ "Invalid parameters");
+ return TRUE;
+ }
+
+ /* args ok, set them and reset the state */
+ client_send_reply(&client->common, POP3_CMD_REPLY_OK, "Updated");
+ return TRUE;
+}
+
+static bool client_command_execute(struct pop3_client *client, const char *cmd,
+ const char *args)
+{
+ if (strcmp(cmd, "CAPA") == 0)
+ return cmd_capa(client, args);
+ if (strcmp(cmd, "USER") == 0)
+ return cmd_user(client, args);
+ if (strcmp(cmd, "PASS") == 0)
+ return cmd_pass(client, args);
+ if (strcmp(cmd, "APOP") == 0)
+ return cmd_apop(client, args);
+ if (strcmp(cmd, "STLS") == 0)
+ return cmd_stls(client);
+ if (strcmp(cmd, "QUIT") == 0)
+ return cmd_quit(client);
+ if (strcmp(cmd, "XCLIENT") == 0)
+ return cmd_xclient(client, args);
+ if (strcmp(cmd, "XOIP") == 0) {
+ /* Compatibility with Zimbra's patched nginx */
+ return cmd_xclient(client, t_strconcat("ADDR=", args, NULL));
+ }
+
+ client_send_reply(&client->common, POP3_CMD_REPLY_ERROR,
+ "Unknown command.");
+ return FALSE;
+}
+
+static void pop3_client_input(struct client *client)
+{
+ i_assert(!client->authenticating);
+
+ if (!client_read(client))
+ return;
+
+ client_ref(client);
+
+ o_stream_cork(client->output);
+ /* if a command starts an authentication, stop processing further
+ commands until the authentication is finished. */
+ while (!client->output->closed && !client->authenticating &&
+ auth_client_is_connected(auth_client)) {
+ if (!client->v.input_next_cmd(client))
+ break;
+ }
+
+ if (auth_client != NULL && !auth_client_is_connected(auth_client))
+ client->input_blocked = TRUE;
+
+ o_stream_uncork(client->output);
+ client_unref(&client);
+}
+
+static bool client_read_cmd_name(struct client *client, const char **cmd_r)
+{
+ const unsigned char *data;
+ size_t size, i;
+ string_t *cmd = t_str_new(CLIENT_MAX_CMD_LEN);
+ if (i_stream_read_more(client->input, &data, &size) <= 0)
+ return FALSE;
+ for(i = 0; i < size; i++) {
+ if (data[i] == '\r') continue;
+ if (data[i] == ' ' ||
+ data[i] == '\n' ||
+ data[i] == '\0' ||
+ i >= CLIENT_MAX_CMD_LEN) {
+ *cmd_r = str_c(cmd);
+ /* only skip ws */
+ i_stream_skip(client->input, i + (data[i] == ' ' ? 1 : 0));
+ return TRUE;
+ }
+ str_append_c(cmd, i_toupper(data[i]));
+ }
+ return FALSE;
+}
+
+static bool pop3_client_input_next_cmd(struct client *client)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+ const char *cmd, *args;
+
+ if (pop3_client->current_cmd == NULL) {
+ if (!client_read_cmd_name(client, &cmd))
+ return FALSE;
+ pop3_client->current_cmd = i_strdup(cmd);
+ }
+
+ if (strcmp(pop3_client->current_cmd, "AUTH") == 0) {
+ if (cmd_auth(pop3_client) <= 0) {
+ /* Need more input / destroyed. We also get here when
+ SASL authentication is actually started. */
+ return FALSE;
+ }
+ /* AUTH command finished already (SASL probe or ERR reply) */
+ i_free(pop3_client->current_cmd);
+ return TRUE;
+ }
+
+ if ((args = i_stream_next_line(client->input)) == NULL)
+ return FALSE;
+
+ if (client_command_execute(pop3_client, pop3_client->current_cmd, args))
+ client->bad_counter = 0;
+ else if (++client->bad_counter >= CLIENT_MAX_BAD_COMMANDS) {
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ "Too many invalid bad commands.");
+ client_destroy(client,
+ "Disconnected: Too many bad commands");
+ return FALSE;
+ }
+ i_free(pop3_client->current_cmd);
+ return TRUE;
+}
+
+static struct client *pop3_client_alloc(pool_t pool)
+{
+ struct pop3_client *pop3_client;
+
+ pop3_client = p_new(pool, struct pop3_client, 1);
+ return &pop3_client->common;
+}
+
+static void pop3_client_create(struct client *client ATTR_UNUSED,
+ void **other_sets ATTR_UNUSED)
+{
+}
+
+static void pop3_client_destroy(struct client *client)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+
+ i_free_and_null(pop3_client->current_cmd);
+ i_free_and_null(pop3_client->last_user);
+ i_free_and_null(pop3_client->apop_challenge);
+}
+
+static char *get_apop_challenge(struct pop3_client *client)
+{
+ unsigned char buffer[16];
+ unsigned char buffer_base64[MAX_BASE64_ENCODED_SIZE(sizeof(buffer)) + 1];
+ buffer_t buf;
+
+ if (sasl_server_find_available_mech(&client->common, "APOP") == NULL) {
+ /* disabled, no need to present the challenge */
+ return NULL;
+ }
+
+ auth_client_get_connect_id(auth_client, &client->apop_server_pid,
+ &client->apop_connect_uid);
+
+ random_fill(buffer, sizeof(buffer));
+ buffer_create_from_data(&buf, buffer_base64, sizeof(buffer_base64));
+ base64_encode(buffer, sizeof(buffer), &buf);
+ buffer_append_c(&buf, '\0');
+
+ return i_strdup_printf("<%x.%x.%lx.%s@%s>",
+ client->apop_server_pid,
+ client->apop_connect_uid,
+ (unsigned long)ioloop_time,
+ (const char *)buf.data, my_hostname);
+}
+
+static void pop3_client_notify_auth_ready(struct client *client)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+ string_t *str;
+
+ client->io = io_add_istream(client->input, client_input, client);
+
+ str = t_str_new(128);
+ if (client->trusted) {
+ /* Dovecot extension to avoid extra roundtrip for CAPA */
+ str_append(str, "[XCLIENT] ");
+ }
+ str_append(str, client->set->login_greeting);
+
+ pop3_client->apop_challenge = get_apop_challenge(pop3_client);
+ if (pop3_client->apop_challenge != NULL)
+ str_printfa(str, " %s", pop3_client->apop_challenge);
+ client_send_reply(client, POP3_CMD_REPLY_OK, str_c(str));
+
+ client->banner_sent = TRUE;
+}
+
+static void
+pop3_client_notify_starttls(struct client *client,
+ bool success, const char *text)
+{
+ if (success)
+ client_send_reply(client, POP3_CMD_REPLY_OK, text);
+ else
+ client_send_reply(client, POP3_CMD_REPLY_ERROR, text);
+}
+
+static void pop3_client_starttls(struct client *client ATTR_UNUSED)
+{
+}
+
+void client_send_reply(struct client *client, enum pop3_cmd_reply reply,
+ const char *text)
+{
+ const char *prefix = "-ERR";
+
+ switch (reply) {
+ case POP3_CMD_REPLY_OK:
+ prefix = "+OK";
+ break;
+ case POP3_CMD_REPLY_TEMPFAIL:
+ prefix = "-ERR [SYS/TEMP]";
+ break;
+ case POP3_CMD_REPLY_AUTH_ERROR:
+ if (text[0] == '[')
+ prefix = "-ERR";
+ else
+ prefix = "-ERR [AUTH]";
+ break;
+ case POP3_CMD_REPLY_ERROR:
+ break;
+ }
+
+ T_BEGIN {
+ string_t *line = t_str_new(256);
+
+ str_append(line, prefix);
+ str_append_c(line, ' ');
+ str_append(line, text);
+ str_append(line, "\r\n");
+
+ client_send_raw_data(client, str_data(line), str_len(line));
+ } T_END;
+}
+
+static void
+pop3_client_notify_disconnect(struct client *client,
+ enum client_disconnect_reason reason,
+ const char *text)
+{
+ if (reason == CLIENT_DISCONNECT_INTERNAL_ERROR)
+ client_send_reply(client, POP3_CMD_REPLY_TEMPFAIL, text);
+ else
+ client_send_reply(client, POP3_CMD_REPLY_ERROR, text);
+}
+
+static void pop3_login_die(void)
+{
+ /* do nothing. pop3 connections typically die pretty quick anyway. */
+}
+
+static void pop3_login_preinit(void)
+{
+ login_set_roots = pop3_login_setting_roots;
+}
+
+static void pop3_login_init(void)
+{
+ /* override the default login_die() */
+ master_service_set_die_callback(master_service, pop3_login_die);
+}
+
+static void pop3_login_deinit(void)
+{
+ clients_destroy_all();
+}
+
+static struct client_vfuncs pop3_client_vfuncs = {
+ .alloc = pop3_client_alloc,
+ .create = pop3_client_create,
+ .destroy = pop3_client_destroy,
+ .notify_auth_ready = pop3_client_notify_auth_ready,
+ .notify_disconnect = pop3_client_notify_disconnect,
+ .notify_starttls = pop3_client_notify_starttls,
+ .starttls = pop3_client_starttls,
+ .input = pop3_client_input,
+ .auth_result = pop3_client_auth_result,
+ .proxy_reset = pop3_proxy_reset,
+ .proxy_parse_line = pop3_proxy_parse_line,
+ .proxy_failed = pop3_proxy_failed,
+ .proxy_get_state = pop3_proxy_get_state,
+ .send_raw_data = client_common_send_raw_data,
+ .input_next_cmd = pop3_client_input_next_cmd,
+ .free = client_common_default_free,
+};
+
+static struct login_binary pop3_login_binary = {
+ .protocol = "pop3",
+ .process_name = "pop3-login",
+ .default_port = 110,
+ .default_ssl_port = 995,
+
+ .event_category = {
+ .name = "pop3",
+ },
+
+ .client_vfuncs = &pop3_client_vfuncs,
+ .preinit = pop3_login_preinit,
+ .init = pop3_login_init,
+ .deinit = pop3_login_deinit,
+
+ .sasl_support_final_reply = FALSE,
+ .anonymous_login_acceptable = TRUE,
+};
+
+int main(int argc, char *argv[])
+{
+ return login_binary_run(&pop3_login_binary, argc, argv);
+}
diff --git a/src/pop3-login/client.h b/src/pop3-login/client.h
new file mode 100644
index 0000000..3823685
--- /dev/null
+++ b/src/pop3-login/client.h
@@ -0,0 +1,40 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "net.h"
+#include "client-common.h"
+#include "auth-client.h"
+
+enum pop3_proxy_state {
+ POP3_PROXY_BANNER = 0,
+ POP3_PROXY_STARTTLS,
+ POP3_PROXY_XCLIENT,
+ POP3_PROXY_LOGIN1,
+ POP3_PROXY_LOGIN2,
+
+ POP3_PROXY_STATE_COUNT
+};
+
+struct pop3_client {
+ struct client common;
+
+ char *current_cmd;
+ char *last_user;
+ char *apop_challenge;
+ unsigned int apop_server_pid, apop_connect_uid;
+ enum pop3_proxy_state proxy_state;
+ bool proxy_xclient;
+ bool auth_mech_name_parsed;
+};
+
+enum pop3_cmd_reply {
+ POP3_CMD_REPLY_OK,
+ POP3_CMD_REPLY_ERROR,
+ POP3_CMD_REPLY_AUTH_ERROR,
+ POP3_CMD_REPLY_TEMPFAIL
+};
+
+void client_send_reply(struct client *client, enum pop3_cmd_reply reply,
+ const char *text);
+
+#endif
diff --git a/src/pop3-login/pop3-login-settings.c b/src/pop3-login/pop3-login-settings.c
new file mode 100644
index 0000000..747e51e
--- /dev/null
+++ b/src/pop3-login/pop3-login-settings.c
@@ -0,0 +1,75 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "login-settings.h"
+#include "pop3-login-settings.h"
+
+#include <stddef.h>
+
+/* <settings checks> */
+static struct inet_listener_settings pop3_login_inet_listeners_array[] = {
+ { .name = "pop3", .address = "", .port = 110 },
+ { .name = "pop3s", .address = "", .port = 995, .ssl = TRUE }
+};
+static struct inet_listener_settings *pop3_login_inet_listeners[] = {
+ &pop3_login_inet_listeners_array[0],
+ &pop3_login_inet_listeners_array[1]
+};
+static buffer_t pop3_login_inet_listeners_buf = {
+ { { pop3_login_inet_listeners, sizeof(pop3_login_inet_listeners) } }
+};
+
+/* </settings checks> */
+struct service_settings pop3_login_service_settings = {
+ .name = "pop3-login",
+ .protocol = "pop3",
+ .type = "login",
+ .executable = "pop3-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &pop3_login_inet_listeners_buf,
+ sizeof(pop3_login_inet_listeners[0]) } }
+};
+
+static const struct setting_define pop3_login_setting_defines[] = {
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct setting_parser_info *pop3_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info pop3_login_setting_parser_info = {
+ .module_name = "pop3-login",
+ .defines = pop3_login_setting_defines,
+
+ .type_offset = SIZE_MAX,
+ .parent_offset = SIZE_MAX,
+
+ .dependencies = pop3_login_setting_dependencies
+};
+
+const struct setting_parser_info *pop3_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &pop3_login_setting_parser_info,
+ NULL
+};
diff --git a/src/pop3-login/pop3-login-settings.h b/src/pop3-login/pop3-login-settings.h
new file mode 100644
index 0000000..1c497eb
--- /dev/null
+++ b/src/pop3-login/pop3-login-settings.h
@@ -0,0 +1,6 @@
+#ifndef POP3_LOGIN_SETTINGS_H
+#define POP3_LOGIN_SETTINGS_H
+
+extern const struct setting_parser_info *pop3_login_setting_roots[];
+
+#endif
diff --git a/src/pop3-login/pop3-proxy.c b/src/pop3-login/pop3-proxy.c
new file mode 100644
index 0000000..04ee265
--- /dev/null
+++ b/src/pop3-login/pop3-proxy.c
@@ -0,0 +1,320 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "dsasl-client.h"
+#include "client.h"
+#include "pop3-proxy.h"
+
+static const char *pop3_proxy_state_names[POP3_PROXY_STATE_COUNT] = {
+ "banner", "starttls", "xclient", "login1", "login2"
+};
+
+static int proxy_send_login(struct pop3_client *client, struct ostream *output)
+{
+ struct dsasl_client_settings sasl_set;
+ const unsigned char *sasl_output;
+ size_t len;
+ const char *mech_name, *error;
+ string_t *str = t_str_new(128);
+
+ i_assert(client->common.proxy_ttl > 1);
+ if (client->proxy_xclient &&
+ !client->common.proxy_not_trusted) {
+ string_t *fwd = t_str_new(128);
+ for(const char *const *ptr = client->common.auth_passdb_args;*ptr != NULL; ptr++) {
+ if (strncasecmp(*ptr, "forward_", 8) == 0) {
+ if (str_len(fwd) > 0)
+ str_append_c(fwd, '\t');
+ str_append_tabescaped(fwd, (*ptr)+8);
+ }
+ }
+
+ str_printfa(str, "XCLIENT ADDR=%s PORT=%u SESSION=%s TTL=%u",
+ net_ip2addr(&client->common.ip),
+ client->common.remote_port,
+ client_get_session_id(&client->common),
+ client->common.proxy_ttl - 1);
+ if (str_len(fwd) > 0) {
+ str_append(str, " FORWARD=");
+ base64_encode(str_data(fwd), str_len(fwd), str);
+ }
+ str_append(str, "\r\n");
+ /* remote supports XCLIENT, send it */
+ o_stream_nsend(output, str_data(str), str_len(str));
+ client->proxy_state = POP3_PROXY_XCLIENT;
+ } else {
+ client->proxy_state = POP3_PROXY_LOGIN1;
+ }
+
+ str_truncate(str, 0);
+
+ if (client->common.proxy_mech == NULL) {
+ /* send USER command */
+ str_append(str, "USER ");
+ str_append(str, client->common.proxy_user);
+ str_append(str, "\r\n");
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+ }
+
+ i_assert(client->common.proxy_sasl_client == NULL);
+ i_zero(&sasl_set);
+ sasl_set.authid = client->common.proxy_master_user != NULL ?
+ client->common.proxy_master_user : client->common.proxy_user;
+ sasl_set.authzid = client->common.proxy_user;
+ sasl_set.password = client->common.proxy_password;
+ client->common.proxy_sasl_client =
+ dsasl_client_new(client->common.proxy_mech, &sasl_set);
+ mech_name = dsasl_client_mech_get_name(client->common.proxy_mech);
+
+ str_printfa(str, "AUTH %s ", mech_name);
+ if (dsasl_client_output(client->common.proxy_sasl_client,
+ &sasl_output, &len, &error) < 0) {
+ const char *reason = t_strdup_printf(
+ "SASL mechanism %s init failed: %s",
+ mech_name, error);
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ return -1;
+ }
+ if (len == 0)
+ str_append_c(str, '=');
+ else
+ base64_encode(sasl_output, len, str);
+ str_append(str, "\r\n");
+ o_stream_nsend(output, str_data(str), str_len(str));
+
+ if (client->proxy_state != POP3_PROXY_XCLIENT)
+ client->proxy_state = POP3_PROXY_LOGIN2;
+ return 0;
+}
+
+static int
+pop3_proxy_continue_sasl_auth(struct client *client, struct ostream *output,
+ const char *line)
+{
+ string_t *str;
+ const unsigned char *data;
+ size_t data_len;
+ const char *error;
+ int ret;
+
+ str = t_str_new(128);
+ if (base64_decode(line, strlen(line), NULL, str) < 0) {
+ const char *reason = t_strdup_printf(
+ "Invalid base64 data in AUTH response");
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ ret = dsasl_client_input(client->proxy_sasl_client,
+ str_data(str), str_len(str), &error);
+ if (ret == 0) {
+ ret = dsasl_client_output(client->proxy_sasl_client,
+ &data, &data_len, &error);
+ }
+ if (ret < 0) {
+ const char *reason = t_strdup_printf(
+ "Invalid authentication data: %s", error);
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ i_assert(ret == 0);
+
+ str_truncate(str, 0);
+ base64_encode(data, data_len, str);
+ str_append(str, "\r\n");
+
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+}
+
+int pop3_proxy_parse_line(struct client *client, const char *line)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+ struct ostream *output;
+ enum login_proxy_ssl_flags ssl_flags;
+
+ i_assert(!client->destroyed);
+
+ output = login_proxy_get_ostream(client->login_proxy);
+ switch (pop3_client->proxy_state) {
+ case POP3_PROXY_BANNER:
+ /* this is a banner */
+ if (!str_begins(line, "+OK")) {
+ const char *reason = t_strdup_printf(
+ "Invalid banner: %s", str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ pop3_client->proxy_xclient =
+ str_begins(line+3, " [XCLIENT]");
+
+ ssl_flags = login_proxy_get_ssl_flags(client->login_proxy);
+ if ((ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0) {
+ if (proxy_send_login(pop3_client, output) < 0)
+ return -1;
+ } else {
+ o_stream_nsend_str(output, "STLS\r\n");
+ pop3_client->proxy_state = POP3_PROXY_STARTTLS;
+ }
+ return 0;
+ case POP3_PROXY_STARTTLS:
+ if (!str_begins(line, "+OK")) {
+ const char *reason = t_strdup_printf(
+ "STLS failed: %s", str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE, reason);
+ return -1;
+ }
+ if (login_proxy_starttls(client->login_proxy) < 0)
+ return -1;
+ /* i/ostreams changed. */
+ output = login_proxy_get_ostream(client->login_proxy);
+ if (proxy_send_login(pop3_client, output) < 0)
+ return -1;
+ return 1;
+ case POP3_PROXY_XCLIENT:
+ if (!str_begins(line, "+OK")) {
+ const char *reason = t_strdup_printf(
+ "XCLIENT failed: %s", str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE, reason);
+ return -1;
+ }
+ pop3_client->proxy_state = client->proxy_sasl_client == NULL ?
+ POP3_PROXY_LOGIN1 : POP3_PROXY_LOGIN2;
+ return 0;
+ case POP3_PROXY_LOGIN1:
+ i_assert(client->proxy_sasl_client == NULL);
+ if (!str_begins(line, "+OK"))
+ break;
+
+ /* USER successful, send PASS */
+ o_stream_nsend_str(output, t_strdup_printf(
+ "PASS %s\r\n", client->proxy_password));
+ pop3_client->proxy_state = POP3_PROXY_LOGIN2;
+ return 0;
+ case POP3_PROXY_LOGIN2:
+ if (str_begins(line, "+ ") &&
+ client->proxy_sasl_client != NULL) {
+ /* continue SASL authentication */
+ if (pop3_proxy_continue_sasl_auth(client, output,
+ line+2) < 0)
+ return -1;
+ return 0;
+ }
+ if (!str_begins(line, "+OK"))
+ break;
+
+ /* Login successful. Send this line to client. */
+ line = t_strconcat(line, "\r\n", NULL);
+ o_stream_nsend_str(client->output, line);
+
+ client_proxy_finish_destroy_client(client);
+ return 1;
+ case POP3_PROXY_STATE_COUNT:
+ i_unreached();
+ }
+
+ /* Login failed. Pass through the error message to client.
+
+ If the backend server isn't Dovecot, the error message may
+ be different from Dovecot's "user doesn't exist" error. This
+ would allow an attacker to find out what users exist in the
+ system.
+
+ The optimal way to handle this would be to replace the
+ backend's "password failed" error message with Dovecot's
+ AUTH_FAILED_MSG, but this would require a new setting and
+ the sysadmin to actually bother setting it properly.
+
+ So for now we'll just forward the error message. This
+ shouldn't be a real problem since of course everyone will
+ be using only Dovecot as their backend :) */
+ enum login_proxy_failure_type failure_type =
+ LOGIN_PROXY_FAILURE_TYPE_AUTH;
+ if (!str_begins(line, "-ERR ")) {
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ AUTH_FAILED_MSG);
+ } else if (str_begins(line, "-ERR [SYS/TEMP]")) {
+ /* delay sending the reply until we know if we reconnect */
+ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL;
+ line += 5;
+ } else {
+ client_send_raw(client, t_strconcat(line, "\r\n", NULL));
+ line += 5;
+ }
+
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ failure_type, line);
+ return -1;
+}
+
+void pop3_proxy_reset(struct client *client)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+
+ pop3_client->proxy_state = POP3_PROXY_BANNER;
+}
+
+static void
+pop3_proxy_send_failure_reply(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason)
+{
+ switch (type) {
+ case LOGIN_PROXY_FAILURE_TYPE_CONNECT:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE:
+ case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+ client_send_reply(client, POP3_CMD_REPLY_TEMPFAIL,
+ LOGIN_PROXY_FAILURE_MSG);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+ client_send_reply(client, POP3_CMD_REPLY_ERROR,
+ LOGIN_PROXY_FAILURE_MSG);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL:
+ /* [SYS/TEMP] prefix is already in the reason string */
+ client_send_reply(client, POP3_CMD_REPLY_ERROR, reason);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH:
+ /* reply was already sent */
+ break;
+ }
+}
+
+void pop3_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting)
+{
+ if (!reconnecting)
+ pop3_proxy_send_failure_reply(client, type, reason);
+ client_common_proxy_failed(client, type, reason, reconnecting);
+}
+
+const char *pop3_proxy_get_state(struct client *client)
+{
+ struct pop3_client *pop3_client = (struct pop3_client *)client;
+
+ return pop3_proxy_state_names[pop3_client->proxy_state];
+}
diff --git a/src/pop3-login/pop3-proxy.h b/src/pop3-login/pop3-proxy.h
new file mode 100644
index 0000000..9e23475
--- /dev/null
+++ b/src/pop3-login/pop3-proxy.h
@@ -0,0 +1,12 @@
+#ifndef POP3_PROXY_H
+#define POP3_PROXY_H
+
+void pop3_proxy_reset(struct client *client);
+int pop3_proxy_parse_line(struct client *client, const char *line);
+
+void pop3_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting);
+const char *pop3_proxy_get_state(struct client *client);
+
+#endif
diff --git a/src/pop3/Makefile.am b/src/pop3/Makefile.am
new file mode 100644
index 0000000..fe49741
--- /dev/null
+++ b/src/pop3/Makefile.am
@@ -0,0 +1,39 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = pop3
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ $(BINARY_CFLAGS)
+
+pop3_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+pop3_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+pop3_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+pop3_SOURCES = \
+ main.c \
+ pop3-client.c \
+ pop3-commands.c \
+ pop3-settings.c
+
+headers = \
+ pop3-capability.h \
+ pop3-client.h \
+ pop3-commands.h \
+ pop3-common.h \
+ pop3-settings.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/pop3/Makefile.in b/src/pop3/Makefile.in
new file mode 100644
index 0000000..18675a3
--- /dev/null
+++ b/src/pop3/Makefile.in
@@ -0,0 +1,887 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = pop3$(EXEEXT)
+subdir = src/pop3
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_pop3_OBJECTS = main.$(OBJEXT) pop3-client.$(OBJEXT) \
+ pop3-commands.$(OBJEXT) pop3-settings.$(OBJEXT)
+pop3_OBJECTS = $(am_pop3_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+pop3_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(pop3_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)/main.Po ./$(DEPDIR)/pop3-client.Po \
+ ./$(DEPDIR)/pop3-commands.Po ./$(DEPDIR)/pop3-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 = $(pop3_SOURCES)
+DIST_SOURCES = $(pop3_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; }; \
+ }
+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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ $(BINARY_CFLAGS)
+
+pop3_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+pop3_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT)
+
+pop3_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+pop3_SOURCES = \
+ main.c \
+ pop3-client.c \
+ pop3-commands.c \
+ pop3-settings.c
+
+headers = \
+ pop3-capability.h \
+ pop3-client.h \
+ pop3-commands.h \
+ pop3-common.h \
+ pop3-settings.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/pop3/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/pop3/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+pop3$(EXEEXT): $(pop3_OBJECTS) $(pop3_DEPENDENCIES) $(EXTRA_pop3_DEPENDENCIES)
+ @rm -f pop3$(EXEEXT)
+ $(AM_V_CCLD)$(pop3_LINK) $(pop3_OBJECTS) $(pop3_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3-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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/pop3-client.Po
+ -rm -f ./$(DEPDIR)/pop3-commands.Po
+ -rm -f ./$(DEPDIR)/pop3-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-pkglibexecPROGRAMS
+
+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)/main.Po
+ -rm -f ./$(DEPDIR)/pop3-client.Po
+ -rm -f ./$(DEPDIR)/pop3-commands.Po
+ -rm -f ./$(DEPDIR)/pop3-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 uninstall-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/pop3/main.c b/src/pop3/main.c
new file mode 100644
index 0000000..c929cdb
--- /dev/null
+++ b/src/pop3/main.c
@@ -0,0 +1,433 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "pop3-common.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "ostream.h"
+#include "path-util.h"
+#include "base64.h"
+#include "str.h"
+#include "process-title.h"
+#include "restrict-access.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-login.h"
+#include "master-interface.h"
+#include "var-expand.h"
+#include "mail-error.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-service.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+static bool verbose_proctitle = FALSE;
+static struct mail_storage_service_ctx *storage_service;
+static struct master_login *master_login = NULL;
+
+pop3_client_created_func_t *hook_client_created = NULL;
+
+pop3_client_created_func_t *
+pop3_client_created_hook_set(pop3_client_created_func_t *new_hook)
+{
+ pop3_client_created_func_t *old_hook = hook_client_created;
+
+ hook_client_created = new_hook;
+ return old_hook;
+}
+
+void pop3_refresh_proctitle(void)
+{
+ struct client *client;
+ string_t *title = t_str_new(128);
+
+ if (!verbose_proctitle)
+ return;
+
+ str_append_c(title, '[');
+ switch (pop3_client_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ client = pop3_clients;
+ str_append(title, client->user->username);
+ if (client->user->conn.remote_ip != NULL) {
+ str_append_c(title, ' ');
+ str_append(title,
+ net_ip2addr(client->user->conn.remote_ip));
+ }
+ if (client->destroyed)
+ str_append(title, " (deinit)");
+ break;
+ default:
+ str_printfa(title, "%u connections", pop3_client_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void pop3_die(void)
+{
+ /* do nothing. pop3 connections typically die pretty quick anyway. */
+}
+
+static void client_add_input(struct client *client, const buffer_t *buf)
+{
+ struct ostream *output;
+
+ if (buf != NULL && buf->used > 0) {
+ struct istream *inputs[] = {
+ i_stream_create_copy_from_data(buf->data, buf->used),
+ client->input,
+ NULL
+ };
+ client->input = i_stream_create_concat(inputs);
+ i_stream_copy_fd(client->input, inputs[1]);
+ i_stream_unref(&inputs[0]);
+ i_stream_unref(&inputs[1]);
+ i_stream_set_input_pending(client->input, TRUE);
+ }
+
+ output = client->output;
+ o_stream_ref(output);
+ o_stream_cork(output);
+ (void)client_handle_input(client);
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+}
+
+static int
+client_create_from_input(const struct mail_storage_service_input *input,
+ int fd_in, int fd_out, struct client **client_r,
+ const char **error_r)
+{
+ const char *lookup_error_str =
+ "-ERR [SYS/TEMP] "MAIL_ERRSTR_CRITICAL_MSG"\r\n";
+ struct mail_storage_service_user *user;
+ struct mail_user *mail_user;
+ struct pop3_settings *set;
+ const char *errstr;
+
+ if (mail_storage_service_lookup_next(storage_service, input,
+ &user, &mail_user, error_r) <= 0) {
+ if (write(fd_out, lookup_error_str, strlen(lookup_error_str)) < 0) {
+ /* ignored */
+ }
+ return -1;
+ }
+ restrict_access_allow_coredumps(TRUE);
+
+ set = mail_storage_service_user_get_set(user)[1];
+ if (set->verbose_proctitle)
+ verbose_proctitle = TRUE;
+
+ if (settings_var_expand(&pop3_setting_parser_info, set,
+ mail_user->pool, mail_user_var_expand_table(mail_user),
+ &errstr) <= 0) {
+ *error_r = t_strdup_printf("Failed to expand settings: %s", errstr);
+ mail_user_deinit(&mail_user);
+ mail_storage_service_user_unref(&user);
+ return -1;
+ }
+
+ *client_r = client_create(fd_in, fd_out, mail_user, user, set);
+ return 0;
+}
+
+static int lock_session(struct client *client)
+{
+ int ret;
+
+ i_assert(client->user->namespaces != NULL);
+ i_assert(client->set->pop3_lock_session);
+
+ if ((ret = pop3_lock_session(client)) <= 0) {
+ client_send_line(client, ret < 0 ?
+ "-ERR [SYS/TEMP] Failed to create POP3 session lock." :
+ "-ERR [IN-USE] Mailbox is locked by another POP3 session.");
+ client_destroy(client, "Couldn't lock POP3 session");
+ return -1;
+ }
+
+ return 0;
+}
+
+#define MSG_BYE_INTERNAL_ERROR "-ERR "MAIL_ERRSTR_CRITICAL_MSG
+static int init_namespaces(struct client *client, bool already_logged_in)
+{
+ const char *error;
+
+ /* finish initializing the user (see comment in main()) */
+ if (mail_namespaces_init(client->user, &error) < 0) {
+ if (!already_logged_in)
+ client_send_line(client, MSG_BYE_INTERNAL_ERROR);
+
+ i_error("%s", error);
+ client_destroy(client, error);
+ return -1;
+ }
+
+ i_assert(client->inbox_ns == NULL);
+ client->inbox_ns = mail_namespace_find_inbox(client->user->namespaces);
+ i_assert(client->inbox_ns != NULL);
+
+ return 0;
+}
+
+static void client_init_session(struct client *client)
+{
+ const char *error;
+
+ /*
+ * RFC 1939 requires that the session lock gets acquired before the
+ * positive response is sent to the client indicating a transition
+ * to the TRANSACTION state.
+ *
+ * Since the session lock is stored under the INBOX's storage
+ * directory, the locking code requires that the namespaces are
+ * initialized first.
+ *
+ * If the system administrator configured dovecot to not use session
+ * locks, we can send back the positive response before the
+ * potentially long-running namespace initialization occurs. This
+ * avoids the client possibly timing out during authentication due
+ * to storage initialization taking too long.
+ */
+ if (client->set->pop3_lock_session) {
+ if (init_namespaces(client, FALSE) < 0)
+ return; /* no need to propagate an error */
+
+ if (lock_session(client) < 0)
+ return; /* no need to propagate an error */
+
+ if (!IS_STANDALONE())
+ client_send_line(client, "+OK Logged in.");
+ } else {
+ if (!IS_STANDALONE())
+ client_send_line(client, "+OK Logged in.");
+
+ if (init_namespaces(client, TRUE) < 0)
+ return; /* no need to propagate an error */
+ }
+
+ struct event_reason *reason = event_reason_begin("pop3:initialize");
+ int ret = client_init_mailbox(client, &error);
+ event_reason_end(&reason);
+
+ if (ret < 0) {
+ i_error("%s", error);
+ client_destroy(client, error);
+ }
+}
+
+static void main_stdio_run(const char *username)
+{
+ struct client *client;
+ struct mail_storage_service_input input;
+ buffer_t *input_buf;
+ const char *value, *error, *input_base64;
+
+ i_zero(&input);
+ input.module = input.service = "pop3";
+ input.username = username != NULL ? username : getenv("USER");
+ if (input.username == NULL && IS_STANDALONE())
+ input.username = getlogin();
+ if (input.username == NULL)
+ i_fatal("USER environment missing");
+ if ((value = getenv("IP")) != NULL)
+ (void)net_addr2ip(value, &input.remote_ip);
+ if ((value = getenv("LOCAL_IP")) != NULL)
+ (void)net_addr2ip(value, &input.local_ip);
+
+ input_base64 = getenv("CLIENT_INPUT");
+ input_buf = input_base64 == NULL ? NULL :
+ t_base64_decode_str(input_base64);
+
+ if (client_create_from_input(&input, STDIN_FILENO, STDOUT_FILENO,
+ &client, &error) < 0)
+ i_fatal("%s", error);
+ client_add_input(client, input_buf);
+ client_create_finish(client);
+
+ client_init_session(client);
+ /* client may be destroyed now */
+}
+
+static void
+login_client_connected(const struct master_login_client *login_client,
+ const char *username, const char *const *extra_fields)
+{
+ struct client *client;
+ struct mail_storage_service_input input;
+ enum mail_auth_request_flags flags = login_client->auth_req.flags;
+ const char *error;
+ buffer_t input_buf;
+
+ i_zero(&input);
+ input.module = input.service = "pop3";
+ input.local_ip = login_client->auth_req.local_ip;
+ input.remote_ip = login_client->auth_req.remote_ip;
+ input.local_port = login_client->auth_req.local_port;
+ input.remote_port = login_client->auth_req.remote_port;
+ input.username = username;
+ input.userdb_fields = extra_fields;
+ input.session_id = login_client->session_id;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SECURED) != 0)
+ input.conn_secured = TRUE;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED) != 0)
+ input.conn_ssl_secured = TRUE;
+
+ buffer_create_from_const_data(&input_buf, login_client->data,
+ login_client->auth_req.data_size);
+ if (client_create_from_input(&input, login_client->fd, login_client->fd,
+ &client, &error) < 0) {
+ int fd = login_client->fd;
+
+ i_error("%s", error);
+ i_close_fd(&fd);
+ master_service_client_connection_destroyed(master_service);
+ return;
+ }
+ client_add_input(client, &input_buf);
+ client_create_finish(client);
+
+ client_init_session(client);
+ /* client may be destroyed now */
+}
+
+static void login_client_failed(const struct master_login_client *client,
+ const char *errormsg)
+{
+ const char *msg;
+
+ msg = t_strdup_printf("-ERR [SYS/TEMP] %s\r\n", errormsg);
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ /* when running standalone, we shouldn't even get here */
+ i_assert(master_login != NULL);
+
+ master_service_client_connection_accept(conn);
+ master_login_add(master_login, conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &pop3_setting_parser_info,
+ NULL
+ };
+ struct master_login_settings login_set;
+ enum master_service_flags service_flags = 0;
+ enum mail_storage_service_flags storage_service_flags =
+ MAIL_STORAGE_SERVICE_FLAG_NO_SSL_CA;
+ const char *username = NULL, *auth_socket_path = "auth-master";
+ int c;
+
+ i_zero(&login_set);
+ login_set.postlogin_timeout_secs = MASTER_POSTLOGIN_TIMEOUT_DEFAULT;
+
+ if (IS_STANDALONE() && getuid() == 0 &&
+ net_getpeername(1, NULL, NULL) == 0) {
+ printf("-ERR [SYS/PERM] pop3 binary must not be started from "
+ "inetd, use pop3-login instead.\n");
+ return 1;
+ }
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ }
+
+ /*
+ * We include MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES so that the
+ * mail_user initialization is fast and we can quickly send back the
+ * OK response to LOGIN/AUTHENTICATE. Otherwise we risk a very slow
+ * namespace initialization to cause client timeouts on login.
+ */
+ storage_service_flags |= MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES;
+
+ master_service = master_service_init("pop3", service_flags,
+ &argc, &argv, "a:t:u:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a':
+ auth_socket_path = optarg;
+ break;
+ case 't':
+ if (str_to_uint(optarg, &login_set.postlogin_timeout_secs) < 0 ||
+ login_set.postlogin_timeout_secs == 0)
+ i_fatal("Invalid -t parameter: %s", optarg);
+ break;
+ case 'u':
+ storage_service_flags |=
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ username = optarg;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ const char *error;
+ if (t_abspath(auth_socket_path, &login_set.auth_socket_path, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", auth_socket_path, error);
+ }
+ if (argv[optind] != NULL) {
+ if (t_abspath(argv[optind], &login_set.postlogin_socket_path, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", argv[optind], error);
+ }
+ }
+ login_set.callback = login_client_connected;
+ login_set.failure_callback = login_client_failed;
+ login_set.update_proctitle =
+ getenv(MASTER_VERBOSE_PROCTITLE_ENV) != NULL &&
+ master_service_get_client_limit(master_service) == 1;
+ if (!IS_STANDALONE())
+ master_login = master_login_init(master_service, &login_set);
+
+ master_service_set_die_callback(master_service, pop3_die);
+
+ storage_service =
+ mail_storage_service_init(master_service,
+ set_roots, storage_service_flags);
+ master_service_init_finish(master_service);
+ /* NOTE: login_set.*_socket_path are now invalid due to data stack
+ having been freed */
+
+ /* fake that we're running, so we know if client was destroyed
+ while handling its initial input */
+ io_loop_set_running(current_ioloop);
+
+ if (IS_STANDALONE()) {
+ T_BEGIN {
+ main_stdio_run(username);
+ } T_END;
+ } else {
+ io_loop_set_running(current_ioloop);
+ }
+
+ if (io_loop_is_running(current_ioloop))
+ master_service_run(master_service, client_connected);
+ clients_destroy_all();
+
+ if (master_login != NULL)
+ master_login_deinit(&master_login);
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/pop3/pop3-capability.h b/src/pop3/pop3-capability.h
new file mode 100644
index 0000000..3cd48ce
--- /dev/null
+++ b/src/pop3/pop3-capability.h
@@ -0,0 +1,14 @@
+#ifndef POP3_CAPABILITY_H
+#define POP3_CAPABILITY_H
+
+#define POP3_CAPABILITY_REPLY \
+ "CAPA\r\n" \
+ "TOP\r\n" \
+ "UIDL\r\n" \
+ "RESP-CODES\r\n" \
+ "PIPELINING\r\n" \
+ "AUTH-RESP-CODE\r\n"
+
+/* + SASL */
+
+#endif
diff --git a/src/pop3/pop3-client.c b/src/pop3/pop3-client.c
new file mode 100644
index 0000000..4bdbea2
--- /dev/null
+++ b/src/pop3/pop3-client.c
@@ -0,0 +1,869 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "pop3-common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-rawlog.h"
+#include "crc32.h"
+#include "str.h"
+#include "llist.h"
+#include "hostpid.h"
+#include "file-dotlock.h"
+#include "var-expand.h"
+#include "master-service.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-autoexpunge.h"
+#include "pop3-commands.h"
+#include "mail-search-build.h"
+#include "mail-namespace.h"
+
+#include <unistd.h>
+
+/* max. length of input command line (spec says 512) */
+#define MAX_INBUF_SIZE 2048
+
+/* Disconnect client when it sends too many bad commands in a row */
+#define CLIENT_MAX_BAD_COMMANDS 20
+
+/* Disconnect client after idling this many milliseconds */
+#define CLIENT_IDLE_TIMEOUT_MSECS (10*60*1000)
+/* If client starts idling for this many milliseconds, commit the current
+ transaction. This allows the mailbox to become unlocked. */
+#define CLIENT_COMMIT_TIMEOUT_MSECS (10*1000)
+
+#define POP3_LOCK_FNAME "dovecot-pop3-session.lock"
+#define POP3_SESSION_DOTLOCK_STALE_TIMEOUT_SECS (60*5)
+
+extern struct pop3_client_vfuncs pop3_client_vfuncs;
+
+struct pop3_module_register pop3_module_register = { 0 };
+
+struct client *pop3_clients;
+unsigned int pop3_client_count;
+
+static enum mail_sort_type pop3_sort_program[] = {
+ MAIL_SORT_POP3_ORDER,
+ MAIL_SORT_END
+};
+
+static const struct dotlock_settings session_dotlock_set = {
+ .timeout = 10,
+ .stale_timeout = POP3_SESSION_DOTLOCK_STALE_TIMEOUT_SECS,
+ .lock_suffix = "",
+ .use_io_notify = TRUE
+};
+
+static void client_input(struct client *client);
+static int client_output(struct client *client);
+
+static void client_commit_timeout(struct client *client)
+{
+ if (client->cmd != NULL) {
+ /* Can't commit while commands are running */
+ return;
+ }
+
+ (void)mailbox_transaction_commit(&client->trans);
+ client->trans = mailbox_transaction_begin(client->mailbox, 0, __func__);
+}
+
+static void client_idle_timeout(struct client *client)
+{
+ if (client->cmd != NULL) {
+ client_destroy(client, t_strdup_printf(
+ "Client has not read server output for for %"PRIdTIME_T" secs",
+ ioloop_time - client->last_output));
+ } else {
+ client_send_line(client, "-ERR Disconnected for inactivity.");
+ client_destroy(client, t_strdup_printf(
+ "Inactivity - no input for %"PRIdTIME_T" secs",
+ ioloop_time - client->last_input));
+ }
+}
+
+static int
+pop3_mail_get_size(struct client *client, struct mail *mail, uoff_t *size_r)
+{
+ int ret;
+
+ if (!client->set->pop3_fast_size_lookups)
+ return mail_get_virtual_size(mail, size_r);
+
+ /* first try to get the virtual size */
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ ret = mail_get_virtual_size(mail, size_r);
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+ if (ret == 0)
+ return 0;
+
+ if (mailbox_get_last_mail_error(mail->box) != MAIL_ERROR_LOOKUP_ABORTED)
+ return -1;
+
+ /* virtual size not available with a fast lookup.
+ fallback to trying the physical size */
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ ret = mail_get_physical_size(mail, size_r);
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+ if (ret == 0)
+ return 0;
+
+ if (mailbox_get_last_mail_error(mail->box) != MAIL_ERROR_LOOKUP_ABORTED)
+ return -1;
+
+ /* no way to quickly get the size. fallback to doing a slow virtual
+ size lookup */
+ return mail_get_virtual_size(mail, size_r);
+}
+
+static void
+msgnum_to_seq_map_add(ARRAY_TYPE(uint32_t) *msgnum_to_seq_map,
+ struct client *client, struct mail *mail,
+ unsigned int msgnum)
+{
+ uint32_t seq;
+
+ if (mail->seq == msgnum+1)
+ return;
+
+ if (!array_is_created(msgnum_to_seq_map))
+ i_array_init(msgnum_to_seq_map, client->messages_count);
+
+ /* add any messages between this and the previous one that had
+ a POP3 order defined */
+ seq = array_count(msgnum_to_seq_map) + 1;
+ for (; seq <= msgnum; seq++)
+ array_push_back(msgnum_to_seq_map, &seq);
+ array_push_back(msgnum_to_seq_map, &mail->seq);
+}
+
+static int read_mailbox(struct client *client, uint32_t *failed_uid_r)
+{
+ struct mailbox_status status;
+ struct mailbox_transaction_context *t;
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ uoff_t size;
+ ARRAY(uoff_t) message_sizes;
+ ARRAY_TYPE(uint32_t) msgnum_to_seq_map = ARRAY_INIT;
+ unsigned int msgnum;
+ int ret = 1;
+
+ *failed_uid_r = 0;
+
+ mailbox_get_open_status(client->mailbox, STATUS_UIDVALIDITY, &status);
+ client->uid_validity = status.uidvalidity;
+ client->messages_count = status.messages;
+
+ t = mailbox_transaction_begin(client->mailbox, 0, __func__);
+
+ search_args = mail_search_build_init();
+ if (client->deleted_kw != NULL) {
+ sarg = mail_search_build_add(search_args, SEARCH_KEYWORDS);
+ sarg->match_not = TRUE;
+ sarg->value.str = p_strdup(search_args->pool,
+ client->set->pop3_deleted_flag);
+ i_array_init(&client->all_seqs, 32);
+ } else {
+ mail_search_build_add_all(search_args);
+ }
+ mail_search_args_init(search_args, client->mailbox, TRUE, NULL);
+
+ ctx = mailbox_search_init(t, search_args, pop3_sort_program,
+ client->set->pop3_fast_size_lookups ? 0 :
+ MAIL_FETCH_VIRTUAL_SIZE, NULL);
+ mail_search_args_unref(&search_args);
+
+ client->last_seen_pop3_msn = 0;
+ client->total_size = 0;
+ i_array_init(&message_sizes, client->messages_count);
+
+ msgnum = 0;
+ while (mailbox_search_next(ctx, &mail)) {
+ if (pop3_mail_get_size(client, mail, &size) < 0) {
+ ret = mail->expunged ? 0 : -1;
+ *failed_uid_r = mail->uid;
+ break;
+ }
+ if (array_is_created(&client->all_seqs))
+ seq_range_array_add(&client->all_seqs, mail->seq);
+ msgnum_to_seq_map_add(&msgnum_to_seq_map, client, mail, msgnum);
+
+ if ((mail_get_flags(mail) & MAIL_SEEN) != 0)
+ client->last_seen_pop3_msn = msgnum + 1;
+ client->total_size += size;
+ if (client->highest_seq < mail->seq)
+ client->highest_seq = mail->seq;
+
+ array_push_back(&message_sizes, &size);
+ msgnum++;
+ }
+
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = -1;
+
+ if (ret <= 0) {
+ /* commit the transaction instead of rolling back to make sure
+ we don't lose data (virtual sizes) added to cache file */
+ (void)mailbox_transaction_commit(&t);
+ array_free(&message_sizes);
+ if (array_is_created(&msgnum_to_seq_map))
+ array_free(&msgnum_to_seq_map);
+ return ret;
+ }
+ i_assert(msgnum <= client->messages_count);
+ client->messages_count = msgnum;
+
+ if (!array_is_created(&client->all_seqs)) {
+ i_array_init(&client->all_seqs, 1);
+ seq_range_array_add_range(&client->all_seqs, 1, msgnum);
+ }
+
+ client->trans = t;
+ client->message_sizes =
+ array_free_without_data(&message_sizes);
+ if (array_is_created(&msgnum_to_seq_map)) {
+ client->msgnum_to_seq_map_count =
+ array_count(&msgnum_to_seq_map);
+ client->msgnum_to_seq_map =
+ array_free_without_data(&msgnum_to_seq_map);
+ }
+ return 1;
+}
+
+static int init_pop3_deleted_flag(struct client *client, const char **error_r)
+{
+ const char *deleted_keywords[2];
+
+ if (client->set->pop3_deleted_flag[0] == '\0')
+ return 0;
+
+ deleted_keywords[0] = client->set->pop3_deleted_flag;
+ deleted_keywords[1] = NULL;
+ if (mailbox_keywords_create(client->mailbox, deleted_keywords,
+ &client->deleted_kw) < 0) {
+ *error_r = t_strdup_printf(
+ "pop3_deleted_flags: Invalid keyword '%s': %s",
+ client->set->pop3_deleted_flag,
+ mailbox_get_last_internal_error(client->mailbox, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int init_mailbox(struct client *client, const char **error_r)
+{
+ uint32_t failed_uid = 0, last_failed_uid = 0;
+ int i, ret = -1;
+
+ for (i = 0;; i++) {
+ if (mailbox_sync(client->mailbox,
+ MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
+ ret = -1;
+ break;
+ }
+
+ ret = read_mailbox(client, &failed_uid);
+ if (ret > 0)
+ return 0;
+ if (i == 2)
+ break;
+
+ /* well, sync and try again. maybe it works the second time. */
+ last_failed_uid = failed_uid;
+ failed_uid = 0;
+ }
+
+ if (ret < 0) {
+ *error_r = mailbox_get_last_internal_error(client->mailbox, NULL);
+ client_send_storage_error(client);
+ } else {
+ if (failed_uid == last_failed_uid && failed_uid != 0) {
+ /* failed twice in same message */
+ *error_r = t_strdup_printf(
+ "Getting size of message UID=%u failed",
+ failed_uid);
+ } else {
+ *error_r = "Can't sync mailbox: "
+ "Messages keep getting expunged";
+ }
+ client_send_line(client, "-ERR [SYS/TEMP] Couldn't sync mailbox.");
+ }
+ return -1;
+}
+
+static enum uidl_keys parse_uidl_keymask(const char *format)
+{
+ enum uidl_keys mask = 0;
+
+ for (; *format != '\0'; format++) {
+ if (format[0] == '%' && format[1] != '\0') {
+ switch (var_get_key(++format)) {
+ case 'v':
+ mask |= UIDL_UIDVALIDITY;
+ break;
+ case 'u':
+ mask |= UIDL_UID;
+ break;
+ case 'm':
+ mask |= UIDL_MD5;
+ break;
+ case 'f':
+ mask |= UIDL_FILE_NAME;
+ break;
+ case 'g':
+ mask |= UIDL_GUID;
+ break;
+ }
+ }
+ }
+ return mask;
+}
+
+static void pop3_lock_session_refresh(struct client *client)
+{
+ if (file_dotlock_touch(client->session_dotlock) < 0) {
+ client_send_line(client,
+ "-ERR [SYS/TEMP] Couldn't update POP3 session lock.");
+ client_destroy(client, "Couldn't lock POP3 session");
+ }
+}
+
+int pop3_lock_session(struct client *client)
+{
+ const struct mail_storage_settings *mail_set =
+ mail_storage_service_user_get_mail_set(client->service_user);
+ struct dotlock_settings dotlock_set;
+ enum mailbox_list_path_type type;
+ const char *dir, *path;
+ int ret;
+
+ if (mailbox_list_get_root_path(client->inbox_ns->list,
+ MAILBOX_LIST_PATH_TYPE_INDEX, &dir)) {
+ type = MAILBOX_LIST_PATH_TYPE_INDEX;
+ } else if (mailbox_list_get_root_path(client->inbox_ns->list,
+ MAILBOX_LIST_PATH_TYPE_DIR, &dir)) {
+ type = MAILBOX_LIST_PATH_TYPE_DIR;
+ } else {
+ i_error("pop3_lock_session: Storage has no root/index directory, "
+ "can't create a POP3 session lock file");
+ return -1;
+ }
+ if (mailbox_list_mkdir_root(client->inbox_ns->list, dir, type) < 0) {
+ i_error("pop3_lock_session: Couldn't create root directory %s: %s",
+ dir, mailbox_list_get_last_internal_error(client->inbox_ns->list, NULL));
+ return -1;
+ }
+ path = t_strdup_printf("%s/"POP3_LOCK_FNAME, dir);
+
+ dotlock_set = session_dotlock_set;
+ dotlock_set.use_excl_lock = mail_set->dotlock_use_excl;
+ dotlock_set.nfs_flush = mail_set->mail_nfs_storage;
+
+ ret = file_dotlock_create(&dotlock_set, path, 0,
+ &client->session_dotlock);
+ if (ret < 0)
+ i_error("file_dotlock_create(%s) failed: %m", path);
+ else if (ret > 0) {
+ client->to_session_dotlock_refresh =
+ timeout_add(POP3_SESSION_DOTLOCK_STALE_TIMEOUT_SECS*1000,
+ pop3_lock_session_refresh, client);
+ }
+ return ret;
+}
+
+struct client *client_create(int fd_in, int fd_out,
+ struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct pop3_settings *set)
+{
+ struct client *client;
+ pool_t pool;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd_in, TRUE);
+ net_set_nonblock(fd_out, TRUE);
+
+ pool = pool_alloconly_create("pop3 client", 256);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+ client->service_user = service_user;
+ client->v = pop3_client_vfuncs;
+ client->set = set;
+ client->fd_in = fd_in;
+ client->fd_out = fd_out;
+ client->input = i_stream_create_fd(fd_in, MAX_INBUF_SIZE);
+ client->output = o_stream_create_fd(fd_out, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ o_stream_set_flush_callback(client->output, client_output, client);
+
+ p_array_init(&client->module_contexts, client->pool, 5);
+ client->last_input = ioloop_time;
+ client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS,
+ client_idle_timeout, client);
+ client->to_commit = timeout_add(CLIENT_COMMIT_TIMEOUT_MSECS,
+ client_commit_timeout, client);
+
+ client->user = user;
+
+ client->mail_set = mail_user_set_get_storage_set(user);
+ client->uidl_keymask =
+ parse_uidl_keymask(client->mail_set->pop3_uidl_format);
+ if (client->uidl_keymask == 0)
+ i_fatal("Invalid pop3_uidl_format");
+
+ if (var_has_key(set->pop3_logout_format, 'u', "uidl_change")) {
+ /* logging uidl_change. we need hashes of the UIDLs */
+ client->message_uidls_save = TRUE;
+ } else if (strcmp(set->pop3_uidl_duplicates, "allow") != 0) {
+ /* UIDL duplicates aren't allowed, so we'll need to
+ keep track of them */
+ client->message_uidls_save = TRUE;
+ }
+
+ pop3_client_count++;
+ DLLIST_PREPEND(&pop3_clients, client);
+
+ if (hook_client_created != NULL)
+ hook_client_created(&client);
+
+ return client;
+}
+
+void client_create_finish(struct client *client)
+{
+ if (client->set->rawlog_dir[0] != '\0') {
+ (void)iostream_rawlog_create(client->set->rawlog_dir,
+ &client->input, &client->output);
+ }
+ client->io = io_add_istream(client->input, client_input, client);
+}
+
+int client_init_mailbox(struct client *client, const char **error_r)
+{
+ enum mailbox_flags flags;
+ const char *ident, *errmsg;
+
+ /* refresh proctitle before a potentially long-running init_mailbox() */
+ pop3_refresh_proctitle();
+
+ flags = MAILBOX_FLAG_POP3_SESSION;
+ if (!client->set->pop3_no_flag_updates)
+ flags |= MAILBOX_FLAG_DROP_RECENT;
+ client->mailbox = mailbox_alloc(client->inbox_ns->list, "INBOX", flags);
+ if (mailbox_open(client->mailbox) < 0) {
+ *error_r = t_strdup_printf("Couldn't open INBOX: %s",
+ mailbox_get_last_internal_error(client->mailbox, NULL));
+ client_send_storage_error(client);
+ return -1;
+ }
+
+ if (init_pop3_deleted_flag(client, &errmsg) < 0 ||
+ init_mailbox(client, &errmsg) < 0) {
+ *error_r = t_strdup_printf("Couldn't init INBOX: %s", errmsg);
+ return -1;
+ }
+
+ if (!client->set->pop3_no_flag_updates && client->messages_count > 0)
+ client->seen_bitmask = i_malloc(MSGS_BITMASK_SIZE(client));
+
+ ident = mail_user_get_anvil_userip_ident(client->user);
+ if (ident != NULL) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\tpop3/", ident, "\n", NULL));
+ client->anvil_sent = TRUE;
+ }
+ return 0;
+}
+
+static const char *client_build_uidl_change_string(struct client *client)
+{
+ uint32_t i, old_hash, new_hash;
+ unsigned int old_msg_count, new_msg_count;
+
+ if (client->message_uidls == NULL) {
+ /* UIDL command not given */
+ return "";
+ }
+
+ /* 1..new-1 were probably left to mailbox by previous POP3 session */
+ old_msg_count = client->lowest_retr_pop3_msn > 0 ?
+ client->lowest_retr_pop3_msn - 1 : client->messages_count;
+ for (i = 0, old_hash = 0; i < old_msg_count; i++)
+ old_hash ^= crc32_str(client->message_uidls[i]);
+
+ /* assume all except deleted messages were sent to POP3 client */
+ if (!client->deleted) {
+ for (i = 0, new_hash = 0; i < client->messages_count; i++)
+ new_hash ^= crc32_str(client->message_uidls[i]);
+ } else {
+ for (i = 0, new_hash = 0; i < client->messages_count; i++) {
+ if ((client->deleted_bitmask[i / CHAR_BIT] &
+ (1 << (i % CHAR_BIT))) != 0)
+ continue;
+ new_hash ^= crc32_str(client->message_uidls[i]);
+ }
+ }
+
+ new_msg_count = client->messages_count - client->deleted_count;
+ if (old_hash == new_hash && old_msg_count == new_msg_count)
+ return t_strdup_printf("%u/%08x", old_msg_count, old_hash);
+ else {
+ return t_strdup_printf("%u/%08x -> %u/%08x",
+ old_msg_count, old_hash,
+ new_msg_count, new_hash);
+ }
+}
+
+static const char *client_stats(struct client *client)
+{
+ const char *uidl_change = "";
+ if (var_has_key(client->set->pop3_logout_format,
+ 'o', "uidl_change"))
+ uidl_change = client_build_uidl_change_string(client);
+
+ const struct var_expand_table logout_tab[] = {
+ { 'p', dec2str(client->top_bytes), "top_bytes" },
+ { 't', dec2str(client->top_count), "top_count" },
+ { 'b', dec2str(client->retr_bytes), "retr_bytes" },
+ { 'r', dec2str(client->retr_count), "retr_count" },
+ { 'd', !client->delete_success ? "0" :
+ dec2str(client->deleted_count), "deleted_count" },
+ { 'm', dec2str(client->messages_count), "message_count" },
+ { 's', dec2str(client->total_size), "message_bytes" },
+ { 'i', dec2str(client->input->v_offset), "input" },
+ { 'o', dec2str(client->output->offset), "output" },
+ { 'u', uidl_change, "uidl_change" },
+ { '\0', !client->delete_success ? "0" :
+ dec2str(client->deleted_size), "deleted_bytes" },
+ { '\0', NULL, NULL }
+ };
+ const struct var_expand_table *user_tab =
+ mail_user_var_expand_table(client->user);
+ const struct var_expand_table *tab =
+ t_var_expand_merge_tables(logout_tab, user_tab);
+ string_t *str;
+ const char *error;
+
+ str = t_str_new(128);
+ if (var_expand_with_funcs(str, client->set->pop3_logout_format,
+ tab, mail_user_var_expand_func_table,
+ client->user, &error) < 0) {
+ i_error("Failed to expand pop3_logout_format=%s: %s",
+ client->set->pop3_logout_format, error);
+ }
+ return str_c(str);
+}
+
+void client_destroy(struct client *client, const char *reason)
+{
+ client->v.destroy(client, reason);
+}
+
+static void client_default_destroy(struct client *client, const char *reason)
+{
+ i_assert(!client->destroyed);
+
+ client->destroyed = TRUE;
+
+ if (client->seen_change_count > 0)
+ (void)client_update_mails(client);
+
+ if (!client->disconnected) {
+ if (reason == NULL) {
+ reason = io_stream_get_disconnect_reason(client->input,
+ client->output);
+ }
+ i_info("Disconnected: %s %s", reason, client_stats(client));
+ }
+
+ if (client->cmd != NULL) {
+ /* deinitialize command */
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+ client->cmd(client);
+ i_assert(client->cmd == NULL);
+ }
+
+ if (client->trans != NULL) {
+ /* client didn't QUIT, but we still want to save any changes
+ done in this transaction. especially the cached virtual
+ message sizes. */
+ (void)mailbox_transaction_commit(&client->trans);
+ }
+ if (array_is_created(&client->all_seqs))
+ array_free(&client->all_seqs);
+ if (client->deleted_kw != NULL)
+ mailbox_keywords_unref(&client->deleted_kw);
+ if (client->mailbox != NULL)
+ mailbox_free(&client->mailbox);
+ if (client->anvil_sent) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\tpop3/",
+ mail_user_get_anvil_userip_ident(client->user),
+ "\n", NULL));
+ }
+
+ if (client->session_dotlock != NULL)
+ file_dotlock_delete(&client->session_dotlock);
+ timeout_remove(&client->to_session_dotlock_refresh);
+
+ pool_unref(&client->uidl_pool);
+ i_free(client->message_sizes);
+ i_free(client->deleted_bitmask);
+ i_free(client->seen_bitmask);
+ i_free(client->msgnum_to_seq_map);
+
+ io_remove(&client->io);
+ timeout_remove(&client->to_idle);
+ timeout_remove(&client->to_commit);
+
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+
+ fd_close_maybe_stdio(&client->fd_in, &client->fd_out);
+
+ /* Autoexpunging might run for a long time. Disconnect the client
+ before it starts, and refresh proctitle so it's clear that it's
+ doing autoexpunging. We've also sent DISCONNECT to anvil already,
+ because this is background work and shouldn't really be counted
+ as an active POP3 session for the user. */
+ pop3_refresh_proctitle();
+ mail_user_autoexpunge(client->user);
+ mail_user_deinit(&client->user);
+ mail_storage_service_user_unref(&client->service_user);
+
+ pop3_client_count--;
+ DLLIST_REMOVE(&pop3_clients, client);
+ pool_unref(&client->pool);
+
+ master_service_client_connection_destroyed(master_service);
+ pop3_refresh_proctitle();
+}
+
+static void client_destroy_timeout(struct client *client)
+{
+ client_destroy(client, NULL);
+}
+
+void client_disconnect(struct client *client, const char *reason)
+{
+ if (client->disconnected)
+ return;
+
+ client->disconnected = TRUE;
+ i_info("Disconnected: %s %s", reason, client_stats(client));
+
+ (void)o_stream_flush(client->output);
+
+ i_stream_close(client->input);
+ o_stream_close(client->output);
+
+ timeout_remove(&client->to_idle);
+ client->to_idle = timeout_add(0, client_destroy_timeout, client);
+}
+
+void client_send_line(struct client *client, const char *fmt, ...)
+{
+ va_list va;
+ ssize_t ret;
+
+ if (client->output->closed)
+ return;
+
+ va_start(va, fmt);
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ str_vprintfa(str, fmt, va);
+ str_append(str, "\r\n");
+
+ ret = o_stream_send(client->output,
+ str_data(str), str_len(str));
+ i_assert(ret < 0 || (size_t)ret == str_len(str));
+ } T_END;
+ if (ret >= 0) {
+ if (!POP3_CLIENT_OUTPUT_FULL(client))
+ client->last_output = ioloop_time;
+ else if (client->io != NULL) {
+ /* no more input until client has read
+ our output */
+ io_remove(&client->io);
+
+ /* If someone happens to flush output, we want to get
+ our IO handler back in flush callback */
+ o_stream_set_flush_pending(client->output, TRUE);
+ }
+ }
+ va_end(va);
+}
+
+void client_send_storage_error(struct client *client)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ if (mailbox_is_inconsistent(client->mailbox)) {
+ client_send_line(client, "-ERR [SYS/TEMP] Mailbox is in inconsistent "
+ "state, please relogin.");
+ client_disconnect(client, "Mailbox is in inconsistent state.");
+ return;
+ }
+
+ errstr = mailbox_get_last_error(client->mailbox, &error);
+ switch (error) {
+ case MAIL_ERROR_TEMP:
+ case MAIL_ERROR_NOQUOTA:
+ case MAIL_ERROR_INUSE:
+ client_send_line(client, "-ERR [SYS/TEMP] %s", errstr);
+ break;
+ default:
+ client_send_line(client, "-ERR [SYS/PERM] %s", errstr);
+ break;
+ }
+}
+
+bool client_handle_input(struct client *client)
+{
+ char *line, *args;
+ int ret;
+
+ o_stream_cork(client->output);
+ while (!client->output->closed &&
+ (line = i_stream_next_line(client->input)) != NULL) {
+ args = strchr(line, ' ');
+ if (args != NULL)
+ *args++ = '\0';
+
+ const struct pop3_command *cmd = pop3_command_find(line);
+ if (cmd == NULL) {
+ client_send_line(client, "-ERR Unknown command: %s", line);
+ ret = -1;
+ } else T_BEGIN {
+ const char *reason_code =
+ event_reason_code_prefix("pop3", "cmd_",
+ cmd->name);
+ struct event_reason *reason =
+ event_reason_begin(reason_code);
+ ret = client_command_execute(client, cmd,
+ args != NULL ? args : "");
+ event_reason_end(&reason);
+ } T_END;
+ if (ret >= 0) {
+ client->bad_counter = 0;
+ if (client->cmd != NULL) {
+ o_stream_set_flush_pending(client->output,
+ TRUE);
+ client->waiting_input = TRUE;
+ break;
+ }
+ } else if (++client->bad_counter > CLIENT_MAX_BAD_COMMANDS) {
+ client_send_line(client, "-ERR Too many bad commands.");
+ client_disconnect(client, "Too many bad commands.");
+ }
+ }
+ o_stream_uncork(client->output);
+
+ if (client->output->closed) {
+ client_destroy(client, NULL);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void client_input(struct client *client)
+{
+ if (client->cmd != NULL) {
+ /* we're still processing a command. wait until it's
+ finished. */
+ io_remove(&client->io);
+ client->waiting_input = TRUE;
+ return;
+ }
+
+ client->waiting_input = FALSE;
+ client->last_input = ioloop_time;
+ timeout_reset(client->to_idle);
+ if (client->to_commit != NULL)
+ timeout_reset(client->to_commit);
+
+ switch (i_stream_read(client->input)) {
+ case -1:
+ /* disconnected */
+ client_destroy(client, NULL);
+ return;
+ case -2:
+ /* line too long, kill it */
+ client_send_line(client, "-ERR Input line too long.");
+ client_destroy(client, "Input line too long");
+ return;
+ }
+
+ (void)client_handle_input(client);
+}
+
+static int client_output(struct client *client)
+{
+ if (o_stream_flush(client->output) < 0) {
+ client_destroy(client, NULL);
+ return 1;
+ }
+
+ client->last_output = ioloop_time;
+ timeout_reset(client->to_idle);
+ if (client->to_commit != NULL)
+ timeout_reset(client->to_commit);
+
+ if (client->cmd != NULL)
+ client->cmd(client);
+
+ if (client->cmd == NULL) {
+ if (o_stream_get_buffer_used_size(client->output) <
+ POP3_OUTBUF_THROTTLE_SIZE/2 && client->io == NULL &&
+ !client->input->closed) {
+ /* enable input again */
+ client->io = io_add_istream(client->input, client_input,
+ client);
+ }
+ if (client->io != NULL && client->waiting_input) {
+ if (!client_handle_input(client)) {
+ /* client got destroyed */
+ return 1;
+ }
+ }
+ }
+
+ if (client->cmd != NULL) {
+ /* command not finished yet */
+ return 0;
+ } else if (client->io == NULL) {
+ /* data still in output buffer, get back here to add IO */
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+void clients_destroy_all(void)
+{
+ while (pop3_clients != NULL) {
+ mail_storage_service_io_activate_user(pop3_clients->service_user);
+ if (pop3_clients->cmd == NULL) {
+ client_send_line(pop3_clients,
+ "-ERR [SYS/TEMP] Server shutting down.");
+ }
+ client_destroy(pop3_clients, "Server shutting down.");
+ }
+}
+
+struct pop3_client_vfuncs pop3_client_vfuncs = {
+ client_default_destroy
+};
diff --git a/src/pop3/pop3-client.h b/src/pop3/pop3-client.h
new file mode 100644
index 0000000..5cb3f46
--- /dev/null
+++ b/src/pop3/pop3-client.h
@@ -0,0 +1,146 @@
+#ifndef POP3_CLIENT_H
+#define POP3_CLIENT_H
+
+#include "seq-range-array.h"
+
+struct client;
+struct mail_storage;
+struct mail_storage_service_ctx;
+
+typedef void command_func_t(struct client *client);
+
+#define MSGS_BITMASK_SIZE(client) \
+ (MALLOC_ADD((client)->messages_count, (CHAR_BIT-1)) / CHAR_BIT)
+
+/* Stop reading input when output buffer has this many bytes. Once the buffer
+ size has dropped to half of it, start reading input again. */
+#define POP3_OUTBUF_THROTTLE_SIZE 4096
+
+#define POP3_CLIENT_OUTPUT_FULL(client) \
+ (o_stream_get_buffer_used_size((client)->output) >= POP3_OUTBUF_THROTTLE_SIZE)
+
+struct pop3_client_vfuncs {
+ void (*destroy)(struct client *client, const char *reason);
+
+};
+
+/*
+ pop3_msn = 1..n in the POP3 protocol
+ msgnum = 0..n-1 = pop3_msn-1
+ seq = 1..n = mail's sequence number in lib-storage. when pop3 sort ordering
+ is used, msgnum_to_seq_map[] can be used for translation.
+*/
+struct client {
+ struct client *prev, *next;
+
+ struct pop3_client_vfuncs v;
+
+ int fd_in, fd_out;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_idle, *to_commit;
+
+ command_func_t *cmd;
+ void *cmd_context;
+
+ pool_t pool;
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+ struct mail_namespace *inbox_ns;
+ struct mailbox *mailbox;
+ struct mailbox_transaction_context *trans;
+ struct mail_keywords *deleted_kw;
+
+ struct timeout *to_session_dotlock_refresh;
+ struct dotlock *session_dotlock;
+
+ time_t last_input, last_output;
+ unsigned int bad_counter;
+ unsigned int highest_expunged_fetch_msgnum;
+
+ unsigned int uid_validity;
+ unsigned int messages_count;
+ unsigned int deleted_count, seen_change_count;
+ uoff_t total_size;
+ uoff_t deleted_size;
+ uint32_t last_seen_pop3_msn, lowest_retr_pop3_msn;
+
+ /* All sequences currently visible in the mailbox. */
+ ARRAY_TYPE(seq_range) all_seqs;
+ uint32_t highest_seq;
+
+ /* [msgnum] contains mail seq. anything after it has seq = msgnum+1 */
+ uint32_t *msgnum_to_seq_map;
+ uint32_t msgnum_to_seq_map_count;
+
+ uoff_t top_bytes;
+ uoff_t retr_bytes;
+ unsigned int top_count;
+ unsigned int retr_count;
+
+ /* [msgnum] */
+ const char **message_uidls;
+ uoff_t *message_sizes;
+ /* [msgnum/8] & msgnum%8 */
+ unsigned char *deleted_bitmask;
+ unsigned char *seen_bitmask;
+
+ /* settings: */
+ const struct pop3_settings *set;
+ const struct mail_storage_settings *mail_set;
+ pool_t uidl_pool;
+ enum uidl_keys uidl_keymask;
+
+ /* Module-specific contexts. */
+ ARRAY(union pop3_module_context *) module_contexts;
+
+ bool destroyed:1;
+ bool disconnected:1;
+ bool deleted:1;
+ bool waiting_input:1;
+ bool anvil_sent:1;
+ bool message_uidls_save:1;
+ bool delete_success:1;
+ bool quit_seen:1;
+};
+
+struct pop3_module_register {
+ unsigned int id;
+};
+
+union pop3_module_context {
+ struct pop3_client_vfuncs super;
+ struct pop3_module_register *reg;
+};
+extern struct pop3_module_register pop3_module_register;
+
+extern struct client *pop3_clients;
+extern unsigned int pop3_client_count;
+
+/* Create new client with specified input/output handles. socket specifies
+ if the handle is a socket. */
+struct client *client_create(int fd_in, int fd_out,
+ struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct pop3_settings *set);
+void client_create_finish(struct client *client);
+int client_init_mailbox(struct client *client, const char **error_r);
+void client_destroy(struct client *client, const char *reason) ATTR_NULL(2);
+
+/* Disconnect client connection */
+void client_disconnect(struct client *client, const char *reason);
+
+/* Send a line of data to client */
+void client_send_line(struct client *client, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+void client_send_storage_error(struct client *client);
+
+bool client_handle_input(struct client *client);
+bool client_update_mails(struct client *client);
+
+void clients_destroy_all(void);
+
+int pop3_lock_session(struct client *client);
+
+#endif
diff --git a/src/pop3/pop3-commands.c b/src/pop3/pop3-commands.c
new file mode 100644
index 0000000..b0261e6
--- /dev/null
+++ b/src/pop3/pop3-commands.c
@@ -0,0 +1,956 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "pop3-common.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hash.h"
+#include "str.h"
+#include "var-expand.h"
+#include "message-size.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "mail-search-build.h"
+#include "pop3-capability.h"
+#include "pop3-commands.h"
+
+static enum mail_sort_type pop3_sort_program[] = {
+ MAIL_SORT_POP3_ORDER,
+ MAIL_SORT_END
+};
+
+static uint32_t msgnum_to_seq(struct client *client, uint32_t msgnum)
+{
+ return msgnum < client->msgnum_to_seq_map_count ?
+ client->msgnum_to_seq_map[msgnum] : msgnum+1;
+}
+
+static const char *get_msgnum(struct client *client, const char *args,
+ unsigned int *msgnum, bool thenspace)
+{
+ unsigned int num;
+
+ if (*args < '0' || *args > '9') {
+ client_send_line(client,
+ "-ERR Invalid message number: %s", args);
+ return NULL;
+ }
+ if (str_parse_uint(args, &num, &args) < 0) {
+ client_send_line(client,
+ "-ERR Message number too large: %s", args);
+ return NULL;
+ }
+ if (*args != (thenspace ? ' ' : '\0')) {
+ client_send_line(client,
+ "-ERR Noise after message number: %s", args);
+ return NULL;
+ }
+ if (num == 0 || num > client->messages_count) {
+ client_send_line(client,
+ "-ERR There's no message %u.", num);
+ return NULL;
+ }
+ num--;
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[num / CHAR_BIT] &
+ (1 << (num % CHAR_BIT))) != 0) {
+ client_send_line(client, "-ERR Message is deleted.");
+ return NULL;
+ }
+ }
+
+ while (*args == ' ') args++;
+
+ *msgnum = num;
+ return args;
+}
+
+static const char *get_size(struct client *client, const char *args,
+ uoff_t *size, bool thenspace)
+{
+ uoff_t num;
+
+ if (*args < '0' || *args > '9') {
+ client_send_line(client, "-ERR Invalid size: %s",
+ args);
+ return NULL;
+ }
+ if (str_parse_uoff(args, &num, &args) < 0) {
+ client_send_line(client, "-ERR Size too large: %s",
+ args);
+ return NULL;
+ }
+ if (*args != (thenspace ? ' ' : '\0')) {
+ client_send_line(client, "-ERR Noise after size: %s", args);
+ return NULL;
+ }
+
+ while (*args == ' ') args++;
+
+ *size = num;
+ return args;
+}
+
+static int cmd_capa(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK\r\n"POP3_CAPABILITY_REPLY".");
+ return 1;
+}
+
+static int cmd_dele(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ if (!client->deleted) {
+ client->deleted_bitmask = i_malloc(MSGS_BITMASK_SIZE(client));
+ client->deleted = TRUE;
+ }
+
+ client->deleted_bitmask[msgnum / CHAR_BIT] |= 1 << (msgnum % CHAR_BIT);
+ client->deleted_count++;
+ client->deleted_size += client->message_sizes[msgnum];
+ client_send_line(client, "+OK Marked to be deleted.");
+ return 1;
+}
+
+struct cmd_list_context {
+ unsigned int msgnum;
+};
+
+static void cmd_list_callback(struct client *client)
+{
+ struct cmd_list_context *ctx = client->cmd_context;
+
+ for (; ctx->msgnum != client->messages_count; ctx->msgnum++) {
+ if (client->output->closed)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client)) {
+ /* buffer full */
+ return;
+ }
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[ctx->msgnum / CHAR_BIT] &
+ (1 << (ctx->msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+
+ client_send_line(client, "%u %"PRIuUOFF_T, ctx->msgnum+1,
+ client->message_sizes[ctx->msgnum]);
+ }
+
+ client_send_line(client, ".");
+
+ i_free(ctx);
+ client->cmd = NULL;
+}
+
+static int cmd_list(struct client *client, const char *args)
+{
+ struct cmd_list_context *ctx;
+
+ if (*args == '\0') {
+ ctx = i_new(struct cmd_list_context, 1);
+ client_send_line(client, "+OK %u messages:",
+ client->messages_count - client->deleted_count);
+
+ client->cmd = cmd_list_callback;
+ client->cmd_context = ctx;
+ cmd_list_callback(client);
+ } else {
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ client_send_line(client, "+OK %u %"PRIuUOFF_T, msgnum+1,
+ client->message_sizes[msgnum]);
+ }
+
+ return 1;
+}
+
+static int cmd_last(struct client *client, const char *args ATTR_UNUSED)
+{
+ if (client->set->pop3_enable_last)
+ client_send_line(client, "+OK %u", client->last_seen_pop3_msn);
+ else
+ client_send_line(client, "-ERR LAST command not enabled");
+ return 1;
+}
+
+static int cmd_noop(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK");
+ return 1;
+}
+
+static struct mail_search_args *
+pop3_search_build_seqset(ARRAY_TYPE(seq_range) *seqset)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+
+ search_args = mail_search_build_init();
+ sarg = mail_search_build_add(search_args, SEARCH_SEQSET);
+ sarg->value.seqset = *seqset;
+ return search_args;
+}
+
+static struct mail_search_args *
+pop3_search_build(struct client *client, uint32_t seq)
+{
+ struct mail_search_args *search_args;
+
+ if (seq == 0)
+ return pop3_search_build_seqset(&client->all_seqs);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq, seq);
+ return search_args;
+}
+
+static int client_verify_ordering(struct client *client,
+ struct mail *mail, uint32_t msgnum)
+{
+ uint32_t seq;
+
+ seq = msgnum_to_seq(client, msgnum);
+ if (seq != mail->seq) {
+ i_error("Message ordering changed unexpectedly "
+ "(msg #%u: storage seq %u -> %u)",
+ msgnum+1, seq, mail->seq);
+ return -1;
+ }
+ return 0;
+}
+
+static void client_expunge(struct client *client, struct mail *mail)
+{
+ switch (client->set->parsed_delete_type) {
+ case POP3_DELETE_TYPE_EXPUNGE:
+ mail_expunge(mail);
+ break;
+ case POP3_DELETE_TYPE_FLAG:
+ i_assert(client->deleted_kw != NULL);
+ mail_update_keywords(mail, MODIFY_ADD, client->deleted_kw);
+ break;
+ }
+}
+
+bool client_update_mails(struct client *client)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ ARRAY_TYPE(seq_range) deleted_msgs, seen_msgs;
+ uint32_t msgnum, bit;
+ bool ret = TRUE;
+
+ if (mailbox_is_readonly(client->mailbox)) {
+ /* silently ignore */
+ return TRUE;
+ }
+
+ /* translate msgnums to sequences (in case POP3 ordering is
+ different) */
+ t_array_init(&deleted_msgs, 8);
+ if (client->deleted_bitmask != NULL && client->quit_seen) {
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ bit = 1 << (msgnum % CHAR_BIT);
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] & bit) != 0)
+ seq_range_array_add(&deleted_msgs, msgnum_to_seq(client, msgnum));
+ }
+ }
+ t_array_init(&seen_msgs, 8);
+ if (client->seen_bitmask != NULL) {
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ bit = 1 << (msgnum % CHAR_BIT);
+ if ((client->seen_bitmask[msgnum / CHAR_BIT] & bit) != 0)
+ seq_range_array_add(&seen_msgs, msgnum_to_seq(client, msgnum));
+ }
+ }
+
+ if (array_count(&deleted_msgs) > 0) {
+ /* expunge DELEted mails */
+ search_args = pop3_search_build_seqset(&deleted_msgs);
+ ctx = mailbox_search_init(client->trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail))
+ client_expunge(client, mail);
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = FALSE;
+ /* don't bother setting \Seen flags for deleted messages */
+ seq_range_array_invert(&deleted_msgs, 1, client->highest_seq);
+ seq_range_array_intersect(&seen_msgs, &deleted_msgs);
+ }
+
+ if (array_count(&seen_msgs) > 0) {
+ /* add \Seen flags for RETRed mails */
+ search_args = pop3_search_build_seqset(&seen_msgs);
+ ctx = mailbox_search_init(client->trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail))
+ mail_update_flags(mail, MODIFY_ADD, MAIL_SEEN);
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = FALSE;
+ }
+
+ client->seen_change_count = 0;
+ return ret;
+}
+
+static int cmd_quit(struct client *client, const char *args ATTR_UNUSED)
+{
+ client->quit_seen = TRUE;
+ if (client->deleted || client->seen_bitmask != NULL) {
+ if (!client_update_mails(client)) {
+ client_send_storage_error(client);
+ client_disconnect(client,
+ "Storage error during logout.");
+ return 1;
+ }
+ }
+
+ if (mailbox_transaction_commit(&client->trans) < 0 ||
+ mailbox_sync(client->mailbox, MAILBOX_SYNC_FLAG_FULL_WRITE) < 0) {
+ client_send_storage_error(client);
+ client_disconnect(client, "Storage error during logout.");
+ return 1;
+ } else {
+ client->delete_success = TRUE;
+ }
+
+ if (!client->deleted)
+ client_send_line(client, "+OK Logging out.");
+ else
+ client_send_line(client, "+OK Logging out, messages deleted.");
+
+ client_disconnect(client, "Logged out");
+ return 1;
+}
+
+struct fetch_context {
+ struct mail *mail;
+ struct istream *stream;
+ uoff_t body_lines;
+
+ uoff_t *byte_counter;
+ uoff_t byte_counter_offset;
+
+ unsigned char last;
+ bool cr_skipped, in_body;
+};
+
+static void fetch_deinit(struct fetch_context *ctx)
+{
+ mail_free(&ctx->mail);
+ i_free(ctx);
+}
+
+static void fetch_callback(struct client *client)
+{
+ struct fetch_context *ctx = client->cmd_context;
+ const unsigned char *data;
+ unsigned char add;
+ size_t i, size;
+ int ret;
+
+ while ((ctx->body_lines > 0 || !ctx->in_body) &&
+ i_stream_read_more(ctx->stream, &data, &size) > 0) {
+ if (size > 4096)
+ size = 4096;
+
+ add = '\0';
+ for (i = 0; i < size; i++) {
+ if ((data[i] == '\r' || data[i] == '\n') &&
+ !ctx->in_body) {
+ if (i == 0 && (ctx->last == '\0' ||
+ ctx->last == '\n'))
+ ctx->in_body = TRUE;
+ else if (i > 0 && data[i-1] == '\n')
+ ctx->in_body = TRUE;
+ }
+
+ if (data[i] == '\n') {
+ if ((i == 0 && ctx->last != '\r') ||
+ (i > 0 && data[i-1] != '\r')) {
+ /* missing CR */
+ add = '\r';
+ break;
+ }
+
+ if (ctx->in_body) {
+ if (--ctx->body_lines == 0) {
+ i++;
+ break;
+ }
+ }
+ } else if (data[i] == '.' &&
+ ((i == 0 && ctx->last == '\n') ||
+ (i > 0 && data[i-1] == '\n'))) {
+ /* escape the dot */
+ add = '.';
+ break;
+ } else if (data[i] == '\0' &&
+ (client->set->parsed_workarounds &
+ WORKAROUND_OUTLOOK_NO_NULS) != 0) {
+ add = 0x80;
+ break;
+ }
+ }
+
+ if (i > 0) {
+ if (o_stream_send(client->output, data, i) < 0)
+ break;
+ ctx->last = data[i-1];
+ i_stream_skip(ctx->stream, i);
+ }
+
+ if (o_stream_get_buffer_used_size(client->output) >= 4096) {
+ if ((ret = o_stream_flush(client->output)) < 0)
+ break;
+ if (ret == 0) {
+ /* continue later */
+ return;
+ }
+ }
+
+ if (add != '\0') {
+ if (o_stream_send(client->output, &add, 1) < 0)
+ break;
+
+ ctx->last = add;
+ if (add == 0x80)
+ i_stream_skip(ctx->stream, 1);
+ }
+ }
+
+ if (ctx->last != '\n') {
+ /* didn't end with CRLF */
+ o_stream_nsend(client->output, "\r\n", 2);
+ }
+
+ if (!ctx->in_body &&
+ (client->set->parsed_workarounds & WORKAROUND_OE_NS_EOH) != 0) {
+ /* Add the missing end of headers line. */
+ o_stream_nsend(client->output, "\r\n", 2);
+ }
+
+ *ctx->byte_counter +=
+ client->output->offset - ctx->byte_counter_offset;
+
+ client_send_line(client, ".");
+ fetch_deinit(ctx);
+ client->cmd = NULL;
+}
+
+static int client_reply_msg_expunged(struct client *client, unsigned int msgnum)
+{
+ client_send_line(client, "-ERR Message %u expunged.", msgnum + 1);
+ if (msgnum <= client->highest_expunged_fetch_msgnum) {
+ /* client tried to fetch an expunged message again.
+ treat this as error so we'll eventually disconnect the
+ client instead of letting it loop forever. */
+ return -1;
+ }
+ client->highest_expunged_fetch_msgnum = msgnum;
+ return 1;
+}
+
+static int fetch(struct client *client, unsigned int msgnum, uoff_t body_lines,
+ const char *reason, uoff_t *byte_counter)
+{
+ struct fetch_context *ctx;
+ int ret;
+
+ ctx = i_new(struct fetch_context, 1);
+ ctx->byte_counter = byte_counter;
+ ctx->byte_counter_offset = client->output->offset;
+ ctx->mail = mail_alloc(client->trans,
+ MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY, NULL);
+ mail_set_seq(ctx->mail, msgnum_to_seq(client, msgnum));
+
+ if (mail_get_stream_because(ctx->mail, NULL, NULL, reason, &ctx->stream) < 0) {
+ ret = client_reply_msg_expunged(client, msgnum);
+ fetch_deinit(ctx);
+ return ret;
+ }
+
+ if (body_lines == UOFF_T_MAX && client->seen_bitmask != NULL) {
+ if ((mail_get_flags(ctx->mail) & MAIL_SEEN) == 0) {
+ /* mark the message seen with RETR command */
+ client->seen_bitmask[msgnum / CHAR_BIT] |=
+ 1 << (msgnum % CHAR_BIT);
+ client->seen_change_count++;
+ }
+ }
+
+ ctx->body_lines = body_lines;
+ if (body_lines == UOFF_T_MAX) {
+ client_send_line(client, "+OK %"PRIuUOFF_T" octets",
+ client->message_sizes[msgnum]);
+ } else {
+ client_send_line(client, "+OK");
+ ctx->body_lines++; /* internally we count the empty line too */
+ }
+
+ client->cmd = fetch_callback;
+ client->cmd_context = ctx;
+ fetch_callback(client);
+ return 1;
+}
+
+static int cmd_retr(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ if (client->lowest_retr_pop3_msn > msgnum+1 ||
+ client->lowest_retr_pop3_msn == 0)
+ client->lowest_retr_pop3_msn = msgnum+1;
+ if (client->last_seen_pop3_msn < msgnum+1)
+ client->last_seen_pop3_msn = msgnum+1;
+
+ client->retr_count++;
+ return fetch(client, msgnum, UOFF_T_MAX, "RETR", &client->retr_bytes);
+}
+
+static int cmd_rset(struct client *client, const char *args ATTR_UNUSED)
+{
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ struct mail_search_args *search_args;
+
+ client->last_seen_pop3_msn = 0;
+
+ if (client->deleted) {
+ client->deleted = FALSE;
+ memset(client->deleted_bitmask, 0, MSGS_BITMASK_SIZE(client));
+ client->deleted_count = 0;
+ client->deleted_size = 0;
+ }
+ if (client->seen_change_count > 0) {
+ memset(client->seen_bitmask, 0, MSGS_BITMASK_SIZE(client));
+ client->seen_change_count = 0;
+ }
+
+ if (client->set->pop3_enable_last) {
+ /* remove all \Seen flags (as specified by RFC 1460) */
+ search_args = pop3_search_build(client, 0);
+ search_ctx = mailbox_search_init(client->trans,
+ search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail))
+ mail_update_flags(mail, MODIFY_REMOVE, MAIL_SEEN);
+ (void)mailbox_search_deinit(&search_ctx);
+
+ (void)mailbox_transaction_commit(&client->trans);
+ client->trans = mailbox_transaction_begin(client->mailbox, 0,
+ __func__);
+ }
+
+ client_send_line(client, "+OK");
+ return 1;
+}
+
+static int cmd_stat(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK %u %"PRIuUOFF_T,
+ client->messages_count - client->deleted_count,
+ client->total_size - client->deleted_size);
+ return 1;
+}
+
+static int cmd_top(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+ uoff_t max_lines;
+
+ args = get_msgnum(client, args, &msgnum, TRUE);
+ if (args == NULL)
+ return -1;
+ if (get_size(client, args, &max_lines, FALSE) == NULL)
+ return -1;
+
+ client->top_count++;
+ return fetch(client, msgnum, max_lines, "TOP", &client->top_bytes);
+}
+
+struct cmd_uidl_context {
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ uint32_t msgnum;
+ bool list_all;
+};
+
+static int
+pop3_get_uid(struct client *client, struct mail *mail, string_t *str,
+ bool *permanent_uidl_r)
+{
+ char uid_str[MAX_INT_STRLEN] = { 0 };
+ const char *uidl;
+ const char *hdr_md5 = NULL, *filename = NULL, *guid = NULL;
+
+ if (mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &uidl) == 0 &&
+ *uidl != '\0') {
+ str_append(str, uidl);
+ /* UIDL is already permanent */
+ *permanent_uidl_r = TRUE;
+ return 0;
+ }
+
+ *permanent_uidl_r = FALSE;
+
+ if (client->set->pop3_reuse_xuidl &&
+ mail_get_first_header(mail, "X-UIDL", &uidl) > 0) {
+ str_append(str, uidl);
+ return 0;
+ }
+
+ if ((client->uidl_keymask & UIDL_UID) != 0) {
+ if (i_snprintf(uid_str, sizeof(uid_str), "%u", mail->uid) < 0)
+ i_unreached();
+ }
+ if ((client->uidl_keymask & UIDL_MD5) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_HEADER_MD5,
+ &hdr_md5) < 0) {
+ i_error("UIDL: Header MD5 lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (hdr_md5[0] == '\0') {
+ i_error("UIDL: Header MD5 not found "
+ "(pop3_uidl_format=%%m not supported by storage?)");
+ return -1;
+ }
+ }
+ if ((client->uidl_keymask & UIDL_FILE_NAME) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_STORAGE_ID,
+ &filename) < 0) {
+ i_error("UIDL: File name lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (filename[0] == '\0') {
+ i_error("UIDL: File name not found "
+ "(pop3_uidl_format=%%f not supported by storage?)");
+ return -1;
+ }
+ }
+ if ((client->uidl_keymask & UIDL_GUID) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_GUID,
+ &guid) < 0) {
+ i_error("UIDL: Message GUID lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (guid[0] == '\0') {
+ i_error("UIDL: Message GUID not found "
+ "(pop3_uidl_format=%%g not supported by storage?)");
+ return -1;
+ }
+ }
+
+ const struct var_expand_table tab[] = {
+ { 'v', dec2str(client->uid_validity), "uidvalidity" },
+ { 'u', uid_str, "uid" },
+ { 'm', hdr_md5, "md5" },
+ { 'f', filename, "filename" },
+ { 'g', guid, "guid" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+
+ if (var_expand(str, client->mail_set->pop3_uidl_format,
+ tab, &error) <= 0) {
+ i_error("UIDL: Failed to expand pop3_uidl_format=%s: %s",
+ client->mail_set->pop3_uidl_format, error);
+ return -1;
+ }
+ return 0;
+}
+
+static bool
+list_uidls_saved_iter(struct client *client, struct cmd_uidl_context *ctx)
+{
+ bool found = FALSE;
+
+ while (ctx->msgnum < client->messages_count) {
+ uint32_t msgnum = ctx->msgnum++;
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] &
+ (1 << (msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+ found = TRUE;
+
+ client_send_line(client,
+ ctx->list_all ? "%u %s" : "+OK %u %s",
+ msgnum+1, client->message_uidls[msgnum]);
+ if (client->output->closed || !ctx->list_all)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client)) {
+ /* output is being buffered, continue when there's
+ more space */
+ return FALSE;
+ }
+ }
+ /* finished */
+ client->cmd = NULL;
+
+ if (ctx->list_all)
+ client_send_line(client, ".");
+ i_free(ctx);
+ return found;
+}
+
+static bool list_uids_iter(struct client *client, struct cmd_uidl_context *ctx)
+{
+ string_t *str;
+ bool permanent_uidl, found = FALSE;
+ bool failed = FALSE;
+
+ if (client->message_uidls != NULL)
+ return list_uidls_saved_iter(client, ctx);
+
+ str = t_str_new(128);
+ while (mailbox_search_next(ctx->search_ctx, &ctx->mail)) {
+ uint32_t msgnum = ctx->msgnum++;
+
+ if (client_verify_ordering(client, ctx->mail, msgnum) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (client->deleted) {
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] &
+ (1 << (msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+ found = TRUE;
+
+ str_truncate(str, 0);
+ if (pop3_get_uid(client, ctx->mail, str, &permanent_uidl) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (client->set->pop3_save_uidl && !permanent_uidl)
+ mail_update_pop3_uidl(ctx->mail, str_c(str));
+
+ client_send_line(client, ctx->list_all ? "%u %s" : "+OK %u %s",
+ msgnum+1, str_c(str));
+ if (client->output->closed)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client) && ctx->list_all) {
+ /* output is being buffered, continue when there's
+ more space */
+ return FALSE;
+ }
+ }
+
+ /* finished */
+ (void)mailbox_search_deinit(&ctx->search_ctx);
+
+ client->cmd = NULL;
+
+ if (ctx->list_all && !failed)
+ client_send_line(client, ".");
+ i_free(ctx);
+ if (failed)
+ client_disconnect(client, "POP3 UIDLs couldn't be listed");
+ return found || failed;
+}
+
+static void cmd_uidl_callback(struct client *client)
+{
+ struct cmd_uidl_context *ctx = client->cmd_context;
+
+ (void)list_uids_iter(client, ctx);
+}
+
+HASH_TABLE_DEFINE_TYPE(uidl_counter, char *, void *);
+
+static void
+uidl_rename_duplicate(string_t *uidl, HASH_TABLE_TYPE(uidl_counter) prev_uidls)
+{
+ char *key;
+ void *value;
+ unsigned int counter;
+
+ while (hash_table_lookup_full(prev_uidls, str_c(uidl), &key, &value)) {
+ /* duplicate. the value contains the number of duplicates. */
+ counter = POINTER_CAST_TO(value, unsigned int) + 1;
+ hash_table_update(prev_uidls, key, POINTER_CAST(counter));
+ str_printfa(uidl, "-%u", counter);
+ /* the second lookup really should return NULL, but just in
+ case of some weird UIDLs do this as many times as needed */
+ }
+}
+
+static void client_uidls_save(struct client *client)
+{
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct mail *mail;
+ HASH_TABLE_TYPE(uidl_counter) prev_uidls;
+ const char **seq_uidls;
+ string_t *str;
+ char *uidl;
+ enum mail_fetch_field wanted_fields;
+ uint32_t msgnum;
+ bool permanent_uidl, uidl_duplicates_rename, failed = FALSE;
+
+ i_assert(client->message_uidls == NULL);
+
+ search_args = pop3_search_build(client, 0);
+ wanted_fields = 0;
+ if ((client->uidl_keymask & UIDL_MD5) != 0)
+ wanted_fields |= MAIL_FETCH_HEADER_MD5;
+
+ search_ctx = mailbox_search_init(client->trans, search_args,
+ NULL, wanted_fields, NULL);
+ mail_search_args_unref(&search_args);
+
+ uidl_duplicates_rename =
+ strcmp(client->set->pop3_uidl_duplicates, "rename") == 0;
+ if (uidl_duplicates_rename)
+ hash_table_create(&prev_uidls, default_pool, 0, str_hash,
+ strcmp);
+ client->uidl_pool = pool_alloconly_create("message uidls", 1024);
+
+ /* first read all the UIDLs into a temporary [seq] array */
+ seq_uidls = i_new(const char *, client->highest_seq);
+ str = t_str_new(128);
+ while (mailbox_search_next(search_ctx, &mail)) {
+ str_truncate(str, 0);
+ if (pop3_get_uid(client, mail, str, &permanent_uidl) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (uidl_duplicates_rename)
+ uidl_rename_duplicate(str, prev_uidls);
+
+ uidl = p_strdup(client->uidl_pool, str_c(str));
+ if (client->set->pop3_save_uidl && !permanent_uidl)
+ mail_update_pop3_uidl(mail, uidl);
+
+ i_assert(mail->seq <= client->highest_seq);
+ seq_uidls[mail->seq-1] = uidl;
+ if (uidl_duplicates_rename)
+ hash_table_update(prev_uidls, uidl, POINTER_CAST(1));
+ }
+ (void)mailbox_search_deinit(&search_ctx);
+ if (uidl_duplicates_rename)
+ hash_table_destroy(&prev_uidls);
+
+ if (failed) {
+ pool_unref(&client->uidl_pool);
+ i_free(seq_uidls);
+ return;
+ }
+ /* map UIDLs to msgnums (in case POP3 sort ordering is different) */
+ client->message_uidls = p_new(client->uidl_pool, const char *,
+ MALLOC_ADD(client->messages_count, 1));
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ client->message_uidls[msgnum] =
+ seq_uidls[msgnum_to_seq(client, msgnum) - 1];
+ }
+ i_free(seq_uidls);
+}
+
+static struct cmd_uidl_context *
+cmd_uidl_init(struct client *client, uint32_t seq)
+{
+ struct cmd_uidl_context *ctx;
+ struct mail_search_args *search_args;
+ enum mail_fetch_field wanted_fields;
+
+ if (client->message_uidls_save && client->message_uidls == NULL &&
+ client->messages_count > 0)
+ client_uidls_save(client);
+
+ ctx = i_new(struct cmd_uidl_context, 1);
+ ctx->list_all = seq == 0;
+
+ if (client->message_uidls == NULL) {
+ wanted_fields = 0;
+ if ((client->uidl_keymask & UIDL_MD5) != 0)
+ wanted_fields |= MAIL_FETCH_HEADER_MD5;
+
+ search_args = pop3_search_build(client, seq);
+ ctx->search_ctx = mailbox_search_init(client->trans, search_args,
+ pop3_sort_program,
+ wanted_fields, NULL);
+ mail_search_args_unref(&search_args);
+ }
+
+ if (seq == 0) {
+ client->cmd = cmd_uidl_callback;
+ client->cmd_context = ctx;
+ }
+ return ctx;
+}
+
+static int cmd_uidl(struct client *client, const char *args)
+{
+ struct cmd_uidl_context *ctx;
+ uint32_t seq;
+
+ if (*args == '\0') {
+ client_send_line(client, "+OK");
+ ctx = cmd_uidl_init(client, 0);
+ (void)list_uids_iter(client, ctx);
+ } else {
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ seq = msgnum_to_seq(client, msgnum);
+ ctx = cmd_uidl_init(client, seq);
+ ctx->msgnum = msgnum;
+ if (!list_uids_iter(client, ctx))
+ return client_reply_msg_expunged(client, msgnum);
+ }
+
+ return 1;
+}
+
+static const struct pop3_command pop3_commands[] = {
+ { "capa", cmd_capa },
+ { "dele", cmd_dele },
+ { "list", cmd_list },
+ { "last", cmd_last },
+ { "noop", cmd_noop },
+ { "quit", cmd_quit },
+ { "retr", cmd_retr },
+ { "rset", cmd_rset },
+ { "stat", cmd_stat },
+ { "top", cmd_top },
+ { "uidl", cmd_uidl },
+};
+
+const struct pop3_command *pop3_command_find(const char *name)
+{
+ for (unsigned int i = 0; i < N_ELEMENTS(pop3_commands); i++) {
+ if (strcasecmp(pop3_commands[i].name, name) == 0)
+ return &pop3_commands[i];
+ }
+ return NULL;
+}
+
+int client_command_execute(struct client *client,
+ const struct pop3_command *cmd, const char *args)
+{
+ while (*args == ' ') args++;
+
+ return cmd->func(client, args);
+}
diff --git a/src/pop3/pop3-commands.h b/src/pop3/pop3-commands.h
new file mode 100644
index 0000000..37dd229
--- /dev/null
+++ b/src/pop3/pop3-commands.h
@@ -0,0 +1,13 @@
+#ifndef POP3_COMMANDS_H
+#define POP3_COMMANDS_H
+
+struct pop3_command {
+ const char *name;
+ int (*func)(struct client *client, const char *args);
+};
+
+const struct pop3_command *pop3_command_find(const char *name);
+int client_command_execute(struct client *client,
+ const struct pop3_command *cmd, const char *args);
+
+#endif
diff --git a/src/pop3/pop3-common.h b/src/pop3/pop3-common.h
new file mode 100644
index 0000000..eb3f521
--- /dev/null
+++ b/src/pop3/pop3-common.h
@@ -0,0 +1,27 @@
+#ifndef POP3_COMMON_H
+#define POP3_COMMON_H
+
+enum uidl_keys {
+ UIDL_UIDVALIDITY = 0x01,
+ UIDL_UID = 0x02,
+ UIDL_MD5 = 0x04,
+ UIDL_FILE_NAME = 0x08,
+ UIDL_GUID = 0x10
+};
+
+#include "lib.h"
+#include "pop3-client.h"
+#include "pop3-settings.h"
+
+typedef void pop3_client_created_func_t(struct client **client);
+
+extern pop3_client_created_func_t *hook_client_created;
+
+/* Sets the hook_client_created and returns the previous hook,
+ which the new_hook should call if it's non-NULL. */
+pop3_client_created_func_t *
+pop3_client_created_hook_set(pop3_client_created_func_t *new_hook);
+
+void pop3_refresh_proctitle(void);
+
+#endif
diff --git a/src/pop3/pop3-settings.c b/src/pop3/pop3-settings.c
new file mode 100644
index 0000000..0606dc8
--- /dev/null
+++ b/src/pop3/pop3-settings.c
@@ -0,0 +1,184 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "pop3-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+static bool pop3_settings_verify(void *_set, pool_t pool,
+ const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings pop3_unix_listeners_array[] = {
+ { "login/pop3", 0666, "", "" }
+};
+static struct file_listener_settings *pop3_unix_listeners[] = {
+ &pop3_unix_listeners_array[0]
+};
+static buffer_t pop3_unix_listeners_buf = {
+ { { pop3_unix_listeners, sizeof(pop3_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings pop3_service_settings = {
+ .name = "pop3",
+ .protocol = "pop3",
+ .type = "",
+ .executable = "pop3",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &pop3_unix_listeners_buf,
+ sizeof(pop3_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct pop3_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct pop3_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define pop3_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(BOOL, pop3_no_flag_updates),
+ DEF(BOOL, pop3_enable_last),
+ DEF(BOOL, pop3_reuse_xuidl),
+ DEF(BOOL, pop3_save_uidl),
+ DEF(BOOL, pop3_lock_session),
+ DEF(BOOL, pop3_fast_size_lookups),
+ DEF(STR, pop3_client_workarounds),
+ DEF(STR, pop3_logout_format),
+ DEF(ENUM, pop3_uidl_duplicates),
+ DEF(STR, pop3_deleted_flag),
+ DEF(ENUM, pop3_delete_type),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct pop3_settings pop3_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ .pop3_no_flag_updates = FALSE,
+ .pop3_enable_last = FALSE,
+ .pop3_reuse_xuidl = FALSE,
+ .pop3_save_uidl = FALSE,
+ .pop3_lock_session = FALSE,
+ .pop3_fast_size_lookups = FALSE,
+ .pop3_client_workarounds = "",
+ .pop3_logout_format = "top=%t/%p, retr=%r/%b, del=%d/%m, size=%s",
+ .pop3_uidl_duplicates = "allow:rename",
+ .pop3_deleted_flag = "",
+ .pop3_delete_type = "default:expunge:flag"
+};
+
+static const struct setting_parser_info *pop3_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info pop3_setting_parser_info = {
+ .module_name = "pop3",
+ .defines = pop3_setting_defines,
+ .defaults = &pop3_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct pop3_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = pop3_settings_verify,
+ .dependencies = pop3_setting_dependencies
+};
+
+/* <settings checks> */
+struct pop3_client_workaround_list {
+ const char *name;
+ enum pop3_client_workarounds num;
+};
+
+static const struct pop3_client_workaround_list pop3_client_workaround_list[] = {
+ { "outlook-no-nuls", WORKAROUND_OUTLOOK_NO_NULS },
+ { "oe-ns-eoh", WORKAROUND_OE_NS_EOH },
+ { NULL, 0 }
+};
+
+static int
+pop3_settings_parse_workarounds(struct pop3_settings *set,
+ const char **error_r)
+{
+ enum pop3_client_workarounds client_workarounds = 0;
+ const struct pop3_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->pop3_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = pop3_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("pop3_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+pop3_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct pop3_settings *set = _set;
+
+ if (pop3_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+ if (strcmp(set->pop3_delete_type, "default") == 0) {
+ if (set->pop3_deleted_flag[0] == '\0')
+ set->parsed_delete_type = POP3_DELETE_TYPE_EXPUNGE;
+ else
+ set->parsed_delete_type = POP3_DELETE_TYPE_FLAG;
+ } else if (strcmp(set->pop3_delete_type, "expunge") == 0) {
+ set->parsed_delete_type = POP3_DELETE_TYPE_EXPUNGE;
+ } else if (strcmp(set->pop3_delete_type, "flag") == 0) {
+ if (set->pop3_deleted_flag[0] == '\0') {
+ *error_r = "pop3_delete_type=flag, but pop3_deleted_flag not set";
+ return FALSE;
+ }
+ set->parsed_delete_type = POP3_DELETE_TYPE_FLAG;
+ } else {
+ *error_r = t_strdup_printf("pop3_delete_type: Unknown value '%s'",
+ set->pop3_delete_type);
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/pop3/pop3-settings.h b/src/pop3/pop3-settings.h
new file mode 100644
index 0000000..cf50f98
--- /dev/null
+++ b/src/pop3/pop3-settings.h
@@ -0,0 +1,40 @@
+#ifndef POP3_SETTINGS_H
+#define POP3_SETTINGS_H
+
+struct mail_user_settings;
+
+/* <settings checks> */
+enum pop3_client_workarounds {
+ WORKAROUND_OUTLOOK_NO_NULS = 0x01,
+ WORKAROUND_OE_NS_EOH = 0x02
+};
+enum pop3_delete_type {
+ POP3_DELETE_TYPE_EXPUNGE = 0,
+ POP3_DELETE_TYPE_FLAG,
+};
+/* </settings checks> */
+
+struct pop3_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ /* pop3: */
+ bool pop3_no_flag_updates;
+ bool pop3_enable_last;
+ bool pop3_reuse_xuidl;
+ bool pop3_save_uidl;
+ bool pop3_lock_session;
+ bool pop3_fast_size_lookups;
+ const char *pop3_client_workarounds;
+ const char *pop3_logout_format;
+ const char *pop3_uidl_duplicates;
+ const char *pop3_deleted_flag;
+ const char *pop3_delete_type;
+
+ enum pop3_client_workarounds parsed_workarounds;
+ enum pop3_delete_type parsed_delete_type;
+};
+
+extern const struct setting_parser_info pop3_setting_parser_info;
+
+#endif
diff --git a/src/replication/Makefile.am b/src/replication/Makefile.am
new file mode 100644
index 0000000..c9ddbd0
--- /dev/null
+++ b/src/replication/Makefile.am
@@ -0,0 +1,4 @@
+SUBDIRS = aggregator replicator
+
+noinst_HEADERS = \
+ replication-common.h
diff --git a/src/replication/Makefile.in b/src/replication/Makefile.in
new file mode 100644
index 0000000..a9dd5bb
--- /dev/null
+++ b/src/replication/Makefile.in
@@ -0,0 +1,773 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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/replication
+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_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 =
+SOURCES =
+DIST_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
+HEADERS = $(noinst_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
+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 = aggregator replicator
+noinst_HEADERS = \
+ replication-common.h
+
+all: all-recursive
+
+.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/replication/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/replication/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):
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+# 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 $(HEADERS)
+installdirs: installdirs-recursive
+installdirs-am:
+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 mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f Makefile
+distclean-am: clean-am distclean-generic distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+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 Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \
+ check-am clean clean-generic clean-libtool cscopelist-am ctags \
+ ctags-am distclean 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 \
+ installdirs-am maintainer-clean maintainer-clean-generic \
+ mostlyclean 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/replication/aggregator/Makefile.am b/src/replication/aggregator/Makefile.am
new file mode 100644
index 0000000..a0b3501
--- /dev/null
+++ b/src/replication/aggregator/Makefile.am
@@ -0,0 +1,29 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = aggregator
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/replication \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ $(BINARY_CFLAGS)
+
+aggregator_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+aggregator_LDADD = $(LIBDOVECOT)
+aggregator_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+aggregator_SOURCES = \
+ aggregator.c \
+ aggregator-settings.c \
+ notify-connection.c \
+ replicator-connection.c
+
+noinst_HEADERS = \
+ aggregator-settings.h \
+ notify-connection.h \
+ replicator-connection.h
diff --git a/src/replication/aggregator/Makefile.in b/src/replication/aggregator/Makefile.in
new file mode 100644
index 0000000..a7bfbe5
--- /dev/null
+++ b/src/replication/aggregator/Makefile.in
@@ -0,0 +1,828 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = aggregator$(EXEEXT)
+subdir = src/replication/aggregator
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_aggregator_OBJECTS = aggregator.$(OBJEXT) \
+ aggregator-settings.$(OBJEXT) notify-connection.$(OBJEXT) \
+ replicator-connection.$(OBJEXT)
+aggregator_OBJECTS = $(am_aggregator_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+aggregator_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(aggregator_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)/aggregator-settings.Po \
+ ./$(DEPDIR)/aggregator.Po ./$(DEPDIR)/notify-connection.Po \
+ ./$(DEPDIR)/replicator-connection.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 = $(aggregator_SOURCES)
+DIST_SOURCES = $(aggregator_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/replication \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ $(BINARY_CFLAGS)
+
+aggregator_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+aggregator_LDADD = $(LIBDOVECOT)
+aggregator_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+aggregator_SOURCES = \
+ aggregator.c \
+ aggregator-settings.c \
+ notify-connection.c \
+ replicator-connection.c
+
+noinst_HEADERS = \
+ aggregator-settings.h \
+ notify-connection.h \
+ replicator-connection.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/replication/aggregator/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/replication/aggregator/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+aggregator$(EXEEXT): $(aggregator_OBJECTS) $(aggregator_DEPENDENCIES) $(EXTRA_aggregator_DEPENDENCIES)
+ @rm -f aggregator$(EXEEXT)
+ $(AM_V_CCLD)$(aggregator_LINK) $(aggregator_OBJECTS) $(aggregator_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/aggregator-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/aggregator.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator-connection.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/aggregator-settings.Po
+ -rm -f ./$(DEPDIR)/aggregator.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/replicator-connection.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-pkglibexecPROGRAMS
+
+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)/aggregator-settings.Po
+ -rm -f ./$(DEPDIR)/aggregator.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/replicator-connection.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/replication/aggregator/aggregator-settings.c b/src/replication/aggregator/aggregator-settings.c
new file mode 100644
index 0000000..98597bd
--- /dev/null
+++ b/src/replication/aggregator/aggregator-settings.c
@@ -0,0 +1,85 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "aggregator-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings aggregator_unix_listeners_array[] = {
+ { "replication-notify", 0600, "", "" }
+};
+static struct file_listener_settings *aggregator_unix_listeners[] = {
+ &aggregator_unix_listeners_array[0]
+};
+static buffer_t aggregator_unix_listeners_buf = {
+ { { aggregator_unix_listeners, sizeof(aggregator_unix_listeners) } }
+};
+
+static struct file_listener_settings aggregator_fifo_listeners_array[] = {
+ { "replication-notify-fifo", 0600, "", "" }
+};
+static struct file_listener_settings *aggregator_fifo_listeners[] = {
+ &aggregator_fifo_listeners_array[0]
+};
+static buffer_t aggregator_fifo_listeners_buf = {
+ { { aggregator_fifo_listeners, sizeof(aggregator_fifo_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings aggregator_service_settings = {
+ .name = "aggregator",
+ .protocol = "",
+ .type = "",
+ .executable = "aggregator",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = ".",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &aggregator_unix_listeners_buf,
+ sizeof(aggregator_unix_listeners[0]) } },
+ .fifo_listeners = { { &aggregator_fifo_listeners_buf,
+ sizeof(aggregator_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct aggregator_settings)
+
+static const struct setting_define aggregator_setting_defines[] = {
+ DEF(STR, replicator_host),
+ DEF(IN_PORT, replicator_port),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct aggregator_settings aggregator_default_settings = {
+ .replicator_host = "replicator",
+ .replicator_port = 0
+};
+
+const struct setting_parser_info aggregator_setting_parser_info = {
+ .module_name = "aggregator",
+ .defines = aggregator_setting_defines,
+ .defaults = &aggregator_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct aggregator_settings),
+
+ .parent_offset = SIZE_MAX
+};
+
+const struct aggregator_settings *aggregator_settings;
diff --git a/src/replication/aggregator/aggregator-settings.h b/src/replication/aggregator/aggregator-settings.h
new file mode 100644
index 0000000..06c8ac3
--- /dev/null
+++ b/src/replication/aggregator/aggregator-settings.h
@@ -0,0 +1,12 @@
+#ifndef AGGREGATOR_SETTINGS_H
+#define AGGREGATOR_SETTINGS_H
+
+struct aggregator_settings {
+ const char *replicator_host;
+ in_port_t replicator_port;
+};
+
+extern const struct setting_parser_info aggregator_setting_parser_info;
+extern const struct aggregator_settings *aggregator_settings;
+
+#endif
diff --git a/src/replication/aggregator/aggregator.c b/src/replication/aggregator/aggregator.c
new file mode 100644
index 0000000..1677f9f
--- /dev/null
+++ b/src/replication/aggregator/aggregator.c
@@ -0,0 +1,74 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "aggregator-settings.h"
+#include "notify-connection.h"
+#include "replicator-connection.h"
+
+struct replicator_connection *replicator;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ notify_connection_create(conn->fd, conn->fifo);
+}
+
+static void main_preinit(void)
+{
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ const struct aggregator_settings *set;
+ void **sets;
+ int ret;
+
+ sets = master_service_settings_get_others(master_service);
+ set = sets[0];
+
+ if (set->replicator_port != 0) {
+ ret = net_gethostbyname(set->replicator_host, &ips, &ips_count);
+ if (ret != 0) {
+ i_fatal("replicator_host: gethostbyname(%s) failed: %s",
+ set->replicator_host, net_gethosterror(ret));
+ }
+ replicator = replicator_connection_create_inet(ips, ips_count,
+ set->replicator_port,
+ notify_connection_sync_callback);
+ } else {
+ replicator = replicator_connection_create_unix(set->replicator_host,
+ notify_connection_sync_callback);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &aggregator_setting_parser_info,
+ NULL
+ };
+ const char *error;
+
+ master_service = master_service_init("aggregator", 0, &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+
+ main_preinit();
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+
+ notify_connections_destroy_all();
+ replicator_connection_destroy(&replicator);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/replication/aggregator/notify-connection.c b/src/replication/aggregator/notify-connection.c
new file mode 100644
index 0000000..f9587fe
--- /dev/null
+++ b/src/replication/aggregator/notify-connection.c
@@ -0,0 +1,154 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "replication-common.h"
+#include "replicator-connection.h"
+#include "notify-connection.h"
+
+#define MAX_INBUF_SIZE 8192
+
+#define CONNECTION_IS_FIFO(conn) \
+ ((conn)->output == NULL)
+
+struct notify_connection {
+ struct notify_connection *prev, *next;
+ int refcount;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+};
+
+static struct notify_connection *conns = NULL;
+
+static void notify_connection_unref(struct notify_connection *conn);
+static void notify_connection_destroy(struct notify_connection *conn);
+
+static bool notify_input_error(struct notify_connection *conn)
+{
+ if (CONNECTION_IS_FIFO(conn))
+ return TRUE;
+ notify_connection_destroy(conn);
+ return FALSE;
+}
+
+void notify_connection_sync_callback(bool success, void *context)
+{
+ struct notify_connection *conn = context;
+
+ o_stream_nsend_str(conn->output, success ? "+\n" : "-\n");
+ notify_connection_unref(conn);
+}
+
+static int
+notify_input_line(struct notify_connection *conn, const char *line)
+{
+ const char *const *args;
+ enum replication_priority priority;
+
+ /* <username> \t <priority> */
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 2) {
+ i_error("Client sent invalid input");
+ return -1;
+ }
+ if (replication_priority_parse(args[1], &priority) < 0) {
+ i_error("Client sent invalid priority: %s", args[1]);
+ return -1;
+ }
+ if (priority != REPLICATION_PRIORITY_SYNC)
+ replicator_connection_notify(replicator, args[0], priority);
+ else {
+ conn->refcount++;
+ replicator_connection_notify_sync(replicator, args[0], conn);
+ }
+ return 0;
+}
+
+static void notify_input(struct notify_connection *conn)
+{
+ const char *line;
+ int ret;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ /* buffer full */
+ i_error("Client sent too long line");
+ (void)notify_input_error(conn);
+ return;
+ case -1:
+ /* disconnected */
+ notify_connection_destroy(conn);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ ret = notify_input_line(conn, line);
+ } T_END;
+ if (ret < 0) {
+ if (!notify_input_error(conn))
+ return;
+ }
+ }
+}
+
+void notify_connection_create(int fd, bool fifo)
+{
+ struct notify_connection *conn;
+
+ conn = i_new(struct notify_connection, 1);
+ conn->refcount = 1;
+ conn->fd = fd;
+ conn->io = io_add(fd, IO_READ, notify_input, conn);
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ if (!fifo) {
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ }
+
+ DLLIST_PREPEND(&conns, conn);
+}
+
+static void notify_connection_unref(struct notify_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return;
+
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ i_free(conn);
+}
+
+static void notify_connection_destroy(struct notify_connection *conn)
+{
+ i_assert(conn->fd != -1);
+
+ if (!CONNECTION_IS_FIFO(conn))
+ master_service_client_connection_destroyed(master_service);
+
+ DLLIST_REMOVE(&conns, conn);
+
+ io_remove(&conn->io);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+
+ notify_connection_unref(conn);
+}
+
+void notify_connections_destroy_all(void)
+{
+ while (conns != NULL)
+ notify_connection_destroy(conns);
+}
diff --git a/src/replication/aggregator/notify-connection.h b/src/replication/aggregator/notify-connection.h
new file mode 100644
index 0000000..790ae45
--- /dev/null
+++ b/src/replication/aggregator/notify-connection.h
@@ -0,0 +1,9 @@
+#ifndef NOTIFY_CONNECTION_H
+#define NOTIFY_CONNECTION_H
+
+void notify_connection_create(int fd, bool fifo);
+void notify_connections_destroy_all(void);
+
+void notify_connection_sync_callback(bool success, void *context);
+
+#endif
diff --git a/src/replication/aggregator/replicator-connection.c b/src/replication/aggregator/replicator-connection.c
new file mode 100644
index 0000000..9275376
--- /dev/null
+++ b/src/replication/aggregator/replicator-connection.c
@@ -0,0 +1,326 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "buffer.h"
+#include "hash.h"
+#include "llist.h"
+#include "strescape.h"
+#include "replicator-connection.h"
+
+#define MAX_INBUF_SIZE 1024
+#define REPLICATOR_RECONNECT_MSECS 5000
+#define REPLICATOR_MEMBUF_MAX_SIZE 1024*1024
+#define REPLICATOR_HANDSHAKE "VERSION\treplicator-notify\t1\t0\n"
+
+struct replicator_connection {
+ char *path;
+ struct ip_addr *ips;
+ unsigned int ips_count, ip_idx;
+ in_port_t port;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to;
+
+ buffer_t *queue[REPLICATION_PRIORITY_SYNC + 1];
+
+ HASH_TABLE(void *, void *) requests;
+ unsigned int request_id_counter;
+ replicator_sync_callback_t *callback;
+};
+
+static void replicator_connection_disconnect(struct replicator_connection *conn);
+
+static int
+replicator_input_line(struct replicator_connection *conn, const char *line)
+{
+ void *context;
+ unsigned int id;
+
+ /* <+|-> \t <id> */
+ if ((line[0] != '+' && line[0] != '-') || line[1] != '\t' ||
+ str_to_uint(line+2, &id) < 0 || id == 0) {
+ i_error("Replicator sent invalid input: %s", line);
+ return -1;
+ }
+
+ context = hash_table_lookup(conn->requests, POINTER_CAST(id));
+ if (context == NULL) {
+ i_error("Replicator sent invalid ID: %u", id);
+ return -1;
+ }
+ hash_table_remove(conn->requests, POINTER_CAST(id));
+ conn->callback(line[0] == '+', context);
+ return 0;
+}
+
+static void replicator_input(struct replicator_connection *conn)
+{
+ const char *line;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ /* buffer full */
+ i_error("Replicator sent too long line");
+ replicator_connection_disconnect(conn);
+ return;
+ case -1:
+ /* disconnected */
+ replicator_connection_disconnect(conn);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL)
+ (void)replicator_input_line(conn, line);
+}
+
+static bool
+replicator_send_buf(struct replicator_connection *conn, buffer_t *buf)
+{
+ const unsigned char *data = buf->data;
+ size_t len = IO_BLOCK_SIZE;
+
+ /* try to send about IO_BLOCK_SIZE amount of data,
+ but only full lines */
+ if (len > buf->used)
+ len = buf->used;
+ for (;; len++) {
+ i_assert(len < buf->used); /* there is always LF */
+ if (data[len] == '\n') {
+ len++;
+ break;
+ }
+ }
+
+ if (o_stream_send(conn->output, data, len) < 0) {
+ replicator_connection_disconnect(conn);
+ return FALSE;
+ }
+ buffer_delete(buf, 0, len);
+ return TRUE;
+}
+
+static int replicator_output(struct replicator_connection *conn)
+{
+ enum replication_priority p;
+
+ if (o_stream_flush(conn->output) < 0) {
+ replicator_connection_disconnect(conn);
+ return 1;
+ }
+
+ for (p = REPLICATION_PRIORITY_SYNC;;) {
+ if (o_stream_get_buffer_used_size(conn->output) > 0) {
+ o_stream_set_flush_pending(conn->output, TRUE);
+ break;
+ }
+ /* output buffer is empty, send more data */
+ if (conn->queue[p]->used > 0) {
+ if (!replicator_send_buf(conn, conn->queue[p]))
+ break;
+ } else {
+ if (p == REPLICATION_PRIORITY_LOW)
+ break;
+ p--;
+ }
+ }
+ return 1;
+}
+
+static void replicator_connection_connect(struct replicator_connection *conn)
+{
+ unsigned int n;
+ int fd = -1;
+
+ if (conn->fd != -1)
+ return;
+
+ if (conn->port == 0) {
+ fd = net_connect_unix(conn->path);
+ if (fd == -1)
+ i_error("net_connect_unix(%s) failed: %m", conn->path);
+ } else {
+ for (n = 0; n < conn->ips_count; n++) {
+ unsigned int idx = conn->ip_idx;
+
+ conn->ip_idx = (conn->ip_idx + 1) % conn->ips_count;
+ fd = net_connect_ip(&conn->ips[idx], conn->port, NULL);
+ if (fd != -1)
+ break;
+ i_error("connect(%s, %u) failed: %m",
+ net_ip2addr(&conn->ips[idx]), conn->port);
+ }
+ }
+
+ if (fd == -1) {
+ if (conn->to == NULL) {
+ conn->to = timeout_add(REPLICATOR_RECONNECT_MSECS,
+ replicator_connection_connect,
+ conn);
+ }
+ return;
+ }
+
+ timeout_remove(&conn->to);
+ conn->fd = fd;
+ conn->io = io_add(fd, IO_READ, replicator_input, conn);
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ o_stream_nsend_str(conn->output, REPLICATOR_HANDSHAKE);
+ o_stream_set_flush_callback(conn->output, replicator_output, conn);
+}
+
+static void replicator_abort_all_requests(struct replicator_connection *conn)
+{
+ struct hash_iterate_context *iter;
+ void *key, *value;
+
+ iter = hash_table_iterate_init(conn->requests);
+ while (hash_table_iterate(iter, conn->requests, &key, &value))
+ conn->callback(FALSE, value);
+ hash_table_iterate_deinit(&iter);
+ hash_table_clear(conn->requests, TRUE);
+}
+
+static void replicator_connection_disconnect(struct replicator_connection *conn)
+{
+ if (conn->fd == -1)
+ return;
+
+ replicator_abort_all_requests(conn);
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+}
+
+static struct replicator_connection *replicator_connection_create(void)
+{
+ struct replicator_connection *conn;
+ unsigned int i;
+
+ conn = i_new(struct replicator_connection, 1);
+ conn->fd = -1;
+ hash_table_create_direct(&conn->requests, default_pool, 0);
+ for (i = REPLICATION_PRIORITY_LOW; i <= REPLICATION_PRIORITY_SYNC; i++)
+ conn->queue[i] = buffer_create_dynamic(default_pool, 1024);
+ return conn;
+}
+
+struct replicator_connection *
+replicator_connection_create_unix(const char *path,
+ replicator_sync_callback_t *callback)
+{
+ struct replicator_connection *conn;
+
+ conn = replicator_connection_create();
+ conn->callback = callback;
+ conn->path = i_strdup(path);
+ return conn;
+}
+
+struct replicator_connection *
+replicator_connection_create_inet(const struct ip_addr *ips,
+ unsigned int ips_count, in_port_t port,
+ replicator_sync_callback_t *callback)
+{
+ struct replicator_connection *conn;
+
+ conn = replicator_connection_create();
+ conn->callback = callback;
+ conn->ips = i_new(struct ip_addr, ips_count);
+ memcpy(conn->ips, ips, sizeof(*ips) * ips_count);
+ conn->ips_count = ips_count;
+ conn->port = port;
+ return conn;
+}
+
+void replicator_connection_destroy(struct replicator_connection **_conn)
+{
+ struct replicator_connection *conn = *_conn;
+ unsigned int i;
+
+ *_conn = NULL;
+ replicator_connection_disconnect(conn);
+
+ for (i = REPLICATION_PRIORITY_LOW; i <= REPLICATION_PRIORITY_SYNC; i++)
+ buffer_free(&conn->queue[i]);
+
+ timeout_remove(&conn->to);
+ hash_table_destroy(&conn->requests);
+ i_free(conn->ips);
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void
+replicator_send(struct replicator_connection *conn,
+ enum replication_priority priority, const char *data)
+{
+ size_t data_len = strlen(data);
+
+ if (conn->fd != -1 &&
+ o_stream_get_buffer_used_size(conn->output) == 0) {
+ /* we can send data immediately */
+ o_stream_nsend(conn->output, data, data_len);
+ } else if (conn->queue[priority]->used + data_len >=
+ REPLICATOR_MEMBUF_MAX_SIZE) {
+ /* FIXME: compress duplicates, start writing to file */
+ } else {
+ /* queue internally to separate queues */
+ buffer_append(conn->queue[priority], data, data_len);
+ if (conn->output != NULL)
+ o_stream_set_flush_pending(conn->output, TRUE);
+ }
+}
+
+void replicator_connection_notify(struct replicator_connection *conn,
+ const char *username,
+ enum replication_priority priority)
+{
+ const char *priority_str = "";
+
+ replicator_connection_connect(conn);
+
+ switch (priority) {
+ case REPLICATION_PRIORITY_NONE:
+ case REPLICATION_PRIORITY_SYNC:
+ i_unreached();
+ case REPLICATION_PRIORITY_LOW:
+ priority_str = "low";
+ break;
+ case REPLICATION_PRIORITY_HIGH:
+ priority_str = "high";
+ break;
+ }
+
+ T_BEGIN {
+ replicator_send(conn, priority, t_strdup_printf(
+ "U\t%s\t%s\n", str_tabescape(username), priority_str));
+ } T_END;
+}
+
+void replicator_connection_notify_sync(struct replicator_connection *conn,
+ const char *username, void *context)
+{
+ unsigned int id;
+
+ replicator_connection_connect(conn);
+
+ id = ++conn->request_id_counter;
+ if (id == 0) id++;
+ hash_table_insert(conn->requests, POINTER_CAST(id), context);
+
+ T_BEGIN {
+ replicator_send(conn, REPLICATION_PRIORITY_SYNC, t_strdup_printf(
+ "U\t%s\tsync\t%u\n", str_tabescape(username), id));
+ } T_END;
+}
diff --git a/src/replication/aggregator/replicator-connection.h b/src/replication/aggregator/replicator-connection.h
new file mode 100644
index 0000000..bc2c82a
--- /dev/null
+++ b/src/replication/aggregator/replicator-connection.h
@@ -0,0 +1,25 @@
+#ifndef REPLICATOR_CONNECTION_H
+#define REPLICATOR_CONNECTION_H
+
+#include "replication-common.h"
+
+typedef void replicator_sync_callback_t(bool success, void *context);
+
+struct replicator_connection *
+replicator_connection_create_unix(const char *path,
+ replicator_sync_callback_t *callback);
+struct replicator_connection *
+replicator_connection_create_inet(const struct ip_addr *ips,
+ unsigned int ips_count, in_port_t port,
+ replicator_sync_callback_t *callback);
+void replicator_connection_destroy(struct replicator_connection **conn);
+
+void replicator_connection_notify(struct replicator_connection *conn,
+ const char *username,
+ enum replication_priority priority);
+void replicator_connection_notify_sync(struct replicator_connection *conn,
+ const char *username, void *context);
+
+extern struct replicator_connection *replicator;
+
+#endif
diff --git a/src/replication/replication-common.h b/src/replication/replication-common.h
new file mode 100644
index 0000000..77f711c
--- /dev/null
+++ b/src/replication/replication-common.h
@@ -0,0 +1,48 @@
+#ifndef REPLICATION_COMMON_H
+#define REPLICATION_COMMON_H
+
+enum replication_priority {
+ /* user is fully replicated, as far as we know */
+ REPLICATION_PRIORITY_NONE = 0,
+ /* flag changes, expunges, etc. */
+ REPLICATION_PRIORITY_LOW,
+ /* new emails */
+ REPLICATION_PRIORITY_HIGH,
+ /* synchronously wait for new emails to be replicated */
+ REPLICATION_PRIORITY_SYNC
+};
+
+static inline const char *
+replicator_priority_to_str(enum replication_priority priority)
+{
+ switch (priority) {
+ case REPLICATION_PRIORITY_NONE:
+ return "none";
+ case REPLICATION_PRIORITY_LOW:
+ return "low";
+ case REPLICATION_PRIORITY_HIGH:
+ return "high";
+ case REPLICATION_PRIORITY_SYNC:
+ return "sync";
+ }
+ i_unreached();
+}
+
+static inline int
+replication_priority_parse(const char *str,
+ enum replication_priority *priority_r)
+{
+ if (strcmp(str, "none") == 0)
+ *priority_r = REPLICATION_PRIORITY_NONE;
+ else if (strcmp(str, "low") == 0)
+ *priority_r = REPLICATION_PRIORITY_LOW;
+ else if (strcmp(str, "high") == 0)
+ *priority_r = REPLICATION_PRIORITY_HIGH;
+ else if (strcmp(str, "sync") == 0)
+ *priority_r = REPLICATION_PRIORITY_SYNC;
+ else
+ return -1;
+ return 0;
+}
+
+#endif
diff --git a/src/replication/replicator/Makefile.am b/src/replication/replicator/Makefile.am
new file mode 100644
index 0000000..dd2fc1d
--- /dev/null
+++ b/src/replication/replicator/Makefile.am
@@ -0,0 +1,60 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = replicator
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/replication \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ $(BINARY_CFLAGS)
+
+replicator_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+replicator_LDADD = $(LIBDOVECOT)
+replicator_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+replicator_SOURCES = \
+ doveadm-connection.c \
+ dsync-client.c \
+ replicator.c \
+ replicator-brain.c \
+ replicator-queue.c \
+ replicator-queue-auth.c \
+ replicator-settings.c \
+ notify-connection.c
+
+noinst_HEADERS = \
+ doveadm-connection.h \
+ dsync-client.h \
+ replicator-brain.h \
+ replicator-queue.h \
+ replicator-settings.h \
+ notify-connection.h
+
+test_programs = \
+ test-replicator-queue
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+
+test_deps = $(test_libs)
+
+test_replicator_queue_SOURCES = \
+ replicator-queue.c \
+ replicator-settings.c \
+ test-replicator-queue.c
+test_replicator_queue_LDADD = $(test_libs)
+test_replicator_queue_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/replication/replicator/Makefile.in b/src/replication/replicator/Makefile.in
new file mode 100644
index 0000000..8547161
--- /dev/null
+++ b/src/replication/replicator/Makefile.in
@@ -0,0 +1,897 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = replicator$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/replication/replicator
+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-replicator-queue$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am_replicator_OBJECTS = doveadm-connection.$(OBJEXT) \
+ dsync-client.$(OBJEXT) replicator.$(OBJEXT) \
+ replicator-brain.$(OBJEXT) replicator-queue.$(OBJEXT) \
+ replicator-queue-auth.$(OBJEXT) replicator-settings.$(OBJEXT) \
+ notify-connection.$(OBJEXT)
+replicator_OBJECTS = $(am_replicator_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+replicator_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(replicator_LDFLAGS) $(LDFLAGS) -o $@
+am_test_replicator_queue_OBJECTS = replicator-queue.$(OBJEXT) \
+ replicator-settings.$(OBJEXT) test-replicator-queue.$(OBJEXT)
+test_replicator_queue_OBJECTS = $(am_test_replicator_queue_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)/doveadm-connection.Po \
+ ./$(DEPDIR)/dsync-client.Po ./$(DEPDIR)/notify-connection.Po \
+ ./$(DEPDIR)/replicator-brain.Po \
+ ./$(DEPDIR)/replicator-queue-auth.Po \
+ ./$(DEPDIR)/replicator-queue.Po \
+ ./$(DEPDIR)/replicator-settings.Po ./$(DEPDIR)/replicator.Po \
+ ./$(DEPDIR)/test-replicator-queue.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 = $(replicator_SOURCES) $(test_replicator_queue_SOURCES)
+DIST_SOURCES = $(replicator_SOURCES) $(test_replicator_queue_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/replication \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ $(BINARY_CFLAGS)
+
+replicator_LDFLAGS = -export-dynamic \
+ $(BINARY_LDFLAGS)
+
+replicator_LDADD = $(LIBDOVECOT)
+replicator_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+replicator_SOURCES = \
+ doveadm-connection.c \
+ dsync-client.c \
+ replicator.c \
+ replicator-brain.c \
+ replicator-queue.c \
+ replicator-queue-auth.c \
+ replicator-settings.c \
+ notify-connection.c
+
+noinst_HEADERS = \
+ doveadm-connection.h \
+ dsync-client.h \
+ replicator-brain.h \
+ replicator-queue.h \
+ replicator-settings.h \
+ notify-connection.h
+
+test_programs = \
+ test-replicator-queue
+
+test_libs = \
+ ../../lib-test/libtest.la \
+ ../../lib/liblib.la
+
+test_deps = $(test_libs)
+test_replicator_queue_SOURCES = \
+ replicator-queue.c \
+ replicator-settings.c \
+ test-replicator-queue.c
+
+test_replicator_queue_LDADD = $(test_libs)
+test_replicator_queue_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/replication/replicator/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/replication/replicator/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+replicator$(EXEEXT): $(replicator_OBJECTS) $(replicator_DEPENDENCIES) $(EXTRA_replicator_DEPENDENCIES)
+ @rm -f replicator$(EXEEXT)
+ $(AM_V_CCLD)$(replicator_LINK) $(replicator_OBJECTS) $(replicator_LDADD) $(LIBS)
+
+test-replicator-queue$(EXEEXT): $(test_replicator_queue_OBJECTS) $(test_replicator_queue_DEPENDENCIES) $(EXTRA_test_replicator_queue_DEPENDENCIES)
+ @rm -f test-replicator-queue$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_replicator_queue_OBJECTS) $(test_replicator_queue_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator-brain.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator-queue-auth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator-queue.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replicator.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-replicator-queue.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/dsync-client.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/replicator-brain.Po
+ -rm -f ./$(DEPDIR)/replicator-queue-auth.Po
+ -rm -f ./$(DEPDIR)/replicator-queue.Po
+ -rm -f ./$(DEPDIR)/replicator-settings.Po
+ -rm -f ./$(DEPDIR)/replicator.Po
+ -rm -f ./$(DEPDIR)/test-replicator-queue.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-pkglibexecPROGRAMS
+
+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)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/dsync-client.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/replicator-brain.Po
+ -rm -f ./$(DEPDIR)/replicator-queue-auth.Po
+ -rm -f ./$(DEPDIR)/replicator-queue.Po
+ -rm -f ./$(DEPDIR)/replicator-settings.Po
+ -rm -f ./$(DEPDIR)/replicator.Po
+ -rm -f ./$(DEPDIR)/test-replicator-queue.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-pkglibexecPROGRAMS
+
+.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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/replication/replicator/doveadm-connection.c b/src/replication/replicator/doveadm-connection.c
new file mode 100644
index 0000000..1932bc6
--- /dev/null
+++ b/src/replication/replicator/doveadm-connection.c
@@ -0,0 +1,354 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "connection.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "wildcard-match.h"
+#include "master-service.h"
+#include "replicator-brain.h"
+#include "replicator-queue.h"
+#include "replicator-settings.h"
+#include "dsync-client.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+#define REPLICATOR_DOVEADM_MAJOR_VERSION 1
+#define REPLICATOR_DOVEADM_MINOR_VERSION 0
+
+struct doveadm_connection {
+ struct connection conn;
+ struct replicator_brain *brain;
+};
+static struct connection_list *doveadm_connections;
+
+static int client_input_status_overview(struct doveadm_connection *client)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ struct replicator_queue_iter *iter;
+ struct replicator_user *user;
+ enum replication_priority priority;
+ unsigned int pending_counts[REPLICATION_PRIORITY_SYNC+1];
+ unsigned int user_count, next_secs, pending_failed_count;
+ unsigned int pending_full_resync_count, waiting_failed_count;
+ string_t *str = t_str_new(256);
+
+ memset(pending_counts, 0, sizeof(pending_counts));
+ pending_failed_count = 0; waiting_failed_count = 0;
+ pending_full_resync_count = 0;
+
+ user_count = 0;
+ iter = replicator_queue_iter_init(queue);
+ while ((user = replicator_queue_iter_next(iter)) != NULL) {
+ if (user->priority != REPLICATION_PRIORITY_NONE)
+ pending_counts[user->priority]++;
+ else if (replicator_queue_want_sync_now(user, &next_secs)) {
+ if (user->last_sync_failed)
+ pending_failed_count++;
+ else
+ pending_full_resync_count++;
+ } else {
+ if (user->last_sync_failed)
+ waiting_failed_count++;
+ }
+ user_count++;
+ }
+ replicator_queue_iter_deinit(&iter);
+
+ for (priority = REPLICATION_PRIORITY_SYNC; priority > 0; priority--) {
+ str_printfa(str, "Queued '%s' requests\t%u\n",
+ replicator_priority_to_str(priority),
+ pending_counts[priority]);
+ }
+ str_printfa(str, "Queued 'failed' requests\t%u\n",
+ pending_failed_count);
+ str_printfa(str, "Queued 'full resync' requests\t%u\n",
+ pending_full_resync_count);
+ str_printfa(str, "Waiting 'failed' requests\t%u\n",
+ waiting_failed_count);
+ str_printfa(str, "Total number of known users\t%u\n", user_count);
+ str_append_c(str, '\n');
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ return 0;
+}
+
+static int
+client_input_status(struct doveadm_connection *client, const char *const *args)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ struct replicator_queue_iter *iter;
+ struct replicator_user *user;
+ const char *mask = args[0];
+ unsigned int next_secs;
+ string_t *str = t_str_new(128);
+
+ if (mask == NULL)
+ return client_input_status_overview(client);
+
+ iter = replicator_queue_iter_init(queue);
+ while ((user = replicator_queue_iter_next(iter)) != NULL) {
+ if (!wildcard_match(user->username, mask))
+ continue;
+
+ str_truncate(str, 0);
+ str_append_tabescaped(str, user->username);
+ str_append_c(str, '\t');
+ str_append(str, replicator_priority_to_str(user->priority));
+ if (replicator_queue_want_sync_now(user, &next_secs))
+ next_secs = 0;
+ str_printfa(str, "\t%lld\t%lld\t%d\t%lld\t%u\n",
+ (long long)user->last_fast_sync,
+ (long long)user->last_full_sync,
+ user->last_sync_failed ? 1 : 0,
+ (long long)user->last_successful_sync,
+ next_secs);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ }
+ replicator_queue_iter_deinit(&iter);
+ o_stream_nsend(client->conn.output, "\n", 1);
+ return 0;
+}
+
+static int
+client_input_status_dsyncs(struct doveadm_connection *client)
+{
+ string_t *str = t_str_new(256);
+ const ARRAY_TYPE(dsync_client) *clients;
+ struct dsync_client *dsync_client;
+ const char *username;
+
+ clients = replicator_brain_get_dsync_clients(client->brain);
+ array_foreach_elem(clients, dsync_client) {
+ username = dsync_client_get_username(dsync_client);
+ if (username != NULL) {
+ str_append_tabescaped(str, username);
+ str_append_c(str, '\t');
+ switch (dsync_client_get_type(dsync_client)) {
+ case DSYNC_TYPE_FULL:
+ str_append(str, "full");
+ break;
+ case DSYNC_TYPE_NORMAL:
+ str_append(str, "normal");
+ break;
+ case DSYNC_TYPE_INCREMENTAL:
+ str_append(str, "incremental");
+ break;
+ }
+ } else {
+ str_append(str, "\t-");
+ }
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, dsync_client_get_state(dsync_client));
+ str_append_c(str, '\n');
+ }
+
+ str_append_c(str, '\n');
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ return 0;
+}
+
+static int
+client_input_replicate(struct doveadm_connection *client, const char *const *args)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ struct replicator_queue_iter *iter;
+ struct replicator_user *user;
+ const char *usermask;
+ enum replication_priority priority;
+ unsigned int match_count;
+ bool full;
+
+ /* <priority> <flags> <username>|<mask> */
+ if (str_array_length(args) != 3) {
+ i_error("%s: REPLICATE: Invalid parameters", client->conn.name);
+ return -1;
+ }
+ if (replication_priority_parse(args[0], &priority) < 0) {
+ o_stream_nsend_str(client->conn.output, "-Invalid priority\n");
+ return 0;
+ }
+ full = strchr(args[1], 'f') != NULL;
+ usermask = args[2];
+ if (strchr(usermask, '*') == NULL && strchr(usermask, '?') == NULL) {
+ struct replicator_user *user =
+ replicator_queue_get(queue, usermask);
+ if (full)
+ user->force_full_sync = TRUE;
+ replicator_queue_update(queue, user, priority);
+ replicator_queue_add(queue, user);
+ o_stream_nsend_str(client->conn.output, "+1\n");
+ return 0;
+ }
+
+ match_count = 0;
+ iter = replicator_queue_iter_init(queue);
+ while ((user = replicator_queue_iter_next(iter)) != NULL) {
+ if (!wildcard_match(user->username, usermask))
+ continue;
+ if (full)
+ user->force_full_sync = TRUE;
+ replicator_queue_update(queue, user, priority);
+ replicator_queue_add(queue, user);
+ match_count++;
+ }
+ replicator_queue_iter_deinit(&iter);
+ o_stream_nsend_str(client->conn.output,
+ t_strdup_printf("+%u\n", match_count));
+ return 0;
+}
+
+static int
+client_input_add(struct doveadm_connection *client, const char *const *args)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ const struct replicator_settings *set =
+ replicator_brain_get_settings(client->brain);
+
+ /* <usermask> */
+ if (str_array_length(args) != 1) {
+ i_error("%s: ADD: Invalid parameters", client->conn.name);
+ return -1;
+ }
+
+ if (strchr(args[0], '*') == NULL && strchr(args[0], '?') == NULL) {
+ struct replicator_user *user =
+ replicator_queue_get(queue, args[0]);
+ replicator_queue_add(queue, user);
+ } else {
+ replicator_queue_add_auth_users(queue, set->auth_socket_path,
+ args[0], ioloop_time);
+ }
+ o_stream_nsend_str(client->conn.output, "+\n");
+ return 0;
+}
+
+static int
+client_input_remove(struct doveadm_connection *client, const char *const *args)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ struct replicator_user *user;
+
+ /* <username> */
+ if (str_array_length(args) != 1) {
+ i_error("%s: REMOVE: Invalid parameters", client->conn.name);
+ return -1;
+ }
+ user = replicator_queue_lookup(queue, args[0]);
+ if (user == NULL)
+ o_stream_nsend_str(client->conn.output, "-User not found\n");
+ else {
+ replicator_queue_remove(queue, &user);
+ o_stream_nsend_str(client->conn.output, "+\n");
+ }
+ return 0;
+}
+
+static int
+client_input_notify(struct doveadm_connection *client, const char *const *args)
+{
+ struct replicator_queue *queue =
+ replicator_brain_get_queue(client->brain);
+ struct replicator_user *user;
+
+ /* <username> <flags> <state> */
+ if (str_array_length(args) < 3) {
+ i_error("%s: NOTIFY: Invalid parameters", client->conn.name);
+ return -1;
+ }
+
+ user = replicator_queue_get(queue, args[0]);
+ if (args[1][0] == 'f')
+ user->last_full_sync = ioloop_time;
+ user->last_fast_sync = ioloop_time;
+ user->last_update = ioloop_time;
+ replicator_queue_add(queue, user);
+
+ if (args[2][0] != '\0') {
+ i_free(user->state);
+ user->state = i_strdup(args[2]);
+ }
+ o_stream_nsend_str(client->conn.output, "+\n");
+ return 0;
+}
+
+static int client_input_args(struct connection *conn, const char *const *args)
+{
+ struct doveadm_connection *client = (struct doveadm_connection *)conn;
+ const char *cmd = args[0];
+
+ if (cmd == NULL) {
+ i_error("%s: Empty command", conn->name);
+ return 0;
+ }
+ args++;
+
+ if (strcmp(cmd, "STATUS") == 0)
+ return client_input_status(client, args);
+ else if (strcmp(cmd, "STATUS-DSYNC") == 0)
+ return client_input_status_dsyncs(client);
+ else if (strcmp(cmd, "REPLICATE") == 0)
+ return client_input_replicate(client, args);
+ else if (strcmp(cmd, "ADD") == 0)
+ return client_input_add(client, args);
+ else if (strcmp(cmd, "REMOVE") == 0)
+ return client_input_remove(client, args);
+ else if (strcmp(cmd, "NOTIFY") == 0)
+ return client_input_notify(client, args);
+ i_error("%s: Unknown command: %s", conn->name, cmd);
+ return -1;
+}
+
+static void client_destroy(struct connection *conn)
+{
+ struct doveadm_connection *client = (struct doveadm_connection *)conn;
+
+ connection_deinit(&client->conn);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+void doveadm_connection_create(struct replicator_brain *brain, int fd)
+{
+ struct doveadm_connection *client;
+
+ client = i_new(struct doveadm_connection, 1);
+ client->brain = brain;
+ connection_init_server(doveadm_connections, &client->conn,
+ "doveadm-client", fd, fd);
+}
+
+static struct connection_settings doveadm_conn_set = {
+ .service_name_in = "replicator-doveadm-client",
+ .service_name_out = "replicator-doveadm-server",
+ .major_version = REPLICATOR_DOVEADM_MAJOR_VERSION,
+ .minor_version = REPLICATOR_DOVEADM_MINOR_VERSION,
+
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs doveadm_conn_vfuncs = {
+ .destroy = client_destroy,
+ .input_args = client_input_args
+};
+
+void doveadm_connections_init(void)
+{
+ doveadm_connections = connection_list_init(&doveadm_conn_set,
+ &doveadm_conn_vfuncs);
+}
+
+void doveadm_connections_deinit(void)
+{
+ connection_list_deinit(&doveadm_connections);
+}
diff --git a/src/replication/replicator/doveadm-connection.h b/src/replication/replicator/doveadm-connection.h
new file mode 100644
index 0000000..066fc7b
--- /dev/null
+++ b/src/replication/replicator/doveadm-connection.h
@@ -0,0 +1,11 @@
+#ifndef DOVEADM_CONNECTION_H
+#define DOVEADM_CONNECTION_H
+
+struct replicator_brain;
+
+void doveadm_connection_create(struct replicator_brain *brain, int fd);
+
+void doveadm_connections_init(void);
+void doveadm_connections_deinit(void);
+
+#endif
diff --git a/src/replication/replicator/dsync-client.c b/src/replication/replicator/dsync-client.c
new file mode 100644
index 0000000..2df12da
--- /dev/null
+++ b/src/replication/replicator/dsync-client.c
@@ -0,0 +1,274 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "dsync-client.h"
+
+#include <unistd.h>
+
+#define DSYNC_FAIL_TIMEOUT_MSECS (1000*5)
+#define DOVEADM_HANDSHAKE "VERSION\tdoveadm-server\t1\t0\n"
+
+struct dsync_client {
+ char *path;
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to;
+
+ char *dsync_params;
+ char *username;
+ char *state;
+ enum dsync_type sync_type;
+ dsync_callback_t *callback;
+ void *context;
+
+ time_t last_connect_failure;
+ bool handshaked:1;
+ bool cmd_sent:1;
+};
+
+struct dsync_client *
+dsync_client_init(const char *path, const char *dsync_params)
+{
+ struct dsync_client *client;
+
+ client = i_new(struct dsync_client, 1);
+ client->path = i_strdup(path);
+ client->fd = -1;
+ client->dsync_params = i_strdup(dsync_params);
+ return client;
+}
+
+static void dsync_callback(struct dsync_client *client,
+ const char *state, enum dsync_reply reply)
+{
+ dsync_callback_t *callback = client->callback;
+ void *context = client->context;
+
+ timeout_remove(&client->to);
+
+ client->callback = NULL;
+ client->context = NULL;
+
+ /* make sure callback doesn't try to reuse this connection, since
+ we can't currently handle it */
+ i_assert(!client->cmd_sent);
+ client->cmd_sent = TRUE;
+ callback(reply, state, context);
+ client->cmd_sent = FALSE;
+}
+
+static void dsync_close(struct dsync_client *client)
+{
+ client->cmd_sent = FALSE;
+ client->handshaked = FALSE;
+ i_free_and_null(client->state);
+ i_free_and_null(client->username);
+
+ if (client->fd == -1)
+ return;
+
+ io_remove(&client->io);
+ o_stream_destroy(&client->output);
+ i_stream_destroy(&client->input);
+ if (close(client->fd) < 0)
+ i_error("close(dsync) failed: %m");
+ client->fd = -1;
+}
+
+static void dsync_disconnect(struct dsync_client *client)
+{
+ dsync_close(client);
+ if (client->callback != NULL)
+ dsync_callback(client, "", DSYNC_REPLY_FAIL);
+}
+
+void dsync_client_deinit(struct dsync_client **_client)
+{
+ struct dsync_client *client = *_client;
+
+ *_client = NULL;
+
+ dsync_disconnect(client);
+ i_free(client->dsync_params);
+ i_free(client->path);
+ i_free(client);
+}
+
+static int dsync_input_line(struct dsync_client *client, const char *line)
+{
+ const char *state;
+
+ if (!client->handshaked) {
+ if (strcmp(line, "+") != 0) {
+ i_error("%s: Unexpected handshake: %s",
+ client->path, line);
+ return -1;
+ }
+ client->handshaked = TRUE;
+ return 0;
+ }
+ if (client->callback == NULL) {
+ i_error("%s: Unexpected input: %s", client->path, line);
+ return -1;
+ }
+ if (client->state == NULL) {
+ client->state = i_strdup(t_strcut(line, '\t'));
+ return 0;
+ }
+ state = t_strdup(client->state);
+ line = t_strdup(line);
+ dsync_close(client);
+
+ if (line[0] == '+')
+ dsync_callback(client, state, DSYNC_REPLY_OK);
+ else if (line[0] == '-') {
+ if (strcmp(line+1, "NOUSER") == 0)
+ dsync_callback(client, "", DSYNC_REPLY_NOUSER);
+ else if (strcmp(line+1, "NOREPLICATE") == 0)
+ dsync_callback(client, "", DSYNC_REPLY_NOREPLICATE);
+ else
+ dsync_callback(client, "", DSYNC_REPLY_FAIL);
+ } else {
+ i_error("%s: Invalid input: %s", client->path, line);
+ return -1;
+ }
+ /* FIXME: disconnect after each request for now.
+ doveadm server's getopt() handling seems to break otherwise.
+ also with multiple UIDs doveadm-server fails because setid() fails */
+ return -1;
+}
+
+static void dsync_input(struct dsync_client *client)
+{
+ const char *line;
+
+ while ((line = i_stream_read_next_line(client->input)) != NULL) {
+ if (dsync_input_line(client, line) < 0) {
+ dsync_disconnect(client);
+ return;
+ }
+ }
+ if (client->input->eof)
+ dsync_disconnect(client);
+}
+
+static int dsync_connect(struct dsync_client *client)
+{
+ if (client->fd != -1)
+ return 0;
+
+ if (client->last_connect_failure == ioloop_time)
+ return -1;
+
+ client->fd = net_connect_unix(client->path);
+ if (client->fd == -1) {
+ i_error("net_connect_unix(%s) failed: %m", client->path);
+ client->last_connect_failure = ioloop_time;
+ return -1;
+ }
+ client->last_connect_failure = 0;
+ client->io = io_add(client->fd, IO_READ, dsync_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);
+ o_stream_nsend_str(client->output, DOVEADM_HANDSHAKE);
+ return 0;
+}
+
+static void dsync_fail_timeout(struct dsync_client *client)
+{
+ dsync_disconnect(client);
+}
+
+void dsync_client_sync(struct dsync_client *client,
+ const char *username, const char *state, bool full,
+ dsync_callback_t *callback, void *context)
+{
+ string_t *cmd;
+ unsigned int pos;
+ char *p;
+
+ i_assert(callback != NULL);
+ i_assert(!dsync_client_is_busy(client));
+
+ client->username = i_strdup(username);
+ client->cmd_sent = TRUE;
+ client->callback = callback;
+ client->context = context;
+ if (full)
+ client->sync_type = DSYNC_TYPE_FULL;
+ else if (state != NULL && state[0] != '\0')
+ client->sync_type = DSYNC_TYPE_INCREMENTAL;
+ else
+ client->sync_type = DSYNC_TYPE_NORMAL;
+
+ if (dsync_connect(client) < 0) {
+ i_assert(client->to == NULL);
+ client->to = timeout_add(DSYNC_FAIL_TIMEOUT_MSECS,
+ dsync_fail_timeout, client);
+ } else {
+ /* <flags> <username> <command> [<args>] */
+ cmd = t_str_new(256);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, username);
+ str_append(cmd, "\tsync\t");
+ pos = str_len(cmd);
+ /* insert the parameters. we can do it simply by converting
+ spaces into tabs, it's unlikely we'll ever need anything
+ more complex here. */
+ str_append(cmd, client->dsync_params);
+ p = str_c_modifiable(cmd) + pos;
+ for (; *p != '\0'; p++) {
+ if (*p == ' ')
+ *p = '\t';
+ }
+ if (full)
+ str_append(cmd, "\t-f");
+ str_append(cmd, "\t-s\t");
+ if (state != NULL)
+ str_append(cmd, state);
+ str_append_c(cmd, '\n');
+ o_stream_nsend(client->output, str_data(cmd), str_len(cmd));
+ }
+}
+
+bool dsync_client_is_busy(struct dsync_client *client)
+{
+ return client->cmd_sent;
+}
+
+const char *dsync_client_get_username(struct dsync_client *conn)
+{
+ return conn->username;
+}
+
+enum dsync_type dsync_client_get_type(struct dsync_client *conn)
+{
+ return conn->sync_type;
+}
+
+const char *dsync_client_get_state(struct dsync_client *conn)
+{
+ if (conn->fd == -1) {
+ if (conn->last_connect_failure == 0)
+ return "Not connected";
+ return t_strdup_printf("Failed to connect to '%s' - last attempt %ld secs ago", conn->path,
+ (long)(ioloop_time - conn->last_connect_failure));
+ }
+ if (!dsync_client_is_busy(conn))
+ return "Idle";
+ if (!conn->handshaked)
+ return "Waiting for handshake";
+ if (conn->state == NULL)
+ return "Waiting for dsync to finish";
+ else
+ return "Waiting for dsync to finish (second line)";
+}
diff --git a/src/replication/replicator/dsync-client.h b/src/replication/replicator/dsync-client.h
new file mode 100644
index 0000000..c55b815
--- /dev/null
+++ b/src/replication/replicator/dsync-client.h
@@ -0,0 +1,37 @@
+#ifndef DSYNC_CLIENT_H
+#define DSYNC_CLIENT_H
+
+struct dsync_client;
+
+enum dsync_reply {
+ DSYNC_REPLY_OK,
+ DSYNC_REPLY_FAIL,
+ DSYNC_REPLY_NOUSER,
+ DSYNC_REPLY_NOREPLICATE,
+};
+
+enum dsync_type {
+ DSYNC_TYPE_FULL,
+ DSYNC_TYPE_NORMAL,
+ DSYNC_TYPE_INCREMENTAL
+};
+
+ARRAY_DEFINE_TYPE(dsync_client, struct dsync_client *);
+
+typedef void dsync_callback_t(enum dsync_reply reply,
+ const char *state, void *context);
+
+struct dsync_client *
+dsync_client_init(const char *path, const char *dsync_params);
+void dsync_client_deinit(struct dsync_client **conn);
+
+void dsync_client_sync(struct dsync_client *conn,
+ const char *username, const char *state, bool full,
+ dsync_callback_t *callback, void *context);
+bool dsync_client_is_busy(struct dsync_client *conn);
+
+const char *dsync_client_get_username(struct dsync_client *conn);
+enum dsync_type dsync_client_get_type(struct dsync_client *conn);
+const char *dsync_client_get_state(struct dsync_client *conn);
+
+#endif
diff --git a/src/replication/replicator/notify-connection.c b/src/replication/replicator/notify-connection.c
new file mode 100644
index 0000000..0f2c386
--- /dev/null
+++ b/src/replication/replicator/notify-connection.c
@@ -0,0 +1,206 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "replicator-queue.h"
+#include "notify-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE (1024*64)
+#define NOTIFY_CLIENT_PROTOCOL_MAJOR_VERSION 1
+#define NOTIFY_CLIENT_PROTOCOL_MINOR_VERSION 0
+
+struct notify_connection {
+ struct notify_connection *prev, *next;
+ int refcount;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ struct replicator_queue *queue;
+
+ bool version_received:1;
+ bool destroyed:1;
+};
+
+struct notify_sync_request {
+ struct notify_connection *conn;
+ unsigned int id;
+};
+
+static struct notify_connection *connections;
+
+static void notify_connection_destroy(struct notify_connection *conn);
+
+static void notify_sync_callback(bool success, void *context)
+{
+ struct notify_sync_request *request = context;
+
+ o_stream_nsend_str(request->conn->output, t_strdup_printf(
+ "%c\t%u\n", success ? '+' : '-', request->id));
+
+ notify_connection_unref(&request->conn);
+ i_free(request);
+}
+
+static int
+notify_connection_input_line(struct notify_connection *conn, const char *line)
+{
+ struct notify_sync_request *request;
+ const char *const *args;
+ enum replication_priority priority;
+ unsigned int id;
+
+ /* U \t <username> \t <priority> [\t <sync id>] */
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 2) {
+ i_error("notify client sent invalid input: %s", line);
+ return -1;
+ }
+ if (strcmp(args[0], "U") != 0) {
+ i_error("notify client sent unknown command: %s", args[0]);
+ return -1;
+ }
+ if (replication_priority_parse(args[2], &priority) < 0) {
+ i_error("notify client sent invalid priority: %s", args[2]);
+ return -1;
+ }
+ if (priority != REPLICATION_PRIORITY_SYNC) {
+ struct replicator_user *user =
+ replicator_queue_get(conn->queue, args[1]);
+ replicator_queue_update(conn->queue, user, priority);
+ replicator_queue_add(conn->queue, user);
+ } else if (args[3] == NULL || str_to_uint(args[3], &id) < 0) {
+ i_error("notify client sent invalid sync id: %s", line);
+ return -1;
+ } else {
+ request = i_new(struct notify_sync_request, 1);
+ request->conn = conn;
+ request->id = id;
+ notify_connection_ref(conn);
+ struct replicator_user *user =
+ replicator_queue_get(conn->queue, args[1]);
+ replicator_queue_update(conn->queue, user,
+ REPLICATION_PRIORITY_SYNC);
+ replicator_queue_add_sync_callback(conn->queue, user,
+ notify_sync_callback,
+ request);
+ }
+ return 0;
+}
+
+static void notify_connection_input(struct notify_connection *conn)
+{
+ const char *line;
+ int ret;
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ i_error("BUG: Client connection sent too much data");
+ notify_connection_destroy(conn);
+ return;
+ case -1:
+ notify_connection_destroy(conn);
+ return;
+ }
+
+ if (!conn->version_received) {
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "replicator-notify",
+ NOTIFY_CLIENT_PROTOCOL_MAJOR_VERSION)) {
+ i_error("Notify client not compatible with this server "
+ "(mixed old and new binaries?)");
+ notify_connection_destroy(conn);
+ return;
+ }
+ conn->version_received = TRUE;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ ret = notify_connection_input_line(conn, line);
+ } T_END;
+ if (ret < 0) {
+ notify_connection_destroy(conn);
+ break;
+ }
+ }
+}
+
+struct notify_connection *
+notify_connection_create(int fd, struct replicator_queue *queue)
+{
+ struct notify_connection *conn;
+
+ i_assert(fd >= 0);
+
+ conn = i_new(struct notify_connection, 1);
+ conn->refcount = 1;
+ conn->queue = queue;
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ conn->io = io_add(fd, IO_READ, notify_connection_input, conn);
+ conn->queue = queue;
+
+ DLLIST_PREPEND(&connections, conn);
+ return conn;
+}
+
+static void notify_connection_destroy(struct notify_connection *conn)
+{
+ if (conn->destroyed)
+ return;
+ conn->destroyed = TRUE;
+
+ DLLIST_REMOVE(&connections, conn);
+
+ io_remove(&conn->io);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(notify connection) failed: %m");
+ conn->fd = -1;
+
+ notify_connection_unref(&conn);
+ master_service_client_connection_destroyed(master_service);
+}
+
+void notify_connection_ref(struct notify_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+
+ conn->refcount++;
+}
+
+void notify_connection_unref(struct notify_connection **_conn)
+{
+ struct notify_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ notify_connection_destroy(conn);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ i_free(conn);
+}
+
+void notify_connections_destroy_all(void)
+{
+ while (connections != NULL)
+ notify_connection_destroy(connections);
+}
diff --git a/src/replication/replicator/notify-connection.h b/src/replication/replicator/notify-connection.h
new file mode 100644
index 0000000..fa62fc7
--- /dev/null
+++ b/src/replication/replicator/notify-connection.h
@@ -0,0 +1,13 @@
+#ifndef NOTIFY_CONNECTION_H
+#define NOTIFY_CONNECTION_H
+
+struct replicator_queue;
+
+struct notify_connection *
+notify_connection_create(int fd, struct replicator_queue *queue);
+void notify_connection_ref(struct notify_connection *conn);
+void notify_connection_unref(struct notify_connection **conn);
+
+void notify_connections_destroy_all(void);
+
+#endif
diff --git a/src/replication/replicator/replicator-brain.c b/src/replication/replicator/replicator-brain.c
new file mode 100644
index 0000000..65cfcec
--- /dev/null
+++ b/src/replication/replicator/replicator-brain.c
@@ -0,0 +1,202 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "dsync-client.h"
+#include "replicator-settings.h"
+#include "replicator-queue.h"
+#include "replicator-brain.h"
+
+struct replicator_sync_context {
+ struct replicator_brain *brain;
+ struct replicator_user *user;
+};
+
+struct replicator_brain {
+ pool_t pool;
+ struct replicator_queue *queue;
+ const struct replicator_settings *set;
+ struct timeout *to;
+
+ ARRAY_TYPE(dsync_client) dsync_clients;
+
+ bool deinitializing:1;
+};
+
+static void replicator_brain_fill(struct replicator_brain *brain);
+
+static void replicator_brain_timeout(struct replicator_brain *brain)
+{
+ timeout_remove(&brain->to);
+ replicator_brain_fill(brain);
+}
+
+static void replicator_brain_queue_changed(void *context)
+{
+ struct replicator_brain *brain = context;
+
+ /* Delay a bit filling the replication. We could have gotten here
+ before the replicator_user change was fully filled out. */
+ timeout_remove(&brain->to);
+ brain->to = timeout_add_short(0, replicator_brain_timeout, brain);
+}
+
+struct replicator_brain *
+replicator_brain_init(struct replicator_queue *queue,
+ const struct replicator_settings *set)
+{
+ struct replicator_brain *brain;
+ pool_t pool;
+
+ pool = pool_alloconly_create("replication brain", 1024);
+ brain = p_new(pool, struct replicator_brain, 1);
+ brain->pool = pool;
+ brain->queue = queue;
+ brain->set = set;
+ p_array_init(&brain->dsync_clients, pool, 16);
+ replicator_queue_set_change_callback(queue,
+ replicator_brain_queue_changed, brain);
+ replicator_brain_fill(brain);
+ return brain;
+}
+
+void replicator_brain_deinit(struct replicator_brain **_brain)
+{
+ struct replicator_brain *brain = *_brain;
+ struct dsync_client *conn;
+
+ *_brain = NULL;
+
+ brain->deinitializing = TRUE;
+ array_foreach_elem(&brain->dsync_clients, conn)
+ dsync_client_deinit(&conn);
+ timeout_remove(&brain->to);
+ pool_unref(&brain->pool);
+}
+
+struct replicator_queue *
+replicator_brain_get_queue(struct replicator_brain *brain)
+{
+ return brain->queue;
+}
+
+const struct replicator_settings *
+replicator_brain_get_settings(struct replicator_brain *brain)
+{
+ return brain->set;
+}
+
+const ARRAY_TYPE(dsync_client) *
+replicator_brain_get_dsync_clients(struct replicator_brain *brain)
+{
+ return &brain->dsync_clients;
+}
+
+static struct dsync_client *
+get_dsync_client(struct replicator_brain *brain)
+{
+ struct dsync_client *conn;
+
+ array_foreach_elem(&brain->dsync_clients, conn) {
+ if (!dsync_client_is_busy(conn))
+ return conn;
+ }
+ if (array_count(&brain->dsync_clients) ==
+ brain->set->replication_max_conns)
+ return NULL;
+
+ conn = dsync_client_init(brain->set->doveadm_socket_path,
+ brain->set->replication_dsync_parameters);
+ array_push_back(&brain->dsync_clients, &conn);
+ return conn;
+}
+
+static void dsync_callback(enum dsync_reply reply, const char *state,
+ void *context)
+{
+ struct replicator_sync_context *ctx = context;
+ struct replicator_user *user = ctx->user;
+
+ if (!replicator_user_unref(&user)) {
+ /* user was already removed */
+ } else if (reply == DSYNC_REPLY_NOUSER ||
+ reply == DSYNC_REPLY_NOREPLICATE) {
+ /* user no longer exists, or is not wanted for replication,
+ remove from replication */
+ replicator_queue_remove(ctx->brain->queue, &ctx->user);
+ } else {
+ i_free(ctx->user->state);
+ ctx->user->state = i_strdup_empty(state);
+ ctx->user->last_sync_failed = reply != DSYNC_REPLY_OK;
+ if (reply == DSYNC_REPLY_OK)
+ ctx->user->last_successful_sync = ioloop_time;
+ replicator_queue_push(ctx->brain->queue, ctx->user);
+ }
+ if (!ctx->brain->deinitializing)
+ replicator_brain_fill(ctx->brain);
+ i_free(ctx);
+}
+
+static bool
+dsync_replicate(struct replicator_brain *brain, struct replicator_user *user)
+{
+ struct replicator_sync_context *ctx;
+ struct dsync_client *conn;
+ time_t next_full_sync;
+ bool full;
+
+ conn = get_dsync_client(brain);
+ if (conn == NULL)
+ return FALSE;
+
+ next_full_sync = user->last_full_sync +
+ brain->set->replication_full_sync_interval;
+ full = next_full_sync <= ioloop_time;
+ /* update the sync times immediately. if the replication fails we still
+ wouldn't want it to be retried immediately. */
+ user->last_fast_sync = ioloop_time;
+ if (full || user->force_full_sync) {
+ user->last_full_sync = ioloop_time;
+ user->force_full_sync = FALSE;
+ }
+ /* reset priority also. if more updates arrive during replication
+ we'll do another replication to make sure nothing gets lost */
+ user->priority = REPLICATION_PRIORITY_NONE;
+
+ ctx = i_new(struct replicator_sync_context, 1);
+ ctx->brain = brain;
+ ctx->user = user;
+ replicator_user_ref(user);
+ dsync_client_sync(conn, user->username, user->state, full,
+ dsync_callback, ctx);
+ return TRUE;
+}
+
+static bool replicator_brain_fill_next(struct replicator_brain *brain)
+{
+ struct replicator_user *user;
+ unsigned int next_secs;
+
+ user = replicator_queue_pop(brain->queue, &next_secs);
+ if (user == NULL) {
+ /* nothing more to do */
+ timeout_remove(&brain->to);
+ brain->to = timeout_add(next_secs * 1000,
+ replicator_brain_timeout, brain);
+ return FALSE;
+ }
+
+ if (!dsync_replicate(brain, user)) {
+ /* all connections were full, put the user back to queue */
+ replicator_queue_push(brain->queue, user);
+ return FALSE;
+ }
+ /* replication started for the user */
+ return TRUE;
+}
+
+static void replicator_brain_fill(struct replicator_brain *brain)
+{
+ while (replicator_brain_fill_next(brain)) ;
+}
diff --git a/src/replication/replicator/replicator-brain.h b/src/replication/replicator/replicator-brain.h
new file mode 100644
index 0000000..6e9ae23
--- /dev/null
+++ b/src/replication/replicator/replicator-brain.h
@@ -0,0 +1,20 @@
+#ifndef REPLICATOR_BRAIN_H
+#define REPLICATOR_BRAIN_H
+
+struct replicator_settings;
+struct replicator_queue;
+
+struct replicator_brain *
+replicator_brain_init(struct replicator_queue *queue,
+ const struct replicator_settings *set);
+void replicator_brain_deinit(struct replicator_brain **brain);
+
+struct replicator_queue *
+replicator_brain_get_queue(struct replicator_brain *brain);
+const struct replicator_settings *
+replicator_brain_get_settings(struct replicator_brain *brain);
+
+const ARRAY_TYPE(dsync_client) *
+replicator_brain_get_dsync_clients(struct replicator_brain *brain);
+
+#endif
diff --git a/src/replication/replicator/replicator-queue-auth.c b/src/replication/replicator/replicator-queue-auth.c
new file mode 100644
index 0000000..eedb814
--- /dev/null
+++ b/src/replication/replicator/replicator-queue-auth.c
@@ -0,0 +1,37 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "auth-master.h"
+#include "replicator-queue.h"
+
+#define REPLICATOR_AUTH_SERVICE_NAME "replicator"
+
+void replicator_queue_add_auth_users(struct replicator_queue *queue,
+ const char *auth_socket_path,
+ const char *usermask, time_t last_update)
+{
+ struct auth_master_connection *auth_conn;
+ struct auth_master_user_list_ctx *ctx;
+ struct auth_user_info user_info;
+ struct replicator_user *user;
+ const char *username;
+
+ auth_conn = auth_master_init(auth_socket_path,
+ AUTH_MASTER_FLAG_NO_IDLE_TIMEOUT);
+
+ i_zero(&user_info);
+ user_info.service = REPLICATOR_AUTH_SERVICE_NAME;
+
+ /* add all users into replication queue, so that we can start doing
+ full syncs for everyone whose state can't be found */
+ ctx = auth_master_user_list_init(auth_conn, usermask, &user_info);
+ while ((username = auth_master_user_list_next(ctx)) != NULL) {
+ user = replicator_queue_get(queue, username);
+ replicator_queue_update(queue, user, REPLICATION_PRIORITY_NONE);
+ replicator_queue_add(queue, user);
+ user->last_update = last_update;
+ }
+ if (auth_master_user_list_deinit(&ctx) < 0)
+ i_error("listing users failed, can't replicate existing data");
+ auth_master_deinit(&auth_conn);
+}
diff --git a/src/replication/replicator/replicator-queue.c b/src/replication/replicator/replicator-queue.c
new file mode 100644
index 0000000..31f296a
--- /dev/null
+++ b/src/replication/replicator/replicator-queue.c
@@ -0,0 +1,527 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "hash.h"
+#include "replicator-queue.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+struct replicator_sync_lookup {
+ struct replicator_user *user;
+
+ replicator_sync_callback_t *callback;
+ void *context;
+
+ bool wait_for_next_push;
+};
+
+struct replicator_queue {
+ struct priorityq *user_queue;
+ /* username => struct replicator_user* */
+ HASH_TABLE(char *, struct replicator_user *) user_hash;
+
+ ARRAY(struct replicator_sync_lookup) sync_lookups;
+
+ unsigned int full_sync_interval;
+ unsigned int failure_resync_interval;
+
+ void (*change_callback)(void *context);
+ void *change_context;
+};
+
+struct replicator_queue_iter {
+ struct replicator_queue *queue;
+ struct hash_iterate_context *iter;
+};
+
+static unsigned int replicator_full_sync_interval = 0;
+static unsigned int replicator_failure_resync_interval = 0;
+
+static time_t replicator_user_next_sync_time(const struct replicator_user *user)
+{
+ /* The idea is that the higher the priority, the more likely it will
+ be prioritized over low priority syncs. However, to avoid permanent
+ starvation of lower priority users, the priority boost is only
+ temporary.
+
+ The REPLICATION_PRIORITY_*_SECS macros effectively specify how long
+ lower priority requests are allowed to be waiting. */
+#define REPLICATION_PRIORITY_LOW_SECS (60*15)
+#define REPLICATION_PRIORITY_HIGH_SECS (60*30)
+#define REPLICATION_PRIORITY_SYNC_SECS (60*45)
+ /* When priority != none, user needs to be replicated ASAP.
+ The question is just whether the queue is already busy and other
+ users need to be synced even more faster. */
+ if (user->last_fast_sync == 0) {
+ /* User has never been synced yet. These will be replicated
+ first. Still, try to replicate higher priority users faster
+ than lower priority users. */
+ if (user->priority != REPLICATION_PRIORITY_NONE)
+ return REPLICATION_PRIORITY_SYNC - user->priority;
+ }
+ switch (user->priority) {
+ case REPLICATION_PRIORITY_NONE:
+ break;
+ case REPLICATION_PRIORITY_LOW:
+ i_assert(user->last_update >= REPLICATION_PRIORITY_LOW_SECS);
+ return user->last_update - REPLICATION_PRIORITY_LOW_SECS;
+ case REPLICATION_PRIORITY_HIGH:
+ i_assert(user->last_update >= REPLICATION_PRIORITY_HIGH_SECS);
+ return user->last_update - REPLICATION_PRIORITY_HIGH_SECS;
+ case REPLICATION_PRIORITY_SYNC:
+ i_assert(user->last_update >= REPLICATION_PRIORITY_HIGH_SECS);
+ return user->last_update - REPLICATION_PRIORITY_SYNC_SECS;
+ }
+ if (user->last_sync_failed) {
+ /* failures need to be retried at specific intervals */
+ return user->last_fast_sync +
+ replicator_failure_resync_interval;
+ }
+ /* full resyncs should be done at configured intervals */
+ return user->last_full_sync + replicator_full_sync_interval;
+}
+
+static int user_priority_cmp(const void *p1, const void *p2)
+{
+ const struct replicator_user *user1 = p1, *user2 = p2;
+ time_t next_sync1 = replicator_user_next_sync_time(user1);
+ time_t next_sync2 = replicator_user_next_sync_time(user2);
+ if (next_sync1 < next_sync2)
+ return -1;
+ if (next_sync1 > next_sync2)
+ return 1;
+ return 0;
+}
+
+struct replicator_queue *
+replicator_queue_init(unsigned int full_sync_interval,
+ unsigned int failure_resync_interval)
+{
+ struct replicator_queue *queue;
+
+ /* priorityq callback needs to access these */
+ i_assert(replicator_full_sync_interval == 0 ||
+ replicator_full_sync_interval == full_sync_interval);
+ replicator_full_sync_interval = full_sync_interval;
+ i_assert(replicator_failure_resync_interval == 0 ||
+ replicator_failure_resync_interval == failure_resync_interval);
+ replicator_full_sync_interval = full_sync_interval;
+ replicator_failure_resync_interval = failure_resync_interval;
+
+ queue = i_new(struct replicator_queue, 1);
+ queue->full_sync_interval = full_sync_interval;
+ queue->failure_resync_interval = failure_resync_interval;
+ queue->user_queue = priorityq_init(user_priority_cmp, 1024);
+ hash_table_create(&queue->user_hash, default_pool, 1024,
+ str_hash, strcmp);
+ i_array_init(&queue->sync_lookups, 32);
+ return queue;
+}
+
+void replicator_queue_deinit(struct replicator_queue **_queue)
+{
+ struct replicator_queue *queue = *_queue;
+ struct priorityq_item *item;
+
+ *_queue = NULL;
+
+ queue->change_callback = NULL;
+
+ while ((item = priorityq_pop(queue->user_queue)) != NULL) {
+ struct replicator_user *user = (struct replicator_user *)item;
+
+ user->popped = TRUE;
+ replicator_queue_remove(queue, &user);
+ }
+
+ priorityq_deinit(&queue->user_queue);
+ hash_table_destroy(&queue->user_hash);
+ i_assert(array_count(&queue->sync_lookups) == 0);
+ array_free(&queue->sync_lookups);
+ i_free(queue);
+}
+
+void replicator_queue_set_change_callback(struct replicator_queue *queue,
+ void (*callback)(void *context),
+ void *context)
+{
+ queue->change_callback = callback;
+ queue->change_context = context;
+}
+
+void replicator_user_ref(struct replicator_user *user)
+{
+ i_assert(user->refcount > 0);
+ user->refcount++;
+}
+
+bool replicator_user_unref(struct replicator_user **_user)
+{
+ struct replicator_user *user = *_user;
+
+ i_assert(user->refcount > 0);
+ *_user = NULL;
+ if (--user->refcount > 0)
+ return TRUE;
+
+ i_free(user->state);
+ i_free(user->username);
+ i_free(user);
+ return FALSE;
+}
+
+struct replicator_user *
+replicator_queue_lookup(struct replicator_queue *queue, const char *username)
+{
+ return hash_table_lookup(queue->user_hash, username);
+}
+
+struct replicator_user *
+replicator_queue_get(struct replicator_queue *queue, const char *username)
+{
+ struct replicator_user *user;
+
+ user = replicator_queue_lookup(queue, username);
+ if (user == NULL) {
+ user = i_new(struct replicator_user, 1);
+ user->refcount = 1;
+ user->username = i_strdup(username);
+ user->last_update = ioloop_time;
+ hash_table_insert(queue->user_hash, user->username, user);
+ if (!user->popped)
+ priorityq_add(queue->user_queue, &user->item);
+ }
+ return user;
+}
+
+void replicator_queue_update(struct replicator_queue *queue ATTR_UNUSED,
+ struct replicator_user *user,
+ enum replication_priority priority)
+{
+ if (user->priority >= priority) {
+ /* user already has at least this high priority */
+ return;
+ }
+ user->priority = priority;
+ user->last_update = ioloop_time;
+}
+
+void replicator_queue_add(struct replicator_queue *queue,
+ struct replicator_user *user)
+{
+ if (!user->popped) {
+ priorityq_remove(queue->user_queue, &user->item);
+ priorityq_add(queue->user_queue, &user->item);
+ }
+ if (queue->change_callback != NULL)
+ queue->change_callback(queue->change_context);
+}
+
+void replicator_queue_add_sync_callback(struct replicator_queue *queue,
+ struct replicator_user *user,
+ replicator_sync_callback_t *callback,
+ void *context)
+{
+ struct replicator_sync_lookup *lookup;
+
+ i_assert(user->priority == REPLICATION_PRIORITY_SYNC);
+
+ lookup = array_append_space(&queue->sync_lookups);
+ lookup->user = user;
+ lookup->callback = callback;
+ lookup->context = context;
+ lookup->wait_for_next_push = user->popped;
+
+ replicator_queue_add(queue, user);
+}
+
+void replicator_queue_remove(struct replicator_queue *queue,
+ struct replicator_user **_user)
+{
+ struct replicator_user *user = *_user;
+
+ *_user = NULL;
+ if (!user->popped)
+ priorityq_remove(queue->user_queue, &user->item);
+ hash_table_remove(queue->user_hash, user->username);
+ replicator_user_unref(&user);
+
+ if (queue->change_callback != NULL)
+ queue->change_callback(queue->change_context);
+}
+
+unsigned int replicator_queue_count(struct replicator_queue *queue)
+{
+ return priorityq_count(queue->user_queue);
+}
+
+bool replicator_queue_want_sync_now(struct replicator_user *user,
+ unsigned int *next_secs_r)
+{
+ time_t next_sync = replicator_user_next_sync_time(user);
+ if (next_sync <= ioloop_time) {
+ *next_secs_r = 0;
+ return TRUE;
+ }
+ *next_secs_r = next_sync - ioloop_time;
+ return FALSE;
+}
+
+struct replicator_user *
+replicator_queue_peek(struct replicator_queue *queue,
+ unsigned int *next_secs_r)
+{
+ struct priorityq_item *item;
+ struct replicator_user *user;
+
+ item = priorityq_peek(queue->user_queue);
+ if (item == NULL) {
+ /* no users defined. we shouldn't normally get here */
+ *next_secs_r = 3600;
+ return NULL;
+ }
+ user = (struct replicator_user *)item;
+ (void)replicator_queue_want_sync_now(user, next_secs_r);
+ return user;
+}
+
+struct replicator_user *
+replicator_queue_pop(struct replicator_queue *queue,
+ unsigned int *next_secs_r)
+{
+ struct replicator_user *user;
+
+ user = replicator_queue_peek(queue, next_secs_r);
+ if (*next_secs_r > 0) {
+ /* we don't want to sync the user yet */
+ return NULL;
+ }
+ if (user != NULL) {
+ priorityq_remove(queue->user_queue, &user->item);
+ user->popped = TRUE;
+ }
+ return user;
+}
+
+static void
+replicator_queue_handle_sync_lookups(struct replicator_queue *queue,
+ struct replicator_user *user)
+{
+ struct replicator_sync_lookup *lookups;
+ ARRAY(struct replicator_sync_lookup) callbacks;
+ unsigned int i, count;
+ bool success = !user->last_sync_failed;
+
+ t_array_init(&callbacks, 8);
+ lookups = array_get_modifiable(&queue->sync_lookups, &count);
+ for (i = 0; i < count; ) {
+ if (lookups[i].user != user)
+ i++;
+ else if (lookups[i].wait_for_next_push) {
+ /* another sync request came while user was being
+ replicated */
+ i_assert(user->priority == REPLICATION_PRIORITY_SYNC);
+ lookups[i].wait_for_next_push = FALSE;
+ i++;
+ } else {
+ array_push_back(&callbacks, &lookups[i]);
+ array_delete(&queue->sync_lookups, i, 1);
+ }
+ }
+
+ array_foreach_modifiable(&callbacks, lookups)
+ lookups->callback(success, lookups->context);
+}
+
+void replicator_queue_push(struct replicator_queue *queue,
+ struct replicator_user *user)
+{
+ i_assert(user->popped);
+
+ priorityq_add(queue->user_queue, &user->item);
+ user->popped = FALSE;
+
+ T_BEGIN {
+ replicator_queue_handle_sync_lookups(queue, user);
+ } T_END;
+}
+
+static int
+replicator_queue_import_line(struct replicator_queue *queue, const char *line)
+{
+ const char *const *args, *username, *state;
+ unsigned int priority;
+ struct replicator_user *user, tmp_user;
+
+ /* <user> <priority> <last update> <last fast sync> <last full sync>
+ <last failed> <state> <last successful sync>*/
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 7)
+ return -1;
+
+ i_zero(&tmp_user);
+ username = args[0];
+ state = t_strdup_noconst(args[6]);
+ if (username[0] == '\0' ||
+ str_to_uint(args[1], &priority) < 0 ||
+ str_to_time(args[2], &tmp_user.last_update) < 0 ||
+ str_to_time(args[3], &tmp_user.last_fast_sync) < 0 ||
+ str_to_time(args[4], &tmp_user.last_full_sync) < 0)
+ return -1;
+ tmp_user.priority = priority;
+ tmp_user.last_sync_failed = args[5][0] != '0';
+
+ if (str_array_length(args) >= 8) {
+ if (str_to_time(args[7], &tmp_user.last_successful_sync) < 0)
+ return -1;
+ } else {
+ tmp_user.last_successful_sync = 0;
+ /* On-disk format didn't have this yet */
+ }
+
+ user = hash_table_lookup(queue->user_hash, username);
+ if (user != NULL) {
+ if (user->last_update > tmp_user.last_update) {
+ /* we already have a newer state */
+ return 0;
+ }
+ if (user->last_update == tmp_user.last_update) {
+ /* either one of these could be newer. use the one
+ with higher priority. */
+ if (user->priority > tmp_user.priority)
+ return 0;
+ }
+ } else {
+ user = replicator_queue_get(queue, username);
+ }
+ user->priority = tmp_user.priority;
+ user->last_update = tmp_user.last_update;
+ user->last_fast_sync = tmp_user.last_fast_sync;
+ user->last_full_sync = tmp_user.last_full_sync;
+ user->last_successful_sync = tmp_user.last_successful_sync;
+ user->last_sync_failed = tmp_user.last_sync_failed;
+ i_free(user->state);
+ user->state = i_strdup(state);
+ replicator_queue_add(queue, user);
+ return 0;
+}
+
+int replicator_queue_import(struct replicator_queue *queue, const char *path)
+{
+ struct istream *input;
+ const char *line;
+ int fd, ret = 0;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+ i_error("open(%s) failed: %m", path);
+ return -1;
+ }
+
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ T_BEGIN {
+ ret = replicator_queue_import_line(queue, line);
+ } T_END;
+ if (ret < 0) {
+ i_error("Corrupted replicator record in %s: %s",
+ path, line);
+ break;
+ }
+ }
+ if (input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", path, i_stream_get_error(input));
+ ret = -1;
+ }
+ i_stream_destroy(&input);
+ return ret;
+}
+
+static void
+replicator_queue_export_user(struct replicator_user *user, string_t *str)
+{
+ str_append_tabescaped(str, user->username);
+ str_printfa(str, "\t%d\t%lld\t%lld\t%lld\t%d\t", (int)user->priority,
+ (long long)user->last_update,
+ (long long)user->last_fast_sync,
+ (long long)user->last_full_sync,
+ user->last_sync_failed ? 1 : 0);
+ if (user->state != NULL)
+ str_append_tabescaped(str, user->state);
+ str_printfa(str, "\t%lld\n", (long long)user->last_successful_sync);
+}
+
+int replicator_queue_export(struct replicator_queue *queue, const char *path)
+{
+ struct replicator_queue_iter *iter;
+ struct replicator_user *user;
+ struct ostream *output;
+ string_t *str;
+ int fd, ret = 0;
+
+ fd = creat(path, 0600);
+ if (fd == -1) {
+ i_error("creat(%s) failed: %m", path);
+ return -1;
+ }
+ output = o_stream_create_fd_file_autoclose(&fd, 0);
+ o_stream_cork(output);
+
+ str = t_str_new(128);
+ iter = replicator_queue_iter_init(queue);
+ while ((user = replicator_queue_iter_next(iter)) != NULL) {
+ str_truncate(str, 0);
+ replicator_queue_export_user(user, str);
+ if (o_stream_send(output, str_data(str), str_len(str)) < 0)
+ break;
+ }
+ replicator_queue_iter_deinit(&iter);
+ 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;
+}
+
+struct replicator_queue_iter *
+replicator_queue_iter_init(struct replicator_queue *queue)
+{
+ struct replicator_queue_iter *iter;
+
+ iter = i_new(struct replicator_queue_iter, 1);
+ iter->queue = queue;
+ iter->iter = hash_table_iterate_init(queue->user_hash);
+ return iter;
+}
+
+struct replicator_user *
+replicator_queue_iter_next(struct replicator_queue_iter *iter)
+{
+ struct replicator_user *user;
+ char *username;
+
+ if (!hash_table_iterate(iter->iter, iter->queue->user_hash,
+ &username, &user))
+ return NULL;
+ return user;
+}
+
+void replicator_queue_iter_deinit(struct replicator_queue_iter **_iter)
+{
+ struct replicator_queue_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ hash_table_iterate_deinit(&iter->iter);
+ i_free(iter);
+}
diff --git a/src/replication/replicator/replicator-queue.h b/src/replication/replicator/replicator-queue.h
new file mode 100644
index 0000000..4e021e2
--- /dev/null
+++ b/src/replication/replicator/replicator-queue.h
@@ -0,0 +1,104 @@
+#ifndef REPLICATOR_QUEUE_H
+#define REPLICATOR_QUEUE_H
+
+#include "priorityq.h"
+#include "replication-common.h"
+
+struct replicator_user {
+ struct priorityq_item item;
+
+ char *username;
+ /* dsync state for incremental syncing */
+ char *state;
+ /* last time this user's state was updated */
+ time_t last_update;
+ /* last_fast_sync is always >= last_full_sync. */
+ time_t last_fast_sync, last_full_sync, last_successful_sync;
+
+ int refcount;
+ enum replication_priority priority;
+ /* User isn't currently in replication queue */
+ bool popped:1;
+ /* Last replication sync failed */
+ bool last_sync_failed:1;
+ /* Force a full sync on the next replication */
+ bool force_full_sync:1;
+};
+
+typedef void replicator_sync_callback_t(bool success, void *context);
+
+struct replicator_queue *
+replicator_queue_init(unsigned int full_sync_interval,
+ unsigned int failure_resync_interval);
+void replicator_queue_deinit(struct replicator_queue **queue);
+
+/* Call the specified callback when data is added/removed/moved in queue
+ via _add(), _add_sync() or _remove() functions (not push/pop). */
+void replicator_queue_set_change_callback(struct replicator_queue *queue,
+ void (*callback)(void *context),
+ void *context);
+
+/* Reference the user */
+void replicator_user_ref(struct replicator_user *user);
+/* Unreference the user. Returns TRUE if refcount is still >0. */
+bool replicator_user_unref(struct replicator_user **user);
+
+/* Lookup an existing user */
+struct replicator_user *
+replicator_queue_lookup(struct replicator_queue *queue, const char *username);
+/* Lookup or create a user and return it. Afterwards replicator_queue_add()
+ must be called to add/move the user to the proper place in the queue. */
+struct replicator_user *
+replicator_queue_get(struct replicator_queue *queue, const char *username);
+/* Update user's priority if it's currently lower. */
+void replicator_queue_update(struct replicator_queue *queue,
+ struct replicator_user *user,
+ enum replication_priority priority);
+void replicator_queue_add(struct replicator_queue *queue,
+ struct replicator_user *user);
+/* Call the callback when user with SYNC priority has finished syncing. */
+void replicator_queue_add_sync_callback(struct replicator_queue *queue,
+ struct replicator_user *user,
+ replicator_sync_callback_t *callback,
+ void *context);
+/* Remove user from replication queue and free it. */
+void replicator_queue_remove(struct replicator_queue *queue,
+ struct replicator_user **user);
+/* Return the number of users in the queue. */
+unsigned int replicator_queue_count(struct replicator_queue *queue);
+
+/* Return the next user from replication queue and how many seconds from now
+ the returned user should be synced (0 = immediately). Returns NULL only if
+ there are no users in the queue. */
+struct replicator_user *
+replicator_queue_peek(struct replicator_queue *queue,
+ unsigned int *next_secs_r);
+/* Return the next user from replication queue, and remove it from the queue.
+ If there's nothing to be replicated currently, returns NULL and sets
+ next_secs_r to when there should be more work to do. */
+struct replicator_user *
+replicator_queue_pop(struct replicator_queue *queue,
+ unsigned int *next_secs_r);
+/* Add user back to queue. */
+void replicator_queue_push(struct replicator_queue *queue,
+ struct replicator_user *user);
+
+int replicator_queue_import(struct replicator_queue *queue, const char *path);
+int replicator_queue_export(struct replicator_queue *queue, const char *path);
+
+/* Returns TRUE if user replication can be started now, FALSE if not. When
+ returning FALSE, next_secs_r is set to user's next replication time. */
+bool replicator_queue_want_sync_now(struct replicator_user *user,
+ unsigned int *next_secs_r);
+/* Iterate through all users in the queue. */
+struct replicator_queue_iter *
+replicator_queue_iter_init(struct replicator_queue *queue);
+struct replicator_user *
+replicator_queue_iter_next(struct replicator_queue_iter *iter);
+void replicator_queue_iter_deinit(struct replicator_queue_iter **iter);
+
+void replicator_queue_add_auth_users(struct replicator_queue *queue,
+ const char *auth_socket_path,
+ const char *usermask, time_t last_update);
+
+#endif
diff --git a/src/replication/replicator/replicator-settings.c b/src/replication/replicator/replicator-settings.c
new file mode 100644
index 0000000..3965100
--- /dev/null
+++ b/src/replication/replicator/replicator-settings.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "replicator-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings replicator_unix_listeners_array[] = {
+ { "replicator", 0600, "$default_internal_user", "" },
+ { "replicator-doveadm", 0, "$default_internal_user", "" }
+};
+static struct file_listener_settings *replicator_unix_listeners[] = {
+ &replicator_unix_listeners_array[0],
+ &replicator_unix_listeners_array[1]
+};
+static buffer_t replicator_unix_listeners_buf = {
+ { { replicator_unix_listeners, sizeof(replicator_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings replicator_service_settings = {
+ .name = "replicator",
+ .protocol = "",
+ .type = "",
+ .executable = "replicator",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &replicator_unix_listeners_buf,
+ sizeof(replicator_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct replicator_settings)
+
+static const struct setting_define replicator_setting_defines[] = {
+ DEF(STR, auth_socket_path),
+ DEF(STR, doveadm_socket_path),
+ DEF(STR, replication_dsync_parameters),
+
+ DEF(TIME, replication_full_sync_interval),
+ DEF(UINT, replication_max_conns),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct replicator_settings replicator_default_settings = {
+ .auth_socket_path = "auth-userdb",
+ .doveadm_socket_path = "doveadm-server",
+ .replication_dsync_parameters = "-d -N -l 30 -U",
+
+ .replication_full_sync_interval = 60*60*24,
+ .replication_max_conns = 10
+};
+
+const struct setting_parser_info replicator_setting_parser_info = {
+ .module_name = "replicator",
+ .defines = replicator_setting_defines,
+ .defaults = &replicator_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct replicator_settings),
+
+ .parent_offset = SIZE_MAX
+};
+
+const struct replicator_settings *replicator_settings;
diff --git a/src/replication/replicator/replicator-settings.h b/src/replication/replicator/replicator-settings.h
new file mode 100644
index 0000000..947bcf8
--- /dev/null
+++ b/src/replication/replicator/replicator-settings.h
@@ -0,0 +1,16 @@
+#ifndef REPLICATOR_SETTINGS_H
+#define REPLICATOR_SETTINGS_H
+
+struct replicator_settings {
+ const char *auth_socket_path;
+ const char *doveadm_socket_path;
+ const char *replication_dsync_parameters;
+
+ unsigned int replication_full_sync_interval;
+ unsigned int replication_max_conns;
+};
+
+extern const struct setting_parser_info replicator_setting_parser_info;
+extern const struct replicator_settings *replicator_settings;
+
+#endif
diff --git a/src/replication/replicator/replicator.c b/src/replication/replicator/replicator.c
new file mode 100644
index 0000000..3600934
--- /dev/null
+++ b/src/replication/replicator/replicator.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "restrict-access.h"
+#include "auth-master.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "notify-connection.h"
+#include "doveadm-connection.h"
+#include "replicator-brain.h"
+#include "replicator-queue.h"
+#include "replicator-settings.h"
+
+#define REPLICATOR_DB_DUMP_INTERVAL_MSECS (1000*60*15)
+/* if syncing fails, try again in 5 minutes */
+#define REPLICATOR_FAILURE_RESYNC_INTERVAL_SECS (60*5)
+#define REPLICATOR_DB_FNAME "replicator.db"
+
+static struct replicator_queue *queue;
+static struct replicator_brain *brain;
+static const struct master_service_settings *service_set;
+static const struct replicator_settings *set;
+static struct timeout *to_dump;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ master_service_client_connection_accept(conn);
+ if (strcmp(conn->name, "replicator-doveadm") == 0)
+ doveadm_connection_create(brain, conn->fd);
+ else
+ (void)notify_connection_create(conn->fd, queue);
+}
+
+static void replication_add_users(struct replicator_queue *queue)
+{
+ const char *path;
+
+ replicator_queue_add_auth_users(queue, set->auth_socket_path, "*", 0);
+
+ /* add updates from replicator db, if it exists */
+ path = t_strconcat(service_set->state_dir, "/"REPLICATOR_DB_FNAME, NULL);
+ (void)replicator_queue_import(queue, path);
+}
+
+static void ATTR_NULL(1)
+replicator_dump_timeout(void *context ATTR_UNUSED)
+{
+ const char *path;
+
+ path = t_strconcat(service_set->state_dir, "/"REPLICATOR_DB_FNAME, NULL);
+ (void)replicator_queue_export(queue, path);
+}
+
+static void main_init(void)
+{
+ void **sets;
+
+ service_set = master_service_settings_get(master_service);
+ sets = master_service_settings_get_others(master_service);
+ set = sets[0];
+
+ queue = replicator_queue_init(set->replication_full_sync_interval,
+ REPLICATOR_FAILURE_RESYNC_INTERVAL_SECS);
+ replication_add_users(queue);
+ to_dump = timeout_add(REPLICATOR_DB_DUMP_INTERVAL_MSECS,
+ replicator_dump_timeout, NULL);
+ brain = replicator_brain_init(queue, set);
+ doveadm_connections_init();
+}
+
+static void main_deinit(void)
+{
+ const char *path;
+
+ doveadm_connections_deinit();
+ notify_connections_destroy_all();
+ replicator_brain_deinit(&brain);
+ timeout_remove(&to_dump);
+ path = t_strconcat(service_set->state_dir, "/"REPLICATOR_DB_FNAME, NULL);
+ (void)replicator_queue_export(queue, path);
+ replicator_queue_deinit(&queue);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &replicator_setting_parser_info,
+ NULL
+ };
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE;
+ const char *error;
+
+ master_service = master_service_init("replicator", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+ /* finish init before we get list of users from auth, because that
+ can take long enough for master process to kill us otherwise. */
+ master_service_init_finish(master_service);
+
+ main_init();
+ master_service_run(master_service, client_connected);
+ main_deinit();
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/replication/replicator/test-replicator-queue.c b/src/replication/replicator/test-replicator-queue.c
new file mode 100644
index 0000000..9ffa5ff
--- /dev/null
+++ b/src/replication/replicator/test-replicator-queue.c
@@ -0,0 +1,260 @@
+/* Copyright (c) 2022 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "test-common.h"
+#include "replicator-queue.h"
+
+#define TEST_REPLICATION_FULL_SYNC_INTERVAL 60
+#define TEST_REPLICATION_FAILURE_RESYNC_INTERVAL 10
+
+static void test_replicator_queue(void)
+{
+ struct replicator_queue *queue;
+ struct replicator_user *user1, *user2, *user3, *user4;
+ unsigned int next_secs;
+
+ test_begin("replicator queue");
+ queue = replicator_queue_init(TEST_REPLICATION_FULL_SYNC_INTERVAL,
+ TEST_REPLICATION_FAILURE_RESYNC_INTERVAL);
+ ioloop_time = time(NULL);
+
+ /* 1) Add users */
+
+ /* add the 1st user with priority=none */
+ user1 = replicator_queue_get(queue, "user1");
+ replicator_queue_update(queue, user1, REPLICATION_PRIORITY_NONE);
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_count(queue) == 1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* add the 2nd user with priority=none */
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_NONE);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_count(queue) == 2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* add the 3rd user with priority=none */
+ user3 = replicator_queue_get(queue, "user3");
+ replicator_queue_update(queue, user3, REPLICATION_PRIORITY_NONE);
+ replicator_queue_add(queue, user3);
+ test_assert(replicator_queue_count(queue) == 3);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* 2) User hasn't been synced yet, but priority is updated */
+
+ /* update the 2nd user's priority to low */
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_LOW);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* update the 1st user's priority to high */
+ user1 = replicator_queue_get(queue, "user1");
+ replicator_queue_update(queue, user1, REPLICATION_PRIORITY_HIGH);
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* update the 2nd user's priority to sync */
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_SYNC);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* 3) User hasn't been synced, and priority is being updated.
+ user1 was synced 1 second before user2. */
+ user1->last_fast_sync = ioloop_time;
+ user1->last_full_sync = ioloop_time;
+ user1->priority = REPLICATION_PRIORITY_NONE;
+ replicator_queue_add(queue, user1);
+ ioloop_time++;
+ user2->last_fast_sync = ioloop_time;
+ user2->last_full_sync = ioloop_time;
+ user2->priority = REPLICATION_PRIORITY_NONE;
+ replicator_queue_add(queue, user2);
+ ioloop_time++;
+ user3->last_fast_sync = ioloop_time;
+ user3->last_full_sync = ioloop_time;
+ user3->priority = REPLICATION_PRIORITY_NONE;
+ replicator_queue_add(queue, user3);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs > 0);
+
+ /* update the 2nd user's priority to low */
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_LOW);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* update the 1st user's priority to high */
+ user1 = replicator_queue_get(queue, "user1");
+ replicator_queue_update(queue, user1, REPLICATION_PRIORITY_HIGH);
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* update the 2nd user's priority to sync */
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_SYNC);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* 4) Test failed sync with a new user */
+ user1->priority = REPLICATION_PRIORITY_NONE;
+ replicator_queue_add(queue, user1);
+ user2->priority = REPLICATION_PRIORITY_NONE;
+ replicator_queue_add(queue, user2);
+
+ user4 = replicator_queue_get(queue, "user4");
+ user4->last_fast_sync = ioloop_time - 5;
+ user4->last_sync_failed = TRUE;
+ replicator_queue_add(queue, user4);
+
+ test_assert(replicator_queue_count(queue) == 4);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user4 &&
+ next_secs == TEST_REPLICATION_FAILURE_RESYNC_INTERVAL - 5);
+
+ /* low priority sync is prioritized over failed sync */
+ replicator_queue_update(queue, user1, REPLICATION_PRIORITY_LOW);
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* However, if the last failure was old enough it will be before
+ the low priority one. Test the edge case. */
+ user4->last_fast_sync = ioloop_time -
+ TEST_REPLICATION_FAILURE_RESYNC_INTERVAL -
+ (60*15) - 1;
+ replicator_queue_add(queue, user4);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user4 && next_secs == 0);
+ user4->last_fast_sync++;
+ replicator_queue_add(queue, user4);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+
+ /* 5) Test priority starvation */
+
+ /* high priority is normally prioritized over low priority */
+ i_assert(user1->priority == REPLICATION_PRIORITY_LOW);
+ user2 = replicator_queue_get(queue, "user2");
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_HIGH);
+ replicator_queue_add(queue, user2);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* if low priority is old enough, it gets prioritized over high */
+ user1->last_update = ioloop_time - (60*15) - 1;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+ user1->last_update++;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* similarly low priority eventually gets prioritized over sync
+ priority */
+ replicator_queue_update(queue, user2, REPLICATION_PRIORITY_SYNC);
+ replicator_queue_add(queue, user2);
+ user1->last_update = ioloop_time - (60*30) - 1;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+ user1->last_update++;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ /* likewise for none priority also */
+ user1->priority = REPLICATION_PRIORITY_NONE;
+ user1->last_update = ioloop_time;
+ user1->last_fast_sync = ioloop_time;
+ user1->last_full_sync = ioloop_time - (60*45) -
+ TEST_REPLICATION_FULL_SYNC_INTERVAL - 1;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user1 && next_secs == 0);
+ user1->last_full_sync++;
+ replicator_queue_add(queue, user1);
+ test_assert(replicator_queue_peek(queue, &next_secs) == user2 && next_secs == 0);
+
+ replicator_queue_deinit(&queue);
+ test_end();
+}
+
+static void test_replicator_queue_verify_drained(struct replicator_queue *queue)
+{
+ struct replicator_queue_iter *iter =
+ replicator_queue_iter_init(queue);
+ struct replicator_user *user;
+ while ((user = replicator_queue_iter_next(iter)) != NULL) {
+ i_assert(user->priority == REPLICATION_PRIORITY_NONE);
+ i_assert(user->last_sync_failed ||
+ ioloop_time - user->last_full_sync < TEST_REPLICATION_FULL_SYNC_INTERVAL);
+ }
+ replicator_queue_iter_deinit(&iter);
+}
+
+static void test_replicator_queue_drain(struct replicator_queue *queue)
+{
+ struct replicator_user *user;
+ unsigned int next_secs;
+ enum replication_priority prev_priority = REPLICATION_PRIORITY_SYNC;
+ time_t prev_sync = INT_MAX;
+
+ while ((user = replicator_queue_pop(queue, &next_secs)) != NULL) {
+ if (user->priority < prev_priority) {
+ prev_sync = INT_MAX;
+ } else {
+ test_assert(user->priority == prev_priority);
+ if (user->priority == REPLICATION_PRIORITY_NONE) {
+ test_assert(user->last_full_sync <= prev_sync);
+ prev_sync = user->last_full_sync;
+ } else {
+ test_assert(user->last_fast_sync <= prev_sync);
+ prev_sync = user->last_fast_sync;
+ }
+ }
+ user->priority = REPLICATION_PRIORITY_NONE;
+ user->last_fast_sync = user->last_full_sync = ioloop_time-1;
+ /* dsync runs here */
+ if (i_rand_limit(5) == 0)
+ user->last_sync_failed = TRUE;
+ else {
+ user->last_successful_sync = ioloop_time;
+ user->last_sync_failed = FALSE;
+ }
+ replicator_queue_push(queue, user);
+ }
+ test_replicator_queue_verify_drained(queue);
+}
+
+static void test_replicator_queue_random(void)
+{
+ struct replicator_queue *queue;
+ struct replicator_user *user;
+
+ test_begin("replicator queue random");
+ queue = replicator_queue_init(TEST_REPLICATION_FULL_SYNC_INTERVAL,
+ TEST_REPLICATION_FAILURE_RESYNC_INTERVAL);
+ /* fill some users */
+ ioloop_time = time(NULL);
+ for (unsigned int i = 0; i < 1000; i++) T_BEGIN {
+ enum replication_priority priority =
+ i_rand_minmax(REPLICATION_PRIORITY_NONE,
+ REPLICATION_PRIORITY_SYNC);
+ const char *username =
+ t_strdup_printf("test%u", i_rand_minmax(1, 200));
+ user = replicator_queue_get(queue, username);
+ replicator_queue_update(queue, user, priority);
+ replicator_queue_add(queue, user);
+ ioloop_time++;
+ } T_END;
+ for (unsigned int i = 0; i < 1000; i++) {
+ test_replicator_queue_drain(queue);
+ ioloop_time++;
+ }
+ replicator_queue_deinit(&queue);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_replicator_queue,
+ test_replicator_queue_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/stats/Makefile.am b/src/stats/Makefile.am
new file mode 100644
index 0000000..0d6f598
--- /dev/null
+++ b/src/stats/Makefile.am
@@ -0,0 +1,102 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = stats
+
+noinst_LTLIBRARIES = libstats_local.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-http \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-test \
+ $(BINARY_CFLAGS)
+
+stats_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(DOVECOT_SSL_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+stats_DEPENDENCIES = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT_DEPS)
+
+stats_services = \
+ stats-service-openmetrics.c
+
+stats_SOURCES = \
+ main.c
+
+libstats_local_la_SOURCES = \
+ client-reader.c \
+ client-writer.c \
+ client-http.c \
+ event-exporter-fmt.c \
+ event-exporter-fmt-json.c \
+ event-exporter-fmt-none.c \
+ event-exporter-fmt-tab-text.c \
+ event-exporter-transport-drop.c \
+ event-exporter-transport-http-post.c \
+ event-exporter-transport-log.c \
+ $(stats_services) \
+ stats-service.c \
+ stats-event-category.c \
+ stats-metrics.c \
+ stats-settings.c
+
+noinst_HEADERS = \
+ stats-common.h \
+ client-reader.h \
+ client-writer.h \
+ client-http.h\
+ event-exporter.h \
+ stats-service.h \
+ stats-service-private.h \
+ stats-event-category.h \
+ stats-metrics.h \
+ stats-settings.h \
+ test-stats-common.h
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT_DEPS)
+
+test_stats_metrics_SOURCES = test-stats-metrics.c test-stats-common.c
+test_stats_metrics_LDADD = $(test_libs)
+test_stats_metrics_DEPENDENCIES = $(test_deps)
+
+test_client_writer_SOURCES = test-client-writer.c test-stats-common.c
+test_client_writer_LDADD = $(test_libs)
+test_client_writer_DEPENDENCIES = $(test_deps)
+
+test_client_reader_SOURCES = test-client-reader.c test-stats-common.c
+test_client_reader_LDADD = $(test_libs)
+test_client_reader_DEPENDENCIES = $(test_deps)
+
+test_programs = test-stats-metrics test-client-writer test-client-reader
+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)
diff --git a/src/stats/Makefile.in b/src/stats/Makefile.in
new file mode 100644
index 0000000..b3bdf4d
--- /dev/null
+++ b/src/stats/Makefile.in
@@ -0,0 +1,1024 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = stats$(EXEEXT)
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/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 $(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-stats-metrics$(EXEEXT) test-client-writer$(EXEEXT) \
+ test-client-reader$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstats_local_la_LIBADD =
+am__objects_1 = stats-service-openmetrics.lo
+am_libstats_local_la_OBJECTS = client-reader.lo client-writer.lo \
+ client-http.lo event-exporter-fmt.lo \
+ event-exporter-fmt-json.lo event-exporter-fmt-none.lo \
+ event-exporter-fmt-tab-text.lo \
+ event-exporter-transport-drop.lo \
+ event-exporter-transport-http-post.lo \
+ event-exporter-transport-log.lo $(am__objects_1) \
+ stats-service.lo stats-event-category.lo stats-metrics.lo \
+ stats-settings.lo
+libstats_local_la_OBJECTS = $(am_libstats_local_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_stats_OBJECTS = main.$(OBJEXT)
+stats_OBJECTS = $(am_stats_OBJECTS)
+am__DEPENDENCIES_1 =
+am_test_client_reader_OBJECTS = test-client-reader.$(OBJEXT) \
+ test-stats-common.$(OBJEXT)
+test_client_reader_OBJECTS = $(am_test_client_reader_OBJECTS)
+am__DEPENDENCIES_2 = $(noinst_LTLIBRARIES) $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_1)
+am_test_client_writer_OBJECTS = test-client-writer.$(OBJEXT) \
+ test-stats-common.$(OBJEXT)
+test_client_writer_OBJECTS = $(am_test_client_writer_OBJECTS)
+am_test_stats_metrics_OBJECTS = test-stats-metrics.$(OBJEXT) \
+ test-stats-common.$(OBJEXT)
+test_stats_metrics_OBJECTS = $(am_test_stats_metrics_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)/client-http.Plo \
+ ./$(DEPDIR)/client-reader.Plo ./$(DEPDIR)/client-writer.Plo \
+ ./$(DEPDIR)/event-exporter-fmt-json.Plo \
+ ./$(DEPDIR)/event-exporter-fmt-none.Plo \
+ ./$(DEPDIR)/event-exporter-fmt-tab-text.Plo \
+ ./$(DEPDIR)/event-exporter-fmt.Plo \
+ ./$(DEPDIR)/event-exporter-transport-drop.Plo \
+ ./$(DEPDIR)/event-exporter-transport-http-post.Plo \
+ ./$(DEPDIR)/event-exporter-transport-log.Plo \
+ ./$(DEPDIR)/main.Po ./$(DEPDIR)/stats-event-category.Plo \
+ ./$(DEPDIR)/stats-metrics.Plo \
+ ./$(DEPDIR)/stats-service-openmetrics.Plo \
+ ./$(DEPDIR)/stats-service.Plo ./$(DEPDIR)/stats-settings.Plo \
+ ./$(DEPDIR)/test-client-reader.Po \
+ ./$(DEPDIR)/test-client-writer.Po \
+ ./$(DEPDIR)/test-stats-common.Po \
+ ./$(DEPDIR)/test-stats-metrics.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 = $(libstats_local_la_SOURCES) $(stats_SOURCES) \
+ $(test_client_reader_SOURCES) $(test_client_writer_SOURCES) \
+ $(test_stats_metrics_SOURCES)
+DIST_SOURCES = $(libstats_local_la_SOURCES) $(stats_SOURCES) \
+ $(test_client_reader_SOURCES) $(test_client_writer_SOURCES) \
+ $(test_stats_metrics_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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 = libstats_local.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-http \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-test \
+ $(BINARY_CFLAGS)
+
+stats_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
+ $(DOVECOT_SSL_LIBS) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+stats_DEPENDENCIES = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT_DEPS)
+
+stats_services = \
+ stats-service-openmetrics.c
+
+stats_SOURCES = \
+ main.c
+
+libstats_local_la_SOURCES = \
+ client-reader.c \
+ client-writer.c \
+ client-http.c \
+ event-exporter-fmt.c \
+ event-exporter-fmt-json.c \
+ event-exporter-fmt-none.c \
+ event-exporter-fmt-tab-text.c \
+ event-exporter-transport-drop.c \
+ event-exporter-transport-http-post.c \
+ event-exporter-transport-log.c \
+ $(stats_services) \
+ stats-service.c \
+ stats-event-category.c \
+ stats-metrics.c \
+ stats-settings.c
+
+noinst_HEADERS = \
+ stats-common.h \
+ client-reader.h \
+ client-writer.h \
+ client-http.h\
+ event-exporter.h \
+ stats-service.h \
+ stats-service-private.h \
+ stats-event-category.h \
+ stats-metrics.h \
+ stats-settings.h \
+ test-stats-common.h
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS) \
+ -lm
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT_DEPS)
+
+test_stats_metrics_SOURCES = test-stats-metrics.c test-stats-common.c
+test_stats_metrics_LDADD = $(test_libs)
+test_stats_metrics_DEPENDENCIES = $(test_deps)
+test_client_writer_SOURCES = test-client-writer.c test-stats-common.c
+test_client_writer_LDADD = $(test_libs)
+test_client_writer_DEPENDENCIES = $(test_deps)
+test_client_reader_SOURCES = test-client-reader.c test-stats-common.c
+test_client_reader_LDADD = $(test_libs)
+test_client_reader_DEPENDENCIES = $(test_deps)
+test_programs = test-stats-metrics test-client-writer test-client-reader
+LIBDOVECOT_TEST_DEPS = \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+LIBDOVECOT_TEST = \
+ $(LIBDOVECOT_TEST_DEPS) \
+ $(MODULE_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/stats/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/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-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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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}; \
+ }
+
+libstats_local.la: $(libstats_local_la_OBJECTS) $(libstats_local_la_DEPENDENCIES) $(EXTRA_libstats_local_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstats_local_la_OBJECTS) $(libstats_local_la_LIBADD) $(LIBS)
+
+stats$(EXEEXT): $(stats_OBJECTS) $(stats_DEPENDENCIES) $(EXTRA_stats_DEPENDENCIES)
+ @rm -f stats$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(stats_OBJECTS) $(stats_LDADD) $(LIBS)
+
+test-client-reader$(EXEEXT): $(test_client_reader_OBJECTS) $(test_client_reader_DEPENDENCIES) $(EXTRA_test_client_reader_DEPENDENCIES)
+ @rm -f test-client-reader$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_client_reader_OBJECTS) $(test_client_reader_LDADD) $(LIBS)
+
+test-client-writer$(EXEEXT): $(test_client_writer_OBJECTS) $(test_client_writer_DEPENDENCIES) $(EXTRA_test_client_writer_DEPENDENCIES)
+ @rm -f test-client-writer$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_client_writer_OBJECTS) $(test_client_writer_LDADD) $(LIBS)
+
+test-stats-metrics$(EXEEXT): $(test_stats_metrics_OBJECTS) $(test_stats_metrics_DEPENDENCIES) $(EXTRA_test_stats_metrics_DEPENDENCIES)
+ @rm -f test-stats-metrics$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_stats_metrics_OBJECTS) $(test_stats_metrics_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-http.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-reader.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-writer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-fmt-json.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-fmt-none.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-fmt-tab-text.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-fmt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-transport-drop.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-transport-http-post.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-exporter-transport-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-event-category.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-metrics.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-service-openmetrics.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-service.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-client-reader.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-client-writer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-stats-common.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-stats-metrics.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) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client-http.Plo
+ -rm -f ./$(DEPDIR)/client-reader.Plo
+ -rm -f ./$(DEPDIR)/client-writer.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-json.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-none.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-tab-text.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-drop.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-http-post.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-log.Plo
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/stats-event-category.Plo
+ -rm -f ./$(DEPDIR)/stats-metrics.Plo
+ -rm -f ./$(DEPDIR)/stats-service-openmetrics.Plo
+ -rm -f ./$(DEPDIR)/stats-service.Plo
+ -rm -f ./$(DEPDIR)/stats-settings.Plo
+ -rm -f ./$(DEPDIR)/test-client-reader.Po
+ -rm -f ./$(DEPDIR)/test-client-writer.Po
+ -rm -f ./$(DEPDIR)/test-stats-common.Po
+ -rm -f ./$(DEPDIR)/test-stats-metrics.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-pkglibexecPROGRAMS
+
+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)/client-http.Plo
+ -rm -f ./$(DEPDIR)/client-reader.Plo
+ -rm -f ./$(DEPDIR)/client-writer.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-json.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-none.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt-tab-text.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-fmt.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-drop.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-http-post.Plo
+ -rm -f ./$(DEPDIR)/event-exporter-transport-log.Plo
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/stats-event-category.Plo
+ -rm -f ./$(DEPDIR)/stats-metrics.Plo
+ -rm -f ./$(DEPDIR)/stats-service-openmetrics.Plo
+ -rm -f ./$(DEPDIR)/stats-service.Plo
+ -rm -f ./$(DEPDIR)/stats-settings.Plo
+ -rm -f ./$(DEPDIR)/test-client-reader.Po
+ -rm -f ./$(DEPDIR)/test-client-writer.Po
+ -rm -f ./$(DEPDIR)/test-stats-common.Po
+ -rm -f ./$(DEPDIR)/test-stats-metrics.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-pkglibexecPROGRAMS
+
+.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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/stats/client-http.c b/src/stats/client-http.c
new file mode 100644
index 0000000..1811917
--- /dev/null
+++ b/src/stats/client-http.c
@@ -0,0 +1,233 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "str.h"
+#include "array.h"
+#include "strescape.h"
+#include "connection.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "http-server.h"
+#include "http-url.h"
+#include "stats-metrics.h"
+#include "stats-service.h"
+#include "client-http.h"
+
+struct stats_http_client;
+
+struct stats_http_client {
+ struct http_server_connection *http_conn;
+};
+
+struct stats_http_resource {
+ pool_t pool;
+ const char *title;
+ struct http_server_resource *resource;
+
+ stats_http_resource_callback_t *callback;
+ void *context;
+};
+
+static struct http_server *stats_http_server;
+static ARRAY(struct stats_http_resource *) stats_http_resources;
+
+/*
+ * Request
+ */
+
+static void
+stats_http_server_handle_request(void *context ATTR_UNUSED,
+ struct http_server_request *http_sreq)
+{
+ http_server_request_fail(http_sreq, 404, "Path Not Found");
+}
+
+/*
+ * Connection
+ */
+
+static void
+stats_http_server_connection_destroy(void *context, const char *reason);
+
+static const struct http_server_callbacks stats_http_callbacks = {
+ .connection_destroy = stats_http_server_connection_destroy,
+ .handle_request = stats_http_server_handle_request
+};
+
+void client_http_create(struct master_service_connection *conn)
+{
+ struct stats_http_client *client;
+
+ client = i_new(struct stats_http_client, 1);
+
+ client->http_conn = http_server_connection_create(
+ stats_http_server, conn->fd, conn->fd, conn->ssl,
+ &stats_http_callbacks, client);
+}
+
+static void stats_http_client_destroy(struct stats_http_client *client)
+{
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+stats_http_server_connection_destroy(void *context,
+ const char *reason ATTR_UNUSED)
+{
+ struct stats_http_client *client = context;
+
+ if (client->http_conn == NULL) {
+ /* Already destroying client directly */
+ return;
+ }
+
+ /* HTTP connection is destroyed already now */
+ client->http_conn = NULL;
+
+ /* Destroy the connection itself */
+ stats_http_client_destroy(client);
+}
+
+/*
+ * Resources
+ */
+
+/* Registry */
+
+static void
+stats_http_resource_callback(struct stats_http_resource *res,
+ struct http_server_request *req,
+ const char *sub_path)
+{
+ res->callback(res->context, req, sub_path);
+}
+
+#undef stats_http_resource_add
+void stats_http_resource_add(const char *path, const char *title,
+ stats_http_resource_callback_t *callback,
+ void *context)
+{
+ struct stats_http_resource *res;
+ pool_t pool;
+
+ pool = pool_alloconly_create("stats http resource", 2048);
+ res = p_new(pool, struct stats_http_resource, 1);
+ res->pool = pool;
+ res->title = p_strdup(pool, title);
+ res->callback = callback;
+ res->context = context;
+
+ res->resource = http_server_resource_create(
+ stats_http_server, pool, stats_http_resource_callback, res);
+ http_server_resource_add_location(res->resource, path);
+
+ pool_unref(&pool);
+ array_append(&stats_http_resources, &res, 1);
+}
+
+/* Root */
+
+static void
+stats_http_resource_root_make_response(struct http_server_response *resp,
+ const struct http_request *hreq)
+{
+ struct stats_http_resource *res;
+ struct http_url url;
+ string_t *msg;
+
+ http_url_init_authority_from(&url, hreq->target.url);
+
+ msg = t_str_new(1024);
+
+ str_append(msg, "<!DOCTYPE html>\n");
+ str_append(msg, "<html lang=\"en\">\n");
+ str_append(msg, "\n");
+ str_append(msg, "<head>\n");
+ str_append(msg, "<meta charset=\"utf-8\">\n");
+ str_append(msg, "<title>Dovecot Stats</title>\n");
+ str_append(msg, "</head>\n");
+ str_append(msg, "\n");
+ str_append(msg, "<body>\n");
+
+ str_append(msg, "<h1>Dovecot Stats:</h1>\n");
+ str_append(msg, "<p><ul>\n");
+
+ array_foreach_elem(&stats_http_resources, res) {
+ if (res->title == NULL)
+ continue;
+
+ /* List the resource at its primary location. */
+ url.path = http_server_resource_get_path(res->resource);
+
+ str_append(msg, "<li><a href=\"");
+ str_append(msg, http_url_create(&url));
+ str_append(msg, "\">");
+ str_append(msg, res->title);
+ str_append(msg, "</a></li>\n");
+ }
+
+ str_append(msg, "</ul></p>\n");
+ str_append(msg, "</body>\n");
+ str_append(msg, "\n");
+ str_append(msg, "</html>\n");
+
+ http_server_response_set_payload_data(
+ resp, str_data(msg), str_len(msg));
+}
+
+static void
+stats_http_resource_root_request(void *context ATTR_UNUSED,
+ struct http_server_request *req,
+ const char *sub_path)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ struct http_server_response *resp;
+
+ if (strcmp(hreq->method, "OPTIONS") == 0) {
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_add_header(resp, "Allow", "GET");
+ http_server_response_submit(resp);
+ return;
+ }
+ if (strcmp(hreq->method, "GET") != 0) {
+ http_server_request_fail_bad_method(req, "GET");
+ return;
+ }
+ if (*sub_path != '\0') {
+ http_server_request_fail(req, 404, "Not Found");
+ return;
+ }
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_add_header(resp, "Content-Type",
+ "text/html; charset=utf-8");
+
+ stats_http_resource_root_make_response(resp, hreq);
+
+ http_server_response_submit(resp);
+}
+
+/*
+ * Server
+ */
+
+void client_http_init(const struct stats_settings *set)
+{
+ struct http_server_settings http_set = {
+ .rawlog_dir = set->stats_http_rawlog_dir,
+ };
+
+ i_array_init(&stats_http_resources, 8);
+
+ stats_http_server = http_server_init(&http_set);
+ stats_http_resource_add("/", NULL,
+ stats_http_resource_root_request, NULL);
+}
+
+void client_http_deinit(void)
+{
+ http_server_deinit(&stats_http_server);
+ array_free(&stats_http_resources);
+}
diff --git a/src/stats/client-http.h b/src/stats/client-http.h
new file mode 100644
index 0000000..4251ce3
--- /dev/null
+++ b/src/stats/client-http.h
@@ -0,0 +1,28 @@
+#ifndef CLIENT_HTTP_H
+#define CLIENT_HTTP_H
+
+struct master_service_connection;
+struct http_server_request;
+
+typedef void
+(stats_http_resource_callback_t)(void *context,
+ struct http_server_request *req,
+ const char *sub_path);
+
+void client_http_create(struct master_service_connection *conn);
+
+void stats_http_resource_add(const char *path, const char *title,
+ stats_http_resource_callback_t *callback,
+ void *context);
+#define stats_http_resource_add(path, title, callback, context) \
+ stats_http_resource_add(path, title, \
+ (stats_http_resource_callback_t *)callback, \
+ (TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ typeof(context), struct http_server_request *req, \
+ const char *sub_path))))
+
+void client_http_init(const struct stats_settings *set);
+void client_http_deinit(void);
+
+#endif
diff --git a/src/stats/client-reader.c b/src/stats/client-reader.c
new file mode 100644
index 0000000..e944001
--- /dev/null
+++ b/src/stats/client-reader.c
@@ -0,0 +1,253 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "array.h"
+#include "str.h"
+#include "stats-dist.h"
+#include "strescape.h"
+#include "connection.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "stats-metrics.h"
+#include "stats-settings.h"
+#include "client-reader.h"
+#include "client-writer.h"
+
+struct reader_client {
+ struct connection conn;
+};
+
+static struct connection_list *reader_clients = NULL;
+
+void client_reader_create(int fd)
+{
+ struct reader_client *client;
+
+ client = i_new(struct reader_client, 1);
+ connection_init_server(reader_clients, &client->conn,
+ "stats-reader", fd, fd);
+}
+
+static void reader_client_destroy(struct connection *conn)
+{
+ connection_deinit(conn);
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void reader_client_dump_stats(string_t *str, struct stats_dist *stats,
+ const char *const *fields)
+{
+ for (unsigned int i = 0; fields[i] != NULL; i++) {
+ const char *field = fields[i];
+
+ str_append_c(str, '\t');
+ if (strcmp(field, "count") == 0)
+ str_printfa(str, "%u", stats_dist_get_count(stats));
+ else if (strcmp(field, "sum") == 0)
+ str_printfa(str, "%"PRIu64, stats_dist_get_sum(stats));
+ else if (strcmp(field, "min") == 0)
+ str_printfa(str, "%"PRIu64, stats_dist_get_min(stats));
+ else if (strcmp(field, "max") == 0)
+ str_printfa(str, "%"PRIu64, stats_dist_get_max(stats));
+ else if (strcmp(field, "avg") == 0)
+ str_printfa(str, "%.02f", stats_dist_get_avg(stats));
+ else if (strcmp(field, "median") == 0)
+ str_printfa(str, "%"PRIu64, stats_dist_get_median(stats));
+ else if (strcmp(field, "variance") == 0)
+ str_printfa(str, "%.02f", stats_dist_get_variance(stats));
+ else if (field[0] == '%') {
+ str_printfa(str, "%"PRIu64,
+ stats_dist_get_percentile(stats, strtod(field+1, NULL)/100.0));
+ } else {
+ /* return unknown fields as empty */
+ }
+ }
+}
+
+static void reader_client_dump_metric(string_t *str, const struct metric *metric,
+ const char *const *fields)
+{
+ reader_client_dump_stats(str, metric->duration_stats, fields);
+ for (unsigned int i = 0; i < metric->fields_count; i++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, metric->fields[i].field_key);
+ reader_client_dump_stats(str, metric->fields[i].stats, fields);
+ }
+ str_append_c(str, '\n');
+}
+
+static void
+reader_client_append_sub_name(string_t *str, const char *sub_name)
+{
+ for (; *sub_name != '\0'; sub_name++) {
+ switch (*sub_name) {
+ case '\t':
+ case '\n':
+ case '\r':
+ case ' ':
+ str_append_c(str, '_');
+ break;
+ default:
+ str_append_c(str, *sub_name);
+ }
+ }
+}
+
+static void
+reader_client_dump_sub_metrics(struct ostream *output, const struct metric *metric,
+ const char *sub_name, const char *const *fields)
+{
+ size_t root_pos, name_pos;
+ struct metric *const *sub_metrics;
+ if (!array_is_created(&metric->sub_metrics))
+ return;
+ string_t *str = t_str_new(128);
+ reader_client_append_sub_name(str, sub_name);
+ str_append_c(str, '_');
+ root_pos = str->used;
+
+ array_foreach(&metric->sub_metrics, sub_metrics) {
+ str_truncate(str, root_pos);
+ reader_client_append_sub_name(str, (*sub_metrics)->sub_name);
+ name_pos = str->used;
+ reader_client_dump_metric(str, *sub_metrics, fields);
+ o_stream_nsend(output, str_data(str), str_len(str));
+ str_truncate(str, name_pos);
+ reader_client_dump_sub_metrics(output, *sub_metrics,
+ str_c(str), fields);
+ }
+}
+
+static int
+reader_client_input_dump(struct reader_client *client, const char *const *args)
+{
+ struct stats_metrics_iter *iter;
+ const struct metric *metric;
+
+ o_stream_cork(client->conn.output);
+ iter = stats_metrics_iterate_init(stats_metrics);
+ while ((metric = stats_metrics_iterate(iter)) != NULL) T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_append_tabescaped(str, metric->name);
+ reader_client_dump_metric(str, metric, args);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ reader_client_dump_sub_metrics(client->conn.output, metric,
+ metric->name, args);
+ } T_END;
+ o_stream_nsend(client->conn.output, "\n", 1);
+ stats_metrics_iterate_deinit(&iter);
+ o_stream_uncork(client->conn.output);
+ return 1;
+}
+
+static int
+reader_client_input_dump_reset(struct reader_client *client,
+ const char *const *args)
+{
+ (void)reader_client_input_dump(client, args);
+ stats_metrics_reset(stats_metrics);
+ return 1;
+}
+
+static int
+reader_client_input_metrics_add(struct reader_client *client,
+ const char *const *args)
+{
+ const char *error;
+
+ if (str_array_length(args) < 7) {
+ e_error(client->conn.event, "METRICS-ADD: Not enough parameters");
+ return -1;
+ }
+
+ struct stats_metric_settings set = {
+ .metric_name = args[0],
+ .description = args[1],
+ .fields = args[2],
+ .group_by = args[3],
+ .filter = args[4],
+ .exporter = args[5],
+ .exporter_include = args[6],
+ };
+ o_stream_cork(client->conn.output);
+ if (stats_metrics_add_dynamic(stats_metrics, &set, &error)) {
+ client_writer_update_connections();
+ o_stream_nsend(client->conn.output, "+", 1);
+ } else {
+ o_stream_nsend(client->conn.output, "-", 1);
+ o_stream_nsend_str(client->conn.output, "METRICS-ADD: ");
+ o_stream_nsend_str(client->conn.output, error);
+ }
+ o_stream_nsend(client->conn.output, "\n", 1);
+ o_stream_uncork(client->conn.output);
+ return 1;
+}
+
+static int
+reader_client_input_metrics_remove(struct reader_client *client,
+ const char *const *args)
+{
+ if (str_array_length(args) < 1) {
+ e_error(client->conn.event, "METRICS-REMOVE: Not enough parameters");
+ return -1;
+ }
+
+ if (stats_metrics_remove_dynamic(stats_metrics, args[0])) {
+ client_writer_update_connections();
+ o_stream_nsend(client->conn.output, "+\n", 2);
+ } else {
+ o_stream_nsend_str(client->conn.output,
+ t_strdup_printf("-metrics '%s' not found\n", args[0]));
+ }
+ return 1;
+}
+
+static int
+reader_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct reader_client *client = (struct reader_client *)conn;
+ const char *cmd = args[0];
+
+ if (cmd == NULL) {
+ i_error("Client sent empty line");
+ return 1;
+ }
+ args++;
+ if (strcmp(cmd, "DUMP") == 0)
+ return reader_client_input_dump(client, args);
+ else if (strcmp(cmd, "METRICS-ADD") == 0)
+ return reader_client_input_metrics_add(client, args);
+ else if (strcmp(cmd, "METRICS-REMOVE") == 0)
+ return reader_client_input_metrics_remove(client, args);
+ else if (strcmp(cmd, "DUMP-RESET") == 0)
+ return reader_client_input_dump_reset(client, args);
+ return 1;
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-reader-client",
+ .service_name_out = "stats-reader-server",
+ .major_version = 2,
+ .minor_version = 0,
+
+ .input_max_size = 1024,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = reader_client_destroy,
+ .input_args = reader_client_input_args,
+};
+
+void client_readers_init(void)
+{
+ reader_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void client_readers_deinit(void)
+{
+ connection_list_deinit(&reader_clients);
+}
diff --git a/src/stats/client-reader.h b/src/stats/client-reader.h
new file mode 100644
index 0000000..2acab19
--- /dev/null
+++ b/src/stats/client-reader.h
@@ -0,0 +1,11 @@
+#ifndef CLIENT_READER_H
+#define CLIENT_READER_H
+
+struct stats_metrics;
+
+void client_reader_create(int fd);
+
+void client_readers_init(void);
+void client_readers_deinit(void);
+
+#endif
diff --git a/src/stats/client-writer.c b/src/stats/client-writer.c
new file mode 100644
index 0000000..40772cf
--- /dev/null
+++ b/src/stats/client-writer.c
@@ -0,0 +1,373 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "array.h"
+#include "llist.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "ostream.h"
+#include "connection.h"
+#include "master-service.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+#include "stats-settings.h"
+#include "client-writer.h"
+
+#define STATS_UPDATE_CLIENTS_DELAY_MSECS 1000
+
+struct stats_event {
+ struct stats_event *prev, *next;
+
+ uint64_t id;
+ struct event *event;
+};
+
+struct writer_client {
+ struct connection conn;
+
+ struct stats_event *events;
+ HASH_TABLE(struct stats_event *, struct stats_event *) events_hash;
+};
+
+static struct timeout *to_update_clients;
+static struct connection_list *writer_clients = NULL;
+
+static void client_writer_send_handshake(struct writer_client *client)
+{
+ string_t *filter = t_str_new(128);
+ string_t *str = t_str_new(128);
+
+ event_filter_export(stats_metrics_get_event_filter(stats_metrics), filter);
+
+ str_append(str, "FILTER\t");
+ str_append_tabescaped(str, str_c(filter));
+ str_append_c(str, '\n');
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static unsigned int stats_event_hash(const struct stats_event *event)
+{
+ return (unsigned int)event->id;
+}
+
+static int stats_event_cmp(const struct stats_event *event1,
+ const struct stats_event *event2)
+{
+ return event1->id == event2->id ? 0 : 1;
+}
+
+void client_writer_create(int fd)
+{
+ struct writer_client *client;
+
+ client = i_new(struct writer_client, 1);
+ hash_table_create(&client->events_hash, default_pool, 0,
+ stats_event_hash, stats_event_cmp);
+
+ connection_init_server(writer_clients, &client->conn,
+ "stats", fd, fd);
+ client_writer_send_handshake(client);
+}
+
+static void writer_client_destroy(struct connection *conn)
+{
+ struct writer_client *client = (struct writer_client *)conn;
+ struct stats_event *event, *next;
+
+ for (event = client->events; event != NULL; event = next) {
+ next = event->next;
+ event_unref(&event->event);
+ i_free(event);
+ }
+ hash_table_destroy(&client->events_hash);
+
+ connection_deinit(conn);
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static struct stats_event *
+writer_client_find_event(struct writer_client *client, uint64_t event_id)
+{
+ struct stats_event lookup_event = { .id = event_id };
+ return hash_table_lookup(client->events_hash, &lookup_event);
+}
+
+static bool
+writer_client_run_event(struct writer_client *client,
+ uint64_t parent_event_id, const char *const *args,
+ struct event **event_r, const char **error_r)
+{
+ struct event *parent_event;
+ unsigned int log_type;
+
+ if (parent_event_id == 0)
+ parent_event = NULL;
+ else {
+ struct stats_event *stats_parent_event =
+ writer_client_find_event(client, parent_event_id);
+ if (stats_parent_event == NULL) {
+ *error_r = "Unknown parent event ID";
+ return FALSE;
+ }
+ parent_event = stats_parent_event->event;
+ }
+ if (args[0] == NULL || str_to_uint(args[0], &log_type) < 0 ||
+ log_type >= LOG_TYPE_COUNT) {
+ *error_r = "Invalid log type";
+ return FALSE;
+ }
+ const struct failure_context ctx = {
+ .type = (enum log_type)log_type
+ };
+ args++;
+
+ struct event *event = event_create(parent_event);
+ if (!event_import_unescaped(event, args, error_r)) {
+ event_unref(&event);
+ return FALSE;
+ }
+ stats_metrics_event(stats_metrics, event, &ctx);
+ *event_r = event;
+ return TRUE;
+}
+
+static bool
+writer_client_input_event(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct event *event, *global_event = NULL;
+ uint64_t parent_event_id, global_event_id;
+ bool ret;
+
+ if (args[1] == NULL || str_to_uint64(args[0], &global_event_id) < 0) {
+ *error_r = "Invalid global event ID";
+ return FALSE;
+ }
+ if (args[1] == NULL || str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid parent ID";
+ return FALSE;
+ }
+
+ if (global_event_id != 0) {
+ struct stats_event *stats_global_event =
+ writer_client_find_event(client, global_event_id);
+ if (stats_global_event == NULL) {
+ *error_r = "Unknown global event ID";
+ return FALSE;
+ }
+ global_event = stats_global_event->event;
+ event_push_global(global_event);
+ }
+
+ ret = writer_client_run_event(client, parent_event_id, args+2,
+ &event, error_r);
+ if (global_event != NULL)
+ event_pop_global(global_event);
+ if (!ret)
+ return FALSE;
+ event_unref(&event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_event_begin(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct event *event;
+ struct stats_event *stats_event;
+ uint64_t event_id, parent_event_id;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ str_to_uint64(args[0], &event_id) < 0 ||
+ str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid event IDs";
+ return FALSE;
+ }
+ if (writer_client_find_event(client, event_id) != NULL) {
+ *error_r = "Duplicate event ID";
+ return FALSE;
+ }
+ if (!writer_client_run_event(client, parent_event_id, args+2, &event, error_r))
+ return FALSE;
+
+ stats_event = i_new(struct stats_event, 1);
+ stats_event->id = event_id;
+ stats_event->event = event;
+ DLLIST_PREPEND(&client->events, stats_event);
+ hash_table_insert(client->events_hash, stats_event, stats_event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_event_update(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct stats_event *stats_event, *parent_stats_event;
+ struct event *parent_event;
+ uint64_t event_id, parent_event_id;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ str_to_uint64(args[0], &event_id) < 0 ||
+ str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid event IDs";
+ return FALSE;
+ }
+ stats_event = writer_client_find_event(client, event_id);
+ if (stats_event == NULL) {
+ *error_r = "Unknown event ID";
+ return FALSE;
+ }
+ parent_stats_event = parent_event_id == 0 ? NULL :
+ writer_client_find_event(client, parent_event_id);
+ parent_event = parent_stats_event == NULL ? NULL :
+ parent_stats_event->event;
+ if (stats_event->event->parent != parent_event) {
+ *error_r = "Event unexpectedly changed parent";
+ return FALSE;
+ }
+ return event_import_unescaped(stats_event->event, args+2, error_r);
+}
+
+static bool
+writer_client_input_event_end(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct stats_event *stats_event;
+ uint64_t event_id;
+
+ if (args[0] == NULL || str_to_uint64(args[0], &event_id) < 0) {
+ *error_r = "Invalid event ID";
+ return FALSE;
+ }
+ stats_event = writer_client_find_event(client, event_id);
+ if (stats_event == NULL) {
+ *error_r = "Unknown event ID";
+ return FALSE;
+ }
+
+ DLLIST_REMOVE(&client->events, stats_event);
+ hash_table_remove(client->events_hash, stats_event);
+ event_unref(&stats_event->event);
+ i_free(stats_event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_category(struct writer_client *client ATTR_UNUSED,
+ const char *const *args, const char **error_r)
+{
+ struct event_category *category, *parent;
+
+ if (args[0] == NULL) {
+ *error_r = "Missing category name";
+ return FALSE;
+ }
+ if (args[1] == NULL)
+ parent = NULL;
+ else if ((parent = event_category_find_registered(args[1])) == NULL) {
+ *error_r = "Unknown parent category";
+ return FALSE;
+ }
+
+ category = event_category_find_registered(args[0]);
+ if (category == NULL) {
+ /* new category - create */
+ stats_event_category_register(args[0], parent);
+ } else if (category->parent != parent) {
+ *error_r = t_strdup_printf(
+ "Category parent '%s' changed to '%s'",
+ category->parent == NULL ? "" : category->parent->name,
+ parent == NULL ? "" : parent->name);
+ return FALSE;
+ } else {
+ /* duplicate - ignore */
+ return TRUE;
+ }
+ return TRUE;
+}
+
+static int
+writer_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct writer_client *client = (struct writer_client *)conn;
+ const char *error, *cmd = args[0];
+ bool ret;
+
+ if (cmd == NULL) {
+ i_error("Client sent empty line");
+ return 1;
+ }
+ if (strcmp(cmd, "EVENT") == 0)
+ ret = writer_client_input_event(client, args+1, &error);
+ else if (strcmp(cmd, "BEGIN") == 0)
+ ret = writer_client_input_event_begin(client, args+1, &error);
+ else if (strcmp(cmd, "UPDATE") == 0)
+ ret = writer_client_input_event_update(client, args+1, &error);
+ else if (strcmp(cmd, "END") == 0)
+ ret = writer_client_input_event_end(client, args+1, &error);
+ else if (strcmp(cmd, "CATEGORY") == 0)
+ ret = writer_client_input_category(client, args+1, &error);
+ else {
+ error = "Unknown command";
+ ret = FALSE;
+ }
+ if (!ret) {
+ i_error("Client sent invalid input for %s: %s (input: %s)",
+ cmd, error, t_strarray_join(args, "\t"));
+ return -1;
+ }
+ return 1;
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-client",
+ .service_name_out = "stats-server",
+ .major_version = 4,
+ .minor_version = 0,
+
+ .input_max_size = 1024*128, /* "big enough" */
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = writer_client_destroy,
+ .input_args = writer_client_input_args,
+};
+
+static void
+client_writer_update_connections_internal(void *context ATTR_UNUSED)
+{
+ struct connection *conn;
+ for (conn = writer_clients->connections; conn != NULL; conn = conn->next) {
+ struct writer_client *client =
+ container_of(conn, struct writer_client, conn);
+ client_writer_send_handshake(client);
+ }
+ timeout_remove(&to_update_clients);
+}
+
+void client_writer_update_connections(void)
+{
+ if (to_update_clients != NULL)
+ return;
+ to_update_clients = timeout_add(STATS_UPDATE_CLIENTS_DELAY_MSECS,
+ client_writer_update_connections_internal,
+ NULL);
+}
+
+void client_writers_init(void)
+{
+ writer_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void client_writers_deinit(void)
+{
+ timeout_remove(&to_update_clients);
+ connection_list_deinit(&writer_clients);
+}
diff --git a/src/stats/client-writer.h b/src/stats/client-writer.h
new file mode 100644
index 0000000..a88de38
--- /dev/null
+++ b/src/stats/client-writer.h
@@ -0,0 +1,13 @@
+#ifndef CLIENT_WRITER_H
+#define CLIENT_WRITER_H
+
+struct stats_metrics;
+
+void client_writer_create(int fd);
+
+void client_writer_update_connections(void);
+
+void client_writers_init(void);
+void client_writers_deinit(void);
+
+#endif
diff --git a/src/stats/event-exporter-fmt-json.c b/src/stats/event-exporter-fmt-json.c
new file mode 100644
index 0000000..25c6116
--- /dev/null
+++ b/src/stats/event-exporter-fmt-json.c
@@ -0,0 +1,249 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "lib-event-private.h"
+#include "event-exporter.h"
+#include "str.h"
+#include "json-parser.h"
+#include "hostpid.h"
+
+static void append_str(string_t *dest, const char *str)
+{
+ str_append_c(dest, '"');
+ json_append_escaped(dest, str);
+ str_append_c(dest, '"');
+}
+
+static void append_str_max_len(string_t *dest, const char *str,
+ const struct metric_export_info *info)
+{
+ str_append_c(dest, '"');
+ if (info->exporter->format_max_field_len == 0)
+ json_append_escaped(dest, str);
+ else {
+ size_t len = strlen(str);
+ json_append_escaped_data(dest, (const unsigned char *)str,
+ I_MIN(len, info->exporter->format_max_field_len));
+ if (len > info->exporter->format_max_field_len)
+ str_append(dest, "...");
+ }
+ str_append_c(dest, '"');
+}
+
+static void
+append_strlist(string_t *dest, const ARRAY_TYPE(const_string) *strlist,
+ const struct metric_export_info *info)
+{
+ const char *value;
+ bool first = TRUE;
+
+ str_append_c(dest, '[');
+ array_foreach_elem(strlist, value) {
+ if (first)
+ first = FALSE;
+ else
+ str_append_c(dest, ',');
+ append_str_max_len(dest, value, info);
+ }
+ str_append_c(dest, ']');
+}
+
+static void append_int(string_t *dest, intmax_t val)
+{
+ str_printfa(dest, "%jd", val);
+}
+
+static void append_time(string_t *dest, const struct timeval *time,
+ enum event_exporter_time_fmt fmt)
+{
+ switch (fmt) {
+ case EVENT_EXPORTER_TIME_FMT_NATIVE:
+ i_panic("JSON does not have a native date/time type");
+ case EVENT_EXPORTER_TIME_FMT_UNIX:
+ event_export_helper_fmt_unix_time(dest, time);
+ break;
+ case EVENT_EXPORTER_TIME_FMT_RFC3339:
+ str_append_c(dest, '"');
+ event_export_helper_fmt_rfc3339_time(dest, time);
+ str_append_c(dest, '"');
+ break;
+ }
+}
+
+static void append_field_value(string_t *dest, const struct event_field *field,
+ const struct metric_export_info *info)
+{
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ append_str_max_len(dest, field->value.str, info);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ append_int(dest, field->value.intmax);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ append_time(dest, &field->value.timeval,
+ info->exporter->time_format);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ append_strlist(dest, &field->value.strlist, info);
+ break;
+ }
+}
+
+static void json_export_name(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_NAME) == 0)
+ return;
+
+ append_str(dest, "event");
+ str_append_c(dest, ':');
+ append_str(dest, event->sending_name);
+ str_append_c(dest, ',');
+}
+
+static void json_export_hostname(string_t *dest,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_HOSTNAME) == 0)
+ return;
+
+ append_str(dest, "hostname");
+ str_append_c(dest, ':');
+ append_str(dest, my_hostname);
+ str_append_c(dest, ',');
+}
+
+static void json_export_timestamps(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_TIMESTAMPS) == 0)
+ return;
+
+ append_str(dest, "start_time");
+ str_append_c(dest, ':');
+ append_time(dest, &event->tv_created, info->exporter->time_format);
+ str_append_c(dest, ',');
+
+ append_str(dest, "end_time");
+ str_append_c(dest, ':');
+ append_time(dest, &ioloop_timeval, info->exporter->time_format);
+ str_append_c(dest, ',');
+}
+
+static void json_export_categories(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ struct event_category *const *cats;
+ unsigned int count;
+
+ if ((info->include & EVENT_EXPORTER_INCL_CATEGORIES) == 0)
+ return;
+
+ append_str(dest, "categories");
+ str_append(dest, ":[");
+
+ cats = event_get_categories(event, &count);
+ event_export_helper_fmt_categories(dest, cats, count,
+ append_str, ",");
+
+ str_append(dest, "],");
+}
+
+static void json_export_fields(string_t *dest, struct event *event,
+ const struct metric_export_info *info,
+ const unsigned int fields_count,
+ const struct metric_field *fields)
+{
+ bool appended = FALSE;
+
+ if ((info->include & EVENT_EXPORTER_INCL_FIELDS) == 0)
+ return;
+
+ append_str(dest, "fields");
+ str_append(dest, ":{");
+
+ if (fields_count == 0) {
+ /* include all fields */
+ const struct event_field *fields;
+ unsigned int count;
+
+ fields = event_get_fields(event, &count);
+
+ for (unsigned int i = 0; i < count; i++) {
+ const struct event_field *field = &fields[i];
+
+ append_str(dest, field->key);
+ str_append_c(dest, ':');
+ append_field_value(dest, field, info);
+ str_append_c(dest, ',');
+
+ appended = TRUE;
+ }
+ } else {
+ for (unsigned int i = 0; i < fields_count; i++) {
+ const char *name = fields[i].field_key;
+ const struct event_field *field;
+
+ field = event_find_field_recursive(event, name);
+ if (field == NULL)
+ continue; /* doesn't exist, skip it */
+
+ append_str(dest, name);
+ str_append_c(dest, ':');
+ append_field_value(dest, field, info);
+ str_append_c(dest, ',');
+
+ appended = TRUE;
+ }
+ }
+
+ /* remove trailing comma */
+ if (appended)
+ str_truncate(dest, str_len(dest) - 1);
+
+ str_append(dest, "},");
+}
+
+/*
+ * Serialize the event as:
+ *
+ * {
+ * "name": <event name>,
+ * "hostname": <hostname>,
+ * "start_time": <event creation timestamp>,
+ * "end_time": <event export timestamp>,
+ * "categories": [ <categories>, ... ],
+ * "fields": {
+ * <name>: <value>,
+ * ...
+ * }
+ * }
+ *
+ */
+void event_export_fmt_json(const struct metric *metric,
+ struct event *event, buffer_t *dest)
+{
+ const struct metric_export_info *info = &metric->export_info;
+
+ if (info->include == EVENT_EXPORTER_INCL_NONE) {
+ str_append(dest, "{}");
+ return;
+ }
+
+ str_append_c(dest, '{');
+
+ json_export_name(dest, event, info);
+ json_export_hostname(dest, info);
+ json_export_timestamps(dest, event, info);
+ json_export_categories(dest, event, info);
+ json_export_fields(dest, event, info, metric->fields_count,
+ metric->fields);
+
+ /* remove trailing comma */
+ str_truncate(dest, str_len(dest) - 1);
+
+ str_append_c(dest, '}');
+}
diff --git a/src/stats/event-exporter-fmt-none.c b/src/stats/event-exporter-fmt-none.c
new file mode 100644
index 0000000..cd052c2
--- /dev/null
+++ b/src/stats/event-exporter-fmt-none.c
@@ -0,0 +1,12 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "event-exporter.h"
+
+void event_export_fmt_none(const struct metric *metric ATTR_UNUSED,
+ struct event *event ATTR_UNUSED,
+ buffer_t *dest ATTR_UNUSED)
+{
+ /* nothing to do */
+}
diff --git a/src/stats/event-exporter-fmt-tab-text.c b/src/stats/event-exporter-fmt-tab-text.c
new file mode 100644
index 0000000..49a065b
--- /dev/null
+++ b/src/stats/event-exporter-fmt-tab-text.c
@@ -0,0 +1,212 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "lib-event-private.h"
+#include "event-exporter.h"
+#include "str.h"
+#include "strescape.h"
+#include "hostpid.h"
+
+static void append_strlist(string_t *dest, const ARRAY_TYPE(const_string) *strlist)
+{
+ string_t *str = t_str_new(64);
+ const char *value;
+ bool first = TRUE;
+
+ /* append the strings first escaped into a temporary string */
+ array_foreach_elem(strlist, value) {
+ if (first)
+ first = FALSE;
+ else
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, value);
+ }
+ /* append the temporary string (double-)escaped as the value */
+ str_append_tabescaped(dest, str_c(str));
+}
+
+static void append_int(string_t *dest, intmax_t val)
+{
+ str_printfa(dest, "%jd", val);
+}
+
+static void append_time(string_t *dest, const struct timeval *time,
+ enum event_exporter_time_fmt fmt)
+{
+ switch (fmt) {
+ case EVENT_EXPORTER_TIME_FMT_NATIVE:
+ i_panic("tab-text format does not have a native date/time type");
+ case EVENT_EXPORTER_TIME_FMT_UNIX:
+ event_export_helper_fmt_unix_time(dest, time);
+ break;
+ case EVENT_EXPORTER_TIME_FMT_RFC3339:
+ event_export_helper_fmt_rfc3339_time(dest, time);
+ break;
+ }
+}
+
+static void append_field_str(string_t *dest, const char *str,
+ const struct metric_export_info *info)
+{
+ if (info->exporter->format_max_field_len == 0)
+ str_append_tabescaped(dest, str);
+ else {
+ size_t len = strlen(str);
+ str_append_tabescaped_n(dest, (const unsigned char *)str,
+ I_MIN(len, info->exporter->format_max_field_len));
+ if (len > info->exporter->format_max_field_len)
+ str_append(dest, "...");
+ }
+}
+
+static void append_field_value(string_t *dest, const struct event_field *field,
+ const struct metric_export_info *info)
+{
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ append_field_str(dest, field->value.str, info);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ append_int(dest, field->value.intmax);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ append_time(dest, &field->value.timeval,
+ info->exporter->time_format);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ append_strlist(dest, &field->value.strlist);
+ break;
+ }
+}
+
+static void tabtext_export_name(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_NAME) == 0)
+ return;
+
+ str_append(dest, "event:");
+ str_append_tabescaped(dest, event->sending_name);
+ str_append_c(dest, '\t');
+}
+
+static void tabtext_export_hostname(string_t *dest,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_HOSTNAME) == 0)
+ return;
+
+ str_append(dest, "hostname:");
+ str_append_tabescaped(dest, my_hostname);
+ str_append_c(dest, '\t');
+}
+
+static void tabtext_export_timestamps(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_TIMESTAMPS) == 0)
+ return;
+
+ str_append(dest, "start_time:");
+ append_time(dest, &event->tv_created, info->exporter->time_format);
+ str_append(dest, "\tend_time:");
+ append_time(dest, &ioloop_timeval, info->exporter->time_format);
+ str_append_c(dest, '\t');
+}
+
+static void append_category(string_t *dest, const char *cat)
+{
+ str_append(dest, "category:");
+ str_append_tabescaped(dest, cat);
+}
+
+static void tabtext_export_categories(string_t *dest, struct event *event,
+ const struct metric_export_info *info)
+{
+ struct event_category *const *cats;
+ unsigned int count;
+
+ if ((info->include & EVENT_EXPORTER_INCL_CATEGORIES) == 0)
+ return;
+
+ cats = event_get_categories(event, &count);
+ event_export_helper_fmt_categories(dest, cats, count,
+ append_category, "\t");
+
+ str_append_c(dest, '\t'); /* extra \t to have something to remove later */
+}
+
+static void tabtext_export_fields(string_t *dest, struct event *event,
+ const struct metric_export_info *info,
+ const unsigned int fields_count,
+ const struct metric_field *fields)
+{
+ if ((info->include & EVENT_EXPORTER_INCL_FIELDS) == 0)
+ return;
+
+ if (fields_count == 0) {
+ /* include all fields */
+ const struct event_field *fields;
+ unsigned int count;
+
+ fields = event_get_fields(event, &count);
+
+ for (unsigned int i = 0; i < count; i++) {
+ const struct event_field *field = &fields[i];
+
+ str_append(dest, "field:");
+ str_append_tabescaped(dest, field->key);
+ str_append_c(dest, '=');
+ append_field_value(dest, field, info);
+ str_append_c(dest, '\t');
+ }
+ } else {
+ for (unsigned int i = 0; i < fields_count; i++) {
+ const char *name = fields[i].field_key;
+ const struct event_field *field;
+
+ field = event_find_field_recursive(event, name);
+ if (field == NULL)
+ continue; /* doesn't exist, skip it */
+
+ str_append(dest, "field:");
+ str_append_tabescaped(dest, name);
+ str_append_c(dest, '=');
+ append_field_value(dest, field, info);
+ str_append_c(dest, '\t');
+ }
+ }
+}
+
+/*
+ * Serialize the event as tab delimited collection of the following:
+ *
+ * event:<event name>
+ * hostname:<tab escaped hostname>
+ * start_time:<event creation timestamp>
+ * end_time:<event export timestamp>
+ * category:<category>
+ * field:<name>=<tab escaped value>
+ *
+ * Note: cat and field can occur multiple times.
+ */
+void event_export_fmt_tabescaped_text(const struct metric *metric,
+ struct event *event, buffer_t *dest)
+{
+ const struct metric_export_info *info = &metric->export_info;
+
+ if (info->include == EVENT_EXPORTER_INCL_NONE)
+ return;
+
+ tabtext_export_name(dest, event, info);
+ tabtext_export_hostname(dest, info);
+ tabtext_export_timestamps(dest, event, info);
+ tabtext_export_categories(dest, event, info);
+ tabtext_export_fields(dest, event, info, metric->fields_count,
+ metric->fields);
+
+ /* remove trailing tab */
+ str_truncate(dest, str_len(dest) - 1);
+}
diff --git a/src/stats/event-exporter-fmt.c b/src/stats/event-exporter-fmt.c
new file mode 100644
index 0000000..a4ec5a8
--- /dev/null
+++ b/src/stats/event-exporter-fmt.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hash.h"
+#include "ioloop.h"
+#include "event-exporter.h"
+
+void event_export_helper_fmt_unix_time(string_t *dest,
+ const struct timeval *time)
+{
+ str_printfa(dest, "%"PRIdTIME_T".%06u", time->tv_sec,
+ (unsigned int) time->tv_usec);
+}
+
+void event_export_helper_fmt_rfc3339_time(string_t *dest,
+ const struct timeval *time)
+{
+ const struct tm *tm;
+
+ tm = gmtime(&time->tv_sec);
+
+ str_printfa(dest, "%04d-%02d-%02dT%02d:%02d:%02d.%06luZ",
+ tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
+ tm->tm_hour, tm->tm_min, tm->tm_sec,
+ time->tv_usec);
+}
+
+HASH_TABLE_DEFINE_TYPE(category_set, void *, const struct event_category *);
+
+static void insert_category(HASH_TABLE_TYPE(category_set) hash,
+ const struct event_category * const cat)
+{
+ /* insert this category (key == the unique internal pointer) */
+ hash_table_update(hash, cat->internal, cat);
+
+ /* insert parent's categories */
+ if (cat->parent != NULL)
+ insert_category(hash, cat->parent);
+}
+
+void event_export_helper_fmt_categories(string_t *dest,
+ struct event_category * const *cats,
+ unsigned int count,
+ void (*append)(string_t *, const char *),
+ const char *separator)
+{
+ HASH_TABLE_TYPE(category_set) hash;
+ struct hash_iterate_context *iter;
+ const struct event_category *cat;
+ void *key ATTR_UNUSED;
+ unsigned int i;
+ bool first = TRUE;
+
+ if (count == 0)
+ return;
+
+ hash_table_create_direct(&hash, pool_datastack_create(),
+ 3 * count /* estimate */);
+
+ /* insert all the categories into the hash table */
+ for (i = 0; i < count; i++)
+ insert_category(hash, cats[i]);
+
+ /* output each category from hash table */
+ iter = hash_table_iterate_init(hash);
+ while (hash_table_iterate(iter, hash, &key, &cat)) {
+ if (!first)
+ str_append(dest, separator);
+
+ append(dest, cat->name);
+
+ first = FALSE;
+ }
+ hash_table_iterate_deinit(&iter);
+
+ hash_table_destroy(&hash);
+}
diff --git a/src/stats/event-exporter-transport-drop.c b/src/stats/event-exporter-transport-drop.c
new file mode 100644
index 0000000..943f305
--- /dev/null
+++ b/src/stats/event-exporter-transport-drop.c
@@ -0,0 +1,9 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "event-exporter.h"
+
+void event_export_transport_drop(const struct exporter *exporter ATTR_UNUSED,
+ const buffer_t *buf ATTR_UNUSED)
+{
+}
diff --git a/src/stats/event-exporter-transport-http-post.c b/src/stats/event-exporter-transport-http-post.c
new file mode 100644
index 0000000..6d328cd
--- /dev/null
+++ b/src/stats/event-exporter-transport-http-post.c
@@ -0,0 +1,76 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "event-exporter.h"
+#include "http-client.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+
+/* the http client used to export all events with exporter=http-post */
+static struct http_client *exporter_http_client;
+
+void event_export_transport_http_post_deinit(void)
+{
+ if (exporter_http_client != NULL)
+ http_client_deinit(&exporter_http_client);
+}
+
+static void response_fxn(const struct http_response *response,
+ void *context ATTR_UNUSED)
+{
+ static time_t last_log;
+ static unsigned suppressed;
+
+ if (http_response_is_success(response))
+ return;
+
+ if (last_log == ioloop_time) {
+ suppressed++;
+ return; /* don't spam the log */
+ }
+
+ if (suppressed == 0)
+ i_error("Failed to export event via HTTP POST: %d %s",
+ response->status, response->reason);
+ else
+ i_error("Failed to export event via HTTP POST: %d %s (%u more errors suppressed)",
+ response->status, response->reason, suppressed);
+
+ last_log = ioloop_time;
+ suppressed = 0;
+}
+
+void event_export_transport_http_post(const struct exporter *exporter,
+ const buffer_t *buf)
+{
+ struct http_client_request *req;
+
+ if (exporter_http_client == NULL) {
+ const struct master_service_ssl_settings *master_ssl_set =
+ master_service_ssl_settings_get(master_service);
+ struct ssl_iostream_settings ssl_set;
+
+ struct http_client_settings set = {
+ .dns_client_socket_path = "dns-client",
+ };
+ if (master_ssl_set != NULL) {
+ master_service_ssl_client_settings_to_iostream_set(
+ master_ssl_set, pool_datastack_create(),
+ &ssl_set);
+ set.ssl = &ssl_set;
+ }
+ exporter_http_client = http_client_init(&set);
+ }
+
+ req = http_client_request_url_str(exporter_http_client, "POST",
+ exporter->transport_args,
+ response_fxn, NULL);
+ http_client_request_add_header(req, "Content-Type", exporter->format_mime_type);
+ http_client_request_set_payload_data(req, buf->data, buf->used);
+
+ http_client_request_set_timeout_msecs(req, exporter->transport_timeout);
+ http_client_request_submit(req);
+}
diff --git a/src/stats/event-exporter-transport-log.c b/src/stats/event-exporter-transport-log.c
new file mode 100644
index 0000000..a0cf67f
--- /dev/null
+++ b/src/stats/event-exporter-transport-log.c
@@ -0,0 +1,12 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "event-exporter.h"
+
+void event_export_transport_log(const struct exporter *exporter ATTR_UNUSED,
+ const buffer_t *buf)
+{
+ i_info("%.*s", (int)buf->used, (const char *)buf->data);
+}
diff --git a/src/stats/event-exporter.h b/src/stats/event-exporter.h
new file mode 100644
index 0000000..64bb708
--- /dev/null
+++ b/src/stats/event-exporter.h
@@ -0,0 +1,31 @@
+#ifndef EVENT_EXPORTER_H
+#define EVENT_EXPORTER_H
+
+#include "stats-metrics.h"
+
+/* fmt functions */
+void event_export_fmt_json(const struct metric *metric, struct event *event, buffer_t *dest);
+void event_export_fmt_none(const struct metric *metric, struct event *event, buffer_t *dest);
+void event_export_fmt_tabescaped_text(const struct metric *metric, struct event *event, buffer_t *dest);
+
+/* transport functions */
+void event_export_transport_drop(const struct exporter *exporter, const buffer_t *buf);
+void event_export_transport_http_post(const struct exporter *exporter, const buffer_t *buf);
+void event_export_transport_http_post_deinit(void);
+void event_export_transport_log(const struct exporter *exporter, const buffer_t *buf);
+
+/* append a microsecond resolution RFC3339 UTC timestamp */
+void event_export_helper_fmt_rfc3339_time(string_t *dest, const struct timeval *time);
+/* append a microsecond resolution unix timestamp in seconds (i.e., %u.%06u) */
+void event_export_helper_fmt_unix_time(string_t *dest, const struct timeval *time);
+/* append category names using 'append' function pointer, separated by 'separator' arg
+
+ The result has no duplicates regardless of if the array has any or if any
+ of the categories' ancestors are implictly or explicitly duplicated. */
+void event_export_helper_fmt_categories(string_t *dest,
+ struct event_category *const *cats,
+ unsigned int count,
+ void (*append)(string_t *, const char *),
+ const char *separator);
+
+#endif
diff --git a/src/stats/main.c b/src/stats/main.c
new file mode 100644
index 0000000..15e0733
--- /dev/null
+++ b/src/stats/main.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "restrict-access.h"
+#include "ioloop.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "stats-settings.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+#include "stats-service.h"
+#include "client-writer.h"
+#include "client-reader.h"
+#include "client-http.h"
+
+struct stats_metrics *stats_metrics;
+time_t stats_startup_time;
+
+static const struct stats_settings *stats_settings;
+
+static bool client_is_writer(const char *path)
+{
+ const char *name, *suffix;
+
+ name = strrchr(path, '/');
+ if (name == NULL)
+ name = path;
+ else
+ name++;
+
+ suffix = strrchr(name, '-');
+ if (suffix == NULL)
+ suffix = name;
+ else
+ suffix++;
+
+ return strcmp(suffix, "writer") == 0;
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (strcmp(conn->name, "http") == 0)
+ client_http_create(conn);
+ else if (client_is_writer(conn->name))
+ client_writer_create(conn->fd);
+ else
+ client_reader_create(conn->fd);
+ master_service_client_connection_accept(conn);
+}
+
+static void stats_die(void)
+{
+ /* just wait for existing stats clients to disconnect from us */
+}
+
+static void main_preinit(void)
+{
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_init(void)
+{
+ void **sets = master_service_settings_get_others(master_service);
+ stats_settings = sets[0];
+
+ stats_startup_time = ioloop_time;
+ stats_metrics = stats_metrics_init(stats_settings);
+ stats_event_categories_init();
+ client_readers_init();
+ client_writers_init();
+ client_http_init(stats_settings);
+ stats_services_init();
+}
+
+static void main_deinit(void)
+{
+ stats_services_deinit();
+ client_readers_deinit();
+ client_writers_deinit();
+ client_http_deinit();
+ stats_event_categories_deinit();
+ stats_metrics_deinit(&stats_metrics);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &stats_setting_parser_info,
+ NULL
+ };
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_SSL_INIT |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE |
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+ const char *error;
+
+ master_service = master_service_init("stats", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+ master_service_init_log(master_service);
+ master_service_set_die_callback(master_service, stats_die);
+
+ main_preinit();
+
+ main_init();
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/stats/stats-common.h b/src/stats/stats-common.h
new file mode 100644
index 0000000..09af424
--- /dev/null
+++ b/src/stats/stats-common.h
@@ -0,0 +1,10 @@
+#ifndef STATS_COMMON_H
+#define STATS_COMMON_H
+
+#include "lib.h"
+#include "stats-settings.h"
+
+extern struct stats_metrics *stats_metrics;
+extern time_t stats_startup_time;
+
+#endif
diff --git a/src/stats/stats-event-category.c b/src/stats/stats-event-category.c
new file mode 100644
index 0000000..cb01e8b
--- /dev/null
+++ b/src/stats/stats-event-category.c
@@ -0,0 +1,32 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "stats-event-category.h"
+
+static pool_t categories_pool;
+
+void stats_event_category_register(const char *name,
+ struct event_category *parent)
+{
+ struct event_category *category =
+ p_new(categories_pool, struct event_category, 1);
+ category->parent = parent;
+ category->name = p_strdup(categories_pool, name);
+
+ /* Create a temporary event to register the category. A bit slower
+ than necessary, but this code won't be called often. */
+ struct event *event = event_create(NULL);
+ struct event_category *categories[] = { category, NULL };
+ event_add_categories(event, categories);
+ event_unref(&event);
+}
+
+void stats_event_categories_init(void)
+{
+ categories_pool = pool_alloconly_create("categories", 1024);
+}
+
+void stats_event_categories_deinit(void)
+{
+ pool_unref(&categories_pool);
+}
diff --git a/src/stats/stats-event-category.h b/src/stats/stats-event-category.h
new file mode 100644
index 0000000..b56fc91
--- /dev/null
+++ b/src/stats/stats-event-category.h
@@ -0,0 +1,12 @@
+#ifndef STATS_EVENT_CATEGORY_H
+#define STATS_EVENT_CATEGORY_H
+
+/* Register a new event category if it doesn't already exist.
+ parent may be NULL. */
+void stats_event_category_register(const char *name,
+ struct event_category *parent);
+
+void stats_event_categories_init(void);
+void stats_event_categories_deinit(void);
+
+#endif
diff --git a/src/stats/stats-metrics.c b/src/stats/stats-metrics.c
new file mode 100644
index 0000000..b582bb4
--- /dev/null
+++ b/src/stats/stats-metrics.c
@@ -0,0 +1,765 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "array.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "stats-dist.h"
+#include "time-util.h"
+#include "event-filter.h"
+#include "event-exporter.h"
+#include "stats-settings.h"
+#include "stats-metrics.h"
+#include "settings-parser.h"
+
+#include <ctype.h>
+
+#define LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN 1000
+
+struct stats_metrics {
+ pool_t pool;
+ struct event_filter *filter; /* stats & export */
+ ARRAY(struct exporter *) exporters;
+ ARRAY(struct metric *) metrics;
+};
+
+static void
+stats_metric_event(struct metric *metric, struct event *event, pool_t pool);
+static struct metric *
+stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool);
+static void stats_metric_free(struct metric *metric);
+
+static void stats_exporters_add_set(struct stats_metrics *metrics,
+ const struct stats_exporter_settings *set)
+{
+ struct exporter *exporter;
+
+ exporter = p_new(metrics->pool, struct exporter, 1);
+ exporter->name = p_strdup(metrics->pool, set->name);
+ exporter->transport_args = p_strdup(metrics->pool, set->transport_args);
+ exporter->transport_timeout = set->transport_timeout;
+ exporter->time_format = set->parsed_time_format;
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporter_settings_check().
+ */
+ if (strcmp(set->format, "none") == 0) {
+ exporter->format = event_export_fmt_none;
+ exporter->format_mime_type = "application/octet-stream";
+ } else if (strcmp(set->format, "json") == 0) {
+ exporter->format = event_export_fmt_json;
+ exporter->format_mime_type = "application/json";
+ } else if (strcmp(set->format, "tab-text") == 0) {
+ exporter->format = event_export_fmt_tabescaped_text;
+ exporter->format_mime_type = "text/plain";
+ } else {
+ i_unreached();
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporter_settings_check().
+ */
+ if (strcmp(set->transport, "drop") == 0) {
+ exporter->transport = event_export_transport_drop;
+ } else if (strcmp(set->transport, "http-post") == 0) {
+ exporter->transport = event_export_transport_http_post;
+ } else if (strcmp(set->transport, "log") == 0) {
+ exporter->transport = event_export_transport_log;
+ exporter->format_max_field_len =
+ LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN;
+ } else {
+ i_unreached();
+ }
+
+ exporter->transport_args = set->transport_args;
+
+ array_push_back(&metrics->exporters, &exporter);
+}
+
+static struct metric *
+stats_metric_alloc(pool_t pool, const char *name,
+ const struct stats_metric_settings *set,
+ const char *const *fields)
+{
+ struct metric *metric = p_new(pool, struct metric, 1);
+ metric->name = p_strdup(pool, name);
+ metric->set = set;
+ metric->duration_stats = stats_dist_init();
+ metric->fields_count = str_array_length(fields);
+ if (metric->fields_count > 0) {
+ metric->fields = p_new(pool, struct metric_field,
+ metric->fields_count);
+ for (unsigned int i = 0; i < metric->fields_count; i++) {
+ metric->fields[i].field_key = p_strdup(pool, fields[i]);
+ metric->fields[i].stats = stats_dist_init();
+ }
+ }
+ return metric;
+}
+
+static void stats_metrics_add_set(struct stats_metrics *metrics,
+ const struct stats_metric_settings *set)
+{
+ struct exporter *exporter;
+ struct metric *metric;
+ const char *const *fields;
+ const char *const *tmp;
+
+ fields = t_strsplit_spaces(set->fields, " ");
+ metric = stats_metric_alloc(metrics->pool, set->metric_name, set, fields);
+
+ if (array_is_created(&set->parsed_group_by))
+ metric->group_by = array_get(&set->parsed_group_by,
+ &metric->group_by_count);
+
+ array_push_back(&metrics->metrics, &metric);
+
+ event_filter_merge_with_context(metrics->filter, set->parsed_filter, metric);
+
+ /*
+ * Metrics may also be exported - make sure exporter info is set
+ */
+
+ if (set->exporter[0] == '\0')
+ return; /* not exported */
+
+ array_foreach_elem(&metrics->exporters, exporter) {
+ if (strcmp(set->exporter, exporter->name) == 0) {
+ metric->export_info.exporter = exporter;
+ break;
+ }
+ }
+
+ if (metric->export_info.exporter == NULL)
+ i_panic("Could not find exporter (%s) for metric (%s)",
+ set->exporter, set->metric_name);
+
+ /* Defaults */
+ metric->export_info.include = EVENT_EXPORTER_INCL_NONE;
+
+ tmp = t_strsplit_spaces(set->exporter_include, " ");
+ for (; *tmp != NULL; tmp++) {
+ if (strcmp(*tmp, "name") == 0)
+ metric->export_info.include |= EVENT_EXPORTER_INCL_NAME;
+ else if (strcmp(*tmp, "hostname") == 0)
+ metric->export_info.include |= EVENT_EXPORTER_INCL_HOSTNAME;
+ else if (strcmp(*tmp, "timestamps") == 0)
+ metric->export_info.include |= EVENT_EXPORTER_INCL_TIMESTAMPS;
+ else if (strcmp(*tmp, "categories") == 0)
+ metric->export_info.include |= EVENT_EXPORTER_INCL_CATEGORIES;
+ else if (strcmp(*tmp, "fields") == 0)
+ metric->export_info.include |= EVENT_EXPORTER_INCL_FIELDS;
+ else
+ i_warning("Ignoring unknown exporter include '%s'", *tmp);
+ }
+}
+
+static struct stats_metric_settings *
+stats_metric_settings_dup(pool_t pool, const struct stats_metric_settings *src)
+{
+ struct stats_metric_settings *set = p_new(pool, struct stats_metric_settings, 1);
+
+ set->metric_name = p_strdup(pool, src->metric_name);
+ set->description = p_strdup(pool, src->description);
+ set->fields = p_strdup(pool, src->fields);
+ set->group_by = p_strdup(pool, src->group_by);
+ set->filter = p_strdup(pool, src->filter);
+ set->exporter = p_strdup(pool, src->exporter);
+ set->exporter_include = p_strdup(pool, src->exporter_include);
+
+ return set;
+}
+
+static struct metric *
+stats_metrics_find(struct stats_metrics *metrics,
+ const char *name, unsigned int *idx_r)
+{
+ struct metric *const *m;
+ array_foreach(&metrics->metrics, m) {
+ if (strcmp((*m)->name, name) == 0) {
+ *idx_r = array_foreach_idx(&metrics->metrics, m);
+ return *m;
+ }
+ }
+ return NULL;
+}
+
+static bool
+stats_metrics_check_for_exporter(struct stats_metrics *metrics, const char *name)
+{
+ struct exporter *exporter;
+
+ /* Allow registering metrics with empty/missing exporters. */
+ if (name[0] == '\0')
+ return TRUE;
+
+ if (!array_is_created(&metrics->exporters))
+ return FALSE;
+
+ bool is_found = FALSE;
+ array_foreach_elem(&metrics->exporters, exporter) {
+ if (strcmp(exporter->name, name) == 0) {
+ is_found = TRUE;
+ break;
+ }
+ }
+
+ return is_found;
+}
+
+bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
+ struct stats_metric_settings *set,
+ const char **error_r)
+{
+ unsigned int existing_idx ATTR_UNUSED;
+ if (stats_metrics_find(metrics, set->metric_name, &existing_idx) != NULL) {
+ *error_r = "Metric already exists";
+ return FALSE;
+ }
+
+ struct stats_metric_settings *_set =
+ stats_metric_settings_dup(metrics->pool, set);
+ if (!stats_metric_setting_parser_info.check_func(_set, metrics->pool, error_r))
+ return FALSE;
+
+ if (!stats_metrics_check_for_exporter(metrics, set->exporter)) {
+ *error_r = t_strdup_printf("Exporter '%s' does not exist.",
+ set->exporter);
+ return FALSE;
+ }
+
+ stats_metrics_add_set(metrics, _set);
+ return TRUE;
+}
+
+bool stats_metrics_remove_dynamic(struct stats_metrics *metrics,
+ const char *name)
+{
+ unsigned int m_idx;
+ bool ret = FALSE;
+ struct metric *m = stats_metrics_find(metrics, name, &m_idx);
+ if (m != NULL) {
+ array_delete(&metrics->metrics, m_idx, 1);
+ ret = event_filter_remove_queries_with_context(metrics->filter, m);
+ stats_metric_free(m);
+ }
+ return ret;
+}
+
+static void
+stats_metrics_add_from_settings(struct stats_metrics *metrics,
+ const struct stats_settings *set)
+{
+ /* add all the exporters first */
+ if (!array_is_created(&set->exporters)) {
+ p_array_init(&metrics->exporters, metrics->pool, 0);
+ } else {
+ struct stats_exporter_settings *exporter_set;
+
+ p_array_init(&metrics->exporters, metrics->pool,
+ array_count(&set->exporters));
+ array_foreach_elem(&set->exporters, exporter_set)
+ stats_exporters_add_set(metrics, exporter_set);
+ }
+
+ /* then add all the metrics */
+ if (!array_is_created(&set->metrics)) {
+ p_array_init(&metrics->metrics, metrics->pool, 0);
+ } else {
+ struct stats_metric_settings *metric_set;
+
+ p_array_init(&metrics->metrics, metrics->pool,
+ array_count(&set->metrics));
+ array_foreach_elem(&set->metrics, metric_set) T_BEGIN {
+ stats_metrics_add_set(metrics, metric_set);
+ } T_END;
+ }
+}
+
+struct stats_metrics *stats_metrics_init(const struct stats_settings *set)
+{
+ struct stats_metrics *metrics;
+ pool_t pool = pool_alloconly_create("stats metrics", 1024);
+
+ metrics = p_new(pool, struct stats_metrics, 1);
+ metrics->pool = pool;
+ metrics->filter = event_filter_create();
+ stats_metrics_add_from_settings(metrics, set);
+ return metrics;
+}
+
+static void stats_metric_free(struct metric *metric)
+{
+ struct metric *sub_metric;
+ stats_dist_deinit(&metric->duration_stats);
+ for (unsigned int i = 0; i < metric->fields_count; i++)
+ stats_dist_deinit(&metric->fields[i].stats);
+ if (!array_is_created(&metric->sub_metrics))
+ return;
+ array_foreach_elem(&metric->sub_metrics, sub_metric)
+ stats_metric_free(sub_metric);
+}
+
+static void stats_export_deinit(void)
+{
+ /* no need for event_export_transport_drop_deinit() - no-op */
+ event_export_transport_http_post_deinit();
+ /* no need for event_export_transport_log_deinit() - no-op */
+}
+
+void stats_metrics_deinit(struct stats_metrics **_metrics)
+{
+ struct stats_metrics *metrics = *_metrics;
+ struct metric *metric;
+
+ *_metrics = NULL;
+
+ stats_export_deinit();
+
+ array_foreach_elem(&metrics->metrics, metric)
+ stats_metric_free(metric);
+ event_filter_unref(&metrics->filter);
+ pool_unref(&metrics->pool);
+}
+
+static void stats_metric_reset(struct metric *metric)
+{
+ struct metric *sub_metric;
+ stats_dist_reset(metric->duration_stats);
+ for (unsigned int i = 0; i < metric->fields_count; i++)
+ stats_dist_reset(metric->fields[i].stats);
+ if (!array_is_created(&metric->sub_metrics))
+ return;
+ array_foreach_elem(&metric->sub_metrics, sub_metric)
+ stats_metric_reset(sub_metric);
+}
+
+void stats_metrics_reset(struct stats_metrics *metrics)
+{
+ struct metric *metric;
+
+ array_foreach_elem(&metrics->metrics, metric)
+ stats_metric_reset(metric);
+}
+
+struct event_filter *
+stats_metrics_get_event_filter(struct stats_metrics *metrics)
+{
+ return metrics->filter;
+}
+
+static struct metric *
+stats_metric_find_sub_metric(struct metric *metric,
+ const struct metric_value *value)
+{
+ struct metric *sub_metrics;
+
+ /* lookup sub-metric */
+ array_foreach_elem(&metric->sub_metrics, sub_metrics) {
+ switch (sub_metrics->group_value.type) {
+ case METRIC_VALUE_TYPE_STR:
+ if (memcmp(sub_metrics->group_value.hash, value->hash,
+ SHA1_RESULTLEN) == 0)
+ return sub_metrics;
+ break;
+ case METRIC_VALUE_TYPE_INT:
+ if (sub_metrics->group_value.intmax == value->intmax)
+ return sub_metrics;
+ break;
+ case METRIC_VALUE_TYPE_BUCKET_INDEX:
+ if (sub_metrics->group_value.intmax == value->intmax)
+ return sub_metrics;
+ break;
+ }
+ }
+ return NULL;
+}
+
+static struct metric *
+stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool)
+{
+ struct metric *sub_metric;
+ ARRAY_TYPE(const_string) fields;
+ t_array_init(&fields, metric->fields_count);
+ for (unsigned int i = 0; i < metric->fields_count; i++)
+ array_append(&fields, &metric->fields[i].field_key, 1);
+ array_append_zero(&fields);
+ sub_metric = stats_metric_alloc(pool, metric->name, metric->set,
+ array_idx(&fields, 0));
+ sub_metric->sub_name = p_strdup(pool, str_sanitize_utf8(name, 32));
+ array_append(&metric->sub_metrics, &sub_metric, 1);
+ return sub_metric;
+}
+
+static bool
+stats_metric_group_by_discrete(const struct event_field *field,
+ struct metric_value *value_r)
+{
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ value_r->type = METRIC_VALUE_TYPE_STR;
+ /* use sha1 of value to avoid excessive memory usage in case the
+ actual value is quite long */
+ sha1_get_digest(field->value.str, strlen(field->value.str),
+ value_r->hash);
+ return TRUE;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ value_r->type = METRIC_VALUE_TYPE_INT;
+ value_r->intmax = field->value.intmax;
+ return TRUE;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ return FALSE;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ return FALSE;
+ }
+
+ i_unreached();
+}
+
+/* convert the value to a bucket index */
+static bool
+stats_metric_group_by_quantized(const struct event_field *field,
+ struct metric_value *value_r,
+ const struct stats_metric_settings_group_by *group_by)
+{
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ return FALSE;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ break;
+ }
+
+ value_r->type = METRIC_VALUE_TYPE_BUCKET_INDEX;
+
+ for (unsigned int i = 0; i < group_by->num_ranges; i++) {
+ if ((field->value.intmax <= group_by->ranges[i].min) ||
+ (field->value.intmax > group_by->ranges[i].max))
+ continue;
+
+ value_r->intmax = i;
+ return TRUE;
+ }
+
+ i_panic("failed to find a matching bucket for '%s'=%jd",
+ group_by->field, field->value.intmax);
+}
+
+/* convert value to a bucket label */
+static const char *
+stats_metric_group_by_quantized_label(const struct event_field *field,
+ const struct stats_metric_settings_group_by *group_by,
+ const size_t bucket_index)
+{
+ const struct stats_metric_settings_bucket_range *range = &group_by->ranges[bucket_index];
+ const char *name = group_by->field;
+ const char *label;
+
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ i_unreached();
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ break;
+ }
+
+ if (range->min == INTMAX_MIN)
+ label = t_strdup_printf("%s_ninf_%jd", name, range->max);
+ else if (range->max == INTMAX_MAX)
+ label = t_strdup_printf("%s_%jd_inf", name, range->min + 1);
+ else
+ label = t_strdup_printf("%s_%jd_%jd", name,
+ range->min + 1, range->max);
+
+ return label;
+}
+
+static bool
+stats_metric_group_by_get_value(const struct event_field *field,
+ const struct stats_metric_settings_group_by *group_by,
+ struct metric_value *value_r)
+{
+ switch (group_by->func) {
+ case STATS_METRIC_GROUPBY_DISCRETE:
+ if (!stats_metric_group_by_discrete(field, value_r))
+ return FALSE;
+ return TRUE;
+ case STATS_METRIC_GROUPBY_QUANTIZED:
+ if (!stats_metric_group_by_quantized(field, value_r, group_by))
+ return FALSE;
+ return TRUE;
+ }
+
+ i_panic("unknown group-by function %d", group_by->func);
+}
+
+static const char *
+stats_metric_group_by_get_label(const struct event_field *field,
+ const struct stats_metric_settings_group_by *group_by,
+ const struct metric_value *value)
+{
+ switch (group_by->func) {
+ case STATS_METRIC_GROUPBY_DISCRETE:
+ i_unreached();
+ case STATS_METRIC_GROUPBY_QUANTIZED:
+ return stats_metric_group_by_quantized_label(field, group_by,
+ value->intmax);
+ }
+
+ i_panic("unknown group-by function %d", group_by->func);
+}
+
+static const char *
+stats_metric_group_by_value_label(const struct event_field *field,
+ const struct stats_metric_settings_group_by *group_by,
+ const struct metric_value *value)
+{
+ switch (value->type) {
+ case METRIC_VALUE_TYPE_STR:
+ return field->value.str;
+ case METRIC_VALUE_TYPE_INT:
+ return dec2str(field->value.intmax);
+ case METRIC_VALUE_TYPE_BUCKET_INDEX:
+ return stats_metric_group_by_get_label(field, group_by, value);
+ }
+ i_unreached();
+}
+
+static struct metric *
+stats_metric_get_sub_metric(struct metric *metric,
+ const struct event_field *field,
+ const struct metric_value *value,
+ pool_t pool)
+{
+ struct metric *sub_metric;
+
+ sub_metric = stats_metric_find_sub_metric(metric, value);
+ if (sub_metric != NULL)
+ return sub_metric;
+
+ T_BEGIN {
+ const char *value_label =
+ stats_metric_group_by_value_label(field,
+ &metric->group_by[0], value);
+ sub_metric = stats_metric_sub_metric_alloc(metric, value_label,
+ pool);
+ } T_END;
+ if (metric->group_by_count > 1) {
+ sub_metric->group_by_count = metric->group_by_count - 1;
+ sub_metric->group_by = &metric->group_by[1];
+ }
+ sub_metric->group_value.type = value->type;
+ sub_metric->group_value.intmax = value->intmax;
+ memcpy(sub_metric->group_value.hash, value->hash, SHA1_RESULTLEN);
+ return sub_metric;
+}
+
+static void
+stats_metric_group_by_field(struct metric *metric, struct event *event,
+ const struct event_field *field, pool_t pool)
+{
+ struct metric *sub_metric;
+ struct metric_value value;
+
+ if (!stats_metric_group_by_get_value(field, &metric->group_by[0], &value))
+ return;
+
+ if (!array_is_created(&metric->sub_metrics))
+ p_array_init(&metric->sub_metrics, pool, 8);
+ sub_metric = stats_metric_get_sub_metric(metric, field, &value, pool);
+
+ /* sub-metrics are recursive, so each sub-metric can have additional
+ sub-metrics. */
+ stats_metric_event(sub_metric, event, pool);
+}
+
+static void
+stats_event_get_strlist(struct event *event, const char *name,
+ ARRAY_TYPE(const_string) *strings)
+{
+ if (event == NULL)
+ return;
+
+ const struct event_field *field =
+ event_find_field_nonrecursive(event, name);
+ if (field != NULL) {
+ const char *str;
+ array_foreach_elem(&field->value.strlist, str)
+ array_push_back(strings, &str);
+ }
+ stats_event_get_strlist(event_get_parent(event), name, strings);
+}
+
+static void
+stats_metric_group_by(struct metric *metric, struct event *event, pool_t pool)
+{
+ const struct event_field *field =
+ event_find_field_recursive(event, metric->group_by[0].field);
+
+ /* ignore missing field */
+ if (field == NULL)
+ return;
+
+ if (field->value_type != EVENT_FIELD_VALUE_TYPE_STRLIST)
+ stats_metric_group_by_field(metric, event, field, pool);
+ else {
+ /* Handle each string in strlist separately. The strlist needs
+ to be combined from the event and its parents, as well as
+ the global event and its parents. */
+ ARRAY_TYPE(const_string) strings;
+
+ t_array_init(&strings, 8);
+ stats_event_get_strlist(event, metric->group_by[0].field,
+ &strings);
+ stats_event_get_strlist(event_get_global(),
+ metric->group_by[0].field, &strings);
+
+ struct event_field str_field = {
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ };
+ const char *str;
+
+ /* sort strings so duplicates can be easily skipped */
+ array_sort(&strings, i_strcmp_p);
+ array_foreach_elem(&strings, str) {
+ if (str_field.value.str == NULL ||
+ strcmp(str_field.value.str, str) != 0) {
+ str_field.value.str = str;
+ stats_metric_group_by_field(metric, event,
+ &str_field, pool);
+ }
+ }
+ }
+}
+
+static void
+stats_metric_event_field(struct event *event, const char *fieldname,
+ struct stats_dist *stats)
+{
+ const struct event_field *field =
+ event_find_field_recursive(event, fieldname);
+ intmax_t num = 0;
+
+ if (field == NULL)
+ return;
+
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ num = field->value.intmax;
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ num = field->value.timeval.tv_sec * 1000000ULL +
+ field->value.timeval.tv_usec;
+ break;
+ }
+
+ stats_dist_add(stats, num);
+}
+
+static void
+stats_metric_event(struct metric *metric, struct event *event, pool_t pool)
+{
+ /* duration is special - we always add it */
+ stats_metric_event_field(event, STATS_EVENT_FIELD_NAME_DURATION,
+ metric->duration_stats);
+
+ for (unsigned int i = 0; i < metric->fields_count; i++)
+ stats_metric_event_field(event,
+ metric->fields[i].field_key,
+ metric->fields[i].stats);
+
+ if (metric->group_by != NULL)
+ stats_metric_group_by(metric, event, pool);
+}
+
+static void
+stats_export_event(struct metric *metric, struct event *oldevent)
+{
+ const struct metric_export_info *info = &metric->export_info;
+ const struct exporter *exporter = info->exporter;
+ struct event *event;
+
+ i_assert(exporter != NULL);
+
+ event = event_flatten(oldevent);
+
+ T_BEGIN {
+ buffer_t *buf;
+
+ buf = t_buffer_create(128);
+
+ exporter->format(metric, event, buf);
+ exporter->transport(exporter, buf);
+ } T_END;
+
+ event_unref(&event);
+}
+
+void stats_metrics_event(struct stats_metrics *metrics, struct event *event,
+ const struct failure_context *ctx)
+{
+ struct event_filter_match_iter *iter;
+ struct metric *metric;
+ uintmax_t duration;
+
+ /* Note: Adding the field here means that it will get exported
+ below. This is necessary to allow group-by functions to quantize
+ based on the event duration. */
+ event_get_last_duration(event, &duration);
+ event_add_int(event, STATS_EVENT_FIELD_NAME_DURATION, duration);
+
+ /* process stats & exports */
+ iter = event_filter_match_iter_init(metrics->filter, event, ctx);
+ while ((metric = event_filter_match_iter_next(iter)) != NULL) T_BEGIN {
+ /* every metric is fed into stats */
+ stats_metric_event(metric, event, metrics->pool);
+
+ /* some metrics are exported */
+ if (metric->export_info.exporter != NULL)
+ stats_export_event(metric, event);
+ } T_END;
+ event_filter_match_iter_deinit(&iter);
+}
+
+struct stats_metrics_iter {
+ struct stats_metrics *metrics;
+ unsigned int idx;
+};
+
+struct stats_metrics_iter *
+stats_metrics_iterate_init(struct stats_metrics *metrics)
+{
+ struct stats_metrics_iter *iter;
+
+ iter = i_new(struct stats_metrics_iter, 1);
+ iter->metrics = metrics;
+ return iter;
+}
+
+const struct metric *stats_metrics_iterate(struct stats_metrics_iter *iter)
+{
+ struct metric *const *metrics;
+ unsigned int count;
+
+ metrics = array_get(&iter->metrics->metrics, &count);
+ if (iter->idx >= count)
+ return NULL;
+ return metrics[iter->idx++];
+}
+
+void stats_metrics_iterate_deinit(struct stats_metrics_iter **_iter)
+{
+ struct stats_metrics_iter *iter = *_iter;
+
+ *_iter = NULL;
+ i_free(iter);
+}
diff --git a/src/stats/stats-metrics.h b/src/stats/stats-metrics.h
new file mode 100644
index 0000000..6d7d745
--- /dev/null
+++ b/src/stats/stats-metrics.h
@@ -0,0 +1,134 @@
+#ifndef STATS_METRICS_H
+#define STATS_METRICS_H
+
+#include "stats-settings.h"
+#include "sha1.h"
+
+#define STATS_EVENT_FIELD_NAME_DURATION "duration"
+
+struct metric;
+struct stats_metrics;
+
+struct exporter {
+ const char *name;
+
+ /*
+ * serialization format options
+ *
+ * the "how do we encode the event before sending it" knobs
+ */
+ enum event_exporter_time_fmt time_format;
+
+ /* Max length for string field values */
+ size_t format_max_field_len;
+
+ /* function to serialize the event */
+ void (*format)(const struct metric *, struct event *, buffer_t *);
+
+ /* mime type for the format */
+ const char *format_mime_type;
+
+ /*
+ * transport options
+ *
+ * the "how do we get the event to the external location" knobs
+ */
+ const char *transport_args;
+ unsigned int transport_timeout;
+
+ /* function to send the event */
+ void (*transport)(const struct exporter *, const buffer_t *);
+};
+
+struct metric_export_info {
+ const struct exporter *exporter;
+
+ enum event_exporter_includes {
+ EVENT_EXPORTER_INCL_NONE = 0,
+ EVENT_EXPORTER_INCL_NAME = 0x01,
+ EVENT_EXPORTER_INCL_HOSTNAME = 0x02,
+ EVENT_EXPORTER_INCL_TIMESTAMPS = 0x04,
+ EVENT_EXPORTER_INCL_CATEGORIES = 0x08,
+ EVENT_EXPORTER_INCL_FIELDS = 0x10,
+ } include;
+};
+
+struct metric_field {
+ const char *field_key;
+ struct stats_dist *stats;
+};
+
+enum metric_value_type {
+ METRIC_VALUE_TYPE_STR,
+ METRIC_VALUE_TYPE_INT,
+ METRIC_VALUE_TYPE_BUCKET_INDEX,
+};
+
+struct metric_value {
+ enum metric_value_type type;
+ unsigned char hash[SHA1_RESULTLEN];
+ intmax_t intmax;
+};
+
+struct metric {
+ const struct stats_metric_settings *set;
+ const char *name;
+ /* When this metric is a sub-metric, then this is the
+ suffix for name and any sub_names before it.
+
+ So if we have
+
+ struct metric imap_command {
+ event_name = imap_command_finished
+ group_by = cmd_name
+ }
+
+ The metric.name will always be imap_command and for each sub-metric
+ metric.sub_name will be whatever the cmd_name is, such as 'select'.
+
+ This is a display name and does not guarantee uniqueness.
+ */
+ const char *sub_name;
+
+ /* Timing for how long the event existed */
+ struct stats_dist *duration_stats;
+
+ unsigned int fields_count;
+ struct metric_field *fields;
+
+ unsigned int group_by_count;
+ const struct stats_metric_settings_group_by *group_by;
+ struct metric_value group_value;
+ ARRAY(struct metric *) sub_metrics;
+
+ struct metric_export_info export_info;
+};
+
+bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
+ struct stats_metric_settings *set,
+ const char **error_r);
+
+bool stats_metrics_remove_dynamic(struct stats_metrics *metrics,
+ const char *name);
+
+struct stats_metrics *stats_metrics_init(const struct stats_settings *set);
+void stats_metrics_deinit(struct stats_metrics **metrics);
+
+/* Reset all metrics */
+void stats_metrics_reset(struct stats_metrics *metrics);
+
+/* Returns event filter created from the stats_settings. */
+struct event_filter *
+stats_metrics_get_event_filter(struct stats_metrics *metrics);
+
+/* Update metrics with given event. */
+void stats_metrics_event(struct stats_metrics *metrics, struct event *event,
+ const struct failure_context *ctx);
+
+/* Iterate through all the tracked metrics. */
+struct stats_metrics_iter *
+stats_metrics_iterate_init(struct stats_metrics *metrics);
+const struct metric *stats_metrics_iterate(struct stats_metrics_iter *iter);
+void stats_metrics_iterate_deinit(struct stats_metrics_iter **iter);
+
+#endif
diff --git a/src/stats/stats-service-openmetrics.c b/src/stats/stats-service-openmetrics.c
new file mode 100644
index 0000000..371e8d9
--- /dev/null
+++ b/src/stats/stats-service-openmetrics.c
@@ -0,0 +1,826 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "dovecot-version.h"
+#include "str.h"
+#include "array.h"
+#include "json-parser.h"
+#include "ioloop.h"
+#include "ostream.h"
+#include "stats-dist.h"
+#include "http-server.h"
+#include "client-http.h"
+#include "stats-settings.h"
+#include "stats-metrics.h"
+#include "stats-service-private.h"
+
+#define OPENMETRICS_CONTENT_VERSION "0.0.1"
+
+#ifdef DOVECOT_REVISION
+#define OPENMETRICS_BUILD_INFO \
+ "version=\""DOVECOT_VERSION"\"," \
+ "revision=\""DOVECOT_REVISION"\""
+#else
+#define OPENMETRICS_BUILD_INFO \
+ "version=\""DOVECOT_VERSION"\""
+#endif
+
+enum openmetrics_metric_type {
+ OPENMETRICS_METRIC_TYPE_COUNT,
+ OPENMETRICS_METRIC_TYPE_DURATION,
+ OPENMETRICS_METRIC_TYPE_FIELD,
+ OPENMETRICS_METRIC_TYPE_HISTOGRAM,
+};
+
+enum openmetrics_request_state {
+ OPENMETRICS_REQUEST_STATE_INIT = 0,
+ OPENMETRICS_REQUEST_STATE_METRIC,
+ OPENMETRICS_REQUEST_STATE_METRIC_HEADER,
+ OPENMETRICS_REQUEST_STATE_SUB_METRICS,
+ OPENMETRICS_REQUEST_STATE_METRIC_BODY,
+ OPENMETRICS_REQUEST_STATE_FINISHED,
+};
+
+struct openmetrics_request_sub_metric {
+ size_t labels_pos;
+ const struct metric *metric;
+ unsigned int sub_index;
+};
+
+struct openmetrics_request {
+ struct ostream *output;
+
+ enum openmetrics_request_state state;
+ struct stats_metrics_iter *stats_iter;
+ const struct metric *metric;
+ enum openmetrics_metric_type metric_type;
+ string_t *labels;
+ size_t labels_pos;
+ unsigned int field_pos;
+ ARRAY(struct openmetrics_request_sub_metric) sub_metric_stack;
+
+ bool has_submetric:1;
+};
+
+/* https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels:
+
+ Every time series is uniquely identified by its metric name and optional
+ key-value pairs called labels.
+
+ The metric name specifies the general feature of a system that is measured
+ (e.g. http_requests_total - the total number of HTTP requests received). It
+ may contain ASCII letters and digits, as well as underscores and colons. It
+ must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*.
+ */
+
+static bool openmetrics_check_name(const char *name)
+{
+ const unsigned char *p, *pend;
+
+ p = (const unsigned char *)name;
+ pend = p + strlen(name);
+
+ if (p == pend)
+ return FALSE;
+
+ if (!(*p >= 'a' && *p <= 'z') && !(*p >= 'A' && *p <= 'Z') &&
+ *p != '_' && *p != ':')
+ return FALSE;
+ p++;
+ while (p < pend) {
+ if (!(*p >= 'a' && *p <= 'z') && !(*p >= 'A' && *p <= 'Z') &&
+ !(*p >= '0' && *p <= '9') && *p != '_' && *p != ':')
+ return FALSE;
+ p++;
+ }
+ return TRUE;
+}
+
+static void openmetrics_export_dovecot(string_t *out)
+{
+ i_assert(stats_startup_time <= ioloop_time);
+ str_append(out, "# HELP process_start_time_seconds "
+ "Timestamp of service start\n");
+ str_append(out, "# TYPE process_start_time_seconds gauge\n");
+ str_printfa(out, "process_start_time_seconds %"PRIdTIME_T"\n",
+ stats_startup_time);
+
+ str_append(out, "# HELP dovecot_build "
+ "Dovecot build information\n");
+ str_append(out, "# TYPE dovecot_build info\n");
+ str_append(out, "dovecot_build_info{"OPENMETRICS_BUILD_INFO"} 1\n");
+}
+
+static void openmetrics_export_eof(string_t *out)
+{
+ str_append(out, "# EOF\n");
+}
+
+static void
+openmetrics_export_metric_value(struct openmetrics_request *req, string_t *out,
+ const struct metric *metric)
+{
+ const struct metric_field *field;
+ /* Metric name */
+ str_append(out, "dovecot_");
+ str_append(out, req->metric->name);
+ switch (req->metric_type) {
+ case OPENMETRICS_METRIC_TYPE_COUNT:
+ if (req->metric->group_by != NULL && str_len(req->labels) == 0)
+ str_append(out, "_count");
+ else
+ str_append(out, "_total");
+ break;
+ case OPENMETRICS_METRIC_TYPE_DURATION:
+ if (req->metric->group_by != NULL && str_len(req->labels) == 0)
+ str_append(out, "_duration_seconds_sum");
+ else
+ str_append(out, "_duration_seconds_total");
+ break;
+ case OPENMETRICS_METRIC_TYPE_FIELD:
+ field = &metric->fields[req->field_pos];
+ if (req->metric->group_by != NULL && str_len(req->labels) == 0)
+ str_printfa(out, "_%s_sum", field->field_key);
+ else
+ str_printfa(out, "_%s_total", field->field_key);
+ break;
+ case OPENMETRICS_METRIC_TYPE_HISTOGRAM:
+ i_unreached();
+ }
+ /* Labels */
+ if (str_len(req->labels) > 0) {
+ str_append_c(out, '{');
+ str_append_str(out, req->labels);
+ str_append_c(out, '}');
+ }
+ /* Value */
+ switch (req->metric_type) {
+ case OPENMETRICS_METRIC_TYPE_COUNT:
+ str_printfa(out, " %u\n",
+ stats_dist_get_count(metric->duration_stats));
+ break;
+ case OPENMETRICS_METRIC_TYPE_DURATION:
+ /* Convert from microseconds to seconds */
+ str_printfa(out, " %.6f\n",
+ stats_dist_get_sum(metric->duration_stats)/1e6F);
+ break;
+ case OPENMETRICS_METRIC_TYPE_FIELD:
+ field = &metric->fields[req->field_pos];
+ str_printfa(out, " %"PRIu64"\n",
+ stats_dist_get_sum(field->stats));
+ break;
+ case OPENMETRICS_METRIC_TYPE_HISTOGRAM:
+ i_unreached();
+ }
+}
+
+static const struct metric *
+openmetrics_find_histogram_bucket(const struct metric *metric,
+ unsigned int index)
+{
+ struct metric *sub_metric;
+
+ if (!array_is_created(&metric->sub_metrics))
+ return NULL;
+
+ array_foreach_elem(&metric->sub_metrics, sub_metric) {
+ if (sub_metric->group_value.type !=
+ METRIC_VALUE_TYPE_BUCKET_INDEX)
+ continue;
+ if (sub_metric->group_value.intmax == index)
+ return sub_metric;
+ }
+
+ return NULL;
+}
+
+static void
+openmetrics_export_histogram_bucket(struct openmetrics_request *req,
+ string_t *out, const struct metric *metric,
+ intmax_t bucket_limit, int64_t count)
+{
+ /* Metric name */
+ str_append(out, "dovecot_");
+ str_append(out, metric->name);
+ str_append(out, "_bucket");
+ /* Labels */
+ str_append_c(out, '{');
+ if (str_len(req->labels) > 0) {
+ str_append_str(out, req->labels);
+ str_append_c(out, ',');
+ }
+ if (bucket_limit == INTMAX_MAX)
+ str_append(out, "le=\"+Inf\"");
+ else if (strcmp(metric->group_by->field,
+ STATS_EVENT_FIELD_NAME_DURATION) == 0) {
+ /* Convert from microseconds to seconds */
+ str_printfa(out, "le=\"%.6f\"", bucket_limit/1e6F);
+ } else {
+ str_printfa(out, "le=\"%jd\"", bucket_limit);
+ }
+ str_printfa(out, "} %"PRIu64"\n", count);
+}
+
+static void
+openmetrics_export_histogram(struct openmetrics_request *req, string_t *out,
+ const struct metric *metric)
+{
+ const struct stats_metric_settings_group_by *group_by =
+ metric->group_by;
+ float sum = 0;
+ uint64_t count = 0;
+
+ /* Buckets */
+ for (unsigned int i = 0; i < group_by->num_ranges; i++) {
+ const struct metric *sub_metric =
+ openmetrics_find_histogram_bucket(metric, i);
+
+ if (sub_metric != NULL) {
+ sum += stats_dist_get_sum(sub_metric->duration_stats);
+ count += stats_dist_get_count(sub_metric->duration_stats);
+ }
+
+ openmetrics_export_histogram_bucket(req, out, metric,
+ group_by->ranges[i].max,
+ count);
+ }
+
+ /* There is either no data in histogram, which adding the optional
+ sum and count metrics doesn't add any new information or
+ these have already been exported for submetrics. */
+ if (count == 0)
+ return;
+
+ /* Sum */
+ str_append(out, "dovecot_");
+ str_append(out, metric->name);
+ str_append(out, "_sum");
+ /* Labels */
+ if (str_len(req->labels) > 0) {
+ str_append_c(out, '{');
+ str_append_str(out, req->labels);
+ str_append_c(out, '}');
+ }
+ if (strcmp(metric->group_by->field,
+ STATS_EVENT_FIELD_NAME_DURATION) == 0) {
+ /* Convert from microseconds to seconds */
+ sum /= 1e6F;
+ }
+ str_printfa(out, " %.6f\n", sum);
+ /* Count */
+ str_append(out, "dovecot_");
+ str_append(out, metric->name);
+ str_append(out, "_count");
+ /* Labels */
+ if (str_len(req->labels) > 0) {
+ str_append_c(out, '{');
+ str_append_str(out, req->labels);
+ str_append_c(out, '}');
+ }
+ str_printfa(out, " %"PRIu64"\n", count);
+}
+
+static void
+openmetrics_export_metric_header(struct openmetrics_request *req, string_t *out)
+{
+ const struct metric *metric = req->metric;
+ const struct metric_field *field;
+
+ /* Description */
+ str_append(out, "# HELP dovecot_");
+ str_append(out, metric->name);
+ switch (req->metric_type) {
+ case OPENMETRICS_METRIC_TYPE_COUNT:
+ str_append(out, " Total number of all events of this kind");
+ break;
+ case OPENMETRICS_METRIC_TYPE_DURATION:
+ str_append(out, "_duration_seconds Total duration of all events of this kind");
+ break;
+ case OPENMETRICS_METRIC_TYPE_FIELD:
+ field = &metric->fields[req->field_pos];
+ str_printfa(out, "_%s Total of field value for events of this kind",
+ field->field_key);
+ break;
+ case OPENMETRICS_METRIC_TYPE_HISTOGRAM:
+ str_append(out, " Histogram");
+ break;
+ }
+ if (*metric->set->description != '\0') {
+ str_append(out, " of ");
+ str_append(out, metric->set->description);
+ }
+ str_append_c(out, '\n');
+ /* Type */
+ str_append(out, "# TYPE dovecot_");
+ str_append(out, metric->name);
+ switch (req->metric_type) {
+ case OPENMETRICS_METRIC_TYPE_COUNT:
+ str_append(out, " counter\n");
+ break;
+ case OPENMETRICS_METRIC_TYPE_DURATION:
+ str_append(out, "_duration_seconds counter\n");
+ break;
+ case OPENMETRICS_METRIC_TYPE_FIELD:
+ field = &metric->fields[req->field_pos];
+ str_printfa(out, "_%s counter\n", field->field_key);
+ break;
+ case OPENMETRICS_METRIC_TYPE_HISTOGRAM:
+ str_append(out, " histogram\n");
+ break;
+ }
+}
+
+static void
+openmetrics_export_submetric(struct openmetrics_request *req, string_t *out,
+ const struct metric *metric)
+{
+ /* This metric may be a submetric and therefore have a label
+ associated with it. */
+ if (metric->sub_name != NULL) {
+ str_append_c(req->labels, '"');
+ json_append_escaped(req->labels, metric->sub_name);
+ str_append_c(req->labels, '"');
+ }
+
+ if (req->metric_type == OPENMETRICS_METRIC_TYPE_HISTOGRAM) {
+ if (metric->group_by == NULL ||
+ metric->group_by[0].func != STATS_METRIC_GROUPBY_QUANTIZED)
+ return;
+
+ openmetrics_export_histogram(req, out, metric);
+ return;
+ }
+
+ openmetrics_export_metric_value(req, out, metric);
+
+ req->has_submetric = TRUE;
+}
+
+static const struct metric *
+openmetrics_export_sub_metric_get(struct openmetrics_request_sub_metric *reqsm)
+{
+ if (reqsm->sub_index >= array_count(&reqsm->metric->sub_metrics))
+ return NULL;
+
+ return array_idx_elem(&reqsm->metric->sub_metrics, reqsm->sub_index);
+}
+
+static const struct metric *
+openmetrics_export_sub_metric_get_next(
+ struct openmetrics_request_sub_metric *reqsm)
+{
+ /* Get the next valid sub-metric */
+ reqsm->sub_index++;
+ return openmetrics_export_sub_metric_get(reqsm);
+}
+
+static struct openmetrics_request_sub_metric *
+openmetrics_export_sub_metric_down(struct openmetrics_request *req)
+{
+ struct openmetrics_request_sub_metric *reqsm =
+ array_back_modifiable(&req->sub_metric_stack);
+ const struct metric *sub_metric;
+
+ /* Descend further into sub-metric tree */
+
+ if (reqsm->metric->group_by == NULL ||
+ !openmetrics_check_name(reqsm->metric->group_by->field) ||
+ !array_is_created(&reqsm->metric->sub_metrics) ||
+ array_count(&reqsm->metric->sub_metrics) == 0)
+ return NULL;
+ if (reqsm->metric->group_by[0].func == STATS_METRIC_GROUPBY_QUANTIZED) {
+ /* Never descend into quantized group_by sub-metrics.
+ Histograms are exported as a single blob. */
+ return NULL;
+ }
+
+ /* Find sub-metric to descend into */
+ sub_metric = openmetrics_export_sub_metric_get(reqsm);
+ if (sub_metric == NULL) {
+ /* None valid */
+ return NULL;
+ }
+
+ if (str_len(req->labels) > 0)
+ str_append_c(req->labels, ',');
+ str_append(req->labels, reqsm->metric->group_by->field);
+ str_append_c(req->labels, '=');
+ reqsm->labels_pos = str_len(req->labels);
+
+ /* Descend */
+ reqsm = array_append_space(&req->sub_metric_stack);
+ reqsm->metric = sub_metric;
+
+ return reqsm;
+}
+
+static struct openmetrics_request_sub_metric *
+openmetrics_export_sub_metric_up_next(struct openmetrics_request *req)
+{
+ struct openmetrics_request_sub_metric *reqsm;
+ const struct metric *sub_metric = NULL;
+
+ /* Ascend to next sub-metric of an ancestor */
+
+ while (array_count(&req->sub_metric_stack) > 1) {
+ /* Ascend */
+ array_pop_back(&req->sub_metric_stack);
+ reqsm = array_back_modifiable(&req->sub_metric_stack);
+ str_truncate(req->labels, reqsm->labels_pos);
+
+ /* Find next sub-metric */
+ sub_metric = openmetrics_export_sub_metric_get_next(reqsm);
+ if (sub_metric != NULL) {
+ /* None valid */
+ break;
+ }
+ }
+ if (sub_metric == NULL) {
+ /* End of sub-metric tree */
+ return NULL;
+ }
+
+ /* Descend */
+ reqsm = array_append_space(&req->sub_metric_stack);
+ reqsm->metric = sub_metric;
+ return reqsm;
+}
+
+static struct openmetrics_request_sub_metric *
+openmetrics_export_sub_metric_current(struct openmetrics_request *req)
+{
+ struct openmetrics_request_sub_metric *reqsm;
+
+ /* Get state for current sub-metric */
+
+ if (!array_is_created(&req->sub_metric_stack))
+ i_array_init(&req->sub_metric_stack, 8);
+ if (array_count(&req->sub_metric_stack) >= 2) {
+ /* Already walking the sub-metric tree */
+ return array_back_modifiable(&req->sub_metric_stack);
+ }
+
+ /* Start tree walking */
+
+ reqsm = array_append_space(&req->sub_metric_stack);
+ reqsm->metric = req->metric;
+ reqsm->labels_pos = str_len(req->labels);
+
+ return openmetrics_export_sub_metric_down(req);
+}
+
+static bool
+openmetrics_export_sub_metrics(struct openmetrics_request *req, string_t *out)
+{
+ struct openmetrics_request_sub_metric *reqsm = NULL;
+
+ if (!array_is_created(&req->metric->sub_metrics))
+ return TRUE;
+
+ reqsm = openmetrics_export_sub_metric_current(req);
+ if (reqsm == NULL) {
+ /* No valid sub-metrics to export */
+ return TRUE;
+ }
+ openmetrics_export_submetric(req, out, reqsm->metric);
+
+ /* Try do descend into sub-metrics tree for next sub-metric to export.
+ */
+ reqsm = openmetrics_export_sub_metric_down(req);
+ if (reqsm == NULL) {
+ /* Sub-metrics of this metric exhausted; ascend to the next
+ parent sub-metric.
+ */
+ reqsm = openmetrics_export_sub_metric_up_next(req);
+ }
+
+ if (reqsm == NULL) {
+ /* Finished */
+ array_clear(&req->sub_metric_stack);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+openmetrics_export_metric_body(struct openmetrics_request *req, string_t *out)
+{
+ openmetrics_export_metric_value(req, out, req->metric);
+}
+
+static int
+openmetrics_send_buffer(struct openmetrics_request *req, buffer_t *buffer)
+{
+ ssize_t sent;
+
+ if (buffer->used == 0)
+ return 1;
+
+ sent = o_stream_send(req->output, buffer->data, buffer->used);
+ if (sent < 0)
+ return -1;
+
+ /* Max buffer size is enormous */
+ i_assert((size_t)sent == buffer->used);
+
+ if (o_stream_get_buffer_used_size(req->output) >= IO_BLOCK_SIZE)
+ return 0;
+ return 1;
+}
+
+static bool openmetrics_export_has_histogram(struct openmetrics_request *req)
+{
+ const struct metric *metric = req->metric;
+ unsigned int i;
+
+ if (metric->group_by_count == 0) {
+ /* No group_by */
+ return FALSE;
+ }
+
+ /* We can only support quantized group_by when it is the last group
+ item. */
+ for (i = 0; i < (metric->group_by_count - 1); i++) {
+ if (metric->group_by[i].func ==
+ STATS_METRIC_GROUPBY_QUANTIZED)
+ return FALSE;
+ }
+
+ return (metric->group_by[metric->group_by_count - 1].func ==
+ STATS_METRIC_GROUPBY_QUANTIZED);
+}
+
+static void openmetrics_export_next(struct openmetrics_request *req)
+{
+ /* Determine what to export next. */
+ switch (req->metric_type) {
+ case OPENMETRICS_METRIC_TYPE_COUNT:
+ /* Continue with duration output for this metric. */
+ req->metric_type = OPENMETRICS_METRIC_TYPE_DURATION;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ break;
+ case OPENMETRICS_METRIC_TYPE_DURATION:
+ if (openmetrics_export_has_histogram(req)) {
+ /* Continue with histogram output for this metric. */
+ req->metric_type = OPENMETRICS_METRIC_TYPE_HISTOGRAM;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ } else if (req->metric->fields_count > 0) {
+ req->field_pos = 0;
+ req->metric_type = OPENMETRICS_METRIC_TYPE_FIELD;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ } else {
+ /* No histogram; continue with next metric */
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC;
+ }
+ break;
+ case OPENMETRICS_METRIC_TYPE_FIELD:
+ req->field_pos++;
+ if (req->field_pos < req->metric->fields_count) {
+ req->metric_type = OPENMETRICS_METRIC_TYPE_FIELD;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ } else {
+ /* all fields consumed */
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC;
+ }
+ break;
+ case OPENMETRICS_METRIC_TYPE_HISTOGRAM:
+ if (req->metric->fields_count > 0) {
+ /* Continue with fields */
+ req->field_pos = 0;
+ req->metric_type = OPENMETRICS_METRIC_TYPE_FIELD;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ } else {
+ /* Continue with next metric */
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC;
+ }
+ break;
+ }
+}
+
+static void
+openmetrics_export_continue(struct openmetrics_request *req, string_t *out)
+{
+ switch (req->state) {
+ case OPENMETRICS_REQUEST_STATE_INIT:
+ /* Export the Dovecot base metrics. */
+ i_assert(req->stats_iter == NULL);
+ req->stats_iter = stats_metrics_iterate_init(stats_metrics);
+ openmetrics_export_dovecot(out);
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC;
+ break;
+ case OPENMETRICS_REQUEST_STATE_METRIC:
+ /* Export the next metric. */
+ i_assert(req->stats_iter != NULL);
+ do {
+ req->metric = stats_metrics_iterate(req->stats_iter);
+ } while (req->metric != NULL &&
+ !openmetrics_check_name(req->metric->name));
+ if (req->metric == NULL) {
+ /* Finished exporting metrics. */
+ req->state = OPENMETRICS_REQUEST_STATE_FINISHED;
+ break;
+ }
+
+ if (req->labels == NULL)
+ req->labels = str_new(default_pool, 32);
+ else
+ str_truncate(req->labels, 0);
+ req->labels_pos = 0;
+
+ /* Start with count output for this metric if the type
+ is not histogram. If the metric is of type histogram,
+ start with quantiles. */
+ if (openmetrics_export_has_histogram(req))
+ req->metric_type = OPENMETRICS_METRIC_TYPE_HISTOGRAM;
+ else
+ req->metric_type = OPENMETRICS_METRIC_TYPE_COUNT;
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_HEADER;
+ /* Fall through */
+ case OPENMETRICS_REQUEST_STATE_METRIC_HEADER:
+ /* Export the HELP/TYPE header for the current metric */
+ str_truncate(req->labels, req->labels_pos);
+ req->has_submetric = FALSE;
+ if (array_is_created(&req->sub_metric_stack))
+ array_clear(&req->sub_metric_stack);
+ openmetrics_export_metric_header(req, out);
+ req->state = OPENMETRICS_REQUEST_STATE_SUB_METRICS;
+ break;
+ case OPENMETRICS_REQUEST_STATE_SUB_METRICS:
+ /* Export the sub-metrics for the current metric. This will
+ return for each sub-metric, so that the out string buffer
+ stays small. */
+ if (!openmetrics_export_sub_metrics(req, out))
+ break;
+ /* All sub-metrics written. */
+ req->state = OPENMETRICS_REQUEST_STATE_METRIC_BODY;
+ break;
+ case OPENMETRICS_REQUEST_STATE_METRIC_BODY:
+ /* Export the body of the current metric. */
+ str_truncate(req->labels, req->labels_pos);
+ if (req->metric_type == OPENMETRICS_METRIC_TYPE_HISTOGRAM)
+ openmetrics_export_histogram(req, out, req->metric);
+ else
+ openmetrics_export_metric_body(req, out);
+ openmetrics_export_next(req);
+ break;
+ case OPENMETRICS_REQUEST_STATE_FINISHED:
+ i_unreached();
+ }
+}
+
+static void openmetrics_handle_write_error(struct openmetrics_request *req)
+{
+ i_info("openmetrics: write(%s) failed: %s",
+ o_stream_get_name(req->output),
+ o_stream_get_error(req->output));
+ o_stream_destroy(&req->output);
+}
+
+static void openmetrics_request_deinit(struct openmetrics_request *req)
+{
+ stats_metrics_iterate_deinit(&req->stats_iter);
+ str_free(&req->labels);
+ array_free(&req->sub_metric_stack);
+}
+
+static int openmetrics_export(struct openmetrics_request *req)
+{
+ string_t *out;
+ int ret;
+
+ ret = o_stream_flush(req->output);
+ if (ret < 0) {
+ openmetrics_handle_write_error(req);
+ return -1;
+ }
+ if (ret == 0) {
+ /* Output stream buffer needs to be flushed further */
+ return 0;
+ }
+
+ if (req->state == OPENMETRICS_REQUEST_STATE_FINISHED) {
+ /* All metrics were exported already, so we can finish the
+ HTTP request now. */
+ o_stream_destroy(&req->output);
+ return 1;
+ }
+
+ /* Export metrics into a string buffer and write that buffer to the
+ output stream after each (sub-)metric, so that the string buffer
+ stays small. The output stream buffer can grow bigger, but writing is
+ stopped for later resumption when the output stream buffer has grown
+ beyond an optimal size. */
+ out = t_str_new(1024);
+ for (;;) {
+ str_truncate(out, 0);
+
+ openmetrics_export_continue(req, out);
+
+ ret = openmetrics_send_buffer(req, out);
+ if (ret < 0) {
+ openmetrics_handle_write_error(req);
+ return -1;
+ }
+ if (req->state == OPENMETRICS_REQUEST_STATE_FINISHED) {
+ /* Finished export of metrics, but the output stream
+ buffer may still contain data. */
+ break;
+ }
+ if (ret == 0) {
+ /* Output stream buffer is filled up beyond the optimal
+ size; wait until we can write more. */
+ return ret;
+ }
+ }
+
+ /* Send EOF */
+ str_truncate(out, 0);
+ openmetrics_export_eof(out);
+ ret = openmetrics_send_buffer(req, out);
+ if (ret < 0) {
+ openmetrics_handle_write_error(req);
+ return -1;
+ }
+
+ /* Cleanup everything except the output stream */
+ openmetrics_request_deinit(req);
+
+ /* Finished; flush output */
+ ret = o_stream_finish(req->output);
+ if (ret < 0) {
+ openmetrics_handle_write_error(req);
+ return -1;
+ }
+ return ret;
+}
+
+static void openmetrics_request_destroy(struct openmetrics_request *req)
+{
+ o_stream_destroy(&req->output);
+ openmetrics_request_deinit(req);
+}
+
+static void
+stats_service_openmetrics_request(void *context ATTR_UNUSED,
+ struct http_server_request *hsreq,
+ const char *sub_path)
+{
+ const struct http_request *hreq = http_server_request_get(hsreq);
+ struct http_server_response *hsresp;
+ struct openmetrics_request *req;
+ pool_t pool;
+
+ if (strcmp(hreq->method, "OPTIONS") == 0) {
+ hsresp = http_server_response_create(hsreq, 200, "OK");
+ http_server_response_add_header(hsresp, "Allow", "GET");
+ http_server_response_submit(hsresp);
+ return;
+ }
+ if (strcmp(hreq->method, "GET") != 0) {
+ http_server_request_fail_bad_method(hsreq, "GET");
+ return;
+ }
+ if (*sub_path != '\0') {
+ http_server_request_fail(hsreq, 404, "Not Found");
+ return;
+ }
+
+ pool = http_server_request_get_pool(hsreq);
+ req = p_new(pool, struct openmetrics_request, 1);
+
+ http_server_request_set_destroy_callback(
+ hsreq, openmetrics_request_destroy, req);
+
+ hsresp = http_server_response_create(hsreq, 200, "OK");
+ http_server_response_add_header(
+ hsresp, "Content-Type",
+ "application/openmetrics-text; version="OPENMETRICS_CONTENT_VERSION"; "
+ "charset=utf-8");
+
+ req->output = http_server_response_get_payload_output(
+ hsresp, SIZE_MAX, FALSE);
+
+ o_stream_set_flush_callback(req->output, openmetrics_export, req);
+ o_stream_set_flush_pending(req->output, TRUE);
+}
+
+void stats_service_openmetrics_init(void)
+{
+ struct stats_metrics_iter *iter;
+ const struct metric *metric;
+
+ iter = stats_metrics_iterate_init(stats_metrics);
+ while ((metric = stats_metrics_iterate(iter)) != NULL) {
+ if (!openmetrics_check_name(metric->name)) {
+ i_warning(
+ "stats: openmetrics: "
+ "Metric `%s' is not valid for OpenMetrics"
+ "(invalid metric name; skipped)",
+ metric->name);
+ }
+ }
+ stats_metrics_iterate_deinit(&iter);
+
+ stats_http_resource_add("/metrics", "OpenMetrics",
+ stats_service_openmetrics_request, NULL);
+}
diff --git a/src/stats/stats-service-private.h b/src/stats/stats-service-private.h
new file mode 100644
index 0000000..5f5ab20
--- /dev/null
+++ b/src/stats/stats-service-private.h
@@ -0,0 +1,8 @@
+#ifndef STATS_SERVICE_PRIVATE_H
+#define STATS_SERVICE_PRIVATE_H
+
+#include "stats-service.h"
+
+void stats_service_openmetrics_init(void);
+
+#endif
diff --git a/src/stats/stats-service.c b/src/stats/stats-service.c
new file mode 100644
index 0000000..fa67864
--- /dev/null
+++ b/src/stats/stats-service.c
@@ -0,0 +1,15 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "http-server.h"
+#include "stats-service-private.h"
+
+void stats_services_init(void)
+{
+ stats_service_openmetrics_init();
+}
+
+void stats_services_deinit(void)
+{
+ /* Nothing yet */
+}
diff --git a/src/stats/stats-service.h b/src/stats/stats-service.h
new file mode 100644
index 0000000..fab477b
--- /dev/null
+++ b/src/stats/stats-service.h
@@ -0,0 +1,7 @@
+#ifndef STATS_SERVICE_H
+#define STATS_SERVICE_H
+
+void stats_services_init(void);
+void stats_services_deinit(void);
+
+#endif
diff --git a/src/stats/stats-settings.c b/src/stats/stats-settings.c
new file mode 100644
index 0000000..f75a737
--- /dev/null
+++ b/src/stats/stats-settings.c
@@ -0,0 +1,538 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "stats-settings.h"
+#include "array.h"
+
+/* <settings checks> */
+#include "event-filter.h"
+#include <math.h>
+/* </settings checks> */
+
+static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool stats_exporter_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool stats_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings stats_unix_listeners_array[] = {
+ { "stats-reader", 0600, "", "" },
+ { "stats-writer", 0660, "", "$default_internal_group" },
+ { "login/stats-writer", 0600, "$default_login_user", "" },
+};
+static struct file_listener_settings *stats_unix_listeners[] = {
+ &stats_unix_listeners_array[0],
+ &stats_unix_listeners_array[1],
+ &stats_unix_listeners_array[2],
+};
+static buffer_t stats_unix_listeners_buf = {
+ { { stats_unix_listeners, sizeof(stats_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings stats_service_settings = {
+ .name = "stats",
+ .protocol = "",
+ .type = "",
+ .executable = "stats",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &stats_unix_listeners_buf,
+ sizeof(stats_unix_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+};
+
+/*
+ * event_exporter { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_exporter_settings)
+
+static const struct setting_define stats_exporter_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, transport),
+ DEF(STR, transport_args),
+ DEF(TIME_MSECS, transport_timeout),
+ DEF(STR, format),
+ DEF(STR, format_args),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_exporter_settings stats_exporter_default_settings = {
+ .name = "",
+ .transport = "",
+ .transport_args = "",
+ .transport_timeout = 250, /* ms */
+ .format = "",
+ .format_args = "",
+};
+
+const struct setting_parser_info stats_exporter_setting_parser_info = {
+ .defines = stats_exporter_setting_defines,
+ .defaults = &stats_exporter_default_settings,
+
+ .type_offset = offsetof(struct stats_exporter_settings, name),
+ .struct_size = sizeof(struct stats_exporter_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_exporter_settings_check,
+};
+
+/*
+ * metric { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_metric_settings)
+
+static const struct setting_define stats_metric_setting_defines[] = {
+ DEF(STR, metric_name),
+ DEF(STR, fields),
+ DEF(STR, group_by),
+ DEF(STR, filter),
+ DEF(STR, exporter),
+ DEF(STR, exporter_include),
+ DEF(STR, description),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_metric_settings stats_metric_default_settings = {
+ .metric_name = "",
+ .fields = "",
+ .filter = "",
+ .exporter = "",
+ .group_by = "",
+ .exporter_include = STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE,
+ .description = "",
+};
+
+const struct setting_parser_info stats_metric_setting_parser_info = {
+ .defines = stats_metric_setting_defines,
+ .defaults = &stats_metric_default_settings,
+
+ .type_offset = offsetof(struct stats_metric_settings, metric_name),
+ .struct_size = sizeof(struct stats_metric_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_metric_settings_check,
+};
+
+/*
+ * top-level settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_settings)
+#undef DEFLIST_UNIQUE
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct stats_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define stats_setting_defines[] = {
+ DEF(STR, stats_http_rawlog_dir),
+
+ DEFLIST_UNIQUE(metrics, "metric", &stats_metric_setting_parser_info),
+ DEFLIST_UNIQUE(exporters, "event_exporter", &stats_exporter_setting_parser_info),
+ SETTING_DEFINE_LIST_END
+};
+
+const struct stats_settings stats_default_settings = {
+ .stats_http_rawlog_dir = "",
+
+ .metrics = ARRAY_INIT,
+ .exporters = ARRAY_INIT,
+};
+
+const struct setting_parser_info stats_setting_parser_info = {
+ .module_name = "stats",
+ .defines = stats_setting_defines,
+ .defaults = &stats_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct stats_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_settings_check,
+};
+
+/* <settings checks> */
+static bool parse_format_args_set_time(struct stats_exporter_settings *set,
+ enum event_exporter_time_fmt fmt,
+ const char **error_r)
+{
+ if ((set->parsed_time_format != EVENT_EXPORTER_TIME_FMT_NATIVE) &&
+ (set->parsed_time_format != fmt)) {
+ *error_r = t_strdup_printf("Exporter '%s' specifies multiple "
+ "time format args", set->name);
+ return FALSE;
+ }
+
+ set->parsed_time_format = fmt;
+
+ return TRUE;
+}
+
+static bool parse_format_args(struct stats_exporter_settings *set,
+ const char **error_r)
+{
+ const char *const *tmp;
+
+ /* Defaults */
+ set->parsed_time_format = EVENT_EXPORTER_TIME_FMT_NATIVE;
+
+ tmp = t_strsplit_spaces(set->format_args, " ");
+
+ /*
+ * If the config contains multiple types of the same type (e.g.,
+ * both time-rfc3339 and time-unix) we fail the config check.
+ *
+ * Note: At the moment, we have only time-* tokens. In the future
+ * when we have other tokens, they should be parsed here.
+ */
+ for (; *tmp != NULL; tmp++) {
+ enum event_exporter_time_fmt fmt;
+
+ if (strcmp(*tmp, "time-rfc3339") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_RFC3339;
+ } else if (strcmp(*tmp, "time-unix") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_UNIX;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format "
+ "arg: %s", *tmp);
+ return FALSE;
+ }
+
+ if (!parse_format_args_set_time(set, fmt, error_r))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool stats_exporter_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_exporter_settings *set = _set;
+ bool time_fmt_required;
+
+ if (set->name[0] == '\0') {
+ *error_r = "Exporter name can't be empty";
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->format[0] == '\0') {
+ *error_r = "Exporter format name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->format, "none") == 0) {
+ time_fmt_required = FALSE;
+ } else if (strcmp(set->format, "json") == 0) {
+ time_fmt_required = TRUE;
+ } else if (strcmp(set->format, "tab-text") == 0) {
+ time_fmt_required = TRUE;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format '%s'",
+ set->format);
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->transport[0] == '\0') {
+ *error_r = "Exporter transport name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->transport, "drop") == 0 ||
+ strcmp(set->transport, "http-post") == 0 ||
+ strcmp(set->transport, "log") == 0) {
+ /* no-op */
+ } else {
+ *error_r = t_strdup_printf("Unknown transport type '%s'",
+ set->transport);
+ return FALSE;
+ }
+
+ if (!parse_format_args(set, error_r))
+ return FALSE;
+
+ /* Some formats don't have a native way of serializing time stamps */
+ if (time_fmt_required &&
+ set->parsed_time_format == EVENT_EXPORTER_TIME_FMT_NATIVE) {
+ *error_r = t_strdup_printf("%s exporter format requires a "
+ "time-* argument", set->format);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_common(const char *func,
+ const char *const *params,
+ intmax_t *min_r,
+ intmax_t *max_r,
+ intmax_t *other_r,
+ const char **error_r)
+{
+ intmax_t min, max, other;
+
+ if ((str_array_length(params) != 3) ||
+ (str_to_intmax(params[0], &min) < 0) ||
+ (str_to_intmax(params[1], &max) < 0) ||
+ (str_to_intmax(params[2], &other) < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function takes "
+ "3 int args", func);
+ return FALSE;
+ }
+
+ if ((min < 0) || (max < 0) || (other < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "arguments must be >= 0", func);
+ return FALSE;
+ }
+
+ if (min >= max) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "min must be < max (%ju must be < %ju)",
+ func, min, max);
+ return FALSE;
+ }
+
+ *min_r = min;
+ *max_r = max;
+ *other_r = other;
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_exp(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, base;
+
+ if (!parse_metric_group_by_common("exponential", params, &min, &max, &base, error_r))
+ return FALSE;
+
+ if ((base != 2) && (base != 10)) {
+ *error_r = t_strdup_printf("group_by 'exponential' aggregate function "
+ "base must be one of: 2, 10 (base=%ju)",
+ base);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'base^min'. The last bucket is also special - it
+ * contains everything greater than 'base^max'.
+ *
+ * The second bucket begins at 'base^min + 1', the third bucket
+ * begins at 'base^(min + 1) + 1', and so on.
+ */
+ group_by->num_ranges = max - min + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = pow(base, min);
+ group_by->ranges[group_by->num_ranges - 1].min = pow(base, max);
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = pow(base, min + (i - 1));
+ group_by->ranges[i].max = pow(base, min + i);
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_lin(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, step;
+
+ if (!parse_metric_group_by_common("linear", params, &min, &max, &step, error_r))
+ return FALSE;
+
+ if ((min + step) > max) {
+ *error_r = t_strdup_printf("group_by 'linear' aggregate function "
+ "min+step must be <= max (%ju must be <= %ju)",
+ min + step, max);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'min'. The last bucket is also special - it contains
+ * everything greater than 'max'.
+ *
+ * The second bucket begins at 'min + 1', the third bucket begins at
+ * 'min + 1 * step + 1', the fourth at 'min + 2 * step + 1', and so on.
+ */
+ group_by->num_ranges = (max - min) / step + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = min;
+ group_by->ranges[group_by->num_ranges - 1].min = max;
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = min + (i - 1) * step;
+ group_by->ranges[i].max = min + i * step;
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by(struct stats_metric_settings *set,
+ pool_t pool, const char **error_r)
+{
+ const char *const *tmp = t_strsplit_spaces(set->group_by, " ");
+
+ if (tmp[0] == NULL)
+ return TRUE;
+
+ p_array_init(&set->parsed_group_by, pool, str_array_length(tmp));
+
+ /* For each group_by field */
+ for (; *tmp != NULL; tmp++) {
+ struct stats_metric_settings_group_by group_by;
+ const char *const *params;
+
+ i_zero(&group_by);
+
+ /* <field name>:<aggregation func>... */
+ params = t_strsplit(*tmp, ":");
+
+ if (params[1] == NULL) {
+ /* <field name> - alias for <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ } else if (strcmp(params[1], "discrete") == 0) {
+ /* <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ if (params[2] != NULL) {
+ *error_r = "group_by 'discrete' aggregate function "
+ "does not take any args";
+ return FALSE;
+ }
+ } else if (strcmp(params[1], "exponential") == 0) {
+ /* <field>:exponential:<min mag>:<max mag>:<base> */
+ if (!parse_metric_group_by_exp(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else if (strcmp(params[1], "linear") == 0) {
+ /* <field>:linear:<min val>:<max val>:<step> */
+ if (!parse_metric_group_by_lin(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else {
+ *error_r = t_strdup_printf("unknown aggregation function "
+ "'%s' on field '%s'", params[1], params[0]);
+ return FALSE;
+ }
+
+ group_by.field = p_strdup(pool, params[0]);
+
+ array_push_back(&set->parsed_group_by, &group_by);
+ }
+
+ return TRUE;
+}
+
+static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r)
+{
+ struct stats_metric_settings *set = _set;
+
+ if (set->metric_name[0] == '\0') {
+ *error_r = "Metric name can't be empty";
+ return FALSE;
+ }
+
+ if (set->filter[0] == '\0') {
+ *error_r = t_strdup_printf("metric %s { filter } is empty - "
+ "will not match anything", set->metric_name);
+ return FALSE;
+ }
+
+ set->parsed_filter = event_filter_create_fragment(pool);
+ if (event_filter_parse(set->filter, set->parsed_filter, error_r) < 0)
+ return FALSE;
+
+ if (!parse_metric_group_by(set, pool, error_r))
+ return FALSE;
+
+ return TRUE;
+}
+
+static bool stats_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_settings *set = _set;
+ struct stats_exporter_settings *exporter;
+ struct stats_metric_settings *metric;
+
+ if (!array_is_created(&set->metrics) || !array_is_created(&set->exporters))
+ return TRUE;
+
+ /* check that all metrics refer to exporters that exist */
+ array_foreach_elem(&set->metrics, metric) {
+ bool found = FALSE;
+
+ if (metric->exporter[0] == '\0')
+ continue; /* metric not exported */
+
+ array_foreach_elem(&set->exporters, exporter) {
+ if (strcmp(metric->exporter, exporter->name) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ *error_r = t_strdup_printf("metric %s refers to "
+ "non-existent exporter '%s'",
+ metric->metric_name,
+ metric->exporter);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/stats/stats-settings.h b/src/stats/stats-settings.h
new file mode 100644
index 0000000..54b7f9e
--- /dev/null
+++ b/src/stats/stats-settings.h
@@ -0,0 +1,125 @@
+#ifndef STATS_SETTINGS_H
+#define STATS_SETTINGS_H
+
+#define STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE \
+ "name hostname timestamps categories fields"
+
+/* <settings checks> */
+/*
+ * We allow a selection of a timestamp format.
+ *
+ * The 'time-unix' format generates a number with the number of seconds
+ * since 1970-01-01 00:00 UTC.
+ *
+ * The 'time-rfc3339' format uses the YYYY-MM-DDTHH:MM:SS.uuuuuuZ format as
+ * defined by RFC 3339.
+ *
+ * The special native format (not explicitly selectable in the config, but
+ * default if no time-* token is used) uses the format's native timestamp
+ * format. Note that not all formats have a timestamp data format.
+ *
+ * The native format and the rules below try to address the question: can a
+ * parser that doesn't have any knowledge of fields' values' types losslessly
+ * reconstruct the fields?
+ *
+ * For example, JSON only has strings and numbers, so it cannot represent a
+ * timestamp in a "context-free lossless" way. Therefore, when making a
+ * JSON blob, we need to decide which way to serialize timestamps. No
+ * matter how we do it, we incur some loss. If a decoder sees 1557232304 in
+ * a field, it cannot be certain if the field is an integer that just
+ * happens to be a reasonable timestamp, or if it actually is a timestamp.
+ * Same goes with RFC3339 - it could just be that the user supplied a string
+ * that looks like a timestamp, and that string made it into an event field.
+ *
+ * Other common serialization formats, such as CBOR, have a lossless way of
+ * encoding timestamps.
+ *
+ * Note that there are two concepts at play: native and default.
+ *
+ * The rules for how the format's timestamp formats are used:
+ *
+ * 1. The default time format is the native format.
+ * 2. The native time format may or may not exist for a given format (e.g.,
+ * in JSON)
+ * 3. If the native format doesn't exist and no time format was specified in
+ * the config, it is a config error.
+ *
+ * We went with these rules because:
+ *
+ * 1. It prevents type information loss by default.
+ * 2. It completely isolates the policy from the algorithm.
+ * 3. It defers the decision whether each format without a native timestamp
+ * type should have a default acting as native until after we've had some
+ * operational experience.
+ * 4. A future decision to add a default (via 3. point) will be 100% compatible.
+ */
+enum event_exporter_time_fmt {
+ EVENT_EXPORTER_TIME_FMT_NATIVE = 0,
+ EVENT_EXPORTER_TIME_FMT_UNIX,
+ EVENT_EXPORTER_TIME_FMT_RFC3339,
+};
+/* </settings checks> */
+
+struct stats_exporter_settings {
+ const char *name;
+ const char *transport;
+ const char *transport_args;
+ unsigned int transport_timeout;
+ const char *format;
+ const char *format_args;
+
+ /* parsed values */
+ enum event_exporter_time_fmt parsed_time_format;
+};
+
+/* <settings checks> */
+enum stats_metric_group_by_func {
+ STATS_METRIC_GROUPBY_DISCRETE = 0,
+ STATS_METRIC_GROUPBY_QUANTIZED,
+};
+
+/*
+ * A range covering a stats bucket. The the interval is half closed - the
+ * minimum is excluded and the maximum is included. In other words: (min, max].
+ * Because we don't have a +Inf and -Inf, we use INTMAX_MIN and INTMAX_MAX
+ * respectively.
+ */
+struct stats_metric_settings_bucket_range {
+ intmax_t min;
+ intmax_t max;
+};
+
+struct stats_metric_settings_group_by {
+ const char *field;
+ enum stats_metric_group_by_func func;
+ unsigned int num_ranges;
+ struct stats_metric_settings_bucket_range *ranges;
+};
+/* </settings checks> */
+
+struct stats_metric_settings {
+ const char *metric_name;
+ const char *description;
+ const char *fields;
+ const char *group_by;
+ const char *filter;
+
+ ARRAY(struct stats_metric_settings_group_by) parsed_group_by;
+ struct event_filter *parsed_filter;
+
+ /* exporter related fields */
+ const char *exporter;
+ const char *exporter_include;
+};
+
+struct stats_settings {
+ const char *stats_http_rawlog_dir;
+
+ ARRAY(struct stats_exporter_settings *) exporters;
+ ARRAY(struct stats_metric_settings *) metrics;
+};
+
+extern const struct setting_parser_info stats_setting_parser_info;
+extern const struct setting_parser_info stats_metric_setting_parser_info;
+
+#endif
diff --git a/src/stats/test-client-reader.c b/src/stats/test-client-reader.c
new file mode 100644
index 0000000..e9eed0c
--- /dev/null
+++ b/src/stats/test-client-reader.c
@@ -0,0 +1,235 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-reader.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct connection_list *conn_list;
+
+struct test_connection {
+ struct connection conn;
+
+ unsigned int row_count;
+};
+
+static void test_reader_server_destroy(struct connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-reader-server",
+ .service_name_out = "stats-reader-client",
+ .major_version = 2,
+ .minor_version = 0,
+ .allow_empty_args_input = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+};
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (stats_metrics != NULL) {
+ stats_metrics_event(stats_metrics, event, ctx);
+ struct event_filter *filter =
+ stats_metrics_get_event_filter(stats_metrics);
+ return !event_filter_match(filter, event, ctx);
+ }
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/metric_name=test\n"
+"metric/test/filter=event=test\n"
+"\n";
+
+static int test_reader_server_input_args(struct connection *conn ATTR_UNUSED,
+ const char *const *args)
+{
+ if (args[0] == NULL)
+ return -1;
+
+ test_assert_strcmp(args[0], "test");
+ test_assert_strcmp(args[1], "1");
+
+ return 1;
+}
+
+static void test_dump_metrics(void)
+{
+ int fds[2];
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ struct connection *conn = i_new(struct connection, 1);
+
+ struct ioloop *loop = io_loop_create();
+
+ client_reader_create(fds[1]);
+ connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+ o_stream_nsend_str(conn->output, "DUMP\tcount\n");
+
+ io_loop_run(loop);
+ connection_deinit(conn);
+ i_free(conn);
+
+ /* allow client-reader to finish up */
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+
+ io_loop_destroy(&loop);
+}
+
+static void test_client_reader(void)
+{
+ const struct connection_vfuncs client_vfuncs = {
+ .input_args = test_reader_server_input_args,
+ .destroy = test_reader_server_destroy,
+ };
+
+ test_begin("client reader");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ client_readers_init();
+ conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ /* check output from reader */
+ test_dump_metrics();
+
+ test_deinit();
+
+ client_readers_deinit();
+ connection_list_deinit(&conn_list);
+
+ test_end();
+}
+
+static const char *settings_blob_2 =
+"metric=test\n"
+"metric/test/metric_name=test\n"
+"metric/test/filter=event=test\n"
+"metric/test/group_by=test_name\n"
+"\n";
+
+static int
+test_reader_server_input_args_group_by(struct connection *conn,
+ const char *const *args)
+{
+ struct test_connection *tconn =
+ container_of(conn, struct test_connection, conn);
+
+ if (args[0] == NULL)
+ return -1;
+
+ tconn->row_count++;
+
+ if (tconn->row_count == 1) {
+ test_assert_strcmp(args[0], "test");
+ test_assert_strcmp(args[1], "1");
+ } else if (tconn->row_count == 2) {
+ test_assert_strcmp(args[0], "test_alpha");
+ test_assert_strcmp(args[1], "1");
+ }
+ return 1;
+}
+
+static void test_dump_metrics_group_by(void)
+{
+ int fds[2];
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ struct test_connection *conn = i_new(struct test_connection, 1);
+
+ struct ioloop *loop = io_loop_create();
+
+ client_reader_create(fds[1]);
+ connection_init_client_fd(conn_list, &conn->conn, "stats", fds[0], fds[0]);
+ o_stream_nsend_str(conn->conn.output, "DUMP\tcount\n");
+
+ io_loop_run(loop);
+ connection_deinit(&conn->conn);
+ i_free(conn);
+
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+
+ io_loop_destroy(&loop);
+}
+
+static void test_client_reader_group_by(void)
+{
+ const struct connection_vfuncs client_vfuncs = {
+ .input_args = test_reader_server_input_args_group_by,
+ .destroy = test_reader_server_destroy,
+ };
+
+ test_begin("client reader (group by)");
+
+ /* register some stats */
+ test_init(settings_blob_2);
+
+ client_readers_init();
+ conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "event_name", "alpha");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ /* check output from reader */
+ test_dump_metrics_group_by();
+
+ test_deinit();
+
+ client_readers_deinit();
+ connection_list_deinit(&conn_list);
+
+ test_end();
+}
+
+int main(void) {
+ /* fake master service to pretend destroying
+ connections. */
+ struct master_service local_master_service = {
+ .stopping = TRUE,
+ .total_available_count = 100,
+ .service_count_left = 100,
+ };
+ void (*const test_functions[])(void) = {
+ test_client_reader,
+ test_client_reader_group_by,
+ NULL
+ };
+
+ master_service = &local_master_service;
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}
diff --git a/src/stats/test-client-writer.c b/src/stats/test-client-writer.c
new file mode 100644
index 0000000..1256da9
--- /dev/null
+++ b/src/stats/test-client-writer.c
@@ -0,0 +1,152 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-writer.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct event *last_sent_event = NULL;
+static bool recurse_back = FALSE;
+static struct connection_list *conn_list;
+
+static void test_writer_server_destroy(struct connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+}
+
+static int test_writer_server_input_args(struct connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ /* check filter */
+ test_assert_strcmp(args[0], "FILTER");
+ test_assert_strcmp(args[1], "(event=\"test\")");
+ /* send commands now */
+ string_t *send_buf = t_str_new(128);
+ o_stream_nsend_str(conn->output, "CATEGORY\ttest\n");
+ str_printfa(send_buf, "BEGIN\t%"PRIu64"\t0\t0\t", last_sent_event->id);
+ event_export(last_sent_event, send_buf);
+ str_append_c(send_buf, '\n');
+ o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+ str_truncate(send_buf, 0);
+ str_printfa(send_buf, "END\t%"PRIu64"\n", last_sent_event->id);
+ o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+ /* disconnect immediately */
+ return -1;
+}
+
+static struct connection_settings 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 client_vfuncs = {
+ .input_args = test_writer_server_input_args,
+ .destroy = test_writer_server_destroy,
+};
+
+static void test_write_one(struct event *event ATTR_UNUSED)
+{
+ int fds[2];
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ struct connection *conn = i_new(struct connection, 1);
+
+ struct ioloop *loop = io_loop_create();
+
+ client_writer_create(fds[1]);
+ connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+
+ last_sent_event = event;
+ io_loop_run(loop);
+ last_sent_event = NULL;
+ connection_deinit(conn);
+ i_free(conn);
+
+ /* client-writer needs two loops to deinit */
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+
+ io_loop_destroy(&loop);
+}
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx ATTR_UNUSED,
+ const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (recurse_back)
+ return TRUE;
+
+ recurse_back = TRUE;
+ if (stats_metrics != NULL) {
+ test_write_one(event);
+ }
+ recurse_back = FALSE;
+
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/metric_name=test\n"
+"metric/test/filter=event=test\n"
+"\n";
+
+static void test_client_writer(void)
+{
+ test_begin("client writer");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ client_writers_init();
+ conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+
+ client_writers_deinit();
+ connection_list_deinit(&conn_list);
+
+ test_end();
+}
+
+int main(void) {
+ /* fake master service to pretend destroying
+ connections. */
+ struct master_service local_master_service = {
+ .stopping = TRUE,
+ .total_available_count = 100,
+ .service_count_left = 100,
+ };
+ void (*const test_functions[])(void) = {
+ test_client_writer,
+ NULL
+ };
+
+ master_service = &local_master_service;
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}
diff --git a/src/stats/test-stats-common.c b/src/stats/test-stats-common.c
new file mode 100644
index 0000000..3e307a9
--- /dev/null
+++ b/src/stats/test-stats-common.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include <time.h>
+#include <unistd.h>
+
+struct event_category test_category = {
+ .name = "test",
+};
+
+struct event_category child_test_category = {
+ .name = "child",
+ .parent = &test_category,
+};
+
+pool_t test_pool;
+struct stats_metrics *stats_metrics = NULL;
+time_t stats_startup_time;
+
+static bool callback_added = FALSE;
+
+static struct stats_settings *read_settings(const char *settings)
+{
+ struct istream *is = test_istream_create(settings);
+ const char *error;
+ struct setting_parser_context *ctx =
+ settings_parser_init(test_pool, &stats_setting_parser_info, 0);
+ if (settings_parse_stream_read(ctx, is) < 0)
+ i_fatal("Failed to parse settings: %s",
+ settings_parser_get_error(ctx));
+ if (!settings_parser_check(ctx, test_pool, &error))
+ i_fatal("Failed to parse settings: %s",
+ error);
+ struct stats_settings *set = settings_parser_get(ctx);
+ settings_parser_deinit(&ctx);
+ i_stream_unref(&is);
+ return set;
+}
+
+void test_init(const char *settings_blob)
+{
+ if (!callback_added) {
+ event_register_callback(test_stats_callback);
+ callback_added = TRUE;
+ }
+
+ stats_event_categories_init();
+ test_pool = pool_alloconly_create(MEMPOOL_GROWING"test pool", 2048);
+ stats_startup_time = time(NULL);
+
+ /* register test categories */
+ stats_event_category_register(test_category.name, NULL);
+ stats_event_category_register(child_test_category.name,
+ &test_category);
+ struct stats_settings *set = read_settings(settings_blob);
+ stats_metrics = stats_metrics_init(set);
+}
+
+void test_deinit(void)
+{
+ stats_metrics_deinit(&stats_metrics);
+ stats_event_categories_deinit();
+ pool_unref(&test_pool);
+}
+
+void test_event_send(struct event *event)
+{
+ struct failure_context ctx = {
+ .type = LOG_TYPE_DEBUG,
+ };
+
+ usleep(1); /* make sure duration>0 always */
+ event_send(event, &ctx, "hello");
+}
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field)
+{
+ struct stats_metrics_iter *iter =
+ stats_metrics_iterate_init(stats_metrics);
+ const struct metric *metric;
+ while((metric = stats_metrics_iterate(iter)) != NULL)
+ if (strcmp(metric->name, metric_name) == 0)
+ break;
+
+ /* bug in test if not found */
+ i_assert(metric != NULL);
+
+ stats_metrics_iterate_deinit(&iter);
+
+ switch(field) {
+ case STATS_DIST_COUNT:
+ return stats_dist_get_count(metric->duration_stats);
+ case STATS_DIST_SUM:
+ return stats_dist_get_sum(metric->duration_stats);
+ default:
+ i_unreached();
+ }
+}
+
diff --git a/src/stats/test-stats-common.h b/src/stats/test-stats-common.h
new file mode 100644
index 0000000..86c03f4
--- /dev/null
+++ b/src/stats/test-stats-common.h
@@ -0,0 +1,36 @@
+#ifndef TEST_STATS_COMMON
+#define TEST_STATS_COMMON 1
+
+#include "stats-common.h"
+#include "event-filter.h"
+#include "istream.h"
+#include "settings-parser.h"
+#include "str.h"
+#include "test-common.h"
+#include "lib-event-private.h"
+#include "stats-dist.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+
+extern struct event_category test_category;
+extern struct event_category child_test_category;
+extern pool_t test_pool;
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED);
+
+void test_init(const char *settings_blob);
+void test_deinit(void);
+
+void test_event_send(struct event *event);
+
+enum stats_dist_field {
+ STATS_DIST_COUNT,
+ STATS_DIST_SUM,
+};
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field);
+
+#endif
diff --git a/src/stats/test-stats-metrics.c b/src/stats/test-stats-metrics.c
new file mode 100644
index 0000000..c384014
--- /dev/null
+++ b/src/stats/test-stats-metrics.c
@@ -0,0 +1,449 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "array.h"
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (stats_metrics != NULL) {
+ stats_metrics_event(stats_metrics, event, ctx);
+ struct event_filter *filter =
+ stats_metrics_get_event_filter(stats_metrics);
+ return !event_filter_match(filter, event, ctx);
+ }
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/metric_name=test\n"
+"metric/test/filter=event=test\n"
+"\n";
+
+static void test_stats_metrics(void)
+{
+ test_begin("stats metrics (event counting)");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+ test_end();
+}
+
+static const char *settings_blob_2 =
+"metric=test\n"
+"metric/test/metric_name=test\n"
+"metric/test/filter=(event=test AND test_field=value)\n"
+"\n";
+
+static void test_stats_metrics_filter(void)
+{
+ test_begin("stats metrics (filter)");
+
+ test_init(settings_blob_2);
+
+ /* check filter */
+ struct event_filter *filter =
+ stats_metrics_get_event_filter(stats_metrics);
+ string_t *str_filter = t_str_new(64);
+ event_filter_export(filter, str_filter);
+ test_assert_strcmp("((event=\"test\" AND \"test_field\"=\"value\"))",
+ str_c(str_filter));
+
+ /* send event */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_field", "value");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ /* send another event */
+ event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_field", "nother value");
+ e_debug(event, "test");
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+ test_end();
+}
+
+static void test_stats_metrics_group_by_check_one(const struct metric *metric,
+ const char *sub_name,
+ unsigned int total_count,
+ unsigned int submetric_count,
+ unsigned int group_by_count,
+ enum stats_metric_group_by_func group_by_func,
+ const char *group_by_field,
+ enum metric_value_type value_type)
+{
+ test_assert_strcmp(metric->name, "test");
+
+ if (sub_name != NULL)
+ test_assert_strcmp(metric->sub_name, sub_name);
+ else
+ test_assert(metric->sub_name == NULL);
+
+ test_assert(stats_dist_get_count(metric->duration_stats) == total_count);
+
+ if (submetric_count > 0) {
+ test_assert(array_is_created(&metric->sub_metrics));
+ test_assert(array_count(&metric->sub_metrics) == submetric_count);
+ } else {
+ test_assert(!array_is_created(&metric->sub_metrics));
+ }
+
+ if (group_by_count > 0) {
+ test_assert(metric->group_by_count == group_by_count);
+ i_assert(metric->group_by != NULL);
+ test_assert(metric->group_by[0].func == group_by_func);
+ test_assert_strcmp(metric->group_by[0].field, group_by_field);
+ } else {
+ test_assert(metric->group_by_count == 0);
+ test_assert(metric->group_by == NULL);
+ }
+
+ test_assert(metric->group_value.type == value_type);
+}
+
+#define DISCRETE_TEST_VAL_COUNT 3
+struct discrete_test {
+ const char *settings_blob;
+ unsigned int num_values;
+ const char *values_first[DISCRETE_TEST_VAL_COUNT];
+ const char *values_second[DISCRETE_TEST_VAL_COUNT];
+};
+
+static const struct discrete_test discrete_tests[] = {
+ {
+ "test_name sub_name",
+ 3,
+ { "eta", "kappa", "nu", },
+ { "upsilon", "pi", "epsilon", },
+ },
+ {
+ "test_name:discrete sub_name:discrete",
+ 3,
+ { "apple", "bannana", "orange", },
+ { "pie", "yoghurt", "cobbler", },
+ },
+ {
+ "test_name sub_name:discrete",
+ 3,
+ { "apollo", "gaia", "hermes", },
+ { "thor", "odin", "loki", },
+ },
+};
+
+static void test_stats_metrics_group_by_discrete_real(const struct discrete_test *test)
+{
+ struct event *event;
+ unsigned int i, j;
+
+ test_begin(t_strdup_printf("stats metrics (discrete group by) - %s",
+ test->settings_blob));
+
+ test_init(t_strdup_printf("metric=test\n"
+ "metric/test/metric_name=test\n"
+ "metric/test/filter=event=test\n"
+ "metric/test/group_by=%s\n"
+ "\n", test->settings_blob));
+
+ for (i = 0; i < test->num_values; i++) {
+ for (j = 0; j < test->num_values; j++) {
+ event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_name", test->values_first[i]);
+ event_add_str(event, "sub_name", test->values_second[j]);
+ test_event_send(event);
+ event_unref(&event);
+ }
+ }
+
+ /* check total number of events */
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == test->num_values * test->num_values);
+
+ /* analyze the structure */
+ struct stats_metrics_iter *iter = stats_metrics_iterate_init(stats_metrics);
+ const struct metric *root_metric = stats_metrics_iterate(iter);
+ stats_metrics_iterate_deinit(&iter);
+
+ test_stats_metrics_group_by_check_one(root_metric,
+ NULL,
+ test->num_values * test->num_values,
+ test->num_values,
+ 2, STATS_METRIC_GROUPBY_DISCRETE,
+ "test_name", 0);
+
+ struct metric *const *first = array_idx(&root_metric->sub_metrics, 0);
+
+ /* examime each sub-metric */
+ for (i = 0; i < test->num_values; i++) {
+ test_stats_metrics_group_by_check_one(first[i],
+ test->values_first[i],
+ test->num_values,
+ test->num_values,
+ 1, STATS_METRIC_GROUPBY_DISCRETE,
+ "sub_name",
+ METRIC_VALUE_TYPE_STR);
+
+ struct metric *const *second = array_idx(&first[i]->sub_metrics, 0);
+
+ /* examine each sub-sub-metric */
+ for (j = 0; j < test->num_values; j++) {
+ test_stats_metrics_group_by_check_one(second[j],
+ test->values_second[j],
+ 1, 0, 0, 0, NULL,
+ METRIC_VALUE_TYPE_STR);
+ }
+ }
+
+ test_deinit();
+ test_end();
+}
+
+static void test_stats_metrics_group_by_discrete(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(discrete_tests); i++)
+ test_stats_metrics_group_by_discrete_real(&discrete_tests[i]);
+}
+
+#define QUANTIZED_TEST_VAL_COUNT 15
+struct quantized_test {
+ const char *settings_blob;
+ unsigned int num_inputs;
+ intmax_t input_vals[QUANTIZED_TEST_VAL_COUNT];
+
+ unsigned int num_sub_metrics;
+
+ unsigned int num_ranges;
+ struct {
+ struct stats_metric_settings_bucket_range range;
+ intmax_t count;
+ } ranges[QUANTIZED_TEST_VAL_COUNT];
+};
+
+static const struct quantized_test quantized_tests[] = {
+ {
+ "linear:100:1000:100",
+ 13,
+ { 0, 50, 100, 101, 200, 201, 250, 301, 900, 901, 1000, 1001, 2000 },
+ 7,
+ 11,
+ { { { INTMAX_MIN, 100 }, 3 },
+ { { 100, 200 }, 2 },
+ { { 200, 300 }, 2 },
+ { { 300, 400 }, 1 },
+ { { 400, 500 }, 0 },
+ { { 500, 600 }, 0 },
+ { { 600, 700 }, 0 },
+ { { 700, 800 }, 0 },
+ { { 800, 900 }, 1 },
+ { { 900, 1000 }, 2 },
+ { { 1000, INTMAX_MAX }, 2 },
+ }
+ },
+ {
+ /* start at 0 */
+ "exponential:0:6:10",
+ 12,
+ { 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
+ 7,
+ 8,
+ { { { INTMAX_MIN, 1 }, 1 },
+ { { 1, 10 }, 2 },
+ { { 10, 100 }, 2 },
+ { { 100, 1000 }, 3 },
+ { { 1000, 10000 }, 1 },
+ { { 10000, 100000 }, 0 },
+ { { 100000, 1000000 }, 1 },
+ { { 1000000, INTMAX_MAX }, 2 },
+ }
+ },
+ {
+ /* start at 0 */
+ "exponential:0:6:2",
+ 9,
+ { 0, 1, 2, 4, 5, 20, 64, 65, 100 },
+ 7,
+ 8,
+ { { { INTMAX_MIN, 1 }, 2 },
+ { { 1, 2 }, 1 },
+ { { 2, 4 }, 1 },
+ { { 4, 8 }, 1 },
+ { { 8, 16 }, 0 },
+ { { 16, 32 }, 1 },
+ { { 32, 64 }, 1 },
+ { { 64, INTMAX_MAX }, 2 },
+ }
+ },
+ {
+ /* start at >0 */
+ "exponential:2:6:10",
+ 12,
+ { 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
+ 5,
+ 6,
+ { { { INTMAX_MIN, 100 }, 5 },
+ { { 100, 1000 }, 3 },
+ { { 1000, 10000 }, 1 },
+ { { 10000, 100000 }, 0 },
+ { { 100000, 1000000 }, 1 },
+ { { 1000000, INTMAX_MAX }, 2 },
+ }
+ },
+ {
+ /* start at >0 */
+ "exponential:2:6:2",
+ 9,
+ { 0, 1, 2, 4, 5, 20, 64, 65, 100 },
+ 5,
+ 6,
+ { { { INTMAX_MIN, 4 }, 4 },
+ { { 4, 8 }, 1 },
+ { { 8, 16 }, 0 },
+ { { 16, 32 }, 1 },
+ { { 32, 64 }, 1 },
+ { { 64, INTMAX_MAX }, 2 },
+ }
+ },
+};
+
+static void test_stats_metrics_group_by_quantized_real(const struct quantized_test *test)
+{
+ unsigned int i;
+
+ test_begin(t_strdup_printf("stats metrics (quantized group by) - %s",
+ test->settings_blob));
+
+ test_init(t_strdup_printf("metric=test\n"
+ "metric/test/metric_name=test\n"
+ "metric/test/filter=event=test\n"
+ "metric/test/group_by=test_name foobar:%s\n"
+ "\n", test->settings_blob));
+
+ struct event *event;
+
+ for (i = 0; i < test->num_inputs; i++) {
+ event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_name", "alpha");
+ event_add_int(event, "foobar", test->input_vals[i]);
+ test_event_send(event);
+ event_unref(&event);
+ }
+
+ /* check total number of events */
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == test->num_inputs);
+
+ /* analyze the structure */
+ struct stats_metrics_iter *iter = stats_metrics_iterate_init(stats_metrics);
+ const struct metric *root_metric = stats_metrics_iterate(iter);
+ stats_metrics_iterate_deinit(&iter);
+
+ test_stats_metrics_group_by_check_one(root_metric, NULL, test->num_inputs,
+ 1, 2, STATS_METRIC_GROUPBY_DISCRETE,
+ "test_name", 0);
+
+ /* examine first level sub-metric */
+ struct metric *const *first = array_idx(&root_metric->sub_metrics, 0);
+ test_stats_metrics_group_by_check_one(first[0],
+ "alpha",
+ test->num_inputs,
+ test->num_sub_metrics,
+ 1,
+ STATS_METRIC_GROUPBY_QUANTIZED,
+ "foobar",
+ METRIC_VALUE_TYPE_STR);
+
+ /* check the ranges */
+ test_assert(first[0]->group_by[0].num_ranges == test->num_ranges);
+ for (i = 0; i < test->num_ranges; i++) {
+ test_assert(first[0]->group_by[0].ranges[i].min == test->ranges[i].range.min);
+ test_assert(first[0]->group_by[0].ranges[i].max == test->ranges[i].range.max);
+ }
+
+ /* examine second level sub-metrics */
+ struct metric *const *second = array_idx(&first[0]->sub_metrics, 0);
+
+ for (i = 0; i < test->num_sub_metrics; i++) {
+ const char *sub_name;
+ intmax_t range_idx;
+
+ /* we check these first, before we use the value below */
+ test_assert(second[i]->group_value.type == METRIC_VALUE_TYPE_BUCKET_INDEX);
+ test_assert(second[i]->group_value.intmax < test->num_ranges);
+
+ range_idx = second[i]->group_value.intmax;
+
+ /* construct the expected sub-metric name */
+ if (test->ranges[range_idx].range.min == INTMAX_MIN) {
+ sub_name = t_strdup_printf("foobar_ninf_%jd",
+ test->ranges[range_idx].range.max);
+ } else if (test->ranges[range_idx].range.max == INTMAX_MAX) {
+ sub_name = t_strdup_printf("foobar_%jd_inf",
+ test->ranges[range_idx].range.min + 1);
+ } else {
+ sub_name = t_strdup_printf("foobar_%jd_%jd",
+ test->ranges[range_idx].range.min + 1,
+ test->ranges[range_idx].range.max);
+ }
+
+ test_stats_metrics_group_by_check_one(second[i],
+ sub_name,
+ test->ranges[second[i]->group_value.intmax].count,
+ 0, 0, 0, NULL,
+ METRIC_VALUE_TYPE_BUCKET_INDEX);
+ }
+
+ test_deinit();
+ test_end();
+}
+
+static void test_stats_metrics_group_by_quantized(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(quantized_tests); i++)
+ test_stats_metrics_group_by_quantized_real(&quantized_tests[i]);
+}
+
+int main(void) {
+ void (*const test_functions[])(void) = {
+ test_stats_metrics,
+ test_stats_metrics_filter,
+ test_stats_metrics_group_by_discrete,
+ test_stats_metrics_group_by_quantized,
+ NULL
+ };
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}
diff --git a/src/submission-login/Makefile.am b/src/submission-login/Makefile.am
new file mode 100644
index 0000000..2385faf
--- /dev/null
+++ b/src/submission-login/Makefile.am
@@ -0,0 +1,32 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = submission-login
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/login-common
+
+submission_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS)
+submission_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+submission_login_SOURCES = \
+ client.c \
+ client-authenticate.c \
+ submission-login-settings.c \
+ submission-proxy.c
+
+noinst_HEADERS = \
+ client.h \
+ client-authenticate.h \
+ submission-login-settings.h \
+ submission-proxy.h
diff --git a/src/submission-login/Makefile.in b/src/submission-login/Makefile.in
new file mode 100644
index 0000000..7417007
--- /dev/null
+++ b/src/submission-login/Makefile.in
@@ -0,0 +1,830 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = submission-login$(EXEEXT)
+subdir = src/submission-login
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_submission_login_OBJECTS = client.$(OBJEXT) \
+ client-authenticate.$(OBJEXT) \
+ submission-login-settings.$(OBJEXT) submission-proxy.$(OBJEXT)
+submission_login_OBJECTS = $(am_submission_login_OBJECTS)
+am__DEPENDENCIES_1 =
+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)/client-authenticate.Po \
+ ./$(DEPDIR)/client.Po ./$(DEPDIR)/submission-login-settings.Po \
+ ./$(DEPDIR)/submission-proxy.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 = $(submission_login_SOURCES)
+DIST_SOURCES = $(submission_login_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/login-common
+
+submission_login_LDADD = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT) \
+ $(SSL_LIBS)
+
+submission_login_DEPENDENCIES = \
+ $(LIBDOVECOT_LOGIN) \
+ $(LIBDOVECOT_DEPS)
+
+submission_login_SOURCES = \
+ client.c \
+ client-authenticate.c \
+ submission-login-settings.c \
+ submission-proxy.c
+
+noinst_HEADERS = \
+ client.h \
+ client-authenticate.h \
+ submission-login-settings.h \
+ submission-proxy.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/submission-login/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/submission-login/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+submission-login$(EXEEXT): $(submission_login_OBJECTS) $(submission_login_DEPENDENCIES) $(EXTRA_submission_login_DEPENDENCIES)
+ @rm -f submission-login$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(submission_login_OBJECTS) $(submission_login_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client-authenticate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-login-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-proxy.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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/submission-login-settings.Po
+ -rm -f ./$(DEPDIR)/submission-proxy.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-pkglibexecPROGRAMS
+
+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)/client-authenticate.Po
+ -rm -f ./$(DEPDIR)/client.Po
+ -rm -f ./$(DEPDIR)/submission-login-settings.Po
+ -rm -f ./$(DEPDIR)/submission-proxy.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-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/submission-login/client-authenticate.c b/src/submission-login/client-authenticate.c
new file mode 100644
index 0000000..e1a5ae2
--- /dev/null
+++ b/src/submission-login/client-authenticate.c
@@ -0,0 +1,360 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "auth-client.h"
+#include "master-service-ssl-settings.h"
+#include "client.h"
+#include "client-authenticate.h"
+#include "submission-proxy.h"
+#include "submission-login-settings.h"
+
+static void cmd_helo_reply(struct submission_client *subm_client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct client *client = &subm_client->common;
+ const struct submission_login_settings *set = subm_client->set;
+ enum smtp_capability backend_caps = subm_client->backend_capabilities;
+ bool exotic_backend =
+ HAS_ALL_BITS(set->parsed_workarounds,
+ SUBMISSION_LOGIN_WORKAROUND_EXOTIC_BACKEND);
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_reply_create_ehlo(cmd->cmd);
+ if (!data->helo.old_smtp) {
+ if ((backend_caps & SMTP_CAPABILITY_8BITMIME) != 0)
+ smtp_server_reply_ehlo_add(reply, "8BITMIME");
+
+ if (client->secured ||
+ strcmp(client->ssl_set->ssl, "required") != 0) {
+ const struct auth_mech_desc *mechs;
+ unsigned int count, i;
+ string_t *param = t_str_new(128);
+
+ mechs = sasl_server_get_advertised_mechs(client,
+ &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(param, ' ');
+ str_append(param, mechs[i].name);
+ }
+ smtp_server_reply_ehlo_add_param(reply,
+ "AUTH", "%s", str_c(param));
+ }
+
+ if ((backend_caps & SMTP_CAPABILITY_BINARYMIME) != 0 &&
+ (backend_caps & SMTP_CAPABILITY_CHUNKING) != 0)
+ smtp_server_reply_ehlo_add(reply, "BINARYMIME");
+ if (!exotic_backend ||
+ (backend_caps & SMTP_CAPABILITY_BURL) != 0) {
+ smtp_server_reply_ehlo_add_param(reply, "BURL", "imap");
+ }
+ if (!exotic_backend ||
+ (backend_caps & SMTP_CAPABILITY_CHUNKING) != 0) {
+ smtp_server_reply_ehlo_add(reply,
+ "CHUNKING");
+ }
+ if ((backend_caps & SMTP_CAPABILITY_DSN) != 0)
+ smtp_server_reply_ehlo_add(reply, "DSN");
+ if (!exotic_backend ||
+ (backend_caps & SMTP_CAPABILITY_ENHANCEDSTATUSCODES) != 0) {
+ smtp_server_reply_ehlo_add(
+ reply, "ENHANCEDSTATUSCODES");
+ }
+
+ if (subm_client->set->submission_max_mail_size > 0) {
+ smtp_server_reply_ehlo_add_param(reply,
+ "SIZE", "%"PRIuUOFF_T,
+ subm_client->set->submission_max_mail_size);
+ } else if (!exotic_backend ||
+ (backend_caps & SMTP_CAPABILITY_SIZE) != 0) {
+ smtp_server_reply_ehlo_add(reply, "SIZE");
+ }
+
+ if (client_is_tls_enabled(client) && !client->tls)
+ smtp_server_reply_ehlo_add(reply, "STARTTLS");
+ if (!exotic_backend ||
+ (backend_caps & SMTP_CAPABILITY_PIPELINING) != 0)
+ smtp_server_reply_ehlo_add(reply, "PIPELINING");
+ if ((backend_caps & SMTP_CAPABILITY_VRFY) != 0)
+ smtp_server_reply_ehlo_add(reply, "VRFY");
+ smtp_server_reply_ehlo_add_xclient(reply);
+ }
+ smtp_server_reply_submit(reply);
+}
+
+int cmd_helo(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct submission_client *subm_client = conn_ctx;
+
+ T_BEGIN {
+ cmd_helo_reply(subm_client, cmd, data);
+ } T_END;
+
+ return 1;
+}
+
+void submission_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply ATTR_UNUSED,
+ const char *text)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+ struct smtp_server_cmd_ctx *cmd = subm_client->pending_auth;
+
+ if (subm_client->conn == NULL)
+ return;
+
+ subm_client->pending_auth = NULL;
+ i_assert(cmd != NULL);
+
+ switch (result) {
+ case CLIENT_AUTH_RESULT_SUCCESS:
+ /* nothing to be done for SMTP */
+ if (client->login_proxy != NULL)
+ subm_client->pending_auth = cmd;
+ break;
+ case CLIENT_AUTH_RESULT_TEMPFAIL:
+ /* RFC4954, Section 6:
+
+ 454 4.7.0 Temporary authentication failure
+
+ This response to the AUTH command indicates that the
+ authentication failed due to a temporary server failure.
+ */
+ smtp_server_reply(cmd, 454, "4.7.0", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_ABORTED:
+ /* RFC4954, Section 4:
+
+ If the client wishes to cancel the authentication exchange,
+ it issues a line with a single "*". If the server receives
+ such a response, it MUST reject the AUTH command by sending
+ a 501 reply. */
+ smtp_server_reply(cmd, 501, "5.5.2", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_INVALID_BASE64:
+ /* RFC4954, Section 4:
+
+ If the server cannot [BASE64] decode any client response, it
+ MUST reject the AUTH command with a 501 reply (and an
+ enhanced status code of 5.5.2). */
+ smtp_server_reply(cmd, 501, "5.5.2", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_SSL_REQUIRED:
+ /* RFC3207, 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_reply(cmd, 530, "5.7.0", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED:
+ /* RFC5248, Section 2.4:
+
+ 523 X.7.10 Encryption Needed
+
+ This indicates that an external strong privacy layer is
+ needed in order to use the requested authentication
+ mechanism. This is primarily intended for use with clear text
+ authentication mechanisms. A client that receives this may
+ activate a security layer such as TLS prior to
+ authenticating, or attempt to use a stronger mechanism. */
+ smtp_server_reply(cmd, 523, "5.7.10", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_MECH_INVALID:
+ /* RFC4954, Section 4:
+
+ If the requested authentication mechanism is invalid (e.g.,
+ is not supported or requires an encryption layer), the server
+ rejects the AUTH command with a 504 reply. If the server
+ supports the [ESMTP-CODES] extension, it SHOULD return a
+ 5.5.4 enhanced response code. */
+ smtp_server_reply(cmd, 504, "5.5.4", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_LOGIN_DISABLED:
+ case CLIENT_AUTH_RESULT_ANONYMOUS_DENIED:
+ /* RFC5248, Section 2.4:
+
+ 525 X.7.13 User Account Disabled
+
+ Sometimes a system administrator will have to disable a
+ user's account (e.g., due to lack of payment, abuse, evidence
+ of a break-in attempt, etc.). This error code occurs after a
+ successful authentication to a disabled account. This informs
+ the client that the failure is permanent until the user
+ contacts their system administrator to get the account
+ re-enabled. */
+ smtp_server_reply(cmd, 525, "5.7.13", "%s", text);
+ break;
+ case CLIENT_AUTH_RESULT_PASS_EXPIRED:
+ default:
+ /* FIXME: RFC4954, Section 4:
+
+ If the client uses an initial-response argument to the AUTH
+ command with a SASL mechanism in which the client does not
+ begin the authentication exchange, the server MUST reject the
+ AUTH command with a 501 reply. Servers using the enhanced
+ status codes extension [ESMTP-CODES] SHOULD return an
+ enhanced status code of 5.7.0 in this case.
+
+ >> Currently, this is checked at the server side, but only a
+ generic error is ever produced.
+ */
+ /* NOTE: RFC4954, Section 4:
+
+ If, during an authentication exchange, the server receives a
+ line that is longer than the server's authentication buffer,
+ the server fails the AUTH command with the 500 reply. Servers
+ using the enhanced status codes extension [ESMTP-CODES]
+ SHOULD return an enhanced status code of 5.5.6 in this case.
+
+ >> Currently, client is disconnected from login-common.
+ */
+ /* RFC4954, Section 4:
+
+ If the server is unable to authenticate the client, it SHOULD
+ reject the AUTH command with a 535 reply unless a more
+ specific error code is appropriate.
+
+ RFC4954, Section 6:
+
+ 535 5.7.8 Authentication credentials invalid
+
+ This response to the AUTH command indicates that the
+ authentication failed due to invalid or insufficient
+ authentication credentials.
+ */
+ smtp_server_reply(cmd, 535, "5.7.8", "%s", text);
+ break;
+ }
+}
+
+int cmd_auth_continue(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ const char *response)
+{
+ struct submission_client *subm_client = conn_ctx;
+ struct client *client = &subm_client->common;
+
+ if (strcmp(response, "*") == 0) {
+ client_auth_abort(client);
+ return 0;
+ }
+
+ client_auth_respond(client, response);
+ return 0;
+}
+
+void submission_client_auth_send_challenge(struct client *client,
+ const char *data)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+ struct smtp_server_cmd_ctx *cmd = subm_client->pending_auth;
+
+ i_assert(cmd != NULL);
+
+ smtp_server_cmd_auth_send_challenge(cmd, data);
+}
+
+static void
+cmd_auth_set_master_data_prefix(struct submission_client *subm_client,
+ const char *mail_params) ATTR_NULL(2)
+{
+ struct client *client = &subm_client->common;
+ struct smtp_server_helo_data *helo;
+ struct smtp_proxy_data proxy;
+
+ buffer_t *buf = buffer_create_dynamic(default_pool, 2048);
+
+ /* pass ehlo parameter to post-login service upon successful login */
+ helo = smtp_server_connection_get_helo_data(subm_client->conn);
+ if (helo->domain_valid) {
+ i_assert(helo->domain != NULL);
+ buffer_append(buf, helo->domain, strlen(helo->domain));
+ }
+ buffer_append_c(buf, '\0');
+
+ /* pass proxied ehlo parameter to post-login service upon successful
+ login */
+ smtp_server_connection_get_proxy_data(subm_client->conn, &proxy);
+ if (proxy.helo != NULL)
+ buffer_append(buf, proxy.helo, strlen(proxy.helo));
+ buffer_append_c(buf, '\0');
+
+ /* Pass MAIL command to post-login service if any. */
+ if (mail_params != NULL) {
+ buffer_append(buf, "MAIL ", 5);
+ buffer_append(buf, mail_params, strlen(mail_params));
+ buffer_append(buf, "\r\n", 2);
+ }
+
+ i_free(client->master_data_prefix);
+ client->master_data_prefix_len = buf->used;
+ client->master_data_prefix = buffer_free_without_data(&buf);
+}
+
+int cmd_auth(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data)
+{
+ struct submission_client *subm_client = conn_ctx;
+ struct client *client = &subm_client->common;
+
+ cmd_auth_set_master_data_prefix(subm_client, NULL);
+
+ i_assert(subm_client->pending_auth == NULL);
+ subm_client->pending_auth = cmd;
+
+ (void)client_auth_begin(client, data->sasl_mech, data->initial_response);
+ return 0;
+}
+
+void cmd_mail(struct smtp_server_cmd_ctx *cmd, const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct submission_client *subm_client =
+ smtp_server_connection_get_context(conn);
+ struct client *client = &subm_client->common;
+ enum submission_login_client_workarounds workarounds =
+ subm_client->set->parsed_workarounds;
+
+ if (HAS_NO_BITS(workarounds,
+ SUBMISSION_LOGIN_WORKAROUND_IMPLICIT_AUTH_EXTERNAL) ||
+ sasl_server_find_available_mech(client, "EXTERNAL") == NULL) {
+ smtp_server_command_fail(cmd->cmd, 530, "5.7.0",
+ "Authentication required.");
+ return;
+ }
+
+ e_debug(cmd->event,
+ "Performing implicit EXTERNAL authentication");
+
+ smtp_server_command_input_lock(cmd);
+
+ cmd_auth_set_master_data_prefix(subm_client, params);
+
+ i_assert(subm_client->pending_auth == NULL);
+ subm_client->pending_auth = cmd;
+
+ (void)client_auth_begin_implicit(client, "EXTERNAL", "=");
+}
diff --git a/src/submission-login/client-authenticate.h b/src/submission-login/client-authenticate.h
new file mode 100644
index 0000000..8353b49
--- /dev/null
+++ b/src/submission-login/client-authenticate.h
@@ -0,0 +1,21 @@
+#ifndef CLIENT_AUTHENTICATE_H
+#define CLIENT_AUTHENTICATE_H
+
+void submission_client_auth_result(struct client *client,
+ enum client_auth_result result,
+ const struct client_auth_reply *reply,
+ const char *text);
+
+void submission_client_auth_send_challenge(struct client *client,
+ const char *data);
+
+int cmd_helo(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+int cmd_auth_continue(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ const char *response);
+int cmd_auth(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data);
+
+void cmd_mail(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+#endif
diff --git a/src/submission-login/client.c b/src/submission-login/client.c
new file mode 100644
index 0000000..9922c9d
--- /dev/null
+++ b/src/submission-login/client.c
@@ -0,0 +1,329 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "base64.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "randgen.h"
+#include "hostpid.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "client.h"
+#include "client-authenticate.h"
+#include "auth-client.h"
+#include "submission-proxy.h"
+#include "submission-login-settings.h"
+
+/* Disconnect client when it sends too many bad commands */
+#define CLIENT_MAX_BAD_COMMANDS 10
+
+static const struct smtp_server_callbacks smtp_callbacks;
+
+static struct smtp_server *smtp_server = NULL;
+
+static void
+client_parse_backend_capabilities(struct submission_client *subm_client )
+{
+ const struct submission_login_settings *set = subm_client->set;
+ const char *const *str;
+
+ if (set->submission_backend_capabilities == NULL) {
+ subm_client->backend_capabilities = SMTP_CAPABILITY_8BITMIME;
+ return;
+ }
+
+ subm_client->backend_capabilities = SMTP_CAPABILITY_NONE;
+ str = t_strsplit_spaces(set->submission_backend_capabilities, " ,");
+ for (; *str != NULL; str++) {
+ enum smtp_capability cap = smtp_capability_find_by_name(*str);
+
+ if (cap == SMTP_CAPABILITY_NONE) {
+ i_warning("Unknown SMTP capability in submission_backend_capabilities: "
+ "%s", *str);
+ continue;
+ }
+
+ subm_client->backend_capabilities |= cap;
+ }
+
+ /* Make sure CHUNKING support is always enabled when BINARYMIME is
+ enabled by explicit configuration. */
+ if (HAS_ALL_BITS(subm_client->backend_capabilities,
+ SMTP_CAPABILITY_BINARYMIME)) {
+ subm_client->backend_capabilities |= SMTP_CAPABILITY_CHUNKING;
+ }
+}
+
+static int submission_login_start_tls(void *conn_ctx,
+ struct istream **input, struct ostream **output)
+{
+ struct submission_client *subm_client = conn_ctx;
+ struct client *client = &subm_client->common;
+
+ client->starttls = TRUE;
+ if (client_init_ssl(client) < 0) {
+ client_notify_disconnect(client,
+ CLIENT_DISCONNECT_INTERNAL_ERROR,
+ "TLS initialization failed.");
+ client_destroy(client,
+ "Disconnected: TLS initialization failed.");
+ return -1;
+ }
+ login_refresh_proctitle();
+
+ *input = client->input;
+ *output = client->output;
+ return 0;
+}
+
+static struct client *submission_client_alloc(pool_t pool)
+{
+ struct submission_client *subm_client;
+
+ subm_client = p_new(pool, struct submission_client, 1);
+ return &subm_client->common;
+}
+
+static void submission_client_create(struct client *client,
+ void **other_sets)
+{
+ static const char *const xclient_extensions[] =
+ { "FORWARD", NULL };
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+ struct smtp_server_settings smtp_set;
+
+ subm_client->set = other_sets[0];
+ client_parse_backend_capabilities(subm_client);
+
+ i_zero(&smtp_set);
+ smtp_set.capabilities = SMTP_CAPABILITY_SIZE |
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES | SMTP_CAPABILITY_AUTH |
+ SMTP_CAPABILITY_XCLIENT;
+ if (client_is_tls_enabled(client))
+ smtp_set.capabilities |= SMTP_CAPABILITY_STARTTLS;
+ smtp_set.hostname = subm_client->set->hostname;
+ smtp_set.login_greeting = client->set->login_greeting;
+ smtp_set.tls_required = !client->secured &&
+ (strcmp(client->ssl_set->ssl, "required") == 0);
+ smtp_set.xclient_extensions = xclient_extensions;
+ smtp_set.command_limits.max_parameters_size = LOGIN_MAX_INBUF_SIZE;
+ smtp_set.command_limits.max_auth_size = LOGIN_MAX_AUTH_BUF_SIZE;
+ smtp_set.debug = client->set->auth_debug;
+
+ subm_client->conn = smtp_server_connection_create_from_streams(
+ smtp_server, client->input, client->output,
+ &client->real_remote_ip, client->real_remote_port,
+ &smtp_set, &smtp_callbacks, subm_client);
+}
+
+static void submission_client_destroy(struct client *client)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+
+ if (subm_client->conn != NULL)
+ smtp_server_connection_close(&subm_client->conn, NULL);
+ i_free_and_null(subm_client->proxy_xclient);
+}
+
+static void submission_client_notify_auth_ready(struct client *client)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+
+ client->banner_sent = TRUE;
+ smtp_server_connection_start(subm_client->conn);
+}
+
+static void
+submission_client_notify_disconnect(struct client *_client,
+ enum client_disconnect_reason reason,
+ const char *text)
+{
+ struct submission_client *client =
+ container_of(_client, struct submission_client, common);
+ struct smtp_server_connection *conn;
+
+ conn = client->conn;
+ client->conn = NULL;
+ if (conn != NULL) {
+ switch (reason) {
+ case CLIENT_DISCONNECT_TIMEOUT:
+ smtp_server_connection_terminate(&conn, "4.4.2", text);
+ break;
+ case CLIENT_DISCONNECT_SYSTEM_SHUTDOWN:
+ smtp_server_connection_terminate(&conn, "4.3.2", text);
+ break;
+ case CLIENT_DISCONNECT_INTERNAL_ERROR:
+ default:
+ smtp_server_connection_terminate(&conn, "4.0.0", text);
+ break;
+ }
+ }
+}
+
+static void
+client_connection_cmd_xclient(void *context,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_proxy_data *data)
+{
+ unsigned int i;
+
+ struct submission_client *client = context;
+
+ if (data->source_ip.family != 0)
+ client->common.ip = data->source_ip;
+ if (data->source_port != 0)
+ client->common.remote_port = data->source_port;
+ if (data->ttl_plus_1 > 0)
+ client->common.proxy_ttl = data->ttl_plus_1 - 1;
+ if (data->session != NULL) {
+ client->common.session_id =
+ p_strdup(client->common.pool, data->session);
+ }
+
+ for (i = 0; i < data->extra_fields_count; i++) {
+ const char *name = data->extra_fields[i].name;
+ const char *value = data->extra_fields[i].value;
+
+ if (strcasecmp(name, "FORWARD") == 0) {
+ size_t value_len = strlen(value);
+ if (client->common.forward_fields != NULL) {
+ str_truncate(client->common.forward_fields, 0);
+ } else {
+ client->common.forward_fields = str_new(
+ client->common.preproxy_pool,
+ MAX_BASE64_DECODED_SIZE(value_len));
+ if (base64_decode(value, value_len, NULL,
+ client->common.forward_fields) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid FORWARD parameter");
+ }
+ }
+ }
+ }
+}
+
+static void client_connection_disconnect(void *context, const char *reason)
+{
+ struct submission_client *client = context;
+
+ client->pending_auth = NULL;
+ client_disconnect(&client->common, reason,
+ !client->common.login_success);
+}
+
+static void client_connection_free(void *context)
+{
+ struct submission_client *client = context;
+
+ if (client->conn == NULL)
+ return;
+ client->conn = NULL;
+ client_destroy(&client->common, NULL);
+}
+
+static bool client_connection_is_trusted(void *context)
+{
+ struct submission_client *client = context;
+
+ return client->common.trusted;
+}
+
+static void submission_login_die(void)
+{
+ /* do nothing. submission connections typically die pretty quick anyway.
+ */
+}
+
+static void submission_login_preinit(void)
+{
+ login_set_roots = submission_login_setting_roots;
+}
+
+static void submission_login_init(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ /* override the default login_die() */
+ master_service_set_die_callback(master_service, submission_login_die);
+
+ /* initialize SMTP server */
+ i_zero(&smtp_server_set);
+ smtp_server_set.protocol = SMTP_PROTOCOL_SMTP;
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.max_bad_commands = CLIENT_MAX_BAD_COMMANDS;
+ smtp_server_set.reason_code_module = "submission";
+ /* Pre-auth state is always logged either as GREETING or READY.
+ It's not very useful. */
+ smtp_server_set.no_state_in_reason = TRUE;
+ smtp_server = smtp_server_init(&smtp_server_set);
+ smtp_server_command_override(smtp_server, "MAIL", cmd_mail,
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+}
+
+static void submission_login_deinit(void)
+{
+ clients_destroy_all();
+
+ smtp_server_deinit(&smtp_server);
+}
+
+static const struct smtp_server_callbacks smtp_callbacks = {
+ .conn_cmd_helo = cmd_helo,
+
+ .conn_start_tls = submission_login_start_tls,
+
+ .conn_cmd_auth = cmd_auth,
+ .conn_cmd_auth_continue = cmd_auth_continue,
+
+ .conn_cmd_xclient = client_connection_cmd_xclient,
+
+ .conn_disconnect = client_connection_disconnect,
+ .conn_free = client_connection_free,
+
+ .conn_is_trusted = client_connection_is_trusted
+};
+
+static struct client_vfuncs submission_client_vfuncs = {
+ .alloc = submission_client_alloc,
+ .create = submission_client_create,
+ .destroy = submission_client_destroy,
+ .notify_auth_ready = submission_client_notify_auth_ready,
+ .notify_disconnect = submission_client_notify_disconnect,
+ .auth_send_challenge = submission_client_auth_send_challenge,
+ .auth_result = submission_client_auth_result,
+ .proxy_reset = submission_proxy_reset,
+ .proxy_parse_line = submission_proxy_parse_line,
+ .proxy_failed = submission_proxy_failed,
+ .proxy_get_state = submission_proxy_get_state,
+};
+
+static struct login_binary submission_login_binary = {
+ .protocol = "submission",
+ .process_name = "submission-login",
+ .default_port = 587,
+
+ .event_category = {
+ .name = "submission",
+ },
+
+ .client_vfuncs = &submission_client_vfuncs,
+ .preinit = submission_login_preinit,
+ .init = submission_login_init,
+ .deinit = submission_login_deinit,
+
+ .sasl_support_final_reply = FALSE,
+ .anonymous_login_acceptable = FALSE,
+};
+
+int main(int argc, char *argv[])
+{
+ return login_binary_run(&submission_login_binary, argc, argv);
+}
diff --git a/src/submission-login/client.h b/src/submission-login/client.h
new file mode 100644
index 0000000..ba932af
--- /dev/null
+++ b/src/submission-login/client.h
@@ -0,0 +1,38 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "net.h"
+#include "client-common.h"
+#include "auth-client.h"
+#include "smtp-server.h"
+
+enum submission_proxy_state {
+ SUBMISSION_PROXY_BANNER = 0,
+ SUBMISSION_PROXY_EHLO,
+ SUBMISSION_PROXY_STARTTLS,
+ SUBMISSION_PROXY_TLS_EHLO,
+ SUBMISSION_PROXY_XCLIENT,
+ SUBMISSION_PROXY_XCLIENT_EHLO,
+ SUBMISSION_PROXY_AUTHENTICATE,
+
+ SUBMISSION_PROXY_STATE_COUNT
+};
+
+struct submission_client {
+ struct client common;
+ const struct submission_login_settings *set;
+ enum smtp_capability backend_capabilities;
+
+ struct smtp_server_connection *conn;
+ struct smtp_server_cmd_ctx *pending_auth;
+
+ enum submission_proxy_state proxy_state;
+ enum smtp_capability proxy_capability;
+ char *proxy_sasl_ir;
+ unsigned int proxy_reply_status;
+ struct smtp_server_reply *proxy_reply;
+ const char **proxy_xclient;
+ unsigned int proxy_xclient_replies_expected;
+};
+
+#endif
diff --git a/src/submission-login/submission-login-settings.c b/src/submission-login/submission-login-settings.c
new file mode 100644
index 0000000..b35f97f
--- /dev/null
+++ b/src/submission-login/submission-login-settings.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "login-settings.h"
+#include "submission-login-settings.h"
+
+#include <stddef.h>
+
+static bool
+submission_login_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct inet_listener_settings submission_login_inet_listeners_array[] = {
+ { .name = "submission", .address = "", .port = 587 },
+ { .name = "submissions", .address = "", .port = 465, .ssl = TRUE }
+};
+static struct inet_listener_settings *submission_login_inet_listeners[] = {
+ &submission_login_inet_listeners_array[0]
+};
+static buffer_t submission_login_inet_listeners_buf = {
+ { { submission_login_inet_listeners,
+ sizeof(submission_login_inet_listeners) } }
+};
+
+/* </settings checks> */
+struct service_settings submission_login_service_settings = {
+ .name = "submission-login",
+ .protocol = "submission",
+ .type = "login",
+ .executable = "submission-login",
+ .user = "$default_login_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "login",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 0,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = { { &submission_login_inet_listeners_buf,
+ sizeof(submission_login_inet_listeners[0]) } }
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct submission_login_settings)
+
+static const struct setting_define submission_login_setting_defines[] = {
+ DEF(STR, hostname),
+
+ DEF(SIZE, submission_max_mail_size),
+ DEF(STR, submission_client_workarounds),
+ DEF(STR, submission_backend_capabilities),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct submission_login_settings submission_login_default_settings = {
+ .hostname = "",
+
+ .submission_max_mail_size = 0,
+ .submission_client_workarounds = "",
+ .submission_backend_capabilities = NULL
+};
+
+static const struct setting_parser_info *submission_login_setting_dependencies[] = {
+ &login_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info submission_login_setting_parser_info = {
+ .module_name = "submission-login",
+ .defines = submission_login_setting_defines,
+ .defaults = &submission_login_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct submission_login_settings),
+ .parent_offset = SIZE_MAX,
+
+ .check_func = submission_login_settings_check,
+ .dependencies = submission_login_setting_dependencies
+};
+
+const struct setting_parser_info *submission_login_setting_roots[] = {
+ &login_setting_parser_info,
+ &submission_login_setting_parser_info,
+ NULL
+};
+
+/* <settings checks> */
+struct submission_login_client_workaround_list {
+ const char *name;
+ enum submission_login_client_workarounds num;
+};
+
+/* These definitions need to be kept in sync with equivalent definitions present
+ in src/submission/submission-settings.c. Workarounds that are not relevant
+ to the submission-login service are defined as 0 here to prevent "Unknown
+ workaround" errors below. */
+static const struct submission_login_client_workaround_list
+submission_login_client_workaround_list[] = {
+ { "whitespace-before-path", 0},
+ { "mailbox-for-path", 0 },
+ { "implicit-auth-external",
+ SUBMISSION_LOGIN_WORKAROUND_IMPLICIT_AUTH_EXTERNAL },
+ { "exotic-backend",
+ SUBMISSION_LOGIN_WORKAROUND_EXOTIC_BACKEND },
+ { NULL, 0 }
+};
+
+static int
+submission_login_settings_parse_workarounds(
+ struct submission_login_settings *set, const char **error_r)
+{
+ enum submission_login_client_workarounds client_workarounds = 0;
+ const struct submission_login_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->submission_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = submission_login_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "submission_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+submission_login_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct submission_login_settings *set = _set;
+
+ if (submission_login_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+#ifndef CONFIG_BINARY
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+#endif
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/submission-login/submission-login-settings.h b/src/submission-login/submission-login-settings.h
new file mode 100644
index 0000000..57334b4
--- /dev/null
+++ b/src/submission-login/submission-login-settings.h
@@ -0,0 +1,24 @@
+#ifndef SUBMISSION_LOGIN_SETTINGS_H
+#define SUBMISSION_LOGIN_SETTINGS_H
+
+/* <settings checks> */
+enum submission_login_client_workarounds {
+ SUBMISSION_LOGIN_WORKAROUND_IMPLICIT_AUTH_EXTERNAL = BIT(0),
+ SUBMISSION_LOGIN_WORKAROUND_EXOTIC_BACKEND = BIT(1),
+};
+/* </settings checks> */
+
+struct submission_login_settings {
+ const char *hostname;
+
+ /* submission: */
+ uoff_t submission_max_mail_size;
+ const char *submission_client_workarounds;
+ const char *submission_backend_capabilities;
+
+ enum submission_login_client_workarounds parsed_workarounds;
+};
+
+extern const struct setting_parser_info *submission_login_setting_roots[];
+
+#endif
diff --git a/src/submission-login/submission-proxy.c b/src/submission-login/submission-proxy.c
new file mode 100644
index 0000000..190617b
--- /dev/null
+++ b/src/submission-login/submission-proxy.c
@@ -0,0 +1,709 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "login-common.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "safe-memset.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "dsasl-client.h"
+#include "client.h"
+#include "smtp-syntax.h"
+#include "submission-login-settings.h"
+#include "submission-proxy.h"
+
+#include <ctype.h>
+
+static const char *submission_proxy_state_names[SUBMISSION_PROXY_STATE_COUNT] = {
+ "banner", "ehlo", "starttls", "tls-ehlo", "xclient", "xclient-ehlo", "authenticate"
+};
+
+static void
+submission_proxy_success_reply_sent(
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct submission_client *subm_client)
+{
+ client_proxy_finish_destroy_client(&subm_client->common);
+}
+
+static int
+proxy_send_starttls(struct submission_client *client, struct ostream *output)
+{
+ enum login_proxy_ssl_flags ssl_flags;
+
+ ssl_flags = login_proxy_get_ssl_flags(client->common.login_proxy);
+ if ((ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0)
+ return 0;
+
+ if ((client->proxy_capability & SMTP_CAPABILITY_STARTTLS) == 0) {
+ login_proxy_failed(
+ client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ "STARTTLS not supported");
+ return -1;
+ }
+ o_stream_nsend_str(output, "STARTTLS\r\n");
+ client->proxy_state = SUBMISSION_PROXY_STARTTLS;
+ return 1;
+}
+
+static buffer_t *
+proxy_compose_xclient_forward(struct submission_client *client)
+{
+ const char *const *arg;
+ string_t *str;
+
+ if (*client->common.auth_passdb_args == NULL)
+ return NULL;
+
+ str = t_str_new(128);
+ for (arg = client->common.auth_passdb_args; *arg != NULL; arg++) {
+ if (strncasecmp(*arg, "forward_", 8) == 0) {
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, (*arg)+8);
+ }
+ }
+ if (str_len(str) == 0)
+ return NULL;
+
+ return t_base64_encode(0, 0, str_data(str), str_len(str));
+}
+
+static void
+proxy_send_xclient_more_data(struct submission_client *client,
+ struct ostream *output, string_t *buf,
+ const char *field, const unsigned char *value,
+ size_t value_size)
+{
+ const size_t cmd_len = strlen("XCLIENT");
+ size_t prev_len = str_len(buf);
+
+ str_append_c(buf, ' ');
+ str_append(buf, field);
+ str_append_c(buf, '=');
+ smtp_xtext_encode(buf, value, value_size);
+
+ if (str_len(buf) > 512) {
+ if (prev_len <= cmd_len)
+ prev_len = str_len(buf);
+ o_stream_nsend(output, str_data(buf), prev_len);
+ o_stream_nsend(output, "\r\n", 2);
+ client->proxy_xclient_replies_expected++;
+ str_delete(buf, cmd_len, prev_len - cmd_len);
+ }
+}
+
+static void
+proxy_send_xclient_more(struct submission_client *client,
+ struct ostream *output, string_t *buf,
+ const char *field, const char *value)
+{
+ proxy_send_xclient_more_data(client, output, buf, field,
+ (const unsigned char *)value,
+ strlen(value));
+}
+
+static int
+proxy_send_xclient(struct submission_client *client, struct ostream *output)
+{
+ string_t *str;
+
+ if ((client->proxy_capability & SMTP_CAPABILITY_XCLIENT) == 0 ||
+ client->common.proxy_not_trusted)
+ return 0;
+
+ struct smtp_proxy_data proxy_data;
+
+ smtp_server_connection_get_proxy_data(client->conn, &proxy_data);
+ i_assert(client->common.proxy_ttl > 1);
+
+ /* remote supports XCLIENT, send it */
+ client->proxy_xclient_replies_expected = 0;
+ str = t_str_new(128);
+ str_append(str, "XCLIENT");
+ if (str_array_icase_find(client->proxy_xclient, "HELO")) {
+ if (proxy_data.helo != NULL) {
+ proxy_send_xclient_more(client, output, str, "HELO",
+ proxy_data.helo);
+ } else {
+ proxy_send_xclient_more(client, output, str, "HELO",
+ "[UNAVAILABLE]");
+ }
+ }
+ if (str_array_icase_find(client->proxy_xclient, "PROTO")) {
+ const char *proto = "[UNAVAILABLE]";
+
+ switch (proxy_data.proto) {
+ case SMTP_PROXY_PROTOCOL_UNKNOWN:
+ break;
+ case SMTP_PROXY_PROTOCOL_SMTP:
+ proto = "SMTP";
+ break;
+ case SMTP_PROXY_PROTOCOL_ESMTP:
+ proto = "ESMTP";
+ break;
+ case SMTP_PROXY_PROTOCOL_LMTP:
+ proto = "LMTP";
+ break;
+ }
+ proxy_send_xclient_more(client, output, str, "PROTO", proto);
+ }
+ if (client->common.proxy_noauth &&
+ str_array_icase_find(client->proxy_xclient, "LOGIN")) {
+ if (proxy_data.login != NULL) {
+ proxy_send_xclient_more(client, output, str, "LOGIN",
+ proxy_data.login);
+ } else if (client->common.virtual_user != NULL) {
+ proxy_send_xclient_more(client, output, str, "LOGIN",
+ client->common.virtual_user);
+ } else {
+ proxy_send_xclient_more(client, output, str, "LOGIN",
+ "[UNAVAILABLE]");
+ }
+ }
+ if (str_array_icase_find(client->proxy_xclient, "TTL")) {
+ proxy_send_xclient_more(
+ client, output, str, "TTL",
+ t_strdup_printf("%u",client->common.proxy_ttl - 1));
+ }
+ if (str_array_icase_find(client->proxy_xclient, "PORT")) {
+ proxy_send_xclient_more(
+ client, output, str, "PORT",
+ t_strdup_printf("%u", client->common.remote_port));
+ }
+ if (str_array_icase_find(client->proxy_xclient, "ADDR")) {
+ proxy_send_xclient_more(client, output, str, "ADDR",
+ net_ip2addr(&client->common.ip));
+ }
+ if (str_array_icase_find(client->proxy_xclient, "SESSION")) {
+ proxy_send_xclient_more(client, output, str, "SESSION",
+ client_get_session_id(&client->common));
+ }
+ if (str_array_icase_find(client->proxy_xclient, "FORWARD")) {
+ buffer_t *fwd = proxy_compose_xclient_forward(client);
+
+ if (fwd != NULL) {
+ proxy_send_xclient_more_data(
+ client, output, str, "FORWARD",
+ fwd->data, fwd->used);
+ }
+ }
+ str_append(str, "\r\n");
+ o_stream_nsend(output, str_data(str), str_len(str));
+ client->proxy_state = SUBMISSION_PROXY_XCLIENT;
+ client->proxy_xclient_replies_expected++;
+ return 1;
+}
+
+static int
+proxy_send_login(struct submission_client *client, struct ostream *output)
+{
+ struct dsasl_client_settings sasl_set;
+ const unsigned char *sasl_output;
+ size_t sasl_output_len;
+ const char *mech_name, *error;
+ string_t *str;
+
+ if ((client->proxy_capability & SMTP_CAPABILITY_AUTH) == 0) {
+ /* Prevent sending credentials to a server that has login
+ disabled; i.e., due to the lack of TLS */
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG,
+ "Authentication support not advertised (TLS required?)");
+ return -1;
+ }
+
+ str = t_str_new(128);
+
+ if (client->common.proxy_mech == NULL)
+ client->common.proxy_mech = &dsasl_client_mech_plain;
+
+ i_assert(client->common.proxy_sasl_client == NULL);
+ i_zero(&sasl_set);
+ sasl_set.authid = client->common.proxy_master_user != NULL ?
+ client->common.proxy_master_user : client->common.proxy_user;
+ sasl_set.authzid = client->common.proxy_user;
+ sasl_set.password = client->common.proxy_password;
+ client->common.proxy_sasl_client =
+ dsasl_client_new(client->common.proxy_mech, &sasl_set);
+ mech_name = dsasl_client_mech_get_name(client->common.proxy_mech);
+
+ str_printfa(str, "AUTH %s", mech_name);
+ if (dsasl_client_output(client->common.proxy_sasl_client,
+ &sasl_output, &sasl_output_len, &error) < 0) {
+ const char *reason = t_strdup_printf(
+ "SASL mechanism %s init failed: %s",
+ mech_name, error);
+ login_proxy_failed(client->common.login_proxy,
+ login_proxy_get_event(client->common.login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_INTERNAL, reason);
+ return -1;
+ }
+
+ string_t *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.
+ */
+
+ i_assert(client->proxy_sasl_ir == NULL);
+ if (str_len(sasl_output_base64) == 0)
+ str_append(str, " =");
+ else if ((5 + strlen(mech_name) + 1 + str_len(sasl_output_base64)) >
+ SMTP_BASE_LINE_LENGTH_LIMIT)
+ client->proxy_sasl_ir = i_strdup(str_c(sasl_output_base64));
+ else {
+ str_append_c(str, ' ');
+ str_append_str(str, sasl_output_base64);
+ }
+
+ str_append(str, "\r\n");
+ o_stream_nsend(output, str_data(str), str_len(str));
+
+ client->proxy_state = SUBMISSION_PROXY_AUTHENTICATE;
+ return 0;
+}
+
+static int
+proxy_handle_ehlo_reply(struct submission_client *client,
+ struct ostream *output)
+{
+ struct smtp_server_cmd_ctx *cmd = client->pending_auth;
+ int ret;
+
+ switch (client->proxy_state) {
+ case SUBMISSION_PROXY_EHLO:
+ ret = proxy_send_starttls(client, output);
+ if (ret < 0)
+ return -1;
+ if (ret != 0)
+ return 0;
+ /* Fall through */
+ case SUBMISSION_PROXY_TLS_EHLO:
+ ret = proxy_send_xclient(client, output);
+ if (ret < 0)
+ return -1;
+ if (ret != 0) {
+ client->proxy_capability = 0;
+ i_free_and_null(client->proxy_xclient);
+ o_stream_nsend_str(output, t_strdup_printf(
+ "EHLO %s\r\n",
+ client->set->hostname));
+ return 0;
+ }
+ break;
+ case SUBMISSION_PROXY_XCLIENT_EHLO:
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (client->common.proxy_noauth) {
+ smtp_server_connection_input_lock(cmd->conn);
+
+ smtp_server_command_add_hook(
+ cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ submission_proxy_success_reply_sent, client);
+ client->pending_auth = NULL;
+
+ smtp_server_reply(cmd, 235, "2.7.0", "Logged in.");
+ return 1;
+ }
+
+ return proxy_send_login(client, output);
+}
+
+static int
+submission_proxy_continue_sasl_auth(struct client *client,
+ struct ostream *output, const char *line,
+ bool last_line)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+ string_t *str;
+ const unsigned char *data;
+ size_t data_len;
+ const char *error;
+ int ret;
+
+ if (!last_line) {
+ const char *reason = t_strdup_printf(
+ "Server returned multi-line challenge: 334 %s",
+ str_sanitize(line, 1024));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ if (subm_client->proxy_sasl_ir != NULL) {
+ if (*line == '\0') {
+ /* Send initial response */
+ o_stream_nsend(output, subm_client->proxy_sasl_ir,
+ strlen(subm_client->proxy_sasl_ir));
+ o_stream_nsend_str(output, "\r\n");
+ i_free(subm_client->proxy_sasl_ir);
+ return 0;
+ }
+ const char *reason = t_strdup_printf(
+ "Server sent unexpected server-first challenge: "
+ "334 %s", str_sanitize(line, 1024));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+
+ str = t_str_new(128);
+ if (base64_decode(line, strlen(line), NULL, str) < 0) {
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL,
+ "Invalid base64 data in AUTH response");
+ return -1;
+ }
+ ret = dsasl_client_input(client->proxy_sasl_client,
+ str_data(str), str_len(str), &error);
+ if (ret == 0) {
+ ret = dsasl_client_output(client->proxy_sasl_client,
+ &data, &data_len, &error);
+ }
+ if (ret < 0) {
+ const char *reason = t_strdup_printf(
+ "Invalid authentication data: %s", error);
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ i_assert(ret == 0);
+
+ str_truncate(str, 0);
+ base64_encode(data, data_len, str);
+ str_append(str, "\r\n");
+
+ o_stream_nsend(output, str_data(str), str_len(str));
+ return 0;
+}
+
+static const char *
+strip_enhanced_code(const char *text, const char **enh_code_r)
+{
+ const char *p = text;
+ unsigned int digits;
+
+ *enh_code_r = NULL;
+
+ if (*p != '2' && *p != '4' && *p != '5')
+ return text;
+ p++;
+ if (*p != '.')
+ return text;
+ p++;
+
+ digits = 0;
+ while (i_isdigit(*p) && digits < 3) {
+ p++;
+ digits++;
+ }
+ if (*p != '.')
+ return text;
+ p++;
+
+ digits = 0;
+ while (i_isdigit(*p) && digits < 3) {
+ p++;
+ digits++;
+ }
+ if (*p != ' ')
+ return text;
+ *enh_code_r = t_strdup_until(text, p);
+ p++;
+ return p;
+}
+
+int submission_proxy_parse_line(struct client *client, const char *line)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+ struct smtp_server_cmd_ctx *cmd = subm_client->pending_auth;
+ struct smtp_server_command *command = cmd->cmd;
+ struct ostream *output;
+ bool last_line = FALSE, invalid_line = FALSE;
+ const char *text = NULL, *enh_code = NULL;
+ unsigned int status = 0;
+
+ i_assert(!client->destroyed);
+ i_assert(cmd != NULL);
+
+ if ((line[3] != ' ' && line[3] != '-') ||
+ str_parse_uint(line, &status, &text) < 0 ||
+ status < 200 || status >= 560) {
+ invalid_line = TRUE;
+ } else {
+ text++;
+
+ if ((subm_client->proxy_capability &
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES) != 0)
+ text = strip_enhanced_code(text, &enh_code);
+ }
+ if (subm_client->proxy_reply_status != 0 &&
+ subm_client->proxy_reply_status != status) {
+ const char *reason = t_strdup_printf(
+ "Inconsistent SMTP reply: %s (status != %u)",
+ str_sanitize(line, 160),
+ subm_client->proxy_reply_status);
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ if (line[3] == ' ') {
+ last_line = TRUE;
+ subm_client->proxy_reply_status = 0;
+ } else {
+ subm_client->proxy_reply_status = status;
+ }
+
+ output = login_proxy_get_ostream(client->login_proxy);
+ switch (subm_client->proxy_state) {
+ case SUBMISSION_PROXY_BANNER:
+ /* this is a banner */
+ if (invalid_line || status != 220) {
+ const char *reason = t_strdup_printf(
+ "Invalid banner: %s", str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+ if (!last_line)
+ return 0;
+
+ subm_client->proxy_state = SUBMISSION_PROXY_EHLO;
+ o_stream_nsend_str(output, t_strdup_printf("EHLO %s\r\n",
+ subm_client->set->hostname));
+ return 0;
+ case SUBMISSION_PROXY_EHLO:
+ case SUBMISSION_PROXY_TLS_EHLO:
+ case SUBMISSION_PROXY_XCLIENT_EHLO:
+ if (invalid_line || (status / 100) != 2) {
+ const char *reason = t_strdup_printf(
+ "Invalid EHLO line: %s",
+ str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_PROTOCOL, reason);
+ return -1;
+ }
+
+ if (strncasecmp(text, "XCLIENT ", 8) == 0) {
+ subm_client->proxy_capability |=
+ SMTP_CAPABILITY_XCLIENT;
+ i_free_and_null(subm_client->proxy_xclient);
+ subm_client->proxy_xclient = p_strarray_dup(
+ default_pool, t_strsplit_spaces(text + 8, " "));
+ } else if (strncasecmp(text, "STARTTLS", 9) == 0) {
+ subm_client->proxy_capability |=
+ SMTP_CAPABILITY_STARTTLS;
+ } else if (strncasecmp(text, "AUTH", 4) == 0 &&
+ text[4] == ' ' && text[5] != '\0') {
+ subm_client->proxy_capability |=
+ SMTP_CAPABILITY_AUTH;
+ } else if (strcasecmp(text, "ENHANCEDSTATUSCODES") == 0) {
+ subm_client->proxy_capability |=
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES;
+ }
+ if (!last_line)
+ return 0;
+
+ return proxy_handle_ehlo_reply(subm_client, output);
+ case SUBMISSION_PROXY_STARTTLS:
+ if (invalid_line || status != 220) {
+ const char *reason = t_strdup_printf(
+ "STARTTLS failed: %s",
+ str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE, reason);
+ return -1;
+ }
+ if (!last_line)
+ return 0;
+ if (login_proxy_starttls(client->login_proxy) < 0)
+ return -1;
+ /* i/ostreams changed. */
+ output = login_proxy_get_ostream(client->login_proxy);
+
+ subm_client->proxy_capability = 0;
+ i_free_and_null(subm_client->proxy_xclient);
+ subm_client->proxy_state = SUBMISSION_PROXY_TLS_EHLO;
+ o_stream_nsend_str(output, t_strdup_printf(
+ "EHLO %s\r\n", subm_client->set->hostname));
+ return 0;
+ case SUBMISSION_PROXY_XCLIENT:
+ if (invalid_line || (status / 100) != 2) {
+ const char *reason = t_strdup_printf(
+ "XCLIENT failed: %s", str_sanitize(line, 160));
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ LOGIN_PROXY_FAILURE_TYPE_REMOTE, reason);
+ return -1;
+ }
+ if (!last_line)
+ return 0;
+ i_assert(subm_client->proxy_xclient_replies_expected > 0);
+ if (--subm_client->proxy_xclient_replies_expected > 0)
+ return 0;
+ subm_client->proxy_state = SUBMISSION_PROXY_XCLIENT_EHLO;
+ return 0;
+ case SUBMISSION_PROXY_AUTHENTICATE:
+ if (invalid_line)
+ break;
+ if (status == 334 && client->proxy_sasl_client != NULL) {
+ /* continue SASL authentication */
+ if (submission_proxy_continue_sasl_auth(
+ client, output, text, last_line) < 0)
+ return -1;
+ return 0;
+ }
+
+ i_assert(subm_client->proxy_reply == NULL);
+ subm_client->proxy_reply = smtp_server_reply_create(
+ command, status, enh_code);
+ smtp_server_reply_add_text(subm_client->proxy_reply, text);
+
+ if (!last_line)
+ return 0;
+ if ((status / 100) != 2)
+ break;
+
+ smtp_server_connection_input_lock(cmd->conn);
+
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ submission_proxy_success_reply_sent, subm_client);
+
+ subm_client->pending_auth = NULL;
+
+ /* Login successful. Send this reply to client. */
+ smtp_server_reply_submit(subm_client->proxy_reply);
+
+ return 1;
+ case SUBMISSION_PROXY_STATE_COUNT:
+ i_unreached();
+ }
+
+ /* Login failed. Pass through the error message to client.
+
+ If the backend server isn't Dovecot, the error message may
+ be different from Dovecot's "user doesn't exist" error. This
+ would allow an attacker to find out what users exist in the
+ system.
+
+ The optimal way to handle this would be to replace the
+ backend's "password failed" error message with Dovecot's
+ AUTH_FAILED_MSG, but this would require a new setting and
+ the sysadmin to actually bother setting it properly.
+
+ So for now we'll just forward the error message. This
+ shouldn't be a real problem since of course everyone will
+ be using only Dovecot as their backend :) */
+ enum login_proxy_failure_type failure_type =
+ LOGIN_PROXY_FAILURE_TYPE_AUTH;
+ if ((status / 100) == 4)
+ failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL;
+ else {
+ i_assert((status / 100) != 2);
+ i_assert(subm_client->proxy_reply != NULL);
+ smtp_server_reply_submit(subm_client->proxy_reply);
+ subm_client->pending_auth = NULL;
+ }
+
+ login_proxy_failed(client->login_proxy,
+ login_proxy_get_event(client->login_proxy),
+ failure_type, text);
+ return -1;
+}
+
+void submission_proxy_reset(struct client *client)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+
+ subm_client->proxy_state = SUBMISSION_PROXY_BANNER;
+ subm_client->proxy_capability = 0;
+ i_free_and_null(subm_client->proxy_xclient);
+ i_free(subm_client->proxy_sasl_ir);
+ subm_client->proxy_reply_status = 0;
+ subm_client->proxy_reply = NULL;
+}
+
+static void
+submission_proxy_send_failure_reply(struct submission_client *subm_client,
+ enum login_proxy_failure_type type,
+ const char *reason ATTR_UNUSED)
+{
+ struct smtp_server_cmd_ctx *cmd = subm_client->pending_auth;
+
+ switch (type) {
+ case LOGIN_PROXY_FAILURE_TYPE_CONNECT:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL:
+ case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE:
+ case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+ case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+ i_assert(cmd != NULL);
+ subm_client->pending_auth = NULL;
+ smtp_server_reply(cmd, 454, "4.7.0", LOGIN_PROXY_FAILURE_MSG);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL:
+ i_assert(cmd != NULL);
+ subm_client->pending_auth = NULL;
+
+ i_assert(subm_client->proxy_reply != NULL);
+ smtp_server_reply_submit(subm_client->proxy_reply);
+ break;
+ case LOGIN_PROXY_FAILURE_TYPE_AUTH:
+ /* reply was already sent */
+ i_assert(cmd == NULL);
+ break;
+ }
+}
+
+void submission_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+
+ if (!reconnecting)
+ submission_proxy_send_failure_reply(subm_client, type, reason);
+ client_common_proxy_failed(client, type, reason, reconnecting);
+}
+
+const char *submission_proxy_get_state(struct client *client)
+{
+ struct submission_client *subm_client =
+ container_of(client, struct submission_client, common);
+
+ i_assert(subm_client->proxy_state < SUBMISSION_PROXY_STATE_COUNT);
+ return submission_proxy_state_names[subm_client->proxy_state];
+}
diff --git a/src/submission-login/submission-proxy.h b/src/submission-login/submission-proxy.h
new file mode 100644
index 0000000..19342cf
--- /dev/null
+++ b/src/submission-login/submission-proxy.h
@@ -0,0 +1,12 @@
+#ifndef SUBMISSION_PROXY_H
+#define SUBMISSION_PROXY_H
+
+void submission_proxy_reset(struct client *client);
+int submission_proxy_parse_line(struct client *client, const char *line);
+
+void submission_proxy_failed(struct client *client,
+ enum login_proxy_failure_type type,
+ const char *reason, bool reconnecting);
+const char *submission_proxy_get_state(struct client *client);
+
+#endif
diff --git a/src/submission/Makefile.am b/src/submission/Makefile.am
new file mode 100644
index 0000000..6d37277
--- /dev/null
+++ b/src/submission/Makefile.am
@@ -0,0 +1,55 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = submission
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ -I$(top_srcdir)/src/lib-smtp
+
+urlauth_libs = \
+ $(top_builddir)/src/lib-imap-urlauth/libimap-urlauth.la
+
+submission_LDFLAGS = -export-dynamic
+
+submission_LDADD = \
+ $(urlauth_libs) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(MODULE_LIBS)
+submission_DEPENDENCIES = \
+ $(urlauth_libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+submission_SOURCES = \
+ main.c \
+ submission-backend.c \
+ submission-backend-relay.c \
+ submission-recipient.c \
+ submission-client.c \
+ submission-commands.c \
+ submission-settings.c
+
+headers = \
+ submission-common.h \
+ submission-backend.h \
+ submission-backend-relay.h \
+ submission-commands.h \
+ submission-recipient.h \
+ submission-client.h \
+ submission-settings.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/submission/Makefile.in b/src/submission/Makefile.in
new file mode 100644
index 0000000..ce028c8
--- /dev/null
+++ b/src/submission/Makefile.in
@@ -0,0 +1,918 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = submission$(EXEEXT)
+subdir = src/submission
+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__installdirs = "$(DESTDIR)$(pkglibexecdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+PROGRAMS = $(pkglibexec_PROGRAMS)
+am_submission_OBJECTS = main.$(OBJEXT) submission-backend.$(OBJEXT) \
+ submission-backend-relay.$(OBJEXT) \
+ submission-recipient.$(OBJEXT) submission-client.$(OBJEXT) \
+ submission-commands.$(OBJEXT) submission-settings.$(OBJEXT)
+submission_OBJECTS = $(am_submission_OBJECTS)
+am__DEPENDENCIES_1 =
+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 =
+submission_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(submission_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)/main.Po \
+ ./$(DEPDIR)/submission-backend-relay.Po \
+ ./$(DEPDIR)/submission-backend.Po \
+ ./$(DEPDIR)/submission-client.Po \
+ ./$(DEPDIR)/submission-commands.Po \
+ ./$(DEPDIR)/submission-recipient.Po \
+ ./$(DEPDIR)/submission-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 = $(submission_SOURCES)
+DIST_SOURCES = $(submission_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; }; \
+ }
+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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-settings \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage \
+ -I$(top_srcdir)/src/lib-imap-urlauth \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/raw \
+ -I$(top_srcdir)/src/lib-smtp
+
+urlauth_libs = \
+ $(top_builddir)/src/lib-imap-urlauth/libimap-urlauth.la
+
+submission_LDFLAGS = -export-dynamic
+submission_LDADD = \
+ $(urlauth_libs) \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(MODULE_LIBS)
+
+submission_DEPENDENCIES = \
+ $(urlauth_libs) \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+submission_SOURCES = \
+ main.c \
+ submission-backend.c \
+ submission-backend-relay.c \
+ submission-recipient.c \
+ submission-client.c \
+ submission-commands.c \
+ submission-settings.c
+
+headers = \
+ submission-common.h \
+ submission-backend.h \
+ submission-backend-relay.h \
+ submission-commands.h \
+ submission-recipient.h \
+ submission-client.h \
+ submission-settings.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/submission/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/submission/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+submission$(EXEEXT): $(submission_OBJECTS) $(submission_DEPENDENCIES) $(EXTRA_submission_DEPENDENCIES)
+ @rm -f submission$(EXEEXT)
+ $(AM_V_CCLD)$(submission_LINK) $(submission_OBJECTS) $(submission_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-backend-relay.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-backend.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-commands.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-recipient.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/submission-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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-pkglibexecPROGRAMS \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/submission-backend-relay.Po
+ -rm -f ./$(DEPDIR)/submission-backend.Po
+ -rm -f ./$(DEPDIR)/submission-client.Po
+ -rm -f ./$(DEPDIR)/submission-commands.Po
+ -rm -f ./$(DEPDIR)/submission-recipient.Po
+ -rm -f ./$(DEPDIR)/submission-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-pkglibexecPROGRAMS
+
+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)/main.Po
+ -rm -f ./$(DEPDIR)/submission-backend-relay.Po
+ -rm -f ./$(DEPDIR)/submission-backend.Po
+ -rm -f ./$(DEPDIR)/submission-client.Po
+ -rm -f ./$(DEPDIR)/submission-commands.Po
+ -rm -f ./$(DEPDIR)/submission-recipient.Po
+ -rm -f ./$(DEPDIR)/submission-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 uninstall-pkglibexecPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibexecPROGRAMS \
+ 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-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS
+
+.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/submission/main.c b/src/submission/main.c
new file mode 100644
index 0000000..9168edf
--- /dev/null
+++ b/src/submission/main.c
@@ -0,0 +1,436 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "array.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "path-util.h"
+#include "process-title.h"
+#include "restrict-access.h"
+#include "fd-util.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-login.h"
+#include "master-service-settings.h"
+#include "master-interface.h"
+#include "var-expand.h"
+#include "mail-error.h"
+#include "mail-user.h"
+#include "mail-storage-service.h"
+#include "smtp-server.h"
+#include "smtp-client.h"
+
+#include "submission-commands.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define DNS_CLIENT_SOCKET_PATH "dns-client"
+
+#define LMTP_MASTER_FIRST_LISTEN_FD 3
+
+#define IS_STANDALONE() \
+ (getenv(MASTER_IS_PARENT_ENV) == NULL)
+
+struct smtp_server *smtp_server = NULL;
+struct smtp_client *smtp_client = NULL;
+
+static bool verbose_proctitle = FALSE;
+static struct mail_storage_service_ctx *storage_service;
+static struct master_login *master_login = NULL;
+
+submission_client_created_func_t *hook_client_created = NULL;
+bool submission_debug = FALSE;
+
+submission_client_created_func_t *
+submission_client_created_hook_set(submission_client_created_func_t *new_hook)
+{
+ submission_client_created_func_t *old_hook = hook_client_created;
+
+ hook_client_created = new_hook;
+ return old_hook;
+}
+
+void submission_refresh_proctitle(void)
+{
+ struct client *client;
+ string_t *title = t_str_new(128);
+
+ if (!verbose_proctitle)
+ return;
+
+ str_append_c(title, '[');
+ switch (submission_client_count) {
+ case 0:
+ str_append(title, "idling");
+ break;
+ case 1:
+ client = submission_clients;
+ str_append(title, client->user->username);
+ if (client->user->conn.remote_ip != NULL) {
+ str_append_c(title, ' ');
+ str_append(title,
+ net_ip2addr(client->user->conn.remote_ip));
+ }
+ str_append_c(title, ' ');
+ str_append(title, smtp_server_state_names[client->state.state]);
+ if (client->state.args != NULL && *client->state.args != '\0') {
+ str_append_c(title, ' ');
+ str_append(title, client->state.args);
+ }
+ break;
+ default:
+ str_printfa(title, "%u connections", submission_client_count);
+ break;
+ }
+ str_append_c(title, ']');
+ process_title_set(str_c(title));
+}
+
+static void submission_die(void)
+{
+ /* do nothing. submission connections typically die pretty quick anyway.
+ */
+}
+
+static void
+send_error(int fd_out, const char *hostname, const char *error_code,
+ const char *error_msg)
+{
+ const char *msg;
+
+ msg = t_strdup_printf("451 %s %s\r\n"
+ "421 4.3.2 %s Shutting down due to fatal error\r\n",
+ error_code, error_msg, hostname);
+ if (write(fd_out, msg, strlen(msg)) < 0) {
+ if (errno != EAGAIN && errno != EPIPE && errno != ECONNRESET)
+ i_error("write(client) failed: %m");
+ }
+}
+
+static bool
+extract_input_data_field(const unsigned char **data, size_t *data_len,
+ const char **value_r)
+{
+ size_t value_len = 0;
+
+ if (*data_len == 0)
+ return FALSE;
+
+ if (**data == '\0') {
+ value_len = 1;
+ } else {
+ *value_r = t_strndup(*data, *data_len);
+ value_len = strlen(*value_r) + 1;
+ }
+
+ if (value_len > *data_len) {
+ *data = &uchar_nul;
+ *data_len = 0;
+ } else {
+ *data = *data + value_len;
+ *data_len = *data_len - value_len;
+ }
+ return TRUE;
+}
+
+static int
+client_create_from_input(const struct mail_storage_service_input *input,
+ enum mail_auth_request_flags login_flags,
+ int fd_in, int fd_out, const buffer_t *input_buf,
+ const char **error_r)
+{
+ struct mail_storage_service_user *user;
+ struct mail_user *mail_user;
+ struct submission_settings *set;
+ bool no_greeting = HAS_ALL_BITS(login_flags,
+ MAIL_AUTH_REQUEST_FLAG_IMPLICIT);
+ const char *errstr;
+ const char *helo = NULL;
+ struct smtp_proxy_data proxy_data;
+ const unsigned char *data;
+ size_t data_len;
+
+ if (mail_storage_service_lookup_next(storage_service, input,
+ &user, &mail_user, error_r) <= 0) {
+ send_error(fd_out, my_hostname,
+ "4.7.0", MAIL_ERRSTR_CRITICAL_MSG);
+ return -1;
+ }
+ restrict_access_allow_coredumps(TRUE);
+
+ set = mail_storage_service_user_get_set(user)[1];
+ if (set->verbose_proctitle)
+ verbose_proctitle = TRUE;
+
+ if (settings_var_expand(&submission_setting_parser_info, set,
+ mail_user->pool, mail_user_var_expand_table(mail_user),
+ &errstr) <= 0) {
+ *error_r = t_strdup_printf("Failed to expand settings: %s", errstr);
+ send_error(fd_out, set->hostname,
+ "4.3.5", MAIL_ERRSTR_CRITICAL_MSG);
+ mail_user_deinit(&mail_user);
+ mail_storage_service_user_unref(&user);
+ return -1;
+ }
+
+ if (set->submission_relay_host == NULL ||
+ *set->submission_relay_host == '\0') {
+ *error_r = "No relay host configured for submission proxy "
+ "(submission_relay_host is unset)";
+ send_error(fd_out, set->hostname,
+ "4.3.5", MAIL_ERRSTR_CRITICAL_MSG);
+ mail_user_deinit(&mail_user);
+ mail_storage_service_user_unref(&user);
+ return -1;
+ }
+
+ /* parse input data */
+ data = NULL;
+ data_len = 0;
+ i_zero(&proxy_data);
+ if (input_buf != NULL && input_buf->used > 0) {
+ data = input_buf->data;
+ data_len = input_buf->used;
+
+ if (extract_input_data_field(&data, &data_len, &helo) &&
+ extract_input_data_field(&data, &data_len,
+ &proxy_data.helo)) {
+ /* nothing to do */
+ }
+
+ /* NOTE: actually, pipelining the AUTH command is stricly
+ speaking not allowed, but we support it anyway.
+ */
+ }
+
+ (void)client_create(fd_in, fd_out, mail_user,
+ user, set, helo, &proxy_data, data, data_len,
+ no_greeting);
+ return 0;
+}
+
+static void main_stdio_run(const char *username)
+{
+ struct mail_storage_service_input input;
+ buffer_t *input_buf;
+ const char *value, *error, *input_base64;
+
+ i_zero(&input);
+ input.module = input.service = "submission";
+ input.username = username != NULL ? username : getenv("USER");
+ if (input.username == NULL && IS_STANDALONE())
+ input.username = getlogin();
+ if (input.username == NULL)
+ i_fatal("USER environment missing");
+ if ((value = getenv("IP")) != NULL)
+ (void)net_addr2ip(value, &input.remote_ip);
+ if ((value = getenv("LOCAL_IP")) != NULL)
+ (void)net_addr2ip(value, &input.local_ip);
+
+ input_base64 = getenv("CLIENT_INPUT");
+ input_buf = input_base64 == NULL ? NULL :
+ t_base64_decode_str(input_base64);
+
+ if (client_create_from_input(&input, 0, STDIN_FILENO, STDOUT_FILENO,
+ input_buf, &error) < 0)
+ i_fatal("%s", error);
+}
+
+static void
+login_client_connected(const struct master_login_client *login_client,
+ const char *username, const char *const *extra_fields)
+{
+ struct mail_storage_service_input input;
+ enum mail_auth_request_flags flags = login_client->auth_req.flags;
+ const char *error;
+ buffer_t input_buf;
+
+ i_zero(&input);
+ input.module = input.service = "submission";
+ input.local_ip = login_client->auth_req.local_ip;
+ input.remote_ip = login_client->auth_req.remote_ip;
+ input.local_port = login_client->auth_req.local_port;
+ input.remote_port = login_client->auth_req.remote_port;
+ input.username = username;
+ input.userdb_fields = extra_fields;
+ input.session_id = login_client->session_id;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SECURED) != 0)
+ input.conn_secured = TRUE;
+ if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED) != 0)
+ input.conn_ssl_secured = TRUE;
+
+ buffer_create_from_const_data(&input_buf, login_client->data,
+ login_client->auth_req.data_size);
+ if (client_create_from_input(&input, flags,
+ login_client->fd, login_client->fd,
+ &input_buf, &error) < 0) {
+ int fd = login_client->fd;
+ i_error("%s", error);
+ i_close_fd(&fd);
+ master_service_client_connection_destroyed(master_service);
+ }
+}
+
+static void login_client_failed(const struct master_login_client *client,
+ const char *errormsg)
+{
+ const char *msg;
+
+ msg = t_strdup_printf("451 4.7.0 %s\r\n"
+ "421 4.3.2 %s Shutting down due to fatal error\r\n",
+ errormsg, my_hostname);
+ if (write(client->fd, msg, strlen(msg)) < 0) {
+ /* ignored */
+ }
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ /* when running standalone, we shouldn't even get here */
+ i_assert(master_login != NULL);
+
+ master_service_client_connection_accept(conn);
+ master_login_add(master_login, conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ static const struct setting_parser_info *set_roots[] = {
+ &submission_setting_parser_info,
+ NULL
+ };
+ struct master_login_settings login_set;
+ enum master_service_flags service_flags = 0;
+ enum mail_storage_service_flags storage_service_flags = 0;
+ struct smtp_server_settings smtp_server_set;
+ struct smtp_client_settings smtp_client_set;
+ const char *username = NULL, *auth_socket_path = "auth-master";
+ const char *tmp_socket_path;
+ const char *error;
+ int c;
+
+ i_zero(&login_set);
+ login_set.postlogin_timeout_secs = MASTER_POSTLOGIN_TIMEOUT_DEFAULT;
+ login_set.request_auth_token = TRUE;
+
+ if (IS_STANDALONE() && getuid() == 0 &&
+ net_getpeername(1, NULL, NULL) == 0) {
+ printf("421 5.3.5 The submission binary must not be started "
+ "from inetd, use submission-login instead.\r\n");
+ return 1;
+ }
+
+ if (IS_STANDALONE()) {
+ service_flags |= MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_STD_CLIENT;
+ } else {
+ service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN;
+ }
+
+ master_service = master_service_init("submission", service_flags,
+ &argc, &argv, "a:Dt:u:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'a':
+ auth_socket_path = optarg;
+ break;
+ case 't':
+ if (str_to_uint(optarg,
+ &login_set.postlogin_timeout_secs) < 0 ||
+ login_set.postlogin_timeout_secs == 0)
+ i_fatal("Invalid -t parameter: %s", optarg);
+ break;
+ case 'u':
+ storage_service_flags |=
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+ username = optarg;
+ break;
+ case 'D':
+ submission_debug = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+
+ if (t_abspath(auth_socket_path, &login_set.auth_socket_path,
+ &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s", auth_socket_path,
+ error);
+ }
+ if (argv[optind] != NULL) {
+ if (t_abspath(argv[optind],
+ &login_set.postlogin_socket_path, &error) < 0) {
+ i_fatal("t_abspath(%s) failed: %s",
+ argv[optind], error);
+ }
+ }
+ login_set.callback = login_client_connected;
+ login_set.failure_callback = login_client_failed;
+ login_set.update_proctitle =
+ getenv(MASTER_VERBOSE_PROCTITLE_ENV) != NULL &&
+ master_service_get_client_limit(master_service) == 1;
+
+ master_service_set_die_callback(master_service, submission_die);
+
+ storage_service =
+ mail_storage_service_init(master_service,
+ set_roots, storage_service_flags);
+
+ /* initialize SMTP server */
+ i_zero(&smtp_server_set);
+ smtp_server_set.capabilities = SMTP_CAPABILITY_DSN;
+ smtp_server_set.protocol = SMTP_PROTOCOL_SMTP;
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.debug = submission_debug;
+ smtp_server_set.reason_code_module = "submission";
+ smtp_server = smtp_server_init(&smtp_server_set);
+ smtp_server_command_register(smtp_server, "BURL", cmd_burl, 0);
+
+ if (t_abspath(DNS_CLIENT_SOCKET_PATH, &tmp_socket_path, &error) < 0)
+ i_fatal("t_abspath(%s) failed: %s", DNS_CLIENT_SOCKET_PATH, error);
+
+ /* initialize SMTP client */
+ i_zero(&smtp_client_set);
+ smtp_client_set.my_hostname = my_hostdomain();
+ smtp_client_set.debug = submission_debug;
+ smtp_client_set.dns_client_socket_path = tmp_socket_path;
+ smtp_client = smtp_client_init(&smtp_client_set);
+
+ if (!IS_STANDALONE())
+ master_login = master_login_init(master_service, &login_set);
+
+ master_service_init_finish(master_service);
+ /* NOTE: login_set.*_socket_path are now invalid due to data stack
+ having been freed */
+
+ /* fake that we're running, so we know if client was destroyed
+ while handling its initial input */
+ io_loop_set_running(current_ioloop);
+
+ if (IS_STANDALONE()) {
+ T_BEGIN {
+ main_stdio_run(username);
+ } T_END;
+ } else {
+ io_loop_set_running(current_ioloop);
+ }
+
+ if (io_loop_is_running(current_ioloop))
+ master_service_run(master_service, client_connected);
+ clients_destroy_all();
+
+ smtp_client_deinit(&smtp_client);
+ smtp_server_deinit(&smtp_server);
+
+ if (master_login != NULL)
+ master_login_deinit(&master_login);
+ mail_storage_service_deinit(&storage_service);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/submission/submission-backend-relay.c b/src/submission/submission-backend-relay.c
new file mode 100644
index 0000000..eededa0
--- /dev/null
+++ b/src/submission/submission-backend-relay.c
@@ -0,0 +1,1260 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "mail-user.h"
+#include "iostream-ssl.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+#include "smtp-client-command.h"
+
+#include "submission-recipient.h"
+#include "submission-backend-relay.h"
+
+struct submission_backend_relay {
+ struct submission_backend backend;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+
+ bool trans_started:1;
+ bool trusted:1;
+ bool quit_confirmed:1;
+};
+
+static struct submission_backend_vfuncs backend_relay_vfuncs;
+
+/*
+ * Common
+ */
+
+/* The command handling of the submission relay service aims to follow the
+ following rules:
+
+ - Attempt to keep pipelined commands pipelined when relaying them to the
+ actual relay service.
+ - Don't forward commands if they're known to fail at the relay server. Errors
+ can still occur if pipelined commands fail. Abort subsequent pending
+ commands if such failures affect those commands.
+ - Keep predictable errors consistent as much as possible; send our own reply
+ if the error condition is clear (e.g. missing MAIL, RCPT).
+*/
+
+static bool
+backend_relay_handle_relay_reply(struct submission_backend_relay *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const struct smtp_reply *reply,
+ struct smtp_reply *reply_r) ATTR_NULL(2)
+{
+ struct client *client = backend->backend.client;
+ struct mail_user *user = client->user;
+ const char *enh_code, *msg, *log_msg = NULL;
+ const char *const *reply_lines;
+ bool result = TRUE;
+
+ *reply_r = *reply;
+
+ switch (reply->status) {
+ case SMTP_CLIENT_COMMAND_ERROR_ABORTED:
+ return FALSE;
+ case SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED:
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED:
+ case SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED:
+ enh_code = "4.4.0";
+ msg = "Failed to connect to relay server";
+ result = FALSE;
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED:
+ enh_code = smtp_reply_get_enh_code(reply);
+ log_msg = "Lost connection to relay server";
+ reply_lines = smtp_reply_get_text_lines_omit_prefix(reply);
+ msg = t_strconcat("Lost connection to relay server:\n",
+ t_strarray_join(reply_lines, "\n"), NULL);
+ result = FALSE;
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST:
+ case SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY:
+ case SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT:
+ enh_code = "4.4.0";
+ log_msg = msg = "Lost connection to relay server";
+ result = FALSE;
+ break;
+ /* RFC 4954, Section 6: 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. */
+ case 530:
+ log_msg = "Relay server requires authentication";
+ enh_code = "4.3.5",
+ msg = "Internal error occurred. "
+ "Refer to server log for more information.";
+ result = FALSE;
+ break;
+ default:
+ break;
+ }
+
+ if (!result) {
+ const char *detail = "", *reason;
+
+ i_assert(msg != NULL);
+
+ switch (reply->status) {
+ case SMTP_CLIENT_COMMAND_ERROR_ABORTED:
+ i_unreached();
+ case SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED:
+ detail = " (DNS lookup)";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED:
+ case SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED:
+ detail = " (connect)";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST:
+ case SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED:
+ if (backend->quit_confirmed)
+ return FALSE;
+ detail = " (connection lost)";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY:
+ detail = " (bad reply)";
+ break;
+ case SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT:
+ detail = " (timed out)";
+ break;
+ default:
+ break;
+ }
+
+ reason = t_strdup_printf("%s%s", msg, detail);
+ smtp_client_transaction_destroy(&backend->trans);
+ if (log_msg != NULL) {
+ if (smtp_reply_is_remote(reply)) {
+ i_error("%s: %s",
+ log_msg, smtp_reply_log(reply));
+ } else if (user->mail_debug) {
+ i_debug("%s: %s",
+ log_msg, smtp_reply_log(reply));
+ }
+ }
+ submission_backend_fail(&backend->backend, cmd,
+ enh_code, reason);
+ return FALSE;
+ }
+
+ if (!smtp_reply_has_enhanced_code(reply)) {
+ reply_r->enhanced_code =
+ SMTP_REPLY_ENH_CODE(reply->status / 100, 0, 0);
+ }
+ return TRUE;
+}
+
+/*
+ * Mail transaction
+ */
+
+static void
+backend_relay_trans_finished(struct submission_backend_relay *backend)
+{
+ backend->trans = NULL;
+}
+
+static void
+backend_relay_trans_start_callback(
+ const struct smtp_reply *relay_reply ATTR_UNUSED,
+ struct submission_backend_relay *backend ATTR_UNUSED)
+{
+ /* nothing to do */
+}
+
+static void
+backend_relay_trans_start(struct submission_backend *_backend,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ const struct smtp_address *path,
+ const struct smtp_params_mail *params)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ if (backend->trans == NULL) {
+ backend->trans_started = TRUE;
+ backend->trans = smtp_client_transaction_create(
+ backend->conn, path, params, 0,
+ backend_relay_trans_finished, backend);
+ smtp_client_transaction_set_immediate(backend->trans, TRUE);
+ smtp_client_transaction_start(
+ backend->trans, backend_relay_trans_start_callback,
+ backend);
+ } else if (!backend->trans_started) {
+ backend->trans_started = TRUE;
+ smtp_client_transaction_start_empty(
+ backend->trans, path, params,
+ backend_relay_trans_start_callback, backend);
+ }
+}
+
+static void
+backend_relay_trans_free(struct submission_backend *_backend,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ backend->trans_started = FALSE;
+
+ if (backend->trans == NULL)
+ return;
+
+ smtp_client_transaction_destroy(&backend->trans);
+}
+
+struct smtp_client_transaction *
+submission_backend_relay_init_transaction(
+ struct submission_backend_relay *backend,
+ enum smtp_client_transaction_flags flags)
+{
+ i_assert(backend->trans == NULL);
+
+ backend->trans = smtp_client_transaction_create_empty(
+ backend->conn, flags,
+ backend_relay_trans_finished, backend);
+ smtp_client_transaction_set_immediate(backend->trans, TRUE);
+
+ return backend->trans;
+}
+
+/*
+ * EHLO, HELO commands
+ */
+
+struct relay_cmd_helo_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+ struct smtp_server_cmd_helo *data;
+
+ struct smtp_client_command *cmd_relayed;
+};
+
+static void
+relay_cmd_helo_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_helo_context *helo_cmd)
+{
+ i_assert(helo_cmd != NULL);
+ if (helo_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&helo_cmd->cmd_relayed);
+}
+
+static void
+relay_cmd_helo_update_xclient(struct submission_backend_relay *backend,
+ struct smtp_server_cmd_helo *data)
+{
+ struct smtp_proxy_data proxy_data;
+
+ if (!backend->trusted)
+ return;
+
+ i_zero(&proxy_data);
+ proxy_data.helo = data->helo.domain;
+ smtp_client_connection_update_proxy_data(backend->conn, &proxy_data);
+}
+
+static void
+relay_cmd_helo_reply(struct smtp_server_cmd_ctx *cmd,
+ struct relay_cmd_helo_context *helo_cmd)
+{
+ struct submission_backend_relay *backend = helo_cmd->backend;
+
+ if (helo_cmd->data->changed)
+ relay_cmd_helo_update_xclient(backend, helo_cmd->data);
+
+ T_BEGIN {
+ submission_backend_helo_reply_submit(&backend->backend, cmd,
+ helo_cmd->data);
+ } T_END;
+}
+
+static void
+relay_cmd_helo_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_helo_context *helo_cmd)
+{
+ i_assert(helo_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = helo_cmd->cmd;
+ struct submission_backend_relay *backend = helo_cmd->backend;
+ struct smtp_reply reply;
+
+ /* finished relaying EHLO command to relay server */
+ helo_cmd->cmd_relayed = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(&reply)) {
+ relay_cmd_helo_reply(cmd, helo_cmd);
+ } else {
+ /* RFC 2034, Section 4:
+
+ These codes must appear in all 2xx, 4xx, and 5xx response
+ lines other than initial greeting and any response to HELO
+ or EHLO.
+ */
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE_NONE;
+ smtp_server_reply_forward(cmd, &reply);
+ }
+}
+
+static void
+relay_cmd_helo_start(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_helo_context *helo_cmd)
+{
+ struct submission_backend_relay *backend = helo_cmd->backend;
+
+ if (helo_cmd->data->changed)
+ relay_cmd_helo_update_xclient(backend, helo_cmd->data);
+}
+
+static int
+backend_relay_cmd_helo(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_helo_context *helo_cmd;
+
+ helo_cmd = p_new(cmd->pool, struct relay_cmd_helo_context, 1);
+ helo_cmd->backend = backend;
+ helo_cmd->cmd = cmd;
+ helo_cmd->data = data;
+
+ /* This is not the first HELO/EHLO; just relay a RSET command */
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ relay_cmd_helo_start, helo_cmd);
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ relay_cmd_helo_destroy, helo_cmd);
+ helo_cmd->cmd_relayed = smtp_client_command_rset_submit(
+ backend->conn, 0, relay_cmd_helo_callback, helo_cmd);
+ return 0;
+}
+
+/*
+ * MAIL command
+ */
+
+struct relay_cmd_mail_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+ struct smtp_server_cmd_mail *data;
+
+ struct smtp_client_transaction_mail *relay_mail;
+};
+
+static void
+relay_cmd_mail_replied(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_mail_context *mail_cmd)
+{
+ if (mail_cmd->relay_mail != NULL)
+ smtp_client_transaction_mail_abort(&mail_cmd->relay_mail);
+}
+
+static void
+relay_cmd_mail_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_mail_context *mail_cmd)
+{
+ i_assert(mail_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = mail_cmd->cmd;
+ struct submission_backend_relay *backend = mail_cmd->backend;
+ struct smtp_reply reply;
+
+ /* finished relaying MAIL command to relay server */
+ mail_cmd->relay_mail = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(relay_reply)) {
+ /* if relay accepts it, we accept it too */
+
+ /* the default 2.0.0 code won't do */
+ if (!smtp_reply_has_enhanced_code(relay_reply))
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(2, 1, 0);
+ }
+
+ /* forward reply */
+ smtp_server_reply_forward(cmd, &reply);
+}
+
+static int
+relay_cmd_mail_parameter_auth(struct submission_backend_relay *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ enum smtp_capability relay_caps,
+ struct smtp_server_cmd_mail *data)
+{
+ struct client *client = backend->backend.client;
+ struct smtp_params_mail *params = &data->params;
+ struct smtp_address *auth_addr;
+ const char *error;
+
+ if ((relay_caps & SMTP_CAPABILITY_AUTH) == 0)
+ return 0;
+
+ auth_addr = NULL;
+ if (smtp_address_parse_username(cmd->pool, client->user->username,
+ &auth_addr, &error) < 0) {
+ i_warning("Username `%s' is not a valid SMTP address: %s",
+ client->user->username, error);
+ }
+
+ params->auth = auth_addr;
+ return 0;
+}
+
+static int
+relay_cmd_mail_parameter_size(struct submission_backend_relay *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ enum smtp_capability relay_caps,
+ struct smtp_server_cmd_mail *data)
+{
+ struct client *client = backend->backend.client;
+ uoff_t max_size;
+
+ /* SIZE=<size-value>: RFC 1870 */
+
+ if (data->params.size == 0 || (relay_caps & SMTP_CAPABILITY_SIZE) == 0)
+ return 0;
+
+ /* determine actual size limit (account for our additions) */
+ max_size = client_get_max_mail_size(client);
+ if (max_size > 0 && data->params.size > max_size) {
+ smtp_server_reply(
+ cmd, 552, "5.3.4",
+ "Message size exceeds fixed maximum message size");
+ return -1;
+ }
+
+ /* relay the SIZE parameter (account for additional size) */
+ data->params.size += SUBMISSION_MAX_ADDITIONAL_MAIL_SIZE;
+ return 0;
+}
+
+static int
+backend_relay_cmd_mail(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ enum smtp_capability relay_caps =
+ smtp_client_connection_get_capabilities(backend->conn);
+ struct relay_cmd_mail_context *mail_cmd;
+
+ /* check and adjust parameters where necessary */
+ if (relay_cmd_mail_parameter_auth(backend, cmd, relay_caps, data) < 0)
+ return -1;
+ if (relay_cmd_mail_parameter_size(backend, cmd, relay_caps, data) < 0)
+ return -1;
+
+ /* queue command (pipeline) */
+ mail_cmd = p_new(cmd->pool, struct relay_cmd_mail_context, 1);
+ mail_cmd->backend = backend;
+ mail_cmd->cmd = cmd;
+ mail_cmd->data = data;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ relay_cmd_mail_replied, mail_cmd);
+
+ if (backend->trans == NULL) {
+ /* start client transaction */
+ backend->trans_started = TRUE;
+ backend->trans = smtp_client_transaction_create(
+ backend->conn, data->path, &data->params, 0,
+ backend_relay_trans_finished, backend);
+ smtp_client_transaction_set_immediate(backend->trans, TRUE);
+ smtp_client_transaction_start(
+ backend->trans, relay_cmd_mail_callback, mail_cmd);
+ } else {
+ /* forward pipelined MAIL command */
+ i_assert(backend->trans_started);
+ mail_cmd->relay_mail = smtp_client_transaction_add_mail(
+ backend->trans, data->path, &data->params,
+ relay_cmd_mail_callback, mail_cmd);
+ }
+ return 0;
+}
+
+/*
+ * RCPT command
+ */
+
+struct relay_cmd_rcpt_context {
+ struct submission_backend_relay *backend;
+ struct submission_recipient *rcpt;
+
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct smtp_client_transaction_rcpt *relay_rcpt;
+};
+
+static void
+relay_cmd_rcpt_replied(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_rcpt_context *rcpt_cmd)
+{
+ if (rcpt_cmd->relay_rcpt != NULL)
+ smtp_client_transaction_rcpt_abort(&rcpt_cmd->relay_rcpt);
+}
+
+static void
+relay_cmd_rcpt_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_rcpt_context *rcpt_cmd)
+{
+ i_assert(rcpt_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = rcpt_cmd->cmd;
+ struct submission_backend_relay *backend = rcpt_cmd->backend;
+ struct submission_recipient *srcpt = rcpt_cmd->rcpt;
+ struct smtp_server_recipient *rcpt = srcpt->rcpt;
+ struct smtp_client_transaction_rcpt *relay_rcpt = rcpt_cmd->relay_rcpt;
+ struct smtp_reply reply;
+
+ /* finished relaying RCPT command to relay server */
+ rcpt_cmd->relay_rcpt = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(&reply)) {
+ /* the default 2.0.0 code won't do */
+ if (!smtp_reply_has_enhanced_code(&reply))
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(2, 1, 5);
+
+ i_assert(relay_rcpt != NULL);
+ srcpt->backend_context = relay_rcpt;
+ }
+
+ /* forward reply */
+ smtp_server_recipient_reply_forward(rcpt, &reply);
+}
+
+static int
+backend_relay_cmd_rcpt(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct smtp_server_recipient *rcpt = srcpt->rcpt;
+ struct relay_cmd_rcpt_context *rcpt_cmd;
+
+ /* queue command (pipeline) */
+ rcpt_cmd = p_new(cmd->pool, struct relay_cmd_rcpt_context, 1);
+ rcpt_cmd->backend = backend;
+ rcpt_cmd->cmd = cmd;
+ rcpt_cmd->rcpt = srcpt;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ relay_cmd_rcpt_replied, rcpt_cmd);
+
+ if (backend->trans == NULL)
+ (void)submission_backend_relay_init_transaction(backend, 0);
+ rcpt_cmd->relay_rcpt = smtp_client_transaction_add_pool_rcpt(
+ backend->trans, rcpt->pool, rcpt->path, &rcpt->params,
+ relay_cmd_rcpt_callback, rcpt_cmd);
+ return 0;
+}
+
+/*
+ * RSET command
+ */
+
+struct relay_cmd_rset_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct smtp_client_command *cmd_relayed;
+};
+
+static void
+relay_cmd_rset_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_rset_context *rset_cmd)
+{
+ i_assert(rset_cmd != NULL);
+ if (rset_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&rset_cmd->cmd_relayed);
+}
+
+static void
+relay_cmd_rset_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_rset_context *rset_cmd)
+{
+ i_assert(rset_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = rset_cmd->cmd;
+ struct submission_backend_relay *backend = rset_cmd->backend;
+ struct smtp_reply reply;
+
+ /* finished relaying MAIL command to relay server */
+ rset_cmd->cmd_relayed = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ /* forward reply */
+ smtp_server_reply_forward(cmd, &reply);
+}
+
+static int
+backend_relay_cmd_rset(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_rset_context *rset_cmd;
+
+ rset_cmd = p_new(cmd->pool, struct relay_cmd_rset_context, 1);
+ rset_cmd->backend = backend;
+ rset_cmd->cmd = cmd;
+
+ if (backend->trans != NULL) {
+ /* RSET pipelined after MAIL */
+ smtp_client_transaction_reset(backend->trans,
+ relay_cmd_rset_callback,
+ rset_cmd);
+ } else {
+ /* RSET alone */
+ smtp_server_command_add_hook(cmd->cmd,
+ SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ relay_cmd_rset_destroy, rset_cmd);
+ rset_cmd->cmd_relayed = smtp_client_command_rset_submit(
+ backend->conn, 0, relay_cmd_rset_callback, rset_cmd);
+ }
+ return 0;
+}
+
+/*
+ * DATA/BDAT commands
+ */
+
+struct relay_cmd_data_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+ struct smtp_server_transaction *trans;
+};
+
+static void
+relay_cmd_data_rcpt_callback(const struct smtp_reply *relay_reply,
+ struct submission_recipient *srcpt)
+{
+ struct smtp_server_recipient *rcpt = srcpt->rcpt;
+ struct smtp_server_cmd_ctx *cmd = rcpt->cmd;
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)srcpt->backend;
+ struct client *client = srcpt->backend->client;
+ struct smtp_server_transaction *trans =
+ smtp_server_connection_get_transaction(client->conn);
+ struct smtp_reply reply;
+
+ i_assert(HAS_ALL_BITS(trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT));
+
+ /* check for fatal problems */
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(&reply)) {
+ i_info("Successfully relayed message: "
+ "from=<%s>, to=<%s>, size=%"PRIuUOFF_T", "
+ "id=%s, rcpt=%u/%u, reply=`%s'",
+ smtp_address_encode(trans->mail_from),
+ smtp_address_encode(rcpt->path),
+ client->state.data_size, trans->id,
+ rcpt->index, array_count(&trans->rcpt_to),
+ str_sanitize(smtp_reply_log(&reply), 128));
+
+ } else {
+ i_info("Failed to relay message: "
+ "from=<%s>, to=<%s>, size=%"PRIuUOFF_T", "
+ "rcpt=%u/%u, reply=`%s'",
+ smtp_address_encode(trans->mail_from),
+ smtp_address_encode(rcpt->path),
+ client->state.data_size, rcpt->index,
+ array_count(&trans->rcpt_to),
+ str_sanitize(smtp_reply_log(&reply), 128));
+ }
+
+ smtp_server_recipient_reply_forward(rcpt, &reply);
+}
+
+static void
+relay_cmd_data_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_data_context *data_ctx)
+{
+ i_assert(data_ctx != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = data_ctx->cmd;
+ struct smtp_server_transaction *trans = data_ctx->trans;
+ struct submission_backend_relay *backend = data_ctx->backend;
+ struct client *client = backend->backend.client;
+ struct smtp_reply reply;
+
+ /* finished relaying message to relay server */
+
+ if (HAS_ALL_BITS(trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT)) {
+ /* handled recipient replies individually */
+ return;
+ }
+
+ /* check for fatal problems */
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(&reply)) {
+ i_info("Successfully relayed message: "
+ "from=<%s>, size=%"PRIuUOFF_T", "
+ "id=%s, nrcpt=%u, reply=`%s'",
+ smtp_address_encode(trans->mail_from),
+ client->state.data_size, trans->id,
+ array_count(&trans->rcpt_to),
+ str_sanitize(smtp_reply_log(&reply), 128));
+
+ } else {
+ i_info("Failed to relay message: "
+ "from=<%s>, size=%"PRIuUOFF_T", nrcpt=%u, reply=`%s'",
+ smtp_address_encode(trans->mail_from),
+ client->state.data_size, array_count(&trans->rcpt_to),
+ str_sanitize(smtp_reply_log(&reply), 128));
+ }
+
+ smtp_server_reply_forward(cmd, &reply);
+}
+
+static void
+backend_relay_cmd_data_init_callbacks(struct submission_backend_relay *backend,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = backend->backend.client;
+ struct submission_recipient *rcpt;
+
+ if (!HAS_ALL_BITS(trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT))
+ return;
+
+ array_foreach_elem(&client->rcpt_to, rcpt) {
+ struct smtp_client_transaction_rcpt *relay_rcpt =
+ rcpt->backend_context;
+
+ smtp_client_transaction_rcpt_set_data_callback(
+ relay_rcpt, relay_cmd_data_rcpt_callback, rcpt);
+ }
+}
+
+static int
+backend_relay_cmd_data(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size ATTR_UNUSED)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_data_context *data_ctx;
+
+ /* start relaying to relay server */
+ data_ctx = p_new(trans->pool, struct relay_cmd_data_context, 1);
+ data_ctx->backend = backend;
+ data_ctx->cmd = cmd;
+ data_ctx->trans = trans;
+ trans->context = (void*)data_ctx;
+
+ i_assert(backend->trans != NULL);
+
+ backend_relay_cmd_data_init_callbacks(backend, trans);
+
+ smtp_client_transaction_send(backend->trans, data_input,
+ relay_cmd_data_callback, data_ctx);
+ return 0;
+}
+
+/*
+ * VRFY command
+ */
+
+struct relay_cmd_vrfy_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct smtp_client_command *cmd_relayed;
+};
+
+static void
+relay_cmd_vrfy_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_vrfy_context *vrfy_cmd)
+{
+ i_assert(vrfy_cmd != NULL);
+ if (vrfy_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&vrfy_cmd->cmd_relayed);
+}
+
+static void
+relay_cmd_vrfy_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_vrfy_context *vrfy_cmd)
+{
+ i_assert(vrfy_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = vrfy_cmd->cmd;
+ struct submission_backend_relay *backend = vrfy_cmd->backend;
+ struct smtp_reply reply;
+
+ /* finished relaying VRFY command to relay server */
+ vrfy_cmd->cmd_relayed = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ 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.
+ */
+ if (reply.status == 500 || reply.status == 502) {
+ smtp_server_cmd_vrfy_reply_default(cmd);
+ return;
+ }
+
+ if (!smtp_reply_has_enhanced_code(&reply)) {
+ switch (relay_reply->status) {
+ case 250:
+ case 251:
+ case 252:
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(2, 5, 0);
+ break;
+ default:
+ break;
+ }
+ }
+
+ smtp_server_reply_forward(cmd, &reply);
+}
+
+static int
+backend_relay_cmd_vrfy(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd, const char *param)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_vrfy_context *vrfy_cmd;
+
+ vrfy_cmd = p_new(cmd->pool, struct relay_cmd_vrfy_context, 1);
+ vrfy_cmd->backend = backend;
+ vrfy_cmd->cmd = cmd;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ relay_cmd_vrfy_destroy, vrfy_cmd);
+ vrfy_cmd->cmd_relayed = smtp_client_command_vrfy_submit(
+ backend->conn, 0, param, relay_cmd_vrfy_callback, vrfy_cmd);
+ return 0;
+}
+
+/*
+ * NOOP command
+ */
+
+struct relay_cmd_noop_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct smtp_client_command *cmd_relayed;
+};
+
+static void
+relay_cmd_noop_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_noop_context *noop_cmd)
+{
+ i_assert(noop_cmd != NULL);
+ if (noop_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&noop_cmd->cmd_relayed);
+}
+
+static void
+relay_cmd_noop_callback(const struct smtp_reply *relay_reply,
+ struct relay_cmd_noop_context *noop_cmd)
+{
+ i_assert(noop_cmd != NULL);
+
+ struct smtp_server_cmd_ctx *cmd = noop_cmd->cmd;
+ struct submission_backend_relay *backend = noop_cmd->backend;
+ struct smtp_reply reply;
+
+ /* finished relaying NOOP command to relay server */
+ noop_cmd->cmd_relayed = NULL;
+
+ if (!backend_relay_handle_relay_reply(backend, cmd, relay_reply,
+ &reply))
+ return;
+
+ if (smtp_reply_is_success(&reply))
+ smtp_server_cmd_noop_reply_success(cmd);
+ else
+ smtp_server_reply_forward(cmd, &reply);
+}
+
+static int
+backend_relay_cmd_noop(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_noop_context *noop_cmd;
+
+ noop_cmd = p_new(cmd->pool, struct relay_cmd_noop_context, 1);
+ noop_cmd->backend = backend;
+ noop_cmd->cmd = cmd;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ relay_cmd_noop_destroy, noop_cmd);
+ noop_cmd->cmd_relayed = smtp_client_command_noop_submit(
+ backend->conn, 0, relay_cmd_noop_callback, noop_cmd);
+ return 0;
+}
+
+/*
+ * QUIT command
+ */
+
+struct relay_cmd_quit_context {
+ struct submission_backend_relay *backend;
+
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct smtp_client_command *cmd_relayed;
+};
+
+static void
+relay_cmd_quit_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_quit_context *quit_cmd)
+{
+ i_assert(quit_cmd != NULL);
+ if (quit_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&quit_cmd->cmd_relayed);
+}
+
+static void relay_cmd_quit_relayed_destroy(void *context)
+{
+ struct relay_cmd_quit_context *quit_cmd = context;
+
+ i_assert(quit_cmd != NULL);
+ quit_cmd->cmd_relayed = NULL;
+}
+
+static void
+relay_cmd_quit_replied(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_quit_context *quit_cmd)
+{
+ if (quit_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&quit_cmd->cmd_relayed);
+}
+
+static void relay_cmd_quit_finish(struct relay_cmd_quit_context *quit_cmd)
+{
+ struct smtp_server_cmd_ctx *cmd = quit_cmd->cmd;
+
+ quit_cmd->backend->quit_confirmed = TRUE;
+ if (quit_cmd->cmd_relayed != NULL)
+ smtp_client_command_abort(&quit_cmd->cmd_relayed);
+ smtp_server_reply_quit(cmd);
+}
+
+static void
+relay_cmd_quit_callback(const struct smtp_reply *relay_reply ATTR_UNUSED,
+ struct relay_cmd_quit_context *quit_cmd)
+{
+ i_assert(quit_cmd != NULL);
+ quit_cmd->cmd_relayed = NULL;
+ relay_cmd_quit_finish(quit_cmd);
+}
+
+static void relay_cmd_quit_relay(struct relay_cmd_quit_context *quit_cmd)
+{
+ struct submission_backend_relay *backend = quit_cmd->backend;
+ struct smtp_server_cmd_ctx *cmd = quit_cmd->cmd;
+
+ if (quit_cmd->cmd_relayed != NULL)
+ return;
+
+ if (smtp_client_connection_get_state(backend->conn)
+ < SMTP_CLIENT_CONNECTION_STATE_READY) {
+ /* Don't bother relaying QUIT command when relay is not
+ fully initialized. */
+ quit_cmd->backend->quit_confirmed = TRUE;
+ smtp_server_reply_quit(cmd);
+ return;
+ }
+
+ /* RFC 5321, Section 4.1.1.10:
+
+ The sender MUST NOT intentionally close the transmission channel
+ until it sends a QUIT command, and it SHOULD wait until it receives
+ the reply (even if there was an error response to a previous
+ command). */
+ quit_cmd->cmd_relayed =
+ smtp_client_command_new(backend->conn, 0,
+ relay_cmd_quit_callback, quit_cmd);
+ smtp_client_command_write(quit_cmd->cmd_relayed, "QUIT");
+ smtp_client_command_set_abort_callback(
+ quit_cmd->cmd_relayed,
+ relay_cmd_quit_relayed_destroy, quit_cmd);
+ smtp_client_command_submit(quit_cmd->cmd_relayed);
+}
+
+static void
+relay_cmd_quit_next(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct relay_cmd_quit_context *quit_cmd)
+{
+ /* QUIT command is next to reply */
+ relay_cmd_quit_relay(quit_cmd);
+}
+
+static int
+backend_relay_cmd_quit(struct submission_backend *_backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+ struct relay_cmd_quit_context *quit_cmd;
+
+ quit_cmd = p_new(cmd->pool, struct relay_cmd_quit_context, 1);
+ quit_cmd->backend = backend;
+ quit_cmd->cmd = cmd;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ relay_cmd_quit_next, quit_cmd);
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ relay_cmd_quit_replied, quit_cmd);
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ relay_cmd_quit_destroy, quit_cmd);
+
+ if (smtp_client_connection_get_state(backend->conn)
+ >= SMTP_CLIENT_CONNECTION_STATE_READY)
+ relay_cmd_quit_relay(quit_cmd);
+ return 0;
+}
+
+/*
+ * Relay backend
+ */
+
+struct submission_backend_relay *
+submission_backend_relay_create(
+ struct client *client,
+ const struct submision_backend_relay_settings *set)
+{
+ struct submission_backend_relay *backend;
+ struct mail_user *user = client->user;
+ struct ssl_iostream_settings ssl_set;
+ struct smtp_client_settings smtp_set;
+ pool_t pool;
+
+ pool = pool_alloconly_create("submission relay backend", 1024);
+ backend = p_new(pool, struct submission_backend_relay, 1);
+ submission_backend_init(&backend->backend, pool, client,
+ &backend_relay_vfuncs);
+
+ mail_user_init_ssl_client_settings(user, &ssl_set);
+ if (set->ssl_verify)
+ ssl_set.verbose_invalid_cert = TRUE;
+ else
+ ssl_set.allow_invalid_cert = TRUE;
+
+ /* make relay connection */
+ i_zero(&smtp_set);
+ smtp_set.my_hostname = set->my_hostname;
+ smtp_set.extra_capabilities = set->extra_capabilities;
+ smtp_set.ssl = &ssl_set;
+ smtp_set.debug = user->mail_debug;
+
+ if (set->rawlog_dir != NULL) {
+ smtp_set.rawlog_dir =
+ mail_user_home_expand(user, set->rawlog_dir);
+ }
+
+ if (set->trusted) {
+ backend->trusted = TRUE;
+ smtp_set.peer_trusted = TRUE;
+
+ smtp_server_connection_get_proxy_data(client->conn,
+ &smtp_set.proxy_data);
+
+ if (user->conn.remote_ip != NULL) {
+ smtp_set.proxy_data.source_ip =
+ *user->conn.remote_ip;
+ smtp_set.proxy_data.source_port =
+ user->conn.remote_port;
+ }
+ smtp_set.proxy_data.login = user->username;
+ smtp_set.xclient_defer = TRUE;
+ }
+
+ smtp_set.username = set->user;
+ smtp_set.master_user = set->master_user;
+ smtp_set.password = set->password;
+ smtp_set.sasl_mech = set->sasl_mech;
+ smtp_set.connect_timeout_msecs = set->connect_timeout_msecs;
+ smtp_set.command_timeout_msecs = set->command_timeout_msecs;
+
+ if (set->path != NULL) {
+ backend->conn = smtp_client_connection_create_unix(
+ smtp_client, set->protocol, set->path, &smtp_set);
+ } else if (set->ip.family == 0) {
+ backend->conn = smtp_client_connection_create(
+ smtp_client, set->protocol, set->host, set->port,
+ set->ssl_mode, &smtp_set);
+ } else {
+ backend->conn = smtp_client_connection_create_ip(
+ smtp_client, set->protocol, &set->ip, set->port,
+ set->host, set->ssl_mode, &smtp_set);
+ }
+
+ return backend;
+}
+
+struct submission_backend *
+submission_backend_relay_get(struct submission_backend_relay *backend)
+{
+ return &backend->backend;
+}
+
+struct smtp_client_connection *
+submission_backend_relay_get_connection(
+ struct submission_backend_relay *backend)
+{
+ return backend->conn;
+}
+
+struct smtp_client_transaction *
+submission_backend_relay_get_transaction(
+ struct submission_backend_relay *backend)
+{
+ return backend->trans;
+}
+
+static void backend_relay_destroy(struct submission_backend *_backend)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ if (backend->trans != NULL)
+ smtp_client_transaction_destroy(&backend->trans);
+ if (backend->conn != NULL)
+ smtp_client_connection_close(&backend->conn);
+}
+
+static void backend_relay_ready_cb(const struct smtp_reply *reply,
+ void *context)
+{
+ struct submission_backend_relay *backend = context;
+ struct smtp_reply dummy;
+
+ /* check relay status */
+ if (!backend_relay_handle_relay_reply(backend, NULL, reply, &dummy))
+ return;
+ if (!smtp_reply_is_success(reply)) {
+ i_error("Failed to establish relay connection: %s",
+ smtp_reply_log(reply));
+ submission_backend_fail(
+ &backend->backend, NULL, "4.4.0",
+ "Failed to establish relay connection");
+ return;
+ }
+
+ /* notify the backend API about the fact that we're ready and propagate
+ our capabilities */
+ submission_backend_started(&backend->backend,
+ smtp_client_connection_get_capabilities(backend->conn));
+}
+
+static void backend_relay_start(struct submission_backend *_backend)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ smtp_client_connection_connect(backend->conn,
+ backend_relay_ready_cb, backend);
+}
+
+/* try to proxy pipelined commands in a similarly pipelined fashion */
+static void
+backend_relay_client_input_pre(struct submission_backend *_backend)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ if (backend->conn != NULL)
+ smtp_client_connection_cork(backend->conn);
+}
+static void
+backend_relay_client_input_post(struct submission_backend *_backend)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ if (backend->conn != NULL)
+ smtp_client_connection_uncork(backend->conn);
+}
+
+static uoff_t
+backend_relay_get_max_mail_size(struct submission_backend *_backend)
+{
+ struct submission_backend_relay *backend =
+ (struct submission_backend_relay *)_backend;
+
+ return smtp_client_connection_get_size_capability(backend->conn);
+}
+
+static struct submission_backend_vfuncs backend_relay_vfuncs = {
+ .destroy = backend_relay_destroy,
+
+ .start = backend_relay_start,
+
+ .client_input_pre = backend_relay_client_input_pre,
+ .client_input_post = backend_relay_client_input_post,
+
+ .get_max_mail_size = backend_relay_get_max_mail_size,
+
+ .trans_start = backend_relay_trans_start,
+ .trans_free = backend_relay_trans_free,
+
+ .cmd_helo = backend_relay_cmd_helo,
+
+ .cmd_mail = backend_relay_cmd_mail,
+ .cmd_rcpt = backend_relay_cmd_rcpt,
+ .cmd_rset = backend_relay_cmd_rset,
+ .cmd_data = backend_relay_cmd_data,
+
+ .cmd_vrfy = backend_relay_cmd_vrfy,
+ .cmd_noop = backend_relay_cmd_noop,
+
+ .cmd_quit = backend_relay_cmd_quit,
+};
diff --git a/src/submission/submission-backend-relay.h b/src/submission/submission-backend-relay.h
new file mode 100644
index 0000000..82a600a
--- /dev/null
+++ b/src/submission/submission-backend-relay.h
@@ -0,0 +1,64 @@
+#ifndef SUBMISSION_BACKEND_RELAY_H
+#define SUBMISSION_BACKEND_RELAY_H
+
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+
+#include "submission-backend.h"
+
+struct client;
+struct submission_backend_relay;
+
+struct submision_backend_relay_settings {
+ const char *my_hostname;
+
+ enum smtp_protocol protocol;
+ const char *path, *host;
+ struct ip_addr ip; /* if empty, resolve host */
+ in_port_t port;
+
+ const char *const *extra_capabilities;
+
+ const char *user, *master_user;
+ const char *password;
+ const struct dsasl_client_mech *sasl_mech;
+
+ enum smtp_client_connection_ssl_mode ssl_mode;
+
+ const char *rawlog_dir;
+ unsigned int max_idle_time;
+
+ unsigned int connect_timeout_msecs;
+ unsigned int command_timeout_msecs;
+
+ bool ssl_verify:1;
+ bool trusted:1;
+};
+
+struct submission_backend_relay *
+submission_backend_relay_create(
+ struct client *client,
+ const struct submision_backend_relay_settings *set);
+
+/* Returns the base backend object for this relay backend */
+struct submission_backend *
+submission_backend_relay_get(struct submission_backend_relay *backend)
+ ATTR_PURE;
+
+/* Returns the client connection for this relay backend */
+struct smtp_client_connection *
+submission_backend_relay_get_connection(
+ struct submission_backend_relay *backend) ATTR_PURE;
+/* Returns the current client transaction for this relay backend */
+struct smtp_client_transaction *
+submission_backend_relay_get_transaction(
+ struct submission_backend_relay *backend) ATTR_PURE;
+
+/* Initializes the client transaction manually, which allows providing
+ alternative transaction flags. */
+struct smtp_client_transaction *
+submission_backend_relay_init_transaction(
+ struct submission_backend_relay *backend,
+ enum smtp_client_transaction_flags flags);
+
+#endif
diff --git a/src/submission/submission-backend.c b/src/submission/submission-backend.c
new file mode 100644
index 0000000..889a24b
--- /dev/null
+++ b/src/submission/submission-backend.c
@@ -0,0 +1,448 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+#include "llist.h"
+#include "istream.h"
+#include "istream-sized.h"
+
+#include "submission-recipient.h"
+#include "submission-client.h"
+#include "submission-commands.h"
+#include "submission-backend.h"
+
+struct submission_backend_module_register
+submission_backend_module_register = { 0 };
+
+void submission_backend_init(struct submission_backend *backend,
+ pool_t pool, struct client *client,
+ const struct submission_backend_vfuncs *vfunc)
+{
+ backend->pool = pool;
+ backend->client = client;
+ backend->v = *vfunc;
+
+ p_array_init(&backend->module_contexts, pool, 5);
+
+ client->backends_count++;
+ DLLIST_PREPEND(&client->backends, backend);
+}
+
+static void submission_backend_destroy(struct submission_backend *backend)
+{
+ struct client *client = backend->client;
+
+ i_stream_unref(&backend->data_input);
+
+ i_free(backend->fail_enh_code);
+ i_free(backend->fail_reason);
+
+ DLLIST_REMOVE(&client->backends, backend);
+ backend->v.destroy(backend);
+ pool_unref(&backend->pool);
+}
+
+void submission_backends_destroy_all(struct client *client)
+{
+ while (client->backends != NULL)
+ submission_backend_destroy(client->backends);
+ array_clear(&client->rcpt_backends);
+ client->state.backend = NULL;
+}
+
+void submission_backend_start(struct submission_backend *backend)
+{
+ if (backend->started)
+ return;
+ if (backend->fail_reason != NULL) {
+ /* Don't restart until failure is reset at transaction end */
+ return;
+ }
+ backend->started = TRUE;
+ backend->v.start(backend);
+}
+
+void submission_backend_started(struct submission_backend *backend,
+ enum smtp_capability caps)
+{
+ struct client *client = backend->client;
+
+ if (backend == client->backend_default)
+ client_default_backend_started(client, caps);
+ backend->ready = TRUE;
+ if (backend->v.ready != NULL)
+ backend->v.ready(backend, caps);
+}
+
+static void
+submission_backend_fail_rcpts(struct submission_backend *backend)
+{
+ struct client *client = backend->client;
+ struct submission_recipient *srcpt;
+ const char *enh_code = backend->fail_enh_code;
+ const char *reason = backend->fail_reason;
+
+ i_assert(array_count(&client->rcpt_to) > 0);
+
+ i_assert(reason != NULL);
+ if (enh_code == NULL)
+ enh_code = "4.0.0";
+
+ array_foreach_elem(&client->rcpt_to, srcpt) {
+ struct smtp_server_recipient *rcpt = srcpt->rcpt;
+
+ if (srcpt->backend != backend)
+ continue;
+ if (rcpt->cmd == NULL)
+ continue;
+
+ smtp_server_recipient_reply(rcpt, 451, enh_code, "%s", reason);
+ }
+}
+
+static inline void
+submission_backend_reply_failure(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ const char *enh_code = backend->fail_enh_code;
+ const char *reason = backend->fail_reason;
+
+ if (enh_code == NULL)
+ enh_code = "4.0.0";
+
+ i_assert(smtp_server_command_get_reply_count(cmd->cmd) == 1);
+ smtp_server_reply(cmd, 451, enh_code, "%s", reason);
+}
+
+static inline bool
+submission_backend_handle_failure(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ if (backend->fail_reason == NULL)
+ return TRUE;
+
+ /* already failed */
+ submission_backend_reply_failure(backend, cmd);
+ return TRUE;
+}
+
+void submission_backend_fail(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *enh_code, const char *reason)
+{
+ struct client *client = backend->client;
+ bool failed_before = (backend->fail_reason != NULL);
+
+ /* Can be called several times */
+
+ if (backend == client->backend_default) {
+ /* default backend: fail the whole client */
+ client_destroy(&client, enh_code, reason);
+ return;
+ }
+
+ /* Non-default backend for this transaction (maybe even for only
+ some of the approved recipients): fail only the affected
+ transaction and/or specific recipients. */
+
+ /* Remember the failure only once */
+ if (!failed_before) {
+ backend->fail_enh_code = i_strdup(enh_code);
+ backend->fail_reason = i_strdup(reason);
+ }
+ if (cmd == NULL) {
+ /* Called outside command context: just remember the failure */
+ } else if (smtp_server_command_get_reply_count(cmd->cmd) > 1) {
+ /* Fail DATA/BDAT/BURL command expecting several replies */
+ submission_backend_fail_rcpts(backend);
+ } else {
+ /* Single command */
+ submission_backend_reply_failure(backend, cmd);
+ }
+
+ /* Call the fail vfunc only once */
+ if (!failed_before && backend->v.fail != NULL)
+ backend->v.fail(backend, enh_code, reason);
+ backend->started = FALSE;
+ backend->ready = FALSE;
+}
+
+void submission_backends_client_input_pre(struct client *client)
+{
+ struct submission_backend *backend;
+
+ for (backend = client->backends; backend != NULL;
+ backend = backend->next) {
+ if (!backend->started)
+ continue;
+ if (backend->v.client_input_pre != NULL)
+ backend->v.client_input_pre(backend);
+
+ }
+}
+
+void submission_backends_client_input_post(struct client *client)
+{
+ struct submission_backend *backend;
+
+ for (backend = client->backends; backend != NULL;
+ backend = backend->next) {
+ if (!backend->started)
+ continue;
+ if (backend->v.client_input_post != NULL)
+ backend->v.client_input_post(backend);
+ }
+}
+
+uoff_t submission_backend_get_max_mail_size(struct submission_backend *backend)
+{
+ if (backend->v.get_max_mail_size != NULL)
+ return backend->v.get_max_mail_size(backend);
+ return UOFF_T_MAX;
+}
+
+void submission_backend_trans_start(struct submission_backend *backend,
+ struct smtp_server_transaction *trans)
+{
+ submission_backend_start(backend);
+
+ if (backend->trans_started)
+ return;
+ backend->trans_started = TRUE;
+
+ if (backend->v.trans_start != NULL) {
+ backend->v.trans_start(backend, trans,
+ trans->mail_from, &trans->params);
+ }
+}
+
+static void
+submission_backend_trans_free(struct submission_backend *backend,
+ struct smtp_server_transaction *trans)
+{
+ i_stream_unref(&backend->data_input);
+ if (backend->v.trans_free != NULL)
+ backend->v.trans_free(backend, trans);
+ backend->trans_started = FALSE;
+
+ i_free(backend->fail_enh_code);
+ i_free(backend->fail_reason);
+}
+
+void submission_backends_trans_start(struct client *client,
+ struct smtp_server_transaction *trans)
+{
+ struct submission_backend *backend;
+
+ i_assert(client->state.backend != NULL);
+ submission_backend_trans_start(client->state.backend, trans);
+
+ array_foreach_elem(&client->pending_backends, backend)
+ submission_backend_trans_start(backend, trans);
+ array_clear(&client->pending_backends);
+}
+
+void submission_backends_trans_free(struct client *client,
+ struct smtp_server_transaction *trans)
+{
+ struct submission_backend *backend;
+
+ i_assert(client->state.backend != NULL ||
+ array_count(&client->rcpt_backends) == 0);
+
+ array_foreach_elem(&client->rcpt_backends, backend)
+ submission_backend_trans_free(backend, trans);
+ array_clear(&client->pending_backends);
+ array_clear(&client->rcpt_backends);
+ client->state.backend = NULL;
+}
+
+int submission_backend_cmd_helo(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ /* failure on default backend closes the client connection */
+ i_assert(backend->fail_reason == NULL);
+
+ if (!backend->started || backend->v.cmd_helo == NULL) {
+ /* default backend is not interested, respond right away */
+ submission_helo_reply_submit(cmd, data);
+ return 1;
+ }
+
+ return backend->v.cmd_helo(backend, cmd, data);
+}
+
+void submission_backend_helo_reply_submit(
+ struct submission_backend *backend ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd, struct smtp_server_cmd_helo *data)
+{
+ submission_helo_reply_submit(cmd, data);
+}
+
+int submission_backend_cmd_mail(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ if (!submission_backend_handle_failure(backend, cmd))
+ return -1;
+
+ submission_backend_start(backend);
+
+ if (backend->v.cmd_mail == NULL) {
+ /* mail backend is not interested, respond right away */
+ return 1;
+ }
+
+ return backend->v.cmd_mail(backend, cmd, data);
+}
+
+static void
+submission_backend_add_pending(struct submission_backend *backend)
+{
+ struct client *client = backend->client;
+ struct submission_backend *pending_backend;
+
+ array_foreach_elem(&client->pending_backends, pending_backend) {
+ if (backend == pending_backend)
+ return;
+ }
+
+ array_push_back(&client->pending_backends, &backend);
+}
+
+int submission_backend_cmd_rcpt(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt)
+{
+ struct smtp_server_transaction *trans;
+
+ if (!submission_backend_handle_failure(backend, cmd))
+ return -1;
+
+ i_assert(backend->started);
+
+ if (backend->v.cmd_rcpt == NULL) {
+ /* backend is not interested, respond right away */
+ return 1;
+ }
+
+ trans = smtp_server_connection_get_transaction(cmd->conn);
+ if (trans != NULL)
+ submission_backend_trans_start(srcpt->backend, trans);
+ else
+ submission_backend_add_pending(srcpt->backend);
+
+ return backend->v.cmd_rcpt(backend, cmd, srcpt);
+}
+
+int submission_backend_cmd_rset(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ if (!submission_backend_handle_failure(backend, cmd))
+ return -1;
+
+ submission_backend_start(backend);
+
+ if (backend->v.cmd_rset == NULL) {
+ /* backend is not interested, respond right away */
+ return 1;
+ }
+ return backend->v.cmd_rset(backend, cmd);
+}
+
+static int
+submission_backend_cmd_data(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ if (backend->fail_reason != NULL) {
+ submission_backend_fail_rcpts(backend);
+ return 0;
+ }
+
+ i_assert(backend->started);
+
+ return backend->v.cmd_data(backend, cmd, trans,
+ backend->data_input, backend->data_size);
+}
+
+int submission_backends_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size)
+{
+ struct submission_backend *backend;
+ int ret = 0;
+
+ i_assert(array_count(&client->rcpt_backends) > 0);
+
+ /* Make sure data input stream is at the beginning (plugins may have
+ messed with it. */
+ i_stream_seek(data_input, 0);
+
+ /* create the data_input streams first */
+ array_foreach_elem(&client->rcpt_backends, backend) {
+ backend->data_input =
+ i_stream_create_sized(data_input, data_size);
+ backend->data_size = data_size;
+ }
+
+ /* now that all the streams are created, start reading them
+ (reading them earlier could have caused the data_input parent's
+ offset to change) */
+ array_foreach_elem(&client->rcpt_backends, backend) {
+ ret = submission_backend_cmd_data(backend, cmd, trans);
+ if (ret < 0)
+ break;
+ }
+
+ return ret;
+}
+
+int submission_backend_cmd_vrfy(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *param)
+{
+ /* failure on default backend closes the client connection */
+ i_assert(backend->fail_reason == NULL);
+
+ submission_backend_start(backend);
+
+ if (backend->v.cmd_vrfy == NULL) {
+ /* backend is not interested, respond right away */
+ return 1;
+ }
+ return backend->v.cmd_vrfy(backend, cmd, param);
+}
+
+int submission_backend_cmd_noop(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ /* failure on default backend closes the client connection */
+ i_assert(backend->fail_reason == NULL);
+
+ submission_backend_start(backend);
+
+ if (backend->v.cmd_noop == NULL) {
+ /* backend is not interested, respond right away */
+ return 1;
+ }
+ return backend->v.cmd_noop(backend, cmd);
+}
+
+int submission_backend_cmd_quit(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ /* failure on default backend closes the client connection */
+ i_assert(backend->fail_reason == NULL);
+
+ if (!backend->started) {
+ /* quit before backend even started */
+ return 1;
+ }
+ if (backend->v.cmd_quit == NULL) {
+ /* backend is not interested, respond right away */
+ return 1;
+ }
+ return backend->v.cmd_quit(backend, cmd);
+}
diff --git a/src/submission/submission-backend.h b/src/submission/submission-backend.h
new file mode 100644
index 0000000..25d2389
--- /dev/null
+++ b/src/submission/submission-backend.h
@@ -0,0 +1,170 @@
+#ifndef SUBMISSION_BACKEND_H
+#define SUBMISSION_BACKEND_H
+
+struct submission_recipient;
+struct submission_backend;
+union submission_backend_module_context;
+
+struct submission_backend_vfuncs {
+ void (*destroy)(struct submission_backend *backend);
+
+ void (*start)(struct submission_backend *backend);
+ void (*ready)(struct submission_backend *backend,
+ enum smtp_capability caps);
+
+ void (*fail)(struct submission_backend *backend, const char *enh_code,
+ const char *reason);
+
+ void (*client_input_pre)(struct submission_backend *backend);
+ void (*client_input_post)(struct submission_backend *backend);
+
+ uoff_t (*get_max_mail_size)(struct submission_backend *backend);
+
+ void (*trans_start)(struct submission_backend *backend,
+ struct smtp_server_transaction *trans,
+ const struct smtp_address *path,
+ const struct smtp_params_mail *params);
+ void (*trans_free)(struct submission_backend *backend,
+ struct smtp_server_transaction *trans);
+
+ /* Command handlers:
+
+ These implement the behavior of the various core SMTP commands.
+ SMTP commands are handled asynchronously, which means that the
+ command is not necessarily finished when these handlers end. A
+ command is finished either when 1 is returned or a reply is submitted
+ for it. When a handler 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 submitted
+ implicitly. Not submitting an error reply when -1 is returned causes
+ an assert fail. See src/lib-smtp/smtp-server.h for details.
+
+ When overriding these handler vfuncs, the base implementation should
+ usually be called at some point. When it is called immediately, its
+ result can be returned as normal. When the override returns 0, the
+ base implementation would be called at a later time when some
+ external state is achieved. Note that the overriding function then
+ assumes the responsibility to submit the default reply when none is
+ submitted and the base implementation returns 1.
+
+ Also note that only the default backend actually triggers all of
+ these command callbacks. Secondary backends only get called for
+ transaction commands and only when that backend is tied to the
+ transaction somehow; e.g., as the primary transaction backend or when
+ it is tied to one of the approved recipients.
+ */
+ int (*cmd_helo)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+ int (*cmd_mail)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+ int (*cmd_rcpt)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt);
+ int (*cmd_rset)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+ int (*cmd_data)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+ int (*cmd_vrfy)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *param);
+ int (*cmd_noop)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+
+ int (*cmd_quit)(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+};
+
+struct submission_backend {
+ pool_t pool;
+ struct client *client;
+
+ struct submission_backend *prev, *next;
+
+ struct submission_backend_vfuncs v;
+
+ struct istream *data_input;
+ uoff_t data_size;
+
+ char *fail_enh_code;
+ char *fail_reason;
+
+ /* Module-specific contexts. */
+ ARRAY(union submission_backend_module_context *) module_contexts;
+
+ bool started:1;
+ bool ready:1;
+ bool trans_started:1;
+};
+
+struct submission_backend_module_register {
+ unsigned int id;
+};
+
+union submission_backend_module_context {
+ struct submission_backend_module_register *reg;
+};
+extern struct submission_backend_module_register
+submission_backend_module_register;
+
+void submission_backend_init(struct submission_backend *backend,
+ pool_t pool, struct client *client,
+ const struct submission_backend_vfuncs *vfunc);
+void submission_backends_destroy_all(struct client *client);
+
+void submission_backend_start(struct submission_backend *backend);
+void submission_backend_started(struct submission_backend *backend,
+ enum smtp_capability caps);
+
+void submission_backend_fail(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *enh_code, const char *reason)
+ ATTR_NULL(2);
+
+void submission_backends_client_input_pre(struct client *client);
+void submission_backends_client_input_post(struct client *client);
+
+uoff_t submission_backend_get_max_mail_size(struct submission_backend *backend);
+
+void submission_backend_trans_start(struct submission_backend *backend,
+ struct smtp_server_transaction *trans);
+void submission_backends_trans_start(struct client *client,
+ struct smtp_server_transaction *trans);
+void submission_backends_trans_free(struct client *client,
+ struct smtp_server_transaction *trans);
+
+void submission_backend_helo_reply_submit(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+int submission_backend_cmd_helo(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+int submission_backend_cmd_mail(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+int submission_backend_cmd_rcpt(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt);
+int submission_backend_cmd_rset(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+int submission_backends_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+int submission_backend_cmd_vrfy(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *param);
+int submission_backend_cmd_noop(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+
+int submission_backend_cmd_quit(struct submission_backend *backend,
+ struct smtp_server_cmd_ctx *cmd);
+
+#endif
diff --git a/src/submission/submission-client.c b/src/submission/submission-client.c
new file mode 100644
index 0000000..2ced163
--- /dev/null
+++ b/src/submission/submission-client.c
@@ -0,0 +1,555 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+#include "array.h"
+#include "ioloop.h"
+#include "base64.h"
+#include "str.h"
+#include "llist.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "raw-storage.h"
+#include "imap-urlauth.h"
+#include "smtp-syntax.h"
+#include "smtp-client-connection.h"
+
+#include "submission-backend-relay.h"
+#include "submission-recipient.h"
+#include "submission-commands.h"
+#include "submission-settings.h"
+
+#include <unistd.h>
+
+/* max. length of input command line */
+#define MAX_INBUF_SIZE 4096
+
+/* Stop reading input when output buffer has this many bytes. Once the buffer
+ size has dropped to half of it, start reading input again. */
+#define OUTBUF_THROTTLE_SIZE 4096
+
+/* Disconnect client when it sends too many bad commands in a row */
+#define CLIENT_MAX_BAD_COMMANDS 20
+
+/* Disconnect client after idling this many milliseconds */
+#define CLIENT_IDLE_TIMEOUT_MSECS (10*60*1000)
+
+static const struct smtp_server_callbacks smtp_callbacks;
+static const struct submission_client_vfuncs submission_client_vfuncs;
+
+struct submission_module_register submission_module_register = { 0 };
+
+struct client *submission_clients;
+unsigned int submission_client_count;
+
+static void client_input_pre(void *context)
+{
+ struct client *client = context;
+
+ submission_backends_client_input_pre(client);
+}
+static void client_input_post(void *context)
+{
+ struct client *client = context;
+
+ submission_backends_client_input_post(client);
+}
+
+static void client_parse_backend_capabilities(struct client *client)
+{
+ const struct submission_settings *set = client->set;
+ const char *const *str;
+
+ client->backend_capabilities = SMTP_CAPABILITY_NONE;
+ if (set->submission_backend_capabilities == NULL)
+ return;
+
+ str = t_strsplit_spaces(set->submission_backend_capabilities, " ,");
+ for (; *str != NULL; str++) {
+ enum smtp_capability cap = smtp_capability_find_by_name(*str);
+
+ if (cap == SMTP_CAPABILITY_NONE) {
+ i_warning("Unknown SMTP capability in submission_backend_capabilities: "
+ "%s", *str);
+ continue;
+ }
+
+ client->backend_capabilities |= cap;
+ }
+
+ /* Make sure CHUNKING support is always enabled when BINARYMIME is
+ enabled by explicit configuration. */
+ if (HAS_ALL_BITS(client->backend_capabilities,
+ SMTP_CAPABILITY_BINARYMIME)) {
+ client->backend_capabilities |= SMTP_CAPABILITY_CHUNKING;
+ }
+
+ client->backend_capabilities_configured = TRUE;
+}
+
+void client_apply_backend_capabilities(struct client *client)
+{
+ enum smtp_capability caps = client->backend_capabilities;
+
+ /* propagate capabilities */
+ caps |= SMTP_CAPABILITY_AUTH | SMTP_CAPABILITY_PIPELINING |
+ SMTP_CAPABILITY_SIZE | SMTP_CAPABILITY_ENHANCEDSTATUSCODES |
+ SMTP_CAPABILITY_CHUNKING | SMTP_CAPABILITY_BURL;
+ caps &= SUBMISSION_SUPPORTED_SMTP_CAPABILITIES;
+ smtp_server_connection_set_capabilities(client->conn, caps);
+}
+
+void client_default_backend_started(struct client *client,
+ enum smtp_capability caps)
+{
+ /* propagate capabilities from backend to frontend */
+ if (!client->backend_capabilities_configured) {
+ client->backend_capabilities = caps;
+ client_apply_backend_capabilities(client);
+
+ /* resume the server now that we have the backend
+ capabilities */
+ smtp_server_connection_resume(client->conn);
+ }
+}
+
+static void
+client_create_backend_default(struct client *client,
+ const struct submission_settings *set)
+{
+ struct submision_backend_relay_settings relay_set;
+
+ i_zero(&relay_set);
+ relay_set.my_hostname = set->hostname;
+ relay_set.protocol = SMTP_PROTOCOL_SMTP;
+ relay_set.host = set->submission_relay_host;
+ relay_set.port = set->submission_relay_port;
+ relay_set.user = set->submission_relay_user;
+ relay_set.master_user = set->submission_relay_master_user;
+ relay_set.password = set->submission_relay_password;
+ relay_set.rawlog_dir = set->submission_relay_rawlog_dir;
+ relay_set.max_idle_time = set->submission_relay_max_idle_time;
+ relay_set.connect_timeout_msecs = set->submission_relay_connect_timeout;
+ relay_set.command_timeout_msecs = set->submission_relay_command_timeout;
+ relay_set.trusted = set->submission_relay_trusted;
+
+ if (strcmp(set->submission_relay_ssl, "smtps") == 0)
+ relay_set.ssl_mode = SMTP_CLIENT_SSL_MODE_IMMEDIATE;
+ else if (strcmp(set->submission_relay_ssl, "starttls") == 0)
+ relay_set.ssl_mode = SMTP_CLIENT_SSL_MODE_STARTTLS;
+ else
+ relay_set.ssl_mode = SMTP_CLIENT_SSL_MODE_NONE;
+ relay_set.ssl_verify = set->submission_relay_ssl_verify;
+
+ client->backend_default_relay =
+ submission_backend_relay_create(client, &relay_set);
+ client->backend_default =
+ submission_backend_relay_get(client->backend_default_relay);
+}
+
+static void client_init_urlauth(struct client *client)
+{
+ static const char *access_apps[] = { "submit+", NULL };
+ struct imap_urlauth_config config;
+
+ i_zero(&config);
+ config.url_host = client->set->imap_urlauth_host;
+ config.url_port = client->set->imap_urlauth_port;
+ config.socket_path = t_strconcat(client->user->set->base_dir,
+ "/"IMAP_URLAUTH_SOCKET_NAME, NULL);
+ config.session_id = client->user->session_id;
+ config.access_anonymous = client->user->anonymous;
+ config.access_user = client->user->username;
+ config.access_service = "submission";
+ config.access_applications = access_apps;
+
+ client->urlauth_ctx = imap_urlauth_init(client->user, &config);
+}
+
+struct client *
+client_create(int fd_in, int fd_out, struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct submission_settings *set, const char *helo,
+ const struct smtp_proxy_data *proxy_data,
+ const unsigned char *pdata, unsigned int pdata_len,
+ bool no_greeting)
+{
+ enum submission_client_workarounds workarounds =
+ set->parsed_workarounds;
+ const struct mail_storage_settings *mail_set;
+ struct smtp_server_settings smtp_set;
+ const char *ident;
+ struct client *client;
+ pool_t pool;
+
+ /* always use nonblocking I/O */
+ net_set_nonblock(fd_in, TRUE);
+ net_set_nonblock(fd_out, TRUE);
+
+ pool = pool_alloconly_create("submission client", 2048);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+ client->v = submission_client_vfuncs;
+ client->user = user;
+ client->service_user = service_user;
+ client->set = set;
+
+ i_array_init(&client->pending_backends, 4);
+ i_array_init(&client->rcpt_to, 8);
+ i_array_init(&client->rcpt_backends, 8);
+
+ i_zero(&smtp_set);
+ smtp_set.hostname = set->hostname;
+ smtp_set.login_greeting = set->login_greeting;
+ smtp_set.max_recipients = set->submission_max_recipients;
+ smtp_set.max_client_idle_time_msecs = CLIENT_IDLE_TIMEOUT_MSECS;
+ smtp_set.max_message_size = set->submission_max_mail_size;
+ smtp_set.rawlog_dir = set->rawlog_dir;
+ smtp_set.no_greeting = no_greeting;
+ smtp_set.debug = user->mail_debug;
+
+ if ((workarounds & SUBMISSION_WORKAROUND_WHITESPACE_BEFORE_PATH) != 0) {
+ smtp_set.workarounds |=
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH;
+ }
+ if ((workarounds & SUBMISSION_WORKAROUND_MAILBOX_FOR_PATH) != 0) {
+ smtp_set.workarounds |=
+ SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH;
+ }
+
+ client_parse_backend_capabilities(client);
+
+ p_array_init(&client->module_contexts, client->pool, 5);
+
+ client->conn = smtp_server_connection_create(smtp_server,
+ fd_in, fd_out, user->conn.remote_ip, user->conn.remote_port,
+ FALSE, &smtp_set, &smtp_callbacks, client);
+ smtp_server_connection_set_proxy_data(client->conn, proxy_data);
+ smtp_server_connection_login(client->conn, client->user->username, helo,
+ pdata, pdata_len, user->conn.ssl_secured);
+
+ client_create_backend_default(client, set);
+
+ mail_set = mail_user_set_get_storage_set(user);
+ if (*set->imap_urlauth_host != '\0' &&
+ *mail_set->mail_attribute_dict != '\0') {
+ /* Enable BURL capability only when urlauth dict is
+ configured correctly */
+ client_init_urlauth(client);
+ }
+
+ submission_client_count++;
+ DLLIST_PREPEND(&submission_clients, client);
+
+ ident = mail_user_get_anvil_userip_ident(client->user);
+ if (ident != NULL) {
+ master_service_anvil_send(
+ master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\tsubmission/", ident,
+ "\n", NULL));
+ client->anvil_sent = TRUE;
+ }
+
+ if (hook_client_created != NULL)
+ hook_client_created(&client);
+
+ if (user->anonymous && !client->anonymous_allowed) {
+ smtp_server_connection_abort(
+ &client->conn, 534, "5.7.9",
+ "Anonymous login is not allowed for submission");
+ } else if (client->backend_capabilities_configured) {
+ client_apply_backend_capabilities(client);
+ smtp_server_connection_start(client->conn);
+ } else {
+ submission_backend_start(client->backend_default);
+ smtp_server_connection_start_pending(client->conn);
+ }
+
+ submission_refresh_proctitle();
+ return client;
+}
+
+static void client_state_reset(struct client *client)
+{
+ i_free(client->state.args);
+ i_stream_unref(&client->state.data_input);
+ pool_unref(&client->state.pool);
+
+ i_zero(&client->state);
+}
+
+void client_destroy(struct client **_client, const char *prefix,
+ const char *reason)
+{
+ struct client *client = *_client;
+ struct smtp_server_connection *conn = client->conn;
+
+ *_client = NULL;
+
+ smtp_server_connection_terminate(
+ &conn, (prefix == NULL ? "4.0.0" : prefix), reason);
+}
+
+static void
+client_default_destroy(struct client *client)
+{
+ i_assert(client->disconnected);
+
+ if (client->destroyed)
+ return;
+ client->destroyed = TRUE;
+
+ submission_backends_destroy_all(client);
+ array_free(&client->pending_backends);
+ array_free(&client->rcpt_to);
+ array_free(&client->rcpt_backends);
+
+ submission_client_count--;
+ DLLIST_REMOVE(&submission_clients, client);
+
+ if (client->anvil_sent) {
+ master_service_anvil_send(
+ master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\tsubmission/",
+ mail_user_get_anvil_userip_ident(client->user),
+ "\n", NULL));
+ }
+
+ if (client->urlauth_ctx != NULL)
+ imap_urlauth_deinit(&client->urlauth_ctx);
+
+ mail_user_deinit(&client->user);
+ mail_storage_service_user_unref(&client->service_user);
+
+ client_state_reset(client);
+
+ pool_unref(&client->pool);
+
+ master_service_client_connection_destroyed(master_service);
+ submission_refresh_proctitle();
+}
+
+static void
+client_connection_trans_start(void *context,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = context;
+
+ client->state.pool =
+ pool_alloconly_create("submission client state", 1024);
+
+ client->v.trans_start(client, trans);
+}
+
+static void
+client_default_trans_start(struct client *client,
+ struct smtp_server_transaction *trans)
+{
+ submission_backends_trans_start(client, trans);
+}
+
+static void
+client_connection_trans_free(void *context,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = context;
+
+ client->v.trans_free(client, trans);
+}
+
+static void
+client_default_trans_free(struct client *client,
+ struct smtp_server_transaction *trans)
+{
+ array_clear(&client->rcpt_to);
+
+ submission_backends_trans_free(client, trans);
+ client_state_reset(client);
+}
+
+static void
+client_connection_state_changed(void *context ATTR_UNUSED,
+ enum smtp_server_state new_state,
+ const char *new_args)
+{
+ struct client *client = context;
+
+ i_free(client->state.args);
+
+ client->state.state = new_state;
+ client->state.args = i_strdup(new_args);
+
+ if (submission_client_count == 1)
+ submission_refresh_proctitle();
+}
+
+static const char *client_stats(struct client *client)
+{
+ const struct smtp_server_stats *stats =
+ smtp_server_connection_get_stats(client->conn);
+ const char *trans_id =
+ smtp_server_connection_get_transaction_id(client->conn);
+ const struct var_expand_table logout_tab[] = {
+ { 'i', dec2str(stats->input), "input" },
+ { 'o', dec2str(stats->output), "output" },
+ { '\0', dec2str(stats->command_count), "command_count" },
+ { '\0', dec2str(stats->reply_count), "reply_count" },
+ { '\0', trans_id, "transaction_id" },
+ { '\0', NULL, NULL }
+ };
+ const struct var_expand_table *user_tab =
+ mail_user_var_expand_table(client->user);
+ const struct var_expand_table *tab =
+ t_var_expand_merge_tables(logout_tab, user_tab);
+ string_t *str;
+ const char *error;
+
+ str = t_str_new(128);
+ if (var_expand_with_funcs(str, client->set->submission_logout_format,
+ tab, mail_user_var_expand_func_table,
+ client->user, &error) < 0) {
+ i_error("Failed to expand submission_logout_format=%s: %s",
+ client->set->submission_logout_format, error);
+ }
+ return str_c(str);
+}
+
+static void client_connection_disconnect(void *context, const char *reason)
+{
+ struct client *client = context;
+ const char *log_reason;
+
+ if (client->disconnected)
+ return;
+ client->disconnected = TRUE;
+
+ timeout_remove(&client->to_quit);
+ submission_backends_destroy_all(client);
+
+ if (array_is_created(&client->rcpt_to))
+ array_clear(&client->rcpt_to);
+
+ if (reason == NULL)
+ log_reason = "Connection closed";
+ else
+ log_reason = t_str_oneline(reason);
+ i_info("Disconnected: %s %s", log_reason, client_stats(client));
+}
+
+static void client_connection_free(void *context)
+{
+ struct client *client = context;
+
+ client->v.destroy(client);
+}
+
+uoff_t client_get_max_mail_size(struct client *client)
+{
+ struct submission_backend *backend;
+ uoff_t max_size, limit;
+
+ /* Account for backend SIZE limits and calculate our own relative to
+ those. */
+ max_size = client->set->submission_max_mail_size;
+ if (max_size == 0)
+ max_size = UOFF_T_MAX;
+ for (backend = client->backends; backend != NULL;
+ backend = backend->next) {
+ limit = submission_backend_get_max_mail_size(backend);
+
+ if (limit <= SUBMISSION_MAX_ADDITIONAL_MAIL_SIZE)
+ continue;
+ limit -= SUBMISSION_MAX_ADDITIONAL_MAIL_SIZE;
+ if (limit < max_size)
+ max_size = limit;
+ }
+
+ return max_size;
+}
+
+void client_add_extra_capability(struct client *client, const char *capability,
+ const char *params)
+{
+ struct client_extra_capability cap;
+
+ /* Don't add capabilties handled by lib-smtp here */
+ i_assert(smtp_capability_find_by_name(capability)
+ == SMTP_CAPABILITY_NONE);
+
+ /* Avoid committing protocol errors */
+ i_assert(smtp_ehlo_keyword_is_valid(capability));
+ i_assert(params == NULL || smtp_ehlo_params_str_is_valid(params));
+
+ i_zero(&cap);
+ cap.capability = p_strdup(client->pool, capability);
+ cap.params = p_strdup(client->pool, params);
+
+ if (!array_is_created(&client->extra_capabilities))
+ p_array_init(&client->extra_capabilities, client->pool, 5);
+
+ array_push_back(&client->extra_capabilities, &cap);
+}
+
+void clients_destroy_all(void)
+{
+ while (submission_clients != NULL) {
+ struct client *client = submission_clients;
+
+ mail_storage_service_io_activate_user(client->service_user);
+ client_destroy(&client, "4.3.2", "Shutting down");
+ }
+}
+
+static const struct smtp_server_callbacks smtp_callbacks = {
+ .conn_cmd_helo = cmd_helo,
+
+ .conn_cmd_mail = cmd_mail,
+ .conn_cmd_rcpt = cmd_rcpt,
+ .conn_cmd_rset = cmd_rset,
+
+ .conn_cmd_data_begin = cmd_data_begin,
+ .conn_cmd_data_continue = cmd_data_continue,
+
+ .conn_cmd_vrfy = cmd_vrfy,
+
+ .conn_cmd_noop = cmd_noop,
+ .conn_cmd_quit = cmd_quit,
+
+ .conn_cmd_input_pre = client_input_pre,
+ .conn_cmd_input_post = client_input_post,
+
+ .conn_trans_start = client_connection_trans_start,
+ .conn_trans_free = client_connection_trans_free,
+
+ .conn_state_changed = client_connection_state_changed,
+
+ .conn_disconnect = client_connection_disconnect,
+ .conn_free = client_connection_free,
+};
+
+static const struct submission_client_vfuncs submission_client_vfuncs = {
+ client_default_destroy,
+
+ .trans_start = client_default_trans_start,
+ .trans_free = client_default_trans_free,
+
+ .cmd_helo = client_default_cmd_helo,
+
+ .cmd_mail = client_default_cmd_mail,
+ .cmd_rcpt = client_default_cmd_rcpt,
+ .cmd_rset = client_default_cmd_rset,
+ .cmd_data = client_default_cmd_data,
+
+ .cmd_vrfy = client_default_cmd_vrfy,
+
+ .cmd_noop = client_default_cmd_noop,
+ .cmd_quit = client_default_cmd_quit,
+};
diff --git a/src/submission/submission-client.h b/src/submission/submission-client.h
new file mode 100644
index 0000000..4bafd03
--- /dev/null
+++ b/src/submission/submission-client.h
@@ -0,0 +1,161 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "net.h"
+
+struct smtp_reply;
+
+struct submission_recipient;
+struct submission_backend;
+struct submission_backend_relay;
+struct client;
+
+struct client_state {
+ pool_t pool;
+ enum smtp_server_state state;
+ char *args;
+
+ struct submission_backend *backend;
+ struct istream *data_input;
+ uoff_t data_size;
+
+ bool anonymous_allowed:1;
+};
+
+struct client_extra_capability {
+ const char *capability;
+ const char *params;
+};
+
+struct submission_client_vfuncs {
+ void (*destroy)(struct client *client);
+
+ void (*trans_start)(struct client *client,
+ struct smtp_server_transaction *trans);
+ void (*trans_free)(struct client *client,
+ struct smtp_server_transaction *trans);
+
+ /* Command handlers:
+
+ These implement the behavior of the various core SMTP commands.
+ SMTP commands are handled asynchronously, which means that the
+ command is not necessarily finished when these handlers end. A
+ command is finished either when 1 is returned or a reply is submitted
+ for it. When a handler 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 submitted
+ implicitly. Not submitting an error reply when -1 is returned causes
+ an assert fail. See src/lib-smtp/smtp-server.h for details.
+
+ When overriding these handler vfuncs, the base implementation should
+ usually be called at some point. When it is called immediately, its
+ result can be returned as normal. When the override returns 0, the
+ base implementation would be called at a later time when some
+ external state is achieved. Note that the overriding function then
+ assumes the responsibility to submit the default reply when none is
+ submitted and the base implementation returns 1.
+ */
+ int (*cmd_helo)(struct client *client, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+ int (*cmd_mail)(struct client *client, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+ int (*cmd_rcpt)(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt);
+ int (*cmd_rset)(struct client *client, struct smtp_server_cmd_ctx *cmd);
+ int (*cmd_data)(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+ int (*cmd_vrfy)(struct client *client, struct smtp_server_cmd_ctx *cmd,
+ const char *param);
+
+ int (*cmd_noop)(struct client *client, struct smtp_server_cmd_ctx *cmd);
+ int (*cmd_quit)(struct client *client, struct smtp_server_cmd_ctx *cmd);
+};
+
+struct client {
+ struct client *prev, *next;
+ pool_t pool;
+
+ struct submission_client_vfuncs v;
+
+ const struct setting_parser_info *user_set_info;
+ const struct submission_settings *set;
+
+ struct smtp_server_connection *conn;
+ struct client_state state;
+ ARRAY(struct submission_backend *) pending_backends;
+ ARRAY(struct submission_recipient *) rcpt_to;
+ ARRAY(struct submission_backend *) rcpt_backends;
+
+ struct mail_storage_service_user *service_user;
+ struct mail_user *user;
+
+ /* IMAP URLAUTH context (RFC4467) for BURL (RFC4468) */
+ struct imap_urlauth_context *urlauth_ctx;
+
+ struct timeout *to_quit;
+
+ enum smtp_capability backend_capabilities;
+ struct submission_backend *backend_default;
+ struct submission_backend_relay *backend_default_relay;
+ struct submission_backend *backends;
+ unsigned int backends_count;
+
+ /* Extra (non-standard) capabilities */
+ ARRAY(struct client_extra_capability) extra_capabilities;
+
+ /* Module-specific contexts. */
+ ARRAY(union submission_module_context *) module_contexts;
+
+ bool standalone:1;
+ bool disconnected:1;
+ bool destroyed:1;
+ bool anvil_sent:1;
+ bool backend_capabilities_configured:1;
+ bool anonymous_allowed:1;
+};
+
+struct submission_module_register {
+ unsigned int id;
+};
+
+union submission_module_context {
+ struct submission_client_vfuncs super;
+ struct submission_module_register *reg;
+};
+extern struct submission_module_register submission_module_register;
+
+extern struct client *submission_clients;
+extern unsigned int submission_client_count;
+
+struct client *
+client_create(int fd_in, int fd_out, struct mail_user *user,
+ struct mail_storage_service_user *service_user,
+ const struct submission_settings *set, const char *helo,
+ const struct smtp_proxy_data *proxy_data,
+ const unsigned char *pdata, unsigned int pdata_len,
+ bool no_greeting);
+void client_destroy(struct client **client, const char *prefix,
+ const char *reason) ATTR_NULL(2, 3);
+
+typedef void (*client_input_callback_t)(struct client *context);
+
+void client_apply_backend_capabilities(struct client *client);
+void client_default_backend_started(struct client *client,
+ enum smtp_capability caps);
+
+uoff_t client_get_max_mail_size(struct client *client);
+
+void client_add_extra_capability(struct client *client, const char *capability,
+ const char *params) ATTR_NULL(2);
+
+int client_input_read(struct client *client);
+int client_handle_input(struct client *client);
+
+void clients_destroy_all(void);
+
+#endif
diff --git a/src/submission/submission-commands.c b/src/submission/submission-commands.c
new file mode 100644
index 0000000..8f6fbfb
--- /dev/null
+++ b/src/submission/submission-commands.c
@@ -0,0 +1,615 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "istream-seekable.h"
+#include "mail-storage.h"
+#include "imap-url.h"
+#include "imap-msgpart.h"
+#include "imap-msgpart-url.h"
+#include "imap-urlauth.h"
+#include "imap-urlauth-fetch.h"
+
+#include "submission-recipient.h"
+#include "submission-commands.h"
+#include "submission-backend-relay.h"
+
+/*
+ * EHLO, HELO commands
+ */
+
+static void
+submission_helo_reply_add_extra(struct client *client,
+ struct smtp_server_reply *reply)
+{
+ const struct client_extra_capability *cap;
+
+ if (!array_is_created(&client->extra_capabilities))
+ return;
+
+ array_foreach(&client->extra_capabilities, cap) {
+ if (cap->params == NULL) {
+ smtp_server_reply_ehlo_add(reply, cap->capability);
+ } else {
+ smtp_server_reply_ehlo_add_param(reply, cap->capability,
+ "%s", cap->params);
+ }
+ }
+}
+
+void submission_helo_reply_submit(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct client *client = smtp_server_connection_get_context(cmd->conn);
+ enum smtp_capability backend_caps = client->backend_capabilities;
+ struct smtp_server_reply *reply;
+ uoff_t cap_size;
+
+ reply = smtp_server_reply_create_ehlo(cmd->cmd);
+ if (!data->helo.old_smtp) {
+ string_t *burl_params = t_str_new(256);
+
+ str_append(burl_params, "imap");
+ if (*client->set->imap_urlauth_host == '\0' ||
+ strcmp(client->set->imap_urlauth_host,
+ URL_HOST_ALLOW_ANY) == 0) {
+ str_printfa(burl_params, " imap://%s",
+ client->set->hostname);
+ } else {
+ str_printfa(burl_params, " imap://%s",
+ client->set->imap_urlauth_host);
+ }
+ if (client->set->imap_urlauth_port != 143) {
+ str_printfa(burl_params, ":%u",
+ client->set->imap_urlauth_port);
+ }
+
+ if ((backend_caps & SMTP_CAPABILITY_8BITMIME) != 0)
+ smtp_server_reply_ehlo_add(reply, "8BITMIME");
+ smtp_server_reply_ehlo_add(reply, "AUTH");
+ if ((backend_caps & SMTP_CAPABILITY_BINARYMIME) != 0 &&
+ (backend_caps & SMTP_CAPABILITY_CHUNKING) != 0)
+ smtp_server_reply_ehlo_add(reply, "BINARYMIME");
+ smtp_server_reply_ehlo_add_param(reply,
+ "BURL", "%s", str_c(burl_params));
+ smtp_server_reply_ehlo_add(reply, "CHUNKING");
+ if ((backend_caps & SMTP_CAPABILITY_DSN) != 0)
+ smtp_server_reply_ehlo_add(reply, "DSN");
+ smtp_server_reply_ehlo_add(reply,
+ "ENHANCEDSTATUSCODES");
+ smtp_server_reply_ehlo_add(reply,
+ "PIPELINING");
+
+ cap_size = client_get_max_mail_size(client);
+ if (cap_size > 0) {
+ smtp_server_reply_ehlo_add_param(reply,
+ "SIZE", "%"PRIuUOFF_T, cap_size);
+ } else {
+ smtp_server_reply_ehlo_add(reply, "SIZE");
+ }
+ if ((backend_caps & SMTP_CAPABILITY_VRFY) != 0)
+ smtp_server_reply_ehlo_add(reply, "VRFY");
+
+ submission_helo_reply_add_extra(client, reply);
+ }
+ smtp_server_reply_submit(reply);
+}
+
+int cmd_helo(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct client *client = conn_ctx;
+
+ if (!data->first ||
+ smtp_server_connection_get_state(client->conn, NULL)
+ >= SMTP_SERVER_STATE_READY)
+ return client->v.cmd_helo(client, cmd, data);
+
+ /* respond right away */
+ submission_helo_reply_submit(cmd, data);
+ return 1;
+}
+
+int client_default_cmd_helo(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ return submission_backend_cmd_helo(client->backend_default, cmd, data);
+}
+
+
+/*
+ * MAIL command
+ */
+
+int cmd_mail(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ struct client *client = conn_ctx;
+
+ client->state.backend = client->backend_default;
+
+ return client->v.cmd_mail(client, cmd, data);
+}
+
+int client_default_cmd_mail(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ if (client->user->anonymous && !client->state.anonymous_allowed) {
+ /* NOTE: may need to allow anonymous BURL access in the future,
+ but while that is not supported, deny all anonymous access
+ explicitly. */
+ smtp_server_reply(cmd, 554, "5.7.1",
+ "Access denied (anonymous user)");
+ return -1;
+ }
+
+ return submission_backend_cmd_mail(client->state.backend, cmd, data);
+}
+
+/*
+ * RCPT command
+ */
+
+int cmd_rcpt(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_recipient *rcpt)
+{
+ struct client *client = conn_ctx;
+ struct submission_recipient *srcpt;
+
+ srcpt = submission_recipient_create(client, rcpt);
+
+ return client->v.cmd_rcpt(client, cmd, srcpt);
+}
+
+int client_default_cmd_rcpt(struct client *client ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt)
+{
+ if (client->user->anonymous && !srcpt->anonymous_allowed) {
+ /* NOTE: may need to allow anonymous BURL access in the future,
+ but while that is not supported, deny all anonymous access
+ explicitly. */
+ smtp_server_recipient_reply(
+ srcpt->rcpt, 554, "5.7.1",
+ "Access denied (anonymous user)");
+ return -1;
+ }
+
+ return submission_backend_cmd_rcpt(srcpt->backend, cmd, srcpt);
+}
+
+/*
+ * RSET command
+ */
+
+int cmd_rset(void *conn_ctx, struct smtp_server_cmd_ctx *cmd)
+{
+ struct client *client = conn_ctx;
+
+ return client->v.cmd_rset(client, cmd);
+}
+
+int client_default_cmd_rset(struct client *client,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct submission_backend *backend = client->state.backend;
+
+ if (backend == NULL)
+ backend = client->backend_default;
+
+ /* all backends will also be notified through trans_free(), but that
+ doesn't allow changing the RSET command response. */
+ return submission_backend_cmd_rset(backend, cmd);
+}
+
+/*
+ * DATA/BDAT commands
+ */
+
+int cmd_data_continue(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct client *client = conn_ctx;
+ struct istream *data_input = client->state.data_input;
+ uoff_t data_size;
+ struct istream *inputs[3];
+ string_t *added_headers;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ while ((ret = i_stream_read_more(data_input, &data, &size)) > 0) {
+ 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)
+ return -1;
+
+ /* Done reading DATA stream; remove it from state and continue with
+ local variable. */
+ client->state.data_input = NULL;
+
+ /* Current data stream position is the data size */
+ client->state.data_size = data_input->v_offset;
+
+ /* prepend our own headers */
+ added_headers = t_str_new(200);
+ smtp_server_transaction_write_trace_record(
+ added_headers, trans, SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL);
+
+ i_stream_seek(data_input, 0);
+ inputs[0] = i_stream_create_copy_from_data(
+ str_data(added_headers), str_len(added_headers));
+ inputs[1] = data_input;
+ inputs[2] = NULL;
+
+ data_input = i_stream_create_concat(inputs);
+ i_stream_set_name(data_input, "<submission DATA>");
+ data_size = client->state.data_size + str_len(added_headers);
+
+ i_stream_unref(&inputs[0]);
+ i_stream_unref(&inputs[1]);
+
+ ret = client->v.cmd_data(client, cmd, trans, data_input, data_size);
+
+ i_stream_unref(&data_input);
+ return ret;
+}
+
+int cmd_data_begin(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input)
+{
+ struct client *client = conn_ctx;
+ struct istream *inputs[2];
+ string_t *path;
+
+ if (client->user->anonymous && !client->state.anonymous_allowed) {
+ smtp_server_reply(cmd, 554, "5.7.1",
+ "Access denied (anonymous user)");
+ return -1;
+ }
+
+ inputs[0] = data_input;
+ inputs[1] = NULL;
+
+ path = t_str_new(256);
+ mail_user_set_get_temp_prefix(path, client->user->set);
+ client->state.data_input = i_stream_create_seekable_path(inputs,
+ SUBMISSION_MAIL_DATA_MAX_INMEMORY_SIZE, str_c(path));
+ return 0;
+}
+
+int client_default_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size)
+{
+ return submission_backends_cmd_data(client, cmd, trans,
+ data_input, data_size);
+}
+
+/*
+ * BURL command
+ */
+
+/* FIXME: RFC 4468
+ If the URL argument to BURL refers to binary data, then the submit server
+ MAY refuse the command or down convert as described in Binary SMTP.
+ */
+
+struct cmd_burl_context {
+ struct client *client;
+ struct smtp_server_cmd_ctx *cmd;
+
+ struct imap_urlauth_fetch *urlauth_fetch;
+ struct imap_msgpart_url *url_fetch;
+
+ bool chunk_last:1;
+};
+
+static void
+cmd_burl_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct cmd_burl_context *burl_cmd)
+{
+ if (burl_cmd->urlauth_fetch != NULL)
+ imap_urlauth_fetch_deinit(&burl_cmd->urlauth_fetch);
+ if (burl_cmd->url_fetch != NULL)
+ imap_msgpart_url_free(&burl_cmd->url_fetch);
+}
+
+static int
+cmd_burl_fetch_cb(struct imap_urlauth_fetch_reply *reply,
+ bool last, void *context)
+{
+ struct cmd_burl_context *burl_cmd = context;
+ struct smtp_server_cmd_ctx *cmd = burl_cmd->cmd;
+ int ret;
+
+ i_assert(last);
+
+ if (reply == NULL) {
+ /* fatal failure */
+ // FIXME: make this an internal error
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URLAUTH resolution failed");
+ return -1;
+ }
+ if (!reply->succeeded) {
+ /* URL fetch failed */
+ if (reply->error != NULL) {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URLAUTH resolution failed: %s",
+ reply->error);
+ } else {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URLAUTH resolution failed");
+ }
+ return 1;
+ }
+
+ /* URL fetch succeeded */
+ ret = smtp_server_connection_data_chunk_add(cmd,
+ reply->input, reply->size, burl_cmd->chunk_last, FALSE);
+ if (ret < 0)
+ return -1;
+
+ /* Command is likely not yet complete at this point, so return 0 */
+ return 0;
+}
+
+static int
+cmd_burl_fetch_trusted(struct cmd_burl_context *burl_cmd,
+ struct imap_url *imap_url)
+{
+ struct smtp_server_cmd_ctx *cmd = burl_cmd->cmd;
+ struct client *client = burl_cmd->client;
+ const char *host_name = client->set->imap_urlauth_host;
+ in_port_t host_port = client->set->imap_urlauth_port;
+ struct imap_msgpart_open_result result;
+ const char *error;
+
+ /* validate host */
+ if (imap_url->host.name == NULL ||
+ (strcmp(host_name, URL_HOST_ALLOW_ANY) != 0 &&
+ strcmp(imap_url->host.name, host_name) != 0)) {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URL resolution failed: "
+ "Inappropriate or missing host name");
+ return -1;
+ }
+
+ /* validate port */
+ if ((imap_url->port == 0 && host_port != 143) ||
+ (imap_url->port != 0 && host_port != imap_url->port)) {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URL resolution failed: "
+ "Inappropriate server port");
+ return -1;
+ }
+
+ /* retrieve URL */
+ if (imap_msgpart_url_create
+ (client->user, imap_url, &burl_cmd->url_fetch, &error) < 0) {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URL resolution failed: %s", error);
+ return -1;
+ }
+ if (imap_msgpart_url_read_part(burl_cmd->url_fetch,
+ &result, &error) <= 0) {
+ smtp_server_reply(cmd, 554, "5.6.6",
+ "IMAP URL resolution failed: %s", error);
+ return -1;
+ }
+
+ return smtp_server_connection_data_chunk_add(cmd,
+ result.input, result.size, burl_cmd->chunk_last, FALSE);
+}
+
+static int
+cmd_burl_fetch(struct cmd_burl_context *burl_cmd, const char *url,
+ struct imap_url *imap_url)
+{
+ struct smtp_server_cmd_ctx *cmd = burl_cmd->cmd;
+ struct client *client = burl_cmd->client;
+
+ if (client->urlauth_ctx == NULL) {
+ /* RFC5248, Section 2.4:
+
+ 554 5.7.14 Trust relationship required
+
+ The submission server requires a configured trust
+ relationship with a third-party server in order to access
+ the message content. This value replaces the prior use of
+ X.7.8 for this error condition, thereby updating [RFC4468].
+ */
+ smtp_server_reply(cmd, 554, "5.7.14",
+ "No IMAP URLAUTH access available");
+ return -1;
+ }
+
+ /* urlauth */
+ burl_cmd->urlauth_fetch =
+ imap_urlauth_fetch_init(client->urlauth_ctx,
+ cmd_burl_fetch_cb, burl_cmd);
+ if (imap_urlauth_fetch_url_parsed(burl_cmd->urlauth_fetch,
+ url, imap_url, IMAP_URLAUTH_FETCH_FLAG_BODY) == 0) {
+ /* wait for URL fetch */
+ return 0;
+ }
+ return 1;
+}
+
+void cmd_burl(struct smtp_server_cmd_ctx *cmd, const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct client *client = smtp_server_connection_get_context(conn);
+ struct cmd_burl_context *burl_cmd;
+ const char *const *argv;
+ enum imap_url_parse_flags url_parse_flags =
+ IMAP_URL_PARSE_ALLOW_URLAUTH;
+ struct imap_url *imap_url;
+ const char *url, *error;
+ bool chunk_last = FALSE;
+ int ret = 1;
+
+ smtp_server_connection_data_chunk_init(cmd);
+
+ /* burl-cmd = "BURL" SP absolute-URI [SP end-marker] CRLF
+ end-marker = "LAST"
+ */
+ argv = t_strsplit(params, " ");
+ url = argv[0];
+ if (url == NULL) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Missing chunk URL parameter");
+ ret = -1;
+ } else if (imap_url_parse(url, NULL, url_parse_flags,
+ &imap_url, &error) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid chunk URL: %s", error);
+ ret = -1;
+ } else if (argv[1] != NULL) {
+ if (strcasecmp(argv[1], "LAST") != 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid end marker parameter");
+ ret = -1;
+ } else if (argv[2] != NULL) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid parameters");
+ ret = -1;
+ } else {
+ chunk_last = TRUE;
+ }
+ }
+
+ if (ret < 0 || !smtp_server_connection_data_check_state(cmd))
+ return;
+
+ if (client->user->anonymous) {
+ smtp_server_reply(cmd, 554, "5.7.1",
+ "Access denied (anonymous user)");
+ return;
+ }
+
+ burl_cmd = p_new(cmd->pool, struct cmd_burl_context, 1);
+ burl_cmd->client = client;
+ burl_cmd->cmd = cmd;
+ burl_cmd->chunk_last = chunk_last;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ cmd_burl_destroy, burl_cmd);
+
+ if (imap_url->uauth_rumpurl == NULL) {
+ /* direct local url */
+ ret = cmd_burl_fetch_trusted(burl_cmd, imap_url);
+ } else {
+ ret = cmd_burl_fetch(burl_cmd, url, imap_url);
+ }
+
+ if (ret == 0 && chunk_last)
+ smtp_server_command_input_lock(cmd);
+}
+
+/*
+ * VRFY command
+ */
+
+int cmd_vrfy(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ const char *param)
+{
+ struct client *client = conn_ctx;
+
+ if (client->user->anonymous) {
+ smtp_server_reply(cmd, 550, "5.7.1",
+ "Access denied (anonymous user)");
+ return -1;
+ }
+
+ return client->v.cmd_vrfy(client, cmd, param);
+}
+
+int client_default_cmd_vrfy(struct client *client,
+ struct smtp_server_cmd_ctx *cmd, const char *param)
+{
+ return submission_backend_cmd_vrfy(client->backend_default, cmd, param);
+}
+
+/*
+ * NOOP command
+ */
+
+int cmd_noop(void *conn_ctx, struct smtp_server_cmd_ctx *cmd)
+{
+ struct client *client = conn_ctx;
+
+ return client->v.cmd_noop(client, cmd);
+}
+
+int client_default_cmd_noop(struct client *client,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ return submission_backend_cmd_noop(client->backend_default, cmd);
+}
+
+/*
+ * QUIT command
+ */
+
+struct cmd_quit_context {
+ struct client *client;
+
+ struct smtp_server_cmd_ctx *cmd;
+};
+
+static void cmd_quit_finish(struct cmd_quit_context *quit_cmd)
+{
+ struct client *client = quit_cmd->client;
+ struct smtp_server_cmd_ctx *cmd = quit_cmd->cmd;
+
+ timeout_remove(&client->to_quit);
+ smtp_server_reply_quit(cmd);
+}
+
+static void
+cmd_quit_next(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct cmd_quit_context *quit_cmd)
+{
+ struct client *client = quit_cmd->client;
+
+ /* give backend a brief interval to generate a quit reply */
+ client->to_quit = timeout_add(SUBMISSION_MAX_WAIT_QUIT_REPLY_MSECS,
+ cmd_quit_finish, quit_cmd);
+}
+
+int cmd_quit(void *conn_ctx, struct smtp_server_cmd_ctx *cmd)
+{
+ struct client *client = conn_ctx;
+ struct cmd_quit_context *quit_cmd;
+
+ quit_cmd = p_new(cmd->pool, struct cmd_quit_context, 1);
+ quit_cmd->client = client;
+ quit_cmd->cmd = cmd;
+
+ smtp_server_command_add_hook(cmd->cmd, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_quit_next, quit_cmd);
+
+ return client->v.cmd_quit(client, cmd);
+}
+
+int client_default_cmd_quit(struct client *client,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ return submission_backend_cmd_quit(client->backend_default, cmd);
+}
+
+
diff --git a/src/submission/submission-commands.h b/src/submission/submission-commands.h
new file mode 100644
index 0000000..3fa25cc
--- /dev/null
+++ b/src/submission/submission-commands.h
@@ -0,0 +1,98 @@
+#ifndef SUBMISSION_COMMANDS_H
+#define SUBMISSION_COMMANDS_H
+
+/*
+ * HELO command
+ */
+
+void submission_helo_reply_submit(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+int cmd_helo(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+int client_default_cmd_helo(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+
+/*
+ * MAIL command
+ */
+
+int cmd_mail(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+
+int client_default_cmd_mail(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+
+/*
+ * RCPT command
+ */
+
+int cmd_rcpt(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_recipient *rcpt);
+
+int client_default_cmd_rcpt(struct client *client ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct submission_recipient *srcpt);
+
+/*
+ * RSET command
+ */
+
+int cmd_rset(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+
+int client_default_cmd_rset(struct client *client,
+ struct smtp_server_cmd_ctx *cmd);
+
+/*
+ * DATA/BDAT commands
+ */
+
+int cmd_data_begin(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input);
+int cmd_data_continue(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans);
+
+int client_default_cmd_data(struct client *client,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input, uoff_t data_size);
+
+/*
+ * BURL command
+ */
+
+void cmd_burl(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+/*
+ * VRFY command
+ */
+
+int cmd_vrfy(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ const char *param);
+
+int client_default_cmd_vrfy(struct client *client,
+ struct smtp_server_cmd_ctx *cmd, const char *param);
+
+/*
+ * NOOP command
+ */
+
+int cmd_noop(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+
+int client_default_cmd_noop(struct client *client,
+ struct smtp_server_cmd_ctx *cmd);
+
+/*
+ * QUIT command
+ */
+
+int cmd_quit(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+
+int client_default_cmd_quit(struct client *client,
+ struct smtp_server_cmd_ctx *cmd);
+
+#endif
diff --git a/src/submission/submission-common.h b/src/submission/submission-common.h
new file mode 100644
index 0000000..093d503
--- /dev/null
+++ b/src/submission/submission-common.h
@@ -0,0 +1,46 @@
+#ifndef SUBMISSION_COMMON_H
+#define SUBMISSION_COMMON_H
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "smtp-reply.h"
+#include "smtp-server.h"
+#include "submission-client.h"
+#include "submission-settings.h"
+
+#define URL_HOST_ALLOW_ANY "*"
+
+/* Maximum number of bytes added to a relayed message. This is used to
+ calculate the SIZE capability based on what the backend server states. */
+#define SUBMISSION_MAX_ADDITIONAL_MAIL_SIZE 1024
+#define SUBMISSION_MAIL_DATA_MAX_INMEMORY_SIZE (1024*128)
+
+/* Maximum time to wait for QUIT reply from relay server */
+#define SUBMISSION_MAX_WAIT_QUIT_REPLY_MSECS 2000
+
+#define SUBMISSION_SUPPORTED_SMTP_CAPABILITIES \
+ (SMTP_CAPABILITY_AUTH | SMTP_CAPABILITY_PIPELINING | \
+ SMTP_CAPABILITY_SIZE | SMTP_CAPABILITY_ENHANCEDSTATUSCODES | \
+ SMTP_CAPABILITY_8BITMIME | SMTP_CAPABILITY_CHUNKING | \
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_BURL | \
+ SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_VRFY)
+
+typedef void submission_client_created_func_t(struct client **client);
+
+extern submission_client_created_func_t *hook_client_created;
+extern bool submission_debug;
+
+extern struct smtp_server *smtp_server;
+extern struct smtp_client *smtp_client;
+
+/* Sets the hook_client_created and returns the previous hook,
+ which the new_hook should call if it's non-NULL. */
+submission_client_created_func_t *
+submission_client_created_hook_set(submission_client_created_func_t *new_hook);
+
+void submission_refresh_proctitle(void);
+
+void client_handshake(struct client *client);
+
+#endif
diff --git a/src/submission/submission-recipient.c b/src/submission/submission-recipient.c
new file mode 100644
index 0000000..a49b038
--- /dev/null
+++ b/src/submission/submission-recipient.c
@@ -0,0 +1,55 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "submission-common.h"
+
+#include "submission-backend.h"
+#include "submission-recipient.h"
+
+struct submission_recipient_module_register
+submission_recipient_module_register = { 0 };
+
+static void
+submission_recipient_approved(struct smtp_server_recipient *rcpt ATTR_UNUSED,
+ struct submission_recipient *srcpt);
+
+struct submission_recipient *
+submission_recipient_create(struct client *client,
+ struct smtp_server_recipient *rcpt)
+{
+ struct submission_recipient *srcpt;
+
+ srcpt = p_new(rcpt->pool, struct submission_recipient, 1);
+ srcpt->rcpt = rcpt;
+ srcpt->backend = client->state.backend;
+
+ rcpt->context = srcpt;
+
+ p_array_init(&srcpt->module_contexts, rcpt->pool, 5);
+
+ smtp_server_recipient_add_hook(
+ rcpt, SMTP_SERVER_RECIPIENT_HOOK_APPROVED,
+ submission_recipient_approved, srcpt);
+
+ return srcpt;
+}
+
+static void
+submission_recipient_approved(struct smtp_server_recipient *rcpt ATTR_UNUSED,
+ struct submission_recipient *srcpt)
+{
+ struct submission_backend *backend = srcpt->backend;
+ struct client *client = backend->client;
+ struct submission_backend *rcpt_backend;
+ bool backend_found = FALSE;
+
+ array_push_back(&client->rcpt_to, &srcpt);
+
+ array_foreach_elem(&client->rcpt_backends, rcpt_backend) {
+ if (rcpt_backend == backend) {
+ backend_found = TRUE;
+ break;
+ }
+ }
+ if (!backend_found)
+ array_push_back(&client->rcpt_backends, &backend);
+}
diff --git a/src/submission/submission-recipient.h b/src/submission/submission-recipient.h
new file mode 100644
index 0000000..d80046f
--- /dev/null
+++ b/src/submission/submission-recipient.h
@@ -0,0 +1,33 @@
+#ifndef SUBMISSION_RECIPIENT_H
+#define SUBMISSION_RECIPIENT_H
+
+struct submission_backend;
+struct client;
+
+struct submission_recipient {
+ struct smtp_server_recipient *rcpt;
+
+ struct submission_backend *backend;
+ void *backend_context;
+
+ /* Module-specific contexts. */
+ ARRAY(union submission_recipient_module_context *) module_contexts;
+
+ bool anonymous_allowed:1;
+};
+
+struct submission_recipient_module_register {
+ unsigned int id;
+};
+
+union submission_recipient_module_context {
+ struct submission_recipient_module_register *reg;
+};
+extern struct submission_recipient_module_register
+submission_recipient_module_register;
+
+struct submission_recipient *
+submission_recipient_create(struct client *client,
+ struct smtp_server_recipient *rcpt);
+
+#endif
diff --git a/src/submission/submission-settings.c b/src/submission/submission-settings.c
new file mode 100644
index 0000000..6764b3d
--- /dev/null
+++ b/src/submission/submission-settings.c
@@ -0,0 +1,225 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "mail-storage-settings.h"
+#include "submission-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+static bool submission_settings_verify(void *_set, pool_t pool,
+ const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings submission_unix_listeners_array[] = {
+ { "login/submission", 0666, "", "" }
+};
+static struct file_listener_settings *submission_unix_listeners[] = {
+ &submission_unix_listeners_array[0]
+};
+static buffer_t submission_unix_listeners_buf = {
+ { { submission_unix_listeners, sizeof(submission_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings submission_service_settings = {
+ .name = "submission",
+ .protocol = "submission",
+ .type = "",
+ .executable = "submission",
+ .user = "",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "$default_internal_group",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1024,
+ .client_limit = 1,
+ .service_count = 1,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &submission_unix_listeners_buf,
+ sizeof(submission_unix_listeners[0]) } },
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct submission_settings)
+
+static const struct setting_define submission_setting_defines[] = {
+ DEF(BOOL, verbose_proctitle),
+ DEF(STR_VARS, rawlog_dir),
+
+ DEF(STR, hostname),
+
+ DEF(STR, login_greeting),
+ DEF(STR, login_trusted_networks),
+
+ DEF(STR, recipient_delimiter),
+
+ DEF(SIZE, submission_max_mail_size),
+ DEF(UINT, submission_max_recipients),
+ DEF(STR, submission_client_workarounds),
+ DEF(STR, submission_logout_format),
+
+ DEF(STR, submission_backend_capabilities),
+
+ DEF(STR, submission_relay_host),
+ DEF(IN_PORT, submission_relay_port),
+ DEF(BOOL, submission_relay_trusted),
+
+ DEF(STR, submission_relay_user),
+ DEF(STR, submission_relay_master_user),
+ DEF(STR, submission_relay_password),
+
+ DEF(ENUM, submission_relay_ssl),
+ DEF(BOOL, submission_relay_ssl_verify),
+
+ DEF(STR_VARS, submission_relay_rawlog_dir),
+ DEF(TIME, submission_relay_max_idle_time),
+
+ DEF(TIME_MSECS, submission_relay_connect_timeout),
+ DEF(TIME_MSECS, submission_relay_command_timeout),
+
+ DEF(STR, imap_urlauth_host),
+ DEF(IN_PORT, imap_urlauth_port),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct submission_settings submission_default_settings = {
+ .verbose_proctitle = FALSE,
+ .rawlog_dir = "",
+
+ .hostname = "",
+
+ .login_greeting = PACKAGE_NAME" ready.",
+ .login_trusted_networks = "",
+
+ .recipient_delimiter = "+",
+
+ .submission_max_mail_size = 40*1024*1024,
+ .submission_max_recipients = 0,
+ .submission_client_workarounds = "",
+ .submission_logout_format = "in=%i out=%o",
+
+ .submission_backend_capabilities = NULL,
+
+ .submission_relay_host = "",
+ .submission_relay_port = 25,
+ .submission_relay_trusted = FALSE,
+
+ .submission_relay_user = "",
+ .submission_relay_master_user = "",
+ .submission_relay_password = "",
+
+ .submission_relay_ssl = "no:smtps:starttls",
+ .submission_relay_ssl_verify = TRUE,
+
+ .submission_relay_rawlog_dir = "",
+ .submission_relay_max_idle_time = 60*29,
+
+ .submission_relay_connect_timeout = 30*1000,
+ .submission_relay_command_timeout = 60*5*1000,
+
+ .imap_urlauth_host = "",
+ .imap_urlauth_port = 143,
+};
+
+static const struct setting_parser_info *submission_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info submission_setting_parser_info = {
+ .module_name = "submission",
+ .defines = submission_setting_defines,
+ .defaults = &submission_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct submission_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = submission_settings_verify,
+ .dependencies = submission_setting_dependencies
+};
+
+/* <settings checks> */
+struct submission_client_workaround_list {
+ const char *name;
+ enum submission_client_workarounds num;
+};
+
+/* These definitions need to be kept in sync with equivalent definitions present
+ in src/submission-login/submission-login-settings.c. Workarounds that are not
+ relevant to the submission service are defined as 0 here to prevent "Unknown
+ workaround" errors below. */
+static const struct submission_client_workaround_list
+submission_client_workaround_list[] = {
+ { "whitespace-before-path",
+ SUBMISSION_WORKAROUND_WHITESPACE_BEFORE_PATH },
+ { "mailbox-for-path",
+ SUBMISSION_WORKAROUND_MAILBOX_FOR_PATH },
+ { "implicit-auth-external", 0 },
+ { "exotic-backend", 0 },
+ { NULL, 0 }
+};
+
+static int
+submission_settings_parse_workarounds(struct submission_settings *set,
+ const char **error_r)
+{
+ enum submission_client_workarounds client_workarounds = 0;
+ const struct submission_client_workaround_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->submission_client_workarounds, " ,");
+ for (; *str != NULL; str++) {
+ list = submission_client_workaround_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ client_workarounds |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf(
+ "submission_client_workarounds: "
+ "Unknown workaround: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_workarounds = client_workarounds;
+ return 0;
+}
+
+static bool
+submission_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct submission_settings *set = _set;
+
+ if (submission_settings_parse_workarounds(set, error_r) < 0)
+ return FALSE;
+
+#ifndef CONFIG_BINARY
+ if (set->submission_relay_max_idle_time == 0) {
+ *error_r = "submission_relay_max_idle_time must not be 0";
+ return FALSE;
+ }
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+#endif
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/submission/submission-settings.h b/src/submission/submission-settings.h
new file mode 100644
index 0000000..04f1beb
--- /dev/null
+++ b/src/submission/submission-settings.h
@@ -0,0 +1,60 @@
+#ifndef SUBMISSION_SETTINGS_H
+#define SUBMISSION_SETTINGS_H
+
+#include "smtp-server.h"
+
+/* <settings checks> */
+enum submission_client_workarounds {
+ SUBMISSION_WORKAROUND_WHITESPACE_BEFORE_PATH = BIT(0),
+ SUBMISSION_WORKAROUND_MAILBOX_FOR_PATH = BIT(1),
+};
+/* </settings checks> */
+
+struct submission_settings {
+ bool verbose_proctitle;
+ const char *rawlog_dir;
+
+ const char *hostname;
+
+ const char *login_greeting;
+ const char *login_trusted_networks;
+
+ const char *recipient_delimiter;
+
+ /* submission: */
+ uoff_t submission_max_mail_size;
+ unsigned int submission_max_recipients;
+ const char *submission_client_workarounds;
+ const char *submission_logout_format;
+
+ /* submission backend: */
+ const char *submission_backend_capabilities;
+
+ /* submission relay: */
+ const char *submission_relay_host;
+ in_port_t submission_relay_port;
+ bool submission_relay_trusted;
+
+ const char *submission_relay_user;
+ const char *submission_relay_master_user;
+ const char *submission_relay_password;
+
+ const char *submission_relay_ssl;
+ bool submission_relay_ssl_verify;
+
+ const char *submission_relay_rawlog_dir;
+ unsigned int submission_relay_max_idle_time;
+
+ unsigned int submission_relay_connect_timeout;
+ unsigned int submission_relay_command_timeout;
+
+ /* imap urlauth: */
+ const char *imap_urlauth_host;
+ in_port_t imap_urlauth_port;
+
+ enum submission_client_workarounds parsed_workarounds;
+};
+
+extern const struct setting_parser_info submission_setting_parser_info;
+
+#endif
diff --git a/src/util/Makefile.am b/src/util/Makefile.am
new file mode 100644
index 0000000..2c7621d
--- /dev/null
+++ b/src/util/Makefile.am
@@ -0,0 +1,90 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = \
+ rawlog \
+ script \
+ script-login \
+ $(TCPWRAP_BIN) \
+ gdbhelper \
+ maildirlock
+
+noinst_PROGRAMS = \
+ test-fs
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -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/auth \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+test_fs_SOURCES = test-fs.c
+test_fs_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+test_fs_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+rawlog_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+rawlog_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+rawlog_SOURCES = \
+ rawlog.c
+
+script_login_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+script_login_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+script_login_SOURCES = \
+ script-login.c
+
+script_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+script_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+script_SOURCES = \
+ script.c \
+ health-check-settings.c
+
+if TCPWRAPPERS
+TCPWRAP_BIN = tcpwrap
+tcpwrap_LDADD = $(LIBDOVECOT) $(LIBWRAP_LIBS) \
+ $(BINARY_LDFLAGS)
+
+tcpwrap_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+tcpwrap_SOURCES = \
+ tcpwrap.c \
+ tcpwrap-settings.c
+endif
+
+gdbhelper_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+gdbhelper_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+gdbhelper_SOURCES = \
+ gdbhelper.c
+
+maildirlock_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+maildirlock_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+maildirlock_SOURCES = \
+ maildirlock.c
+
+pkglibexec_SCRIPTS = health-check.sh
+
+bin_SCRIPTS = dovecot-sysreport
+
+EXTRA_DIST = $(pkglibexec_SCRIPTS) \
+ $(bin_SCRIPTS) \ No newline at end of file
diff --git a/src/util/Makefile.in b/src/util/Makefile.in
new file mode 100644
index 0000000..65b7d45
--- /dev/null
+++ b/src/util/Makefile.in
@@ -0,0 +1,1049 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 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@
+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@
+pkglibexec_PROGRAMS = rawlog$(EXEEXT) script$(EXEEXT) \
+ script-login$(EXEEXT) $(am__EXEEXT_1) gdbhelper$(EXEEXT) \
+ maildirlock$(EXEEXT)
+noinst_PROGRAMS = test-fs$(EXEEXT)
+subdir = src/util
+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 =
+@TCPWRAPPERS_TRUE@am__EXEEXT_1 = tcpwrap$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(bindir)" \
+ "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am_gdbhelper_OBJECTS = gdbhelper.$(OBJEXT)
+gdbhelper_OBJECTS = $(am_gdbhelper_OBJECTS)
+am__DEPENDENCIES_1 =
+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_maildirlock_OBJECTS = maildirlock.$(OBJEXT)
+maildirlock_OBJECTS = $(am_maildirlock_OBJECTS)
+am_rawlog_OBJECTS = rawlog.$(OBJEXT)
+rawlog_OBJECTS = $(am_rawlog_OBJECTS)
+am_script_OBJECTS = script.$(OBJEXT) health-check-settings.$(OBJEXT)
+script_OBJECTS = $(am_script_OBJECTS)
+am_script_login_OBJECTS = script-login.$(OBJEXT)
+script_login_OBJECTS = $(am_script_login_OBJECTS)
+am__tcpwrap_SOURCES_DIST = tcpwrap.c tcpwrap-settings.c
+@TCPWRAPPERS_TRUE@am_tcpwrap_OBJECTS = tcpwrap.$(OBJEXT) \
+@TCPWRAPPERS_TRUE@ tcpwrap-settings.$(OBJEXT)
+tcpwrap_OBJECTS = $(am_tcpwrap_OBJECTS)
+am_test_fs_OBJECTS = test-fs.$(OBJEXT)
+test_fs_OBJECTS = $(am_test_fs_OBJECTS)
+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; }; \
+ }
+SCRIPTS = $(bin_SCRIPTS) $(pkglibexec_SCRIPTS)
+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)/gdbhelper.Po \
+ ./$(DEPDIR)/health-check-settings.Po \
+ ./$(DEPDIR)/maildirlock.Po ./$(DEPDIR)/rawlog.Po \
+ ./$(DEPDIR)/script-login.Po ./$(DEPDIR)/script.Po \
+ ./$(DEPDIR)/tcpwrap-settings.Po ./$(DEPDIR)/tcpwrap.Po \
+ ./$(DEPDIR)/test-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 = $(gdbhelper_SOURCES) $(maildirlock_SOURCES) \
+ $(rawlog_SOURCES) $(script_SOURCES) $(script_login_SOURCES) \
+ $(tcpwrap_SOURCES) $(test_fs_SOURCES)
+DIST_SOURCES = $(gdbhelper_SOURCES) $(maildirlock_SOURCES) \
+ $(rawlog_SOURCES) $(script_SOURCES) $(script_login_SOURCES) \
+ $(am__tcpwrap_SOURCES_DIST) $(test_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)
+pkglibexecdir = $(libexecdir)/dovecot
+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-auth \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -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/auth \
+ -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ $(BINARY_CFLAGS)
+
+test_fs_SOURCES = test-fs.c
+test_fs_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+test_fs_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+rawlog_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+rawlog_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+rawlog_SOURCES = \
+ rawlog.c
+
+script_login_LDADD = \
+ $(LIBDOVECOT_STORAGE) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+script_login_DEPENDENCIES = \
+ $(LIBDOVECOT_STORAGE_DEPS) \
+ $(LIBDOVECOT_DEPS)
+
+script_login_SOURCES = \
+ script-login.c
+
+script_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+script_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+script_SOURCES = \
+ script.c \
+ health-check-settings.c
+
+@TCPWRAPPERS_TRUE@TCPWRAP_BIN = tcpwrap
+@TCPWRAPPERS_TRUE@tcpwrap_LDADD = $(LIBDOVECOT) $(LIBWRAP_LIBS) \
+@TCPWRAPPERS_TRUE@ $(BINARY_LDFLAGS)
+
+@TCPWRAPPERS_TRUE@tcpwrap_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+@TCPWRAPPERS_TRUE@tcpwrap_SOURCES = \
+@TCPWRAPPERS_TRUE@ tcpwrap.c \
+@TCPWRAPPERS_TRUE@ tcpwrap-settings.c
+
+gdbhelper_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+gdbhelper_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+gdbhelper_SOURCES = \
+ gdbhelper.c
+
+maildirlock_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+maildirlock_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+maildirlock_SOURCES = \
+ maildirlock.c
+
+pkglibexec_SCRIPTS = health-check.sh
+bin_SCRIPTS = dovecot-sysreport
+EXTRA_DIST = $(pkglibexec_SCRIPTS) \
+ $(bin_SCRIPTS)
+
+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/util/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/util/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-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_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
+
+gdbhelper$(EXEEXT): $(gdbhelper_OBJECTS) $(gdbhelper_DEPENDENCIES) $(EXTRA_gdbhelper_DEPENDENCIES)
+ @rm -f gdbhelper$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(gdbhelper_OBJECTS) $(gdbhelper_LDADD) $(LIBS)
+
+maildirlock$(EXEEXT): $(maildirlock_OBJECTS) $(maildirlock_DEPENDENCIES) $(EXTRA_maildirlock_DEPENDENCIES)
+ @rm -f maildirlock$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(maildirlock_OBJECTS) $(maildirlock_LDADD) $(LIBS)
+
+rawlog$(EXEEXT): $(rawlog_OBJECTS) $(rawlog_DEPENDENCIES) $(EXTRA_rawlog_DEPENDENCIES)
+ @rm -f rawlog$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(rawlog_OBJECTS) $(rawlog_LDADD) $(LIBS)
+
+script$(EXEEXT): $(script_OBJECTS) $(script_DEPENDENCIES) $(EXTRA_script_DEPENDENCIES)
+ @rm -f script$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(script_OBJECTS) $(script_LDADD) $(LIBS)
+
+script-login$(EXEEXT): $(script_login_OBJECTS) $(script_login_DEPENDENCIES) $(EXTRA_script_login_DEPENDENCIES)
+ @rm -f script-login$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(script_login_OBJECTS) $(script_login_LDADD) $(LIBS)
+
+tcpwrap$(EXEEXT): $(tcpwrap_OBJECTS) $(tcpwrap_DEPENDENCIES) $(EXTRA_tcpwrap_DEPENDENCIES)
+ @rm -f tcpwrap$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(tcpwrap_OBJECTS) $(tcpwrap_LDADD) $(LIBS)
+
+test-fs$(EXEEXT): $(test_fs_OBJECTS) $(test_fs_DEPENDENCIES) $(EXTRA_test_fs_DEPENDENCIES)
+ @rm -f test-fs$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fs_OBJECTS) $(test_fs_LDADD) $(LIBS)
+install-binSCRIPTS: $(bin_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(bin_SCRIPTS)'; test -n "$(bindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-binSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(bin_SCRIPTS)'; test -n "$(bindir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(bindir)'; $(am__uninstall_files_from_dir)
+install-pkglibexecSCRIPTS: $(pkglibexec_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_SCRIPTS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_SCRIPTS)'; test -n "$(pkglibexecdir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(pkglibexecdir)'; $(am__uninstall_files_from_dir)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gdbhelper.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/health-check-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildirlock.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rawlog.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/script-login.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/script.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/tcpwrap-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/tcpwrap.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-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
+check: check-am
+all-am: Makefile $(PROGRAMS) $(SCRIPTS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkglibexecdir)"; 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-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/gdbhelper.Po
+ -rm -f ./$(DEPDIR)/health-check-settings.Po
+ -rm -f ./$(DEPDIR)/maildirlock.Po
+ -rm -f ./$(DEPDIR)/rawlog.Po
+ -rm -f ./$(DEPDIR)/script-login.Po
+ -rm -f ./$(DEPDIR)/script.Po
+ -rm -f ./$(DEPDIR)/tcpwrap-settings.Po
+ -rm -f ./$(DEPDIR)/tcpwrap.Po
+ -rm -f ./$(DEPDIR)/test-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-binSCRIPTS install-pkglibexecPROGRAMS \
+ install-pkglibexecSCRIPTS
+
+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)/gdbhelper.Po
+ -rm -f ./$(DEPDIR)/health-check-settings.Po
+ -rm -f ./$(DEPDIR)/maildirlock.Po
+ -rm -f ./$(DEPDIR)/rawlog.Po
+ -rm -f ./$(DEPDIR)/script-login.Po
+ -rm -f ./$(DEPDIR)/script.Po
+ -rm -f ./$(DEPDIR)/tcpwrap-settings.Po
+ -rm -f ./$(DEPDIR)/tcpwrap.Po
+ -rm -f ./$(DEPDIR)/test-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: uninstall-binSCRIPTS uninstall-pkglibexecPROGRAMS \
+ uninstall-pkglibexecSCRIPTS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS 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-binSCRIPTS \
+ 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-pkglibexecPROGRAMS \
+ install-pkglibexecSCRIPTS 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-binSCRIPTS uninstall-pkglibexecPROGRAMS \
+ uninstall-pkglibexecSCRIPTS
+
+.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/util/dovecot-sysreport b/src/util/dovecot-sysreport
new file mode 100755
index 0000000..59afcb9
--- /dev/null
+++ b/src/util/dovecot-sysreport
@@ -0,0 +1,216 @@
+#!/usr/bin/env bash
+
+set -eu
+
+dest="dovecot-sysreport-$(uname -n)-$(date +'%s').tar.gz"
+conf_flag=""
+binary=""
+core=""
+copy_files=""
+keep_temp=0
+
+PARAMS=""
+SRTEMP="`mktemp -d`"
+
+if test "x$SRTEMP" = x; then
+ echo "Could not create temp directory"
+ exit 1
+fi
+
+while (( "$#" )); do
+ case "$1" in
+ -d|--destination)
+
+ if [ "$#" -lt "2" ] ; then
+ echo "Usage: $0 $1 <destination.tar.gz>"
+ exit 1
+ fi
+ dest=$2
+ shift 2
+ ;;
+
+ -c|--config)
+
+ if [ "$#" -lt "2" ] ; then
+ echo "Usage: $0 $1 <config_file>"
+ exit 1
+ fi
+ conf_flag="-c $2"
+ shift 2
+ ;;
+
+ -o|--core)
+
+ gdb=`which gdb`
+ if [ "$gdb" = "" ]; then
+ echo "gdb not found"
+ exit 1
+ fi
+
+ if [[ "$#" -lt 2 ]] ; then
+ echo "Usage: $0 $1 [<binary>] <core> [...]"
+ exit 1
+ fi
+
+ while [[ "$#" -ge 2 ]]; do
+ # see if binary parameter is specified
+ binary=$2
+ if ! [ -r "$binary" ]; then
+ echo "$binary not readable"
+ exit 1
+ fi
+ binary_info=$(file "$binary")
+ if echo "$binary_info" | grep "core file.*execfn: '" >/dev/null; then
+ # no binary specified - detect it
+ binary=$(echo "$binary_info" | sed "s;^.*execfn: '\([^\']\+\)'.*$;\1;")
+ if ! [ -r "$binary" ]; then
+ echo "Detected binary path '$binary' from core file, but it is not readable"
+ exit 1
+ fi
+ echo "Core file was detected to be generated by $binary"
+ else
+ shift
+ fi
+
+ core=$2
+ shift
+ if ! [ -s "$core" ]; then
+ echo "$core not found or it is empty"
+ exit 1
+ fi
+
+ echo "gathering core file dependencies..."
+ core_files=$((echo "info shared"; sleep 1) | $gdb $binary $core | grep '^0x.*/' | sed 's,^[^/]*,,')
+ copy_files="$copy_files $binary $core_files"
+ cp $core $SRTEMP
+ done
+ shift
+ ;;
+
+ -k|--keeptemp)
+
+ keep_temp=1
+ shift
+ ;;
+
+ -h|--help)
+
+ echo -e "dovecot-sysreport \t[-h|--help] [-o|--core [binary] core [...]] [-d|--destination dest]
+ \t\t\t[-k|--keeptemp] -- utility to gather information from the current
+ \t\t\tsystem to be reported for dovecot bug fixes."
+ echo ""
+ echo -e "where:"
+ echo ""
+ echo -e "\t-h, --help\t\tShow the contents of this help."
+ echo -e "\t-d, --destination\tThe file location which the report archive should be put to.
+ \t\t\t\tThe default value is dovecot-sysreport-<hostname>-<current_timestamp>.tar.gz"
+ echo -e "\t-c, --config\t\tSpecify the root configuration file of dovecot."
+ echo -e "\t-o, --core\t\tInclude an specific core file along with its dependencies."
+ echo -e "\t-k, --keeptemp\t\tDo not remove temp files at the end."
+ exit 0
+ ;;
+
+ --)
+
+ shift
+ break
+ ;;
+
+ -*|--*=)
+
+ echo "Error: Unsupported flag $1" >&2
+ exit 1
+ ;;
+
+ *)
+
+ PARAMS="$PARAMS $1"
+ shift
+ ;;
+
+ esac
+done
+
+eval set -- "$PARAMS"
+
+mkdir $SRTEMP/conf
+
+doveconf $conf_flag -n > $SRTEMP/conf/dovecot.conf
+
+unwrap_and_hide_pass () {
+ files=`grep -zPo 'dict\s*{[^}]*}' $1 | grep -zPo '.*=.*:\K(.*)' | tr '\0' '\n'`
+ files="$files `grep -zPo 'args\s*=\s*\K(.*)' $1 | tr '\0' '\n'`"
+ for cf in $files; do
+ if [ -r "$cf" ]; then
+ if [[ ! -z `grep -vhIE '^([^:]*:){6}[^:]*$' $cf` ]]; then
+ unwrap_and_hide_pass $cf
+ mkdir -p $SRTEMP/conf"$(dirname "$cf")"
+ if [[ -x "$(command -v python)" ]]; then
+ python <<HEREDOC
+import re
+conf = open('$cf', 'r').read()
+hidden = re.sub('(?<!no)((?:password|key|nonce|dnpass)\s*=\s*).*?(?=$|\s)', '\g<1>#hidden', conf)
+f = open('$SRTEMP/conf$cf', "w")
+f.write(hidden)
+f.close()
+HEREDOC
+ elif [[ -x "$(command -v perl)" ]]; then
+ perl -pe 's/(?<!no)((?:password|key|nonce|dnpass)\s*=\s*).*?(?=$|\s)/\1#hidden/g' \
+ $cf > $SRTEMP/conf$cf
+ else
+ echo "perl or python is required to hide your passwords in dovecot's"
+ echo "configuration files. Either install at least one of them or"
+ echo "continue at your own peril. Do you want to continue (N/y)? "
+ read permit
+ if [ "$permit" != "Y" ] && [ "$permit" != "y" ]; then
+ exit 1
+ fi
+ cat $cf > $SRTEMP/conf$cf
+ fi
+ fi
+ fi
+ done
+}
+
+echo "Gathering configurations ..."
+unwrap_and_hide_pass $SRTEMP/conf/dovecot.conf
+
+echo "Gathering system informations ..."
+doveadm $conf_flag log errors > $SRTEMP/log_errors || :
+(printf "# Start: "; date) >$SRTEMP/ps_output
+ps auxwww | grep '[d]ovecot' >> $SRTEMP/ps_output
+(printf "# End: "; date) >>$SRTEMP/ps_output
+doveadm $conf_flag service status > $SRTEMP/service_status || :
+doveadm $conf_flag process status > $SRTEMP/process_status || :
+uptime > $SRTEMP/uptime_output
+doveadm $conf_flag stats dump > $SRTEMP/stats_dump || :
+sleep 3
+echo -e "\n\n###################### AFTER 3 SECONDS ######################\n\n" | \
+ tee -a $SRTEMP/ps_output $SRTEMP/service_status $SRTEMP/process_status \
+ $SRTEMP/uptime_output $SRTEMP/stats_dump > /dev/null
+(printf "# Start: "; date) >>$SRTEMP/ps_output
+ps auxwww | grep '[d]ovecot' >> $SRTEMP/ps_output
+(printf "# End: "; date) >>$SRTEMP/ps_output
+doveadm $conf_flag service status >> $SRTEMP/service_status || :
+doveadm $conf_flag process status >> $SRTEMP/process_status || :
+uptime >> $SRTEMP/uptime_output
+doveadm $conf_flag stats dump >> $SRTEMP/stats_dump || :
+
+cf=`pwd`
+cd $SRTEMP
+echo "Creating archive ..."
+tar -czf `if [[ "$dest" = /* ]]; then echo $dest; else echo $cf/$dest; fi` --dereference \
+ $copy_files *
+
+function cleanup {
+ if [ $keep_temp = 0 ]; then
+ echo "Removing temp files at $SRTEMP ..."
+ rm -rf $SRTEMP
+ else
+ echo "Temp files remains untouched at $SRTEMP ..."
+ fi
+}
+
+trap cleanup EXIT
+
+echo "All done! Please report file $dest"
diff --git a/src/util/gdbhelper.c b/src/util/gdbhelper.c
new file mode 100644
index 0000000..f1ec2af
--- /dev/null
+++ b/src/util/gdbhelper.c
@@ -0,0 +1,71 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <time.h>
+#include <sys/wait.h>
+
+int main(int argc, char *argv[])
+{
+ pid_t pid = fork();
+ const char *path, *cmd;
+ int fd_in[2], fd_out[2], fd_log, status;
+
+ if (argc < 2)
+ i_fatal("Usage: gdbhelper <program> [<args>]");
+
+ switch (pid) {
+ case 1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ /* child */
+ (void)execvp(argv[1], argv+1);
+ i_fatal("execvp(%s) failed: %m", argv[1]);
+ default:
+ if (pipe(fd_in) < 0 || pipe(fd_out) < 0)
+ i_fatal("pipe() failed: %m");
+ cmd = "handle SIGPIPE nostop\n"
+ "handle SIGALRM nostop\n"
+ "handle SIG32 nostop\n"
+ "cont\n"
+ "bt full\n"
+ "quit\n";
+ if (write(fd_in[1], cmd, strlen(cmd)) < 0)
+ i_fatal("write() failed: %m");
+
+ if (dup2(fd_in[0], 0) < 0 ||
+ dup2(fd_out[1], 1) < 0 ||
+ dup2(fd_out[1], 2) < 0)
+ i_fatal("dup2() failed: %m");
+
+ cmd = t_strdup_printf("gdb %s %s", argv[1], dec2str(pid));
+ if (system(cmd) < 0)
+ i_fatal("system() failed: %m");
+
+ if (wait(&status) < 0)
+ i_fatal("wait() failed: %m");
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ char buf[1024];
+ ssize_t ret;
+
+ path = t_strdup_printf("/tmp/gdbhelper.%s.%s",
+ dec2str(time(NULL)),
+ dec2str(pid));
+ fd_log = open(path, O_CREAT | O_WRONLY, 0600);
+ if (fd_log < 0)
+ i_fatal("open(%s) failed: %m", path);
+
+ while ((ret = read(fd_out[0], buf, sizeof(buf))) > 0) {
+ if (write(fd_log, buf, ret) < 0)
+ i_fatal("write(%s) failed: %m", path);
+ }
+ if (ret < 0)
+ i_fatal("read(pipe) failed: %m");
+ i_close_fd(&fd_log);
+ }
+ }
+ return 0;
+}
diff --git a/src/util/health-check-settings.c b/src/util/health-check-settings.c
new file mode 100644
index 0000000..82a70ac
--- /dev/null
+++ b/src/util/health-check-settings.c
@@ -0,0 +1,31 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+struct service_settings health_check_service_settings = {
+ .name = "health-check",
+ .protocol = "",
+ .type = "",
+ .executable = "script -p health-check.sh",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = TRUE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
diff --git a/src/util/health-check.sh b/src/util/health-check.sh
new file mode 100755
index 0000000..4604f4e
--- /dev/null
+++ b/src/util/health-check.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+#
+# This script is intended to be called by the script service and to be
+# connected to a socket using a service configuration like this:
+# executable = script -p /path/to/health-check.sh
+#
+# This simple example merely answers "PONG\n" if the input is "PING\n". It
+# stops waiting for input after $timeout which causes the process to die
+# which causes the script module to close the socket. Inputs and outputs
+# can be written to STDIN and STDOUT, they are duplicated file-descriptors
+# if called from the script service.
+
+timeout=10
+
+# timeout the read via trap for POSIX shell compatibility
+trap "exit 0" QUIT
+{
+ sleep $timeout
+ kill -3 $$ 2>/dev/null
+} &
+read -r input
+
+exit_code=$?
+cleaned_input=$(echo ${input} | sed "s/[^a-zA-Z0-9]//g")
+
+if [ ${exit_code} -eq 0 ] && [ "${cleaned_input}" = "PING" ];then
+ echo "PONG"
+fi
+# always exit successful
+exit 0
diff --git a/src/util/maildirlock.c b/src/util/maildirlock.c
new file mode 100644
index 0000000..89fbacc
--- /dev/null
+++ b/src/util/maildirlock.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "ioloop.h"
+#include "write-full.h"
+#include "file-dotlock.h"
+#include "index/maildir/maildir-uidlist.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+
+static struct dotlock_settings dotlock_settings = {
+ .stale_timeout = MAILDIR_UIDLIST_LOCK_STALE_TIMEOUT,
+ .use_io_notify = TRUE
+};
+
+static struct ioloop *ioloop;
+
+static void sig_die(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static int maildir_lock(const char *path, unsigned int timeout,
+ struct dotlock **dotlock_r)
+{
+ dotlock_settings.timeout = timeout;
+ dotlock_settings.use_excl_lock = getenv("DOTLOCK_USE_EXCL") != NULL;
+ dotlock_settings.nfs_flush = getenv("MAIL_NFS_STORAGE") != NULL;
+
+ path = t_strconcat(path, "/" MAILDIR_UIDLIST_NAME, NULL);
+ return file_dotlock_create(&dotlock_settings, path, 0, dotlock_r);
+}
+
+int main(int argc, const char *argv[])
+{
+ struct dotlock *dotlock;
+ unsigned int timeout;
+ pid_t pid;
+ int fd[2], ret;
+ char c;
+
+ if (argc != 3) {
+ fprintf(stderr, "Usage: maildirlock <path> <timeout>\n"
+ " - SIGTERM will release the lock.\n");
+ return 1;
+ }
+ if (pipe(fd) != 0) {
+ fprintf(stderr, "pipe() failed: %s", strerror(errno));
+ return 1;
+ }
+
+ pid = fork();
+ if (pid == (pid_t)-1) {
+ fprintf(stderr, "fork() failed: %s", strerror(errno));
+ return 1;
+ }
+
+ /* call lib_init() only after fork so that PID gets set correctly */
+ lib_init();
+ lib_signals_init();
+ ioloop = io_loop_create();
+ lib_signals_set_handler(SIGINT, LIBSIG_FLAG_DELAYED, sig_die, NULL);
+ lib_signals_set_handler(SIGTERM, LIBSIG_FLAG_DELAYED, sig_die, NULL);
+
+ if (pid != 0) {
+ i_close_fd(&fd[1]);
+ ret = read(fd[0], &c, 1);
+ if (ret < 0) {
+ i_error("read(pipe) failed: %m");
+ return 1;
+ }
+ if (ret != 1) {
+ /* locking timed out */
+ return 1;
+ }
+
+ printf("%s", dec2str(pid));
+ return 0;
+ }
+
+ /* child process - stdout has to be closed so that caller knows when
+ to stop reading it. */
+ if (dup2(STDERR_FILENO, STDOUT_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+
+ if (str_to_uint(argv[2], &timeout) < 0)
+ i_fatal("Invalid timeout value: %s", argv[2]);
+ if (maildir_lock(argv[1], timeout, &dotlock) <= 0)
+ return 1;
+
+ /* locked - send a byte */
+ if (write_full(fd[1], "", 1) < 0)
+ i_fatal("write(pipe) failed: %m");
+
+ io_loop_run(ioloop);
+
+ file_dotlock_delete(&dotlock);
+ lib_signals_deinit();
+
+ io_loop_destroy(&ioloop);
+ lib_deinit();
+ return 0;
+}
diff --git a/src/util/rawlog.c b/src/util/rawlog.c
new file mode 100644
index 0000000..f4e428b
--- /dev/null
+++ b/src/util/rawlog.c
@@ -0,0 +1,428 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "ioloop.h"
+#include "net.h"
+#include "str.h"
+#include "write-full.h"
+#include "istream.h"
+#include "ostream.h"
+#include "process-title.h"
+#include "restrict-access.h"
+#include "time-util.h"
+#include "master-service.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+
+#define OUTBUF_THRESHOLD IO_BLOCK_SIZE
+#define RAWLOG_TIMEOUT_FLUSH_MSECS 1000
+
+static struct ioloop *ioloop;
+
+enum rawlog_flags {
+ RAWLOG_FLAG_LOG_INPUT = 0x01,
+ RAWLOG_FLAG_LOG_OUTPUT = 0x02,
+ RAWLOG_FLAG_LOG_TIMESTAMPS = 0x04,
+ RAWLOG_FLAG_LOG_BOUNDARIES = 0x10,
+ RAWLOG_FLAG_LOG_IP_IN_FILENAME = 0x20
+};
+
+struct rawlog_proxy {
+ int client_in_fd, client_out_fd, server_fd;
+ struct io *client_io, *server_io;
+ struct ostream *client_output, *server_output;
+ struct timeout *to_flush;
+
+ struct ostream *in_output, *out_output;
+ enum rawlog_flags flags;
+ bool prev_lf_in, prev_lf_out;
+};
+
+static void rawlog_proxy_destroy(struct rawlog_proxy *proxy)
+{
+ if (proxy->in_output != NULL) {
+ o_stream_uncork(proxy->in_output);
+ if (o_stream_finish(proxy->in_output) < 0) {
+ i_error("write(in) failed: %s",
+ o_stream_get_error(proxy->in_output));
+ }
+ o_stream_destroy(&proxy->in_output);
+ }
+ if (proxy->out_output != NULL) {
+ o_stream_uncork(proxy->out_output);
+ if (o_stream_finish(proxy->out_output) < 0) {
+ i_error("write(out) failed: %s",
+ o_stream_get_error(proxy->out_output));
+ }
+ o_stream_destroy(&proxy->out_output);
+ }
+ io_remove(&proxy->client_io);
+ io_remove(&proxy->server_io);
+ timeout_remove(&proxy->to_flush);
+
+ o_stream_destroy(&proxy->client_output);
+ o_stream_destroy(&proxy->server_output);
+
+ if (close(proxy->client_in_fd) < 0)
+ i_error("close(client_in_fd) failed: %m");
+ if (close(proxy->client_out_fd) < 0)
+ i_error("close(client_out_fd) failed: %m");
+ if (close(proxy->server_fd) < 0)
+ i_error("close(server_fd) failed: %m");
+ i_free(proxy);
+
+ io_loop_stop(ioloop);
+}
+
+static void
+write_with_timestamps(struct ostream *output, bool *prev_lf,
+ const unsigned char *data, size_t size)
+{
+ T_BEGIN {
+ const char *timestamp = t_strdup_printf("%ld.%06lu ",
+ (long)ioloop_timeval.tv_sec,
+ (unsigned long)ioloop_timeval.tv_usec);
+ string_t *str = t_str_new(size + 128);
+ size_t i;
+
+ if (*prev_lf)
+ str_append(str, timestamp);
+
+ for (i = 0; i < size; i++) {
+ str_append_c(str, data[i]);
+ if (data[i] == '\n' && i+1 != size)
+ str_append(str, timestamp);
+ }
+ *prev_lf = data[i-1] == '\n';
+ o_stream_nsend(output, str_data(str), str_len(str));
+ } T_END;
+}
+
+static void proxy_flush_timeout(struct rawlog_proxy *proxy)
+{
+ bool flushed = TRUE;
+
+ if (o_stream_flush(proxy->in_output) == 0)
+ flushed = FALSE;
+ if (o_stream_flush(proxy->out_output) == 0)
+ flushed = FALSE;
+ if (flushed)
+ timeout_remove(&proxy->to_flush);
+}
+
+static void proxy_write_data(struct rawlog_proxy *proxy, struct ostream *output,
+ bool *prev_lf, const void *data, size_t size)
+{
+ if (output == NULL || output->closed || size == 0)
+ return;
+
+ if ((proxy->flags & RAWLOG_FLAG_LOG_BOUNDARIES) != 0)
+ o_stream_nsend_str(output, "<<<\n");
+
+ if ((proxy->flags & RAWLOG_FLAG_LOG_TIMESTAMPS) != 0)
+ write_with_timestamps(output, prev_lf, data, size);
+ else
+ o_stream_nsend(output, data, size);
+
+ if ((proxy->flags & RAWLOG_FLAG_LOG_BOUNDARIES) != 0)
+ o_stream_nsend_str(output, ">>>\n");
+
+ if (proxy->to_flush == NULL) {
+ proxy->to_flush = timeout_add(RAWLOG_TIMEOUT_FLUSH_MSECS,
+ proxy_flush_timeout, proxy);
+ }
+}
+
+static void proxy_write_in(struct rawlog_proxy *proxy,
+ const void *data, size_t size)
+{
+ proxy_write_data(proxy, proxy->in_output, &proxy->prev_lf_in,
+ data, size);
+}
+
+static void proxy_write_out(struct rawlog_proxy *proxy,
+ const void *data, size_t size)
+{
+ proxy_write_data(proxy, proxy->out_output, &proxy->prev_lf_out,
+ data, size);
+}
+
+static void server_input(struct rawlog_proxy *proxy)
+{
+ unsigned char buf[OUTBUF_THRESHOLD];
+ ssize_t ret;
+
+ if (o_stream_get_buffer_used_size(proxy->client_output) >
+ OUTBUF_THRESHOLD) {
+ /* client's output buffer is already quite full.
+ don't send more until we're below threshold. */
+ io_remove(&proxy->server_io);
+ return;
+ }
+
+ ret = net_receive(proxy->server_fd, buf, sizeof(buf));
+ if (ret > 0) {
+ o_stream_nsend(proxy->client_output, buf, ret);
+ proxy_write_out(proxy, buf, ret);
+ } else if (ret <= 0)
+ rawlog_proxy_destroy(proxy);
+}
+
+static void client_input(struct rawlog_proxy *proxy)
+{
+ unsigned char buf[OUTBUF_THRESHOLD];
+ ssize_t ret;
+
+ if (o_stream_get_buffer_used_size(proxy->server_output) >
+ OUTBUF_THRESHOLD) {
+ /* proxy's output buffer is already quite full.
+ don't send more until we're below threshold. */
+ io_remove(&proxy->client_io);
+ return;
+ }
+
+ ret = net_receive(proxy->client_in_fd, buf, sizeof(buf));
+ if (ret > 0) {
+ o_stream_nsend(proxy->server_output, buf, ret);
+ proxy_write_in(proxy, buf, ret);
+ } else if (ret < 0)
+ rawlog_proxy_destroy(proxy);
+}
+
+static int server_output(struct rawlog_proxy *proxy)
+{
+ if (o_stream_flush(proxy->server_output) < 0) {
+ rawlog_proxy_destroy(proxy);
+ return 1;
+ }
+
+ if (proxy->client_io == NULL &&
+ o_stream_get_buffer_used_size(proxy->server_output) <
+ OUTBUF_THRESHOLD) {
+ /* there's again space in proxy's output buffer, so we can
+ read more from client. */
+ proxy->client_io = io_add(proxy->client_in_fd, IO_READ,
+ client_input, proxy);
+ }
+ return 1;
+}
+
+static int client_output(struct rawlog_proxy *proxy)
+{
+ if (o_stream_flush(proxy->client_output) < 0) {
+ rawlog_proxy_destroy(proxy);
+ return 1;
+ }
+
+ if (proxy->server_io == NULL &&
+ o_stream_get_buffer_used_size(proxy->client_output) <
+ OUTBUF_THRESHOLD) {
+ /* there's again space in client's output buffer, so we can
+ read more from proxy. */
+ proxy->server_io =
+ io_add(proxy->server_fd, IO_READ, server_input, proxy);
+ }
+ return 1;
+}
+
+static void proxy_open_logs(struct rawlog_proxy *proxy, const char *path,
+ const char *ip_addr)
+{
+ const char *fname, *timestamp;
+ string_t *path_prefix;
+ int fd;
+
+ timestamp = t_strflocaltime("%Y%m%d-%H%M%S", time(NULL));
+ path_prefix = t_str_new(128);
+ str_printfa(path_prefix, "%s/", path);
+ if (ip_addr != NULL &&
+ (proxy->flags & RAWLOG_FLAG_LOG_IP_IN_FILENAME) != 0)
+ str_printfa(path_prefix, "%s-", ip_addr);
+ str_printfa(path_prefix, "%s-%s", timestamp, dec2str(getpid()));
+
+ if ((proxy->flags & RAWLOG_FLAG_LOG_INPUT) != 0) {
+ fname = t_strdup_printf("%s.in", str_c(path_prefix));
+ fd = open(fname, O_CREAT|O_EXCL|O_WRONLY, 0600);
+ if (fd == -1) {
+ i_error("rawlog_open: creat(%s): %m", fname);
+ return;
+ }
+ proxy->in_output = o_stream_create_fd_file_autoclose(&fd, 0);
+ o_stream_cork(proxy->in_output);
+ }
+
+ if ((proxy->flags & RAWLOG_FLAG_LOG_OUTPUT) != 0) {
+ fname = t_strdup_printf("%s.out", str_c(path_prefix));
+ fd = open(fname, O_CREAT|O_EXCL|O_WRONLY, 0600);
+ if (fd == -1) {
+ i_error("rawlog_open: creat(%s): %m", fname);
+ o_stream_destroy(&proxy->in_output);
+ return;
+ }
+ proxy->out_output = o_stream_create_fd_file_autoclose(&fd, 0);
+ o_stream_cork(proxy->out_output);
+ }
+}
+
+static struct rawlog_proxy *
+rawlog_proxy_create(int client_in_fd, int client_out_fd, int server_fd,
+ const char *path, const char *ip_addr,
+ enum rawlog_flags flags)
+{
+ struct rawlog_proxy *proxy;
+
+ proxy = i_new(struct rawlog_proxy, 1);
+ proxy->server_fd = server_fd;
+ proxy->server_output = o_stream_create_fd(server_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(proxy->server_output, TRUE);
+ o_stream_set_flush_callback(proxy->server_output, server_output, proxy);
+ proxy->server_io = io_add(server_fd, IO_READ, server_input, proxy);
+
+ proxy->client_in_fd = client_in_fd;
+ proxy->client_out_fd = client_out_fd;
+ proxy->client_output =
+ o_stream_create_fd(client_out_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(proxy->client_output, TRUE);
+ proxy->client_io = io_add(proxy->client_in_fd, IO_READ,
+ client_input, proxy);
+ o_stream_set_flush_callback(proxy->client_output, client_output, proxy);
+
+ fd_set_nonblock(client_in_fd, TRUE);
+ fd_set_nonblock(client_out_fd, TRUE);
+
+ proxy->flags = flags;
+
+ proxy->prev_lf_in = proxy->prev_lf_out = TRUE;
+ proxy_open_logs(proxy, path, ip_addr);
+ return proxy;
+}
+
+static void rawlog_open(enum rawlog_flags flags)
+{
+ const char *chroot_dir, *home, *path;
+ struct stat st;
+ int sfd[2];
+ pid_t pid;
+
+ chroot_dir = getenv("RESTRICT_CHROOT");
+ home = getenv("HOME");
+ if (chroot_dir != NULL)
+ home = t_strconcat(chroot_dir, home, NULL);
+ else if (home == NULL)
+ home = ".";
+
+ /* see if we want rawlog */
+ path = t_strconcat(home, "/dovecot.rawlog", NULL);
+ if (lstat(path, &st) < 0) {
+ if (errno != ENOENT)
+ i_warning("lstat() failed for %s: %m", path);
+ else if (getenv("DEBUG") != NULL)
+ i_info("rawlog: %s doesn't exist", path);
+ return;
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ if (getenv("DEBUG") != NULL)
+ i_info("rawlog: %s is not a directory", path);
+ return;
+ }
+
+ if (chroot_dir != NULL) {
+ /* we'll chroot soon. skip over the chroot in the path. */
+ path += strlen(chroot_dir);
+ }
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sfd) < 0)
+ i_fatal("socketpair() failed: %m");
+ fd_set_nonblock(sfd[0], TRUE);
+ fd_set_nonblock(sfd[1], TRUE);
+
+ pid = fork();
+ if (pid < 0)
+ i_fatal("fork() failed: %m");
+
+ if (pid > 0) {
+ /* parent */
+ if (dup2(sfd[1], 0) < 0)
+ i_fatal("dup2(sfd, 0)");
+ if (dup2(sfd[1], 1) < 0)
+ i_fatal("dup2(sfd, 1)");
+ i_close_fd(&sfd[0]);
+ i_close_fd(&sfd[1]);
+ return;
+ }
+ i_close_fd(&sfd[1]);
+
+ restrict_access_by_env(0, getenv("HOME"));
+
+ process_title_set(t_strdup_printf("[%s:%s rawlog]", getenv("USER"),
+ dec2str(getppid())));
+
+ ioloop = io_loop_create();
+ (void)rawlog_proxy_create(0, 1, sfd[0], path, getenv("IP"), flags);
+ io_loop_run(ioloop);
+ io_loop_destroy(&ioloop);
+
+ lib_deinit();
+ lib_exit(0);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ char *executable, *p;
+ enum rawlog_flags flags =
+ RAWLOG_FLAG_LOG_INPUT | RAWLOG_FLAG_LOG_OUTPUT;
+ int c;
+
+ master_service = master_service_init("rawlog", service_flags,
+ &argc, &argv, "+f:bIt");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'f':
+ if (strcmp(optarg, "in") == 0)
+ flags &= ENUM_NEGATE(RAWLOG_FLAG_LOG_OUTPUT);
+ else if (strcmp(optarg, "out") == 0)
+ flags &= ENUM_NEGATE(RAWLOG_FLAG_LOG_INPUT);
+ else
+ i_fatal("Invalid filter: %s", optarg);
+ break;
+ case 'b':
+ flags |= RAWLOG_FLAG_LOG_BOUNDARIES;
+ break;
+ case 'I':
+ flags |= RAWLOG_FLAG_LOG_IP_IN_FILENAME;
+ break;
+ case 't':
+ flags |= RAWLOG_FLAG_LOG_TIMESTAMPS;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 1)
+ i_fatal("Usage: rawlog [-f in|out] [-I] [-b] [-t] <binary> <arguments>");
+
+ master_service_init_log(master_service);
+ master_service_init_finish(master_service);
+
+ executable = argv[0];
+ rawlog_open(flags);
+
+ /* hide the executable path, it's ugly */
+ p = strrchr(argv[0], '/');
+ if (p != NULL) argv[0] = p+1;
+ execv(executable, argv);
+
+ i_fatal_status(FATAL_EXEC, "execv(%s) failed: %m", executable);
+
+ /* not reached */
+ return FATAL_EXEC;
+}
diff --git a/src/util/script-login.c b/src/util/script-login.c
new file mode 100644
index 0000000..062a1a6
--- /dev/null
+++ b/src/util/script-login.c
@@ -0,0 +1,249 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "fdpass.h"
+#include "restrict-access.h"
+#include "str.h"
+#include "strescape.h"
+#include "settings-parser.h"
+#include "mail-storage-service.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+
+#include <unistd.h>
+
+#define SCRIPT_LOGIN_PROTOCOL_VERSION_MAJOR 1
+#define SCRIPT_LOGIN_READ_TIMEOUT_SECS 10
+#define ENV_USERDB_KEYS "USERDB_KEYS"
+#define SCRIPT_COMM_FD 3
+
+static const char **exec_args;
+static bool drop_to_userdb_privileges = FALSE;
+
+static void client_connected(struct master_service_connection *conn)
+{
+ enum mail_storage_service_flags flags =
+ MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS;
+ string_t *instr, *keys;
+ const char *const *args, *key, *value, *error, *version_line, *data_line;
+ struct mail_storage_service_ctx *service_ctx;
+ struct mail_storage_service_input input;
+ struct mail_storage_service_user *user;
+ char buf[1024];
+ unsigned int i, socket_count;
+ int fd = -1;
+ ssize_t ret;
+
+ alarm(SCRIPT_LOGIN_READ_TIMEOUT_SECS);
+
+ net_set_nonblock(conn->fd, FALSE);
+ instr = t_str_new(1024);
+ ret = fd_read(conn->fd, buf, sizeof(buf), &fd);
+ while (ret > 0) {
+ str_append_data(instr, buf, ret);
+ if (buf[ret-1] == '\n' &&
+ strchr(str_c(instr), '\n')[1] != '\0') {
+ str_truncate(instr, str_len(instr)-1);
+ break;
+ }
+
+ ret = read(conn->fd, buf, sizeof(buf));
+ }
+
+ version_line = str_c(instr);
+ data_line = strchr(version_line, '\n');
+ if (data_line != NULL)
+ version_line = t_strdup_until(version_line, data_line++);
+ else
+ version_line = NULL;
+
+ if (ret > 0 || version_line != NULL) {
+ if (version_line == NULL ||
+ !version_string_verify(version_line, "script-login",
+ SCRIPT_LOGIN_PROTOCOL_VERSION_MAJOR)) {
+ i_fatal("Client not compatible with this binary "
+ "(connecting to wrong socket?)");
+ }
+ }
+
+ if (ret <= 0) {
+ if (ret < 0)
+ i_fatal("read() failed: %m");
+ else
+ i_fatal("read() failed: disconnected");
+ }
+ if (fd == -1)
+ i_fatal("client fd not received");
+
+ alarm(0);
+
+ /* put everything to environment */
+ env_clean();
+ keys = t_str_new(256);
+ args = t_strsplit_tabescaped(data_line);
+
+ if (str_array_length(args) < 3)
+ i_fatal("Missing input fields");
+
+ i = 0;
+ i_zero(&input);
+ input.module = "mail"; /* need to get mail_uid, mail_gid */
+ input.service = "script-login";
+ (void)net_addr2ip(args[i++], &input.local_ip);
+ (void)net_addr2ip(args[i++], &input.remote_ip);
+ input.username = args[i++];
+ input.userdb_fields = args + i;
+
+ env_put("LOCAL_IP", net_ip2addr(&input.local_ip));
+ env_put("IP", net_ip2addr(&input.remote_ip));
+ env_put("USER", input.username);
+
+ for (; args[i] != NULL; i++) {
+ value = strchr(args[i], '=');
+ if (value != NULL) {
+ key = t_str_ucase(t_strdup_until(args[i], value++));
+ env_put(key, value);
+ str_printfa(keys, "%s ", key);
+ }
+ }
+ env_put(ENV_USERDB_KEYS, str_c(keys));
+
+ master_service_init_log_with_prefix(master_service,
+ t_strdup_printf("script-login(%s): ", input.username));
+
+ if (drop_to_userdb_privileges) {
+ service_ctx = mail_storage_service_init(master_service, NULL, flags);
+ if (mail_storage_service_lookup(service_ctx, &input, &user, &error) <= 0)
+ i_fatal("%s", error);
+ mail_storage_service_restrict_setenv(service_ctx, user);
+ /* we can't exec anything in a chroot */
+ env_remove("RESTRICT_CHROOT");
+ restrict_access_by_env(0, getenv("HOME"));
+ }
+
+ if (dup2(fd, STDIN_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ if (dup2(fd, STDOUT_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ if (close(fd) < 0)
+ i_fatal("close() failed: %m");
+ if (conn->fd != SCRIPT_COMM_FD) {
+ if (dup2(conn->fd, SCRIPT_COMM_FD) < 0)
+ i_fatal("dup2() failed: %m");
+ if (close(conn->fd) < 0)
+ i_fatal("close() failed: %m");
+ }
+
+ /* close all listener sockets */
+ socket_count = master_service_get_socket_count(master_service);
+ for (i = 0; i < socket_count; i++) {
+ if (close(MASTER_LISTEN_FD_FIRST + i) < 0)
+ i_error("close(listener) failed: %m");
+ }
+ if (close(MASTER_STATUS_FD) < 0)
+ i_error("close(status) failed: %m");
+
+ execvp_const(exec_args[0], exec_args);
+}
+
+static void script_execute_finish(void)
+{
+ const char *keys_str, *username, *const *keys, *value;
+ string_t *reply = t_str_new(512);
+ ssize_t ret;
+
+ keys_str = getenv(ENV_USERDB_KEYS);
+ if (keys_str == NULL)
+ i_fatal(ENV_USERDB_KEYS" environment missing");
+
+ username = getenv("USER");
+ if (username == NULL)
+ i_fatal("USER environment missing");
+ str_append(reply, username);
+
+ for (keys = t_strsplit_spaces(keys_str, " "); *keys != NULL; keys++) {
+ value = getenv(t_str_ucase(*keys));
+ if (value != NULL) {
+ str_append_c(reply, '\t');
+ str_append_tabescaped(reply,
+ t_strconcat(t_str_lcase(*keys), "=",
+ value, NULL));
+ }
+ }
+ str_append_c(reply, '\n');
+
+ /* finish by sending the fd to the mail process */
+ ret = fd_send(SCRIPT_COMM_FD, STDOUT_FILENO,
+ str_data(reply), str_len(reply));
+ if (ret == (ssize_t)str_len(reply)) {
+ /* success */
+ } else {
+ if (ret < 0)
+ i_error("fd_send() failed: %m");
+ else
+ i_error("fd_send() sent partial output");
+ /* exit with 0 even though we failed. non-0 exit just makes
+ master log an unnecessary error. */
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ enum master_service_flags flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ int i, c;
+
+ if (getenv(MASTER_IS_PARENT_ENV) == NULL)
+ flags |= MASTER_SERVICE_FLAG_STANDALONE;
+
+ master_service = master_service_init("script-login", flags,
+ &argc, &argv, "+d");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'd':
+ drop_to_userdb_privileges = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ master_service_init_log(master_service);
+
+ if (!drop_to_userdb_privileges &&
+ (flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ /* drop to privileges defined by service settings */
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ }
+
+ master_service_init_finish(master_service);
+ master_service_set_service_count(master_service, 1);
+
+ if ((flags & MASTER_SERVICE_FLAG_STANDALONE) != 0) {
+ /* The last post-login script is calling us to finish login */
+ script_execute_finish();
+ } else {
+ if (argv[0] == NULL)
+ i_fatal("Missing script path");
+ exec_args = i_new(const char *, argc + 2);
+ for (i = 0; i < argc; i++)
+ exec_args[i] = argv[i];
+ exec_args[i] = PKG_LIBEXECDIR"/script-login";
+ exec_args[i+1] = NULL;
+
+ if (exec_args[0][0] != '/') {
+ exec_args[0] = t_strconcat(PKG_LIBEXECDIR"/",
+ exec_args[0], NULL);
+ }
+
+ master_service_run(master_service, client_connected);
+ }
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/util/script.c b/src/util/script.c
new file mode 100644
index 0000000..6ae4621
--- /dev/null
+++ b/src/util/script.c
@@ -0,0 +1,321 @@
+/* Copyright (c) 2010-2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "write-full.h"
+#include "restrict-access.h"
+#include "master-interface.h"
+#include "master-service.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/socket.h>
+
+#define SCRIPT_MAJOR_VERSION 4
+#define SCRIPT_READ_TIMEOUT_SECS 10
+
+static ARRAY_TYPE(const_string) exec_args;
+static const char **accepted_envs;
+static bool passthrough = FALSE;
+
+static void script_verify_version(const char *line)
+{
+ if (line == NULL ||
+ !version_string_verify(line, "script", SCRIPT_MAJOR_VERSION)) {
+ i_fatal("Client not compatible with this binary "
+ "(connecting to wrong socket?)");
+ }
+}
+
+
+static void
+exec_child(struct master_service_connection *conn,
+ const char *const *args, const char *const *envs)
+{
+ unsigned int i, socket_count;
+
+ if (dup2(conn->fd, STDIN_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ if (dup2(conn->fd, STDOUT_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+
+ /* close all fds */
+ socket_count = master_service_get_socket_count(master_service);
+ for (i = 0; i < socket_count; i++) {
+ if (close(MASTER_LISTEN_FD_FIRST + i) < 0)
+ i_error("close(listener) failed: %m");
+ }
+ if (close(MASTER_STATUS_FD) < 0)
+ i_error("close(status) failed: %m");
+ if (close(conn->fd) < 0)
+ i_error("close(conn->fd) failed: %m");
+
+ for (; args != NULL && *args != NULL; args++) {
+ const char *arg = t_str_tabunescape(*args);
+ array_push_back(&exec_args, &arg);
+ }
+ array_append_zero(&exec_args);
+
+ env_clean();
+ if (envs != NULL)
+ env_put_array(envs);
+
+ args = array_front(&exec_args);
+ execvp_const(args[0], args);
+}
+
+static bool
+parse_input(ARRAY_TYPE(const_string)* envs, const char *const **args_r,
+ struct master_service_connection* conn, ssize_t *output_r)
+{
+ string_t *input;
+ void *buf;
+ size_t prev_size, scanpos;
+ bool header_complete = FALSE, noreply = FALSE;
+
+ input = t_buffer_create(IO_BLOCK_SIZE);
+
+ /* Input contains:
+
+ VERSION .. <lf>
+ [alarm=<secs> <lf>]
+ "noreply" | "-" (or anything really) <lf>
+
+ arg 1 <lf>
+ arg 2 <lf>
+ ...
+ <lf>
+ DATA
+
+ This is quite a horrible protocol. If alarm is specified, it MUST be
+ before "noreply". If "noreply" isn't given, something other string
+ (typically "-") must be given which is eaten away.
+ */
+ alarm(SCRIPT_READ_TIMEOUT_SECS);
+ scanpos = 1;
+ while (!header_complete) {
+ const unsigned char *pos, *end;
+
+ prev_size = input->used;
+ buf = buffer_append_space_unsafe(input, IO_BLOCK_SIZE);
+
+ /* peek in socket input buffer */
+ *output_r = recv(conn->fd, buf, IO_BLOCK_SIZE, MSG_PEEK);
+ if (*output_r <= 0) {
+ buffer_set_used_size(input, prev_size);
+ if (strchr(str_c(input), '\n') != NULL)
+ script_verify_version(t_strcut(str_c(input), '\n'));
+
+ if (*output_r < 0)
+ i_fatal("recv(MSG_PEEK) failed: %m");
+
+ i_fatal("recv(MSG_PEEK) failed: disconnected");
+ }
+
+ /* scan for final \n\n */
+ pos = CONST_PTR_OFFSET(input->data, scanpos);
+ end = CONST_PTR_OFFSET(input->data, prev_size + *output_r);
+ for (; pos < end; pos++) {
+ if (pos[-1] == '\n' && pos[0] == '\n') {
+ header_complete = TRUE;
+ pos++;
+ break;
+ }
+ }
+ scanpos = pos - (const unsigned char *)input->data;
+
+ /* read data for real (up to and including \n\n) */
+ *output_r = recv(conn->fd, buf, scanpos-prev_size, 0);
+ if (prev_size+(*output_r) != scanpos) {
+ if (*output_r < 0)
+ i_fatal("recv() failed: %m");
+ if (*output_r == 0)
+ i_fatal("recv() failed: disconnected");
+ i_fatal("recv() failed: size of definitive recv() differs from peek");
+ }
+ buffer_set_used_size(input, scanpos);
+ }
+ alarm(0);
+
+ /* drop the last two LFs */
+ buffer_set_used_size(input, scanpos-2);
+
+ *args_r = t_strsplit(str_c(input), "\n");
+ script_verify_version(**args_r); (*args_r)++;
+ if (**args_r != NULL) {
+ const char *p;
+
+ if (str_begins(**args_r, "alarm=")) {
+ unsigned int seconds;
+ if (str_to_uint((**args_r) + 6, &seconds) < 0)
+ i_fatal("invalid alarm option");
+ alarm(seconds);
+ (*args_r)++;
+ }
+ while (str_begins(**args_r, "env_")) {
+ const char *envname, *env;
+
+ env = t_str_tabunescape((**args_r)+4);
+ p = strchr(env, '=');
+ if (p == NULL)
+ i_fatal("invalid environment variable");
+ envname = t_strdup_until((**args_r)+4, p);
+
+ if (str_array_find(accepted_envs, envname))
+ array_push_back(envs, &env);
+ (*args_r)++;
+ }
+ if (strcmp(**args_r, "noreply") == 0) {
+ noreply = TRUE;
+ }
+ if (***args_r == '\0')
+ i_fatal("empty options");
+ (*args_r)++;
+ }
+ array_append_zero(envs);
+
+ return noreply;
+}
+
+static bool client_exec_script(struct master_service_connection *conn)
+{
+ ARRAY_TYPE(const_string) envs;
+ const char *const *args = NULL;
+ ssize_t ret;
+ int status;
+ pid_t pid;
+
+ t_array_init(&envs, 16);
+
+ net_set_nonblock(conn->fd, FALSE);
+
+ if (!passthrough && parse_input(&envs, &args, conn, &ret)) {
+ /* parse_input returns TRUE if noreply is set in the input.
+ * In that case there is no need to fork and check exit
+ * status. Parsing the input must only happen if passthrough
+ * is not enabled. */
+ exec_child(conn, args, array_front(&envs));
+ i_unreached();
+ }
+
+ if ((pid = fork()) == (pid_t)-1) {
+ i_error("fork() failed: %m");
+ return FALSE;
+ }
+
+ if (pid == 0) {
+ /* child */
+ if (!passthrough)
+ exec_child(conn, args, array_front(&envs));
+ else
+ exec_child(conn, NULL, NULL);
+ i_unreached();
+ }
+
+ /* parent */
+
+ /* check script exit status */
+ if (waitpid(pid, &status, 0) < 0) {
+ i_error("waitpid() failed: %m");
+ return FALSE;
+ } else if (WIFEXITED(status)) {
+ ret = WEXITSTATUS(status);
+ if (ret != 0) {
+ i_error("Script terminated abnormally, exit status %d", (int)ret);
+ return FALSE;
+ }
+ } else if (WIFSIGNALED(status)) {
+ i_error("Script terminated abnormally, signal %d", WTERMSIG(status));
+ return FALSE;
+ } else if (WIFSTOPPED(status)) {
+ i_fatal("Script stopped, signal %d", WSTOPSIG(status));
+ return FALSE;
+ } else {
+ i_fatal("Script terminated abnormally, return status %d", status);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (!passthrough) {
+ char response[2];
+
+ response[0] = client_exec_script(conn) ? '+' : '-';
+ response[1] = '\n';
+ if (write_full(conn->fd, &response, 2) < 0)
+ i_error("write(response) failed: %m");
+ } else {
+ client_exec_script(conn);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ ARRAY_TYPE(const_string) aenvs;
+ const char *binary;
+ const char *const *envs;
+ int c, i;
+
+ master_service = master_service_init("script", service_flags,
+ &argc, &argv, "+e:p");
+
+ t_array_init(&aenvs, 16);
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'e':
+ envs = t_strsplit_spaces(optarg,", \t");
+ while (*envs != NULL) {
+ array_push_back(&aenvs, envs);
+ envs++;
+ }
+ break;
+ case 'p':
+ passthrough = TRUE;
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ array_append_zero(&aenvs);
+ accepted_envs = p_strarray_dup(default_pool, array_front(&aenvs));
+
+ master_service_init_log(master_service);
+ if (argv[0] == NULL)
+ i_fatal("Missing script path");
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ master_service_init_finish(master_service);
+ master_service_set_service_count(master_service, 1);
+
+ if (argv[0][0] == '/')
+ binary = argv[0];
+ else
+ binary = t_strconcat(PKG_LIBEXECDIR"/", argv[0], NULL);
+
+ i_array_init(&exec_args, argc + 16);
+ array_push_back(&exec_args, &binary);
+ for (i = 1; i < argc; i++) {
+ const char *arg = argv[i];
+
+ array_push_back(&exec_args, &arg);
+ }
+
+ master_service_run(master_service, client_connected);
+ array_free(&exec_args);
+ i_free(accepted_envs);
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/util/tcpwrap-settings.c b/src/util/tcpwrap-settings.c
new file mode 100644
index 0000000..ec7ceaf
--- /dev/null
+++ b/src/util/tcpwrap-settings.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+
+#ifdef HAVE_LIBWRAP
+struct service_settings tcpwrap_service_settings = {
+ .name = "tcpwrap",
+ .protocol = "",
+ .type = "",
+ .executable = "tcpwrap",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 0,
+ .client_limit = 1,
+ .service_count = 0,
+ .idle_kill = 0,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = ARRAY_INIT,
+ .fifo_listeners = ARRAY_INIT,
+ .inet_listeners = ARRAY_INIT
+};
+#endif
diff --git a/src/util/tcpwrap.c b/src/util/tcpwrap.c
new file mode 100644
index 0000000..0d492bb
--- /dev/null
+++ b/src/util/tcpwrap.c
@@ -0,0 +1,131 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "fdpass.h"
+#include "write-full.h"
+#include "restrict-access.h"
+#include "master-service.h"
+
+#include <unistd.h>
+#include <syslog.h>
+#include <tcpd.h>
+
+struct tcpwrap_client {
+ int fd;
+ struct io *io;
+ struct timeout *to;
+};
+
+#define INPUT_TIMEOUT_MSECS (1000*10)
+
+/* for tcpwrap library */
+int allow_severity = LOG_INFO;
+int deny_severity = LOG_WARNING;
+
+static struct tcpwrap_client *tcpwrap_client = NULL;
+
+static void tcpwrap_client_destroy(struct tcpwrap_client **client);
+
+static void tcpwrap_client_handle(struct tcpwrap_client *client, int check_fd,
+ const char *daemon_name)
+{
+ struct request_info request;
+
+ request_init(&request, RQ_DAEMON, daemon_name,
+ RQ_FILE, check_fd, 0);
+ fromhost(&request);
+
+ if (!hosts_access(&request))
+ (void)write_full(client->fd, "0\n", 2);
+ else
+ (void)write_full(client->fd, "1\n", 2);
+ lib_exit(0);
+}
+
+static void tcpwrap_client_input(struct tcpwrap_client *client)
+{
+ unsigned char buf[1024];
+ ssize_t ret;
+ int check_fd = -1;
+
+ ret = fd_read(client->fd, buf, sizeof(buf), &check_fd);
+ if (ret <= 0) {
+ i_error("fd_read() failed: %m");
+ } else if (ret > 1 && (size_t)ret < sizeof(buf) && buf[ret-1] == '\n') {
+ tcpwrap_client_handle(client, check_fd, t_strndup(buf, ret-1));
+ } else {
+ i_error("Invalid input from client");
+ }
+
+ i_close_fd(&check_fd);
+ tcpwrap_client_destroy(&client);
+}
+
+static void tcpwrap_client_timeout(struct tcpwrap_client *client)
+{
+ tcpwrap_client_destroy(&client);
+}
+
+static struct tcpwrap_client *tcpwrap_client_create(int fd)
+{
+ struct tcpwrap_client *client;
+
+ client = i_new(struct tcpwrap_client, 1);
+ client->fd = fd;
+ client->io = io_add(fd, IO_READ, tcpwrap_client_input, client);
+ client->to = timeout_add(INPUT_TIMEOUT_MSECS, tcpwrap_client_timeout,
+ client);
+ return client;
+}
+
+static void tcpwrap_client_destroy(struct tcpwrap_client **_client)
+{
+ struct tcpwrap_client *client = *_client;
+
+ *_client = NULL;
+
+ timeout_remove(&client->to);
+ io_remove(&client->io);
+ if (close(client->fd) < 0)
+ i_error("close() failed: %m");
+ i_free(client);
+
+ tcpwrap_client = NULL;
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ if (tcpwrap_client != NULL) {
+ i_error("tcpwrap must be configured with client_limit=1");
+ return;
+ }
+
+ master_service_client_connection_accept(conn);
+ tcpwrap_client = tcpwrap_client_create(conn->fd);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+
+ master_service = master_service_init("tcpwrap", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+
+ master_service_init_log(master_service);
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+ if (tcpwrap_client != NULL)
+ tcpwrap_client_destroy(&tcpwrap_client);
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/util/test-fs.c b/src/util/test-fs.c
new file mode 100644
index 0000000..ecad781
--- /dev/null
+++ b/src/util/test-fs.c
@@ -0,0 +1,449 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "guid.h"
+#include "llist.h"
+#include "master-service.h"
+#include "dict.h"
+#include "fs-api.h"
+
+#define DEFAULT_MAX_PARALLEL_OPS 30
+#define DEFAULT_FILES_COUNT 100
+/* Allow +-10% difference in number of files */
+#define FILES_COUNT_APPROX 0.9
+
+struct test_read {
+ struct test_read *prev, *next;
+
+ struct test_ctx *ctx;
+ struct fs_file *file;
+ struct istream *input;
+ struct io *io;
+};
+
+struct test_write {
+ struct test_write *prev, *next;
+
+ struct test_ctx *ctx;
+ struct fs_file *file;
+};
+
+struct test_delete {
+ struct test_delete *prev, *next;
+
+ struct test_ctx *ctx;
+ struct fs_file *file;
+};
+
+struct test_iter {
+ struct test_iter *prev, *next;
+
+ struct test_ctx *ctx;
+ pool_t pool;
+ ARRAY_TYPE(const_string) files;
+ struct fs_iter *iter;
+ bool object_ids;
+};
+
+struct test_ctx {
+ struct fs *fs;
+ const char *prefix;
+ bool sync_only, async_only;
+ unsigned int files_count;
+ unsigned int max_parallel_ops;
+
+ pool_t files_pool;
+ ARRAY_TYPE(const_string) files;
+
+ struct timeout *to;
+ struct test_read *reads;
+ struct test_write *writes;
+ struct test_delete *deletes;
+ struct test_iter *iters;
+ unsigned int running_op_count;
+
+ unsigned int total_reads, total_writes, total_deletes, total_iters;
+};
+
+static struct ioloop *root_ioloop;
+
+static void test_more(struct test_ctx *ctx);
+static void test_read_callback(struct test_read *r);
+
+static bool test_want_async(struct test_ctx *ctx)
+{
+ if (ctx->async_only)
+ return TRUE;
+ if (ctx->sync_only)
+ return FALSE;
+ return (i_rand_limit(2) == 0);
+}
+
+static void test_op_finish(struct test_ctx *ctx)
+{
+ i_assert(ctx->running_op_count > 0);
+ ctx->running_op_count--;
+
+ if (ctx->to == NULL)
+ ctx->to = timeout_add_short_to(root_ioloop, 0, test_more, ctx);
+}
+
+static void test_write_finish(struct test_write *w)
+{
+ DLLIST_REMOVE(&w->ctx->writes, w);
+ fs_file_deinit(&w->file);
+
+ w->ctx->total_writes++;
+ test_op_finish(w->ctx);
+ i_free(w);
+}
+
+static void test_write_callback(struct test_write *w)
+{
+ int ret;
+
+ ret = fs_write_stream_finish_async(w->file);
+ if (ret < 0) {
+ i_error("fs_write_stream() failed: %s",
+ fs_file_last_error(w->file));
+ }
+ if (ret != 0)
+ test_write_finish(w);
+}
+
+static void test_next_op_write(struct test_ctx *ctx)
+{
+ struct test_write *w;
+ struct istream *input, *input2;
+ struct ostream *output;
+ const char *path;
+
+ w = i_new(struct test_write, 1);
+ w->ctx = ctx;
+ DLLIST_PREPEND(&ctx->writes, w);
+
+ path = t_strconcat(ctx->prefix, guid_generate(), NULL);
+ w->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_REPLACE |
+ (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
+ input = i_stream_create_file("/dev/urandom", IO_BLOCK_SIZE);
+ input2 = i_stream_create_limit(input, i_rand_limit(1024*1024));
+ output = fs_write_stream(w->file);
+ switch (o_stream_send_istream(output, input2)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ 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_fatal("read(/dev/urandom) failed: %s",
+ i_stream_get_error(input));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_fatal("write() failed: %s",
+ o_stream_get_error(output));
+ }
+ i_stream_unref(&input);
+ i_stream_unref(&input2);
+
+ int ret = fs_write_stream_finish(w->file, &output);
+ if (ret < 0) {
+ i_error("fs_write_stream(%s) failed: %s",
+ path, fs_file_last_error(w->file));
+ }
+ if (ret == 0)
+ fs_file_set_async_callback(w->file, test_write_callback, w);
+ else
+ test_write_finish(w);
+}
+
+static void test_iter_finish(struct test_iter *i)
+{
+ const char *error;
+
+ DLLIST_REMOVE(&i->ctx->iters, i);
+
+ if (fs_iter_deinit(&i->iter, &error) < 0) {
+ i_error("fs_iter_deinit() failed: %s", error);
+ pool_unref(&i->pool);
+ } else {
+ pool_unref(&i->ctx->files_pool);
+ i->ctx->files_pool = i->pool;
+ i->ctx->files = i->files;
+ }
+
+ i->ctx->total_iters++;
+ test_op_finish(i->ctx);
+ i_free(i);
+}
+
+static void test_iter_callback(struct test_iter *i)
+{
+ const char *fname;
+
+ while ((fname = fs_iter_next(i->iter)) != NULL) {
+ if (i->object_ids) {
+ /* skip object ID */
+ fname = strchr(fname, '/');
+ i_assert(fname != NULL);
+ fname++;
+ }
+ fname = p_strdup(i->pool, fname);
+ array_push_back(&i->files, &fname);
+ }
+
+ if (!fs_iter_have_more(i->iter))
+ test_iter_finish(i);
+}
+
+static void test_next_op_iter(struct test_ctx *ctx)
+{
+ struct test_iter *i;
+
+ i = i_new(struct test_iter, 1);
+ i->ctx = ctx;
+ i->pool = pool_alloconly_create(MEMPOOL_GROWING"test iter", 256);
+ p_array_init(&i->files, i->pool, 16);
+ DLLIST_PREPEND(&ctx->iters, i);
+
+ i->object_ids = (fs_get_properties(ctx->fs) &
+ FS_PROPERTY_OBJECTIDS) == 0 ?
+ FALSE : (i_rand_limit(2) == 0);
+ i->iter = fs_iter_init(ctx->fs, ctx->prefix,
+ (test_want_async(ctx) ? FS_ITER_FLAG_ASYNC : 0) |
+ (i->object_ids ? FS_ITER_FLAG_OBJECTIDS : 0));
+ fs_iter_set_async_callback(i->iter, test_iter_callback, i);
+ test_iter_callback(i);
+}
+
+static void test_read_finish(struct test_read *r)
+{
+ DLLIST_REMOVE(&r->ctx->reads, r);
+ io_remove(&r->io);
+ i_stream_unref(&r->input);
+ fs_file_deinit(&r->file);
+
+ r->ctx->total_reads++;
+ test_op_finish(r->ctx);
+ i_free(r);
+}
+
+static void test_read_callback(struct test_read *r)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ while ((ret = i_stream_read_more(r->input, &data, &size)) > 0)
+ i_stream_skip(r->input, size);
+ if (ret == 0) {
+ if (r->io == NULL) {
+ r->io = io_add_istream(r->input, test_read_callback, r);
+ fs_file_set_async_callback(r->file, test_read_callback, r);
+ }
+ return;
+ }
+ i_assert(ret == -1);
+ if (r->input->stream_errno != 0 &&
+ r->input->stream_errno != ENOENT)
+ i_error("read() failed: %s", i_stream_get_error(r->input));
+ test_read_finish(r);
+}
+
+static void test_next_read(struct test_ctx *ctx)
+{
+ struct test_read *r;
+
+ r = i_new(struct test_read, 1);
+ r->ctx = ctx;
+ DLLIST_PREPEND(&ctx->reads, r);
+
+ const char *const *fnamep =
+ array_idx(&ctx->files, i_rand_limit(array_count(&ctx->files)));
+ const char *path = t_strconcat(ctx->prefix, *fnamep, NULL);
+ r->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_READONLY |
+ (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
+ r->input = fs_read_stream(r->file, IO_BLOCK_SIZE);
+ test_read_callback(r);
+}
+
+static void test_delete_finish(struct test_delete *d)
+{
+ DLLIST_REMOVE(&d->ctx->deletes, d);
+ fs_file_deinit(&d->file);
+
+ d->ctx->total_deletes++;
+ test_op_finish(d->ctx);
+ i_free(d);
+}
+
+static void test_delete_callback(struct test_delete *d)
+{
+ int ret = fs_delete(d->file);
+ if (ret < 0 && errno != ENOENT) {
+ if (errno == EAGAIN)
+ return;
+ i_error("fs_delete() failed: %s", fs_file_last_error(d->file));
+ }
+ test_delete_finish(d);
+}
+
+static void test_next_delete(struct test_ctx *ctx)
+{
+ struct test_delete *d;
+
+ d = i_new(struct test_delete, 1);
+ d->ctx = ctx;
+ DLLIST_PREPEND(&ctx->deletes, d);
+
+ const char *const *fnamep =
+ array_idx(&ctx->files, i_rand_limit(array_count(&ctx->files)));
+ const char *path = t_strconcat(ctx->prefix, *fnamep, NULL);
+ d->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_READONLY |
+ (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
+ int ret = fs_delete(d->file);
+ if (ret < 0 && errno == EAGAIN) {
+ fs_file_set_async_callback(d->file, test_delete_callback, d);
+ return;
+ }
+ if (ret < 0 && errno != ENOENT)
+ i_error("fs_delete() failed: %s", fs_file_last_error(d->file));
+ test_delete_finish(d);
+}
+
+static void test_next_op(struct test_ctx *ctx)
+{
+ switch (i_rand_limit(4)) {
+ case 0:
+ if (array_is_created(&ctx->files) &&
+ array_count(&ctx->files) * FILES_COUNT_APPROX > ctx->files_count)
+ break;
+ ctx->running_op_count++;
+ test_next_op_write(ctx);
+ break;
+ case 1:
+ ctx->running_op_count++;
+ test_next_op_iter(ctx);
+ break;
+ case 2:
+ if (!array_is_created(&ctx->files) ||
+ array_count(&ctx->files) == 0)
+ break;
+ ctx->running_op_count++;
+ test_next_read(ctx);
+ break;
+ case 3:
+ if (!array_is_created(&ctx->files) ||
+ array_count(&ctx->files) / FILES_COUNT_APPROX < ctx->files_count)
+ break;
+ ctx->running_op_count++;
+ test_next_delete(ctx);
+ break;
+ }
+}
+
+static void test_more(struct test_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+ /* note that all operations may be synchronous */
+ for (unsigned int i = ctx->running_op_count; i < ctx->max_parallel_ops; i++)
+ test_next_op(ctx);
+ if (ctx->to == NULL) {
+ ctx->to = timeout_add(ctx->running_op_count == 0 ? 0 :
+ i_rand_limit(100), test_more, ctx);
+ }
+}
+
+static void stats_output(struct test_ctx *ctx)
+{
+ printf("%u iters, %u reads, %u writes, %u deletes\n",
+ ctx->total_iters, ctx->total_reads, ctx->total_writes,
+ ctx->total_deletes);
+}
+
+int main(int argc, char *argv[])
+{
+ struct fs_settings set;
+ struct test_ctx ctx;
+ const char *error;
+ unsigned int timeout_secs = 0;
+ struct timeout *to_stop = NULL;
+ int c;
+
+ i_zero(&ctx);
+ ctx.max_parallel_ops = DEFAULT_MAX_PARALLEL_OPS;
+ ctx.files_count = DEFAULT_FILES_COUNT;
+
+ i_zero(&set);
+ set.base_dir = PKG_RUNDIR;
+ master_service = master_service_init("test-fs",
+ MASTER_SERVICE_FLAG_STANDALONE,
+ &argc, &argv, "Daf:p:st:u:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ set.debug = TRUE;
+ break;
+ case 'a':
+ ctx.async_only = TRUE;
+ break;
+ case 'f':
+ if (str_to_uint(optarg, &ctx.files_count) < 0)
+ i_fatal("Invalid -f parameter: %s", optarg);
+ break;
+ case 'p':
+ if (str_to_uint(optarg, &ctx.max_parallel_ops) < 0)
+ i_fatal("Invalid -p parameter: %s", optarg);
+ break;
+ case 's':
+ ctx.sync_only = TRUE;
+ break;
+ case 'u':
+ set.username = optarg;
+ break;
+ case 't':
+ if (str_to_uint(optarg, &timeout_secs) < 0)
+ i_fatal("Invalid -t parameter: %s", optarg);
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 3)
+ i_fatal("Usage: [-a|-s] [-D] [-f <files#>] [-p <max ops>] [-t <secs>] [-u <user>] <driver> <args> <prefix>");
+
+ master_service_init_finish(master_service);
+ dict_drivers_register_builtin();
+
+ if (fs_init(argv[0], argv[1], &set, &ctx.fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+ ctx.prefix = argv[2];
+
+ root_ioloop = current_ioloop;
+ test_more(&ctx);
+ if (timeout_secs != 0)
+ to_stop = timeout_add(timeout_secs*1000, io_loop_stop, current_ioloop);
+ struct timeout *to_stats = timeout_add(1000, stats_output, &ctx);
+ io_loop_run(current_ioloop);
+ timeout_remove(&to_stats);
+ timeout_remove(&to_stop);
+
+ while (ctx.writes != NULL)
+ test_write_finish(ctx.writes);
+ while (ctx.iters != NULL)
+ test_iter_finish(ctx.iters);
+ while (ctx.reads != NULL)
+ test_read_finish(ctx.reads);
+ while (ctx.deletes != NULL)
+ test_delete_finish(ctx.deletes);
+
+ stats_output(&ctx);
+ timeout_remove(&ctx.to);
+ fs_deinit(&ctx.fs);
+ master_service_deinit(&master_service);
+}