From 4a53c13932df08f23eb049831cb559bfc534502a Mon Sep 17 00:00:00 2001 From: Thomas Roessler Date: Mon, 14 Feb 2000 16:07:54 +0000 Subject: [PATCH] Brendan Cully's patch from <20000212185021.A7365@xanadu.kublai.com>. --- .cvsignore | 1 + Makefile.am | 8 +- browser.c | 39 +++++-- browser.h | 6 +- configure.in | 68 +++++++----- doc/manual.sgml.head | 30 ++++- imap/Makefile.am | 6 +- imap/auth.c | 239 +--------------------------------------- imap/auth_gss.c | 253 +++++++++++++++++++++++++++++++++++++++++++ imap/browse.c | 65 ++++------- imap/imap.c | 8 ++ mx.c | 84 ++++++++------ 12 files changed, 452 insertions(+), 355 deletions(-) create mode 100644 imap/auth_gss.c diff --git a/.cvsignore b/.cvsignore index 5148a94e..b9529254 100644 --- a/.cvsignore +++ b/.cvsignore @@ -9,6 +9,7 @@ configure config.cache config.log config.status +keymap_alldefs.h keymap_defs.h makedoc mutt_dotlock diff --git a/Makefile.am b/Makefile.am index f4ef76f7..1d74d241 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,9 @@ mutt_SOURCES = $(BUILT_SOURCES) \ muttbug_SOURCES = muttbug.sh.in -mutt_LDADD = @MUTT_LIB_OBJECTS@ @LIBOBJS@ $(LIBIMAP) $(GSSLIBS) $(INTLLIBS) +mutt_LDADD = @MUTT_LIB_OBJECTS@ @LIBOBJS@ $(LIBIMAP) $(MUTTLIBS) \ + $(INTLLIBS) + mutt_DEPENDENCIES = @MUTT_LIB_OBJECTS@ @LIBOBJS@ $(LIBIMAPDEPS) $(INTLDEPS) makedoc_SOURCES = makedoc.c @@ -47,7 +49,9 @@ CPP=@CPP@ DEFS=-DSHAREDIR=\"$(sharedir)\" -DSYSCONFDIR=\"$(sysconfdir)\" \ -DBINDIR=\"$(bindir)\" -DHAVE_CONFIG_H=1 -INCLUDES=-I. $(IMAP_INCLUDES) -I$(includedir) + +# top_srcdir is for building outside of the source tree +INCLUDES=-I$(top_srcdir) -I. $(IMAP_INCLUDES) -I$(includedir) non_us_sources = pgp.c pgpinvoke.c pgpkey.c pgplib.c sha1dgst.c \ gnupgparse.c sha.h sha_locl.h \ diff --git a/browser.c b/browser.c index d1ae8dec..67b4ac43 100644 --- a/browser.c +++ b/browser.c @@ -172,7 +172,7 @@ folder_format_str (char *dest, size_t destlen, char op, const char *src, case 'f': #ifdef USE_IMAP - if (mx_is_imap (folder->ff->name)) + if (folder->ff->imap) strfcpy (fn, NONULL(folder->ff->desc), sizeof (fn)); else #endif @@ -207,10 +207,13 @@ folder_format_str (char *dest, size_t destlen, char op, const char *src, else { #ifdef USE_IMAP - if (mx_is_imap (folder->ff->name)) + if (folder->ff->imap) { + sprintf (permission, "IMAP %c%c", + folder->ff->inferiors ? '+' : ' ', + folder->ff->selectable ? 'S' : ' '); snprintf (tmp, sizeof (tmp), "%%%ss", fmt); - snprintf (dest, destlen, tmp, "IMAP"); + snprintf (dest, destlen, tmp, permission); } #endif } @@ -328,9 +331,8 @@ static void add_folder (MUTTMENU *m, struct browser_state *state, (state->entry)[state->entrylen].name = safe_strdup (name); (state->entry)[state->entrylen].desc = safe_strdup (name); #ifdef USE_IMAP - (state->entry)[state->entrylen].notfolder = 0; + (state->entry)[state->entrylen].imap = 0; #endif - (state->entrylen)++; } @@ -421,7 +423,7 @@ static int examine_mailboxes (MUTTMENU *menu, struct browser_state *state) do { #ifdef USE_IMAP - if (tmp->path[0] == '{') + if (mx_is_imap (tmp->path)) { add_folder (menu, state, tmp->path, NULL, tmp->new); continue; @@ -623,7 +625,7 @@ void _mutt_select_file (char *f, size_t flen, int buffy, (S_ISLNK (state.entry[menu->current].mode) && link_is_dir (LastDir, state.entry[menu->current].name)) #ifdef USE_IMAP - || state.entry[menu->current].notfolder + || state.entry[menu->current].inferiors #endif ) { @@ -645,7 +647,7 @@ void _mutt_select_file (char *f, size_t flen, int buffy, if ((mx_get_magic (buf) <= 0) #ifdef USE_IMAP - || state.entry[menu->current].notfolder + || state.entry[menu->current].inferiors #endif ) { @@ -681,8 +683,17 @@ void _mutt_select_file (char *f, size_t flen, int buffy, #ifdef USE_IMAP else if (state.imap_browse) { + int n; + strfcpy (LastDir, state.entry[menu->current].name, sizeof (LastDir)); + /* tack on delimiter here */ + if ((state.entry[menu->current].delim != '\0') && + (n = strlen (LastDir)+1) < sizeof (LastDir)) + { + LastDir[n] = '\0'; + LastDir[n-1] = state.entry[menu->current].delim; + } } #endif else @@ -806,7 +817,7 @@ void _mutt_select_file (char *f, size_t flen, int buffy, break; case OP_DELETE_MAILBOX: - if (!mx_is_imap (state.entry[menu->current].name)) + if (!state.entry[menu->current].imap) mutt_error (_("Delete is only supported for IMAP mailboxes")); else { @@ -1058,6 +1069,16 @@ void _mutt_select_file (char *f, size_t flen, int buffy, break; } +#ifdef USE_IMAP + if (state.entry[menu->current].selectable) + { + strfcpy (f, state.entry[menu->current].name, flen); + destroy_state (&state); + mutt_menuDestroy (&menu); + return; + } + else +#endif if (S_ISDIR (state.entry[menu->current].mode) || (S_ISLNK (state.entry[menu->current].mode) && link_is_dir (LastDir, state.entry[menu->current].name))) diff --git a/browser.h b/browser.h index 48805abb..8fe7187f 100644 --- a/browser.h +++ b/browser.h @@ -30,7 +30,11 @@ struct folder_file char *name; char *desc; #ifdef USE_IMAP - short notfolder; + char delim; + + unsigned imap : 1; + unsigned selectable : 1; + unsigned inferiors : 1; #endif unsigned tagged : 1; unsigned is_new : 1; diff --git a/configure.in b/configure.in index eef1e1dd..82a43ddf 100644 --- a/configure.in +++ b/configure.in @@ -65,7 +65,7 @@ if test -f $srcdir/EXPORTABLE ; then else SUBVERSION="i" - AC_ARG_ENABLE(pgp, [ --disable-pgp Disable PGP support], + AC_ARG_ENABLE(pgp, [ --disable-pgp Disable PGP support], [ if test x$enableval = xno ; then HAVE_PGP=no fi @@ -79,7 +79,7 @@ else OPS="$OPS \$(srcdir)/OPS.PGP" fi - AC_ARG_WITH(mixmaster, [ --with-mixmaster[=PATH] include Mixmaster support], + AC_ARG_WITH(mixmaster, [ --with-mixmaster[=PATH] Include Mixmaster support], [if test -x "$withval" ; then MIXMASTER="$withval" else @@ -103,7 +103,7 @@ if test $ISPELL != no; then AC_DEFINE_UNQUOTED(ISPELL, "$ISPELL") fi -AC_ARG_WITH(slang, [ --with-slang[=DIR] use S-Lang instead of ncurses], +AC_ARG_WITH(slang, [ --with-slang[=DIR] Use S-Lang instead of ncurses], [AC_CACHE_CHECK([if this is a BSD system], mutt_cv_bsdish, [AC_TRY_RUN([#include @@ -151,7 +151,6 @@ main () fi fi AC_MSG_RESULT($mutt_cv_slang) - LIBS="$LIBS -lslang -lm" if test $mutt_cv_bsdish = yes; then AC_CHECK_LIB(termlib, main) fi @@ -159,17 +158,16 @@ main () AC_DEFINE(HAVE_COLOR) MUTT_LIB_OBJECTS="$MUTT_LIB_OBJECTS resize.o" - dnl --- try to link a sample program to check if we're ok + dnl --- now that we've found it, check the link - AC_MSG_CHECKING(if I can compile a test SLang program) - AC_TRY_LINK([], [SLtt_get_terminfo ();], - [AC_MSG_RESULT(yes)], - [AC_MSG_ERROR(unable to compile. check config.log)]) + AC_CHECK_LIB(slang, SLtt_get_terminfo, + [MUTTLIBS="$MUTTLIBS -lslang -lm"], + [AC_MSG_ERROR(unable to compile. check config.log)], -lm) ], [mutt_cv_curses=/usr - AC_ARG_WITH(curses, [ --with-curses=DIR ncurses is installed in ], + AC_ARG_WITH(curses, [ --with-curses=DIR Where ncurses is installed ], [if test $withval != yes; then mutt_cv_curses=$withval fi @@ -180,13 +178,13 @@ main () AC_CHECK_LIB(ncurses, initscr, - [LIBS="$LIBS -lncurses" + [MUTTLIBS="$MUTTLIBS -lncurses" if test x$mutt_cv_curses = x/usr -a -d /usr/include/ncurses; then CPPFLAGS="$CPPFLAGS -I/usr/include/ncurses" fi AC_CHECK_HEADERS(ncurses.h)], - [LIBS="$LIBS -lcurses" + [MUTTLIBS="$MUTTLIBS -lcurses" if test -f /usr/ccs/lib/libcurses.a; then LDFLAGS="$LDFLAGS -L/usr/ccs/lib" else @@ -196,9 +194,13 @@ main () fi fi]) + old_LIBS="$LIBS" + LIBS="$LIBS $MUTTLIBS" AC_CHECK_FUNC(start_color, [AC_DEFINE(HAVE_COLOR)]) AC_CHECK_FUNCS(typeahead bkgdset curs_set meta use_default_colors) - AC_CHECK_FUNCS(resizeterm, [MUTT_LIB_OBJECTS="$MUTT_LIB_OBJECTS resize.o"]) + AC_CHECK_FUNCS(resizeterm, + [MUTT_LIB_OBJECTS="$MUTT_LIB_OBJECTS resize.o"]) + LIBS="$old_LIBS" ]) AC_HEADER_STDC @@ -248,7 +250,7 @@ AC_CHECK_FUNCS(strftime, break, [AC_CHECK_LIB(intl, strftime)]) dnl AIX may not have fchdir() AC_CHECK_FUNCS(fchdir, [AC_DEFINE(HAVE_FCHDIR)], [mutt_cv_fchdir=no]) -AC_ARG_WITH(regex, [ --with-regex Use the GNU regex library ], +AC_ARG_WITH(regex, [ --with-regex Use the GNU regex library ], [mutt_cv_regex=yes], [AC_CHECK_FUNCS(regcomp, mutt_cv_regex=no, mutt_cv_regex=yes)]) @@ -272,7 +274,7 @@ if test $mutt_cv_regex = yes; then fi -AC_ARG_WITH(homespool, [ --with-homespool[=FILE] file in user's directory where new mail is spooled], with_homespool=${withval}) +AC_ARG_WITH(homespool, [ --with-homespool[=FILE] File in user's directory where new mail is spooled], with_homespool=${withval}) if test x$with_homespool != x; then if test $with_homespool = yes; then with_homespool=mailbox @@ -282,7 +284,7 @@ if test x$with_homespool != x; then AC_DEFINE(USE_DOTLOCK) mutt_cv_setgid=no else - AC_ARG_WITH(mailpath, [ --with-mailpath=DIR directory where spool mailboxes are located], + AC_ARG_WITH(mailpath, [ --with-mailpath=DIR Directory where spool mailboxes are located], [mutt_cv_mailpath=$withval], [ AC_CACHE_CHECK(where new mail is stored, mutt_cv_mailpath, [mutt_cv_mailpath=no @@ -338,7 +340,7 @@ int main (int argc, char **argv) fi fi -AC_ARG_ENABLE(external_dotlock, [ --enable-external-dotlock Force use of an external dotlock program], +AC_ARG_ENABLE(external_dotlock, [ --enable-external-dotlock Force use of an external dotlock program], [mutt_cv_external_dotlock="$enableval"]) if test "x$mutt_cv_setgid" = "xyes" || test "x$mutt_cv_fchdir" = "xno" \ @@ -353,7 +355,7 @@ fi AC_SUBST(DOTLOCK_TARGET) -AC_ARG_WITH(libdir, [ --with-libdir=PATH specify where to put arch dependent files], +AC_ARG_WITH(libdir, [ --with-libdir=PATH Specify where to put arch dependent files], [mutt_cv_libdir=$withval], [ AC_CACHE_CHECK(where to put architecture-dependent files, mutt_cv_libdir, @@ -363,7 +365,7 @@ AC_ARG_WITH(libdir, [ --with-libdir=PATH specify where to put arch depend libdir=$mutt_cv_libdir AC_SUBST(libdir) -AC_ARG_WITH(sharedir, [ --with-sharedir=PATH specify where to put arch independent files], +AC_ARG_WITH(sharedir, [ --with-sharedir=PATH Specify where to put arch independent files], [mutt_cv_sharedir=$withval], [ AC_CACHE_CHECK(where to put architecture-independent data files, mutt_cv_sharedir, @@ -378,7 +380,7 @@ sharedir=$mutt_cv_sharedir AC_SUBST(sharedir) mutt_cv_charmaps=/usr/share/i18n/charmaps -AC_ARG_WITH(charmaps, [--with-charmaps=PATH specify where on the system mutt can find character set definitions], +AC_ARG_WITH(charmaps, [ --with-charmaps=PATH Where to find character set definitions], [mutt_cv_charmaps=$withval]) mutt_cv_fake_charmaps=yes @@ -406,7 +408,7 @@ charmaps=$mutt_cv_charmaps AC_SUBST(charmaps) AM_CONDITIONAL(BUILD_CHARMAPS, test x$need_charmaps = xyes) -AC_ARG_WITH(docdir, [ --with-docdir=PATH specify where to put the documentation], +AC_ARG_WITH(docdir, [ --with-docdir=PATH Specify where to put the documentation], [mutt_cv_docdir=$withval], [ AC_CACHE_CHECK(where to put the documentation, mutt_cv_docdir, @@ -436,7 +438,7 @@ AC_ARG_WITH(domain, [ --with-domain=DOMAIN Specify your DNS domain name ] AC_DEFINE_UNQUOTED(DOMAIN, "$withval") fi]) -AC_ARG_ENABLE(pop, [ --enable-pop Enable POP3 support], +AC_ARG_ENABLE(pop, [ --enable-pop Enable POP3 support], [ if test x$enableval = xyes ; then AC_DEFINE(USE_POP) AC_CHECK_FUNC(setsockopt, , AC_CHECK_LIB(socket, setsockopt)) @@ -457,8 +459,13 @@ AC_ARG_ENABLE(imap, [ --enable-imap Enable IMAP support], ]) AM_CONDITIONAL(BUILD_IMAP, test x$need_imap = xyes) -AC_ARG_WITH(gss, [ --with-gss[=DIR] Compile in GSSAPI authentication for IMAP], +AC_ARG_WITH(gss, [ --with-gss[=DIR] Compile in GSSAPI authentication for IMAP], [ + if test "$need_imap" != "yes" + then + AC_MSG_ERROR([GSS support is only for IMAP, but IMAP is not enabled]) + fi + if test "$with_gss" != "no" then if test "$with_gss" != "yes" @@ -473,24 +480,29 @@ AC_ARG_WITH(gss, [ --with-gss[=DIR] Compile in GSSAPI authentication for AC_CHECK_LIB(gssapi_krb5, gss_init_sec_context,, AC_MSG_ERROR([could not find libgssapi_krb5 which is needed for GSS authentication]), -lkrb5) LIBS="$saved_LIBS" - GSSLIBS="-lgssapi_krb5 -lkrb5" + MUTTLIBS="$MUTTLIBS -lgssapi_krb5 -lkrb5" AC_DEFINE(USE_GSS) + need_gss="yes" fi ]) +AM_CONDITIONAL(USE_GSS, test x$need_gss = xyes) -AC_ARG_ENABLE(ssl, [ --enable-ssl Enable SSL support], +AC_ARG_ENABLE(ssl, [ --enable-ssl Enable SSL support for IMAP], [ if test "$need_imap" != "yes"; then - AC_MSG_ERROR([Sorry, SSL support only for IMAP]) + AC_MSG_ERROR([SSL support is only for IMAP, but IMAP is not enabled]) fi + saved_LIBS="$LIBS" AC_CHECK_LIB(crypto, X509_new,, AC_MSG_ERROR([Unable to find SSL library])) AC_CHECK_LIB(ssl, SSL_new,, AC_MSG_ERROR([Unable to find SSL library]), -lcrypto) AC_DEFINE(USE_SSL) + LIBS="$saved_LIBS" + MUTTLIBS="$MUTTLIBS -lssl -lcrypto" need_ssl=yes ]) AM_CONDITIONAL(USE_SSL, test x$need_ssl = xyes) -AC_ARG_ENABLE(debug, [ --enable-debug Enable debugging support], +AC_ARG_ENABLE(debug, [ --enable-debug Enable debugging support], [ if test x$enableval = xyes ; then AC_DEFINE(DEBUG) fi @@ -544,10 +556,10 @@ AC_ARG_ENABLE(exact-address, [ --enable-exact-address enable regeneration o AC_DEFINE(EXACT_ADDRESS) fi]) +AC_SUBST(MUTTLIBS) AC_SUBST(MUTT_LIB_OBJECTS) AC_SUBST(LIBIMAP) AC_SUBST(LIBIMAPDEPS) -AC_SUBST(GSSLIBS) MUTT_AM_GNU_GETTEXT CPPFLAGS="$CPPFLAGS -I\$(top_srcdir)/intl" diff --git a/doc/manual.sgml.head b/doc/manual.sgml.head index f4eae534..345ab48d 100644 --- a/doc/manual.sgml.head +++ b/doc/manual.sgml.head @@ -1994,14 +1994,38 @@ variable, which defaults to every 60 seconds. Mutt is designed to work with IMAP4rev1 servers, and was originally tested with both the UWash IMAP server v11.241 and the Cyrus IMAP server v1.5.14. -Nowadays it is primarily developed against UW-IMAP 12.250. It works -more-or-less correctly against Cyrus 1.6.11, though there are a few minor -quirks in the folder browser. +Nowadays it is primarily developed against UW-IMAP 12.250. It appears +to work more-or-less correctly against Cyrus 1.6.11 as well. Note that if you are using mbox as the mail store on UW servers prior to v12.250, the server has been reported to disconnect a client if another client selects the same folder. +The Folder Browser +

+ +As of version 1.2, mutt supports browsing mailboxes on an IMAP +server. This is mostly the same as the local file browser, with the +following differences: + +In lieu of file permissions, mutt displays the string "IMAP", + possibly followed by the symbol "S", indicating that the entry is + selectable (contains messages), and/or the symbol "+", indicating + that the entry may contain subfolders. If your server is like the + UW-IMAP server, you won't see both symbols, but if you are using a + Cyrus-like server folders will often contain both messages and + subfolders. +For the case where an entry can contain both messages and + subfolders, the selection key (bound to enter by default) + will choose to descend into the subfolder view. If you wish to view + the messages in that folder, you must use view-file instead + (bound to space by default). +You can delete mailboxes with the delete-mailbox + command (bound to d by default. You may also + subscribe and unsubscribe to mailboxes (normally + these are bound to s and u, respectively). + + Authentication

diff --git a/imap/Makefile.am b/imap/Makefile.am index d0452468..3713dbbc 100644 --- a/imap/Makefile.am +++ b/imap/Makefile.am @@ -2,6 +2,10 @@ AUTOMAKE_OPTIONS = foreign +if USE_GSS +GSSSOURCES = auth_gss.c +endif + if USE_SSL SSLSOURCES = imap_ssl.c SSLHEADERS = imap_ssl.h @@ -15,4 +19,4 @@ noinst_LIBRARIES = libimap.a noinst_HEADERS = imap_private.h imap_socket.h md5.h message.h $(SSLHEADERS) libimap_a_SOURCES = auth.c browse.c command.c imap.c imap.h md5c.c message.c \ - socket.c util.c $(SSLSOURCES) + socket.c util.c $(GSSSOURCES) $(SSLSOURCES) diff --git a/imap/auth.c b/imap/auth.c index b440c317..fbdcfd34 100644 --- a/imap/auth.c +++ b/imap/auth.c @@ -1,7 +1,7 @@ /* * Copyright (C) 1996-8 Michael R. Elkins * Copyright (C) 1996-9 Brandon Long - * Copyright (C) 1999 Brendan Cully + * Copyright (C) 1999-2000 Brendan Cully * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,16 +27,9 @@ #define MD5_BLOCK_LEN 64 #define MD5_DIGEST_LEN 16 +/* external authenticator prototypes */ #ifdef USE_GSS -#include -#include -#include - -#define GSS_BUFSIZE 8192 - -#define GSS_AUTH_P_NONE 1 -#define GSS_AUTH_P_INTEGRITY 2 -#define GSS_AUTH_P_PRIVACY 4 +int imap_auth_gss (IMAP_DATA* idata, const char* user); #endif /* forward declarations */ @@ -45,9 +38,6 @@ static void hmac_md5 (const char* password, char* challenge, static int imap_auth_cram_md5 (IMAP_DATA* idata, const char* user, const char* pass); static int imap_auth_anon (IMAP_DATA *idata); -#ifdef USE_GSS -static int imap_auth_gss (IMAP_DATA* idata, const char* user); -#endif /* hmac_md5: produce CRAM-MD5 challenge response. */ static void hmac_md5 (const char* password, char* challenge, @@ -100,229 +90,6 @@ static void hmac_md5 (const char* password, char* challenge, MD5Final (response, &ctx); } -#ifdef USE_GSS -/* imap_auth_gss: AUTH=GSSAPI support. Used unconditionally if the server - * supports it */ -static int imap_auth_gss (IMAP_DATA* idata, const char* user) -{ - gss_buffer_desc request_buf, send_token; - gss_buffer_t sec_token; - gss_name_t target_name; - gss_ctx_id_t context; - gss_OID mech_name; - gss_qop_t quality; - int cflags; - OM_uint32 maj_stat, min_stat; - char buf1[GSS_BUFSIZE], buf2[GSS_BUFSIZE], server_conf_flags; - unsigned long buf_size; - - char seq[16]; - - dprint (2, (debugfile, "Attempting GSS login...\n")); - - /* get an IMAP service ticket for the server */ - snprintf (buf1, sizeof (buf1), "imap@%s", idata->conn->mx.host); - request_buf.value = buf1; - request_buf.length = strlen (buf1) + 1; - maj_stat = gss_import_name (&min_stat, &request_buf, gss_nt_service_name, - &target_name); - if (maj_stat != GSS_S_COMPLETE) - { - dprint (2, (debugfile, "Couldn't get service name for [%s]\n", buf1)); - return -1; - } - else if (debuglevel >= 2) - { - maj_stat = gss_display_name (&min_stat, target_name, &request_buf, - &mech_name); - dprint (2, (debugfile, "Using service name [%s]\n", - (char*) request_buf.value)); - maj_stat = gss_release_buffer (&min_stat, &request_buf); - } - - /* now begin login */ - mutt_message _("Authenticating (GSSAPI)..."); - imap_make_sequence (seq, sizeof (seq)); - snprintf (buf1, sizeof (buf1), "%s AUTHENTICATE GSSAPI\r\n", seq); - mutt_socket_write (idata->conn, buf1); - - /* expect a null continuation response ("+") */ - if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) - { - dprint (1, (debugfile, "Error receiving server response.\n")); - gss_release_name (&min_stat, &target_name); - - return -1; - } - - if (buf1[0] != '+') - { - dprint (2, (debugfile, "Invalid response from server: %s\n", buf1)); - gss_release_name (&min_stat, &target_name); - - return -1; - } - - /* now start the security context initialisation loop... */ - dprint (2, (debugfile, "Sending credentials\n")); - sec_token = GSS_C_NO_BUFFER; - context = GSS_C_NO_CONTEXT; - do - { - /* build token */ - maj_stat = gss_init_sec_context (&min_stat, - GSS_C_NO_CREDENTIAL, - &context, - target_name, - GSS_C_NO_OID, - GSS_C_MUTUAL_FLAG | GSS_C_SEQUENCE_FLAG, - 0, - GSS_C_NO_CHANNEL_BINDINGS, - sec_token, - NULL, - &send_token, - (unsigned int*) &cflags, - NULL); - if (maj_stat != GSS_S_COMPLETE && maj_stat != GSS_S_CONTINUE_NEEDED) - { - dprint (1, (debugfile, "Error exchanging credentials\n")); - gss_release_name (&min_stat, &target_name); - /* end authentication attempt */ - mutt_socket_write (idata->conn, "*\r\n"); - mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn); - - return -1; - } - - /* send token */ - mutt_to_base64 ((unsigned char*) buf1, send_token.value, - send_token.length); - gss_release_buffer (&min_stat, &send_token); - strcpy (buf1 + strlen (buf1), "\r\n"); - mutt_socket_write (idata->conn, buf1); - - if (maj_stat == GSS_S_CONTINUE_NEEDED) - { - if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) - { - dprint (1, (debugfile, "Error receiving server response.\n")); - gss_release_name (&min_stat, &target_name); - - return -1; - } - - request_buf.length = mutt_from_base64 (buf2, buf1 + 2); - request_buf.value = buf2; - sec_token = &request_buf; - } - } - while (maj_stat == GSS_S_CONTINUE_NEEDED); - - gss_release_name (&min_stat, &target_name); - - /* get security flags and buffer size */ - if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) - { - dprint (1, (debugfile, "Error receiving server response.\n")); - - return -1; - } - request_buf.length = mutt_from_base64 (buf2, buf1 + 2); - request_buf.value = buf2; - - maj_stat = gss_unwrap (&min_stat, context, &request_buf, &send_token, - &cflags, &quality); - if (maj_stat != GSS_S_COMPLETE) - { - dprint (2, (debugfile, "Couldn't unwrap security level data\n")); - gss_release_buffer (&min_stat, &send_token); - - mutt_socket_write(idata->conn, "*\r\n"); - return -1; - } - dprint (2, (debugfile, "Credential exchange complete\n")); - - /* first octet is security levels supported. We want NONE */ - server_conf_flags = ((char*) send_token.value)[0]; - if ( !(((char*) send_token.value)[0] & GSS_AUTH_P_NONE) ) - { - dprint (2, (debugfile, "Server requires integrity or privace\n")); - gss_release_buffer (&min_stat, &send_token); - - mutt_socket_write(idata->conn, "*\r\n"); - return -1; - } - - /* we don't care about buffer size if we don't wrap content. But here it is */ - ((char*) send_token.value)[0] = 0; - buf_size = ntohl (*((long *) send_token.value)); - gss_release_buffer (&min_stat, &send_token); - dprint (2, (debugfile, "Unwrapped security level flags: %c%c%c\n", - server_conf_flags & GSS_AUTH_P_NONE ? 'N' : '-', - server_conf_flags & GSS_AUTH_P_INTEGRITY ? 'I' : '-', - server_conf_flags & GSS_AUTH_P_PRIVACY ? 'P' : '-')); - dprint (2, (debugfile, "Maximum GSS token size is %ld\n", buf_size)); - - /* agree to terms (hack!) */ - buf_size = htonl (buf_size); /* not relevant without integrity/privacy */ - memcpy (buf1, &buf_size, 4); - buf1[0] = GSS_AUTH_P_NONE; - /* server decides if principal can log in as user */ - strncpy (buf1 + 4, user, sizeof (buf1) - 4); - request_buf.value = buf1; - request_buf.length = 4 + strlen (user) + 1; - maj_stat = gss_wrap (&min_stat, context, 0, GSS_C_QOP_DEFAULT, &request_buf, - &cflags, &send_token); - if (maj_stat != GSS_S_COMPLETE) - { - dprint (2, (debugfile, "Error creating login request\n")); - - mutt_socket_write(idata->conn, "*\r\n"); - return -1; - } - - mutt_to_base64 ((unsigned char*) buf1, send_token.value, send_token.length); - dprint (2, (debugfile, "Requesting authorisation as %s\n", user)); - strncat (buf1, "\r\n", sizeof (buf1)); - mutt_socket_write (idata->conn, buf1); - - /* Joy of victory or agony of defeat? */ - if (mutt_socket_read_line_d (buf1, GSS_BUFSIZE, idata->conn) < 0) - { - dprint (1, (debugfile, "Error receiving server response.\n")); - - mutt_socket_write(idata->conn, "*\r\n"); - return -1; - } - if (imap_code (buf1)) - { - /* flush the security context */ - dprint (2, (debugfile, "Releasing GSS credentials\n")); - maj_stat = gss_delete_sec_context (&min_stat, &context, &send_token); - if (maj_stat != GSS_S_COMPLETE) - { - dprint (1, (debugfile, "Error releasing credentials\n")); - - return -1; - } - /* send_token may contain a notification to the server to flush - * credentials. RFC 1731 doesn't specify what to do, and since this - * support is only for authentication, we'll assume the server knows - * enough to flush its own credentials */ - gss_release_buffer (&min_stat, &send_token); - - dprint (2, (debugfile, "GSS login complete\n")); - - return 0; - } - - /* logon failed */ - dprint (2, (debugfile, "GSS login failed.\n")); - - return -1; -} -#endif - /* imap_auth_cram_md5: AUTH=CRAM-MD5 support. Used unconditionally if the * server supports it */ static int imap_auth_cram_md5 (IMAP_DATA* idata, const char* user, diff --git a/imap/auth_gss.c b/imap/auth_gss.c new file mode 100644 index 00000000..acf9387d --- /dev/null +++ b/imap/auth_gss.c @@ -0,0 +1,253 @@ +/* + * Copyright (C) 1999-2000 Brendan Cully + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +/* GSS login/authentication code */ + +#include "mutt.h" +#include "imap_private.h" + +#include +#include +#include + +#define GSS_BUFSIZE 8192 + +#define GSS_AUTH_P_NONE 1 +#define GSS_AUTH_P_INTEGRITY 2 +#define GSS_AUTH_P_PRIVACY 4 + +/* imap_auth_gss: AUTH=GSSAPI support. Used unconditionally if the server + * supports it */ +int imap_auth_gss (IMAP_DATA* idata, const char* user) +{ + gss_buffer_desc request_buf, send_token; + gss_buffer_t sec_token; + gss_name_t target_name; + gss_ctx_id_t context; + gss_OID mech_name; + gss_qop_t quality; + int cflags; + OM_uint32 maj_stat, min_stat; + char buf1[GSS_BUFSIZE], buf2[GSS_BUFSIZE], server_conf_flags; + unsigned long buf_size; + + char seq[16]; + + dprint (2, (debugfile, "Attempting GSS login...\n")); + + /* get an IMAP service ticket for the server */ + snprintf (buf1, sizeof (buf1), "imap@%s", idata->conn->mx.host); + request_buf.value = buf1; + request_buf.length = strlen (buf1) + 1; + maj_stat = gss_import_name (&min_stat, &request_buf, gss_nt_service_name, + &target_name); + if (maj_stat != GSS_S_COMPLETE) + { + dprint (2, (debugfile, "Couldn't get service name for [%s]\n", buf1)); + return -1; + } + else if (debuglevel >= 2) + { + maj_stat = gss_display_name (&min_stat, target_name, &request_buf, + &mech_name); + dprint (2, (debugfile, "Using service name [%s]\n", + (char*) request_buf.value)); + maj_stat = gss_release_buffer (&min_stat, &request_buf); + } + + /* now begin login */ + mutt_message _("Authenticating (GSSAPI)..."); + imap_make_sequence (seq, sizeof (seq)); + snprintf (buf1, sizeof (buf1), "%s AUTHENTICATE GSSAPI\r\n", seq); + mutt_socket_write (idata->conn, buf1); + + /* expect a null continuation response ("+") */ + if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) + { + dprint (1, (debugfile, "Error receiving server response.\n")); + gss_release_name (&min_stat, &target_name); + + return -1; + } + + if (buf1[0] != '+') + { + dprint (2, (debugfile, "Invalid response from server: %s\n", buf1)); + gss_release_name (&min_stat, &target_name); + + return -1; + } + + /* now start the security context initialisation loop... */ + dprint (2, (debugfile, "Sending credentials\n")); + sec_token = GSS_C_NO_BUFFER; + context = GSS_C_NO_CONTEXT; + do + { + /* build token */ + maj_stat = gss_init_sec_context (&min_stat, + GSS_C_NO_CREDENTIAL, + &context, + target_name, + GSS_C_NO_OID, + GSS_C_MUTUAL_FLAG | GSS_C_SEQUENCE_FLAG, + 0, + GSS_C_NO_CHANNEL_BINDINGS, + sec_token, + NULL, + &send_token, + (unsigned int*) &cflags, + NULL); + if (maj_stat != GSS_S_COMPLETE && maj_stat != GSS_S_CONTINUE_NEEDED) + { + dprint (1, (debugfile, "Error exchanging credentials\n")); + gss_release_name (&min_stat, &target_name); + /* end authentication attempt */ + mutt_socket_write (idata->conn, "*\r\n"); + mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn); + + return -1; + } + + /* send token */ + mutt_to_base64 ((unsigned char*) buf1, send_token.value, + send_token.length); + gss_release_buffer (&min_stat, &send_token); + strcpy (buf1 + strlen (buf1), "\r\n"); + mutt_socket_write (idata->conn, buf1); + + if (maj_stat == GSS_S_CONTINUE_NEEDED) + { + if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) + { + dprint (1, (debugfile, "Error receiving server response.\n")); + gss_release_name (&min_stat, &target_name); + + return -1; + } + + request_buf.length = mutt_from_base64 (buf2, buf1 + 2); + request_buf.value = buf2; + sec_token = &request_buf; + } + } + while (maj_stat == GSS_S_CONTINUE_NEEDED); + + gss_release_name (&min_stat, &target_name); + + /* get security flags and buffer size */ + if (mutt_socket_read_line_d (buf1, sizeof (buf1), idata->conn) < 0) + { + dprint (1, (debugfile, "Error receiving server response.\n")); + + return -1; + } + request_buf.length = mutt_from_base64 (buf2, buf1 + 2); + request_buf.value = buf2; + + maj_stat = gss_unwrap (&min_stat, context, &request_buf, &send_token, + &cflags, &quality); + if (maj_stat != GSS_S_COMPLETE) + { + dprint (2, (debugfile, "Couldn't unwrap security level data\n")); + gss_release_buffer (&min_stat, &send_token); + + mutt_socket_write(idata->conn, "*\r\n"); + return -1; + } + dprint (2, (debugfile, "Credential exchange complete\n")); + + /* first octet is security levels supported. We want NONE */ + server_conf_flags = ((char*) send_token.value)[0]; + if ( !(((char*) send_token.value)[0] & GSS_AUTH_P_NONE) ) + { + dprint (2, (debugfile, "Server requires integrity or privace\n")); + gss_release_buffer (&min_stat, &send_token); + + mutt_socket_write(idata->conn, "*\r\n"); + return -1; + } + + /* we don't care about buffer size if we don't wrap content. But here it is */ + ((char*) send_token.value)[0] = 0; + buf_size = ntohl (*((long *) send_token.value)); + gss_release_buffer (&min_stat, &send_token); + dprint (2, (debugfile, "Unwrapped security level flags: %c%c%c\n", + server_conf_flags & GSS_AUTH_P_NONE ? 'N' : '-', + server_conf_flags & GSS_AUTH_P_INTEGRITY ? 'I' : '-', + server_conf_flags & GSS_AUTH_P_PRIVACY ? 'P' : '-')); + dprint (2, (debugfile, "Maximum GSS token size is %ld\n", buf_size)); + + /* agree to terms (hack!) */ + buf_size = htonl (buf_size); /* not relevant without integrity/privacy */ + memcpy (buf1, &buf_size, 4); + buf1[0] = GSS_AUTH_P_NONE; + /* server decides if principal can log in as user */ + strncpy (buf1 + 4, user, sizeof (buf1) - 4); + request_buf.value = buf1; + request_buf.length = 4 + strlen (user) + 1; + maj_stat = gss_wrap (&min_stat, context, 0, GSS_C_QOP_DEFAULT, &request_buf, + &cflags, &send_token); + if (maj_stat != GSS_S_COMPLETE) + { + dprint (2, (debugfile, "Error creating login request\n")); + + mutt_socket_write(idata->conn, "*\r\n"); + return -1; + } + + mutt_to_base64 ((unsigned char*) buf1, send_token.value, send_token.length); + dprint (2, (debugfile, "Requesting authorisation as %s\n", user)); + strncat (buf1, "\r\n", sizeof (buf1)); + mutt_socket_write (idata->conn, buf1); + + /* Joy of victory or agony of defeat? */ + if (mutt_socket_read_line_d (buf1, GSS_BUFSIZE, idata->conn) < 0) + { + dprint (1, (debugfile, "Error receiving server response.\n")); + + mutt_socket_write(idata->conn, "*\r\n"); + return -1; + } + if (imap_code (buf1)) + { + /* flush the security context */ + dprint (2, (debugfile, "Releasing GSS credentials\n")); + maj_stat = gss_delete_sec_context (&min_stat, &context, &send_token); + if (maj_stat != GSS_S_COMPLETE) + { + dprint (1, (debugfile, "Error releasing credentials\n")); + + return -1; + } + /* send_token may contain a notification to the server to flush + * credentials. RFC 1731 doesn't specify what to do, and since this + * support is only for authentication, we'll assume the server knows + * enough to flush its own credentials */ + gss_release_buffer (&min_stat, &send_token); + + dprint (2, (debugfile, "GSS login complete\n")); + + return 0; + } + + /* logon failed */ + dprint (2, (debugfile, "GSS login failed.\n")); + + return -1; +} diff --git a/imap/browse.c b/imap/browse.c index adb354f7..f49fa372 100644 --- a/imap/browse.c +++ b/imap/browse.c @@ -283,14 +283,12 @@ static int add_list_result (CONNECTION *conn, const char *seq, const char *cmd, } /* imap_add_folder: add a folder name to the browser list, formatting it as - * necessary. NOTE: check for duplicate folders removed, believed to be - * useless. Tell me if otherwise (brendan@kublai.com) */ + * necessary. */ static void imap_add_folder (char delim, char *folder, int noselect, int noinferiors, struct browser_state *state, short isparent) { char tmp[LONG_STRING]; char relpath[LONG_STRING]; - int flen = strlen (folder); IMAP_MBOX mx; if (imap_parse_path (state->folder, &mx)) @@ -298,14 +296,10 @@ static void imap_add_folder (char delim, char *folder, int noselect, imap_unquote_string (folder); - /* plus 2: folder may be selectable AND have inferiors */ - if (state->entrylen + 2 == state->entrymax) + if (state->entrylen + 1 == state->entrymax) { safe_realloc ((void **) &state->entry, sizeof (struct folder_file) * (state->entrymax += 256)); - /* apparently linux+glibc2.1 was zeroing this for me? Jon Hellan reports - * that the browser segfaults on more than 256 entries. I never had this - * problem */ memset (state->entry + state->entrylen, 0, (sizeof (struct folder_file) * (state->entrymax - state->entrylen))); } @@ -325,34 +319,19 @@ static void imap_add_folder (char delim, char *folder, int noselect, if (!((regexec (Mask.rx, relpath, 0, NULL, 0) == 0) ^ Mask.not)) return; - if (!noselect) - { - imap_qualify_path (tmp, sizeof (tmp), &mx, folder, NULL); - (state->entry)[state->entrylen].name = safe_strdup (tmp); - - (state->entry)[state->entrylen].desc = safe_strdup (relpath); - - (state->entry)[state->entrylen].notfolder = 0; - (state->entrylen)++; - } - if (!noinferiors) - { - char trailing_delim[2]; - - /* create trailing delimiter if necessary */ - trailing_delim[1] = '\0'; - trailing_delim[0] = (flen && folder[flen - 1] != delim) ? delim : '\0'; - - imap_qualify_path (tmp, sizeof (tmp), &mx, folder, trailing_delim); - (state->entry)[state->entrylen].name = safe_strdup (tmp); + imap_qualify_path (tmp, sizeof (tmp), &mx, folder, NULL); + (state->entry)[state->entrylen].name = safe_strdup (tmp); - if (!isparent && (strlen (relpath) < sizeof (relpath) - 2)) - strcat (relpath, trailing_delim); - (state->entry)[state->entrylen].desc = safe_strdup (relpath); + (state->entry)[state->entrylen].desc = safe_strdup (relpath); - (state->entry)[state->entrylen].notfolder = 1; - (state->entrylen)++; - } + (state->entry)[state->entrylen].imap = 1; + /* delimiter at the root is useless. */ + if (folder[0] == '\0') + delim = '\0'; + (state->entry)[state->entrylen].delim = delim; + (state->entry)[state->entrylen].selectable = !noselect; + (state->entry)[state->entrylen].inferiors = !noinferiors; + (state->entrylen)++; } static int compare_names(struct folder_file *a, struct folder_file *b) @@ -382,9 +361,7 @@ static int get_namespace (IMAP_DATA *idata, char *nsbuf, int nsblen, do { if (mutt_socket_read_line_d (buf, sizeof (buf), idata->conn) < 0) - { - return (-1); - } + return -1; if (buf[0] == '*') { @@ -459,7 +436,7 @@ static int get_namespace (IMAP_DATA *idata, char *nsbuf, int nsblen, return 0; } -/* Check which namespaces actually exist */ +/* Check which namespaces have contents */ static int verify_namespace (CONNECTION *conn, IMAP_NAMESPACE_INFO *nsi, int nns) { @@ -472,8 +449,11 @@ static int verify_namespace (CONNECTION *conn, IMAP_NAMESPACE_INFO *nsi, for (i = 0; i < nns; i++, nsi++) { imap_make_sequence (seq, sizeof (seq)); - snprintf (buf, sizeof (buf), "%s %s \"\" \"%s\"\r\n", seq, - option (OPTIMAPLSUB) ? "LSUB" : "LIST", nsi->prefix); + /* Cyrus gives back nothing if the % isn't added. This may return lots + * of data in some cases, I guess, but I currently feel that's better + * than invisible namespaces */ + snprintf (buf, sizeof (buf), "%s %s \"\" \"%s%c%%\"\r\n", seq, + option (OPTIMAPLSUB) ? "LSUB" : "LIST", nsi->prefix, nsi->delim); mutt_socket_write (conn, buf); nsi->listable = 0; @@ -481,9 +461,8 @@ static int verify_namespace (CONNECTION *conn, IMAP_NAMESPACE_INFO *nsi, do { if (imap_parse_list_response(conn, buf, sizeof(buf), &name, - &(nsi->noselect), &(nsi->noinferiors), - &delim) != 0) - return (-1); + &(nsi->noselect), &(nsi->noinferiors), &delim) != 0) + return -1; nsi->listable |= (name != NULL); } while ((mutt_strncmp (buf, seq, SEQLEN) != 0)); diff --git a/imap/imap.c b/imap/imap.c index 828af0b0..36cda2bc 100644 --- a/imap/imap.c +++ b/imap/imap.c @@ -1113,6 +1113,13 @@ void imap_fastclose_mailbox (CONTEXT *ctx) safe_free ((void **) &CTX_DATA->cache[i].path); } } + +#if 0 + /* This is not the right place to logout, actually. There are two dangers: + * 1. status is set to IMAP_LOGOUT as soon as the user says q, even if she + * cancels a bit later. + * 2. We may get here when closing the $received folder, but before we sync + * the spool. So the sync will currently cause an abort. */ if (CTX_DATA->status == IMAP_BYE || CTX_DATA->status == IMAP_FATAL || CTX_DATA->status == IMAP_LOGOUT) { @@ -1120,6 +1127,7 @@ void imap_fastclose_mailbox (CONTEXT *ctx) CTX_DATA->conn->data = NULL; safe_free ((void **) &ctx->data); } +#endif } /* use the NOOP command to poll for new mail diff --git a/mx.c b/mx.c index e562b1c5..5ad4255e 100644 --- a/mx.c +++ b/mx.c @@ -818,9 +818,6 @@ int mx_close_mailbox (CONTEXT *ctx) } mutt_expand_path (mbox, sizeof (mbox)); -#ifdef USE_IMAP - if (!mx_is_imap (ctx->path)) -#endif if (isSpool) { snprintf (buf, sizeof (buf), _("Move read messages to %s?"), mbox); @@ -853,29 +850,54 @@ int mx_close_mailbox (CONTEXT *ctx) if (move_messages) { - if (mx_open_mailbox (mbox, M_APPEND, &f) == NULL) - return (-1); - mutt_message (_("Moving read messages to %s..."), mbox); - for (i = 0; i < ctx->msgcount; i++) +#ifdef USE_IMAP + /* try to use server-side copy first */ + i = 1; + + if (ctx->magic == M_IMAP && mx_is_imap (mbox)) { - if (ctx->hdrs[i]->read && !ctx->hdrs[i]->deleted) - { - if (mutt_append_message (&f, ctx, ctx->hdrs[i], 0, CH_UPDATE_LEN) == 0) - { - ctx->hdrs[i]->deleted = 1; - ctx->deleted++; - } + /* tag messages for moving, and clear old tags, if any */ + for (i = 0; i < ctx->msgcount; i++) + if (ctx->hdrs[i]->read && !ctx->hdrs[i]->deleted) + ctx->hdrs[i]->tagged = 1; else - { - mx_close_mailbox (&f); - return -1; + ctx->hdrs[i]->tagged = 0; + + i = imap_copy_messages (ctx, NULL, mbox, 1); + } + + if (i == 0) /* success */ + mutt_clear_error (); + else if (i == -1) /* horrible error, bail */ + return -1; + else /* use regular append-copy mode */ +#endif + { + if (mx_open_mailbox (mbox, M_APPEND, &f) == NULL) + return -1; + + for (i = 0; i < ctx->msgcount; i++) + { + if (ctx->hdrs[i]->read && !ctx->hdrs[i]->deleted) + { + if (mutt_append_message (&f, ctx, ctx->hdrs[i], 0, CH_UPDATE_LEN) == 0) + { + ctx->hdrs[i]->deleted = 1; + ctx->deleted++; + } + else + { + mx_close_mailbox (&f); + return -1; + } } } + + mx_close_mailbox (&f); } - - mx_close_mailbox (&f); + } else if (!ctx->changed && ctx->deleted == 0) { @@ -890,8 +912,6 @@ int mx_close_mailbox (CONTEXT *ctx) { if (imap_sync_mailbox (ctx, purge) == -1) return -1; - if (ctx->magic == M_IMAP && !purge) - mutt_message (_("%d kept."), ctx->msgcount); } else #endif @@ -908,19 +928,19 @@ int mx_close_mailbox (CONTEXT *ctx) if (sync_mailbox (ctx) == -1) return -1; } + } - if (move_messages) - mutt_message (_("%d kept, %d moved, %d deleted."), - ctx->msgcount - ctx->deleted, read_msgs, ctx->deleted); - else - mutt_message (_("%d kept, %d deleted."), - ctx->msgcount - ctx->deleted, ctx->deleted); + if (move_messages) + mutt_message (_("%d kept, %d moved, %d deleted."), + ctx->msgcount - ctx->deleted, read_msgs, ctx->deleted); + else + mutt_message (_("%d kept, %d deleted."), + ctx->msgcount - ctx->deleted, ctx->deleted); - if (ctx->msgcount == ctx->deleted && - (ctx->magic == M_MMDF || ctx->magic == M_MBOX) && - !mutt_is_spool(ctx->path) && !option (OPTSAVEEMPTY)) - mx_unlink_empty (ctx->path); - } + if (ctx->msgcount == ctx->deleted && + (ctx->magic == M_MMDF || ctx->magic == M_MBOX) && + !mutt_is_spool(ctx->path) && !option (OPTSAVEEMPTY)) + mx_unlink_empty (ctx->path); mx_fastclose_mailbox (ctx); -- 2.40.0