- C# 99.1%
- Just 0.9%
| assets | ||
| data | ||
| specs | ||
| src | ||
| .gitignore | ||
| AGENT.md | ||
| justfile | ||
| LICENSE | ||
| README.md | ||
| Ubermannschaft.sln | ||
Ubermannschaft
A local tool that syncs your Microsoft Teams conversations into a portable SQLite database.
Connecting without an Azure app registration
The linked guide assumes you can register an Azure application, and most people cannot. Getting Graph access on a locked-down tenant turned out to need two fallbacks; the tool now handles both.
Why the obvious routes don't work here
The supported route is Microsoft Graph via the device code flow against a Microsoft-published public client — no app registration needed. On this tenant that hits two walls in turn:
- AADSTS50105 — the admin blocked the Graph command-line app for unassigned users. That block is per-application, so the tool tries other first-party apps (Azure CLI, Office, Teams) too.
- AADSTS65002 — but those apps aren't preauthorized to call Graph with Teams scopes like
Chat.Read. Only the blocked app was. So device sign-in cannot work here at all.
How it connects instead: reuse the Teams client's own token
The Teams desktop client signs in normally — its app is preauthorized for those scopes — and
Windows caches the Microsoft Graph tokens it obtains in the local token broker
(%LOCALAPPDATA%\Microsoft\TokenBroker\Cache), sealed with DPAPI for your account. The tool reads
that cache, decrypts the tokens you already hold, and reuses them against Graph. No sign-in, no
consent prompt, no app registration — it sees exactly what you see in Teams, nothing more.
Different Teams sub-apps cache different scopes, so the tool picks the right cached token per request (a channel-capable token for channel messages, a chat token for chats, and so on). Because Teams keeps refreshing these tokens, a long-running monitor stays authenticated as long as Teams is installed and signed in. If a needed token has expired and Teams hasn't refreshed it, the tool asks you to open Teams.
Run just login to confirm it can connect and see which capabilities are available:
account you@example.com
channels available
╭─────────────────────┬─────────────────────────┬───────────╮
│ Capability │ Scope │ Available │
├─────────────────────┼─────────────────────────┼───────────┤
│ chats & group chats │ Chat.Read │ yes │
│ channel messages │ ChannelMessage.Read.All │ yes │
│ … │ │ │
╰─────────────────────┴─────────────────────────┴───────────╯
Choosing the method
AuthMode in data/settings.json (or appsettings.json) controls this:
| Value | Behaviour |
|---|---|
auto |
Reuse Teams' tokens on Windows; fall back to device sign-in elsewhere. (Default) |
broker |
Always reuse Teams' cached tokens. Windows + Teams only. |
device |
Always use the interactive device code flow (needs a tenant that allows it). |
In device mode you can force a specific client with just login --client teams (also
azure-cli, office, graph-cli), or pin your own registration with { "ClientId": "<guid>" }.
Note on token reuse: it reads an undocumented Windows cache, so it is Windows-only and may break with a future Teams or Windows update. It never sends your tokens anywhere — they are used only for your own Graph requests and are not copied into the database.
Running it
just login # sign in and confirm which client your tenant accepts
just monitor # backfill, then keep watching
just sync-once # one pass and exit
just viewer # open the desktop app to browse and search messages
just status --chats # what is stored locally, no network
just logout # forget cached tokens
Without just, the same thing directly:
dotnet run --project src/Ubermannschaft/Ubermannschaft.csproj -- monitor
First run prints a device code; open the URL, enter the code, and approve the sign-in. Tokens are
cached in data/token-cache.bin so later runs are silent.
What it does
On startup it scans everything not yet persisted, then stays resident and polls for new activity
every 60 seconds (--interval to change). Graph change notifications would need a public HTTPS
endpoint that a machine-local tool cannot offer, so the monitor polls; repeat passes use Graph
delta queries, so they only transfer what changed.
Per pass it walks:
- every one-on-one chat, group chat and meeting chat (
/me/chats), including membership; - every channel of every team you have joined (
/me/joinedTeams), including reply threads; - every message body, sender, edit and deletion;
- every attachment — files via the Graph
/sharesendpoint, inline images and code snippets viahostedContents.
Browsing and searching
just viewer opens a native Windows desktop app (WPF, in src/Ubermannschaft.Viewer) over the
database — read-only, so it is safe to browse while the monitor keeps syncing.
- Global search (top bar): type a term and press Enter. Results are cards, one per matching message, showing the conversation and a preview with the term highlighted, most recent first, case-insensitive. Click a card to jump to that message in context.
- Conversation list (left): chats first, then teams & channels, most recent at the top. The box above filters the list as you type. Click a conversation to open it, newest message at the bottom; scroll up to load earlier messages.
- Find in conversation: press Ctrl+F while a conversation is open. Enter or ↑ / Ctrl+Up searches into the past, ↓ / Ctrl+Down searches toward the present; the match is highlighted and centered. Esc closes the find bar.
The message-body HTML is rendered as plain text, so nothing from a message can execute, and remote images are never fetched.
Data layout
data/ubermannschaft.sqlite3, lower-case tables:
| Table | Purpose |
|---|---|
chats |
One row per chat, group chat or channel: type, topic, team/channel, delta link. |
chat_members |
The users in each chat. |
messages |
Message bodies, senders, timestamps, edits and deletions. |
attachments |
One row per attachment, referencing its message and its file on disk. |
sync_state |
Bookkeeping (signed-in account, last completed pass). |
Attachment bytes land in data/attachments/{chat}/{message}/; attachments.local_path holds the
path relative to data/. Every row also keeps the original Graph JSON in a raw_json column, so
nothing is lost if a field is needed later.
Permissions
The tool only ever uses access you already have. In token-reuse mode that is whatever the Teams
client was granted (typically chats, channels, and files all together). In device mode, chats need
only permissions you can consent to yourself (Chat.Read, Files.Read.All, Sites.Read.All),
while channels additionally need ChannelMessage.Read.All, which requires an administrator. When a
capability isn't available the tool says so and continues with what it can reach — it does not fail.
Configuration
src/Ubermannschaft/appsettings.json holds the defaults. Override them per machine by creating
data/settings.json with the same keys, or with UBERMANNSCHAFT_-prefixed environment variables
(for example UBERMANNSCHAFT_PollIntervalSeconds=30).
| Key | Default | Meaning |
|---|---|---|
AuthMode |
auto |
auto, broker (reuse Teams tokens), or device (sign in). |
ClientId |
(empty) | Device mode: pin sign-in to one app (GUID or alias). Empty = auto-discover. |
ClientCandidates |
four first-party apps | Device mode: the apps auto-discovery tries, in order. |
TenantId |
organizations |
Tenant to sign in against. |
PollIntervalSeconds |
60 |
Wait between passes. |
SyncChannels |
true |
Include teams and channels. |
DownloadAttachments |
true |
Fetch attachment bytes, not just metadata. |
MaxAttachmentBytes |
100 MB |
Skip anything larger. 0 disables the limit. |
ChannelReplyLookbackDays |
30 |
How far back to re-check channel reply threads. |
Projects
| Project | What it is |
|---|---|
src/Ubermannschaft |
The ubermannschaft CLI: monitor, login, status, logout. |
src/Ubermannschaft.Viewer |
The WPF desktop viewer launched by just viewer. |
Requirements
.NET 10 SDK, and just for the shortcuts. Open Ubermannschaft.sln in Visual Studio 2026. The
viewer is a WPF app, so it builds and runs on Windows.
License
Licensed under the GNU Affero General Public License v3.0. Because the AGPL's network clause applies, anyone you offer this to over a network is entitled to its corresponding source.