From 44ba4b0bbb1a342659ca93d9ba6f475fbf9cff99 Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:24 +0530 Subject: imap-send: fix bug causing cfg->folder being set to NULL 6d1f198f34 (imap-send: fix leaking memory in `imap_server_conf`, 2024-06-07) resulted a change in static int git_imap_config which resulted in cfg->folder being incorrectly set to NULL in case imap.user, imap.pass, imap.tunnel and imap.authmethod were defined. Because of this, since Git 2.46.0, git-imap-send is not usable at all. The bug seems to have been unnoticed for a long time, likely due to better options like git-send-email. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index 27dc033c7f..37f94a37e8 100644 --- a/imap-send.c +++ b/imap-send.c @@ -1316,16 +1316,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); -- cgit v1.2.3 From ac4e02c5030c05d71b20127a7118b0a0fc3c1c64 Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:25 +0530 Subject: imap-send: fix memory leak in case auth_cram_md5 fails This patch fixes a memory leak by running free(response) in case auth_cram_md5 fails. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index 37f94a37e8..1a582c8443 100644 --- a/imap-send.c +++ b/imap-send.c @@ -905,8 +905,10 @@ 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)) + if (ret != strlen(response)) { + free(response); return error("IMAP error: sending response failed"); + } free(response); -- cgit v1.2.3 From b9e766604df2c50b2f721479bb405409db3344d1 Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:26 +0530 Subject: imap-send: gracefully fail if CRAM-MD5 authentication is requested without OpenSSL Unlike PLAIN, XOAUTH2 and OAUTHBEARER, CRAM-MD5 authentication is not supported by libcurl and requires OpenSSL. If the user tries to use CRAM-MD5 authentication without OpenSSL, the previous behaviour was to attempt to authenticate and fail with a die(error). Handle this in a better way by first checking if OpenSSL is available and then attempting to authenticate. If OpenSSL is not available, print an error message and exit gracefully. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 66 ++++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 27 deletions(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index 1a582c8443..f55399cd9e 100644 --- a/imap-send.c +++ b/imap-send.c @@ -885,18 +885,6 @@ static char *cram(const char *challenge_64, const char *user, const char *pass) return (char *)response_64; } -#else - -static char *cram(const char *challenge_64 UNUSED, - const char *user UNUSED, - const char *pass UNUSED) -{ - die("If you want to use CRAM-MD5 authenticate method, " - "you have to build git-imap-send with OpenSSL library."); -} - -#endif - static int auth_cram_md5(struct imap_store *ctx, const char *prompt) { int ret; @@ -915,6 +903,12 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt) return 0; } +#else + +#define auth_cram_md5 NULL + +#endif + static void server_fill_credential(struct imap_server_conf *srvc, struct credential *cred) { if (srvc->user && srvc->pass) @@ -934,6 +928,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; @@ -1089,23 +1115,9 @@ 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); - 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"); + if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5)) goto bail; - } } else { fprintf(stderr, "Unknown authentication method:%s\n", srvc->host); goto bail; -- cgit v1.2.3 From 103d7b12b7adeee88a95e642ffd105a25335bfef Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:27 +0530 Subject: imap-send: add support for OAuth2.0 authentication OAuth2.0 is a new way of authentication supported by various email providers these days. OAUTHBEARER and XOAUTH2 are the two most common mechanisms used for OAuth2.0. OAUTHBEARER is described in RFC5801[1] and RFC7628[2], whereas XOAUTH2 is Google's proprietary mechanism (See [3]). [1]: https://datatracker.ietf.org/doc/html/rfc5801 [2]: https://datatracker.ietf.org/doc/html/rfc7628 [3]: https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- Documentation/config/imap.adoc | 5 +- Documentation/git-imap-send.adoc | 47 +++++++++++-- imap-send.c | 144 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 183 insertions(+), 13 deletions(-) (limited to 'imap-send.c') diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index 3d28f72643..29b998d5ff 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -40,5 +40,6 @@ imap.authMethod:: Specify the authentication method for authenticating with the IMAP server. If Git was built with the NO_CURL option, or if your curl version is older than 7.34.0, or if you're running git-imap-send with the `--no-curl` - option, the only supported method is 'CRAM-MD5'. If this is not set - then 'git imap-send' uses the basic IMAP plaintext LOGIN command. + option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and + `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP + plaintext `LOGIN` command. diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 26ccf4e433..8adf0e5aac 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -102,12 +102,18 @@ Using Gmail's IMAP interface: --------- [imap] - folder = "[Gmail]/Drafts" - host = imaps://imap.gmail.com - user = user@gmail.com - port = 993 + folder = "[Gmail]/Drafts" + host = imaps://imap.gmail.com + user = user@gmail.com + port = 993 --------- +Gmail does not allow using your regular password for `git imap-send`. +If you have multi-factor authentication set up on your Gmail account, you +can generate an app-specific password for use with `git imap-send`. +Visit https://security.google.com/settings/security/apppasswords to create +it. Alternatively, use OAuth2.0 authentication as described below. + [NOTE] You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error that the "Folder doesn't exist". @@ -116,6 +122,35 @@ that the "Folder doesn't exist". If your Gmail account is set to another language than English, the name of the "Drafts" folder will be localized. +If you want to use OAuth2.0 based authentication, you can specify +`OAUTHBEARER` or `XOAUTH2` mechanism in your config. It is more secure +than using app-specific passwords, and also does not enforce the need of +having multi-factor authentication. You will have to use an OAuth2.0 +access token in place of your password when using this authentication. + +--------- +[imap] + folder = "[Gmail]/Drafts" + host = imaps://imap.gmail.com + user = user@gmail.com + port = 993 + authmethod = OAUTHBEARER +--------- + +Using Outlook's IMAP interface: + +Unlike Gmail, Outlook only supports OAuth2.0 based authentication. Also, it +supports only `XOAUTH2` as the mechanism. + +--------- +[imap] + folder = "Drafts" + host = imaps://outlook.office365.com + user = user@outlook.com + port = 993 + authmethod = XOAUTH2 +--------- + Once the commits are ready to be sent, run the following command: $ git format-patch --cover-letter -M --stdout origin/master | git imap-send @@ -124,6 +159,10 @@ Just make sure to disable line wrapping in the email client (Gmail's web interface will wrap lines no matter what, so you need to use a real IMAP client). +In case you are using OAuth2.0 authentication, it is easier to use credential +helpers to generate tokens. Credential helpers suggested in +linkgit:git-send-email[1] can be used for `git imap-send` as well. + CAUTION ------- It is still your responsibility to make sure that the email message diff --git a/imap-send.c b/imap-send.c index f55399cd9e..6c33318102 100644 --- a/imap-send.c +++ b/imap-send.c @@ -139,7 +139,9 @@ enum CAPABILITY { LITERALPLUS, NAMESPACE, STARTTLS, - AUTH_CRAM_MD5 + AUTH_CRAM_MD5, + AUTH_OAUTHBEARER, + AUTH_XOAUTH2, }; static const char *cap_list[] = { @@ -149,6 +151,8 @@ static const char *cap_list[] = { "NAMESPACE", "STARTTLS", "AUTH=CRAM-MD5", + "AUTH=OAUTHBEARER", + "AUTH=XOAUTH2", }; #define RESP_OK 0 @@ -885,6 +889,64 @@ static char *cram(const char *challenge_64, const char *user, const char *pass) return (char *)response_64; } +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 *xoauth2_base64(const char *user, const char *access_token) +{ + 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; +} + static int auth_cram_md5(struct imap_store *ctx, const char *prompt) { int ret; @@ -903,9 +965,51 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt) 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_cram_md5 NULL +#define auth_oauthbearer NULL +#define auth_xoauth2 NULL #endif @@ -1118,6 +1222,12 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c 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); goto bail; @@ -1419,7 +1529,16 @@ 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); @@ -1437,11 +1556,22 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred) curl_easy_setopt(curl, CURLOPT_PORT, 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) -- cgit v1.2.3 From ea8681e3a4ca579f03d8dfb7a425a2a9da4b3493 Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:28 +0530 Subject: imap-send: add PLAIN authentication method to OpenSSL The current implementation for PLAIN in imap-send works just fine if using curl, but if attempted to use for OpenSSL, it is treated as an invalid mechanism. The default implementation for OpenSSL is IMAP LOGIN command rather than AUTH PLAIN. Since AUTH PLAIN is still used today by many email providers in form of app passwords, lets add an implementation that can use AUTH PLAIN if specified. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- Documentation/config/imap.adoc | 4 +-- imap-send.c | 60 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) (limited to 'imap-send.c') diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index 29b998d5ff..7c8b2dcce4 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -40,6 +40,6 @@ imap.authMethod:: Specify the authentication method for authenticating with the IMAP server. If Git was built with the NO_CURL option, or if your curl version is older than 7.34.0, or if you're running git-imap-send with the `--no-curl` - option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and - `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP + option, the only supported methods are `PLAIN`, `CRAM-MD5`, `OAUTHBEARER` + and `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP plaintext `LOGIN` command. diff --git a/imap-send.c b/imap-send.c index 6c33318102..54dab627d6 100644 --- a/imap-send.c +++ b/imap-send.c @@ -139,6 +139,7 @@ enum CAPABILITY { LITERALPLUS, NAMESPACE, STARTTLS, + AUTH_PLAIN, AUTH_CRAM_MD5, AUTH_OAUTHBEARER, AUTH_XOAUTH2, @@ -150,6 +151,7 @@ static const char *cap_list[] = { "LITERAL+", "NAMESPACE", "STARTTLS", + "AUTH=PLAIN", "AUTH=CRAM-MD5", "AUTH=OAUTHBEARER", "AUTH=XOAUTH2", @@ -851,6 +853,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; @@ -947,6 +981,26 @@ static char *xoauth2_base64(const char *user, const char *access_token) return b64; } +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) { int ret; @@ -1007,6 +1061,7 @@ static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED) #else +#define auth_plain NULL #define auth_cram_md5 NULL #define auth_oauthbearer NULL #define auth_xoauth2 NULL @@ -1219,7 +1274,10 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c server_fill_credential(srvc, &cred); if (srvc->auth_method) { - if (!strcmp(srvc->auth_method, "CRAM-MD5")) { + if (!strcmp(srvc->auth_method, "PLAIN")) { + if (try_auth_method(srvc, ctx, imap, "PLAIN", AUTH_PLAIN, auth_plain)) + goto bail; + } 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")) { -- cgit v1.2.3 From 3168514e6b72668d699ae192f6492cddb4eebb4a Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:29 +0530 Subject: imap-send: enable specifying the folder using the command line Some users may very often want to imap-send messages to a folder other than the default set in the config. Add a command line argument for the same. While at it, fix minor mark-up inconsistencies in the existing documentation text. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- Documentation/config/imap.adoc | 6 ++++-- Documentation/git-imap-send.adoc | 15 +++++++++++---- imap-send.c | 9 ++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) (limited to 'imap-send.c') diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index 7c8b2dcce4..4682a6bd03 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -1,7 +1,9 @@ imap.folder:: The folder to drop the mails into, which is typically the Drafts - folder. For example: "INBOX.Drafts", "INBOX/Drafts" or - "[Gmail]/Drafts". Required. + folder. For example: `INBOX.Drafts`, `INBOX/Drafts` or + `[Gmail]/Drafts`. The IMAP folder to interact with MUST be specified; + the value of this configuration variable is used as the fallback + default value when the `--folder` option is not given. imap.tunnel:: Command used to set up a tunnel to the IMAP server through which diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 8adf0e5aac..4a0487b66e 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -9,21 +9,23 @@ git-imap-send - Send a collection of patches from stdin to an IMAP folder SYNOPSIS -------- [verse] -'git imap-send' [-v] [-q] [--[no-]curl] +'git imap-send' [-v] [-q] [--[no-]curl] [(--folder|-f) ] DESCRIPTION ----------- -This command uploads a mailbox generated with 'git format-patch' +This command uploads a mailbox generated with `git format-patch` into an IMAP drafts folder. This allows patches to be sent as other email is when using mail clients that cannot read mailbox files directly. The command also works with any general mailbox -in which emails have the fields "From", "Date", and "Subject" in +in which emails have the fields `From`, `Date`, and `Subject` in that order. Typical usage is something like: -git format-patch --signoff --stdout --attach origin | git imap-send +------ +$ git format-patch --signoff --stdout --attach origin | git imap-send +------ OPTIONS @@ -37,6 +39,11 @@ OPTIONS --quiet:: Be quiet. +-f :: +--folder=:: + Specify the folder in which the emails have to saved. + For example: `--folder=[Gmail]/Drafts` or `-f INBOX/Drafts`. + --curl:: Use libcurl to communicate with the IMAP server, unless tunneling into it. Ignored if Git was built without the USE_CURL_FOR_IMAP_SEND diff --git a/imap-send.c b/imap-send.c index 54dab627d6..ccf7195fd7 100644 --- a/imap-send.c +++ b/imap-send.c @@ -46,12 +46,14 @@ static int verbosity; 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] < ", NULL }; +static const char * const imap_send_usage[] = { "git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) ] < ", 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_END() }; @@ -1722,6 +1724,11 @@ int cmd_main(int argc, const char **argv) 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); -- cgit v1.2.3 From 067a91b03f3216acf1248c0e540600f2af4038ae Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:30 +0530 Subject: imap-send: add ability to list the available folders Various IMAP servers have different ways to name common folders. For example, the folder where all deleted messages are stored is often named "[Gmail]/Trash" on Gmail servers, and "Deleted" on Outlook. Similarly, the Drafts folder is simply named "Drafts" on Outlook, but on Gmail it is named "[Gmail]/Drafts". This commit adds a `--list` command to the `imap-send` tool that lists the available folders on the IMAP server, allowing users to see which folders are available and how they are named. A sample output looks like this when run against a Gmail server: Fetching the list of available folders... * LIST (\HasNoChildren) "/" "INBOX" * LIST (\HasChildren \Noselect) "/" "[Gmail]" * LIST (\All \HasNoChildren) "/" "[Gmail]/All Mail" * LIST (\Drafts \HasNoChildren) "/" "[Gmail]/Drafts" * LIST (\HasNoChildren \Important) "/" "[Gmail]/Important" * LIST (\HasNoChildren \Sent) "/" "[Gmail]/Sent Mail" * LIST (\HasNoChildren \Junk) "/" "[Gmail]/Spam" * LIST (\Flagged \HasNoChildren) "/" "[Gmail]/Starred" * LIST (\HasNoChildren \Trash) "/" "[Gmail]/Trash" For OpenSSL, this is achived by running the 'IMAP LIST' command and parsing the response. This command is specified in RFC6154: https://datatracker.ietf.org/doc/html/rfc6154#section-5.1 For libcurl, the example code published in the libcurl documentation is used to implement this functionality: https://curl.se/libcurl/c/imap-list.html Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- Documentation/git-imap-send.adoc | 6 ++- imap-send.c | 98 +++++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 17 deletions(-) (limited to 'imap-send.c') diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 4a0487b66e..17147f93c3 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -10,6 +10,7 @@ SYNOPSIS -------- [verse] 'git imap-send' [-v] [-q] [--[no-]curl] [(--folder|-f) ] +'git imap-send' --list DESCRIPTION @@ -54,6 +55,8 @@ OPTIONS using libcurl. Ignored if Git was built with the NO_OPENSSL option set. +--list:: + Run the IMAP LIST command to output a list of all the folders present. CONFIGURATION ------------- @@ -123,7 +126,8 @@ it. Alternatively, use OAuth2.0 authentication as described below. [NOTE] You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error -that the "Folder doesn't exist". +that the "Folder doesn't exist". You can also run `git imap-send --list` to get a +list of available folders. [NOTE] If your Gmail account is set to another language than English, the name of the "Drafts" diff --git a/imap-send.c b/imap-send.c index ccf7195fd7..c88d842819 100644 --- a/imap-send.c +++ b/imap-send.c @@ -45,15 +45,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] [(--folder|-f) ] < ", NULL }; +static char const * const imap_send_usage[] = { + N_("git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) ] < "), + "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() }; @@ -429,7 +435,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; } @@ -1572,6 +1578,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) { @@ -1605,11 +1631,13 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred) 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); @@ -1640,10 +1668,6 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred) 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); - if (0 < verbosity || getenv("GIT_CURL_VERBOSE")) http_trace_curl_no_data(); setup_curl_trace(curl); @@ -1662,6 +1686,10 @@ 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" : ""); @@ -1707,6 +1735,31 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server, 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(the_repository, &cred); + } + + credential_clear(&cred); + + return res != CURLE_OK; +} #endif int cmd_main(int argc, const char **argv) @@ -1747,11 +1800,6 @@ 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"); @@ -1761,6 +1809,24 @@ int cmd_main(int argc, const char **argv) 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) { + fprintf(stderr, "no imap store specified\n"); + ret = 1; + goto out; + } + /* read the messages */ if (strbuf_read(&all_msgs, 0, 0) < 0) { error_errno(_("could not read from stdin")); -- cgit v1.2.3 From 2dacd35731d9d9947913eac991599ff000468cf3 Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:31 +0530 Subject: imap-send: display port alongwith host when git credential is invoked When requesting for passsword, git credential helper used to display only the host name. For example: Password for 'imaps://gargaditya08%40live.com@outlook.office365.com': Now, it will display the port along with the host name: Password for 'imaps://gargaditya08%40live.com@outlook.office365.com:993': This has been done to make credential helpers more specific for ports. Also, this behaviour will also mimic git send-email, which displays the port along with the host name when requesting for a password. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index c88d842819..1894a18f90 100644 --- a/imap-send.c +++ b/imap-send.c @@ -1082,7 +1082,7 @@ static void server_fill_credential(struct imap_server_conf *srvc, struct credent 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); -- cgit v1.2.3 From bf22c370b966cfccf43539f6e6698dbfc79eceff Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:32 +0530 Subject: imap-send: display the destination mailbox when sending a message Whenever we sent a message using the `imap-send` command, it would display a log showing the number of messages which are to be sent. For example: sending 1 message 100% (1/1) done This had been made more informative by adding the name of the destination folder as well: Sending 1 message to Drafts folder... 100% (1/1) done Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index 1894a18f90..2a445edcda 100644 --- a/imap-send.c +++ b/imap-send.c @@ -1556,7 +1556,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; @@ -1692,7 +1693,8 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server, 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; -- cgit v1.2.3 From 5ec81b33b04124c6f52e165e898e99902c5f68af Mon Sep 17 00:00:00 2001 From: Aditya Garg Date: Fri, 20 Jun 2025 12:10:33 +0530 Subject: imap-send: fix minor mistakes in the logs Some minor mistakes have been found in the logs. Most of them include error messages starting with a capital letter, and ending with a period. Abbreviations like "IMAP" and "OK" should also be in uppercase. Another mistake was that the error message showing unknown authentication mechanism used was displaying the host rather than the mechanism in the logs. Fix them. Signed-off-by: Aditya Garg Signed-off-by: Junio C Hamano --- imap-send.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'imap-send.c') diff --git a/imap-send.c b/imap-send.c index 2a445edcda..8f4801dc07 100644 --- a/imap-send.c +++ b/imap-send.c @@ -211,7 +211,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; } @@ -1019,7 +1019,7 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt) ret = socket_write(&ctx->imap->buf.sock, response, strlen(response)); if (ret != strlen(response)) { free(response); - return error("IMAP error: sending response failed"); + return error("IMAP error: sending CRAM-MD5 response failed"); } free(response); @@ -1159,7 +1159,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; @@ -1178,7 +1178,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]; @@ -1216,7 +1216,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]); @@ -1230,7 +1230,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; } @@ -1242,7 +1242,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 */ @@ -1295,12 +1295,12 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c 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; } @@ -1804,7 +1804,7 @@ int cmd_main(int argc, const char **argv) if (!server.host) { if (!server.tunnel) { - fprintf(stderr, "no imap host specified\n"); + fprintf(stderr, "no IMAP host specified\n"); ret = 1; goto out; } @@ -1824,7 +1824,7 @@ int cmd_main(int argc, const char **argv) } if (!server.folder) { - fprintf(stderr, "no imap store specified\n"); + fprintf(stderr, "no IMAP store specified\n"); ret = 1; goto out; } @@ -1844,7 +1844,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; } -- cgit v1.2.3