mailbox-kit 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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +104 -0
  4. data/app/controllers/mailbox_kit/management/mailboxes_controller.rb +171 -0
  5. data/app/controllers/mailbox_kit/management/styles_controller.rb +11 -0
  6. data/app/views/layouts/mailbox_kit/management.html.erb +29 -0
  7. data/app/views/mailbox_kit/management/mailboxes/index.html.erb +65 -0
  8. data/app/views/mailbox_kit/management/mailboxes/message.html.erb +34 -0
  9. data/app/views/mailbox_kit/management/mailboxes/show.html.erb +86 -0
  10. data/docs/integration.md +283 -0
  11. data/docs/upgrading.md +60 -0
  12. data/lib/generators/mailbox_kit/install/install_generator.rb +24 -0
  13. data/lib/generators/mailbox_kit/install/templates/create_mailbox_kit_mailboxes.rb +51 -0
  14. data/lib/generators/mailbox_kit/install/templates/create_mailbox_kit_receiving_domains.rb +16 -0
  15. data/lib/generators/mailbox_kit/upgrade/templates/allow_provider_neutral_receiving_domains.rb +9 -0
  16. data/lib/generators/mailbox_kit/upgrade/templates/index_mailbox_kit_inbound_messages.rb +12 -0
  17. data/lib/generators/mailbox_kit/upgrade/upgrade_generator.rb +23 -0
  18. data/lib/mailbox-kit.rb +11 -0
  19. data/lib/mailbox_kit/active_record/base.rb +58 -0
  20. data/lib/mailbox_kit/address_syntax.rb +19 -0
  21. data/lib/mailbox_kit/error.rb +14 -0
  22. data/lib/mailbox_kit/inbound_email.rb +56 -0
  23. data/lib/mailbox_kit/mailboxes/configuration.rb +19 -0
  24. data/lib/mailbox_kit/mailboxes/inbound_retention.rb +16 -0
  25. data/lib/mailbox_kit/mailboxes/models.rb +152 -0
  26. data/lib/mailbox_kit/mailboxes/service.rb +312 -0
  27. data/lib/mailbox_kit/mailboxes.rb +9 -0
  28. data/lib/mailbox_kit/management/adapter.rb +29 -0
  29. data/lib/mailbox_kit/management/configuration.rb +15 -0
  30. data/lib/mailbox_kit/management/engine.rb +13 -0
  31. data/lib/mailbox_kit/management/management.css +68 -0
  32. data/lib/mailbox_kit/management/routes.rb +14 -0
  33. data/lib/mailbox_kit/management.rb +6 -0
  34. data/lib/mailbox_kit/railtie.rb +21 -0
  35. data/lib/mailbox_kit/tenancy.rb +76 -0
  36. data/lib/mailbox_kit/tenant_job_context.rb +88 -0
  37. data/lib/mailbox_kit/version.rb +3 -0
  38. metadata +81 -0
@@ -0,0 +1,76 @@
1
+ require "mailbox_kit/error"
2
+
3
+ module MailboxKit
4
+ # An adapter boundary: the host owns tenant discovery and connection switching.
5
+ # Configure once, before requiring any optional Active Record models.
6
+ module Tenancy
7
+ CONTEXT_KEY = :cloudflare_email_tenant_key
8
+
9
+ class << self
10
+ def configure(base_class:, switch:, current:)
11
+ raise ConfigurationError, "configure tenancy before loading Mailbox Kit models" if @models_loaded
12
+ raise ConfigurationError, "tenancy is already configured" if enabled?
13
+ unless base_class.is_a?(Class) && base_class.respond_to?(:abstract_class?) && base_class.abstract_class?
14
+ raise ConfigurationError, "base_class must be an abstract Active Record class"
15
+ end
16
+ unless switch.respond_to?(:call) && current.respond_to?(:call)
17
+ raise ConfigurationError, "switch and current must be callable"
18
+ end
19
+
20
+ @base_class, @switch, @current = base_class, switch, current
21
+ self
22
+ end
23
+
24
+ def enabled?
25
+ !@switch.nil?
26
+ end
27
+
28
+ def model_base(default)
29
+ @models_loaded = true
30
+ @base_class || default
31
+ end
32
+
33
+ def current_key
34
+ Thread.current[CONTEXT_KEY]
35
+ end
36
+
37
+ # Only trusted host integration code should use this to capture new work.
38
+ # Model access and persisted job deserialization still require explicit
39
+ # gem context; the adapter must not invent a default tenant here.
40
+ def host_current_key
41
+ return unless enabled?
42
+ key = @current.call
43
+ normalize_key(key) unless key.nil?
44
+ end
45
+
46
+ def normalize_key(key)
47
+ unless key.is_a?(String) && !key.empty? && key == key.strip && !key.match?(/[[:cntrl:]]/)
48
+ raise ConfigurationError, "tenant key must be a nonempty string without surrounding whitespace or control characters"
49
+ end
50
+ key.dup.freeze
51
+ end
52
+
53
+ def require_context!
54
+ key = current_key
55
+ raise ConfigurationError, "an explicit Mailbox Kit tenant context is required" unless key
56
+ if enabled? && @current.call != key
57
+ raise ConfigurationError, "host database tenant does not match Mailbox Kit tenant context"
58
+ end
59
+ key
60
+ end
61
+
62
+ def with(key)
63
+ key = normalize_key(key)
64
+ previous = current_key
65
+ run = proc do
66
+ Thread.current[CONTEXT_KEY] = key
67
+ require_context!
68
+ yield
69
+ ensure
70
+ Thread.current[CONTEXT_KEY] = previous
71
+ end
72
+ enabled? ? @switch.call(key, &run) : run.call
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,88 @@
1
+ require "mailbox_kit/tenancy"
2
+
3
+ module MailboxKit
4
+ # Opt in with `prepend MailboxKit::TenantJobContext` on jobs whose
5
+ # arguments or work reference tenant records. Queue payloads must be trusted.
6
+ module TenantJobContext
7
+ PAYLOAD_KEY = "cloudflare_email_tenant_key".freeze
8
+
9
+ def serialize
10
+ key = cloudflare_email_job_tenant_key
11
+ return super unless key
12
+ Tenancy.with(key) do
13
+ payload = super
14
+ verify_cloudflare_email_host_tenant!(key, payload["tenant"])
15
+ payload.merge(PAYLOAD_KEY => key)
16
+ end
17
+ end
18
+
19
+ def deserialize(job_data)
20
+ # Bind before super: host job integrations may deserialize model arguments
21
+ # eagerly. Never infer a missing tenant from the worker's ambient context.
22
+ if !job_data.key?(PAYLOAD_KEY) && cloudflare_email_optional_job_context?
23
+ @cloudflare_email_job_tenant_key = nil
24
+ return super
25
+ end
26
+ @cloudflare_email_job_tenant_key = Tenancy.normalize_key(job_data[PAYLOAD_KEY])
27
+ verify_cloudflare_email_host_tenant!(@cloudflare_email_job_tenant_key, job_data["tenant"])
28
+ Tenancy.with(@cloudflare_email_job_tenant_key) { super }
29
+ end
30
+
31
+ def perform_now
32
+ # Active Job resolves GlobalIDs before around_perform, so that callback
33
+ # cannot safely implement database tenant selection.
34
+ key = cloudflare_email_job_tenant_key
35
+ return super unless key
36
+ if defined?(::ActiveRecord::Tenanted::Job) && respond_to?(:tenant)
37
+ verify_cloudflare_email_host_tenant!(key, tenant)
38
+ end
39
+ Tenancy.with(key) { super }
40
+ end
41
+
42
+ def self.install_framework_jobs!
43
+ if defined?(::ActionMailbox)
44
+ %i[RoutingJob IncinerationJob].each do |name|
45
+ next unless ::ActionMailbox.const_defined?(name)
46
+ klass = ::ActionMailbox.const_get(name)
47
+ klass.define_singleton_method(:cloudflare_email_optional_job_context?) { !Tenancy.enabled? }
48
+ klass.prepend(self) unless klass.ancestors.include?(self)
49
+ end
50
+ end
51
+ if defined?(::ActiveStorage)
52
+ %i[BaseJob AnalyzeJob PurgeJob MirrorJob TransformJob PreviewImageJob].each do |name|
53
+ next unless ::ActiveStorage.const_defined?(name)
54
+ klass = ::ActiveStorage.const_get(name)
55
+ klass.define_singleton_method(:cloudflare_email_optional_job_context?) { !Tenancy.enabled? }
56
+ klass.prepend(self) unless klass.ancestors.include?(self)
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def cloudflare_email_job_tenant_key
64
+ # Once serialized/deserialized, retries retain their original tenant even
65
+ # if re-enqueued from a different tenant or outside a tenant context.
66
+ return @cloudflare_email_job_tenant_key if instance_variable_defined?(:@cloudflare_email_job_tenant_key)
67
+ return nil if cloudflare_email_optional_job_context? && !Tenancy.current_key
68
+ # Native uploads/mailbox work may originate in the host's own with_tenant
69
+ # block. Framework jobs can capture that trusted context on first use.
70
+ # deserialize never takes this path for missing persisted metadata.
71
+ if !Tenancy.current_key && self.class.respond_to?(:cloudflare_email_optional_job_context?)
72
+ key = Tenancy.host_current_key
73
+ return @cloudflare_email_job_tenant_key = key if key
74
+ end
75
+ @cloudflare_email_job_tenant_key = Tenancy.require_context!
76
+ end
77
+
78
+ def cloudflare_email_optional_job_context?
79
+ self.class.respond_to?(:cloudflare_email_optional_job_context?) && self.class.cloudflare_email_optional_job_context?
80
+ end
81
+
82
+ def verify_cloudflare_email_host_tenant!(key, host_key)
83
+ if host_key && host_key != key
84
+ raise ConfigurationError, "job host tenant conflicts with Mailbox Kit tenant context"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module MailboxKit
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailbox-kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cole
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Provider-neutral mailbox identities, aliases, memberships, retention,
13
+ optional tenancy, and a server-rendered management engine built on Rails Action
14
+ Mailbox.
15
+ email:
16
+ - cole@dscribeai.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE.txt
22
+ - README.md
23
+ - app/controllers/mailbox_kit/management/mailboxes_controller.rb
24
+ - app/controllers/mailbox_kit/management/styles_controller.rb
25
+ - app/views/layouts/mailbox_kit/management.html.erb
26
+ - app/views/mailbox_kit/management/mailboxes/index.html.erb
27
+ - app/views/mailbox_kit/management/mailboxes/message.html.erb
28
+ - app/views/mailbox_kit/management/mailboxes/show.html.erb
29
+ - docs/integration.md
30
+ - docs/upgrading.md
31
+ - lib/generators/mailbox_kit/install/install_generator.rb
32
+ - lib/generators/mailbox_kit/install/templates/create_mailbox_kit_mailboxes.rb
33
+ - lib/generators/mailbox_kit/install/templates/create_mailbox_kit_receiving_domains.rb
34
+ - lib/generators/mailbox_kit/upgrade/templates/allow_provider_neutral_receiving_domains.rb
35
+ - lib/generators/mailbox_kit/upgrade/templates/index_mailbox_kit_inbound_messages.rb
36
+ - lib/generators/mailbox_kit/upgrade/upgrade_generator.rb
37
+ - lib/mailbox-kit.rb
38
+ - lib/mailbox_kit/active_record/base.rb
39
+ - lib/mailbox_kit/address_syntax.rb
40
+ - lib/mailbox_kit/error.rb
41
+ - lib/mailbox_kit/inbound_email.rb
42
+ - lib/mailbox_kit/mailboxes.rb
43
+ - lib/mailbox_kit/mailboxes/configuration.rb
44
+ - lib/mailbox_kit/mailboxes/inbound_retention.rb
45
+ - lib/mailbox_kit/mailboxes/models.rb
46
+ - lib/mailbox_kit/mailboxes/service.rb
47
+ - lib/mailbox_kit/management.rb
48
+ - lib/mailbox_kit/management/adapter.rb
49
+ - lib/mailbox_kit/management/configuration.rb
50
+ - lib/mailbox_kit/management/engine.rb
51
+ - lib/mailbox_kit/management/management.css
52
+ - lib/mailbox_kit/management/routes.rb
53
+ - lib/mailbox_kit/railtie.rb
54
+ - lib/mailbox_kit/tenancy.rb
55
+ - lib/mailbox_kit/tenant_job_context.rb
56
+ - lib/mailbox_kit/version.rb
57
+ homepage: https://github.com/cole-robertson/cloudflare-email
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ source_code_uri: https://github.com/cole-robertson/cloudflare-email/tree/main/mailbox-kit
62
+ changelog_uri: https://github.com/cole-robertson/cloudflare-email/blob/main/CHANGELOG.md
63
+ bug_tracker_uri: https://github.com/cole-robertson/cloudflare-email/issues
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.2'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.6.9
79
+ specification_version: 4
80
+ summary: Persistent inboxes and management UI built on Rails Action Mailbox.
81
+ test_files: []