<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Isds on Lessons learned</title><link>https://eghuro.cz/tags/isds/</link><description>Recent content in Isds on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sun, 13 Sep 2026 14:09:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/isds/index.xml" rel="self" type="application/rss+xml"/><item><title>A read with legal effect: listing ISDS messages counts as delivery</title><link>https://eghuro.cz/posts/isds-listing-messages-is-legal-delivery/</link><pubDate>Sun, 13 Sep 2026 14:09:00 +0200</pubDate><guid>https://eghuro.cz/posts/isds-listing-messages-is-legal-delivery/</guid><description>&lt;p&gt;Polling an inbox from a cron job looks harmless, since reading has no side effects. In the Czech Data Box system (ISDS, &lt;em&gt;datové schránky&lt;/em&gt;) it does: the moment an authorized person logs in to the box is the moment every message waiting in it is legally delivered (&lt;em&gt;doručení&lt;/em&gt;). 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&amp;rsquo;s request starts the deadline to answer it, whether anyone reads the message or not.&lt;/p&gt;&#10;&lt;p&gt;If your code talks to ISDS, every scheduled retrieval and every copy of your database that still holds the box&amp;rsquo;s password is a legal act.&lt;/p&gt;&#10;&#10;&lt;h2 id="setup"&gt;Setup &lt;/h2&gt;&#10;&lt;p&gt;An Odoo 18 addon files incoming Data Box messages into the company&amp;rsquo;s document store (the OCA &lt;code&gt;dms&lt;/code&gt; addon). It talks to ISDS over SOAP with zeep (&lt;a&#10; href="https://eghuro.cz/posts/zeep-repeated-sequence-value-1"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;the parsing bug from that client is here&lt;/a&gt;&#10;). A retrieval calls &lt;code&gt;GetListOfReceivedMessages&lt;/code&gt; for the last 100 days and downloads each new message with its signed copy.&lt;/p&gt;&#10;&lt;p&gt;A possible next feature was a cron job, so that the documents appear without anyone pressing a button.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-a-read-starts-a-deadline"&gt;Why a read starts a deadline &lt;/h2&gt;&#10;&lt;p&gt;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&amp;rsquo;s arrival, it counts as delivered on the last day of that period, which Czech lawyers call delivery by fiction.&lt;/p&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;p&gt;Not every call has this effect. &lt;code&gt;GetOwnerInfoFromLogin&lt;/code&gt; 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.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-design"&gt;The design &lt;/h2&gt;&#10;&lt;p&gt;Once the domain model had the rule written down (&amp;ldquo;Every Retrieval causes Delivery of the Messages waiting in the box&amp;rdquo;), the decisions followed from it:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Each connection has a Retrieval Mode, On demand or Scheduled, and On demand is the default.&lt;/li&gt;&#10;&lt;li&gt;The &amp;ldquo;Check data box&amp;rdquo; button asks for confirmation: &amp;ldquo;Checking the data box counts as Delivery (doručení) of every Message waiting in it, which starts legal deadlines. Continue?&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;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.&lt;/li&gt;&#10;&lt;li&gt;The one-step setup wizard says that its first retrieval counts as delivery.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="copies-of-the-production-database"&gt;Copies of the production database &lt;/h2&gt;&#10;&lt;p&gt;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 &amp;ldquo;Check data box&amp;rdquo; to see what it does, delivers the production company&amp;rsquo;s official mail.&lt;/p&gt;&#10;&lt;p&gt;Odoo has a hook for this. When you run &lt;code&gt;odoo-bin neutralize&lt;/code&gt;, or restore a database with &lt;strong&gt;Neutralize&lt;/strong&gt; checked in the database manager, it runs each installed module&amp;rsquo;s &lt;code&gt;data/neutralize.sql&lt;/code&gt;. Core modules use it to switch off outgoing mail and crons. The ISDS addon adds its own:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- a neutralized copy must never cause Delivery: drop the Credential passwords&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- and stop Scheduled Retrievals&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dms_isds_connection&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;retrieval_mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;on_demand&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- unfinished Quick Starts hold a password too, and connecting retrieves at once&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dms_isds_quick_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A retrieval on a neutralized copy fails on the missing password before it contacts ISDS. The test checks exactly that. It runs the module&amp;rsquo;s neutralization queries, presses the button, triggers the cron, and asserts that the ISDS listing was never called:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_neutralize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_check_data_box&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CRON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method_direct_trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assert_not_called&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The development setup restores production data and neutralizes it on every refresh, so nobody has to remember.&lt;/p&gt;&#10;&lt;p&gt;Neutralization changes only the copy. The original backup still holds the password in plaintext, so restrict access to backups and encrypt them.&lt;/p&gt;&#10;&#10;&lt;h2 id="other-reads-with-side-effects"&gt;Other reads with side effects &lt;/h2&gt;&#10;&lt;p&gt;ISDS is an extreme case because the side effect is legal. Plenty of other APIs change state when you read:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;IMAP &lt;code&gt;FETCH BODY[]&lt;/code&gt; sets the &lt;code&gt;\Seen&lt;/code&gt; flag; &lt;code&gt;BODY.PEEK[]&lt;/code&gt; doesn&amp;rsquo;t.&lt;/li&gt;&#10;&lt;li&gt;Receiving from a queue hides or consumes the message.&lt;/li&gt;&#10;&lt;li&gt;Opening a message can send a read receipt the sender treats as proof.&lt;/li&gt;&#10;&lt;li&gt;One-time codes and signed links stop working after the first use.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;Before you schedule a &amp;ldquo;read-only&amp;rdquo; 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&amp;rsquo;t log in.&lt;/p&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;</description></item></channel></rss>