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.
What is redirect_uri?[!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).
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_secretYour 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/
- 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
--- How the Authentication Flow Works[!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.
Key Point: One-Time Login, Then Silent ForeversequenceDiagram 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!
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 CodeHere'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 lSentYour 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
)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
Alternative: Application Permissions (No User Login at All)[!WARNING]
Per-User AuthenticationThe 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.
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