Skip to content

Commit 10caa4a

Browse files
committed
Merge branch 'ag/send-email-outlook' into jch
* ag/send-email-outlook: send-email: add option to generate passswords like OAuth2 tokens send-email: retrieve Message-ID from outlook SMTP server send-email: implement SMTP bearer authentication
2 parents d4e6dbd + 6e6f95b commit 10caa4a

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

Documentation/git-send-email.adoc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ SMTP server and if it is supported by the utilized SASL library, the mechanism
213213
is used for authentication. If neither 'sendemail.smtpAuth' nor `--smtp-auth`
214214
is specified, all mechanisms supported by the SASL library can be used. The
215215
special value 'none' maybe specified to completely disable authentication
216-
independently of `--smtp-user`
216+
independently of `--smtp-user`. Specifying `OAUTHBEARER` or `XOAUTH2` will
217+
bypass SASL negotiation and force bearer authentication. In this case the
218+
bearer token must be provided with `--smtp-pass` or using a credential helper
219+
and `--smtp-encryption=tls` must be set.
217220

218221
--smtp-pass[=<password>]::
219222
Password for SMTP-AUTH. The argument is optional: If no
@@ -227,6 +230,14 @@ or on the command line. If a username has been specified (with
227230
specified (with `--smtp-pass` or `sendemail.smtpPass`), then
228231
a password is obtained using 'git-credential'.
229232

233+
--smtp-passeval[=<command>]::
234+
Generate password like OAuth2 token for SMTP AUTH. If specified,
235+
it will use the output of the command specified as a password for
236+
authentication.
237+
+
238+
Note that it will override any existing password specified using
239+
`--smtp-pass` or a `sendemail.smtpPass`.
240+
230241
--no-smtp-auth::
231242
Disable SMTP authentication. Short hand for `--smtp-auth=none`
232243

git-send-email.perl

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ sub usage {
5959
--smtp-server-port <int> * Outgoing SMTP server port.
6060
--smtp-user <str> * Username for SMTP-AUTH.
6161
--smtp-pass <str> * Password for SMTP-AUTH; not necessary.
62+
--smtp-passeval <str> * Path to script or a command to generate
63+
password like OAuth2 token for SMTP-AUTH.
6264
--smtp-encryption <str> * tls or ssl; anything else disables.
6365
--smtp-ssl * Deprecated. Use '--smtp-encryption ssl'.
6466
--smtp-ssl-cert-path <str> * Path to ca-certificates (either directory or file).
@@ -280,6 +282,7 @@ sub do_edit {
280282
my ($auto_8bit_encoding);
281283
my ($compose_encoding);
282284
my ($sendmail_cmd);
285+
my ($smtp_authpasseval);
283286
my ($mailmap_file, $mailmap_blob);
284287
# Variables with corresponding config settings & hardcoded defaults
285288
my ($debug_net_smtp) = 0; # Net::SMTP, see send_message()
@@ -316,6 +319,7 @@ sub do_edit {
316319
"smtppass" => \$smtp_authpass,
317320
"smtpdomain" => \$smtp_domain,
318321
"smtpauth" => \$smtp_auth,
322+
"smtppasseval" => \$smtp_authpasseval,
319323
"smtpbatchsize" => \$batch_size,
320324
"smtprelogindelay" => \$relogin_delay,
321325
"to" => \@config_to,
@@ -516,6 +520,7 @@ sub config_regexp {
516520
"smtp-server-port=s" => \$smtp_server_port,
517521
"smtp-user=s" => \$smtp_authuser,
518522
"smtp-pass:s" => \$smtp_authpass,
523+
"smtp-passeval=s" => \$smtp_authpasseval,
519524
"smtp-ssl" => sub { $smtp_encryption = 'ssl' },
520525
"smtp-encryption=s" => \$smtp_encryption,
521526
"smtp-ssl-cert-path=s" => \$smtp_ssl_cert_path,
@@ -1398,6 +1403,63 @@ sub smtp_host_string {
13981403
}
13991404
}
14001405

1406+
sub generate_oauthbearer_string {
1407+
# This will generate the oauthbearer string used for authentication.
1408+
#
1409+
# "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A
1410+
#
1411+
# The first part `n,a=" {User} ",` is the gs2 header described in RFC5801.
1412+
# * gs2-cb-flag `n` -> client does not support CB
1413+
# * gs2-authzid `a=" {User} "`
1414+
#
1415+
# The second part are key value pairs containing host, port and auth as
1416+
# described in RFC7628.
1417+
#
1418+
# https://datatracker.ietf.org/doc/html/rfc5801
1419+
# https://datatracker.ietf.org/doc/html/rfc7628
1420+
my $username = shift;
1421+
my $token = shift;
1422+
return "n,a=$username,\001port=$smtp_server_port\001auth=Bearer $token\001\001";
1423+
}
1424+
1425+
sub generate_xoauth2_string {
1426+
# "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A"
1427+
# https://developers.google.com/gmail/imap/xoauth2-protocol#initial_client_response
1428+
my $username = shift;
1429+
my $token = shift;
1430+
return "user=$username\001auth=Bearer $token\001\001";
1431+
}
1432+
1433+
sub smtp_bearer_auth {
1434+
my $username = shift;
1435+
my $token = shift;
1436+
my $auth_string;
1437+
if ($smtp_encryption ne "tls") {
1438+
# As described in RFC7628 TLS is required and will be enforced
1439+
# at this point.
1440+
#
1441+
# https://datatracker.ietf.org/doc/html/rfc7628#section-3
1442+
die sprintf(__("For %s TLS is required."), $smtp_auth);
1443+
}
1444+
if ($smtp_auth eq "OAUTHBEARER") {
1445+
$auth_string = generate_oauthbearer_string($username, $token);
1446+
} elsif ($smtp_auth eq "XOAUTH2") {
1447+
$auth_string = generate_xoauth2_string($username, $token);
1448+
}
1449+
my $encoded_auth_string = MIME::Base64::encode($auth_string, "");
1450+
$smtp->command("AUTH $smtp_auth $encoded_auth_string\r\n");
1451+
use Net::Cmd qw(CMD_OK);
1452+
if ($smtp->response() == CMD_OK){
1453+
return 1;
1454+
} else {
1455+
# Send dummy request on authentication failure according to rfc7628.
1456+
# https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.3
1457+
$smtp->command(MIME::Base64::encode("\001"));
1458+
$smtp->response();
1459+
return 0;
1460+
}
1461+
}
1462+
14011463
# Returns 1 if authentication succeeded or was not necessary
14021464
# (smtp_user was not specified), and 0 otherwise.
14031465

@@ -1406,6 +1468,16 @@ sub smtp_auth_maybe {
14061468
return 1;
14071469
}
14081470

1471+
# If smtpPassEval is set, run the user specified command to get the password
1472+
if (defined $smtp_authpasseval) {
1473+
printf __("Executing token generating script: %s\n"), $smtp_authpasseval;
1474+
chomp(my $generated_password = `$smtp_authpasseval 2>&1`);
1475+
if ($? != 0) {
1476+
die sprintf(__("Failed to execute token generating script: %s\n"), $smtp_authpasseval);
1477+
}
1478+
$smtp_authpass = $generated_password;
1479+
}
1480+
14091481
# Workaround AUTH PLAIN/LOGIN interaction defect
14101482
# with Authen::SASL::Cyrus
14111483
eval {
@@ -1436,7 +1508,12 @@ sub smtp_auth_maybe {
14361508

14371509
# catch all SMTP auth error in a unified eval block
14381510
eval {
1439-
if ($smtp_auth) {
1511+
if (defined $smtp_auth && ($smtp_auth eq "OAUTHBEARER" || $smtp_auth eq "XOAUTH2")) {
1512+
# Since Authen:SASL does not support XOAUTH2 nor OAUTHBEARER we will
1513+
# manually authenticate for these types. The password field should
1514+
# contain the auth token at this point.
1515+
$result = smtp_bearer_auth($cred->{'username'}, $cred->{'password'});
1516+
} elsif ($smtp_auth) {
14401517
my $sasl = Authen::SASL->new(
14411518
mechanism => $smtp_auth,
14421519
callback => {
@@ -1574,6 +1651,11 @@ sub gen_header {
15741651
return ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header);
15751652
}
15761653

1654+
sub is_outlook {
1655+
my ($host) = @_;
1656+
return ($host eq 'smtp.office365.com' || $host eq 'smtp-mail.outlook.com');
1657+
}
1658+
15771659
# Prepares the email, then asks the user what to do.
15781660
#
15791661
# If the user chooses to send the email, it's sent and 1 is returned.
@@ -1737,6 +1819,21 @@ sub send_message {
17371819
$smtp->datasend("$line") or die $smtp->message;
17381820
}
17391821
$smtp->dataend() or die $smtp->message;
1822+
1823+
# Outlook discards the Message-ID header we set while sending the email.
1824+
# It instead saves it in its proprietary X-Microsoft-Original-Message-ID
1825+
# header and assigns a new random Message-ID to the email. So in order to
1826+
# avoid breaking threads, we simply retrieve the Message-ID from the server
1827+
# response and assign it to $message_id.
1828+
if (is_outlook($smtp_server)) {
1829+
if ($smtp->message =~ /<([^>]+)>/) {
1830+
$message_id = "<$1>";
1831+
printf __("Outlook reassigned Message-ID to: %s\n"), $message_id;
1832+
} else {
1833+
warn __("Warning: Could not retrieve Message-ID from server response.\n");
1834+
}
1835+
}
1836+
17401837
$smtp->code =~ /250|200/ or die sprintf(__("Failed to send %s\n"), $subject).$smtp->message;
17411838
}
17421839
if ($quiet) {

0 commit comments

Comments
 (0)