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 <gargaditya08@live.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Aditya Garg committed Jun 20, 2025 at 12:10 UTC 103d7b12b7adeee88a95e642ffd105a25335bfef
3 files changed +183 -13
Documentation/config/imap.adoc
+3 -2
@@ -40,5 +40,6 @@ imap.authMethod::
40 Specify the authentication method for authenticating with the IMAP server.
41 If Git was built with the NO_CURL option, or if your curl version is older
42 than 7.34.0, or if you're running git-imap-send with the `--no-curl`
43 - option, the only supported method is 'CRAM-MD5'. If this is not set
44 - then 'git imap-send' uses the basic IMAP plaintext LOGIN command.
43 + option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and
44 + `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP
45 + plaintext `LOGIN` command.
Documentation/git-imap-send.adoc
+43 -4
@@ -102,12 +102,18 @@ Using Gmail's IMAP interface:
102
103 ---------
104 [imap]
105 - folder = "[Gmail]/Drafts"
106 - host = imaps://imap.gmail.com
107 - user = user@gmail.com
108 - port = 993
105 + folder = "[Gmail]/Drafts"
106 + host = imaps://imap.gmail.com
107 + user = user@gmail.com
108 + port = 993
109 ---------
110
111 +Gmail does not allow using your regular password for `git imap-send`.
112 +If you have multi-factor authentication set up on your Gmail account, you
113 +can generate an app-specific password for use with `git imap-send`.
114 +Visit https://security.google.com/settings/security/apppasswords to create
115 +it. Alternatively, use OAuth2.0 authentication as described below.
116 +
117 [NOTE]
118 You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
119 that the "Folder doesn't exist".
@@ -116,6 +122,35 @@ that the "Folder doesn't exist".
122 If your Gmail account is set to another language than English, the name of the "Drafts"
123 folder will be localized.
124
125 +If you want to use OAuth2.0 based authentication, you can specify
126 +`OAUTHBEARER` or `XOAUTH2` mechanism in your config. It is more secure
127 +than using app-specific passwords, and also does not enforce the need of
128 +having multi-factor authentication. You will have to use an OAuth2.0
129 +access token in place of your password when using this authentication.
130 +
131 +---------
132 +[imap]
133 + folder = "[Gmail]/Drafts"
134 + host = imaps://imap.gmail.com
135 + user = user@gmail.com
136 + port = 993
137 + authmethod = OAUTHBEARER
138 +---------
139 +
140 +Using Outlook's IMAP interface:
141 +
142 +Unlike Gmail, Outlook only supports OAuth2.0 based authentication. Also, it
143 +supports only `XOAUTH2` as the mechanism.
144 +
145 +---------
146 +[imap]
147 + folder = "Drafts"
148 + host = imaps://outlook.office365.com
149 + user = user@outlook.com
150 + port = 993
151 + authmethod = XOAUTH2
152 +---------
153 +
154 Once the commits are ready to be sent, run the following command:
155
156 $ 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
159 interface will wrap lines no matter what, so you need to use a real
160 IMAP client).
161
162 +In case you are using OAuth2.0 authentication, it is easier to use credential
163 +helpers to generate tokens. Credential helpers suggested in
164 +linkgit:git-send-email[1] can be used for `git imap-send` as well.
165 +
166 CAUTION
167 -------
168 It is still your responsibility to make sure that the email message
imap-send.c
+137 -7
@@ -139,7 +139,9 @@ enum CAPABILITY {
139 LITERALPLUS,
140 NAMESPACE,
141 STARTTLS,
142 - AUTH_CRAM_MD5
142 + AUTH_CRAM_MD5,
143 + AUTH_OAUTHBEARER,
144 + AUTH_XOAUTH2,
145 };
146
147 static const char *cap_list[] = {
@@ -149,6 +151,8 @@ static const char *cap_list[] = {
151 "NAMESPACE",
152 "STARTTLS",
153 "AUTH=CRAM-MD5",
154 + "AUTH=OAUTHBEARER",
155 + "AUTH=XOAUTH2",
156 };
157
158 #define RESP_OK 0
@@ -885,6 +889,64 @@ static char *cram(const char *challenge_64, const char *user, const char *pass)
889 return (char *)response_64;
890 }
891
892 +static char *oauthbearer_base64(const char *user, const char *access_token)
893 +{
894 + int b64_len;
895 + char *raw, *b64;
896 +
897 + /*
898 + * Compose the OAUTHBEARER string
899 + *
900 + * "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A
901 + *
902 + * The first part `n,a=" {User} ",` is the gs2 header described in RFC5801.
903 + * * gs2-cb-flag `n` -> client does not support CB
904 + * * gs2-authzid `a=" {User} "`
905 + *
906 + * The second part are key value pairs containing host, port and auth as
907 + * described in RFC7628.
908 + *
909 + * https://datatracker.ietf.org/doc/html/rfc5801
910 + * https://datatracker.ietf.org/doc/html/rfc7628
911 + */
912 + raw = xstrfmt("n,a=%s,\001auth=Bearer %s\001\001", user, access_token);
913 +
914 + /* Base64 encode */
915 + b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
916 + b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
917 + free(raw);
918 +
919 + if (b64_len < 0) {
920 + free(b64);
921 + return NULL;
922 + }
923 + return b64;
924 +}
925 +
926 +static char *xoauth2_base64(const char *user, const char *access_token)
927 +{
928 + int b64_len;
929 + char *raw, *b64;
930 +
931 + /*
932 + * Compose the XOAUTH2 string
933 + * "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A"
934 + * https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response
935 + */
936 + raw = xstrfmt("user=%s\001auth=Bearer %s\001\001", user, access_token);
937 +
938 + /* Base64 encode */
939 + b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
940 + b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
941 + free(raw);
942 +
943 + if (b64_len < 0) {
944 + free(b64);
945 + return NULL;
946 + }
947 + return b64;
948 +}
949 +
950 static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
951 {
952 int ret;
@@ -903,9 +965,51 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
965 return 0;
966 }
967
968 +static int auth_oauthbearer(struct imap_store *ctx, const char *prompt UNUSED)
969 +{
970 + int ret;
971 + char *b64;
972 +
973 + b64 = oauthbearer_base64(ctx->cfg->user, ctx->cfg->pass);
974 + if (!b64)
975 + return error("OAUTHBEARER: base64 encoding failed");
976 +
977 + /* Send the base64-encoded response */
978 + ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
979 + if (ret != (int)strlen(b64)) {
980 + free(b64);
981 + return error("IMAP error: sending OAUTHBEARER response failed");
982 + }
983 +
984 + free(b64);
985 + return 0;
986 +}
987 +
988 +static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED)
989 +{
990 + int ret;
991 + char *b64;
992 +
993 + b64 = xoauth2_base64(ctx->cfg->user, ctx->cfg->pass);
994 + if (!b64)
995 + return error("XOAUTH2: base64 encoding failed");
996 +
997 + /* Send the base64-encoded response */
998 + ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
999 + if (ret != (int)strlen(b64)) {
1000 + free(b64);
1001 + return error("IMAP error: sending XOAUTH2 response failed");
1002 + }
1003 +
1004 + free(b64);
1005 + return 0;
1006 +}
1007 +
1008 #else
1009
1010 #define auth_cram_md5 NULL
1011 +#define auth_oauthbearer NULL
1012 +#define auth_xoauth2 NULL
1013
1014 #endif
1015
@@ -1118,6 +1222,12 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
1222 if (!strcmp(srvc->auth_method, "CRAM-MD5")) {
1223 if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5))
1224 goto bail;
1225 + } else if (!strcmp(srvc->auth_method, "OAUTHBEARER")) {
1226 + if (try_auth_method(srvc, ctx, imap, "OAUTHBEARER", AUTH_OAUTHBEARER, auth_oauthbearer))
1227 + goto bail;
1228 + } else if (!strcmp(srvc->auth_method, "XOAUTH2")) {
1229 + if (try_auth_method(srvc, ctx, imap, "XOAUTH2", AUTH_XOAUTH2, auth_xoauth2))
1230 + goto bail;
1231 } else {
1232 fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
1233 goto bail;
@@ -1419,7 +1529,16 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
1529
1530 server_fill_credential(srvc, cred);
1531 curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
1422 - curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
1532 +
1533 + /*
1534 + * Use CURLOPT_PASSWORD irrespective of whether there is
1535 + * an auth method specified or not, unless it's OAuth2.0,
1536 + * where we use CURLOPT_XOAUTH2_BEARER.
1537 + */
1538 + if (!srvc->auth_method ||
1539 + (strcmp(srvc->auth_method, "XOAUTH2") &&
1540 + strcmp(srvc->auth_method, "OAUTHBEARER")))
1541 + curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
1542
1543 strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
1544 strbuf_addstr(&path, srvc->host);
@@ -1437,11 +1556,22 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
1556 curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
1557
1558 if (srvc->auth_method) {
1440 - struct strbuf auth = STRBUF_INIT;
1441 - strbuf_addstr(&auth, "AUTH=");
1442 - strbuf_addstr(&auth, srvc->auth_method);
1443 - curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1444 - strbuf_release(&auth);
1559 + if (!strcmp(srvc->auth_method, "XOAUTH2") ||
1560 + !strcmp(srvc->auth_method, "OAUTHBEARER")) {
1561 +
1562 + /*
1563 + * While CURLOPT_XOAUTH2_BEARER looks as if it only supports XOAUTH2,
1564 + * upon debugging, it has been found that it is capable of detecting
1565 + * the best option out of OAUTHBEARER and XOAUTH2.
1566 + */
1567 + curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, srvc->pass);
1568 + } else {
1569 + struct strbuf auth = STRBUF_INIT;
1570 + strbuf_addstr(&auth, "AUTH=");
1571 + strbuf_addstr(&auth, srvc->auth_method);
1572 + curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1573 + strbuf_release(&auth);
1574 + }
1575 }
1576
1577 if (!srvc->use_ssl)