Skip to main content

hermes-yandex-mail

PyPI version CI E2E (live) Coverage CodeFactor Ruff License: MIT

Let your Hermes Agent work through your Yandex inbox. "What came in overnight?" — "Read me the one from the bank." — "File everything from GitHub into Archive and mark it read." The agent works on your real mailbox, over IMAP, with no third-party service in the middle.

  • 📬 Eight tools, one toolset — list folders with unread counts, search, read (paged text plus attachment list), flag, move, delete, and — only if you switch them on — save attachments to files and send.
  • 📎 Attachments become files, never context — the agent saves one to Hermes' document cache and gets back its path, so a PDF can be opened by other tools or sent to you in chat without its bytes passing through the model.
  • ✉️ Sending and saving are off until you name them — send_message and save_attachment are not in all and not granted by leaving the allow-list empty, so upgrading never hands a running agent the ability to write as you, or the content of your attachments.
  • 🔒 You choose what it may touch — restrict it to specific folders, and to specific actions (read, read,write, …). A disallowed action is not in the toolset at all, so the model cannot be talked into calling it.
  • 🛟 Nothing is lost, and nothing is over-claimed — a copy exists before an original is removed, deletion means Trash unless you insist otherwise, an expunge only ever names the UIDs it was given, and a UID that no longer exists is refused instead of reported as done.
  • 👓 Reading does not mark as read — the agent peeks; \Seen changes only when you ask.
  • 🪶 No runtime dependencies — IMAP and MIME parsing come from Python's own standard library.
  • 🔑 App password, not your account password — scoped to mail, revocable in one click.

Checked before every release against the latest Hermes release and Hermes main, each installed by its official installer; Python 3.11–3.14.

Quick start

# 1. Install into Hermes (other ways: see Installing the plugin). It asks for
#    your login and an app password, which comes from
#    https://id.yandex.ru/security/app-passwords (scope: "Почта" / Mail)
hermes plugins install akinfold/hermes-yandex-mail/hermes_yandex_mail --enable

# 2. Skipped the questions, or installed another way? Add the credentials yourself:
printf 'YANDEX_MAIL_LOGIN=%s\nYANDEX_MAIL_APP_PASSWORD=%s\n' \
  'you@yandex.ru' 'your-app-password' >> ~/.hermes/.env

--enable writes the plugin's name into ~/.hermes/config.yaml for you — third-party plugins are off until something does:

plugins:
  enabled: [yandex_mail]

Before the first run, switch IMAP on for the mailbox — see Enabling IMAP. Yandex refuses to log in otherwise, and the error looks exactly like a wrong password.

That's it. Ask the agent "anything unread in my inbox?" and it will tell you.

Not ready to hand over write access? Add YANDEX_MAIL_ACTIONS=read and it can only look — see Restricting what the agent can do.

The tools

Up to eight standalone tools, in the yandex_mail toolset:

Tool Purpose
yandex_mail_list_folders List folders with their role (inbox, sent, trash, junk, drafts, archive) and total/unread counts.
yandex_mail_search_messages Search a folder by sender, recipient, subject, full text, date range, unread or flagged state; returns subject, addresses, date, size, flags, and the uid. Pages with offset, and reports total so you know whether more exist.
yandex_mail_read_message Read one message: headers, the text body in pages (HTML-only mail is converted to text), and the attachment list. Attachments are not downloaded. Peeks by default.
yandex_mail_save_attachment Off by default. Save one attachment to a file in Hermes' document cache and return its path, name, type, size and SHA-256 — never its content.
yandex_mail_mark_message Mark messages read/unread and flagged/unflagged.
yandex_mail_move_message Move messages to another folder, reporting which UID each message was verified to have on arrival.
yandex_mail_delete_message Delete messages — to Trash by default. A message already there is left untouched; permanent deletion is a separate, irreversible request.
yandex_mail_send_message Off by default. Send a plain-text message, optionally threaded as a reply. Files a copy in Sent and reports exactly which recipients the server accepted.

Yandex Mail has no public REST API, so this plugin speaks IMAP (imap.yandex.ru:993) directly — the same protocol Yandex documents for mail clients. Nothing is proxied through anyone else's servers.

Messages are addressed by folder + uid, and both are required for read, save, mark, move, and delete: UID numbering is independent per folder, so a UID paired with the wrong folder would silently name a different message. Every result reports the folder in the server's own spelling — pass that value straight back. Several UIDs can be given at once, comma-separated: "101,102"; they are acted on all-or-nothing, so a batch containing a UID that no longer exists is refused rather than half-applied.

Folder names are matched generously on the way in — spam, Spam, junk and Корзина all resolve to the right mailbox — because IMAP itself is case-sensitive and would simply answer "No such folder".

Sending goes over SMTP (smtp.yandex.ru:465) with the same app password, and is off unless you switch it on — see Sending mail. So is saving attachments — see Saving attachments.

Reading long messages

yandex_mail_read_message returns the body a page at a time: 20 000 characters by default, up to 100 000 with max_chars. While the result says eof: false, the next call passes its next_offset as offset:

{"uid": "101", "folder": "INBOX", "offset": 20000}

Offsets count characters of the text the tool returns — after an HTML body has been converted — so the pages join back together exactly. On the last page eof is true and next_offset is null.

Only the text parts of a message are downloaded, in blocks of 64 KiB, and never its attachments: a message carrying a 25 MB attachment reads as quickly as one without. The plain-text body is preferred over an HTML one, and the text of a forwarded message is part of the body. Attachments are listed with a part_id, name, MIME type, and a size estimated from their encoded size on the server, which can be off by a few bytes. To get one as a file, see Saving attachments.

Paging keeps no copy of your mail: every page decodes the text from its beginning again, so a deep page of a very long message costs more time and traffic than the first. A structure description over 1 MiB, nesting deeper than 40 levels, or a character-set decoder holding more than 64 KiB is refused with an error rather than buffered.

Configuration

Env var Required Default Meaning
YANDEX_MAIL_LOGIN yes — Your full Yandex address, e.g. you@yandex.ru. Sending needs the complete address: it becomes From and the envelope sender, and yandex_mail_send_message refuses a bare login.
YANDEX_MAIL_APP_PASSWORD yes — App password with the Mail scope — an account password will not work.
YANDEX_MAIL_IMAP_HOST no imap.yandex.ru Override for a Yandex 360 domain or for testing.
YANDEX_MAIL_IMAP_PORT no 993 IMAP over TLS.
YANDEX_MAIL_FOLDERS no (all) Comma-separated allow-list of folders, spelled as yandex_mail_list_folders reports them; case is ignored, but role words and synonyms such as sent or spam are not expanded here, so on an account whose Sent folder carries a localised name, that localised name is the one to list. The first entry is the default folder.
YANDEX_MAIL_ACTIONS no (all but sending and saving attachments) Comma-separated allow-list of actions the agent may perform — see below.
YANDEX_MAIL_SMTP_HOST no smtp.yandex.ru Override for a Yandex 360 domain or for testing.
YANDEX_MAIL_SMTP_PORT no 465 SMTP over implicit TLS.
YANDEX_MAIL_SEND_TO no (any address) Comma-separated fence on who may be written to: full addresses, or @domain for a whole domain. A value that is set but names nothing usable — whitespace, or nothing after the = — refuses every recipient; only removing the variable removes the fence.
YANDEX_MAIL_ATTACHMENT_MAX_BYTES no 104857600 (100 MiB) The largest attachment yandex_mail_save_attachment will save, in bytes. A value that is not a positive whole number — 100MB, say — refuses every save rather than falling back to the default.

Credentials are read from the environment first, then from ~/.hermes/.env, so they work in gateway and subprocess runs. Secret values are never logged.

Dates are ISO 8601 (2026-07-25). Folder names may be in any language — the Cyrillic names Yandex gives Russian accounts (Отправленные, Спам) are encoded and decoded for you.

Restricting what the agent can do

YANDEX_MAIL_ACTIONS decides which of the eight tools are registered at all. A disallowed action is not merely refused at call time: the tool never appears in the agent's toolset, so it cannot be invoked, and the model is not tempted to try.

Accepted values, comma-separated and case-insensitive — individual actions (list_folders, search_messages, read_message, save_attachment, mark_message, move_message, delete_message, send_message), full tool names (yandex_mail_delete_message), or the shorthands:

Shorthand Expands to
read list_folders, search_messages, read_message
write mark_message, move_message
delete delete_message
all every action except save_attachment and send_message (the default)

read_message with mark_read=true changes the \Seen flag, so it also needs mark_message. Under YANDEX_MAIL_ACTIONS=read such a call is refused and nothing is read.

# Read the mail, change nothing:
YANDEX_MAIL_ACTIONS=read

# Full triage, but the agent can never delete anything:
YANDEX_MAIL_ACTIONS=read,write

# Just enough to report what is unread:
YANDEX_MAIL_ACTIONS=list_folders,search_messages

Leave it unset for the six reading and organising tools. Saving attachments and sending are the two exceptions to "unset means everything": each has to be named, either on its own or alongside a shorthand —

# Read mail and save attachments as files, change nothing:
YANDEX_MAIL_ACTIONS=read,save_attachment

# Everything:
YANDEX_MAIL_ACTIONS=all,save_attachment,send_message

There is deliberately no short spelling for either — no send, no attachments. A short word could already be sitting in somebody's configuration from before the action existed, where it was silently ignored; giving it meaning now would switch the action on by upgrading alone, which is precisely what these gates exist to prevent.

A name that matches nothing is ignored, so a typo can only ever withhold a tool, never grant one — and a value that names nothing recognisable therefore registers nothing at all.

Permissions are checked again each time a registered tool runs, against the value the running Hermes holds at that moment. A narrower value written in ~/.hermes/.env therefore takes effect without a restart once Hermes has re-read the file — the gateway does so for every message, and the CLI when you run /reload. Deleting or commenting out the line is not such a value, even though unset means the default set: re-reading the file does not remove a plugin's variable that is no longer in it, so Hermes keeps the value it already loaded until it restarts. To narrow without a restart, write the narrower value instead — YANDEX_MAIL_ACTIONS=all for the default set. A value exported into the environment Hermes was started from, rather than written in ~/.hermes/.env, stays in force until Hermes restarts. Restart Hermes after changing configuration in any case, so its visible toolset also reflects the change.

Reading with mark_read=true also requires mark_message permission. The read group alone always leaves the message's read/unread state unchanged.

Pair it with YANDEX_MAIL_FOLDERS to fence off the rest of the mailbox: with YANDEX_MAIL_FOLDERS=INBOX, every other folder is invisible to the agent and cannot be named — as a source or as a move destination.

Two folders stay reachable by the plugin itself, never by name. A delete still moves the message into the folder the server flags as Trash, and a sent message is still filed into the one it flags as Sent, whether or not the allow-list mentions them. Naming either folder in a tool call is still refused; only these two built-in steps may reach it.

Saving attachments

yandex_mail_read_message lists every attachment with a part_id. yandex_mail_save_attachment takes that part_id, with the message's uid and folder, streams the attachment from the server straight into a file, and answers with where the file is:

{"attachment": {"uid": "101", "folder": "INBOX", "part_id": "2",
  "filename": "Отчёт за сентябрь.pdf", "content_type": "application/pdf",
  "size": 300032, "sha256": "…",
  "path": "/home/you/.hermes/cache/documents/yandex-mail_3f9c1a2b4d5e_Отчёт за сентябрь.pdf"}}

The content never enters the model's context. It is off until you name it:

YANDEX_MAIL_ACTIONS=read,save_attachment
  • Where it goes. Hermes' document cache, ~/.hermes/cache/documents (or that of the active profile) — the directory Hermes itself files incoming documents in. Other tools can open the file there, and in a Docker, Modal or SSH sandbox the path returned is the one the sandbox sees.
  • Getting it to you. On a messaging platform the agent sends the file by writing MEDIA:<path> in its reply, which Hermes' platform prompts already teach it; the tool result itself carries no such tag, so a file reaches you only when the agent decides to send it. In the CLI the path is simply shown.
  • How long it stays. Files are kept for 24 hours. The Hermes gateway prunes its document cache on that schedule, and since the CLI and TUI do not, each save also deletes this plugin's own files older than that — never anyone else's.
  • How it is written. The name comes from the sender, so it is reduced to a single safe file name — no directories, no control or bidirectional-override characters — and prefixed with yandex-mail_ and a random token. The bytes go to a temporary file readable only by you (0600), which is linked into place once complete: a half-written file never appears, and an existing file is never replaced. An attachment larger than YANDEX_MAIL_ATTACHMENT_MAX_BYTES (100 MiB unless set) is refused, and anything already written is removed.
  • Not marked as read. Fetching an attachment leaves \Seen alone.

Sending mail

Sending is the one thing here that cannot be undone: the message leaves your mailbox and reaches the people named. Everything about the design follows from that.

Switch it on, and decide who the agent may write to:

YANDEX_MAIL_ACTIONS=all,send_message

# Optional, and worth setting: who may be written to at all.
YANDEX_MAIL_SEND_TO=you@yandex.ru,@yourcompany.example

The fence is checked before a socket is opened. A full entry matches one mailbox (@ya.ru and @yandex.ru are understood to be the same account); an @domain entry matches that domain exactly — not its subdomains, and not a domain that merely ends with it.

A value that is set but names nothing usable — bogus, whitespace, or nothing at all after the = — allows nobody through: a mistyped fence fails closed. Removing the variable is the only thing that removes the fence.

This changed in 0.3.1, deliberately. An empty or whitespace-only YANDEX_MAIL_SEND_TO used to read as "unset" and so allowed every address. It now refuses every recipient, with the usual YANDEX_MAIL_SEND_TO does not allow sending to … error and nothing sent. If your configuration carries an empty YANDEX_MAIL_SEND_TO= and you rely on sending, either delete the line (no fence) or fill it in (a fence).

What "set" means has a limit worth knowing. The plugin looks for the name in the process environment and in ~/.hermes/.env. A value that reaches it only through Hermes' own configuration layer cannot be seen there, and if it strips to nothing it still reads as no fence at all. Keep the fence in the environment or in ~/.hermes/.env.

The rules the tool enforces, whatever it is asked to do:

  • You are always the sender. From and the envelope sender are YANDEX_MAIL_LOGIN. No argument can change them.
  • Recipients are only ever what the caller states. Replying threads a message onto another one — it never takes an address from it. This matters: From, Reply-To, To and Cc are all written by whoever sent you the message, so deriving a reply's recipients from them would let a sender choose where your reply goes.
  • An address is an address, not a display name. Bob <bob@example.org> is refused; pass bob@example.org. A display name containing an @ parses as a second address, which is how a reply quietly acquires an extra recipient.
  • A reply must name the message it answers. reply_to_uid, reply_to_folder and reply_to_message_id are required together. A UID identifies a slot, not a message; comparing the message_id against what the server reports now is what makes "reply to the message I read" mean that. A message carrying no Message-ID of its own cannot be replied to — the tool says so rather than threading onto nothing.
  • Threading needs the grant that reading needs. Answering a message means reading its headers, so a reply also requires read_message. Enabling send_message alone gives a tool that can write, not one that can also look.
  • Nothing gets to become a header. A line break in a subject, a recipient or a copied Message-ID is refused, not stripped.
  • A copy is filed in Sent, and the message being answered is flagged \Answered if flagging is allowed. Neither can fail the send: once the message has gone, the result says so and a bookkeeping problem is a note.

What the result tells you. sent, the recipients the server actually accepted, anything it refused and why, and — on a reply — where each recipient stands in that thread:

{
  "sent": true,
  "delivery": "confirmed",
  "recipients": ["counterparty@example.org"],
  "from": "you@yandex.ru",
  "subject": "Re: Contract",
  "message_id": "<178985821691.1252.15561456921@yandex.ru>",
  "recipient_sources": {"counterparty@example.org": "from"},
  "in_reply_to": "<original@example.org>",
  "replied_to": {
    "uid": "8",
    "folder": "INBOX",
    "subject": "Contract",
    "from": ["Counterparty <counterparty@example.org>"],
    "message_id": "<original@example.org>"
  },
  "saved_to_sent": true,
  "sent_folder": "Sent",
  "marked_answered": true,
  "notes": []
}

recipient_sources is always present on a reply, and says whether each address is your own account (self), sent the original (from), was among its To recipients (to), appeared only in its Reply-To (reply_to_only), or is none of those (new). The original's Cc is deliberately not consulted, so someone who was only Cc'd on it also comes back as new. reply_to_only and new are the two worth reading: a message asking for replies at an address it was not sent from is the standard shape of a phishing redirect, and the tool says so in notes rather than deciding for you.

Once the message has been written, three things can happen, and the result says which. The reply the server gives to the finished message is its verdict on the whole thing, so those three are genuinely different outcomes:

  • "delivery": "confirmed" — the server answered 250 and took the message on. It is out of your hands now.
  • An error quoting what the server said. The server answered the finished message with a 4xx or a 5xx, which means it declined to take it on: nothing was delivered, to anybody — not even to the recipients it accepted a moment earlier — and no copy is filed in Sent. The error carries the code and the server's own words, so 554 5.7.1 Message rejected under suspicion of SPAM reaches you as itself. A 5xx will be refused the same way until the cause is addressed; a 4xx is the server saying "not now", and the same message can be sent again later.
  • "delivery": "unconfirmed" — the message went out and no verdict came back: either no reply at all, because the connection dropped, or a reply that is neither the 250 acceptance nor a 4xx/5xx refusal. This is the one genuinely unknown outcome. It is not a failure and must not be retried: the message may well have arrived, and sending it again would deliver a second copy.

Every error from sending ends with "Nothing was sent" — whether it failed before a single byte was written or the server refused the finished message, nobody received anything, so trying again cannot deliver a duplicate. unconfirmed is the only outcome that carries that risk.

Plain text only: no HTML, no attachments, and no Cc or Bcc — every recipient goes in to, and an argument the tool does not know (cc, bcc, from, attachments, …) refuses the whole call instead of being silently dropped. At most 10 recipients, a 500-character subject, and a 100 000-character body per message.

Enabling IMAP in Yandex Mail

A mailbox that has never been used with a mail client does not accept IMAP connections until you switch the protocol on. Until you do, every login fails with [AUTHENTICATIONFAILED] ... invalid credentials or IMAP is disabled, which reads exactly like a wrong password.

  1. Open https://mail.yandex.ru/#setup/client (⚙ Настройки → Почтовые программы).
  2. Tick «С сервера imap.yandex.ru по протоколу IMAP» — "allow access to the mailbox over IMAP".
  3. Save. It takes effect within seconds.

In a Yandex 360 organisation an administrator may have to allow mail clients for the whole domain first.

Getting the app password

IMAP does not accept your normal account password.

  1. Open https://id.yandex.ru/security/app-passwords.
  2. Add a password with the Почта (IMAP, SMTP) / Mail scope. A password created for another service — a CalDAV one, for instance — will not work here.
  3. Copy it into YANDEX_MAIL_APP_PASSWORD. It is shown only once, and you can revoke it at any time without touching your account password.

If a tool answers "Authentication failed", it is one of these two things: IMAP is off, or the app password lacks the Mail scope.

Good to know

  • A just-arrived message may not match a subject search for a second or two. Yandex indexes SEARCH SUBJECT asynchronously; searching by text, or simply asking again a moment later, finds it.
  • UIDs change when a message moves, so yandex_mail_move_message returns a destination_uids map from each source UID to the one the message was verified to have on arrival. Use it rather than searching — Yandex cannot search by Message-ID. A message that could not be verified is simply absent from the map, never guessed.
  • Reading is paged. Only the text parts are fetched, never the attachments. Continue with next_offset to read beyond the page limit; a large attachment does not force a whole-message download. See Reading long messages.
  • The TLS certificate and hostname are verified, and every connection carries a 30-second timeout. A private or self-signed CA is supplied the standard way, via SSL_CERT_FILE / SSL_CERT_DIR; there is no setting for turning verification off. (Releases 0.1.0 and 0.2.0 did not verify — see Security.)
  • Yandex' SMTP does not advertise SMTPUTF8, so an address with non-ASCII characters in it cannot be sent to at all. It is refused with a sentence saying why, rather than failing somewhere inside the standard library. Subjects and message bodies in any language are fine; display names are not, because every recipient must be given as a bare address.

Installing the plugin into Hermes

hermes plugins install akinfold/hermes-yandex-mail/hermes_yandex_mail --enable

Note the /hermes_yandex_mail at the end. The plugin lives in that directory, not at the repository root, and Hermes reads the manifest from whatever you point it at. Name the directory and the install is a plugin: Hermes prompts for YANDEX_MAIL_LOGIN and YANDEX_MAIL_APP_PASSWORD, installs under the manifest name yandex_mail, and --enable enables that name. It also scans only that directory, so the tests and workflows in this repository stay out of the security report.

Point it at the repository root instead and Hermes copies a directory with no manifest and no register(ctx) in it: it warns that it "may not be a valid Hermes plugin", asks for no credentials, and does not enable the plugin. hermes plugins list then still shows yandex_mail — the package nested in the clone is found — but shows it as not enabled, which is the symptom to look for. If you installed that way, remove ~/.hermes/plugins/hermes-yandex-mail and install again with the directory named.

Option B — from PyPI

For a Hermes whose Python environment you manage yourself — your own virtualenv, a Nix build: install hermes-yandex-mail into that environment, then enable the plugin. Hermes finds it through the hermes_agent.plugins entry point.

hermes plugins enable yandex_mail

Not for a standard Hermes install. Since 24 September 2026 the official installer runs Hermes from environments its package manager builds and replaces, and Hermes does not support adding packages to them by hand: use Option A or C.

A Hermes in the older layout — one that runs from ~/.hermes/hermes-agent/venv and keeps its own uv in ~/.hermes/bin, with no ~/.hermes/installs, which is what Hermes 0.21.5 and anything installed before that date and not updated since look like — takes the package like this:

~/.hermes/bin/uv pip install --python ~/.hermes/hermes-agent/venv/bin/python hermes-yandex-mail
hermes plugins enable yandex_mail

Switch such an install to Option A before you run hermes update: the update moves Hermes onto the new environments, and a package installed this way does not come along. The loss is silent: yandex_mail stays enabled in the configuration, but its tools are gone, and neither the update nor hermes doctor mentions it. If you have already updated, run the Option A install now: it brings the tools back and keeps your credentials in ~/.hermes/.env. A bare pip install hermes-yandex-mail never reached Hermes at all — the pip on your PATH belongs to some other Python.

Nothing asks for credentials on this path — add them to ~/.hermes/.env as in the Quick start.

Option C — drop-in directory

Download hermes-yandex-mail-plugin-<version>.zip from the release — not the wheel, the .tar.gz, or GitHub's "Source code" archives — then unzip it into ~/.hermes/plugins/ and enable it:

unzip hermes-yandex-mail-plugin-<version>.zip -d ~/.hermes/plugins/
hermes plugins enable yandex_mail

You should end up with ~/.hermes/plugins/yandex_mail/plugin.yaml. Nothing asks for credentials here either: add them to ~/.hermes/.env yourself.

Upgrading

Upgrade the way you installed. Option A — Hermes 0.21.5 and later install the new version from the source they recorded:

hermes plugins update yandex_mail

Older Hermes cannot update an install from a plugin directory, and no Hermes updates one pinned with --ref: run the install command again with --force, which replaces the installed copy and keeps your credentials.

hermes plugins install akinfold/hermes-yandex-mail/hermes_yandex_mail --enable --force

Both scan the new version again, but unlike a first install neither stops to ask when Hermes' security scan reports a caution.

Option C — unzip the new release's archive over the old one:

unzip -o hermes-yandex-mail-plugin-<version>.zip -d ~/.hermes/plugins/

Option B — upgrade the package in the same environment: in the older layout,

~/.hermes/bin/uv pip install --upgrade --python ~/.hermes/hermes-agent/venv/bin/python hermes-yandex-mail

Options A and C, and their upgrades, are checked before every release by installing the build into a real Hermes — the latest release and main, each set up by its official installer — exactly as written here; the Option B install is checked on the latest release while it still has the older layout. See Checking the install paths.

Development

python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
ruff check . && ruff format --check .
pytest                       # unit tests, no network

Running the live E2E tests

The e2e-marked tests hit a real Yandex mailbox and are deselected by default. They upload one throwaway message with a unique marker via IMAP APPEND, then search, read, flag, move, and erase it, and the cleanup sweeps until the server agrees nothing is left — so a successful run leaves the mailbox as it found it.

They also save an attachment of that message to a temporary Hermes home and compare it with what was uploaded, byte for byte.

Since 0.3.0 they also really send, which is the only way to test sending at all. Every message goes to the test account itself and nowhere else, held there by two independent guards: the suite sets YANDEX_MAIL_SEND_TO to that account, so the plugin's own fence refuses anything else before a socket opens, and the test process asserts the same thing again before each call. Use a dedicated test mailbox regardless.

Locally

YANDEX_MAIL_LOGIN=you@yandex.ru \
YANDEX_MAIL_APP_PASSWORD=xxxx \
pytest -m e2e

Or keep both out of the command line, in ~/.yandex-mail-login and ~/.yandex-mail-app-password, and just run pytest -m e2e — see tests/e2e/conftest.py.

On GitHub Actions

The E2E (live) workflow is manual (workflow_dispatch). It reads YANDEX_MAIL_LOGIN and YANDEX_MAIL_APP_PASSWORD from a GitHub Environment named yandex-mail-e2e. It runs the plugin inside a real Hermes, installed by its official installer: the latest Hermes release by default, and its hermes input switches to Hermes main or to no Hermes at all. With Hermes, the tests run in Hermes' own Python, with this checkout on its path rather than installed into Hermes' environment, and the run fails outright if the plugin cannot import Hermes, rather than testing the plugin's stand-ins instead.

Checking the install paths

The install-marked tests in tests/install/ install the built plugin into a real Hermes, set up by its official installer, by each option in Installing the plugin into Hermes — running the README's own commands, upgrades included — and then ask Hermes what it loaded: the plugin must be listed as enabled, load without error, and give the agent its tools. They also check that installing from the repository root still looks the way this README describes. A fast unit test keeps the commands in the tests and in this README identical.

The Install check workflow runs them against the latest Hermes release and against Hermes main on every pull request, and on every release tag before anything is published: the GitHub Release and the PyPI upload both wait for it. They install into a real ~/.hermes, so run them yourself only in a container or VM — see the docstring of tests/install/test_install.py.

Part of a family of Yandex plugins for Hermes Agent:

Security

0.1.0 and 0.2.0 connected without verifying the server's TLS certificate. imaplib.IMAP4_SSL with no explicit ssl_context falls back to ssl._create_stdlib_context(), which sets verify_mode=CERT_NONE and check_hostname=False: the connection was encrypted but unauthenticated, so anyone positioned to intercept it — a hostile Wi-Fi network, a spoofed DNS answer, an intercepting proxy — could present their own certificate and read the app password and every message. 0.2.1 fixes this; upgrade — see Upgrading.

If you ran an earlier version over a network you do not control, revoke the app password at https://id.yandex.ru/security/app-passwords and issue a new one.

Found with thanks by @ukko in #2.

Please report security issues privately rather than as a public issue — see CONTRIBUTING.md.

Contributing

Issues and PRs are welcome — see CONTRIBUTING.md for the layout, the plugin contract rules worth knowing, and the release process.

License

MIT — see LICENSE.

Metadata

Release files for hermes-yandex-mail 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hermes-yandex-mail 0.4.2
File Size Uploaded
hermes_yandex_mail-0.4.2.tar.gz 151.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hermes-yandex-mail 0.4.2
File Interpreter ABI Platform
hermes_yandex_mail-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 232.1 kB

Release files / hermes_yandex_mail-0.4.2.tar.gz

Download URL hermes_yandex_mail-0.4.2.tar.gz
Size 151.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fdc310766d0f0b956e7f5da034b294b3fcf05657cf172632daf4c110754080b9
BLAKE2b-256 checksum
How to use checksums
43c2323e3b5603b57dac6529f64d5e56aec69f8028b4010b26c0912c5dea80c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / hermes_yandex_mail-0.4.2-py3-none-any.whl

Download URL hermes_yandex_mail-0.4.2-py3-none-any.whl
Size 80.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
22db4b3217eb9f1e4f797c108bc007108955d62de237ea072d66a58aa55e9ace
BLAKE2b-256 checksum
How to use checksums
ba8056460d4f515c834502c3f42b4187344702ace8203aefbd7f7dcd7d6adf95
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page