Extracting your email(s) from MS Outlook client
Introduction
Extracting corporate data securely often comes with a major hurdle: credential management. When building tools to parse, back up, or process emails, handling user credentials safely is paramount.
In this post, we’ll break down how to build a desktop application using Electron, TypeScript, and the Microsoft Graph API that allows users to authenticate securely via OAuth 2.0 Authorization Code Flow with PKCE, discover their Outlook folder hierarchy, and safely extract message payloads locally without exposing sensitive access tokens to an external server.
⚠️ Disclaimer & Didactic Scope
Important Note: This application is intended solely for personal use and should be treated as a didactical demonstration. It is explicitly not designed or vetted for accessing corporate production email servers. In an enterprise environment, rigorous security controls — including robust credential encryption at rest, token validation lifecycles, advanced error boundary auditing, and corporate compliance guardrails — must be strictly enforced. Because this project prioritizes architectural clarity over enterprise hardening, it should not be used as a production-ready corporate solution.
The Architecture: Why PKCE in a Desktop App?
Standard OAuth 2.0 authorization code flows traditionally rely on a client_secret hosted on a secure backend server. Desktop applications are considered "public clients"—they run entirely on the user's machine, meaning a client secret cannot be safely hidden within the source code.
To overcome this, we implement Proof Key for Code Exchange (PKCE). Instead of a static secret, the application dynamically generates a cryptographic random string called a code_verifier, hashes it to create a code_challenge, and sends the challenge to the Microsoft Identity Platform. When exchanging the authorization code for tokens, the app sends the original verifier, proving it was the initiator of the login request.
The Lifecycle of the Extraction Flow
Here is exactly how the Electron Main process coordinates with the Renderer UI, a temporary local loopback server, and Microsoft’s APIs to pull the data:
-
Triggering Auth: The UI (Renderer) sends a message via IPC (
IPC "connect") to the Main process. -
Spinning Up a Local Server: The Main process boots a temporary, short-lived HTTP loopback server (
localhost:8765) to await the authentication callback. - User Login: The app opens the system’s default browser targeting Microsoft’s
/authorizeendpoint, passing along the PKCE code challenge. -
Token Exchange: Once the user consents, Microsoft redirects to
localhost:8765with an authorization code. The local server captures it, shuts down immediately, and the Main process POSTs this code alongside the plain textcode_verifierto get theaccess_tokenandrefresh_token. -
Data Extraction: The tokens are securely cached locally, enabling recursive calls to Microsoft Graph (
/me/mailFoldersand/me/mailFolders/{id}/messages) to map folders and stream paginated emails down to disk.
Core Implementation Deep Dive
Let’s look at the actual mechanics making this happen under the hood, starting with how the UI triggers the action and how the Main process orchestrates the secure loopback.
Inter-Process Communication (IPC) Bridge
Electron keeps the UI isolated from the operating system for security. To talk to the filesystem or network libraries safely, we use a preload.ts script to expose a sanitized context bridge:
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
connect: () => ipcRenderer.send('connect'),
listFolders: () => ipcRenderer.send('list-folders'),
startExtraction: (folderIds: string[], exportParams: any) =>
ipcRenderer.send('start-extraction', { folderIds, exportParams }),
onProgress: (callback: (status: string) => void) =>
ipcRenderer.on('progress', (_event, status) => callback(status)),
onFoldersReturned: (callback: (folders: any[]) => void) =>
ipcRenderer.on('return-folders', (_event, folders) => callback(folders)),
onDone: (callback: (result: any) => void) =>
ipcRenderer.on('done', (_event, result) => callback(result))
});
The Main Process: Managing the PKCE Challenge & Local Loopback
In main.ts, we handle the complex orchestration. When connect is invoked, we spin up a micro HTTP server via Node's native http module to intercept the incoming redirect from the browser.
Here is the structural framework of the token acquisition process:
import { app, BrowserWindow, ipcMain, shell } from 'electron';
import * as http from 'http';
import * as crypto from 'crypto';
import axios from 'axios';
let accessToken: string | null = null;
let codeVerifier: string = '';
// Helper to generate PKCE Code Verifier and Challenge
function generatePKCE() {
codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return codeChallenge;
}
ipcMain.on('connect', async (event) => {
const challenge = generatePKCE();
// 1. Start temporary loopback server
const server = http.createServer((req, res) => {
const urlParams = new URL(req.url!, `http://${req.headers.host}`);
const authCode = urlParams.searchParams.get('code');
if (authCode) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Authentication successful! You can close this window.</h1>');
// Close the server immediately after capturing the code
server.close();
// 2. Exchange Code + Verifier for Tokens
exchangeCodeForToken(authCode, event);
} else {
res.writeHead(400).end('Auth code missing');
}
});
server.listen(8765, () => {
// 3. Open system browser for explicit user authentication
const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
`client_id=YOUR_CLIENT_ID` +
`&response_type=code` +
`&redirect_uri=http://localhost:8765` +
`&scope=Mail.Read offline_access` +
`&code_challenge=${challenge}` +
`&code_challenge_method=S256`;
shell.openExternal(authUrl);
});
});
async function exchangeCodeForToken(code: string, event: Electron.IpcMainEvent) {
try {
const response = await axios.post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
grant_type: 'authorization_code',
code: code,
redirect_uri: 'http://localhost:8765',
code_verifier: codeVerifier, // Proves identity without client_secret
}).toString(),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
accessToken = response.data.access_token;
// Cache tokens locally if persistence across sessions is desired
event.sender.send('progress', 'authenticated');
} catch (error) {
console.error('Token exchange failed', error);
}
}
Fetching and Extracting Hierarchical Data
Once accessToken is set, the application uses it to target the Microsoft Graph API endpoints asynchronously. Because mailboxes can grow infinitely large, the extraction handles recursive folder mapping and cursor-based pagination using the @odata.nextLink property returned by Microsoft.
ipcMain.on('list-folders', async (event) => {
try {
// Graph call to list recursive mail folders
const response = await axios.get('https://graph.microsoft.com/v1.0/me/mailFolders?$top=100', {
headers: { Authorization: `Bearer ${accessToken}` }
});
event.sender.send('return-folders', response.data.value);
} catch (error) {
console.error('Failed to fetch mail folders', error);
}
});
ipcMain.on('start-extraction', async (event, { folderIds, exportParams }) => {
for (const id of folderIds) {
let url = `https://graph.microsoft.com/v1.0/me/mailFolders/${id}/messages?$top=50`;
while (url) {
const res = await axios.get(url, { headers: { Authorization: `Bearer ${accessToken}` } });
const messages = res.data.value;
// Filter, transform, and stream raw email content to a local file path
// e.g., outputting to JSON, CSV, or raw .eml strings
url = res.data['@odata.nextLink']; // Keep pulling until pagination ends
}
}
event.sender.send('done', { count: 150, format: 'JSON' });
});
Conclusion
By marrying Electron’s isolated execution architecture with OAuth 2.0 + PKCE, you eliminate the need to maintain an intermediate backend service just to hold secrets or route tokens. The credentials live where the data lives: completely on the client machine. This keeps the integration footprint small, highly performant, and perfectly aligned with privacy best practices.
Thanks for reading 😊
Links
- Github repo for this post: https://github.com/aairom/outlook-bob
- IBM Bob: https://bob.ibm.com/





Top comments (0)