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.
- checksums.yaml +7 -0
- data/roda-3.106.0/MIT-LICENSE +21 -0
- data/roda-3.106.0/lib/roda/cache.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
- data/roda-3.106.0/lib/roda/plugins/_after_hook.rb +11 -0
- data/roda-3.106.0/lib/roda/plugins/_base64.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/_before_hook.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/_optimized_matching.rb +227 -0
- data/roda-3.106.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
- data/roda-3.106.0/lib/roda/plugins/additional_render_engines.rb +61 -0
- data/roda-3.106.0/lib/roda/plugins/additional_view_directories.rb +66 -0
- data/roda-3.106.0/lib/roda/plugins/all_verbs.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/assets.rb +902 -0
- data/roda-3.106.0/lib/roda/plugins/assets_preloading.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/assume_ssl.rb +28 -0
- data/roda-3.106.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
- data/roda-3.106.0/lib/roda/plugins/backtracking_array.rb +88 -0
- data/roda-3.106.0/lib/roda/plugins/bearer_token.rb +28 -0
- data/roda-3.106.0/lib/roda/plugins/branch_locals.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/break.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/caching.rb +206 -0
- data/roda-3.106.0/lib/roda/plugins/capture_erb.rb +94 -0
- data/roda-3.106.0/lib/roda/plugins/chunked.rb +333 -0
- data/roda-3.106.0/lib/roda/plugins/class_level_routing.rb +111 -0
- data/roda-3.106.0/lib/roda/plugins/class_matchers.rb +155 -0
- data/roda-3.106.0/lib/roda/plugins/common_logger.rb +82 -0
- data/roda-3.106.0/lib/roda/plugins/conditional_sessions.rb +67 -0
- data/roda-3.106.0/lib/roda/plugins/content_for.rb +105 -0
- data/roda-3.106.0/lib/roda/plugins/content_security_policy.rb +333 -0
- data/roda-3.106.0/lib/roda/plugins/cookie_flags.rb +157 -0
- data/roda-3.106.0/lib/roda/plugins/cookies.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/csrf.rb +81 -0
- data/roda-3.106.0/lib/roda/plugins/custom_block_results.rb +85 -0
- data/roda-3.106.0/lib/roda/plugins/custom_matchers.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/default_headers.rb +64 -0
- data/roda-3.106.0/lib/roda/plugins/default_status.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/delay_build.rb +18 -0
- data/roda-3.106.0/lib/roda/plugins/delegate.rb +77 -0
- data/roda-3.106.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
- data/roda-3.106.0/lib/roda/plugins/direct_call.rb +38 -0
- data/roda-3.106.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
- data/roda-3.106.0/lib/roda/plugins/drop_body.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/each_part.rb +87 -0
- data/roda-3.106.0/lib/roda/plugins/early_hints.rb +25 -0
- data/roda-3.106.0/lib/roda/plugins/empty_root.rb +49 -0
- data/roda-3.106.0/lib/roda/plugins/environments.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/erb_h.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/error_email.rb +152 -0
- data/roda-3.106.0/lib/roda/plugins/error_handler.rb +133 -0
- data/roda-3.106.0/lib/roda/plugins/error_mail.rb +149 -0
- data/roda-3.106.0/lib/roda/plugins/exception_page.rb +445 -0
- data/roda-3.106.0/lib/roda/plugins/filter_common_logger.rb +46 -0
- data/roda-3.106.0/lib/roda/plugins/flash.rb +119 -0
- data/roda-3.106.0/lib/roda/plugins/h.rb +59 -0
- data/roda-3.106.0/lib/roda/plugins/halt.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/hash_branches.rb +145 -0
- data/roda-3.106.0/lib/roda/plugins/hash_matcher.rb +39 -0
- data/roda-3.106.0/lib/roda/plugins/hash_paths.rb +128 -0
- data/roda-3.106.0/lib/roda/plugins/hash_public.rb +125 -0
- data/roda-3.106.0/lib/roda/plugins/hash_public_cache.rb +91 -0
- data/roda-3.106.0/lib/roda/plugins/hash_routes.rb +304 -0
- data/roda-3.106.0/lib/roda/plugins/head.rb +99 -0
- data/roda-3.106.0/lib/roda/plugins/header_matchers.rb +88 -0
- data/roda-3.106.0/lib/roda/plugins/heartbeat.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/hmac_paths.rb +413 -0
- data/roda-3.106.0/lib/roda/plugins/hooks.rb +92 -0
- data/roda-3.106.0/lib/roda/plugins/host_authorization.rb +155 -0
- data/roda-3.106.0/lib/roda/plugins/host_routing.rb +194 -0
- data/roda-3.106.0/lib/roda/plugins/hsts.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/indifferent_params.rb +126 -0
- data/roda-3.106.0/lib/roda/plugins/inject_erb.rb +33 -0
- data/roda-3.106.0/lib/roda/plugins/invalid_request_body.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/ip_from_header.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/json.rb +119 -0
- data/roda-3.106.0/lib/roda/plugins/json_parser.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/link_to.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/mail_processor.rb +629 -0
- data/roda-3.106.0/lib/roda/plugins/mailer.rb +285 -0
- data/roda-3.106.0/lib/roda/plugins/map_matcher.rb +48 -0
- data/roda-3.106.0/lib/roda/plugins/match_affix.rb +64 -0
- data/roda-3.106.0/lib/roda/plugins/match_hook.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/match_hook_args.rb +93 -0
- data/roda-3.106.0/lib/roda/plugins/middleware.rb +225 -0
- data/roda-3.106.0/lib/roda/plugins/middleware_stack.rb +101 -0
- data/roda-3.106.0/lib/roda/plugins/module_include.rb +96 -0
- data/roda-3.106.0/lib/roda/plugins/multi_public.rb +95 -0
- data/roda-3.106.0/lib/roda/plugins/multi_route.rb +156 -0
- data/roda-3.106.0/lib/roda/plugins/multi_run.rb +144 -0
- data/roda-3.106.0/lib/roda/plugins/multi_view.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
- data/roda-3.106.0/lib/roda/plugins/named_routes.rb +167 -0
- data/roda-3.106.0/lib/roda/plugins/named_templates.rb +102 -0
- data/roda-3.106.0/lib/roda/plugins/not_allowed.rb +151 -0
- data/roda-3.106.0/lib/roda/plugins/not_found.rb +55 -0
- data/roda-3.106.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
- data/roda-3.106.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/padrino_render.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/param_matchers.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/params_capturing.rb +118 -0
- data/roda-3.106.0/lib/roda/plugins/part.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/partials.rb +74 -0
- data/roda-3.106.0/lib/roda/plugins/pass.rb +47 -0
- data/roda-3.106.0/lib/roda/plugins/path.rb +255 -0
- data/roda-3.106.0/lib/roda/plugins/path_matchers.rb +54 -0
- data/roda-3.106.0/lib/roda/plugins/path_rewriter.rb +111 -0
- data/roda-3.106.0/lib/roda/plugins/permissions_policy.rb +340 -0
- data/roda-3.106.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
- data/roda-3.106.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
- data/roda-3.106.0/lib/roda/plugins/precompile_templates.rb +156 -0
- data/roda-3.106.0/lib/roda/plugins/public.rb +182 -0
- data/roda-3.106.0/lib/roda/plugins/r.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
- data/roda-3.106.0/lib/roda/plugins/redirect_path.rb +59 -0
- data/roda-3.106.0/lib/roda/plugins/relative_path.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/render.rb +998 -0
- data/roda-3.106.0/lib/roda/plugins/render_coverage.rb +105 -0
- data/roda-3.106.0/lib/roda/plugins/render_each.rb +224 -0
- data/roda-3.106.0/lib/roda/plugins/render_locals.rb +102 -0
- data/roda-3.106.0/lib/roda/plugins/request_aref.rb +77 -0
- data/roda-3.106.0/lib/roda/plugins/request_headers.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/response_attachment.rb +94 -0
- data/roda-3.106.0/lib/roda/plugins/response_content_type.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/response_request.rb +36 -0
- data/roda-3.106.0/lib/roda/plugins/route_block_args.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/route_csrf.rb +388 -0
- data/roda-3.106.0/lib/roda/plugins/run_append_slash.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/run_handler.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/run_require_slash.rb +46 -0
- data/roda-3.106.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
- data/roda-3.106.0/lib/roda/plugins/send_file.rb +127 -0
- data/roda-3.106.0/lib/roda/plugins/sessions.rb +537 -0
- data/roda-3.106.0/lib/roda/plugins/shape_friendly.rb +232 -0
- data/roda-3.106.0/lib/roda/plugins/shared_vars.rb +86 -0
- data/roda-3.106.0/lib/roda/plugins/sinatra_helpers.rb +444 -0
- data/roda-3.106.0/lib/roda/plugins/slash_path_empty.rb +26 -0
- data/roda-3.106.0/lib/roda/plugins/static.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/static_routing.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/status_303.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/status_handler.rb +86 -0
- data/roda-3.106.0/lib/roda/plugins/streaming.rb +170 -0
- data/roda-3.106.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_matchers.rb +188 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_status.rb +32 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_views.rb +37 -0
- data/roda-3.106.0/lib/roda/plugins/timestamp_public.rb +78 -0
- data/roda-3.106.0/lib/roda/plugins/type_routing.rb +217 -0
- data/roda-3.106.0/lib/roda/plugins/typecast_params.rb +1185 -0
- data/roda-3.106.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/unescape_path.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/view_options.rb +203 -0
- data/roda-3.106.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
- data/roda-3.106.0/lib/roda/plugins.rb +68 -0
- data/roda-3.106.0/lib/roda/request.rb +722 -0
- data/roda-3.106.0/lib/roda/response.rb +221 -0
- data/roda-3.106.0/lib/roda/session_middleware.rb +184 -0
- data/roda-3.106.0/lib/roda/version.rb +18 -0
- data/roda-3.106.0/lib/roda.rb +741 -0
- data/ultra-max-sys.gemspec +12 -0
- 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
|
+
|