teams4e is an Emacs client for Microsoft Teams chats, channels, and meetings. It is inspired by mu4e: use bookmarks to select conversations, read them in a headers/message layout, and act on them without leaving Emacs. Replies can be composed in Org mode, and conversations can be captured into Org with links back to their source.
The package provides interactive commands, mode-local keymaps, and user
options. The bindings below are defaults, not a required workflow; use
M-x, bind the commands you need, and configure the views and actions to
suit your setup. mu4e, Evil, Spacemacs, and AI services are not required.
- Bookmarks defined by queries or Lisp predicates, with an unread-only filter that can be applied to the current view and removed again.
- Conversation actions for Org capture, Markdown export, read/unread state, snooze, and opening the source in Teams. Marks and bulk actions allow several conversations to be processed together.
- An Org-mode composer with HTML conversion on send, recoverable drafts, participant mentions, and attachments. Markdown and plain text are alternative editors. Recent messages remain visible while replying.
- One reusable reader for chats and channel threads, with images, attachments, message navigation, and optional rich rendering, link-hint, and expand-region integration.
- A meeting view with start times, intervals, participants, location, RSVP, joining, and an availability workspace for proposing another time: a daily timeline, availability and proximity rankings, and participant calendar blocks. Upcoming meetings use a calendar-first lookup for the next 14 days (configurable), without resolving every historical meeting chat.
- Optional Agent Shell commands for analysing an exported thread or discussing outstanding requests. These run only when invoked.
The main connection is between correspondence and the rest of an Emacs workflow. A conversation may need a reply, a task in Org, a reference in a project note, or no further attention. Bookmarks let you choose what to work on; the reader and conversation actions let you deal with it in place.
Like mu4e's bookmarks, teams4e bookmarks are named queries with configurable shortcut keys. Its Org capture actions serve a similar purpose to mu4e's Org integration: keep actionable material in your notes with enough context to return to the original correspondence.
This is an independent client, not a mu4e backend or an exact keymap clone. The usual unit of triage is a Teams conversation rather than a mail message. Org capture uses package-provided entries, not mu4e's link types or capture template fields. See Org capture and customization below.
Actual graphical Emacs captures using Moe Dark, Iosevka, and synthetic Teams data. Incoming formatting uses Agent Shell's optional Markdown renderer. Reader, Org reply, meeting details, and capture instructions.
Emacs 29.1+, Python 3.10+, and Git for installation from source. The bundled Python backend has no third-party Python dependencies.
Live access requires a compatible, approved Microsoft 365 integration to provide authentication, or a custom backend adapter. teams4e does not register an Entra application or implement device-code login. See authentication; the mock below needs no account.
Requirements: Emacs 29.1+ and Python 3.10+. The bundled backend uses only Python's standard library. Git is needed for a source-based installation.
Evaluate this in Emacs:
(require 'package-vc)
(package-vc-install "https://github.com/guibor/teams4e")
(require 'teams4e)
(setq teams4e-mock-mode t)
(teams4e)The persistent mock has synthetic people, chats, channels, messages, and meetings. It uses the real frontend/backend contract without reading tokens or contacting Graph. You can send mock replies, mark, snooze, search, and export.
Run M-x teams4e-mock-disable before configuring a live account.
The optional Agent Shell companion still uses your selected agent, even when
Teams itself is in mock mode. See LOCAL-TESTING.md.
In a managed organization, another Microsoft 365 integration may already own login, consent, and refresh. teams4e keeps those responsibilities there.
| Your existing integration can... | Configure... |
|---|---|
| Return a fresh delegated Microsoft Graph token | teams4e-token-command, the recommended boundary |
| Maintain a compatible credential record | A read-only teams4e-credentials-file |
| Expose tools but not tokens | Your own executable adapter behind teams4e-backend-program |
For a token helper:
(setq teams4e-token-command '("my-token-helper" "graph-token"))That executable is a placeholder, not a bundled command. Your helper must reuse the approved integration and print only a raw Graph access token or:
{"access_token":"REDACTED","expires_at":1786123456}It is invoked as an argument list, without a shell. The external owner handles login and refresh; teams4e consumes the short-lived token.
An existing MCP or DavMail login alone does not establish compatibility. If a service does not export Graph tokens, an adapter must implement the needed operations. No universal MCP adapter is bundled. This architecture avoids duplicating an approved login setup; it does not expand its permissions or bypass Entra consent or conditional access.
After configuration, run M-x teams4e-status, then M-x teams4e.
Use M-x teams4e-login only when an external bootstrap helper is configured.
AUTHENTICATION.md covers all three patterns, exact JSON formats, helper contracts, permissions, and troubleshooting. Chat can work without calendar access. Sovereign-cloud endpoints are not currently configurable.
Use the install command above, then configure normally:
(use-package teams4e
:commands (teams4e teams4e-inbox teams4e-meetings teams4e-status))Add the package recipe to your layer:
(teams4e :location
(recipe :fetcher git
:url "https://github.com/guibor/teams4e.git"
:files ("*.el" ("bin" "bin/*"))))Keep the bin/ directory: the package is Emacs Lisp and its Python backend.
This public recipe needs no private repository or personal configuration.
git clone https://github.com/guibor/teams4e.git ~/src/teams4e(add-to-list 'load-path (expand-file-name "~/src/teams4e"))
(require 'teams4e)Choose your own checkout directory. Make sure python3 is on Emacs's
executable path, especially when launching Emacs from a desktop icon.
Update the installation Emacs actually loads:
- package-vc:
M-x package-vc-upgrade RET teams4e RET. - Spacemacs/Quelpa: update through the package manager that installed it,
retaining the recipe's
binfiles. - Manual:
git -C ~/src/teams4e pull --ff-only, with your checkout path.
Restart Emacs so its Lisp and persistent backend use the same version.
M-x find-library RET teams4e RET identifies the loaded installation.
Pulling an unrelated checkout does not update your installed package.
Start with M-x teams4e. Use M-x customize-group RET teams4e RET for user
options, C-h f for a command's documentation, and C-h m for the active
mode's bindings. M-x teams4e-dispatch provides command help.
Use M-x teams4e-switch-to-buffer to resume Teams without reloading. An open
draft takes precedence; otherwise it returns to the last Teams view and
restores its reading layout where possible. First use opens the inbox normally.
A prefix argument refreshes the resumed view; in a draft it refreshes the
inbox without replacing the draft. For example, choose a global binding:
(global-set-key (kbd "M-s c") #'teams4e-switch-to-buffer)The package does not install a global binding. Explicit inbox and refresh commands retain their existing behavior. Background sync, when enabled, remains independent of this navigation command.
The following are selected default local bindings, primarily in the headers buffer. The chat reader delegates conversation actions to the linked headers buffer. They can be changed independently of the commands.
| Command | Default binding | Purpose |
|---|---|---|
teams4e-bookmark-jump |
b |
Select a configured bookmark |
teams4e-filter |
s |
Enter a query |
teams4e-toggle-unread-filter |
F |
Toggle unread-only within the current view |
teams4e-recent-open |
RET |
Open the selected conversation |
teams4e-reply / teams4e-send |
R / c |
Reply / compose |
teams4e-mark-read-later |
r or i |
Queue mark-read |
teams4e-execute-marks |
x |
Apply queued actions |
teams4e-snooze-quick / teams4e-snooze |
z / Z |
Default snooze / choose a wake time |
teams4e-capture-current-summary |
a a |
Start a compact Org capture |
teams4e-capture-current-thread |
a A |
Capture the available transcript |
teams4e-jump-to-capture |
a j |
Visit an existing capture |
teams4e-export-current-thread |
a e |
Export Markdown |
teams4e-analyze-current-thread |
a g |
Export for Agent Shell analysis |
The full usage reference covers reader navigation, bulk actions, snooze choices, message selection, and channel-specific commands. See changing bindings for examples.
teams4e-capture-current-summary starts an editable org-capture entry
containing the conversation title, source link, dates, participants when
available, and a short last/selected-message excerpt. It does not download
the whole thread. Meeting captures also include available calendar context.
Use this to turn a request into a task: capture it, give the heading a
meaningful title and a TODO keyword, and schedule or refile it using Org's
ordinary commands. The source link remains with the task. To include the
destination in your agenda, add it to org-agenda-files as you would any
other Org file; teams4e does not change your agenda configuration.
The destination is configurable:
(with-eval-after-load 'teams4e
(setq teams4e-capture-file "~/org/teams.org")) ; choose your own fileThe default value is nil, which resolves to teams.org under
org-directory, or under ~/Documents if necessary. The compact capture
opens an existing matching entry when one is found instead of creating another.
teams4e-jump-to-capture also returns to that entry.
For reference material, use teams4e-capture-current-thread to capture the
available transcript, or teams4e-export-current-thread to save Markdown.
teams4e-capture-current-message writes the selected message to the capture
file. These are separate choices: capturing an action need not copy a long
conversation into your notes.
The compact and full-thread commands supply their own editable capture
entries. They do not currently expose a user-defined capture-template option
or mu4e-compatible org-store-link fields. The captured entry is ordinary
Org text, not a second task database synchronized with Teams.
The teams4e-action-map prefix groups these commands with read/unread,
snooze, browser, and other conversation actions. You can bind its commands
directly or add bindings for your own interactive functions.
New drafts use org-mode with a compose minor mode.
teams4e-reply and teams4e-send open the editor; normal Org commands
remain available for emphasis, lists, links, tables, and source blocks.
For example:
*Quick update*
- *Done:* the pagination fix.
- *Next:* review [[https://example.org/review][the notes]].
teams4e-compose-send (default C-c C-c) exports an HTML fragment, so people
in Teams receive formatted text and clickable links, not raw Org syntax.
Recipients do not need Emacs or Org. Org composition uses Emacs's built-in
exporter; it does not require Pandoc or an AI service.
The reply buffer opens below the reader, keeping recent exchanges visible
while you write. Recoverable drafts retain their source and editor choice.
Real Teams @ mentions and attachments work in all three editors.
(setq teams4e-compose-editor 'org) ; default, built in
;; (setq teams4e-compose-editor 'markdown) ; markdown-mode + Pandoc
;; (setq teams4e-compose-editor 'text) ; legacy plain/direct-HTML editorThe default C-c C-k binding discards the draft; closing it does not close
your Emacs frame. Conversion errors preserve the draft instead of sending raw markup.
Org export does not execute Babel code; includes, setup files, macros, and
calls are rejected.
Markdown drafts similarly become HTML on send. They need the Emacs
markdown-mode package and Pandoc.
The text option retains the original plain/direct-HTML editor.
For rich incoming-message rendering, install Agent Shell;
its renderer runs locally without starting an agent. Otherwise, reading falls
back to plain text. Composition details.
In a composer, C-c C-s queues the message for the next configured workday
at teams4e-workday-start (07:00 by default); C-c C-t chooses a date/time.
M-x teams4e-reply-next-workday opens a scheduled reply draft, and
M-x teams4e-outbox lists pending deliveries with edit and cancel actions.
Workdays and timezone are customizable; Org/Markdown source and reply identity
are preserved.
This is a durable local outbox using the documented Graph send API, not Teams' native scheduled-message service. Emacs must be running and authenticated. Late jobs are held; ambiguous send outcomes are not automatically retried. Scheduled file attachments are not yet supported. See scheduled sending for setup, keymaps, DST behavior, recovery, and limitations.
M-x teams4e-find-chat (default M-f in the inbox and reader) finds
existing chats outside your recents. Enter a group name, participant name/email,
or several words such as launch ada; all words must match across the chat's
name and membership. Choose a result with completion and press RET to open it
in the normal reader. One-to-one, group, and meeting chats are included.
This explicitly scans all pages of your chat metadata, ignoring inbox filters and the recent-chat limit, without fetching messages or calendars. Large chat collections can take longer. It uses the documented list-chats API with expanded members; potentially truncated member lists are checked separately when needed. Failed membership lookups are reported as incomplete search rather than silently implying there is no matching group. It does not create chats, change the inbox snapshot, or add a separate persistent directory.
Use teams4e-server-search for message contents and channel threads;
teams4e-search searches only cached messages. Finding chats requires online
mode and an updated backend as well as Lisp. teams-find-chat is an alias;
the command is also available in the H command palette.
Opening a chat fetches the newest 100 messages, without a date cutoff.
Automatic previews remain limited to 50. The live request starts alongside
the SQLite cache read; a late cache result cannot replace newer network data.
teams4e-chat-load-more (default L) loads more;
teams4e-chat-load-all (default G) requests complete available history.
Configure teams4e-message-days and teams4e-message-limit to change the
initial bounds; nil removes the respective bound.
Export and agent analysis make a separate request that follows every Graph pagination link, records counts and the date range, and rejects incomplete results. "Complete" means everything the API makes available to your account, not messages deleted or unavailable under retention/access policies.
M-x teams4e-meetings orders upcoming/active meetings by start time, with
intervals, location, and response state. Normal message views stay ordered by last message.
Inspect participants, RSVP, join, or open the Outlook event. The availability workspace compares returned free/busy information and calendar blocks, offers alternate slots, and lets you propose a new time. Calendar permissions and sharing policy determine what is visible; unavailable data is not "free."
In the time picker, D compares everyone's working day, f offers common-free
stretches, and m enters an exact time. d, a, and c select the continuous
timeline, availability ranking, and proximity-to-original ranking. RET reviews
the old and new times, duration, and conflicts; C-c C-c sends the update or
proposal, while C-c C-k goes back. Your own and shared calendar blocks remain
visible alongside the choice (C toggles that pane). These are customizable
bindings in teams4e-availability-mode-map, not required interaction rules.
See calendar actions and availability for sharing limits and
conservative handling of the original meeting's occupied slot.
This is a useful calendar companion, not a complete replacement for Outlook. Meeting workflow.
M-x teams4e-calendar opens an event-first Outlook calendar, separate from the
Teams meeting-chat view. Ordinary appointments, focus blocks, all-day events,
and recurring occurrences appear even when there is no associated Teams chat.
The day-grouped agenda centers on the current or nearest time on first load.
By default it shows seven days from today, including next week's workdays even
when opened on Thursday. t resets that window to today; w starts it on the
selected date. Traditional calendar-week boundaries remain configurable with
teams4e-calendar-week-start.
Availability, not RSVP, drives the colors: busy, tentative, out of office,
working elsewhere, and subdued free events. Free gaps within configurable
working hours use the full snapshot, even when a text filter hides events;
incomplete loads never imply that time is free. Exact overlap intervals share
labels with the affected events; the invite reader links to conflicting events,
even when a text filter hides them from the agenda.
Each day has a foldable summary: highlighted free intervals and total available
hours, unanswered invitations first, then conflicts, tentative and accepted
meetings, and events you organize. Its compact lists omit declined, cancelled,
and free/following invitations. Actual free slots have a full-width green band
and [FREE] label, separate from non-blocking appointments. Accepted-summary
[Cx] markers flag clashes with other accepted meetings only.
TAB folds the Summary heading or a conflict group, never the day or timeline.
Expanding a conflict lists its meetings with actions to respond, propose another
time, reschedule your own event, or open Outlook. Summaries and actions use the
same calendar snapshot; unfolding does not fetch anything.
j/k (also M-j/M-k or J/K) visit summary headings and entries; in the
timeline they move between events. h/l (or H/L) move between
days. t centers on now; N goes to the ongoing/next slot, or tomorrow after
today's last slot. . or G chooses a date with Org's date reader,
and d/w/m select day/week/month. [/] move between whole ranges.
RET opens a reusable invite reader; / filters locally and c selects
another calendar. Invitation actions (a), availability and rescheduling (r or A),
Teams chat (T), Org capture (C), join (v), and Outlook (o) act
on the selected event. All bindings, faces, and working-hour defaults are
customizable. The time workspace proposes alternatives for invitations and moves
one timed occurrence for events you organize, without changing the series.
In either the agenda or invitation reader, a a accepts, a t responds
tentatively, a d declines, and a r chooses a new time. The action menu also
offers Org capture, Teams chat, join, and Outlook when applicable. Accept with
an empty note to avoid notifying the organizer. ? remains the full calendar
command menu; all these bindings are customizable.
The time picker opens a companion calendar pane: your own day's events first,
then the participants' shared blocks, with full titles, locations, and highlighted
overlaps for the selected slot. j/k updates it locally; [/] changes day.
C toggles the pane (teams4e-meeting-show-calendar-context controls its default).
b still shows the full block sheet. Your own default calendar is queried for
event details once per loaded search range; shared subjects and locations depend
on Exchange sharing permissions. Private blocks remain masked, and unavailable
details are never presented as confirmed free time. No new consent is requested.
S toggles optional duration-scaled blocks: longer meetings and free intervals
take more vertical space. The compact agenda remains the default. Customize
teams4e-calendar-duration-scaled and teams4e-calendar-minutes-per-row to suit
your reading preferences. This remains an agenda rather than a spatial grid;
overlapping meetings are still separate entries.
A compact working-hours map makes the day's shape visible without expanding it:
free time, occupied intervals, genuine overlaps, and unknown availability have
distinct marks. F finds a future free slot using completion (C-u F changes
the minimum duration), and + opens an Org meeting draft that fits the selected
gap. ! visits invitations still needing a response; C-u ! goes backward.
These commands reuse the loaded calendar and never send invitations or responses
just by browsing. See calendar planning for
the defaults, customizations, and limits of this single-calendar view.
For quick planning, s jumps to a visible meeting without changing your filter;
} / { cycle through conflicts and unfold their per-meeting actions. B
drafts a personal focus block in a fitting free slot, with no attendees or Teams
link. The draft stays editable in Org and is saved only on explicit submission.
W opens the week-at-a-glance overview: each day's blocked/free working hours,
longest free stretch, blocking-event count, conflicts, and unanswered invitations.
Overlaps count only once toward occupied time. Select a day to enter its agenda.
? opens context-sensitive actions in the agenda and invite reader, showing
your actual active bindings rather than hard-coded defaults. Both features reuse
the current snapshot; see overview and actions
for metric definitions and customization.
+ opens an Org meeting draft beside the agenda. Edit its title, times,
required/optional attendees, location, and description; C-c C-a searches the
directory and C-c C-t chooses a time. Only C-c C-c creates the event and sends
invitations. Org descriptions become HTML, and the optional Teams link is created
by Outlook. This first version supports single timed events, not recurring or
all-day creation. See meeting drafts and retry behavior.
It reuses your existing token provider and rate-limited backend, fetching only
the displayed range. Reopening does not reload; g or a prefix argument does.
Refresh preserves the selected item and each window's reading position, with a
nearby-item fallback if an event disappears. Native Follow is not supported by
the documented Graph response API; see the Follow limitation.
No event-to-chat conversion or calendar-to-Org synchronization is involved.
Update the installed backend as well as the Lisp package for this feature.
Staged runtimes need all Python sibling modules, including teams4e_calendar.py.
See Calendar setup, defaults, and boundaries, including the event reader and expanded conflict actions.
teams4e-analyze-current-threadexports the full available conversation, then opens Agent Shell with a configurable prompt and agent.teams4e-companionopens a reusable discussion of open requests, commitments, and possible next actions. Monitoring starts when you invoke it; it is bounded, visible, and pausable.
Neither starts during ordinary reading. Calendar context and reference files are opt-in. The companion does not send messages or mutate calendars itself, but it does not restrict your agent's tools or permissions. Choose a provider approved for the content. Companion setup and limits.
User options belong to the teams4e customization group. Use Customize or
set them in your init file after loading the package. For example:
(with-eval-after-load 'teams4e
;; These values show the current defaults; change them to suit your setup.
(setq teams4e-compose-editor 'org
teams4e-mark-read-on-open nil
teams4e-preview-on-move nil
teams4e-message-order 'oldest-first
teams4e-message-days nil
teams4e-message-limit 100
teams4e-chat-metadata-limit 300
teams4e-default-snooze-minutes 180))Authentication, paths, work hours, browser/app launchers, rendering, and agent
instructions are also configurable. Sending and applying marks do not ask for
an extra confirmation by default; set teams4e-confirm-send and
teams4e-confirm-apply to t to enable one.
teams4e-bookmarks is a list of plists with :name, :query, and
:key. A query can be a built-in view symbol, a query string, or a function
accepting a chat. For example, add a project view without replacing the
built-in bookmarks:
(with-eval-after-load 'teams4e
(add-to-list 'teams4e-bookmarks
'(:name "Project Atlas"
:query "name:Atlas"
:key ?p)
t))With the default bookmark binding, this makes b p select that view.
Choose an unused shortcut of your own. Query terms include unread,
favorite, mentioned, type:meeting, name:TEXT, and after:7d.
See query syntax for combinations and predicates.
teams4e-toggle-unread-filter narrows the current view without replacing
its query; invoke it again to remove that restriction. Active snoozes stay
hidden from ordinary views, including All; the Snoozed bookmark exposes them.
Snooze is local and does not change Teams read state.
Use the package's mode maps rather than global bindings for buffer-local actions. For example, add an alternative unread-filter binding in both the headers and chat reader, and an Org-capture alias under the action prefix:
(with-eval-after-load 'teams4e
(keymap-set teams4e-recent-mode-map "C-c u"
#'teams4e-toggle-unread-filter)
(keymap-set teams4e-chat-mode-map "C-c u"
#'teams4e-chat-toggle-unread-filter)
(keymap-set teams4e-action-map "n"
#'teams4e-capture-current-summary))These are examples, not additional package defaults. The last form adds
a n alongside the default a a binding. Use keymap-unset if you
also want to remove an old binding. Your own interactive commands can be added
to the same prefix map; this does not add them to the separate dispatcher menu.
With Evil, state-specific maps may take precedence over ordinary mode maps. For example, add the same reader binding in Normal and Motion states from your init file:
(with-eval-after-load 'teams4e
(with-eval-after-load 'evil
(evil-define-key* '(normal motion) teams4e-chat-mode-map
(kbd "C-c u") #'teams4e-chat-toggle-unread-filter)))The package refreshes some built-in Evil bindings when its modes start. When overriding those keys, put your customization in a mode hook registered after loading teams4e. Ordinary mode hooks can also configure buffer-local settings; no Evil or Spacemacs configuration is needed for non-modal use.
The package ships no organization-specific login owner, tenant ID, credentials, private-repository dependency, or required agent skill. Demos and fixtures use synthetic data. Ordinary use does not require AI services.
Your runtime data is still sensitive. Caches, drafts, images, downloads, exports, captures, and agent snapshots may contain workplace information. Local storage is not an encrypted vault; keep it outside Git and use suitable device protections. Redact diagnostics before sharing them.
SECURITY.md describes data boundaries and safe reporting. PUBLIC-READINESS.md records the publication audit and its limits; a clean secret scan is not a guarantee of security.
The Emacs frontend owns views, composition, capture, and navigation. The bundled Python adapter owns Graph requests, pagination, retries, caching, and the mock. An external provider owns authentication. See DESIGN.md.
make test
make compile
git diff --checkNo account is needed for the test suite. CI also exercises real Evil, link-hint, expand-region, and Org/Markdown composition. Live tenant behavior still needs live validation.
| Guide | What it covers |
|---|---|
| Authentication | Token helpers, credential files, adapters, and permissions |
| Usage | All keys, views, snooze, composition, meetings, capture, and exports |
| Local testing | Synthetic tenant, latency simulation, fixtures, and integration testing |
| Companion | Optional ongoing agent discussion and its data scope |
| Contributing | Portable changes and reproducible, redacted reports |
| Changelog | Changes and compatibility notes |
Feedback and focused PRs are welcome, especially on accessibility, plain-Emacs workflows, and adapters for approved integrations. Please use synthetic examples in public reports. An r/emacs announcement draft is included.
M-x teams4e, M-x teams, and M-x teams4e-inbox open the same inbox.
The former msteams Lisp package and executable remain compatibility shims;
new configuration should use teams4e. Microsoft's msteams:// URL scheme
is unrelated to that rename.
GNU General Public License version 3 or later. See LICENSE.


