rhales 0.4.0 → 0.5.3

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json5 +52 -0
  3. data/.github/workflows/ci.yml +123 -0
  4. data/.github/workflows/claude-code-review.yml +69 -0
  5. data/.github/workflows/claude.yml +49 -0
  6. data/.github/workflows/code-smells.yml +146 -0
  7. data/.github/workflows/ruby-lint.yml +78 -0
  8. data/.github/workflows/yardoc.yml +126 -0
  9. data/.gitignore +55 -0
  10. data/.pr_agent.toml +63 -0
  11. data/.pre-commit-config.yaml +89 -0
  12. data/.prettierignore +8 -0
  13. data/.prettierrc +38 -0
  14. data/.reek.yml +98 -0
  15. data/.rubocop.yml +428 -0
  16. data/.serena/.gitignore +3 -0
  17. data/.yardopts +56 -0
  18. data/CHANGELOG.md +44 -0
  19. data/CLAUDE.md +1 -1
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +686 -868
  23. data/Rakefile +46 -0
  24. data/debug_context.rb +25 -0
  25. data/demo/rhales-roda-demo/.gitignore +7 -0
  26. data/demo/rhales-roda-demo/Gemfile +32 -0
  27. data/demo/rhales-roda-demo/Gemfile.lock +151 -0
  28. data/demo/rhales-roda-demo/MAIL.md +405 -0
  29. data/demo/rhales-roda-demo/README.md +376 -0
  30. data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
  31. data/demo/rhales-roda-demo/Rakefile +49 -0
  32. data/demo/rhales-roda-demo/app.rb +325 -0
  33. data/demo/rhales-roda-demo/bin/rackup +26 -0
  34. data/demo/rhales-roda-demo/config.ru +13 -0
  35. data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
  36. data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
  37. data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
  38. data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
  39. data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
  40. data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
  41. data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
  42. data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
  43. data/demo/rhales-roda-demo/templates/home.rue +78 -0
  44. data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
  45. data/demo/rhales-roda-demo/templates/login.rue +65 -0
  46. data/demo/rhales-roda-demo/templates/logout.rue +25 -0
  47. data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
  48. data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
  49. data/demo/rhales-roda-demo/test_full_output.rb +27 -0
  50. data/demo/rhales-roda-demo/test_simple.rb +24 -0
  51. data/docs/.gitignore +9 -0
  52. data/docs/architecture/data-flow.md +499 -0
  53. data/examples/dashboard-with-charts.rue +271 -0
  54. data/examples/form-with-validation.rue +180 -0
  55. data/examples/simple-page.rue +61 -0
  56. data/examples/vue.rue +136 -0
  57. data/generate-json-schemas.ts +158 -0
  58. data/json_schemer_migration_summary.md +172 -0
  59. data/lib/rhales/adapters/base_auth.rb +2 -0
  60. data/lib/rhales/adapters/base_request.rb +2 -0
  61. data/lib/rhales/adapters/base_session.rb +2 -0
  62. data/lib/rhales/adapters.rb +7 -0
  63. data/lib/rhales/configuration.rb +47 -0
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
  67. data/lib/rhales/{view.rb → core/view.rb} +112 -135
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
  69. data/lib/rhales/core.rb +9 -0
  70. data/lib/rhales/errors/hydration_collision_error.rb +2 -0
  71. data/lib/rhales/errors.rb +2 -0
  72. data/lib/rhales/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
  75. data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
  76. data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
  77. data/lib/rhales/hydration/hydrator.rb +102 -0
  78. data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
  79. data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
  80. data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
  84. data/lib/rhales/integrations.rb +6 -0
  85. data/lib/rhales/middleware/json_responder.rb +191 -0
  86. data/lib/rhales/middleware/schema_validator.rb +300 -0
  87. data/lib/rhales/middleware.rb +6 -0
  88. data/lib/rhales/parsers/handlebars_parser.rb +2 -0
  89. data/lib/rhales/parsers/rue_format_parser.rb +9 -7
  90. data/lib/rhales/parsers.rb +9 -0
  91. data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
  92. data/lib/rhales/utils/json_serializer.rb +114 -0
  93. data/lib/rhales/utils/logging_helpers.rb +75 -0
  94. data/lib/rhales/utils/schema_extractor.rb +132 -0
  95. data/lib/rhales/utils/schema_generator.rb +194 -0
  96. data/lib/rhales/utils.rb +40 -0
  97. data/lib/rhales/version.rb +3 -1
  98. data/lib/rhales.rb +41 -24
  99. data/lib/tasks/rhales_schema.rake +197 -0
  100. data/package.json +10 -0
  101. data/pnpm-lock.yaml +345 -0
  102. data/pnpm-workspace.yaml +2 -0
  103. data/proofs/error_handling.rb +79 -0
  104. data/proofs/expanded_object_inheritance.rb +82 -0
  105. data/proofs/partial_context_scoping_fix.rb +168 -0
  106. data/proofs/ui_context_partial_inheritance.rb +236 -0
  107. data/rhales.gemspec +14 -6
  108. data/schema_vs_data_comparison.md +254 -0
  109. data/test_direct_access.rb +36 -0
  110. metadata +141 -23
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -239
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -221
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ # Rakefile
2
+
3
+ $:.unshift(File.expand_path('lib', __dir__))
4
+
5
+ require 'bundler/gem_tasks'
6
+ require 'rspec/core/rake_task'
7
+
8
+ # Load task files
9
+ Dir.glob('lib/tasks/**/*.rake').each { |r| load r }
10
+
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ task default: :spec
14
+
15
+ # Rhales specific tasks
16
+ namespace :rhales do
17
+ desc 'Run Rhales tests only'
18
+ task :test do
19
+ system('bundle exec rspec spec/rhales/')
20
+ end
21
+
22
+ desc 'Generate Rhales documentation'
23
+ task :docs do
24
+ system('bundle exec yard doc lib/rhales/')
25
+ end
26
+
27
+ desc 'Validate Rhales templates in examples'
28
+ task :validate do
29
+ require 'rhales'
30
+
31
+ examples_dir = File.join(__dir__, 'examples', 'templates')
32
+ if Dir.exist?(examples_dir)
33
+ Dir.glob(File.join(examples_dir, '**', '*.rue')).each do |file|
34
+ puts "Validating #{file}..."
35
+ begin
36
+ Rhales::RueDocument.parse_file(file)
37
+ puts ' ✓ Valid'
38
+ rescue StandardError => ex
39
+ puts " ✗ Error: #{ex.message}"
40
+ end
41
+ end
42
+ else
43
+ puts "No examples directory found at #{examples_dir}"
44
+ end
45
+ end
46
+ end
data/debug_context.rb ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'lib/rhales'
4
+
5
+ # Test props with both symbols and strings
6
+ props = {
7
+ 'greeting' => 'Hello World',
8
+ 'user' => { 'name' => 'John Doe', 'role' => 'admin' },
9
+ 'authenticated' => true,
10
+ 'items' => [
11
+ { 'name' => 'Item 1', 'active' => true },
12
+ { 'name' => 'Item 2', 'active' => false }
13
+ ]
14
+ }
15
+
16
+ context = Rhales::Context.minimal(props: props)
17
+
18
+ puts "=== Context Variables ==="
19
+ puts "greeting: #{context.get('greeting')}"
20
+ puts "user.name: #{context.get('user.name')}"
21
+ puts "items.0.name: #{context.get('items.0.name')}"
22
+ puts "items.0.active: #{context.get('items.0.active')}"
23
+ puts "items.1.name: #{context.get('items.1.name')}"
24
+ puts "items.1.active: #{context.get('items.1.active')}"
25
+ puts "========================="
@@ -0,0 +1,7 @@
1
+ db/*.db
2
+ .DS_Store
3
+ *.log
4
+ tmp/
5
+
6
+ # Generated JSON Schemas (regenerate with: rake rhales:schema:generate)
7
+ public/schemas/
@@ -0,0 +1,32 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Web framework
4
+ gem 'puma', '~> 6.4'
5
+ gem 'rack-session', '~> 2.0'
6
+ gem 'rackup'
7
+ gem 'roda', '~> 3.84'
8
+
9
+ # Authentication
10
+ gem 'bcrypt', '~> 3.1'
11
+ gem 'rodauth', '~> 2.36'
12
+ gem 'rotp'
13
+ gem 'rqrcode'
14
+
15
+ # Database
16
+ gem 'sequel', '~> 5.85'
17
+ gem 'sqlite3', '~> 2.0'
18
+
19
+ # Templates and utilities
20
+ gem 'logger'
21
+ gem 'mail'
22
+ gem 'rake'
23
+ gem 'rhales', path: '../..' # Using local gem
24
+ gem 'rspec'
25
+ gem 'tilt'
26
+
27
+ # Development
28
+ group :development do
29
+ gem 'pry'
30
+ gem 'pry-byebug'
31
+ gem 'rerun'
32
+ end
@@ -0,0 +1,151 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ rhales (0.5.1)
5
+ json_schemer (~> 2.3)
6
+ logger
7
+ tilt (~> 2)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ base64 (0.3.0)
13
+ bcrypt (3.1.20)
14
+ bigdecimal (3.2.2)
15
+ byebug (12.0.0)
16
+ chunky_png (1.4.0)
17
+ coderay (1.1.3)
18
+ date (3.4.1)
19
+ diff-lcs (1.6.2)
20
+ ffi (1.17.2-aarch64-linux-gnu)
21
+ ffi (1.17.2-aarch64-linux-musl)
22
+ ffi (1.17.2-arm-linux-gnu)
23
+ ffi (1.17.2-arm-linux-musl)
24
+ ffi (1.17.2-arm64-darwin)
25
+ ffi (1.17.2-x86-linux-gnu)
26
+ ffi (1.17.2-x86-linux-musl)
27
+ ffi (1.17.2-x86_64-darwin)
28
+ ffi (1.17.2-x86_64-linux-gnu)
29
+ ffi (1.17.2-x86_64-linux-musl)
30
+ hana (1.3.7)
31
+ json_schemer (2.4.0)
32
+ bigdecimal
33
+ hana (~> 1.3)
34
+ regexp_parser (~> 2.0)
35
+ simpleidn (~> 0.2)
36
+ listen (3.9.0)
37
+ rb-fsevent (~> 0.10, >= 0.10.3)
38
+ rb-inotify (~> 0.9, >= 0.9.10)
39
+ logger (1.7.0)
40
+ mail (2.8.1)
41
+ mini_mime (>= 0.1.1)
42
+ net-imap
43
+ net-pop
44
+ net-smtp
45
+ method_source (1.1.0)
46
+ mini_mime (1.1.5)
47
+ net-imap (0.5.9)
48
+ date
49
+ net-protocol
50
+ net-pop (0.1.2)
51
+ net-protocol
52
+ net-protocol (0.2.2)
53
+ timeout
54
+ net-smtp (0.5.1)
55
+ net-protocol
56
+ nio4r (2.7.4)
57
+ pry (0.15.2)
58
+ coderay (~> 1.1)
59
+ method_source (~> 1.0)
60
+ pry-byebug (3.11.0)
61
+ byebug (~> 12.0)
62
+ pry (>= 0.13, < 0.16)
63
+ puma (6.6.0)
64
+ nio4r (~> 2.0)
65
+ rack (3.1.16)
66
+ rack-session (2.1.1)
67
+ base64 (>= 0.1.0)
68
+ rack (>= 3.0.0)
69
+ rackup (2.2.1)
70
+ rack (>= 3)
71
+ rake (13.3.0)
72
+ rb-fsevent (0.11.2)
73
+ rb-inotify (0.11.1)
74
+ ffi (~> 1.0)
75
+ regexp_parser (2.11.3)
76
+ rerun (0.14.0)
77
+ listen (~> 3.0)
78
+ roda (3.93.0)
79
+ rack
80
+ rodauth (2.39.0)
81
+ roda (>= 2.6.0)
82
+ sequel (>= 4)
83
+ rotp (6.3.0)
84
+ rqrcode (3.1.0)
85
+ chunky_png (~> 1.0)
86
+ rqrcode_core (~> 2.0)
87
+ rqrcode_core (2.0.0)
88
+ rspec (3.13.1)
89
+ rspec-core (~> 3.13.0)
90
+ rspec-expectations (~> 3.13.0)
91
+ rspec-mocks (~> 3.13.0)
92
+ rspec-core (3.13.5)
93
+ rspec-support (~> 3.13.0)
94
+ rspec-expectations (3.13.5)
95
+ diff-lcs (>= 1.2.0, < 2.0)
96
+ rspec-support (~> 3.13.0)
97
+ rspec-mocks (3.13.5)
98
+ diff-lcs (>= 1.2.0, < 2.0)
99
+ rspec-support (~> 3.13.0)
100
+ rspec-support (3.13.4)
101
+ sequel (5.94.0)
102
+ bigdecimal
103
+ simpleidn (0.2.3)
104
+ sqlite3 (2.7.1-aarch64-linux-gnu)
105
+ sqlite3 (2.7.1-aarch64-linux-musl)
106
+ sqlite3 (2.7.1-arm-linux-gnu)
107
+ sqlite3 (2.7.1-arm-linux-musl)
108
+ sqlite3 (2.7.1-arm64-darwin)
109
+ sqlite3 (2.7.1-x86-linux-gnu)
110
+ sqlite3 (2.7.1-x86-linux-musl)
111
+ sqlite3 (2.7.1-x86_64-darwin)
112
+ sqlite3 (2.7.1-x86_64-linux-gnu)
113
+ sqlite3 (2.7.1-x86_64-linux-musl)
114
+ tilt (2.3.0)
115
+ timeout (0.4.3)
116
+
117
+ PLATFORMS
118
+ aarch64-linux-gnu
119
+ aarch64-linux-musl
120
+ arm-linux-gnu
121
+ arm-linux-musl
122
+ arm64-darwin
123
+ x86-linux-gnu
124
+ x86-linux-musl
125
+ x86_64-darwin
126
+ x86_64-linux-gnu
127
+ x86_64-linux-musl
128
+
129
+ DEPENDENCIES
130
+ bcrypt (~> 3.1)
131
+ logger
132
+ mail
133
+ pry
134
+ pry-byebug
135
+ puma (~> 6.4)
136
+ rack-session (~> 2.0)
137
+ rackup
138
+ rake
139
+ rerun
140
+ rhales!
141
+ roda (~> 3.84)
142
+ rodauth (~> 2.36)
143
+ rotp
144
+ rqrcode
145
+ rspec
146
+ sequel (~> 5.85)
147
+ sqlite3 (~> 2.0)
148
+ tilt
149
+
150
+ BUNDLED WITH
151
+ 2.6.6
@@ -0,0 +1,405 @@
1
+ # Mail Configuration in Rodauth
2
+
3
+ This document explains how to configure SMTP and mail settings in Rodauth for sending authentication emails.
4
+
5
+ ## Overview
6
+
7
+ Rodauth uses the **Mail gem** for email functionality. Email configuration happens at two levels:
8
+
9
+ 1. **Global Mail Configuration** - SMTP settings and delivery method
10
+ 2. **Rodauth Email Configuration** - Email addresses, subjects, and custom logic
11
+
12
+ ## Global Mail Configuration (SMTP Settings)
13
+
14
+ Configure SMTP **before** loading Rodauth:
15
+
16
+ ```ruby
17
+ require 'mail'
18
+
19
+ # SMTP configuration
20
+ Mail.defaults do
21
+ delivery_method :smtp, {
22
+ address: 'smtp.gmail.com',
23
+ port: 587,
24
+ domain: 'yoursite.com',
25
+ user_name: 'your-email@gmail.com',
26
+ password: 'your-app-password',
27
+ authentication: 'plain',
28
+ enable_starttls_auto: true
29
+ }
30
+ end
31
+
32
+ # Then configure Rodauth
33
+ plugin :rodauth do
34
+ enable :login, :reset_password, :verify_account
35
+ # ... other config
36
+ end
37
+ ```
38
+
39
+ ## Rodauth Email Configuration
40
+
41
+ Within your Rodauth configuration block:
42
+
43
+ ```ruby
44
+ plugin :rodauth do
45
+ enable :reset_password, :verify_account
46
+
47
+ # Basic email settings
48
+ email_from 'noreply@yoursite.com'
49
+ email_subject_prefix '[YourSite] '
50
+
51
+ # Customize recipient (defaults to account email)
52
+ email_to do
53
+ account[:email] # or custom logic
54
+ end
55
+
56
+ # Override email sending for custom delivery
57
+ send_email do |email|
58
+ # Custom delivery logic
59
+ email.deliver!
60
+ end
61
+ end
62
+ ```
63
+
64
+ ## Common SMTP Providers
65
+
66
+ ### Gmail
67
+
68
+ ```ruby
69
+ Mail.defaults do
70
+ delivery_method :smtp, {
71
+ address: 'smtp.gmail.com',
72
+ port: 587,
73
+ user_name: 'your-email@gmail.com',
74
+ password: 'your-app-password', # Use app password, not account password
75
+ authentication: 'plain',
76
+ enable_starttls_auto: true
77
+ }
78
+ end
79
+ ```
80
+
81
+ **Note**: For Gmail, you need to use an App Password, not your regular Gmail password.
82
+
83
+ ### SendGrid
84
+
85
+ ```ruby
86
+ Mail.defaults do
87
+ delivery_method :smtp, {
88
+ address: 'smtp.sendgrid.net',
89
+ port: 587,
90
+ user_name: 'apikey',
91
+ password: 'your-sendgrid-api-key',
92
+ authentication: 'plain',
93
+ enable_starttls_auto: true
94
+ }
95
+ end
96
+ ```
97
+
98
+ ### Mailgun
99
+
100
+ ```ruby
101
+ Mail.defaults do
102
+ delivery_method :smtp, {
103
+ address: 'smtp.mailgun.org',
104
+ port: 587,
105
+ user_name: 'postmaster@your-domain.mailgun.org',
106
+ password: 'your-mailgun-password',
107
+ authentication: 'plain',
108
+ enable_starttls_auto: true
109
+ }
110
+ end
111
+ ```
112
+
113
+ ### Amazon SES
114
+
115
+ ```ruby
116
+ Mail.defaults do
117
+ delivery_method :smtp, {
118
+ address: 'email-smtp.us-east-1.amazonaws.com',
119
+ port: 587,
120
+ user_name: 'your-ses-smtp-username',
121
+ password: 'your-ses-smtp-password',
122
+ authentication: 'plain',
123
+ enable_starttls_auto: true
124
+ }
125
+ end
126
+ ```
127
+
128
+ ## Environment-Based Configuration
129
+
130
+ Configure different delivery methods per environment:
131
+
132
+ ```ruby
133
+ case ENV['RACK_ENV']
134
+ when 'production'
135
+ Mail.defaults do
136
+ delivery_method :smtp, {
137
+ address: ENV['SMTP_ADDRESS'],
138
+ port: ENV['SMTP_PORT'],
139
+ user_name: ENV['SMTP_USERNAME'],
140
+ password: ENV['SMTP_PASSWORD'],
141
+ authentication: 'plain',
142
+ enable_starttls_auto: true
143
+ }
144
+ end
145
+ when 'development'
146
+ Mail.defaults do
147
+ delivery_method :file, location: 'tmp/mails'
148
+ end
149
+ when 'test'
150
+ Mail.defaults do
151
+ delivery_method :test
152
+ end
153
+ end
154
+ ```
155
+
156
+ ## Custom Email Templates
157
+
158
+ Override email content by defining methods in your Rodauth configuration:
159
+
160
+ ### Custom Reset Password Email
161
+
162
+ ```ruby
163
+ plugin :rodauth do
164
+ enable :reset_password
165
+
166
+ # Custom email subject
167
+ reset_password_email_subject do
168
+ "Reset your #{domain} password"
169
+ end
170
+
171
+ # Custom email body
172
+ reset_password_email_body do
173
+ <<~EMAIL
174
+ Hello,
175
+
176
+ Someone has requested a password reset for your account.
177
+
178
+ Click here to reset: #{reset_password_email_link}
179
+
180
+ If you didn't request this, please ignore this email.
181
+
182
+ Thanks,
183
+ The #{domain} Team
184
+ EMAIL
185
+ end
186
+ end
187
+ ```
188
+
189
+ ### Custom Verification Email
190
+
191
+ ```ruby
192
+ plugin :rodauth do
193
+ enable :verify_account
194
+
195
+ verify_account_email_subject do
196
+ "Please verify your #{domain} account"
197
+ end
198
+
199
+ verify_account_email_body do
200
+ <<~EMAIL
201
+ Welcome to #{domain}!
202
+
203
+ Please verify your account by clicking this link:
204
+ #{verify_account_email_link}
205
+
206
+ Thanks for signing up!
207
+ EMAIL
208
+ end
209
+ end
210
+ ```
211
+
212
+ ## Email Configuration Methods
213
+
214
+ Key configuration methods available in Rodauth:
215
+
216
+ ```ruby
217
+ plugin :rodauth do
218
+ # Sender email address (defaults to "webmaster@#{domain}")
219
+ email_from 'noreply@yoursite.com'
220
+
221
+ # Subject prefix for all emails
222
+ email_subject_prefix '[YourSite] '
223
+
224
+ # Customize recipient email address
225
+ email_to do
226
+ account[:email] # or custom logic
227
+ end
228
+
229
+ # Custom email creation
230
+ create_email do |subject, body|
231
+ mail = Mail.new
232
+ mail.from = email_from
233
+ mail.to = email_to
234
+ mail.subject = "#{email_subject_prefix}#{subject}"
235
+ mail.body = body
236
+ # Add custom headers, attachments, etc.
237
+ mail
238
+ end
239
+
240
+ # Custom email sending
241
+ send_email do |email|
242
+ # Add logging, error handling, etc.
243
+ puts "Sending email to #{email.to}: #{email.subject}"
244
+ email.deliver!
245
+ end
246
+ end
247
+ ```
248
+
249
+ ## Alternative Delivery Methods
250
+
251
+ ### File Delivery (Development)
252
+
253
+ Saves emails to files instead of sending them:
254
+
255
+ ```ruby
256
+ Mail.defaults do
257
+ delivery_method :file, location: 'tmp/mails'
258
+ end
259
+ ```
260
+
261
+ ### Test Delivery
262
+
263
+ Captures emails in memory for testing:
264
+
265
+ ```ruby
266
+ Mail.defaults do
267
+ delivery_method :test
268
+ end
269
+
270
+ # Access sent emails in tests
271
+ Mail::TestMailer.deliveries.last
272
+ ```
273
+
274
+ ### Sendmail
275
+
276
+ Use local sendmail binary:
277
+
278
+ ```ruby
279
+ Mail.defaults do
280
+ delivery_method :sendmail
281
+ end
282
+ ```
283
+
284
+ ## Email Features
285
+
286
+ Features that send emails and can be customized:
287
+
288
+ - **reset_password** - Password reset emails
289
+ - **verify_account** - Account verification emails
290
+ - **verify_login_change** - Login change verification emails
291
+ - **change_password_notify** - Password change notifications
292
+ - **lockout** - Account lockout notifications
293
+ - **email_auth** - Passwordless email authentication
294
+ - **otp_lockout_email** - OTP lockout notifications
295
+ - **otp_modify_email** - OTP setup/disable notifications
296
+ - **webauthn_modify_email** - WebAuthn setup/removal notifications
297
+
298
+ ## Security Considerations
299
+
300
+ ### HMAC Protection
301
+
302
+ Enable HMAC protection for email tokens:
303
+
304
+ ```ruby
305
+ plugin :rodauth do
306
+ # Set an HMAC secret for token security
307
+ hmac_secret 'your-secret-key-here'
308
+
309
+ # Disable raw token acceptance (recommended for production)
310
+ allow_raw_email_token? false
311
+ end
312
+ ```
313
+
314
+ ### Rate Limiting
315
+
316
+ Built-in rate limiting prevents email spam:
317
+
318
+ ```ruby
319
+ plugin :rodauth do
320
+ enable :reset_password
321
+
322
+ # Don't resend reset email within 5 minutes
323
+ reset_password_skip_resend_email_within 300
324
+ end
325
+ ```
326
+
327
+ ## Testing Email
328
+
329
+ In your test suite:
330
+
331
+ ```ruby
332
+ # Configure test delivery
333
+ Mail.defaults do
334
+ delivery_method :test
335
+ end
336
+
337
+ # In tests, check sent emails
338
+ def test_password_reset_email
339
+ post '/reset-password', email: 'user@example.com'
340
+
341
+ email = Mail::TestMailer.deliveries.last
342
+ assert_includes email.to, 'user@example.com'
343
+ assert_includes email.subject, 'Reset Password'
344
+ assert_includes email.body.to_s, 'reset'
345
+ end
346
+
347
+ # Clear deliveries between tests
348
+ def setup
349
+ Mail::TestMailer.deliveries.clear
350
+ end
351
+ ```
352
+
353
+ ## Troubleshooting
354
+
355
+ ### Common Issues
356
+
357
+ 1. **Authentication failures**: Ensure you're using the correct credentials and authentication method
358
+ 2. **Port blocked**: Try different ports (25, 465, 587, 2525)
359
+ 3. **Gmail App Passwords**: Regular Gmail passwords won't work with SMTP
360
+ 4. **Development emails not visible**: Check your delivery method and file location
361
+
362
+ ### Debug Logging
363
+
364
+ Enable Mail gem logging:
365
+
366
+ ```ruby
367
+ Mail.defaults do
368
+ delivery_method :smtp, {
369
+ # ... SMTP settings
370
+ openssl_verify_mode: 'none',
371
+ enable_starttls_auto: true
372
+ }
373
+ end
374
+
375
+ # Enable logging
376
+ Mail.defaults do
377
+ retriever_method :imap, {
378
+ # ... settings
379
+ }
380
+ end
381
+ ```
382
+
383
+ ### Testing SMTP Configuration
384
+
385
+ Test your SMTP settings independently:
386
+
387
+ ```ruby
388
+ require 'mail'
389
+
390
+ Mail.defaults do
391
+ delivery_method :smtp, {
392
+ # your SMTP settings
393
+ }
394
+ end
395
+
396
+ mail = Mail.new do
397
+ from 'test@yoursite.com'
398
+ to 'recipient@example.com'
399
+ subject 'Test email'
400
+ body 'This is a test email'
401
+ end
402
+
403
+ mail.deliver!
404
+ puts "Email sent successfully!"
405
+ ```