Apart from SMTP, Microsoft also provides a REST API, branded as Microsoft Graph for sending mails. Upon testing a bit, I have found a few benefits over SMTP. Firstly, SMTP servers of Microsoft are kinda slow. On an average, initialising the SMTP server even on a fast internet connection takes around 8-10 seconds with send-email. Once initialised, subsequent messages sometimes also face delays, taking around 3-5 seconds per message, and other times they are sent almost instantaneously. Secondly, their SMTP server does not respect the Message-ID specified by the user and replaces it with their own generated string. Microsoft Graph API solves both these problems. It is extremely fast, taking around 1 second to send a series of 5 patches, and also respects the Message-ID specified by the user. After this patch, users can use the graph API by having their config as: [sendemail] useMSGraph = true MSGraphUser = yourname@xxxxxxxxxxx The API requires an OAuth2.0 access token for authentication, and users can either generate them manually, or use a credential helper TODO: I still need to write the documentation for this feature, but I wanted to get the code reviewed first. PS: This patch is sent using MS Graph. Note the fixed Message-ID ;) Signed-off-by: Aditya Garg <gargaditya08@xxxxxxxx> --- git-send-email.perl | 71 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/git-send-email.perl b/git-send-email.perl index b09251c4fc..edb96c7e1a 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -73,7 +73,10 @@ sub usage { --no-smtp-auth * Disable SMTP authentication. Shorthand for `--smtp-auth=none` --smtp-debug <0|1> * Disable, enable Net::SMTP debug. - + --[no-]use-msgraph <str> * Use Microsoft Graph API instead of SMTP. + --msgraph-user <str> * Username in case an email API is used. + If not specified, the from address is used. + --msgraph-token <str> * OAuth2.0 access token for the email API. --batch-size <int> * send max <int> message per connection. --relogin-delay <int> * delay <int> seconds between two successive login. This option can only be used with --batch-size @@ -201,7 +204,7 @@ sub format_2822_time { # Variables we fill in automatically, or via prompting: my (@to,@cc,@xh,$envelope_sender, $initial_in_reply_to,$reply_to,$initial_subject,@files, - $author,$sender,$smtp_authpass,$annotate,$compose,$time); + $author,$sender,$smtp_authpass,$annotate,$compose,$time,$msgraph_token); # Things we either get from config, *or* are overridden on the # command-line. my ($no_cc, $no_to, $no_bcc, $no_identity, $no_header_cmd); @@ -283,6 +286,7 @@ sub do_edit { my ($compose_encoding); my ($sendmail_cmd); my ($mailmap_file, $mailmap_blob); +my ($msgraph_user); # Variables with corresponding config settings & hardcoded defaults my ($debug_net_smtp) = 0; # Net::SMTP, see send_message() my $thread = 1; @@ -293,6 +297,7 @@ sub do_edit { my $target_xfer_encoding = 'auto'; my $forbid_sendmail_variables = 1; my $outlook_id_fix = 'auto'; +my $use_msgraph = 0; my %config_bool_settings = ( "thread" => \$thread, @@ -309,6 +314,7 @@ sub do_edit { "forbidsendmailvariables" => \$forbid_sendmail_variables, "mailmap" => \$mailmap, "outlookidfix" => \$outlook_id_fix, + "usemsgraph" => \$use_msgraph, ); my %config_settings = ( @@ -337,6 +343,8 @@ sub do_edit { "composeencoding" => \$compose_encoding, "transferencoding" => \$target_xfer_encoding, "sendmailcmd" => \$sendmail_cmd, + "msgraphuser" => \$msgraph_user, + "msgraphtoken" => \$msgraph_token, ); my %config_path_settings = ( @@ -556,6 +564,9 @@ sub config_regexp { "git-completion-helper" => \$git_completion_helper, "v=s" => \$reroll_count, "outlook-id-fix!" => \$outlook_id_fix, + "use-msgraph!" => \$use_msgraph, + "msgraph-user=s" => \$msgraph_user, + "msgraph-token:s" => \$msgraph_token, ); $rc = GetOptions(%options); @@ -1095,7 +1106,7 @@ sub expand_one_alias { $reply_to = sanitize_address($reply_to); } -if (!defined $sendmail_cmd && !defined $smtp_server) { +if (!defined $sendmail_cmd && !defined $smtp_server && !$use_msgraph) { my @sendmail_paths = qw( /usr/sbin/sendmail /usr/lib/sendmail ); push @sendmail_paths, map {"$_/sendmail"} split /:/, $ENV{PATH}; foreach (@sendmail_paths) { @@ -1603,6 +1614,7 @@ sub is_outlook { sub send_message { my ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header) = gen_header(); my @recipients = @$recipients_ref; + my $msgraph_response_code; my @sendmail_parameters = ('-i', @recipients); my $raw_from = $sender; @@ -1680,6 +1692,52 @@ sub send_message { } print $sm "$header\n$message"; close $sm or die $!; + } elsif ($use_msgraph) { + # Use Microsoft Graph API + # https://learn.microsoft.com/en-us/graph/api/user-sendmail + my $auth_api; + if (!defined $msgraph_user) { + $msgraph_user = (defined $smtp_authuser + ? $smtp_authuser + : $raw_from + ); + } + require MIME::Base64; + require HTTP::Tiny; + $auth_api = Git::credential({ + 'protocol' => 'https', + 'host' => 'graph.microsoft.com', + 'username' => $msgraph_user, + 'password' => $msgraph_token, + }, sub { + my $cred = shift; + $msgraph_token = $cred->{'password'}; + }); + my $email_url = "https://graph.microsoft.com/v1.0/users/$msgraph_user/sendMail"; + my $real_message = "$header\n$message\n"; + # Convert LF to CRLF + unless ($real_message =~ /\r\n/) { + $real_message =~ s/\r?\n/\r\n/g; + } + # The API requires the message to be base64 encoded if sending in MIME format. + my $encoded_message = MIME::Base64::encode_base64($real_message, ""); + my $http = HTTP::Tiny->new( + default_headers => { + 'Authorization' => "Bearer $msgraph_token", + 'Content-Type' => 'text/plain', + }, + ); + my $response = $http->post($email_url, { + content => $encoded_message, + }); + + + if (!$response->{success}) { + die sprintf(__("Failed to send email: %s\nResponse: %s"), + $response->{status}, $response->{content}); + } else { + $msgraph_response_code = $response->{status}; + } } else { if (!defined $smtp_server) { @@ -1790,12 +1848,15 @@ sub send_message { } else { print($dry_run ? __("Dry-OK. Log says:") : __("OK. Log says:")); print "\n"; - if (!defined $sendmail_cmd && !file_name_is_absolute($smtp_server)) { + if (!defined $sendmail_cmd && !file_name_is_absolute($smtp_server) + && !$use_msgraph) { print "Server: $smtp_server\n"; print "MAIL FROM:<$raw_from>\n"; foreach my $entry (@recipients) { print "RCPT TO:<$entry>\n"; } + } elsif ($use_msgraph) { + print "Email API: Microsoft Graph API\n"; } else { my $sm; if (defined $sendmail_cmd) { @@ -1810,6 +1871,8 @@ sub send_message { if ($smtp) { print __("Result: "), $smtp->code, ' ', ($smtp->message =~ /\n([^\n]+\n)$/s); + } elsif (defined $msgraph_response_code) { + print __("Result: "), $msgraph_response_code; } else { print __("Result: OK"); } -- 2.49.0