Migrating from Legacy OLE to Microsoft 365 Graph API

Post Reply
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Migrating from Legacy OLE to Microsoft 365 Graph API

Post by Antonio Linares »

TOutlookMail Guide: Migrating from Legacy OLE to Microsoft 365 Graph API Your Questions Answered What is client_id?

No, it is NOT the user's email address. The client_id is a unique Application (client) ID — a GUID like a1b2c3d4-e5f6-7890-abcd-ef1234567890 — that you get when you register your application in Azure Active Directory (Microsoft Entra ID).

Think of it as your application's license plate number with Microsoft. Every app that wants to talk to Microsoft 365 must be registered, and Microsoft gives it this unique ID.

What is client_secret?

No, it is NOT the user's network password. The client_secret is a secret key that Microsoft generates for your registered application — a random string like ABC~defGHI.jklMNO_pqrSTU123. It acts as your application's password to prove to Microsoft that API requests are really coming from your app.

[!CAUTION]
The client_secret should be treated like a master password. Never hard-code it in source files that could be shared. Store it securely (e.g., encrypted in a config file or database).

What is redirect_uri?

This is the callback URL where Microsoft sends the authorization code after the user logs in. The TOutlookMail class spins up a tiny local HTTP server (via THttpServer in [oauth.prg](file:///c:/fwteam/source/classes/oauth.prg)) to listen on this URL and capture the code automatically. http://localhost:5500/ is the default.

--- How to Get Your client_id and client_secret

Your agency's IT administrator (or someone with access to the organization's Azure/Microsoft 365 admin portal) needs to do this once:

  1. Go to Azure Portal → App Registrations
  2. Click "New registration"
  3. Fill in:
    • Name: e.g., "SC Leave Request App"
    • Supported account types: "Accounts in this organizational directory only"
    • Redirect URI: Select Web, enter http://localhost:5500/
  4. Click Register
  5. Copy the Application (client) ID → this is your client_id
  6. Go to Certificates & secrets → New client secret
    • Add a description (e.g., "Leave App Secret")
    • Choose expiration (e.g., 24 months)
    • Click Add and immediately copy the Value → this is your client_secret

[!IMPORTANT]
You must also grant API Permissions for Mail.Send under Microsoft Graph (Delegated permissions). An admin must click "Grant admin consent" for the organization.

--- How the Authentication Flow Works
sequenceDiagram participant App as Your Leave App participant Browser as WebView2 Browser participant MS as Microsoft Login participant LocalHTTP as Local HTTP Server (5500) App->>LocalHTTP: Start listening on port 5500 App->>Browser: Open Microsoft login page Browser->>MS: User logs in & grants permission MS->>LocalHTTP: Redirect with authorization code LocalHTTP->>App: Return authorization code App->>MS: Exchange code for access_token + refresh_token MS->>App: Tokens returned App->>App: Save refresh_token to outlookmail.json Note over App: Future launches use saved refresh_token<br/>No user interaction needed!
Key Point: One-Time Login, Then Silent Forever

The first time a user runs the app, a browser window opens for them to log into their Microsoft 365 account. After that, the refresh_token is saved to [outlookmail.json](file:///c:/fwteam/samples/outlookmail.json), and all future emails are sent silently without any user interaction.

--- Adapting Your Legacy Code Your Legacy OLE Code vs. New Graph API Code FeatureLegacy OLE (TOleAuto)New Graph API (TOutlookMail) Requires Outlook installed✅ Yes❌ No Works with Microsoft 365❌ No (blocked)✅ Yes Silent sending✅ Yes✅ Yes (after first auth) Shows in Sent Items✅ Yes✅ Yes (saveToSentItems := .t.) Shows in Recipient Inbox✅ Yes✅ Yes Supports attachments✅ Yes✅ Yes Supports To/CC/BCC✅ Yes✅ Yes HTML bodyVia HTMLBody property✅ Yes (lHTML parameter)
Your New Silent Email Function

Here's how to replace your legacy code with a function that works with Microsoft 365:

Code: Select all | Expand

// One-time setup at app startup (or in your main module)
static oOutlookMail, hStore

function InitOutlook365()

   local cConfigFile := hb_dirBase() + "outlookmail.json"
   
   hStore := readStore( cConfigFile )
   
   oOutlookMail := TOutlookMail():new()
   
   oOutlookMail:setConfig( {;
      "client_id"     => "paste_your_client_id_here",;
      "client_secret" => "paste_your_client_secret_here",;
      "redirect_uri"  => "http://localhost:5500/";
   } )
   
   if !empty( hStore[ "token" ] )
      oOutlookMail:setToken( hStore[ "token" ] )
   endif
   
   // Check if we need to authenticate (first time only)
   if !oOutlookMail:isAuth()
      local cToken := oOutlookMail:auth()  // Opens browser for user login
      if !empty( cToken )
         hStore[ "token" ] := cToken
         saveStore( cConfigFile, hStore )
      else
         MsgStop( "Microsoft 365 authentication failed!" )
         return .f.
      endif
   endif

return .t.


// Drop-in replacement for your legacy silent email function
function SendOutlook365Mail( cTo, cCC, cSubject, cBody, cAttachment )

   local lSent := .f.

   // Make sure we're connected
   if hb_isNil( oOutlookMail ) .or. !oOutlookMail:isAuth()
      if !InitOutlook365()
         return .f.
      endif
   endif

   // Send the email - this is completely silent!
   // Parameters: aTo, cSubject, cMessage, lHTML, aAttachment, aBCC, aCC
   lSent := oOutlookMail:send( ;
      cTo,         ;   // To recipient(s) - string or array
      cSubject,    ;   // Subject line
      cBody,       ;   // Body content (HTML supported)
      .t.,         ;   // .t. = HTML body, .f. = plain text
      cAttachment, ;   // Attachment path(s) - string or array
      nil,         ;   // BCC (not used here)
      cCC          ;   // CC recipients
   )

   if !lSent
      MsgInfo( "Failed to send email via Outlook 365" )
   endif

return lSent
Translating Your Specific Leave Request Workflow

Your existing calls like this:

Code: Select all | Expand

// OLD LEGACY CODE:
oOutLook  := TOleAuto():New("Outlook.Application")
oMailItem := oOutLook:Invoke("CreateItem", 0)
oMailitem:to := cTo
oMailitem:CC := cCC
oMailItem:Subject := cSubject
oMailItem:Body := cBody
oMailItem:Attachments:Add("c:\dbtmp\"+cPdfName+".pdf")
oMailItem:Send()

Become simply:

Code: Select all | Expand

// NEW OUTLOOK 365 CODE:
SendOutlook365Mail( ;
   cTo,                                ;  // Employee's supervisor email
   cCC,                                ;  // CC recipients
   cSubject,                           ;  // "Leave Request from John Doe"
   cBody,                              ;  // HTML body with link to app
   "c:\dbtmp\" + cPdfName + ".pdf"     ;  // PDF attachment
)
--- Will Emails Show Up in Inboxes and Outboxes?

Yes, absolutely. Looking at line 316 of [outlookmail.prg](file:///c:/fwteam/source/classes/outlookmail.prg):

Code: Select all | Expand

hPost[ "saveToSentItems" ] := .t.

This tells Microsoft Graph API to save a copy in the sender's Sent Items folder. The email will:

  • ✅ Appear in the sender's Sent Items / Outbox
  • ✅ Appear in the recipient's Inbox
  • ✅ Look exactly like a normal Outlook email
  • ✅ Support Reply, Reply All, Forward from the recipient
--- Important Considerations for Your SC State Agency

[!WARNING]

Per-User Authentication

The current TOutlookMail class uses delegated permissions (OAuth2 Authorization Code flow). This means:

  • Each employee who sends emails needs to authenticate once on their machine
  • The refresh token is saved locally and reused silently after that
  • If the token expires (typically 90 days of inactivity), the user will need to re-authenticate

This is actually ideal for your use case because each employee sends leave requests from their own account, and the email appears as coming from them.

Alternative: Application Permissions (No User Login at All)

If your agency prefers that the app sends emails from a single service account (e.g., [email protected]) without any user ever logging in, the IT admin would need to configure Application permissions instead of Delegated permissions in Azure AD. This would require a modified version of TOutlookMail that uses the Client Credentials flow instead of Authorization Code flow. Let me know if you'd prefer this approach.

--- Quick Checklist
  • [ ] Ask agency IT admin to register the app in Azure portal
  • [ ] Get the client_id (Application ID) and client_secret
  • [ ] Ensure IT admin grants Mail.Send permission with admin consent
  • [ ] Set the redirect URI to http://localhost:5500/ in both Azure and your code
  • [ ] Replace your legacy TOleAuto calls with the new SendOutlook365Mail() function
  • [ ] Test with one user: first run will show login prompt, subsequent runs are silent
  • [ ] Deploy to all users — each authenticates once on first use
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: Migrating from Legacy OLE to Microsoft 365 Graph API

Post by Antonio Linares »

I've reviewed the full TOutlookMail class source code, the TOAuth parent class, and the testoutlook.prg sample, and created a comprehensive guide for you. Here are the quick answers:

client_id — This is NOT the user's email. It's a unique GUID assigned to your application when you register it in the Azure Portal (Microsoft Entra ID). Think of it as your app's registration number with Microsoft.

client_secret — This is NOT the user's password. It's a secret key Microsoft generates for your registered app. It's like a password for the app itself, not for any user.

Your agency's IT admin needs to register your Leave app in the Azure portal (a one-time 5-minute process), and from that registration you get both values.

Good news for your workflow:

✅ After the first login on each machine, all subsequent emails are completely silent — the refresh token is saved and reused automatically
✅ Emails do show up in the sender's Sent Items (saveToSentItems := .t. is hardcoded in the class)
✅ Emails appear normally in the recipient's Inbox
✅ Attachments, To, CC, BCC, and HTML body are all supported
✅ The send() method is a direct drop-in replacement for your legacy oMailItem:Send()
The guide includes ready-to-use SendOutlook365Mail() function code that replaces your legacy TOleAuto calls. Have a look!

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Lailton
Posts: 231
Joined: Fri Jul 20, 2012 1:49 am
Location: Brazil
Contact:

Re: Migrating from Legacy OLE to Microsoft 365 Graph API

Post by Lailton »

Correct, and here you have the step by step how to configure it to with the FWH sample:

How to setup Microsoft Azure Portal ( Office365/Outlook )

Code: Select all | Expand

# after connect to azure portal

1 - Microsoft Entra ID
https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade

2 - App Registration
https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps

Click in "+ New Registration"
Enter a Name to your application and choose who can connect.
in my test I have checked:

"[X] Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)"

In the redirect URI

Platform select "Web"
Enter the address (example): "http://localhost:5500/"
( It will be the redirect_uri )

and click in Register

On the next screen on the right side you will see the option: Client credentials -> Add a certificate or secret
( or from Menu go to the option "Certificates & secrets" )

Click in New client secret, in description field enter something "I have added FiveTech" and next select number of month
expires ( I have selected 24 months ) and click in ADD.

In the next screen copy the VALUE and Secret ID

( client_secret )
VALUE: nvF8Q~u~~....

( the client_id you need to take from the app registration )
Secret ID: 15ca3c39-.....

Now on the left side menu, click in API Permissions

Click in Add Permission:
You need add "Microsoft Graph"
Then click in Delegated Permissions
and select the options below:

Section OpenID permission:
-> offline_access
-> openid
-> profile

Section IMAP:
-> IMAP.AccessAsUser.All

Section POP:
-> POP.AccessAsUser.All

Section SMTP:
-> SMTP.Send

Section User:
-> User.Read

Then click in ADD permissions

That's all.
Regards,
Lailton Fernando Mariano
Post Reply