Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ bundle exec bin/mail_mcp generate \
| `list_mailboxes` | — | List all IMAP folders |
| `list_mail_messages` | `folder`, `page`, `per_page` | List messages with pagination |
| `get_mail_message` | `folder`, `uid` | Fetch full message; attachments uploaded to S3 and returned as presigned URLs |
| `search_mail_messages` | `folder`, `query` | Raw IMAP SEARCH criteria, e.g. `UNSEEN` or `FROM alice@example.com SINCE 01-Jan-2025` |
| `search_mail_messages` | `folder`, `query` | Raw IMAP SEARCH criteria, e.g. `UNSEEN` or `FROM alice@example.com SINCE 01-Jan-2025`; returns UIDs |
| `send_mail_message` | `to`, `subject`, `text_body`, `cc`, `bcc`, `html_body`, `attachment_urls`, `folder` | Send via SMTP and append to the Sent folder via IMAP; attachments fetched from S3 presigned URLs |
| `create_draft_mail_message` | `to`, `subject`, `text_body`, `cc`, `bcc`, `html_body`, `attachment_urls`, `folder` | Append to Drafts via IMAP APPEND; attachments fetched from S3 presigned URLs |
| `delete_mail_message` | `folder`, `uid` | Mark `\Deleted` + EXPUNGE |
Expand Down
5 changes: 4 additions & 1 deletion lib/mail_mcp/imap_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ def get_message(folder:, uid:)
def search_messages(folder:, query:)
MailMCP.logger.info { "IMAP search_messages folder=#{folder.inspect} query=#{query.inspect}" }
@imap.examine(folder)
results = @imap.search(query.split)
# UID SEARCH, not SEARCH: the ids returned here are fed back into
# get_message/delete_message/move_message/update_flags, which all issue
# UID commands. Sequence numbers would only match until the first expunge.
results = @imap.uid_search(query.split)
MailMCP.logger.debug { "IMAP search_messages matched=#{results.size}" }
results
end
Expand Down
3 changes: 2 additions & 1 deletion lib/mail_mcp/tools/search_mail_messages_tool.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module MailMCP
class SearchMailMessagesTool < Tool
tool_name "search_mail_messages"
description "Search messages in an IMAP folder using raw IMAP SEARCH criteria"
description "Search messages in an IMAP folder using raw IMAP SEARCH criteria. " \
"Returns message UIDs usable with get_mail_message"
annotations(
title: "Search Mail Messages",
read_only_hint: true,
Expand Down
6 changes: 3 additions & 3 deletions spec/mail_mcp/imap_client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@
end

describe "#search_messages" do
it "passes raw query string to IMAP SEARCH" do
allow(imap).to receive(:search).with(["UNSEEN"]).and_return([1, 2, 3])
it "passes raw query string to IMAP UID SEARCH" do
allow(imap).to receive(:uid_search).with(["UNSEEN"]).and_return([1, 2, 3])
client = described_class.new(imap)
result = client.search_messages(folder: "INBOX", query: "UNSEEN")
expect(result).to eq([1, 2, 3])
end

it "supports multi-word criteria" do
allow(imap).to receive(:search).with(["FROM", "alice@example.com"]).and_return([5])
allow(imap).to receive(:uid_search).with(["FROM", "alice@example.com"]).and_return([5])
client = described_class.new(imap)
result = client.search_messages(folder: "INBOX", query: "FROM alice@example.com")
expect(result).to eq([5])
Expand Down
Loading