@@ -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 {
280282my ($auto_8bit_encoding );
281283my ($compose_encoding );
282284my ($sendmail_cmd );
285+ my ($smtp_authpasseval );
283286my ($mailmap_file , $mailmap_blob );
284287# Variables with corresponding config settings & hardcoded defaults
285288my ($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 ,\001 port=$smtp_server_port \001 auth=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 \001 auth=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