isds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5f6c4b4643e4f08410bb292d56afe96a75db717055ec20579dafea9da1dbadd
4
+ data.tar.gz: 7658f9a652173800354c7b8b2f5ce5d305d5c2d850237514df18027eccb86fa2
5
+ SHA512:
6
+ metadata.gz: 8682b7e75cd0e581350ae9c000f43a323e914acb5224a8501f88f706f9d227c99eb51cdfcc958898c8368c9c940c269fd9a4ef0be85a206e5b64088daa20f152
7
+ data.tar.gz: 662f41fd534dca323546dbe80e3d20984eac614e160c8193cba1af8bf9b36a8cf92d6c11c26497507cdc48eb748378573f95a6c0cf0df55be8e98ef8d842836d
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,44 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.1
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+ Exclude:
9
+ - "vendor/**/*"
10
+ - "pkg/**/*"
11
+ - "*.md"
12
+
13
+ Style/Documentation:
14
+ Enabled: false
15
+
16
+ Style/FrozenStringLiteralComment:
17
+ Enabled: true
18
+
19
+ Metrics/ClassLength:
20
+ Max: 150
21
+
22
+ Metrics/MethodLength:
23
+ Max: 25
24
+
25
+ Metrics/BlockLength:
26
+ Exclude:
27
+ - "spec/**/*"
28
+ - "*.gemspec"
29
+
30
+ Layout/LineLength:
31
+ Max: 120
32
+
33
+ RSpec/ExampleLength:
34
+ Max: 20
35
+
36
+ RSpec/MultipleExpectations:
37
+ Max: 5
38
+
39
+ RSpec/NestedGroups:
40
+ Max: 4
41
+
42
+ RSpec/DescribeClass:
43
+ Exclude:
44
+ - "spec/integration/**/*"
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Segfault Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # ISDS
2
+
3
+ Ruby client for the Czech Data Box system (ISDS -- Informacni system datovych schranek).
4
+
5
+ Provides an idiomatic Ruby interface to the [ISDS SOAP web services](https://www.datoveschranky.info/) for sending and receiving official data messages, searching databoxes, managing users, and handling large messages (VoDZ).
6
+
7
+ ## Requirements
8
+
9
+ - Ruby >= 3.1
10
+ - Active ISDS account (production or [test environment on czebox.cz](https://www.czebox.cz/))
11
+
12
+ ## Installation
13
+
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem 'isds'
18
+ ```
19
+
20
+ Then run `bundle install`.
21
+
22
+ ## Quick start
23
+
24
+ ```ruby
25
+ require 'isds'
26
+
27
+ client = ISDS::Client.new(ISDS::Configuration.new(
28
+ environment: :test, # :test (czebox.cz) or :production (mojedatovaschranka.cz)
29
+ auth_method: :basic,
30
+ username: 'your_username',
31
+ password: 'your_password'
32
+ ))
33
+
34
+ client.authenticate!
35
+ puts client.owner_info
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ### Global configuration
41
+
42
+ ```ruby
43
+ ISDS.configure do |config|
44
+ config.environment = :production
45
+ config.auth_method = :basic
46
+ config.username = ENV.fetch('ISDS_USERNAME')
47
+ config.password = ENV.fetch('ISDS_PASSWORD')
48
+ config.timeout = 30
49
+ config.request_timeout = 120
50
+ config.enable_request_logging = false
51
+ end
52
+
53
+ client = ISDS::Client.new
54
+ ```
55
+
56
+ ### Per-client configuration
57
+
58
+ ```ruby
59
+ config = ISDS::Configuration.new(
60
+ environment: :test,
61
+ auth_method: :basic,
62
+ username: 'user',
63
+ password: 'pass'
64
+ )
65
+
66
+ client = ISDS::Client.new(config)
67
+ ```
68
+
69
+ ### Authentication methods
70
+
71
+ **Basic authentication** (username + password):
72
+
73
+ ```ruby
74
+ ISDS::Configuration.new(auth_method: :basic, username: 'user', password: 'pass')
75
+ ```
76
+
77
+ **Certificate authentication**:
78
+
79
+ ```ruby
80
+ ISDS::Configuration.new(
81
+ auth_method: :certificate,
82
+ certificate_path: '/path/to/cert.pem',
83
+ private_key_path: '/path/to/key.pem',
84
+ key_password: 'optional_key_password'
85
+ )
86
+ ```
87
+
88
+ ### SSL options
89
+
90
+ ```ruby
91
+ ISDS::Configuration.new(
92
+ ssl_verify_peer: true,
93
+ ssl_ca_file: '/path/to/ca-bundle.crt',
94
+ ssl_version: :TLSv1_2
95
+ )
96
+ ```
97
+
98
+ ### Custom endpoints
99
+
100
+ Override default endpoints if needed:
101
+
102
+ ```ruby
103
+ ISDS::Configuration.new(
104
+ endpoints: {
105
+ ws_url: 'https://custom-proxy.example.com/soap',
106
+ ws2_url: 'https://custom-proxy.example.com/vdz_ws/'
107
+ }
108
+ )
109
+ ```
110
+
111
+ ## Usage
112
+
113
+ ### Messages
114
+
115
+ **Send a message:**
116
+
117
+ ```ruby
118
+ attachment = ISDS::Attachment.new(
119
+ filename: 'document.pdf',
120
+ file_path: '/path/to/document.pdf'
121
+ )
122
+
123
+ message_id = client.send_message(
124
+ 'abc1234', # recipient databox ID
125
+ subject: 'Test message',
126
+ attachments: [attachment],
127
+ personal_delivery: true, # optional
128
+ to_hands: 'Jan Novak' # optional
129
+ )
130
+ ```
131
+
132
+ **List received messages:**
133
+
134
+ ```ruby
135
+ messages = client.receive_messages(limit: 50, unread_only: true)
136
+
137
+ messages.each do |msg|
138
+ puts "#{msg.id}: #{msg.subject} from #{msg.sender}"
139
+ puts " Delivered: #{msg.delivered?}, Read: #{msg.read?}"
140
+ end
141
+ ```
142
+
143
+ **List sent messages:**
144
+
145
+ ```ruby
146
+ messages = client.sent_messages(limit: 20)
147
+ ```
148
+
149
+ **Download a specific message with attachments:**
150
+
151
+ ```ruby
152
+ message = client.find_message('12345678')
153
+ message.download_all_attachments('/tmp/attachments')
154
+ ```
155
+
156
+ **Mark messages as read:**
157
+
158
+ ```ruby
159
+ client.mark_messages_read(['12345678', '12345679'])
160
+ ```
161
+
162
+ ### Databox search
163
+
164
+ **Search by name:**
165
+
166
+ ```ruby
167
+ results = client.search_databoxes('Ministerstvo')
168
+ results.each do |databox|
169
+ puts "#{databox.id}: #{databox.name} (#{databox.type})"
170
+ end
171
+ ```
172
+
173
+ **Find by databox ID:**
174
+
175
+ ```ruby
176
+ databox = client.find_databox('abc1234')
177
+ puts databox.name if databox
178
+ ```
179
+
180
+ **Advanced search:**
181
+
182
+ ```ruby
183
+ # Search by ICO
184
+ ISDS::Search.by_ico('12345678', connection: client.connection)
185
+
186
+ # Search by name with exact matching
187
+ ISDS::Search.by_name('Exact Name s.r.o.', connection: client.connection, exact: true)
188
+
189
+ # Search by type (FO, PFO, PO, OVM)
190
+ ISDS::Search.by_type(:ovm, connection: client.connection, query: 'Praha')
191
+
192
+ # Search OVM only
193
+ ISDS::Search.ovm_only(connection: client.connection, query: 'Ministerstvo')
194
+
195
+ # Search by address
196
+ ISDS::Search.by_address(connection: client.connection, city: 'Praha', zip_code: '11000')
197
+
198
+ # Paginated search
199
+ page = ISDS::Search.paginated_search('Test', connection: client.connection, page: 1, per_page: 50)
200
+ page[:results].each { |db| puts db.name }
201
+ ```
202
+
203
+ **Check databox status:**
204
+
205
+ ```ruby
206
+ status = client.check_databox_status('abc1234')
207
+ puts status[:active] # => true/false
208
+ ```
209
+
210
+ ### Databox info
211
+
212
+ ```ruby
213
+ info = client.owner_info
214
+ password_info = client.password_info
215
+ ```
216
+
217
+ ### User management
218
+
219
+ ```ruby
220
+ # List users
221
+ users = ISDS::User.list(connection: client.connection)
222
+
223
+ # Add user
224
+ ISDS::User.add(
225
+ connection: client.connection,
226
+ databox_id: 'abc1234',
227
+ first_name: 'Jan',
228
+ last_name: 'Novak'
229
+ )
230
+
231
+ # Remove user
232
+ ISDS::User.remove(connection: client.connection, user_id: '123')
233
+ ```
234
+
235
+ ### Attachments
236
+
237
+ ```ruby
238
+ # From file
239
+ attachment = ISDS::Attachment.new(filename: 'doc.pdf', file_path: '/path/to/doc.pdf')
240
+
241
+ # From content
242
+ attachment = ISDS::Attachment.new(filename: 'doc.pdf', content: pdf_bytes)
243
+
244
+ # Properties
245
+ attachment.size # => byte size
246
+ attachment.mime_type # => 'application/pdf' (auto-detected)
247
+ attachment.encoded_content # => Base64-encoded string
248
+ ```
249
+
250
+ Standard message attachments are limited to 20 MB. Large messages (VoDZ) support up to 100 MB.
251
+
252
+ ## Error handling
253
+
254
+ All ISDS errors inherit from `ISDS::Error`:
255
+
256
+ ```ruby
257
+ begin
258
+ client.authenticate!
259
+ client.send_message('abc1234', subject: 'Test', attachments: [])
260
+ rescue ISDS::InvalidCredentialsError => e
261
+ puts "Auth failed: #{e.message}"
262
+ rescue ISDS::DataboxNotFoundError => e
263
+ puts "Databox not found: #{e.message}"
264
+ rescue ISDS::TimeoutError => e
265
+ puts "Request timed out: #{e.message}"
266
+ rescue ISDS::NetworkError => e
267
+ puts "Network issue: #{e.message}"
268
+ rescue ISDS::Error => e
269
+ puts "ISDS error [#{e.code}]: #{e.message}"
270
+ end
271
+ ```
272
+
273
+ Error hierarchy:
274
+
275
+ ```
276
+ ISDS::Error
277
+ ConfigurationError
278
+ NetworkError
279
+ TimeoutError
280
+ ConnectionError
281
+ AuthenticationError
282
+ InvalidCredentialsError
283
+ CertificateError
284
+ DataboxNotFoundError
285
+ MessageNotFoundError
286
+ PermissionDeniedError
287
+ InvalidMessageError
288
+ ServiceUnavailableError
289
+ TemporaryUnavailableError
290
+ QuotaExceededError
291
+ AttachmentError
292
+ FileSizeExceededError
293
+ UnsupportedFormatError
294
+ ```
295
+
296
+ ISDS status codes are mapped automatically:
297
+
298
+ | Code | Error class |
299
+ |------|-------------|
300
+ | 0001 | `InvalidCredentialsError` |
301
+ | 0002 | `PermissionDeniedError` |
302
+ | 0003 | `DataboxNotFoundError` |
303
+ | 0004 | `MessageNotFoundError` |
304
+ | 0005 | `InvalidMessageError` |
305
+ | 0006 | `QuotaExceededError` |
306
+ | 9999 | `ServiceUnavailableError` |
307
+
308
+ ## Endpoints
309
+
310
+ The gem uses the official ISDS SOAP endpoints:
311
+
312
+ | Environment | Standard operations | Large messages (VoDZ) |
313
+ |-------------|--------------------|-----------------------|
314
+ | Production | `https://ws1.mojedatovaschranka.cz/soap` | `https://ws1.mojedatovaschranka.cz/vdz_ws/` |
315
+ | Test | `https://www.czebox.cz/soap` | `https://www.czebox.cz/vdz_ws/` |
316
+
317
+ All standard services (operations, info, search, access, supplementary) are routed through the single `/soap` endpoint. Large message operations use the `/vdz_ws/` endpoint.
318
+
319
+ ## Development
320
+
321
+ ```bash
322
+ # Run unit tests
323
+ bundle exec rspec
324
+
325
+ # Run rubocop
326
+ bundle exec rubocop
327
+
328
+ # Run integration tests (requires czebox.cz credentials)
329
+ ISDS_USERNAME=your_user ISDS_PASSWORD=your_pass bundle exec rake integration
330
+ ```
331
+
332
+ Integration tests run against the [czebox.cz](https://www.czebox.cz/) test environment and are excluded from the default test suite.
333
+
334
+ ## ISDS specification references
335
+
336
+ - [Provozni rad ISDS (PDF)](https://www.datoveschranky.info/documents/1744842/1746058/Provozni_rad_ISDS.pdf) -- official operational rules
337
+ - [ISDS web services documentation](https://www.datoveschranky.info/documents/1744842/1746058/WS_ISDS_Manipulace_s_datovymi_zpravami.pdf) -- SOAP API for message operations
338
+ - [ISDS WSDL definitions](https://www.czebox.cz/static/wsdl/v20/) -- WSDL schemas (test environment)
339
+ - [datoveschranky.info](https://www.datoveschranky.info/) -- official ISDS portal with full documentation
340
+ - [czebox.cz](https://www.czebox.cz/) -- ISDS test environment
341
+
342
+ The gem implements the ISDS v20 namespace (`http://isds.czechpoint.cz/v20`).
343
+
344
+ ## License
345
+
346
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ RSpec::Core::RakeTask.new(:integration) do |t|
9
+ t.rspec_opts = '--tag integration'
10
+ end
11
+
12
+ require 'rubocop/rake_task'
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[spec rubocop]
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'mime/types'
5
+
6
+ module ISDS
7
+ class Attachment
8
+ MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB per attachment (standard messages)
9
+ LARGE_MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB for VoDZ
10
+
11
+ attr_reader :filename, :mime_type, :content, :meta_type, :description
12
+
13
+ # rubocop:disable Metrics/ParameterLists -- all params are keyword args with sensible defaults
14
+ def initialize(filename:, content: nil, file_path: nil, mime_type: nil, meta_type: 'main', description: nil)
15
+ @filename = filename
16
+ @content = content || (file_path ? File.binread(file_path) : nil)
17
+ @mime_type = mime_type || detect_mime_type(filename)
18
+ @meta_type = meta_type
19
+ @description = description || filename
20
+ validate!
21
+ end
22
+ # rubocop:enable Metrics/ParameterLists
23
+
24
+ def size
25
+ content&.bytesize || 0
26
+ end
27
+
28
+ def encoded_content
29
+ Base64.strict_encode64(content)
30
+ end
31
+
32
+ def self.build_soap_element(attachment)
33
+ attachment = from_hash(attachment) if attachment.is_a?(Hash)
34
+
35
+ {
36
+ 'isds:dmMimeType' => attachment.mime_type,
37
+ 'isds:dmFileMetaType' => attachment.meta_type,
38
+ 'isds:dmFileDescr' => attachment.description,
39
+ 'isds:dmEncodedContent' => attachment.encoded_content
40
+ }
41
+ end
42
+
43
+ def self.from_hash(hash)
44
+ new(
45
+ filename: hash[:filename] || hash[:file_path]&.then { |p| File.basename(p) },
46
+ file_path: hash[:file_path],
47
+ content: hash[:content],
48
+ mime_type: hash[:mime_type],
49
+ meta_type: hash[:meta_type] || 'main',
50
+ description: hash[:description]
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ def validate!
57
+ raise AttachmentError, 'Content is required' if content.nil? || content.empty?
58
+ raise FileSizeExceededError, 'File exceeds maximum size of 20MB' if size > MAX_FILE_SIZE
59
+ end
60
+
61
+ def detect_mime_type(filename)
62
+ types = MIME::Types.type_for(filename)
63
+ types.first&.content_type || 'application/octet-stream'
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ISDS
4
+ module Authentication
5
+ class AccessInterface < Base
6
+ def apply(savon_options)
7
+ savon_options[:basic_auth] = [configuration.username, configuration.password]
8
+ savon_options[:headers] = (savon_options[:headers] || {}).merge(
9
+ 'X-ISDS-Access-Interface' => '1'
10
+ )
11
+ savon_options
12
+ end
13
+
14
+ def valid?
15
+ !configuration.username.nil? && !configuration.username.empty? &&
16
+ !configuration.password.nil? && !configuration.password.empty?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ISDS
4
+ module Authentication
5
+ class Base
6
+ attr_reader :configuration
7
+
8
+ def initialize(configuration)
9
+ @configuration = configuration
10
+ end
11
+
12
+ def apply(savon_options)
13
+ raise NotImplementedError, "#{self.class}#apply must be implemented"
14
+ end
15
+
16
+ def valid?
17
+ raise NotImplementedError, "#{self.class}#valid? must be implemented"
18
+ end
19
+
20
+ def self.build(configuration)
21
+ case configuration.auth_method
22
+ when :basic
23
+ Basic.new(configuration)
24
+ when :certificate
25
+ Certificate.new(configuration)
26
+ when :access_interface
27
+ AccessInterface.new(configuration)
28
+ else
29
+ raise ConfigurationError, "Unknown auth method: #{configuration.auth_method}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ISDS
4
+ module Authentication
5
+ class Basic < Base
6
+ def apply(savon_options)
7
+ savon_options[:basic_auth] = [configuration.username, configuration.password]
8
+ savon_options
9
+ end
10
+
11
+ def valid?
12
+ !configuration.username.nil? && !configuration.username.empty? &&
13
+ !configuration.password.nil? && !configuration.password.empty?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module ISDS
6
+ module Authentication
7
+ class Certificate < Base
8
+ def apply(savon_options)
9
+ savon_options[:ssl_cert_file] = configuration.certificate_path
10
+ savon_options[:ssl_cert_key_file] = configuration.private_key_path
11
+ savon_options[:ssl_cert_key_password] = configuration.key_password if configuration.key_password
12
+ savon_options
13
+ end
14
+
15
+ def valid?
16
+ certificate_exists? && key_exists? && certificate_readable?
17
+ end
18
+
19
+ def certificate
20
+ @certificate ||= OpenSSL::X509::Certificate.new(File.read(configuration.certificate_path))
21
+ end
22
+
23
+ def expires_at
24
+ certificate.not_after
25
+ end
26
+
27
+ def expired?
28
+ expires_at < Time.now
29
+ end
30
+
31
+ def subject_info
32
+ certificate.subject.to_s
33
+ end
34
+
35
+ def self.from_p12(p12_path, password)
36
+ p12 = OpenSSL::PKCS12.new(File.read(p12_path), password)
37
+ { certificate: p12.certificate, key: p12.key }
38
+ end
39
+
40
+ private
41
+
42
+ def certificate_exists?
43
+ !configuration.certificate_path.nil? && File.exist?(configuration.certificate_path)
44
+ end
45
+
46
+ def key_exists?
47
+ !configuration.private_key_path.nil? && File.exist?(configuration.private_key_path)
48
+ end
49
+
50
+ def certificate_readable?
51
+ OpenSSL::X509::Certificate.new(File.read(configuration.certificate_path))
52
+ true
53
+ rescue OpenSSL::X509::CertificateError, Errno::ENOENT
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end