To authenticate to IMAP servers, mbsync uses SASL if linked with the
Cyrus SASL library (libsasl2) and falls back to LOGIN otherwise. This
logic was inline in the IMAP driver but was mostly independent of the
surrounding logic.
Factor out this authentication logic into its own independent interface
(imap_auth_*). This isolates the logic and thereby allows it to be more
easily extended with a different SASL library (e.g. GNU SASL).
The interface's implementation adds minor changes in behaviour to
simplify the authentication logic. This includes always attempting to
fall back to LOGIN when SASL fails and unifying similar error messages.
---
src/Makefile.am | 2 +
src/common.h | 3 +
src/drv_imap.c | 318 ++++++-------------------------
src/imap_auth.c | 498 ++++++++++++++++++++++++++++++++++++++++++++++++
src/imap_auth.h | 106 +++++++++++
src/util.c | 34 ++++
6 files changed, 697 insertions(+), 264 deletions(-)
create mode 100644 src/imap_auth.c
create mode 100644 src/imap_auth.h
diff --git a/src/Makefile.am b/src/Makefile.am
index 63ff10b717b0..21b7a5acee30 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -8,6 +8,7 @@ mbsync_SOURCES = \
drv_imap.c imap_msgs.c imap_utf7.c \
drv_maildir.c \
sync.c sync_state.c sync_msg_cvt.c \
+ imap_auth.c \
main.c main_sync.c main_list.c
EXTRA_mbsync_SOURCES = \
drv_proxy.c
@@ -17,6 +18,7 @@ noinst_HEADERS = \
common.h config.h socket.h \
driver.h imap_p.h \
sync.h sync_p.h \
+ imap_auth.h \
main_p.h
mbsync_LDADD = $(DB_LIBS) $(SSL_LIBS) $(SOCK_LIBS) $(SASL_LIBS) $(Z_LIBS)
$(KEYCHAIN_LIBS)
diff --git a/src/common.h b/src/common.h
index 35b4e640e1ea..aa89ff37f112 100644
--- a/src/common.h
+++ b/src/common.h
@@ -239,6 +239,9 @@ void add_string_list_n( string_list_t **list, const char
*str, uint len );
void add_string_list( string_list_t **list, const char *str );
void free_string_list( string_list_t *list );
+char *find_string_list( const string_list_t *list, const char *str );
+char *find_string_list_case( const string_list_t *list, const char *str );
+
#ifndef HAVE_MEMRCHR
void *memrchr( const void *s, int c, size_t n );
#endif
diff --git a/src/drv_imap.c b/src/drv_imap.c
index fc38d908bb49..26b205ab8c0d 100644
--- a/src/drv_imap.c
+++ b/src/drv_imap.c
@@ -8,16 +8,12 @@
#include "imap_p.h"
+#include "imap_auth.h"
#include "socket.h"
#include <ctype.h>
#include <sys/wait.h>
-#ifdef HAVE_LIBSASL
-# include <sasl/sasl.h>
-# include <sasl/saslutil.h>
-#endif
-
#ifdef HAVE_MACOS_KEYCHAIN
# include <Security/Security.h>
#endif
@@ -176,10 +172,7 @@ union imap_store {
void (*imap_cancel)( void *aux );
} callbacks;
void *callback_aux;
-#ifdef HAVE_LIBSASL
- sasl_conn_t *sasl;
- int sasl_cont;
-#endif
+ imap_auth_client_t auth;
void (*expunge_callback)( message_t *msg, void *aux );
void (*bad_callback)( void *aux );
@@ -273,9 +266,7 @@ typedef union {
enum CAPABILITY {
IMAP4REV1,
NOLOGIN,
-#ifdef HAVE_LIBSASL
SASLIR,
-#endif
#ifdef HAVE_LIBSSL
STARTTLS,
#endif
@@ -295,9 +286,7 @@ static const struct {
} cap_list[] = {
{ "IMAP4REV1", 9 },
{ "LOGINDISABLED", 13 },
-#ifdef HAVE_LIBSASL
{ "SASL-IR", 7 },
-#endif
#ifdef HAVE_LIBSSL
{ "STARTTLS", 8 },
#endif
@@ -1967,9 +1956,7 @@ imap_cancel_store( store_t *gctx )
{
imap_store_t *ctx = (imap_store_t *)gctx;
-#ifdef HAVE_LIBSASL
- sasl_dispose( &ctx->sasl );
-#endif
+ imap_auth_free_client( &ctx->auth );
socket_close( &ctx->conn );
cancel_sent_imap_cmds( ctx );
cancel_pending_imap_cmds( ctx );
@@ -2332,6 +2319,13 @@ ensure_user( imap_server_conf_t *srvc )
return srvc->user;
}
+static const char *
+ensure_user_cb( void *arg )
+{
+ imap_server_conf_t *srvc = arg;
+ return ensure_user( srvc );
+}
+
#ifdef HAVE_MACOS_KEYCHAIN
/*
* Queries the macOS keychain for items matching the paramters specified by the
@@ -2471,126 +2465,26 @@ ensure_password( imap_server_conf_t *srvc )
return srvc->pass;
}
-#ifdef HAVE_LIBSASL
-
-static sasl_callback_t sasl_callbacks[] = {
- { SASL_CB_USER, NULL, NULL },
- { SASL_CB_AUTHNAME, NULL, NULL },
- { SASL_CB_PASS, NULL, NULL },
- { SASL_CB_LIST_END, NULL, NULL }
-};
-
-static int
-process_sasl_interact( sasl_interact_t *interact, imap_server_conf_t *srvc )
-{
- const char *val;
-
- for (;; ++interact) {
- switch (interact->id) {
- case SASL_CB_LIST_END:
- return 0;
- case SASL_CB_USER: // aka authorization id - who to act as
- case SASL_CB_AUTHNAME: // who is really logging in
- val = ensure_user( srvc );
- break;
- case SASL_CB_PASS:
- val = ensure_password( srvc );
- break;
- default:
- error( "Error: Unknown SASL interaction ID\n" );
- return -1;
- }
- if (!val)
- return -1;
- interact->result = val;
- interact->len = strlen( val );
- }
-}
-
-static int
-process_sasl_step( imap_store_t *ctx, int rc, const char *in, uint in_len,
- sasl_interact_t *interact, const char **out, uint *out_len )
+static const char *
+ensure_password_cb( void *arg )
{
- imap_server_conf_t *srvc = ctx->conf->server;
-
- while (rc == SASL_INTERACT) {
- if (process_sasl_interact( interact, srvc ) < 0)
- return -1;
- rc = sasl_client_step( ctx->sasl, in, in_len, &interact, out,
out_len );
- }
- if (rc == SASL_CONTINUE) {
- ctx->sasl_cont = 1;
- } else if (rc == SASL_OK) {
- ctx->sasl_cont = 0;
- } else {
- error( "Error performing SASL authentication step: %s\n",
sasl_errdetail( ctx->sasl ) );
- return -1;
- }
- return 0;
-}
-
-static int
-decode_sasl_data( const char *prompt, char **in, uint *in_len )
-{
- if (prompt) {
- int rc;
- uint prompt_len = strlen( prompt );
- /* We're decoding, the output will be shorter than prompt_len.
*/
- *in = nfmalloc( prompt_len );
- rc = sasl_decode64( prompt, prompt_len, *in, prompt_len, in_len
);
- if (rc != SASL_OK) {
- free( *in );
- error( "Error decoding SASL prompt: %s\n",
sasl_errstring( rc, NULL, NULL ) );
- return -1;
- }
- } else {
- *in = NULL;
- *in_len = 0;
- }
- return 0;
-}
-
-static int
-encode_sasl_data( const char *out, uint out_len, char **enc, uint *enc_len )
-{
- int rc;
- uint enc_len_max = ((out_len + 2) / 3) * 4 + 1;
- *enc = nfmalloc( enc_len_max );
- rc = sasl_encode64( out, out_len, *enc, enc_len_max, enc_len );
- if (rc != SASL_OK) {
- free( *enc );
- error( "Error encoding SASL response: %s\n", sasl_errstring(
rc, NULL, NULL ) );
- return -1;
- }
- return 0;
+ imap_server_conf_t *srvc = arg;
+ return ensure_password( srvc );
}
static int
do_sasl_auth( imap_store_t *ctx, imap_cmd_t *cmdp ATTR_UNUSED, const char
*prompt )
{
- int rc, ret, iovcnt = 0;
- uint in_len, out_len, enc_len;
- const char *out;
- char *in, *enc;
- sasl_interact_t *interact = NULL;
+ int iovcnt = 0;
conn_iovec_t iov[2];
- if (!ctx->sasl_cont) {
- error( "Error: IMAP wants more steps despite successful SASL
authentication.\n" );
- goto bail;
- }
- if (decode_sasl_data( prompt, &in, &in_len ) < 0)
+ char *enc;
+ size_t enc_len;
+ int bad_auth = imap_auth_cont( &ctx->auth, prompt, &enc, &enc_len );
+ if (bad_auth)
goto bail;
- rc = sasl_client_step( ctx->sasl, in, in_len, &interact, &out, &out_len
);
- ret = process_sasl_step( ctx, rc, in, in_len, interact, &out, &out_len
);
- free( in );
- if (ret < 0)
- goto bail;
-
- if (out) {
- if (encode_sasl_data( out, out_len, &enc, &enc_len ) < 0)
- goto bail;
+ if (enc) {
iov[0].buf = enc;
iov[0].len = enc_len;
iov[0].takeOwn = GiveOwn;
@@ -2621,34 +2515,14 @@ do_sasl_auth( imap_store_t *ctx, imap_cmd_t *cmdp
ATTR_UNUSED, const char *promp
static void
done_sasl_auth( imap_store_t *ctx, imap_cmd_t *cmd ATTR_UNUSED, int response )
{
- if (response == RESP_OK && ctx->sasl_cont) {
- sasl_interact_t *interact = NULL;
- const char *out;
- uint out_len;
- int rc = sasl_client_step( ctx->sasl, NULL, 0, &interact, &out,
&out_len );
- if (process_sasl_step( ctx, rc, NULL, 0, interact, &out,
&out_len ) < 0)
- warn( "Warning: SASL reported failure despite
successful IMAP authentication. Ignoring...\n" );
- else if (out_len > 0)
- warn( "Warning: SASL wants more steps despite
successful IMAP authentication. Ignoring...\n" );
- }
-
+ imap_auth_done( &ctx->auth, (response == RESP_OK) );
imap_open_store_authenticate2_p2( ctx, NULL, response );
}
-#endif
-
static void
imap_open_store_authenticate2( imap_store_t *ctx )
{
imap_server_conf_t *srvc = ctx->conf->server;
- string_list_t *mech, *cmech;
- int auth_login = 0;
- int skipped_login = 0;
-#ifdef HAVE_LIBSASL
- const char *saslavail;
- char saslmechs[1024], *saslend = saslmechs;
- int want_external = 0;
-#endif
// Ensure that there are no leftovers from previous runs. This is
needed in case
// the credentials have a timing dependency or otherwise lose validity
after use.
@@ -2662,130 +2536,46 @@ imap_open_store_authenticate2( imap_store_t *ctx )
}
info( "Logging in...\n" );
- for (mech = srvc->auth_mechs; mech; mech = mech->next) {
- int any = equals( mech->string, -1, "*", 1 );
- for (cmech = ctx->auth_mechs; cmech; cmech = cmech->next) {
- if (any || !strcasecmp( mech->string, cmech->string )) {
- if (!strcasecmp( cmech->string, "LOGIN" )) {
-#ifdef HAVE_LIBSSL
- if (ctx->conn.ssl ||
ctx->conn.conf->tunnel || !any)
-#else
- if (ctx->conn.conf->tunnel || !any)
-#endif
- auth_login = 1;
- else
- skipped_login = 1;
-#ifdef HAVE_LIBSASL
- } else {
- uint len = strlen( cmech->string );
- if (saslend + len + 2 > saslmechs +
sizeof(saslmechs))
- oob();
- *saslend++ = ' ';
- memcpy( saslend, cmech->string, len + 1
);
- saslend += len;
-
- if (!strcasecmp( cmech->string,
"EXTERNAL" ))
- want_external = 1;
-#endif
- }
- }
- }
+ static int auth_inited = 0;
+ if (!auth_inited) {
+ int bad_init = imap_auth_init();
+ if (bad_init)
+ goto bail;
+ auth_inited = 1;
}
-#ifdef HAVE_LIBSASL
- if (saslend != saslmechs) {
- int rc;
- uint out_len = 0;
- char *enc = NULL;
- const char *gotmech = NULL, *out = NULL;
- sasl_interact_t *interact = NULL;
- imap_cmd_t *cmd;
- static int sasl_inited;
-
- if (!sasl_inited) {
- rc = sasl_client_init( sasl_callbacks );
- if (rc != SASL_OK) {
- saslbail:
- error( "Error initializing SASL client: %s\n",
sasl_errstring( rc, NULL, NULL ) );
- goto bail;
- }
- sasl_inited = 1;
- }
- rc = sasl_client_new( "imap", srvc->sconf.host, NULL, NULL,
NULL, 0, &ctx->sasl );
- if (rc != SASL_OK) {
- if (rc == SASL_NOMECH)
- goto notsasl;
- if (!ctx->sasl)
- goto saslbail;
- error( "Error initializing SASL context: %s\n",
sasl_errdetail( ctx->sasl ) );
- goto bail;
- }
+ char mechs[1024];
+ imap_auth_filter_mechs(
+ mechs, sizeof(mechs), srvc->auth_mechs, ctx->auth_mechs
);
- // The built-in EXTERNAL mechanism wants the authentication id
to be set
- // even before instantiation; consequently it won't prompt for
it, either.
- // While this clearly makes sense on the server side, it
arguably does not
- // on the client side. Ah, well ...
- if (want_external && ensure_user( srvc )) {
- rc = sasl_setprop( ctx->sasl, SASL_AUTH_EXTERNAL,
srvc->user );
- if (rc != SASL_OK ) {
- error( "Error setting SASL authentication id:
%s\n", sasl_errdetail( ctx->sasl ) );
- goto bail;
- }
- }
+ imap_auth_new_client( &ctx->auth );
- rc = sasl_client_start( ctx->sasl, saslmechs + 1, &interact,
CAP(SASLIR) ? &out : NULL, &out_len, &gotmech );
- if (rc == SASL_NOMECH)
- goto notsasl;
- if (gotmech)
- info( "Authenticating with SASL mechanism %s...\n",
gotmech );
- /* Technically, we are supposed to loop over
sasl_client_start(),
- * but it just calls sasl_client_step() anyway. */
- if (process_sasl_step( ctx, rc, NULL, 0, interact, CAP(SASLIR)
? &out : NULL, &out_len ) < 0)
- goto bail;
- if (out) {
- if (!out_len)
- enc = nfstrdup( "=" ); /* A zero-length initial
response is encoded as padding. */
- else if (encode_sasl_data( out, out_len, &enc, NULL ) <
0)
- goto bail;
- }
+ ctx->auth.user_cb = ensure_user_cb;
+ ctx->auth.user_arg = srvc;
+ ctx->auth.pass_cb = ensure_password_cb;
+ ctx->auth.pass_arg = srvc;
- cmd = new_imap_cmd( sizeof(*cmd) );
- cmd->param.cont = do_sasl_auth;
- ctx->caps = 0;
- imap_exec( ctx, cmd, done_sasl_auth, enc ? "AUTHENTICATE %s %s"
: "AUTHENTICATE %s", gotmech, enc );
- free( enc );
- return;
- notsasl:
- if (!ctx->sasl || sasl_listmech( ctx->sasl, NULL, "", " ", "",
&saslavail, NULL, NULL ) != SASL_OK)
- saslavail = "(none)";
- if (!auth_login) {
- error( "IMAP error: selected SASL mechanism(s) not
available;\n"
- " selected:%s\n available: %s\n", saslmechs,
saslavail );
- goto skipnote;
- }
- info( "NOT using available SASL mechanism(s): %s\n", saslavail
);
- sasl_dispose( &ctx->sasl );
- }
-#endif
- if (auth_login) {
- if (!ensure_user( srvc ) || !ensure_password( srvc ))
- goto bail;
+ ctx->auth.domain = srvc->sconf.host;
+ ctx->auth.force = !!find_string_list_case( srvc->auth_mechs, "LOGIN" );
+ ctx->auth.saslir = CAP(SASLIR);
+ ctx->auth.secure =
#ifdef HAVE_LIBSSL
- if (!ctx->conn.ssl && !ctx->conn.conf->tunnel)
-#endif
- warn( "*** IMAP Warning *** Password is being sent in
the clear\n" );
- ctx->caps = 0;
- imap_exec( ctx, NULL, imap_open_store_authenticate2_p2,
- "LOGIN \"%\\s\" \"%\\s\"", srvc->user, srvc->pass );
- return;
- }
- error( "IMAP error: server supports no acceptable authentication
mechanism\n" );
-#ifdef HAVE_LIBSASL
- skipnote:
+ (ctx->conn.ssl || ctx->conn.conf->tunnel);
+#else
+ !!ctx->conn.conf->tunnel;
#endif
- if (skipped_login)
- error( "Note: not using LOGIN because connection is not
encrypted;\n"
- " use 'AuthMechs LOGIN' explicitly to force it.\n"
);
+
+ char *out = imap_auth_create_cmd( &ctx->auth, mechs );
+ if (!out)
+ goto bail;
+
+ ctx->caps = 0;
+ imap_cmd_t *cmd = new_imap_cmd( sizeof(*cmd) );
+ cmd->cmd = out;
+ cmd->param.cont = do_sasl_auth;
+ cmd->param.done = done_sasl_auth;
+ submit_imap_cmd( ctx, cmd );
+ return;
bail:
imap_open_store_bail( ctx, FAIL_FINAL );
diff --git a/src/imap_auth.c b/src/imap_auth.c
new file mode 100644
index 000000000000..a0398b5cd7ad
--- /dev/null
+++ b/src/imap_auth.c
@@ -0,0 +1,498 @@
+// SPDX-FileCopyrightText: 2026 Oswald Buddenhagen <[email protected]>
+// SPDX-FileCopyrightText: 2026 Seth McDonald <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later WITH
LicenseRef-isync-GPL-exception
+
+#define DEBUG_FLAG DEBUG_NET
+
+#include "imap_auth.h"
+
+#if defined(HAVE_LIBSASL)
+# include <sasl/sasl.h>
+# include <sasl/saslutil.h>
+#endif
+
+#define MIN_SASL_MECH_LEN 1
+#define MAX_SASL_MECH_LEN 20
+
+static const char *
+cred_stub( void *arg )
+{
+ (void)arg;
+ return NULL;
+}
+
+static char *
+find_mech( const char *list, const char *mech, size_t len )
+{
+ assert( list );
+ assert( mech );
+ assert( len >= MIN_SASL_MECH_LEN );
+ assert( len <= MAX_SASL_MECH_LEN );
+
+ char name[MAX_SASL_MECH_LEN + 3];
+ name[0] = ' ';
+ memcpy( name + 1, mech, len );
+ name[len + 1] = ' ';
+ name[len + 2] = '\0';
+
+ char *elm = strstr( list, name );
+ return elm ? elm + 1 : NULL;
+}
+
+#if defined(HAVE_LIBSASL)
+
+static sasl_callback_t sasl_callbacks[] = {
+ { SASL_CB_USER, NULL, NULL },
+ { SASL_CB_AUTHNAME, NULL, NULL },
+ { SASL_CB_PASS, NULL, NULL },
+ { SASL_CB_LIST_END, NULL, NULL }
+};
+
+static const char *
+query_sasl_error( sasl_conn_t *ctx, int rc )
+{
+ const char *err = ctx
+ ? sasl_errdetail( ctx )
+ : sasl_errstring( rc, NULL, NULL );
+ return err ? err : "Unknown error";
+}
+
+static int
+process_sasl_interact( imap_auth_client_t *client, sasl_interact_t *interact )
+{
+ const char *val;
+
+ for (;; ++interact) {
+ switch (interact->id) {
+ case SASL_CB_LIST_END:
+ return 0;
+ case SASL_CB_USER: // aka authorization id - who to act as
+ case SASL_CB_AUTHNAME: // who is really logging in
+ val = client->user_cb( client->user_arg );
+ break;
+ case SASL_CB_PASS:
+ val = client->pass_cb( client->pass_arg );
+ break;
+ default:
+ error( "Error: Unknown SASL interaction ID\n" );
+ return -1;
+ }
+ if (!val)
+ return -1;
+ interact->result = val;
+ interact->len = strlen( val );
+ }
+}
+
+static int
+process_sasl_step(
+ imap_auth_client_t *client,
+ int rc,
+ const char *in,
+ uint in_len,
+ sasl_interact_t *interact,
+ const char **out,
+ uint *out_len )
+{
+ sasl_conn_t *session = client->session;
+
+ while (rc == SASL_INTERACT) {
+ int bad_interact = process_sasl_interact( client, interact );
+ if (bad_interact)
+ return -1;
+ rc = sasl_client_step( session, in, in_len, &interact, out,
out_len );
+ }
+ if (rc == SASL_CONTINUE) {
+ client->cont = 1;
+ } else if (rc == SASL_OK) {
+ client->cont = 0;
+ } else {
+ const char *err = query_sasl_error( session, rc );
+ error( "Error performing SASL authentication step: %s\n", err );
+ return -1;
+ }
+ return 0;
+}
+
+static int
+decode_sasl_data( const char *prompt, char **in, uint *in_len )
+{
+ if (prompt) {
+ int rc;
+ uint prompt_len = strlen( prompt );
+ /* We're decoding, the output will be shorter than prompt_len.
*/
+ *in = nfmalloc( prompt_len );
+ rc = sasl_decode64( prompt, prompt_len, *in, prompt_len, in_len
);
+ if (rc != SASL_OK) {
+ free( *in );
+ const char *err = query_sasl_error( NULL, rc );
+ error( "Error decoding SASL prompt: %s\n", err );
+ return -1;
+ }
+ } else {
+ *in = NULL;
+ *in_len = 0;
+ }
+ return 0;
+}
+
+static int
+encode_sasl_data( const char *out, uint out_len, char **enc, uint *enc_len )
+{
+ int rc;
+ uint enc_len_max = ((out_len + 2) / 3) * 4 + 1;
+ *enc = nfmalloc( enc_len_max );
+ rc = sasl_encode64( out, out_len, *enc, enc_len_max, enc_len );
+ if (rc != SASL_OK) {
+ free( *enc );
+ const char *err = query_sasl_error( NULL, rc );
+ error( "Error encoding SASL response: %s\n", err );
+ return -1;
+ }
+ return 0;
+}
+
+#endif
+
+int
+imap_auth_init( void )
+{
+#if defined(HAVE_LIBSASL)
+ int rc = sasl_client_init( sasl_callbacks );
+ if (rc != SASL_OK) {
+ const char *err = query_sasl_error( NULL, rc );
+ error( "Error initializing SASL library: %s\n", err );
+ return -1;
+ }
+#endif
+ return 0;
+}
+
+void
+imap_auth_cleanup( void )
+{
+#if defined(HAVE_LIBSASL)
+ sasl_client_done();
+#endif
+}
+
+void
+imap_auth_new_client( imap_auth_client_t *client )
+{
+ assert( client );
+
+ memset( client, 0, sizeof(*client) );
+ client->user_cb = cred_stub;
+ client->pass_cb = cred_stub;
+ client->domain = "";
+}
+
+void
+imap_auth_free_client( imap_auth_client_t *client )
+{
+ if (!client)
+ return;
+
+#if defined(HAVE_LIBSASL)
+ sasl_conn_t *session = client->session;
+ sasl_dispose( &session );
+#endif
+}
+
+static void
+intersect_mechs(
+ char *restrict common_mechs,
+ size_t size,
+ const string_list_t *client_mechs,
+ const string_list_t *server_mechs )
+{
+ assert( common_mechs );
+ assert( size > 0 );
+
+ size_t i = 0;
+ for (const string_list_t *client_mech = client_mechs; client_mech;
+ client_mech = client_mech->next) {
+ const char *mech = client_mech->string;
+ if (!find_string_list_case( server_mechs, mech ))
+ continue;
+
+ size_t len = strlen( mech );
+ if (i + len + 2 > size)
+ oob();
+
+ memcpy( common_mechs + i, mech, len );
+ to_upper( common_mechs + i, len );
+ i += len;
+ common_mechs[i] = ' ';
+ i++;
+ }
+ common_mechs[i] = '\0';
+}
+
+void
+imap_auth_filter_mechs(
+ char *restrict common_mechs,
+ size_t size,
+ const string_list_t *client_mechs,
+ const string_list_t *server_mechs )
+{
+ assert( common_mechs );
+ assert( size > 0 );
+
+ int any_mech = !!find_string_list( client_mechs, "*" );
+ common_mechs[0] = ' ';
+ intersect_mechs(
+ common_mechs + 1,
+ size - 1,
+ any_mech ? server_mechs : client_mechs,
+ server_mechs );
+}
+
+static char *
+attempt_sasl_auth( imap_auth_client_t *client, const char *mechs )
+{
+ assert( client );
+ assert( mechs );
+
+#if defined(HAVE_LIBSASL)
+ const char *service = "imap";
+ const char *client_ipaddr = NULL;
+ const char *server_ipaddr = NULL;
+ const sasl_callback_t *prompts = NULL;
+ uint session_flags = 0;
+ sasl_conn_t *session = NULL;
+ int rc = sasl_client_new(
+ service,
+ client->domain,
+ client_ipaddr,
+ server_ipaddr,
+ prompts,
+ session_flags,
+ &session );
+ client->session = session;
+ if (rc != SASL_OK) {
+ const char *err = query_sasl_error( session, rc );
+ error( "Error initializing SASL client: %s\n", err );
+ return NULL;
+ }
+
+ // The built-in EXTERNAL mechanism wants the authentication id to be set
+ // even before instantiation; consequently it won't prompt for it,
either.
+ // While this clearly makes sense on the server side, it arguably does
not
+ // on the client side. Ah, well ...
+ if (find_mech( mechs, "EXTERNAL", strlen( "EXTERNAL" ) )) {
+ const char *authname = client->user_cb( client->user_arg );
+ rc = sasl_setprop( session, SASL_AUTH_EXTERNAL, authname );
+ if (rc != SASL_OK) {
+ const char *err = query_sasl_error( session, rc );
+ error( "Error setting SASL authentication id: %s\n",
err );
+ return NULL;
+ }
+ }
+
+ sasl_interact_t *interact = NULL;
+ const char *out = NULL;
+ uint out_len = 0;
+ const char *mech = NULL;
+ rc = sasl_client_start(
+ session,
+ mechs,
+ &interact,
+ client->saslir ? &out : NULL,
+ &out_len,
+ &mech );
+ if (rc == SASL_NOMECH) {
+ const char *user = NULL;
+ const char *prefix = "";
+ const char *sep = " ";
+ const char *suffix = "";
+ const char *available_mechs;
+ rc = sasl_listmech(
+ session,
+ user,
+ prefix,
+ sep,
+ suffix,
+ &available_mechs,
+ NULL,
+ NULL );
+ if (rc != SASL_OK)
+ available_mechs = "(none)";
+
+ info( "NOT using available SASL mechanism(s): %s\n",
available_mechs );
+ return NULL;
+ }
+ if (mech)
+ info( "Authenticating with SASL mechanism %s...\n", mech );
+
+ // Technically, we are supposed to loop over sasl_client_start(),
+ // but it just calls sasl_client_step() anyway.
+ int bad_step = process_sasl_step(
+ client,
+ rc,
+ NULL,
+ 0,
+ interact,
+ client->saslir ? &out : NULL,
+ &out_len );
+ if (bad_step)
+ return NULL;
+
+ char *enc = NULL;
+ if (out) {
+ if (!out_len) {
+ // A zero-length initial response is encoded as padding.
+ enc = nfstrdup( "=" );
+ } else {
+ int bad_enc = encode_sasl_data( out, out_len, &enc,
NULL );
+ if (bad_enc)
+ return NULL;
+ }
+ }
+
+ char *cmd;
+ nfasprintf(
+ &cmd, enc ? "AUTHENTICATE %s %s" : "AUTHENTICATE %s",
mech, enc );
+ free( enc );
+ return cmd;
+#else
+ (void)client;
+ (void)mechs;
+ return NULL;
+#endif
+}
+
+static char *
+attempt_login_auth( imap_auth_client_t *client, const char *mechs )
+{
+ assert( client );
+ assert( mechs );
+
+ if (!find_mech( mechs, "LOGIN", strlen( "LOGIN" ) ))
+ return NULL;
+ if (!client->force && !client->secure) {
+ notice(
+ "Note: not using LOGIN because connection is
not encrypted;\n"
+ " use 'AuthMechs LOGIN' explicitly to
force it.\n" );
+ return NULL;
+ }
+
+ const char *username = client->user_cb( client->user_arg );
+ if (!username)
+ return NULL;
+ const char *password = client->pass_cb( client->pass_arg );
+ if (!password)
+ return NULL;
+
+ if (!client->secure)
+ warn( "*** IMAP Warning *** Password is being sent in the
clear\n" );
+
+ // Ensure credentials are backslash-escaped
+ return xasprintf( "LOGIN \"%\\s\" \"%\\s\"", username, password );
+}
+
+char *
+imap_auth_create_cmd( imap_auth_client_t *client, const char *mechs )
+{
+ assert( client );
+ assert( mechs );
+
+ char *sasl_cmd = attempt_sasl_auth( client, mechs );
+ if (sasl_cmd)
+ return sasl_cmd;
+
+ char *login_cmd = attempt_login_auth( client, mechs );
+ if (login_cmd)
+ return login_cmd;
+
+ error( "Error: server supports no acceptable authentication
mechanism\n" );
+ return NULL;
+}
+
+int
+imap_auth_cont(
+ imap_auth_client_t *client,
+ const char *prompt,
+ char **resp,
+ size_t *len )
+{
+ assert( client );
+ assert( resp );
+ assert( len );
+
+ if (!client->cont) {
+ error(
+ "Error: IMAP server wants more steps despite "
+ "successful SASL authentication.\n" );
+ return -1;
+ }
+
+#if defined(HAVE_LIBSASL)
+ sasl_conn_t *session = client->session;
+ char *in;
+ uint in_len;
+ int bad_dec = decode_sasl_data( prompt, &in, &in_len );
+ if (bad_dec)
+ return -1;
+
+ sasl_interact_t *interact = NULL;
+ const char *out;
+ uint out_len;
+ int rc = sasl_client_step( session, in, in_len, &interact, &out,
&out_len );
+ int bad_step = process_sasl_step(
+ client, rc, in, in_len, interact, &out, &out_len );
+ free( in );
+ if (bad_step)
+ return -1;
+
+ char *enc;
+ uint enc_len;
+ if (out) {
+ int bad_enc = encode_sasl_data( out, out_len, &enc, &enc_len );
+ if (bad_enc)
+ return -1;
+ } else {
+ enc = NULL;
+ enc_len = 0;
+ }
+
+ *resp = enc;
+ *len = enc_len;
+ return 0;
+#else
+ (void)prompt;
+ (void)resp;
+ (void)len;
+
+ // LOGIN should be single-step
+ return -1;
+#endif
+}
+
+void
+imap_auth_done( imap_auth_client_t *client, int ok )
+{
+ assert( client );
+
+ if (!ok || !client->cont)
+ return;
+
+#if defined(HAVE_LIBSASL)
+ sasl_conn_t *session = client->session;
+ sasl_interact_t *interact = NULL;
+ const char *out;
+ uint out_len;
+ int rc = sasl_client_step( session, NULL, 0, &interact, &out, &out_len
);
+ int bad_step = process_sasl_step(
+ client, rc, NULL, 0, interact, &out, &out_len );
+ if (bad_step) {
+ warn(
+ "Warning: SASL reported failure despite "
+ "successful IMAP authentication. Ignoring...\n"
);
+ } else if (out_len > 0) {
+ warn(
+ "Warning: SASL wants more steps despite "
+ "successful IMAP authentication. Ignoring...\n"
);
+ }
+#endif
+}
diff --git a/src/imap_auth.h b/src/imap_auth.h
new file mode 100644
index 000000000000..2e62a0be041e
--- /dev/null
+++ b/src/imap_auth.h
@@ -0,0 +1,106 @@
+// SPDX-FileCopyrightText: 2026 Seth McDonald <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later WITH
LicenseRef-isync-GPL-exception
+
+#ifndef IMAP_AUTH_H
+#define IMAP_AUTH_H
+
+#include "common.h"
+
+/*
+ * A callback to obtain an authentication credential.
+ * It is passed a specified pointer and returns the credential as a string.
+ * Returning NULL is permitted but may cause authentication to fail.
+ * The caller does NOT retain ownership of the returned string, hence it must
+ * remain valid for the duration of the authentication process.
+ */
+typedef const char *(*imap_auth_cred_t)( void * );
+
+/*
+ * The client side state of an IMAP authentication process.
+ */
+typedef struct imap_auth_client {
+ /* Callback to obtain username and authorization identity. */
+ imap_auth_cred_t user_cb; /* Is non-NULL. */
+ void *user_arg; /* May be NULL. */
+
+ /* Callback to obtain password. */
+ imap_auth_cred_t pass_cb; /* Is non-NULL. */
+ void *pass_arg; /* May be NULL. */
+
+ const char *domain; /* E.g. "imap.server.com". Is non-NULL. */
+ int force; /* Whether to allow use of LOGIN over an insecure channel. */
+ int saslir; /* Whether the SASL-IR capability is available. */
+ int secure; /* Whether the connection is over a secure channel. */
+ int cont; /* Whether another authentication step is expected. */
+
+ /*
+ * Library-dependent session state.
+ * Currently holds:
+ * - Cyrus SASL: sasl_conn_t *
+ * - No library: NULL
+ */
+ void *session;
+} imap_auth_client_t;
+
+/*
+ * Initializes the SASL library.
+ * Must be called once prior to any other imap_auth_* functions.
+ * Returns zero on success and nonzero otherwise.
+ */
+int imap_auth_init( void );
+
+/*
+ * Deinitializes the SASL library.
+ * Should be called once following all other imap_auth_* functions.
+ */
+void imap_auth_cleanup( void );
+
+/*
+ * Initializes a new auth client with a default state.
+ */
+void imap_auth_new_client( imap_auth_client_t *client );
+
+/*
+ * Frees all resources associated with an auth client.
+ * The client structure should not be reused unless it is reinitialized.
+ */
+void imap_auth_free_client( imap_auth_client_t *client );
+
+/*
+ * Formats a list of SASL mechanisms supported by both client and server side.
+ * The list is space-delimited, including starting and ending with whitespace.
+ * Mechanisms are parsed in a case-insensitive manner.
+ */
+void imap_auth_filter_mechs(
+ char *restrict common_mechs,
+ size_t size,
+ const string_list_t *client_mechs,
+ const string_list_t *server_mechs );
+
+/*
+ * Constructs an appropriate IMAP command to commence authentication of the
+ * client via SASL or LOGIN.
+ * Returns the command on success and NULL otherwise.
+ * The caller retains ownership of the returned string.
+ */
+char *imap_auth_create_cmd( imap_auth_client_t *client, const char *mechs );
+
+/*
+ * Continues the SASL authentication process by constructing an appropriate
+ * response to the prompt received from the IMAP server.
+ * A NULL '*resp' indicates an empty response.
+ * Returns zero on success and nonzero otherwise.
+ */
+int imap_auth_cont(
+ imap_auth_client_t *client,
+ const char *prompt,
+ char **resp,
+ size_t *len );
+
+/*
+ * Takes appropriate action upon completion of the authentication process
+ * according to whether the IMAP server response was OK (i.e. success).
+ */
+void imap_auth_done( imap_auth_client_t *client, int ok );
+
+#endif
diff --git a/src/util.c b/src/util.c
index 773d765f0faa..928b82f60f06 100644
--- a/src/util.c
+++ b/src/util.c
@@ -467,6 +467,40 @@ free_string_list( string_list_t *list )
}
}
+char *
+find_string_list( const string_list_t *list, const char *str )
+{
+ assert( str );
+
+ for (const string_list_t *elem = list; elem; elem = elem->next) {
+ int found = !strcmp( elem->string, str );
+ if (found) {
+DIAG_PUSH
+DIAG_DISABLE("-Wcast-qual")
+ return (char *)elem->string;
+DIAG_POP
+ }
+ }
+ return NULL;
+}
+
+char *
+find_string_list_case( const string_list_t *list, const char *str )
+{
+ assert( str );
+
+ for (const string_list_t *elem = list; elem; elem = elem->next) {
+ int found = !strcasecmp( elem->string, str );
+ if (found) {
+DIAG_PUSH
+DIAG_DISABLE("-Wcast-qual")
+ return (char *)elem->string;
+DIAG_POP
+ }
+ }
+ return NULL;
+}
+
#ifndef HAVE_VASPRINTF
static int
vasprintf( char **strp, const char *fmt, va_list ap )
--
2.47.3
_______________________________________________
isync-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/isync-devel