diff options
Diffstat (limited to 'imap-send.c')
| -rw-r--r-- | imap-send.c | 431 |
1 files changed, 358 insertions, 73 deletions
diff --git a/imap-send.c b/imap-send.c index 68ab1aea83..254ec83ab7 100644 --- a/imap-send.c +++ b/imap-send.c @@ -25,8 +25,10 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" +#include "advice.h" #include "config.h" #include "credential.h" +#include "environment.h" #include "gettext.h" #include "run-command.h" #include "parse-options.h" @@ -45,13 +47,21 @@ #endif static int verbosity; +static int list_folders; static int use_curl = USE_CURL_DEFAULT; +static char *opt_folder; -static const char * const imap_send_usage[] = { "git imap-send [-v] [-q] [--[no-]curl] < <mbox>", NULL }; +static char const * const imap_send_usage[] = { + N_("git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) <folder>] < <mbox>"), + "git imap-send --list", + NULL +}; static struct option imap_send_options[] = { OPT__VERBOSITY(&verbosity), OPT_BOOL(0, "curl", &use_curl, "use libcurl to communicate with the IMAP server"), + OPT_STRING('f', "folder", &opt_folder, "folder", "specify the IMAP folder"), + OPT_BOOL(0, "list", &list_folders, "list all folders on the IMAP server"), OPT_END() }; @@ -139,7 +149,10 @@ enum CAPABILITY { LITERALPLUS, NAMESPACE, STARTTLS, - AUTH_CRAM_MD5 + AUTH_PLAIN, + AUTH_CRAM_MD5, + AUTH_OAUTHBEARER, + AUTH_XOAUTH2, }; static const char *cap_list[] = { @@ -148,7 +161,10 @@ static const char *cap_list[] = { "LITERAL+", "NAMESPACE", "STARTTLS", + "AUTH=PLAIN", "AUTH=CRAM-MD5", + "AUTH=OAUTHBEARER", + "AUTH=XOAUTH2", }; #define RESP_OK 0 @@ -197,7 +213,7 @@ static int ssl_socket_connect(struct imap_socket *sock UNUSED, const struct imap_server_conf *cfg UNUSED, int use_tls_only UNUSED) { - fprintf(stderr, "SSL requested but SSL support not compiled in\n"); + fprintf(stderr, "SSL requested, but SSL support is not compiled in\n"); return -1; } @@ -324,6 +340,8 @@ static int ssl_socket_connect(struct imap_socket *sock, cert = SSL_get_peer_certificate(sock->ssl); if (!cert) return error("unable to get peer certificate."); + if (SSL_get_verify_result(sock->ssl) != X509_V_OK) + return error("unable to verify peer certificate"); if (verify_hostname(cert, cfg->host) < 0) return -1; } @@ -419,7 +437,7 @@ static int buffer_gets(struct imap_buffer *b, char **s) if (b->buf[b->offset + 1] == '\n') { b->buf[b->offset] = 0; /* terminate the string */ b->offset += 2; /* next line */ - if (0 < verbosity) + if ((0 < verbosity) || (list_folders && strstr(*s, "* LIST"))) puts(*s); return 0; } @@ -845,6 +863,38 @@ static char hexchar(unsigned int b) } #define ENCODED_SIZE(n) (4 * DIV_ROUND_UP((n), 3)) +static char *plain_base64(const char *user, const char *pass) +{ + struct strbuf raw = STRBUF_INIT; + int b64_len; + char *b64; + + /* + * Compose the PLAIN string + * + * The username and password are combined to one string and base64 encoded. + * "\0user\0pass" + * + * The method has been described in RFC4616. + * + * https://datatracker.ietf.org/doc/html/rfc4616 + */ + strbuf_addch(&raw, '\0'); + strbuf_addstr(&raw, user); + strbuf_addch(&raw, '\0'); + strbuf_addstr(&raw, pass); + + b64 = xmallocz(ENCODED_SIZE(raw.len)); + b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw.buf, raw.len); + strbuf_release(&raw); + + if (b64_len < 0) { + free(b64); + return NULL; + } + return b64; +} + static char *cram(const char *challenge_64, const char *user, const char *pass) { int i, resp_len, encoded_len, decoded_len; @@ -883,17 +933,83 @@ static char *cram(const char *challenge_64, const char *user, const char *pass) return (char *)response_64; } -#else +static char *oauthbearer_base64(const char *user, const char *access_token) +{ + int b64_len; + char *raw, *b64; + + /* + * Compose the OAUTHBEARER string + * + * "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A + * + * The first part `n,a=" {User} ",` is the gs2 header described in RFC5801. + * * gs2-cb-flag `n` -> client does not support CB + * * gs2-authzid `a=" {User} "` + * + * The second part are key value pairs containing host, port and auth as + * described in RFC7628. + * + * https://datatracker.ietf.org/doc/html/rfc5801 + * https://datatracker.ietf.org/doc/html/rfc7628 + */ + raw = xstrfmt("n,a=%s,\001auth=Bearer %s\001\001", user, access_token); + + /* Base64 encode */ + b64 = xmallocz(ENCODED_SIZE(strlen(raw))); + b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw)); + free(raw); + + if (b64_len < 0) { + free(b64); + return NULL; + } + return b64; +} -static char *cram(const char *challenge_64 UNUSED, - const char *user UNUSED, - const char *pass UNUSED) +static char *xoauth2_base64(const char *user, const char *access_token) { - die("If you want to use CRAM-MD5 authenticate method, " - "you have to build git-imap-send with OpenSSL library."); + int b64_len; + char *raw, *b64; + + /* + * Compose the XOAUTH2 string + * "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A" + * https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response + */ + raw = xstrfmt("user=%s\001auth=Bearer %s\001\001", user, access_token); + + /* Base64 encode */ + b64 = xmallocz(ENCODED_SIZE(strlen(raw))); + b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw)); + free(raw); + + if (b64_len < 0) { + free(b64); + return NULL; + } + return b64; } -#endif +static int auth_plain(struct imap_store *ctx, const char *prompt UNUSED) +{ + int ret; + char *b64; + + b64 = plain_base64(ctx->cfg->user, ctx->cfg->pass); + if (!b64) + return error("PLAIN: base64 encoding failed"); + + /* Send the base64-encoded response */ + ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64)); + if (ret != (int)strlen(b64)) { + free(b64); + return error("IMAP error: sending PLAIN response failed"); + } + + free(b64); + return 0; +} static int auth_cram_md5(struct imap_store *ctx, const char *prompt) { @@ -903,26 +1019,77 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt) response = cram(prompt, ctx->cfg->user, ctx->cfg->pass); ret = socket_write(&ctx->imap->buf.sock, response, strlen(response)); - if (ret != strlen(response)) - return error("IMAP error: sending response failed"); + if (ret != strlen(response)) { + free(response); + return error("IMAP error: sending CRAM-MD5 response failed"); + } free(response); return 0; } +static int auth_oauthbearer(struct imap_store *ctx, const char *prompt UNUSED) +{ + int ret; + char *b64; + + b64 = oauthbearer_base64(ctx->cfg->user, ctx->cfg->pass); + if (!b64) + return error("OAUTHBEARER: base64 encoding failed"); + + /* Send the base64-encoded response */ + ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64)); + if (ret != (int)strlen(b64)) { + free(b64); + return error("IMAP error: sending OAUTHBEARER response failed"); + } + + free(b64); + return 0; +} + +static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED) +{ + int ret; + char *b64; + + b64 = xoauth2_base64(ctx->cfg->user, ctx->cfg->pass); + if (!b64) + return error("XOAUTH2: base64 encoding failed"); + + /* Send the base64-encoded response */ + ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64)); + if (ret != (int)strlen(b64)) { + free(b64); + return error("IMAP error: sending XOAUTH2 response failed"); + } + + free(b64); + return 0; +} + +#else + +#define auth_plain NULL +#define auth_cram_md5 NULL +#define auth_oauthbearer NULL +#define auth_xoauth2 NULL + +#endif + static void server_fill_credential(struct imap_server_conf *srvc, struct credential *cred) { if (srvc->user && srvc->pass) return; cred->protocol = xstrdup(srvc->use_ssl ? "imaps" : "imap"); - cred->host = xstrdup(srvc->host); + cred->host = xstrfmt("%s:%d", srvc->host, srvc->port); cred->username = xstrdup_or_null(srvc->user); cred->password = xstrdup_or_null(srvc->pass); - credential_fill(cred, 1); + credential_fill(the_repository, cred, 1); if (!srvc->user) srvc->user = xstrdup(cred->username); @@ -930,6 +1097,38 @@ static void server_fill_credential(struct imap_server_conf *srvc, struct credent srvc->pass = xstrdup(cred->password); } +static int try_auth_method(struct imap_server_conf *srvc, + struct imap_store *ctx, + struct imap *imap, + const char *auth_method, + enum CAPABILITY cap, + int (*fn)(struct imap_store *, const char *)) +{ + struct imap_cmd_cb cb = {0}; + + if (!CAP(cap)) { + fprintf(stderr, "You specified " + "%s as authentication method, " + "but %s doesn't support it.\n", + auth_method, srvc->host); + return -1; + } + cb.cont = fn; + + if (NOT_CONSTANT(!cb.cont)) { + fprintf(stderr, "If you want to use %s authentication mechanism, " + "you have to build git-imap-send with OpenSSL library.", + auth_method); + return -1; + } + if (imap_exec(ctx, &cb, "AUTHENTICATE %s", auth_method) != RESP_OK) { + fprintf(stderr, "IMAP error: AUTHENTICATE %s failed\n", + auth_method); + return -1; + } + return 0; +} + static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const char *folder) { struct credential cred = CREDENTIAL_INIT; @@ -962,7 +1161,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c imap->buf.sock.fd[0] = tunnel.out; imap->buf.sock.fd[1] = tunnel.in; - imap_info("ok\n"); + imap_info("OK\n"); } else { #ifndef NO_IPV6 struct addrinfo hints, *ai0, *ai; @@ -981,7 +1180,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai)); goto bail; } - imap_info("ok\n"); + imap_info("OK\n"); for (ai0 = ai; ai; ai = ai->ai_next) { char addr[NI_MAXHOST]; @@ -1019,7 +1218,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c perror("gethostbyname"); goto bail; } - imap_info("ok\n"); + imap_info("OK\n"); addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); @@ -1033,7 +1232,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c } #endif if (s < 0) { - fputs("Error: unable to connect to server.\n", stderr); + fputs("error: unable to connect to server\n", stderr); goto bail; } @@ -1045,7 +1244,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c close(s); goto bail; } - imap_info("ok\n"); + imap_info("OK\n"); } /* read the greeting string */ @@ -1085,30 +1284,25 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c server_fill_credential(srvc, &cred); if (srvc->auth_method) { - struct imap_cmd_cb cb; - - if (!strcmp(srvc->auth_method, "CRAM-MD5")) { - if (!CAP(AUTH_CRAM_MD5)) { - fprintf(stderr, "You specified " - "CRAM-MD5 as authentication method, " - "but %s doesn't support it.\n", srvc->host); + if (!strcmp(srvc->auth_method, "PLAIN")) { + if (try_auth_method(srvc, ctx, imap, "PLAIN", AUTH_PLAIN, auth_plain)) goto bail; - } - /* CRAM-MD5 */ - - memset(&cb, 0, sizeof(cb)); - cb.cont = auth_cram_md5; - if (imap_exec(ctx, &cb, "AUTHENTICATE CRAM-MD5") != RESP_OK) { - fprintf(stderr, "IMAP error: AUTHENTICATE CRAM-MD5 failed\n"); + } else if (!strcmp(srvc->auth_method, "CRAM-MD5")) { + if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5)) + goto bail; + } else if (!strcmp(srvc->auth_method, "OAUTHBEARER")) { + if (try_auth_method(srvc, ctx, imap, "OAUTHBEARER", AUTH_OAUTHBEARER, auth_oauthbearer)) + goto bail; + } else if (!strcmp(srvc->auth_method, "XOAUTH2")) { + if (try_auth_method(srvc, ctx, imap, "XOAUTH2", AUTH_XOAUTH2, auth_xoauth2)) goto bail; - } } else { - fprintf(stderr, "Unknown authentication method:%s\n", srvc->host); + fprintf(stderr, "unknown authentication mechanism: %s\n", srvc->auth_method); goto bail; } } else { if (CAP(NOLOGIN)) { - fprintf(stderr, "Skipping account %s@%s, server forbids LOGIN\n", + fprintf(stderr, "skipping account %s@%s, server forbids LOGIN\n", srvc->user, srvc->host); goto bail; } @@ -1123,7 +1317,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c } /* !preauth */ if (cred.username) - credential_approve(&cred); + credential_approve(the_repository, &cred); credential_clear(&cred); /* check the target mailbox exists */ @@ -1150,7 +1344,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c bail: if (cred.username) - credential_reject(&cred); + credential_reject(the_repository, &cred); credential_clear(&cred); out: @@ -1314,16 +1508,16 @@ static int git_imap_config(const char *var, const char *val, FREE_AND_NULL(cfg->folder); return git_config_string(&cfg->folder, var, val); } else if (!strcmp("imap.user", var)) { - FREE_AND_NULL(cfg->folder); + FREE_AND_NULL(cfg->user); return git_config_string(&cfg->user, var, val); } else if (!strcmp("imap.pass", var)) { - FREE_AND_NULL(cfg->folder); + FREE_AND_NULL(cfg->pass); return git_config_string(&cfg->pass, var, val); } else if (!strcmp("imap.tunnel", var)) { - FREE_AND_NULL(cfg->folder); + FREE_AND_NULL(cfg->tunnel); return git_config_string(&cfg->tunnel, var, val); } else if (!strcmp("imap.authmethod", var)) { - FREE_AND_NULL(cfg->folder); + FREE_AND_NULL(cfg->auth_method); return git_config_string(&cfg->auth_method, var, val); } else if (!strcmp("imap.port", var)) { cfg->port = git_config_int(var, val, ctx->kvi); @@ -1364,7 +1558,8 @@ static int append_msgs_to_imap(struct imap_server_conf *server, } ctx->name = server->folder; - fprintf(stderr, "sending %d message%s\n", total, (total != 1) ? "s" : ""); + fprintf(stderr, "Sending %d message%s to %s folder...\n", + total, (total != 1) ? "s" : "", server->folder); while (1) { unsigned percent = n * 100 / total; @@ -1386,6 +1581,26 @@ static int append_msgs_to_imap(struct imap_server_conf *server, return 0; } +static int list_imap_folders(struct imap_server_conf *server) +{ + struct imap_store *ctx = imap_open_store(server, "INBOX"); + if (!ctx) { + fprintf(stderr, "failed to connect to IMAP server\n"); + return 1; + } + + fprintf(stderr, "Fetching the list of available folders...\n"); + /* Issue the LIST command and print the results */ + if (imap_exec(ctx, NULL, "LIST \"\" \"*\"") != RESP_OK) { + fprintf(stderr, "failed to list folders\n"); + imap_close_store(ctx); + return 1; + } + + imap_close_store(ctx); + return 0; +} + #ifdef USE_CURL_FOR_IMAP_SEND static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred) { @@ -1403,40 +1618,58 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred) server_fill_credential(srvc, cred); curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user); - curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass); + + /* + * Use CURLOPT_PASSWORD irrespective of whether there is + * an auth method specified or not, unless it's OAuth2.0, + * where we use CURLOPT_XOAUTH2_BEARER. + */ + if (!srvc->auth_method || + (strcmp(srvc->auth_method, "XOAUTH2") && + strcmp(srvc->auth_method, "OAUTHBEARER"))) + curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass); strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://"); strbuf_addstr(&path, srvc->host); if (!path.len || path.buf[path.len - 1] != '/') strbuf_addch(&path, '/'); - uri_encoded_folder = curl_easy_escape(curl, srvc->folder, 0); - if (!uri_encoded_folder) - die("failed to encode server folder"); - strbuf_addstr(&path, uri_encoded_folder); - curl_free(uri_encoded_folder); + if (!list_folders) { + uri_encoded_folder = curl_easy_escape(curl, srvc->folder, 0); + if (!uri_encoded_folder) + die("failed to encode server folder"); + strbuf_addstr(&path, uri_encoded_folder); + curl_free(uri_encoded_folder); + } curl_easy_setopt(curl, CURLOPT_URL, path.buf); strbuf_release(&path); - curl_easy_setopt(curl, CURLOPT_PORT, srvc->port); + curl_easy_setopt(curl, CURLOPT_PORT, (long)srvc->port); if (srvc->auth_method) { - struct strbuf auth = STRBUF_INIT; - strbuf_addstr(&auth, "AUTH="); - strbuf_addstr(&auth, srvc->auth_method); - curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf); - strbuf_release(&auth); + if (!strcmp(srvc->auth_method, "XOAUTH2") || + !strcmp(srvc->auth_method, "OAUTHBEARER")) { + + /* + * While CURLOPT_XOAUTH2_BEARER looks as if it only supports XOAUTH2, + * upon debugging, it has been found that it is capable of detecting + * the best option out of OAUTHBEARER and XOAUTH2. + */ + curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, srvc->pass); + } else { + struct strbuf auth = STRBUF_INIT; + strbuf_addstr(&auth, "AUTH="); + strbuf_addstr(&auth, srvc->auth_method); + curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf); + strbuf_release(&auth); + } } if (!srvc->use_ssl) curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, srvc->ssl_verify); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, srvc->ssl_verify); - - curl_easy_setopt(curl, CURLOPT_READFUNCTION, fread_buffer); - - curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, (long)srvc->ssl_verify); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, (long)srvc->ssl_verify); if (0 < verbosity || getenv("GIT_CURL_VERBOSE")) http_trace_curl_no_data(); @@ -1456,9 +1689,14 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server, struct credential cred = CREDENTIAL_INIT; curl = setup_curl(server, &cred); + + curl_easy_setopt(curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_READDATA, &msgbuf); - fprintf(stderr, "sending %d message%s\n", total, (total != 1) ? "s" : ""); + fprintf(stderr, "Sending %d message%s to %s folder...\n", + total, (total != 1) ? "s" : "", server->folder); while (1) { unsigned percent = n * 100 / total; int prev_len; @@ -1492,9 +1730,34 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server, if (cred.username) { if (res == CURLE_OK) - credential_approve(&cred); + credential_approve(the_repository, &cred); + else if (res == CURLE_LOGIN_DENIED) + credential_reject(the_repository, &cred); + } + + credential_clear(&cred); + + return res != CURLE_OK; +} + +static int curl_list_imap_folders(struct imap_server_conf *server) +{ + CURL *curl; + CURLcode res = CURLE_OK; + struct credential cred = CREDENTIAL_INIT; + + fprintf(stderr, "Fetching the list of available folders...\n"); + curl = setup_curl(server, &cred); + res = curl_easy_perform(curl); + + curl_easy_cleanup(curl); + curl_global_cleanup(); + + if (cred.username) { + if (res == CURLE_OK) + credential_approve(the_repository, &cred); else if (res == CURLE_LOGIN_DENIED) - credential_reject(&cred); + credential_reject(the_repository, &cred); } credential_clear(&cred); @@ -1514,10 +1777,15 @@ int cmd_main(int argc, const char **argv) int ret; setup_git_directory_gently(&nongit_ok); - git_config(git_imap_config, &server); + repo_config(the_repository, git_imap_config, &server); argc = parse_options(argc, (const char **)argv, "", imap_send_options, imap_send_usage, 0); + if (opt_folder) { + free(server.folder); + server.folder = xstrdup(opt_folder); + } + if (argc) usage_with_options(imap_send_usage, imap_send_options); @@ -1536,20 +1804,37 @@ int cmd_main(int argc, const char **argv) if (!server.port) server.port = server.use_ssl ? 993 : 143; - if (!server.folder) { - fprintf(stderr, "no imap store specified\n"); - ret = 1; - goto out; - } if (!server.host) { if (!server.tunnel) { - fprintf(stderr, "no imap host specified\n"); + error(_("no IMAP host specified")); + advise(_("set the IMAP host with 'git config imap.host <host>'.\n" + "(e.g., 'git config imap.host imaps://imap.example.com')")); ret = 1; goto out; } server.host = xstrdup("tunnel"); } + if (list_folders) { + if (server.tunnel) + ret = list_imap_folders(&server); +#ifdef USE_CURL_FOR_IMAP_SEND + else if (use_curl) + ret = curl_list_imap_folders(&server); +#endif + else + ret = list_imap_folders(&server); + goto out; + } + + if (!server.folder) { + error(_("no IMAP folder specified")); + advise(_("set the target folder with 'git config imap.folder <folder>'.\n" + "(e.g., 'git config imap.folder Drafts')")); + ret = 1; + goto out; + } + /* read the messages */ if (strbuf_read(&all_msgs, 0, 0) < 0) { error_errno(_("could not read from stdin")); @@ -1565,7 +1850,7 @@ int cmd_main(int argc, const char **argv) total = count_messages(&all_msgs); if (!total) { - fprintf(stderr, "no messages to send\n"); + fprintf(stderr, "no messages found to send\n"); ret = 1; goto out; } |
