FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour Migrating from Legacy OLE to Microsoft 365 Graph API
Posts: 44162
Joined: Thu Oct 06, 2005 05:47 PM
Migrating from Legacy OLE to Microsoft 365 Graph API
Posted: Fri Mar 06, 2026 11:54 PM

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/
  • Click Register
  • Copy the Application (client) ID โ†’ this is your client_id
  • 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

    </s>
    sequenceDiagram
        participant App as Your Leave App
        participant Browser as WebView2 Browser
        participant MS as Microsoft Login
        participant LocalHTTP as Local HTTP Server (5500)
    
    <i>    </i>App-&gt;&gt;LocalHTTP: Start listening on port 5500
    <i>    </i>App-&gt;&gt;Browser: Open Microsoft login page
    <i>    </i>Browser-&gt;&gt;MS: User logs in &amp; grants permission
    <i>    </i>MS-&gt;&gt;LocalHTTP: Redirect with authorization code
    <i>    </i>LocalHTTP-&gt;&gt;App: Return authorization code
    <i>    </i>App-&gt;&gt;MS: Exchange code for access_token + refresh_token
    <i>    </i>MS-&gt;&gt;App: Tokens returned
    <i>    </i>App-&gt;&gt;App: Save refresh_token to outlookmail.json
    <i>    </i>Note over App: Future launches use saved refresh_token&lt;br/&gt;No user interaction needed!
    <e>

    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 (clipper): Select all Collapse
    // 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 (clipper): Select all Collapse
    // 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 (clipper): Select all Collapse
    // 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 (clipper): Select all Collapse
    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., leaverequests@agency.sc.gov) 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
    Posts: 44162
    Joined: Thu Oct 06, 2005 05:47 PM
    Re: Migrating from Legacy OLE to Microsoft 365 Graph API
    Posted: Sat Mar 07, 2026 12:05 AM

    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
    Posts: 231
    Joined: Fri Jul 20, 2012 01:49 AM
    Re: Migrating from Legacy OLE to Microsoft 365 Graph API
    Posted: Sat Mar 07, 2026 01:17 AM

    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 )

    # 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

    Continue the discussion