diff --git a/mail/cyrus-imapd34/Makefile b/mail/cyrus-imapd34/Makefile index d5bedaead1d9..35a8252929ee 100644 --- a/mail/cyrus-imapd34/Makefile +++ b/mail/cyrus-imapd34/Makefile @@ -1,264 +1,266 @@ PORTNAME= cyrus-imapd PORTVERSION= 3.4.7 -PORTREVISION= 0 +PORTREVISION= 1 CATEGORIES= mail MASTER_SITES= https://github.com/cyrusimap/cyrus-imapd/releases/download/${PORTNAME}-${PORTVERSION}/ PKGNAMESUFFIX= ${CYRUS_IMAPD_VER} MAINTAINER= ume@FreeBSD.org COMMENT= Cyrus mail server, supporting POP3 and IMAP4 protocols ${COMMENT_${FLAVOR}} WWW= https://www.cyrusimap.org/ COMMENT_http= (with HTTP) LICENSE= BSD4CLAUSE LICENSE_FILE= ${WRKSRC}/COPYING BROKEN_riscv64= fails to build: lib/chartable.c: Error 1 FLAVORS= basic http http_PKGNAMESUFFIX= ${CYRUS_IMAPD_VER}-http CYRUS_IMAPD_VER= 34 +EXTRA_PATCHES= ${FILESDIR}/v34-CVE-2024-34055.patch:-p1 + LIB_DEPENDS= libsasl2.so:security/cyrus-sasl2 \ libicuuc.so:devel/icu \ libjansson.so:devel/jansson \ libuuid.so:misc/e2fsprogs-libuuid CONFLICTS_INSTALL= cyrus-imapd2? cyrus-imapd3[0-35-] cyrus-imapd3[0-35-]-http USES= compiler:c11 cpe gmake libtool perl5 pkgconfig ssl USE_RC_SUBR= imapd CYRUS_PREFIX= ${PREFIX}/cyrus GNU_CONFIGURE= yes CONFIGURE_ARGS= --libexecdir=${CYRUS_PREFIX}/libexec \ --sbindir=${CYRUS_PREFIX}/sbin \ --sysconfdir=${PREFIX}/etc \ --with-cyrus-user=${CYRUS_USER} \ --with-sasl=${LOCALBASE} \ --with-com_err \ --with-openssl=${OPENSSLBASE} \ --with-perl=${PERL} CONFIGURE_ENV+= LIBS="-L${LOCALBASE}/lib" CPPFLAGS+= -I${LOCALBASE}/include MAKE_JOBS_UNSAFE= yes USES+= shebangfix SHEBANG_FILES= imap/promdatagen tools/config2header \ tools/masssievec tools/mkimap tools/translatesieve \ perl/sieve/scripts/*.pl CPE_VENDOR= cmu CPE_PRODUCT= cyrus_imap_server OPTIONS_DEFINE= AUTOCREATE BACKUP CLAMAV HTTP IDLED LDAP MURDER \ MYSQL NNTP PCRE2 PGSQL REPLICATION SQLITE SQUAT SRS \ XAPIAN DOCS OPTIONS_DEFAULT= AUTOCREATE IDLED READLINE_GNU SQLITE SQUAT SRS .if ${FLAVOR:U} == http OPTIONS_DEFAULT+= HTTP .endif OPTIONS_SUB= yes AUTOCREATE_DESC= Enable autocreate support AUTOCREATE_CONFIGURE_ENABLE= autocreate BACKUP_DESC= Enable backup support (experimental) BACKUP_CONFIGURE_ENABLE=backup CLAMAV_DESC= Use ClamAV CLAMAV_CONFIGURE_WITH= clamav CLAMAV_LIB_DEPENDS= libclamav.so:security/clamav # OPTIONS_DEFINE+= CLD2 # OPTIONS_DEFAULT+= CLD2 # CLD2_DESC= Use CLD2 # CLD2_CONFIGURE_WITH= cld2 # CLD2_CONFIGURE_ENV= CLD2_CFLAGS="-I${LOCALBASE}/include" \ # CLD2_LIBS="-L${LOCALBASE}/lib -lcld2" # CLD2_LIB_DEPENDS= libcld2.so:devel/cld2 HTTP_DESC= Enable HTTP support HTTP_CONFIGURE_ENABLE= http HTTP_LIB_DEPENDS= libical.so:devel/libical \ libnghttp2.so:www/libnghttp2 \ libshp.so:devel/shapelib \ libbrotlidec.so:archivers/brotli # Need additional patch to opendkim #HTTP_LIB_DEPENDS+= libopendkim.so:mail/opendkim #HTTP_CPPFLAGS+= -I${LOCALBASE}/include/opendkim HTTP_USES= gnome HTTP_USE= GNOME=libxml2 IDLED_DESC= Enable IMAP idled support IDLED_CONFIGURE_ENABLE= idled LDAP_DESC= Enable LDAP support (experimental) LDAP_USES= ldap LDAP_CONFIGURE_ON= --with-ldap=${LOCALBASE} MURDER_DESC= Enable IMAP Murder support MURDER_CONFIGURE_ENABLE=murder MURDER_MAKE_ENV= PTHREAD_LIBS="-lpthread" MYSQL_USES= mysql MYSQL_CONFIGURE_WITH= mysql NNTP_DESC= Enable NNTP support NNTP_CONFIGURE_ENABLE= nntp PCRE2_DESC= Use PCRE2 rather than PCRE PCRE2_LIB_DEPENDS= libpcre2-posix.so:devel/pcre2 PCRE2_LIB_DEPENDS_OFF= libpcre.so:devel/pcre PCRE2_CONFIGURE_ON= --disable-pcre PCRE2_CONFIGURE_OFF= --disable-pcre2 PGSQL_USES= pgsql PGSQL_CONFIGURE_ON= --with-pgsql=${LOCALBASE} REPLICATION_DESC= Enable replication (experimental) REPLICATION_CONFIGURE_ENABLE=replication SRS_DESC= Enable Sender Rewriting Scheme support SRS_CONFIGURE_ENABLE= srs SRS_LIB_DEPENDS= libsrs2.so:mail/libsrs2 SQLITE_CONFIGURE_ON= --with-sqlite=${LOCALBASE} SQLITE_BROKEN_OFF= SQLITE is required SQUAT_DESC= Enable Squat support SQUAT_CONFIGURE_OFF= --disable-squat XAPIAN_DESC= Enable Xapian support XAPIAN_CONFIGURE_ENABLE=xapian XAPIAN_LIB_DEPENDS= libxapian.so:databases/xapian-core XAPIAN_BUILD_DEPENDS= rsync:net/rsync XAPIAN_RUN_DEPENDS= ${XAPIAN_BUILD_DEPENDS} OPTIONS_RADIO= GSSAPI READLINE OPTIONS_RADIO_GSSAPI= GSSAPI_HEIMDAL GSSAPI_MIT .if exists(/usr/lib/libkrb5.a) OPTIONS_RADIO_GSSAPI+= GSSAPI_BASE OPTIONS_DEFAULT+= GSSAPI_BASE .endif GSSAPI_BASE_USES= gssapi GSSAPI_BASE_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_HEIMDAL_USES= gssapi:heimdal,flags GSSAPI_HEIMDAL_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_MIT_USES= gssapi:mit GSSAPI_MIT_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=mit OPTIONS_RADIO_READLINE= READLINE_GNU READLINE_PERL READLINE_GNU_DESC= Use Term::Readline::GNU for cyradm READLINE_GNU_RUN_DEPENDS= p5-Term-ReadLine-Gnu>=0:devel/p5-Term-ReadLine-Gnu READLINE_PERL_DESC= Use Term::Readline::Perl for cyradm READLINE_PERL_RUN_DEPENDS= p5-Term-ReadLine-Perl>=0:devel/p5-Term-ReadLine-Perl MANDIRS= ${CYRUS_PREFIX}/man PORTDOCS= * SUB_FILES= pkg-message pkg-install pkg-deinstall cyrus-imapd-man.conf SUB_LIST= CYRUS_USER=${CYRUS_USER} CYRUS_GROUP=${CYRUS_GROUP} CYRUS_USER?= cyrus CYRUS_GROUP?= cyrus MAN_MAN1= httptest imtest installsieve lmtptest mupdatetest nntptest \ pop3test sieveshell sivtest smtptest synctest MAN_MAN3= imclient MAN_MAN5= cyrus.conf imapd.conf krb.equiv CYRUS_MAN8= arbitron backupd chk_cyrus ctl_backups ctl_conversationsdb \ ctl_cyrusdb ctl_deliver ctl_mboxlist cvt_cyrusdb \ cvt_xlist_specialuse cyr_backup cyr_buildinfo cyr_dbtool \ cyr_deny cyr_df cyr_expire cyr_info cyr_synclog cyr_userseen \ cyr_virusscan cyradm cyrdump deliver fud idled imapd ipurge \ lmtpd lmtpproxyd master mbexamine mbpath mbtool notifyd pop3d \ pop3proxyd promstatsd proxyd ptdump ptexpire ptloader quota \ reconstruct restore sievec sieved smmapd timsieved tls_prune \ unexpunge CYRUS_PERL_MAN1=cyradm CYRUS_PERL_MAN3=Cyrus::Annotator::Daemon Cyrus::Annotator::Message \ Cyrus::IMAP Cyrus::IMAP::Admin Cyrus::IMAP::IMSP \ Cyrus::IMAP::Shell Cyrus::SIEVE::managesieve INSTALL_TARGET= install-strip REINPLACE_ARGS= -i '' .include .if ${PORT_OPTIONS:MHTTP} || ${PORT_OPTIONS:MSQLITE} USES+= sqlite .endif .if !${PORT_OPTIONS:MGSSAPI_BASE} && !${PORT_OPTIONS:MGSSAPI_HEIMDAL} && \ !${PORT_OPTIONS:MGSSAPI_MIT} CONFIGURE_ARGS+=--disable-gssapi .endif .if ${PORT_OPTIONS:MHTTP} CYRUS_MAN8+= ctl_zoneinfo httpd MAN_MAN1+= dav_reconstruct .endif .if ${PORT_OPTIONS:MNNTP} CYRUS_MAN8+= fetchnews nntpd .endif .if ${PORT_OPTIONS:MMURDER} CYRUS_MAN8+= mupdate .endif .if ${PORT_OPTIONS:MREPLICATION} CYRUS_MAN8+= sync_client sync_reset sync_server .endif .if ${PORT_OPTIONS:MSQUAT} || ${PORT_OPTIONS:MXAPIAN} CYRUS_MAN8+= squatter PLIST_SUB+= SQUATTER="" .else PLIST_SUB+= SQUATTER="@comment " .endif post-patch: @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|" \ -e "s|%%CYRUS_USER%%|${CYRUS_USER}|g" \ -e "s|%%CYRUS_GROUP%%|${CYRUS_GROUP}|g" \ ${WRKSRC}/tools/mkimap .for f in masssievec translatesieve @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|g" \ -e "s|/usr/sieve|/var/imap/sieve|g" \ ${WRKSRC}/tools/${f} .endfor @${REINPLACE_CMD} \ -e 's|$$(libdir)/\(pkgconfig\)|${PREFIX}/libdata/\1|g' \ -e 's|$$(mandir)/\(man[8]\)|${PREFIX}/cyrus/man/\1|g' \ ${WRKSRC}/Makefile.in post-install: ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/IMAP/IMAP.so ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/SIEVE/managesieve/managesieve.so ${MKDIR} ${STAGEDIR}${EXAMPLESDIR} ${INSTALL_DATA} ${FILESDIR}/imapd.conf \ ${STAGEDIR}${EXAMPLESDIR} ${SED} -e 's,/run/cyrus/socket,/var/imap/socket,' \ ${WRKSRC}/doc/examples/cyrus_conf/normal.conf \ > ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .if !${PORT_OPTIONS:MHTTP} ${REINPLACE_CMD} -e 's/^\( http\)/#\1/' \ ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .endif .for f in mkimap masssievec translatesieve ${INSTALL_SCRIPT} ${WRKSRC}/tools/${f} \ ${STAGEDIR}${CYRUS_PREFIX}/sbin/${f} .endfor ${INSTALL_DATA} ${WRKDIR}/cyrus-imapd-man.conf \ ${STAGEDIR}${PREFIX}/etc/man.d/cyrus-imapd.conf .for s in 1 3 5 . for m in ${MAN_MAN${s}} @${ECHO_CMD} share/man/man${s}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for s in 1 3 . for m in ${CYRUS_PERL_MAN${s}} @${ECHO_CMD} ${SITE_MAN${s}}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for m in ${CYRUS_MAN8} @${ECHO_CMD} ${CYRUS_PREFIX}/man/man8/${m}.8.gz >> ${TMPPLIST} .endfor post-install-DOCS-on: ${MKDIR} ${STAGEDIR}${DOCSDIR} cd ${WRKSRC}/doc && ${COPYTREE_SHARE} . ${STAGEDIR}${DOCSDIR} \ "! ( -path */html/_sources* -o -name .buildinfo )" ${RM} -r ${STAGEDIR}${DOCSDIR}/rst ${STAGEDIR}${DOCSDIR}/source .include diff --git a/mail/cyrus-imapd34/files/v34-CVE-2024-34055.patch b/mail/cyrus-imapd34/files/v34-CVE-2024-34055.patch new file mode 100644 index 000000000000..c1719ea49b28 --- /dev/null +++ b/mail/cyrus-imapd34/files/v34-CVE-2024-34055.patch @@ -0,0 +1,5815 @@ +From b6682068bf8c754a87f98ee59d2616d48ed756c7 Mon Sep 17 00:00:00 2001 +From: Robert Stepanek +Date: Wed, 3 Jan 2024 09:51:36 +0100 +Subject: [PATCH 01/22] SearchFuzzy.pm: do not use non-standard XSNIPPETS + command + +The XSNIPPETS and XCONVMULTISTANDARD commands in Cyrus got +deprecated, so don't keep our test using it. + +Signed-off-by: Robert Stepanek +--- + cassandane/Cassandane/Cyrus/SearchFuzzy.pm | 344 +++++++++------------ + 1 file changed, 146 insertions(+), 198 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +index 1ac00dc49..dd1a369bd 100644 +--- a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm ++++ b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +@@ -43,6 +43,8 @@ use warnings; + use Cwd qw(abs_path); + use DateTime; + use Data::Dumper; ++use MIME::Base64 qw(encode_base64); ++use Encode qw(decode encode); + + use lib '.'; + use base qw(Cassandane::Cyrus::TestCase); +@@ -50,10 +52,19 @@ use Cassandane::Util::Log; + + sub new + { ++ + my ($class, @args) = @_; + my $config = Cassandane::Config->default()->clone(); +- $config->set(conversations => 'on'); +- return $class->SUPER::new({ config => $config }, @args); ++ $config->set( ++ conversations => 'on', ++ httpallowcompress => 'no', ++ httpmodules => 'jmap', ++ ); ++ return $class->SUPER::new({ ++ config => $config, ++ jmap => 1, ++ services => [ 'imap', 'http' ] ++ }, @args); + } + + sub set_up +@@ -134,6 +145,55 @@ sub create_testmessages + $self->{instance}->run_command({cyrus => 1}, 'squatter'); + } + ++sub get_snippets ++{ ++ # Previous versions of this test module used XSNIPPETS to ++ # assert snippets but this command got removed from Cyrus. ++ # Use JMAP instead. ++ ++ my ($self, $folder, $uids, $filter) = @_; ++ ++ my $imap = $self->{store}->get_client(); ++ my $jmap = $self->{jmap}; ++ ++ $self->assert_not_null($jmap); ++ ++ $imap->select($folder); ++ my $res = $imap->fetch($uids, ['emailid']); ++ my %emailIdToImapUid = map { $res->{$_}{emailid}[0] => $_ } keys %$res; ++ ++ $res = $jmap->CallMethods([ ++ ['SearchSnippet/get', { ++ filter => $filter, ++ emailIds => [ keys %emailIdToImapUid ], ++ }, 'R1'], ++ ]); ++ ++ my @snippets; ++ foreach (@{$res->[0][1]{list}}) { ++ if ($_->{subject}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'SUBJECT', ++ $_->{subject}, ++ ]); ++ } ++ if ($_->{preview}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'BODY', ++ $_->{preview}, ++ ]); ++ } ++ } ++ ++ return { ++ snippets => [ sort { $a->[1] <=> $b->[1] } @snippets ], ++ }; ++} ++ + sub test_copy_messages + :needs_search_xapian + { +@@ -151,12 +211,13 @@ sub test_copy_messages + } + + sub test_stem_verbs +- :min_version_3_0 :needs_search_xapian ++ :min_version_3_0 :needs_search_xapian :JMAPExtensions + { + my ($self) = @_; + $self->create_testmessages(); + + my $talk = $self->{store}->get_client(); ++ $self->assert_not_null($self->{jmap}); + + xlog $self, "Select INBOX"; + my $r = $talk->select("INBOX") || die; +@@ -175,11 +236,8 @@ sub test_stem_verbs + $r = $talk->search('fuzzy', ['subject', { Quote => "runs" }]) || die; + $self->assert_num_equals(3, scalar @$r); + +- xlog $self, 'XSNIPPETS for FUZZY subject "runs"'; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'subject', { Quote => 'runs' }] +- ) || die; ++ xlog $self, 'Get snippets for FUZZY subject "runs"'; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'runs' }); + $self->assert_num_equals(3, scalar @{$r->{snippets}}); + } + +@@ -250,12 +308,8 @@ sub test_snippet_wildcard + $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + +- xlog $self, "XSNIPPETS for $term"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => "$term*" }] +- ) || die; +- xlog $self, Dumper($r); ++ xlog $self, "Get snippets for $term"; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => "$term*" }); + $self->assert_num_equals(2, scalar @{$r->{snippets}}); + } + +@@ -358,13 +412,17 @@ sub test_normalize_snippets + my ($self) = @_; + + # Set up test message with funny characters +- my $body = "foo gären советской diĝir naïve léger"; +- my @terms = split / /, $body; ++use utf8; ++ my @terms = ( "gären", "советской", "diĝir", "naïve", "léger" ); ++no utf8; ++ my $body = encode_base64(encode('UTF-8', join(' ', @terms))); ++ $body =~ s/\r?\n/\r\n/gs; + + xlog $self, "Generate and index test messages."; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + +@@ -380,24 +438,20 @@ sub test_normalize_snippets + + # Assert that diacritics are matched and returned + foreach my $term (@terms) { +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + } + + # Assert that search without diacritics matches + if ($self->{skipdiacrit}) { + my $term = "naive"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => $term }); ++use utf8; ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++no utf8; + } ++ + } + + sub test_skipdiacrit +@@ -499,38 +553,23 @@ sub test_snippets_termcover + my $r = $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- my $want = "favourite cereal"; ++ my $want = "favourite cereal"; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite', +- 'fuzzy', 'text', 'cereal', +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ operator => 'AND', ++ conditions => [{ ++ text => 'favourite', ++ }, { ++ text => 'cereal', ++ }, { ++ text => '"bogus gnarly"' ++ }], ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite cereal' +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- # Regression - a phrase is treated as a loose term +- $r = $talk->xsnippets( [ [ 'INBOX', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite nope cereal' }, +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite cereal' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ text => 'favourite cereal', ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + } + +@@ -542,18 +581,28 @@ sub test_cjk_words + + xlog $self, "Generate and index test messages."; + ++use utf8; + my $body = "明末時已經有香港地方的概念"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + + # Splits into the words: "み, 円, 月額, 申込 ++use utf8; + $body = "申込み!月額円"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("2", %params) || die; + +@@ -569,50 +618,45 @@ sub test_cjk_words + + my $term; + # Search for a two-character CJK word ++use utf8; + $term = "已經"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + + # Search for the CJK words 明末 and 時, note that the + # word order is reversed to the original message ++use utf8; + $term = "時明末"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + + # Search for the partial CJK word 月 ++use utf8; + $term = "月"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for the interleaved, partial CJK word 額申 ++use utf8; + $term = "額申"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for three of four words: "み, 月額, 申込", + # in different order than the original. ++use utf8; + $term = "月額み申込"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + } + +@@ -805,86 +849,6 @@ sub test_xattachmentname + } + + +-sub test_xapianv2 +- :min_version_3_0 :needs_search_xapian +-{ +- my ($self) = @_; +- +- my $talk = $self->{store}->get_client(); +- +- # This is a smallish regression test to check if we break something +- # obvious by moving Xapian indexing from folder:uid to message guids. +- # +- # Apart from the tests in this module, at least also the following +- # imodules are relevant: Metadata for SORT, Thread for THREAD. +- +- xlog $self, "Generate message"; +- my $r = $self->make_message("I run", body => "Run, Forrest! Run!" ) || die; +- my $uid = $r->{attrs}->{uid}; +- +- xlog $self, "Copy message into INBOX"; +- $talk->copy($uid, "INBOX"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Create target mailbox"; +- $talk->create("INBOX.target"); +- +- xlog $self, "Copy message into INBOX.target"; +- $talk->copy($uid, "INBOX.target"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(3, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Generate message"; +- $self->make_message("You run", body => "A running joke" ) || die; +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}}); +- +- xlog $self, "SEARCH FUZZY"; +- $r = $talk->search( +- "charset", "utf-8", "fuzzy", "text", "run", +- ) || die; +- $self->assert_num_equals(3, scalar @$r); +- +- xlog $self, "Select INBOX"; +- $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- +- xlog $self, "XSNIPPETS"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'body', 'run'], +- ) || die; +- $self->assert_num_equals(3, scalar @{$r->{snippets}}); +-} +- + sub test_snippets_escapehtml + :min_version_3_0 :needs_search_xapian + { +@@ -914,21 +878,15 @@ sub test_snippets_escapehtml + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); + my %m; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test1' ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test1' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); +- $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test2' ] +- ) || die; ++ $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); ++ $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); + ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test2' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); +- $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); ++ $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); ++ $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); + } + + sub test_search_exactmatch +@@ -963,13 +921,10 @@ sub test_search_exactmatch + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { body => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert(index($m{body}, "some text") != -1); +- $self->assert(index($m{body}, "some long text") == -1); ++ $self->assert(index($m{body}, "some text") != -1); ++ $self->assert(index($m{body}, "some long text") == -1); + } + + sub test_search_subjectsnippet +@@ -1004,10 +959,7 @@ sub test_search_subjectsnippet + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { text => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; + $self->assert_matches(qr/^\[plumbing\]/, $m{subject}); + } +@@ -1317,11 +1269,10 @@ sub test_detect_language + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', 'atmet' ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++ $r = $self->get_snippets('INBOX', $uids, { body => 'atmet' }); ++use utf8; ++ $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++no utf8; + } + + sub test_detect_language_subject +@@ -1377,12 +1328,9 @@ sub test_detect_language_subject + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'subject', 'Landschaft' ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'Landschaft' }); + $self->assert_str_equals( +- 'A subject with the German word Landschaften', ++ 'A subject with the German word Landschaften', + $r->{snippets}[0][3] + ); + } +-- +2.39.2 + + +From 00aafb0fd51aaac1badc3370a250605cff4313b0 Mon Sep 17 00:00:00 2001 +From: Bron Gondwana +Date: Fri, 20 Nov 2020 11:24:58 +1100 +Subject: [PATCH 02/22] imapd: maxsize for appends + +--- + imap/imapd.c | 4 ++++ + lib/imapoptions | 4 ++++ + 2 files changed, 8 insertions(+) + +diff --git a/imap/imapd.c b/imap/imapd.c +index a617ff80c..48055ccce 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3829,6 +3829,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + const char *parseerr = NULL, *url = NULL; + struct appendstage *curstage; + mbentry_t *mbentry = NULL; ++ size_t maxsize = config_getint(IMAPOPT_APPEND_MAXSIZE) * 1024; ++ if (!maxsize) maxsize = UINT32_MAX; + + memset(&appendstate, 0, sizeof(struct appendstate)); + +@@ -4004,12 +4006,14 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + size = 0; + r = append_catenate(curstage->f, cur_name, &size, + &(curstage->binary), &parseerr, &url); ++ if (!r && size > maxsize) r = IMAP_MESSAGE_TOO_LARGE; + if (r) goto done; + } + else { + /* Read size from literal */ + r = getliteralsize(arg.s, c, &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; ++ if (!r && size > maxsize) r = IMAP_MESSAGE_TOO_LARGE; + if (r) goto done; + + /* Copy message to stage */ +diff --git a/lib/imapoptions b/lib/imapoptions +index 5cb8ef7b8..786b288fe 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -296,6 +296,10 @@ Blank lines and lines beginning with ``#'' are ignored. + but might be useful in the meantime for supporting old clients that + do not implement the RFC 5464 IMAP METADATA extension. */ + ++{ "append_maxsize", 0, INT, "3.3.2" } ++/* The size in kilobytes of the largest message that can be appended ++ via IMAP. If zero, no limit (i.e UINT32_MAX) */ ++ + { "aps_topic", NULL, STRING, "3.0.0" } + /* Topic for Apple Push Service registration. */ + { "aps_topic_caldav", NULL, STRING, "3.0.0" } +-- +2.39.2 + + +From 02f158782578d4d99e0915c317ffe9d339180cca Mon Sep 17 00:00:00 2001 +From: Bron Gondwana +Date: Fri, 20 Nov 2020 12:54:58 +1100 +Subject: [PATCH 03/22] imapd: push the maxsize down into each parser to avoid + spooling + +--- + imap/imap_proxy.c | 7 ++++++- + imap/imap_proxy.h | 2 +- + imap/imapd.c | 42 ++++++++++++++++++------------------------ + imap/index.c | 7 ++++++- + imap/index.h | 2 +- + 5 files changed, 32 insertions(+), 28 deletions(-) + +diff --git a/imap/imap_proxy.c b/imap/imap_proxy.c +index fb585e680..2dac80455 100644 +--- a/imap/imap_proxy.c ++++ b/imap/imap_proxy.c +@@ -1207,7 +1207,7 @@ void proxy_copy(const char *tag, char *sequence, char *name, int myrights, + /* xxx end of separate proxy-only code */ + + int proxy_catenate_url(struct backend *s, struct imapurl *url, FILE *f, +- unsigned long *size, const char **parseerr) ++ size_t maxsize, unsigned long *size, const char **parseerr) + { + char mytag[128]; + int c, r = 0, found = 0; +@@ -1309,6 +1309,11 @@ int proxy_catenate_url(struct backend *s, struct imapurl *url, FILE *f, + if (c == '}') c = prot_getc(s->in); + if (c == '\r') c = prot_getc(s->in); + if (c != '\n') c = EOF; ++ if (sz > maxsize) { ++ r = IMAP_MESSAGE_TOO_LARGE; ++ eatline(s->in, c); ++ goto next_resp; ++ } + } + else if (c == 'n' || c == 'N') { + c = chomp(s->in, "il"); +diff --git a/imap/imap_proxy.h b/imap/imap_proxy.h +index aa2170960..89cb02002 100644 +--- a/imap/imap_proxy.h ++++ b/imap/imap_proxy.h +@@ -86,7 +86,7 @@ void proxy_copy(const char *tag, char *sequence, char *name, int myrights, + int usinguid, struct backend *s); + + int proxy_catenate_url(struct backend *s, struct imapurl *url, FILE *f, +- unsigned long *size, const char **parseerr); ++ size_t maxsize, unsigned long *size, const char **parseerr); + + int annotate_fetch_proxy(const char *server, const char *mbox_pat, + const strarray_t *entry_pat, +diff --git a/imap/imapd.c b/imap/imapd.c +index 48055ccce..2e55a6285 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3534,7 +3534,7 @@ static int isokflag(char *s, int *isseen) + } + } + +-static int getliteralsize(const char *p, int c, ++static int getliteralsize(const char *p, int c, size_t maxsize, + unsigned *size, int *binary, const char **parseerr) + + { +@@ -3573,6 +3573,9 @@ static int getliteralsize(const char *p, int c, + return IMAP_PROTOCOL_ERROR; + } + ++ if (num > maxsize) ++ return IMAP_MESSAGE_TOO_LARGE; ++ + if (!isnowait) { + /* Tell client to send the message */ + prot_printf(imapd_out, "+ go ahead\r\n"); +@@ -3584,7 +3587,7 @@ static int getliteralsize(const char *p, int c, + return 0; + } + +-static int catenate_text(FILE *f, unsigned *totalsize, int *binary, ++static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *binary, + const char **parseerr) + { + int c; +@@ -3597,11 +3600,9 @@ static int catenate_text(FILE *f, unsigned *totalsize, int *binary, + c = getword(imapd_in, &arg); + + /* Read size from literal */ +- r = getliteralsize(arg.s, c, &size, binary, parseerr); ++ r = getliteralsize(arg.s, c, maxsize - *totalsize, &size, binary, parseerr); + if (r) return r; + +- if (*totalsize > UINT_MAX - size) r = IMAP_MESSAGE_TOO_LARGE; +- + /* Catenate message part to stage */ + while (size) { + n = prot_read(imapd_in, buf, size > 4096 ? 4096 : size); +@@ -3629,7 +3630,7 @@ static int catenate_text(FILE *f, unsigned *totalsize, int *binary, + } + + static int catenate_url(const char *s, const char *cur_name, FILE *f, +- unsigned *totalsize, const char **parseerr) ++ size_t maxsize, unsigned *totalsize, const char **parseerr) + { + struct imapurl url; + struct index_state *state; +@@ -3668,11 +3669,8 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + proxy_userid, &backend_cached, + &backend_current, &backend_inbox, imapd_in); + if (be) { +- r = proxy_catenate_url(be, &url, f, &size, parseerr); +- if (*totalsize > UINT_MAX - size) +- r = IMAP_MESSAGE_TOO_LARGE; +- else +- *totalsize += size; ++ r = proxy_catenate_url(be, &url, f, maxsize - *totalsize, &size, parseerr); ++ *totalsize += size; + } + else + r = IMAP_SERVER_UNAVAILABLE; +@@ -3727,14 +3725,12 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + struct protstream *s = prot_new(fileno(f), 1); + + r = index_urlfetch(state, msgno, 0, url.section, +- url.start_octet, url.octet_count, s, &size); ++ url.start_octet, url.octet_count, s, ++ maxsize - *totalsize, &size); + if (r == IMAP_BADURL) + *parseerr = "No such message part"; + else if (!r) { +- if (*totalsize > UINT_MAX - size) +- r = IMAP_MESSAGE_TOO_LARGE; +- else +- *totalsize += size; ++ *totalsize += size; + } + + prot_flush(s); +@@ -3751,7 +3747,7 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + return r; + } + +-static int append_catenate(FILE *f, const char *cur_name, unsigned *totalsize, ++static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsigned *totalsize, + int *binary, const char **parseerr, const char **url) + { + int c, r = 0; +@@ -3765,7 +3761,7 @@ static int append_catenate(FILE *f, const char *cur_name, unsigned *totalsize, + } + + if (!strcasecmp(arg.s, "TEXT")) { +- int r1 = catenate_text(f, totalsize, binary, parseerr); ++ int r1 = catenate_text(f, maxsize, totalsize, binary, parseerr); + if (r1) return r1; + + /* if we see a SP, we're trying to catenate more than one part */ +@@ -3781,7 +3777,7 @@ static int append_catenate(FILE *f, const char *cur_name, unsigned *totalsize, + } + + if (!r) { +- r = catenate_url(arg.s, cur_name, f, totalsize, parseerr); ++ r = catenate_url(arg.s, cur_name, f, maxsize, totalsize, parseerr); + if (r) { + *url = arg.s; + return r; +@@ -4004,16 +4000,14 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, &size, ++ r = append_catenate(curstage->f, cur_name, maxsize, &size, + &(curstage->binary), &parseerr, &url); +- if (!r && size > maxsize) r = IMAP_MESSAGE_TOO_LARGE; + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(arg.s, c, maxsize, &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; +- if (!r && size > maxsize) r = IMAP_MESSAGE_TOO_LARGE; + if (r) goto done; + + /* Copy message to stage */ +@@ -14010,7 +14004,7 @@ static void cmd_urlfetch(char *tag) + } else { + r = index_urlfetch(state, msgno, params, url.section, + url.start_octet, url.octet_count, +- imapd_out, NULL); ++ imapd_out, UINT32_MAX, NULL); + } + + err: +diff --git a/imap/index.c b/imap/index.c +index ef537aa55..35ca866aa 100644 +--- a/imap/index.c ++++ b/imap/index.c +@@ -4550,7 +4550,7 @@ static int index_fetchreply(struct index_state *state, uint32_t msgno, + EXPORTED int index_urlfetch(struct index_state *state, uint32_t msgno, + unsigned params, const char *section, + unsigned long start_octet, unsigned long octet_count, +- struct protstream *pout, unsigned long *outsize) ++ struct protstream *pout, size_t maxsize, unsigned long *outsize) + { + /* dumbass eM_Client sends this: + * A4 APPEND "INBOX.Junk Mail" () "14-Jul-2013 17:01:02 +0000" +@@ -4723,6 +4723,11 @@ EXPORTED int index_urlfetch(struct index_state *state, uint32_t msgno, + n = size - start_octet; + } + ++ if (n > maxsize) { ++ r = IMAP_MESSAGE_TOO_LARGE; ++ goto done; ++ } ++ + if (outsize) { + /* Return size (CATENATE) */ + *outsize = n; +diff --git a/imap/index.h b/imap/index.h +index 196607f3f..bf8006d9b 100644 +--- a/imap/index.h ++++ b/imap/index.h +@@ -303,7 +303,7 @@ extern struct seqset *index_vanished(struct index_state *state, + extern int index_urlfetch(struct index_state *state, uint32_t msgno, + unsigned params, const char *section, + unsigned long start_octet, unsigned long octet_count, +- struct protstream *pout, unsigned long *size); ++ struct protstream *pout, size_t maxsize, unsigned long *size); + extern char *index_get_msgid(struct index_state *state, uint32_t msgno); + extern struct nntp_overview *index_overview(struct index_state *state, + uint32_t msgno); +-- +2.39.2 + + +From 133a11ebfd9e3f659da3081d8e7c9f416c8ead3b Mon Sep 17 00:00:00 2001 +From: Bron Gondwana +Date: Tue, 1 Dec 2020 08:11:31 +1100 +Subject: [PATCH 04/22] use maxmessagesize rather than our own config option + +--- + imap/imapd.c | 2 +- + lib/imapoptions | 4 ---- + 2 files changed, 1 insertion(+), 5 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 2e55a6285..d9a9dd776 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3825,7 +3825,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + const char *parseerr = NULL, *url = NULL; + struct appendstage *curstage; + mbentry_t *mbentry = NULL; +- size_t maxsize = config_getint(IMAPOPT_APPEND_MAXSIZE) * 1024; ++ size_t maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE) * 1024; + if (!maxsize) maxsize = UINT32_MAX; + + memset(&appendstate, 0, sizeof(struct appendstate)); +diff --git a/lib/imapoptions b/lib/imapoptions +index 786b288fe..5cb8ef7b8 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -296,10 +296,6 @@ Blank lines and lines beginning with ``#'' are ignored. + but might be useful in the meantime for supporting old clients that + do not implement the RFC 5464 IMAP METADATA extension. */ + +-{ "append_maxsize", 0, INT, "3.3.2" } +-/* The size in kilobytes of the largest message that can be appended +- via IMAP. If zero, no limit (i.e UINT32_MAX) */ +- + { "aps_topic", NULL, STRING, "3.0.0" } + /* Topic for Apple Push Service registration. */ + { "aps_topic_caldav", NULL, STRING, "3.0.0" } +-- +2.39.2 + + +From ddc431769b61eef06550da624c1c99a2fd620dbb Mon Sep 17 00:00:00 2001 +From: ellie timoney +Date: Wed, 27 Mar 2024 11:31:58 +1100 +Subject: [PATCH 05/22] imapd: read maxmsgsize once at startup + +Based on: +40793dfde8c96797d86f80e9f461bea61bca3bc9 imapd.c: Advertise APPENDLIMIT= capability + +but without introducing the APPENDLIMIT= capability +--- + imap/imapd.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index d9a9dd776..e7cf600c7 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -135,6 +135,7 @@ static int imaps = 0; + static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; ++static size_t maxsize = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -908,6 +909,9 @@ int service_init(int argc, char **argv, char **envp) + + prometheus_increment(CYRUS_IMAP_READY_LISTENERS); + ++ maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE) * 1024; ++ if (!maxsize) maxsize = UINT32_MAX; ++ + return 0; + } + +@@ -3825,8 +3829,6 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + const char *parseerr = NULL, *url = NULL; + struct appendstage *curstage; + mbentry_t *mbentry = NULL; +- size_t maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE) * 1024; +- if (!maxsize) maxsize = UINT32_MAX; + + memset(&appendstate, 0, sizeof(struct appendstate)); + +-- +2.39.2 + + +From a32fe042bc503a36393e7d888b26b6c1759cf6b0 Mon Sep 17 00:00:00 2001 +From: Matthew Horsfall +Date: Wed, 15 Jun 2022 14:57:02 -0400 +Subject: [PATCH 06/22] imap/imapd.c: IMAPOPT_MAXMESSAGESIZE is bytes, not + kilobytes + +I think this was a mistake added in bf28aa3fb6 when replacing +IMAPOPT_APPEND_MAXSIZE. + +Signed-off-by: Matthew Horsfall +--- + imap/imapd.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index e7cf600c7..ce8c6f675 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -909,7 +909,7 @@ int service_init(int argc, char **argv, char **envp) + + prometheus_increment(CYRUS_IMAP_READY_LISTENERS); + +- maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE) * 1024; ++ maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE); + if (!maxsize) maxsize = UINT32_MAX; + + return 0; +-- +2.39.2 + + +From 2139228a5f43371258e1c460d0db509dcae7a4aa Mon Sep 17 00:00:00 2001 +From: ellie timoney +Date: Wed, 20 Jan 2021 13:25:16 +1100 +Subject: [PATCH 07/22] tools/config2header: allow "UNRELEASED" as imapoptions + version string + +--- + tools/config2header | 26 ++++++++++++++++++++++++-- + 1 file changed, 24 insertions(+), 2 deletions(-) + +diff --git a/tools/config2header b/tools/config2header +index 98418e147..4393e06e0 100755 +--- a/tools/config2header ++++ b/tools/config2header +@@ -128,6 +128,7 @@ EXPORTED struct imapopt_s imapopts[] = + EOF + ; + ++my $__warned_unreleased = 0; + sub parse_last_modified + { + my ($version) = @_; +@@ -141,6 +142,23 @@ sub parse_last_modified + + return sprintf "0x%2.2X%2.2X%2.2X00", $maj, $min, $rev; + } ++ elsif ($version eq 'UNRELEASED') { ++ if (not $__warned_unreleased) { ++ # This warning is to remind the release manager to replace ++ # "UNRELEASED" strings in lib/imapoptions with the version ++ # number that is about to be released. ++ # If you're not building a release, ignore it. :) ++ my $w = join q{ }, ++ "$0:", ++ -t STDERR ? "\033[33;1mwarning:\033[0m" : 'warning:', ++ 'build contains UNRELEASED config options'; ++ print STDERR "$w\n"; ++ ++ $__warned_unreleased ++; ++ } ++ ++ return "0xFFFFFFFF"; ++ } + else { + die "unparseable version: $version"; + } +@@ -301,15 +319,19 @@ while () { + # option is deprecated + if ($6 =~ m| + ,\s* # comma and optional whitespace +- (\"[^,]+\") # $1: 'deprecated since' version string ++ \"([^,]+)\" # $1: 'deprecated since' version string + \s* # optional whitespace + ( # $2: (unused) + ,\s* # comma and optional whitespace + \"(.+)\" # $3: 'in favour of' option name + )? + |x) { +- $depver = $1; ++ $depver = qq{"$1"}; + $newopt = $3 if $3; ++ ++ # we don't use the parsed value here, but we do still want to ++ # detect and report if "UNRELEASED" is seen ++ (undef) = parse_last_modified($1); + } else { + #chomp; + #print "rejected '$6'\n"; +-- +2.39.2 + + +From 75533e89b6fa79695b6f2cc0aec28add82660419 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:00:00 -0500 +Subject: [PATCH 08/22] imapd.c: UIDVALIDITY should be uint32_t and parse it as + such + +--- + imap/imapd.c | 10 +++------- + imap/index.h | 2 +- + 2 files changed, 4 insertions(+), 8 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index ce8c6f675..f3ccf2006 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -4256,15 +4256,11 @@ static void cmd_select(char *tag, char *cmd, char *name) + } + else if ((client_capa & CAPA_QRESYNC) && + !strcmp(arg.s, "QRESYNC")) { +- char *p; +- + if (c != ' ') goto badqresync; + c = prot_getc(imapd_in); + if (c != '(') goto badqresync; +- c = getastring(imapd_in, imapd_out, &arg); +- v->uidvalidity = strtoul(arg.s, &p, 10); +- if (*p || !v->uidvalidity || v->uidvalidity == ULONG_MAX) goto badqresync; +- if (c != ' ') goto badqresync; ++ c = getuint32(imapd_in, &v->uidvalidity); ++ if (c != ' ' || !v->uidvalidity) goto badqresync; + c = getmodseq(imapd_in, &v->modseq); + if (c == EOF) goto badqresync; + if (c == ' ') { +@@ -4404,7 +4400,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + prot_printf(backend_current->out, "%s %s {" SIZE_T_FMT "+}\r\n%s", + tag, cmd, strlen(name), name); + if (v->uidvalidity) { +- prot_printf(backend_current->out, " (QRESYNC (%lu " MODSEQ_FMT, ++ prot_printf(backend_current->out, " (QRESYNC (%u " MODSEQ_FMT, + v->uidvalidity, v->modseq); + if (v->sequence) { + prot_printf(backend_current->out, " %s", v->sequence); +diff --git a/imap/index.h b/imap/index.h +index bf8006d9b..5530ed61a 100644 +--- a/imap/index.h ++++ b/imap/index.h +@@ -72,7 +72,7 @@ extern unsigned client_capa; + struct message; + + struct vanished_params { +- unsigned long uidvalidity; ++ uint32_t uidvalidity; + modseq_t modseq; + const char *match_seq; + const char *match_uid; +-- +2.39.2 + + +From 9b6bc78da02d04a5fc639fd557c49922066409ab Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 08:13:05 -0500 +Subject: [PATCH 09/22] imapd.c: consolidate ID field-value parse error + response + +--- + imap/imapd.c | 17 +++++------------ + 1 file changed, 5 insertions(+), 12 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index f3ccf2006..7bbb99740 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3085,19 +3085,12 @@ static void cmd_id(char *tag) + + /* get field name */ + c = getstring(imapd_in, imapd_out, &field); +- if (c != ' ') { ++ if (c != ' ' || ++ /* get field value */ ++ (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || ++ (c != ' ' && c != ')')) { + prot_printf(imapd_out, +- "%s BAD Invalid/missing field name in Id\r\n", +- tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* get field value */ +- c = getnstring(imapd_in, imapd_out, &arg); +- if (c != ' ' && c != ')') { +- prot_printf(imapd_out, +- "%s BAD Invalid/missing value in Id\r\n", ++ "%s BAD Invalid field-value pair in Id\r\n", + tag); + eatline(imapd_in, c); + return; +-- +2.39.2 + + +From 64521529535738a933041e5b4c41a454df65b8dc Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:12:41 -0500 +Subject: [PATCH 10/22] imapd.c: response code in fatal() string MUST + immediately follow "* BYE" + +--- + imap/imapd.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 7bbb99740..c3b9b42ea 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1182,7 +1182,8 @@ EXPORTED void fatal(const char *s, int code) + } + recurse_code = code; + if (imapd_out) { +- prot_printf(imapd_out, "* BYE Fatal error: %s\r\n", s); ++ prot_printf(imapd_out, "* BYE %s%s\r\n", ++ *s == '[' /* resp-text-code */ ? "" : "Fatal error: ", s); + prot_flush(imapd_out); + } + if (stages.count) { +-- +2.39.2 + + +From afd1e5f4ceb98b6caf0ee01b83f61468ccb1ca96 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 11:00:19 -0500 +Subject: [PATCH 11/22] imapparse.c: include [TOOBIG] response code for + oversized word/qstring + +--- + imap/imapparse.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index b2852a357..e8e6f1b94 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_IOERR); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_IOERR); + } + } + +@@ -212,6 +212,9 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + return c; + } + buf_putc(buf, c); ++ if (config_maxword && buf_len(buf) > config_maxword) { ++ fatal("[TOOBIG] Word too long", EX_IOERR); ++ } + c = prot_getc(pin); + } + /* never gets here */ +-- +2.39.2 + + +From 23d153f65745bba51c70a92644cf0d5ea286539f Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 13:31:11 -0500 +Subject: [PATCH 12/22] imapparse.c: fatal() when a client violates LITERAL- + limit + +--- + imap/imap_err.et | 3 +++ + imap/imapparse.c | 7 +++++-- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 8d6ca361e..eab15f0b1 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,9 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++ec IMAP_LITERAL_MINUS_TOO_LARGE, ++ "[TOOBIG] Non-synchronizing literal size exceeds 4K" ++ + ec IMAP_USERFLAG_EXHAUSTED, + "Too many user flags in mailbox" + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index e8e6f1b94..80b29354c 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -153,8 +153,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + buf_reset(buf); + c = getint32(pin, &len); + if (c == '+') { +- // LITERAL- says maximum size is 4096! +- if (lminus && len > 4096) return EOF; ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && len > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +-- +2.39.2 + + +From f4827451e59bc04169ab462c3805f72e9dd134c4 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 12 Feb 2024 10:54:03 -0500 +Subject: [PATCH 13/22] Cleanup and document the use of prot_setisclient() + +Only IMAP(-like) clients send LITERAL+ syntax +--- + backup/backupd.c | 3 +-- + backup/lcb.c | 1 - + backup/lcb_read.c | 2 -- + backup/lcb_verify.c | 2 -- + cunit/getxstring.testc | 4 ---- + imap/append.c | 1 - + imap/backend.c | 2 -- + imap/cyr_dbtool.c | 1 - + imap/dlist.c | 5 ++++- + imap/imapd.c | 4 ++++ + imap/imapparse.c | 5 +++-- + imap/message.c | 3 --- + imap/mupdate.c | 3 +++ + imap/sync_server.c | 3 +-- + imap/sync_support.c | 4 ---- + lib/prot.h | 2 +- + 16 files changed, 17 insertions(+), 28 deletions(-) + +diff --git a/backup/backupd.c b/backup/backupd.c +index e34c8ab3a..275711bb3 100644 +--- a/backup/backupd.c ++++ b/backup/backupd.c +@@ -229,9 +229,8 @@ EXPORTED int service_main(int argc __attribute__((unused)), + backupd_in = prot_new(0, 0); + backupd_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow use of LITERAL+ */ + prot_setisclient(backupd_in, 1); +- prot_setisclient(backupd_out, 1); + + /* Find out name of client host */ + backupd_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/backup/lcb.c b/backup/lcb.c +index 53bf8dc21..8f0de3b8f 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -595,7 +595,6 @@ EXPORTED int backup_reindex(const char *name, + fprintf(out, "\nfound chunk at offset " OFF_T_FMT "\n\n", member_offset); + + struct protstream *member = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(member, 1); /* don't sync literals */ + + // FIXME stricter timestamp sequence checks + time_t member_start_ts = -1; +diff --git a/backup/lcb_read.c b/backup/lcb_read.c +index 201b59696..cc9410242 100644 +--- a/backup/lcb_read.c ++++ b/backup/lcb_read.c +@@ -113,7 +113,6 @@ EXPORTED int backup_read_message_data(struct backup *backup, + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + +@@ -203,7 +202,6 @@ EXPORTED int backup_prepare_message_upload(struct backup *backup, + if (!r) { + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); + int c; +- prot_setisclient(ps, 1); /* don't sync literals */ + c = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + ps = NULL; +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index 45a08bb66..a59984471 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -228,7 +228,6 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, vmrock->gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + + if (r == EOF) { +@@ -527,7 +526,6 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + goto done; + } + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + + struct buf cmd = BUF_INITIALIZER; + while (1) { +diff --git a/cunit/getxstring.testc b/cunit/getxstring.testc +index 3de9b8569..5946c5676 100644 +--- a/cunit/getxstring.testc ++++ b/cunit/getxstring.testc +@@ -72,9 +72,6 @@ static int tear_down(void) + + /* + * Run a single testcase. +- * +- * Note: prot_setisclient() turns off off literal synchronising so +- * we don't have to futz around with testing that. + */ + #define _TESTCASE_PRE(fut, input, retval, consumed) \ + do { \ +@@ -83,7 +80,6 @@ static int tear_down(void) + int c; \ + p = prot_readmap(input, sizeof(input)-1); \ + CU_ASSERT_PTR_NOT_NULL_FATAL(p); \ +- prot_setisclient(p, 1); \ + c = fut(p, NULL, &b); \ + CU_ASSERT_EQUAL(c, retval); \ + if (consumed >= 0) { \ +diff --git a/imap/append.c b/imap/append.c +index 55eb140b0..81526b09b 100644 +--- a/imap/append.c ++++ b/imap/append.c +@@ -436,7 +436,6 @@ static int callout_receive_reply(const char *callout, + } + + p = prot_new(fd, /*write*/0); +- prot_setisclient(p, 1); + + /* read and parse the reply as a dlist */ + c = dlist_parse(results, /*parsekeys*/0, /*isbackup*/0, p); +diff --git a/imap/backend.c b/imap/backend.c +index 08429c915..4d4af461e 100644 +--- a/imap/backend.c ++++ b/imap/backend.c +@@ -955,7 +955,6 @@ EXPORTED struct backend *backend_connect_pipe(int infd, int outfd, + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +@@ -1153,7 +1152,6 @@ EXPORTED struct backend *backend_connect(struct backend *ret_backend, const char + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +diff --git a/imap/cyr_dbtool.c b/imap/cyr_dbtool.c +index 56cb4dd92..3f750149c 100644 +--- a/imap/cyr_dbtool.c ++++ b/imap/cyr_dbtool.c +@@ -155,7 +155,6 @@ static void batch_commands(struct db *db) + int r = 0; + + prot_setisclient(in, 1); +- prot_setisclient(out, 1); + + while (1) { + buf_reset(&cmd); +diff --git a/imap/dlist.c b/imap/dlist.c +index 8a3a975b4..5d2782356 100644 +--- a/imap/dlist.c ++++ b/imap/dlist.c +@@ -1167,7 +1167,10 @@ EXPORTED int dlist_parsemap(struct dlist **dlp, int parsekey, int isbackup, + struct dlist *dl = NULL; + + stream = prot_readmap(base, len); +- prot_setisclient(stream, 1); /* don't sync literals */ ++ ++ /* Allow LITERAL+ - this is silly, but required to parse personal CALDATA */ ++ prot_setisclient(stream, 1); ++ + c = dlist_parse(&dl, parsekey, isbackup, stream); + prot_free(stream); + +diff --git a/imap/imapd.c b/imap/imapd.c +index c3b9b42ea..abf0e7153 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -953,6 +953,10 @@ int service_main(int argc __attribute__((unused)), + + imapd_in = prot_new(0, 0); + imapd_out = prot_new(1, 1); ++ ++ /* Allow LITERAL+ */ ++ prot_setisclient(imapd_in, 1); ++ + protgroup_insert(protin, imapd_in); + + /* Find out name of client host */ +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 80b29354c..14e6be226 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -149,10 +149,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + /* Literal */ +- isnowait = pin->isclient; ++ isnowait = !pin->isclient; + buf_reset(buf); + c = getint32(pin, &len); +- if (c == '+') { ++ ++ if (pin->isclient && c == '+') { + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +diff --git a/imap/message.c b/imap/message.c +index 30e2003d1..b9ccb1b0d 100644 +--- a/imap/message.c ++++ b/imap/message.c +@@ -3371,7 +3371,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read envelope response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_ENVELOPE), + cacheitem_size(record, CACHE_ENVELOPE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_envelope(strm, *body); + prot_free(strm); +@@ -3379,7 +3378,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read bodystructure response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_BODYSTRUCTURE), + cacheitem_size(record, CACHE_BODYSTRUCTURE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_body(strm, *body, NULL); + prot_free(strm); +@@ -4646,7 +4644,6 @@ static int message_parse_cbodystructure(message_t *m) + cacheitem_size(&m->record, CACHE_BODYSTRUCTURE)); + if (!prot) + return IMAP_MAILBOX_BADFORMAT; +- prot_setisclient(prot, 1); /* don't crash parsing literals */ + + m->body = xzmalloc(sizeof(struct body)); + r = parse_bodystructure_part(prot, m->body, NULL); +diff --git a/imap/mupdate.c b/imap/mupdate.c +index eef0f4b83..f6087e019 100644 +--- a/imap/mupdate.c ++++ b/imap/mupdate.c +@@ -249,6 +249,9 @@ static struct conn *conn_new(int fd) + C->pin = prot_new(C->fd, 0); + C->pout = prot_new(C->fd, 1); + ++ /* Allow LITERAL+ */ ++ prot_setisclient(C->pin, 1); ++ + prot_setflushonread(C->pin, C->pout); + prot_settimeout(C->pin, 180*60); + +diff --git a/imap/sync_server.c b/imap/sync_server.c +index 27f219636..f834cac5c 100644 +--- a/imap/sync_server.c ++++ b/imap/sync_server.c +@@ -316,9 +316,8 @@ int service_main(int argc __attribute__((unused)), + sync_in = prot_new(0, 0); + sync_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow LITERAL+ */ + prot_setisclient(sync_in, 1); +- prot_setisclient(sync_out, 1); + + /* Find out name of client host */ + sync_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/imap/sync_support.c b/imap/sync_support.c +index e7fe3cbdb..16595d50c 100644 +--- a/imap/sync_support.c ++++ b/imap/sync_support.c +@@ -7516,10 +7516,6 @@ connected: + if (timeout < 3) timeout = 3; + prot_settimeout(backend->in, timeout); + +- /* Force use of LITERAL+ so we don't need two way communications */ +- prot_setisclient(backend->in, 1); +- prot_setisclient(backend->out, 1); +- + sync_cs->backend = backend; + + return 0; +diff --git a/lib/prot.h b/lib/prot.h +index 98af5d160..89b0b0a2a 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -133,7 +133,7 @@ struct protstream { + int can_unget; + int bytes_in; + int bytes_out; +- int isclient; ++ int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ + prot_readcallback_t *readcallback_proc; +-- +2.39.2 + + +From 05a832afb53643944b49497ab658251366ce3828 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 22:48:58 -0400 +Subject: [PATCH 14/22] imapd.c: LITERAL- also applies to APPEND + +imap_err.et: add IMAP_MESSAGE_TOOBIG error message +--- + imap/imap_err.et | 4 ++++ + imap/imapd.c | 12 ++++++++++++ + 2 files changed, 16 insertions(+) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index eab15f0b1..77297a405 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,10 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++# Same as IMAP_MESSAGE_TOO_LARGE, but with TOOBIG response code ++ec IMAP_MESSAGE_TOOBIG, ++ "[TOOBIG] Message size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index abf0e7153..9ebd11d09 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3542,6 +3542,9 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + { + int isnowait = 0; + uint32_t num; ++ static int lminus = -1; ++ ++ if (lminus == -1) lminus = config_getswitch(IMAPOPT_LITERALMINUS); + + /* Check for literal8 */ + if (*p == '~') { +@@ -3562,6 +3565,15 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + } + + if (*p == '+') { ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && num > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } ++ if (num > maxsize) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ } + isnowait++; + p++; + } +-- +2.39.2 + + +From e5e874efe32e3afc90469c493f3a114e9bc30a54 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 14:52:22 -0500 +Subject: [PATCH 15/22] imapd.c: remove XSNIPPETS and XCONV* commands + +--- + cassandane/Cassandane/Cyrus/Conversations.pm | 71 -- + imap/imapd.c | 997 ------------------- + 2 files changed, 1068 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/Conversations.pm b/cassandane/Cassandane/Cyrus/Conversations.pm +index acafb3f74..e857f6d52 100755 +--- a/cassandane/Cassandane/Cyrus/Conversations.pm ++++ b/cassandane/Cassandane/Cyrus/Conversations.pm +@@ -706,77 +706,6 @@ sub bogus_test_replication_clash + $self->check_messages(\%exp, store => $replica_store); + } + +-sub test_xconvfetch +- :min_version_3_0 +-{ +- my ($self) = @_; +- my $store = $self->{store}; +- +- # check IMAP server has the XCONVERSATIONS capability +- $self->assert($store->get_client()->capability()->{xconversations}); +- +- xlog $self, "generating messages"; +- my $generator = Cassandane::ThreadedGenerator->new(); +- $store->write_begin(); +- while (my $msg = $generator->generate()) +- { +- $store->write_message($msg); +- } +- $store->write_end(); +- +- xlog $self, "reading the whole folder again to discover CIDs etc"; +- my %cids; +- my %uids; +- $store->read_begin(); +- while (my $msg = $store->read_message()) +- { +- my $uid = $msg->get_attribute('uid'); +- my $cid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- if (defined $cids{$cid}) +- { +- $self->assert_num_equals($threadid, $cids{$cid}); +- } +- else +- { +- $cids{$cid} = $threadid; +- xlog $self, "Found CID $cid"; +- } +- $self->assert_null($uids{$uid}); +- $uids{$uid} = 1; +- } +- $store->read_end(); +- +- xlog $self, "Using XCONVFETCH on each conversation"; +- foreach my $cid (keys %cids) +- { +- xlog $self, "XCONVFETCHing CID $cid"; +- +- my $result = $store->xconvfetch_begin($cid); +- $self->assert_not_null($result->{xconvmeta}); +- $self->assert_num_equals(1, scalar keys %{$result->{xconvmeta}}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}->{modseq}); +- while (my $msg = $store->xconvfetch_message()) +- { +- my $muid = $msg->get_attribute('uid'); +- my $mcid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- $self->assert_str_equals($cid, $mcid); +- $self->assert_num_equals($cids{$cid}, $threadid); +- $self->assert_num_equals(1, $uids{$muid}); +- $uids{$muid} |= 2; +- } +- $store->xconvfetch_end(); +- } +- +- xlog $self, "checking that all the UIDs in the folder were XCONVFETCHed"; +- foreach my $uid (keys %uids) +- { +- $self->assert_num_equals(3, $uids{$uid}); +- } +-} +- + # + # Test APPEND of a new composed draft message to the Drafts folder by + # the Fastmail webui, which sets the X-ME-Message-ID header to thread +diff --git a/imap/imapd.c b/imap/imapd.c +index 9ebd11d09..67e864d1a 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -438,14 +438,6 @@ static void cmd_idle(char* tag); + + static void cmd_starttls(char *tag, int imaps); + +-static void cmd_xconvsort(char *tag, int updates); +-static void cmd_xconvmultisort(char *tag); +-static void cmd_xconvmeta(const char *tag); +-static void cmd_xconvfetch(const char *tag); +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs); +-static void cmd_xsnippets(char *tag); + static void cmd_xstats(char *tag); + + static void cmd_xapplepushservice(const char *tag, +@@ -502,12 +494,8 @@ static int parse_metadata_store_data(const char *tag, + static int getlistselopts(char *tag, struct listargs *args); + static int getlistretopts(char *tag, struct listargs *args); + +-static int get_snippetargs(struct snippetargs **sap); +-static void free_snippetargs(struct snippetargs **sap); + static int getsortcriteria(char *tag, struct sortcrit **sortcrit); + static int getdatetime(time_t *date); +-static int parse_windowargs(const char *tag, struct windowargs **, int); +-static void free_windowargs(struct windowargs *wa); + + static void appendfieldlist(struct fieldlist **l, char *section, + strarray_t *fields, char *trail, +@@ -2273,32 +2261,6 @@ static void cmdloop(void) + + prometheus_increment(CYRUS_IMAP_XBACKUP_TOTAL); + } +- else if (!strcmp(cmd.s, "Xconvfetch")) { +- cmd_xconvfetch(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVFETCH_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvmultisort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvmultisort(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVMULTISORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvsort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 0); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVSORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvupdates")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 1); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVUPDATES_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xfer")) { + if (readonly) goto noreadonly; + int havepartition = 0; +@@ -2324,9 +2286,6 @@ static void cmdloop(void) + (havepartition ? arg3.s : NULL)); + /* XXX prometheus_increment(CYRUS_IMAP_XFER_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xconvmeta")) { +- cmd_xconvmeta(tag.s); +- } + else if (!strcmp(cmd.s, "Xlist")) { + struct listargs listargs; + +@@ -2359,13 +2318,6 @@ static void cmdloop(void) + cmd_xrunannotator(tag.s, arg1.s, usinguid); + /* XXX prometheus_increment(CYRUS_IMAP_XRUNANNOTATOR_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xsnippets")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xsnippets(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XSNIPPETS_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xstats")) { + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xstats(tag.s); +@@ -5165,8 +5117,6 @@ badannotation: + } + if (config_getswitch(IMAPOPT_CONVERSATIONS) + && (fa->fetchitems & (FETCH_MAILBOXIDS|FETCH_MAILBOXES))) { +- // annoyingly, this codepath COULD be called from xconv* commands, but it never is, +- // in reality, so it's safe leaving this as shared + int r = conversations_open_user(imapd_userid, 0/*shared*/, &fa->convstate); + if (r) { + syslog(LOG_WARNING, "error opening conversations for %s: %s", +@@ -5254,136 +5204,6 @@ static void cmd_fetch(char *tag, char *sequence, int usinguid) + fetchargs_fini(&fetchargs); + } + +-static void do_one_xconvmeta(struct conversations_state *state, +- conversation_id_t cid, +- conversation_t *conv, +- struct dlist *itemlist) +-{ +- struct dlist *item = dlist_newpklist(NULL, ""); +- struct dlist *fl; +- +- assert(conv); +- assert(itemlist); +- +- for (fl = itemlist->head; fl; fl = fl->next) { +- const char *key = dlist_cstring(fl); +- +- /* xxx - parse to a fetchitems? */ +- if (!strcasecmp(key, "MODSEQ")) +- dlist_setnum64(item, "MODSEQ", conv->modseq); +- else if (!strcasecmp(key, "EXISTS")) +- dlist_setnum32(item, "EXISTS", conv->exists); +- else if (!strcasecmp(key, "UNSEEN")) +- dlist_setnum32(item, "UNSEEN", conv->unseen); +- else if (!strcasecmp(key, "SIZE")) +- dlist_setnum32(item, "SIZE", conv->size); +- else if (!strcasecmp(key, "COUNT")) { +- struct dlist *flist = dlist_newlist(item, "COUNT"); +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *lookup = dlist_cstring(tmp); +- int i = strarray_find_case(state->counted_flags, lookup, 0); +- if (i >= 0) { +- dlist_setflag(flist, "FLAG", lookup); +- dlist_setnum32(flist, "COUNT", conv->counts[i]); +- } +- } +- } +- } +- else if (!strcasecmp(key, "SENDERS")) { +- conv_sender_t *sender; +- struct dlist *slist = dlist_newlist(item, "SENDERS"); +- for (sender = conv->senders; sender; sender = sender->next) { +- struct dlist *sli = dlist_newlist(slist, ""); +- dlist_setatom(sli, "NAME", sender->name); +- dlist_setatom(sli, "ROUTE", sender->route); +- dlist_setatom(sli, "MAILBOX", sender->mailbox); +- dlist_setatom(sli, "DOMAIN", sender->domain); +- } +- } +- /* XXX - maybe rename FOLDERCOUNTS or something? */ +- else if (!strcasecmp(key, "FOLDEREXISTS")) { +- struct dlist *flist = dlist_newlist(item, "FOLDEREXISTS"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "EXISTS", folder ? folder->exists : 0); +- } +- } +- } +- else if (!strcasecmp(key, "FOLDERUNSEEN")) { +- struct dlist *flist = dlist_newlist(item, "FOLDERUNSEEN"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "UNSEEN", folder ? folder->unseen : 0); +- } +- } +- } +- else { +- dlist_setatom(item, key, NULL); /* add a NIL response */ +- } +- } +- +- prot_printf(imapd_out, "* XCONVMETA %s ", conversation_id_encode(cid)); +- dlist_print(item, 0, imapd_out); +- prot_printf(imapd_out, "\r\n"); +- +- dlist_free(&item); +-} +- +-static void do_xconvmeta(const char *tag, +- struct conversations_state *state, +- struct dlist *cidlist, +- struct dlist *itemlist) +-{ +- conversation_id_t cid; +- struct dlist *dl; +- int r; +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- const char *cidstr = dlist_cstring(dl); +- conversation_t *conv = NULL; +- +- if (!conversation_id_decode(&cid, cidstr) || !cid) { +- prot_printf(imapd_out, "%s BAD Invalid CID %s\r\n", tag, cidstr); +- return; +- } +- +- r = conversation_load(state, cid, &conv); +- if (r) { +- prot_printf(imapd_out, "%s BAD Failed to read %s\r\n", tag, cidstr); +- conversation_free(conv); +- return; +- } +- +- if (conv && conv->exists) +- do_one_xconvmeta(state, cid, conv, itemlist); +- +- conversation_free(conv); +- } +- +- prot_printf(imapd_out, "%s OK Completed\r\n", tag); +-} +- + static int do_xbackup(const char *channel, + const ptrarray_t *list) + { +@@ -5527,261 +5347,6 @@ done: + } + } + +-/* +- * Parse and perform a XCONVMETA command. +- */ +-void cmd_xconvmeta(const char *tag) +-{ +- int r; +- int c = ' '; +- struct conversations_state *state = NULL; +- struct dlist *cidlist = NULL; +- struct dlist *itemlist = NULL; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVMETA ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Failed to parse CID list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&itemlist, 0, imapd_in); +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, "%s BAD Failed to parse item list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- // this one is OK, xconvmeta doesn't do an expunge +- r = conversations_open_user(imapd_userid, 1/*shared*/, &state); +- if (r) { +- prot_printf(imapd_out, "%s BAD failed to open db: %s\r\n", +- tag, error_message(r)); +- goto done; +- } +- +- do_xconvmeta(tag, state, cidlist, itemlist); +- +- done: +- +- dlist_free(&itemlist); +- dlist_free(&cidlist); +- conversations_commit(&state); +-} +- +-/* +- * Parse and perform a XCONVFETCH command. +- */ +-void cmd_xconvfetch(const char *tag) +-{ +- int c = ' '; +- struct fetchargs fetchargs; +- int r; +- clock_t start = clock(); +- modseq_t ifchangedsince = 0; +- char mytime[100]; +- struct dlist *cidlist = NULL; +- struct dlist *item; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVFETCH ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* local mailbox */ +- memset(&fetchargs, 0, sizeof(struct fetchargs)); +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') +- goto syntax_error; +- +- /* check CIDs */ +- for (item = cidlist->head; item; item = item->next) { +- if (!dlist_ishex64(item)) { +- prot_printf(imapd_out, "%s BAD Invalid CID\r\n", tag); +- eatline(imapd_in, c); +- goto freeargs; +- } +- } +- +- c = getmodseq(imapd_in, &ifchangedsince); +- if (c != ' ') +- goto syntax_error; +- +- r = parse_fetch_args(tag, "Xconvfetch", 0, &fetchargs); +- if (r) +- goto freeargs; +- fetchargs.fetchitems |= (FETCH_UIDVALIDITY|FETCH_FOLDER); +- fetchargs.namespace = &imapd_namespace; +- fetchargs.userid = imapd_userid; +- +- r = do_xconvfetch(cidlist, ifchangedsince, &fetchargs); +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- +- if (r) { +- prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, +- error_message(r), mytime); +- } else { +- prot_printf(imapd_out, "%s OK Completed (%s sec)\r\n", +- tag, mytime); +- } +- +-freeargs: +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +- return; +- +-syntax_error: +- prot_printf(imapd_out, "%s BAD Syntax error\r\n", tag); +- eatline(imapd_in, c); +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +-} +- +-static int xconvfetch_lookup(struct conversations_state *statep, +- conversation_id_t cid, +- modseq_t ifchangedsince, +- hash_table *wanted_cids, +- strarray_t *folder_list) +-{ +- const char *key = conversation_id_encode(cid); +- conversation_t *conv = NULL; +- conv_folder_t *folder; +- int r; +- +- r = conversation_load(statep, cid, &conv); +- if (r) return r; +- +- if (!conv) +- goto out; +- +- if (!conv->exists) +- goto out; +- +- /* output the metadata for this conversation */ +- { +- struct dlist *dl = dlist_newlist(NULL, ""); +- dlist_setatom(dl, "", "MODSEQ"); +- do_one_xconvmeta(statep, cid, conv, dl); +- dlist_free(&dl); +- } +- +- if (ifchangedsince >= conv->modseq) +- goto out; +- +- hash_insert(key, (void *)1, wanted_cids); +- +- for (folder = conv->folders; folder; folder = folder->next) { +- /* no contents */ +- if (!folder->exists) +- continue; +- +- /* finally, something worth looking at */ +- strarray_add(folder_list, strarray_nth(statep->folder_names, folder->number)); +- } +- +-out: +- conversation_free(conv); +- return 0; +-} +- +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs) +-{ +- struct conversations_state *state = NULL; +- int r = 0; +- struct index_state *index_state = NULL; +- struct dlist *dl; +- hash_table wanted_cids = HASH_TABLE_INITIALIZER; +- strarray_t folder_list = STRARRAY_INITIALIZER; +- struct index_init init; +- int i; +- +- // this one expunges each mailbox it enters, so we need to lock exclusively +- r = conversations_open_user(imapd_userid, 0/*shared*/, &state); +- if (r) goto out; +- +- construct_hash_table(&wanted_cids, 1024, 0); +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- r = xconvfetch_lookup(state, dlist_num(dl), ifchangedsince, +- &wanted_cids, &folder_list); +- if (r) goto out; +- } +- +- /* unchanged, woot */ +- if (!folder_list.count) +- goto out; +- +- fetchargs->cidhash = &wanted_cids; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- +- for (i = 0; i < folder_list.count; i++) { +- const char *mboxname = folder_list.data[i]; +- +- r = index_open(mboxname, &init, &index_state); +- if (r == IMAP_MAILBOX_NONEXISTENT) +- continue; +- if (r) +- goto out; +- +- index_checkflags(index_state, 0, 0); +- +- /* make sure \Deleted messages are expunged. Will also lock the +- * mailbox state and read any new information */ +- r = index_expunge(index_state, NULL, 1); +- +- if (!r) +- index_fetchresponses(index_state, NULL, /*usinguid*/1, +- fetchargs, NULL); +- +- index_close(&index_state); +- +- if (r) goto out; +- } +- +- r = 0; +- +-out: +- index_close(&index_state); +- conversations_commit(&state); +- free_hash_table(&wanted_cids, NULL); +- strarray_fini(&folder_list); +- return r; +-} +- + #undef PARSE_PARTIAL /* cleanup */ + + /* +@@ -6176,314 +5741,6 @@ error: + freesearchargs(searchargs); + } + +-/* +- * Perform a XCONVSORT or XCONVUPDATES command +- */ +-void cmd_xconvsort(char *tag, int updates) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct index_init init; +- struct index_state *oldstate = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvsort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, updates); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- if (updates) { +- /* in XCONVUPDATES, need to force a re-read from scratch into +- * a new index, because we ask for deleted messages */ +- +- oldstate = imapd_index; +- imapd_index = NULL; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- init.want_expunged = 1; +- +- r = index_open(index_mboxname(oldstate), &init, &imapd_index); +- if (r) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- index_checkflags(imapd_index, 0, 0); +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xconvsort\r\n", tag); +- goto error; +- } +- +- if (updates) +- r = index_convupdates(imapd_index, sortcrit, searchargs, windowargs); +- else +- r = index_convsort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (oldstate) { +- index_close(&imapd_index); +- imapd_index = oldstate; +- } +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVSORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- if (oldstate) { +- if (imapd_index) index_close(&imapd_index); +- imapd_index = oldstate; +- } +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-/* +- * Perform a XCONVMULTISORT command. This is like XCONVSORT but returns +- * search results from multiple folders. It still requires a selected +- * mailbox, for two reasons: +- * +- * a) it's a useful shorthand for choosing what the current +- * conversations scope is, and +- * +- * b) the code to parse a search program currently relies on a selected +- * mailbox. +- * +- * Unlike ESEARCH it doesn't take folder names for scope, instead the +- * search scope is implicitly the current conversation scope. This is +- * implemented more or less by accident because both the Sphinx index +- * and the conversations database are hardcoded to be per-user. +- */ +-static void cmd_xconvmultisort(char *tag) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvmultisort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvMultiSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, /*updates*/0); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to XconvMultiSort\r\n", tag); +- goto error; +- } +- +- r = index_convmultisort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVMULTISORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-static void cmd_xsnippets(char *tag) +-{ +- int c; +- struct searchargs *searchargs = NULL; +- struct snippetargs *snippetargs = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xsnippets"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- c = get_snippetargs(&snippetargs); +- if (c == EOF) { +- prot_printf(imapd_out, "%s BAD Syntax error in snippet arguments\r\n", tag); +- goto error; +- } +- if (c != ' ') { +- prot_printf(imapd_out, +- "%s BAD Unexpected arguments in Xsnippets\r\n", tag); +- goto error; +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xsnippets\r\n", tag); +- goto error; +- } +- +- r = index_snippets(imapd_index, snippetargs, searchargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- freesearchargs(searchargs); +- free_snippetargs(&snippetargs); +- return; +- +-error: +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- + static void cmd_xstats(char *tag) + { + int metric; +@@ -10800,81 +10057,6 @@ out_noprint: + if (uids) seqset_free(uids); + } + +-static void free_snippetargs(struct snippetargs **sap) +-{ +- while (*sap) { +- struct snippetargs *sa = *sap; +- *sap = sa->next; +- free(sa->mboxname); +- free(sa->uids.data); +- free(sa); +- } +-} +- +-static int get_snippetargs(struct snippetargs **sap) +-{ +- int c; +- struct snippetargs **prevp = sap; +- struct snippetargs *sa = NULL; +- struct buf arg = BUF_INITIALIZER; +- uint32_t uid; +- char *intname = NULL; +- +- c = prot_getc(imapd_in); +- if (c != '(') goto syntax_error; +- +- for (;;) { +- c = prot_getc(imapd_in); +- if (c == ')') break; +- if (c != '(') goto syntax_error; +- +- c = getastring(imapd_in, imapd_out, &arg); +- if (c != ' ') goto syntax_error; +- +- intname = mboxname_from_external(buf_cstring(&arg), &imapd_namespace, imapd_userid); +- +- /* allocate a new snippetargs */ +- sa = xzmalloc(sizeof(struct snippetargs)); +- sa->mboxname = xstrdup(intname); +- /* append to the list */ +- *prevp = sa; +- prevp = &sa->next; +- +- c = getuint32(imapd_in, &sa->uidvalidity); +- if (c != ' ') goto syntax_error; +- +- c = prot_getc(imapd_in); +- if (c != '(') break; +- for (;;) { +- c = getuint32(imapd_in, &uid); +- if (c != ' ' && c != ')') goto syntax_error; +- if (sa->uids.count + 1 > sa->uids.alloc) { +- sa->uids.alloc += 64; +- sa->uids.data = xrealloc(sa->uids.data, +- sizeof(uint32_t) * sa->uids.alloc); +- } +- sa->uids.data[sa->uids.count++] = uid; +- if (c == ')') break; +- } +- +- c = prot_getc(imapd_in); +- if (c != ')') goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') goto syntax_error; +- +-out: +- free(intname); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free_snippetargs(sap); +- c = EOF; +- goto out; +-} +- + static void cmd_dump(char *tag, char *name, int uid_start) + { + int r = 0; +@@ -12329,185 +11511,6 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + return EOF; + } + +-static int parse_windowargs(const char *tag, +- struct windowargs **wa, +- int updates) +-{ +- struct windowargs windowargs; +- struct buf arg = BUF_INITIALIZER; +- struct buf ext_folder = BUF_INITIALIZER; +- int c; +- +- memset(&windowargs, 0, sizeof(windowargs)); +- +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c != '(') { +- /* no window args at all */ +- prot_ungetc(c, imapd_in); +- goto out; +- } +- +- for (;;) +- { +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c == ')') +- break; /* end of window args */ +- +- prot_ungetc(c, imapd_in); +- c = getword(imapd_in, &arg); +- if (!arg.len) +- goto syntax_error; +- +- if (!strcasecmp(arg.s, "CONVERSATIONS")) +- windowargs.conversations = 1; +- else if (!strcasecmp(arg.s, "POSITION")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.position); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.position == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "ANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "MULTIANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getastring(imapd_in, imapd_out, &ext_folder); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "CHANGEDSINCE")) { +- if (!updates) +- goto syntax_error; +- windowargs.changedsince = 1; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getmodseq(imapd_in, &windowargs.modseq); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.uidnext); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- } else if (!strcasecmp(arg.s, "UPTO")) { +- if (!updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.upto); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- +- if (windowargs.upto == 0) +- goto syntax_error; +- } +- else +- goto syntax_error; +- +- if (c == ')') +- break; +- if (c != ' ') +- goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') +- goto syntax_error; +- +-out: +- /* these two are mutually exclusive */ +- if (windowargs.anchor && windowargs.position) +- goto syntax_error; +- /* changedsince is mandatory for XCONVUPDATES +- * and illegal for XCONVSORT */ +- if (!!updates != windowargs.changedsince) +- goto syntax_error; +- +- if (ext_folder.len) { +- windowargs.anchorfolder = mboxname_from_external(buf_cstring(&ext_folder), +- &imapd_namespace, +- imapd_userid); +- } +- +- *wa = xmemdup(&windowargs, sizeof(windowargs)); +- buf_free(&ext_folder); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free(windowargs.anchorfolder); +- buf_free(&ext_folder); +- prot_printf(imapd_out, "%s BAD Syntax error in window arguments\r\n", tag); +- if (c != EOF) prot_ungetc(c, imapd_in); +- return EOF; +-} +- +-static void free_windowargs(struct windowargs *wa) +-{ +- if (!wa) +- return; +- free(wa->anchorfolder); +- free(wa); +-} +- + /* + * Parse LIST selection options. + * The command has been parsed up to and including the opening '('. +-- +2.39.2 + + +From 9901ee2eae0d2c99ecb4e7057d7e3802fb5b64e4 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 26 Feb 2024 10:11:15 -0500 +Subject: [PATCH 16/22] imapd.c: add 'maxliteral' option + +--- + changes/next/imap_literal_limits | 19 +++ + imap/imap_err.et | 3 + + imap/imapd.c | 214 ++++++++++++++++++++++--------- + imap/imapparse.c | 69 +++++----- + lib/imapoptions | 12 +- + lib/libconfig.c | 3 + + lib/libconfig.h | 1 + + 7 files changed, 229 insertions(+), 92 deletions(-) + create mode 100644 changes/next/imap_literal_limits + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +new file mode 100644 +index 000000000..c7fc35bbc +--- /dev/null ++++ b/changes/next/imap_literal_limits +@@ -0,0 +1,19 @@ ++Description: ++ ++Adds a config option to limit the size of a single literal allowed ++by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++ ++ ++Config changes: ++ ++New 'maxliteral' option. ++ ++ ++Upgrade instructions: ++ ++None. ++ ++ ++GitHub issue: ++ ++None. +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 77297a405..e309c1203 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_LITERAL_TOO_LARGE, ++ "[TOOBIG] Literal size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 67e864d1a..28d0f299d 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1428,7 +1428,7 @@ static void cmdloop(void) + if (c == '\r') goto missingargs; + if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/0); +@@ -1441,7 +1441,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1468,7 +1468,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 0, 0); + +@@ -1480,7 +1480,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, NULL); + +@@ -1525,7 +1525,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Examine")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -1556,7 +1556,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Getacl")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getacl(tag.s, arg1.s); + +@@ -1581,7 +1581,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquota")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquota(tag.s, arg1.s); + +@@ -1590,7 +1590,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquotaroot")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquotaroot(tag.s, arg1.s); + +@@ -1715,7 +1715,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1731,7 +1731,7 @@ static void cmdloop(void) + /* delete a mailbox locally only */ + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 1, 1); + +@@ -1744,7 +1744,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Myrights")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_myrights(tag.s, arg1.s); + +@@ -1754,7 +1754,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if(c == EOF) goto missingargs; ++ if(c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_mupdatepush(tag.s, arg1.s); + +@@ -1770,7 +1770,7 @@ static void cmdloop(void) + if (c == '\r') goto missingargs; + if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/1); +@@ -1805,7 +1805,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + havepartition = 1; + c = getword(imapd_in, &arg3); +@@ -1878,7 +1878,7 @@ static void cmdloop(void) + if (c == ' ') { + have_mbox = 1; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + have_mech = 1; + c = getword(imapd_in, &arg2); +@@ -1950,7 +1950,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Select")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -1976,7 +1976,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 1); +@@ -1994,7 +1994,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg2); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg3); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, arg3.s); + +@@ -2195,7 +2195,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 0); +@@ -2342,7 +2342,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Xmeid")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xmeid(tag.s, arg1.s); + } +@@ -2354,7 +2354,7 @@ static void cmdloop(void) + + do { + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto aps_missingargs; ++ if (c <= EOF) goto aps_missingargs; + + if (!strcmp(arg1.s, "mailboxes")) { + c = prot_getc(imapd_in); +@@ -2366,7 +2366,7 @@ static void cmdloop(void) + prot_ungetc(c, imapd_in); + do { + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) break; ++ if (c <= EOF) break; + strarray_push(&applepushserviceargs.mailboxes, arg2.s); + } while (c == ' '); + } +@@ -2447,6 +2447,8 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + missingargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Missing required argument to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); +@@ -2459,11 +2461,18 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + extraargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); + continue; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in %s\r\n", ++ tag.s, error_message(IMAP_LITERAL_TOO_LARGE), cmd.s); ++ continue; ++ + badsequence: + prot_printf(imapd_out, + "%s BAD Invalid sequence in %s\r\n", tag.s, cmd.s); +@@ -2630,10 +2639,14 @@ static void cmd_login(char *tag, char *user) + + if (!IS_EOL(c, imapd_in)) { + buf_free(&passwdbuf); +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to LOGIN\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in LOGIN\r\n", tag, error_message(c)); ++ } else { ++ prot_printf(imapd_out, ++ "%s BAD Unexpected extra arguments to LOGIN\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3046,10 +3059,16 @@ static void cmd_id(char *tag) + /* get field value */ + (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || + (c != ' ' && c != ')')) { +- prot_printf(imapd_out, +- "%s BAD Invalid field-value pair in Id\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Id\r\n", ++ tag, error_message(c)); ++ } ++ else { ++ prot_printf(imapd_out, ++ "%s BAD Invalid field-value pair in Id\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3737,6 +3756,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + else if (!strcasecmp(arg.s, "URL")) { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ' && c != ')') { + *parseerr = "Missing URL in Append command"; + return IMAP_PROTOCOL_ERROR; +@@ -3935,8 +3955,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + c = parse_annotate_store_data(tag, + /*permessage_flag*/1, + &curstage->annotations); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto cleanup; + } + qdiffs[QUOTA_ANNOTSTORAGE] += sizeentryatts(curstage->annotations); +@@ -4061,6 +4080,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + if (r == IMAP_PROTOCOL_ERROR && parseerr) { + prot_printf(imapd_out, "%s BAD %s\r\n", tag, parseerr); ++ } else if (r == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); + } else if (r == IMAP_BADURL) { + prot_printf(imapd_out, "%s NO [BADURL \"%s\"] %s\r\n", + tag, url, parseerr); +@@ -4603,8 +4624,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + /*permessage_flag*/1, + &fa->entries, + &fa->attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + if (c != ')') { +@@ -4717,6 +4737,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -4947,6 +4972,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -5462,8 +5492,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + c = parse_annotate_store_data(tag, /*permessage_flag*/1, + &storeargs.entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeflags; + } + storeargs.namespace = &imapd_namespace; +@@ -5637,6 +5666,12 @@ static void cmd_search(char *tag, int usinguid) + return; + } + ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Search\r\n", tag, error_message(c)); ++ freesearchargs(searchargs); ++ return; ++ } ++ + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Search\r\n", tag); + eatline(imapd_in, c); +@@ -5812,16 +5847,19 @@ static void cmd_thread(char *tag, int usinguid) + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +- freesearchargs(searchargs); +- return; ++ goto done; ++ } ++ ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Thread\r\n", tag, error_message(c)); ++ goto done; + } + + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to Thread\r\n", tag); + eatline(imapd_in, c); +- freesearchargs(searchargs); +- return; ++ goto done; + } + + n = index_thread(imapd_index, alg, searchargs, usinguid); +@@ -5830,6 +5868,7 @@ static void cmd_thread(char *tag, int usinguid) + prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, + error_message(IMAP_OK_COMPLETED), n, mytime); + ++ done: + freesearchargs(searchargs); + return; + } +@@ -7333,8 +7372,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == '(') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistselopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + return; + } + } +@@ -7346,6 +7384,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + + /* Read in reference name */ + c = getastring(imapd_in, imapd_out, &reference); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF && !*reference.s) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: reference name\r\n", +@@ -7368,6 +7407,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); + if (c != ' ') break; +@@ -7383,6 +7423,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + else { + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: mailbox pattern\r\n", +@@ -7397,8 +7438,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == ' ') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistretopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + } +@@ -7417,6 +7457,10 @@ static void getlistargs(char *tag, struct listargs *listargs) + + return; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in List\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ + freeargs: + strarray_fini(&listargs->pat); + strarray_fini(&listargs->metaitems); +@@ -8816,6 +8860,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -8843,6 +8888,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -8865,6 +8911,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute(s)\r\n", tag); +@@ -8892,6 +8939,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -8904,8 +8952,13 @@ static int parse_annotate_fetch_data(const char *tag, + return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -8936,6 +8989,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -8962,6 +9016,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* single entry -- add it to the list */ + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -8980,8 +9035,13 @@ static int parse_metadata_string_or_list(const char *tag, + if (c == ' ' || c == '\r' || c == ')') return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9033,6 +9093,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -9053,6 +9114,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &attrib); + else + c = getqstring(imapd_in, imapd_out, &attrib); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -9066,6 +9128,7 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation value\r\n", tag); +@@ -9107,8 +9170,14 @@ static int parse_annotate_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9141,6 +9210,7 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9153,6 +9223,7 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata value\r\n", tag); +@@ -9193,7 +9264,7 @@ static int parse_metadata_store_data(const char *tag, + + if (c != ')') { + prot_printf(imapd_out, +- "%s BAD Missing close paren in annotation entry list \r\n", ++ "%s BAD Missing close paren in metadata entry list \r\n", + tag); + goto baddata; + } +@@ -9204,8 +9275,14 @@ static int parse_metadata_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + static void getannotation_response(const char *mboxname, +@@ -9384,8 +9461,7 @@ static void cmd_getannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_fetch_data(tag, /*permessage_flag*/0, &entries, &attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9619,8 +9695,10 @@ static void cmd_getmetadata(const char *tag) + while (nlists < 3) + { + c = parse_metadata_string_or_list(tag, &lists[nlists], &is_list[nlists]); ++ if (c <= EOF) goto freeargs; ++ + nlists++; +- if (c == '\r' || c == EOF) ++ if (c == '\r') + break; + } + +@@ -9777,8 +9855,7 @@ static void cmd_setannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_store_data(tag, 0, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9845,8 +9922,7 @@ static void cmd_setmetadata(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_metadata_store_data(tag, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9954,6 +10030,10 @@ static void cmd_xwarmup(const char *tag) + /* parse arguments: expect '('')' */ + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Xwarmup\r\n", tag, error_message(c)); ++ goto out_noprint; ++ } + if (c != ' ') { + syntax_error: + prot_printf(imapd_out, "%s BAD syntax error in %s\r\n", tag, cmd); +@@ -11423,9 +11503,11 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_ANNOTATION; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ') goto missingarg; + (*sortcrit)[n].args.annot.entry = xstrdup(criteria.s); + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + if (!strcmp(criteria.s, "value.shared")) + userid = ""; +@@ -11443,6 +11525,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11456,6 +11539,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASCONVFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11561,10 +11645,10 @@ static int getlistselopts(char *tag, struct listargs *args) + + strarray_t options = STRARRAY_INITIALIZER; + c = parse_metadata_string_or_list(tag, &options, NULL); ++ if (c <= EOF) return c; + parse_getmetadata_options(&options, &opts); + args->metaopts = opts; + strarray_fini(&options); +- if (c == EOF) return EOF; + } else { + prot_printf(imapd_out, + "%s BAD Invalid List selection option \"%s\"\r\n", +@@ -11592,7 +11676,7 @@ static int getlistselopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -11662,7 +11746,7 @@ static int getlistretopts(char *tag, struct listargs *args) + args->ret |= LIST_RET_METADATA; + /* outputs the error for us */ + c = parse_metadata_string_or_list(tag, &args->metaitems, NULL); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else { + prot_printf(imapd_out, +@@ -11683,7 +11767,7 @@ static int getlistretopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -12847,6 +12931,11 @@ static void cmd_urlfetch(char *tag) + else prot_ungetc(c, imapd_in); + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Urlfetch\r\n", ++ tag, error_message(c)); ++ return; ++ } + (void)prot_putc(' ', imapd_out); + prot_printstring(imapd_out, arg.s); + +@@ -13079,6 +13168,11 @@ static void cmd_genurlauth(char *tag) + char *intname = NULL; + + c = getastring(imapd_in, imapd_out, &arg1); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Genurlauth\r\n", ++ tag, error_message(c)); ++ return; ++ } + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing required argument to Genurlauth\r\n", +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 14e6be226..ddf5c2756 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -159,6 +159,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* Fail per RFC 7888, Section 4, choice 2 */ + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +@@ -181,6 +185,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + if (!isnowait) { ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ return IMAP_LITERAL_TOO_LARGE; ++ } ++ + prot_printf(pout, "+ go ahead\r\n"); + prot_flush(pout); + } +@@ -689,7 +697,7 @@ static int get_search_annotation(struct protstream *pin, + + /* parse the value */ + c = getbnstring(pin, pout, &value); +- if (c == EOF) ++ if (c <= EOF) + goto bad; + + sa = xzmalloc(sizeof(*sa)); +@@ -710,6 +718,7 @@ bad: + buf_free(&attrib); + buf_free(&value); + ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != EOF) prot_ungetc(c, pin); + return EOF; + } +@@ -842,7 +851,7 @@ static int get_search_criterion(struct protstream *pin, + do { + c = get_search_criterion(pin, pout, e, base); + } while (c == ' '); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ')') { + prot_printf(pout, "%s BAD Missing required close paren in Search command\r\n", + base->tag); +@@ -878,7 +887,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "annotation")) { /* RFC 5257 */ + struct searchannot *annot = NULL; + c = get_search_annotation(pin, pout, base, c, &annot); +- if (c == EOF) ++ if (c <= EOF) + goto badcri; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("annotation"); +@@ -899,23 +908,15 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "bcc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "body")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } +- else if (!strcmp(criteria.s, "fuzzy")) { +- if (c != ' ') goto missingarg; +- base->fuzzy_depth++; +- c = get_search_criterion(pin, pout, parent, base); +- base->fuzzy_depth--; +- if (c == EOF) return EOF; +- break; +- } + else goto badcri; + break; + +@@ -923,7 +924,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "cc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (hasconv && !strcmp(criteria.s, "convflag")) { /* nonstandard */ +@@ -986,7 +987,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "deliveredto")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -996,7 +997,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "emailid")) { /* draft-gondwana-imap-uniqueid */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1009,7 +1010,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "folder")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("folder"); + e->value.s = mboxname_from_external(arg.s, base->namespace, base->userid); +@@ -1017,7 +1018,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "from")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "fuzzy")) { /* RFC 6203 */ +@@ -1025,7 +1026,7 @@ static int get_search_criterion(struct protstream *pin, + base->fuzzy_depth++; + c = get_search_criterion(pin, pout, parent, base); + base->fuzzy_depth--; +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else goto badcri; + break; +@@ -1036,7 +1037,7 @@ static int get_search_criterion(struct protstream *pin, + c = getastring(pin, pout, &arg); + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg2); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find_field(arg.s); +@@ -1098,7 +1099,7 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_NOT); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "new")) { /* RFC 3501 */ + e = search_expr_new(parent, SEOP_AND); +@@ -1113,10 +1114,10 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_OR); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ' ') goto missingarg; + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "old")) { /* RFC 3501 */ + indexflag_match(parent, MESSAGE_RECENT, /*not*/1); +@@ -1235,6 +1236,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spamabove")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_GE); + e->attr = search_attr_find("spamscore"); +@@ -1243,6 +1245,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spambelow")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_LT); + e->attr = search_attr_find("spamscore"); +@@ -1251,7 +1254,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "subject")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1261,19 +1264,19 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "to")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "text")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "threadid")) { /* draft-gondwana-imap-uniqueid */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1323,25 +1326,25 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "xattachmentname")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentname", base); + } + else if (!strcmp(criteria.s, "xattachmentbody")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentbody", base); + } + else if (!strcmp(criteria.s, "xlistid")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "listid", base); + } + else if (!strcmp(criteria.s, "xcontenttype")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "contenttype", base); + } + else goto badcri; +@@ -1372,6 +1375,8 @@ static int get_search_criterion(struct protstream *pin, + + default: + badcri: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Invalid Search criteria\r\n", base->tag); + if (c != EOF) prot_ungetc(c, pin); + return EOF; +@@ -1384,6 +1389,8 @@ static int get_search_criterion(struct protstream *pin, + return c; + + missingarg: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Missing required argument to Search %s\r\n", + base->tag, criteria.s); + if (c != EOF) prot_ungetc(c, pin); +diff --git a/lib/imapoptions b/lib/imapoptions +index 5cb8ef7b8..833245069 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1571,11 +1571,21 @@ Blank lines and lines beginning with ``#'' are ignored. + messages larger than \fImaxmessagesize\fR bytes. If set to 0, this + will allow messages of any size (the default). */ + ++{ "maxliteral", 131072, INT, "UNRELEASED" } ++/* Maximum size in bytes of a single literal allowed by the IMAP parser. ++.PP ++ Literals used for message [part] data in APPEND are only limited by ++ the 'maxmessagesize' option. ++.PP ++ If the 'literalminus' option is enabled, non-synchonizing literals ++ will be limited to the lesser of 4K and either 'maxliteral' or ++ 'maxmessagesize', depending on the use-case. */ ++ + { "maxquoted", 131072, INT, "2.3.17" } + /* Maximum size of a single quoted string for the parser. Default 128k */ + + { "maxword", 131072, INT, "2.3.17" } +-/* Maximum size of a single word for the parser. Default 128k */ ++/* Maximum size of a single word allowed by the IMAP parser. Default 128k */ + + { "mboxkey_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" } + /* The cyrusdb backend to use for mailbox keys. */ +diff --git a/lib/libconfig.c b/lib/libconfig.c +index 860c34863..de9591b7d 100644 +--- a/lib/libconfig.c ++++ b/lib/libconfig.c +@@ -84,6 +84,7 @@ EXPORTED int config_auditlog; + EXPORTED int config_iolog; + EXPORTED unsigned config_maxword; + EXPORTED unsigned config_maxquoted; ++EXPORTED unsigned config_maxliteral; + EXPORTED int config_qosmarking; + EXPORTED int config_debug; + +@@ -473,6 +474,7 @@ EXPORTED void config_reset(void) + config_defdomain = NULL; + config_auditlog = 0; + config_serverinfo = 0; ++ config_maxliteral = 0; + config_maxquoted = 0; + config_maxword = 0; + config_qosmarking = 0; +@@ -659,6 +661,7 @@ EXPORTED void config_read(const char *alt_config, const int config_need_data) + config_serverinfo = config_getenum(IMAPOPT_SERVERINFO); + + /* set some limits */ ++ config_maxliteral = config_getint(IMAPOPT_MAXLITERAL); + config_maxquoted = config_getint(IMAPOPT_MAXQUOTED); + config_maxword = config_getint(IMAPOPT_MAXWORD); + +diff --git a/lib/libconfig.h b/lib/libconfig.h +index dd9eee2e3..8c8fed54a 100644 +--- a/lib/libconfig.h ++++ b/lib/libconfig.h +@@ -89,6 +89,7 @@ extern enum enum_value config_virtdomains; + extern enum enum_value config_mupdate_config; + extern int config_auditlog; + extern int config_iolog; ++extern unsigned config_maxliteral; + extern unsigned config_maxquoted; + extern unsigned config_maxword; + extern int config_qosmarking; +-- +2.39.2 + + +From e32406ce63ce69161a37d59507e978b3ae6710fb Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:18:52 -0500 +Subject: [PATCH 17/22] prot.h: change bytes_in/out to uint64_t (for + long-running imapd) + +--- + backup/lcb.c | 4 ++-- + backup/lcb_compact.c | 4 ++-- + backup/lcb_verify.c | 8 ++++---- + imap/httpd.c | 14 ++++++++------ + imap/imapd.c | 18 ++++++++++-------- + imap/pop3d.c | 18 ++++++++++-------- + lib/prot.h | 8 ++++---- + 7 files changed, 40 insertions(+), 34 deletions(-) + +diff --git a/backup/lcb.c b/backup/lcb.c +index 8f0de3b8f..cb3c4a595 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -609,11 +609,11 @@ EXPORTED int backup_reindex(const char *name, + const char *error = prot_error(member); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + name, member_offset, prot_bytes_in(member), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + member_offset, prot_bytes_in(member), error); + + r = IMAP_IOERROR; +diff --git a/backup/lcb_compact.c b/backup/lcb_compact.c +index 6a6cb5282..3f8693ef2 100644 +--- a/backup/lcb_compact.c ++++ b/backup/lcb_compact.c +@@ -521,11 +521,11 @@ EXPORTED int backup_compact(const char *name, + const char *error = prot_error(in); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + name, chunk->offset, prot_bytes_in(in), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + chunk->offset, prot_bytes_in(in), error); + + /* chunk is corrupt, discard the rest of it and get on with +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index a59984471..1fbd7bca5 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -234,10 +234,10 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, message->id, message->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + message->id, message->offset, prot_bytes_in(ps), error); + } + prot_free(ps); +@@ -539,10 +539,10 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, chunk->id, chunk->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + chunk->id, chunk->offset, prot_bytes_in(ps), error); + r = EOF; + } +diff --git a/imap/httpd.c b/imap/httpd.c +index 069038a95..851bcc643 100644 +--- a/imap/httpd.c ++++ b/imap/httpd.c +@@ -584,8 +584,8 @@ struct namespace_t *http_namespaces[] = { + static void httpd_reset(struct http_connection *conn) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + /* Do any namespace specific cleanup */ + for (i = 0; http_namespaces[i]; i++) { +@@ -627,7 +627,8 @@ static void httpd_reset(struct http_connection *conn) + + if (config_auditlog) { + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + } + +@@ -998,8 +999,8 @@ void usage(void) + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1058,7 +1059,8 @@ void shut_down(int code) + + if (config_auditlog) + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + + #ifdef HAVE_SSL +diff --git a/imap/imapd.c b/imap/imapd.c +index 28d0f299d..0a6574c19 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -729,8 +729,8 @@ static int mlookup(const char *tag, const char *ext_name, + static void imapd_reset(void) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + proc_cleanup(); + +@@ -777,8 +777,9 @@ static void imapd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + imapd_in = imapd_out = NULL; + +@@ -1072,8 +1073,8 @@ void shut_down(int code) __attribute__((noreturn)); + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1138,8 +1139,9 @@ void shut_down(int code) + : CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_OK); + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + if (protin) protgroup_free(protin); + +diff --git a/imap/pop3d.c b/imap/pop3d.c +index 167e5c75e..349f4bf92 100644 +--- a/imap/pop3d.c ++++ b/imap/pop3d.c +@@ -325,8 +325,8 @@ static struct sasl_callback mysasl_cb[] = { + + static void popd_reset(void) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + proc_cleanup(); + +@@ -361,8 +361,9 @@ static void popd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + popd_in = popd_out = NULL; + +@@ -598,8 +599,8 @@ static void usage(void) + */ + void shut_down(int code) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -644,8 +645,9 @@ void shut_down(int code) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + #ifdef HAVE_SSL + tls_shutdown_serverengine(); +diff --git a/lib/prot.h b/lib/prot.h +index 89b0b0a2a..94b22fad8 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -131,8 +131,8 @@ struct protstream { + struct buf *writetobuf; + + int can_unget; +- int bytes_in; +- int bytes_out; ++ uint64_t bytes_in; ++ uint64_t bytes_out; + int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ +@@ -224,8 +224,8 @@ extern int prot_free(struct protstream *s); + extern int prot_setlog(struct protstream *s, int fd); + + /* Get traffic counts */ +-extern int prot_bytes_in(struct protstream *s); +-extern int prot_bytes_out(struct protstream *s); ++extern uint64_t prot_bytes_in(struct protstream *s); ++extern uint64_t prot_bytes_out(struct protstream *s); + #define prot_bytes_in(s) ((s)->bytes_in) + #define prot_bytes_out(s) ((s)->bytes_out) + +-- +2.39.2 + + +From 0c8af18b9ad12bd59556f033e25ba8f2828bc969 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:35:44 -0500 +Subject: [PATCH 18/22] imapd.c: rename 'maxsize' to 'maxmsgsize' + +--- + imap/imapd.c | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 0a6574c19..5ef0ce778 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -135,7 +135,7 @@ static int imaps = 0; + static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; +-static size_t maxsize = 0; ++static size_t maxmsgsize = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -898,8 +898,8 @@ int service_init(int argc, char **argv, char **envp) + + prometheus_increment(CYRUS_IMAP_READY_LISTENERS); + +- maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE); +- if (!maxsize) maxsize = UINT32_MAX; ++ maxmsgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); ++ if (!maxmsgsize) maxmsgsize = UINT32_MAX; + + return 0; + } +@@ -3985,13 +3985,13 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxsize, &size, ++ r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From 046bf9a6ec7516cb728d9ef003029fc853c2c02f Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 15:08:42 -0500 +Subject: [PATCH 19/22] imapd.c: limit the total size of IMAP command arguments + +Only concerned with commands that can have an unlimited +number of arguments. +--- + changes/next/imap_literal_limits | 7 ++- + imap/imap_err.et | 3 + + imap/imapd.c | 97 +++++++++++++++++++++++++++++--- + imap/imapd.h | 1 + + imap/imapparse.c | 10 ++++ + lib/imapoptions | 5 ++ + 6 files changed, 112 insertions(+), 11 deletions(-) + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +index c7fc35bbc..f1ea34a0b 100644 +--- a/changes/next/imap_literal_limits ++++ b/changes/next/imap_literal_limits +@@ -1,12 +1,13 @@ + Description: + +-Adds a config option to limit the size of a single literal allowed +-by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++Adds config options to limit the size of a single literal allowed ++by the IMAP parser and to limit the total size of IMAP command arguments. ++Also properly applies LITERAL- to IMAP APPEND. + + + Config changes: + +-New 'maxliteral' option. ++New 'maxliteral' and 'maxargssize' options. + + + Upgrade instructions: +diff --git a/imap/imap_err.et b/imap/imap_err.et +index e309c1203..29ba44953 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_ARGS_TOO_LARGE, ++ "[TOOBIG] Command arguments total size exceeds fixed limit" ++ + ec IMAP_LITERAL_TOO_LARGE, + "[TOOBIG] Literal size exceeds fixed limit" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 5ef0ce778..e9451d35e 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -136,6 +136,8 @@ static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; + static size_t maxmsgsize = 0; ++static int64_t maxargssize = 0; ++static uint64_t maxargssize_mark = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -901,6 +903,9 @@ int service_init(int argc, char **argv, char **envp) + maxmsgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); + if (!maxmsgsize) maxmsgsize = UINT32_MAX; + ++ maxargssize = config_getint(IMAPOPT_MAXARGSSIZE); ++ if (maxargssize <= 0) maxargssize = UINT32_MAX; ++ + return 0; + } + +@@ -1345,6 +1350,9 @@ static void cmdloop(void) + allowed when not logged in */ + if (!imapd_userid && !strchr("AELNCIS", cmd.s[0])) goto nologin; + ++ /* Set limit on the total number of bytes allowed for arguments */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + maxargssize; ++ + /* Start command timer */ + cmdtime_starttimer(); + +@@ -3902,6 +3910,9 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + curstage = xzmalloc(sizeof(*curstage)); + ptrarray_push(&stages, curstage); + ++ /* Set limit on the total number of bytes allowed for mailbox+append-opts */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + (maxargssize - strlen(name)); ++ + /* now parsing "append-opts" in the ABNF */ + + /* Parse flags */ +@@ -3910,6 +3921,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + strarray_init(&curstage->flags); + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4017,15 +4030,23 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + } + + done: +- if (r) { +- eatline(imapd_in, c); +- } else { ++ switch (r) { ++ case IMAP_ZERO_LENGTH_LITERAL: ++ case IMAP_MESSAGE_TOO_LARGE: ++ break; ++ ++ case 0: + /* we should be looking at the end of the line */ +- if (!IS_EOL(c, imapd_in)) { +- parseerr = "junk after literal"; +- r = IMAP_PROTOCOL_ERROR; +- eatline(imapd_in, c); +- } ++ if (IS_EOL(c, imapd_in)) break; ++ ++ parseerr = "junk after literal"; ++ r = IMAP_PROTOCOL_ERROR; ++ ++ GCC_FALLTHROUGH ++ ++ default: ++ eatline(imapd_in, c); ++ break; + } + + /* Append from the stage(s) */ +@@ -4235,6 +4256,9 @@ static void cmd_select(char *tag, char *cmd, char *name) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto badlist; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { + client_capa |= CAPA_CONDSTORE; +@@ -4609,6 +4633,9 @@ static int parse_fetch_args(const char *tag, const char *cmd, + c = getword(imapd_in, &fetchatt); + } + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + switch (fetchatt.s[0]) { + case 'A': +@@ -4739,6 +4766,8 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5073,6 +5102,9 @@ badannotation: + } + do { + c = getword(imapd_in, &fetchatt); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { + if (c != ' ') { +@@ -5419,6 +5451,9 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + do { + c = getword(imapd_in, &storemod); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { + if (c != ' ') { +@@ -5511,6 +5546,8 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + for (;;) { + c = getword(imapd_in, &flagname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -5639,6 +5676,8 @@ static void cmd_search(char *tag, int usinguid) + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); + ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + /* Set FUZZY search according to config and quirks */ + static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always"; + char *inbox = mboxname_user_mbox(imapd_userid, NULL); +@@ -5730,6 +5769,9 @@ static void cmd_sort(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + if (imapd_id.quirks & QUIRK_SEARCHFUZZY) + searchargs->fuzzy_depth++; + +@@ -5846,6 +5888,9 @@ static void cmd_thread(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +@@ -7409,6 +7454,8 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8252,6 +8299,9 @@ void cmd_setquota(const char *tag, const char *quotaroot) + newquotas[res] = limit; + if (c == ')') break; + else if (c != ' ') goto badlist; ++ ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8404,6 +8454,9 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto bad; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { + statusitems |= STATUS_MESSAGES; +@@ -8786,6 +8839,9 @@ static int parsecreateargs(struct dlist **extargs) + /* new style RFC 4466 arguments */ + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + name = ucase(arg.s); + if (c != ' ') goto fail; + c = prot_getc(imapd_in); +@@ -8794,6 +8850,9 @@ static int parsecreateargs(struct dlist **extargs) + sub = dlist_newlist(res, name); + do { + c = getword(imapd_in, &val); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + dlist_setatom(sub, name, val.s); + } while (c == ' '); + if (c != ')') goto fail; +@@ -8862,6 +8921,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -8913,6 +8974,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -8991,6 +9054,8 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9095,6 +9160,8 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9137,6 +9204,9 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); + +@@ -9212,6 +9282,8 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9225,6 +9297,8 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11466,6 +11540,9 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + nsort = 0; + n = 0; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ + nsort += SORTGROWSIZE; +@@ -11615,6 +11692,8 @@ static int getlistselopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -11719,6 +11798,8 @@ static int getlistretopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapd.h b/imap/imapd.h +index a6724af89..e6537dd95 100644 +--- a/imap/imapd.h ++++ b/imap/imapd.h +@@ -222,6 +222,7 @@ struct searchargs { + int state; + /* used only during parsing */ + int fuzzy_depth; ++ uint64_t maxargssize_mark; + + /* For ESEARCH & XCONVMULTISORT */ + const char *tag; +diff --git a/imap/imapparse.c b/imap/imapparse.c +index ddf5c2756..1fdb8b312 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -634,6 +634,11 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + goto bad; + } + ++ if (searchargs->maxargssize_mark && ++ prot_bytes_in(pin) > searchargs->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + } while (c == ' '); + + /* RFC 4731: +@@ -1382,6 +1387,11 @@ static int get_search_criterion(struct protstream *pin, + return EOF; + } + ++ if (base->maxargssize_mark && ++ prot_bytes_in(pin) > base->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + if (!keep_charset) + base->state &= ~GETSEARCH_CHARSET_KEYWORD; + base->state &= ~GETSEARCH_RETURN; +diff --git a/lib/imapoptions b/lib/imapoptions +index 833245069..75950e5c4 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1566,6 +1566,11 @@ Blank lines and lines beginning with ``#'' are ignored. + /* Maximum number of logged in sessions allowed per user, + zero means no limit */ + ++{ "maxargssize", 0, INT, "UNRELEASED" } ++/* Maximum total size of arguments to an IMAP command that will be ++ accepted by Cyrus. ++ Commands with arguments that exceed this limit will be rejected. ++ + { "maxmessagesize", 0, INT, "2.3.17" } + /* Maximum incoming LMTP message size. If non-zero, lmtpd will reject + messages larger than \fImaxmessagesize\fR bytes. If set to 0, this +-- +2.39.2 + + +From 8d72de770eb354e52a659a1264809f773ff8fcf1 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Tue, 12 Mar 2024 23:16:30 -0400 +Subject: [PATCH 20/22] Add IMAPLimits.pm + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 518 ++++++++++++++++++++++ + cassandane/Cassandane/IMAPMessageStore.pm | 8 +- + 2 files changed, 523 insertions(+), 3 deletions(-) + create mode 100644 cassandane/Cassandane/Cyrus/IMAPLimits.pm + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +new file mode 100644 +index 000000000..2275c5cf7 +--- /dev/null ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -0,0 +1,518 @@ ++#!/usr/bin/perl ++# ++# Copyright (c) 2011-2023 FastMail Pty Ltd. 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. The name "Fastmail Pty Ltd" must not be used to ++# endorse or promote products derived from this software without ++# prior written permission. For permission or any legal ++# details, please contact ++# FastMail Pty Ltd ++# PO Box 234 ++# Collins St West 8007 ++# Victoria ++# Australia ++# ++# 4. Redistributions of any form whatsoever must retain the following ++# acknowledgment: ++# "This product includes software developed by Fastmail Pty. Ltd." ++# ++# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, ++# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO ++# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT ++# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF ++# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER ++# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE ++# OF THIS SOFTWARE. ++# ++ ++package Cassandane::Cyrus::IMAPLimits; ++use strict; ++use warnings; ++use Mail::JMAPTalk 0.13; ++use Data::Dumper; ++ ++use lib '.'; ++use base qw(Cassandane::Cyrus::TestCase); ++use Cassandane::Util::Log; ++ ++my $email = < ++ ++Body ++EOF ++ ++$email =~ s/\r?\n/\r\n/gs; ++ ++my $toobig_email = $email . "X" x 100; ++ ++sub assert_bye_toobig ++{ ++ my ($self, $store) = @_; ++ ++ $store = $self->{store} if (!defined $store); ++ ++ # We want to override Mail::IMAPTalk's builtin handling of the BYE ++ # untagged response, as it will 'die' immediately without parsing ++ # the remainder of the line and especially without picking out the ++ # [TOOBIG] response code that we want to see. ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ bye => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ # Check that we got a BYE [TOOBIG] response ++ $store->idle_response($handlers, 1); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub assert_cmd_bye_toobig ++{ ++ my $self = shift; ++ my $cmd = shift; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->enable('qresync'); # IMAPTalk requires lower-case ++ $talk->select('INBOX'); ++ ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_bye_toobig(); ++} ++ ++sub assert_cmd_no_toobig ++{ ++ my $self = shift; ++ my $talk = shift; ++ my $cmd = shift; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ # Pick out the [TOOBIG] response code ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ $talk->_imap_cmd($cmd, 0, $handlers, @_); ++ ++ # Check that we got a NO [TOOBIG] response ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub new ++{ ++ my $class = shift; ++ ++ my $config = Cassandane::Config->default()->clone(); ++ $config->set(maxword => 25); ++ $config->set(maxquoted => 25); ++ $config->set(maxliteral => 25); ++ $config->set(literalminus => 1); ++ $config->set(maxargssize => 45); ++ $config->set(maxmessagesize => 100); ++ $config->set(event_groups => "message mailbox applepushservice"); ++ $config->set(aps_topic => "mail"); ++ ++ return $class->SUPER::new({ ++ adminstore => 1, ++ config => $config, ++ services => ['imap'], ++ }, @_); ++} ++ ++sub set_up ++{ ++ my ($self) = @_; ++ $self->SUPER::set_up(); ++} ++ ++sub tear_down ++{ ++ my ($self) = @_; ++ $self->SUPER::tear_down(); ++} ++ ++sub test_maxword ++{ ++ my ($self) = @_; ++ ++ # Oversized command name ++ $self->assert_cmd_bye_toobig("X" x 26); ++} ++ ++sub test_maxword_astring ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', "X" x 26); ++} ++ ++sub test_maxquoted ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', { Quote => "X" x 26 }); ++} ++ ++sub test_maxliteral_nosync ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " SELECT {26+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_maxliteral_sync ++{ ++ my ($self) = @_; ++ ++ # Unlike oversized non-sync literals which fatal() in one central location, ++ # oversized sync literals fail with a NO response in multiple places, ++ # so we test as many of those places as possible. ++ # Having said that, arguments parsed in cmdloop() or in get_search_criterion() ++ # are mostly handled centrally. ++ ++ # Authenticated State ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'SELECT', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Literal => "X" x 26 } ]); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Quote => 'foo' }, { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Quote => '' }, { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LISTRIGHTS', ++ 'INBOX', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETACL', ++ 'INBOX', 'anyone', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ '/comment', { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ 'FOO', { Literal => "X" x 26 }); ++ ++ # Selected State ++ $talk->select('INBOX'); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'ANNOTATION', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'BODY[HEADER.FIELDS', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'RFC822.HEADER.LINES', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Quote => 'value' }, ++ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', 'SUBJECT', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', ++ 'value', { Literal => "X" x 26 } ); ++} ++ ++sub test_maxargssize_append_flags ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ [ "X" x 25, "X" x 25 ], { Literal => $email } ); ++} ++ ++sub test_maxargssize_append_annot ++{ ++ my ($self) = @_; ++ ++ # Use MULTIAPPEND, fail the second ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ { Literal => $email }, ++ 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ], ++ { Literal => $email } ); ++} ++ ++sub test_maxargssize_create ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', "X" x 25, [ "X" x 25 ] ); ++} ++ ++sub test_maxargssize_create_ext ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', ++ "X" x 5, [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_fetch ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY', 'ENVELOPE', 'FLAGS', ++ 'INTERNALDATE', 'RFC822.SIZE' ]); ++} ++ ++sub test_maxargssize_fetch_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ [ "X" x 25, "X" x 25 ] ], "X" x 5 ] ); ++} ++ ++sub test_maxargssize_fetch_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ] ); ++} ++ ++sub test_maxargssize_fetch_headers ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY[HEADER.FIELDS', [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_getmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('GETMETADATA', 'INBOX', [ "X" x 25, "X" x 25 ] ); ++} ++ ++sub test_maxargssize_list_multi ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', { Quote => '' }, [ "X" x 25, "X" x 25 ]); ++} ++ ++sub test_maxargssize_list_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ [ 'SUBSCRIBED', 'REMOTE', ++ 'RECURSIVEMATCH', 'SPECIAL-USE' ], ++ { Quote => '' }, '*'); ++} ++ ++sub test_maxargssize_list_return ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ { Quote => '' }, '*', 'RETURN', ++ [ 'SUBSCRIBED', 'CHILDREN', ++ 'MYRIGHTS', 'SPECIAL-USE' ] ); ++} ++ ++sub test_maxargssize_search ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SEARCH', ++ 'TEXT', "X" x 25, 'TEXT', { Quote => "X" x 25 } ); ++} ++ ++sub test_maxargssize_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SELECT', 'INBOX', ++ [ 'QRESYNC', [ '1234567890', '1234567890' ], ++ 'ANNOTATE' ] ); ++} ++ ++sub test_maxargssize_setmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ "X" x 25, { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setmetadata2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ '/shared', { Quote => "X" x 25 }, ++ '/shared', { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setquota ++{ ++ my ($self) = @_; ++ ++ my $store = $self->{adminstore}; ++ my $talk = $store->get_client(); ++ ++ $talk->_send_cmd('SETQUOTA', 'user.cassandane', ++ [ 'STORAGE', '1234567890', ++ 'MESSAGE', '1234567890', ++ 'MAILBOX', '1234567890' ] ); ++ $self->assert_bye_toobig($store); ++} ++ ++sub test_maxargssize_sort ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SORT', ++ [ 'ARRIVAL', 'CC', 'DATE', ++ 'FROM', 'REVERSE', 'SIZE', 'TO' ], ++ 'UTF-8', 'ALL'); ++} ++ ++sub test_maxargssize_status ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STATUS', 'INBOX', ++ [ 'MESSAGES', 'UIDNEXT', ++ 'UIDVALIDITY', 'UNSEEN', 'SIZE' ] ); ++} ++ ++sub test_maxargssize_store_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_maxargssize_store_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ], ++ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_append_zero ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_cmd('APPEND', 0, '', 'INBOX', { Literal => '' } ); ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++} ++ ++sub test_maxmessagesize_sync_literal ++{ ++ my ($self) = @_; ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'APPEND', ++ 'INBOX', { Literal => $toobig_email } ); ++} ++ ++sub test_maxmessagesize_nosync_literal ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_literal_minus ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++1; +diff --git a/cassandane/Cassandane/IMAPMessageStore.pm b/cassandane/Cassandane/IMAPMessageStore.pm +index 338d1c5f3..959a9fabc 100644 +--- a/cassandane/Cassandane/IMAPMessageStore.pm ++++ b/cassandane/Cassandane/IMAPMessageStore.pm +@@ -83,7 +83,7 @@ sub new + + sub connect + { +- my ($self) = @_; ++ my ($self, %params) = @_; + + # if already successfully connected, do nothing + return +@@ -115,6 +115,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to '$self->{host}:$self->{port}': $@"; + } +@@ -129,6 +130,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to server: $@"; + } +@@ -323,9 +325,9 @@ sub remove + + sub get_client + { +- my ($self) = @_; ++ my ($self, %params) = @_; + +- $self->connect(); ++ $self->connect(%params); + return $self->{client}; + } + +-- +2.39.2 + + +From 280151cceff96ce5eddd4e71255ce73f80fb1565 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:49:46 -0400 +Subject: [PATCH 21/22] imapd.c: also emit a NO [TOOBIG] response for oversized + no-sync APPEND + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 46 ++++++++++++++--------- + imap/imapd.c | 23 ++++++++---- + 2 files changed, 44 insertions(+), 25 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +index 2275c5cf7..52c1c8117 100644 +--- a/cassandane/Cassandane/Cyrus/IMAPLimits.pm ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -59,6 +59,7 @@ $email =~ s/\r?\n/\r\n/gs; + + my $toobig_email = $email . "X" x 100; + ++# Check that we got an untagged BYE [TOOBIG] response + sub assert_bye_toobig + { + my ($self, $store) = @_; +@@ -79,11 +80,11 @@ sub assert_bye_toobig + } + }; + +- # Check that we got a BYE [TOOBIG] response + $store->idle_response($handlers, 1); + $self->assert_num_equals(1, $got_toobig); + } + ++# Send a command and expect an untagged BYE [TOOBIG] response + sub assert_cmd_bye_toobig + { + my $self = shift; +@@ -97,28 +98,37 @@ sub assert_cmd_bye_toobig + $self->assert_bye_toobig(); + } + ++# Check that we got a tagged NO [TOOBIG] response ++sub assert_no_toobig ++{ ++ my ($self, $talk) = @_; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ eval { ++ $talk->_parse_response($handlers); ++ }; ++ ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++# Send a command and expect a tagged NO [TOOBIG] response + sub assert_cmd_no_toobig + { + my $self = shift; + my $talk = shift; + my $cmd = shift; + +- my $got_toobig = 0; +- my $handlers = +- { +- 'no' => sub +- { +- # Pick out the [TOOBIG] response code +- my (undef, $resp) = @_; +- $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); +- } +- }; +- +- $talk->_imap_cmd($cmd, 0, $handlers, @_); +- +- # Check that we got a NO [TOOBIG] response +- $self->assert_str_equals('no', $talk->get_last_completion_response()); +- $self->assert_num_equals(1, $got_toobig); ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_no_toobig($talk); + } + + sub new +@@ -503,6 +513,7 @@ sub test_maxmessagesize_nosync_literal + my $talk = $self->{store}->get_client(); + # Do this by brute force until we have IMAPTalk v4.06+ + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +@@ -512,6 +523,7 @@ sub test_literal_minus + + my $talk = $self->{store}->get_client(); + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +diff --git a/imap/imapd.c b/imap/imapd.c +index e9451d35e..000fedb22 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3517,7 +3517,7 @@ static int isokflag(char *s, int *isseen) + } + } + +-static int getliteralsize(const char *p, int c, size_t maxsize, ++static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + unsigned *size, int *binary, const char **parseerr) + + { +@@ -3549,10 +3549,14 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + /* LITERAL- says maximum size is 4096! */ + if (lminus && num > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_MESSAGE_TOOBIG)); + fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); + } + isnowait++; +@@ -3582,8 +3586,8 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + return 0; + } + +-static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *binary, +- const char **parseerr) ++static int catenate_text(const char *tag, FILE *f, size_t maxsize, ++ unsigned *totalsize, int *binary, const char **parseerr) + { + int c; + static struct buf arg; +@@ -3595,7 +3599,8 @@ static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *bina + c = getword(imapd_in, &arg); + + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize - *totalsize, &size, binary, parseerr); ++ r = getliteralsize(tag, arg.s, c, maxsize - *totalsize, ++ &size, binary, parseerr); + if (r) return r; + + /* Catenate message part to stage */ +@@ -3742,7 +3747,8 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + return r; + } + +-static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsigned *totalsize, ++static int append_catenate(const char *tag, FILE *f, const char *cur_name, ++ size_t maxsize, unsigned *totalsize, + int *binary, const char **parseerr, const char **url) + { + int c, r = 0; +@@ -3756,7 +3762,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + + if (!strcasecmp(arg.s, "TEXT")) { +- int r1 = catenate_text(f, maxsize, totalsize, binary, parseerr); ++ int r1 = catenate_text(tag, f, maxsize, totalsize, binary, parseerr); + if (r1) return r1; + + /* if we see a SP, we're trying to catenate more than one part */ +@@ -3998,13 +4004,14 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, ++ r = append_catenate(tag, curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(tag, arg.s, c, maxmsgsize, ++ &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From b21941fc79f81208cac4f8a2b32aa4ff80e4cc88 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:55:13 -0400 +Subject: [PATCH 22/22] imapd.c, imapparse.c: call fatal(EX_PROTOCOL) when + client exceeds a limit + +--- + cunit/parse.testc | 12 ++++++------ + imap/imapd.c | 48 +++++++++++++++++++++++------------------------ + imap/imapparse.c | 22 +++++++++++----------- + 3 files changed, 41 insertions(+), 41 deletions(-) + +diff --git a/cunit/parse.testc b/cunit/parse.testc +index 5a97f9b73..1786706cb 100644 +--- a/cunit/parse.testc ++++ b/cunit/parse.testc +@@ -119,7 +119,7 @@ static void test_getint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -188,7 +188,7 @@ static void test_getsint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -255,7 +255,7 @@ static void test_getuint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint32, uint32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint32, uint32_t, STR4, &c, &val, &bytes_in); +@@ -322,7 +322,7 @@ static void test_getint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -391,7 +391,7 @@ static void test_getsint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -458,7 +458,7 @@ static void test_getuint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint64, uint64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint64, uint64_t, STR4, &c, &val, &bytes_in); +diff --git a/imap/imapd.c b/imap/imapd.c +index 000fedb22..ee6519033 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3551,13 +3551,13 @@ static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_MESSAGE_TOOBIG)); +- fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_PROTOCOL); + } + isnowait++; + p++; +@@ -3928,7 +3928,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4264,7 +4264,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + if (arg.s[0] == '\0') goto badlist; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { +@@ -4641,7 +4641,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + } + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + switch (fetchatt.s[0]) { +@@ -4774,7 +4774,7 @@ badannotation: + do { + c = getastring(imapd_in, imapd_out, &fieldname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5110,7 +5110,7 @@ badannotation: + do { + c = getword(imapd_in, &fetchatt); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { +@@ -5459,7 +5459,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + do { + c = getword(imapd_in, &storemod); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { +@@ -5554,7 +5554,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + for (;;) { + c = getword(imapd_in, &flagname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -7462,7 +7462,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8308,7 +8308,7 @@ void cmd_setquota(const char *tag, const char *quotaroot) + else if (c != ' ') goto badlist; + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8462,7 +8462,7 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + if (arg.s[0] == '\0') goto bad; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { +@@ -8847,7 +8847,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + name = ucase(arg.s); + if (c != ' ') goto fail; +@@ -8858,7 +8858,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &val); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + dlist_setatom(sub, name, val.s); + } while (c == ' '); +@@ -8929,7 +8929,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -8982,7 +8982,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9062,7 +9062,7 @@ static int parse_metadata_string_or_list(const char *tag, + do { + c = getastring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9168,7 +9168,7 @@ static int parse_annotate_store_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9212,7 +9212,7 @@ static int parse_annotate_store_data(const char *tag, + } + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); +@@ -9290,7 +9290,7 @@ static int parse_metadata_store_data(const char *tag, + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9305,7 +9305,7 @@ static int parse_metadata_store_data(const char *tag, + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11548,7 +11548,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + n = 0; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ +@@ -11700,7 +11700,7 @@ static int getlistselopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -11806,7 +11806,7 @@ static int getlistretopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 1fdb8b312..5646f8812 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("[TOOBIG] Quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_PROTOCOL); + } + } + +@@ -157,11 +157,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_PROTOCOL); + } + isnowait++; + c = prot_getc(pin); +@@ -225,7 +225,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + c = prot_getc(pin); + } +@@ -284,7 +284,7 @@ EXPORTED int getint32(struct protstream *pin, int32_t *num) + /* INT_MAX == 2147483647 */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 214748364 || (result == 214748364 && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -337,7 +337,7 @@ EXPORTED int getuint32(struct protstream *pin, uint32_t *num) + /* UINT_MAX == 4294967295U */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 429496729 || (result == 429496729 && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -361,7 +361,7 @@ EXPORTED int getint64(struct protstream *pin, int64_t *num) + /* LLONG_MAX == 9223372036854775807LL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 922337203685477580LL || (result == 922337203685477580LL && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -414,7 +414,7 @@ EXPORTED int getuint64(struct protstream *pin, uint64_t *num) + /* ULLONG_MAX == 18446744073709551615ULL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 1844674407370955161ULL || (result == 1844674407370955161ULL && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -636,7 +636,7 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + + if (searchargs->maxargssize_mark && + prot_bytes_in(pin) > searchargs->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + } while (c == ' '); +@@ -1389,7 +1389,7 @@ static int get_search_criterion(struct protstream *pin, + + if (base->maxargssize_mark && + prot_bytes_in(pin) > base->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + if (!keep_charset) +-- +2.39.2 + diff --git a/mail/cyrus-imapd36/Makefile b/mail/cyrus-imapd36/Makefile index 547407c302d0..8fed513b9d5a 100644 --- a/mail/cyrus-imapd36/Makefile +++ b/mail/cyrus-imapd36/Makefile @@ -1,265 +1,267 @@ PORTNAME= cyrus-imapd PORTVERSION= 3.6.4 -PORTREVISION= 0 +PORTREVISION= 1 CATEGORIES= mail MASTER_SITES= https://github.com/cyrusimap/cyrus-imapd/releases/download/${PORTNAME}-${DISTVERSION}/ PKGNAMESUFFIX= ${CYRUS_IMAPD_VER} MAINTAINER= ume@FreeBSD.org COMMENT= Cyrus mail server, supporting POP3 and IMAP4 protocols ${COMMENT_${FLAVOR}} WWW= https://www.cyrusimap.org/ COMMENT_http= (with HTTP) LICENSE= BSD4CLAUSE LICENSE_FILE= ${WRKSRC}/COPYING BROKEN_riscv64= fails to build: lib/chartable.c: Error 1 FLAVORS= basic http http_PKGNAMESUFFIX= ${CYRUS_IMAPD_VER}-http CYRUS_IMAPD_VER= 36 +EXTRA_PATCHES= ${FILESDIR}/v36-CVE-2024-34055.patch:-p1 + LIB_DEPENDS= libsasl2.so:security/cyrus-sasl2 \ libicuuc.so:devel/icu \ libjansson.so:devel/jansson \ libuuid.so:misc/e2fsprogs-libuuid \ libical.so:devel/libical CONFLICTS_INSTALL= cyrus-imapd2? cyrus-imapd3[0-57-] cyrus-imapd3[0-57-]-http USES= compiler:c11 cpe gmake libtool perl5 pkgconfig ssl USE_RC_SUBR= imapd CYRUS_PREFIX= ${PREFIX}/cyrus GNU_CONFIGURE= yes CONFIGURE_ARGS= --libexecdir=${CYRUS_PREFIX}/libexec \ --sbindir=${CYRUS_PREFIX}/sbin \ --sysconfdir=${PREFIX}/etc \ --with-cyrus-user=${CYRUS_USER} \ --with-sasl=${LOCALBASE} \ --with-com_err \ --with-openssl=${OPENSSLBASE} \ --with-perl=${PERL} CONFIGURE_ENV+= LIBS="-L${LOCALBASE}/lib" CPPFLAGS+= -I${LOCALBASE}/include MAKE_JOBS_UNSAFE= yes USES+= shebangfix SHEBANG_FILES= imap/cyr_cd.sh imap/promdatagen tools/config2header \ tools/masssievec tools/mkimap tools/translatesieve \ perl/sieve/scripts/*.pl CPE_VENDOR= cmu CPE_PRODUCT= cyrus_imap_server OPTIONS_DEFINE= AUTOCREATE BACKUP CLAMAV CLD2 HTTP IDLED LDAP MURDER \ MYSQL NNTP PCRE2 PGSQL REPLICATION SQLITE SQUAT SRS \ XAPIAN DOCS OPTIONS_DEFAULT= AUTOCREATE IDLED READLINE_GNU SQLITE SQUAT SRS .if ${FLAVOR:U} == http OPTIONS_DEFAULT+= HTTP .endif OPTIONS_SUB= yes AUTOCREATE_DESC= Enable autocreate support AUTOCREATE_CONFIGURE_ENABLE= autocreate BACKUP_DESC= Enable backup support (experimental) BACKUP_CONFIGURE_ENABLE=backup CLAMAV_DESC= Use ClamAV CLAMAV_CONFIGURE_WITH= clamav CLAMAV_LIB_DEPENDS= libclamav.so:security/clamav CLD2_DESC= Use CLD2 CLD2_CONFIGURE_WITH= cld2 CLD2_CONFIGURE_ENV= CLD2_CFLAGS="-I${LOCALBASE}/include" \ CLD2_LIBS="-L${LOCALBASE}/lib -lcld2" CLD2_LIB_DEPENDS= libcld2.so:devel/cld2 HTTP_DESC= Enable HTTP support HTTP_IMPLIES= SQLITE HTTP_CONFIGURE_ENABLE= http HTTP_LIB_DEPENDS= libnghttp2.so:www/libnghttp2 \ libshp.so:devel/shapelib \ libbrotlidec.so:archivers/brotli \ libwslay_shared.so:www/wslay \ libzstd.so:archivers/zstd HTTP_CONFIGURE_ENV= WSLAY_CFLAGS="-I${LOCALBASE}/include" \ WSLAY_LIBS="-L${LOCALBASE}/lib -lwslay_shared" # Need additional patch to opendkim #HTTP_LIB_DEPENDS+= libopendkim.so:mail/opendkim #HTTP_CPPFLAGS+= -I${LOCALBASE}/include/opendkim HTTP_USES= gnome HTTP_USE= GNOME=libxml2 IDLED_DESC= Enable IMAP idled support IDLED_CONFIGURE_ENABLE= idled LDAP_DESC= Enable LDAP support (experimental) LDAP_USES= ldap LDAP_CONFIGURE_ON= --with-ldap=${LOCALBASE} LDAP_CONFIGURE_OFF= --without-ldap MURDER_DESC= Enable IMAP Murder support MURDER_CONFIGURE_ENABLE=murder MURDER_MAKE_ENV= PTHREAD_LIBS="-lpthread" MYSQL_USES= mysql MYSQL_CONFIGURE_WITH= mysql NNTP_DESC= Enable NNTP support NNTP_CONFIGURE_ENABLE= nntp PCRE2_DESC= Use PCRE2 rather than PCRE PCRE2_LIB_DEPENDS= libpcre2-posix.so:devel/pcre2 PCRE2_LIB_DEPENDS_OFF= libpcre.so:devel/pcre PCRE2_CONFIGURE_ON= --disable-pcre PCRE2_CONFIGURE_OFF= --disable-pcre2 PGSQL_USES= pgsql PGSQL_CONFIGURE_ON= --with-pgsql=${LOCALBASE} PGSQL_CONFIGURE_OFF= --without-pgsql REPLICATION_DESC= Enable replication (experimental) REPLICATION_CONFIGURE_ENABLE=replication SRS_DESC= Enable Sender Rewriting Scheme support SRS_CONFIGURE_ENABLE= srs SRS_LIB_DEPENDS= libsrs2.so:mail/libsrs2 SQLITE_USES= sqlite SQLITE_CONFIGURE_ON= --with-sqlite=${LOCALBASE} SQLITE_CONFIGURE_OFF= --without-sqlite SQLITE_BROKEN_OFF= SQLITE is required SQUAT_DESC= Enable Squat support SQUAT_CONFIGURE_OFF= --disable-squat XAPIAN_DESC= Enable Xapian support XAPIAN_CONFIGURE_ENABLE=xapian XAPIAN_LIB_DEPENDS= libxapian.so:databases/xapian-core XAPIAN_BUILD_DEPENDS= rsync:net/rsync XAPIAN_RUN_DEPENDS= ${XAPIAN_BUILD_DEPENDS} OPTIONS_RADIO= GSSAPI READLINE OPTIONS_RADIO_GSSAPI= GSSAPI_HEIMDAL GSSAPI_MIT .if exists(/usr/lib/libkrb5.a) OPTIONS_RADIO_GSSAPI+= GSSAPI_BASE OPTIONS_DEFAULT+= GSSAPI_BASE .endif GSSAPI_BASE_USES= gssapi GSSAPI_BASE_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_HEIMDAL_USES= gssapi:heimdal,flags GSSAPI_HEIMDAL_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_MIT_USES= gssapi:mit GSSAPI_MIT_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=mit OPTIONS_RADIO_READLINE= READLINE_GNU READLINE_PERL READLINE_GNU_DESC= Use Term::Readline::GNU for cyradm READLINE_GNU_RUN_DEPENDS= p5-Term-ReadLine-Gnu>=0:devel/p5-Term-ReadLine-Gnu READLINE_PERL_DESC= Use Term::Readline::Perl for cyradm READLINE_PERL_RUN_DEPENDS= p5-Term-ReadLine-Perl>=0:devel/p5-Term-ReadLine-Perl MANDIRS= ${CYRUS_PREFIX}/man PORTDOCS= * SUB_FILES= pkg-message pkg-install pkg-deinstall cyrus-imapd-man.conf SUB_LIST= CYRUS_USER=${CYRUS_USER} CYRUS_GROUP=${CYRUS_GROUP} CYRUS_USER?= cyrus CYRUS_GROUP?= cyrus MAN_MAN1= httptest imtest installsieve lmtptest mupdatetest nntptest \ pop3test sieveshell sivtest smtptest synctest MAN_MAN3= imclient MAN_MAN5= cyrus.conf imapd.conf krb.equiv CYRUS_MAN8= arbitron backupd chk_cyrus ctl_backups ctl_conversationsdb \ ctl_cyrusdb ctl_deliver ctl_mboxlist cvt_cyrusdb \ cvt_xlist_specialuse cyr_backup cyr_buildinfo cyr_dbtool \ cyr_deny cyr_df cyr_expire cyr_info cyr_ls cyr_synclog \ cyr_userseen cyr_virusscan cyradm cyrdump deliver fud idled \ imapd ipurge lmtpd lmtpproxyd master mbexamine mbpath mbtool \ notifyd pop3d pop3proxyd promstatsd proxyd ptdump ptexpire \ ptloader quota reconstruct relocate_by_id restore sievec \ sieved smmapd timsieved tls_prune unexpunge CYRUS_PERL_MAN1=cyradm CYRUS_PERL_MAN3=Cyrus::Annotator::Daemon Cyrus::Annotator::Message \ Cyrus::IMAP Cyrus::IMAP::Admin Cyrus::IMAP::Shell \ Cyrus::SIEVE::managesieve INSTALL_TARGET= install-strip REINPLACE_ARGS= -i '' .include .if !${PORT_OPTIONS:MGSSAPI_BASE} && !${PORT_OPTIONS:MGSSAPI_HEIMDAL} && \ !${PORT_OPTIONS:MGSSAPI_MIT} CONFIGURE_ARGS+=--disable-gssapi .endif .if ${PORT_OPTIONS:MHTTP} CYRUS_MAN8+= ctl_zoneinfo httpd MAN_MAN1+= dav_reconstruct .endif .if ${PORT_OPTIONS:MNNTP} CYRUS_MAN8+= fetchnews nntpd .endif .if ${PORT_OPTIONS:MMURDER} CYRUS_MAN8+= mupdate .endif .if ${PORT_OPTIONS:MREPLICATION} CYRUS_MAN8+= sync_client sync_reset sync_server .endif .if ${PORT_OPTIONS:MSQUAT} || ${PORT_OPTIONS:MXAPIAN} CYRUS_MAN8+= squatter PLIST_SUB+= SQUATTER="" .else PLIST_SUB+= SQUATTER="@comment " .endif post-patch: @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|" \ -e "s|%%CYRUS_USER%%|${CYRUS_USER}|g" \ -e "s|%%CYRUS_GROUP%%|${CYRUS_GROUP}|g" \ ${WRKSRC}/tools/mkimap .for f in masssievec translatesieve @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|g" \ -e "s|/usr/sieve|/var/imap/sieve|g" \ ${WRKSRC}/tools/${f} .endfor @${REINPLACE_CMD} \ -e 's|$$(libdir)/\(pkgconfig\)|${PREFIX}/libdata/\1|g' \ -e 's|$$(mandir)/\(man[8]\)|${PREFIX}/cyrus/man/\1|g' \ ${WRKSRC}/Makefile.in post-install: ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/IMAP/IMAP.so ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/SIEVE/managesieve/managesieve.so ${MKDIR} ${STAGEDIR}${EXAMPLESDIR} ${INSTALL_DATA} ${FILESDIR}/imapd.conf \ ${STAGEDIR}${EXAMPLESDIR} ${SED} -e 's,/run/cyrus/socket,/var/imap/socket,' \ ${WRKSRC}/doc/examples/cyrus_conf/normal.conf \ > ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .if !${PORT_OPTIONS:MHTTP} ${REINPLACE_CMD} -e 's/^\( http\)/#\1/' \ ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .endif .for f in mkimap masssievec translatesieve ${INSTALL_SCRIPT} ${WRKSRC}/tools/${f} \ ${STAGEDIR}${CYRUS_PREFIX}/sbin/${f} .endfor ${INSTALL_DATA} ${WRKDIR}/cyrus-imapd-man.conf \ ${STAGEDIR}${PREFIX}/etc/man.d/cyrus-imapd.conf .for s in 1 3 5 . for m in ${MAN_MAN${s}} @${ECHO_CMD} share/man/man${s}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for s in 1 3 . for m in ${CYRUS_PERL_MAN${s}} @${ECHO_CMD} ${SITE_MAN${s}}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for m in ${CYRUS_MAN8} @${ECHO_CMD} ${CYRUS_PREFIX}/man/man8/${m}.8.gz >> ${TMPPLIST} .endfor post-install-DOCS-on: ${MKDIR} ${STAGEDIR}${DOCSDIR} cd ${WRKSRC}/doc && ${COPYTREE_SHARE} . ${STAGEDIR}${DOCSDIR} \ "! ( -path */html/_sources* -o -name .buildinfo )" ${RM} -r ${STAGEDIR}${DOCSDIR}/rst ${STAGEDIR}${DOCSDIR}/source .include diff --git a/mail/cyrus-imapd36/files/v36-CVE-2024-34055.patch b/mail/cyrus-imapd36/files/v36-CVE-2024-34055.patch new file mode 100644 index 000000000000..8761b618599e --- /dev/null +++ b/mail/cyrus-imapd36/files/v36-CVE-2024-34055.patch @@ -0,0 +1,5348 @@ +From d95b0b211e8179b62fb3a814c827db5175968d01 Mon Sep 17 00:00:00 2001 +From: Robert Stepanek +Date: Wed, 3 Jan 2024 09:51:36 +0100 +Subject: [PATCH 01/16] SearchFuzzy.pm: do not use non-standard XSNIPPETS + command + +The XSNIPPETS and XCONVMULTISTANDARD commands in Cyrus got +deprecated, so don't keep our test using it. + +Signed-off-by: Robert Stepanek +--- + cassandane/Cassandane/Cyrus/SearchFuzzy.pm | 344 +++++++++------------ + 1 file changed, 146 insertions(+), 198 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +index af0adaf64..5b3aad0e0 100644 +--- a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm ++++ b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +@@ -43,6 +43,8 @@ use warnings; + use Cwd qw(abs_path); + use DateTime; + use Data::Dumper; ++use MIME::Base64 qw(encode_base64); ++use Encode qw(decode encode); + + use lib '.'; + use base qw(Cassandane::Cyrus::TestCase); +@@ -50,10 +52,19 @@ use Cassandane::Util::Log; + + sub new + { ++ + my ($class, @args) = @_; + my $config = Cassandane::Config->default()->clone(); +- $config->set(conversations => 'on'); +- return $class->SUPER::new({ config => $config }, @args); ++ $config->set( ++ conversations => 'on', ++ httpallowcompress => 'no', ++ httpmodules => 'jmap', ++ ); ++ return $class->SUPER::new({ ++ config => $config, ++ jmap => 1, ++ services => [ 'imap', 'http' ] ++ }, @args); + } + + sub set_up +@@ -134,6 +145,55 @@ sub create_testmessages + $self->{instance}->run_command({cyrus => 1}, 'squatter'); + } + ++sub get_snippets ++{ ++ # Previous versions of this test module used XSNIPPETS to ++ # assert snippets but this command got removed from Cyrus. ++ # Use JMAP instead. ++ ++ my ($self, $folder, $uids, $filter) = @_; ++ ++ my $imap = $self->{store}->get_client(); ++ my $jmap = $self->{jmap}; ++ ++ $self->assert_not_null($jmap); ++ ++ $imap->select($folder); ++ my $res = $imap->fetch($uids, ['emailid']); ++ my %emailIdToImapUid = map { $res->{$_}{emailid}[0] => $_ } keys %$res; ++ ++ $res = $jmap->CallMethods([ ++ ['SearchSnippet/get', { ++ filter => $filter, ++ emailIds => [ keys %emailIdToImapUid ], ++ }, 'R1'], ++ ]); ++ ++ my @snippets; ++ foreach (@{$res->[0][1]{list}}) { ++ if ($_->{subject}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'SUBJECT', ++ $_->{subject}, ++ ]); ++ } ++ if ($_->{preview}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'BODY', ++ $_->{preview}, ++ ]); ++ } ++ } ++ ++ return { ++ snippets => [ sort { $a->[1] <=> $b->[1] } @snippets ], ++ }; ++} ++ + sub test_copy_messages + :needs_search_xapian + { +@@ -151,12 +211,13 @@ sub test_copy_messages + } + + sub test_stem_verbs +- :min_version_3_0 :needs_search_xapian ++ :min_version_3_0 :needs_search_xapian :JMAPExtensions + { + my ($self) = @_; + $self->create_testmessages(); + + my $talk = $self->{store}->get_client(); ++ $self->assert_not_null($self->{jmap}); + + xlog $self, "Select INBOX"; + my $r = $talk->select("INBOX") || die; +@@ -175,11 +236,8 @@ sub test_stem_verbs + $r = $talk->search('fuzzy', ['subject', { Quote => "runs" }]) || die; + $self->assert_num_equals(3, scalar @$r); + +- xlog $self, 'XSNIPPETS for FUZZY subject "runs"'; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'subject', { Quote => 'runs' }] +- ) || die; ++ xlog $self, 'Get snippets for FUZZY subject "runs"'; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'runs' }); + $self->assert_num_equals(3, scalar @{$r->{snippets}}); + } + +@@ -250,12 +308,8 @@ sub test_snippet_wildcard + $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + +- xlog $self, "XSNIPPETS for $term"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => "$term*" }] +- ) || die; +- xlog $self, Dumper($r); ++ xlog $self, "Get snippets for $term"; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => "$term*" }); + $self->assert_num_equals(2, scalar @{$r->{snippets}}); + } + +@@ -358,13 +412,17 @@ sub test_normalize_snippets + my ($self) = @_; + + # Set up test message with funny characters +- my $body = "foo gären советской diĝir naïve léger"; +- my @terms = split / /, $body; ++use utf8; ++ my @terms = ( "gären", "советской", "diĝir", "naïve", "léger" ); ++no utf8; ++ my $body = encode_base64(encode('UTF-8', join(' ', @terms))); ++ $body =~ s/\r?\n/\r\n/gs; + + xlog $self, "Generate and index test messages."; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + +@@ -380,24 +438,20 @@ sub test_normalize_snippets + + # Assert that diacritics are matched and returned + foreach my $term (@terms) { +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + } + + # Assert that search without diacritics matches + if ($self->{skipdiacrit}) { + my $term = "naive"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => $term }); ++use utf8; ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++no utf8; + } ++ + } + + sub test_skipdiacrit +@@ -499,38 +553,23 @@ sub test_snippets_termcover + my $r = $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- my $want = "favourite cereal"; ++ my $want = "favourite cereal"; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite', +- 'fuzzy', 'text', 'cereal', +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ operator => 'AND', ++ conditions => [{ ++ text => 'favourite', ++ }, { ++ text => 'cereal', ++ }, { ++ text => '"bogus gnarly"' ++ }], ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite cereal' +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- # Regression - a phrase is treated as a loose term +- $r = $talk->xsnippets( [ [ 'INBOX', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite nope cereal' }, +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite cereal' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ text => 'favourite cereal', ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + } + +@@ -542,18 +581,28 @@ sub test_cjk_words + + xlog $self, "Generate and index test messages."; + ++use utf8; + my $body = "明末時已經有香港地方的概念"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + + # Splits into the words: "み, 円, 月額, 申込 ++use utf8; + $body = "申込み!月額円"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("2", %params) || die; + +@@ -569,50 +618,45 @@ sub test_cjk_words + + my $term; + # Search for a two-character CJK word ++use utf8; + $term = "已經"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + + # Search for the CJK words 明末 and 時, note that the + # word order is reversed to the original message ++use utf8; + $term = "時明末"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + + # Search for the partial CJK word 月 ++use utf8; + $term = "月"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for the interleaved, partial CJK word 額申 ++use utf8; + $term = "額申"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for three of four words: "み, 月額, 申込", + # in different order than the original. ++use utf8; + $term = "月額み申込"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + } + +@@ -805,86 +849,6 @@ sub test_xattachmentname + } + + +-sub test_xapianv2 +- :min_version_3_0 :needs_search_xapian +-{ +- my ($self) = @_; +- +- my $talk = $self->{store}->get_client(); +- +- # This is a smallish regression test to check if we break something +- # obvious by moving Xapian indexing from folder:uid to message guids. +- # +- # Apart from the tests in this module, at least also the following +- # imodules are relevant: Metadata for SORT, Thread for THREAD. +- +- xlog $self, "Generate message"; +- my $r = $self->make_message("I run", body => "Run, Forrest! Run!" ) || die; +- my $uid = $r->{attrs}->{uid}; +- +- xlog $self, "Copy message into INBOX"; +- $talk->copy($uid, "INBOX"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Create target mailbox"; +- $talk->create("INBOX.target"); +- +- xlog $self, "Copy message into INBOX.target"; +- $talk->copy($uid, "INBOX.target"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(3, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Generate message"; +- $self->make_message("You run", body => "A running joke" ) || die; +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}}); +- +- xlog $self, "SEARCH FUZZY"; +- $r = $talk->search( +- "charset", "utf-8", "fuzzy", "text", "run", +- ) || die; +- $self->assert_num_equals(3, scalar @$r); +- +- xlog $self, "Select INBOX"; +- $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- +- xlog $self, "XSNIPPETS"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'body', 'run'], +- ) || die; +- $self->assert_num_equals(3, scalar @{$r->{snippets}}); +-} +- + sub test_snippets_escapehtml + :min_version_3_0 :needs_search_xapian + { +@@ -914,21 +878,15 @@ sub test_snippets_escapehtml + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); + my %m; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test1' ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test1' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); +- $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test2' ] +- ) || die; ++ $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); ++ $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); + ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test2' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); +- $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); ++ $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); ++ $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); + } + + sub test_search_exactmatch +@@ -963,13 +921,10 @@ sub test_search_exactmatch + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { body => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert(index($m{body}, "some text") != -1); +- $self->assert(index($m{body}, "some long text") == -1); ++ $self->assert(index($m{body}, "some text") != -1); ++ $self->assert(index($m{body}, "some long text") == -1); + } + + sub test_search_subjectsnippet +@@ -1004,10 +959,7 @@ sub test_search_subjectsnippet + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { text => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; + $self->assert_matches(qr/^\[plumbing\]/, $m{subject}); + } +@@ -1317,11 +1269,10 @@ sub test_detect_language + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', 'atmet' ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++ $r = $self->get_snippets('INBOX', $uids, { body => 'atmet' }); ++use utf8; ++ $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++no utf8; + } + + sub test_detect_language_subject +@@ -1377,12 +1328,9 @@ sub test_detect_language_subject + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'subject', 'Landschaft' ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'Landschaft' }); + $self->assert_str_equals( +- 'A subject with the German word Landschaften', ++ 'A subject with the German word Landschaften', + $r->{snippets}[0][3] + ); + } +-- +2.39.2 + + +From cce755f3a49f3768058c4f52b2c32763e890a6b8 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:00:00 -0500 +Subject: [PATCH 02/16] imapd.c: UIDVALIDITY should be uint32_t and parse it as + such + +--- + imap/imapd.c | 10 +++------- + imap/index.h | 2 +- + 2 files changed, 4 insertions(+), 8 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 6f70820ca..8e087b731 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -4279,15 +4279,11 @@ static void cmd_select(char *tag, char *cmd, char *name) + } + else if ((client_capa & CAPA_QRESYNC) && + !strcmp(arg.s, "QRESYNC")) { +- char *p; +- + if (c != ' ') goto badqresync; + c = prot_getc(imapd_in); + if (c != '(') goto badqresync; +- c = getastring(imapd_in, imapd_out, &arg); +- v->uidvalidity = strtoul(arg.s, &p, 10); +- if (*p || !v->uidvalidity || v->uidvalidity == ULONG_MAX) goto badqresync; +- if (c != ' ') goto badqresync; ++ c = getuint32(imapd_in, &v->uidvalidity); ++ if (c != ' ' || !v->uidvalidity) goto badqresync; + c = getmodseq(imapd_in, &v->modseq); + if (c == EOF) goto badqresync; + if (c == ' ') { +@@ -4427,7 +4423,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + prot_printf(backend_current->out, "%s %s {" SIZE_T_FMT "+}\r\n%s", + tag, cmd, strlen(name), name); + if (v->uidvalidity) { +- prot_printf(backend_current->out, " (QRESYNC (%lu " MODSEQ_FMT, ++ prot_printf(backend_current->out, " (QRESYNC (%u " MODSEQ_FMT, + v->uidvalidity, v->modseq); + if (v->sequence) { + prot_printf(backend_current->out, " %s", v->sequence); +diff --git a/imap/index.h b/imap/index.h +index ce8fe36c2..df12e609a 100644 +--- a/imap/index.h ++++ b/imap/index.h +@@ -72,7 +72,7 @@ extern unsigned client_capa; + struct message; + + struct vanished_params { +- unsigned long uidvalidity; ++ uint32_t uidvalidity; + modseq_t modseq; + const char *match_seq; + const char *match_uid; +-- +2.39.2 + + +From f37421f2687b811c4e4bd8c0fc5c66d368cc3ffe Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 08:13:05 -0500 +Subject: [PATCH 03/16] imapd.c: consolidate ID field-value parse error + response + +--- + imap/imapd.c | 17 +++++------------ + 1 file changed, 5 insertions(+), 12 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 8e087b731..8773621e0 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3104,19 +3104,12 @@ static void cmd_id(char *tag) + + /* get field name */ + c = getstring(imapd_in, imapd_out, &field); +- if (c != ' ') { ++ if (c != ' ' || ++ /* get field value */ ++ (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || ++ (c != ' ' && c != ')')) { + prot_printf(imapd_out, +- "%s BAD Invalid/missing field name in Id\r\n", +- tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* get field value */ +- c = getnstring(imapd_in, imapd_out, &arg); +- if (c != ' ' && c != ')') { +- prot_printf(imapd_out, +- "%s BAD Invalid/missing value in Id\r\n", ++ "%s BAD Invalid field-value pair in Id\r\n", + tag); + eatline(imapd_in, c); + return; +-- +2.39.2 + + +From 0ed0466636f43ea32cb765c33fa6a8109d9ea69e Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:12:41 -0500 +Subject: [PATCH 04/16] imapd.c: response code in fatal() string MUST + immediately follow "* BYE" + +--- + imap/imapd.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 8773621e0..464d9d7ad 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1186,7 +1186,8 @@ EXPORTED void fatal(const char *s, int code) + } + recurse_code = code; + if (imapd_out) { +- prot_printf(imapd_out, "* BYE Fatal error: %s\r\n", s); ++ prot_printf(imapd_out, "* BYE %s%s\r\n", ++ *s == '[' /* resp-text-code */ ? "" : "Fatal error: ", s); + prot_flush(imapd_out); + } + if (stages.count) { +-- +2.39.2 + + +From fb0eee5f3c4d67c0ee84d8e33bd835d9f0044e4a Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 11:00:19 -0500 +Subject: [PATCH 05/16] imapparse.c: include [TOOBIG] response code for + oversized word/qstring + +--- + imap/imapparse.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 02cc1ed92..d096584e3 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_IOERR); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_IOERR); + } + } + +@@ -212,6 +212,9 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + return c; + } + buf_putc(buf, c); ++ if (config_maxword && buf_len(buf) > config_maxword) { ++ fatal("[TOOBIG] Word too long", EX_IOERR); ++ } + c = prot_getc(pin); + } + /* never gets here */ +-- +2.39.2 + + +From dc9846028db0022372b4cfe4d5ea92ab22eb2ed0 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 13:31:11 -0500 +Subject: [PATCH 06/16] imapparse.c: fatal() when a client violates LITERAL- + limit + +--- + imap/imap_err.et | 3 +++ + imap/imapparse.c | 7 +++++-- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index a98ec0e1b..072078f94 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,9 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++ec IMAP_LITERAL_MINUS_TOO_LARGE, ++ "[TOOBIG] Non-synchronizing literal size exceeds 4K" ++ + ec IMAP_USERFLAG_EXHAUSTED, + "Too many user flags in mailbox" + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index d096584e3..7a4794600 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -153,8 +153,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + buf_reset(buf); + c = getint32(pin, &len); + if (c == '+') { +- // LITERAL- says maximum size is 4096! +- if (lminus && len > 4096) return EOF; ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && len > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +-- +2.39.2 + + +From 8414e71a9d1fbc56d03d6fddb096484364e3406c Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 12 Feb 2024 10:54:03 -0500 +Subject: [PATCH 07/16] Cleanup and document the use of prot_setisclient() + +Only IMAP(-like) clients send LITERAL+ syntax +--- + backup/backupd.c | 3 +-- + backup/lcb.c | 1 - + backup/lcb_read.c | 2 -- + backup/lcb_verify.c | 2 -- + cunit/getxstring.testc | 4 ---- + imap/append.c | 1 - + imap/backend.c | 2 -- + imap/cyr_dbtool.c | 1 - + imap/dlist.c | 5 ++++- + imap/imapd.c | 4 ++++ + imap/imapparse.c | 5 +++-- + imap/message.c | 3 --- + imap/mupdate.c | 3 +++ + imap/sync_server.c | 3 +-- + imap/sync_support.c | 4 ---- + lib/prot.h | 2 +- + 16 files changed, 17 insertions(+), 28 deletions(-) + +diff --git a/backup/backupd.c b/backup/backupd.c +index 9f8fa7b15..d42ddbb70 100644 +--- a/backup/backupd.c ++++ b/backup/backupd.c +@@ -229,9 +229,8 @@ EXPORTED int service_main(int argc __attribute__((unused)), + backupd_in = prot_new(0, 0); + backupd_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow use of LITERAL+ */ + prot_setisclient(backupd_in, 1); +- prot_setisclient(backupd_out, 1); + + /* Find out name of client host */ + backupd_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/backup/lcb.c b/backup/lcb.c +index dbba85ca7..3f68b1aaa 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -606,7 +606,6 @@ EXPORTED int backup_reindex(const char *name, + fprintf(out, "\nfound chunk at offset " OFF_T_FMT "\n\n", member_offset); + + struct protstream *member = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(member, 1); /* don't sync literals */ + + // FIXME stricter timestamp sequence checks + time_t member_start_ts = -1; +diff --git a/backup/lcb_read.c b/backup/lcb_read.c +index f597c97c5..f2342d69d 100644 +--- a/backup/lcb_read.c ++++ b/backup/lcb_read.c +@@ -113,7 +113,6 @@ EXPORTED int backup_read_message_data(struct backup *backup, + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + +@@ -203,7 +202,6 @@ EXPORTED int backup_prepare_message_upload(struct backup *backup, + if (!r) { + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); + int c; +- prot_setisclient(ps, 1); /* don't sync literals */ + c = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + ps = NULL; +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index fb0477a8b..88e748c51 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -228,7 +228,6 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, vmrock->gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + + if (r == EOF) { +@@ -528,7 +527,6 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + goto done; + } + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + + struct buf cmd = BUF_INITIALIZER; + while (1) { +diff --git a/cunit/getxstring.testc b/cunit/getxstring.testc +index f5a5989a3..12efe86aa 100644 +--- a/cunit/getxstring.testc ++++ b/cunit/getxstring.testc +@@ -72,9 +72,6 @@ static int tear_down(void) + + /* + * Run a single testcase. +- * +- * Note: prot_setisclient() turns off off literal synchronising so +- * we don't have to futz around with testing that. + */ + #define _TESTCASE_PRE(fut, input, retval, consumed) \ + do { \ +@@ -84,7 +81,6 @@ static int tear_down(void) + long long _consumed = (consumed); \ + p = prot_readmap(input, sizeof(input)-1); \ + CU_ASSERT_PTR_NOT_NULL_FATAL(p); \ +- prot_setisclient(p, 1); \ + c = fut(p, NULL, &b); \ + CU_ASSERT_EQUAL(c, retval); \ + if (_consumed >= 0) { \ +diff --git a/imap/append.c b/imap/append.c +index f7cf7e770..b8bc7963a 100644 +--- a/imap/append.c ++++ b/imap/append.c +@@ -436,7 +436,6 @@ static int callout_receive_reply(const char *callout, + } + + p = prot_new(fd, /*write*/0); +- prot_setisclient(p, 1); + + /* read and parse the reply as a dlist */ + c = dlist_parse(results, /*parsekeys*/0, /*isbackup*/0, p); +diff --git a/imap/backend.c b/imap/backend.c +index 36b83b4d9..546f41d25 100644 +--- a/imap/backend.c ++++ b/imap/backend.c +@@ -955,7 +955,6 @@ EXPORTED struct backend *backend_connect_pipe(int infd, int outfd, + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +@@ -1153,7 +1152,6 @@ EXPORTED struct backend *backend_connect(struct backend *ret_backend, const char + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +diff --git a/imap/cyr_dbtool.c b/imap/cyr_dbtool.c +index fec57cede..c6d56ec77 100644 +--- a/imap/cyr_dbtool.c ++++ b/imap/cyr_dbtool.c +@@ -155,7 +155,6 @@ static void batch_commands(struct db *db) + int r = 0; + + prot_setisclient(in, 1); +- prot_setisclient(out, 1); + + while (1) { + buf_reset(&cmd); +diff --git a/imap/dlist.c b/imap/dlist.c +index a2f876cd4..2b73ad5d0 100644 +--- a/imap/dlist.c ++++ b/imap/dlist.c +@@ -1225,7 +1225,10 @@ EXPORTED int dlist_parsemap(struct dlist **dlp, int parsekey, int isbackup, + struct dlist *dl = NULL; + + stream = prot_readmap(base, len); +- prot_setisclient(stream, 1); /* don't sync literals */ ++ ++ /* Allow LITERAL+ - this is silly, but required to parse personal CALDATA */ ++ prot_setisclient(stream, 1); ++ + c = dlist_parse(&dl, parsekey, isbackup, stream); + prot_free(stream); + +diff --git a/imap/imapd.c b/imap/imapd.c +index 464d9d7ad..2610076d6 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -955,6 +955,10 @@ int service_main(int argc __attribute__((unused)), + + imapd_in = prot_new(0, 0); + imapd_out = prot_new(1, 1); ++ ++ /* Allow LITERAL+ */ ++ prot_setisclient(imapd_in, 1); ++ + protgroup_insert(protin, imapd_in); + + /* Find out name of client host */ +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 7a4794600..d195630a5 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -149,10 +149,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + /* Literal */ +- isnowait = pin->isclient; ++ isnowait = !pin->isclient; + buf_reset(buf); + c = getint32(pin, &len); +- if (c == '+') { ++ ++ if (pin->isclient && c == '+') { + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +diff --git a/imap/message.c b/imap/message.c +index 55a317da9..3e4980804 100644 +--- a/imap/message.c ++++ b/imap/message.c +@@ -3419,7 +3419,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read envelope response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_ENVELOPE), + cacheitem_size(record, CACHE_ENVELOPE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_envelope(strm, *body); + prot_free(strm); +@@ -3427,7 +3426,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read bodystructure response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_BODYSTRUCTURE), + cacheitem_size(record, CACHE_BODYSTRUCTURE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_body(strm, *body, NULL); + prot_free(strm); +@@ -4792,7 +4790,6 @@ static int message_parse_cbodystructure(message_t *m) + cacheitem_size(&m->record, CACHE_BODYSTRUCTURE)); + if (!prot) + return IMAP_MAILBOX_BADFORMAT; +- prot_setisclient(prot, 1); /* don't crash parsing literals */ + + m->body = xzmalloc(sizeof(struct body)); + r = parse_bodystructure_part(prot, m->body, NULL); +diff --git a/imap/mupdate.c b/imap/mupdate.c +index 2cc2571bc..82455adae 100644 +--- a/imap/mupdate.c ++++ b/imap/mupdate.c +@@ -249,6 +249,9 @@ static struct conn *conn_new(int fd) + C->pin = prot_new(C->fd, 0); + C->pout = prot_new(C->fd, 1); + ++ /* Allow LITERAL+ */ ++ prot_setisclient(C->pin, 1); ++ + prot_setflushonread(C->pin, C->pout); + prot_settimeout(C->pin, 180*60); + +diff --git a/imap/sync_server.c b/imap/sync_server.c +index 3d6710f56..87a2cf346 100644 +--- a/imap/sync_server.c ++++ b/imap/sync_server.c +@@ -323,9 +323,8 @@ int service_main(int argc __attribute__((unused)), + sync_in = prot_new(0, 0); + sync_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow LITERAL+ */ + prot_setisclient(sync_in, 1); +- prot_setisclient(sync_out, 1); + + /* Find out name of client host */ + sync_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/imap/sync_support.c b/imap/sync_support.c +index 885bc76a9..920c018f3 100644 +--- a/imap/sync_support.c ++++ b/imap/sync_support.c +@@ -8058,10 +8058,6 @@ connected: + if (timeout < 3) timeout = 3; + prot_settimeout(backend->in, timeout); + +- /* Force use of LITERAL+ so we don't need two way communications */ +- prot_setisclient(backend->in, 1); +- prot_setisclient(backend->out, 1); +- + return 0; + } + +diff --git a/lib/prot.h b/lib/prot.h +index 98af5d160..89b0b0a2a 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -133,7 +133,7 @@ struct protstream { + int can_unget; + int bytes_in; + int bytes_out; +- int isclient; ++ int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ + prot_readcallback_t *readcallback_proc; +-- +2.39.2 + + +From 4e65061e15245963008fde649b9f250c10f1b4d7 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 22:48:58 -0400 +Subject: [PATCH 08/16] imapd.c: LITERAL- also applies to APPEND + +imap_err.et: add IMAP_MESSAGE_TOOBIG error message +--- + imap/imap_err.et | 4 ++++ + imap/imapd.c | 12 ++++++++++++ + 2 files changed, 16 insertions(+) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 072078f94..d90e30a4b 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,10 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++# Same as IMAP_MESSAGE_TOO_LARGE, but with TOOBIG response code ++ec IMAP_MESSAGE_TOOBIG, ++ "[TOOBIG] Message size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 2610076d6..c924be6e0 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3563,6 +3563,9 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + { + int isnowait = 0; + uint32_t num; ++ static int lminus = -1; ++ ++ if (lminus == -1) lminus = config_getswitch(IMAPOPT_LITERALMINUS); + + /* Check for literal8 */ + if (*p == '~') { +@@ -3583,6 +3586,15 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + } + + if (*p == '+') { ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && num > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } ++ if (num > maxsize) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ } + isnowait++; + p++; + } +-- +2.39.2 + + +From efa3a69a8ab7cff5dded76faa5c16099caf41e89 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 14:52:22 -0500 +Subject: [PATCH 09/16] imapd.c: remove XSNIPPETS and XCONV* commands + +--- + cassandane/Cassandane/Cyrus/Conversations.pm | 71 -- + imap/imapd.c | 1006 ------------------ + 2 files changed, 1077 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/Conversations.pm b/cassandane/Cassandane/Cyrus/Conversations.pm +index 63645e458..f20cae7bd 100755 +--- a/cassandane/Cassandane/Cyrus/Conversations.pm ++++ b/cassandane/Cassandane/Cyrus/Conversations.pm +@@ -708,77 +708,6 @@ sub bogus_test_replication_clash + $self->check_messages(\%exp, store => $replica_store); + } + +-sub test_xconvfetch +- :min_version_3_0 +-{ +- my ($self) = @_; +- my $store = $self->{store}; +- +- # check IMAP server has the XCONVERSATIONS capability +- $self->assert($store->get_client()->capability()->{xconversations}); +- +- xlog $self, "generating messages"; +- my $generator = Cassandane::ThreadedGenerator->new(); +- $store->write_begin(); +- while (my $msg = $generator->generate()) +- { +- $store->write_message($msg); +- } +- $store->write_end(); +- +- xlog $self, "reading the whole folder again to discover CIDs etc"; +- my %cids; +- my %uids; +- $store->read_begin(); +- while (my $msg = $store->read_message()) +- { +- my $uid = $msg->get_attribute('uid'); +- my $cid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- if (defined $cids{$cid}) +- { +- $self->assert_num_equals($threadid, $cids{$cid}); +- } +- else +- { +- $cids{$cid} = $threadid; +- xlog $self, "Found CID $cid"; +- } +- $self->assert_null($uids{$uid}); +- $uids{$uid} = 1; +- } +- $store->read_end(); +- +- xlog $self, "Using XCONVFETCH on each conversation"; +- foreach my $cid (keys %cids) +- { +- xlog $self, "XCONVFETCHing CID $cid"; +- +- my $result = $store->xconvfetch_begin($cid); +- $self->assert_not_null($result->{xconvmeta}); +- $self->assert_num_equals(1, scalar keys %{$result->{xconvmeta}}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}->{modseq}); +- while (my $msg = $store->xconvfetch_message()) +- { +- my $muid = $msg->get_attribute('uid'); +- my $mcid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- $self->assert_str_equals($cid, $mcid); +- $self->assert_num_equals($cids{$cid}, $threadid); +- $self->assert_num_equals(1, $uids{$muid}); +- $uids{$muid} |= 2; +- } +- $store->xconvfetch_end(); +- } +- +- xlog $self, "checking that all the UIDs in the folder were XCONVFETCHed"; +- foreach my $uid (keys %uids) +- { +- $self->assert_num_equals(3, $uids{$uid}); +- } +-} +- + # + # Test APPEND of a new composed draft message to the Drafts folder by + # the Fastmail webui, which sets the X-ME-Message-ID header to thread +diff --git a/imap/imapd.c b/imap/imapd.c +index c924be6e0..aec48912b 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -440,14 +440,6 @@ static void cmd_idle(char* tag); + + static void cmd_starttls(char *tag, int imaps); + +-static void cmd_xconvsort(char *tag, int updates); +-static void cmd_xconvmultisort(char *tag); +-static void cmd_xconvmeta(const char *tag); +-static void cmd_xconvfetch(const char *tag); +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs); +-static void cmd_xsnippets(char *tag); + static void cmd_xstats(char *tag); + + static void cmd_xapplepushservice(const char *tag, +@@ -504,12 +496,8 @@ static int parse_metadata_store_data(const char *tag, + static int getlistselopts(char *tag, struct listargs *args); + static int getlistretopts(char *tag, struct listargs *args); + +-static int get_snippetargs(struct snippetargs **sap); +-static void free_snippetargs(struct snippetargs **sap); + static int getsortcriteria(char *tag, struct sortcrit **sortcrit); + static int getdatetime(time_t *date); +-static int parse_windowargs(const char *tag, struct windowargs **, int); +-static void free_windowargs(struct windowargs *wa); + + static void appendfieldlist(struct fieldlist **l, char *section, + strarray_t *fields, char *trail, +@@ -2280,32 +2268,6 @@ static void cmdloop(void) + + prometheus_increment(CYRUS_IMAP_XBACKUP_TOTAL); + } +- else if (!strcmp(cmd.s, "Xconvfetch")) { +- cmd_xconvfetch(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVFETCH_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvmultisort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvmultisort(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVMULTISORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvsort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 0); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVSORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvupdates")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 1); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVUPDATES_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xfer")) { + if (readonly) goto noreadonly; + int havepartition = 0; +@@ -2331,9 +2293,6 @@ static void cmdloop(void) + (havepartition ? arg3.s : NULL)); + /* XXX prometheus_increment(CYRUS_IMAP_XFER_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xconvmeta")) { +- cmd_xconvmeta(tag.s); +- } + else if (!strcmp(cmd.s, "Xlist")) { + struct listargs listargs; + +@@ -2366,13 +2325,6 @@ static void cmdloop(void) + cmd_xrunannotator(tag.s, arg1.s, usinguid); + /* XXX prometheus_increment(CYRUS_IMAP_XRUNANNOTATOR_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xsnippets")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xsnippets(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XSNIPPETS_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xstats")) { + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xstats(tag.s); +@@ -5186,8 +5138,6 @@ badannotation: + } + if (config_getswitch(IMAPOPT_CONVERSATIONS) + && (fa->fetchitems & (FETCH_MAILBOXIDS|FETCH_MAILBOXES))) { +- // annoyingly, this codepath COULD be called from xconv* commands, but it never is, +- // in reality, so it's safe leaving this as shared + int r = conversations_open_user(imapd_userid, 0/*shared*/, &fa->convstate); + if (r) { + syslog(LOG_WARNING, "error opening conversations for %s: %s", +@@ -5275,136 +5225,6 @@ static void cmd_fetch(char *tag, char *sequence, int usinguid) + fetchargs_fini(&fetchargs); + } + +-static void do_one_xconvmeta(struct conversations_state *state, +- conversation_id_t cid, +- conversation_t *conv, +- struct dlist *itemlist) +-{ +- struct dlist *item = dlist_newpklist(NULL, ""); +- struct dlist *fl; +- +- assert(conv); +- assert(itemlist); +- +- for (fl = itemlist->head; fl; fl = fl->next) { +- const char *key = dlist_cstring(fl); +- +- /* xxx - parse to a fetchitems? */ +- if (!strcasecmp(key, "MODSEQ")) +- dlist_setnum64(item, "MODSEQ", conv->modseq); +- else if (!strcasecmp(key, "EXISTS")) +- dlist_setnum32(item, "EXISTS", conv->exists); +- else if (!strcasecmp(key, "UNSEEN")) +- dlist_setnum32(item, "UNSEEN", conv->unseen); +- else if (!strcasecmp(key, "SIZE")) +- dlist_setnum32(item, "SIZE", conv->size); +- else if (!strcasecmp(key, "COUNT")) { +- struct dlist *flist = dlist_newlist(item, "COUNT"); +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *lookup = dlist_cstring(tmp); +- int i = strarray_find_case(state->counted_flags, lookup, 0); +- if (i >= 0) { +- dlist_setflag(flist, "FLAG", lookup); +- dlist_setnum32(flist, "COUNT", conv->counts[i]); +- } +- } +- } +- } +- else if (!strcasecmp(key, "SENDERS")) { +- conv_sender_t *sender; +- struct dlist *slist = dlist_newlist(item, "SENDERS"); +- for (sender = conv->senders; sender; sender = sender->next) { +- struct dlist *sli = dlist_newlist(slist, ""); +- dlist_setatom(sli, "NAME", sender->name); +- dlist_setatom(sli, "ROUTE", sender->route); +- dlist_setatom(sli, "MAILBOX", sender->mailbox); +- dlist_setatom(sli, "DOMAIN", sender->domain); +- } +- } +- /* XXX - maybe rename FOLDERCOUNTS or something? */ +- else if (!strcasecmp(key, "FOLDEREXISTS")) { +- struct dlist *flist = dlist_newlist(item, "FOLDEREXISTS"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "EXISTS", folder ? folder->exists : 0); +- } +- } +- } +- else if (!strcasecmp(key, "FOLDERUNSEEN")) { +- struct dlist *flist = dlist_newlist(item, "FOLDERUNSEEN"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "UNSEEN", folder ? folder->unseen : 0); +- } +- } +- } +- else { +- dlist_setatom(item, key, NULL); /* add a NIL response */ +- } +- } +- +- prot_printf(imapd_out, "* XCONVMETA %s ", conversation_id_encode(cid)); +- dlist_print(item, 0, imapd_out); +- prot_printf(imapd_out, "\r\n"); +- +- dlist_free(&item); +-} +- +-static void do_xconvmeta(const char *tag, +- struct conversations_state *state, +- struct dlist *cidlist, +- struct dlist *itemlist) +-{ +- conversation_id_t cid; +- struct dlist *dl; +- int r; +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- const char *cidstr = dlist_cstring(dl); +- conversation_t *conv = NULL; +- +- if (!conversation_id_decode(&cid, cidstr) || !cid) { +- prot_printf(imapd_out, "%s BAD Invalid CID %s\r\n", tag, cidstr); +- return; +- } +- +- r = conversation_load(state, cid, &conv); +- if (r) { +- prot_printf(imapd_out, "%s BAD Failed to read %s\r\n", tag, cidstr); +- conversation_free(conv); +- return; +- } +- +- if (conv && conv->exists) +- do_one_xconvmeta(state, cid, conv, itemlist); +- +- conversation_free(conv); +- } +- +- prot_printf(imapd_out, "%s OK Completed\r\n", tag); +-} +- + static int do_xbackup(const char *channel, + const ptrarray_t *list) + { +@@ -5548,270 +5368,6 @@ done: + } + } + +-/* +- * Parse and perform a XCONVMETA command. +- */ +-void cmd_xconvmeta(const char *tag) +-{ +- int r; +- int c = ' '; +- struct conversations_state *state = NULL; +- struct dlist *cidlist = NULL; +- struct dlist *itemlist = NULL; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVMETA ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Failed to parse CID list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&itemlist, 0, imapd_in); +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, "%s BAD Failed to parse item list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- // this one is OK, xconvmeta doesn't do an expunge +- r = conversations_open_user(imapd_userid, 1/*shared*/, &state); +- if (r) { +- prot_printf(imapd_out, "%s BAD failed to open db: %s\r\n", +- tag, error_message(r)); +- goto done; +- } +- +- do_xconvmeta(tag, state, cidlist, itemlist); +- +- done: +- +- dlist_free(&itemlist); +- dlist_free(&cidlist); +- conversations_commit(&state); +-} +- +-/* +- * Parse and perform a XCONVFETCH command. +- */ +-void cmd_xconvfetch(const char *tag) +-{ +- int c = ' '; +- struct fetchargs fetchargs; +- int r; +- clock_t start = clock(); +- modseq_t ifchangedsince = 0; +- char mytime[100]; +- struct dlist *cidlist = NULL; +- struct dlist *item; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVFETCH ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* local mailbox */ +- memset(&fetchargs, 0, sizeof(struct fetchargs)); +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') +- goto syntax_error; +- +- /* check CIDs */ +- for (item = cidlist->head; item; item = item->next) { +- if (!dlist_ishex64(item)) { +- prot_printf(imapd_out, "%s BAD Invalid CID\r\n", tag); +- eatline(imapd_in, c); +- goto freeargs; +- } +- } +- +- c = getmodseq(imapd_in, &ifchangedsince); +- if (c != ' ') +- goto syntax_error; +- +- r = parse_fetch_args(tag, "Xconvfetch", 0, &fetchargs); +- if (r) +- goto freeargs; +- fetchargs.fetchitems |= (FETCH_UIDVALIDITY|FETCH_FOLDER); +- fetchargs.namespace = &imapd_namespace; +- fetchargs.userid = imapd_userid; +- +- r = do_xconvfetch(cidlist, ifchangedsince, &fetchargs); +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- +- if (r) { +- prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, +- error_message(r), mytime); +- } else { +- prot_printf(imapd_out, "%s OK Completed (%s sec)\r\n", +- tag, mytime); +- } +- +-freeargs: +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +- return; +- +-syntax_error: +- prot_printf(imapd_out, "%s BAD Syntax error\r\n", tag); +- eatline(imapd_in, c); +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +-} +- +-static int xconvfetch_lookup(struct conversations_state *statep, +- conversation_id_t cid, +- modseq_t ifchangedsince, +- hash_table *wanted_cids, +- strarray_t *folder_list) +-{ +- const char *key = conversation_id_encode(cid); +- conversation_t *conv = NULL; +- conv_folder_t *folder; +- int r; +- +- r = conversation_load(statep, cid, &conv); +- if (r) return r; +- +- if (!conv) +- goto out; +- +- if (!conv->exists) +- goto out; +- +- /* output the metadata for this conversation */ +- { +- struct dlist *dl = dlist_newlist(NULL, ""); +- dlist_setatom(dl, "", "MODSEQ"); +- do_one_xconvmeta(statep, cid, conv, dl); +- dlist_free(&dl); +- } +- +- if (ifchangedsince >= conv->modseq) +- goto out; +- +- hash_insert(key, (void *)1, wanted_cids); +- +- for (folder = conv->folders; folder; folder = folder->next) { +- /* no contents */ +- if (!folder->exists) +- continue; +- +- /* finally, something worth looking at */ +- strarray_add(folder_list, strarray_nth(statep->folders, folder->number)); +- } +- +-out: +- conversation_free(conv); +- return 0; +-} +- +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs) +-{ +- struct conversations_state *state = NULL; +- int r = 0; +- struct index_state *index_state = NULL; +- struct dlist *dl; +- hash_table wanted_cids = HASH_TABLE_INITIALIZER; +- strarray_t folder_list = STRARRAY_INITIALIZER; +- struct index_init init; +- int i; +- +- // this one expunges each mailbox it enters, so we need to lock exclusively +- r = conversations_open_user(imapd_userid, 0/*shared*/, &state); +- if (r) goto out; +- +- construct_hash_table(&wanted_cids, 1024, 0); +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- r = xconvfetch_lookup(state, dlist_num(dl), ifchangedsince, +- &wanted_cids, &folder_list); +- if (r) goto out; +- } +- +- /* unchanged, woot */ +- if (!folder_list.count) +- goto out; +- +- fetchargs->cidhash = &wanted_cids; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- +- for (i = 0; i < folder_list.count; i++) { +- const char *mboxname; +- mbentry_t *mbentry = NULL; +- +- if (state->folders_byname) mboxname = folder_list.data[i]; +- else { +- mboxlist_lookup_by_uniqueid(folder_list.data[i], &mbentry, NULL); +- if (!mbentry) continue; +- mboxname = mbentry->name; +- } +- +- r = index_open(mboxname, &init, &index_state); +- mboxlist_entry_free(&mbentry); +- if (r == IMAP_MAILBOX_NONEXISTENT) +- continue; +- if (r) +- goto out; +- +- index_checkflags(index_state, 0, 0); +- +- /* make sure \Deleted messages are expunged. Will also lock the +- * mailbox state and read any new information */ +- r = index_expunge(index_state, NULL, 1); +- +- if (!r) +- index_fetchresponses(index_state, NULL, /*usinguid*/1, +- fetchargs, NULL); +- +- index_close(&index_state); +- +- if (r) goto out; +- } +- +- r = 0; +- +-out: +- index_close(&index_state); +- conversations_commit(&state); +- free_hash_table(&wanted_cids, NULL); +- strarray_fini(&folder_list); +- return r; +-} +- + #undef PARSE_PARTIAL /* cleanup */ + + /* +@@ -6209,314 +5765,6 @@ error: + freesearchargs(searchargs); + } + +-/* +- * Perform a XCONVSORT or XCONVUPDATES command +- */ +-void cmd_xconvsort(char *tag, int updates) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct index_init init; +- struct index_state *oldstate = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvsort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, updates); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- if (updates) { +- /* in XCONVUPDATES, need to force a re-read from scratch into +- * a new index, because we ask for deleted messages */ +- +- oldstate = imapd_index; +- imapd_index = NULL; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- init.want_expunged = 1; +- +- r = index_open(index_mboxname(oldstate), &init, &imapd_index); +- if (r) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- index_checkflags(imapd_index, 0, 0); +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xconvsort\r\n", tag); +- goto error; +- } +- +- if (updates) +- r = index_convupdates(imapd_index, sortcrit, searchargs, windowargs); +- else +- r = index_convsort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (oldstate) { +- index_close(&imapd_index); +- imapd_index = oldstate; +- } +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVSORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- if (oldstate) { +- if (imapd_index) index_close(&imapd_index); +- imapd_index = oldstate; +- } +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-/* +- * Perform a XCONVMULTISORT command. This is like XCONVSORT but returns +- * search results from multiple folders. It still requires a selected +- * mailbox, for two reasons: +- * +- * a) it's a useful shorthand for choosing what the current +- * conversations scope is, and +- * +- * b) the code to parse a search program currently relies on a selected +- * mailbox. +- * +- * Unlike ESEARCH it doesn't take folder names for scope, instead the +- * search scope is implicitly the current conversation scope. This is +- * implemented more or less by accident because both the Sphinx index +- * and the conversations database are hardcoded to be per-user. +- */ +-static void cmd_xconvmultisort(char *tag) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvmultisort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvMultiSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, /*updates*/0); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to XconvMultiSort\r\n", tag); +- goto error; +- } +- +- r = index_convmultisort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVMULTISORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-static void cmd_xsnippets(char *tag) +-{ +- int c; +- struct searchargs *searchargs = NULL; +- struct snippetargs *snippetargs = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xsnippets"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- c = get_snippetargs(&snippetargs); +- if (c == EOF) { +- prot_printf(imapd_out, "%s BAD Syntax error in snippet arguments\r\n", tag); +- goto error; +- } +- if (c != ' ') { +- prot_printf(imapd_out, +- "%s BAD Unexpected arguments in Xsnippets\r\n", tag); +- goto error; +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xsnippets\r\n", tag); +- goto error; +- } +- +- r = index_snippets(imapd_index, snippetargs, searchargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- freesearchargs(searchargs); +- free_snippetargs(&snippetargs); +- return; +- +-error: +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- + static void cmd_xstats(char *tag) + { + int metric; +@@ -10897,81 +10145,6 @@ out_noprint: + seqset_free(&uids); + } + +-static void free_snippetargs(struct snippetargs **sap) +-{ +- while (*sap) { +- struct snippetargs *sa = *sap; +- *sap = sa->next; +- free(sa->mboxname); +- free(sa->uids.data); +- free(sa); +- } +-} +- +-static int get_snippetargs(struct snippetargs **sap) +-{ +- int c; +- struct snippetargs **prevp = sap; +- struct snippetargs *sa = NULL; +- struct buf arg = BUF_INITIALIZER; +- uint32_t uid; +- char *intname = NULL; +- +- c = prot_getc(imapd_in); +- if (c != '(') goto syntax_error; +- +- for (;;) { +- c = prot_getc(imapd_in); +- if (c == ')') break; +- if (c != '(') goto syntax_error; +- +- c = getastring(imapd_in, imapd_out, &arg); +- if (c != ' ') goto syntax_error; +- +- intname = mboxname_from_external(buf_cstring(&arg), &imapd_namespace, imapd_userid); +- +- /* allocate a new snippetargs */ +- sa = xzmalloc(sizeof(struct snippetargs)); +- sa->mboxname = xstrdup(intname); +- /* append to the list */ +- *prevp = sa; +- prevp = &sa->next; +- +- c = getuint32(imapd_in, &sa->uidvalidity); +- if (c != ' ') goto syntax_error; +- +- c = prot_getc(imapd_in); +- if (c != '(') break; +- for (;;) { +- c = getuint32(imapd_in, &uid); +- if (c != ' ' && c != ')') goto syntax_error; +- if (sa->uids.count + 1 > sa->uids.alloc) { +- sa->uids.alloc += 64; +- sa->uids.data = xrealloc(sa->uids.data, +- sizeof(uint32_t) * sa->uids.alloc); +- } +- sa->uids.data[sa->uids.count++] = uid; +- if (c == ')') break; +- } +- +- c = prot_getc(imapd_in); +- if (c != ')') goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') goto syntax_error; +- +-out: +- free(intname); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free_snippetargs(sap); +- c = EOF; +- goto out; +-} +- + static void cmd_dump(char *tag, char *name, int uid_start) + { + int r = 0; +@@ -12467,185 +11640,6 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + return EOF; + } + +-static int parse_windowargs(const char *tag, +- struct windowargs **wa, +- int updates) +-{ +- struct windowargs windowargs; +- struct buf arg = BUF_INITIALIZER; +- struct buf ext_folder = BUF_INITIALIZER; +- int c; +- +- memset(&windowargs, 0, sizeof(windowargs)); +- +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c != '(') { +- /* no window args at all */ +- prot_ungetc(c, imapd_in); +- goto out; +- } +- +- for (;;) +- { +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c == ')') +- break; /* end of window args */ +- +- prot_ungetc(c, imapd_in); +- c = getword(imapd_in, &arg); +- if (!arg.len) +- goto syntax_error; +- +- if (!strcasecmp(arg.s, "CONVERSATIONS")) +- windowargs.conversations = 1; +- else if (!strcasecmp(arg.s, "POSITION")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.position); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.position == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "ANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "MULTIANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getastring(imapd_in, imapd_out, &ext_folder); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "CHANGEDSINCE")) { +- if (!updates) +- goto syntax_error; +- windowargs.changedsince = 1; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getmodseq(imapd_in, &windowargs.modseq); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.uidnext); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- } else if (!strcasecmp(arg.s, "UPTO")) { +- if (!updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.upto); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- +- if (windowargs.upto == 0) +- goto syntax_error; +- } +- else +- goto syntax_error; +- +- if (c == ')') +- break; +- if (c != ' ') +- goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') +- goto syntax_error; +- +-out: +- /* these two are mutually exclusive */ +- if (windowargs.anchor && windowargs.position) +- goto syntax_error; +- /* changedsince is mandatory for XCONVUPDATES +- * and illegal for XCONVSORT */ +- if (!!updates != windowargs.changedsince) +- goto syntax_error; +- +- if (ext_folder.len) { +- windowargs.anchorfolder = mboxname_from_external(buf_cstring(&ext_folder), +- &imapd_namespace, +- imapd_userid); +- } +- +- *wa = xmemdup(&windowargs, sizeof(windowargs)); +- buf_free(&ext_folder); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free(windowargs.anchorfolder); +- buf_free(&ext_folder); +- prot_printf(imapd_out, "%s BAD Syntax error in window arguments\r\n", tag); +- if (c != EOF) prot_ungetc(c, imapd_in); +- return EOF; +-} +- +-static void free_windowargs(struct windowargs *wa) +-{ +- if (!wa) +- return; +- free(wa->anchorfolder); +- free(wa); +-} +- + /* + * Parse LIST selection options. + * The command has been parsed up to and including the opening '('. +-- +2.39.2 + + +From 7638ac52a6d3a27f4ed4ece8f4dfe849d76b1007 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 26 Feb 2024 10:11:15 -0500 +Subject: [PATCH 10/16] imapd.c: add 'maxliteral' option + +--- + changes/next/imap_literal_limits | 19 +++ + imap/imap_err.et | 3 + + imap/imapd.c | 214 ++++++++++++++++++++++--------- + imap/imapparse.c | 69 +++++----- + lib/imapoptions | 12 +- + lib/libconfig.c | 3 + + lib/libconfig.h | 1 + + 7 files changed, 229 insertions(+), 92 deletions(-) + create mode 100644 changes/next/imap_literal_limits + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +new file mode 100644 +index 000000000..c7fc35bbc +--- /dev/null ++++ b/changes/next/imap_literal_limits +@@ -0,0 +1,19 @@ ++Description: ++ ++Adds a config option to limit the size of a single literal allowed ++by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++ ++ ++Config changes: ++ ++New 'maxliteral' option. ++ ++ ++Upgrade instructions: ++ ++None. ++ ++ ++GitHub issue: ++ ++None. +diff --git a/imap/imap_err.et b/imap/imap_err.et +index d90e30a4b..5768f49d1 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_LITERAL_TOO_LARGE, ++ "[TOOBIG] Literal size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index aec48912b..0bf8c199e 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1435,7 +1435,7 @@ static void cmdloop(void) + if (c == '\r') goto missingargs; + if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/0); +@@ -1448,7 +1448,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1475,7 +1475,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 0, 0); + +@@ -1487,7 +1487,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, NULL); + +@@ -1532,7 +1532,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Examine")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -1563,7 +1563,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Getacl")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getacl(tag.s, arg1.s); + +@@ -1588,7 +1588,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquota")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquota(tag.s, arg1.s); + +@@ -1597,7 +1597,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquotaroot")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquotaroot(tag.s, arg1.s); + +@@ -1722,7 +1722,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1738,7 +1738,7 @@ static void cmdloop(void) + /* delete a mailbox locally only */ + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 1, 1); + +@@ -1751,7 +1751,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Myrights")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_myrights(tag.s, arg1.s); + +@@ -1761,7 +1761,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if(c == EOF) goto missingargs; ++ if(c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_mupdatepush(tag.s, arg1.s); + +@@ -1777,7 +1777,7 @@ static void cmdloop(void) + if (c == '\r') goto missingargs; + if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/1); +@@ -1812,7 +1812,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + havepartition = 1; + c = getword(imapd_in, &arg3); +@@ -1885,7 +1885,7 @@ static void cmdloop(void) + if (c == ' ') { + have_mbox = 1; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + have_mech = 1; + c = getword(imapd_in, &arg2); +@@ -1957,7 +1957,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Select")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -1983,7 +1983,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 1); +@@ -2001,7 +2001,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg2); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg3); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, arg3.s); + +@@ -2202,7 +2202,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 0); +@@ -2349,7 +2349,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Xmeid")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xmeid(tag.s, arg1.s); + } +@@ -2361,7 +2361,7 @@ static void cmdloop(void) + + do { + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto aps_missingargs; ++ if (c <= EOF) goto aps_missingargs; + + if (!strcmp(arg1.s, "mailboxes")) { + c = prot_getc(imapd_in); +@@ -2373,7 +2373,7 @@ static void cmdloop(void) + prot_ungetc(c, imapd_in); + do { + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) break; ++ if (c <= EOF) break; + strarray_push(&applepushserviceargs.mailboxes, arg2.s); + } while (c == ' '); + } +@@ -2459,6 +2459,8 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + missingargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Missing required argument to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); +@@ -2471,11 +2473,18 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + extraargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); + continue; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in %s\r\n", ++ tag.s, error_message(IMAP_LITERAL_TOO_LARGE), cmd.s); ++ continue; ++ + badsequence: + prot_printf(imapd_out, + "%s BAD Invalid sequence in %s\r\n", tag.s, cmd.s); +@@ -2648,10 +2657,14 @@ static void cmd_login(char *tag, char *user) + + if (!IS_EOL(c, imapd_in)) { + buf_free(&passwdbuf); +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to LOGIN\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in LOGIN\r\n", tag, error_message(c)); ++ } else { ++ prot_printf(imapd_out, ++ "%s BAD Unexpected extra arguments to LOGIN\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3065,10 +3078,16 @@ static void cmd_id(char *tag) + /* get field value */ + (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || + (c != ' ' && c != ')')) { +- prot_printf(imapd_out, +- "%s BAD Invalid field-value pair in Id\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Id\r\n", ++ tag, error_message(c)); ++ } ++ else { ++ prot_printf(imapd_out, ++ "%s BAD Invalid field-value pair in Id\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3758,6 +3777,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + else if (!strcasecmp(arg.s, "URL")) { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ' && c != ')') { + *parseerr = "Missing URL in Append command"; + return IMAP_PROTOCOL_ERROR; +@@ -3956,8 +3976,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + c = parse_annotate_store_data(tag, + /*permessage_flag*/1, + &curstage->annotations); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto cleanup; + } + qdiffs[QUOTA_ANNOTSTORAGE] += sizeentryatts(curstage->annotations); +@@ -4084,6 +4103,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + if (r == IMAP_PROTOCOL_ERROR && parseerr) { + prot_printf(imapd_out, "%s BAD %s\r\n", tag, parseerr); ++ } else if (r == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); + } else if (r == IMAP_BADURL) { + prot_printf(imapd_out, "%s NO [BADURL \"%s\"] %s\r\n", + tag, url, parseerr); +@@ -4624,8 +4645,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + /*permessage_flag*/1, + &fa->entries, + &fa->attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + if (c != ')') { +@@ -4738,6 +4758,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -4968,6 +4993,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -5483,8 +5513,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + c = parse_annotate_store_data(tag, /*permessage_flag*/1, + &storeargs.entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeflags; + } + storeargs.namespace = &imapd_namespace; +@@ -5658,6 +5687,12 @@ static void cmd_search(char *tag, int usinguid) + return; + } + ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Search\r\n", tag, error_message(c)); ++ freesearchargs(searchargs); ++ return; ++ } ++ + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Search\r\n", tag); + eatline(imapd_in, c); +@@ -5836,16 +5871,19 @@ static void cmd_thread(char *tag, int usinguid) + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +- freesearchargs(searchargs); +- return; ++ goto done; ++ } ++ ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Thread\r\n", tag, error_message(c)); ++ goto done; + } + + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to Thread\r\n", tag); + eatline(imapd_in, c); +- freesearchargs(searchargs); +- return; ++ goto done; + } + + n = index_thread(imapd_index, alg, searchargs, usinguid); +@@ -5854,6 +5892,7 @@ static void cmd_thread(char *tag, int usinguid) + prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, + error_message(IMAP_OK_COMPLETED), n, mytime); + ++ done: + freesearchargs(searchargs); + return; + } +@@ -7408,8 +7447,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == '(') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistselopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + return; + } + } +@@ -7421,6 +7459,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + + /* Read in reference name */ + c = getastring(imapd_in, imapd_out, &reference); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF && !*reference.s) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: reference name\r\n", +@@ -7443,6 +7482,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); + if (c != ' ') break; +@@ -7458,6 +7498,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + else { + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: mailbox pattern\r\n", +@@ -7472,8 +7513,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == ' ') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistretopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + } +@@ -7492,6 +7532,10 @@ static void getlistargs(char *tag, struct listargs *listargs) + + return; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in List\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ + freeargs: + strarray_fini(&listargs->pat); + strarray_fini(&listargs->metaitems); +@@ -8898,6 +8942,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -8925,6 +8970,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -8947,6 +8993,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute(s)\r\n", tag); +@@ -8974,6 +9021,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -8986,8 +9034,13 @@ static int parse_annotate_fetch_data(const char *tag, + return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9018,6 +9071,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9044,6 +9098,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* single entry -- add it to the list */ + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9062,8 +9117,13 @@ static int parse_metadata_string_or_list(const char *tag, + if (c == ' ' || c == '\r' || c == ')') return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9115,6 +9175,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -9135,6 +9196,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &attrib); + else + c = getqstring(imapd_in, imapd_out, &attrib); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -9148,6 +9210,7 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation value\r\n", tag); +@@ -9189,8 +9252,14 @@ static int parse_annotate_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9223,6 +9292,7 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9235,6 +9305,7 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata value\r\n", tag); +@@ -9275,7 +9346,7 @@ static int parse_metadata_store_data(const char *tag, + + if (c != ')') { + prot_printf(imapd_out, +- "%s BAD Missing close paren in annotation entry list \r\n", ++ "%s BAD Missing close paren in metadata entry list \r\n", + tag); + goto baddata; + } +@@ -9286,8 +9357,14 @@ static int parse_metadata_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + static void getannotation_response(const char *mboxname, +@@ -9472,8 +9549,7 @@ static void cmd_getannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_fetch_data(tag, /*permessage_flag*/0, &entries, &attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9707,8 +9783,10 @@ static void cmd_getmetadata(const char *tag) + while (nlists < 3) + { + c = parse_metadata_string_or_list(tag, &lists[nlists], &is_list[nlists]); ++ if (c <= EOF) goto freeargs; ++ + nlists++; +- if (c == '\r' || c == EOF) ++ if (c == '\r') + break; + } + +@@ -9865,8 +9943,7 @@ static void cmd_setannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_store_data(tag, 0, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9933,8 +10010,7 @@ static void cmd_setmetadata(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_metadata_store_data(tag, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -10042,6 +10118,10 @@ static void cmd_xwarmup(const char *tag) + /* parse arguments: expect '('')' */ + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Xwarmup\r\n", tag, error_message(c)); ++ goto out_noprint; ++ } + if (c != ' ') { + syntax_error: + prot_printf(imapd_out, "%s BAD syntax error in %s\r\n", tag, cmd); +@@ -11552,9 +11632,11 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_ANNOTATION; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ') goto missingarg; + (*sortcrit)[n].args.annot.entry = xstrdup(criteria.s); + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + if (!strcmp(criteria.s, "value.shared")) + userid = ""; +@@ -11572,6 +11654,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11585,6 +11668,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASCONVFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11690,10 +11774,10 @@ static int getlistselopts(char *tag, struct listargs *args) + + strarray_t options = STRARRAY_INITIALIZER; + c = parse_metadata_string_or_list(tag, &options, NULL); ++ if (c <= EOF) return c; + parse_getmetadata_options(&options, &opts); + args->metaopts = opts; + strarray_fini(&options); +- if (c == EOF) return EOF; + } else { + prot_printf(imapd_out, + "%s BAD Invalid List selection option \"%s\"\r\n", +@@ -11721,7 +11805,7 @@ static int getlistselopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -11791,7 +11875,7 @@ static int getlistretopts(char *tag, struct listargs *args) + args->ret |= LIST_RET_METADATA; + /* outputs the error for us */ + c = parse_metadata_string_or_list(tag, &args->metaitems, NULL); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else { + prot_printf(imapd_out, +@@ -11812,7 +11896,7 @@ static int getlistretopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -12971,6 +13055,11 @@ static void cmd_urlfetch(char *tag) + else prot_ungetc(c, imapd_in); + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Urlfetch\r\n", ++ tag, error_message(c)); ++ return; ++ } + (void)prot_putc(' ', imapd_out); + prot_printstring(imapd_out, arg.s); + +@@ -13203,6 +13292,11 @@ static void cmd_genurlauth(char *tag) + char *intname = NULL; + + c = getastring(imapd_in, imapd_out, &arg1); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Genurlauth\r\n", ++ tag, error_message(c)); ++ return; ++ } + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing required argument to Genurlauth\r\n", +diff --git a/imap/imapparse.c b/imap/imapparse.c +index d195630a5..f04ad282a 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -159,6 +159,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* Fail per RFC 7888, Section 4, choice 2 */ + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +@@ -181,6 +185,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + if (!isnowait) { ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ return IMAP_LITERAL_TOO_LARGE; ++ } ++ + prot_printf(pout, "+ go ahead\r\n"); + prot_flush(pout); + } +@@ -689,7 +697,7 @@ static int get_search_annotation(struct protstream *pin, + + /* parse the value */ + c = getbnstring(pin, pout, &value); +- if (c == EOF) ++ if (c <= EOF) + goto bad; + + sa = xzmalloc(sizeof(*sa)); +@@ -710,6 +718,7 @@ bad: + buf_free(&attrib); + buf_free(&value); + ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != EOF) prot_ungetc(c, pin); + return EOF; + } +@@ -842,7 +851,7 @@ static int get_search_criterion(struct protstream *pin, + do { + c = get_search_criterion(pin, pout, e, base); + } while (c == ' '); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ')') { + prot_printf(pout, "%s BAD Missing required close paren in Search command\r\n", + base->tag); +@@ -878,7 +887,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "annotation")) { /* RFC 5257 */ + struct searchannot *annot = NULL; + c = get_search_annotation(pin, pout, base, c, &annot); +- if (c == EOF) ++ if (c <= EOF) + goto badcri; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("annotation"); +@@ -899,23 +908,15 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "bcc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "body")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } +- else if (!strcmp(criteria.s, "fuzzy")) { +- if (c != ' ') goto missingarg; +- base->fuzzy_depth++; +- c = get_search_criterion(pin, pout, parent, base); +- base->fuzzy_depth--; +- if (c == EOF) return EOF; +- break; +- } + else goto badcri; + break; + +@@ -923,7 +924,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "cc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (hasconv && !strcmp(criteria.s, "convflag")) { /* nonstandard */ +@@ -986,7 +987,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "deliveredto")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -996,7 +997,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "emailid")) { /* RFC 8474 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1009,7 +1010,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "folder")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("folder"); + e->value.s = mboxname_from_external(arg.s, base->namespace, base->userid); +@@ -1017,7 +1018,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "from")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "fuzzy")) { /* RFC 6203 */ +@@ -1025,7 +1026,7 @@ static int get_search_criterion(struct protstream *pin, + base->fuzzy_depth++; + c = get_search_criterion(pin, pout, parent, base); + base->fuzzy_depth--; +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else goto badcri; + break; +@@ -1036,7 +1037,7 @@ static int get_search_criterion(struct protstream *pin, + c = getastring(pin, pout, &arg); + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg2); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find_field(arg.s); +@@ -1098,7 +1099,7 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_NOT); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "new")) { /* RFC 3501 */ + e = search_expr_new(parent, SEOP_AND); +@@ -1113,10 +1114,10 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_OR); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ' ') goto missingarg; + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "old")) { /* RFC 3501 */ + indexflag_match(parent, MESSAGE_RECENT, /*not*/1); +@@ -1235,6 +1236,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spamabove")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_GE); + e->attr = search_attr_find("spamscore"); +@@ -1243,6 +1245,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spambelow")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_LT); + e->attr = search_attr_find("spamscore"); +@@ -1251,7 +1254,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "subject")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1261,19 +1264,19 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "to")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "text")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "threadid")) { /* RFC 8474 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1323,25 +1326,25 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "xattachmentname")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentname", base); + } + else if (!strcmp(criteria.s, "xattachmentbody")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentbody", base); + } + else if (!strcmp(criteria.s, "xlistid")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "listid", base); + } + else if (!strcmp(criteria.s, "xcontenttype")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "contenttype", base); + } + else goto badcri; +@@ -1372,6 +1375,8 @@ static int get_search_criterion(struct protstream *pin, + + default: + badcri: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Invalid Search criteria\r\n", base->tag); + if (c != EOF) prot_ungetc(c, pin); + return EOF; +@@ -1384,6 +1389,8 @@ static int get_search_criterion(struct protstream *pin, + return c; + + missingarg: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Missing required argument to Search %s\r\n", + base->tag, criteria.s); + if (c != EOF) prot_ungetc(c, pin); +diff --git a/lib/imapoptions b/lib/imapoptions +index 3d43d1c36..735b40897 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1660,11 +1660,21 @@ Blank lines and lines beginning with ``#'' are ignored. + messages larger than \fImaxmessagesize\fR bytes. If set to 0, this + will allow messages of any size (the default). */ + ++{ "maxliteral", 131072, INT, "UNRELEASED" } ++/* Maximum size in bytes of a single literal allowed by the IMAP parser. ++.PP ++ Literals used for message [part] data in APPEND are only limited by ++ the 'maxmessagesize' option. ++.PP ++ If the 'literalminus' option is enabled, non-synchonizing literals ++ will be limited to the lesser of 4K and either 'maxliteral' or ++ 'maxmessagesize', depending on the use-case. */ ++ + { "maxquoted", 131072, INT, "2.3.17" } + /* Maximum size of a single quoted string for the parser. Default 128k */ + + { "maxword", 131072, INT, "2.3.17" } +-/* Maximum size of a single word for the parser. Default 128k */ ++/* Maximum size of a single word allowed by the IMAP parser. Default 128k */ + + { "mboxkey_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" } + /* The cyrusdb backend to use for mailbox keys. */ +diff --git a/lib/libconfig.c b/lib/libconfig.c +index 0ec8b2526..a30b038d6 100644 +--- a/lib/libconfig.c ++++ b/lib/libconfig.c +@@ -84,6 +84,7 @@ EXPORTED int config_auditlog; + EXPORTED int config_iolog; + EXPORTED unsigned config_maxword; + EXPORTED unsigned config_maxquoted; ++EXPORTED unsigned config_maxliteral; + EXPORTED int config_qosmarking; + EXPORTED int config_debug; + +@@ -473,6 +474,7 @@ EXPORTED void config_reset(void) + config_defdomain = NULL; + config_auditlog = 0; + config_serverinfo = 0; ++ config_maxliteral = 0; + config_maxquoted = 0; + config_maxword = 0; + config_qosmarking = 0; +@@ -665,6 +667,7 @@ EXPORTED void config_read(const char *alt_config, const int config_need_data) + config_serverinfo = config_getenum(IMAPOPT_SERVERINFO); + + /* set some limits */ ++ config_maxliteral = config_getint(IMAPOPT_MAXLITERAL); + config_maxquoted = config_getint(IMAPOPT_MAXQUOTED); + config_maxword = config_getint(IMAPOPT_MAXWORD); + +diff --git a/lib/libconfig.h b/lib/libconfig.h +index dd9eee2e3..8c8fed54a 100644 +--- a/lib/libconfig.h ++++ b/lib/libconfig.h +@@ -89,6 +89,7 @@ extern enum enum_value config_virtdomains; + extern enum enum_value config_mupdate_config; + extern int config_auditlog; + extern int config_iolog; ++extern unsigned config_maxliteral; + extern unsigned config_maxquoted; + extern unsigned config_maxword; + extern int config_qosmarking; +-- +2.39.2 + + +From d06d8d072e0a6517e2205929ad0cf2af83ba76d8 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:18:52 -0500 +Subject: [PATCH 11/16] prot.h: change bytes_in/out to uint64_t (for + long-running imapd) + +--- + backup/lcb.c | 4 ++-- + backup/lcb_compact.c | 4 ++-- + backup/lcb_verify.c | 8 ++++---- + imap/httpd.c | 14 ++++++++------ + imap/imapd.c | 18 ++++++++++-------- + imap/pop3d.c | 18 ++++++++++-------- + lib/prot.h | 8 ++++---- + 7 files changed, 40 insertions(+), 34 deletions(-) + +diff --git a/backup/lcb.c b/backup/lcb.c +index 3f68b1aaa..e1eefab89 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -620,11 +620,11 @@ EXPORTED int backup_reindex(const char *name, + const char *error = prot_error(member); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + name, member_offset, prot_bytes_in(member), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + member_offset, prot_bytes_in(member), error); + + r = IMAP_IOERROR; +diff --git a/backup/lcb_compact.c b/backup/lcb_compact.c +index 5fec296fc..779f8604b 100644 +--- a/backup/lcb_compact.c ++++ b/backup/lcb_compact.c +@@ -528,11 +528,11 @@ EXPORTED int backup_compact(const char *name, + const char *error = prot_error(in); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + name, chunk->offset, prot_bytes_in(in), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + chunk->offset, prot_bytes_in(in), error); + + /* chunk is corrupt, discard the rest of it and get on with +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index 88e748c51..cfda6c93b 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -234,10 +234,10 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, message->id, message->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + message->id, message->offset, prot_bytes_in(ps), error); + } + prot_free(ps); +@@ -540,10 +540,10 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, chunk->id, chunk->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + chunk->id, chunk->offset, prot_bytes_in(ps), error); + r = EOF; + } +diff --git a/imap/httpd.c b/imap/httpd.c +index 32ea446e6..68784ae09 100644 +--- a/imap/httpd.c ++++ b/imap/httpd.c +@@ -679,8 +679,8 @@ EXPORTED int http1_resp_body_chunk(struct transaction_t *txn, + static void httpd_reset(struct http_connection *conn) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + /* run any delayed actions */ + libcyrus_run_delayed(); +@@ -722,7 +722,8 @@ static void httpd_reset(struct http_connection *conn) + + if (config_auditlog) { + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + } + +@@ -1104,8 +1105,8 @@ void usage(void) + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1174,7 +1175,8 @@ void shut_down(int code) + + if (config_auditlog) + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + + saslprops_free(&saslprops); +diff --git a/imap/imapd.c b/imap/imapd.c +index 0bf8c199e..9341f041c 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -730,8 +730,8 @@ static int mlookup(const char *tag, const char *ext_name, + static void imapd_reset(void) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + /* run delayed commands first before closing anything */ + libcyrus_run_delayed(); +@@ -779,8 +779,9 @@ static void imapd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + imapd_in = imapd_out = NULL; + +@@ -1074,8 +1075,8 @@ void shut_down(int code) __attribute__((noreturn)); + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1142,8 +1143,9 @@ void shut_down(int code) + : CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_OK); + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + if (protin) protgroup_free(protin); + +diff --git a/imap/pop3d.c b/imap/pop3d.c +index ee5a6fad8..3db7e2315 100644 +--- a/imap/pop3d.c ++++ b/imap/pop3d.c +@@ -325,8 +325,8 @@ static struct sasl_callback mysasl_cb[] = { + + static void popd_reset(void) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + proc_cleanup(); + +@@ -361,8 +361,9 @@ static void popd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + popd_in = popd_out = NULL; + +@@ -598,8 +599,8 @@ static void usage(void) + */ + void shut_down(int code) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -646,8 +647,9 @@ void shut_down(int code) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + #ifdef HAVE_SSL + tls_shutdown_serverengine(); +diff --git a/lib/prot.h b/lib/prot.h +index 89b0b0a2a..94b22fad8 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -131,8 +131,8 @@ struct protstream { + struct buf *writetobuf; + + int can_unget; +- int bytes_in; +- int bytes_out; ++ uint64_t bytes_in; ++ uint64_t bytes_out; + int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ +@@ -224,8 +224,8 @@ extern int prot_free(struct protstream *s); + extern int prot_setlog(struct protstream *s, int fd); + + /* Get traffic counts */ +-extern int prot_bytes_in(struct protstream *s); +-extern int prot_bytes_out(struct protstream *s); ++extern uint64_t prot_bytes_in(struct protstream *s); ++extern uint64_t prot_bytes_out(struct protstream *s); + #define prot_bytes_in(s) ((s)->bytes_in) + #define prot_bytes_out(s) ((s)->bytes_out) + +-- +2.39.2 + + +From 800100d1c062b307d88c6590554da5383e6e6239 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:35:44 -0500 +Subject: [PATCH 12/16] imapd.c: rename 'maxsize' to 'maxmsgsize' + +--- + imap/imapd.c | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 9341f041c..06a32afef 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -135,7 +135,7 @@ static int imaps = 0; + static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; +-static size_t maxsize = 0; ++static size_t maxmsgsize = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -900,8 +900,8 @@ int service_init(int argc, char **argv, char **envp) + + prometheus_increment(CYRUS_IMAP_READY_LISTENERS); + +- maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE); +- if (!maxsize) maxsize = UINT32_MAX; ++ maxmsgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); ++ if (!maxmsgsize) maxmsgsize = UINT32_MAX; + + return 0; + } +@@ -3486,7 +3486,7 @@ static void capa_response(int flags) + prot_printf(imapd_out, " IDLE"); + } + +- prot_printf(imapd_out, " APPENDLIMIT=%zu", maxsize); ++ prot_printf(imapd_out, " APPENDLIMIT=%zu", maxmsgsize); + } + + /* +@@ -4006,13 +4006,13 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxsize, &size, ++ r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From 333de29fd6f1253000b364b402e2771e70c1200f Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 15:08:42 -0500 +Subject: [PATCH 13/16] imapd.c: limit the total size of IMAP command arguments + +Only concerned with commands that can have an unlimited +number of arguments. +--- + changes/next/imap_literal_limits | 7 ++- + imap/imap_err.et | 3 + + imap/imapd.c | 97 +++++++++++++++++++++++++++++--- + imap/imapd.h | 1 + + imap/imapparse.c | 10 ++++ + lib/imapoptions | 5 ++ + 6 files changed, 112 insertions(+), 11 deletions(-) + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +index c7fc35bbc..f1ea34a0b 100644 +--- a/changes/next/imap_literal_limits ++++ b/changes/next/imap_literal_limits +@@ -1,12 +1,13 @@ + Description: + +-Adds a config option to limit the size of a single literal allowed +-by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++Adds config options to limit the size of a single literal allowed ++by the IMAP parser and to limit the total size of IMAP command arguments. ++Also properly applies LITERAL- to IMAP APPEND. + + + Config changes: + +-New 'maxliteral' option. ++New 'maxliteral' and 'maxargssize' options. + + + Upgrade instructions: +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 5768f49d1..d1391199d 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_ARGS_TOO_LARGE, ++ "[TOOBIG] Command arguments total size exceeds fixed limit" ++ + ec IMAP_LITERAL_TOO_LARGE, + "[TOOBIG] Literal size exceeds fixed limit" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 06a32afef..f17e789f1 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -136,6 +136,8 @@ static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; + static size_t maxmsgsize = 0; ++static int64_t maxargssize = 0; ++static uint64_t maxargssize_mark = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -903,6 +905,9 @@ int service_init(int argc, char **argv, char **envp) + maxmsgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); + if (!maxmsgsize) maxmsgsize = UINT32_MAX; + ++ maxargssize = config_getint(IMAPOPT_MAXARGSSIZE); ++ if (maxargssize <= 0) maxargssize = UINT32_MAX; ++ + return 0; + } + +@@ -1352,6 +1357,9 @@ static void cmdloop(void) + allowed when not logged in */ + if (!imapd_userid && !strchr("AELNCIS", cmd.s[0])) goto nologin; + ++ /* Set limit on the total number of bytes allowed for arguments */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + maxargssize; ++ + /* Start command timer */ + cmdtime_starttimer(); + +@@ -3923,6 +3931,9 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + curstage = xzmalloc(sizeof(*curstage)); + ptrarray_push(&stages, curstage); + ++ /* Set limit on the total number of bytes allowed for mailbox+append-opts */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + (maxargssize - strlen(name)); ++ + /* now parsing "append-opts" in the ABNF */ + + /* Parse flags */ +@@ -3931,6 +3942,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + strarray_init(&curstage->flags); + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4038,15 +4051,23 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + } + + done: +- if (r) { +- eatline(imapd_in, c); +- } else { ++ switch (r) { ++ case IMAP_ZERO_LENGTH_LITERAL: ++ case IMAP_MESSAGE_TOO_LARGE: ++ break; ++ ++ case 0: + /* we should be looking at the end of the line */ +- if (!IS_EOL(c, imapd_in)) { +- parseerr = "junk after literal"; +- r = IMAP_PROTOCOL_ERROR; +- eatline(imapd_in, c); +- } ++ if (IS_EOL(c, imapd_in)) break; ++ ++ parseerr = "junk after literal"; ++ r = IMAP_PROTOCOL_ERROR; ++ ++ GCC_FALLTHROUGH ++ ++ default: ++ eatline(imapd_in, c); ++ break; + } + + /* Append from the stage(s) */ +@@ -4258,6 +4279,9 @@ static void cmd_select(char *tag, char *cmd, char *name) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto badlist; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { + client_capa |= CAPA_CONDSTORE; +@@ -4630,6 +4654,9 @@ static int parse_fetch_args(const char *tag, const char *cmd, + c = getword(imapd_in, &fetchatt); + } + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + switch (fetchatt.s[0]) { + case 'A': +@@ -4760,6 +4787,8 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5094,6 +5123,9 @@ badannotation: + } + do { + c = getword(imapd_in, &fetchatt); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { + if (c != ' ') { +@@ -5440,6 +5472,9 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + do { + c = getword(imapd_in, &storemod); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { + if (c != ' ') { +@@ -5532,6 +5567,8 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + for (;;) { + c = getword(imapd_in, &flagname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -5660,6 +5697,8 @@ static void cmd_search(char *tag, int usinguid) + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); + ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + /* Set FUZZY search according to config and quirks */ + static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always"; + char *inbox = mboxname_user_mbox(imapd_userid, NULL); +@@ -5754,6 +5793,9 @@ static void cmd_sort(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + if (imapd_id.quirks & QUIRK_SEARCHFUZZY) + searchargs->fuzzy_depth++; + +@@ -5870,6 +5912,9 @@ static void cmd_thread(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +@@ -7484,6 +7529,8 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8333,6 +8380,9 @@ void cmd_setquota(const char *tag, const char *quotaroot) + newquotas[res] = limit; + if (c == ')') break; + else if (c != ' ') goto badlist; ++ ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8485,6 +8535,9 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto bad; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { + statusitems |= STATUS_MESSAGES; +@@ -8868,6 +8921,9 @@ static int parsecreateargs(struct dlist **extargs) + /* new style RFC 4466 arguments */ + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + name = ucase(arg.s); + if (c != ' ') goto fail; + c = prot_getc(imapd_in); +@@ -8876,6 +8932,9 @@ static int parsecreateargs(struct dlist **extargs) + sub = dlist_newlist(res, name); + do { + c = getword(imapd_in, &val); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + dlist_setatom(sub, name, val.s); + } while (c == ' '); + if (c != ')') goto fail; +@@ -8944,6 +9003,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -8995,6 +9056,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9073,6 +9136,8 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9177,6 +9242,8 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9219,6 +9286,9 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); + +@@ -9294,6 +9364,8 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9307,6 +9379,8 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11595,6 +11669,9 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + nsort = 0; + n = 0; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ + nsort += SORTGROWSIZE; +@@ -11744,6 +11821,8 @@ static int getlistselopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -11848,6 +11927,8 @@ static int getlistretopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapd.h b/imap/imapd.h +index 39dd6d0d9..2030b7466 100644 +--- a/imap/imapd.h ++++ b/imap/imapd.h +@@ -222,6 +222,7 @@ struct searchargs { + int state; + /* used only during parsing */ + int fuzzy_depth; ++ uint64_t maxargssize_mark; + + /* For ESEARCH & XCONVMULTISORT */ + const char *tag; +diff --git a/imap/imapparse.c b/imap/imapparse.c +index f04ad282a..0e8bdc5dd 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -634,6 +634,11 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + goto bad; + } + ++ if (searchargs->maxargssize_mark && ++ prot_bytes_in(pin) > searchargs->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + } while (c == ' '); + + /* RFC 4731: +@@ -1382,6 +1387,11 @@ static int get_search_criterion(struct protstream *pin, + return EOF; + } + ++ if (base->maxargssize_mark && ++ prot_bytes_in(pin) > base->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + if (!keep_charset) + base->state &= ~GETSEARCH_CHARSET_KEYWORD; + base->state &= ~GETSEARCH_RETURN; +diff --git a/lib/imapoptions b/lib/imapoptions +index 735b40897..dac3ed0b8 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1655,6 +1655,11 @@ Blank lines and lines beginning with ``#'' are ignored. + /* Maximum number of logged in sessions allowed per user, + zero means no limit */ + ++{ "maxargssize", 0, INT, "UNRELEASED" } ++/* Maximum total size of arguments to an IMAP command that will be ++ accepted by Cyrus. ++ Commands with arguments that exceed this limit will be rejected. ++ + { "maxmessagesize", 0, INT, "2.3.17" } + /* Maximum incoming LMTP message size. If non-zero, lmtpd will reject + messages larger than \fImaxmessagesize\fR bytes. If set to 0, this +-- +2.39.2 + + +From dcc88bb665836addd6ca9d641b2a0c0ee483c61c Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Tue, 12 Mar 2024 23:16:30 -0400 +Subject: [PATCH 14/16] Add IMAPLimits.pm + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 518 ++++++++++++++++++++++ + cassandane/Cassandane/IMAPMessageStore.pm | 8 +- + 2 files changed, 523 insertions(+), 3 deletions(-) + create mode 100644 cassandane/Cassandane/Cyrus/IMAPLimits.pm + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +new file mode 100644 +index 000000000..2275c5cf7 +--- /dev/null ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -0,0 +1,518 @@ ++#!/usr/bin/perl ++# ++# Copyright (c) 2011-2023 FastMail Pty Ltd. 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. The name "Fastmail Pty Ltd" must not be used to ++# endorse or promote products derived from this software without ++# prior written permission. For permission or any legal ++# details, please contact ++# FastMail Pty Ltd ++# PO Box 234 ++# Collins St West 8007 ++# Victoria ++# Australia ++# ++# 4. Redistributions of any form whatsoever must retain the following ++# acknowledgment: ++# "This product includes software developed by Fastmail Pty. Ltd." ++# ++# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, ++# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO ++# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT ++# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF ++# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER ++# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE ++# OF THIS SOFTWARE. ++# ++ ++package Cassandane::Cyrus::IMAPLimits; ++use strict; ++use warnings; ++use Mail::JMAPTalk 0.13; ++use Data::Dumper; ++ ++use lib '.'; ++use base qw(Cassandane::Cyrus::TestCase); ++use Cassandane::Util::Log; ++ ++my $email = < ++ ++Body ++EOF ++ ++$email =~ s/\r?\n/\r\n/gs; ++ ++my $toobig_email = $email . "X" x 100; ++ ++sub assert_bye_toobig ++{ ++ my ($self, $store) = @_; ++ ++ $store = $self->{store} if (!defined $store); ++ ++ # We want to override Mail::IMAPTalk's builtin handling of the BYE ++ # untagged response, as it will 'die' immediately without parsing ++ # the remainder of the line and especially without picking out the ++ # [TOOBIG] response code that we want to see. ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ bye => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ # Check that we got a BYE [TOOBIG] response ++ $store->idle_response($handlers, 1); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub assert_cmd_bye_toobig ++{ ++ my $self = shift; ++ my $cmd = shift; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->enable('qresync'); # IMAPTalk requires lower-case ++ $talk->select('INBOX'); ++ ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_bye_toobig(); ++} ++ ++sub assert_cmd_no_toobig ++{ ++ my $self = shift; ++ my $talk = shift; ++ my $cmd = shift; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ # Pick out the [TOOBIG] response code ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ $talk->_imap_cmd($cmd, 0, $handlers, @_); ++ ++ # Check that we got a NO [TOOBIG] response ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub new ++{ ++ my $class = shift; ++ ++ my $config = Cassandane::Config->default()->clone(); ++ $config->set(maxword => 25); ++ $config->set(maxquoted => 25); ++ $config->set(maxliteral => 25); ++ $config->set(literalminus => 1); ++ $config->set(maxargssize => 45); ++ $config->set(maxmessagesize => 100); ++ $config->set(event_groups => "message mailbox applepushservice"); ++ $config->set(aps_topic => "mail"); ++ ++ return $class->SUPER::new({ ++ adminstore => 1, ++ config => $config, ++ services => ['imap'], ++ }, @_); ++} ++ ++sub set_up ++{ ++ my ($self) = @_; ++ $self->SUPER::set_up(); ++} ++ ++sub tear_down ++{ ++ my ($self) = @_; ++ $self->SUPER::tear_down(); ++} ++ ++sub test_maxword ++{ ++ my ($self) = @_; ++ ++ # Oversized command name ++ $self->assert_cmd_bye_toobig("X" x 26); ++} ++ ++sub test_maxword_astring ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', "X" x 26); ++} ++ ++sub test_maxquoted ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', { Quote => "X" x 26 }); ++} ++ ++sub test_maxliteral_nosync ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " SELECT {26+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_maxliteral_sync ++{ ++ my ($self) = @_; ++ ++ # Unlike oversized non-sync literals which fatal() in one central location, ++ # oversized sync literals fail with a NO response in multiple places, ++ # so we test as many of those places as possible. ++ # Having said that, arguments parsed in cmdloop() or in get_search_criterion() ++ # are mostly handled centrally. ++ ++ # Authenticated State ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'SELECT', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Literal => "X" x 26 } ]); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Quote => 'foo' }, { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Quote => '' }, { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LISTRIGHTS', ++ 'INBOX', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETACL', ++ 'INBOX', 'anyone', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ '/comment', { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ 'FOO', { Literal => "X" x 26 }); ++ ++ # Selected State ++ $talk->select('INBOX'); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'ANNOTATION', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'BODY[HEADER.FIELDS', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'RFC822.HEADER.LINES', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Quote => 'value' }, ++ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', 'SUBJECT', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', ++ 'value', { Literal => "X" x 26 } ); ++} ++ ++sub test_maxargssize_append_flags ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ [ "X" x 25, "X" x 25 ], { Literal => $email } ); ++} ++ ++sub test_maxargssize_append_annot ++{ ++ my ($self) = @_; ++ ++ # Use MULTIAPPEND, fail the second ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ { Literal => $email }, ++ 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ], ++ { Literal => $email } ); ++} ++ ++sub test_maxargssize_create ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', "X" x 25, [ "X" x 25 ] ); ++} ++ ++sub test_maxargssize_create_ext ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', ++ "X" x 5, [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_fetch ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY', 'ENVELOPE', 'FLAGS', ++ 'INTERNALDATE', 'RFC822.SIZE' ]); ++} ++ ++sub test_maxargssize_fetch_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ [ "X" x 25, "X" x 25 ] ], "X" x 5 ] ); ++} ++ ++sub test_maxargssize_fetch_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ] ); ++} ++ ++sub test_maxargssize_fetch_headers ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY[HEADER.FIELDS', [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_getmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('GETMETADATA', 'INBOX', [ "X" x 25, "X" x 25 ] ); ++} ++ ++sub test_maxargssize_list_multi ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', { Quote => '' }, [ "X" x 25, "X" x 25 ]); ++} ++ ++sub test_maxargssize_list_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ [ 'SUBSCRIBED', 'REMOTE', ++ 'RECURSIVEMATCH', 'SPECIAL-USE' ], ++ { Quote => '' }, '*'); ++} ++ ++sub test_maxargssize_list_return ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ { Quote => '' }, '*', 'RETURN', ++ [ 'SUBSCRIBED', 'CHILDREN', ++ 'MYRIGHTS', 'SPECIAL-USE' ] ); ++} ++ ++sub test_maxargssize_search ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SEARCH', ++ 'TEXT', "X" x 25, 'TEXT', { Quote => "X" x 25 } ); ++} ++ ++sub test_maxargssize_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SELECT', 'INBOX', ++ [ 'QRESYNC', [ '1234567890', '1234567890' ], ++ 'ANNOTATE' ] ); ++} ++ ++sub test_maxargssize_setmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ "X" x 25, { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setmetadata2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ '/shared', { Quote => "X" x 25 }, ++ '/shared', { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setquota ++{ ++ my ($self) = @_; ++ ++ my $store = $self->{adminstore}; ++ my $talk = $store->get_client(); ++ ++ $talk->_send_cmd('SETQUOTA', 'user.cassandane', ++ [ 'STORAGE', '1234567890', ++ 'MESSAGE', '1234567890', ++ 'MAILBOX', '1234567890' ] ); ++ $self->assert_bye_toobig($store); ++} ++ ++sub test_maxargssize_sort ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SORT', ++ [ 'ARRIVAL', 'CC', 'DATE', ++ 'FROM', 'REVERSE', 'SIZE', 'TO' ], ++ 'UTF-8', 'ALL'); ++} ++ ++sub test_maxargssize_status ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STATUS', 'INBOX', ++ [ 'MESSAGES', 'UIDNEXT', ++ 'UIDVALIDITY', 'UNSEEN', 'SIZE' ] ); ++} ++ ++sub test_maxargssize_store_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_maxargssize_store_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ], ++ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_append_zero ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_cmd('APPEND', 0, '', 'INBOX', { Literal => '' } ); ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++} ++ ++sub test_maxmessagesize_sync_literal ++{ ++ my ($self) = @_; ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'APPEND', ++ 'INBOX', { Literal => $toobig_email } ); ++} ++ ++sub test_maxmessagesize_nosync_literal ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_literal_minus ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++1; +diff --git a/cassandane/Cassandane/IMAPMessageStore.pm b/cassandane/Cassandane/IMAPMessageStore.pm +index 338d1c5f3..959a9fabc 100644 +--- a/cassandane/Cassandane/IMAPMessageStore.pm ++++ b/cassandane/Cassandane/IMAPMessageStore.pm +@@ -83,7 +83,7 @@ sub new + + sub connect + { +- my ($self) = @_; ++ my ($self, %params) = @_; + + # if already successfully connected, do nothing + return +@@ -115,6 +115,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to '$self->{host}:$self->{port}': $@"; + } +@@ -129,6 +130,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to server: $@"; + } +@@ -323,9 +325,9 @@ sub remove + + sub get_client + { +- my ($self) = @_; ++ my ($self, %params) = @_; + +- $self->connect(); ++ $self->connect(%params); + return $self->{client}; + } + +-- +2.39.2 + + +From daa4cb2108414d859b106158f1a799bdbca4d1a2 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:49:46 -0400 +Subject: [PATCH 15/16] imapd.c: also emit a NO [TOOBIG] response for oversized + no-sync APPEND + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 46 ++++++++++++++--------- + imap/imapd.c | 23 ++++++++---- + 2 files changed, 44 insertions(+), 25 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +index 2275c5cf7..52c1c8117 100644 +--- a/cassandane/Cassandane/Cyrus/IMAPLimits.pm ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -59,6 +59,7 @@ $email =~ s/\r?\n/\r\n/gs; + + my $toobig_email = $email . "X" x 100; + ++# Check that we got an untagged BYE [TOOBIG] response + sub assert_bye_toobig + { + my ($self, $store) = @_; +@@ -79,11 +80,11 @@ sub assert_bye_toobig + } + }; + +- # Check that we got a BYE [TOOBIG] response + $store->idle_response($handlers, 1); + $self->assert_num_equals(1, $got_toobig); + } + ++# Send a command and expect an untagged BYE [TOOBIG] response + sub assert_cmd_bye_toobig + { + my $self = shift; +@@ -97,28 +98,37 @@ sub assert_cmd_bye_toobig + $self->assert_bye_toobig(); + } + ++# Check that we got a tagged NO [TOOBIG] response ++sub assert_no_toobig ++{ ++ my ($self, $talk) = @_; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ eval { ++ $talk->_parse_response($handlers); ++ }; ++ ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++# Send a command and expect a tagged NO [TOOBIG] response + sub assert_cmd_no_toobig + { + my $self = shift; + my $talk = shift; + my $cmd = shift; + +- my $got_toobig = 0; +- my $handlers = +- { +- 'no' => sub +- { +- # Pick out the [TOOBIG] response code +- my (undef, $resp) = @_; +- $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); +- } +- }; +- +- $talk->_imap_cmd($cmd, 0, $handlers, @_); +- +- # Check that we got a NO [TOOBIG] response +- $self->assert_str_equals('no', $talk->get_last_completion_response()); +- $self->assert_num_equals(1, $got_toobig); ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_no_toobig($talk); + } + + sub new +@@ -503,6 +513,7 @@ sub test_maxmessagesize_nosync_literal + my $talk = $self->{store}->get_client(); + # Do this by brute force until we have IMAPTalk v4.06+ + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +@@ -512,6 +523,7 @@ sub test_literal_minus + + my $talk = $self->{store}->get_client(); + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +diff --git a/imap/imapd.c b/imap/imapd.c +index f17e789f1..8be99a6b2 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3538,7 +3538,7 @@ static int isokflag(char *s, int *isseen) + } + } + +-static int getliteralsize(const char *p, int c, size_t maxsize, ++static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + unsigned *size, int *binary, const char **parseerr) + + { +@@ -3570,10 +3570,14 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + /* LITERAL- says maximum size is 4096! */ + if (lminus && num > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_MESSAGE_TOOBIG)); + fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); + } + isnowait++; +@@ -3603,8 +3607,8 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + return 0; + } + +-static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *binary, +- const char **parseerr) ++static int catenate_text(const char *tag, FILE *f, size_t maxsize, ++ unsigned *totalsize, int *binary, const char **parseerr) + { + int c; + static struct buf arg; +@@ -3616,7 +3620,8 @@ static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *bina + c = getword(imapd_in, &arg); + + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize - *totalsize, &size, binary, parseerr); ++ r = getliteralsize(tag, arg.s, c, maxsize - *totalsize, ++ &size, binary, parseerr); + if (r) return r; + + /* Catenate message part to stage */ +@@ -3763,7 +3768,8 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + return r; + } + +-static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsigned *totalsize, ++static int append_catenate(const char *tag, FILE *f, const char *cur_name, ++ size_t maxsize, unsigned *totalsize, + int *binary, const char **parseerr, const char **url) + { + int c, r = 0; +@@ -3777,7 +3783,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + + if (!strcasecmp(arg.s, "TEXT")) { +- int r1 = catenate_text(f, maxsize, totalsize, binary, parseerr); ++ int r1 = catenate_text(tag, f, maxsize, totalsize, binary, parseerr); + if (r1) return r1; + + /* if we see a SP, we're trying to catenate more than one part */ +@@ -4019,13 +4025,14 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, ++ r = append_catenate(tag, curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(tag, arg.s, c, maxmsgsize, ++ &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From 93161e7cd5e678a93bd6cfb9fa9d5f2f145db5da Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:55:13 -0400 +Subject: [PATCH 16/16] imapd.c, imapparse.c: call fatal(EX_PROTOCOL) when + client exceeds a limit + +--- + cunit/parse.testc | 12 ++++++------ + imap/imapd.c | 48 +++++++++++++++++++++++------------------------ + imap/imapparse.c | 22 +++++++++++----------- + 3 files changed, 41 insertions(+), 41 deletions(-) + +diff --git a/cunit/parse.testc b/cunit/parse.testc +index 5a97f9b73..1786706cb 100644 +--- a/cunit/parse.testc ++++ b/cunit/parse.testc +@@ -119,7 +119,7 @@ static void test_getint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -188,7 +188,7 @@ static void test_getsint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -255,7 +255,7 @@ static void test_getuint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint32, uint32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint32, uint32_t, STR4, &c, &val, &bytes_in); +@@ -322,7 +322,7 @@ static void test_getint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -391,7 +391,7 @@ static void test_getsint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -458,7 +458,7 @@ static void test_getuint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint64, uint64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint64, uint64_t, STR4, &c, &val, &bytes_in); +diff --git a/imap/imapd.c b/imap/imapd.c +index 8be99a6b2..cb8bbcdee 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3572,13 +3572,13 @@ static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_MESSAGE_TOOBIG)); +- fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_PROTOCOL); + } + isnowait++; + p++; +@@ -3949,7 +3949,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4287,7 +4287,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + if (arg.s[0] == '\0') goto badlist; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { +@@ -4662,7 +4662,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + } + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + switch (fetchatt.s[0]) { +@@ -4795,7 +4795,7 @@ badannotation: + do { + c = getastring(imapd_in, imapd_out, &fieldname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5131,7 +5131,7 @@ badannotation: + do { + c = getword(imapd_in, &fetchatt); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { +@@ -5480,7 +5480,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + do { + c = getword(imapd_in, &storemod); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { +@@ -5575,7 +5575,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + for (;;) { + c = getword(imapd_in, &flagname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -7537,7 +7537,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8389,7 +8389,7 @@ void cmd_setquota(const char *tag, const char *quotaroot) + else if (c != ' ') goto badlist; + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8543,7 +8543,7 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + if (arg.s[0] == '\0') goto bad; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { +@@ -8929,7 +8929,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + name = ucase(arg.s); + if (c != ' ') goto fail; +@@ -8940,7 +8940,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &val); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + dlist_setatom(sub, name, val.s); + } while (c == ' '); +@@ -9011,7 +9011,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9064,7 +9064,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9144,7 +9144,7 @@ static int parse_metadata_string_or_list(const char *tag, + do { + c = getastring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9250,7 +9250,7 @@ static int parse_annotate_store_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9294,7 +9294,7 @@ static int parse_annotate_store_data(const char *tag, + } + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); +@@ -9372,7 +9372,7 @@ static int parse_metadata_store_data(const char *tag, + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9387,7 +9387,7 @@ static int parse_metadata_store_data(const char *tag, + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11677,7 +11677,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + n = 0; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ +@@ -11829,7 +11829,7 @@ static int getlistselopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -11935,7 +11935,7 @@ static int getlistretopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 0e8bdc5dd..8b3b952c5 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("[TOOBIG] Quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_PROTOCOL); + } + } + +@@ -157,11 +157,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_PROTOCOL); + } + isnowait++; + c = prot_getc(pin); +@@ -225,7 +225,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + c = prot_getc(pin); + } +@@ -284,7 +284,7 @@ EXPORTED int getint32(struct protstream *pin, int32_t *num) + /* INT_MAX == 2147483647 */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 214748364 || (result == 214748364 && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -337,7 +337,7 @@ EXPORTED int getuint32(struct protstream *pin, uint32_t *num) + /* UINT_MAX == 4294967295U */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 429496729 || (result == 429496729 && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -361,7 +361,7 @@ EXPORTED int getint64(struct protstream *pin, int64_t *num) + /* LLONG_MAX == 9223372036854775807LL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 922337203685477580LL || (result == 922337203685477580LL && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -414,7 +414,7 @@ EXPORTED int getuint64(struct protstream *pin, uint64_t *num) + /* ULLONG_MAX == 18446744073709551615ULL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 1844674407370955161ULL || (result == 1844674407370955161ULL && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -636,7 +636,7 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + + if (searchargs->maxargssize_mark && + prot_bytes_in(pin) > searchargs->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + } while (c == ' '); +@@ -1389,7 +1389,7 @@ static int get_search_criterion(struct protstream *pin, + + if (base->maxargssize_mark && + prot_bytes_in(pin) > base->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + if (!keep_charset) +-- +2.39.2 + diff --git a/mail/cyrus-imapd38/Makefile b/mail/cyrus-imapd38/Makefile index 99082ca9f4d1..9bbb720228d3 100644 --- a/mail/cyrus-imapd38/Makefile +++ b/mail/cyrus-imapd38/Makefile @@ -1,266 +1,268 @@ PORTNAME= cyrus-imapd PORTVERSION= 3.8.2 -PORTREVISION= 0 +PORTREVISION= 1 CATEGORIES= mail MASTER_SITES= https://github.com/cyrusimap/cyrus-imapd/releases/download/${PORTNAME}-${DISTVERSION}/ PKGNAMESUFFIX= ${CYRUS_IMAPD_VER} MAINTAINER= ume@FreeBSD.org COMMENT= Cyrus mail server, supporting POP3 and IMAP4 protocols ${COMMENT_${FLAVOR}} WWW= https://www.cyrusimap.org/ COMMENT_http= (with HTTP) LICENSE= BSD4CLAUSE LICENSE_FILE= ${WRKSRC}/COPYING BROKEN_riscv64= fails to build: lib/chartable.c: Error 1 FLAVORS= basic http http_PKGNAMESUFFIX= ${CYRUS_IMAPD_VER}-http CYRUS_IMAPD_VER= 38 +EXTRA_PATCHES= ${FILESDIR}/v38-CVE-2024-34055.patch:-p1 + LIB_DEPENDS= libsasl2.so:security/cyrus-sasl2 \ libicuuc.so:devel/icu \ libjansson.so:devel/jansson \ libuuid.so:misc/e2fsprogs-libuuid \ libical.so:devel/libical CONFLICTS_INSTALL= cyrus-imapd2? cyrus-imapd3[0-57-] cyrus-imapd3[0-57-]-http USES= compiler:c11 cpe gmake libtool perl5 pkgconfig ssl USE_RC_SUBR= imapd CYRUS_PREFIX= ${PREFIX}/cyrus GNU_CONFIGURE= yes CONFIGURE_ARGS= --libexecdir=${CYRUS_PREFIX}/libexec \ --sbindir=${CYRUS_PREFIX}/sbin \ --sysconfdir=${PREFIX}/etc \ --with-cyrus-user=${CYRUS_USER} \ --with-sasl=${LOCALBASE} \ --with-com_err \ --with-openssl=${OPENSSLBASE} \ --with-perl=${PERL} CONFIGURE_ENV+= LIBS="-L${LOCALBASE}/lib" CPPFLAGS+= -I${LOCALBASE}/include MAKE_JOBS_UNSAFE= yes USES+= shebangfix SHEBANG_FILES= imap/cyr_cd.sh imap/promdatagen tools/config2header \ tools/masssievec tools/mkimap tools/translatesieve \ perl/sieve/scripts/*.pl CPE_VENDOR= cmu CPE_PRODUCT= cyrus_imap_server OPTIONS_DEFINE= AUTOCREATE BACKUP CLAMAV CLD2 HTTP IDLED LDAP MURDER \ MYSQL NNTP PCRE2 PGSQL REPLICATION SQLITE SQUAT SRS \ XAPIAN DOCS OPTIONS_DEFAULT= AUTOCREATE IDLED READLINE_GNU SQLITE SQUAT SRS .if ${FLAVOR:U} == http OPTIONS_DEFAULT+= HTTP .endif OPTIONS_SUB= yes AUTOCREATE_DESC= Enable autocreate support AUTOCREATE_CONFIGURE_ENABLE= autocreate BACKUP_DESC= Enable backup support (experimental) BACKUP_CONFIGURE_ENABLE=backup CLAMAV_DESC= Use ClamAV CLAMAV_CONFIGURE_WITH= clamav CLAMAV_LIB_DEPENDS= libclamav.so:security/clamav CLD2_DESC= Use CLD2 CLD2_CONFIGURE_WITH= cld2 CLD2_CONFIGURE_ENV= CLD2_CFLAGS="-I${LOCALBASE}/include" \ CLD2_LIBS="-L${LOCALBASE}/lib -lcld2" CLD2_LIB_DEPENDS= libcld2.so:devel/cld2 HTTP_DESC= Enable HTTP support HTTP_IMPLIES= SQLITE HTTP_CONFIGURE_ENABLE= http HTTP_LIB_DEPENDS= libnghttp2.so:www/libnghttp2 \ libshp.so:devel/shapelib \ libbrotlidec.so:archivers/brotli \ libwslay_shared.so:www/wslay \ libzstd.so:archivers/zstd HTTP_CONFIGURE_ENV= WSLAY_CFLAGS="-I${LOCALBASE}/include" \ WSLAY_LIBS="-L${LOCALBASE}/lib -lwslay_shared" # Need additional patch to opendkim #HTTP_LIB_DEPENDS+= libopendkim.so:mail/opendkim #HTTP_CPPFLAGS+= -I${LOCALBASE}/include/opendkim HTTP_USES= gnome HTTP_USE= GNOME=libxml2 IDLED_DESC= Enable IMAP idled support IDLED_CONFIGURE_ENABLE= idled LDAP_DESC= Enable LDAP support (experimental) LDAP_USES= ldap LDAP_CONFIGURE_ON= --with-ldap=${LOCALBASE} LDAP_CONFIGURE_OFF= --without-ldap MURDER_DESC= Enable IMAP Murder support MURDER_CONFIGURE_ENABLE=murder MURDER_MAKE_ENV= PTHREAD_LIBS="-lpthread" MYSQL_USES= mysql MYSQL_CONFIGURE_WITH= mysql NNTP_DESC= Enable NNTP support NNTP_CONFIGURE_ENABLE= nntp PCRE2_DESC= Use PCRE2 rather than PCRE PCRE2_LIB_DEPENDS= libpcre2-posix.so:devel/pcre2 PCRE2_LIB_DEPENDS_OFF= libpcre.so:devel/pcre PCRE2_CONFIGURE_ON= --disable-pcre PCRE2_CONFIGURE_OFF= --disable-pcre2 PGSQL_USES= pgsql PGSQL_CONFIGURE_ON= --with-pgsql=${LOCALBASE} PGSQL_CONFIGURE_OFF= --without-pgsql REPLICATION_DESC= Enable replication (experimental) REPLICATION_CONFIGURE_ENABLE=replication SRS_DESC= Enable Sender Rewriting Scheme support SRS_CONFIGURE_ENABLE= srs SRS_LIB_DEPENDS= libsrs2.so:mail/libsrs2 SQLITE_USES= sqlite SQLITE_CONFIGURE_ON= --with-sqlite=${LOCALBASE} SQLITE_CONFIGURE_OFF= --without-sqlite SQLITE_BROKEN_OFF= SQLITE is required SQUAT_DESC= Enable Squat support SQUAT_CONFIGURE_OFF= --disable-squat XAPIAN_DESC= Enable Xapian support XAPIAN_CONFIGURE_ENABLE=xapian XAPIAN_LIB_DEPENDS= libxapian.so:databases/xapian-core XAPIAN_BUILD_DEPENDS= rsync:net/rsync XAPIAN_RUN_DEPENDS= ${XAPIAN_BUILD_DEPENDS} OPTIONS_RADIO= GSSAPI READLINE OPTIONS_RADIO_GSSAPI= GSSAPI_HEIMDAL GSSAPI_MIT .if exists(/usr/lib/libkrb5.a) OPTIONS_RADIO_GSSAPI+= GSSAPI_BASE OPTIONS_DEFAULT+= GSSAPI_BASE .endif GSSAPI_BASE_USES= gssapi GSSAPI_BASE_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_HEIMDAL_USES= gssapi:heimdal,flags GSSAPI_HEIMDAL_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=heimdal GSSAPI_MIT_USES= gssapi:mit GSSAPI_MIT_CONFIGURE_ON= --enable-gssapi="${GSSAPIBASEDIR}" \ --with-gss_impl=mit OPTIONS_RADIO_READLINE= READLINE_GNU READLINE_PERL READLINE_GNU_DESC= Use Term::Readline::GNU for cyradm READLINE_GNU_RUN_DEPENDS= p5-Term-ReadLine-Gnu>=0:devel/p5-Term-ReadLine-Gnu READLINE_PERL_DESC= Use Term::Readline::Perl for cyradm READLINE_PERL_RUN_DEPENDS= p5-Term-ReadLine-Perl>=0:devel/p5-Term-ReadLine-Perl MANDIRS= ${CYRUS_PREFIX}/man PORTDOCS= * SUB_FILES= pkg-message pkg-install pkg-deinstall cyrus-imapd-man.conf SUB_LIST= CYRUS_USER=${CYRUS_USER} CYRUS_GROUP=${CYRUS_GROUP} CYRUS_USER?= cyrus CYRUS_GROUP?= cyrus MAN_MAN1= httptest imtest installsieve lmtptest mupdatetest nntptest \ pop3test sieveshell sivtest smtptest synctest MAN_MAN3= imclient MAN_MAN5= cyrus.conf imapd.conf krb.equiv CYRUS_MAN8= arbitron backupd chk_cyrus ctl_backups ctl_conversationsdb \ ctl_cyrusdb ctl_deliver ctl_mboxlist cvt_cyrusdb \ cvt_xlist_specialuse cyr_backup cyr_buildinfo cyr_dbtool \ cyr_deny cyr_df cyr_expire cyr_info cyr_ls cyr_synclog \ cyr_userseen cyr_virusscan cyradm cyrdump deliver fud idled \ imapd ipurge lmtpd lmtpproxyd master mbexamine mbpath mbtool \ notifyd pop3d pop3proxyd promstatsd proxyd ptdump ptexpire \ ptloader quota reconstruct relocate_by_id restore sievec \ sieved smmapd timsieved tls_prune unexpunge CYRUS_PERL_MAN1=cyradm CYRUS_PERL_MAN3=Cyrus::Annotator::Daemon Cyrus::Annotator::Message \ Cyrus::IMAP Cyrus::IMAP::Admin Cyrus::IMAP::Shell \ Cyrus::SIEVE::managesieve INSTALL_TARGET= install-strip REINPLACE_ARGS= -i '' .include .if !${PORT_OPTIONS:MGSSAPI_BASE} && !${PORT_OPTIONS:MGSSAPI_HEIMDAL} && \ !${PORT_OPTIONS:MGSSAPI_MIT} CONFIGURE_ARGS+=--disable-gssapi .endif .if ${PORT_OPTIONS:MHTTP} CYRUS_MAN8+= ctl_zoneinfo httpd MAN_MAN1+= dav_reconstruct .endif .if ${PORT_OPTIONS:MNNTP} CYRUS_MAN8+= fetchnews nntpd .endif .if ${PORT_OPTIONS:MMURDER} CYRUS_MAN8+= mupdate .endif .if ${PORT_OPTIONS:MREPLICATION} CYRUS_MAN8+= sync_client sync_reset sync_server .endif .if ${PORT_OPTIONS:MSQUAT} || ${PORT_OPTIONS:MXAPIAN} CYRUS_MAN8+= squatter PLIST_SUB+= SQUATTER="" .else PLIST_SUB+= SQUATTER="@comment " .endif post-patch: @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|" \ -e "s|%%CYRUS_USER%%|${CYRUS_USER}|g" \ -e "s|%%CYRUS_GROUP%%|${CYRUS_GROUP}|g" \ ${WRKSRC}/tools/mkimap .for f in masssievec translatesieve @${REINPLACE_CMD} -e "s|/etc/|${PREFIX}/etc/|g" \ -e "s|/usr/sieve|/var/imap/sieve|g" \ ${WRKSRC}/tools/${f} .endfor @${REINPLACE_CMD} \ -e 's|$$(libdir)/\(pkgconfig\)|${PREFIX}/libdata/\1|g' \ -e 's|$$(mandir)/\(man[8]\)|${PREFIX}/cyrus/man/\1|g' \ ${WRKSRC}/Makefile.in post-install: ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/IMAP/IMAP.so ${STRIP_CMD} ${STAGEDIR}${PREFIX}/${SITE_ARCH_REL}/auto/Cyrus/SIEVE/managesieve/managesieve.so ${MKDIR} ${STAGEDIR}${EXAMPLESDIR} ${INSTALL_DATA} ${FILESDIR}/imapd.conf \ ${STAGEDIR}${EXAMPLESDIR} ${SED} -e 's,/run/cyrus/socket,/var/imap/socket,' \ ${WRKSRC}/doc/examples/cyrus_conf/normal.conf \ > ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .if !${PORT_OPTIONS:MHTTP} ${REINPLACE_CMD} -e 's/^\( http\)/#\1/' \ ${STAGEDIR}${EXAMPLESDIR}/cyrus.conf .endif .for f in mkimap masssievec translatesieve ${INSTALL_SCRIPT} ${WRKSRC}/tools/${f} \ ${STAGEDIR}${CYRUS_PREFIX}/sbin/${f} .endfor ${INSTALL_DATA} ${WRKDIR}/cyrus-imapd-man.conf \ ${STAGEDIR}${PREFIX}/etc/man.d/cyrus-imapd.conf .for s in 1 3 5 . for m in ${MAN_MAN${s}} @${ECHO_CMD} share/man/man${s}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for s in 1 3 . for m in ${CYRUS_PERL_MAN${s}} @${ECHO_CMD} ${SITE_MAN${s}}/${m}.${s}.gz >> ${TMPPLIST} . endfor .endfor .for m in ${CYRUS_MAN8} @${ECHO_CMD} ${CYRUS_PREFIX}/man/man8/${m}.8.gz >> ${TMPPLIST} .endfor post-install-DOCS-on: ${MKDIR} ${STAGEDIR}${DOCSDIR} cd ${WRKSRC}/doc && ${COPYTREE_SHARE} . ${STAGEDIR}${DOCSDIR} \ "! ( -path */html/_sources* -o -name .buildinfo )" ${RM} -r ${STAGEDIR}${DOCSDIR}/rst ${STAGEDIR}${DOCSDIR}/source .include diff --git a/mail/cyrus-imapd38/files/v38-CVE-2024-34055.patch b/mail/cyrus-imapd38/files/v38-CVE-2024-34055.patch new file mode 100644 index 000000000000..0b36c7510e06 --- /dev/null +++ b/mail/cyrus-imapd38/files/v38-CVE-2024-34055.patch @@ -0,0 +1,5402 @@ +From 6dc5cbcf9b72fa7e437e4bbcce1c7024f386d454 Mon Sep 17 00:00:00 2001 +From: Robert Stepanek +Date: Wed, 3 Jan 2024 09:51:36 +0100 +Subject: [PATCH 01/16] SearchFuzzy.pm: do not use non-standard XSNIPPETS + command + +The XSNIPPETS and XCONVMULTISTANDARD commands in Cyrus got +deprecated, so don't keep our test using it. + +Signed-off-by: Robert Stepanek +--- + cassandane/Cassandane/Cyrus/SearchFuzzy.pm | 344 +++++++++------------ + 1 file changed, 146 insertions(+), 198 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +index e40564153..56d39ef25 100644 +--- a/cassandane/Cassandane/Cyrus/SearchFuzzy.pm ++++ b/cassandane/Cassandane/Cyrus/SearchFuzzy.pm +@@ -43,6 +43,8 @@ use warnings; + use Cwd qw(abs_path); + use DateTime; + use Data::Dumper; ++use MIME::Base64 qw(encode_base64); ++use Encode qw(decode encode); + + use lib '.'; + use base qw(Cassandane::Cyrus::TestCase); +@@ -50,10 +52,19 @@ use Cassandane::Util::Log; + + sub new + { ++ + my ($class, @args) = @_; + my $config = Cassandane::Config->default()->clone(); +- $config->set(conversations => 'on'); +- return $class->SUPER::new({ config => $config }, @args); ++ $config->set( ++ conversations => 'on', ++ httpallowcompress => 'no', ++ httpmodules => 'jmap', ++ ); ++ return $class->SUPER::new({ ++ config => $config, ++ jmap => 1, ++ services => [ 'imap', 'http' ] ++ }, @args); + } + + sub set_up +@@ -134,6 +145,55 @@ sub create_testmessages + $self->{instance}->run_command({cyrus => 1}, 'squatter'); + } + ++sub get_snippets ++{ ++ # Previous versions of this test module used XSNIPPETS to ++ # assert snippets but this command got removed from Cyrus. ++ # Use JMAP instead. ++ ++ my ($self, $folder, $uids, $filter) = @_; ++ ++ my $imap = $self->{store}->get_client(); ++ my $jmap = $self->{jmap}; ++ ++ $self->assert_not_null($jmap); ++ ++ $imap->select($folder); ++ my $res = $imap->fetch($uids, ['emailid']); ++ my %emailIdToImapUid = map { $res->{$_}{emailid}[0] => $_ } keys %$res; ++ ++ $res = $jmap->CallMethods([ ++ ['SearchSnippet/get', { ++ filter => $filter, ++ emailIds => [ keys %emailIdToImapUid ], ++ }, 'R1'], ++ ]); ++ ++ my @snippets; ++ foreach (@{$res->[0][1]{list}}) { ++ if ($_->{subject}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'SUBJECT', ++ $_->{subject}, ++ ]); ++ } ++ if ($_->{preview}) { ++ push(@snippets, [ ++ 0, ++ $emailIdToImapUid{$_->{emailId}}, ++ 'BODY', ++ $_->{preview}, ++ ]); ++ } ++ } ++ ++ return { ++ snippets => [ sort { $a->[1] <=> $b->[1] } @snippets ], ++ }; ++} ++ + sub test_copy_messages + :needs_search_xapian + { +@@ -151,12 +211,13 @@ sub test_copy_messages + } + + sub test_stem_verbs +- :min_version_3_0 :needs_search_xapian ++ :min_version_3_0 :needs_search_xapian :JMAPExtensions + { + my ($self) = @_; + $self->create_testmessages(); + + my $talk = $self->{store}->get_client(); ++ $self->assert_not_null($self->{jmap}); + + xlog $self, "Select INBOX"; + my $r = $talk->select("INBOX") || die; +@@ -175,11 +236,8 @@ sub test_stem_verbs + $r = $talk->search('fuzzy', ['subject', { Quote => "runs" }]) || die; + $self->assert_num_equals(3, scalar @$r); + +- xlog $self, 'XSNIPPETS for FUZZY subject "runs"'; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'subject', { Quote => 'runs' }] +- ) || die; ++ xlog $self, 'Get snippets for FUZZY subject "runs"'; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'runs' }); + $self->assert_num_equals(3, scalar @{$r->{snippets}}); + } + +@@ -250,12 +308,8 @@ sub test_snippet_wildcard + $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + +- xlog $self, "XSNIPPETS for $term"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => "$term*" }] +- ) || die; +- xlog $self, Dumper($r); ++ xlog $self, "Get snippets for $term"; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => "$term*" }); + $self->assert_num_equals(2, scalar @{$r->{snippets}}); + } + +@@ -358,13 +412,17 @@ sub test_normalize_snippets + my ($self) = @_; + + # Set up test message with funny characters +- my $body = "foo gären советской diĝir naïve léger"; +- my @terms = split / /, $body; ++use utf8; ++ my @terms = ( "gären", "советской", "diĝir", "naïve", "léger" ); ++no utf8; ++ my $body = encode_base64(encode('UTF-8', join(' ', @terms))); ++ $body =~ s/\r?\n/\r\n/gs; + + xlog $self, "Generate and index test messages."; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + +@@ -380,24 +438,20 @@ sub test_normalize_snippets + + # Assert that diacritics are matched and returned + foreach my $term (@terms) { +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + } + + # Assert that search without diacritics matches + if ($self->{skipdiacrit}) { + my $term = "naive"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => $term }); ++use utf8; ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "naïve"), -1); ++no utf8; + } ++ + } + + sub test_skipdiacrit +@@ -499,38 +553,23 @@ sub test_snippets_termcover + my $r = $talk->select("INBOX") || die; + my $uidvalidity = $talk->get_response_code('uidvalidity'); + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- my $want = "favourite cereal"; ++ my $want = "favourite cereal"; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite', +- 'fuzzy', 'text', 'cereal', +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ operator => 'AND', ++ conditions => [{ ++ text => 'favourite', ++ }, { ++ text => 'cereal', ++ }, { ++ text => '"bogus gnarly"' ++ }], ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', 'favourite cereal' +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- # Regression - a phrase is treated as a loose term +- $r = $talk->xsnippets( [ [ 'INBOX', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite nope cereal' }, +- 'fuzzy', 'text', { Quote => 'bogus gnarly' } +- ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ +- 'fuzzy', 'text', { Quote => 'favourite cereal' } +- ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { ++ text => 'favourite cereal', ++ }); + $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], $want)); + } + +@@ -542,18 +581,28 @@ sub test_cjk_words + + xlog $self, "Generate and index test messages."; + ++use utf8; + my $body = "明末時已經有香港地方的概念"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + my %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("1", %params) || die; + + # Splits into the words: "み, 円, 月額, 申込 ++use utf8; + $body = "申込み!月額円"; ++no utf8; ++ $body = encode_base64(encode('UTF-8', $body)); ++ $body =~ s/\r?\n/\r\n/gs; + %params = ( + mime_charset => "utf-8", +- body => $body ++ mime_encoding => 'base64', ++ body => $body, + ); + $self->make_message("2", %params) || die; + +@@ -569,50 +618,45 @@ sub test_cjk_words + + my $term; + # Search for a two-character CJK word ++use utf8; + $term = "已經"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; +- $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); ++ $self->assert_num_not_equals(index($r->{snippets}[0][3], "$term"), -1); + + # Search for the CJK words 明末 and 時, note that the + # word order is reversed to the original message ++use utf8; + $term = "時明末"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + + # Search for the partial CJK word 月 ++use utf8; + $term = "月"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for the interleaved, partial CJK word 額申 ++use utf8; + $term = "額申"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 0); + + # Search for three of four words: "み, 月額, 申込", + # in different order than the original. ++use utf8; + $term = "月額み申込"; +- xlog $self, "XSNIPPETS for FUZZY text \"$term\""; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'text', { Quote => $term }] +- ) || die; ++no utf8; ++ xlog $self, "Get snippets for FUZZY text \"$term\""; ++ $r = $self->get_snippets('INBOX', $uids, { text => $term }); + $self->assert_num_equals(scalar @{$r->{snippets}}, 1); + } + +@@ -805,86 +849,6 @@ sub test_xattachmentname + } + + +-sub test_xapianv2 +- :min_version_3_0 :needs_search_xapian +-{ +- my ($self) = @_; +- +- my $talk = $self->{store}->get_client(); +- +- # This is a smallish regression test to check if we break something +- # obvious by moving Xapian indexing from folder:uid to message guids. +- # +- # Apart from the tests in this module, at least also the following +- # imodules are relevant: Metadata for SORT, Thread for THREAD. +- +- xlog $self, "Generate message"; +- my $r = $self->make_message("I run", body => "Run, Forrest! Run!" ) || die; +- my $uid = $r->{attrs}->{uid}; +- +- xlog $self, "Copy message into INBOX"; +- $talk->copy($uid, "INBOX"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Create target mailbox"; +- $talk->create("INBOX.target"); +- +- xlog $self, "Copy message into INBOX.target"; +- $talk->copy($uid, "INBOX.target"); +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(3, scalar @{$r->{sort}[0]} - 1); +- $self->assert_num_equals(1, scalar @{$r->{sort}}); +- +- xlog $self, "Generate message"; +- $self->make_message("You run", body => "A running joke" ) || die; +- +- xlog $self, "Run squatter"; +- $self->{instance}->run_command({cyrus => 1}, 'squatter'); +- +- $r = $talk->xconvmultisort( +- [ qw(reverse arrival) ], +- [ 'conversations', position => [1,10] ], +- 'utf-8', 'fuzzy', 'text', "run", +- ); +- $self->assert_num_equals(2, scalar @{$r->{sort}}); +- +- xlog $self, "SEARCH FUZZY"; +- $r = $talk->search( +- "charset", "utf-8", "fuzzy", "text", "run", +- ) || die; +- $self->assert_num_equals(3, scalar @$r); +- +- xlog $self, "Select INBOX"; +- $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- my $uids = $talk->search('1:*', 'NOT', 'DELETED'); +- +- xlog $self, "XSNIPPETS"; +- $r = $talk->xsnippets( +- [['INBOX', $uidvalidity, $uids]], 'utf-8', +- ['fuzzy', 'body', 'run'], +- ) || die; +- $self->assert_num_equals(3, scalar @{$r->{snippets}}); +-} +- + sub test_snippets_escapehtml + :min_version_3_0 :needs_search_xapian + { +@@ -914,21 +878,15 @@ sub test_snippets_escapehtml + my $uids = $talk->search('1:*', 'NOT', 'DELETED'); + my %m; + +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test1' ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test1' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); +- $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); +- +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', 'test2' ] +- ) || die; ++ $self->assert_str_equals("Test1 body with the same tag as snippets", $m{body}); ++ $self->assert_str_equals("Test1 subject with an unescaped & in it", $m{subject}); + ++ $r = $self->get_snippets('INBOX', $uids, { 'text' => 'test2' }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); +- $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); ++ $self->assert_str_equals("Test2 body with a <tag/>, although it's plain text", $m{body}); ++ $self->assert_str_equals("Test2 subject with a <tag> in it", $m{subject}); + } + + sub test_search_exactmatch +@@ -963,13 +921,10 @@ sub test_search_exactmatch + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { body => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; +- $self->assert(index($m{body}, "some text") != -1); +- $self->assert(index($m{body}, "some long text") == -1); ++ $self->assert(index($m{body}, "some text") != -1); ++ $self->assert(index($m{body}, "some long text") == -1); + } + + sub test_search_subjectsnippet +@@ -1004,10 +959,7 @@ sub test_search_subjectsnippet + $self->assert_num_equals(1, scalar @$uids); + + my %m; +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'text', $query ] +- ) || die; +- ++ $r = $self->get_snippets('INBOX', $uids, { text => $query }); + %m = map { lc($_->[2]) => $_->[3] } @{ $r->{snippets} }; + $self->assert_matches(qr/^\[plumbing\]/, $m{subject}); + } +@@ -1317,11 +1269,10 @@ sub test_detect_language + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'body', 'atmet' ] +- ) || die; +- $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++ $r = $self->get_snippets('INBOX', $uids, { body => 'atmet' }); ++use utf8; ++ $self->assert_num_not_equals(-1, index($r->{snippets}[0][3], ' Höhe atmeten.')); ++no utf8; + } + + sub test_detect_language_subject +@@ -1377,12 +1328,9 @@ sub test_detect_language_subject + $self->assert_deep_equals([1], $uids); + + my $r = $talk->select("INBOX") || die; +- my $uidvalidity = $talk->get_response_code('uidvalidity'); +- $r = $talk->xsnippets( [ [ 'inbox', $uidvalidity, $uids ] ], +- 'utf-8', [ 'fuzzy', 'subject', 'Landschaft' ] +- ) || die; ++ $r = $self->get_snippets('INBOX', $uids, { subject => 'Landschaft' }); + $self->assert_str_equals( +- 'A subject with the German word Landschaften', ++ 'A subject with the German word Landschaften', + $r->{snippets}[0][3] + ); + } +-- +2.39.2 + + +From d1b5eb32a3df564a2f79cc5e35230a344a632fc6 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:00:00 -0500 +Subject: [PATCH 02/16] imapd.c: UIDVALIDITY should be uint32_t and parse it as + such + +--- + imap/imapd.c | 10 +++------- + imap/index.h | 2 +- + 2 files changed, 4 insertions(+), 8 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 991040241..46ac2df86 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -4302,15 +4302,11 @@ static void cmd_select(char *tag, char *cmd, char *name) + } + else if ((client_capa & CAPA_QRESYNC) && + !strcmp(arg.s, "QRESYNC")) { +- char *p; +- + if (c != ' ') goto badqresync; + c = prot_getc(imapd_in); + if (c != '(') goto badqresync; +- c = getastring(imapd_in, imapd_out, &arg); +- v->uidvalidity = strtoul(arg.s, &p, 10); +- if (*p || !v->uidvalidity || v->uidvalidity == ULONG_MAX) goto badqresync; +- if (c != ' ') goto badqresync; ++ c = getuint32(imapd_in, &v->uidvalidity); ++ if (c != ' ' || !v->uidvalidity) goto badqresync; + c = getmodseq(imapd_in, &v->modseq); + if (c == EOF) goto badqresync; + if (c == ' ') { +@@ -4450,7 +4446,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + prot_printf(backend_current->out, "%s %s {" SIZE_T_FMT "+}\r\n%s", + tag, cmd, strlen(name), name); + if (v->uidvalidity) { +- prot_printf(backend_current->out, " (QRESYNC (%lu " MODSEQ_FMT, ++ prot_printf(backend_current->out, " (QRESYNC (%u " MODSEQ_FMT, + v->uidvalidity, v->modseq); + if (v->sequence) { + prot_printf(backend_current->out, " %s", v->sequence); +diff --git a/imap/index.h b/imap/index.h +index 54705d056..3178ebd04 100644 +--- a/imap/index.h ++++ b/imap/index.h +@@ -72,7 +72,7 @@ extern unsigned client_capa; + struct message; + + struct vanished_params { +- unsigned long uidvalidity; ++ uint32_t uidvalidity; + modseq_t modseq; + const char *match_seq; + const char *match_uid; +-- +2.39.2 + + +From ece8be8fc41d8faf8540fe5cf066cd47a09226b6 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 08:13:05 -0500 +Subject: [PATCH 03/16] imapd.c: consolidate ID field-value parse error + response + +--- + imap/imapd.c | 17 +++++------------ + 1 file changed, 5 insertions(+), 12 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 46ac2df86..fc8bd935a 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3123,19 +3123,12 @@ static void cmd_id(char *tag) + + /* get field name */ + c = getstring(imapd_in, imapd_out, &field); +- if (c != ' ') { ++ if (c != ' ' || ++ /* get field value */ ++ (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || ++ (c != ' ' && c != ')')) { + prot_printf(imapd_out, +- "%s BAD Invalid/missing field name in Id\r\n", +- tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* get field value */ +- c = getnstring(imapd_in, imapd_out, &arg); +- if (c != ' ' && c != ')') { +- prot_printf(imapd_out, +- "%s BAD Invalid/missing value in Id\r\n", ++ "%s BAD Invalid field-value pair in Id\r\n", + tag); + eatline(imapd_in, c); + return; +-- +2.39.2 + + +From 8cd3fb31a672ed45cacb260605ac111d40ac8e69 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 7 Feb 2024 14:12:41 -0500 +Subject: [PATCH 04/16] imapd.c: response code in fatal() string MUST + immediately follow "* BYE" + +--- + imap/imapd.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index fc8bd935a..9f1ca2fe1 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1190,7 +1190,8 @@ EXPORTED void fatal(const char *s, int code) + } + recurse_code = code; + if (imapd_out) { +- prot_printf(imapd_out, "* BYE Fatal error: %s\r\n", s); ++ prot_printf(imapd_out, "* BYE %s%s\r\n", ++ *s == '[' /* resp-text-code */ ? "" : "Fatal error: ", s); + prot_flush(imapd_out); + } + if (stages.count) { +-- +2.39.2 + + +From 6fc15e3b83cd0c2b71ed483330e2afa5497691ad Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 11:00:19 -0500 +Subject: [PATCH 05/16] imapparse.c: include [TOOBIG] response code for + oversized word/qstring + +--- + imap/imapparse.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 5dc987a6e..edeb84215 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_IOERR); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_IOERR); + } + } + +@@ -212,6 +212,9 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + return c; + } + buf_putc(buf, c); ++ if (config_maxword && buf_len(buf) > config_maxword) { ++ fatal("[TOOBIG] Word too long", EX_IOERR); ++ } + c = prot_getc(pin); + } + /* never gets here */ +-- +2.39.2 + + +From 42154ac9984811198f7de21d785e344d6191dce1 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 13:31:11 -0500 +Subject: [PATCH 06/16] imapparse.c: fatal() when a client violates LITERAL- + limit + +--- + imap/imap_err.et | 3 +++ + imap/imapparse.c | 7 +++++-- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index a98ec0e1b..072078f94 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,9 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++ec IMAP_LITERAL_MINUS_TOO_LARGE, ++ "[TOOBIG] Non-synchronizing literal size exceeds 4K" ++ + ec IMAP_USERFLAG_EXHAUSTED, + "Too many user flags in mailbox" + +diff --git a/imap/imapparse.c b/imap/imapparse.c +index edeb84215..4499264ff 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -153,8 +153,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + buf_reset(buf); + c = getint32(pin, &len); + if (c == '+') { +- // LITERAL- says maximum size is 4096! +- if (lminus && len > 4096) return EOF; ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && len > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +-- +2.39.2 + + +From 13fa309e77264ffbea6a0fc5efa41bbb5f1b7be5 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 12 Feb 2024 10:54:03 -0500 +Subject: [PATCH 07/16] Cleanup and document the use of prot_setisclient() + +Only IMAP(-like) clients send LITERAL+ syntax +--- + backup/backupd.c | 3 +-- + backup/lcb.c | 1 - + backup/lcb_read.c | 2 -- + backup/lcb_verify.c | 2 -- + cunit/getxstring.testc | 4 ---- + imap/append.c | 1 - + imap/backend.c | 2 -- + imap/cyr_dbtool.c | 1 - + imap/dlist.c | 5 ++++- + imap/imapd.c | 4 ++++ + imap/imapparse.c | 5 +++-- + imap/message.c | 3 --- + imap/mupdate.c | 3 +++ + imap/sync_server.c | 3 +-- + imap/sync_support.c | 4 ---- + lib/prot.h | 2 +- + 16 files changed, 17 insertions(+), 28 deletions(-) + +diff --git a/backup/backupd.c b/backup/backupd.c +index 04715a561..d0ba24cf9 100644 +--- a/backup/backupd.c ++++ b/backup/backupd.c +@@ -229,9 +229,8 @@ EXPORTED int service_main(int argc __attribute__((unused)), + backupd_in = prot_new(0, 0); + backupd_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow use of LITERAL+ */ + prot_setisclient(backupd_in, 1); +- prot_setisclient(backupd_out, 1); + + /* Find out name of client host */ + backupd_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/backup/lcb.c b/backup/lcb.c +index dbba85ca7..3f68b1aaa 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -606,7 +606,6 @@ EXPORTED int backup_reindex(const char *name, + fprintf(out, "\nfound chunk at offset " OFF_T_FMT "\n\n", member_offset); + + struct protstream *member = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(member, 1); /* don't sync literals */ + + // FIXME stricter timestamp sequence checks + time_t member_start_ts = -1; +diff --git a/backup/lcb_read.c b/backup/lcb_read.c +index f597c97c5..f2342d69d 100644 +--- a/backup/lcb_read.c ++++ b/backup/lcb_read.c +@@ -113,7 +113,6 @@ EXPORTED int backup_read_message_data(struct backup *backup, + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + +@@ -203,7 +202,6 @@ EXPORTED int backup_prepare_message_upload(struct backup *backup, + if (!r) { + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); + int c; +- prot_setisclient(ps, 1); /* don't sync literals */ + c = parse_backup_line(ps, NULL, NULL, &dl); + prot_free(ps); + ps = NULL; +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index a8dac204c..ff2758552 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -228,7 +228,6 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + if (r) return r; + + struct protstream *ps = prot_readcb(_prot_fill_cb, vmrock->gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + r = parse_backup_line(ps, NULL, NULL, &dl); + + if (r == EOF) { +@@ -528,7 +527,6 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + goto done; + } + struct protstream *ps = prot_readcb(_prot_fill_cb, gzuc); +- prot_setisclient(ps, 1); /* don't sync literals */ + + struct buf cmd = BUF_INITIALIZER; + while (1) { +diff --git a/cunit/getxstring.testc b/cunit/getxstring.testc +index f5a5989a3..12efe86aa 100644 +--- a/cunit/getxstring.testc ++++ b/cunit/getxstring.testc +@@ -72,9 +72,6 @@ static int tear_down(void) + + /* + * Run a single testcase. +- * +- * Note: prot_setisclient() turns off off literal synchronising so +- * we don't have to futz around with testing that. + */ + #define _TESTCASE_PRE(fut, input, retval, consumed) \ + do { \ +@@ -84,7 +81,6 @@ static int tear_down(void) + long long _consumed = (consumed); \ + p = prot_readmap(input, sizeof(input)-1); \ + CU_ASSERT_PTR_NOT_NULL_FATAL(p); \ +- prot_setisclient(p, 1); \ + c = fut(p, NULL, &b); \ + CU_ASSERT_EQUAL(c, retval); \ + if (_consumed >= 0) { \ +diff --git a/imap/append.c b/imap/append.c +index bfd30003a..69f3daf99 100644 +--- a/imap/append.c ++++ b/imap/append.c +@@ -438,7 +438,6 @@ static int callout_receive_reply(const char *callout, + } + + p = prot_new(fd, /*write*/0); +- prot_setisclient(p, 1); + + /* read and parse the reply as a dlist */ + c = dlist_parse(results, /*parsekeys*/0, /*isarchive*/0, /*isbackup*/0, p); +diff --git a/imap/backend.c b/imap/backend.c +index 4cc7bc56b..0bbb65ed0 100644 +--- a/imap/backend.c ++++ b/imap/backend.c +@@ -955,7 +955,6 @@ EXPORTED struct backend *backend_connect_pipe(int infd, int outfd, + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +@@ -1153,7 +1152,6 @@ EXPORTED struct backend *backend_connect(struct backend *ret_backend, const char + ret->prot = prot; + + /* use literal+ to send literals */ +- prot_setisclient(ret->in, 1); + prot_setisclient(ret->out, 1); + + /* Start TLS if required */ +diff --git a/imap/cyr_dbtool.c b/imap/cyr_dbtool.c +index 42e21e8a9..3534e9c92 100644 +--- a/imap/cyr_dbtool.c ++++ b/imap/cyr_dbtool.c +@@ -156,7 +156,6 @@ static void batch_commands(struct db *db) + int r = 0; + + prot_setisclient(in, 1); +- prot_setisclient(out, 1); + + while (1) { + buf_reset(&cmd); +diff --git a/imap/dlist.c b/imap/dlist.c +index 1e38d1bfb..fd068f071 100644 +--- a/imap/dlist.c ++++ b/imap/dlist.c +@@ -1235,7 +1235,10 @@ EXPORTED int dlist_parsemap(struct dlist **dlp, int parsekey, int isbackup, + struct dlist *dl = NULL; + + stream = prot_readmap(base, len); +- prot_setisclient(stream, 1); /* don't sync literals */ ++ ++ /* Allow LITERAL+ - this is silly, but required to parse personal CALDATA */ ++ prot_setisclient(stream, 1); ++ + c = dlist_parse(&dl, parsekey, /*isarchive*/ 0, isbackup, stream); + prot_free(stream); + +diff --git a/imap/imapd.c b/imap/imapd.c +index 9f1ca2fe1..97b7732e5 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -959,6 +959,10 @@ int service_main(int argc __attribute__((unused)), + + imapd_in = prot_new(0, 0); + imapd_out = prot_new(1, 1); ++ ++ /* Allow LITERAL+ */ ++ prot_setisclient(imapd_in, 1); ++ + protgroup_insert(protin, imapd_in); + + /* Find out name of client host */ +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 4499264ff..5da6af1b8 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -149,10 +149,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + /* Literal */ +- isnowait = pin->isclient; ++ isnowait = !pin->isclient; + buf_reset(buf); + c = getint32(pin, &len); +- if (c == '+') { ++ ++ if (pin->isclient && c == '+') { + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +diff --git a/imap/message.c b/imap/message.c +index 16985728a..0744cb1d0 100644 +--- a/imap/message.c ++++ b/imap/message.c +@@ -3419,7 +3419,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read envelope response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_ENVELOPE), + cacheitem_size(record, CACHE_ENVELOPE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_envelope(strm, *body); + prot_free(strm); +@@ -3427,7 +3426,6 @@ EXPORTED void message_read_bodystructure(const struct index_record *record, stru + /* Read bodystructure response from cache */ + strm = prot_readmap(cacheitem_base(record, CACHE_BODYSTRUCTURE), + cacheitem_size(record, CACHE_BODYSTRUCTURE)); +- prot_setisclient(strm, 1); /* no-sync literals */ + + message_read_body(strm, *body, NULL); + prot_free(strm); +@@ -4792,7 +4790,6 @@ static int message_parse_cbodystructure(message_t *m) + cacheitem_size(&m->record, CACHE_BODYSTRUCTURE)); + if (!prot) + return IMAP_MAILBOX_BADFORMAT; +- prot_setisclient(prot, 1); /* don't crash parsing literals */ + + m->body = xzmalloc(sizeof(struct body)); + r = parse_bodystructure_part(prot, m->body, NULL); +diff --git a/imap/mupdate.c b/imap/mupdate.c +index 65b78d9a4..6466200a6 100644 +--- a/imap/mupdate.c ++++ b/imap/mupdate.c +@@ -249,6 +249,9 @@ static struct conn *conn_new(int fd) + C->pin = prot_new(C->fd, 0); + C->pout = prot_new(C->fd, 1); + ++ /* Allow LITERAL+ */ ++ prot_setisclient(C->pin, 1); ++ + prot_setflushonread(C->pin, C->pout); + prot_settimeout(C->pin, 180*60); + +diff --git a/imap/sync_server.c b/imap/sync_server.c +index b9917e87b..0febff288 100644 +--- a/imap/sync_server.c ++++ b/imap/sync_server.c +@@ -329,9 +329,8 @@ int service_main(int argc __attribute__((unused)), + sync_in = prot_new(0, 0); + sync_out = prot_new(1, 1); + +- /* Force use of LITERAL+ so we don't need two way communications */ ++ /* Allow LITERAL+ */ + prot_setisclient(sync_in, 1); +- prot_setisclient(sync_out, 1); + + /* Find out name of client host */ + sync_clienthost = get_clienthost(0, &localip, &remoteip); +diff --git a/imap/sync_support.c b/imap/sync_support.c +index 99eb70dad..03ad16bee 100644 +--- a/imap/sync_support.c ++++ b/imap/sync_support.c +@@ -8094,10 +8094,6 @@ connected: + if (timeout < 3) timeout = 3; + prot_settimeout(backend->in, timeout); + +- /* Force use of LITERAL+ so we don't need two way communications */ +- prot_setisclient(backend->in, 1); +- prot_setisclient(backend->out, 1); +- + return 0; + } + +diff --git a/lib/prot.h b/lib/prot.h +index 98af5d160..89b0b0a2a 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -133,7 +133,7 @@ struct protstream { + int can_unget; + int bytes_in; + int bytes_out; +- int isclient; ++ int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ + prot_readcallback_t *readcallback_proc; +-- +2.39.2 + + +From 6663e8e534d0395c797cf74d051f7d0ca3d62621 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 22:48:58 -0400 +Subject: [PATCH 08/16] imapd.c: LITERAL- also applies to APPEND + +imap_err.et: add IMAP_MESSAGE_TOOBIG error message +--- + imap/imap_err.et | 4 ++++ + imap/imapd.c | 12 ++++++++++++ + 2 files changed, 16 insertions(+) + +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 072078f94..d90e30a4b 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -65,6 +65,10 @@ ec IMAP_QUOTA_EXCEEDED, + ec IMAP_MESSAGE_TOO_LARGE, + "Message size exceeds fixed limit" + ++# Same as IMAP_MESSAGE_TOO_LARGE, but with TOOBIG response code ++ec IMAP_MESSAGE_TOOBIG, ++ "[TOOBIG] Message size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 97b7732e5..a301b554e 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3582,6 +3582,9 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + { + int isnowait = 0; + uint32_t num; ++ static int lminus = -1; ++ ++ if (lminus == -1) lminus = config_getswitch(IMAPOPT_LITERALMINUS); + + /* Check for literal8 */ + if (*p == '~') { +@@ -3602,6 +3605,15 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + } + + if (*p == '+') { ++ /* LITERAL- says maximum size is 4096! */ ++ if (lminus && num > 4096) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ } ++ if (num > maxsize) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ } + isnowait++; + p++; + } +-- +2.39.2 + + +From 986079279f972842602694a02f7d19dbd5228645 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 9 Feb 2024 14:52:22 -0500 +Subject: [PATCH 09/16] imapd.c: remove XSNIPPETS and XCONV* commands + +--- + cassandane/Cassandane/Cyrus/Conversations.pm | 71 -- + imap/imapd.c | 1006 ------------------ + 2 files changed, 1077 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/Conversations.pm b/cassandane/Cassandane/Cyrus/Conversations.pm +index 8bdf8aa72..98369cb5c 100755 +--- a/cassandane/Cassandane/Cyrus/Conversations.pm ++++ b/cassandane/Cassandane/Cyrus/Conversations.pm +@@ -708,77 +708,6 @@ sub bogus_test_replication_clash + $self->check_messages(\%exp, store => $replica_store); + } + +-sub test_xconvfetch +- :min_version_3_0 +-{ +- my ($self) = @_; +- my $store = $self->{store}; +- +- # check IMAP server has the XCONVERSATIONS capability +- $self->assert($store->get_client()->capability()->{xconversations}); +- +- xlog $self, "generating messages"; +- my $generator = Cassandane::ThreadedGenerator->new(); +- $store->write_begin(); +- while (my $msg = $generator->generate()) +- { +- $store->write_message($msg); +- } +- $store->write_end(); +- +- xlog $self, "reading the whole folder again to discover CIDs etc"; +- my %cids; +- my %uids; +- $store->read_begin(); +- while (my $msg = $store->read_message()) +- { +- my $uid = $msg->get_attribute('uid'); +- my $cid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- if (defined $cids{$cid}) +- { +- $self->assert_num_equals($threadid, $cids{$cid}); +- } +- else +- { +- $cids{$cid} = $threadid; +- xlog $self, "Found CID $cid"; +- } +- $self->assert_null($uids{$uid}); +- $uids{$uid} = 1; +- } +- $store->read_end(); +- +- xlog $self, "Using XCONVFETCH on each conversation"; +- foreach my $cid (keys %cids) +- { +- xlog $self, "XCONVFETCHing CID $cid"; +- +- my $result = $store->xconvfetch_begin($cid); +- $self->assert_not_null($result->{xconvmeta}); +- $self->assert_num_equals(1, scalar keys %{$result->{xconvmeta}}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}); +- $self->assert_not_null($result->{xconvmeta}->{$cid}->{modseq}); +- while (my $msg = $store->xconvfetch_message()) +- { +- my $muid = $msg->get_attribute('uid'); +- my $mcid = $msg->get_attribute('cid'); +- my $threadid = $msg->get_header('X-Cassandane-Thread'); +- $self->assert_str_equals($cid, $mcid); +- $self->assert_num_equals($cids{$cid}, $threadid); +- $self->assert_num_equals(1, $uids{$muid}); +- $uids{$muid} |= 2; +- } +- $store->xconvfetch_end(); +- } +- +- xlog $self, "checking that all the UIDs in the folder were XCONVFETCHed"; +- foreach my $uid (keys %uids) +- { +- $self->assert_num_equals(3, $uids{$uid}); +- } +-} +- + # + # Test APPEND of a new composed draft message to the Drafts folder by + # the Fastmail webui, which sets the X-ME-Message-ID header to thread +diff --git a/imap/imapd.c b/imap/imapd.c +index a301b554e..0ea16979a 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -443,14 +443,6 @@ static void cmd_idle(char* tag); + + static void cmd_starttls(char *tag, int imaps); + +-static void cmd_xconvsort(char *tag, int updates); +-static void cmd_xconvmultisort(char *tag); +-static void cmd_xconvmeta(const char *tag); +-static void cmd_xconvfetch(const char *tag); +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs); +-static void cmd_xsnippets(char *tag); + static void cmd_xstats(char *tag); + + static void cmd_xapplepushservice(const char *tag, +@@ -507,12 +499,8 @@ static int parse_metadata_store_data(const char *tag, + static int getlistselopts(char *tag, struct listargs *args); + static int getlistretopts(char *tag, struct listargs *args); + +-static int get_snippetargs(struct snippetargs **sap); +-static void free_snippetargs(struct snippetargs **sap); + static int getsortcriteria(char *tag, struct sortcrit **sortcrit); + static int getdatetime(time_t *date); +-static int parse_windowargs(const char *tag, struct windowargs **, int); +-static void free_windowargs(struct windowargs *wa); + + static void appendfieldlist(struct fieldlist **l, char *section, + strarray_t *fields, char *trail, +@@ -2298,32 +2286,6 @@ static void cmdloop(void) + + prometheus_increment(CYRUS_IMAP_XBACKUP_TOTAL); + } +- else if (!strcmp(cmd.s, "Xconvfetch")) { +- cmd_xconvfetch(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVFETCH_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvmultisort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvmultisort(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVMULTISORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvsort")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 0); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVSORT_TOTAL); */ +- } +- else if (!strcmp(cmd.s, "Xconvupdates")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xconvsort(tag.s, 1); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XCONVUPDATES_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xfer")) { + if (readonly) goto noreadonly; + int havepartition = 0; +@@ -2349,9 +2311,6 @@ static void cmdloop(void) + (havepartition ? arg3.s : NULL)); + /* XXX prometheus_increment(CYRUS_IMAP_XFER_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xconvmeta")) { +- cmd_xconvmeta(tag.s); +- } + else if (!strcmp(cmd.s, "Xlist")) { + struct listargs listargs; + +@@ -2384,13 +2343,6 @@ static void cmdloop(void) + cmd_xrunannotator(tag.s, arg1.s, usinguid); + /* XXX prometheus_increment(CYRUS_IMAP_XRUNANNOTATOR_TOTAL); */ + } +- else if (!strcmp(cmd.s, "Xsnippets")) { +- if (c != ' ') goto missingargs; +- if (!imapd_index && !backend_current) goto nomailbox; +- cmd_xsnippets(tag.s); +- +- /* XXX prometheus_increment(CYRUS_IMAP_XSNIPPETS_TOTAL); */ +- } + else if (!strcmp(cmd.s, "Xstats")) { + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xstats(tag.s); +@@ -5209,8 +5161,6 @@ badannotation: + } + if (config_getswitch(IMAPOPT_CONVERSATIONS) + && (fa->fetchitems & (FETCH_MAILBOXIDS|FETCH_MAILBOXES))) { +- // annoyingly, this codepath COULD be called from xconv* commands, but it never is, +- // in reality, so it's safe leaving this as shared + int r = conversations_open_user(imapd_userid, 0/*shared*/, &fa->convstate); + if (r) { + syslog(LOG_WARNING, "error opening conversations for %s: %s", +@@ -5298,136 +5248,6 @@ static void cmd_fetch(char *tag, char *sequence, int usinguid) + fetchargs_fini(&fetchargs); + } + +-static void do_one_xconvmeta(struct conversations_state *state, +- conversation_id_t cid, +- conversation_t *conv, +- struct dlist *itemlist) +-{ +- struct dlist *item = dlist_newpklist(NULL, ""); +- struct dlist *fl; +- +- assert(conv); +- assert(itemlist); +- +- for (fl = itemlist->head; fl; fl = fl->next) { +- const char *key = dlist_cstring(fl); +- +- /* xxx - parse to a fetchitems? */ +- if (!strcasecmp(key, "MODSEQ")) +- dlist_setnum64(item, "MODSEQ", conv->modseq); +- else if (!strcasecmp(key, "EXISTS")) +- dlist_setnum32(item, "EXISTS", conv->exists); +- else if (!strcasecmp(key, "UNSEEN")) +- dlist_setnum32(item, "UNSEEN", conv->unseen); +- else if (!strcasecmp(key, "SIZE")) +- dlist_setnum32(item, "SIZE", conv->size); +- else if (!strcasecmp(key, "COUNT")) { +- struct dlist *flist = dlist_newlist(item, "COUNT"); +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *lookup = dlist_cstring(tmp); +- int i = strarray_find_case(state->counted_flags, lookup, 0); +- if (i >= 0) { +- dlist_setflag(flist, "FLAG", lookup); +- dlist_setnum32(flist, "COUNT", conv->counts[i]); +- } +- } +- } +- } +- else if (!strcasecmp(key, "SENDERS")) { +- conv_sender_t *sender; +- struct dlist *slist = dlist_newlist(item, "SENDERS"); +- for (sender = conv->senders; sender; sender = sender->next) { +- struct dlist *sli = dlist_newlist(slist, ""); +- dlist_setatom(sli, "NAME", sender->name); +- dlist_setatom(sli, "ROUTE", sender->route); +- dlist_setatom(sli, "MAILBOX", sender->mailbox); +- dlist_setatom(sli, "DOMAIN", sender->domain); +- } +- } +- /* XXX - maybe rename FOLDERCOUNTS or something? */ +- else if (!strcasecmp(key, "FOLDEREXISTS")) { +- struct dlist *flist = dlist_newlist(item, "FOLDEREXISTS"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "EXISTS", folder ? folder->exists : 0); +- } +- } +- } +- else if (!strcasecmp(key, "FOLDERUNSEEN")) { +- struct dlist *flist = dlist_newlist(item, "FOLDERUNSEEN"); +- conv_folder_t *folder; +- fl = fl->next; +- if (dlist_isatomlist(fl)) { +- struct dlist *tmp; +- for (tmp = fl->head; tmp; tmp = tmp->next) { +- const char *extname = dlist_cstring(tmp); +- char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); +- folder = conversation_find_folder(state, conv, intname); +- free(intname); +- dlist_setatom(flist, "MBOXNAME", extname); +- /* ok if it's not there */ +- dlist_setnum32(flist, "UNSEEN", folder ? folder->unseen : 0); +- } +- } +- } +- else { +- dlist_setatom(item, key, NULL); /* add a NIL response */ +- } +- } +- +- prot_printf(imapd_out, "* XCONVMETA %s ", conversation_id_encode(cid)); +- dlist_print(item, 0, imapd_out); +- prot_printf(imapd_out, "\r\n"); +- +- dlist_free(&item); +-} +- +-static void do_xconvmeta(const char *tag, +- struct conversations_state *state, +- struct dlist *cidlist, +- struct dlist *itemlist) +-{ +- conversation_id_t cid; +- struct dlist *dl; +- int r; +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- const char *cidstr = dlist_cstring(dl); +- conversation_t *conv = NULL; +- +- if (!conversation_id_decode(&cid, cidstr) || !cid) { +- prot_printf(imapd_out, "%s BAD Invalid CID %s\r\n", tag, cidstr); +- return; +- } +- +- r = conversation_load(state, cid, &conv); +- if (r) { +- prot_printf(imapd_out, "%s BAD Failed to read %s\r\n", tag, cidstr); +- conversation_free(conv); +- return; +- } +- +- if (conv && conv->exists) +- do_one_xconvmeta(state, cid, conv, itemlist); +- +- conversation_free(conv); +- } +- +- prot_printf(imapd_out, "%s OK Completed\r\n", tag); +-} +- + static int do_xbackup(const char *channel, + const ptrarray_t *list) + { +@@ -5571,270 +5391,6 @@ done: + } + } + +-/* +- * Parse and perform a XCONVMETA command. +- */ +-void cmd_xconvmeta(const char *tag) +-{ +- int r; +- int c = ' '; +- struct conversations_state *state = NULL; +- struct dlist *cidlist = NULL; +- struct dlist *itemlist = NULL; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVMETA ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Failed to parse CID list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- c = dlist_parse_asatomlist(&itemlist, 0, imapd_in); +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, "%s BAD Failed to parse item list\r\n", tag); +- eatline(imapd_in, c); +- goto done; +- } +- +- // this one is OK, xconvmeta doesn't do an expunge +- r = conversations_open_user(imapd_userid, 1/*shared*/, &state); +- if (r) { +- prot_printf(imapd_out, "%s BAD failed to open db: %s\r\n", +- tag, error_message(r)); +- goto done; +- } +- +- do_xconvmeta(tag, state, cidlist, itemlist); +- +- done: +- +- dlist_free(&itemlist); +- dlist_free(&cidlist); +- conversations_commit(&state); +-} +- +-/* +- * Parse and perform a XCONVFETCH command. +- */ +-void cmd_xconvfetch(const char *tag) +-{ +- int c = ' '; +- struct fetchargs fetchargs; +- int r; +- clock_t start = clock(); +- modseq_t ifchangedsince = 0; +- char mytime[100]; +- struct dlist *cidlist = NULL; +- struct dlist *item; +- +- if (backend_current) { +- /* remote mailbox */ +- prot_printf(backend_current->out, "%s XCONVFETCH ", tag); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, c); +- return; +- } +- +- /* local mailbox */ +- memset(&fetchargs, 0, sizeof(struct fetchargs)); +- +- c = dlist_parse_asatomlist(&cidlist, 0, imapd_in); +- if (c != ' ') +- goto syntax_error; +- +- /* check CIDs */ +- for (item = cidlist->head; item; item = item->next) { +- if (!dlist_ishex64(item)) { +- prot_printf(imapd_out, "%s BAD Invalid CID\r\n", tag); +- eatline(imapd_in, c); +- goto freeargs; +- } +- } +- +- c = getmodseq(imapd_in, &ifchangedsince); +- if (c != ' ') +- goto syntax_error; +- +- r = parse_fetch_args(tag, "Xconvfetch", 0, &fetchargs); +- if (r) +- goto freeargs; +- fetchargs.fetchitems |= (FETCH_UIDVALIDITY|FETCH_FOLDER); +- fetchargs.namespace = &imapd_namespace; +- fetchargs.userid = imapd_userid; +- +- r = do_xconvfetch(cidlist, ifchangedsince, &fetchargs); +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- +- if (r) { +- prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, +- error_message(r), mytime); +- } else { +- prot_printf(imapd_out, "%s OK Completed (%s sec)\r\n", +- tag, mytime); +- } +- +-freeargs: +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +- return; +- +-syntax_error: +- prot_printf(imapd_out, "%s BAD Syntax error\r\n", tag); +- eatline(imapd_in, c); +- dlist_free(&cidlist); +- fetchargs_fini(&fetchargs); +-} +- +-static int xconvfetch_lookup(struct conversations_state *statep, +- conversation_id_t cid, +- modseq_t ifchangedsince, +- hash_table *wanted_cids, +- strarray_t *folder_list) +-{ +- const char *key = conversation_id_encode(cid); +- conversation_t *conv = NULL; +- conv_folder_t *folder; +- int r; +- +- r = conversation_load(statep, cid, &conv); +- if (r) return r; +- +- if (!conv) +- goto out; +- +- if (!conv->exists) +- goto out; +- +- /* output the metadata for this conversation */ +- { +- struct dlist *dl = dlist_newlist(NULL, ""); +- dlist_setatom(dl, "", "MODSEQ"); +- do_one_xconvmeta(statep, cid, conv, dl); +- dlist_free(&dl); +- } +- +- if (ifchangedsince >= conv->modseq) +- goto out; +- +- hash_insert(key, (void *)1, wanted_cids); +- +- for (folder = conv->folders; folder; folder = folder->next) { +- /* no contents */ +- if (!folder->exists) +- continue; +- +- /* finally, something worth looking at */ +- strarray_add(folder_list, strarray_nth(statep->folders, folder->number)); +- } +- +-out: +- conversation_free(conv); +- return 0; +-} +- +-static int do_xconvfetch(struct dlist *cidlist, +- modseq_t ifchangedsince, +- struct fetchargs *fetchargs) +-{ +- struct conversations_state *state = NULL; +- int r = 0; +- struct index_state *index_state = NULL; +- struct dlist *dl; +- hash_table wanted_cids = HASH_TABLE_INITIALIZER; +- strarray_t folder_list = STRARRAY_INITIALIZER; +- struct index_init init; +- int i; +- +- // this one expunges each mailbox it enters, so we need to lock exclusively +- r = conversations_open_user(imapd_userid, 0/*shared*/, &state); +- if (r) goto out; +- +- construct_hash_table(&wanted_cids, 1024, 0); +- +- for (dl = cidlist->head; dl; dl = dl->next) { +- r = xconvfetch_lookup(state, dlist_num(dl), ifchangedsince, +- &wanted_cids, &folder_list); +- if (r) goto out; +- } +- +- /* unchanged, woot */ +- if (!folder_list.count) +- goto out; +- +- fetchargs->cidhash = &wanted_cids; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- +- for (i = 0; i < folder_list.count; i++) { +- const char *mboxname; +- mbentry_t *mbentry = NULL; +- +- if (state->folders_byname) mboxname = folder_list.data[i]; +- else { +- mboxlist_lookup_by_uniqueid(folder_list.data[i], &mbentry, NULL); +- if (!mbentry) continue; +- mboxname = mbentry->name; +- } +- +- r = index_open(mboxname, &init, &index_state); +- mboxlist_entry_free(&mbentry); +- if (r == IMAP_MAILBOX_NONEXISTENT) +- continue; +- if (r) +- goto out; +- +- index_checkflags(index_state, 0, 0); +- +- /* make sure \Deleted messages are expunged. Will also lock the +- * mailbox state and read any new information */ +- r = index_expunge(index_state, NULL, 1); +- +- if (!r) +- index_fetchresponses(index_state, NULL, /*usinguid*/1, +- fetchargs, NULL); +- +- index_close(&index_state); +- +- if (r) goto out; +- } +- +- r = 0; +- +-out: +- index_close(&index_state); +- conversations_commit(&state); +- free_hash_table(&wanted_cids, NULL); +- strarray_fini(&folder_list); +- return r; +-} +- + #undef PARSE_PARTIAL /* cleanup */ + + /* +@@ -6417,314 +5973,6 @@ error: + freesearchargs(searchargs); + } + +-/* +- * Perform a XCONVSORT or XCONVUPDATES command +- */ +-void cmd_xconvsort(char *tag, int updates) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct index_init init; +- struct index_state *oldstate = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvsort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, updates); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- if (updates) { +- /* in XCONVUPDATES, need to force a re-read from scratch into +- * a new index, because we ask for deleted messages */ +- +- oldstate = imapd_index; +- imapd_index = NULL; +- +- memset(&init, 0, sizeof(struct index_init)); +- init.userid = imapd_userid; +- init.authstate = imapd_authstate; +- init.out = imapd_out; +- init.want_expunged = 1; +- +- r = index_open(index_mboxname(oldstate), &init, &imapd_index); +- if (r) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- index_checkflags(imapd_index, 0, 0); +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xconvsort\r\n", tag); +- goto error; +- } +- +- if (updates) +- r = index_convupdates(imapd_index, sortcrit, searchargs, windowargs); +- else +- r = index_convsort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (oldstate) { +- index_close(&imapd_index); +- imapd_index = oldstate; +- } +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVSORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- if (oldstate) { +- if (imapd_index) index_close(&imapd_index); +- imapd_index = oldstate; +- } +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-/* +- * Perform a XCONVMULTISORT command. This is like XCONVSORT but returns +- * search results from multiple folders. It still requires a selected +- * mailbox, for two reasons: +- * +- * a) it's a useful shorthand for choosing what the current +- * conversations scope is, and +- * +- * b) the code to parse a search program currently relies on a selected +- * mailbox. +- * +- * Unlike ESEARCH it doesn't take folder names for scope, instead the +- * search scope is implicitly the current conversation scope. This is +- * implemented more or less by accident because both the Sphinx index +- * and the conversations database are hardcoded to be per-user. +- */ +-static void cmd_xconvmultisort(char *tag) +-{ +- int c; +- struct sortcrit *sortcrit = NULL; +- struct searchargs *searchargs = NULL; +- struct windowargs *windowargs = NULL; +- struct conversations_state *cstate = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xconvmultisort"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- if (!config_getswitch(IMAPOPT_CONVERSATIONS)) { +- prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag); +- eatline(imapd_in, ' '); +- return; +- } +- +- c = getsortcriteria(tag, &sortcrit); +- if (c == EOF) goto error; +- +- if (c != ' ') { +- prot_printf(imapd_out, "%s BAD Missing window args in XConvMultiSort\r\n", +- tag); +- goto error; +- } +- +- c = parse_windowargs(tag, &windowargs, /*updates*/0); +- if (c != ' ') +- goto error; +- +- /* open the conversations state first - we don't care if it fails, +- * because that probably just means it's already open */ +- // this codepath might expunge, so we can't open shared +- conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate); +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to XconvMultiSort\r\n", tag); +- goto error; +- } +- +- r = index_convmultisort(imapd_index, sortcrit, searchargs, windowargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- if (CONFIG_TIMING_VERBOSE) { +- char *s = sortcrit_as_string(sortcrit); +- syslog(LOG_DEBUG, "XCONVMULTISORT (%s) processing time %s sec", +- s, mytime); +- free(s); +- } +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- if (cstate) conversations_commit(&cstate); +- freesortcrit(sortcrit); +- freesearchargs(searchargs); +- free_windowargs(windowargs); +- return; +- +-error: +- if (cstate) conversations_commit(&cstate); +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- +-static void cmd_xsnippets(char *tag) +-{ +- int c; +- struct searchargs *searchargs = NULL; +- struct snippetargs *snippetargs = NULL; +- clock_t start = clock(); +- char mytime[100]; +- int r; +- +- if (backend_current) { +- /* remote mailbox */ +- const char *cmd = "Xsnippets"; +- +- prot_printf(backend_current->out, "%s %s ", tag, cmd); +- if (!pipe_command(backend_current, 65536)) { +- pipe_including_tag(backend_current, tag, 0); +- } +- return; +- } +- assert(imapd_index); +- +- c = get_snippetargs(&snippetargs); +- if (c == EOF) { +- prot_printf(imapd_out, "%s BAD Syntax error in snippet arguments\r\n", tag); +- goto error; +- } +- if (c != ' ') { +- prot_printf(imapd_out, +- "%s BAD Unexpected arguments in Xsnippets\r\n", tag); +- goto error; +- } +- +- /* need index loaded to even parse searchargs! */ +- searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, +- &imapd_namespace, imapd_userid, imapd_authstate, +- imapd_userisadmin || imapd_userisproxyadmin); +- c = get_search_program(imapd_in, imapd_out, searchargs); +- if (c == EOF) goto error; +- +- if (!IS_EOL(c, imapd_in)) { +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to Xsnippets\r\n", tag); +- goto error; +- } +- +- r = index_snippets(imapd_index, snippetargs, searchargs); +- +- if (r < 0) { +- prot_printf(imapd_out, "%s NO %s\r\n", tag, +- error_message(r)); +- goto error; +- } +- +- snprintf(mytime, sizeof(mytime), "%2.3f", +- (clock() - start) / (double) CLOCKS_PER_SEC); +- prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag, +- error_message(IMAP_OK_COMPLETED), mytime); +- +-out: +- freesearchargs(searchargs); +- free_snippetargs(&snippetargs); +- return; +- +-error: +- eatline(imapd_in, (c == EOF ? ' ' : c)); +- goto out; +-} +- + static void cmd_xstats(char *tag) + { + int metric; +@@ -11124,81 +10372,6 @@ out_noprint: + seqset_free(&uids); + } + +-static void free_snippetargs(struct snippetargs **sap) +-{ +- while (*sap) { +- struct snippetargs *sa = *sap; +- *sap = sa->next; +- free(sa->mboxname); +- free(sa->uids.data); +- free(sa); +- } +-} +- +-static int get_snippetargs(struct snippetargs **sap) +-{ +- int c; +- struct snippetargs **prevp = sap; +- struct snippetargs *sa = NULL; +- struct buf arg = BUF_INITIALIZER; +- uint32_t uid; +- char *intname = NULL; +- +- c = prot_getc(imapd_in); +- if (c != '(') goto syntax_error; +- +- for (;;) { +- c = prot_getc(imapd_in); +- if (c == ')') break; +- if (c != '(') goto syntax_error; +- +- c = getastring(imapd_in, imapd_out, &arg); +- if (c != ' ') goto syntax_error; +- +- intname = mboxname_from_external(buf_cstring(&arg), &imapd_namespace, imapd_userid); +- +- /* allocate a new snippetargs */ +- sa = xzmalloc(sizeof(struct snippetargs)); +- sa->mboxname = xstrdup(intname); +- /* append to the list */ +- *prevp = sa; +- prevp = &sa->next; +- +- c = getuint32(imapd_in, &sa->uidvalidity); +- if (c != ' ') goto syntax_error; +- +- c = prot_getc(imapd_in); +- if (c != '(') break; +- for (;;) { +- c = getuint32(imapd_in, &uid); +- if (c != ' ' && c != ')') goto syntax_error; +- if (sa->uids.count + 1 > sa->uids.alloc) { +- sa->uids.alloc += 64; +- sa->uids.data = xrealloc(sa->uids.data, +- sizeof(uint32_t) * sa->uids.alloc); +- } +- sa->uids.data[sa->uids.count++] = uid; +- if (c == ')') break; +- } +- +- c = prot_getc(imapd_in); +- if (c != ')') goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') goto syntax_error; +- +-out: +- free(intname); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free_snippetargs(sap); +- c = EOF; +- goto out; +-} +- + static void cmd_dump(char *tag, char *name, int uid_start) + { + int r = 0; +@@ -12694,185 +11867,6 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + return EOF; + } + +-static int parse_windowargs(const char *tag, +- struct windowargs **wa, +- int updates) +-{ +- struct windowargs windowargs; +- struct buf arg = BUF_INITIALIZER; +- struct buf ext_folder = BUF_INITIALIZER; +- int c; +- +- memset(&windowargs, 0, sizeof(windowargs)); +- +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c != '(') { +- /* no window args at all */ +- prot_ungetc(c, imapd_in); +- goto out; +- } +- +- for (;;) +- { +- c = prot_getc(imapd_in); +- if (c == EOF) +- goto out; +- if (c == ')') +- break; /* end of window args */ +- +- prot_ungetc(c, imapd_in); +- c = getword(imapd_in, &arg); +- if (!arg.len) +- goto syntax_error; +- +- if (!strcasecmp(arg.s, "CONVERSATIONS")) +- windowargs.conversations = 1; +- else if (!strcasecmp(arg.s, "POSITION")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.position); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.position == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "ANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "MULTIANCHOR")) { +- if (updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.anchor); +- if (c != ' ') +- goto syntax_error; +- c = getastring(imapd_in, imapd_out, &ext_folder); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.offset); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.limit); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (windowargs.anchor == 0) +- goto syntax_error; +- } +- else if (!strcasecmp(arg.s, "CHANGEDSINCE")) { +- if (!updates) +- goto syntax_error; +- windowargs.changedsince = 1; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getmodseq(imapd_in, &windowargs.modseq); +- if (c != ' ') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.uidnext); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- } else if (!strcasecmp(arg.s, "UPTO")) { +- if (!updates) +- goto syntax_error; +- if (c != ' ') +- goto syntax_error; +- c = prot_getc(imapd_in); +- if (c != '(') +- goto syntax_error; +- c = getuint32(imapd_in, &windowargs.upto); +- if (c != ')') +- goto syntax_error; +- c = prot_getc(imapd_in); +- +- if (windowargs.upto == 0) +- goto syntax_error; +- } +- else +- goto syntax_error; +- +- if (c == ')') +- break; +- if (c != ' ') +- goto syntax_error; +- } +- +- c = prot_getc(imapd_in); +- if (c != ' ') +- goto syntax_error; +- +-out: +- /* these two are mutually exclusive */ +- if (windowargs.anchor && windowargs.position) +- goto syntax_error; +- /* changedsince is mandatory for XCONVUPDATES +- * and illegal for XCONVSORT */ +- if (!!updates != windowargs.changedsince) +- goto syntax_error; +- +- if (ext_folder.len) { +- windowargs.anchorfolder = mboxname_from_external(buf_cstring(&ext_folder), +- &imapd_namespace, +- imapd_userid); +- } +- +- *wa = xmemdup(&windowargs, sizeof(windowargs)); +- buf_free(&ext_folder); +- buf_free(&arg); +- return c; +- +-syntax_error: +- free(windowargs.anchorfolder); +- buf_free(&ext_folder); +- prot_printf(imapd_out, "%s BAD Syntax error in window arguments\r\n", tag); +- if (c != EOF) prot_ungetc(c, imapd_in); +- return EOF; +-} +- +-static void free_windowargs(struct windowargs *wa) +-{ +- if (!wa) +- return; +- free(wa->anchorfolder); +- free(wa); +-} +- + /* + * Parse LIST selection options. + * The command has been parsed up to and including the opening '('. +-- +2.39.2 + + +From c34f94d56bc0c872b3ad2616fdf6cd346cf2d69e Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Mon, 26 Feb 2024 10:11:15 -0500 +Subject: [PATCH 10/16] imapd.c: add 'maxliteral' option + +--- + changes/next/imap_literal_limits | 19 +++ + imap/imap_err.et | 3 + + imap/imapd.c | 213 ++++++++++++++++++++++--------- + imap/imapparse.c | 73 ++++++----- + lib/imapoptions | 22 +++- + lib/libconfig.c | 7 + + lib/libconfig.h | 1 + + 7 files changed, 244 insertions(+), 94 deletions(-) + create mode 100644 changes/next/imap_literal_limits + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +new file mode 100644 +index 000000000..c7fc35bbc +--- /dev/null ++++ b/changes/next/imap_literal_limits +@@ -0,0 +1,19 @@ ++Description: ++ ++Adds a config option to limit the size of a single literal allowed ++by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++ ++ ++Config changes: ++ ++New 'maxliteral' option. ++ ++ ++Upgrade instructions: ++ ++None. ++ ++ ++GitHub issue: ++ ++None. +diff --git a/imap/imap_err.et b/imap/imap_err.et +index d90e30a4b..5768f49d1 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_LITERAL_TOO_LARGE, ++ "[TOOBIG] Literal size exceeds fixed limit" ++ + ec IMAP_LITERAL_MINUS_TOO_LARGE, + "[TOOBIG] Non-synchronizing literal size exceeds 4K" + +diff --git a/imap/imapd.c b/imap/imapd.c +index 0ea16979a..000b9416d 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -1441,7 +1441,7 @@ static void cmdloop(void) + if (c != ' ' || (strcmp("$", arg1.s) && !imparse_issequence(arg1.s))) + goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/0); +@@ -1454,7 +1454,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1481,7 +1481,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 0, 0); + +@@ -1493,7 +1493,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, NULL); + +@@ -1545,7 +1545,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Examine")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -1578,7 +1578,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Getacl")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getacl(tag.s, arg1.s); + +@@ -1603,7 +1603,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquota")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquota(tag.s, arg1.s); + +@@ -1612,7 +1612,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Getquotaroot")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_getquotaroot(tag.s, arg1.s); + +@@ -1737,7 +1737,7 @@ static void cmdloop(void) + + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + c = parsecreateargs(&extargs); + if (c == EOF) goto badpartition; +@@ -1753,7 +1753,7 @@ static void cmdloop(void) + /* delete a mailbox locally only */ + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_delete(tag.s, arg1.s, 1, 1); + +@@ -1766,7 +1766,7 @@ static void cmdloop(void) + if (!strcmp(cmd.s, "Myrights")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_myrights(tag.s, arg1.s); + +@@ -1776,7 +1776,7 @@ static void cmdloop(void) + if (readonly) goto noreadonly; + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if(c == EOF) goto missingargs; ++ if(c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_mupdatepush(tag.s, arg1.s); + +@@ -1794,7 +1794,7 @@ static void cmdloop(void) + if (c != ' ' || (strcmp("$", arg1.s) && !imparse_issequence(arg1.s))) + goto badsequence; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + + cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/1); +@@ -1829,7 +1829,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg1); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + havepartition = 1; + c = getword(imapd_in, &arg3); +@@ -1902,7 +1902,7 @@ static void cmdloop(void) + if (c == ' ') { + have_mbox = 1; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (c == ' ') { + have_mech = 1; + c = getword(imapd_in, &arg2); +@@ -1976,7 +1976,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Select")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + prot_ungetc(c, imapd_in); + + cmd_select(tag.s, cmd.s, arg1.s); +@@ -2001,7 +2001,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 1); +@@ -2019,7 +2019,7 @@ static void cmdloop(void) + c = getastring(imapd_in, imapd_out, &arg2); + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg3); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_setacl(tag.s, arg1.s, arg2.s, arg3.s); + +@@ -2220,7 +2220,7 @@ static void cmdloop(void) + havenamespace = 1; + c = getastring(imapd_in, imapd_out, &arg2); + } +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + if (havenamespace) { + cmd_changesub(tag.s, arg1.s, arg2.s, 0); +@@ -2367,7 +2367,7 @@ static void cmdloop(void) + else if (!strcmp(cmd.s, "Xmeid")) { + if (c != ' ') goto missingargs; + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto missingargs; ++ if (c <= EOF) goto missingargs; + if (!IS_EOL(c, imapd_in)) goto extraargs; + cmd_xmeid(tag.s, arg1.s); + } +@@ -2379,7 +2379,7 @@ static void cmdloop(void) + + do { + c = getastring(imapd_in, imapd_out, &arg1); +- if (c == EOF) goto aps_missingargs; ++ if (c <= EOF) goto aps_missingargs; + + if (!strcmp(arg1.s, "mailboxes")) { + c = prot_getc(imapd_in); +@@ -2391,7 +2391,7 @@ static void cmdloop(void) + prot_ungetc(c, imapd_in); + do { + c = getastring(imapd_in, imapd_out, &arg2); +- if (c == EOF) break; ++ if (c <= EOF) break; + strarray_push(&applepushserviceargs.mailboxes, arg2.s); + } while (c == ' '); + } +@@ -2477,6 +2477,8 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + missingargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Missing required argument to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); +@@ -2489,11 +2491,18 @@ static void cmdloop(void) + strarray_fini(&applepushserviceargs.mailboxes); + + extraargs: ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; ++ + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to %s\r\n", tag.s, cmd.s); + eatline(imapd_in, c); + continue; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in %s\r\n", ++ tag.s, error_message(IMAP_LITERAL_TOO_LARGE), cmd.s); ++ continue; ++ + badsequence: + prot_printf(imapd_out, + "%s BAD Invalid sequence in %s\r\n", tag.s, cmd.s); +@@ -2666,10 +2675,14 @@ static void cmd_login(char *tag, char *user) + + if (!IS_EOL(c, imapd_in)) { + buf_free(&passwdbuf); +- prot_printf(imapd_out, +- "%s BAD Unexpected extra arguments to LOGIN\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in LOGIN\r\n", tag, error_message(c)); ++ } else { ++ prot_printf(imapd_out, ++ "%s BAD Unexpected extra arguments to LOGIN\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3084,10 +3097,16 @@ static void cmd_id(char *tag) + /* get field value */ + (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || + (c != ' ' && c != ')')) { +- prot_printf(imapd_out, +- "%s BAD Invalid field-value pair in Id\r\n", +- tag); +- eatline(imapd_in, c); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Id\r\n", ++ tag, error_message(c)); ++ } ++ else { ++ prot_printf(imapd_out, ++ "%s BAD Invalid field-value pair in Id\r\n", ++ tag); ++ eatline(imapd_in, c); ++ } + return; + } + +@@ -3777,6 +3796,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + else if (!strcasecmp(arg.s, "URL")) { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ' && c != ')') { + *parseerr = "Missing URL in Append command"; + return IMAP_PROTOCOL_ERROR; +@@ -3975,8 +3995,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + c = parse_annotate_store_data(tag, + /*permessage_flag*/1, + &curstage->annotations); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto cleanup; + } + qdiffs[QUOTA_ANNOTSTORAGE] += sizeentryatts(curstage->annotations); +@@ -4103,6 +4122,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + if (r == IMAP_PROTOCOL_ERROR && parseerr) { + prot_printf(imapd_out, "%s BAD %s\r\n", tag, parseerr); ++ } else if (r == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); + } else if (r == IMAP_BADURL) { + prot_printf(imapd_out, "%s NO [BADURL \"%s\"] %s\r\n", + tag, url, parseerr); +@@ -4647,8 +4668,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + /*permessage_flag*/1, + &fa->entries, + &fa->attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + if (c != ')') { +@@ -4761,6 +4781,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -4991,6 +5016,11 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in %s %s\r\n", ++ tag, error_message(c), cmd, fetchatt.s); ++ goto freeargs; ++ } + for (p = fieldname.s; *p; p++) { + if (*p <= ' ' || *p & 0x80 || *p == ':') break; + } +@@ -5506,8 +5536,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + c = parse_annotate_store_data(tag, /*permessage_flag*/1, + &storeargs.entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeflags; + } + storeargs.namespace = &imapd_namespace; +@@ -5748,6 +5777,11 @@ static void cmd_search(char *tag, char *cmd) + return; + } + ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Search\r\n", tag, error_message(c)); ++ goto done; ++ } ++ + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Search\r\n", tag); + eatline(imapd_in, c); +@@ -6044,16 +6078,19 @@ static void cmd_thread(char *tag, int usinguid) + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +- freesearchargs(searchargs); +- return; ++ goto done; ++ } ++ ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Thread\r\n", tag, error_message(c)); ++ goto done; + } + + if (!IS_EOL(c, imapd_in)) { + prot_printf(imapd_out, + "%s BAD Unexpected extra arguments to Thread\r\n", tag); + eatline(imapd_in, c); +- freesearchargs(searchargs); +- return; ++ goto done; + } + + n = index_thread(imapd_index, alg, searchargs, usinguid); +@@ -6062,6 +6099,7 @@ static void cmd_thread(char *tag, int usinguid) + prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, + error_message(IMAP_OK_COMPLETED), n, mytime); + ++ done: + freesearchargs(searchargs); + return; + } +@@ -7635,8 +7673,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == '(') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistselopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + return; + } + } +@@ -7648,6 +7685,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + + /* Read in reference name */ + c = getastring(imapd_in, imapd_out, &reference); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF && !*reference.s) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: reference name\r\n", +@@ -7670,6 +7708,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); + if (c != ' ') break; +@@ -7685,6 +7724,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + else { + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &buf); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing required argument to List: mailbox pattern\r\n", +@@ -7699,8 +7739,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + if (c == ' ') { + listargs->cmd = LIST_CMD_EXTENDED; + c = getlistretopts(tag, listargs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + } +@@ -7719,6 +7758,10 @@ static void getlistargs(char *tag, struct listargs *listargs) + + return; + ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in List\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ + freeargs: + strarray_fini(&listargs->pat); + strarray_fini(&listargs->metaitems); +@@ -9125,6 +9168,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -9152,6 +9196,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -9174,6 +9219,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute(s)\r\n", tag); +@@ -9201,6 +9247,7 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -9213,8 +9260,13 @@ static int parse_annotate_fetch_data(const char *tag, + return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9245,6 +9297,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9271,6 +9324,7 @@ static int parse_metadata_string_or_list(const char *tag, + /* single entry -- add it to the list */ + prot_ungetc(c, imapd_in); + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9289,8 +9343,13 @@ static int parse_metadata_string_or_list(const char *tag, + if (c == ' ' || c == '\r' || c == ')') return c; + + baddata: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9342,6 +9401,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation entry\r\n", tag); +@@ -9362,6 +9422,7 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &attrib); + else + c = getqstring(imapd_in, imapd_out, &attrib); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation attribute\r\n", tag); +@@ -9375,6 +9436,7 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing annotation value\r\n", tag); +@@ -9416,8 +9478,14 @@ static int parse_annotate_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + /* +@@ -9450,6 +9518,7 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing metadata entry\r\n", tag); +@@ -9462,6 +9531,7 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, + "%s BAD Missing metadata value\r\n", tag); +@@ -9502,7 +9572,7 @@ static int parse_metadata_store_data(const char *tag, + + if (c != ')') { + prot_printf(imapd_out, +- "%s BAD Missing close paren in annotation entry list \r\n", ++ "%s BAD Missing close paren in metadata entry list \r\n", + tag); + goto baddata; + } +@@ -9513,8 +9583,14 @@ static int parse_metadata_store_data(const char *tag, + + baddata: + if (attvalues) freeattvalues(attvalues); +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; ++ ++ maxliteral: ++ if (attvalues) freeattvalues(attvalues); ++ prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", ++ tag, error_message(IMAP_LITERAL_TOO_LARGE)); ++ return IMAP_LITERAL_TOO_LARGE; + } + + static void getannotation_response(const char *mboxname, +@@ -9699,8 +9775,7 @@ static void cmd_getannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_fetch_data(tag, /*permessage_flag*/0, &entries, &attribs); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -9934,8 +10009,10 @@ static void cmd_getmetadata(const char *tag) + while (nlists < 3) + { + c = parse_metadata_string_or_list(tag, &lists[nlists], &is_list[nlists]); ++ if (c <= EOF) goto freeargs; ++ + nlists++; +- if (c == '\r' || c == EOF) ++ if (c == '\r') + break; + } + +@@ -10092,8 +10169,7 @@ static void cmd_setannotation(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_annotate_store_data(tag, 0, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -10160,8 +10236,7 @@ static void cmd_setmetadata(const char *tag, char *mboxpat) + annotate_state_t *astate = NULL; + + c = parse_metadata_store_data(tag, &entryatts); +- if (c == EOF) { +- eatline(imapd_in, c); ++ if (c <= EOF) { + goto freeargs; + } + +@@ -10269,6 +10344,10 @@ static void cmd_xwarmup(const char *tag) + /* parse arguments: expect '('')' */ + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Xwarmup\r\n", tag, error_message(c)); ++ goto out_noprint; ++ } + if (c != ' ') { + syntax_error: + prot_printf(imapd_out, "%s BAD syntax error in %s\r\n", tag, cmd); +@@ -11779,9 +11858,11 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_ANNOTATION; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ' ') goto missingarg; + (*sortcrit)[n].args.annot.entry = xstrdup(criteria.s); + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + if (!strcmp(criteria.s, "value.shared")) + userid = ""; +@@ -11799,6 +11880,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11812,6 +11894,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + (*sortcrit)[n].key = SORT_HASCONVFLAG; + if (c != ' ') goto missingarg; + c = getastring(imapd_in, imapd_out, &criteria); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto missingarg; + (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); + } +@@ -11917,10 +12000,10 @@ static int getlistselopts(char *tag, struct listargs *args) + + strarray_t options = STRARRAY_INITIALIZER; + c = parse_metadata_string_or_list(tag, &options, NULL); ++ if (c <= EOF) return c; + parse_getmetadata_options(&options, &opts); + args->metaopts = opts; + strarray_fini(&options); +- if (c == EOF) return EOF; + } else { + prot_printf(imapd_out, + "%s BAD Invalid List selection option \"%s\"\r\n", +@@ -11948,7 +12031,7 @@ static int getlistselopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -12018,7 +12101,7 @@ static int getlistretopts(char *tag, struct listargs *args) + args->ret |= LIST_RET_METADATA; + /* outputs the error for us */ + c = parse_metadata_string_or_list(tag, &args->metaitems, NULL); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else { + prot_printf(imapd_out, +@@ -12039,7 +12122,7 @@ static int getlistretopts(char *tag, struct listargs *args) + return prot_getc(imapd_in); + + bad: +- if (c != EOF) prot_ungetc(c, imapd_in); ++ eatline(imapd_in, c); + return EOF; + } + +@@ -13232,6 +13315,11 @@ static void cmd_urlfetch(char *tag) + else prot_ungetc(c, imapd_in); + + c = getastring(imapd_in, imapd_out, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Urlfetch\r\n", ++ tag, error_message(c)); ++ return; ++ } + (void)prot_putc(' ', imapd_out); + prot_printstring(imapd_out, arg.s); + +@@ -13464,6 +13552,11 @@ static void cmd_genurlauth(char *tag) + char *intname = NULL; + + c = getastring(imapd_in, imapd_out, &arg1); ++ if (c == IMAP_LITERAL_TOO_LARGE) { ++ prot_printf(imapd_out, "%s NO %s in Genurlauth\r\n", ++ tag, error_message(c)); ++ return; ++ } + if (c != ' ') { + prot_printf(imapd_out, + "%s BAD Missing required argument to Genurlauth\r\n", +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 5da6af1b8..17e0804ca 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -159,6 +159,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* Fail per RFC 7888, Section 4, choice 2 */ + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ /* Fail per RFC 7888, Section 4, choice 2 */ ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ } + isnowait++; + c = prot_getc(pin); + } +@@ -181,6 +185,10 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + + if (!isnowait) { ++ if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { ++ return IMAP_LITERAL_TOO_LARGE; ++ } ++ + prot_printf(pout, "+ go ahead\r\n"); + prot_flush(pout); + } +@@ -628,6 +636,7 @@ EXPORTED int get_search_source_mboxes(struct protstream *pin, + + bad: + buf_free(&extname); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != EOF) prot_ungetc(c, pin); + return EOF; + } +@@ -691,6 +700,7 @@ EXPORTED int get_search_source_opts(struct protstream *pin, + + } while (c == ' '); + ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != ')') { + prot_printf(pout, + "%s BAD Missing close parenthesis in Search\r\n", +@@ -818,7 +828,7 @@ static int get_search_annotation(struct protstream *pin, + + /* parse the value */ + c = getbnstring(pin, pout, &value); +- if (c == EOF) ++ if (c <= EOF) + goto bad; + + sa = xzmalloc(sizeof(*sa)); +@@ -839,6 +849,7 @@ bad: + buf_free(&attrib); + buf_free(&value); + ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c != EOF) prot_ungetc(c, pin); + return EOF; + } +@@ -971,7 +982,7 @@ static int get_search_criterion(struct protstream *pin, + do { + c = get_search_criterion(pin, pout, e, base); + } while (c == ' '); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ')') { + prot_printf(pout, "%s BAD Missing required close paren in Search command\r\n", + base->tag); +@@ -1022,7 +1033,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "annotation")) { /* RFC 5257 */ + struct searchannot *annot = NULL; + c = get_search_annotation(pin, pout, base, c, &annot); +- if (c == EOF) ++ if (c <= EOF) + goto badcri; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("annotation"); +@@ -1043,23 +1054,15 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "bcc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "body")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } +- else if (!strcmp(criteria.s, "fuzzy")) { +- if (c != ' ') goto missingarg; +- base->fuzzy_depth++; +- c = get_search_criterion(pin, pout, parent, base); +- base->fuzzy_depth--; +- if (c == EOF) return EOF; +- break; +- } + else goto badcri; + break; + +@@ -1067,7 +1070,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "cc")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (hasconv && !strcmp(criteria.s, "convflag")) { /* nonstandard */ +@@ -1130,7 +1133,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "deliveredto")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1140,7 +1143,7 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "emailid")) { /* RFC 8474 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1153,7 +1156,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "folder")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find("folder"); + e->value.s = mboxname_from_external(arg.s, base->namespace, base->userid); +@@ -1161,7 +1164,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "from")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "fuzzy")) { /* RFC 6203 */ +@@ -1169,7 +1172,7 @@ static int get_search_criterion(struct protstream *pin, + base->fuzzy_depth++; + c = get_search_criterion(pin, pout, parent, base); + base->fuzzy_depth--; +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else goto badcri; + break; +@@ -1180,7 +1183,7 @@ static int get_search_criterion(struct protstream *pin, + c = getastring(pin, pout, &arg); + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg2); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + + e = search_expr_new(parent, SEOP_MATCH); + e->attr = search_attr_find_field(arg.s); +@@ -1197,7 +1200,7 @@ static int get_search_criterion(struct protstream *pin, + if ((base->state & GETSEARCH_SOURCE) && + !strcmp(criteria.s, "in")) { /* RFC 7377 */ + c = get_search_source_opts(pin, pout, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + keep_charset = 1; + } + else goto badcri; +@@ -1252,7 +1255,7 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_NOT); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "new")) { /* RFC 3501 */ + e = search_expr_new(parent, SEOP_AND); +@@ -1267,10 +1270,10 @@ static int get_search_criterion(struct protstream *pin, + if (c != ' ') goto missingarg; + e = search_expr_new(parent, SEOP_OR); + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + if (c != ' ') goto missingarg; + c = get_search_criterion(pin, pout, e, base); +- if (c == EOF) return EOF; ++ if (c <= EOF) return c; + } + else if (!strcmp(criteria.s, "old")) { /* RFC 3501 */ + indexflag_match(parent, MESSAGE_RECENT, /*not*/1); +@@ -1389,6 +1392,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spamabove")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_GE); + e->attr = search_attr_find("spamscore"); +@@ -1397,6 +1401,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "spambelow")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; + if (c == EOF) goto badnumber; + e = search_expr_new(parent, SEOP_LT); + e->attr = search_attr_find("spamscore"); +@@ -1405,7 +1410,7 @@ static int get_search_criterion(struct protstream *pin, + else if (!strcmp(criteria.s, "subject")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1415,19 +1420,19 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "to")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "text")) { /* RFC 3501 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, criteria.s, base); + } + else if (!strcmp(criteria.s, "threadid")) { /* RFC 8474 */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + bytestring_match(parent, arg.s, criteria.s, base); + } + else goto badcri; +@@ -1478,25 +1483,25 @@ static int get_search_criterion(struct protstream *pin, + if (!strcmp(criteria.s, "xattachmentname")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentname", base); + } + else if (!strcmp(criteria.s, "xattachmentbody")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "attachmentbody", base); + } + else if (!strcmp(criteria.s, "xlistid")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "listid", base); + } + else if (!strcmp(criteria.s, "xcontenttype")) { /* nonstandard */ + if (c != ' ') goto missingarg; + c = getastring(pin, pout, &arg); +- if (c == EOF) goto missingarg; ++ if (c <= EOF) goto missingarg; + string_match(parent, arg.s, "contenttype", base); + } + else goto badcri; +@@ -1527,6 +1532,8 @@ static int get_search_criterion(struct protstream *pin, + + default: + badcri: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Invalid Search criteria\r\n", base->tag); + if (c != EOF) prot_ungetc(c, pin); + return EOF; +@@ -1542,6 +1549,8 @@ static int get_search_criterion(struct protstream *pin, + return c; + + missingarg: ++ if (c == IMAP_LITERAL_TOO_LARGE) return c; ++ + prot_printf(pout, "%s BAD Missing required argument to Search %s\r\n", + base->tag, criteria.s); + if (c != EOF) prot_ungetc(c, pin); +diff --git a/lib/imapoptions b/lib/imapoptions +index 175604340..8810639b5 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1717,13 +1717,31 @@ Blank lines and lines beginning with ``#'' are ignored. + .PP + If no unit is specified, bytes is assumed. */ + ++{ "maxliteral", "128K", BYTESIZE, "UNRELEASED" } ++/* Maximum size of a single literal allowed by the IMAP parser. ++.PP ++ If set to 0, a large internally-defined limit will be applied. ++.PP ++ If no unit is specified, bytes is assumed. ++.PP ++ Literals used for message [part] data in APPEND are only limited by ++ the 'maxmessagesize' option. ++.PP ++ If the 'literalminus' option is enabled, non-synchonizing literals ++ will be limited to the lesser of 4K and either 'maxliteral' or ++ 'maxmessagesize', depending on the use-case. */ ++ + { "maxquoted", "128K", BYTESIZE, "3.8.0" } +-/* Maximum size of a single quoted string for the parser. ++/* Maximum size of a single quoted string allowed by the IMAP parser. ++.PP ++ If set to 0, a large internally-defined limit will be applied. + .PP + If no unit is specified, bytes is assumed. */ + + { "maxword", "128K", BYTESIZE, "3.8.0" } +-/* Maximum size of a single word for the parser. ++/* Maximum size of a single word allowed by the IMAP parser. ++.PP ++ If set to 0, a large internally-defined limit will be applied. + .PP + If no unit is specified, bytes is assumed. */ + +diff --git a/lib/libconfig.c b/lib/libconfig.c +index df75c1b99..4582988df 100644 +--- a/lib/libconfig.c ++++ b/lib/libconfig.c +@@ -86,6 +86,7 @@ EXPORTED int config_auditlog; + EXPORTED int config_iolog; + EXPORTED unsigned config_maxword; + EXPORTED unsigned config_maxquoted; ++EXPORTED unsigned config_maxliteral; + EXPORTED int config_qosmarking; + EXPORTED int config_debug; + +@@ -599,6 +600,7 @@ EXPORTED void config_reset(void) + config_defdomain = NULL; + config_auditlog = 0; + config_serverinfo = 0; ++ config_maxliteral = 0; + config_maxquoted = 0; + config_maxword = 0; + config_qosmarking = 0; +@@ -808,6 +810,11 @@ EXPORTED void config_read(const char *alt_config, const int config_need_data) + tok_fini(&tok); + + /* set some limits */ ++ i64val = config_getbytesize(IMAPOPT_MAXLITERAL, 'B'); ++ if (i64val <= 0 || i64val > BYTESIZE_UNLIMITED) { ++ i64val = BYTESIZE_UNLIMITED; ++ } ++ config_maxliteral = i64val; + i64val = config_getbytesize(IMAPOPT_MAXQUOTED, 'B'); + if (i64val <= 0 || i64val > BYTESIZE_UNLIMITED) { + i64val = BYTESIZE_UNLIMITED; +diff --git a/lib/libconfig.h b/lib/libconfig.h +index 08de0845b..7796ba511 100644 +--- a/lib/libconfig.h ++++ b/lib/libconfig.h +@@ -97,6 +97,7 @@ extern enum enum_value config_virtdomains; + extern enum enum_value config_mupdate_config; + extern int config_auditlog; + extern int config_iolog; ++extern unsigned config_maxliteral; + extern unsigned config_maxquoted; + extern unsigned config_maxword; + extern int config_qosmarking; +-- +2.39.2 + + +From 5feff906c92200734da7cd654240bf45ab30d0a1 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:18:52 -0500 +Subject: [PATCH 11/16] prot.h: change bytes_in/out to uint64_t (for + long-running imapd) + +--- + backup/lcb.c | 4 ++-- + backup/lcb_compact.c | 4 ++-- + backup/lcb_verify.c | 8 ++++---- + imap/httpd.c | 14 ++++++++------ + imap/imapd.c | 18 ++++++++++-------- + imap/pop3d.c | 18 ++++++++++-------- + lib/prot.h | 8 ++++---- + 7 files changed, 40 insertions(+), 34 deletions(-) + +diff --git a/backup/lcb.c b/backup/lcb.c +index 3f68b1aaa..e1eefab89 100644 +--- a/backup/lcb.c ++++ b/backup/lcb.c +@@ -620,11 +620,11 @@ EXPORTED int backup_reindex(const char *name, + const char *error = prot_error(member); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + name, member_offset, prot_bytes_in(member), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + member_offset, prot_bytes_in(member), error); + + r = IMAP_IOERROR; +diff --git a/backup/lcb_compact.c b/backup/lcb_compact.c +index d398a06c0..b1616bd8f 100644 +--- a/backup/lcb_compact.c ++++ b/backup/lcb_compact.c +@@ -528,11 +528,11 @@ EXPORTED int backup_compact(const char *name, + const char *error = prot_error(in); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %i: %s", ++ "IOERROR: %s: error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + name, chunk->offset, prot_bytes_in(in), error); + + if (out) +- fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %i: %s\n", ++ fprintf(out, "error reading chunk at offset " OFF_T_FMT ", byte %" PRIu64 ": %s\n", + chunk->offset, prot_bytes_in(in), error); + + /* chunk is corrupt, discard the rest of it and get on with +diff --git a/backup/lcb_verify.c b/backup/lcb_verify.c +index ff2758552..347b9d37d 100644 +--- a/backup/lcb_verify.c ++++ b/backup/lcb_verify.c +@@ -234,10 +234,10 @@ static int _verify_message_cb(const struct backup_message *message, void *rock) + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, message->id, message->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading message %i at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + message->id, message->offset, prot_bytes_in(ps), error); + } + prot_free(ps); +@@ -540,10 +540,10 @@ static int verify_chunk_mailbox_links(struct backup *backup, struct backup_chunk + const char *error = prot_error(ps); + if (error && 0 != strcmp(error, PROT_EOF_STRING)) { + syslog(LOG_ERR, +- "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ "%s: error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + __func__, chunk->id, chunk->offset, prot_bytes_in(ps), error); + if (out) +- fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %i: %s", ++ fprintf(out, "error reading chunk %i data at offset " OFF_T_FMT ", byte %" PRIu64 ": %s", + chunk->id, chunk->offset, prot_bytes_in(ps), error); + r = EOF; + } +diff --git a/imap/httpd.c b/imap/httpd.c +index f5aae429f..ef9c314e9 100644 +--- a/imap/httpd.c ++++ b/imap/httpd.c +@@ -680,8 +680,8 @@ EXPORTED int http1_resp_body_chunk(struct transaction_t *txn, + static void httpd_reset(struct http_connection *conn) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + /* run any delayed actions */ + libcyrus_run_delayed(); +@@ -723,7 +723,8 @@ static void httpd_reset(struct http_connection *conn) + + if (config_auditlog) { + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + } + +@@ -1105,8 +1106,8 @@ void usage(void) + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1175,7 +1176,8 @@ void shut_down(int code) + + if (config_auditlog) + syslog(LOG_NOTICE, +- "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", ++ "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", + session_id(), bytes_in, bytes_out); + + saslprops_free(&saslprops); +diff --git a/imap/imapd.c b/imap/imapd.c +index 000b9416d..5e28e17fc 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -734,8 +734,8 @@ static int mlookup(const char *tag, const char *ext_name, + static void imapd_reset(void) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + /* run delayed commands first before closing anything */ + libcyrus_run_delayed(); +@@ -783,8 +783,9 @@ static void imapd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + imapd_in = imapd_out = NULL; + +@@ -1078,8 +1079,8 @@ void shut_down(int code) __attribute__((noreturn)); + void shut_down(int code) + { + int i; +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -1146,8 +1147,9 @@ void shut_down(int code) + : CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_OK); + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + if (protin) protgroup_free(protin); + +diff --git a/imap/pop3d.c b/imap/pop3d.c +index ee5a6fad8..3db7e2315 100644 +--- a/imap/pop3d.c ++++ b/imap/pop3d.c +@@ -325,8 +325,8 @@ static struct sasl_callback mysasl_cb[] = { + + static void popd_reset(void) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + proc_cleanup(); + +@@ -361,8 +361,9 @@ static void popd_reset(void) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + popd_in = popd_out = NULL; + +@@ -598,8 +599,8 @@ static void usage(void) + */ + void shut_down(int code) + { +- int bytes_in = 0; +- int bytes_out = 0; ++ uint64_t bytes_in = 0; ++ uint64_t bytes_out = 0; + + in_shutdown = 1; + +@@ -646,8 +647,9 @@ void shut_down(int code) + } + + if (config_auditlog) +- syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", +- session_id(), bytes_in, bytes_out); ++ syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" ++ " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", ++ session_id(), bytes_in, bytes_out); + + #ifdef HAVE_SSL + tls_shutdown_serverengine(); +diff --git a/lib/prot.h b/lib/prot.h +index 89b0b0a2a..94b22fad8 100644 +--- a/lib/prot.h ++++ b/lib/prot.h +@@ -131,8 +131,8 @@ struct protstream { + struct buf *writetobuf; + + int can_unget; +- int bytes_in; +- int bytes_out; ++ uint64_t bytes_in; ++ uint64_t bytes_out; + int isclient; /* read/write IMAP LITERAL+ */ + + /* Events */ +@@ -224,8 +224,8 @@ extern int prot_free(struct protstream *s); + extern int prot_setlog(struct protstream *s, int fd); + + /* Get traffic counts */ +-extern int prot_bytes_in(struct protstream *s); +-extern int prot_bytes_out(struct protstream *s); ++extern uint64_t prot_bytes_in(struct protstream *s); ++extern uint64_t prot_bytes_out(struct protstream *s); + #define prot_bytes_in(s) ((s)->bytes_in) + #define prot_bytes_out(s) ((s)->bytes_out) + +-- +2.39.2 + + +From ed73edb806a442133eaf439ac1844c95ba450752 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Wed, 21 Feb 2024 11:35:44 -0500 +Subject: [PATCH 12/16] imapd.c: rename 'maxsize' to 'maxmsgsize' + +--- + imap/imapd.c | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/imap/imapd.c b/imap/imapd.c +index 5e28e17fc..b883fcfd7 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -135,7 +135,7 @@ static int imaps = 0; + static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; +-static int64_t maxsize = 0; ++static int64_t maxmsgsize = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -904,8 +904,8 @@ int service_init(int argc, char **argv, char **envp) + + prometheus_increment(CYRUS_IMAP_READY_LISTENERS); + +- maxsize = config_getbytesize(IMAPOPT_MAXMESSAGESIZE, 'B'); +- if (maxsize <= 0) maxsize = BYTESIZE_UNLIMITED; ++ maxmsgsize = config_getbytesize(IMAPOPT_MAXMESSAGESIZE, 'B'); ++ if (maxmsgsize <= 0) maxmsgsize = BYTESIZE_UNLIMITED; + + return 0; + } +@@ -3505,7 +3505,7 @@ static void capa_response(int flags) + prot_printf(imapd_out, " IDLE"); + } + +- prot_printf(imapd_out, " APPENDLIMIT=%" PRIi64, maxsize); ++ prot_printf(imapd_out, " APPENDLIMIT=%" PRIi64, maxmsgsize); + } + + /* +@@ -4025,13 +4025,13 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxsize, &size, ++ r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From 4a321af3d3babe22853cd0421bff12cfe264f4cd Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Fri, 23 Feb 2024 15:08:42 -0500 +Subject: [PATCH 13/16] imapd.c: limit the total size of IMAP command arguments + +Only concerned with commands that can have an unlimited +number of arguments. +--- + changes/next/imap_literal_limits | 7 ++- + imap/imap_err.et | 3 + + imap/imapd.c | 97 +++++++++++++++++++++++++++++--- + imap/imapd.h | 1 + + imap/imapparse.c | 10 ++++ + lib/imapoptions | 9 +++ + 6 files changed, 116 insertions(+), 11 deletions(-) + +diff --git a/changes/next/imap_literal_limits b/changes/next/imap_literal_limits +index c7fc35bbc..f1ea34a0b 100644 +--- a/changes/next/imap_literal_limits ++++ b/changes/next/imap_literal_limits +@@ -1,12 +1,13 @@ + Description: + +-Adds a config option to limit the size of a single literal allowed +-by the IMAP parser. Also properly applies LITERAL- to IMAP APPEND. ++Adds config options to limit the size of a single literal allowed ++by the IMAP parser and to limit the total size of IMAP command arguments. ++Also properly applies LITERAL- to IMAP APPEND. + + + Config changes: + +-New 'maxliteral' option. ++New 'maxliteral' and 'maxargssize' options. + + + Upgrade instructions: +diff --git a/imap/imap_err.et b/imap/imap_err.et +index 5768f49d1..d1391199d 100644 +--- a/imap/imap_err.et ++++ b/imap/imap_err.et +@@ -69,6 +69,9 @@ ec IMAP_MESSAGE_TOO_LARGE, + ec IMAP_MESSAGE_TOOBIG, + "[TOOBIG] Message size exceeds fixed limit" + ++ec IMAP_ARGS_TOO_LARGE, ++ "[TOOBIG] Command arguments total size exceeds fixed limit" ++ + ec IMAP_LITERAL_TOO_LARGE, + "[TOOBIG] Literal size exceeds fixed limit" + +diff --git a/imap/imapd.c b/imap/imapd.c +index b883fcfd7..fb5dace4a 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -136,6 +136,8 @@ static sasl_ssf_t extprops_ssf = 0; + static int nosaslpasswdcheck = 0; + static int apns_enabled = 0; + static int64_t maxmsgsize = 0; ++static int64_t maxargssize = 0; ++static uint64_t maxargssize_mark = 0; + + /* PROXY STUFF */ + /* we want a list of our outgoing connections here and which one we're +@@ -907,6 +909,9 @@ int service_init(int argc, char **argv, char **envp) + maxmsgsize = config_getbytesize(IMAPOPT_MAXMESSAGESIZE, 'B'); + if (maxmsgsize <= 0) maxmsgsize = BYTESIZE_UNLIMITED; + ++ maxargssize = config_getbytesize(IMAPOPT_MAXARGSSIZE, 'B'); ++ if (maxargssize <= 0) maxargssize = BYTESIZE_UNLIMITED; ++ + return 0; + } + +@@ -1356,6 +1361,9 @@ static void cmdloop(void) + allowed when not logged in */ + if (!imapd_userid && !strchr("AELNCIS", cmd.s[0])) goto nologin; + ++ /* Set limit on the total number of bytes allowed for arguments */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + maxargssize; ++ + /* Start command timer */ + cmdtime_starttimer(); + +@@ -3942,6 +3950,9 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + curstage = xzmalloc(sizeof(*curstage)); + ptrarray_push(&stages, curstage); + ++ /* Set limit on the total number of bytes allowed for mailbox+append-opts */ ++ maxargssize_mark = prot_bytes_in(imapd_in) + (maxargssize - strlen(name)); ++ + /* now parsing "append-opts" in the ABNF */ + + /* Parse flags */ +@@ -3950,6 +3961,8 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + strarray_init(&curstage->flags); + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4057,15 +4070,23 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + } + + done: +- if (r) { +- eatline(imapd_in, c); +- } else { ++ switch (r) { ++ case IMAP_ZERO_LENGTH_LITERAL: ++ case IMAP_MESSAGE_TOO_LARGE: ++ break; ++ ++ case 0: + /* we should be looking at the end of the line */ +- if (!IS_EOL(c, imapd_in)) { +- parseerr = "junk after literal"; +- r = IMAP_PROTOCOL_ERROR; +- eatline(imapd_in, c); +- } ++ if (IS_EOL(c, imapd_in)) break; ++ ++ parseerr = "junk after literal"; ++ r = IMAP_PROTOCOL_ERROR; ++ ++ GCC_FALLTHROUGH ++ ++ default: ++ eatline(imapd_in, c); ++ break; + } + + /* Append from the stage(s) */ +@@ -4281,6 +4302,9 @@ static void cmd_select(char *tag, char *cmd, char *name) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto badlist; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { + client_capa |= CAPA_CONDSTORE; +@@ -4653,6 +4677,9 @@ static int parse_fetch_args(const char *tag, const char *cmd, + c = getword(imapd_in, &fetchatt); + } + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + switch (fetchatt.s[0]) { + case 'A': +@@ -4783,6 +4810,8 @@ badannotation: + } + do { + c = getastring(imapd_in, imapd_out, &fieldname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5117,6 +5146,9 @@ badannotation: + } + do { + c = getword(imapd_in, &fetchatt); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { + if (c != ' ') { +@@ -5463,6 +5495,9 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + do { + c = getword(imapd_in, &storemod); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { + if (c != ' ') { +@@ -5555,6 +5590,8 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + + for (;;) { + c = getword(imapd_in, &flagname); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -5750,6 +5787,8 @@ static void cmd_search(char *tag, char *cmd) + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); + ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + /* Set FUZZY search according to config and quirks */ + static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always"; + char *inbox = mboxname_user_mbox(imapd_userid, NULL); +@@ -5961,6 +6000,9 @@ static void cmd_sort(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + if (imapd_id.quirks & QUIRK_SEARCHFUZZY) + searchargs->fuzzy_depth++; + +@@ -6077,6 +6119,9 @@ static void cmd_thread(char *tag, int usinguid) + searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, + &imapd_namespace, imapd_userid, imapd_authstate, + imapd_userisadmin || imapd_userisproxyadmin); ++ ++ searchargs->maxargssize_mark = maxargssize_mark; ++ + c = get_search_program(imapd_in, imapd_out, searchargs); + if (c == EOF) { + eatline(imapd_in, ' '); +@@ -7710,6 +7755,8 @@ static void getlistargs(char *tag, struct listargs *listargs) + listargs->cmd = LIST_CMD_EXTENDED; + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8559,6 +8606,9 @@ void cmd_setquota(const char *tag, const char *quotaroot) + newquotas[res] = limit; + if (c == ')') break; + else if (c != ' ') goto badlist; ++ ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8711,6 +8761,9 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + c = getword(imapd_in, &arg); + if (arg.s[0] == '\0') goto bad; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { + statusitems |= STATUS_MESSAGES; +@@ -9094,6 +9147,9 @@ static int parsecreateargs(struct dlist **extargs) + /* new style RFC 4466 arguments */ + do { + c = getword(imapd_in, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + name = ucase(arg.s); + if (c != ' ') goto fail; + c = prot_getc(imapd_in); +@@ -9102,6 +9158,9 @@ static int parsecreateargs(struct dlist **extargs) + sub = dlist_newlist(res, name); + do { + c = getword(imapd_in, &val); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + dlist_setatom(sub, name, val.s); + } while (c == ' '); + if (c != ')') goto fail; +@@ -9170,6 +9229,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9221,6 +9282,8 @@ static int parse_annotate_fetch_data(const char *tag, + c = getastring(imapd_in, imapd_out, &arg); + else + c = getqstring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9299,6 +9362,8 @@ static int parse_metadata_string_or_list(const char *tag, + /* entry list */ + do { + c = getastring(imapd_in, imapd_out, &arg); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9403,6 +9468,8 @@ static int parse_annotate_store_data(const char *tag, + c = getastring(imapd_in, imapd_out, &entry); + else + c = getqstring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9445,6 +9512,9 @@ static int parse_annotate_store_data(const char *tag, + goto baddata; + } + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); + +@@ -9520,6 +9590,8 @@ static int parse_metadata_store_data(const char *tag, + do { + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9533,6 +9605,8 @@ static int parse_metadata_store_data(const char *tag, + + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11821,6 +11895,9 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + nsort = 0; + n = 0; + for (;;) { ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ + nsort += SORTGROWSIZE; +@@ -11970,6 +12047,8 @@ static int getlistselopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -12074,6 +12153,8 @@ static int getlistretopts(char *tag, struct listargs *args) + for (;;) { + c = getword(imapd_in, &buf); + ++ if (prot_bytes_in(imapd_in) > maxargssize_mark) ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapd.h b/imap/imapd.h +index dd1559a95..d8fe76016 100644 +--- a/imap/imapd.h ++++ b/imap/imapd.h +@@ -234,6 +234,7 @@ struct searchargs { + int state; + /* used only during parsing */ + int fuzzy_depth; ++ uint64_t maxargssize_mark; + + /* For ESEARCH & XCONVMULTISORT */ + const char *tag; +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 17e0804ca..2dceaed94 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -764,6 +764,11 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + goto bad; + } + ++ if (searchargs->maxargssize_mark && ++ prot_bytes_in(pin) > searchargs->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + } while (c == ' '); + + if (!(searchargs->returnopts & ~(SEARCH_RETURN_SAVE|SEARCH_RETURN_RELEVANCY))) { +@@ -1539,6 +1544,11 @@ static int get_search_criterion(struct protstream *pin, + return EOF; + } + ++ if (base->maxargssize_mark && ++ prot_bytes_in(pin) > base->maxargssize_mark) { ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ } ++ + if (!keep_charset) + base->state &= ~GETSEARCH_CHARSET_KEYWORD; + if (base->state & GETSEARCH_SOURCE) +diff --git a/lib/imapoptions b/lib/imapoptions +index 8810639b5..731962510 100644 +--- a/lib/imapoptions ++++ b/lib/imapoptions +@@ -1708,6 +1708,15 @@ Blank lines and lines beginning with ``#'' are ignored. + /* Maximum number of logged in sessions allowed per user, + zero means no limit */ + ++{ "maxargssize", "0", BYTESIZE, "UNRELEASED" } ++/* Maximum total size of arguments to an IMAP command that will be ++ accepted by Cyrus. ++ Commands with arguments that exceed this limit will be rejected. ++.PP ++ If set to 0 (the default), a large internally-defined limit will be applied. ++.PP ++ If no unit is specified, bytes is assumed. */ ++ + { "maxmessagesize", "0", BYTESIZE, "3.8.0" } + /* Maximum size of messages that will be accepted by Cyrus. This affects LMTP + deliveries, IMAP appends, DAV uploads, etc. Messages larger than this will +-- +2.39.2 + + +From 57bf6856838765020f74d4c649c002f7525fccb5 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Tue, 12 Mar 2024 23:16:30 -0400 +Subject: [PATCH 14/16] Add IMAPLimits.pm + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 529 ++++++++++++++++++++++ + cassandane/Cassandane/IMAPMessageStore.pm | 8 +- + 2 files changed, 534 insertions(+), 3 deletions(-) + create mode 100644 cassandane/Cassandane/Cyrus/IMAPLimits.pm + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +new file mode 100644 +index 000000000..9576d1d88 +--- /dev/null ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -0,0 +1,529 @@ ++#!/usr/bin/perl ++# ++# Copyright (c) 2011-2023 FastMail Pty Ltd. 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. The name "Fastmail Pty Ltd" must not be used to ++# endorse or promote products derived from this software without ++# prior written permission. For permission or any legal ++# details, please contact ++# FastMail Pty Ltd ++# PO Box 234 ++# Collins St West 8007 ++# Victoria ++# Australia ++# ++# 4. Redistributions of any form whatsoever must retain the following ++# acknowledgment: ++# "This product includes software developed by Fastmail Pty. Ltd." ++# ++# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, ++# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO ++# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT ++# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF ++# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER ++# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE ++# OF THIS SOFTWARE. ++# ++ ++package Cassandane::Cyrus::IMAPLimits; ++use strict; ++use warnings; ++use Mail::JMAPTalk 0.13; ++use Data::Dumper; ++ ++use lib '.'; ++use base qw(Cassandane::Cyrus::TestCase); ++use Cassandane::Util::Log; ++ ++my $email = < ++ ++Body ++EOF ++ ++$email =~ s/\r?\n/\r\n/gs; ++ ++my $toobig_email = $email . "X" x 100; ++ ++sub assert_bye_toobig ++{ ++ my ($self, $store) = @_; ++ ++ $store = $self->{store} if (!defined $store); ++ ++ # We want to override Mail::IMAPTalk's builtin handling of the BYE ++ # untagged response, as it will 'die' immediately without parsing ++ # the remainder of the line and especially without picking out the ++ # [TOOBIG] response code that we want to see. ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ bye => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ # Check that we got a BYE [TOOBIG] response ++ $store->idle_response($handlers, 1); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub assert_cmd_bye_toobig ++{ ++ my $self = shift; ++ my $cmd = shift; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->enable('qresync'); # IMAPTalk requires lower-case ++ $talk->select('INBOX'); ++ ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_bye_toobig(); ++} ++ ++sub assert_cmd_no_toobig ++{ ++ my $self = shift; ++ my $talk = shift; ++ my $cmd = shift; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ # Pick out the [TOOBIG] response code ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ $talk->_imap_cmd($cmd, 0, $handlers, @_); ++ ++ # Check that we got a NO [TOOBIG] response ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++sub new ++{ ++ my $class = shift; ++ ++ my $config = Cassandane::Config->default()->clone(); ++ $config->set(maxword => 25); ++ $config->set(maxquoted => 25); ++ $config->set(maxliteral => 25); ++ $config->set(literalminus => 1); ++ $config->set(maxargssize => 45); ++ $config->set(maxmessagesize => 100); ++ $config->set(event_groups => "message mailbox applepushservice"); ++ $config->set(aps_topic => "mail"); ++ ++ return $class->SUPER::new({ ++ adminstore => 1, ++ config => $config, ++ services => ['imap'], ++ }, @_); ++} ++ ++sub set_up ++{ ++ my ($self) = @_; ++ $self->SUPER::set_up(); ++} ++ ++sub tear_down ++{ ++ my ($self) = @_; ++ $self->SUPER::tear_down(); ++} ++ ++sub test_maxword ++{ ++ my ($self) = @_; ++ ++ # Oversized command name ++ $self->assert_cmd_bye_toobig("X" x 26); ++} ++ ++sub test_maxword_astring ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', "X" x 26); ++} ++ ++sub test_maxquoted ++{ ++ my ($self) = @_; ++ ++ # Oversized mailbox name ++ $self->assert_cmd_bye_toobig('SELECT', { Quote => "X" x 26 }); ++} ++ ++sub test_maxliteral_nosync ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " SELECT {26+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_maxliteral_sync ++{ ++ my ($self) = @_; ++ ++ # Unlike oversized non-sync literals which fatal() in one central location, ++ # oversized sync literals fail with a NO response in multiple places, ++ # so we test as many of those places as possible. ++ # Having said that, arguments parsed in cmdloop() or in get_search_criterion() ++ # are mostly handled centrally. ++ ++ # Authenticated State ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'SELECT', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Literal => "X" x 26 } ]); ++ ++ $self->assert_cmd_no_toobig($talk, 'ID', ++ [ { Quote => 'foo' }, { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LIST', ++ { Quote => '' }, { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'LISTRIGHTS', ++ 'INBOX', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETACL', ++ 'INBOX', 'anyone', { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'GETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SETMETADATA', ++ 'INBOX', [ '/comment', { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ { Literal => "X" x 26 }); ++ ++ $self->assert_cmd_no_toobig($talk, 'XAPPLEPUSHSERVICE', ++ 'FOO', { Literal => "X" x 26 }); ++ ++ # Selected State ++ $talk->select('INBOX'); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'ANNOTATION', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'BODY[HEADER.FIELDS', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'FETCH', ++ '1', [ 'RFC822.HEADER.LINES', ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', [ { Literal => "X" x 26 } ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'STORE', ++ '1', 'ANNOTATION', ++ [ { Quote => '/comment' }, ++ [ { Quote => 'value' }, ++ { Literal => "X" x 26 } ] ] ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'HEADER', 'SUBJECT', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'SEARCH', ++ 'ANNOTATION', '/comment', ++ 'value', { Literal => "X" x 26 } ); ++ ++ $self->assert_cmd_no_toobig($talk, 'ESEARCH', ++ 'IN', [ 'MAILBOXES', { Literal => "X" x 26 } ] ); ++} ++ ++sub test_maxargssize_append_flags ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ [ "X" x 25, "X" x 25 ], { Literal => $email } ); ++} ++ ++sub test_maxargssize_append_annot ++{ ++ my ($self) = @_; ++ ++ # Use MULTIAPPEND, fail the second ++ $self->assert_cmd_bye_toobig('APPEND', 'INBOX', ++ { Literal => $email }, ++ 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ], ++ { Literal => $email } ); ++} ++ ++sub test_maxargssize_create ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', "X" x 25, [ "X" x 25 ] ); ++} ++ ++sub test_maxargssize_create_ext ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('CREATE', ++ "X" x 5, [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_fetch ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY', 'ENVELOPE', 'FLAGS', ++ 'INTERNALDATE', 'RFC822.SIZE' ]); ++} ++ ++sub test_maxargssize_fetch_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ [ "X" x 25, "X" x 25 ] ], "X" x 5 ] ); ++} ++ ++sub test_maxargssize_fetch_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'ANNOTATION', ++ [ "X" x 5, [ "X" x 25, "X" x 25 ] ] ] ); ++} ++ ++sub test_maxargssize_fetch_headers ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('FETCH', '1', ++ [ 'BODY[HEADER.FIELDS', [ "X" x 25, "X" x 25 ] ] ); ++} ++ ++sub test_maxargssize_getmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('GETMETADATA', 'INBOX', [ "X" x 25, "X" x 25 ] ); ++} ++ ++sub test_maxargssize_list_multi ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', { Quote => '' }, [ "X" x 25, "X" x 25 ]); ++} ++ ++sub test_maxargssize_list_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ [ 'SUBSCRIBED', 'REMOTE', ++ 'RECURSIVEMATCH', 'SPECIAL-USE' ], ++ { Quote => '' }, '*'); ++} ++ ++sub test_maxargssize_list_return ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('LIST', ++ { Quote => '' }, '*', 'RETURN', ++ [ 'SUBSCRIBED', 'CHILDREN', ++ 'MYRIGHTS', 'SPECIAL-USE' ] ); ++} ++ ++sub test_maxargssize_search ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SEARCH', ++ 'TEXT', "X" x 25, 'TEXT', { Quote => "X" x 25 } ); ++} ++ ++sub test_maxargssize_multisearch ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('ESEARCH', ++ 'IN', [ 'MAILBOXES', [ "X" x 25, "X" x 25 ] ]); ++} ++ ++sub test_maxargssize_select ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SELECT', 'INBOX', ++ [ 'QRESYNC', [ '1234567890', '1234567890' ], ++ 'ANNOTATE' ] ); ++} ++ ++sub test_maxargssize_setmetadata ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ "X" x 25, { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setmetadata2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SETMETADATA', 'INBOX', ++ [ '/shared', { Quote => "X" x 25 }, ++ '/shared', { Quote => "X" x 25 } ] ); ++} ++ ++sub test_maxargssize_setquota ++{ ++ my ($self) = @_; ++ ++ my $store = $self->{adminstore}; ++ my $talk = $store->get_client(); ++ ++ $talk->_send_cmd('SETQUOTA', 'user.cassandane', ++ [ 'STORAGE', '1234567890', ++ 'MESSAGE', '1234567890', ++ 'MAILBOX', '1234567890' ] ); ++ $self->assert_bye_toobig($store); ++} ++ ++sub test_maxargssize_sort ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('SORT', ++ [ 'ARRIVAL', 'CC', 'DATE', ++ 'FROM', 'REVERSE', 'SIZE', 'TO' ], ++ 'UTF-8', 'ALL'); ++} ++ ++sub test_maxargssize_status ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STATUS', 'INBOX', ++ [ 'MESSAGES', 'UIDNEXT', ++ 'UIDVALIDITY', 'UNSEEN', 'SIZE' ] ); ++} ++ ++sub test_maxargssize_store_annot ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 25, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_maxargssize_store_annot2 ++{ ++ my ($self) = @_; ++ ++ $self->assert_cmd_bye_toobig('STORE', '1', 'ANNOTATION', ++ [ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ], ++ "X" x 5, [ 'VALUE', { Quote => "X" x 25 } ] ] ); ++} ++ ++sub test_append_zero ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_cmd('APPEND', 0, '', 'INBOX', { Literal => '' } ); ++ $self->assert_str_equals('no', $talk->get_last_completion_response()); ++} ++ ++sub test_maxmessagesize_sync_literal ++{ ++ my ($self) = @_; ++ ++ # Synchronizing literals are the default in IMAPTalk v4.05 (and earlier) ++ my $talk = $self->{store}->get_client(NoLiteralPlus => 1); ++ ++ $self->assert_cmd_no_toobig($talk, 'APPEND', ++ 'INBOX', { Literal => $toobig_email } ); ++} ++ ++sub test_maxmessagesize_nosync_literal ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ # Do this by brute force until we have IMAPTalk v4.06+ ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++sub test_literal_minus ++{ ++ my ($self) = @_; ++ ++ my $talk = $self->{store}->get_client(); ++ $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_bye_toobig(); ++} ++ ++1; +diff --git a/cassandane/Cassandane/IMAPMessageStore.pm b/cassandane/Cassandane/IMAPMessageStore.pm +index 338d1c5f3..959a9fabc 100644 +--- a/cassandane/Cassandane/IMAPMessageStore.pm ++++ b/cassandane/Cassandane/IMAPMessageStore.pm +@@ -83,7 +83,7 @@ sub new + + sub connect + { +- my ($self) = @_; ++ my ($self, %params) = @_; + + # if already successfully connected, do nothing + return +@@ -115,6 +115,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to '$self->{host}:$self->{port}': $@"; + } +@@ -129,6 +130,7 @@ sub connect + Pedantic => 1, + PreserveINBOX => 1, + Uid => 0, ++ NoLiteralPlus => delete $params{NoLiteralPlus} || 0, + ) + or die "Cannot connect to server: $@"; + } +@@ -323,9 +325,9 @@ sub remove + + sub get_client + { +- my ($self) = @_; ++ my ($self, %params) = @_; + +- $self->connect(); ++ $self->connect(%params); + return $self->{client}; + } + +-- +2.39.2 + + +From 9536206ea6750fe50bd974f65129b18b2a32ec6e Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:49:46 -0400 +Subject: [PATCH 15/16] imapd.c: also emit a NO [TOOBIG] response for oversized + no-sync APPEND + +--- + cassandane/Cassandane/Cyrus/IMAPLimits.pm | 46 ++++++++++++++--------- + imap/imapd.c | 23 ++++++++---- + 2 files changed, 44 insertions(+), 25 deletions(-) + +diff --git a/cassandane/Cassandane/Cyrus/IMAPLimits.pm b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +index 9576d1d88..32d069653 100644 +--- a/cassandane/Cassandane/Cyrus/IMAPLimits.pm ++++ b/cassandane/Cassandane/Cyrus/IMAPLimits.pm +@@ -59,6 +59,7 @@ $email =~ s/\r?\n/\r\n/gs; + + my $toobig_email = $email . "X" x 100; + ++# Check that we got an untagged BYE [TOOBIG] response + sub assert_bye_toobig + { + my ($self, $store) = @_; +@@ -79,11 +80,11 @@ sub assert_bye_toobig + } + }; + +- # Check that we got a BYE [TOOBIG] response + $store->idle_response($handlers, 1); + $self->assert_num_equals(1, $got_toobig); + } + ++# Send a command and expect an untagged BYE [TOOBIG] response + sub assert_cmd_bye_toobig + { + my $self = shift; +@@ -97,28 +98,37 @@ sub assert_cmd_bye_toobig + $self->assert_bye_toobig(); + } + ++# Check that we got a tagged NO [TOOBIG] response ++sub assert_no_toobig ++{ ++ my ($self, $talk) = @_; ++ ++ my $got_toobig = 0; ++ my $handlers = ++ { ++ 'no' => sub ++ { ++ my (undef, $resp) = @_; ++ $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); ++ } ++ }; ++ ++ eval { ++ $talk->_parse_response($handlers); ++ }; ++ ++ $self->assert_num_equals(1, $got_toobig); ++} ++ ++# Send a command and expect a tagged NO [TOOBIG] response + sub assert_cmd_no_toobig + { + my $self = shift; + my $talk = shift; + my $cmd = shift; + +- my $got_toobig = 0; +- my $handlers = +- { +- 'no' => sub +- { +- # Pick out the [TOOBIG] response code +- my (undef, $resp) = @_; +- $got_toobig = 1 if (uc($resp->[0]) eq '[TOOBIG]'); +- } +- }; +- +- $talk->_imap_cmd($cmd, 0, $handlers, @_); +- +- # Check that we got a NO [TOOBIG] response +- $self->assert_str_equals('no', $talk->get_last_completion_response()); +- $self->assert_num_equals(1, $got_toobig); ++ $talk->_send_cmd($cmd, @_); ++ $self->assert_no_toobig($talk); + } + + sub new +@@ -514,6 +524,7 @@ sub test_maxmessagesize_nosync_literal + my $talk = $self->{store}->get_client(); + # Do this by brute force until we have IMAPTalk v4.06+ + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {101+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +@@ -523,6 +534,7 @@ sub test_literal_minus + + my $talk = $self->{store}->get_client(); + $talk->_imap_socket_out($talk->{CmdId}++ . " APPEND INBOX {4097+}\015\012"); ++ $self->assert_no_toobig($talk); + $self->assert_bye_toobig(); + } + +diff --git a/imap/imapd.c b/imap/imapd.c +index fb5dace4a..afd58bd66 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3557,7 +3557,7 @@ static int isokflag(char *s, int *isseen) + } + } + +-static int getliteralsize(const char *p, int c, size_t maxsize, ++static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + unsigned *size, int *binary, const char **parseerr) + + { +@@ -3589,10 +3589,14 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + /* LITERAL- says maximum size is 4096! */ + if (lminus && num > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); + fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ ++ prot_printf(imapd_out, "%s NO %s\r\n", tag, ++ error_message(IMAP_MESSAGE_TOOBIG)); + fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); + } + isnowait++; +@@ -3622,8 +3626,8 @@ static int getliteralsize(const char *p, int c, size_t maxsize, + return 0; + } + +-static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *binary, +- const char **parseerr) ++static int catenate_text(const char *tag, FILE *f, size_t maxsize, ++ unsigned *totalsize, int *binary, const char **parseerr) + { + int c; + static struct buf arg; +@@ -3635,7 +3639,8 @@ static int catenate_text(FILE *f, size_t maxsize, unsigned *totalsize, int *bina + c = getword(imapd_in, &arg); + + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxsize - *totalsize, &size, binary, parseerr); ++ r = getliteralsize(tag, arg.s, c, maxsize - *totalsize, ++ &size, binary, parseerr); + if (r) return r; + + /* Catenate message part to stage */ +@@ -3782,7 +3787,8 @@ static int catenate_url(const char *s, const char *cur_name, FILE *f, + return r; + } + +-static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsigned *totalsize, ++static int append_catenate(const char *tag, FILE *f, const char *cur_name, ++ size_t maxsize, unsigned *totalsize, + int *binary, const char **parseerr, const char **url) + { + int c, r = 0; +@@ -3796,7 +3802,7 @@ static int append_catenate(FILE *f, const char *cur_name, size_t maxsize, unsign + } + + if (!strcasecmp(arg.s, "TEXT")) { +- int r1 = catenate_text(f, maxsize, totalsize, binary, parseerr); ++ int r1 = catenate_text(tag, f, maxsize, totalsize, binary, parseerr); + if (r1) return r1; + + /* if we see a SP, we're trying to catenate more than one part */ +@@ -4038,13 +4044,14 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + + /* Catenate the message part(s) to stage */ + size = 0; +- r = append_catenate(curstage->f, cur_name, maxmsgsize, &size, ++ r = append_catenate(tag, curstage->f, cur_name, maxmsgsize, &size, + &(curstage->binary), &parseerr, &url); + if (r) goto done; + } + else { + /* Read size from literal */ +- r = getliteralsize(arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); ++ r = getliteralsize(tag, arg.s, c, maxmsgsize, ++ &size, &(curstage->binary), &parseerr); + if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; + if (r) goto done; + +-- +2.39.2 + + +From d2f960b07cd32ba10cf4cd828e5dd98dea1bfdc2 Mon Sep 17 00:00:00 2001 +From: Ken Murchison +Date: Thu, 21 Mar 2024 23:55:13 -0400 +Subject: [PATCH 16/16] imapd.c, imapparse.c: call fatal(EX_PROTOCOL) when + client exceeds a limit + +--- + cunit/parse.testc | 12 ++++++------ + imap/imapd.c | 48 +++++++++++++++++++++++------------------------ + imap/imapparse.c | 22 +++++++++++----------- + 3 files changed, 41 insertions(+), 41 deletions(-) + +diff --git a/cunit/parse.testc b/cunit/parse.testc +index 5a97f9b73..1786706cb 100644 +--- a/cunit/parse.testc ++++ b/cunit/parse.testc +@@ -119,7 +119,7 @@ static void test_getint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -188,7 +188,7 @@ static void test_getsint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint32, int32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint32, int32_t, STR4, &c, &val, &bytes_in); +@@ -255,7 +255,7 @@ static void test_getuint32(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint32, uint32_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint32, uint32_t, STR4, &c, &val, &bytes_in); +@@ -322,7 +322,7 @@ static void test_getint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -391,7 +391,7 @@ static void test_getsint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getsint64, int64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getsint64, int64_t, STR4, &c, &val, &bytes_in); +@@ -458,7 +458,7 @@ static void test_getuint64(void) + /* test a string with too many digits */ + CU_EXPECT_CYRFATAL_BEGIN; + wrap_int_parser(getuint64, uint64_t, STR3, &c, &val, NULL); +- CU_EXPECT_CYRFATAL_END(EX_IOERR, "num too big"); ++ CU_EXPECT_CYRFATAL_END(EX_PROTOCOL, "num too big"); + + /* test a valid value with a different terminator */ + wrap_int_parser(getuint64, uint64_t, STR4, &c, &val, &bytes_in); +diff --git a/imap/imapd.c b/imap/imapd.c +index afd58bd66..58457601a 100644 +--- a/imap/imapd.c ++++ b/imap/imapd.c +@@ -3591,13 +3591,13 @@ static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (num > maxsize) { + /* Fail per RFC 7888, Section 4, choice 2 */ + prot_printf(imapd_out, "%s NO %s\r\n", tag, + error_message(IMAP_MESSAGE_TOOBIG)); +- fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_IOERR); ++ fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_PROTOCOL); + } + isnowait++; + p++; +@@ -3968,7 +3968,7 @@ static void cmd_append(char *tag, char *name, const char *cur_name) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ + if (!isokflag(arg.s, &sync_seen)) { + parseerr = "Invalid flag in Append command"; +@@ -4310,7 +4310,7 @@ static void cmd_select(char *tag, char *cmd, char *name) + if (arg.s[0] == '\0') goto badlist; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(arg.s); + if (!strcmp(arg.s, "CONDSTORE")) { +@@ -4685,7 +4685,7 @@ static int parse_fetch_args(const char *tag, const char *cmd, + } + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + switch (fetchatt.s[0]) { +@@ -4818,7 +4818,7 @@ badannotation: + do { + c = getastring(imapd_in, imapd_out, &fieldname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) { + prot_printf(imapd_out, "%s NO %s in %s %s\r\n", + tag, error_message(c), cmd, fetchatt.s); +@@ -5154,7 +5154,7 @@ badannotation: + do { + c = getword(imapd_in, &fetchatt); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(fetchatt.s); + if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { +@@ -5503,7 +5503,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + do { + c = getword(imapd_in, &storemod); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + ucase(storemod.s); + if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { +@@ -5598,7 +5598,7 @@ static void cmd_store(char *tag, char *sequence, int usinguid) + for (;;) { + c = getword(imapd_in, &flagname); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { + inlist = 1; + continue; +@@ -7763,7 +7763,7 @@ static void getlistargs(char *tag, struct listargs *listargs) + for (;;) { + c = getastring(imapd_in, imapd_out, &buf); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (*buf.s) + strarray_append(&listargs->pat, buf.s); +@@ -8615,7 +8615,7 @@ void cmd_setquota(const char *tag, const char *quotaroot) + else if (c != ' ') goto badlist; + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + c = prot_getc(imapd_in); + if (!IS_EOL(c, imapd_in)) { +@@ -8769,7 +8769,7 @@ static int parse_statusitems(unsigned *statusitemsp, const char **errstr) + if (arg.s[0] == '\0') goto bad; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + lcase(arg.s); + if (!strcmp(arg.s, "messages")) { +@@ -9155,7 +9155,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + name = ucase(arg.s); + if (c != ' ') goto fail; +@@ -9166,7 +9166,7 @@ static int parsecreateargs(struct dlist **extargs) + do { + c = getword(imapd_in, &val); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + dlist_setatom(sub, name, val.s); + } while (c == ' '); +@@ -9237,7 +9237,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9290,7 +9290,7 @@ static int parse_annotate_fetch_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9370,7 +9370,7 @@ static int parse_metadata_string_or_list(const char *tag, + do { + c = getastring(imapd_in, imapd_out, &arg); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9476,7 +9476,7 @@ static int parse_annotate_store_data(const char *tag, + else + c = getqstring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -9520,7 +9520,7 @@ static int parse_annotate_store_data(const char *tag, + } + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + /* add the attrib-value pair to the list */ + appendattvalue(&attvalues, attrib.s, &value); +@@ -9598,7 +9598,7 @@ static int parse_metadata_store_data(const char *tag, + /* get entry */ + c = getastring(imapd_in, imapd_out, &entry); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c != ' ') { + prot_printf(imapd_out, +@@ -9613,7 +9613,7 @@ static int parse_metadata_store_data(const char *tag, + /* get value */ + c = getbnstring(imapd_in, imapd_out, &value); + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; + if (c == EOF) { + prot_printf(imapd_out, +@@ -11903,7 +11903,7 @@ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) + n = 0; + for (;;) { + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + + if (n >= nsort - 1) { /* leave room for implicit criterion */ + /* (Re)allocate an array for sort criteria */ +@@ -12055,7 +12055,7 @@ static int getlistselopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", +@@ -12161,7 +12161,7 @@ static int getlistretopts(char *tag, struct listargs *args) + c = getword(imapd_in, &buf); + + if (prot_bytes_in(imapd_in) > maxargssize_mark) +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + if (!*buf.s) { + prot_printf(imapd_out, + "%s BAD Invalid syntax in List command\r\n", tag); +diff --git a/imap/imapparse.c b/imap/imapparse.c +index 2dceaed94..010921089 100644 +--- a/imap/imapparse.c ++++ b/imap/imapparse.c +@@ -74,7 +74,7 @@ EXPORTED int getword(struct protstream *in, struct buf *buf) + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + } + } +@@ -138,7 +138,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxquoted && buf_len(buf) > config_maxquoted) { +- fatal("[TOOBIG] Quoted value too long", EX_IOERR); ++ fatal("[TOOBIG] Quoted value too long", EX_PROTOCOL); + } + } + +@@ -157,11 +157,11 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + /* LITERAL- says maximum size is 4096! */ + if (lminus && len > 4096) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); + } + if (config_maxliteral && len >= 0 && (unsigned) len > config_maxliteral) { + /* Fail per RFC 7888, Section 4, choice 2 */ +- fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_LITERAL_TOO_LARGE), EX_PROTOCOL); + } + isnowait++; + c = prot_getc(pin); +@@ -225,7 +225,7 @@ EXPORTED int getxstring(struct protstream *pin, struct protstream *pout, + } + buf_putc(buf, c); + if (config_maxword && buf_len(buf) > config_maxword) { +- fatal("[TOOBIG] Word too long", EX_IOERR); ++ fatal("[TOOBIG] Word too long", EX_PROTOCOL); + } + c = prot_getc(pin); + } +@@ -284,7 +284,7 @@ EXPORTED int getint32(struct protstream *pin, int32_t *num) + /* INT_MAX == 2147483647 */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 214748364 || (result == 214748364 && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -337,7 +337,7 @@ EXPORTED int getuint32(struct protstream *pin, uint32_t *num) + /* UINT_MAX == 4294967295U */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 429496729 || (result == 429496729 && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -361,7 +361,7 @@ EXPORTED int getint64(struct protstream *pin, int64_t *num) + /* LLONG_MAX == 9223372036854775807LL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 922337203685477580LL || (result == 922337203685477580LL && (c > '7'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -414,7 +414,7 @@ EXPORTED int getuint64(struct protstream *pin, uint64_t *num) + /* ULLONG_MAX == 18446744073709551615ULL */ + while ((c = prot_getc(pin)) != EOF && cyrus_isdigit(c)) { + if (result > 1844674407370955161ULL || (result == 1844674407370955161ULL && (c > '5'))) +- fatal("num too big", EX_IOERR); ++ fatal("num too big", EX_PROTOCOL); + result = result * 10 + c - '0'; + gotchar = 1; + } +@@ -766,7 +766,7 @@ EXPORTED int get_search_return_opts(struct protstream *pin, + + if (searchargs->maxargssize_mark && + prot_bytes_in(pin) > searchargs->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + } while (c == ' '); +@@ -1546,7 +1546,7 @@ static int get_search_criterion(struct protstream *pin, + + if (base->maxargssize_mark && + prot_bytes_in(pin) > base->maxargssize_mark) { +- fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_IOERR); ++ fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); + } + + if (!keep_charset) +-- +2.39.2 +