Repository navigation
Releases
0.7.0
Breaking changes
This release changes how the MCP server starts in one case. It is described in full under Changed below.
- MCP server over HTTP: on an address other hosts reach, such as
0.0.0.0in its image, the server no longer starts withoutMAILBOX_MCP_BEARER_TOKENandMAILBOX_MCP_ALLOWED_HOSTSorMAILBOX_MCP_ALLOWED_ORIGINS. Set them before you update the image. On127.0.0.1and over stdio nothing changes.
The database schema stays the same, no migration runs at the first start.
Added
MAILBOX_MCP_CATALOGUE=truestarts the MCP server without a service
or a token: it lists every tool and refuses every call but
server_status. It is meant for directories that read the tool list
in a sandbox. BesideMAILBOX_SERVICE_TOKENthe server does not
start. Without the setting nothing changes.benethos-mailbox-mcp --list-toolsprints every tool of the MCP server
as a client lists it, as JSON, without a service or a token.
Changed
- Breaking: the MCP server over HTTP on an address other hosts
reach, such as0.0.0.0in its image, no longer starts without
MAILBOX_MCP_BEARER_TOKENandMAILBOX_MCP_ALLOWED_HOSTSor
MAILBOX_MCP_ALLOWED_ORIGINS. Before, it started with a warning and
admitted anyone who reached the port. On127.0.0.1and over stdio
nothing changes. - The MCP tools describe every parameter, the ids by the tool that
answers them. Each tool says when to use it rather than a related
one, whether it changes anything, and what it answers. What the tools
do is unchanged. - The images install fastapi 0.162.2, pypdfium2 5.15.2 and
publicsuffixlist 1.1.2.20261109 from the lockfile. pypdfium2 brings
PDFium 8149 and fixes a rare error when its objects are closed while
the garbage collector runs or the interpreter shuts down.
Fixed
- On Python 3.11 and 3.12 the descriptions of the MCP tools carried the
indentation of their source code, spaces a client sent to the model
with every description. They are now the same on every Python version. - A page of the UI given a return address whose host cannot be read,
such asnext=//[, answered with a plain error 500. It now goes to
the page's own default, as for any address outside the UI. - A cursor of a Microsoft account's message list that names a host
which cannot be read answered with an error 500 outside the error
envelope. It is now400with the codebad_request, as any other
forged cursor. - The client raised httpx's own error for an answer it could not read,
such as a body that is not what itsContent-Encodingsays. It is now
anApiErrorwith the codeunexpected_response. The MCP server,
which asks the service what its token may do while it runs, ended on
such an answer. It now keeps its tools and asks again. - The MCP tool
get_threadsaid the bodies together are cut at
max_chars. Each mail keeps at least 200 characters, so twenty mails
can bring more. The description now says so. What the tool does is
unchanged.
0.6.1
Changed
- The images install anyio 4.16.0, python-multipart 0.0.33, platformdirs
4.14.5, typing-inspection 0.4.5, pydantic 2.14.3, publicsuffixlist
1.1.2.20261107, fastapi 0.162.1, click 8.5.1, sse-starlette 3.7.0 and
pypdfium2 5.15.0 from the lockfile. The PDFium of pypdfium2 5.15.0 has
fixes for known vulnerabilities. - The images build with uv 0.13.7, on the build of
python:3.15-slimfrom
2026-11-05, still Python 3.15.2. benethos-mailbox-serviceneeds dnspython 2.9.2 or newer, the first
version without a known vulnerability. The images install it from the
lockfile. The advisory concerns zone files, and the service reads none.- The images name their user by its id,
USER 10001instead of
USER mailbox, so that a host or an orchestrator can see it is not root.
It is the same user. - The UI ships htmx 4.0.1 instead of 2.0.4. Only the page of a sign-in with a
code uses it, and that page works as before.
Fixed
- The UI's
nextandbackvalues no longer lead out of/uiwith a
..in the path, also written as%2e%2e. Such a value lands on the
fallback page. It could not reach another host before either.
0.6.0
Breaking changes
This release changes the contract in one place. It is described in full under Changed below.
- Keywords: a replacement list
keywordsinPATCH /v1/accounts/{account_id}/messages/{message_id}and the batches keeps the marks a message has, such as$answered. Name a mark inremove_keywordsto take it off.
The database schema stays the same, no migration runs at the first start.
Added
benethos-mailbox-mcp: the toolserver_status, always offered. It
names the server's version, the transport, Python, the MCP library
and the uptime. Withdetailsit adds the service as the server last
saw it: whether it answered and how long ago, its version and whether
it matches, whether it took the token, and the tools offered. The
server asks/healthfor the service's version as it starts and each
time it asks what its token may do.benethos-mailbox-client:Folder.subscribed, whether the folder is
subscribed on the server, as the API says.benethos-mailbox-mcp:list_accountsnames three more things an
account may lack inunsupported: a mail in several folders,
conversations and a copy in the sent folder.benethos-mailbox-mcp:send_messageandsend_draftanswer
sent_copy_id, the copy in the sent folder where the account keeps
one.benethos-mailbox-mcp:list_accountsnames per account the marks
it writes inmarks_written. A mark outside them is refused.benethos-mailbox-mcp:search_messagesandwhats_newtake
account_ids, several accounts in one call, besideaccount_id.benethos-mailbox-mcp:share_attachmenttakesexpires_in, a
link that lives fewer seconds than the service's default.benethos-mailbox-mcp: each hit ofsearch_messagescarries the
first 120 characters of its text assnippet.get_messagemarks an
inline attachment, such as a logo in a signature, as(inline).benethos-mailbox-client:ApiError.retry_after, the seconds the
Retry-Afterheader of a limit asks to wait, elseNone.benethos-mailbox-client:Sent.sent_copy_id, the copy in the sent
folder where the service put one.Sent.message_id_headeris a
string, as the API always answers one.benethos-mailbox-client:Failed.code, the code of the error a
batch answered for an id, andPage.incomplete, the accounts that did
not answer as recordsNotAnsweringwith their code.not_answering
stays as it was.benethos-mailbox-client:list_changestakessinceandlimit
as optional, as the API does, andlist_messages,list_draftsand
list_threadstheirlimit.
Changed
- Breaking: a replacement list
keywordsin
PATCH /v1/accounts/{account_id}/messages/{message_id}and the
batches keeps the marks a message has, such as$answered. It took
them off on IMAP and JMAP, but not on Microsoft. Name a mark in
remove_keywordsto take it off. GET /v1/messagesreads at most ten pages of an account for one page
of the list. Where the grants reach little of the folder, a page may
hold fewer messages thanlimit, or none, with anext_cursorto
read on. One request read the whole account before.- The help of
keys import,backup verify --recovery-keyand
restore --recovery-keysays the recovery key is asked for at a
hidden prompt, and read from stdin only when piped. - The service runs on FastAPI 0.162 and Starlette 1.10.
- Both images run on Python 3.15.2. It rejects TLS certificate chains
with the compromised eMudhra intermediate CA keys. - The images install pydantic 2.14.2, platformdirs 4.14.0 and dnspython
2.9.1 from the lockfile. benethos-mailbox-serviceneeds dnspython 2.9.0 or newer, the first
version without a known vulnerability. An install from the lockfile,
as the images do, had it already.POST /v1/users/{user_id}/tokensrefuses anexpires_atmore than
3650 days, ten years, ahead with400, as the UI did. A token without
an end stays possible.benethos-mailbox-client: the repr ofEnvironmentleaves the token
out.benethos-mailbox-mcp: the filterstext,sender,toand
subjectofsearch_messagestake one line of 1 to 200 characters,
as the API does, and refuse other values in their own names. The
name ofcreate_folderandupdate_foldertakes at most 200.benethos-mailbox-mcp: the log names why a tool failed when it was
no refused argument: a PDF that took too long, an attachment too large
to hand over, a mail without a thread.benethos-mailbox-client: thecontent_typeof anAttachmentLink
is a media type, elseapplication/octet-stream, as for the bytes of
an attachment. The sender of the mail chose it.benethos-mailbox-clientandbenethos-mailbox-mcprefuse a
MAILBOX_SERVICE_URLwith a user or password in it. The address is
named in the error when the service cannot be reached, which reaches
the model.benethos-mailbox-mcp:share_attachmentnames the content type
inside the marker, beside the file name, since the sender chose it.- The account page of the UI says what the account can do and which
marks it writes. - A sign-in with a code in the UI answers a page of its own, so a reload
starts no second sign-in at the provider. The batch of the Users list
and Set password for another user ask first.
Fixed
benethos-mailbox-mcp:get_messageshowsbccandreply_to
where a mail has them. A draft replaced withupdate_draftafter
reading it lost both.- A list of messages or threads in a folder the account does not have
answers404on every provider. It reached the provider first, and
Microsoft answered400. - A message id that Microsoft cannot read answers
404when the message
is read, fetched raw, deleted or its keywords changed, as on every
other provider. Graph answered400.
Security
- A folder name with a line break or NUL never reaches a command of an
IMAP server. A folder id is not signed:
a caller withmail.readcould build one whose name endedSELECT
and started a command of their choosing, run with the account's
credentials. Python 3.14 and newer refused it already, 3.11 to 3.13
sent it. The images run on 3.15 and were not affected. - An IMAP folder id, message id or cursor whose folder name holds a
control character answers like one of the wrong form:404for an
id,400for a cursor. No server has such a folder.
Full changelog: v0.5.2...v0.6.0
0.5.2
Changed
- The images build on Python 3.15.1 and uv 0.13.5.
- The service runs on FastAPI 0.161.
- The icons of the UI come from Bootstrap Icons 1.13.2.
Full changelog: v0.5.1...v0.5.2
0.5.1
Changed
- The images take the tag
latestfrom a full release only, not from a
pre-release, andedgefrommainonly. - The service runs on FastAPI 0.149 and Starlette 1.9.
- The images build on
python:3.15-slimanduv0.13.2, each pinned
by its digest.
Full changelog: v0.5.0...v0.5.1
0.5.0
Breaking changes
This release changes the contract in these places. Each is described in full under Changed below.
- Errors: what an account lacks answers
501 not_supportedon every account, before the provider is asked. It answered409,400or404before, and a role without a folder answered404or409. - Errors: a provider's
403answersprovider_erroron every account, a JMAP server's too, also for a JMAP method errorforbidden. JMAP's400and409answerbad_requestandconflict. - Accounts:
PATCH /v1/accounts/{account_id}with settings that name a new server needs every credential of the account in the same request. - Accounts: the capabilities are finer, one per thing they guard.
flagsis gone,stable_ids,deltaandserver_searchleft the API. A client that checks the values strictly fails on the new ones. - Accounts:
operationsinGET /v1/meleaves out what the account cannot do. - Messages:
dateis the Date header on every account, and the new fieldreceivedis when the account received the mail. Lists sort, filter and merge byreceived. - Lists: a list without a folder is the inbox on every account. The role
alllists every folder except the trash and junk. A cursor of a list without a folder handed out before is refused once. - Keywords: keywords keep the provider's spelling and compare without case. Notes a server keeps for itself, such as
$hasattachment, leavekeywords.
The database moves from schema 19 to 22 at the first start. Make a backup before the update: a version before 0.5.0 cannot open the database afterwards.
Added
- The MCP server asks what its token may do every
MAILBOX_MCP_RIGHTS_SECONDS, 60 by default, and when a tool is
refused with403 forbidden, a right missing, at most every five
seconds. With0it asks only at start.
When its tools change it tells its clients with
notifications/tools/list_changedand announcestools.listChanged,
so a right granted or taken away shows without a restart. When the
service refuses the token, every tool goes away, also when a tool is
refused with401. A user that may do nothing keepslist_accounts
alone, which lists no account. A refused token is asked
for no more, since each refusal counts against the address, and a new
one needs a restart. POST /v1/accounts/{account_id}/messages/{message_id}/attachments/{attachment_id}/links
(share_attachment) makes a link to an attachment for another
service, which fetches the file itself withGET /v1/links/{secret}:
once, without a token, untilexpires_at. A link holds
MAILBOX_SERVICE_LINK_SECONDS, 120 by default, andexpires_inasks
for fewer. It needs an API token and the new groupmail.share, which
is in no role template. When the link is used, its token must still
act and holdget_attachmentandshare_attachmenton the message.
Every refusal answers the same404and counts against the client
address as a wrong token does. The audit keepsmailbox.link_shared
andmailbox.link_used, never the secret or the URL. When the mail
server fails to
hand out the file, the link holds for the rest of its time, the answer
is502 provider_unavailableto try again, the audit says
mailbox.link_returned, and the address that asked counts no
failure. Whoever holds the URL fetches the
file. The service logs its path masked, but a reverse proxy in front
writes the whole URL into its access log, where it opens the file
until the link is used or ends. The Python client has
share_attachment, the MCP server a tool of the same name. The tool
is not marked read-only, andlist_accountsnamesshareas a kind
of its own. The database moves to schema 22.- An account answers
operations, inGET /v1/accounts/{id}, the list
andGET /v1/me: the operations the caller can run there, its rights
and what the account can do together, by their names in the catalogue
of rights. A client offers what is in it and needs no knowledge of
providers. The Python client reads it asAccount.operations. - The connect page of the UI offers one form per kind of account to
set up by hand, IMAP, POP3 and JMAP, each with what to know of it.
The forms follow from each kind's description of its connection. - The MCP server shows a mail's marks in words, such as
answered,
apart from its keywords, and its tools name no provider. - A send of a reply or a forward answers
original_marked: whether the
original carries$answeredor$forwardednow. False where the
account does not write that mark, as on Gmail, or the mark failed. - A Microsoft account reads and writes
$answeredand$forwarded, as
the verb Outlook last carried out on a message, so Outlook shows a
reply or a forward of the service too. One of the two at a time, as
Outlook keeps it. Itsmarksname both. A change Exchange refuses as
a conflict right after another one is tried again after a pause. PATCH .../messages/{id}and a batchupdatetakeadd_keywordsand
remove_keywords, which change the keywords a message has instead of
replacing them, andadd_folder_idsandremove_folder_ids, which
put a message into folders and take it out of them where it can be in
several, such as Gmail's labels. Each goes beside the list it changes,
not with it. A message keeps one folder at least.- A draft and a message to send take
attachments_from: attachments of
other messages of the same account by message and attachment id,
attached as they are. It needs the right to read that mail. /v1/mesays per account itsstatus, whether it needsattention
and when its last sync succeeded (last_sync_at).- An account and the accounts of
/v1/mecarrymarks: the marks of
RFC 8621, such as$answeredor$junk, it reads (marks.read) and
those it writes (marks.written). Setting or taking off a mark it
does not write answers501. The Python client has them asMarks. - Messages of IMAP accounts carry a
thread_id. The service threads
them itself, across folders, byMessage-ID,In-Reply-Toand
References. Mail already known is threaded over the next sync
passes. A message that ties two threads together merges the younger
into the older, whose messages show as updated in the change feed.
Every kind of account but POP3 has the capabilitythreads: Gmail,
JMAP and Microsoft keep the threads themselves.
MAILBOX_SERVICE_THREAD_REFERENCES(20) sets how many of the
Referencesof a message count. The database moves to schema 20. GET /v1/accounts/{account_id}/threads/{thread_id}(get_thread)
answers a thread with its messages in every folder, oldest first. A
longer thread lists its newestMAILBOX_SERVICE_THREAD_MESSAGES(500)
and saystruncated.GET /v1/accounts/{account_id}/threads
(list_threads) answers the threads of afolder, each where its
newest message in the folder is, with that message's subject, date
and snippet. Both are inmail.read, and a grant
that names folders sees the messages of a thread in them. POP3 answers
501. The Python client haslist_threadsandget_thread.- The UI lists a folder by conversations where the account keeps
threads, shows a conversation on a page of its own, and names the
others of its conversation below a message. - The MCP server:
- A new tool
get_threadreads a conversation in every folder,
oldest first, from a mail of it such as a search hit or from its
thread id. The newest 20 mails come as plain text and share
max_chars. Search results andget_messagename the thread of
a mail. update_messagesadds and takes off keywords and labels
(add_folders,remove_folders,add_keywords,
remove_keywords). At Gmail it labels a mail and leaves it in the
inbox.send_message,create_draftandupdate_drafttakequote,
forward_as,reply_to,attachmentsas base64 and
attach_from, attachments of other mail of the account.- A new tool
update_folderrenames and moves a folder. list_accountsnames an account that needs a person, and why.- Search results and
get_messagename the folders and keywords of a
mail.get_messagenames the mail a draft answers.
- A new tool
Changed
- Breaking: a provider that refuses a request with
403answers
provider_erroron every kind of account: the server took the
credential and refused the request. A JMAP server's403answered
provider_auth_failedand marked the account as needing a new
sign-in. The same holds for a JMAP method errorforbidden, as from
a read-only token. A JMAP server's400and409answer
bad_requestandconflict, as Gmail's and Microsoft's do. - Breaking:
PATCH /v1/accounts/{account_id}with settings that
name a new server needs every credential of the account in the same
request, else it answers400. A server removed needs none. The UI
says so under the password of an account. benethos-mailbox-mcprequires mcp 2.3 and uvicorn 0.38 at least,
the versions the workspace and the service already ask for.- Breaking:
operationsof an account inGET /v1/meleaves out
what the account cannot do, such aslist_threadson POP3. It named
the caller's rights alone. - Breaking: the capabilities of an account are finer, each guarding
one thing:read_state,stars,keywords,folders,move,
trash,purge,labels,search,drafts,send,sent_copy,
threads,push,external_changesandrefused.flagsis gone,
andstable_ids,deltaandserver_searchleft the API. A client
that checks the values strictly fails on the new ones. - Breaking: what an account lacks answers
501 not_supported, the
same on every account, before the provider is asked: a send without
an SMTP server, a draft without a drafts ...
0.4.1 (beta)
Added
- From this release on, the MCP server is listed in the official MCP
Registry asio.github.benethos-hub/benethos-mailbox-mcp, installed
from PyPI withuvxor as the Claude Desktop extension below. It asks
forMAILBOX_SERVICE_TOKEN, and takesMAILBOX_SERVICE_URLand
MAILBOX_SERVICE_ALLOW_HTTPif set. - Each release carries the MCP server as a Claude Desktop extension,
benethos-mailbox-mcp.mcpb. Claude Desktop installs it with uv and
Python of its own, and asks for the token, the address of the service
and whether plain http to another machine is allowed.
Changed
- The package description of
benethos-mailbox-mcpis one sentence of
at most 100 characters, the same as in its registry entry.
0.4.0 (beta)
Added
- A second factor for the sign-in to the UI, a choice of each user. Its
first method is an authenticator app (TOTP): a code after the
password. A user holds it on up to ten named devices, each with codes
of its own, added, renamed and removed on the page Second factor
with a QR code. The first device brings ten recovery codes shown
once, also as a text file to download. A further device, new
recovery codes and the recovery key need a code. A password set by
someone else is changed only after the code. docs/AUTHENTICATION.md
has the details. Recovery codes made before schema 19 are void: make
new ones on the page Second factor after the update. - Gmail and Google Workspace accounts over the Gmail API, with the
kind of accountgmail. They sign in with Google in the browser,
through a Google client of the deployment's own, set up as
docs/GOOGLE.md says and named inMAILBOX_SERVICE_OAUTH_GOOGLE_CLIENT_ID
with its secret. Without one, Gmail connects over IMAP with an app
password as before. - In a Gmail account labels are folders, and a message can be in
several. "All Mail" is the folderALL_MAILwith the roleall.
Setting keywords answers501 not_supported. The change feed follows
Gmail's history, asked everyMAILBOX_SERVICE_SYNC_INTERVALseconds. - Looking up a Gmail address offers Sign in with Google first,
where the deployment has a Google client. The way over IMAP links to
Google's page for app passwords. - Each user in
/v1/userssays whether it has a second factor for the
UI sign-in, insecond_factor.GET /v1/users/{user_id}/second-factor
(get_second_factor, inusers.read) lists the devices of each
method,totpso far, never a secret.DELETE /v1/users/{user_id}/second-factor(remove_second_factor) removes the
whole second factor,DELETE /v1/users/{user_id}/second-factor/totp/{device_id}
(remove_totp_device) one TOTP device, both inusers.manage, for a
user whose rights the caller holds, never for the caller itself. The
database moves to schema 19. PATCH /v1/webhooks/{webhook_id}(update_webhook) changes a
webhook'surl,eventsoraccounts. A field left out stays,
accountsset tonullis every account the owner may read. Its
deliveries, where its posts stand and its secret stay.POST /v1/webhooks/{webhook_id}/secret(renew_webhook_secret) answers a
new signing secret, shown once. The one before stops at once. Both are
inwebhooks.manage, for the webhook's owner alone, and audited as
webhooks.changedandwebhooks.secret_renewed. The UI's webhook
page has a Change card and New secret.mailbox-clienthas both
asupdate_webhookandrenew_webhook_secret, with the records
WebhookandWebhookSecret. The secret is aSecret, kept out of
reprandstr, read withget_secret_value().users reset-second-factor <name>on the host removes a user's
second factor, for the last administrator who lost every device and
the recovery codes.mailbox-clientcovers the whole REST API: every operation is a
method of both clients, named like itsoperationId. New are the
administration side (accounts, discovery and the sign-in with a
code, users and passwords, tokens, roles, second factors, webhooks,
the audit of administration and of sends, the state of the service,
the catalogue of rights,health), folders renamed, moved and
deleted, a message's source (get_message_raw), one message changed
(update_message), a batch (batch_messages) and the lists across
accounts (list_all_messages,list_all_changes,list_all_sends).
They answer records such asAccount,User,Grant,Role,
Token,WebhookandActivity, a list that pages asPaged. A
secret shown once, a new token, a one-time password or a signing
secret, comes as aSecret, kept out ofreprandstr.- Python 3.15. The packages support 3.11 to 3.15. The images stay on
3.14 until a Python 3.15 base image exists.
Changed
- The MCP server reads its settings as the service does, through
benethos-mailbox-common[settings], and no longer needs
python-dotenv. A wrong value in the environment or the settings
file stops the start and names the setting. The settings file no
longer changes the environment of the process. - The service and the MCP server install a fourth package,
benethos-mailbox-common, of the same version: what both of them
need. The MCP server's log is written as the service's: in colour on
a terminal unlessNO_COLORis set, and its tokens masked as***. - A wrong password or code asked once more before a step, such as the
recovery key or new recovery codes, counts against the client address
as a failed sign-in does, not only against the name. Ten from one
address lock it out, whichever users they were for. - A session of the UI ends after 24 hours however it is used
(MAILBOX_SERVICE_SESSION_MAX_HOURS), and a user holds 10 at most: a
new sign-in ends the oldest (MAILBOX_SERVICE_SESSIONS_PER_USER). A
secret shown once, such as a new token, recovery codes or the
recovery key, waits 5 minutes for the page that shows it
(MAILBOX_SERVICE_SHOWN_ONCE_MINUTES). - Breaking:
mailbox-client:list_messagesandlist_changestake one
account,account_idis required.list_all_messagesand
list_all_changesread every account. A change of the feed is the
recordChangeand a failed id of a batch the recordFailed,
where both were a dict. - Breaking:
mailbox-client: a message is the recordMessage,
a summary in a page, a changed message and a written draft the record
MessageSummary, where each was a dict. The API'sfromis
sender, a time adatetime, an address anAddress, an attachment
of a message anAttachedFile.Change.atis adatetimetoo. - Breaking:
mailbox-client:me()is nowget_me(), named like
its operation, as every method of the client is. - The UI names the company a person signs in at: "Sign in with
Google" for Gmail. It offers a sign-in with a code only where the
provider has one. - The UI shows icons, a set of Bootstrap Icons kept in the service, and
asks before a form that changes much in a dialog of its own. Every
secret shown once has a copy button. Lists show recent times as
"3 minutes ago"./puts the cursor in the search field. Nothing is
loaded from elsewhere. - The UI's sidebar ends in an account menu with your page, Password,
Second factor and Sign out. It folds to icons, and a dot marks
Accounts or Webhooks when something there needs a look. Mail is now
Mail search. The Status page is gone: the Accounts list shows
each account's last sync and error, the overview the sync worker. - Lists in the UI create at their head and act on their rows: a plus in
the card's header, a pencil and a bin on each device, token and the
folder shown. Several devices or tokens are ticked and removed or
revoked together, all of them or none, devices after one password
and one code. A folder is renamed and moved in one form, and a move
asks first. - A user's page in the UI has the tabs Rights, Access and Activity. The
grant editor shows each grant as a line to change or remove. The
Users list disables, enables, gives or takes a role of the ticked
users, all of them or none, and names those it could not change. - Each row of the UI's Accounts list links to the account's mail and
sends. A message has its actions in its header and its keywords as
chips, and Move asks for the folder in a dialog. The batch bar of a
mail list asks for a folder only to move, and its head ticks all.
Fixed
- The MCP server writes a sender's name, a subject and a file name on
one line each. A line break in them could start a line inside the
mail's marker that looked like a header of its own. - The mail form of the UI shows a name with a double quote or a
backslash in it so that a reply or a forward reads it back as it was. - A Gmail or Microsoft account whose OAuth app is missing from the
settings no longer breaks every page of the UI. The Accounts list
says it cannot be used, it counts as needing attention, and it can be
removed. InGET /v1/statusit is notsyncedand needs
attention. - Changing your password checks the current one as any confirmation
does: a wrong one counts against your name and is audited as
auth.confirm_failed. - A webhook's
accountscannot be an empty list, which would hear
nothing. Sendnullfor every account. - Changing a webhook needs
list_changesonly on the accounts the
change adds. An owner who lost the right on one of its accounts can
still change the URL or the events. - The UI's Users list counts only the users a batch changed, not those
that were as asked already. mailbox-client:Mehas the caller'suser_idandname.- The text of a mail written as HTML only, on the mail page and as the
text part of a send, leaves out what its inline style hides as the
MCP server does: too small, too faint, off the page, cut to nothing,
or in the colour of its background. An end tag without its start tag
no longer ends hidden text early. - The MCP server's text of an HTML mail keeps a link's address after
its text and puts a dash before each list item, as the mail page
does. - The text of an HTML mail keeps text whose colour and background are
alike only by a keyword such asinheritorcurrentcolor. Only
colours of their own count, such as#fff,rgb(...)orwhite.
leftandtopfar off the page hide text only withposition
absolute,fixedorrelative.
Security
- A secret shown once in the UI goes when its 5 minutes run out, at
the next request of its session or the next sign-in of anyone, not
only at the next page that could show it. - The audit keeps
auth.session_evictedwhen a ...
0.3.1 (beta)
Added
- The project has a logo. Its icon heads each README, and the
configuration UI shows it in the sidebar and as the favicon. The
sign-in page shows the logo with its word mark.
Changed
containers/dev/is nowcontainers/development/. This concerns a
clone of the repository only, not the packages or the images. A
master key kept incontainers/dev/secrets/moves along by hand.
The compose project keeps its namemailbox-dev, and with it its
volume.
0.3.0 (beta)
The status is beta: usable with real accounts. A breaking change of
the API or the configuration is announced here. Stored data is
carried forward by migrations.
Added
-
benethos-mailbox-client, the Python client of the REST API, as a
package of its own on PyPI:MailboxClientfor async code and
SyncMailboxClientfor code without an event loop, with the same
methods and records. The MCP server is built on it. Its interface has
no stability promise yet. Every error is aMailboxError. An answer it
cannot read, one that is no JSON or lacks a field, is anApiError
with the codeunexpected_response.from_environment()reads
MAILBOX_SERVICE_URL,MAILBOX_SERVICE_TOKENand
MAILBOX_SERVICE_ALLOW_HTTPonce, for a program that makes clients
for a long time. -
Microsoft accounts connect without an app of your own: the service
comes with the project's app, a public client without a secret, used
whenMAILBOX_SERVICE_OAUTH_MICROSOFT_CLIENT_IDis not set. Changing
between it and an app of your own asks every Microsoft account to sign
in again. -
Sign in with a code:
POST /v1/oauth/{provider}/devicereturns a code
to enter at the provider on any device, and
POST /v1/oauth/{provider}/device/{sign_in_id}answers with the
account once the person signed in. It needs no address the provider
sends a browser back to, so it works on a server. The UI offers it
beside the sign-in in the browser, when connecting and on an account's
page. With the project's app away from localhost, it is the only way. -
MAILBOX_SERVICE_PROVIDERS, a JSON list of the kinds of account a
deployment offers, every kind without it. Discovery leaves out the
others, and connecting one answers501. Accounts connected before
keep working. -
JMAP accounts,
"provider": "jmap", for Fastmail, Stalwart, Cyrus and
any JMAP server. The settings namehost,portandpathof the
session (default/.well-known/jmap), andauth:passwordwith
username, ortokenwith the credentialtoken. Everything an IMAP
account does works, sending included, through JMAP without SMTP
settings. Ids stay when a message moves, and a message can be in
several folders (labels). The change feed learns of changes since a
state, and the server's event source wakes the sync. A deleted message
is reported without a folder. A replaced draft gets a new id. -
Discovery asks
https://{domain}/.well-known/jmapand the SRV record
_jmap._tcp, without a credential:jmapis a source, aprovider
of a candidate and aprotocolof a server, whosepathnames the
session. A JMAP candidate comes before IMAP, and POP3 is offered only
where neither is.api_tokenis a newcredential. The Fastmail
preset offers JMAP with an API token. The UI connects a JMAP server
from discovery or by hand, with a password or an API token. -
POP3 accounts,
"provider": "pop3", for mailboxes without IMAP. The
inbox is the one folder. Messages are listed newest first, read and
deleted for good, and sending goes over SMTP. Read state, stars,
keywords, a delete into the trash, search, folders and drafts answer
501. The sync polls the mailbox, so the change feed,whats_newand
webhooks report what arrives and leaves. A server withoutUIDLis
refused. Discovery offers POP3 where it finds no IMAP:pop3is a
providerof a candidate and aprotocolof a server. The UI offers
POP3 when setting up by hand. -
capabilitieson an account and on each account of/v1/me: what it
can do beyond reading its inbox.flags,foldersandsearchare
new. A POP3 account has none of them. The UI offers no action an
account cannot do, and the MCP server'slist_accountsnames what an
account lacks asunsupported. -
containers/production/: Mailbox in operation from the published
images, without a clone of the repository..envnames the version,
the profilehttpsputs Caddy in front with a certificate from Let's
Encrypt. The MCP server is not part of it by default: each client starts
one of its own, with its own token, against the service's address.setup.shdoes the
first start: the master key, the keys, the first administrator. Its
README has the same steps by hand. It keeps the project name
benethos-mailbox, so the data volume of the compose file before
stays: copy itssecrets/master_keyover. -
MCP server instances beside the service in
containers/production/,
for clients that connect over HTTP. Each is a block in
compose.override.yamlthat extendsmcp.yaml, with the profile
mcp, one user's token and a bearer token of its own. It listens on
127.0.0.1, or Caddy routes to it under a path of its own, such as
/mcp/agent, by a file incaddy.d/. -
MAILBOX_TLSincontainers/production/.env: where Caddy's
certificate comes from.acmeis Let's Encrypt as before,internal
Caddy's own CA,filesa certificate of your own insecrets/tls/,
for a network without access from the internet. -
Settings and data in the folders of the operating system, for an
installation without the repository:%LOCALAPPDATA%\benethos-mailbox-service\
withconfiganddataon Windows,~/.config/benethos-mailbox-service/
and~/.local/share/benethos-mailbox-service/on Linux. A file named
with--env-filecomes first, thenconfig/benethos-mailbox-service/
ordata/benethos-mailbox-service/in the working directory, as
before. -
benethos-mailbox-service pathsnames the settings file, the data
folder, the database and where the master key is. -
The MCP server reads an optional
.env:--env-fileor
MAILBOX_MCP_ENV_FILE, elseconfig/benethos-mailbox-mcp/.envin the
working directory, else its config folder of the operating system. The
environment wins over it. -
serviceon users and roles, inPOST,PATCHandPUT: rights of
the service, bound to no account.accounts.connect,users.read,
users.manage,webhooks.manage,audit(below) andadminbelong
there, or single operations of them./v1/permissionslists these
groups asservice. -
accounts.connect:discover_accountandcreate_account, which
were part ofaccounts.manage, and the sign-in with a code. -
users.read:list_users,get_user,list_tokens,list_rolesand
get_role, to see users and roles without changing them.users.manage
keeps every right it had. -
Whoever connects an account gets
accounts.manageon it, unless it
holds that there already. -
expires_aton a grant, with a time zone, null for never. An expired
grant grants nothing and stays until it is removed. A grant is handed
out for no longer than the giver holds it. The editor has a field
"Valid until", and the user's page marks an expired grant. -
/v1/megives each account asendinglist, one entry per grant that
allows sending there: itsrecipients,max_sends_per_dayand
sends_left, how many more the limit allows now. -
The MCP server's
list_accountsnames these limits besidesend, and
warns of an account where the token may read mail and send it anywhere. -
The UI's New role page offers four templates that fill the form:
Reader, Agent, Sender and Operator. -
folderson a grant: reading, writing and deleting mail only in these
folders and their subfolders, named by role, name or id. A message or
folder outside answers404, a move or a new folder outside403.
Deleting to the trash stays allowed, reading the trash needs it in the
list. Folder lists, message lists, the change feed and webhooks leave
out the rest, and a reply or forward needs its original in reach. The
editor has a field "Only in the folders". -
GET /v1/audit(list_activity): the audit of administration, newest
first. Sign-ins, failed ones and refused tokens, and who changed users,
passwords, tokens, roles, accounts and webhooks, with the user, the
credential, the client address, the record touched, the outcome and
what was done. Never a secret, never mail content, and a failed sign-in
with a name that is no user's keeps no name. Filtersuser,
activity(a name or its area),record,after,before. Kept for
MAILBOX_SERVICE_AUDIT_DAYSdays, as the audit of sends. -
auditinservicereads it.auditin a grant stays the audit of
sends./v1/permissionslistsauditamong the groups of the service,
withlist_activityin the group. -
The UI has a page Audit under Service, and a user's page a card Recent
activity, both forauditinservice. -
Filters on the lists, as the UI has them. Each is optional, and a list
without one answers as before.GET /v1/sendsand
GET /v1/accounts/{id}/sends:user,outcome,recipient(part of
an address),afterandbefore(a time with a zone).
GET /v1/users:name(part of it),role,disabled,ui_sign_in.
GET /v1/accounts:address(part of it),provider,status.
GET /v1/webhooks:url(part of it),account,failing. -
A user in the answers of
/v1/userssays how it signs in to the UI:
has_password,must_change(a password set for it, to be changed at
the next sign-in) andlast_sign_in_at. The schema isUserInfo, the
fields ofUserand these three. The user's page in the UI names a
password still to be changed. -
The UI shows a message's keywords in the list and adds or removes them
on the message. Those starting with$stay as they are. -
accountsonGET /v1/sends: the audit of the named accounts alone,
as/v1/messagesand/v1/changestake it. An account the caller
may not audit is left out without a word. -
GET /v1/status(get_status, inaccounts.read): the sync worker
and the accounts the caller may see the status of, as the UI's status
page shows them. Per account its status, the last pass, the last error
and whether it needs a look. Nothing is asked ...