ultra-max-sys 0.0.1

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.

Potentially problematic release.


This version of ultra-max-sys might be problematic. Click here for more details.

Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/roda-3.106.0/MIT-LICENSE +21 -0
  3. data/roda-3.106.0/lib/roda/cache.rb +42 -0
  4. data/roda-3.106.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
  5. data/roda-3.106.0/lib/roda/plugins/_after_hook.rb +11 -0
  6. data/roda-3.106.0/lib/roda/plugins/_base64.rb +34 -0
  7. data/roda-3.106.0/lib/roda/plugins/_before_hook.rb +41 -0
  8. data/roda-3.106.0/lib/roda/plugins/_optimized_matching.rb +227 -0
  9. data/roda-3.106.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
  10. data/roda-3.106.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
  11. data/roda-3.106.0/lib/roda/plugins/additional_render_engines.rb +61 -0
  12. data/roda-3.106.0/lib/roda/plugins/additional_view_directories.rb +66 -0
  13. data/roda-3.106.0/lib/roda/plugins/all_verbs.rb +50 -0
  14. data/roda-3.106.0/lib/roda/plugins/assets.rb +902 -0
  15. data/roda-3.106.0/lib/roda/plugins/assets_preloading.rb +89 -0
  16. data/roda-3.106.0/lib/roda/plugins/assume_ssl.rb +28 -0
  17. data/roda-3.106.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
  18. data/roda-3.106.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
  19. data/roda-3.106.0/lib/roda/plugins/backtracking_array.rb +88 -0
  20. data/roda-3.106.0/lib/roda/plugins/bearer_token.rb +28 -0
  21. data/roda-3.106.0/lib/roda/plugins/branch_locals.rb +76 -0
  22. data/roda-3.106.0/lib/roda/plugins/break.rb +43 -0
  23. data/roda-3.106.0/lib/roda/plugins/caching.rb +206 -0
  24. data/roda-3.106.0/lib/roda/plugins/capture_erb.rb +94 -0
  25. data/roda-3.106.0/lib/roda/plugins/chunked.rb +333 -0
  26. data/roda-3.106.0/lib/roda/plugins/class_level_routing.rb +111 -0
  27. data/roda-3.106.0/lib/roda/plugins/class_matchers.rb +155 -0
  28. data/roda-3.106.0/lib/roda/plugins/common_logger.rb +82 -0
  29. data/roda-3.106.0/lib/roda/plugins/conditional_sessions.rb +67 -0
  30. data/roda-3.106.0/lib/roda/plugins/content_for.rb +105 -0
  31. data/roda-3.106.0/lib/roda/plugins/content_security_policy.rb +333 -0
  32. data/roda-3.106.0/lib/roda/plugins/cookie_flags.rb +157 -0
  33. data/roda-3.106.0/lib/roda/plugins/cookies.rb +51 -0
  34. data/roda-3.106.0/lib/roda/plugins/csrf.rb +81 -0
  35. data/roda-3.106.0/lib/roda/plugins/custom_block_results.rb +85 -0
  36. data/roda-3.106.0/lib/roda/plugins/custom_matchers.rb +89 -0
  37. data/roda-3.106.0/lib/roda/plugins/default_headers.rb +64 -0
  38. data/roda-3.106.0/lib/roda/plugins/default_status.rb +35 -0
  39. data/roda-3.106.0/lib/roda/plugins/delay_build.rb +18 -0
  40. data/roda-3.106.0/lib/roda/plugins/delegate.rb +77 -0
  41. data/roda-3.106.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
  42. data/roda-3.106.0/lib/roda/plugins/direct_call.rb +38 -0
  43. data/roda-3.106.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
  44. data/roda-3.106.0/lib/roda/plugins/drop_body.rb +43 -0
  45. data/roda-3.106.0/lib/roda/plugins/each_part.rb +87 -0
  46. data/roda-3.106.0/lib/roda/plugins/early_hints.rb +25 -0
  47. data/roda-3.106.0/lib/roda/plugins/empty_root.rb +49 -0
  48. data/roda-3.106.0/lib/roda/plugins/environments.rb +79 -0
  49. data/roda-3.106.0/lib/roda/plugins/erb_h.rb +43 -0
  50. data/roda-3.106.0/lib/roda/plugins/error_email.rb +152 -0
  51. data/roda-3.106.0/lib/roda/plugins/error_handler.rb +133 -0
  52. data/roda-3.106.0/lib/roda/plugins/error_mail.rb +149 -0
  53. data/roda-3.106.0/lib/roda/plugins/exception_page.rb +445 -0
  54. data/roda-3.106.0/lib/roda/plugins/filter_common_logger.rb +46 -0
  55. data/roda-3.106.0/lib/roda/plugins/flash.rb +119 -0
  56. data/roda-3.106.0/lib/roda/plugins/h.rb +59 -0
  57. data/roda-3.106.0/lib/roda/plugins/halt.rb +108 -0
  58. data/roda-3.106.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  59. data/roda-3.106.0/lib/roda/plugins/hash_branches.rb +145 -0
  60. data/roda-3.106.0/lib/roda/plugins/hash_matcher.rb +39 -0
  61. data/roda-3.106.0/lib/roda/plugins/hash_paths.rb +128 -0
  62. data/roda-3.106.0/lib/roda/plugins/hash_public.rb +125 -0
  63. data/roda-3.106.0/lib/roda/plugins/hash_public_cache.rb +91 -0
  64. data/roda-3.106.0/lib/roda/plugins/hash_routes.rb +304 -0
  65. data/roda-3.106.0/lib/roda/plugins/head.rb +99 -0
  66. data/roda-3.106.0/lib/roda/plugins/header_matchers.rb +88 -0
  67. data/roda-3.106.0/lib/roda/plugins/heartbeat.rb +41 -0
  68. data/roda-3.106.0/lib/roda/plugins/hmac_paths.rb +413 -0
  69. data/roda-3.106.0/lib/roda/plugins/hooks.rb +92 -0
  70. data/roda-3.106.0/lib/roda/plugins/host_authorization.rb +155 -0
  71. data/roda-3.106.0/lib/roda/plugins/host_routing.rb +194 -0
  72. data/roda-3.106.0/lib/roda/plugins/hsts.rb +35 -0
  73. data/roda-3.106.0/lib/roda/plugins/indifferent_params.rb +126 -0
  74. data/roda-3.106.0/lib/roda/plugins/inject_erb.rb +33 -0
  75. data/roda-3.106.0/lib/roda/plugins/invalid_request_body.rb +107 -0
  76. data/roda-3.106.0/lib/roda/plugins/ip_from_header.rb +34 -0
  77. data/roda-3.106.0/lib/roda/plugins/json.rb +119 -0
  78. data/roda-3.106.0/lib/roda/plugins/json_parser.rb +107 -0
  79. data/roda-3.106.0/lib/roda/plugins/link_to.rb +83 -0
  80. data/roda-3.106.0/lib/roda/plugins/mail_processor.rb +629 -0
  81. data/roda-3.106.0/lib/roda/plugins/mailer.rb +285 -0
  82. data/roda-3.106.0/lib/roda/plugins/map_matcher.rb +48 -0
  83. data/roda-3.106.0/lib/roda/plugins/match_affix.rb +64 -0
  84. data/roda-3.106.0/lib/roda/plugins/match_hook.rb +34 -0
  85. data/roda-3.106.0/lib/roda/plugins/match_hook_args.rb +93 -0
  86. data/roda-3.106.0/lib/roda/plugins/middleware.rb +225 -0
  87. data/roda-3.106.0/lib/roda/plugins/middleware_stack.rb +101 -0
  88. data/roda-3.106.0/lib/roda/plugins/module_include.rb +96 -0
  89. data/roda-3.106.0/lib/roda/plugins/multi_public.rb +95 -0
  90. data/roda-3.106.0/lib/roda/plugins/multi_route.rb +156 -0
  91. data/roda-3.106.0/lib/roda/plugins/multi_run.rb +144 -0
  92. data/roda-3.106.0/lib/roda/plugins/multi_view.rb +83 -0
  93. data/roda-3.106.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
  94. data/roda-3.106.0/lib/roda/plugins/named_routes.rb +167 -0
  95. data/roda-3.106.0/lib/roda/plugins/named_templates.rb +102 -0
  96. data/roda-3.106.0/lib/roda/plugins/not_allowed.rb +151 -0
  97. data/roda-3.106.0/lib/roda/plugins/not_found.rb +55 -0
  98. data/roda-3.106.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
  99. data/roda-3.106.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
  100. data/roda-3.106.0/lib/roda/plugins/padrino_render.rb +42 -0
  101. data/roda-3.106.0/lib/roda/plugins/param_matchers.rb +83 -0
  102. data/roda-3.106.0/lib/roda/plugins/params_capturing.rb +118 -0
  103. data/roda-3.106.0/lib/roda/plugins/part.rb +76 -0
  104. data/roda-3.106.0/lib/roda/plugins/partials.rb +74 -0
  105. data/roda-3.106.0/lib/roda/plugins/pass.rb +47 -0
  106. data/roda-3.106.0/lib/roda/plugins/path.rb +255 -0
  107. data/roda-3.106.0/lib/roda/plugins/path_matchers.rb +54 -0
  108. data/roda-3.106.0/lib/roda/plugins/path_rewriter.rb +111 -0
  109. data/roda-3.106.0/lib/roda/plugins/permissions_policy.rb +340 -0
  110. data/roda-3.106.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
  111. data/roda-3.106.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
  112. data/roda-3.106.0/lib/roda/plugins/precompile_templates.rb +156 -0
  113. data/roda-3.106.0/lib/roda/plugins/public.rb +182 -0
  114. data/roda-3.106.0/lib/roda/plugins/r.rb +35 -0
  115. data/roda-3.106.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
  116. data/roda-3.106.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
  117. data/roda-3.106.0/lib/roda/plugins/redirect_path.rb +59 -0
  118. data/roda-3.106.0/lib/roda/plugins/relative_path.rb +76 -0
  119. data/roda-3.106.0/lib/roda/plugins/render.rb +998 -0
  120. data/roda-3.106.0/lib/roda/plugins/render_coverage.rb +105 -0
  121. data/roda-3.106.0/lib/roda/plugins/render_each.rb +224 -0
  122. data/roda-3.106.0/lib/roda/plugins/render_locals.rb +102 -0
  123. data/roda-3.106.0/lib/roda/plugins/request_aref.rb +77 -0
  124. data/roda-3.106.0/lib/roda/plugins/request_headers.rb +83 -0
  125. data/roda-3.106.0/lib/roda/plugins/response_attachment.rb +94 -0
  126. data/roda-3.106.0/lib/roda/plugins/response_content_type.rb +79 -0
  127. data/roda-3.106.0/lib/roda/plugins/response_request.rb +36 -0
  128. data/roda-3.106.0/lib/roda/plugins/route_block_args.rb +50 -0
  129. data/roda-3.106.0/lib/roda/plugins/route_csrf.rb +388 -0
  130. data/roda-3.106.0/lib/roda/plugins/run_append_slash.rb +51 -0
  131. data/roda-3.106.0/lib/roda/plugins/run_handler.rb +51 -0
  132. data/roda-3.106.0/lib/roda/plugins/run_require_slash.rb +46 -0
  133. data/roda-3.106.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
  134. data/roda-3.106.0/lib/roda/plugins/send_file.rb +127 -0
  135. data/roda-3.106.0/lib/roda/plugins/sessions.rb +537 -0
  136. data/roda-3.106.0/lib/roda/plugins/shape_friendly.rb +232 -0
  137. data/roda-3.106.0/lib/roda/plugins/shared_vars.rb +86 -0
  138. data/roda-3.106.0/lib/roda/plugins/sinatra_helpers.rb +444 -0
  139. data/roda-3.106.0/lib/roda/plugins/slash_path_empty.rb +26 -0
  140. data/roda-3.106.0/lib/roda/plugins/static.rb +42 -0
  141. data/roda-3.106.0/lib/roda/plugins/static_routing.rb +89 -0
  142. data/roda-3.106.0/lib/roda/plugins/status_303.rb +34 -0
  143. data/roda-3.106.0/lib/roda/plugins/status_handler.rb +86 -0
  144. data/roda-3.106.0/lib/roda/plugins/streaming.rb +170 -0
  145. data/roda-3.106.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
  146. data/roda-3.106.0/lib/roda/plugins/symbol_matchers.rb +188 -0
  147. data/roda-3.106.0/lib/roda/plugins/symbol_status.rb +32 -0
  148. data/roda-3.106.0/lib/roda/plugins/symbol_views.rb +37 -0
  149. data/roda-3.106.0/lib/roda/plugins/timestamp_public.rb +78 -0
  150. data/roda-3.106.0/lib/roda/plugins/type_routing.rb +217 -0
  151. data/roda-3.106.0/lib/roda/plugins/typecast_params.rb +1185 -0
  152. data/roda-3.106.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
  153. data/roda-3.106.0/lib/roda/plugins/unescape_path.rb +41 -0
  154. data/roda-3.106.0/lib/roda/plugins/view_options.rb +203 -0
  155. data/roda-3.106.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
  156. data/roda-3.106.0/lib/roda/plugins.rb +68 -0
  157. data/roda-3.106.0/lib/roda/request.rb +722 -0
  158. data/roda-3.106.0/lib/roda/response.rb +221 -0
  159. data/roda-3.106.0/lib/roda/session_middleware.rb +184 -0
  160. data/roda-3.106.0/lib/roda/version.rb +18 -0
  161. data/roda-3.106.0/lib/roda.rb +741 -0
  162. data/ultra-max-sys.gemspec +12 -0
  163. metadata +202 -0
@@ -0,0 +1,629 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'mail'
4
+
5
+ class Roda
6
+ module RodaPlugins
7
+ # The mail_processor plugin allows your Roda application to process mail
8
+ # using a routing tree. Quick example:
9
+ #
10
+ # class MailProcessor < Roda
11
+ # plugin :mail_processor
12
+ #
13
+ # route do |r|
14
+ # # Match based on the To header, extracting the ticket_id
15
+ # r.to /ticket\+(\d+)@example.com/ do |ticket_id|
16
+ # if ticket = Ticket[ticket_id.to_i]
17
+ # # Mark the mail as handled if there is a valid ticket associated
18
+ # r.handle do
19
+ # ticket.add_note(text: mail_text, from: from)
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # # Match based on the To or CC header
25
+ # r.rcpt "post@example.com" do
26
+ # # Match based on the body, capturing the post id and tag
27
+ # r.body(/^Post: (\d+)-(\w+)/) do |post_id, tag|
28
+ # unhandled_mail("no matching post") unless post = Post[post_id.to_i]
29
+ # unhandled_mail("tag doesn't match for post") unless post.tag == tag
30
+ #
31
+ # # Match based on APPROVE somewhere in the mail text,
32
+ # # marking the mail as handled
33
+ # r.handle_text /\bAPPROVE\b/i do
34
+ # post.approve!(from)
35
+ # end
36
+ #
37
+ # # Match based on DENY somewhere in the mail text,
38
+ # # marking the mail as handled
39
+ # r.handle_text /\bDENY\b/i do
40
+ # post.deny!(from)
41
+ # end
42
+ # end
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # = Processing Mail
48
+ #
49
+ # To submit a mail for processing via the mail_processor routing tree, call the +process_mail+
50
+ # method with a +Mail+ instance:
51
+ #
52
+ # MailProcessor.process_mail(Mail.new do
53
+ # # ...
54
+ # end)
55
+ #
56
+ # You can use this to process mail messages from the filesystem:
57
+ #
58
+ # MailProcessor.process_mail(Mail.read('/path/to/message.eml'))
59
+ #
60
+ # If you have a service that delivers mail via an HTTP POST request (for realtime
61
+ # processing), you can have your web routes convert the web request into a +Mail+ instance
62
+ # and then call +process_mail+:
63
+ #
64
+ # r.post "email" do
65
+ # # check request is submitted by trusted sender
66
+ #
67
+ # # If request body is the raw mail body
68
+ # r.body.rewind
69
+ # MailProcessor.process_mail(Mail.new(r.body.read))
70
+ #
71
+ # # If request body is in a parameter named content
72
+ # MailProcessor.process_mail(Mail.new(r.params['content']))
73
+ #
74
+ # # If the HTTP request requires a specific response status code (such as 204)
75
+ # response.status = 204
76
+ #
77
+ # nil
78
+ # end
79
+ #
80
+ # Note that when receiving messages via HTTP, you need to make sure you check that the
81
+ # request is trusted. How to do this depends on the delivery service, but could involve
82
+ # using HTTP basic authentication, checking for valid API tokens, or checking that a message
83
+ # includes a signature/hash that matches the expected value.
84
+ #
85
+ # If you have setup a default retriever_method for +Mail+, you can call +process_mailbox+,
86
+ # which will process all mail in the given mailbox (using +Mail.find_and_delete+):
87
+ #
88
+ # MailProcessor.process_mailbox
89
+ #
90
+ # You can also use a +:retreiver+ option to provide a specific retriever:
91
+ #
92
+ # MailProcessor.process_mailbox(retreiver: Mail::POP3.new)
93
+ #
94
+ # = Routing Mail
95
+ #
96
+ # The mail_processor plugin handles routing similar to Roda's default routing for
97
+ # web requests, but because mail processing may not return a result, the mail_processor
98
+ # plugin uses a more explicit approach to consider whether the message has been handled.
99
+ # If the +r.handle+ method is called during routing, the mail is considered handled,
100
+ # otherwise the mail is considered not handled. The +unhandled_mail+ method can be
101
+ # called at any point to stop routing and consider the mail as not handled (even if
102
+ # inside an +r.handle+ block).
103
+ #
104
+ # Here are the mail routing methods and what they use for matching:
105
+ #
106
+ # from :: match on the mail From address
107
+ # to :: match on the mail To address
108
+ # cc :: match on the mail CC address
109
+ # rcpt :: match on the mail recipients (To and CC addresses by default)
110
+ # subject :: match on the mail subject
111
+ # body :: match on the mail body
112
+ # text :: match on text extracted from the message (same as mail body by default)
113
+ # header :: match on a mail header
114
+ #
115
+ # All of these routing methods accept a single argument, except for +r.header+, which
116
+ # can take two arguments.
117
+ #
118
+ # Each of these routing methods also has a +r.handle_*+ method
119
+ # (e.g. +r.handle_from+), which will call +r.handle+ implicitly to mark the
120
+ # mail as handled if the routing method matches and control is passed to the block.
121
+ #
122
+ # The address matchers (from, to, cc, rcpt) perform a case-insensitive match if
123
+ # given a string or array of strings, and a regular regexp match if given a regexp.
124
+ #
125
+ # The content matchers (subject, body, text) perform a case-sensitive substring search
126
+ # if given a string or array of strings, and a regular regexp match if given a regexp.
127
+ #
128
+ # The header matcher should be called with a key and an optional value. If the matcher is
129
+ # called with a key and not a value, it matches if a header matching the key is present
130
+ # in the message, yielding the header value. If the matcher is called with a key and a
131
+ # value, it matches if a header matching the key is present and the header value matches
132
+ # the value given, using the same criteria as the content matchers.
133
+ #
134
+ # In all cases for matchers, if a string is given and matches, the match block is called without
135
+ # arguments. If an array of strings is given, and one of the strings matches,
136
+ # the match block is called with the matching string argument. If a regexp is given,
137
+ # the match block is called with the regexp captures. This is the same behavior for Roda's
138
+ # general string, array, and regexp matchers.
139
+ #
140
+ # = Recipient-Specific Routing
141
+ #
142
+ # To allow splitting up the mail processor routing tree based on recipients, you can use
143
+ # the +rcpt+ class method, which takes any number of string or regexps arguments for recipient
144
+ # addresses, and a block to handle the routing for those addresses instead of using the
145
+ # default routing.
146
+ #
147
+ # MailProcessor.rcpt('a@example.com') do |r|
148
+ # r.text /Post: (\d+)-(\h+)/ do |post_id, hmac|
149
+ # next unless Post[post_id.to_i]
150
+ # unhandled_mail("no matching Post") unless post = Post[post_id.to_i]
151
+ # unhandled_mail("HMAC for doesn't match for post") unless hmac == post.hmac_for_address(from.first)
152
+ #
153
+ # r.handle_text 'APPROVE' do
154
+ # post.approved_by(from)
155
+ # end
156
+ #
157
+ # r.handle_text 'DENY' do
158
+ # post.denied_by(from)
159
+ # end
160
+ # end
161
+ # end
162
+ #
163
+ # The +rcpt+ class method does not mark the messages as handled, because in most cases you will
164
+ # need to do additional matching to extract the information necessary to handle
165
+ # the mail. You will need to call +r.handle+ or similar method inside the block
166
+ # to mark the mail as handled.
167
+ #
168
+ # Matching on strings provided to the +rcpt+ class method is an O(1) operation as
169
+ # the strings are stored lowercase in a hash. Matching on regexps provided to the
170
+ # +rcpt+ class method is an O(n) operation on the number of regexps.
171
+ #
172
+ # If you would like to break up the routing tree using something other than the
173
+ # recipient address, you can use the multi_route plugin.
174
+ #
175
+ # = Hooks
176
+ #
177
+ # The mail_processor plugin offers hooks for processing mail.
178
+ #
179
+ # For mail that is handled successfully, you can use the handled_mail hook:
180
+ #
181
+ # MailProcessor.handled_mail do
182
+ # # nothing by default
183
+ # end
184
+ #
185
+ # For mail that is not handled successfully, either because +r.handle+ was not called
186
+ # during routing or because the +unhandled_mail+ method was called explicitly,
187
+ # you can use the unhandled_mail hook.
188
+ #
189
+ # The default is to reraise the UnhandledMail exception that was raised during routing,
190
+ # so that calling code will not be able to ignore errors when processing mail. However,
191
+ # you may want to save such mails to a special location or forward them as attachments
192
+ # for manual review, and the unhandled_mail hook allows you to do that:
193
+ #
194
+ # MailProcessor.unhandled_mail do
195
+ # # raise by default
196
+ #
197
+ # # Forward the mail as an attachment to an admin
198
+ # m = Mail.new
199
+ # m.to 'admin@example.com'
200
+ # m.subject '[APP] Unhandled Received Email'
201
+ # m.add_file(filename: 'message.eml', :content=>mail.encoded)
202
+ # m.deliver
203
+ # end
204
+ #
205
+ # Finally, for all processed mail, regardless of whether it was handled or not,
206
+ # there is an after_mail hook, which can be used to archive all processed mail:
207
+ #
208
+ # MailProcessor.after_mail do
209
+ # # nothing by default
210
+ #
211
+ # # Add it to a received_mail table using Sequel
212
+ # DB[:received_mail].insert(:message=>mail.encoded)
213
+ # end
214
+ #
215
+ # The after_mail hook is called after the handled_mail or unhandled_mail hook
216
+ # is called, even if routing, the handled_mail hook, or the unhandled_mail hook
217
+ # raises an exception. The handled_mail and unhandled_mail hooks are not called
218
+ # if an exception is raised during routing (other than for UnhandledMail exceptions).
219
+ #
220
+ # = Extracting Text from Mail
221
+ #
222
+ # The most common use of the mail_processor plugin is to handle replies to mails sent
223
+ # out by the application, so that recipients can reply to mail to make changes without
224
+ # having to access the application directly. When handling replies, it is common to want
225
+ # to extract only the text of the reply, and ignore the text of the message that was
226
+ # replied to. Because there is no consistent way to format replies in mail, there have
227
+ # evolved various approaches to do this, with some gems devoted to extracting the reply
228
+ # text from a message.
229
+ #
230
+ # The mail_processor plugin does not choose any particular approach for extracting text from mail,
231
+ # but it includes the ability to configure how to do that via the +mail_text+ class method.
232
+ # This method affects the +r.text+ match method, as well as +mail_text+ instance method.
233
+ # By default, the decoded body of the mail is used as the mail text.
234
+ #
235
+ # MailProcessor.mail_text do
236
+ # # mail.body.decoded by default
237
+ #
238
+ # # https://github.com/github/email_reply_parser
239
+ # EmailReplyParser.parse_reply(mail.body.decoded)
240
+ #
241
+ # # https://github.com/fiedl/extended_email_reply_parser
242
+ # mail.parse
243
+ # end
244
+ #
245
+ # = Security
246
+ #
247
+ # Note that due to the way mail delivery works via SMTP, the actual sender and recipient of
248
+ # the mail (the SMTP envelope MAIL FROM and RCPT TO addresses) may not match the sender and
249
+ # receiver embedded in the message. Because mail_processor routing relies on parsing the mail,
250
+ # it does not have access to the actual sender and recipient used at the SMTP level, unless
251
+ # a mail server adds that information as a header to the mail (and clears any existing header
252
+ # to prevent spoofing). Keep that in mind when you are setting up your mail routes. If you
253
+ # have setup your mail server to add the SMTP RCPT TO information to a header, you may want
254
+ # to only consider that header when looking for the recipients of the message, instead of
255
+ # looking at the To and CC headers. You can override the default behavior for determining
256
+ # the recipients (this will affect the +rcpt+ class method, +r.rcpt+ match method, and
257
+ # +mail_recipients+ instance method):
258
+ #
259
+ # MailProcessor.mail_recipients do
260
+ # # Assuming the information is in the X-SMTP-To header
261
+ # Array(header['X-SMTP-To'].decoded)
262
+ # end
263
+ #
264
+ # Also note that unlike when handling web requests where you can rely on storing authentication
265
+ # information in the session, when processing mail, you should manually authenticate each message,
266
+ # as email is trivially forged. One way to do this is assigning and storing a unique identifier when
267
+ # sending each message, and checking for a matching identifier when receiving a response. Another
268
+ # option is including a computable authentication code (e.g. HMAC) in the message, and then
269
+ # when receiving a response, recomputing the authentication code and seeing if it matches the
270
+ # authentication code in the message. The unique identifier approach requires storing a large
271
+ # number of identifiers, but allows you to remove the identifier after a reply is received
272
+ # (to ensure only one response is handled). The authentication code approach does not
273
+ # require additional storage, but does not allow you to ensure only a single response is handled.
274
+ #
275
+ # = Avoiding Mail Loops
276
+ #
277
+ # If processing the mail results in sending out additional mail, be careful not to send a
278
+ # response to the sender of the email, otherwise if the sender of the email has an
279
+ # auto-responder, you can end up with a mail loop, where every mail you send results in
280
+ # a response, which you then process and send out a response to.
281
+ module MailProcessor
282
+ # Exception class raised when a mail processed is not handled during routing,
283
+ # either implicitly because the +r.handle+ method was not called, or via an explicit
284
+ # call to +unhandled_mail+.
285
+ class UnhandledMail < StandardError; end
286
+
287
+ module ClassMethods
288
+ # Freeze the rcpt routes if they are present.
289
+ def freeze
290
+ if string_routes = opts[:mail_processor_string_routes].freeze
291
+ string_routes.freeze
292
+ opts[:mail_processor_regexp_routes].freeze
293
+ end
294
+ super
295
+ end
296
+
297
+ # Process the given Mail instance, calling the appropriate hooks depending on
298
+ # whether the mail was handled during processing.
299
+ def process_mail(mail)
300
+ scope = new("PATH_INFO"=>'', 'SCRIPT_NAME'=>'', "REQUEST_METHOD"=>"PROCESSMAIL", 'rack.input'=>StringIO.new, 'roda.mail'=>mail)
301
+
302
+ begin
303
+ begin
304
+ scope.process_mail
305
+ rescue UnhandledMail
306
+ scope.unhandled_mail_hook
307
+ else
308
+ scope.handled_mail_hook
309
+ end
310
+ ensure
311
+ scope.after_mail_hook
312
+ end
313
+ end
314
+
315
+ # Process all mail in the given mailbox. If the +:retriever+ option is
316
+ # given, should be an object supporting the Mail retriever API, otherwise
317
+ # uses the default Mail retriever_method. This deletes retrieved mail from the
318
+ # mailbox after processing, so that when called multiple times it does
319
+ # not reprocess the same mail. If mail should be archived and not deleted,
320
+ # the +after_mail+ method should be used to perform the archiving of the mail.
321
+ def process_mailbox(opts=OPTS)
322
+ (opts[:retriever] || Mail).find_and_delete(opts.dup){|m| process_mail(m)}
323
+ nil
324
+ end
325
+
326
+ # Setup a routing tree for the given recipient addresses, which can be strings or regexps.
327
+ # Any messages matching the given recipient address will use these routing trees instead
328
+ # of the normal routing tree.
329
+ def rcpt(*addresses, &block)
330
+ opts[:mail_processor_string_routes] ||= {}
331
+ opts[:mail_processor_regexp_routes] ||= {}
332
+ string_meth = nil
333
+ regexp_meth = nil
334
+ addresses.each do |address|
335
+ case address
336
+ when String
337
+ unless string_meth
338
+ string_meth = define_roda_method("mail_processor_string_route_#{address}", 1, &convert_route_block(block))
339
+ end
340
+ opts[:mail_processor_string_routes][address] = string_meth
341
+ when Regexp
342
+ unless regexp_meth
343
+ regexp_meth = define_roda_method("mail_processor_regexp_route_#{address}", :any, &convert_route_block(block))
344
+ end
345
+ opts[:mail_processor_regexp_routes][address] = regexp_meth
346
+ else
347
+ raise RodaError, "invalid address format passed to rcpt, should be Array or String"
348
+ end
349
+ end
350
+ nil
351
+ end
352
+
353
+ %w'after_mail handled_mail unhandled_mail'.each do |meth|
354
+ class_eval(<<-END, __FILE__, __LINE__+1)
355
+ def #{meth}(&block)
356
+ define_method(:#{meth}_hook, &block)
357
+ nil
358
+ end
359
+ END
360
+ end
361
+
362
+ %w'mail_recipients mail_text'.each do |meth|
363
+ class_eval(<<-END, __FILE__, __LINE__+1)
364
+ def #{meth}(&block)
365
+ define_method(:#{meth}, &block)
366
+ nil
367
+ end
368
+ END
369
+ end
370
+ end
371
+
372
+ module InstanceMethods
373
+ [:to, :from, :cc, :body, :subject, :header].each do |field|
374
+ class_eval(<<-END, __FILE__, __LINE__+1)
375
+ def #{field}
376
+ mail.#{field}
377
+ end
378
+ END
379
+ end
380
+
381
+ # Perform the processing of mail for this request, first considering
382
+ # routes defined via the class-level +rcpt+ method, and then the
383
+ # normal routing tree passed in as the block.
384
+ def process_mail(&block)
385
+ if string_routes = opts[:mail_processor_string_routes]
386
+ addresses = mail_recipients
387
+
388
+ addresses.each do |address|
389
+ if meth = string_routes[address.to_s.downcase]
390
+ _roda_handle_route{send(meth, @_request)}
391
+ return
392
+ end
393
+ end
394
+
395
+ opts[:mail_processor_regexp_routes].each do |regexp, meth|
396
+ addresses.each do |address|
397
+ if md = regexp.match(address)
398
+ _roda_handle_route{send(meth, @_request, *md.captures)}
399
+ return
400
+ end
401
+ end
402
+ end
403
+ end
404
+
405
+ _roda_handle_main_route
406
+
407
+ nil
408
+ end
409
+
410
+ # Hook called after processing any mail, whether the mail was
411
+ # handled or not. Does nothing by default.
412
+ def after_mail_hook
413
+ nil
414
+ end
415
+
416
+ # Hook called after processing a mail, when the mail was handled.
417
+ # Does nothing by default.
418
+ def handled_mail_hook
419
+ nil
420
+ end
421
+
422
+ # Hook called after processing a mail, when the mail was not handled.
423
+ # Reraises the UnhandledMail exception raised during mail processing
424
+ # by default.
425
+ def unhandled_mail_hook
426
+ raise
427
+ end
428
+
429
+ # The mail instance being processed.
430
+ def mail
431
+ env['roda.mail']
432
+ end
433
+
434
+ # The text of the mail instance being processed, uses the
435
+ # decoded body of the mail by default.
436
+ def mail_text
437
+ mail.body.decoded
438
+ end
439
+
440
+ # The recipients of the mail instance being processed, uses the To and CC
441
+ # headers by default.
442
+ def mail_recipients
443
+ Array(to) + Array(cc)
444
+ end
445
+
446
+ # Raise an UnhandledMail exception with the given reason, used to mark the
447
+ # mail as not handled. A reason why the mail was not handled must be
448
+ # provided, which will be used as the exception message.
449
+ def unhandled_mail(reason)
450
+ raise UnhandledMail, reason
451
+ end
452
+ end
453
+
454
+ module RequestMethods
455
+ [:to, :from, :cc, :body, :subject, :rcpt, :text].each do |field|
456
+ class_eval(<<-END, __FILE__, __LINE__+1)
457
+ def handle_#{field}(val)
458
+ #{field}(val) do |*args|
459
+ handle do
460
+ yield(*args)
461
+ end
462
+ end
463
+ end
464
+
465
+ def #{field}(address, &block)
466
+ on(:#{field}=>address, &block)
467
+ end
468
+ END
469
+
470
+ case field
471
+ when :rcpt, :text, :body, :subject
472
+ next
473
+ end
474
+
475
+ class_eval(<<-END, __FILE__, __LINE__+1)
476
+ private
477
+
478
+ def match_#{field}(address)
479
+ _match_address(:#{field}, address, Array(mail.#{field}))
480
+ end
481
+ END
482
+ end
483
+
484
+ # Same as +header+, but also mark the message as being handled.
485
+ def handle_header(key, value=nil)
486
+ header(key, value) do |*args|
487
+ handle do
488
+ yield(*args)
489
+ end
490
+ end
491
+ end
492
+
493
+ # Match based on a mail header value.
494
+ def header(key, value=nil, &block)
495
+ on(:header=>[key, value], &block)
496
+ end
497
+
498
+ # Mark the mail as having been handled, so routing will not call
499
+ # unhandled_mail implicitly.
500
+ def handle(&block)
501
+ env['roda.mail_handled'] = true
502
+ always(&block)
503
+ end
504
+
505
+ private
506
+
507
+ if RUBY_VERSION >= '2.4.0'
508
+ # Whether the addresses are the same (case insensitive match).
509
+ def address_match?(a1, a2)
510
+ a1.casecmp?(a2)
511
+ end
512
+ else
513
+ # :nocov:
514
+ def address_match?(a1, a2)
515
+ a1.downcase == a2.downcase
516
+ end
517
+ # :nocov:
518
+ end
519
+
520
+ # Match if any of the given addresses match the given val, which
521
+ # can be a string (case insensitive match of the string), array of
522
+ # strings (case insensitive match of any string), or regexp
523
+ # (normal regexp match).
524
+ def _match_address(field, val, addresses)
525
+ case val
526
+ when String
527
+ addresses.any?{|a| address_match?(a, val)}
528
+ when Array
529
+ overlap = []
530
+ addresses.each do |a|
531
+ val.each do |v|
532
+ if address_match?(a, v)
533
+ overlap << a
534
+ end
535
+ end
536
+ end
537
+
538
+ unless overlap.empty?
539
+ @captures.concat(overlap)
540
+ end
541
+ when Regexp
542
+ matched = false
543
+ addresses.each do |v|
544
+ if md = val.match(v)
545
+ matched = true
546
+ @captures.concat(md.captures)
547
+ end
548
+ end
549
+ matched
550
+ else
551
+ unsupported_matcher(:field=>val)
552
+ end
553
+ end
554
+
555
+ # Match if the content matches the given val, which
556
+ # can be a string (case sensitive substring match), array of
557
+ # strings (case sensitive substring match of any string), or regexp
558
+ # (normal regexp match).
559
+ def _match_content(field, val, content)
560
+ case val
561
+ when String
562
+ content.include?(val)
563
+ when Array
564
+ val.each do |v|
565
+ if content.include?(v)
566
+ return @captures << v
567
+ end
568
+ end
569
+ false
570
+ when Regexp
571
+ if md = val.match(content)
572
+ @captures.concat(md.captures)
573
+ end
574
+ else
575
+ unsupported_matcher(field=>val)
576
+ end
577
+ end
578
+
579
+ # Match the value against the full mail body.
580
+ def match_body(val)
581
+ _match_content(:body, val, mail.body.decoded)
582
+ end
583
+
584
+ # Match the value against the mail subject.
585
+ def match_subject(val)
586
+ _match_content(:subject, val, mail.subject)
587
+ end
588
+
589
+ # Match the given address against all recipients in the mail.
590
+ def match_rcpt(address)
591
+ _match_address(:rcpt, address, scope.mail_recipients)
592
+ end
593
+
594
+ # Match the value against the extracted mail text.
595
+ def match_text(val)
596
+ _match_content(:text, val, scope.mail_text)
597
+ end
598
+
599
+ # Match against a header specified by key with the given
600
+ # value (which may be nil).
601
+ def match_header((key, value))
602
+ return unless content = mail.header[key]
603
+
604
+ if value.nil?
605
+ @captures << content.decoded
606
+ else
607
+ _match_content(:header, value, content.decoded)
608
+ end
609
+ end
610
+
611
+ # The mail instance being processed.
612
+ def mail
613
+ env['roda.mail']
614
+ end
615
+
616
+ # If the routing did not explicitly mark the mail as handled
617
+ # mark it as unhandled.
618
+ def block_result_body(_)
619
+ unless env['roda.mail_handled']
620
+ scope.unhandled_mail('mail was not handled during mail_processor routing')
621
+ end
622
+ end
623
+ end
624
+ end
625
+
626
+ register_plugin(:mail_processor, MailProcessor)
627
+ end
628
+ end
629
+