Skip to content

Mail: add tools to move, flag and tag messages #59

Description

@oleksandr-nc

Summary

The Mail tools can list accounts, mailboxes and messages, read a message, and send mail, but they can't organize messages. Add tools to move messages between mailboxes, set or clear flags (read/unread, starred, answered) and tag messages, so an inbox can be triaged through the MCP server.

Which Mail API to use

The Mail app's OCS API only covers reading and sending (checked against nextcloud/mail main). Paths are relative to /ocs/v2.php/:

OCS endpoint Used by
GET apps/mail/account/list list_mail_accounts
GET apps/mail/ocs/mailboxes?accountId= list_mailboxes
GET apps/mail/ocs/mailboxes/{mailboxId}/messages list_mail_messages
GET apps/mail/message/{id} (also /raw, /attachment/{attachmentId}) get_mail_message
POST apps/mail/message/send send_mail

There is no OCS endpoint for moving, flagging or tagging. These exist only as the Mail web UI's JSON routes (appinfo/routes.php, lib/Controller/MessagesController.php, lib/Controller/TagsController.php). Paths are relative to /index.php/apps/mail:

Action Route Request body
Move a message POST /api/messages/{id}/move {"destFolderId": <mailbox id>}
Set flags PUT /api/messages/{id}/flags {"flags": {"seen": true, "flagged": false}}
Tag a message PUT /api/messages/{id}/tags/{imapLabel} none
Untag a message DELETE /api/messages/{id}/tags/{imapLabel} none
Create a tag POST /api/tags {"displayName": "...", "color": "..."}
Update a tag PUT /api/tags/{id} {"displayName": "...", "color": "..."}
Delete a tag DELETE /api/tags/{accountId}/delete/{id} none

What to know about these routes:

  • CSRF (main risk): the controller methods aren't marked #[NoCSRFRequired]. Nextcloud skips the CSRF check for requests that send OCS-APIRequest: true, so Basic auth with an app password should work with that header. Confirm this with an integration test before building the rest.
  • Flags: setFlags loops over flag => bool and calls MailManager::flagMessage(). Supported names are in the map at the top of lib/Service/MailManager.php: seen, answered, flagged, deleted, draft, recent.
  • Moving returns nothing, and the message ID changes. move returns an empty response. IMAP UIDs are per mailbox, so the moved message gets a new UID, and a new database ID once the destination mailbox is synced. Callers must not reuse the old ID. MailManager::moveMessage() also supports moving to a mailbox in another account.
  • Tags belong to the user and are addressed by IMAP label. MailManager::createTag() derives the label from the display name and returns the existing tag if that label already exists, so POST /api/tags is create-or-get and returns the tag as JSON. No JSON route lists all tags (the web UI reads them from page initial state), but message JSON (Db\Message::jsonSerialize()) includes tags, keyed by IMAP label, next to flags. TagsController validates the display name and color.

Prior art: cbcoutinho/nextcloud-mcp-server calls these routes with OCS-APIRequest: true (see its docs/mail.md). It's AGPL-3.0 and this repo is MIT, so use it only to confirm behavior. Don't copy code from it.

Proposed changes

Client (src/nc_mcp_server/client.py)

Add a helper for non-OCS app routes, for example app_request_json(method, path, json_data=None, params=None). It should:

  • call /index.php/apps/{path} with the OCS-APIRequest: true header and an optional JSON body
  • parse plain JSON responses (there's no OCS envelope) and handle empty bodies
  • raise NextcloudError on non-2xx, like the existing helpers
  • go through _do_request, so retries and auth handling stay the same

Tools (src/nc_mcp_server/tools/mail.py)

Tool Permission Annotation
move_mail_message(message_id, destination_mailbox_id) write ADDITIVE
set_mail_message_flags(message_id, seen=None, flagged=None, answered=None) write ADDITIVE_IDEMPOTENT
create_mail_tag(display_name, color) write ADDITIVE_IDEMPOTENT
add_mail_message_tag(message_id, imap_label) write ADDITIVE_IDEMPOTENT
remove_mail_message_tag(message_id, imap_label) write ADDITIVE_IDEMPOTENT
  • move_mail_message: the docstring must say the old message ID stops being valid after the move.
  • set_mail_message_flags: send only the flags that were passed, and reject calls that pass none. Leave out deleted, draft and recent; if deleting is added later, it belongs behind destructive.
  • create_mail_tag: return id, display_name, imap_label and color.
  • Add tags (display name and IMAP label) to the output of list_mail_messages and get_mail_message, so callers can see current tags and look up labels.
  • Updating or deleting tags is optional. If added, deleting needs destructive.

Tests

  • Unit tests in tests/, following tests/test_client_errors.py: the new client helper (URL, OCS-APIRequest header, JSON body, empty responses, error mapping) and the tools' argument validation.
  • Integration tests in tests/integration/test_mail.py, reusing _send_test_email, _sync_mail_account, _get_account_id and _get_inbox_id:
    • move a freshly sent message to another mailbox and find it there after sync
    • set and clear seen and flagged, and check the result with get_mail_message or list_mail_messages
    • create the same tag twice and get the same label back; tag and untag a message and check tags in the output
    • extend TestMailPermissions: read-only blocks each new tool, write allows it
  • CI uses smtp4dev as the IMAP server (.github/workflows/tests-integration.yml). Check early that its IMAP server supports MOVE, flag changes and custom keywords (tags). If it doesn't, move the Mail tests to a fuller IMAP server instead of skipping them (cbcoutinho's project uses GreenMail).

Docs (README.md)

  • Add the new tools to the Mail tool table (around line 343).
  • Update the Mail row in the category table (line 57). It currently lists "accounts, mailboxes, messages, send" with protocol "OCS", but the new tools use the Mail app's JSON routes.
  • Fix the tool count: the heading says 140 tools, but the server already registers 156.

Acceptance criteria

  • The five tools are registered with the permission levels and annotations above
  • list_mail_messages and get_mail_message include tags
  • Unit and integration tests cover success, validation errors and permission checks
  • Mail integration tests pass in CI
  • README updated

Notes

  • Integration CI is currently red for unrelated reasons (see the runs on Pin mcp below 2.0 to fix fresh installs #58): admin endpoints return "Password confirmation is required", Circles calls get HTTP 429, and the NC 33 job can't create an app password during setup. None of those failures are in test_mail.py.
  • ocs/mailboxes/{mailboxId}/messages returns the threaded view unless view=singleton is passed, and list_mail_messages doesn't pass view. Check which one triage needs.
  • Out of scope, possible follow-ups: replying in a thread (send_mail has no In-Reply-To/References support), exposing the list endpoint's filter parameter for server-side search, deleting messages.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions