A read with legal effect: listing ISDS messages counts as delivery

·5 mins

Polling an inbox from a cron job looks harmless, since reading has no side effects. In the Czech Data Box system (ISDS, datové schránky) it does: the moment an authorized person logs in to the box is the moment every message waiting in it is legally delivered (doručení). An application counts as that person when it logs in and fetches the list of received messages. A 3 a.m. retrieval of a tax office’s request starts the deadline to answer it, whether anyone reads the message or not.

If your code talks to ISDS, every scheduled retrieval and every copy of your database that still holds the box’s password is a legal act.

Setup

An Odoo 18 addon files incoming Data Box messages into the company’s document store (the OCA dms addon). It talks to ISDS over SOAP with zeep (the parsing bug from that client is here ). A retrieval calls GetListOfReceivedMessages for the last 100 days and downloads each new message with its signed copy.

A possible next feature was a cron job, so that the documents appear without anyone pressing a button.

Why a read starts a deadline

Act No. 300/2008 Sb. delivers a message in one of two ways. Under § 17(3), it is delivered when a person whose permissions cover it logs in to the box. Under § 17(4), if nobody logs in within 10 days of the message’s arrival, it counts as delivered on the last day of that period, which Czech lawyers call delivery by fiction.

People rely on fiction as a buffer. A box that nobody opens for a week gives you up to 10 extra days before a deadline starts running. A nightly cron throws that buffer away. Every message is delivered the night after it arrives, and its deadline starts then, while the documents sit unread in a folder.

Not every call has this effect. GetOwnerInfoFromLogin checks the credentials and returns the box ID and owner without causing delivery. The addon uses it to validate a login before it creates anything.

The design

Once the domain model had the rule written down (“Every Retrieval causes Delivery of the Messages waiting in the box”), the decisions followed from it:

  • Each connection has a Retrieval Mode, On demand or Scheduled, and On demand is the default.
  • The “Check data box” button asks for confirmation: “Checking the data box counts as Delivery (doručení) of every Message waiting in it, which starts legal deadlines. Continue?”
  • Choosing Scheduled shows a warning, and the form keeps showing it for as long as the connection stays Scheduled. The daily cron job skips On demand connections.
  • The one-step setup wizard says that its first retrieval counts as delivery.

Copies of the production database

The usual way to get a realistic test environment for Odoo is to restore a production backup. That copy holds the ISDS username and password, and possibly a scheduled connection. A staging server that runs the cron, or a developer who presses “Check data box” to see what it does, delivers the production company’s official mail.

Odoo has a hook for this. When you run odoo-bin neutralize, or restore a database with Neutralize checked in the database manager, it runs each installed module’s data/neutralize.sql. Core modules use it to switch off outgoing mail and crons. The ISDS addon adds its own:

-- a neutralized copy must never cause Delivery: drop the Credential passwords
-- and stop Scheduled Retrievals
UPDATE dms_isds_connection
   SET password = NULL,
       retrieval_mode = 'on_demand';

-- unfinished Quick Starts hold a password too, and connecting retrieves at once
DELETE FROM dms_isds_quick_start;

A retrieval on a neutralized copy fails on the missing password before it contacts ISDS. The test checks exactly that. It runs the module’s neutralization queries, presses the button, triggers the cron, and asserts that the ISDS listing was never called:

self._neutralize()

with self.assertRaisesRegex(UserError, "password"):
    self.connection.action_check_data_box()
self.env.ref(CRON).method_direct_trigger()

listing.assert_not_called()

The development setup restores production data and neutralizes it on every refresh, so nobody has to remember.

Neutralization changes only the copy. The original backup still holds the password in plaintext, so restrict access to backups and encrypt them.

Other reads with side effects

ISDS is an extreme case because the side effect is legal. Plenty of other APIs change state when you read:

  • IMAP FETCH BODY[] sets the \Seen flag; BODY.PEEK[] doesn’t.
  • Receiving from a queue hides or consumes the message.
  • Opening a message can send a read receipt the sender treats as proof.
  • One-time codes and signed links stop working after the first use.

Each of these needs the same treatment as outgoing mail: an explicit default, a warning where a person triggers it, and a neutralization step for every copy of the data.

Takeaway

Before you schedule a “read-only” integration, find out what reading does on the other side. In ISDS, it is legal delivery, so the integration retrieves on demand by default, and a restored database can’t log in.

List the integrations in your own staging setup that still hold production credentials, and check whether any of their reads change something for someone else.