Jump to content
Ugochukwu Mmaduekwe

Sending Email via GMail Using OAuth 2.0 via Indy

Recommended Posts

On 11/7/2019 at 11:50 AM, Ugochukwu Mmaduekwe said:

Does anyone have some sample code on how I can send email from Gmail in my Delphi App using OAuth 2.0 via Indy?

Indy does not currently support OAuth yet.  However, it would be fairly simple to create a TIdSASL-derived component that can be added to the TIdSMTP.SASLMechanisms collection to transmit an OAuth bearer token using the SMTP "AUTH XOAUTH2" command.  But getting that token in the first place is the tricky part, and has to be done outside of SMTP.

Edited by Remy Lebeau
  • Like 1
  • Thanks 2

Share this post


Link to post
On 11/11/2019 at 2:02 PM, Remy Lebeau said:

Indy does not currently support OAuth yet.

However, you don't actually need OAuth to access GMail.  You can instead go into your Google account settings and generate an Application-specific password, which works just fine with Indy.

Edited by Remy Lebeau

Share this post


Link to post
On 11/12/2019 at 9:02 AM, Remy Lebeau said:

Indy does not currently support OAuth yet.  However, it would be fairly simple to create a TIdSASL-derived component that can be added to the TIdSMTP.SASLMechanisms collection to transmit an OAuth bearer token using the SMTP "AUTH XOAUTH2" command.  But getting that token in the first place is the tricky part, and has to be done outside of SMTP. 

I have now updated my demo to use an TIdSASL derived component that I created.  I must admit that it does use the Delphi TOAuth2Authenticator component as well which is not a Indy component... but it has been in Delphi going back quite a few versions.

  • Like 1

Share this post


Link to post
10 hours ago, Remy Lebeau said:

However, you don't actually need OAuth to access GMail.  You can instead go into your Google account settings and generate an Application-specific password, which works just fine with Indy.

The problem with application password is that it requires 2FA setup which in turn forces you to authenticate each login even though it's for something as trivial as sending mails from your app.

Edited by Ugochukwu Mmaduekwe
  • Like 1

Share this post


Link to post

Gmail still allows SMTP and POP3 access with basic authentication, provided you ignore all attempts by Google to set-up better security on the account, and accept the odd/regular email that your account is being used by a suspicious application.  But once you have turned on 'better security' (forget it's real name) you can not turn it off, so have to set-up a new gmail account. 

 

The OAuth2 option is not too bad, you only need to authenticate with a Google login using a browser once and the refresh token provided remains valid until not used for six months, or when the account is changed. so you can get a new access token each time you send email without needing to authenticate again.  Other OAuth2 implementations usually expire the refresh token within 24 hours.

 

Angus

 

  • Like 1

Share this post


Link to post
5 hours ago, Ugochukwu Mmaduekwe said:

The problem with application password is that it requires 2FA setup which in turn forces you to authenticate each login even though it's for something as trivial as sending mails from your app.

2FA is a good thing.  And no, you don't actually need to authenticate every login.  An app-specific password is meant to be used in only 1 location and shouldn't be passed around. You can set Google to remember where the password is being used from so you don't have to re-authenticate every time it is used from that location.  I use app-specific passwords when testing Indy with GMail (POP3, SMTP, and IMAP) and don't have to re-authenticate each time.

  • Thanks 1

Share this post


Link to post

Guys, just a heads up: Google is deprecating access to "unsecure apps" for Gmail for Business this February 15th. It is very likely that they'll extend this to all Gmail any time soon. 

  • Like 1

Share this post


Link to post
1 hour ago, LeusKapus said:

Google is deprecating access to "unsecure apps" for Gmail for Business this February 15th.

This should have no impact for application passwords. Your application doesn't even need to be changed. Just use the application password instead of the regular GMail password.

 

 

Share this post


Link to post

@Geoffrey Smith,
I downloaded your sample code but there is a file that did not come and that is dealt with in the uses which is Globals.pas

Share this post


Link to post
18 minutes ago, EduPro said:

@Geoffrey Smith,
I downloaded your sample code but there is a file that did not come and that is dealt with in the uses which is Globals.pas


You need to create that yourself.  In this file you need to add the missing constant values to make the project compile.

Share this post


Link to post

Geoffrey, thank you for this.  I had no problems getting it working (with G Suite from a Delphi 10.4 app.)  Nicely done!

Share this post


Link to post

@Geoffrey Smith

 

Thanks a lot for your sample on Github

 

Patrick

Edited by PatV

Share this post


Link to post
On 11/8/2019 at 1:20 PM, Geoffrey Smith said:

Hi @Ugochukwu Mmaduekwe,

 

Have a look at

https://github.com/geoffsmith82/GmailAuthSMTP/

 

I just created a simple demo for you.  You will need to get a client_id from google in their developer toolbox.

 

Geoffrey

your gmailAuth on github is really good. but due to google strict requirements, I need a solution which does not use idsmtp but IDHTTP, as only have send authorization. I used your code to get the token - works brilliant. but I tried to make some code to post the message I generated using idmessage (into a tstreamstring) but always get a bad request error when posting. I think the problem is when doing IDHTTP.Post, the message is not formatted correctly. please could you advise? many thanks

Share this post


Link to post
6 minutes ago, JLG said:

due to google strict requirements, I need a solution which does not use idsmtp but IDHTTP

Can you show which requirements you are working from?

 

7 minutes ago, JLG said:

I generated using idmessage

If what you are using is purely HTTP-based, then TIdMessage is unlikely to be appropriate. Please show your code.

Share this post


Link to post

thanks foir your reply.

the following is based on your existing code for getting token

 

function TgmailFrm.Sendemail(FromName, emailFrom, emailRecip, emailSubject,
  emailBody, emailAttach: string; Quietly: Boolean): Boolean;


var
  HTTP: TIdHTTP;
  Response: TStringList;
  Url, s: STring;
  Base64: TBase64Encoding;

  IdMessage: TIdMessage;
  MailBuilder: TIdMessageBuilderPlain;
  xoauthSASL: TIdSASLListEntry;
  MS: TStringStream;

  fIdSSLIOHandlerSocketOpenSSL: TIdSSLIOHandlerSocketOpenSSL;
begin

  result := False;

  if not SetupAuthenticator or not HasSavedToken
  then
  begin
    /// Authenticate; this is done in setup authenticator
    IdHTTPServer1.Active := False;
    Exit;
  end;

  // if we only have refresh_token or access token has expired
  // request new access_token to use with request
  OAuth2_Enhanced.RefreshAccessTokenIfRequired;

  if OAuth2_Enhanced.AccessToken.Length = 0
  then
  begin
    raise Exception.Create('Failed to authenticate properly');
    // Exit;   //need to free the stuff
  end;
//  xoauthSASL := IdSMTP1.SASLMechanisms.Add;
//  xoauthSASL.SASL := TIdOAuth2Bearer.Create(nil);
//  TIdOAuth2Bearer(xoauthSASL.SASL).Token := OAuth2_Enhanced.AccessToken;
//  TIdOAuth2Bearer(xoauthSASL.SASL).Host := IdSMTP1.Host;
//  TIdOAuth2Bearer(xoauthSASL.SASL).Port := IdSMTP1.Port;
 // TIdOAuth2Bearer(xoauthSASL.SASL).User := clientaccount;

  try


    Url := 'https://gmail.googleapis.com/gmail/v1/users/' + emailFrom +
      '/messages/send';

    try
      HTTP := TIdHTTP.Create;
      fIdSSLIOHandlerSocketOpenSSL := TIdSSLIOHandlerSocketOpenSSL.Create(HTTP);
      with fIdSSLIOHandlerSocketOpenSSL.SSLOptions do
      begin
        Method := sslvTLSv1_2;
        Mode := sslmClient;
        SSLVersions := [sslvTLSv1_2];
      end;

      HTTP.IOHandler := fIdSSLIOHandlerSocketOpenSSL;
    //  HTTP.Request.CharSet := 'utf-8';
      IdMessage := TIdMessage.Create(Application);
      MailBuilder := TIdMessageBuilderPlain.Create;
      MailBuilder.PlainText.Text := emailBody;
      MailBuilder.PlainTextCharSet := 'iso-8859-1';
    //  MailBuilder.PlainTextContentTransfer := 'base64';
      if (emailAttach <> '') and (FileExists(emailAttach))
      then
      begin
        MailBuilder.Attachments.Add(emailAttach);
        IdMessage.ContentType := 'multipart/mixed';
      end
      else
        IdMessage.ContentType := 'text/plain';

      IdMessage := MailBuilder.NewMessage();
      // if UseHTML then
      // 'text/html';
      // else
      IdMessage.ContentType := 'text/plain';

      IdMessage.Encoding := meMIME;
      IdMessage.From.Address := emailFrom;
      IdMessage.From.Name := FromName;
      IdMessage.ReplyTo.EMailAddresses := IdMessage.From.Address;
      IdMessage.Recipients.Add.Text := emailRecip;
      IdMessage.Subject := emailSubject;
      IdMessage.Body.Text := emailBody;
      IdMessage.NoEncode := False;
      MS := TStringStream.Create('', TEncoding.UTF8); // this enables me to show progress

      try
        if not Quietly
        then
        begin

          IdMessage.SaveToStream(MS);
          Sz := MS.Size;
          HTTP.onWork := EvHandler.DoWork;
          IdSMTP1.onWork := EvHandler.DoWork;
          HTTP.BeginWork(wmRead); // this enables me to show progress
        end; // if not quietly then
        with HTTP do
        begin

          Request.CustomHeaders.Add(Format('Authorization: Bearer %s',
            [OAuth2_Enhanced.AccessToken]));

          HandleRedirects := True;
          Response.KeepAlive := False;
          AllowCookies := True;
        end;

        MS.Position:=0;

        HTTP.Post(Url, MS);


        if not Quietly
        then
          ShowWait(0, 0, TimeToStr(Time) + '  email sent to ' +
            emailRecip, True);
        if not Quietly
        then
          HTTP.EndWork(wmRead);
        result := True;
      except
        On E: Exception do
        begin
          Add2Log('Error while sending email to ' + emailRecip + ': ' +
            E.Message);
          result := False;
          if not Quietly
          then
            ShowTimedMsg('Error while sending email to ' + emailRecip + ': ' +
              E.Message, 10);
        end;
      end;

    finally
      IdSMTP1.Disconnect;
      HTTP.Free;
      // fIdSSLIOHandlerSocketOpenSSL.Free;
      MS.Free;
      IdMessage.Free;
      MailBuilder.Free;

    end;
  finally

    IdHTTPServer1.Active := False;

  end;
end;

Share this post


Link to post

The docs for send say that the body (what you are passing in the content when you Post) should be an instance of Message, i.e. the JSON that is specified at that link.

 

TIdMessage.SaveToStream does not create JSON in this format. You will need to either create code to do this, or find existing code that does.

Share this post


Link to post

thanks. any idea how to do this. is there no way to convert idmessage to json? in the google docs it requires things like a unique message id and other stuff with no explanation of how. do i invent one or retrieve one?? their docs are so unhelpful to delphi user...

Share this post


Link to post

For posting a draft message to GMail I don't need any JSON. I can just post the complete mail (eml-format) to https://www.googleapis.com/upload/gmail/v1/users/<email-adres>/drafts. You do need to set the header Authorization: Bearer with the correct access_token.

 

Ps. My MimeType (with Synapse) is message/rfc822 and accept is application/json. I don't see that in your code.

 

Small snippet from my code (as I said with Synapse).

Quote

Url := 'https://www.googleapis.com/upload/gmail/v1/users/' + gOAuth2.email + '/drafts';
WorkStr := CreatePlainEML;
WriteStrToStream(HTTP.Document, ansistring(WorkStr));
HTTP.MimeType := 'message/rfc822';
HTTP.Headers.Clear;
HTTP.Headers.Add('Authorization: Bearer ' + gOAuth2.access_token);
HTTP.Headers.Add('Accept: application/json');
if HTTP.HTTPMethod('POST', Url) then
begin
  Response.LoadFromStream(HTTP.Document);
  if HTTP.ResultCode <> 200 then
    ShowMessage(Response.Text)
  else
  begin
    obj := SO(Response.Text);
    Url := 'https://mail.google.com/mail?authuser=' + gOAuth2.email + '#drafts/' + urlencode(obj.S['message.id']);
    BrowseURL(Url);
    Result := SUCCESS_SUCCESS;
  end;
end;

The CreatePlainEML creates a mail message which is compatible with Outlook Express and Thunderbird (I believe this is the rfc822 e-mail standard).

With TMimemess and multiple TMimepart parts followed by the TMememess.EncodeMessage-function from Synapse.

 

So there shouldn't be any need for posting JSON (only retrieving the result as JSON).

I don't think TIdMessage creates a much different text but I'm not sure.

 

Edited by rvk
  • Like 1

Share this post


Link to post
2 hours ago, JLG said:

is there no way to convert idmessage to json?

No, unless you do it yourself manually.  You are simply using the wrong tool for the job to begin with.  TIdMessage is designed for email only, not for HTTP posts.  There is nothing in Indy that specifically handles the Gmail REST API you are referring to, so you are going to have to implement it yourself.

Edited by Remy Lebeau

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×