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
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
[!CAUTION]
Theclient_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
---
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:
- Go to Azure Portal โ App Registrations
- Click "New registration"
- 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/
[!IMPORTANT]
You must also grant API Permissions forMail.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->>LocalHTTP: Start listening on port 5500 <i> </i>App->>Browser: Open Microsoft login page <i> </i>Browser->>MS: User logs in & grants permission <i> </i>MS->>LocalHTTP: Redirect with authorization code <i> </i>LocalHTTP->>App: Return authorization code <i> </i>App->>MS: Exchange code for access_token + refresh_token <i> </i>MS->>App: Tokens returned <i> </i>App->>App: Save refresh_token to outlookmail.json <i> </i>Note over App: Future launches use saved refresh_token<br/>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
---
Adapting Your Legacy Code
Your Legacy OLE Code vs. New Graph API Code
| Feature | Legacy OLE ( | New Graph API ( |
|---|---|---|
| Requires Outlook installed | ||
| Works with Microsoft 365 | ||
| Silent sending | ||
| Shows in Sent Items | ||
| Shows in Recipient Inbox | ||
| Supports attachments | ||
| Supports To/CC/BCC | ||
| HTML body | Via HTMLBody property |
Your New Silent Email Function
Here's how to replace your legacy code with a function that works with Microsoft 365:
// 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 lSentTranslating Your Specific Leave Request Workflow
Your existing calls like this:
// 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:
// 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):
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.,
---
Quick Checklist
[ ] Ask agency IT admin to register the app in Azure portal[ ] Get theclient_id (Application ID) andclient_secret [ ] Ensure IT admin grants Mail.Send permission with admin consent[ ] Set the redirect URI tohttp://localhost:5500/ in both Azure and your code[ ] Replace your legacyTOleAuto calls with the newSendOutlook365Mail() 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