├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── examples ├── config │ ├── dreamhost.rb.example │ ├── gmail.rb.example │ └── test.rb ├── db │ └── lists │ │ └── .gitkeep ├── example_helper.rb ├── libre_list_clone.rb ├── live_test.rb ├── ping_pong.rb ├── simple_mailing_list.rb └── views │ ├── non-subscriber-error.erb │ ├── subscribe-error.erb │ ├── subscribe-success.erb │ ├── test │ ├── _echo_body.erb │ ├── _echo_date.erb │ ├── _echo_list.erb │ ├── _echo_sender.erb │ ├── echo.erb │ ├── echo_with_partials.erb │ └── moneyball.erb │ ├── unsubscribe-error.erb │ └── unsubscribe-success.erb ├── lib ├── newman.rb └── newman │ ├── application.rb │ ├── controller.rb │ ├── email_logger.rb │ ├── filters.rb │ ├── mailer.rb │ ├── mailing_list.rb │ ├── recorder.rb │ ├── request_logger.rb │ ├── response_logger.rb │ ├── server.rb │ ├── settings.rb │ ├── store.rb │ ├── test_mailer.rb │ └── version.rb ├── newman.gemspec └── test ├── helper.rb ├── integration ├── acid_tests.rb ├── skip_response_test.rb ├── subject_filter_test.rb ├── template_test.rb └── to_filter_test.rb ├── log └── .gitkeep ├── settings.rb └── suite.rb /.gitignore: -------------------------------------------------------------------------------- 1 | examples/config/*.rb 2 | test/config*.rb 3 | *.store 4 | Gemfile.lock 5 | .bundle 6 | coverage 7 | docs/ 8 | *.log 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.3.0 (2012-03-08) 2 | 3 | **Improvements:** 4 | 5 | - Added documentation of all settings that can be modified via Newman configuration files. 6 | 7 | - Added `Newman::Server#simple!`, which combines some of the flexibility of 8 | manually building a server object with sensible defaults. This method is 9 | useful for building simple tick-based servers, or for tweaking small details 10 | such as which logger you want to use. 11 | 12 | - Allow a locals hash to be passed to tilt via `Newman::Controller#template`, 13 | and added some integration tests for template support. 14 | 15 | **Behavior Changes:** 16 | 17 | - Caching of logger object is less aggressive now, allowing 18 | `Newman::Server#logger=` to be called at any time to change the 19 | logger object used by the server. 20 | 21 | - `Newman::Server.new` no longer accepts a custom logger argument. 22 | Use `Newman::Server#logger=` instead. 23 | 24 | - Locked explicitly to mail v2.3.0, because we're being bit by an upstream 25 | bug. We will try to lock more optimistically in a future release of 26 | Newman. 27 | 28 | [Diff of all changes since 0.2.1](https://github.com/mendicant-university/newman/compare/v0.2.1...v0.3.0#diff-43) 29 | 30 | ### 0.2.1 (2012-02-11) 31 | 32 | - Fixed a bug with `Newman::Application#match`. It now normalizes keys to 33 | strings for easy use with `Newman::Application#compile_regex`, which 34 | fixes our substitution logic in patterns. 35 | 36 | [Diff of all changes since 0.2.0](https://github.com/mendicant-university/newman/compare/v0.2.0...v0.2.1#diff-43) 37 | 38 | ### 0.2.0 (2012-02-08) 39 | 40 | - Internals mostly rewritten, changes too numerous to outline meaningfully. 41 | We'll keep better track of these changes in the future. 42 | 43 | - Basic logging support added. 44 | 45 | - Lots of documentation added. 46 | 47 | - Server no longer crashes upon application errors unless 48 | service.raise_exceptions is set to true. 49 | 50 | - Added Newman::Controller#skip_response which allows disabling delivery of a response 51 | email upon demand. 52 | 53 | - Make Newman::MailingList fail gracefully by checking subscriber status before 54 | attempting subscribe / unsubscribe. 55 | 56 | - Add a generic callback method to Newman::Application which allows for arbitrary 57 | callbacks to be run based on filters against the Mail::Message object. 58 | 59 | [Diff of all changes since 0.1.1](https://github.com/mendicant-university/newman/compare/v0.1.1...v0.2.0#diff-43) 60 | 61 | ### 0.1.1 (2012-02-03) 62 | 63 | - First official release. 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | 5 | # Note: Newman does not have a redcarpet 1.x dependency at runtime, 6 | # this is in the gemspec only to make generating documentation easier 7 | # for developers. 8 | gem 'rocco', '0.8.2' 9 | gem 'redcarpet', '1.17.2' 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Newman](http://i.imgur.com/GCqaT.png) 2 | 3 | Newman is a microframework which aims to do for email-based 4 | applications what Rack and Sinatra have done for web programming. **While our 5 | goals may be ambitious, this project is 6 | currently in a very early experimental stage, and is in no way safe for use in 7 | production.** 8 | 9 | That said, Newman is already capable of doing a number of interesting things. In 10 | particular: 11 | 12 | * A simple polling server provides a basic interface for 13 | reading email from a single inbox and then building up a response email. 14 | 15 | * Filters are provided for handling emails based on their TO and SUBJECT fields. 16 | 17 | * Filters can also be defined for handling messages based on arbitrary 18 | conditions evaluated against a `Mail::Messsage` object. 19 | 20 | * A rudimentary PStore backed storage mechanism is provided for persistence. 21 | 22 | * Basic support for maintaining persistent lists of email addresses is 23 | provided. 24 | 25 | * Basic support for email templates are provided via Tilt. 26 | 27 | * A barebones configuration system allows configuring email settings, service 28 | settings, and application specific settings. 29 | 30 | We are still working on figuring out what belongs in Newman and what doesn't, 31 | and so all of these features are subject to change or disappear entirely. But if 32 | you have a need for this sort of tool, it's worth noting that this software 33 | isn't entirely vaporware, and that we could use your help! 34 | 35 | ### Scary Warning: 36 | 37 | DON'T HOOK UP NEWMAN TO ANY EMAIL INBOX THAT YOU CAN'T COMPLETELY WIPE OUT EVERY TIME IT RUNS. NEWMAN **WILL** DELETE YOUR EMAILS!!! 38 | 39 | ### For a demonstration of how Newman is used: 40 | 41 | Check out [Jester](http://github.com/mendicant-university/jester) as well as some of the 42 | simple examples in this repository. 43 | 44 | ### For a walkthrough of Newman's codebase: 45 | 46 | Check out [Newman's Rocco-based API documentation](http://mendicant-university.github.com/newman/lib/newman.html). 47 | 48 | We update this documentation on each gem release of newman, but to generate this documentation yourself, 49 | you'll need to install the rocco gem. 50 | 51 | ### For general discussion, questions, and ideas about Newman: 52 | 53 | Find seacreature or ericgj in the #newman channel on Freenode or send an email to newman@librelist.org 54 | 55 | ### Contributing to Newman: 56 | 57 | We do not yet have a clear roadmap or contributor guidelines, so be sure to talk 58 | to us before working on bug fixes or patches. But assuming you do want to send 59 | us some code, here is what you need to know: 60 | 61 | * You get to keep the copyright to your code, but you must agree to license it 62 | under the MIT license. 63 | 64 | * Your code should come with tests. Integration tests are fine, but unit tests 65 | would be nice where appropriate. Right now Newman is under tested and we don't 66 | want to make that problem worse. You can of course submit a pull request for 67 | feedback BEFORE writing tests. 68 | 69 | * Your code should be fully documented, and properly formatted for use with 70 | Rocco. Please try to emulate the style and conventions we've been using where 71 | possible. Do the best you can with this, and we'll help tighten up wording and 72 | clean up formatting as needed. You can of course submit a pull request for 73 | feedback BEFORE writing documentation. 74 | 75 | * Newman is taking a use-case oriented approach to design. Be prepared to 76 | justify any proposed change with real or realistic scenarios, rather than 77 | simply addressing theoretical concerns. 78 | 79 | ### Versioning Policy: 80 | 81 | We will try to follow the guidelines below when cutting new releases, 82 | to the extent that it makes sense to do so. 83 | 84 | 1) Clearly mark each object that Newman provides as being part of either 85 | the 'external API' or the 'internal API'. The external API is for 86 | application developers, the internal API is for Newman itself as well as 87 | extension developers 88 | 89 | 2) Before 1.0, allow backwards incompatible internal API changes during 90 | every release, and allow backwards incompatible external API changes 91 | during minor version bumps, but do not add or change external behavior 92 | in tiny version bumps. 93 | 94 | 3) After 1.0, do not allow external or internal API changes during tiny 95 | version bumps (these will be bug fixes only). Allow changes to the 96 | internal API during minor version bumps, but maintain backwards 97 | compatibility with the 1.0 release (i.e. things introduced after 1.0 can 98 | be changed / removed, but things which shipped with 1.0 should stay 99 | supported, even internally). Allow external API changes or 100 | backwards-incompatible changes to the internals only on major version 101 | bumps (i.e. 2.0, 3.0, etc). Use semantic versioning and declare the 102 | external API to be the 'public' API. 103 | 104 | We plan to get to 1.0 quickly to reach a stabilizing point for application 105 | developers and a slower moving target for extension developers. This means 106 | that our 1.0 release will be more of a minimum-viable product and not 107 | necessarily a full-stack framework. 108 | 109 | ### Authorship: 110 | 111 | Newman is being developed by [Gregory Brown](http://community.mendicantuniversity.org/people/sandal) 112 | and [Eric Gjertsen](http://community.mendicantuniversity.org/people/ericgj), along with 113 | help from several other folks. 114 | 115 | It is based on an assignment from [Mendicant 116 | University](http://mendicantuniversity.org)'s January 2011 core 117 | skills course, and was also used as a sample application for the [Practicing Ruby](http://practicingruby.com) 118 | journal. The original inspiration for this project came from some code 119 | written by [Brent Vatne](http://community.mendicantuniversity.org/people/brentvatne), 120 | as well as from the general ideas behind Rack and Sinatra. 121 | 122 | [View the full list of contributors](https://github.com/mendicant-university/newman/contributors) to see who else has helped out. 123 | 124 | ### License: 125 | 126 | Copyright (c) 2012 Gregory Brown, Eric Gjertsen, et al. 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 129 | 130 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 131 | 132 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 133 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # TASKS TAKEN FROM: https://github.com/rtomayko/rocco/blob/master/Rakefile 2 | # 3 | require 'rake/clean' 4 | require 'bundler/setup' 5 | 6 | require 'rocco/tasks' 7 | Rocco::make 'docs/', 'lib/**/*.rb' 8 | 9 | desc 'Build rocco docs' 10 | task :docs => :rocco 11 | directory 'docs/' 12 | 13 | task :default => :docs 14 | 15 | desc 'Build docs and open in browser for the reading' 16 | task :read => :docs do 17 | sh 'open docs/lib/subscription_counter.html' 18 | end 19 | 20 | desc 'Update gh-pages branch' 21 | task :pages => ['docs/.git', :docs] do 22 | rev = `git rev-parse --short HEAD`.strip 23 | Dir.chdir 'docs' do 24 | sh "git add ." 25 | sh "git commit -m 'rebuild pages from #{rev}'" do |ok,res| 26 | if ok 27 | verbose { puts "gh-pages updated" } 28 | sh "git push -q origin HEAD:gh-pages" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /examples/config/dreamhost.rb.example: -------------------------------------------------------------------------------- 1 | # WARNING: DON'T HOOK UP NEWMAN TO ANY EMAIL INBOX THAT 2 | # YOU CAN'T COMPLETELY WIPE OUT EVERY TIME IT RUNS. 3 | # NEWMAN **WILL** DELETE YOUR EMAILS!!! 4 | 5 | imap.address = "mail.<>" 6 | imap.user = "<>@<>" 7 | imap.password = "<>" 8 | 9 | smtp.address = "mail.<>" 10 | smtp.user = "<>@<>" 11 | smtp.password = "<>" 12 | 13 | service.domain = "<>" 14 | service.templates_dir = "views" 15 | service.default_sender = "<>@<>" 16 | service.polling_interval = 10 17 | 18 | application.simplelist_db = "db/lists/simple_mailing_list.store" 19 | application.librelist_db = "db/lists/libre.store" 20 | application.ping_email = "<>+ping@<>" 21 | application.live_test_delay = 5 22 | -------------------------------------------------------------------------------- /examples/config/gmail.rb.example: -------------------------------------------------------------------------------- 1 | # WARNING: DON'T HOOK UP NEWMAN TO ANY EMAIL INBOX THAT 2 | # YOU CAN'T COMPLETELY WIPE OUT EVERY TIME IT RUNS. 3 | # NEWMAN **WILL** DELETE YOUR EMAILS!!! 4 | 5 | imap.address = "imap.gmail.com" 6 | imap.user = "<>@gmail.com" 7 | imap.password = "<>" 8 | imap.ssl_enabled = true 9 | imap.port = 993 10 | 11 | smtp.address = "smtp.gmail.com" 12 | smtp.user = "<>@gmail.com" 13 | smtp.password = "<>" 14 | smtp.starttls_enabled = true 15 | smtp.port = 587 16 | 17 | service.domain = "gmail.com" 18 | service.templates_dir = "views" 19 | service.default_sender = "<>@gmail.com" 20 | service.polling_interval = 10 21 | 22 | application.simplelist_db = "db/lists/simple_mailing_list.store" 23 | application.librelist_db = "db/lists/libre.store" 24 | application.ping_email = "<>+ping@gmail.com" 25 | application.live_test_delay = 5 26 | -------------------------------------------------------------------------------- /examples/config/test.rb: -------------------------------------------------------------------------------- 1 | service.domain = "test.com" 2 | service.templates_dir = "views" 3 | service.default_sender = "test@test.com" 4 | service.polling_interval = 10 5 | 6 | application.simplelist_db = "db/lists/simple_mailing_list-test.store" 7 | application.librelist_db = "db/lists/libre-test.store" 8 | -------------------------------------------------------------------------------- /examples/db/lists/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendicant-original/newman/a5273d0eed07f33d206763873c0f106605576300/examples/db/lists/.gitkeep -------------------------------------------------------------------------------- /examples/example_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/newman" 2 | -------------------------------------------------------------------------------- /examples/libre_list_clone.rb: -------------------------------------------------------------------------------- 1 | require_relative "example_helper" 2 | 3 | # This is a cheap and possibly buggy clone of librelist.org's automatic 4 | # mailing list creation 5 | # 6 | # Settings: 7 | # 8 | # - `application.librelist_db`
9 | # path to mailing list pstore file, relative to application root 10 | # 11 | module Newman 12 | module Examples 13 | LibreList = Newman::Application.new do 14 | helpers do 15 | def load_list(name) 16 | store = Newman::Store.new(settings.application.librelist_db) 17 | Newman::MailingList.new(name, store) 18 | end 19 | end 20 | 21 | match :list_id, "[^.]+" 22 | 23 | to(:tag, "{list_id}.subscribe") do 24 | list = load_list(params[:list_id]) 25 | 26 | if list.subscriber?(sender) 27 | respond :subject => "ERROR: Already subscribed", 28 | :body => template("subscribe-error"), 29 | :reply_to => "test+#{params[:list_id]}@#{domain}" 30 | else 31 | list.subscribe(sender) 32 | 33 | respond :subject => "SUBSCRIBED!", 34 | :body => template("subscribe-success"), 35 | :reply_to => "test+#{params[:list_id]}@#{domain}" 36 | end 37 | end 38 | 39 | to(:tag, "{list_id}.unsubscribe") do 40 | list = load_list(params[:list_id]) 41 | 42 | if list.subscriber?(sender) 43 | list.unsubscribe(sender) 44 | 45 | respond :subject => "UNSUBSCRIBED!", 46 | :body => template("unsubscribe-success") 47 | else 48 | respond :subject => "ERROR: Not on subscriber list", 49 | :body => template("unsubscribe-error"), 50 | :reply_to => "test+#{params[:list_id]}@#{domain}" 51 | end 52 | end 53 | 54 | to(:tag, "{list_id}") do 55 | list = load_list(params[:list_id]) 56 | 57 | if list.subscriber?(sender) 58 | forward_message :bcc => list.subscribers.join(", "), 59 | :reply_to => "test+#{params[:list_id]}@#{domain}" 60 | else 61 | respond :subject => "You are not subscribed", 62 | :body => template("non-subscriber-error"), 63 | :reply_to => "test+#{params[:list_id]}@#{domain}" 64 | end 65 | end 66 | 67 | default do 68 | respond :subject => "FAIL" 69 | end 70 | end 71 | end 72 | end 73 | 74 | if __FILE__ == $PROGRAM_NAME 75 | Newman::Server.simple(Newman::Examples::LibreList, "config/environment.rb") 76 | end 77 | -------------------------------------------------------------------------------- /examples/live_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "ping_pong" 2 | 3 | server = Newman::Server.simple!(Newman::Examples::PingPong, 4 | "config/environment.rb") 5 | mailer = server.mailer 6 | settings = server.settings 7 | 8 | mailer.deliver_message(:to => settings.application.ping_email, 9 | :from => settings.service.default_sender) 10 | 11 | puts "Checking in..." 12 | settings.application.live_test_delay.downto(1) do |i| 13 | print "#{i}. " 14 | sleep(1) 15 | end 16 | puts 17 | 18 | server.tick 19 | 20 | remaining_attempts = 3 21 | loop do 22 | if remaining_attempts == 0 23 | abort "FAIL: Did not receive mail yet" 24 | else 25 | incoming = mailer.messages 26 | case 27 | when incoming.empty? 28 | sleep settings.service.polling_interval 29 | when incoming.length > 1 30 | abort "FAIL: More than one message in inbox" 31 | else 32 | if incoming.first.subject == "pong" 33 | puts "OK" 34 | exit 35 | else 36 | abort "FAIL: Expected 'pong', got #{incoming.first.subject}" 37 | end 38 | end 39 | 40 | remaining_attempts -= 1 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /examples/ping_pong.rb: -------------------------------------------------------------------------------- 1 | require_relative "example_helper" 2 | 3 | module Newman 4 | module Examples 5 | PingPong = Newman::Application.new do 6 | to(:tag, "ping") do 7 | respond(:subject => "pong") 8 | end 9 | 10 | default do 11 | respond(:subject => "unknown command") 12 | end 13 | end 14 | end 15 | end 16 | 17 | if __FILE__ == $PROGRAM_NAME 18 | Newman::Server.simple(Newman::Examples::PingPong, "config/environment.rb") 19 | end 20 | -------------------------------------------------------------------------------- /examples/simple_mailing_list.rb: -------------------------------------------------------------------------------- 1 | require_relative "example_helper" 2 | 3 | # The simplest possible mailing list app 4 | # 5 | # Settings: 6 | # 7 | # - `application.simplelist_db`
8 | # path to mailing list pstore file, relative to application root 9 | # 10 | module Newman 11 | module Examples 12 | 13 | SimpleList = Newman::Application.new do 14 | helpers do 15 | def list 16 | store = Newman::Store.new(settings.application.simplelist_db) 17 | 18 | Newman::MailingList.new("simple_list", store) 19 | end 20 | end 21 | 22 | subject(:match, "subscribe") do 23 | if list.subscriber?(sender) 24 | respond :subject => "ERROR: Already subscribed", 25 | :body => template("subscribe-error") 26 | else 27 | list.subscribe(sender) 28 | 29 | respond :subject => "SUBSCRIBED!", 30 | :body => template("subscribe-success") 31 | end 32 | end 33 | 34 | subject(:match, "unsubscribe") do 35 | if list.subscriber?(sender) 36 | list.unsubscribe(sender) 37 | 38 | respond :subject => "UNSUBSCRIBED!", 39 | :body => template("unsubscribe-success") 40 | else 41 | respond :subject => "ERROR: Not on subscriber list", 42 | :body => template("unsubscribe-error") 43 | end 44 | end 45 | 46 | default do 47 | if list.subscriber?(sender) 48 | forward_message :bcc => list.subscribers.join(", ") 49 | else 50 | respond :subject => "You are not subscribed", 51 | :body => template("non-subscriber-error") 52 | end 53 | end 54 | end 55 | end 56 | end 57 | 58 | if __FILE__ == $PROGRAM_NAME 59 | Newman::Server.simple(Newman::Examples::SimpleList, "config/environment.rb") 60 | end 61 | -------------------------------------------------------------------------------- /examples/views/non-subscriber-error.erb: -------------------------------------------------------------------------------- 1 | Sorry, we don't have <%= sender %> on our list of subscribers. Your message was dropped. 2 | -------------------------------------------------------------------------------- /examples/views/subscribe-error.erb: -------------------------------------------------------------------------------- 1 | <%= sender %> is already a subscriber of this mailing list. 2 | -------------------------------------------------------------------------------- /examples/views/subscribe-success.erb: -------------------------------------------------------------------------------- 1 | Welcome to the club, pal! We've added <%= sender %> to our list of people to spam. 2 | -------------------------------------------------------------------------------- /examples/views/test/_echo_body.erb: -------------------------------------------------------------------------------- 1 | The body of the request follows: 2 | 3 | <%= request.decoded %> 4 | -------------------------------------------------------------------------------- /examples/views/test/_echo_date.erb: -------------------------------------------------------------------------------- 1 | It was sent <%= request.date %>. -------------------------------------------------------------------------------- /examples/views/test/_echo_list.erb: -------------------------------------------------------------------------------- 1 | The list it was routed to was <%= params[:list_id] %>. -------------------------------------------------------------------------------- /examples/views/test/_echo_sender.erb: -------------------------------------------------------------------------------- 1 | The sender of this email was <%= sender %>. -------------------------------------------------------------------------------- /examples/views/test/echo.erb: -------------------------------------------------------------------------------- 1 | The sender of this email was <%= sender %>. 2 | It was sent <%= request.date %>. 3 | The list it was routed to was <%= params[:list_id] %>. 4 | The body of the request follows: 5 | 6 | <%= request.decoded %> 7 | -------------------------------------------------------------------------------- /examples/views/test/echo_with_partials.erb: -------------------------------------------------------------------------------- 1 | <%= template('test/_echo_sender') %> 2 | <%= template('test/_echo_date') %> 3 | <%= template('test/_echo_list') %> 4 | <%= template('test/_echo_body') %> -------------------------------------------------------------------------------- /examples/views/test/moneyball.erb: -------------------------------------------------------------------------------- 1 | Pick 4: <%= pick_4.map(&:to_s).join("") %> 2 | 3 | Your magic code is: <%= magic_code %> -------------------------------------------------------------------------------- /examples/views/unsubscribe-error.erb: -------------------------------------------------------------------------------- 1 | Goose egg! Looks like <%= sender %> isn't on our mailing list. Are you sure you 2 | entered the right address? 3 | -------------------------------------------------------------------------------- /examples/views/unsubscribe-success.erb: -------------------------------------------------------------------------------- 1 | Sorry for bothering you. We've removed <%= sender %> from our list of people to 2 | spam. 3 | -------------------------------------------------------------------------------- /lib/newman.rb: -------------------------------------------------------------------------------- 1 | # [Newman](https://github.com/mendicant-university/newman) is a 2 | # microframework which aims to do for email-based applications what Rack and 3 | # Sinatra have done for web programming. **While our goals may be ambitious, 4 | # this project is currently in a very early experimental stage, 5 | # and is in no way safe for use in production.** 6 | # 7 | # That said, we still welcome contributors to help us work on this project 8 | # as we collectively figure out what exactly Newman should be. Even in its 9 | # very early stages, Newman is already doing some useful things and provides 10 | # a wide range of application development features via its external interface 11 | # as well as extension points via its internal interface. 12 | # 13 | # The documentation you're currently reading is meant to help explain Newman's 14 | # implementation to contributors and alpha testers. Before you dig deeper into 15 | # the source, make sure to read 16 | # [Newman's README](https://github.com/mendicant-university/newman) 17 | # as well as the [Jester](https://github.com/mendicant-university/jester) demo 18 | # application. 19 | # 20 | # Assuming you have done those things and are now familiar with the basic ideas 21 | # behind Newman, the following outline may help you in your explorations of 22 | # its source code. 23 | # 24 | # ### External interface (for application developers) 25 | # 26 | # * [Newman::Server](http://mendicant-university.github.com/newman/lib/newman/server.html) 27 | # takes incoming mesages from a mailer object and passes them to applications as a 28 | # request, and then delivers a response email that built up by its 29 | # applications. 30 | # 31 | # * [Newman::Application](http://mendicant-university.github.com/newman/lib/newman/application.html) 32 | # provides the main entry point for Newman application developers, and exists to tie together 33 | # various Newman objects in a convenient way. 34 | # 35 | # * [Newman::Filters](http://mendicant-university.github.com/newman/lib/newman/filters.html) 36 | # provides high level filters for matching incoming requests. 37 | # 38 | # * [Newman::Controller](http://mendicant-university.github.com/newman/lib/newman/controller.html) 39 | # provides a context for application callbacks to run in, and provides most of 40 | # the core functionality for preparing a response email. 41 | # 42 | # * [Newman::MailingList](http://mendicant-university.github.com/newman/lib/newman/mailing_list.html) 43 | # implements a simple mechanism for storing persistent lists of email addresses keyed 44 | # by a mailing list name. 45 | # 46 | # * [Newman::Store](http://mendicant-university.github.com/newman/lib/newman/store.html) provides 47 | # a minimal persistence layer for storing non-relational data. 48 | # 49 | # * [Newman::Recorder](http://mendicant-university.github.com/newman/lib/newman/recorder.html) 50 | # provides a mechanism for storing records with autoincrementing identifiers 51 | # within a `Newman::Store` and supports some rudimentary CRUD functionality. 52 | # 53 | # * Implicitly, the settings files used by Newman as well as the structure of 54 | # its low level data storage format are also considered part of external API, 55 | # even though these are actually implementation details. This is simply 56 | # because we want to make sure to clearly reflect backwards incompatible 57 | # changes to these features via our versioning policy, as this sort of 58 | # change could potentially cause update problems for application 59 | # developers. 60 | # 61 | # ### Internal interface (for extension developers) 62 | # 63 | # * [Newman::EmailLogger](http://mendicant-university.github.com/newman/lib/newman/email_logger.html) 64 | # provides rudimentary logging support for email objects, and primarily exists to 65 | # support the `Newman::RequestLogger` and `Newman::ResponseLogger` objects. 66 | # 67 | # * [Newman::RequestLogger](http://mendicant-university.github.com/newman/lib/newman/request_logger.html) 68 | # provides a mechanism for logging information about incoming emails. 69 | # 70 | # * [Newman::ResponseLogger](http://mendicant-university.github.com/newman/lib/newman/response_logger.html) 71 | # provides a mechanism for logging information about outgoing emails. 72 | # 73 | # * [Newman::Mailer](http://mendicant-university.github.com/newman/lib/newman/mailer.html) provides a thin 74 | # wrapper on top of the [mail gem](http://github.com/mikel/mail) which is 75 | # designed to have a minimal API so that it can easily be swapped out with 76 | # another mailer object. 77 | # 78 | # * [Newman::TestMailer](http://mendicant-university.github.com/newman/lib/newman/test_mailer.html) 79 | # is a drop-in replacement for `Newman::Mailer` meant for use in automated testing. 80 | # 81 | # * [Newman::Settings](http://mendicant-university.github.com/newman/lib/newman/settings.html) provides 82 | # the base functionality that is used by Newman's configuration files. Note 83 | # that while this object is part of the internals, the settings actually used 84 | # by Newman should be considered part of the external API. 85 | # 86 | # ### Getting help, or helping out: 87 | # 88 | # Please catch up with seacreature or ericgj in the #newman channel on Freenode, 89 | # or send an email to newman@librelist.org. We'd love to hear any questions, 90 | # ideas, or suggestions you'd like to share with us. 91 | 92 | require "logger" 93 | require "pstore" 94 | require "ostruct" 95 | require "fileutils" 96 | 97 | require "mail" 98 | require "tilt" 99 | 100 | require_relative "newman/email_logger" 101 | require_relative "newman/request_logger" 102 | require_relative "newman/response_logger" 103 | require_relative "newman/server" 104 | require_relative "newman/filters" 105 | require_relative "newman/application" 106 | require_relative "newman/controller" 107 | require_relative "newman/mailing_list" 108 | require_relative "newman/settings" 109 | require_relative "newman/store" 110 | require_relative "newman/recorder" 111 | require_relative "newman/mailer" 112 | require_relative "newman/test_mailer" 113 | require_relative "newman/version" 114 | -------------------------------------------------------------------------------- /lib/newman/application.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Application` provides the main entry point for Newman application 2 | # developers, and exists to tie together various Newman objects in a convenient 3 | # way. 4 | # 5 | # For an fairly complete example of a `Newman::Application` object in use, be 6 | # sure to check out [Jester](https://github.com/mendicant-university/jester). 7 | # 8 | # `Newman::Application` is part of Newman's **external interface**. 9 | 10 | module Newman 11 | class Application 12 | 13 | include Filters 14 | # --- 15 | 16 | # A `Newman::Application` object is a blank slate upon creation, with fields 17 | # set to hold `callbacks`, `matchers`, and `extensions`. A block may 18 | # optionally be provided, which then gets executed within the context of the 19 | # newly created `Newman::Application` instance. This is the common way of 20 | # building applications, and is demonstrated by the example below: 21 | # 22 | # ping_pong = Newman::Application.new do 23 | # subject("ping") do 24 | # respond(:subject => "pong") 25 | # end 26 | # end 27 | # 28 | # Any method that can be called on a `Newman::Application` instance can be 29 | # called within the provided block, including those methods mixed in by 30 | # `Newman::Filters`. 31 | 32 | def initialize(&block) 33 | self.callbacks = [] 34 | self.matchers = {} 35 | self.extensions = [] 36 | 37 | instance_eval(&block) if block_given? 38 | end 39 | 40 | # --- 41 | 42 | # `Newman::Application#call` accepts a hash of parameters which gets used to 43 | # create a new `Newman::Controller` object. The controller is then extended 44 | # by all of the modules stored in the `extensions` field on the application 45 | # object, and is finally passed along to 46 | # `Newman::Application#trigger_callbacks`, which does the 47 | # magical work of figuring out which callbacks to run, if any. 48 | # 49 | # This method is meant to be run by a `Newman::Server` object, and isn't 50 | # especially useful on its own. 51 | 52 | def call(params) 53 | controller = Controller.new(params) 54 | extensions.each { |mod| controller.extend(mod) } 55 | trigger_callbacks(controller) 56 | end 57 | 58 | # --- 59 | 60 | # `Newman::Application#default` is used to define a default callback 61 | # which will run when no other callbacks match the incoming request. 62 | # For example, you can define a callback such as the one below: 63 | # 64 | # default do 65 | # respond(:subject => "REQUEST NOT UNDERSTOOD") 66 | # end 67 | # 68 | # Unless you are building an application that will never fail to 69 | # match at least one of its filters, you MUST set up a default callback 70 | # if you want to avoid a possible application error. We know this is 71 | # not exactly the most desireable behavior, and will try to fix this 72 | # in a future version of Newman. 73 | 74 | def default(&callback) 75 | self.default_callback = callback 76 | end 77 | 78 | # --- 79 | 80 | # `Newman:::Application#use` is used to register the extension 81 | # modules that get mixed in to the controller 82 | # objects created by `Newman::Application#call`. This allows 83 | # an application object to provide extensions for use within its 84 | # callbacks, as in the example shown below. 85 | # 86 | # module ListLoader 87 | # def load_list(name) 88 | # store = Newman::Store.new(settings.application.list_db) 89 | # Newman::MailingList.new(name, store) 90 | # end 91 | # end 92 | # 93 | # list_app = Newman::Application.new do 94 | # use ListLoader 95 | # 96 | # match :list_id, "[^.]+" 97 | # 98 | # to(:tag, "{list_id}.subscribe") do 99 | # list = load_list(params[:list_id]) 100 | # 101 | # if list.subscriber?(sender) 102 | # # send a failure message 103 | # else 104 | # # susbcribe the user and send a welcome message 105 | # end 106 | # end 107 | # end 108 | # 109 | # This method is mainly meant to be used with pre-packaged extensions or 110 | # more complicated forms of callback helpers. This example was just shown 111 | # for the sake of its simplicity, but for similar use cases it would 112 | # actually be better to use `Newman::Application#helpers` 113 | 114 | def use(extension) 115 | extensions << extension 116 | end 117 | 118 | # --- 119 | 120 | # `Newman::Application#helpers` is used to build simple controller 121 | # extensions, and is mostly just syntactic sugar. For example, 122 | # rather than using an explicit 123 | # module, the example shown in the `Newman::Application#use` documentation 124 | # can be rewritten as follows: 125 | # 126 | # list_app = Newman::Application.new do 127 | # helpers do 128 | # def load_list(name) 129 | # store = Newman::Store.new(settings.application.list_db) 130 | # Newman::MailingList.new(name, store) 131 | # end 132 | # end 133 | # 134 | # match :list_id, "[^.]+" 135 | # 136 | # to(:tag, "{list_id}.subscribe") do 137 | # list = load_list(params[:list_id]) 138 | # 139 | # if list.subscriber?(sender) 140 | # # send a failure message 141 | # else 142 | # # susbcribe the user and send a welcome message 143 | # end 144 | # end 145 | # end 146 | # 147 | # It's important to note that for any controller extensions that might be 148 | # reusable, or for more complicated logic, `Newman::Application#use` is 149 | # probably a better tool to use. 150 | 151 | def helpers(&block) 152 | use Module.new(&block) 153 | end 154 | 155 | # --- 156 | 157 | # `Newman::Application#match` is used to define patterns which are used for 158 | # extracting callback parameters. An example is shown below: 159 | # 160 | # jester = Newman::Application.new do 161 | # match :genre, '\S+' 162 | # match :title, '.*' 163 | # 164 | # subject(:match, "a {genre} story '{title}'") do 165 | # story_library.add_story(:genre => params[:genre], 166 | # :title => params[:title], 167 | # :body => request.body.to_s) 168 | # 169 | # respond :subject => "Jester saved '#{params[:title]}'" 170 | # end 171 | # end 172 | # 173 | # Because Newman's built in filters are designed to escape regular 174 | # expression syntax by default, `Newman::Application#match` provides the 175 | # only high-level mechanism for dynamic filter matches. Low level matching 176 | # is possible via `Newman::Application#callback`, but would 177 | # be an exercise in tedium for most application developers. 178 | # 179 | # NOTE: `Newman::Application#match` converts the provided `name` to a 180 | # string using `to_s` to make life easier for the internals. Because 181 | # `Newman::Application#matchers` is an implementation detail, you probably 182 | # don't need to worry about this unless you're hacking on Newman itself. 183 | def match(name, pattern) 184 | matchers[name.to_s] = pattern 185 | end 186 | 187 | # --- 188 | 189 | # `Newman::Application#callback` is a low level feature for defining custom 190 | # callbacks. For ideas on how to roll your own filters with it, see the 191 | # implementation of the `Newman::Filters` module. 192 | 193 | def callback(action, filter) 194 | callbacks << { :filter => filter, 195 | :action => action } 196 | end 197 | 198 | # --- 199 | 200 | # `Newman::Application#compile_regex` is used for converting pattern strings 201 | # into a regular expression string suitable for use in filters. This method is a 202 | # low-level feature, and is not meant for use by application developers. See 203 | # the `Newman::Filters` module for how to use it to build your own callback 204 | # filters. 205 | 206 | def compile_regex(pattern) 207 | Regexp.escape(pattern) 208 | .gsub(/\\{(.*?)\\}/) { |m| "(?<#{$1}>#{matchers[$1]})" } 209 | end 210 | 211 | # --- 212 | 213 | # **NOTE: Methods below this point in the file are implementation 214 | # details, and should not be depended upon.** 215 | 216 | private 217 | 218 | # --- 219 | 220 | # `Newman::Application#trigger_callbacks` runs a two step process: 221 | # 222 | # 1) It grabs the `filter` Proc for each callback and executes it, passing in 223 | # the provided `controller`. Any `filter` proc that returns a logically true 224 | # value is selected to be run. 225 | # 226 | # 2) If the selection of `matched_callbacks` is empty, it executes the default 227 | # callback in the context of a controller object. Otherwise, it runs each 228 | # callback in sequence, in the context of a controller object. 229 | 230 | def trigger_callbacks(controller) 231 | match_data = {} 232 | 233 | matched_callbacks = callbacks.select do |e| 234 | filter = e[:filter] 235 | match_data[e] = filter.call(controller) 236 | end 237 | 238 | if matched_callbacks.empty? 239 | controller.instance_exec(&default_callback) 240 | else 241 | matched_callbacks.each do |e| 242 | action = e[:action] 243 | controller.params = match_data[e] 244 | controller.instance_exec(&action) 245 | end 246 | end 247 | end 248 | 249 | # --- 250 | 251 | # These accessors have been made private to reflect the fact that 252 | # `Newman::Application` is meant to be customized via its high level methods 253 | # such as `use`, `helpers`, `match`, and `callback`. Any extensions of 254 | # `Newman::Application` should rely on those methods and not directly 255 | # reference these fields at all. If there is some functionality missing 256 | # that needs to be added to the public API for you to build your 257 | # extension, just let us know. 258 | 259 | attr_accessor :callbacks, :default_callback, :matchers, :extensions 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /lib/newman/controller.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Controller` provides a context for application callbacks to run in, 2 | # and provides most of the core functionality for preparing a response email. 3 | # 4 | # For a full example of `Newman::Controller` in action, be sure to check out 5 | # [Jester](https://github.com/mendicant-university/jester). 6 | # 7 | # `Newman::Controller` is part of Newman's **external interface**. 8 | 9 | module Newman 10 | class Controller 11 | 12 | #--- 13 | 14 | # A `Newman::Controller` object is initialized with parameters that match 15 | # what is provided by the low level `Newman::Server` object. Generally 16 | # speaking, you won't instantiate controller objects yourself, but instead 17 | # will rely on `Newman::Application` to instantiate them for you. 18 | 19 | def initialize(params) 20 | self.settings = params.fetch(:settings) 21 | self.request = params.fetch(:request) 22 | self.response = params.fetch(:response) 23 | self.logger = params.fetch(:logger) 24 | end 25 | 26 | # --- 27 | 28 | # All of the fields on `Newman::Controller` are public, so that they can 29 | # freely be manipulated by callbacks. We may lock this down a bit more 30 | # in a future version of Newman once we figure out what data actually needs 31 | # to be exposed in callbacks, but for now you can feel free to depend on 32 | # any of these fields. 33 | 34 | attr_accessor :settings, :request, :response, :logger, :params 35 | 36 | 37 | # --- 38 | 39 | # `Newman::Controller#respond` is used to modify the response email object, 40 | # and is used in the manner shown below: 41 | # 42 | # respond :subject => "Hello There", 43 | # :body => "It's nice to meet you, pal!" 44 | # 45 | # Because this method simply provides syntactic sugar on top of the 46 | # `Mail::Message` object's interface, you should be sure to take a look at 47 | # the documentation for the [mail gem](http://github.com/mikel/mail) to 48 | # discover what options are available. 49 | 50 | def respond(params) 51 | params.each { |k,v| response.send("#{k}=", v) } 52 | end 53 | 54 | # --- 55 | 56 | # `Newman::Controller#template` is used to invoke a template file within the 57 | # context of the current controller object using Tilt. A `name` for the template is 58 | # provided and then looked up in the directory referenced by 59 | # `settings.service.templates_dir`. If a `locals` hash is provided, these 60 | # local variables will be made available in templates. 61 | # 62 | # **NOTE: This is feature is one we haven't adequately used in Newman, 63 | # if you have trouble with it, please let us know via our [issue 64 | # tracker](http://github.com/mendicant-university/newman/issues).** 65 | # 66 | def template(name,locals={}) 67 | Tilt.new(Dir.glob("#{settings.service.templates_dir}/#{name}.*").first) 68 | .render(self,locals) 69 | end 70 | 71 | # --- 72 | 73 | # `Newman::Controller#skip_response` is used for disabling the delivery of 74 | # the response email. Use this for situations where no response is required, 75 | # such as when a spam email or a bounce has been detected, or if you are 76 | # building an application which simply passively monitors incoming email 77 | # rather than replying to it. 78 | 79 | def skip_response 80 | response.perform_deliveries = false 81 | end 82 | 83 | # --- 84 | 85 | # `Newman::Controller#forward_message` works in a similar fashion to 86 | # `Newman::Controller#response`, but copies the request FROM, SUBJECT 87 | # and BODY fields and sets the REPLY TO field to be equal to 88 | # `settings.service.default_sender`. This feature is convenient for 89 | # implementing mailing-list style functionality, such as in the 90 | # following example: 91 | # 92 | # if list.subscriber?(sender) 93 | # forward_message :bcc => list.subscribers.join(", ") 94 | # else 95 | # respond :subject => "You are not subscribed", 96 | # :body => template("non-subscriber-error") 97 | # end 98 | 99 | def forward_message(params={}) 100 | response.from = request.from 101 | response.reply_to = settings.service.default_sender 102 | response.subject = request.subject 103 | 104 | params.each do |k,v| 105 | response.send("#{k}=", v) 106 | end 107 | 108 | if request.multipart? 109 | response.text_part = request.text_part 110 | response.html_part = request.html_part 111 | else 112 | response.body = request.body.to_s 113 | end 114 | end 115 | 116 | # --- 117 | 118 | # `Newman::Controller#sender` is used as a convenient shortcut for 119 | # retrieving the sender's email address from the request object. 120 | 121 | def sender 122 | request.from.first.to_s 123 | end 124 | 125 | # --- 126 | 127 | # `Newman::Controller#domain` is used as a convenient shortcut for 128 | # referencing `settings.service.domain`. 129 | 130 | def domain 131 | settings.service.domain 132 | end 133 | 134 | 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/newman/email_logger.rb: -------------------------------------------------------------------------------- 1 | # `Newman::EmailLogger` provides rudimentary logging support for email objects, 2 | # and primarily exists to support the `Newman::RequestLogger` and 3 | # `Newman::ResponseLogger` objects. 4 | # 5 | # If you are only interested in making use of this logging functionality and not 6 | # extending it or changing it in some way, you do not need to be familiar with 7 | # the code in this file. Just be sure to note that if you add 8 | # `service.debug_mode = true` to your configuration file, or set the Ruby 9 | # `$DEBUG` global variable, you will get much more verbose output from 10 | # Newman's logging system. 11 | # 12 | # `Newman::EmailLogger` is part of Newman's **internal interface**. 13 | 14 | module Newman 15 | module EmailLogger 16 | 17 | # --- 18 | 19 | # `Newman::EmailLogger#log_email` takes a logger object, a prefix, and a `Mail` object and 20 | # then outputs relevant debugging details. 21 | # 22 | # This method always at least provides a summary of the provided `email` 23 | # at the `INFO` level When in debugging mode, the full contents of 24 | # the `email` will also gets logged. 25 | # 26 | # The main purpose of this method is to be used by `Newman::RequestLogger` 27 | # and `Newman::ResponseLogger`, but may also optionally be used as a helper 28 | # for those who are rolling their own logging functionality. 29 | 30 | def log_email(logger, prefix, email) 31 | logger.debug(prefix) { "\n#{email}" } 32 | logger.info(prefix) { email_summary(email) } 33 | end 34 | 35 | # --- 36 | 37 | # **NOTE: Methods below this point in the file are implementation details, 38 | # and should not be depended upon** 39 | 40 | private 41 | 42 | # --- 43 | 44 | # `Newman::EmailLogger#email_summary` returns a hash with a summary of the provided `email` object. 45 | 46 | def email_summary(email) 47 | { :from => email.from, 48 | :to => email.to, 49 | :bcc => email.bcc, 50 | :subject => email.subject, 51 | :reply_to => email.reply_to } 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/newman/filters.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Filters` provides the standard filtering mechanisms for 2 | # Newman applications. 3 | # 4 | # Unless you are building a server-side extension for 5 | # Newman, you probably only need to be familiar with how these filter methods 6 | # are used and can treat their implementation details as a black box. 7 | # 8 | # `Newman::Filters` is part of Newman's **external interface**. 9 | 10 | module Newman 11 | module Filters 12 | 13 | # --- 14 | 15 | # `Newman::Filters#to` takes a `filter_type`, a `pattern`, 16 | # and an `action` and then registers a callback which gets run for 17 | # each new request the application handles. If the filter matches the 18 | # incoming message, the `action` block gets run in the context of 19 | # a `Newman::Controller` object. Otherwise, the `action` block 20 | # does not get run at all. 21 | # 22 | # Currently, the only supported `filter_type` is `:tag`, which leverages the 23 | # `+` extension syntax for email addresses to filter out emails with certain 24 | # tags in their TO field. For example, we could build a filter that responds 25 | # to messages sent to `USERNAME+ping@HOST` using the following filter 26 | # setup: 27 | # 28 | # to(:tag, "ping") do 29 | # respond(:subject => "pong") 30 | # end 31 | # 32 | # Because this method runs the `pattern` through 33 | # `Newman::Application#compile_regex`, it can also be used in 34 | # combination with `Newman::Application#match` to do more 35 | # complex matching. For example, the code below could be used to support 36 | # complex TO field mappings, such as `USERNAME+somelist.subscribe@HOST`: 37 | # 38 | # match :list_id, "[^.]+" 39 | # 40 | # to(:tag, "{list_id}.subscribe") do 41 | # list = load_list(params[:list_id]) 42 | # 43 | # if list.subscriber?(sender) 44 | # # send failure email, already subscribed 45 | # else 46 | # # add user to list and send success email 47 | # end 48 | # end 49 | # 50 | # Note that currently everything before the `+` in the email address is 51 | # ignored, and that the domain is hardcoded to match the 52 | # `Controller#domain`, which currently directly references the 53 | # `service.domain` setting. It'd be nice to make this a bit more 54 | # flexible and also support other filter types such as a match against the 55 | # whole email address at some point in the future. 56 | 57 | def to(filter_type, pattern, &action) 58 | raise NotImplementedError unless filter_type == :tag 59 | 60 | regex = compile_regex(pattern) 61 | 62 | callback action, ->(controller) { 63 | controller.request.to.each do |e| 64 | md = e.match(/\+#{regex}@#{Regexp.escape(controller.domain)}/) 65 | return md if md 66 | end 67 | 68 | false 69 | } 70 | end 71 | 72 | # --- 73 | 74 | # `Newman::Filters#subject` takes a `filter_type`, 75 | # a `pattern`, and an `action` and 76 | # then registers a callback which gets run for each new request the 77 | # application handles. If the filter matches the incoming message, the 78 | # `action` block gets run in the context of a `Newman::Controller` object. 79 | # Otherwise, the `action` block does not get run at all. 80 | # 81 | # Currently, the only supported `filter_type` is `:match`, which matches the 82 | # pattern against the full SUBJECT field. This can be used for simple 83 | # subject based filtering, such as the code shown below: 84 | # 85 | # subject(:match, "what stories do you know?") do 86 | # respond :subject => "All of Jester's stories", 87 | # :body => story_library.map { |e| e.title }.join("\n") 88 | # end 89 | # 90 | # Because this method runs the `pattern` through 91 | # `Newman::Application#compile_regex`, it can also be used in 92 | # combination with `Newman::Application#match` to do more 93 | # complex matching, such as in the following example: 94 | # 95 | # match :genre, '\S+' 96 | # match :title, '.*' 97 | # 98 | # subject(:match, "a {genre} story '{title}'") do 99 | # story_library.add_story(:genre => params[:genre], 100 | # :title => params[:title], 101 | # :body => request.body.to_s) 102 | # 103 | # respond :subject => "Jester saved '#{params[:title]}'" 104 | # end 105 | # 106 | # It'd be nice to support more kinds of matching strategies at some point in 107 | # the future, so be sure to let us know if you have ideas. 108 | 109 | def subject(filter_type, pattern, &action) 110 | raise NotImplementedError unless filter_type == :match 111 | 112 | regex = compile_regex(pattern) 113 | 114 | callback action, ->(controller) { 115 | subject = controller.request.subject 116 | 117 | return false unless subject 118 | 119 | subject.match(/#{regex}/) || false 120 | } 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/newman/mailer.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Mailer` allows you to easily receive mail via IMAP and send mail via 2 | # SMTP, and is the default mailing strategy used by `Newman::Server.simple`. 3 | # This class mostly exists to serve as an adapter that bridges the gap between 4 | # the mail gem and Newman's configuration system. 5 | # 6 | # `Newman::Mailer`'s interface is minimal by design so that other objects 7 | # can easily stand in for it as long as they respond to the same set of 8 | # messages. Be sure to see `Newman::TestMailer` for an example of how to build a 9 | # custom object that can be used in place of a `Newman::Mailer` object. 10 | # 11 | # 12 | # `Newman::Mailer` is part of Newman's **internal interface**. 13 | 14 | module Newman 15 | class Mailer 16 | 17 | # --- 18 | 19 | # To initialize a `Newman::Mailer` object, a settings object must be 20 | # provided, i.e: 21 | # 22 | # settings = Newman::Settings.from_file('config/environment.rb') 23 | # mailer = Newman::Mailer.new(settings) 24 | # 25 | # This is done automatically for you by `Newman::Server.simple`, but must be 26 | # done manually if you are creating a `Newman::Server` instance from 27 | # scratch. 28 | # 29 | # Currently, not all of the settings supported by the mail gem are mapped by 30 | # Newman. This is by design, to limit the amount of configuration options 31 | # need to think about. However, if this is causing you a problem, 32 | # please [file an issue](https://github.com/mendicant-university/newman/issues). 33 | 34 | def initialize(settings) 35 | imap = settings.imap 36 | smtp = settings.smtp 37 | 38 | self.retriever_settings = { 39 | :address => imap.address, 40 | :user_name => imap.user, 41 | :password => imap.password, 42 | :enable_ssl => imap.ssl_enabled || false, 43 | :port => imap.port 44 | } 45 | 46 | self.delivery_settings = { 47 | :address => smtp.address, 48 | :user_name => smtp.user, 49 | :password => smtp.password, 50 | :authentication => :plain, 51 | :enable_starttls_auto => smtp.starttls_enabled || false, 52 | :port => smtp.port 53 | } 54 | end 55 | 56 | # --- 57 | 58 | # `Newman::Mailer#messages` is used to retrieve all messages currently in the inbox 59 | # and then delete them from the server. This method returns an array of 60 | # `Mail::Message` objects if any messages were found, and returns 61 | # an empty array otherwise. 62 | 63 | def messages 64 | Mail::IMAP.new(retriever_settings).all(:delete_after_find => true) 65 | end 66 | 67 | # --- 68 | 69 | # `Newman::Mailer#new_message` is used to construct a new `Mail::Message` object, 70 | # with the delivery settings that were set up at initialization time. 71 | # This method passes all its arguments on to `Mail.new`, so be sure 72 | # to refer to the [mail gem's documentation](http://github.com/mikel/mail) 73 | # for details. 74 | # 75 | def new_message(*a, &b) 76 | msg = Mail.new(*a, &b) 77 | msg.delivery_method(:smtp, delivery_settings) 78 | 79 | msg 80 | end 81 | 82 | # --- 83 | 84 | # `Newman::Mailer#deliver_message` is used to construct and immediately deliver a 85 | # message using the delivery settings that were set up at initialization 86 | # time. 87 | 88 | def deliver_message(*a, &b) 89 | new_message(*a, &b).deliver 90 | end 91 | 92 | # --- 93 | 94 | # **NOTE: Methods below this point in the file are implementation 95 | # details, and should not be depended upon** 96 | 97 | private 98 | 99 | # --- 100 | 101 | # These accessors have been made private to reflect the fact that 102 | # `Newman::Mailer` objects are meant to be treated as immutable constructs 103 | # once they are created. 104 | 105 | attr_accessor :retriever_settings, :delivery_settings 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/newman/mailing_list.rb: -------------------------------------------------------------------------------- 1 | # `Newman::MailingList` implements a simple mechanism for storing lists of email 2 | # addresses keyed by a mailing list name. 3 | # 4 | # This object is meant to be used in conjunction with a 5 | # `Newman::Store` object which is `PStore` backed, but would fairly easily map to 6 | # arbitrary data stores via adapter objects. 7 | # 8 | # `Newman::MailingList` is part of Newman's **external interface**. 9 | 10 | module Newman 11 | class MailingList 12 | 13 | # --- 14 | 15 | # To initialize a `Newman::MailingList` object, a list name and a store object must 16 | # be provided, i.e: 17 | # 18 | # store = Newman::Store.new('simple.store') 19 | # mailing_list = Newman::MailingList.new("simple_list", store) 20 | 21 | def initialize(name, store) 22 | self.name = name 23 | self.store = store 24 | end 25 | 26 | # --- 27 | 28 | # `Newman::MailingList#subscribe` is used to add subscribers to 29 | # the mailing list, i.e. 30 | # 31 | # mailing_list.subscribe('gregory.t.brown@gmail.com') 32 | # 33 | # If the provided email address is for a new subscriber, a new record gets 34 | # created for that subscriber, adding them to the list. Otherwise, this 35 | # method does not modify the mailing list. 36 | # 37 | # Returns true if list was modified, returns false otherwise. 38 | 39 | def subscribe(email) 40 | return false if subscriber?(email) 41 | 42 | store[name].create(email) 43 | 44 | true 45 | end 46 | 47 | # --- 48 | 49 | # `Newman::MailingList#unsubscribe` is used to remove subscribers from 50 | # the mailing list, i.e. 51 | # 52 | # mailing_list.unsubscribe('gregory.t.brown@gmail.com') 53 | # 54 | # If the provided email address is for an existing subscriber, the record 55 | # for that subscriber is destroyed, removing them from the list. 56 | # Otherwise, this method does not modify the mailing list. 57 | # 58 | # Returns true if list was modified, returns false otherwise. 59 | 60 | def unsubscribe(email) 61 | return false unless subscriber?(email) 62 | 63 | record = store[name].find { |e| e.contents == email } 64 | store[name].destroy(record.id) 65 | 66 | true 67 | end 68 | 69 | 70 | # --- 71 | 72 | # `Newman::MailingList#subscriber?` is used to check if a given email address 73 | # is on the list, i.e. 74 | # 75 | # mailing_list.subscriber?('gregory.t.brown@gmail.com') 76 | # 77 | # Returns true if a record is found which matches the given email address, 78 | # returns false otherwise. 79 | 80 | def subscriber?(email) 81 | store[name].any? { |r| r.contents == email } 82 | end 83 | 84 | # --- 85 | 86 | # `Newman::MailingList#subscribers` is used to access all email addresses for 87 | # the mailing list's subscribers, i.e: 88 | # 89 | # members = mailing_list.subscribers 90 | # 91 | # Returns an array of email addresses. 92 | 93 | def subscribers 94 | store[name].map { |r| r.contents } 95 | end 96 | 97 | # --- 98 | 99 | # **NOTE: Methods below this point in the file are implementation 100 | # details, and should not be depended upon.** 101 | 102 | private 103 | 104 | # --- 105 | 106 | # These accessors have been made private to reflect the fact that 107 | # `Newman::MailingList` objects are meant to point to a single 108 | # named list within a single data store once they are created. 109 | 110 | attr_accessor :name, :store 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/newman/recorder.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Recorder` provides a simple mechanism for storing non-relational 2 | # records within a `Newman::Store` with autoincrementing identifiers. It 3 | # supports basic CRUD operations, and also acts as an `Enumerable` object. 4 | # 5 | # For an example of how to make use of `Newman::Recorder` to implement arbitrary 6 | # persistent models, be sure to check out the implementation of the 7 | # `Newman::MailingList` object. 8 | # 9 | # `Newman::Recorder` is part of Newman's **external interface**. 10 | 11 | module Newman 12 | Record = Struct.new(:column, :id, :contents) 13 | 14 | class Recorder 15 | include Enumerable 16 | 17 | # --- 18 | 19 | # To initialize a `Newman::Recorder` object, a `column` key 20 | # and `store` object must be provided, i.e. 21 | # 22 | # store = Newman::Store.new("sample.store") 23 | # recorder = Newman::Recorder.new(:subscribers, store) 24 | # 25 | # However, in most cases you should not instantiate a 26 | # `Newman::Recorder` directly, and instead should make use of 27 | # `Newman::Store#[]` which is syntactic sugar for the same operation. 28 | # 29 | # The first time a particular `column` key is referenced, two mapping 30 | # is created for the column in the underlying data store: one which 31 | # keeps track of the autoincrementing ids, and one that keeps track 32 | # of the data stored within the column. It's fine to treat these 33 | # mappings as implementation details, but we treat them as part of Newman's 34 | # external interface because backwards-incompatible changes to them will 35 | # result in possible data store corruption. 36 | 37 | def initialize(column, store) 38 | self.column = column 39 | self.store = store 40 | 41 | store.write do |data| 42 | data[:identifiers][column] ||= 0 43 | data[:columns][column] ||= {} 44 | end 45 | end 46 | 47 | # --- 48 | 49 | # `Newman::Recorder#each` iterates over all records stored in the column, 50 | # yielding a `Newman::Record` object for each one. Because `Enumerable` is 51 | # mixed into `Newman::Recorder`, all enumerable methods that get called on a 52 | # recorder object end up making calls to this method. 53 | def each 54 | store.read do |data| 55 | data[:columns][column].each do |id, contents| 56 | yield(Record.new(column, id, contents)) 57 | end 58 | end 59 | end 60 | 61 | # --- 62 | 63 | # `Newman::Recorder#create` store an arbitrary Ruby object in the data 64 | # store and returns a `Newman::Record` object which has fields for the 65 | # `column` key, record `id`, and record `contents`. This method 66 | # automatically generates new ids, starting with `id=1` for the 67 | # first record and then incrementing sequentially. 68 | 69 | def create(contents) 70 | store.write do |data| 71 | id = (data[:identifiers][column] += 1) 72 | 73 | data[:columns][column][id] = contents 74 | 75 | Record.new(column, id, contents) 76 | end 77 | end 78 | 79 | # --- 80 | 81 | # `Newman::Recorder#read` looks up a record by `id` and returns a 82 | # `Newman::Record` object. 83 | 84 | def read(id) 85 | store.read do |data| 86 | Record.new(column, id, data[:columns][column][id]) 87 | end 88 | end 89 | 90 | # --- 91 | 92 | # `Newman::Recorder#update` looks up a record by `id` and yields its 93 | # contents. The record contents are then replaced with the 94 | # return value of the provided block. 95 | 96 | def update(id) 97 | store.write do |data| 98 | data[:columns][column][id] = yield(data[:columns][column][id]) 99 | 100 | Record.new(column, id, data[:columns][column][id]) 101 | end 102 | end 103 | 104 | # --- 105 | 106 | # `Newman::Recorder#destroy` looks up a record by `id` and then removes it 107 | # from the data store. This method returns `true` whether or not a record 108 | # was actually destroyed, which is a somewhat useless behavior and may 109 | # need to be fixed in a future version of Newman. Patches welcome! 110 | 111 | def destroy(id) 112 | store.write do |data| 113 | data[:columns][column].delete(id) 114 | end 115 | 116 | true 117 | end 118 | 119 | # --- 120 | 121 | # **NOTE: Methods below this point in the file are implementation 122 | # details, and should not be depended upon** 123 | private 124 | 125 | # --- 126 | 127 | # These accessors have been made private to reflect the fac that 128 | # `Newman::Recorder` objects are meant to point to a single column within a 129 | # single data store once created. 130 | attr_accessor :column, :store 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/newman/request_logger.rb: -------------------------------------------------------------------------------- 1 | # `Newman::RequestLogger` implements rudimentary request logging functionality, 2 | # which is enabled by default when `Newman::Server.simple` is used to execute 3 | # your applications. 4 | # 5 | # If you are only interested in making use of this logging functionality and not 6 | # extending it or changing it in some way, you do not need to be familiar with 7 | # the code in this file. Just be sure to note that if you add 8 | # `service.debug_mode = true` to your configuration file, or set the Ruby 9 | # `$DEBUG` global variable, you will get much more verbose output from 10 | # Newman's logging system. 11 | # 12 | # `Newman::RequestLogger` is part of Newman's **internal interface**. 13 | 14 | module Newman 15 | RequestLogger = Object.new 16 | 17 | # --- 18 | 19 | # `Newman::RequestLogger` is implemented as a singleton object and is 20 | # completely stateless in nature. It can be added directly as an app to 21 | # any `Newman::Server` instance. The `Newman::Server.simple` helper method 22 | # automatically places a `Newman::RequestLogger` at the beginning of the call chain, 23 | # but it can be inserted at any point and will output the request email 24 | # object at that point in the call chain. 25 | 26 | class << RequestLogger 27 | include EmailLogger 28 | 29 | # --- 30 | 31 | # `Newman::RequestLogger#call` simply delegates to 32 | # `Newman::EmailLogger#log_email`, passing it a logger instance, the 33 | # `"REQUEST"` prefix for the log line, and an instance 34 | # of an email object. See `Newman::Server.tick` and `Newman::EmailLogger#log_email` 35 | # for details. 36 | 37 | def call(params) 38 | log_email(params[:logger], "REQUEST", params[:request]) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/newman/response_logger.rb: -------------------------------------------------------------------------------- 1 | # `Newman::ResponseLogger` supports rudimentary response logging functionality, which is 2 | # enabled by default when `Newman::Server.simple` is used to execute your 3 | # applications. 4 | # 5 | # If you are only interested in making use of this logging functionality and not 6 | # extending it or changing it in some way, you do not need to be familiar with 7 | # the code in this file. Just be sure to note that if you add 8 | # `service.debug_mode = true` to your configuration file, or set the Ruby 9 | # `$DEBUG` global variable, you will get much more verbose output from 10 | # Newman's logging system. 11 | # 12 | # `Newman::ResponseLogger` is part of Newman's **internal interface**. 13 | 14 | module Newman 15 | ResponseLogger = Object.new 16 | 17 | # --- 18 | 19 | # `Newman::ResponseLogger` is implemented as a singleton object and is 20 | # completely stateless in nature. It can be added directly as an app to 21 | # any `Newman::Server` instance. The `Newman::Server.simple` helper method 22 | # automatically places a `ResponseLogger` at the end of the call chain, but 23 | # it can be inserted at any point and will output the response email object 24 | # at that point in the call chain. 25 | 26 | class << ResponseLogger 27 | include EmailLogger 28 | 29 | # --- 30 | 31 | # `Newman::ResponseLogger#call` simply delegates to 32 | # `EmailLogger#log_email`, passing it a logger instance, the 33 | # `"RESPONSE"` prefix for the log line, and an instance of an email 34 | # object. See `Newman::Server.tick` and `Newman::EmailLogger#log_email` 35 | # for details. 36 | 37 | def call(params) 38 | log_email(params[:logger], "RESPONSE", params[:response]) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/newman/server.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Server` takes incoming mesages from a mailer object and passes them 2 | # to applications as a request, and then delivers a response email after the 3 | # applications have modified it. 4 | # 5 | # A `Newman::Server` object can be used in four distinct ways: 6 | # 7 | # 1) Instantiated via `Newman::Server.test_mode` and then run tick by tick 8 | # in integration tests. 9 | # 10 | # 2) Instantiated via `Newman::Server.simple` which immediately executes 11 | # an infinite polling loop. 12 | # 13 | # 3) Instantiated via `Newman::Server.simple!`, and then run manually (either 14 | # in a loop or by tick), using the same defaults used by `Newman::Server.simple` 15 | # 16 | # 4) Instantiated explicitly and manually configured, for maximum control. 17 | # 18 | # All of these different workflows are supported, but if you are simply looking 19 | # to build applications with `Newman`, you are most likely going to end up using 20 | # either `simple()` or `simple!()` because they care of most of the setup work 21 | # for you. 22 | # 23 | # `Newman::Server` is part of Newman's **external interface**. 24 | 25 | module Newman 26 | class Server 27 | # --- 28 | 29 | # `Newman::Server.simple!` automatically generates a `Newman::Mailer` object 30 | # and `Newman::Settings` object from the provided `settings_file`. These 31 | # objects are then passed on to `Newman::Server.new` and a server instance 32 | # is created. 33 | # 34 | # The following example demonstrates how to use this method: 35 | # 36 | # ping_pong = Newman::Application.new do 37 | # subject(:match, "ping") do 38 | # respond(:subject => "pong") 39 | # end 40 | # 41 | # default do 42 | # respond(:subject => "You missed the ball!") 43 | # end 44 | # end 45 | # 46 | # s = Newman::Server.simple!(ping_pong, "config/environment.rb") 47 | # # call s.tick or s.run at some later point. 48 | # 49 | # Given a proper configuration file, this will make it possible to easily 50 | # get your applications up and running with simple request and response 51 | # logging enabled. 52 | def self.simple!(app, settings_file) 53 | settings = Settings.from_file(settings_file) 54 | mailer = Mailer.new(settings) 55 | server = new(settings, mailer) 56 | server.apps = [RequestLogger, app, ResponseLogger] 57 | 58 | server 59 | end 60 | 61 | # --- 62 | 63 | # `Newman::Server#simple` is the same as `Newman::Server#simple!`, but 64 | # automatically starts an infinite polling loop. 65 | 66 | def self.simple(app, settings_file) 67 | server = simple!(app, settings_file) 68 | server.run 69 | end 70 | 71 | # --- 72 | 73 | # `Newman::Server.test_mode` automatically generates a `Newman::TestMailer` object 74 | # and `Newman::Settings` object from the provided `settings_file`. These 75 | # objects are then passed on to `Newman::Server.new` and a server instance 76 | # which is preconfigured for use in integration testing is returned. 77 | # 78 | # Using the application from the `Newman::Server.simple!` documentation 79 | # above, it'd be possible to write a simple integration test using this 80 | # method in the following way: 81 | # 82 | # server = Newman::Server.test_mode("config/environment.rb") 83 | # server.apps << ping_pong 84 | # 85 | # mailer = server.mailer 86 | # mailer.deliver_message(:to => "test@test.com", 87 | # :subject => "ping) 88 | # 89 | # server.tick 90 | # 91 | # mailer.messages.first.subject.must_equal("pong") 92 | # 93 | # It's worth mentioning that although `Newman::Server.test_mode` is part of 94 | # Newman's external interface, the `Newman::TestMailer` object is considered part 95 | # of its internals. This is due to some ugly issues with global state and 96 | # the overall brittleness of the current implementation. Expect a bit of 97 | # weirdness if you plan to use this feature, at least until we improve upon 98 | # it. 99 | 100 | def self.test_mode(settings_file) 101 | settings = Settings.from_file(settings_file) 102 | mailer = TestMailer.new(settings) 103 | 104 | new(settings, mailer) 105 | end 106 | 107 | # --- 108 | 109 | # To initialize a `Newman::Server` object, a settings object and mailer object must 110 | # be provided. 111 | # 112 | # Instantiating a server object directly can be useful for building live 113 | # integration tests, or for building cron jobs which process email 114 | # periodically rather than in a busy-wait loop. See one of Newman's [live 115 | # tests](https://github.com/mendicant-university/newman/blob/master/examples/live_test.rb) 116 | # for an example of how this approach works. 117 | 118 | def initialize(settings, mailer) 119 | self.settings = settings 120 | self.mailer = mailer 121 | self.apps = [] 122 | end 123 | 124 | # --- 125 | 126 | # These accessors are mostly meant for use with server objects under test 127 | # mode, or server objects that have been explicitly instantiated. If you are 128 | # using `Newman::Server.simple` to run your apps, it's safe to treat these 129 | # as an implementation detail; all important data will get passed down 130 | # into your apps on each `tick`. 131 | 132 | attr_accessor :settings, :mailer, :apps 133 | attr_writer :logger 134 | 135 | # --- 136 | 137 | # Returns the logger object that was set via `Newman::Server#logger=`, 138 | # or delegates to `default_logger` if no custom logger was provided. 139 | # 140 | def logger 141 | @logger || default_logger 142 | end 143 | 144 | # --- 145 | 146 | # `Newman::Server.run` kicks off a busy wait loop, alternating between 147 | # calling `Newman::Server.tick` and sleeping for the amount of time 148 | # specified by `settings.service.polling_interval`. We originally planned to 149 | # use an EventMachine periodic timer here to potentially make running 150 | # several servers within a single process easier, but had trouble coming up 151 | # with a use case that made the extra dependency worth it. 152 | 153 | def run 154 | loop do 155 | tick 156 | sleep settings.service.polling_interval 157 | end 158 | end 159 | 160 | # --- 161 | 162 | # `Newman::Server.tick` runs the following sequence for each incoming 163 | # request. 164 | # 165 | # 1) A response is generated with the TO field set to the FROM field of the 166 | # request, and the FROM field set to `settings.service.default_sender`. 167 | # Applications can change these values later, but these are sensible 168 | # defaults that work for most common needs. 169 | # 170 | # 2) The list of `apps` is iterated over sequentially, and each 171 | # application's `call` method is invoked with a parameters hash which 172 | # include the `request` email, the `response` email, the `settings` object 173 | # being used by the server, and the `logger` object being used by the 174 | # server. 175 | # 176 | # 2a) If any application raises an exception, that exception is caught and 177 | # the processing of the current request is halted. Details about the failure 178 | # are logged and if `settings.service.raise_exceptions` is enabled, the 179 | # exception is re-raised, typically taking the server down with it. This 180 | # setting is off by default. 181 | # 182 | # 2b) If there are any server errors (such as an error retrieving messages 183 | # via IMAP), those errors are logged and re-raised, taking the server 184 | # down. Currently, you should use a process watcher to restart 185 | # Newman to protect against such failures, but be careful about restarting 186 | # without knowing what went wrong! 187 | # 188 | # 3) Assuming an exception is not encountered, the response is delivered. 189 | 190 | def tick 191 | mailer.messages.each do |request| 192 | response = mailer.new_message(:to => request.from, 193 | :from => settings.service.default_sender) 194 | 195 | process_request(request, response) && response.deliver 196 | end 197 | rescue StandardError => e 198 | logger.fatal("SERVER ERROR") { "#{e.inspect}\n" + e.backtrace.join("\n ") } 199 | raise 200 | end 201 | 202 | # --- 203 | 204 | # **NOTE: Methods below this point in the file are implementation 205 | # details, and should not be depended upon** 206 | 207 | private 208 | 209 | # --- 210 | 211 | # Implementation details for `Newman::Server#tick`. Check its documentation 212 | # for details. 213 | 214 | def process_request(request, response) 215 | apps.each do |app| 216 | app.call(:request => request, 217 | :response => response, 218 | :settings => settings, 219 | :logger => logger) 220 | end 221 | 222 | return true 223 | rescue StandardError => e 224 | if settings.service.raise_exceptions 225 | raise 226 | else 227 | logger.info("APP ERROR") { e.inspect } 228 | logger.debug("APP ERROR") { "#{e.inspect}\n" + e.backtrace.join("\n ") } 229 | 230 | return false 231 | end 232 | end 233 | 234 | # --- 235 | 236 | # `Newman::Server#default_logger` generates a logger object using 237 | # Ruby's standard library. This object outputs to `STDERR`, and 238 | # runs at info level by default, but will run at debug level if 239 | # either `settings.service.debug_mode` or the Ruby `$DEBUG` 240 | # variable is set. 241 | 242 | def default_logger 243 | self.logger = Logger.new(STDERR) 244 | 245 | if settings.service.debug_mode || $DEBUG 246 | logger.level = Logger::DEBUG 247 | else 248 | logger.level = Logger::INFO 249 | end 250 | 251 | logger 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/newman/settings.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Settings` provides the base functionality that is used by Newman's 2 | # configuration files. It is currently a thin wrapper on top of Ruby's 3 | # `OpenStruct` construct, but will later add some domain specific validations 4 | # and transformations for the various configuration options it supports. 5 | # 6 | # Unless you need to customize the way that Newman's configuration system 7 | # works or make changes to your settings objects at runtime, you probably don't 8 | # need to worry about how this object is implemented. Both 9 | # `Newman::Server.simple` and `Newman::Server.test_mode` create a 10 | # `Newman::Settings` object for you automatically, and you will typically be 11 | # tweaking a sample settings file rather than crafting one from scratch. 12 | # 13 | # `Newman::Settings`is part of Newman's **internal API**, but 14 | # the setting file format and various settings that Newman depends 15 | # on should be considered part of the **external API**. 16 | # 17 | # The following settings are currently supported: 18 | # 19 | # imap.address 20 | # imap.user 21 | # imap.password 22 | # imap.ssl_enabled (default false) 23 | # imap.port 24 | # 25 | # smtp.address 26 | # smtp.user 27 | # smtp.password 28 | # smtp.starttls_enabled (default false) 29 | # smtp.port 30 | # 31 | # service.debug_mode 32 | # log error backtraces and full request and response emails 33 | # 34 | # service.default_sender 35 | # default FROM field for responses 36 | # 37 | # service.domain 38 | # mail domain, used by filters and in building email addresses 39 | # 40 | # service.polling_interval 41 | # idle seconds between server ticks 42 | # 43 | # service.raise_exceptions 44 | # raise exceptions during server ticks, killing the server 45 | # 46 | # service.templates_dir 47 | # directory of template files, relative to application root 48 | 49 | module Newman 50 | class Settings 51 | 52 | # --- 53 | 54 | # The `Newman::Settings.from_file` method is used to create 55 | # a new `Newman::Settings` object and populate it with the data 56 | # contained in a settings file, i.e. 57 | # 58 | # settings = Newman::Settings.from_file('config/environment.rb') 59 | # 60 | # This method is purely syntactic sugar, and is functionally equivalent to 61 | # the following code: 62 | # 63 | # settings = Newman::Settings.new 64 | # settings.load_config('config/environment.rb') 65 | # 66 | # Because there currently is little advantage to explicitly instantiating a 67 | # blank `Newman::Settings` object, this method is the preferred way of doing 68 | # things. 69 | # 70 | def self.from_file(filename) 71 | new.tap { |o| o.load_config(filename) } 72 | end 73 | 74 | # --- 75 | 76 | # A `Newman::Settings` object is a blank slate upon creation. It simply 77 | # assigns an empty `OpenStruct` object for each type of settings data it 78 | # supports. 79 | # 80 | # In most situations, you will not instantiate a `Newman::Settings` object 81 | # directly but instead will make use of a configuration file and the 82 | # `Newman::Settings.from_file` method. Newman provides sample configuration 83 | # files in its `examples/` and `test/` directories, and applications should do the 84 | # same. This will help users discover what fields can be set. 85 | # 86 | # We are aware of the fact that the current configuration system is way too 87 | # flexible and a breeding ground for subtle bugs. This will be fixed in a 88 | # future version of Newman. 89 | 90 | def initialize 91 | self.imap = OpenStruct.new 92 | self.smtp = OpenStruct.new 93 | self.service = OpenStruct.new 94 | self.application = OpenStruct.new 95 | end 96 | 97 | # --- 98 | 99 | # The `imap` and `smtp` fields are used by `Newman::Mailer`, 100 | # the `service` field is used throughout Newman (particularly in 101 | # `Newman::Server`), and the `application` field is reserved for 102 | # application-specific configurations. 103 | 104 | attr_accessor :imap, :smtp, :service, :application 105 | 106 | 107 | # --- 108 | 109 | # `Newman::Settings#load_config` is used for evaluating 110 | # the contents of a file within the context of a `Newman::Settings` 111 | # instance. 112 | # 113 | # In practice, this method is typically called by 114 | # `Newman::Settings#from_file`, but can also be used to apply 115 | # multiple settings files to a single `Newman::Settings` object 116 | 117 | def load_config(filename) 118 | eval(File.read(filename), binding) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/newman/store.rb: -------------------------------------------------------------------------------- 1 | # `Newman::Store` is a minimal persistence layer for storing non-relational 2 | # data. It is meant to make the task of building small applications with simple 3 | # data storage needs easier. 4 | # 5 | # For an example of how `Newman::Store` can be used in your applications, you 6 | # can take a look at how `Newman::MailingList` is implemented. A similar 7 | # approach could be used to develop arbitrary persistent models. 8 | # 9 | # `Newman::Store` is part of Newman's **external interface**. 10 | 11 | module Newman 12 | class Store 13 | 14 | # --- 15 | 16 | # To initialize a `Newman::Store` object, a `filename` string must 17 | # be provided, i.e. 18 | # 19 | # store = Newman::Store.new("simple.store") 20 | # 21 | # This filename will be used to initialize a `PStore` object after first 22 | # running `FileUtils.mkdir_p` to create any directories within the path to 23 | # the filename if they do not already exist. Once that `PStore` object is 24 | # created, two root keys will be mapped to empty Hash objects if they 25 | # are not set already: `:indentifers` and `:columns`. 26 | # 27 | # While it's okay to treat the `PStore` object as an implementation detail, 28 | # we will treat our interactions with it as part of Newman's **external 29 | # interface**, so that we are more conservative about making backwards 30 | # incompatible changes to the databases created by `Newman::Store`. 31 | 32 | def initialize(filename) 33 | FileUtils.mkdir_p(File.dirname(filename)) 34 | 35 | self.data = PStore.new(filename) 36 | 37 | write do 38 | data[:identifiers] ||= {} 39 | data[:columns] ||= {} 40 | end 41 | end 42 | 43 | # --- 44 | 45 | # `Newman::Store#[]` is syntactic sugar for initializing a 46 | # `Newman::Recorder` object, and is meant to be used for 47 | # accessing and manipulating column data by `column_key`, i.e. 48 | # 49 | # store[:subscriptions].create("gregory.t.brown@gmail.com") 50 | # 51 | # This method is functionally equivalent to the following code: 52 | # 53 | # recorder = Newman::Recorder.new(:subscriptions, store) 54 | # recorder.create("gregory.t.brown@gmail.com") 55 | # 56 | # For aesthetic reasons and for forward compatibility, it is 57 | # preferable to use `Newman::Store#[]` rather than instantiating 58 | # a `Newman::Recorder` object directly. 59 | 60 | def [](column_key) 61 | Recorder.new(column_key, self) 62 | end 63 | 64 | # --- 65 | 66 | # `Newman::Store#read` initiates a read only transaction and then yields 67 | # the underlying `PStore` object stored in the `data` field. 68 | 69 | def read 70 | data.transaction(:read_only) { yield(data) } 71 | end 72 | 73 | # --- 74 | 75 | # `Newman::Store#read` initiates a read/write transaction and then yields 76 | # the underlying `PStore` object stored in the `data` field. 77 | 78 | def write 79 | data.transaction { yield(data) } 80 | end 81 | 82 | # --- 83 | 84 | # **NOTE: Methods below this point in the file are implementation 85 | # details, and should not be depended upon** 86 | 87 | private 88 | 89 | # --- 90 | 91 | # The `data` accessor is kept private because a `Newman::Store` object is 92 | # meant to wrap a single `PStore` object once created, and because we want 93 | # to force every interaction with a `Newman::Store` to be transactional in 94 | # nature. 95 | 96 | attr_accessor :data 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/newman/test_mailer.rb: -------------------------------------------------------------------------------- 1 | # `Newman::TestMailer` is a drop-in replacement for `Newman::Mailer` meant for 2 | # use in automated testing. It is a thin wrapper on top of the built in testing 3 | # functionality provided by the mail gem. 4 | # 5 | # `Newman::TestMailer` may be a useful tool for Newman application developers, 6 | # at some point but is a bit tricky to work with due to the fact that it 7 | # relies on global state. This is a known issue and will hopefully be solved in 8 | # a future version of Newman. 9 | # 10 | # `Newman::TestMailer` is part of Newman's **internal interface**, but may 11 | # become part of the **external interface** if we can make it less brittle. 12 | # Patches are welcome! 13 | 14 | module Newman 15 | class TestMailer 16 | # --- 17 | 18 | # To initialize a `Newman::TestMailer` object, a settings object must be 19 | # provided, i.e. 20 | # 21 | # settings = Newman::Settings.from_file('config/environment.rb') 22 | # mailer = Newman::TestMailer.new(settings) 23 | # 24 | # However, there are handful of caveats worth knowing about this 25 | # constructing an instance of this particular object. 26 | # 27 | # 1) Most unit tests won't need a `Newman::TestMailer` object present, and most 28 | # integration tests can make use of `Newman::Server.test_mode`, preventing 29 | # the need to ever explicitly instantiate a `Newman::TestMailer` object. 30 | # 31 | # 2) Because there isn't an obvious way to work with test objects in the 32 | # underlying mail gem without relying on global state, 33 | # `Newman::TestMailer` actually implements the singleton pattern and 34 | # returns references to a single instance rather than creating new 35 | # instances. The constructor interface is simply preserved so that it 36 | # can be a drop-in replacement for a `Newman::Mailer` object. 37 | # 38 | # 3) The settings object is not actually used, and is only part of the 39 | # signature for API compatibility reasons. 40 | # 41 | # With these caveats in mind, be sure to think long and hard about whether 42 | # you actually need to explicitly build instances of this object before 43 | # doing so :) 44 | 45 | class << self 46 | def new(settings) 47 | return self.instance if instance 48 | 49 | Mail.defaults do 50 | retriever_method :test 51 | delivery_method :test 52 | end 53 | 54 | self.instance = allocate 55 | end 56 | 57 | attr_accessor :instance 58 | end 59 | 60 | # --- 61 | 62 | # `Newman::TestMailer#messages` is used to retrieve all messages currently in the inbox 63 | # and then delete them from the underlying `Mail::TestMailer` object so that 64 | # the inbox gets cleared. This method returns an array of 65 | # `Mail::Message` objects if any messages were found, and returns 66 | # an empty array otherwise. 67 | # 68 | # Keep in mind that because only a single `Newman::TestMailer` ever gets 69 | # instantiated no matter how many times you call `Newman::TestMailer.new`, 70 | # you only get one test inbox per process. 71 | 72 | def messages 73 | msgs = Marshal.load(Marshal.dump(Mail::TestMailer.deliveries)) 74 | Mail::TestMailer.deliveries.clear 75 | 76 | msgs 77 | end 78 | 79 | # --- 80 | 81 | # `Newman::TestMailer#new_message` is used to construct a new `Mail::Message` object, 82 | # with the delivery settings set to test mode. 83 | # This method passes all its arguments on to `Mail.new`, so be sure 84 | # to refer to the [mail gem's documentation](http://github.com/mikel/mail) 85 | # for details. 86 | 87 | def new_message(*a, &b) 88 | Mail.new(*a, &b) 89 | end 90 | 91 | # --- 92 | 93 | # `Newman::TestMailer#deliver_message` method is used to construct and immediately deliver a 94 | # message with the delivery settings set to test mode. 95 | 96 | def deliver_message(*a, &b) 97 | new_message(*a, &b).deliver 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/newman/version.rb: -------------------------------------------------------------------------------- 1 | # Newman::Version is used to determine which version of Newman is currently 2 | # running and is part of Newman's **external api**. 3 | 4 | module Newman 5 | module Version 6 | MAJOR = 0 7 | MINOR = 4 8 | TINY = 0 9 | STRING = "#{MAJOR}.#{MINOR}.#{TINY}" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /newman.gemspec: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/lib/newman/version" 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "newman" 5 | s.version = Newman::Version::STRING 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ["Gregory Brown"] 8 | s.email = ["gregory.t.brown@gmail.com"] 9 | s.homepage = "http://github.com/mendicant-university/newman" 10 | s.summary = "A microframework for mail-centric applications" 11 | s.description = "A microframework for mail-centric applications" 12 | s.files = Dir.glob("{lib,examples,test}/**/*") + %w[README.md CHANGELOG.md Gemfile] 13 | s.require_path = 'lib' 14 | s.add_runtime_dependency 'mail', "= 2.3.0" 15 | s.add_runtime_dependency 'tilt', "~> 1.3.3" 16 | 17 | s.add_development_dependency 'minitest', "~> 2.11.1" 18 | s.add_development_dependency 'simplecov' 19 | s.add_development_dependency 'purdytest' 20 | s.add_development_dependency 'rake' 21 | 22 | s.required_ruby_version = ">= 1.9.2" 23 | s.required_rubygems_version = ">= 1.3.6" 24 | s.rubyforge_project = "newman" 25 | end 26 | 27 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | gem "minitest" 2 | 3 | require "minitest/autorun" 4 | require "purdytest" 5 | require_relative "../lib/newman" 6 | 7 | module Newman 8 | TEST_DIR = File.dirname(__FILE__) 9 | 10 | def self.new_test_server(apps) 11 | apps = Array(apps) 12 | server = Newman::Server.test_mode(TEST_DIR + "/settings.rb") 13 | apps.each {|app| server.apps << app } 14 | server.settings.application.simplelist_db = TEST_DIR + "/test.store" 15 | server.settings.service.templates_dir = TEST_DIR + "/../examples/views" 16 | 17 | logger = ::Logger.new( TEST_DIR + "/log/test.log" ) 18 | logger.level = ::Logger::DEBUG 19 | server.logger = logger 20 | 21 | server 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/integration/acid_tests.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | 3 | require_relative "../../examples/ping_pong" 4 | require_relative "../../examples/simple_mailing_list" 5 | 6 | describe "Ping Pong" do 7 | let(:server) { Newman.new_test_server(Newman::Examples::PingPong) } 8 | let(:mailer) { server.mailer } 9 | 10 | it "responds to an email sent to test+ping@test.com" do 11 | mailer.deliver_message(:to => "test+ping@test.com") 12 | server.tick 13 | mailer.messages.first.subject.must_equal("pong") 14 | end 15 | 16 | it "responds to an email sent to test+bad@test.com" do 17 | mailer.deliver_message(:to => "test+bad@test.com") 18 | server.tick 19 | mailer.messages.first.subject.must_equal("unknown command") 20 | end 21 | end 22 | 23 | describe "SimpleList" do 24 | let(:server) { Newman.new_test_server(Newman::Examples::SimpleList) } 25 | let(:mailer) { server.mailer } 26 | 27 | it "emulates a simple mailing list" do 28 | mailer.deliver_message(:from => "tester@test.com", 29 | :to => "test@test.com") 30 | 31 | server.tick 32 | 33 | mailer.messages.first.subject.must_equal("You are not subscribed") 34 | 35 | mailer.deliver_message(:from => "tester@test.com", 36 | :to => "test+subscribe@test.com", 37 | :subject => "subscribe") 38 | 39 | server.tick 40 | mailer.messages.first.subject.must_equal("SUBSCRIBED!") 41 | 42 | 43 | mailer.deliver_message(:from => "tester@test.com", 44 | :to => "test@test.com", 45 | :subject => "WIN!") 46 | 47 | server.tick 48 | mailer.messages.first.subject.must_equal("WIN!") 49 | 50 | mailer.deliver_message(:from => "tester@test.com", 51 | :to => "test@test.com", 52 | :subject => "unsubscribe") 53 | 54 | server.tick 55 | mailer.messages.first.subject.must_equal("UNSUBSCRIBED!") 56 | end 57 | 58 | after do 59 | if File.exist?(server.settings.application.simplelist_db) 60 | File.unlink(server.settings.application.simplelist_db) 61 | end 62 | end 63 | end 64 | 65 | 66 | # move this when test suite set up 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/integration/skip_response_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | 3 | describe "Server handling request without responding" do 4 | 5 | let(:noop) do 6 | Newman::Application.new do 7 | default do 8 | skip_response 9 | end 10 | end 11 | end 12 | 13 | let(:server) { Newman.new_test_server(noop) } 14 | let(:mailer) { server.mailer } 15 | 16 | it "should not deliver message" do 17 | mailer.deliver_message(:from => 'tester@test.com', 18 | :to => 'test+noop@test.com') 19 | server.tick 20 | assert_empty mailer.messages 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/integration/subject_filter_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | 3 | describe "subject filter" do 4 | let(:app) do 5 | Newman::Application.new do 6 | subject :match, "hello" do 7 | respond(:subject => "HELLOOOOOOOO!") 8 | end 9 | 10 | default do 11 | respond(:subject => "No subject, no greeting") 12 | end 13 | end 14 | end 15 | 16 | let(:server) { Newman.new_test_server(app) } 17 | let(:mailer) { server.mailer } 18 | 19 | it "should automatically not match emails without subjects" do 20 | mailer.deliver_message(:to => "test@test.com") 21 | server.tick 22 | 23 | mailer.messages.first.subject.must_equal("No subject, no greeting") 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/integration/template_test.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require_relative "../helper" 3 | 4 | describe "template" do 5 | let(:app) do 6 | Newman::Application.new do 7 | match :list_id, /[^\.]+/ 8 | 9 | to :tag, "{list_id}.echo" do 10 | respond(:subject => "RE: #{request.subject}", 11 | :body => template('test/echo') 12 | ) 13 | end 14 | 15 | to :tag, "{list_id}.moneyball" do 16 | pick_4 = [ rand(9), rand(9), rand(9), rand(9) ] 17 | magic_code = Digest::MD5.hexdigest( 18 | "#{sender}#{params[:list_id]}" 19 | ) 20 | respond(:subject => "Today's Jackpots", 21 | :body => template('test/moneyball', :pick_4 => pick_4, 22 | :magic_code => magic_code) 23 | ) 24 | end 25 | 26 | to :tag, "{list_id}.partials" do 27 | respond(:subject => "RE: #{request.subject}", 28 | :body => template('test/echo_with_partials') 29 | ) 30 | end 31 | 32 | end 33 | end 34 | 35 | let(:server) { 36 | Newman.new_test_server([Newman::RequestLogger, app, Newman::ResponseLogger]) 37 | } 38 | 39 | let(:mailer) { server.mailer } 40 | 41 | # ----- 42 | 43 | it "renders simple template without passed locals hash" do 44 | mailer.deliver_message(:from => "me@example.com", 45 | :to => "test+fizbiz.echo@test.com", 46 | :body => "Could you call me about fizbiz please?") 47 | server.tick 48 | 49 | msgs = mailer.messages 50 | assert_equal 1, msgs.count 51 | 52 | actual = msgs.first.decoded 53 | 54 | # assert that list_id, sender, and body is rendered in view 55 | assert_match /\bme\@example\.com\b/, actual 56 | assert_match /\bfizbiz\b/, actual 57 | assert_match /Could you call me about fizbiz please\?/, actual 58 | end 59 | 60 | it "renders template with passed locals hash" do 61 | mailer.deliver_message(:from => "me@example.com", 62 | :to => "test+fizbiz.moneyball@test.com") 63 | server.tick 64 | 65 | msgs = mailer.messages 66 | assert_equal 1, msgs.count 67 | 68 | actual = msgs.first.decoded 69 | expected_code = Digest::MD5.hexdigest("me@example.comfizbiz") 70 | 71 | # assert that pick_4 and magic_code locals are rendered in view 72 | assert_match(/Pick 4\: \d\d\d\d/, actual) 73 | assert_match(/Your magic code is\: #{expected_code}/, actual) 74 | end 75 | 76 | it "renders template with partials" do 77 | mailer.deliver_message(:from => "me@example.com", 78 | :to => "test+fizbiz.partials@test.com", 79 | :body => "Could you call me about fizbiz please?") 80 | server.tick 81 | 82 | msgs = mailer.messages 83 | assert_equal 1, msgs.count 84 | 85 | actual = msgs.first.decoded 86 | 87 | # assert that list_id, sender, and body is rendered in view 88 | assert_match /\bme\@example\.com\b/, actual 89 | assert_match /\bfizbiz\b/, actual 90 | assert_match /Could you call me about fizbiz please\?/, actual 91 | end 92 | 93 | end -------------------------------------------------------------------------------- /test/integration/to_filter_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | 3 | describe "subject filter" do 4 | let(:app) do 5 | Newman::Application.new do 6 | match :list_id, /[^.]+/ 7 | 8 | to :tag, "{list_id}.subscribe" do 9 | respond(:subject => "Subscribing you to [#{params[:list_id]}]") 10 | end 11 | 12 | default do 13 | respond(:subject => "Didn't understand you!") 14 | end 15 | end 16 | end 17 | 18 | let(:server) { Newman.new_test_server(app) } 19 | let(:mailer) { server.mailer } 20 | 21 | it "should automatically not match emails without subjects" do 22 | mailer.deliver_message(:to => "test+foo.subscribe@test.com") 23 | server.tick 24 | 25 | mailer.messages.first.subject.must_equal("Subscribing you to [foo]") 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /test/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendicant-original/newman/a5273d0eed07f33d206763873c0f106605576300/test/log/.gitkeep -------------------------------------------------------------------------------- /test/settings.rb: -------------------------------------------------------------------------------- 1 | service.domain = "test.com" 2 | service.default_sender = "test@test.com" 3 | service.polling_interval = 10 4 | service.raise_exceptions = true 5 | -------------------------------------------------------------------------------- /test/suite.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | require_relative "integration/acid_tests" 5 | require_relative "integration/skip_response_test" 6 | require_relative "integration/subject_filter_test" 7 | require_relative "integration/template_test" 8 | require_relative "integration/to_filter_test" 9 | --------------------------------------------------------------------------------