├── .gitignore ├── Gemfile ├── Gemfile.lock ├── Procfile ├── Procfile.development ├── Procfile.imap-client-px ├── Procfile.stress-test ├── README.md ├── Rakefile ├── app.json ├── app ├── admin │ ├── admin_user.rb │ ├── delayed_job.rb │ ├── imap_provider.rb │ ├── mail_log.rb │ ├── partner.rb │ ├── partner_connection.rb │ ├── tracer_log.rb │ ├── transmit_log.rb │ └── user.rb ├── assets │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── active_admin.js.coffee │ │ └── application.js │ └── stylesheets │ │ ├── active_admin.css.scss │ │ └── application.css ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── connections_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── link_rel.rb │ ├── users │ │ ├── base_callback_controller.rb │ │ ├── connects_controller.rb │ │ └── disconnects_controller.rb │ └── webhook_test_controller.rb ├── helpers │ ├── application_helper.rb │ ├── oauth2 │ │ ├── connects_helper.rb │ │ └── disconnects_helper.rb │ └── plain │ │ ├── connects_helper.rb │ │ └── disconnects_helper.rb ├── interactors │ ├── base_webhook.rb │ ├── call_new_mail_webhook.rb │ ├── call_user_connected_webhook.rb │ ├── call_user_disconnected_webhook.rb │ └── schedule_tracer_emails.rb ├── mailers │ ├── .keep │ └── tracer_mailer.rb ├── models │ ├── .keep │ ├── admin_user.rb │ ├── concerns │ │ ├── .keep │ │ ├── auth_method_helper.rb │ │ └── connection_fields.rb │ ├── delayed_job.rb │ ├── imap_daemon_heartbeat.rb │ ├── imap_provider.rb │ ├── mail_log.rb │ ├── oauth2 │ │ ├── imap_provider.rb │ │ ├── partner_connection.rb │ │ └── user.rb │ ├── partner.rb │ ├── partner_connection.rb │ ├── plain │ │ ├── imap_provider.rb │ │ ├── partner_connection.rb │ │ └── user.rb │ ├── tracer_log.rb │ ├── transmit_log.rb │ └── user.rb ├── processes │ ├── common │ │ ├── csv_log.rb │ │ ├── db_connection.rb │ │ ├── light_sleep.rb │ │ ├── stoppable.rb │ │ ├── worker_pool.rb │ │ └── wrapped_thread.rb │ ├── imap_client.rb │ ├── imap_client │ │ ├── daemon.rb │ │ ├── process_uid.rb │ │ ├── rendezvous_hash.rb │ │ └── user_thread.rb │ ├── imap_test_server.rb │ └── imap_test_server │ │ ├── daemon.rb │ │ ├── mailboxes.rb │ │ └── socket_state.rb └── views │ ├── api │ └── v1 │ │ ├── connections │ │ ├── index.json.rb │ │ └── show.json.rb │ │ └── users │ │ ├── index.json.rb │ │ └── show.json.rb │ ├── layouts │ ├── application.html.erb │ └── blank.html.erb │ └── tracer_mailer │ └── tracer_email.html.erb ├── bin ├── bundle ├── delayed_job ├── rails ├── rake └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml.example ├── environment.rb ├── environments │ ├── development.rb │ ├── performance.rb │ ├── production.rb │ ├── stress.rb │ └── test.rb ├── initializers │ ├── active_admin.rb │ ├── airbrake.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── delayed_job.rb │ ├── devise.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── log.rb │ ├── mime_types.rb │ ├── ruby_template_handler.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ └── en.yml ├── newrelic.yml ├── routes.rb ├── secrets.yml └── unicorn.rb ├── db ├── migrate │ ├── 20141029214610_devise_create_admin_users.rb │ ├── 20141029214612_create_active_admin_comments.rb │ ├── 20141029215033_create_partners.rb │ ├── 20141029215101_create_mail_logs.rb │ ├── 20141029215105_create_transmit_logs.rb │ ├── 20141031010321_create_imap_providers.rb │ ├── 20141031010353_create_partner_connections.rb │ ├── 20141031010433_create_users.rb │ ├── 20141104202256_create_imap_daemon_heartbeats.rb │ ├── 20141111204248_add_archived_to_users.rb │ ├── 20141113163147_add_type_to_users_and_imap_providers.rb │ ├── 20141114233206_add_locked_at_to_admin_user.rb │ ├── 20141118170010_add_type_to_partner_connection.rb │ ├── 20141121152941_add_oauth2_fields_to_imap_provider.rb │ ├── 20141121182537_add_redirect_urls_to_partner.rb │ ├── 20141121184010_rename_fields.rb │ ├── 20141205024759_rename_partner_webhook_columns.rb │ ├── 20141207191800_add_webhooks_to_partner.rb │ ├── 20141207200312_create_delayed_jobs.rb │ ├── 20141215194630_add_tracer_to_users.rb │ ├── 20141215194652_create_tracer_logs.rb │ ├── 20141215194754_add_smtp_settings_to_imap_provider.rb │ ├── 20141215212628_remove_oauth1_fields.rb │ ├── 20150119185401_encrypt_existing_data.rb │ └── 20150611134232_add_more_indexes_to_mail_logs.rb ├── schema.rb ├── seeds-development.rb ├── seeds-production.rb ├── seeds-stress.rb ├── seeds-test.rb └── seeds.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ └── imap.rake └── xoauth2_authenticator.rb ├── log ├── .keep └── stress │ └── .gitcreate ├── public ├── 404.html ├── 422.html ├── 500.html ├── failure.html ├── favicon.ico ├── index.html ├── robots.txt └── success.html ├── screenshot.png ├── script ├── analyze-stress-test.R └── stress-test ├── test ├── controllers │ ├── .keep │ └── api │ │ └── v1 │ │ ├── connections_controller_test.rb │ │ └── users_controller_test.rb ├── fixtures │ ├── .keep │ ├── imap_daemon_heartbeats.yml │ └── tracer_logs.yml ├── helpers │ └── .keep ├── imap │ └── rendezvous_hash_test.rb ├── integration │ └── .keep ├── mailers │ ├── .keep │ ├── previews │ │ └── tracer_mailer_preview.rb │ └── tracer_mailer_test.rb ├── models │ ├── .keep │ ├── admin_user_test.rb │ ├── imap_daemon_heartbeat_test.rb │ ├── imap_provider_test.rb │ ├── mail_log_test.rb │ ├── partner_connection_test.rb │ ├── partner_credential_test.rb │ ├── partner_test.rb │ ├── tracer_log_test.rb │ ├── transmit_log_test.rb │ └── user_test.rb └── test_helper.rb └── vendor └── assets ├── javascripts └── .keep └── stylesheets └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/stress 16 | /stress-test-results.pdf 17 | /log/*.log 18 | /tmp 19 | config/database.yml 20 | .ruby-version 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby "2.2.0" 4 | 5 | gem 'rails' , '4.1.6' 6 | gem 'sass-rails' , '~> 4.0.3' 7 | gem 'uglifier' , '>= 1.3.0' 8 | gem 'coffee-rails' , '~> 4.0.0' 9 | gem 'jquery-rails' 10 | gem 'turbolinks' 11 | gem 'jbuilder' , '~> 2.0' 12 | gem 'pg' , '~> 0.17.1' 13 | gem 'delayed_job_active_record' , '~> 4.0.2' 14 | gem 'unicorn' , '~> 4.8.3' 15 | gem 'devise' , '~> 3.4.1' 16 | gem 'rest-client' , '~> 1.7.2' 17 | gem 'oauth' , '~> 0.4.7' 18 | gem 'oauth2' , '~> 0.9.3' 19 | gem 'airbrake' , '~> 4.1.0' 20 | gem 'activeadmin' , '1.0.0.pre', :github => 'activeadmin', :ref => '0becbef0' 21 | gem 'gibberish' , '~> 1.4.0' 22 | gem 'foreman' , '~> 0.75.0' 23 | 24 | gem 'rails_stdout_logging' , :group => [:staging, :production] 25 | gem 'rails_12factor' , :group => :production 26 | # gem 'newrelic_rpm' , :group => :production 27 | 28 | gem 'spring' , :group => :development 29 | gem 'sqlite3' , :group => :development 30 | gem 'pry' , :group => :development 31 | gem 'pry-byebug' , :group => :development 32 | gem 'ruby-prof' , :group => :stress 33 | 34 | gem 'sdoc' , '~> 0.4.0', :group => :doc 35 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/activeadmin/activeadmin.git 3 | revision: 0becbef0918a38b91ad120aba7c2f6641e9b5440 4 | ref: 0becbef0 5 | specs: 6 | activeadmin (1.0.0.pre) 7 | arbre (~> 1.0, >= 1.0.2) 8 | bourbon 9 | coffee-rails 10 | formtastic (~> 3.0) 11 | formtastic_i18n 12 | inherited_resources (~> 1.4.1) 13 | jquery-rails 14 | jquery-ui-rails (~> 5.0) 15 | kaminari (~> 0.15) 16 | rails (>= 3.2, < 4.2) 17 | ransack (~> 1.3) 18 | sass-rails 19 | 20 | GEM 21 | remote: https://rubygems.org/ 22 | specs: 23 | actionmailer (4.1.6) 24 | actionpack (= 4.1.6) 25 | actionview (= 4.1.6) 26 | mail (~> 2.5, >= 2.5.4) 27 | actionpack (4.1.6) 28 | actionview (= 4.1.6) 29 | activesupport (= 4.1.6) 30 | rack (~> 1.5.2) 31 | rack-test (~> 0.6.2) 32 | actionview (4.1.6) 33 | activesupport (= 4.1.6) 34 | builder (~> 3.1) 35 | erubis (~> 2.7.0) 36 | activemodel (4.1.6) 37 | activesupport (= 4.1.6) 38 | builder (~> 3.1) 39 | activerecord (4.1.6) 40 | activemodel (= 4.1.6) 41 | activesupport (= 4.1.6) 42 | arel (~> 5.0.0) 43 | activesupport (4.1.6) 44 | i18n (~> 0.6, >= 0.6.9) 45 | json (~> 1.7, >= 1.7.7) 46 | minitest (~> 5.1) 47 | thread_safe (~> 0.1) 48 | tzinfo (~> 1.1) 49 | airbrake (4.1.0) 50 | builder 51 | multi_json 52 | arbre (1.0.3) 53 | activesupport (>= 3.0.0) 54 | arel (5.0.1.20140414130214) 55 | bcrypt (3.1.9) 56 | bourbon (3.2.4) 57 | sass (~> 3.2) 58 | thor 59 | builder (3.2.2) 60 | byebug (3.5.1) 61 | columnize (~> 0.8) 62 | debugger-linecache (~> 1.2) 63 | slop (~> 3.6) 64 | coderay (1.1.0) 65 | coffee-rails (4.0.1) 66 | coffee-script (>= 2.2.0) 67 | railties (>= 4.0.0, < 5.0) 68 | coffee-script (2.3.0) 69 | coffee-script-source 70 | execjs 71 | coffee-script-source (1.8.0) 72 | columnize (0.9.0) 73 | debugger-linecache (1.2.0) 74 | delayed_job (4.0.6) 75 | activesupport (>= 3.0, < 5.0) 76 | delayed_job_active_record (4.0.3) 77 | activerecord (>= 3.0, < 5.0) 78 | delayed_job (>= 3.0, < 4.1) 79 | devise (3.4.1) 80 | bcrypt (~> 3.0) 81 | orm_adapter (~> 0.1) 82 | railties (>= 3.2.6, < 5) 83 | responders 84 | thread_safe (~> 0.1) 85 | warden (~> 1.2.3) 86 | dotenv (0.11.1) 87 | dotenv-deployment (~> 0.0.2) 88 | dotenv-deployment (0.0.2) 89 | erubis (2.7.0) 90 | execjs (2.2.2) 91 | faraday (0.9.1) 92 | multipart-post (>= 1.2, < 3) 93 | foreman (0.75.0) 94 | dotenv (~> 0.11.1) 95 | thor (~> 0.19.1) 96 | formtastic (3.1.3) 97 | actionpack (>= 3.2.13) 98 | formtastic_i18n (0.1.1) 99 | gibberish (1.4.0) 100 | has_scope (0.6.0.rc) 101 | actionpack (>= 3.2, < 5) 102 | activesupport (>= 3.2, < 5) 103 | hike (1.2.3) 104 | i18n (0.7.0) 105 | inherited_resources (1.4.1) 106 | has_scope (~> 0.6.0.rc) 107 | responders (~> 1.0.0.rc) 108 | jbuilder (2.2.6) 109 | activesupport (>= 3.0.0, < 5) 110 | multi_json (~> 1.2) 111 | jquery-rails (3.1.2) 112 | railties (>= 3.0, < 5.0) 113 | thor (>= 0.14, < 2.0) 114 | jquery-ui-rails (5.0.3) 115 | railties (>= 3.2.16) 116 | json (1.8.2) 117 | jwt (1.2.0) 118 | kaminari (0.16.2) 119 | actionpack (>= 3.0.0) 120 | activesupport (>= 3.0.0) 121 | kgio (2.9.3) 122 | mail (2.6.3) 123 | mime-types (>= 1.16, < 3) 124 | method_source (0.8.2) 125 | mime-types (2.4.3) 126 | minitest (5.5.1) 127 | multi_json (1.10.1) 128 | multi_xml (0.5.5) 129 | multipart-post (2.0.0) 130 | netrc (0.10.2) 131 | oauth (0.4.7) 132 | oauth2 (0.9.4) 133 | faraday (>= 0.8, < 0.10) 134 | jwt (~> 1.0) 135 | multi_json (~> 1.3) 136 | multi_xml (~> 0.5) 137 | rack (~> 1.2) 138 | orm_adapter (0.5.0) 139 | pg (0.17.1) 140 | polyamorous (1.1.0) 141 | activerecord (>= 3.0) 142 | pry (0.10.1) 143 | coderay (~> 1.1.0) 144 | method_source (~> 0.8.1) 145 | slop (~> 3.4) 146 | pry-byebug (2.0.0) 147 | byebug (~> 3.4) 148 | pry (~> 0.10) 149 | rack (1.5.2) 150 | rack-test (0.6.3) 151 | rack (>= 1.0) 152 | rails (4.1.6) 153 | actionmailer (= 4.1.6) 154 | actionpack (= 4.1.6) 155 | actionview (= 4.1.6) 156 | activemodel (= 4.1.6) 157 | activerecord (= 4.1.6) 158 | activesupport (= 4.1.6) 159 | bundler (>= 1.3.0, < 2.0) 160 | railties (= 4.1.6) 161 | sprockets-rails (~> 2.0) 162 | rails_12factor (0.0.3) 163 | rails_serve_static_assets 164 | rails_stdout_logging 165 | rails_serve_static_assets (0.0.3) 166 | rails_stdout_logging (0.0.3) 167 | railties (4.1.6) 168 | actionpack (= 4.1.6) 169 | activesupport (= 4.1.6) 170 | rake (>= 0.8.7) 171 | thor (>= 0.18.1, < 2.0) 172 | raindrops (0.13.0) 173 | rake (10.4.2) 174 | ransack (1.6.2) 175 | actionpack (>= 3.0) 176 | activerecord (>= 3.0) 177 | activesupport (>= 3.0) 178 | i18n 179 | polyamorous (~> 1.1) 180 | rdoc (4.2.0) 181 | json (~> 1.4) 182 | responders (1.0.0) 183 | railties (>= 3.2, < 5) 184 | rest-client (1.7.2) 185 | mime-types (>= 1.16, < 3.0) 186 | netrc (~> 0.7) 187 | ruby-prof (0.15.6) 188 | sass (3.2.19) 189 | sass-rails (4.0.5) 190 | railties (>= 4.0.0, < 5.0) 191 | sass (~> 3.2.2) 192 | sprockets (~> 2.8, < 3.0) 193 | sprockets-rails (~> 2.0) 194 | sdoc (0.4.1) 195 | json (~> 1.7, >= 1.7.7) 196 | rdoc (~> 4.0) 197 | slop (3.6.0) 198 | spring (1.2.0) 199 | sprockets (2.12.3) 200 | hike (~> 1.2) 201 | multi_json (~> 1.0) 202 | rack (~> 1.0) 203 | tilt (~> 1.1, != 1.3.0) 204 | sprockets-rails (2.2.2) 205 | actionpack (>= 3.0) 206 | activesupport (>= 3.0) 207 | sprockets (>= 2.8, < 4.0) 208 | sqlite3 (1.3.10) 209 | thor (0.19.1) 210 | thread_safe (0.3.4) 211 | tilt (1.4.1) 212 | turbolinks (2.5.3) 213 | coffee-rails 214 | tzinfo (1.2.2) 215 | thread_safe (~> 0.1) 216 | uglifier (2.7.0) 217 | execjs (>= 0.3.0) 218 | json (>= 1.8.0) 219 | unicorn (4.8.3) 220 | kgio (~> 2.6) 221 | rack 222 | raindrops (~> 0.7) 223 | warden (1.2.3) 224 | rack (>= 1.0) 225 | 226 | PLATFORMS 227 | ruby 228 | 229 | DEPENDENCIES 230 | activeadmin (= 1.0.0.pre)! 231 | airbrake (~> 4.1.0) 232 | coffee-rails (~> 4.0.0) 233 | delayed_job_active_record (~> 4.0.2) 234 | devise (~> 3.4.1) 235 | foreman (~> 0.75.0) 236 | gibberish (~> 1.4.0) 237 | jbuilder (~> 2.0) 238 | jquery-rails 239 | oauth (~> 0.4.7) 240 | oauth2 (~> 0.9.3) 241 | pg (~> 0.17.1) 242 | pry 243 | pry-byebug 244 | rails (= 4.1.6) 245 | rails_12factor 246 | rails_stdout_logging 247 | rest-client (~> 1.7.2) 248 | ruby-prof 249 | sass-rails (~> 4.0.3) 250 | sdoc (~> 0.4.0) 251 | spring 252 | sqlite3 253 | turbolinks 254 | uglifier (>= 1.3.0) 255 | unicorn (~> 4.8.3) 256 | 257 | BUNDLED WITH 258 | 1.10.4 259 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb 2 | worker: bundle exec rake jobs:work 3 | 4 | # Heroku dynos have thread limits. (1x = 256, 2x = 512, px = 5 | # 32767). For 1x and 2x dynos, we set MAX_USER_THREADS way below the 6 | # limit because IMAP also consumes some threads. For px dynos, we use 7 | # foreman to create multiple imap_client instances on a single box. 8 | # 9 | # See https://devcenter.heroku.com/articles/limits#processes-threads 10 | 11 | imap_client_1x: NUM_WORKER_THREADS=1 MAX_USER_THREADS=200 bundle exec rake imap:client 12 | imap_client_2x: NUM_WORKER_THREADS=2 MAX_USER_THREADS=500 bundle exec rake imap:client 13 | imap_client_px: bundle exec foreman s -f Procfile.imap-client-px 14 | -------------------------------------------------------------------------------- /Procfile.development: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb 2 | worker: bundle exec rake jobs:work 3 | imap_client: bundle exec rake imap:client 4 | -------------------------------------------------------------------------------- /Procfile.imap-client-px: -------------------------------------------------------------------------------- 1 | north: DYNO=$DYNO-1 NUM_WORKER_THREADS=3 MAX_USER_THREADS=3750 bundle exec rake imap:client 2 | south: DYNO=$DYNO-2 NUM_WORKER_THREADS=3 MAX_USER_THREADS=3750 bundle exec rake imap:client 3 | east: DYNO=$DYNO-3 NUM_WORKER_THREADS=3 MAX_USER_THREADS=3750 bundle exec rake imap:client 4 | west: DYNO=$DYNO-4 NUM_WORKER_THREADS=3 MAX_USER_THREADS=3750 bundle exec rake imap:client 5 | -------------------------------------------------------------------------------- /Procfile.stress-test: -------------------------------------------------------------------------------- 1 | imap_test_server: rake imap:test_server 2 | imap_client_1: rake imap:client STRESS_TEST_MODE=true ENABLE_PROFILER=$ENABLE_PROFILER 3 | imap_client_2: rake imap:client STRESS_TEST_MODE=true ENABLE_PROFILER=false 4 | imap_client_3: rake imap:client STRESS_TEST_MODE=true ENABLE_PROFILER=false 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SuperIMAP", 3 | "description": "Monitor inboxes for incoming email, at scale.", 4 | "repository": "https://github.com/rustyio/super-imap", 5 | "keywords": ["IMAP", "mail", "webhook", "client", "ruby"], 6 | "success_url": "/admin", 7 | "scripts": { 8 | "postdeploy": "bundle exec rake db:seed" 9 | }, 10 | "addons": [ 11 | "heroku-postgresql" 12 | ], 13 | "env": { 14 | "ENCRYPTION_KEY": { 15 | "description": "Used to encrypt information in the database.", 16 | "generator": "secret" 17 | }, 18 | "SECRET_KEY_BASE": { 19 | "description": "A secret key for verifying the integrity of signed cookies.", 20 | "generator": "secret" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/admin/admin_user.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register AdminUser do 2 | permit_params :email, :password, :password_confirmation 3 | menu priority: 100 4 | config.sort_order = "email_asc" 5 | config.filters = false 6 | 7 | index do 8 | column :tag do |obj| 9 | link_to obj.email, admin_admin_user_path(obj) 10 | end 11 | column :last_sign_in_at 12 | end 13 | 14 | show do |obj| 15 | panel "Details" do 16 | attributes_table_for obj do 17 | row :id 18 | row :email 19 | row :sign_in_count 20 | row :current_sign_in_at 21 | row :current_sign_in_ip 22 | row :last_sign_in_at 23 | row :last_sign_in_ip 24 | row :created_at 25 | end 26 | end 27 | end 28 | 29 | form do |f| 30 | f.inputs "Admin Details" do 31 | f.input :email 32 | f.input :password 33 | f.input :password_confirmation 34 | end 35 | f.actions 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/admin/delayed_job.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register DelayedJob do 2 | menu priority: 90 3 | config.sort_order = "created_at_desc" 4 | config.batch_actions = true 5 | 6 | # Only allow viewing and deleting. 7 | actions :all, :except => [:new, :edit] 8 | 9 | filter :handler 10 | filter :last_error 11 | filter :queue 12 | 13 | index do 14 | column "Handler" do |obj| 15 | link_to obj.handler.slice(0, 250), admin_delayed_job_path(obj) 16 | end 17 | column :created_at 18 | column :failed_at 19 | column :attempts 20 | column :queue 21 | end 22 | 23 | show do |obj| 24 | panel "Details" do 25 | attributes_table_for obj do 26 | row :id 27 | row :queue 28 | row :created_at 29 | row :failed_at if obj.attempts > 0 30 | row :attempts if obj.attempts > 0 31 | row :message do 32 | link_to "Download Message", message_admin_delayed_job_path(obj, "eml") 33 | end if /CallNewMailWebhook/.match(obj.handler) 34 | end 35 | end 36 | 37 | panel "Handler" do 38 | pre obj.handler 39 | end 40 | 41 | panel "Last Error" do 42 | pre obj.last_error 43 | end if obj.attempts > 0 44 | end 45 | 46 | member_action :message, :method => :get do 47 | # HACK - Make sure the class is loaded. 48 | CallNewMailWebhook 49 | 50 | job = YAML.load(resource.handler) 51 | if job.object.class == CallNewMailWebhook 52 | render :text => job.object.raw_eml, :content_type => 'message/rfc822' 53 | else 54 | render :text => "There was a problem." 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/admin/imap_provider.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register ImapProvider do 2 | config.sort_order = "code_asc" 3 | permit_params :code, :title, 4 | :imap_host, :imap_port, :imap_use_ssl, 5 | :smtp_host, :smtp_port, :smtp_domain, :smtp_enable_starttls_auto, 6 | *Plain::ImapProvider.connection_fields, 7 | *Oauth2::ImapProvider.connection_fields 8 | 9 | config.filters = false 10 | 11 | config.clear_action_items! 12 | actions :all, :except => [:edit, :destroy] 13 | 14 | index do 15 | column "Connection Type" do |obj| 16 | link_to "#{obj.title} (#{obj.code})", admin_imap_provider_path(obj) 17 | end 18 | 19 | column "IMAP Server" do |obj| 20 | "#{obj.imap_host}:#{obj.imap_port}" 21 | end 22 | 23 | column "SMTP Server" do |obj| 24 | "#{obj.smtp_host}:#{obj.smtp_port}" 25 | end 26 | end 27 | 28 | show do |obj| 29 | panel "Details" do 30 | attributes_table_for obj do 31 | row :code 32 | row :title 33 | row :type 34 | end 35 | end 36 | 37 | panel "IMAP Settings" do 38 | attributes_table_for obj do 39 | row :imap_host 40 | row :imap_port 41 | row :imap_use_ssl 42 | end 43 | end 44 | 45 | panel "SMTP Settings" do 46 | attributes_table_for obj do 47 | row :smtp_host 48 | row :smtp_port 49 | row :smtp_domain 50 | row :smtp_enable_starttls_auto 51 | end 52 | end 53 | 54 | panel "Connection Settings" do 55 | attributes_table_for obj do 56 | obj.connection_fields.map do |field| 57 | row field 58 | end 59 | end 60 | end if obj.connection_fields.present? 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/admin/mail_log.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register MailLog do 2 | belongs_to :user 3 | 4 | config.sort_order = "created_at_desc" 5 | 6 | # Only allow viewing. 7 | config.clear_action_items! 8 | actions :all, :except => [:new, :edit, :destroy] 9 | 10 | filter :message_id 11 | 12 | breadcrumb do 13 | user = User.find(params[:user_id]) 14 | connection = user.partner_connection 15 | partner = connection.partner 16 | [ 17 | link_to("Partners", admin_partners_path), 18 | link_to(partner.name, admin_partner_path(partner)), 19 | link_to("Connections", admin_partner_partner_connections_path(partner)), 20 | link_to(connection.imap_provider.code, admin_partner_partner_connection_path(partner, connection)), 21 | link_to("Users", admin_partner_connection_users_path(connection)), 22 | link_to(user.email, admin_partner_connection_user_path(connection, user)), 23 | link_to("Mail Logs", admin_user_mail_logs_path(user)) 24 | ] 25 | end 26 | 27 | index do 28 | column :created_at 29 | column "Message ID" do |obj| 30 | link_to obj.message_id, admin_user_mail_log_path(obj.user, obj) 31 | end 32 | column "Transmit Logs", :sortable => :transmit_logs_count do |obj| 33 | link_to("Transmit Logs (#{obj.transmit_logs_count})", admin_mail_log_transmit_logs_path(obj)) 34 | end 35 | actions 36 | end 37 | 38 | show do 39 | attributes_table do 40 | row :created_at 41 | row :id 42 | row :message_id 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/admin/partner.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Partner do 2 | menu :priority => 0 3 | permit_params :name, :api_key, 4 | :success_url, :failure_url, 5 | :new_mail_webhook, 6 | :user_connected_webhook, 7 | :user_disconnected_webhook 8 | 9 | breadcrumb do 10 | [ 11 | link_to("Partners", admin_partners_path) 12 | ] 13 | end 14 | 15 | config.filters = false 16 | 17 | index do 18 | column :name do |partner| 19 | link_to partner.name, admin_partner_path(partner) 20 | end 21 | column :links do |partner| 22 | link_to("Connections (#{partner.partner_connections_count})", 23 | admin_partner_partner_connections_path(partner)) 24 | end 25 | 26 | actions 27 | end 28 | 29 | show do |obj| 30 | panel "Connection Settings" do 31 | attributes_table_for obj do 32 | row :name 33 | row :api_key 34 | end 35 | end 36 | 37 | panel "Client Side Redirects" do 38 | attributes_table_for obj do 39 | row :success_url 40 | row :failure_url 41 | end 42 | end 43 | 44 | panel "Webhooks" do 45 | attributes_table_for obj do 46 | row :user_connected_webhook 47 | row :user_disconnected_webhook 48 | row :new_mail_webhook 49 | end 50 | end 51 | end 52 | 53 | form do |f| 54 | f.inputs "Details" do 55 | f.input :name 56 | f.input :api_key unless f.object.new_record? 57 | end 58 | 59 | f.inputs "Client Side Redirects" do 60 | f.input :success_url 61 | f.input :failure_url 62 | end 63 | 64 | f.inputs "Webhooks" do 65 | f.input :user_connected_webhook 66 | f.input :user_disconnected_webhook 67 | f.input :new_mail_webhook 68 | end 69 | f.actions 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /app/admin/partner_connection.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register PartnerConnection do 2 | belongs_to :partner 3 | 4 | permit_params :imap_provider_id, 5 | *Plain::PartnerConnection.connection_fields, 6 | *Oauth2::PartnerConnection.connection_fields 7 | 8 | controller do 9 | def create 10 | imap_provider = ImapProvider.find(params[:partner_connection][:imap_provider_id]) 11 | new_type = imap_provider.type.gsub("::ImapProvider", "::PartnerConnection") 12 | params[:partner_connection].merge!(:type => new_type) 13 | super 14 | end 15 | end 16 | 17 | breadcrumb do 18 | partner = Partner.find(params[:partner_id]) 19 | [ 20 | link_to("Partners", admin_partners_path), 21 | link_to(partner.name, admin_partner_path(partner)), 22 | link_to("Connections", admin_partner_partner_connections_path(partner)) 23 | ] 24 | end 25 | 26 | config.filters = false 27 | 28 | index do 29 | column "Imap Provider" do |obj| 30 | link_to obj.imap_provider.title, admin_partner_partner_connection_path(obj.partner, obj) 31 | end 32 | 33 | column "Links" do |obj| 34 | raw [ 35 | link_to("Connection Type", 36 | admin_imap_provider_path(obj)), 37 | link_to("Users (#{obj.users_count})", 38 | admin_partner_connection_users_path(obj)) 39 | ].join(", ") 40 | end 41 | actions 42 | end 43 | 44 | show do |obj| 45 | panel "Details" do 46 | attributes_table_for obj do 47 | row :imap_provider_code 48 | end 49 | end 50 | panel "Connection Settings" do 51 | attributes_table_for obj do 52 | obj.connection_fields.map do |field| 53 | row field 54 | end 55 | end 56 | end if obj.connection_fields.present? 57 | end 58 | 59 | form do |f| 60 | f.inputs "Details" do 61 | f.input :imap_provider, :label => "Auth Mechanism", 62 | :as => :select, :collection => ImapProvider.pluck(:title, :id) 63 | end if f.object.new_record? 64 | 65 | if !f.object.new_record? && f.object.connection_fields.present? 66 | f.inputs "Connection Settings" do 67 | f.object.connection_fields.each do |field| 68 | f.input field, :input_html => { :value => f.object.send(field) } 69 | end 70 | end 71 | end 72 | 73 | f.actions 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /app/admin/tracer_log.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register TracerLog do 2 | config.sort_order = "created_at_desc" 3 | 4 | # Only allow viewing. 5 | config.clear_action_items! 6 | actions :all, :except => [:new, :edit, :destroy] 7 | 8 | config.filters = false 9 | 10 | 11 | index do 12 | column :user 13 | column :uid do |obj| 14 | link_to obj.uid, admin_tracer_log_path(obj) 15 | end 16 | column :created_at 17 | column :detected_at 18 | column "Elapsed" do |obj| 19 | if obj.detected_at && obj.created_at 20 | seconds = obj.detected_at - obj.created_at 21 | "#{seconds.round(2)} s" 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/admin/transmit_log.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register TransmitLog do 2 | belongs_to :mail_log 3 | 4 | config.sort_order = "created_at_desc" 5 | 6 | # Only allow viewing. 7 | config.clear_action_items! 8 | actions :all, :except => [:new, :edit, :destroy] 9 | 10 | filter :response_code 11 | filter :response_body 12 | 13 | breadcrumb do 14 | mail_log = MailLog.find(params[:mail_log_id]) 15 | user = mail_log.user 16 | connection = user.partner_connection 17 | partner = connection.partner 18 | [ 19 | link_to("Partners", admin_partners_path), 20 | link_to(partner.name, admin_partner_path(partner)), 21 | link_to("Connections", admin_partner_partner_connections_path(partner)), 22 | link_to(connection.imap_provider.code, admin_partner_partner_connection_path(partner, connection)), 23 | link_to("Users", admin_partner_connection_users_path(connection)), 24 | link_to(user.email, admin_partner_connection_user_path(connection, user)), 25 | link_to("Mail Logs", admin_user_mail_logs_path(user)), 26 | link_to(mail_log.id, admin_user_mail_log_path(user, mail_log)), 27 | link_to("Transmit Logs", admin_mail_log_transmit_logs_path(mail_log)) 28 | ] 29 | end 30 | 31 | index do 32 | column :response_code 33 | column :response_body 34 | column :created_at 35 | actions 36 | end 37 | 38 | show do 39 | attributes_table do 40 | row :created_at 41 | row :response_code 42 | row :response_body 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/admin/user.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register User do 2 | belongs_to :partner_connection 3 | config.sort_order = "email_asc" 4 | permit_params :tag, :enable_tracer, 5 | *Plain::User.connection_fields, 6 | *Oauth2::User.connection_fields 7 | 8 | actions :all, :except => [:destroy] 9 | 10 | action_item :only => :show do 11 | if user.archived 12 | link_to('Restore User', restore_admin_partner_connection_user_path(params[:partner_connection_id], user.id)) 13 | else 14 | link_to('Archive User', archive_admin_partner_connection_user_path(params[:partner_connection_id], user.id)) 15 | end 16 | end 17 | 18 | member_action :archive, :method => :get do 19 | user = User.find(params[:id]) 20 | user.update_attributes!(:archived => true) 21 | redirect_to({:action => :show}, {:notice => "User archived!"}) 22 | end 23 | 24 | member_action :restore, :method => :get do 25 | user = User.find(params[:id]) 26 | user.update_attributes!(:archived => false) 27 | redirect_to({:action => :show}, {:notice => "User restored!"}) 28 | end 29 | 30 | breadcrumb do 31 | connection = PartnerConnection.find(params[:partner_connection_id]) 32 | partner = connection.partner 33 | [ 34 | link_to("Partners", admin_partners_path), 35 | link_to(partner.name, admin_partner_path(partner)), 36 | link_to("Connections", admin_partner_partner_connections_path(partner)), 37 | link_to(connection.imap_provider_code, 38 | admin_partner_partner_connection_path(partner, connection)), 39 | link_to("Users", 40 | admin_partner_connection_users_path(connection)) 41 | ] 42 | end 43 | 44 | filter :tag 45 | filter :email 46 | scope :active 47 | scope :archived 48 | scope :tracer 49 | 50 | index do 51 | column :tag do |obj| 52 | link_to obj.tag, admin_partner_connection_user_path(obj.connection, obj) 53 | end 54 | column :email do |obj| 55 | if obj.email 56 | link_to obj.email, admin_partner_connection_user_path(obj.connection, obj) 57 | end 58 | end 59 | column "Mail Logs", :sortable => :mail_logs_count do |obj| 60 | link_to("Mail Logs (#{obj.mail_logs_count})", 61 | admin_user_mail_logs_path(obj)) 62 | end 63 | column :connected_at 64 | column :last_login_at 65 | column :last_email_at 66 | column :tracer do |obj| 67 | "YES" if obj.enable_tracer 68 | end 69 | column :archived do |obj| 70 | "YES" if obj.archived 71 | end 72 | end 73 | 74 | show do |obj| 75 | panel "Details" do 76 | attributes_table_for obj do 77 | row :id 78 | row :tag 79 | row :connected_at 80 | row :last_login_at 81 | row :last_email_at 82 | row :last_uid 83 | row :last_uid_validity 84 | row :type 85 | row "Links" do 86 | link_to("Connect", new_users_connect_url(obj.signed_request_params)) + 87 | ", " + 88 | link_to("Disconnect", new_users_disconnect_url(obj.signed_request_params)) 89 | # [ 90 | # ].join(", ") 91 | end 92 | row :enable_tracer 93 | row :archived 94 | end 95 | end 96 | 97 | panel "Connection Settings" do 98 | attributes_table_for obj do 99 | obj.connection_fields.map do |field| 100 | row field 101 | end 102 | end 103 | end if obj.connection_fields.present? 104 | end 105 | 106 | form do |f| 107 | f.inputs "Details" do 108 | f.input :tag 109 | f.input :enable_tracer 110 | end 111 | 112 | if !f.object.new_record? && f.object.connection_fields.present? 113 | f.inputs "Connection Settings" do 114 | f.object.connection_fields.each do |field| 115 | f.input field, :input_html => { :value => f.object.send(field) } 116 | end 117 | end 118 | end 119 | 120 | f.actions 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js.coffee: -------------------------------------------------------------------------------- 1 | #= require active_admin/base 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/active_admin.css.scss: -------------------------------------------------------------------------------- 1 | // SASS variable overrides must be declared before loading up Active Admin's styles. 2 | // 3 | // To view the variables that Active Admin provides, take a look at 4 | // `app/assets/stylesheets/active_admin/mixins/_variables.css.scss` in the 5 | // Active Admin source. 6 | // 7 | // For example, to change the sidebar width: 8 | // $sidebar-width: 242px; 9 | 10 | $body-background-color: #FFF !default; 11 | $primary-color: #8B1B89 !default; 12 | $secondary-color: #ffffff !default; 13 | $text-color: #333333 !default; 14 | $table-stripe-color: lighten($primary-color, 66%) !default; 15 | 16 | // Active Admin's got SASS! 17 | @import "active_admin/mixins"; 18 | @import "active_admin/base"; 19 | 20 | body { 21 | font-size: 14px; 22 | } 23 | 24 | th { 25 | white-space: nowrap; 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/controllers/api/v1/connections_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::ConnectionsController < ApplicationController 2 | layout "blank" 3 | respond_to :json 4 | skip_before_action :verify_authenticity_token 5 | before_action :default_format_json 6 | before_action :load_partner 7 | before_action :load_imap_provider, :only => [:create, :update, :show, :destroy] 8 | before_action :load_connection, :only => [:update, :show, :destroy] 9 | 10 | attr_accessor :partner, :imap_provider, :connection 11 | 12 | def index 13 | @connections = self.partner.connections 14 | end 15 | 16 | def create 17 | self.connection = self.partner.connections.where(:imap_provider_id => imap_provider.id).build 18 | self.connection.update_attributes!(connection_params) 19 | render :show 20 | rescue ActiveRecord::RecordInvalid => e 21 | render :status => :bad_request, :text => e.to_s 22 | end 23 | 24 | def update 25 | self.connection.update_attributes!(connection_params) 26 | render :show 27 | rescue ActiveRecord::RecordInvalid => e 28 | render :status => :bad_request, :text => e.to_s 29 | end 30 | 31 | def show 32 | # Pass. 33 | end 34 | 35 | def destroy 36 | self.connection.destroy 37 | render :status => :no_content, :text => "Deleted connection." 38 | end 39 | 40 | private 41 | 42 | def default_format_json 43 | request.format = "json" unless params[:format] 44 | end 45 | 46 | def load_partner 47 | api_key = request.headers['x-api-key'] || params[:api_key] 48 | self.partner = Partner.find_by_api_key(api_key) 49 | if self.partner.nil? 50 | render :status => :not_found, :text => "Partner not found. Check your api_key." 51 | end 52 | end 53 | 54 | def load_imap_provider 55 | code = params[:imap_provider_code] 56 | self.imap_provider = ImapProvider.find_by_code(code) 57 | if self.imap_provider.nil? 58 | render :status => :not_found, :text => "Imap Provider not found for '#{code}'." 59 | end 60 | end 61 | 62 | def load_connection 63 | self.connection = self.partner.connections.where(:imap_provider_id => imap_provider.id).first 64 | if self.connection.nil? 65 | render :status => :not_found, :text => "Connection not found." 66 | end 67 | end 68 | 69 | def connection_params 70 | if self.connection 71 | params.permit(self.connection.connection_fields) 72 | else 73 | params.permit() 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | layout "blank" 3 | respond_to :json 4 | skip_before_action :verify_authenticity_token 5 | before_action :default_format_json 6 | before_action :load_partner 7 | before_action :load_imap_provider 8 | before_action :load_connection 9 | before_action :load_user, :only => [:update, :show, :destroy] 10 | 11 | attr_accessor :partner, :imap_provider, :connection, :user 12 | 13 | def index 14 | @users = self.connection.users.order(:email) 15 | end 16 | 17 | def create 18 | self.user = self.connection.new_typed_user 19 | self.user.update_attributes!(user_params) 20 | render :show 21 | rescue ActiveRecord::RecordInvalid => e 22 | render :status => :bad_request, :text => e.to_s 23 | end 24 | 25 | def update 26 | self.user.update_attributes!(user_params) 27 | render :show 28 | rescue ActiveRecord::RecordInvalid => e 29 | render :status => :bad_request, :text => e.to_s 30 | end 31 | 32 | def show 33 | # pass 34 | end 35 | 36 | def destroy 37 | self.user.update_attributes(:archived => true) 38 | render :status => :no_content, :text => "Archived user." 39 | end 40 | 41 | private 42 | 43 | def default_format_json 44 | request.format = "json" unless params[:format] 45 | end 46 | 47 | def load_partner 48 | api_key = request.headers['x-api-key'] || params[:api_key] 49 | self.partner = Partner.find_by_api_key(api_key) 50 | if partner.nil? 51 | render :status => :not_found, :text => "Partner not found. Check your api_key." 52 | end 53 | end 54 | 55 | def load_imap_provider 56 | code = params[:connection_imap_provider_code] 57 | self.imap_provider = ImapProvider.find_by_code(code) 58 | if self.imap_provider.nil? 59 | render :status => :not_found, :text => "Imap Provider not found for '#{code}'." 60 | end 61 | end 62 | 63 | def load_connection 64 | self.connection = self.partner.connections.find_by_imap_provider_id(self.imap_provider.id) 65 | if self.connection.nil? 66 | render :status => :not_found, :text => "Connection not found." 67 | end 68 | end 69 | 70 | def load_user 71 | tag = params[:tag] 72 | self.user = self.connection.users.find_by_tag(tag) 73 | if self.user.nil? 74 | render :status => :not_found, :text => "User not found for '#{tag}'." 75 | end 76 | end 77 | 78 | def user_params 79 | if self.user 80 | params.permit([:tag, :email, :archived] + self.user.connection_fields) 81 | else 82 | params.permit([:tag, :email, :archived]) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | before_action :ensure_secure 7 | 8 | def ensure_secure 9 | if !request.ssl? && Rails.env.production? 10 | redirect_to request.original_url.gsub(/^http:/, "https:") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/link_rel.rb: -------------------------------------------------------------------------------- 1 | module LinkRel 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | def self.link_rel(tag, url) 6 | @links ||= [] 7 | @links << %(<#{url}; rel="#{tag}") 8 | headers['Link'] = @links.join(', ') if links.present? 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/users/base_callback_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::BaseCallbackController < ApplicationController 2 | before_action :load_user 3 | before_action :validate_signature 4 | 5 | attr_accessor :user 6 | 7 | def new 8 | # Store user in session. 9 | self.user.signed_request_params.each do |key, value| 10 | session[key] = value 11 | end 12 | 13 | # Set up the callback URLs. 14 | session[:success_url] = params[:success] || partner.success_url 15 | session[:failure_url] = params[:failure] || partner.failure_url 16 | 17 | apply_helper 18 | end 19 | 20 | def callback 21 | apply_helper 22 | end 23 | 24 | private 25 | 26 | def apply_helper 27 | helper = self.user.imap_provider.helper_for(params[:action]) 28 | self.send(helper) 29 | end 30 | 31 | def load_user(user_id = nil) 32 | # Load from params or for a specific auth method. 33 | self.user = User.find_by_id(user_id || params[:user_id] || session[:user_id]) 34 | if self.user.nil? 35 | render :status => :not_found, :text => "User not found." 36 | end 37 | end 38 | 39 | def validate_signature(options = {}) 40 | # Validate from params or for a specific auth method. 41 | is_valid = 42 | self.user.valid_signature?(options) || 43 | self.user.valid_signature?(params) || 44 | self.user.valid_signature?(session) 45 | 46 | if !is_valid 47 | render :status => :not_found, :text => "User not found." 48 | end 49 | end 50 | 51 | def connection 52 | self.user.connection 53 | end 54 | 55 | def partner 56 | self.user.connection.partner 57 | end 58 | 59 | def imap_provider 60 | self.user.connection.imap_provider 61 | end 62 | 63 | def redirect_to_success_url 64 | redirect_to session[:success_url] 65 | end 66 | 67 | def redirect_to_failure_url 68 | redirect_to session[:failure_url] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/controllers/users/connects_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::ConnectsController < Users::BaseCallbackController 2 | include Plain::ConnectsHelper 3 | include Oauth2::ConnectsHelper 4 | 5 | # The new and callback actions are contained in 6 | # Users::BaseCallbackController, which itself calls helpers 7 | # according to the authentication type. 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/users/disconnects_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::DisconnectsController < Users::BaseCallbackController 2 | include Plain::DisconnectsHelper 3 | include Oauth2::DisconnectsHelper 4 | 5 | # The new and callback actions are contained in 6 | # Users::BaseCallbackController, which itself calls helpers 7 | # according to the authentication type. 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/webhook_test_controller.rb: -------------------------------------------------------------------------------- 1 | class WebhookTestController < ApplicationController 2 | layout "blank" 3 | skip_before_action :verify_authenticity_token 4 | 5 | attr_accessor :json_params, :user 6 | 7 | def new_mail 8 | Log.info request.body 9 | render :status => :ok, :text => "OK" 10 | end 11 | 12 | def user_connected 13 | Log.info request.body 14 | render :status => :ok, :text => "OK" 15 | end 16 | 17 | def user_disconnected 18 | Log.info request.body 19 | render :status => :ok, :text => "OK" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def array_to_hash(values) 3 | hash = {} 4 | values.each do |k,v| 5 | hash[k] = v 6 | end 7 | return hash 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/oauth2/connects_helper.rb: -------------------------------------------------------------------------------- 1 | module Oauth2::ConnectsHelper 2 | BadRequestError = Class.new(StandardError) 3 | 4 | attr_accessor :oauth2_token 5 | 6 | def oauth2_new_helper 7 | # Construct the client. 8 | client = OAuth2::Client.new( 9 | connection.oauth2_client_id, 10 | connection.oauth2_client_secret_secure, 11 | :site => imap_provider.oauth2_site, 12 | :authorize_url => imap_provider.oauth2_authorize_url) 13 | 14 | # Construct the auth url. 15 | auth_url = client.auth_code.authorize_url( 16 | :redirect_uri => callback_users_connect_url(), 17 | :response_type => imap_provider.oauth2_response_type, 18 | :state => "", 19 | :scope => imap_provider.oauth2_scope, 20 | :access_type => imap_provider.oauth2_access_type, 21 | :approval_prompt => imap_provider.oauth2_approval_prompt) 22 | 23 | # Redirect. 24 | redirect_to auth_url 25 | end 26 | 27 | def oauth2_callback_helper 28 | # Exchange the code for a refresh token. 29 | # https://developers.google.com/accounts/docs/OAuth2WebServer 30 | client = OAuth2::Client.new( 31 | connection.oauth2_client_id, 32 | connection.oauth2_client_secret_secure, 33 | :site => imap_provider.oauth2_site, 34 | :token_url => imap_provider.oauth2_token_url) 35 | 36 | self.oauth2_token = client.auth_code.get_token( 37 | params[:code], 38 | :redirect_uri => callback_users_connect_url()) 39 | 40 | user.update_attributes!( 41 | :email => oauth2_email, 42 | :oauth2_refresh_token => oauth2_token.refresh_token, 43 | :connected_at => Time.now) 44 | 45 | begin 46 | CallUserConnectedWebhook.new(user).run 47 | rescue => e 48 | CallUserConnectedWebhook.new(user).delay.run 49 | end 50 | 51 | redirect_to_success_url 52 | rescue => e 53 | Log.exception(e) 54 | redirect_to_failure_url 55 | end 56 | 57 | def oauth2_email 58 | method = "#{imap_provider.code.downcase}_email".to_sym 59 | send(method) 60 | end 61 | 62 | def gmail_oauth2_email 63 | # Get the google email address. 64 | data = JSON.parse(oauth2_token.get("https://www.googleapis.com/userinfo/email?alt=json").body) 65 | return data["data"]["email"] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/helpers/oauth2/disconnects_helper.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Oauth2::DisconnectsHelper 4 | def oauth2_new_helper 5 | # Disconnect the user. Assume that this succeeds. 6 | token = self.user.oauth2_refresh_token_secure || "" 7 | url = "https://accounts.google.com/o/oauth2/revoke?token=#{URI.escape(token)}" 8 | Net::HTTP.get_response(URI(url)) 9 | 10 | # Throw away our credentials. 11 | self.user.update_attributes!( 12 | :email => nil, 13 | :oauth2_refresh_token => nil, 14 | :connected_at => nil) 15 | 16 | begin 17 | CallUserDisconnectedWebhook.new(user).run 18 | rescue => e 19 | CallUserDisconnectedWebhook.new(user).delay.run 20 | end 21 | 22 | # Redirect. 23 | redirect_to_success_url 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/helpers/plain/connects_helper.rb: -------------------------------------------------------------------------------- 1 | module Plain::ConnectsHelper 2 | def plain_new_helper 3 | raise :todo 4 | end 5 | 6 | def plain_callback_helper 7 | raise :todo 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/plain/disconnects_helper.rb: -------------------------------------------------------------------------------- 1 | module Plain::DisconnectsHelper 2 | def plain_new_helper 3 | raise :todo 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/interactors/base_webhook.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | require 'net/imap' 3 | 4 | class BaseWebhook 5 | private unless Rails.env.test? 6 | 7 | def calculate_signature(api_key, uid, timestamp) 8 | digest = OpenSSL::Digest.new('sha256') 9 | return OpenSSL::HMAC.hexdigest(digest, api_key, "#{timestamp}#{uid}") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/interactors/call_new_mail_webhook.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class CallNewMailWebhook < BaseWebhook 4 | attr_accessor :mail_log, :envelope, :raw_eml 5 | 6 | def initialize(mail_log, envelope, raw_eml) 7 | self.mail_log = mail_log 8 | self.envelope = envelope 9 | self.raw_eml = raw_eml 10 | end 11 | 12 | def run 13 | user = mail_log.user 14 | partner = user.partner_connection.partner 15 | 16 | if partner.new_mail_webhook.blank? 17 | return false 18 | end 19 | 20 | # Assemble the payload. 21 | data = { 22 | :timestamp => Time.now.to_i, 23 | :sha1 => mail_log.sha1, 24 | :user_tag => user.tag, 25 | :imap_provider_code => user.connection.imap_provider_code, 26 | :envelope => envelope, 27 | :rfc822 => raw_eml 28 | } 29 | data[:signature] = calculate_signature(partner.api_key, data[:sha1], data[:timestamp]) 30 | 31 | # START DEBUGGING! 32 | begin 33 | envelope.to_json 34 | rescue => e 35 | Log.info("Problem converting to JSON:\n#{envelope}.") 36 | end 37 | 38 | begin 39 | raw_eml.to_json 40 | rescue => e 41 | Log.info("Problem converting to JSON:\n#{raw_eml}.") 42 | end 43 | # END DEBUGGING! 44 | 45 | # Post the data 46 | begin 47 | transmit_log = mail_log.transmit_logs.create() 48 | 49 | # Post the data. 50 | webhook = RestClient::Resource.new(partner.new_mail_webhook) 51 | response = Timeout::timeout(30) do 52 | webhook.post(data.to_json, :content_type => :json, :accept => :json) 53 | end 54 | 55 | # Update the transmit log record. 56 | transmit_log.update_attributes!(:response_code => response.code.to_i, 57 | :response_body => response.to_s.slice(0, 1024)) 58 | 59 | Log.librato(:count, 'app.call_new_mail_webhook.count', 1) 60 | return true 61 | rescue RestClient::Forbidden => e 62 | response = e.response 63 | transmit_log.update_attributes!(:response_code => response.code.to_i, 64 | :response_body => response.to_s.slice(0, 1024)) 65 | 66 | # The server understood the request but refused it. Mark the 67 | # user as archived, but only if it's not a tracer user. 68 | if !user.enable_tracer 69 | user.update_attributes!(:archived => true) 70 | end 71 | rescue RestClient::Exception => e 72 | response = e.response 73 | transmit_log.update_attributes!(:response_code => response.code.to_i, 74 | :response_body => response.to_s.slice(0, 1024)) 75 | raise e 76 | rescue => e 77 | transmit_log.update_attributes!(:response_code => "ERROR", 78 | :response_body => e.to_s.slice(0, 1024)) 79 | raise e 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/interactors/call_user_connected_webhook.rb: -------------------------------------------------------------------------------- 1 | class CallUserConnectedWebhook < BaseWebhook 2 | attr_accessor :user 3 | 4 | def initialize(user) 5 | self.user = user 6 | end 7 | 8 | def run 9 | partner = user.partner_connection.partner 10 | 11 | if partner.user_connected_webhook.blank? 12 | return false 13 | end 14 | 15 | # Assemble the payload. 16 | data = { 17 | :timestamp => Time.now.to_i, 18 | :sha1 => Digest::SHA1.hexdigest(user.tag), 19 | :user_tag => user.tag, 20 | :imap_provider_code => user.connection.imap_provider_code, 21 | :email => user.email 22 | } 23 | data[:signature] = calculate_signature(partner.api_key, data[:sha1], data[:timestamp]) 24 | 25 | # Post the data. 26 | begin 27 | webhook = RestClient::Resource.new(partner.user_connected_webhook) 28 | response = Timeout::timeout(30) do 29 | webhook.post(data.to_json, :content_type => :json, :accept => :json) 30 | end 31 | Log.librato(:count, 'app.call_user_connected_webhook.count', 1) 32 | rescue RestClient::Forbidden => e 33 | # The server understood the request but refused it. Mark the 34 | # user as archived, but only if it's not a tracer user. 35 | if !user.enable_tracer 36 | user.update_attributes!(:archived => true) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/interactors/call_user_disconnected_webhook.rb: -------------------------------------------------------------------------------- 1 | class CallUserDisconnectedWebhook < BaseWebhook 2 | attr_accessor :user 3 | 4 | def initialize(user) 5 | self.user = user 6 | end 7 | 8 | def run 9 | partner = user.partner_connection.partner 10 | 11 | if partner.user_disconnected_webhook.blank? 12 | return false 13 | end 14 | 15 | # Assemble the payload. 16 | data = { 17 | :timestamp => Time.now.to_i, 18 | :sha1 => Digest::SHA1.hexdigest(user.tag), 19 | :user_tag => user.tag, 20 | :imap_provider_code => user.connection.imap_provider_code 21 | } 22 | data[:signature] = calculate_signature(partner.api_key, data[:sha1], data[:timestamp]) 23 | 24 | begin 25 | # Post the data. 26 | webhook = RestClient::Resource.new(partner.user_disconnected_webhook) 27 | response = Timeout::timeout(30) do 28 | webhook.post(data.to_json, :content_type => :json, :accept => :json) 29 | end 30 | Log.librato(:count, 'app.call_user_disconnected_webhook.count', 1) 31 | rescue RestClient::Forbidden => e 32 | # The server understood the request but refused it. Mark the 33 | # user as archived, but only if it's not a tracer user. 34 | if !user.enable_tracer 35 | user.update_attributes!(:archived => true) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/interactors/schedule_tracer_emails.rb: -------------------------------------------------------------------------------- 1 | class ScheduleTracerEmails 2 | attr_accessor :user, :num_tracers 3 | 4 | def initialize(user, num_tracers) 5 | self.user = user 6 | self.num_tracers = num_tracers 7 | end 8 | 9 | def run 10 | num_tracers.times.each do |n| 11 | send_tracer_to_user(user) 12 | end 13 | end 14 | 15 | def send_tracer_to_user(user) 16 | # Deliver the mail. 17 | uid = SecureRandom.hex(10) 18 | mail = TracerMailer.tracer_email(user, uid) 19 | user.connection.imap_provider.authenticate_smtp(mail, user) 20 | mail.deliver 21 | Log.librato(:count, 'app.schedule_tracer_email.count', 1) 22 | 23 | # Log the tracer. 24 | user.tracer_logs.create!(:uid => uid) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/app/mailers/.keep -------------------------------------------------------------------------------- /app/mailers/tracer_mailer.rb: -------------------------------------------------------------------------------- 1 | class TracerMailer < ActionMailer::Base 2 | def tracer_email(user, uid) 3 | @uid = uid 4 | mail(:from => user.email, 5 | :to => user.email, 6 | :subject => "TRACER: #{uid}") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/app/models/.keep -------------------------------------------------------------------------------- /app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | class AdminUser < ActiveRecord::Base 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable and :omniauthable 4 | devise :database_authenticatable, :trackable, :validatable, :lockable 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/auth_method_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthMethodHelper 2 | def auth_method_plain? 3 | /^plain$/i.match(self.auth_method) 4 | end 5 | 6 | def auth_method_oauth2? 7 | /^oauth2$/i.match(self.oauth_method) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/concerns/connection_fields.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module ConnectionFields 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | @connection_fields = [] 7 | 8 | def self.encrypt(value) 9 | if Rails.application.config.encryption_cipher && value.present? 10 | Rails.application.config.encryption_cipher.encrypt(value) 11 | else 12 | value 13 | end 14 | end 15 | 16 | def self.decrypt(value) 17 | begin 18 | if Rails.application.config.encryption_cipher && value.present? 19 | Rails.application.config.encryption_cipher.decrypt(value) 20 | else 21 | value 22 | end 23 | rescue 24 | value 25 | end 26 | end 27 | 28 | def self.connection_field(field, options = {}) 29 | @connection_fields ||= [] 30 | @connection_fields << field 31 | 32 | # Maybe validate presence. 33 | if options[:required] 34 | validates_presence_of(field) 35 | end 36 | 37 | # Maybe obscure the actual value. 38 | if options[:secure] 39 | define_method(field) do |secure = false| 40 | if !secure && self[field].present? 41 | "- encrypted -" 42 | else 43 | self.class.decrypt(super()) 44 | end 45 | end 46 | 47 | define_method("#{field}_secure".to_sym) do 48 | self.send(field, true) 49 | end 50 | 51 | define_method("#{field}=".to_sym) do |value| 52 | if value != self.send(field) 53 | super(self.class.encrypt(value)) 54 | end 55 | end 56 | end 57 | end 58 | 59 | def self.connection_fields 60 | @connection_fields || [] 61 | end 62 | 63 | def connection_fields 64 | self.class.connection_fields 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/models/delayed_job.rb: -------------------------------------------------------------------------------- 1 | class DelayedJob < ActiveRecord::Base 2 | # Run all existing delayed_job records. Log errors, return true if 3 | # everything was run. 4 | def self.flush 5 | count = 0 6 | while job = Delayed::Job.where("locked_at IS NULL").first do 7 | count += 1 8 | raise "Delayed Job loop?" if count > 10 9 | begin 10 | job.invoke_job 11 | job.destroy 12 | rescue => e 13 | print "Problem processing delayed job:\n#{job.to_yaml}" 14 | raise e 15 | end 16 | end 17 | return true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/imap_daemon_heartbeat.rb: -------------------------------------------------------------------------------- 1 | class ImapDaemonHeartbeat < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/imap_provider.rb: -------------------------------------------------------------------------------- 1 | class ImapProvider < ActiveRecord::Base 2 | include ConnectionFields 3 | has_many :partner_connections 4 | 5 | def display_name 6 | self.code 7 | end 8 | 9 | # Public: Single Table Inheritance helper. Returns the correct 10 | # inherited class depending on the ImapProvider class. 11 | # 12 | # Usage: 13 | # 14 | # imap_provider = Oauth2::ImapProvider.new 15 | # imap_provider.class_for(User) => Oauth2::User 16 | # 17 | # Returns a class. 18 | def class_for(c) 19 | (self.class.parent_name + "::" + c.base_class.name).constantize 20 | end 21 | 22 | # Public: Single Table Inheritance helper. Returns the name of a 23 | # helper method depending on the ImapProvider class. 24 | # 25 | # Usage: 26 | # 27 | # imap_provider = Oauth2::ImapProvider.new 28 | # imap_provider.helper_for(:connects, :new) => :oauth2_new_connects_helper 29 | # 30 | # Returns a symbol. 31 | def helper_for(action) 32 | "#{self.class.parent_name.underscore}_#{action}_helper".to_sym 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/mail_log.rb: -------------------------------------------------------------------------------- 1 | class MailLog < ActiveRecord::Base 2 | belongs_to :user, :counter_cache => true 3 | has_many :transmit_logs, :dependent => :destroy 4 | end 5 | -------------------------------------------------------------------------------- /app/models/oauth2/imap_provider.rb: -------------------------------------------------------------------------------- 1 | require 'xoauth2_authenticator' 2 | 3 | class Oauth2::ImapProvider < ImapProvider 4 | include ConnectionFields 5 | 6 | connection_field :oauth2_grant_type, :required => true 7 | connection_field :oauth2_scope, :required => true 8 | connection_field :oauth2_site, :required => true 9 | connection_field :oauth2_token_method, :required => true 10 | connection_field :oauth2_token_url, :required => true 11 | connection_field :oauth2_authorize_url, :required => true 12 | connection_field :oauth2_response_type, :required => true 13 | connection_field :oauth2_access_type, :required => true 14 | connection_field :oauth2_approval_prompt, :required => true 15 | 16 | def authenticate_imap(client, user) 17 | client.authenticate('XOAUTH2', user.email, _access_token(user)) 18 | end 19 | 20 | def authenticate_smtp(mail, user) 21 | mail.delivery_method.settings.merge!( 22 | :address => smtp_host, 23 | :port => smtp_port, 24 | :domain => smtp_domain, 25 | :user_name => user.email, 26 | :password => _access_token(user), 27 | :authentication => :xoauth2, 28 | :enable_starttls_auto => smtp_enable_starttls_auto 29 | ) 30 | end 31 | 32 | private 33 | 34 | def _access_token(user) 35 | partner_connection = user.connection 36 | 37 | oauth_client = OAuth2::Client.new( 38 | partner_connection.oauth2_client_id, 39 | partner_connection.oauth2_client_secret_secure, 40 | { 41 | :site => oauth2_site, 42 | :token_url => oauth2_token_url, 43 | :token_method => oauth2_token_method.to_sym, 44 | :grant_type => oauth2_grant_type, 45 | :scope => oauth2_scope 46 | }) 47 | 48 | oauth2_access_token = oauth_client.get_token( 49 | { 50 | :client_id => partner_connection.oauth2_client_id, 51 | :client_secret => partner_connection.oauth2_client_secret_secure, 52 | :refresh_token => user.oauth2_refresh_token_secure, 53 | :grant_type => oauth2_grant_type 54 | }) 55 | 56 | oauth2_access_token.token 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/models/oauth2/partner_connection.rb: -------------------------------------------------------------------------------- 1 | class Oauth2::PartnerConnection < PartnerConnection 2 | include ConnectionFields 3 | connection_field :oauth2_client_id, :required => true 4 | connection_field :oauth2_client_secret, :required => true, :secure => true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/oauth2/user.rb: -------------------------------------------------------------------------------- 1 | class Oauth2::User < User 2 | include ConnectionFields 3 | before_save :update_connected_at 4 | 5 | connection_field :email 6 | connection_field :oauth2_refresh_token, :secure => true 7 | 8 | def update_connected_at 9 | if email.present? && oauth2_refresh_token.present? 10 | self.connected_at ||= Time.now 11 | else 12 | self.connected_at = nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/partner.rb: -------------------------------------------------------------------------------- 1 | class Partner < ActiveRecord::Base 2 | # Magic. 3 | before_save :ensure_api_key 4 | 5 | # Relations 6 | has_many :partner_connections, :dependent => :destroy 7 | alias_method :connections, :partner_connections 8 | 9 | # Validations 10 | validates :name, :presence => true 11 | 12 | def ensure_api_key 13 | self.api_key ||= SecureRandom.hex(10) 14 | end 15 | 16 | # Public: Create a new connection that bases it's type on the 17 | # provided imap_provider. In other words, if this is an 18 | # Oauth2::ImapProvider, then return an Oauth2::ImapProvider. 19 | def new_typed_connection(imap_provider) 20 | connection = imap_provider.class_for(PartnerConnection).new 21 | self.connections << connection 22 | connection 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/partner_connection.rb: -------------------------------------------------------------------------------- 1 | class PartnerConnection < ActiveRecord::Base 2 | include ConnectionFields 3 | 4 | # Magic. 5 | before_validation :fix_type 6 | 7 | # Relations. 8 | belongs_to :partner, :counter_cache => true 9 | belongs_to :imap_provider, :counter_cache => true 10 | has_many :users, :dependent => :destroy 11 | 12 | # Validation. 13 | validates_presence_of :imap_provider_id 14 | validates_uniqueness_of :imap_provider_id, :scope => :partner_id 15 | 16 | # Public: Used by ActiveAdmin. 17 | def display_name 18 | self.imap_provider_code 19 | end 20 | 21 | def imap_provider_code 22 | self.imap_provider.code 23 | end 24 | 25 | # Public: Create a new user that bases it's type on the 26 | # PartnerConnection type. In other words, if this is an 27 | # Oauth2::PartnerConnection, then return an Oauth2::User. 28 | def new_typed_user 29 | user = self.imap_provider.class_for(User).new 30 | self.users << user 31 | user 32 | end 33 | 34 | private 35 | 36 | # Private: Automatically set the STI type based on the imap_provider. 37 | def fix_type 38 | self.type ||= self.imap_provider.class_for(PartnerConnection).to_s 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/models/plain/imap_provider.rb: -------------------------------------------------------------------------------- 1 | class Plain::ImapProvider < ImapProvider 2 | include ConnectionFields 3 | 4 | def authenticate_smtp(mail, user) 5 | mail.delivery_method.settings.merge!( 6 | :address => smtp_host, 7 | :port => smtp_port, 8 | :domain => smtp_domain, 9 | :user_name => user.login_username, 10 | :password => user.login_password, 11 | :authentication => :plain, 12 | :enable_starttls_auto => enable_starttls_auto 13 | ) 14 | 15 | client.login(user.login_username, user.login_password_secure) 16 | end 17 | 18 | def authenticate_imap(client, user) 19 | client.login(user.login_username, user.login_password_secure) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/plain/partner_connection.rb: -------------------------------------------------------------------------------- 1 | class Plain::PartnerConnection < PartnerConnection 2 | include ConnectionFields 3 | end 4 | -------------------------------------------------------------------------------- /app/models/plain/user.rb: -------------------------------------------------------------------------------- 1 | class Plain::User < User 2 | include ConnectionFields 3 | before_save :update_connected_at 4 | 5 | connection_field :login_username 6 | connection_field :login_password, :secure => true 7 | 8 | def update_connected_at 9 | if login_username.present? && login_password.present? 10 | self.connected_at ||= Time.now 11 | else 12 | self.connected_at = nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/tracer_log.rb: -------------------------------------------------------------------------------- 1 | class TracerLog < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/models/transmit_log.rb: -------------------------------------------------------------------------------- 1 | class TransmitLog < ActiveRecord::Base 2 | belongs_to :mail_log, :counter_cache => true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include ConnectionFields 3 | 4 | # Magic. 5 | before_validation :fix_type 6 | 7 | # Scopes. 8 | scope :active, proc { where(:archived => false).where.not(:last_email_at => nil) } 9 | scope :archived, proc { where(:archived => true) } 10 | scope :tracer, proc { where(:enable_tracer => true) } 11 | 12 | # Relations. 13 | has_many :mail_logs, :dependent => :destroy 14 | has_many :tracer_logs, :dependent => :destroy 15 | belongs_to :partner_connection, :counter_cache => true 16 | alias_method :connection, :partner_connection 17 | 18 | # Validations. 19 | validates_presence_of :tag 20 | validates_uniqueness_of :tag, :case_sensitive => false, 21 | :scope => :partner_connection_id, 22 | :conditions => -> { where.not(:archived => true) }, 23 | :if => Proc.new { |object| object.tag_changed? } 24 | 25 | validates_uniqueness_of :email, :case_sensitive => false, 26 | :scope => :partner_connection_id, 27 | :allow_nil => true, 28 | :conditions => -> { where.not(:archived => true) }, 29 | :if => Proc.new { |object| object.email_changed? } 30 | 31 | def imap_provider 32 | self.connection.imap_provider 33 | end 34 | 35 | # Public: Calculate a timestamped signature. Used to sign redirect 36 | # URLs. Returns a hash. 37 | def signed_request_params(timestamp = nil) 38 | timestamp ||= Time.now.to_i 39 | data = "#{self.id} - #{timestamp} - #{self.connection.partner.api_key}" 40 | { 41 | 'user_id' => id, 42 | 'ts' => timestamp, 43 | 'sig' => Digest::SHA1.hexdigest(data).slice(0, 10) 44 | } 45 | end 46 | 47 | # Public: Verify a timestamp signature. 48 | def valid_signature?(params) 49 | (Time.at(params['ts'].to_i) > 30.minutes.ago) && 50 | params['sig'] == signed_request_params(params['ts'])['sig'] 51 | end 52 | 53 | private 54 | 55 | # Private: Automatically set the STI type based on the imap_provider. 56 | def fix_type 57 | self.type ||= self.partner_connection.imap_provider.class_for(User).to_s 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/processes/common/csv_log.rb: -------------------------------------------------------------------------------- 1 | class Common::CsvLog 2 | include Common::Stoppable 3 | include Common::WrappedThread 4 | 5 | attr_accessor :log_path 6 | attr_accessor :log_filehandle 7 | attr_accessor :log_queue 8 | attr_accessor :log_thread 9 | 10 | def initialize(log_path) 11 | init_stoppable 12 | self.log_path = log_path 13 | self.log_queue = Queue.new 14 | self.log_thread = wrapped_thread do 15 | _thread_runner 16 | end 17 | end 18 | 19 | def log(*values) 20 | self.log_queue << values 21 | end 22 | 23 | private 24 | 25 | def _thread_runner 26 | self.log_filehandle = File.open(log_path, "w") 27 | while running? 28 | _drain_queue 29 | sleep 0.1 30 | end 31 | _drain_queue 32 | _close_file 33 | end 34 | 35 | def _drain_queue 36 | while true 37 | # Don't block, otherwise we can't exit. 38 | values = log_queue.pop(true) 39 | log_filehandle.write(values.join(",") + "\n") 40 | end 41 | rescue ThreadError => e 42 | # Thrown when queue is empty. 43 | log_filehandle.flush() 44 | end 45 | 46 | def _close_file 47 | log_filehandle.close 48 | rescue IOError 49 | # May fire if we've already closed the stream elsewhere. 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/processes/common/db_connection.rb: -------------------------------------------------------------------------------- 1 | module Common::DbConnection 2 | def db_config 3 | ActiveRecord::Base.configurations[Rails.env] || 4 | Rails.application.config.database_configuration[Rails.env] 5 | end 6 | 7 | def set_db_connection_pool_size(size) 8 | ActiveRecord::Base.connection_pool.disconnect! 9 | ActiveSupport.on_load(:active_record) do 10 | config = ActiveRecord::Base.configurations[Rails.env] || 11 | Rails.application.config.database_configuration[Rails.env] 12 | config['pool'] = size 13 | ActiveRecord::Base.establish_connection(config) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/processes/common/light_sleep.rb: -------------------------------------------------------------------------------- 1 | module Common::LightSleep 2 | include Common::Stoppable 3 | 4 | def light_sleep(seconds = nil) 5 | now = Time.now 6 | while running? 7 | break if seconds.present? && ((Time.now - now) >= seconds) 8 | sleep 1 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/processes/common/stoppable.rb: -------------------------------------------------------------------------------- 1 | module Common::Stoppable 2 | def init_stoppable 3 | @stop = false 4 | @stop_lock = Mutex.new 5 | end 6 | 7 | def trap_signals 8 | Signal.trap("INT") do self.stop! end 9 | Signal.trap("TERM") do self.stop! end 10 | end 11 | 12 | def stop! 13 | @stop_lock.synchronize do 14 | @stop = true 15 | end 16 | end 17 | 18 | def running? 19 | @stop_lock.synchronize do 20 | @stop != true 21 | end 22 | end 23 | 24 | def stopping? 25 | @stop_lock.synchronize do 26 | @stop == true 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/processes/common/worker_pool.rb: -------------------------------------------------------------------------------- 1 | module Common::WorkerPool 2 | include Common::Stoppable 3 | include Common::WrappedThread 4 | include Common::DbConnection 5 | 6 | attr_accessor :worker_rhash 7 | attr_accessor :work_queues, :work_queues_lock 8 | attr_accessor :worker_threads, :worker_threads_lock 9 | attr_accessor :work_queue_latency 10 | 11 | def init_worker_pool 12 | self.work_queues = [] 13 | self.work_queues_lock = Mutex.new 14 | self.worker_threads = [] 15 | self.worker_threads_lock = Mutex.new 16 | self.worker_rhash = ImapClient::RendezvousHash.new 17 | end 18 | 19 | # Public: Start a number of worker threads and begin processing 20 | # scheduled work. 21 | def start_worker_pool(num_worker_threads) 22 | # Create work queues. 23 | work_queues_lock.synchronize do 24 | worker_threads_lock.synchronize do 25 | num_worker_threads.times do |n| 26 | work_queue = Queue.new 27 | worker_thread = _start_worker_thread(work_queue) 28 | self.work_queues << work_queue 29 | self.worker_threads << worker_thread 30 | end 31 | end 32 | end 33 | 34 | tags = num_worker_threads.times.map(&:to_i) 35 | self.worker_rhash.site_tags = tags 36 | end 37 | 38 | # Public: Schedule a task to be executed on one of the work 39 | # queues. If a :hash option is provided, then use this to 40 | # consistently send the work to the same worker. This allows us to 41 | # effectively "single thread" some lines of work. 42 | # 43 | # s - The command to schedule. 44 | # options - Options for the command. 45 | # 46 | # Returns nothing. 47 | def schedule_work(s, options) 48 | raise "No hash specified!" if options[:hash].nil? 49 | index = worker_rhash.hash(options[:hash]) 50 | options.merge!(:'$action' => s, :'$time' => Time.now) 51 | work_queues_lock.synchronize do 52 | work_queues[index] << options 53 | end 54 | end 55 | 56 | # Public: Return the total number of scheduled items in the work queue. 57 | def work_queue_length 58 | work_queues_lock.synchronize do 59 | work_queues.map(&:size).inject(&:+) 60 | end 61 | end 62 | 63 | # Public: Wait for the worker pool to finish processing all items. 64 | def terminate_worker_pool 65 | Log.info("Waiting for worker threads...") 66 | worker_threads_lock.synchronize do 67 | worker_threads.present? && worker_threads.map(&:terminate) 68 | end 69 | end 70 | 71 | private 72 | 73 | def _start_worker_thread(work_queue) 74 | wrapped_thread do 75 | ActiveRecord::Base.connection_pool.with_connection do |conn| 76 | _worker_thread_runner(work_queue) 77 | end 78 | end 79 | end 80 | 81 | def _worker_thread_runner(work_queue) 82 | # Create a work queue. 83 | while running? 84 | _worker_thread_next_action(work_queue) 85 | end 86 | end 87 | 88 | def _worker_thread_next_action(queue) 89 | # Don't block, otherwise we can't exit. 90 | options = queue.pop(true) 91 | method = "action_#{options[:'$action']}".to_sym 92 | 93 | # Run the action. 94 | begin 95 | self.send(method.to_sym, options) 96 | rescue => e 97 | Log.exception(e) 98 | end 99 | 100 | # Track the most recent latency. 101 | self.work_queue_latency = Time.now - options[:'$time'] 102 | rescue ThreadError => e 103 | # Queue is empty. 104 | sleep 0.1 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /app/processes/common/wrapped_thread.rb: -------------------------------------------------------------------------------- 1 | module Common::WrappedThread 2 | def wrapped_thread(&block) 3 | Thread.new do 4 | begin 5 | yield 6 | rescue => e 7 | Log.exception(e) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/processes/imap_client.rb: -------------------------------------------------------------------------------- 1 | class ImapClient 2 | VERSION="1.0.0" 3 | end 4 | 5 | require 'imap_client/rendezvous_hash' 6 | require 'imap_client/process_uid' 7 | require 'imap_client/user_thread' 8 | require 'imap_client/daemon' 9 | -------------------------------------------------------------------------------- /app/processes/imap_client/process_uid.rb: -------------------------------------------------------------------------------- 1 | # Private: Read and act on a single email. This is one place where 2 | # Ruby support for monads would be useful. The challenge is that we 3 | # have to verify a lot of data in a very specific sequence, and at any 4 | # time we could either abort (ie: skip the remaining operations) or 5 | # raise an exception. 6 | # 7 | # We build something similar by creating a 'Maybe' control structure 8 | # where we submit blocks of code to an object. A block is considered 9 | # successful if it returns true and doesn't throw any exceptions. If a 10 | # block is not successful, we skip the remaining blocks. 11 | # 12 | # Note that every database touch is wrapped in a call to 13 | # `user_thread.schedule(&block)`. This allows us to avoid creating a 14 | # separate database connection for each user thread. 15 | class ProcessUid 16 | attr_accessor :user_thread, :uid 17 | attr_accessor :internal_date, :message_size 18 | attr_accessor :raw_eml, :envelope 19 | attr_accessor :message_id, :sha1 20 | attr_accessor :mail_log 21 | 22 | def initialize(user_thread, uid) 23 | self.user_thread = user_thread 24 | self.uid = uid 25 | end 26 | 27 | # Public: Process the email. 28 | def run 29 | # Run all the steps below. Stop as soon as one of them returns 30 | # false or throws an error. 31 | true && 32 | fetch_internal_date_and_size && 33 | check_for_really_old_internal_date && 34 | check_for_pre_creation_internal_date && 35 | check_for_relapsed_internal_date && 36 | check_for_big_messages && 37 | fetch_uid_envelope_rfc822 && 38 | update_user_mark_email_processed && 39 | handle_tracer_email && 40 | check_for_duplicate_message_id && 41 | check_for_duplicate_sha1 && 42 | create_mail_log && 43 | deploy_webhook && 44 | update_daemon_stats 45 | ensure 46 | clean_up 47 | end 48 | 49 | # Private: The User model. 50 | def user 51 | user_thread.user 52 | end 53 | 54 | # Private: The IMAP client instance. 55 | def client 56 | user_thread.client 57 | end 58 | 59 | # Private: The imap_client daemon. 60 | def daemon 61 | user_thread.daemon 62 | end 63 | 64 | private 65 | 66 | def confirm_tracer(tracer_uid) 67 | user_thread.schedule do 68 | tracer = TracerLog.find_by_uid(tracer_uid) || TracerLog.new(:uid => tracer_uid) 69 | tracer.update_attributes!(:detected_at => Time.now) 70 | end 71 | end 72 | 73 | def fetch_internal_date_and_size 74 | responses = Timeout::timeout(30) do 75 | client.uid_fetch([uid], ["INTERNALDATE", "RFC822.SIZE"]) 76 | end 77 | response = responses && responses.first 78 | 79 | # If there was no response, then skip this message. 80 | if response.nil? 81 | user_thread.update_user(:last_uid => uid) 82 | return false 83 | end 84 | 85 | # Save the internal_date and message_size for later. 86 | self.internal_date = Time.parse(response.attr["INTERNALDATE"]) 87 | self.message_size = (response.attr["RFC822.SIZE"] || 0).to_i 88 | 89 | return true 90 | rescue Timeout::Error => e 91 | # If this email triggered a timeout, then skip it. 92 | user_thread.update_user(:last_uid => uid) 93 | raise e 94 | end 95 | 96 | # Private: Check for a really old date. If it's old, then we should 97 | # stop counting on our UID knowledge and go back to loading UIDs by 98 | # date. 99 | def check_for_really_old_internal_date 100 | if internal_date < 4.days.ago 101 | Log.librato(:count, "system.process_uid.really_old_internal_date", 1) 102 | user_thread.update_user(:last_uid => nil, :last_uid_validity => nil) 103 | user_thread.stop! 104 | return false 105 | else 106 | return true 107 | end 108 | end 109 | 110 | # Private: Don't process emails that arrived before this user was 111 | # created. 112 | def check_for_pre_creation_internal_date 113 | if internal_date < user.created_at 114 | Log.librato(:count, "system.process_uid.pre_creation_internal_date", 1) 115 | user_thread.update_user(:last_uid => uid) 116 | return false 117 | else 118 | return true 119 | end 120 | end 121 | 122 | # Private: Don't process emails that are significantly older than 123 | # the last internal date that we've processed. 124 | def check_for_relapsed_internal_date 125 | if user.last_internal_date && internal_date < (user.last_internal_date - 1.hour) 126 | Log.librato(:count, "system.process_uid.relapsed_internal_date", 1) 127 | user_thread.update_user(:last_uid => uid) 128 | return false 129 | else 130 | return true 131 | end 132 | end 133 | 134 | # Private: Skip emails that are too big. 135 | def check_for_big_messages 136 | if message_size > user_thread.options[:max_email_size] 137 | Log.librato(:count, "system.process_uid.big_message", 1) 138 | user_thread.update_user(:last_uid => uid) 139 | return false 140 | else 141 | return true 142 | end 143 | end 144 | 145 | def fetch_uid_envelope_rfc822 146 | # Load the email body. 147 | responses = Timeout::timeout(30) do 148 | self.client.uid_fetch([uid], ["UID", "ENVELOPE", "RFC822"]) 149 | end 150 | response = responses && responses.first 151 | 152 | # If there was no response, then skip this message. 153 | if response.nil? 154 | Log.librato(:count, "system.process_uid.uid_fetch_no_response", 1) 155 | user_thread.update_user(:last_uid => uid) 156 | return false 157 | end 158 | 159 | # Save the internal_date and message_size for later. 160 | self.uid = response.attr["UID"] 161 | self.raw_eml = to_utf8(response.attr["RFC822"]) 162 | self.envelope = response.attr["ENVELOPE"] 163 | self.message_id = (envelope.message_id || "#{user.email} - #{uid} - #{internal_date}").slice(0, 255) 164 | 165 | return true 166 | rescue Timeout::Error => e 167 | # If this email triggered a timeout, then skip it. 168 | user_thread.update_user(:last_uid => uid) 169 | raise e 170 | end 171 | 172 | # Private: Update the high-water mark for which emails we've 173 | # processed. 174 | def update_user_mark_email_processed 175 | # Ignore any suspicious looking internal dates. Sometimes 176 | # misconfigured email servers means that email arrives from the 177 | # future. 178 | if internal_date > Time.now 179 | Log.librato(:count, "system.process_uid.fix_suspicious_internal_date", 1) 180 | self.internal_date = user.last_internal_date 181 | end 182 | 183 | # Update the user. 184 | user_thread.update_user(:last_uid => uid, 185 | :last_email_at => Time.now, 186 | :last_internal_date => internal_date) 187 | return true 188 | end 189 | 190 | # Private: Is this a tracer? If so, update the TracerLog and stop 191 | # processing. 192 | def handle_tracer_email 193 | if m = /^TRACER: (.+)$/.match(envelope.subject) 194 | tracer_uid = m[1] 195 | confirm_tracer(tracer_uid) 196 | user_thread.update_user(:last_uid => uid) 197 | daemon.total_emails_processed += 1 198 | return false 199 | else 200 | return true 201 | end 202 | end 203 | 204 | # Private: Have we already processed this message_id? 205 | def check_for_duplicate_message_id 206 | old_mail_log = nil 207 | user_thread.schedule do 208 | old_mail_log = user.mail_logs.find_by_message_id(message_id) 209 | end 210 | 211 | if old_mail_log 212 | Log.librato(:count, "system.process_uid.duplicate_message_id", 1) 213 | return false 214 | else 215 | return true 216 | end 217 | end 218 | 219 | # Private: Have we already processed this sha1 hash? This helps us 220 | # catch rare cases where an email doesn't have a message_id so we 221 | # make one up, so the message_id is unique, but the email is a 222 | # duplicate. This may be unnecessary. 223 | def check_for_duplicate_sha1 224 | # Generate the SHA1. 225 | self.sha1 = Digest::SHA1.hexdigest(raw_eml) 226 | 227 | old_mail_log = nil 228 | user_thread.schedule do 229 | old_mail_log = user.mail_logs.find_by_sha1(sha1) 230 | end 231 | 232 | if old_mail_log 233 | Log.librato(:count, "system.process_uid.duplicate_sha1", 1) 234 | return false 235 | else 236 | return true 237 | end 238 | end 239 | 240 | # Private: Log the mail. 241 | def create_mail_log 242 | user_thread.schedule do 243 | self.mail_log = user.mail_logs.create(:message_id => message_id, :sha1 => sha1) 244 | end 245 | return true 246 | end 247 | 248 | # Private: Deploy the web hook. 249 | def deploy_webhook 250 | unless daemon.stress_test_mode 251 | user_thread.schedule do 252 | CallNewMailWebhook.new(mail_log, envelope, raw_eml).delay.run 253 | end 254 | end 255 | return true 256 | end 257 | 258 | # Private: Update stats 259 | def update_daemon_stats 260 | daemon.clear_error_count(user.id) 261 | daemon.processed_log && 262 | daemon.processed_log.log(Time.now, user.email, message_id) 263 | daemon.total_emails_processed += 1 264 | return true 265 | end 266 | 267 | # Private: Help the garbage collector know what it can collect. 268 | def clean_up 269 | self.user_thread = nil 270 | self.uid = nil 271 | self.internal_date = nil 272 | self.raw_eml = nil 273 | self.envelope = nil 274 | self.message_id = nil 275 | self.sha1 = nil 276 | self.mail_log = nil 277 | end 278 | 279 | # Private: Convert a string UTF-8 format. 280 | def to_utf8(s) 281 | return nil if s.nil? 282 | 283 | # Attempt to politely transcode the string. 284 | s.encode("UTF-8").scrub 285 | rescue 286 | # If that doesn't work, then overwrite the existing encoding and 287 | # clobber any strange characters. 288 | s.force_encoding("UTF-8").scrub 289 | end 290 | end 291 | -------------------------------------------------------------------------------- /app/processes/imap_client/rendezvous_hash.rb: -------------------------------------------------------------------------------- 1 | # http://en.wikipedia.org/wiki/Rendezvous_hashing 2 | class ImapClient::RendezvousHash 3 | attr_accessor :lock 4 | 5 | def initialize 6 | @site_tags = [] 7 | @lock = Mutex.new 8 | end 9 | 10 | def site_tags=(site_tags) 11 | lock.synchronize do 12 | @site_tags = site_tags 13 | end 14 | end 15 | 16 | # Return the number of sites. 17 | def size 18 | lock.synchronize do 19 | return @site_tags.length 20 | end 21 | end 22 | 23 | # Return the highest priority item. 24 | def hash(object_tag) 25 | hashes = self.lock.synchronize do 26 | @site_tags.map do |site_tag| 27 | h = Digest::SHA1.hexdigest("#{site_tag} - #{object_tag}") 28 | [h, site_tag] 29 | end 30 | end 31 | 32 | priority = hashes.sort 33 | 34 | if priority.length > 0 35 | priority[0][1] 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/processes/imap_test_server.rb: -------------------------------------------------------------------------------- 1 | class ImapTestServer 2 | VERSION="1.0.0" 3 | end 4 | 5 | require 'imap_test_server/daemon' 6 | -------------------------------------------------------------------------------- /app/processes/imap_test_server/daemon.rb: -------------------------------------------------------------------------------- 1 | # ImapTestServer::Daemon - A test IMAP server that can respond to all 2 | # calls made by the ImapClient process. Generates test data and 3 | # (muhahaha) deliberately responds to some calls with gibberish in 4 | # order to test how well ImapClient recovers. 5 | # 6 | # The daemon has three threads: 7 | # + The Connection thread listens for incoming connections. 8 | # + The New Mail thread generates new emails to users. 9 | # + The Process Sockets (main) thread sits in a tight loop, sending and receiving IMAP commands. 10 | # 11 | # The code is organized as follows: 12 | # 13 | # + The Daemon class contains high level connection logic. 14 | # + SocketState holds the state of a socket and contains our IMAP logic. 15 | # + Mailboxes holds mail for all users. 16 | 17 | require 'socket' 18 | 19 | class ImapTestServer::Daemon 20 | include Common::Stoppable 21 | include Common::LightSleep 22 | include Common::WrappedThread 23 | include Common::DbConnection 24 | 25 | attr_accessor :port, :enable_chaos, :emails_per_minute, :length_of_test 26 | attr_accessor :stats_thread 27 | attr_accessor :connection_thread 28 | attr_accessor :new_sockets, :sockets, :socket_states 29 | attr_accessor :mailboxes 30 | attr_accessor :total_emails_generated, :total_emails_fetched 31 | attr_accessor :generated_log, :fetched_log, :events_log 32 | 33 | def initialize(options = {}) 34 | # Initialize mixins. 35 | init_stoppable 36 | 37 | # Config stuff. 38 | self.port = options.fetch(:port) 39 | self.enable_chaos = options.fetch(:enable_chaos) 40 | self.emails_per_minute = options.fetch(:emails_per_minute) 41 | self.length_of_test = options.fetch(:length_of_test) 42 | 43 | # Socket stuff. 44 | self.new_sockets = Queue.new 45 | self.sockets = [] 46 | self.socket_states = {} 47 | 48 | # Mailboxes. 49 | self.mailboxes = Mailboxes.new() 50 | 51 | # Stats. 52 | self.total_emails_generated = 0 53 | self.total_emails_fetched = 0 54 | end 55 | 56 | # Public: Start threads and begin servicing connections. 57 | def run 58 | trap_signals 59 | 60 | self.generated_log = Common::CsvLog.new("./log/stress/generated_emails.csv") 61 | self.fetched_log = Common::CsvLog.new("./log/stress/fetched_emails.csv") 62 | self.events_log = Common::CsvLog.new("./log/stress/events.csv") 63 | 64 | start_stats_thread 65 | start_connection_thread 66 | start_new_mail_thread 67 | start_process_sockets_thread 68 | 69 | stop_time = self.length_of_test.minutes.from_now 70 | while running? 71 | if Time.now > stop_time 72 | Log.info("Test finished! Shutting down.") 73 | stop! 74 | end 75 | light_sleep 1.0 76 | end 77 | rescue => e 78 | stop! 79 | Log.exception(e) 80 | raise e 81 | ensure 82 | stop! 83 | connection_thread && connection_thread.terminate 84 | sockets.map(&:close) 85 | self.generated_log.stop! 86 | self.fetched_log.stop! 87 | self.events_log.stop! 88 | Log.info("Generated #{total_emails_generated} emails.") 89 | Log.info("Served #{total_emails_fetched} emails.") 90 | end 91 | 92 | 93 | private 94 | 95 | 96 | def start_stats_thread 97 | self.stats_thread = wrapped_thread do 98 | stats_thread_runner 99 | end 100 | end 101 | 102 | def start_connection_thread 103 | self.connection_thread = wrapped_thread do 104 | connection_thread_runner 105 | end 106 | end 107 | 108 | def start_new_mail_thread 109 | self.connection_thread = wrapped_thread do 110 | new_mail_thread_runner 111 | end 112 | end 113 | 114 | def stats_thread_runner 115 | while running? 116 | Log.info("Stats (connections = #{sockets.count}, emails_generated = #{total_emails_generated}, emails_fetched = #{total_emails_fetched})") 117 | light_sleep 10 118 | end 119 | end 120 | 121 | # Private: Accepts incoming connections. 122 | def connection_thread_runner 123 | Log.info("Waiting for connections on port 0.0.0.0:#{port}.") 124 | server = TCPServer.new("0.0.0.0", port) 125 | while running? 126 | begin 127 | socket = server.accept 128 | new_sockets << socket 129 | rescue IO::EAGAINWaitReadable 130 | sleep 0.2 131 | rescue => e 132 | Log.exception(e) 133 | end 134 | end 135 | rescue => e 136 | Log.exception(e) 137 | end 138 | 139 | # Private: Sends and receives IMAP commands. 140 | def start_process_sockets_thread 141 | wrapped_thread do 142 | process_sockets_runner 143 | end 144 | end 145 | 146 | def process_sockets_runner 147 | while running? 148 | process_new_sockets 149 | process_incoming_messages 150 | send_exists_messages 151 | sleep 0.1 152 | end 153 | end 154 | 155 | def process_new_sockets 156 | while running? && !new_sockets.empty? 157 | socket = new_sockets.pop(true) 158 | process_new_socket(socket) 159 | end 160 | end 161 | 162 | def process_new_socket(socket) 163 | options = { 164 | :enable_chaos => self.enable_chaos 165 | } 166 | socket_state = ImapTestServer::SocketState.new(self, socket, options) 167 | socket_state.handle_connect 168 | 169 | # Add to our list of existing sockets. 170 | self.sockets << socket 171 | self.socket_states[socket.hash] = socket_state 172 | rescue => e 173 | Log.exception(e) 174 | close_socket(socket) 175 | end 176 | 177 | def process_incoming_messages 178 | # Which sockets need attention? 179 | response = IO.select(sockets, [], [], 0) 180 | return if response.nil? 181 | 182 | # Attend to the sockets. 183 | read_sockets, _, _ = response 184 | read_sockets.each do |socket| 185 | process_incoming_message(socket) 186 | end 187 | end 188 | 189 | def process_incoming_message(socket) 190 | command = socket.gets 191 | if command.present? 192 | socket_state = socket_states[socket.hash] 193 | socket_state.handle_command(command) 194 | else 195 | close_socket(socket) 196 | end 197 | rescue ImapTestServer::SocketState::NormalDisconnect => e 198 | close_socket(socket) 199 | rescue ImapTestServer::SocketState::ChaosDisconnect => e 200 | close_socket(socket) 201 | rescue => e 202 | Log.exception(e) 203 | close_socket(socket) 204 | end 205 | 206 | def send_exists_messages 207 | socket_states.values.each do |socket_state| 208 | send_exists_message(socket_state) 209 | end 210 | end 211 | 212 | def send_exists_message(socket_state) 213 | socket_state.send_exists_messages 214 | rescue => e 215 | Log.exception(e) 216 | close_socket(socket_state.socket) 217 | end 218 | 219 | def close_socket(socket) 220 | sockets.delete(socket) 221 | socket_states.delete(socket.hash) 222 | socket.close() 223 | rescue => e 224 | Log.exception(e) 225 | end 226 | 227 | def new_mail_thread_runner 228 | sleep_seconds = 1 229 | 230 | while running? 231 | if self.mailboxes.count > 0 232 | n = (1.0 * sleep_seconds / 60) * emails_per_minute 233 | generate_new_mail(n) 234 | end 235 | light_sleep sleep_seconds 236 | end 237 | end 238 | 239 | def generate_new_mail(n) 240 | # What's our chance of generating an email for an individual user? 241 | prob_of_email = n / self.mailboxes.count 242 | self.mailboxes.each do |mailbox| 243 | if rand() < prob_of_email 244 | self.total_emails_generated += 1 245 | mailbox.add_fake_message do |message_id| 246 | self.generated_log.log(Time.now, mailbox.username, message_id) 247 | end 248 | end 249 | end 250 | end 251 | end 252 | -------------------------------------------------------------------------------- /app/processes/imap_test_server/mailboxes.rb: -------------------------------------------------------------------------------- 1 | class ImapTestServer::Mailboxes 2 | attr_accessor :mailboxes, :mailboxes_mutex 3 | 4 | def initialize(options = {}) 5 | self.mailboxes = {} 6 | self.mailboxes_mutex = Mutex.new 7 | end 8 | 9 | def count 10 | return self.mailboxes.count 11 | end 12 | 13 | def find(username) 14 | self.mailboxes_mutex.synchronize do 15 | self.mailboxes[username] ||= Mailbox.new(username) 16 | end 17 | end 18 | 19 | def each(&block) 20 | usernames = self.mailboxes_mutex.synchronize do 21 | self.mailboxes.keys.dup 22 | end 23 | 24 | usernames.each do |username| 25 | yield mailboxes[username] 26 | end 27 | end 28 | 29 | private 30 | 31 | class Mailbox 32 | attr_accessor :username 33 | attr_accessor :last_uid, :mails 34 | MailStruct = Struct.new(:uid, :date, :message_id) 35 | 36 | def initialize(username) 37 | self.username = username 38 | self.last_uid = rand(999999) 39 | self.mails = [] 40 | end 41 | 42 | def count 43 | return mails.length 44 | end 45 | 46 | def add_fake_message 47 | self.last_uid += 1 48 | message_id = "message-#{username}-#{last_uid}-#{rand(999999)}@localhost" 49 | self.mails << MailStruct.new(last_uid, Time.now, message_id) 50 | yield(message_id) 51 | end 52 | 53 | def uid_search(from_uid, to_uid) 54 | mails.select do |mail| 55 | mail.uid >= from_uid && mail.uid <= to_uid 56 | end.map(&:uid) 57 | end 58 | 59 | def date_search(since_date) 60 | mails.select do |mail| 61 | mail.date > since_date 62 | end.map(&:uid) 63 | end 64 | 65 | def fetch(uid) 66 | email = self.username 67 | mail = mails.find do |mail| 68 | mail.uid == uid 69 | end 70 | 71 | Mail.new do 72 | from email 73 | to email 74 | date mail.date 75 | message_id mail.message_id 76 | subject "MySubject" 77 | body "MyBody" 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/processes/imap_test_server/socket_state.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | class ImapTestServer::SocketState 4 | NormalDisconnect = Class.new(StandardError) 5 | ChaosDisconnect = Class.new(StandardError) 6 | 7 | attr_accessor :daemon, :mailbox, :socket 8 | attr_accessor :enable_chaos 9 | attr_accessor :username 10 | attr_accessor :uid_validity 11 | attr_accessor :last_count 12 | attr_accessor :idling, :idle_tag 13 | attr_accessor :new_email, :inbox 14 | 15 | def initialize(daemon, socket, options) 16 | self.daemon = daemon 17 | self.socket = socket 18 | self.enable_chaos = options[:enable_chaos] 19 | self.uid_validity = rand(999) 20 | self.idling = false 21 | self.last_count = 0 22 | end 23 | 24 | # Public: Return true if we are idling. 25 | def idling? 26 | self.idling 27 | end 28 | 29 | # Public: Greet the new connection. 30 | def handle_connect() 31 | handle_command("TAG HELLO") 32 | end 33 | 34 | # Public: Handle the specified IMAP command, respond to the socket. 35 | def handle_command(s) 36 | tag, verb, args = parse_command(s) 37 | method = verb_to_method(verb) 38 | self.daemon.events_log.log(Time.now, username, method) 39 | send(method, tag, args) 40 | end 41 | 42 | def send_exists_messages 43 | if self.mailbox && self.last_count < self.mailbox.count 44 | respond("*", "#{self.mailbox.count} EXISTS") 45 | self.last_count = mailbox.count 46 | end 47 | end 48 | 49 | private 50 | 51 | def parse_command(s) 52 | # Get the tag. 53 | tag, s = /(.+?)\s+(.*)/.match(s).captures 54 | 55 | # Special case for an IDLE done command. 56 | return [nil, "DONE", []] if tag == "DONE" 57 | 58 | # Parse the rest of the command. 59 | verb, s = /(UID SEARCH|UID FETCH|\w+)\s*(.*)/.match(s).captures 60 | args = s.split(/\s+/) 61 | [tag, verb, args] 62 | end 63 | 64 | # Private: Given an IMAP verb, return a method. This is our chance 65 | # to inject some chaos into the system. (Muhahaha.) 66 | def verb_to_method(verb) 67 | verb = verb.downcase.gsub(/\s/, "_") 68 | choices = [[400, "imap_#{verb}".to_sym]] 69 | 70 | if self.enable_chaos 71 | choices += [ 72 | [3, "imap_#{verb}_chaos".to_sym], 73 | [1, :imap_chaos_respond_no], 74 | [1, :imap_chaos_respond_bad], 75 | [1, :imap_chaos_gibberish_tagged], 76 | [1, :imap_chaos_gibberish_untagged], 77 | [1, :imap_chaos_soft_disconnect], 78 | [1, :imap_chaos_hard_disconnect], 79 | ] 80 | end 81 | 82 | choose(choices) 83 | end 84 | 85 | # Private: Choose from a list of weighted choices, of the form 86 | # [[weight1, choice1], [weight2, choice2], ...]. Weights are 87 | # normalized, they do not need to add to 1.0. 88 | # 89 | # Returns the selected choice. 90 | def choose(choices) 91 | r = rand() 92 | w = 0 93 | total_weight = choices.map(&:first).inject(&:+) 94 | choices.each do |weight, choice| 95 | w += (1.0 * weight / total_weight) 96 | return choice if r <= w 97 | end 98 | end 99 | 100 | # Private: Write a response to the socket. 101 | def respond(tag, s) 102 | socket.write("#{tag} #{s}\r\n") 103 | socket.flush 104 | end 105 | 106 | # CONNECT 107 | 108 | def imap_hello(tag, args) 109 | respond("*", "OK ImapTestServer ready.") 110 | end 111 | 112 | def imap_hello_chaos(tag, args) 113 | respond("*", "ERROR Not ready.") 114 | end 115 | 116 | # LOGIN Command 117 | # https://tools.ietf.org/html/rfc3501#section-6.2.3 118 | 119 | def imap_login(tag, args) 120 | self.username = args[0] 121 | self.mailbox = self.daemon.mailboxes.find(username) 122 | respond(tag, "OK Logged in.") 123 | end 124 | 125 | def imap_login_chaos(tag, args) 126 | respond(tag, "NO") 127 | end 128 | 129 | # LIST Command 130 | # https://tools.ietf.org/html/rfc3501#section-6.3.8 131 | 132 | def imap_list(tag, args) 133 | respond("*", %(LIST (\HasNoChildren) "." "INBOX")) 134 | respond("*", %(LIST (\HasNoChildren) "." "FOLDER1")) 135 | respond("*", %(LIST (\HasNoChildren) "." "FOLDER2")) 136 | respond(tag, "OK LIST Completed") 137 | end 138 | 139 | def imap_list_chaos(tag, args) 140 | respond("*", %(LIST (\HasNoChildren) "." "FOLDER1")) 141 | respond("*", %(LIST (\HasNoChildren) "." "FOLDER2")) 142 | respond(tag, "OK LIST Completed") 143 | end 144 | 145 | # EXAMINE Command 146 | # https://tools.ietf.org/html/rfc3501#section-6.3.2 147 | 148 | def imap_examine(tag, args) 149 | respond("*", %(FLAGS (\Answered \Flagged \Deleted \Seen \Draft))) 150 | respond("*", %(OK [PERMANENTFLAGS ()] Read-only mailbox.)) 151 | respond("*", %(#{mailbox.count} EXISTS)) 152 | respond("*", %(OK [UIDVALIDITY #{uid_validity}] UIDs valid)) 153 | respond(tag, %(OK [READ-ONLY] Select completed.)) 154 | end 155 | 156 | def imap_examine_chaos(tag, args) 157 | self.uid_validity = rand(999) 158 | imap_examine(tag, args) 159 | end 160 | 161 | # STATUS Command 162 | # https://tools.ietf.org/html/rfc3501#section-6.3.10 163 | 164 | def imap_status(tag, args) 165 | mailbox_name = args[0] 166 | respond("*", %(STATUS #{mailbox_name} (UIDVALIDITY #{uid_validity}))) 167 | respond(tag, %(OK STATUS completed)) 168 | end 169 | 170 | def imap_status_chaos(tag, args) 171 | self.uid_validity = rand(999) 172 | imap_status(tag, args) 173 | end 174 | 175 | # UID SEARCH Command 176 | # https://tools.ietf.org/html/rfc3501#section-6.4.4 177 | 178 | def imap_uid_search(tag, args) 179 | if args.index("UID") 180 | imap_uid_search_by_uid(tag, args) 181 | elsif args.index("SINCE") 182 | imap_uid_search_by_date(tag, args) 183 | else 184 | raise "Unhandled search: #{args}" 185 | end 186 | end 187 | 188 | def imap_uid_search_by_uid(tag, args) 189 | 190 | # Parse the search request. 191 | from_uid, to_uid = args[args.index("UID") + 1].split(":") 192 | from_uid = from_uid.to_i - self.uid_validity 193 | to_uid = to_uid.to_i - self.uid_validity 194 | 195 | # Get a list of uids, offset by uid validity. 196 | uids = self.mailbox.uid_search(from_uid, to_uid).map do |uid| 197 | uid + self.uid_validity 198 | end 199 | 200 | respond("*", %(SEARCH #{uids.join(' ')})) 201 | respond(tag, %(OK SEARCH completed)) 202 | end 203 | 204 | def imap_uid_search_by_date(tag, args) 205 | # Parse the search request. 206 | since_date = Time.parse(args[args.index("SINCE") + 1]) 207 | 208 | # Make sure we have a valid date. 209 | if since_date.nil? 210 | raise "Unhandled date: #{args}" 211 | end 212 | 213 | # Get a list of uids, offset by uid validity. 214 | uids = mailbox.date_search(since_date).map do |uid| 215 | uid + self.uid_validity 216 | end 217 | 218 | respond("*", %(SEARCH #{uids.join(' ')})) 219 | respond(tag, %(OK SEARCH completed)) 220 | end 221 | 222 | def imap_uid_search_chaos(tag, args) 223 | imap_uid_search(tag, args) 224 | end 225 | 226 | # UID FETCH Command 227 | # https://tools.ietf.org/html/rfc3501#section-6.4.5 228 | # https://tools.ietf.org/html/rfc3501#section-7.4.2 229 | 230 | def imap_uid_fetch(tag, args) 231 | # Looks like this: 1103963 (INTERNALDATE RFC822.SIZE UID) 232 | m = /(\d+)\s\((.*)\)/.match(args.join(' ')) 233 | uid = m[1].to_i 234 | fields = m[2].split 235 | mail = mailbox.fetch(uid - self.uid_validity) 236 | values = fields.map do |field| 237 | case field 238 | when "UID" 239 | [field, as_integer(uid)] 240 | when "INTERNALDATE" 241 | [field, as_date(mail.date)] 242 | when "ENVELOPE" 243 | [field, as_list( 244 | as_date(mail.date), 245 | as_string(mail.subject), 246 | as_address_structure(mail.from), 247 | as_address_structure(mail.from), 248 | as_address_structure(mail.reply_to), 249 | as_address_structure(mail.to), 250 | as_string(nil), 251 | as_string(nil), 252 | as_string(nil), 253 | as_string(mail.message_id) 254 | )] 255 | when "RFC822.SIZE" 256 | [field, as_integer(mail.encoded.size)] 257 | when "RFC822" 258 | self.daemon.total_emails_fetched += 1 259 | self.daemon.fetched_log.log(Time.now, username, mail.message_id) 260 | [field, as_multiline_string(mail.encoded)] 261 | else 262 | raise "Unknown field: #{field}" 263 | end 264 | end 265 | values += ["UID", as_integer(uid)] unless fields.include?("RFC822") 266 | respond("*", "#{uid} FETCH #{as_list(values)}") 267 | respond(tag, "OK FETCH complete") 268 | end 269 | 270 | def as_list(*values) 271 | s = values.map do |value| 272 | if value.instance_of?(Array) 273 | value.join(" ") 274 | else 275 | value 276 | end 277 | end.join(' ') 278 | 279 | return "(#{s})" 280 | end 281 | 282 | def as_address_structure(addresses) 283 | # https://tools.ietf.org/html/rfc3501#section-7.4.2 284 | return as_string(nil) if addresses.blank? 285 | 286 | values = addresses.map do |address| 287 | if !address.instance_of?(Mail::Address) 288 | address = Mail::Address.new(address) 289 | end 290 | as_list(as_string(address.display_name), 291 | as_string(nil), 292 | as_string(address.local), 293 | as_string(address.domain)) 294 | end 295 | as_list(values) 296 | end 297 | 298 | def as_date(date) 299 | as_string(date.strftime("%a, %b %e %Y %H:%M:%S %z (%Z)")) 300 | end 301 | 302 | def as_integer(n) 303 | n.to_s 304 | end 305 | 306 | def as_string(s) 307 | s.nil? ? "NIL" : "\"#{s}\"" 308 | end 309 | 310 | def as_multiline_string(s) 311 | "{#{s.length}}\r\n#{s}" 312 | end 313 | 314 | def imap_uid_fetch_chaos(tag, args) 315 | imap_uid_fetch(tag, args) 316 | end 317 | 318 | # IDLE Command 319 | # http://tools.ietf.org/html/rfc2177 320 | 321 | def imap_idle(tag, args) 322 | self.idling = true 323 | self.idle_tag = tag 324 | respond("+", "idling") 325 | end 326 | 327 | def imap_idle_chaos(tag, args) 328 | imap_idle(tag, args) 329 | end 330 | 331 | def imap_done(tag, args) 332 | respond(idle_tag, "OK IDLE terminated") 333 | end 334 | 335 | def imap_done_chaos(tag, args) 336 | imap_done(tag, args) 337 | end 338 | 339 | # LOGOUT Command 340 | # https://tools.ietf.org/html/rfc3501#section-6.1.3 341 | 342 | def imap_logout(tag, args) 343 | respond("*", "BYE ImapTestServer logging out") 344 | respond(tag, "OK LOGOUT completed") 345 | raise NormalDisconnect.new() 346 | end 347 | 348 | def imap_logout_chaos(tag, args) 349 | imap_logout(tag, args) 350 | end 351 | 352 | # GENERAL CHAOS 353 | 354 | def imap_chaos_respond_no(tag, args) 355 | respond(tag, "BAD") 356 | end 357 | 358 | def imap_chaos_respond_bad(tag, args) 359 | respond(tag, "BAD") 360 | end 361 | 362 | def imap_chaos_gibberish_tagged(tag, args) 363 | respond(tag, "ZZZ") 364 | end 365 | 366 | def imap_chaos_gibberish_untagged(tag, args) 367 | respond("*", "ZZZ") 368 | end 369 | 370 | def imap_chaos_soft_disconnect(tag, args) 371 | respond("*", "BYE") 372 | end 373 | 374 | def imap_chaos_hard_disconnect(tag, args) 375 | raise ChaosDisconnect.new("Disconnect!") 376 | end 377 | end 378 | -------------------------------------------------------------------------------- /app/views/api/v1/connections/index.json.rb: -------------------------------------------------------------------------------- 1 | fields = [ 2 | :imap_provider_code, 3 | :users_count 4 | ] 5 | 6 | @connections.map do |user| 7 | values = fields.map do |field| 8 | [field, user.send(field)] 9 | end 10 | array_to_hash(values) 11 | end.to_json 12 | -------------------------------------------------------------------------------- /app/views/api/v1/connections/show.json.rb: -------------------------------------------------------------------------------- 1 | fields = [ 2 | :imap_provider_code, 3 | :users_count 4 | ] 5 | values = fields.map do |field| 6 | [field, @connection.send(field)] 7 | end 8 | array_to_hash(values).to_json 9 | -------------------------------------------------------------------------------- /app/views/api/v1/users/index.json.rb: -------------------------------------------------------------------------------- 1 | fields = [:tag, :email] 2 | 3 | @users.map do |user| 4 | values = fields.map do |field| 5 | [field, user.send(field)] 6 | end 7 | array_to_hash(values) 8 | end.to_json 9 | -------------------------------------------------------------------------------- /app/views/api/v1/users/show.json.rb: -------------------------------------------------------------------------------- 1 | { 2 | :tag => @user.tag, 3 | :email => @user.email, 4 | :connect_url => new_users_connect_url(@user.signed_request_params), 5 | :disconnect_url => new_users_disconnect_url(@user.signed_request_params), 6 | :connected_at => @user.connected_at 7 | }.to_json 8 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SuperIMAP 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 |

<%= notice %>

12 |

<%= alert %>

13 | 14 | <%= yield %> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/views/layouts/blank.html.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/tracer_mailer/tracer_email.html.erb: -------------------------------------------------------------------------------- 1 | TRACER: <%= @uid %> 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module SuperIMAP 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | config.autoload_paths += Dir["#{config.root}/app/interactors"] 24 | config.autoload_paths += Dir["#{config.root}/app/processes"] 25 | 26 | encryption_key = ENV['ENCRYPTION_KEY'] 27 | if encryption_key.present? 28 | config.encryption_cipher = Gibberish::AES.new(encryption_key) 29 | else 30 | config.encryption_cipher = nil 31 | end 32 | 33 | config.log_level = String(ENV['LOG_LEVEL'] || "info").upcase 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | host: localhost 4 | pool: 5 5 | timeout: 5000 6 | user: username 7 | password: password 8 | 9 | development: 10 | <<: *default 11 | database: super_imap_development 12 | 13 | stress: 14 | <<: *default 15 | database: super_imap_stress 16 | 17 | test: 18 | <<: *default 19 | database: super_imap_test 20 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /config/environments/performance.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | 79 | if config.encryption_cipher.nil? 80 | raise "Must set ENCRYPTION_KEY environment variable." 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /config/environments/stress.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /config/initializers/active_admin.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ActiveAdmin.setup do |config| 3 | 4 | # == Site Title 5 | # 6 | # Set the title that is displayed on the main layout 7 | # for each of the active admin pages. 8 | # 9 | config.site_title = "SuperIMAP" 10 | 11 | # Set the link url for the title. For example, to take 12 | # users to your main site. Defaults to no link. 13 | # 14 | # config.site_title_link = "/" 15 | 16 | # Set an optional image to be displayed for the header 17 | # instead of a string (overrides :site_title) 18 | # 19 | # Note: Aim for an image that's 21px high so it fits in the header. 20 | # 21 | # config.site_title_image = "logo.png" 22 | 23 | # == Default Namespace 24 | # 25 | # Set the default namespace each administration resource 26 | # will be added to. 27 | # 28 | # eg: 29 | # config.default_namespace = :hello_world 30 | # 31 | # This will create resources in the HelloWorld module and 32 | # will namespace routes to /hello_world/* 33 | # 34 | # To set no namespace by default, use: 35 | # config.default_namespace = false 36 | # 37 | # Default: 38 | # config.default_namespace = :admin 39 | # 40 | # You can customize the settings for each namespace by using 41 | # a namespace block. For example, to change the site title 42 | # within a namespace: 43 | # 44 | # config.namespace :admin do |admin| 45 | # admin.site_title = "Custom Admin Title" 46 | # end 47 | # 48 | # This will ONLY change the title for the admin section. Other 49 | # namespaces will continue to use the main "site_title" configuration. 50 | 51 | # == User Authentication 52 | # 53 | # Active Admin will automatically call an authentication 54 | # method in a before filter of all controller actions to 55 | # ensure that there is a currently logged in admin user. 56 | # 57 | # This setting changes the method which Active Admin calls 58 | # within the application controller. 59 | config.authentication_method = :authenticate_admin_user! 60 | 61 | # == User Authorization 62 | # 63 | # Active Admin will automatically call an authorization 64 | # method in a before filter of all controller actions to 65 | # ensure that there is a user with proper rights. You can use 66 | # CanCanAdapter or make your own. Please refer to documentation. 67 | # config.authorization_adapter = ActiveAdmin::CanCanAdapter 68 | 69 | # You can customize your CanCan Ability class name here. 70 | # config.cancan_ability_class = "Ability" 71 | 72 | # You can specify a method to be called on unauthorized access. 73 | # This is necessary in order to prevent a redirect loop which happens 74 | # because, by default, user gets redirected to Dashboard. If user 75 | # doesn't have access to Dashboard, he'll end up in a redirect loop. 76 | # Method provided here should be defined in application_controller.rb. 77 | # config.on_unauthorized_access = :access_denied 78 | 79 | # == Current User 80 | # 81 | # Active Admin will associate actions with the current 82 | # user performing them. 83 | # 84 | # This setting changes the method which Active Admin calls 85 | # (within the application controller) to return the currently logged in user. 86 | config.current_user_method = :current_admin_user 87 | 88 | 89 | # == Logging Out 90 | # 91 | # Active Admin displays a logout link on each screen. These 92 | # settings configure the location and method used for the link. 93 | # 94 | # This setting changes the path where the link points to. If it's 95 | # a string, the strings is used as the path. If it's a Symbol, we 96 | # will call the method to return the path. 97 | # 98 | # Default: 99 | config.logout_link_path = :destroy_admin_user_session_path 100 | 101 | # This setting changes the http method used when rendering the 102 | # link. For example :get, :delete, :put, etc.. 103 | # 104 | # Default: 105 | # config.logout_link_method = :get 106 | 107 | 108 | # == Root 109 | # 110 | # Set the action to call for the root path. You can set different 111 | # roots for each namespace. 112 | # 113 | # Default: 114 | config.root_to = 'partners#index' 115 | 116 | 117 | # == Admin Comments 118 | # 119 | # This allows your users to comment on any resource registered with Active Admin. 120 | # 121 | # You can completely disable comments: 122 | config.allow_comments = false 123 | # 124 | # You can disable the menu item for the comments index page: 125 | # config.show_comments_in_menu = false 126 | # 127 | # You can change the name under which comments are registered: 128 | # config.comments_registration_name = 'AdminComment' 129 | 130 | 131 | # == Batch Actions 132 | # 133 | # Enable and disable Batch Actions 134 | # 135 | config.batch_actions = false 136 | 137 | 138 | # == Controller Filters 139 | # 140 | # You can add before, after and around filters to all of your 141 | # Active Admin resources and pages from here. 142 | # 143 | # config.before_filter :do_something_awesome 144 | 145 | 146 | # == Setting a Favicon 147 | # 148 | # config.favicon = '/assets/favicon.ico' 149 | 150 | 151 | # == Removing Breadcrumbs 152 | # 153 | # Breadcrumbs are enabled by default. You can customize them for individual 154 | # resources or you can disable them globally from here. 155 | # 156 | # config.breadcrumb = false 157 | 158 | 159 | # == Register Stylesheets & Javascripts 160 | # 161 | # We recommend using the built in Active Admin layout and loading 162 | # up your own stylesheets / javascripts to customize the look 163 | # and feel. 164 | # 165 | # To load a stylesheet: 166 | # config.register_stylesheet 'my_stylesheet.css' 167 | # 168 | # You can provide an options hash for more control, which is passed along to stylesheet_link_tag(): 169 | # config.register_stylesheet 'my_print_stylesheet.css', :media => :print 170 | # 171 | # To load a javascript file: 172 | # config.register_javascript 'my_javascript.js' 173 | 174 | 175 | # == CSV options 176 | # 177 | # Set the CSV builder separator 178 | # config.csv_options = { :col_sep => ';' } 179 | # 180 | # Force the use of quotes 181 | # config.csv_options = { :force_quotes => true } 182 | 183 | 184 | # == Menu System 185 | # 186 | # You can add a navigation menu to be used in your application, or configure a provided menu 187 | # 188 | # To change the default utility navigation to show a link to your website & a logout btn 189 | # 190 | # config.namespace :admin do |admin| 191 | # admin.build_menu :utility_navigation do |menu| 192 | # menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank } 193 | # admin.add_logout_button_to_menu menu 194 | # end 195 | # end 196 | # 197 | # If you wanted to add a static menu item to the default menu provided: 198 | # 199 | # config.namespace :admin do |admin| 200 | # admin.build_menu :default do |menu| 201 | # menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank } 202 | # end 203 | # end 204 | 205 | # == Download Links 206 | # 207 | # You can disable download links on resource listing pages, 208 | # or customize the formats shown per namespace/globally 209 | # 210 | # To disable/customize for the :admin namespace: 211 | # 212 | config.namespace :admin do |admin| 213 | 214 | # Disable the links entirely 215 | admin.download_links = false 216 | 217 | # # Only show XML & PDF options 218 | # admin.download_links = [:xml, :pdf] 219 | 220 | # # Enable/disable the links based on block 221 | # # (for example, with cancan) 222 | # admin.download_links = proc { can?(:view_download_links) } 223 | 224 | end 225 | 226 | 227 | # == Pagination 228 | # 229 | # Pagination is enabled by default for all resources. 230 | # You can control the default per page count for all resources here. 231 | # 232 | # config.default_per_page = 30 233 | 234 | 235 | # == Filters 236 | # 237 | # By default the index screen includes a “Filters” sidebar on the right 238 | # hand side with a filter for each attribute of the registered model. 239 | # You can enable or disable them for all resources here. 240 | # 241 | # config.filters = true 242 | 243 | end 244 | -------------------------------------------------------------------------------- /config/initializers/airbrake.rb: -------------------------------------------------------------------------------- 1 | if defined?(Airbrake) && ENV['AIRBRAKE_KEY'] 2 | Airbrake.configure do |config| 3 | config.api_key = ENV['AIRBRAKE_KEY'] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /config/initializers/delayed_job.rb: -------------------------------------------------------------------------------- 1 | # config/initializers/delayed_job.rb 2 | 3 | # Update settings. 4 | Delayed::Worker.destroy_failed_jobs = false 5 | Delayed::Worker.read_ahead = 10 6 | Delayed::Worker.default_priority = 10 7 | Delayed::Worker.max_run_time = 20.minutes 8 | Delayed::Worker.default_queue_name = "worker" 9 | Delayed::Worker.max_attempts = 6 10 | 11 | if !Delayed::Worker.instance_methods.include?(:handle_failed_job) 12 | raise "Could not update Delayed::Worker!" 13 | end 14 | 15 | # Patch to log errors. 16 | class Delayed::Worker 17 | alias_method :original_handle_failed_job, :handle_failed_job 18 | 19 | def handle_failed_job(job, error) 20 | begin 21 | # Send an alert. 22 | Log.exception(error) 23 | rescue => e 24 | # Shouldn't get here, but if it does, at least log it. 25 | Log.exception(e) 26 | end 27 | 28 | # Handle as usual. 29 | original_handle_failed_job(job, error) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/log.rb: -------------------------------------------------------------------------------- 1 | class MyLogger 2 | def initialize 3 | end 4 | 5 | def exception(exception) 6 | msg = "#{exception.class} (#{exception.message}):\n " + 7 | clean_backtrace(exception).join("\n ") 8 | self.error(msg) 9 | 10 | # Trigger an exception email... 11 | if defined?(Airbrake) 12 | Airbrake.notify(exception) 13 | end 14 | rescue => e 15 | print e.to_s 16 | end 17 | 18 | def clean_backtrace(exception) 19 | if backtrace = exception.backtrace 20 | if defined?(RAILS_ROOT) 21 | return backtrace.map { |line| line.sub RAILS_ROOT, '' } 22 | else 23 | return backtrace 24 | end 25 | else 26 | return [] 27 | end 28 | end 29 | 30 | def librato(mode, key, value) 31 | dyno_name = ENV['DYNO'] 32 | 33 | if dyno_name 34 | info("source=#{dyno_name} #{mode}\##{key}=#{value}") 35 | else 36 | info("#{mode}\##{key}=#{value}") 37 | end 38 | end 39 | 40 | private 41 | 42 | def method_missing(method, *args, &block) 43 | Rails.logger.send(method, *args, &block) 44 | end 45 | end 46 | 47 | Log = MyLogger.new() 48 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/ruby_template_handler.rb: -------------------------------------------------------------------------------- 1 | # handler = ->(template) { template.source } 2 | ActionView::Template.register_template_handler(:rb, :source.to_proc) 3 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: "_SuperIMAP_session_#{Rails.env}" 4 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | omniauth_callbacks: 27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 28 | success: "Successfully authenticated from %{kind} account." 29 | passwords: 30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 33 | updated: "Your password has been changed successfully. You are now signed in." 34 | updated_not_active: "Your password has been changed successfully." 35 | registrations: 36 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 37 | signed_up: "Welcome! You have signed up successfully." 38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 42 | updated: "Your account has been updated successfully." 43 | sessions: 44 | signed_in: "Signed in successfully." 45 | signed_out: "Signed out successfully." 46 | already_signed_out: "Signed out successfully." 47 | unlocks: 48 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 49 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 50 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 51 | errors: 52 | messages: 53 | already_confirmed: "was already confirmed, please try signing in" 54 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 55 | expired: "has expired, please request a new one" 56 | not_found: "not found" 57 | not_locked: "was not locked" 58 | not_saved: 59 | one: "1 error prohibited this %{resource} from being saved:" 60 | other: "%{count} errors prohibited this %{resource} from being saved:" 61 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors 3 | # Ruby, Java, .NET, PHP, and Python applications with deep visibility and low overhead. 4 | # For more information, visit www.newrelic.com. 5 | # 6 | # Generated September 02, 2013 7 | # 8 | # This configuration file is custom generated for app17845580@heroku.com 9 | 10 | 11 | # Here are the settings that are common to all environments 12 | common: &default_settings 13 | # ============================== LICENSE KEY =============================== 14 | license_key: '<%= ENV["NEW_RELIC_LICENSE_KEY"] %>' 15 | 16 | # Agent Enabled (Ruby/Rails Only) 17 | # Use this setting to force the agent to run or not run. 18 | # Default is 'auto' which means the agent will install and run only 19 | # if a valid dispatcher such as Mongrel is running. This prevents 20 | # it from running with Rake or the console. Set to false to 21 | # completely turn the agent off regardless of the other settings. 22 | # Valid values are true, false and auto. 23 | # 24 | # agent_enabled: auto 25 | 26 | # Application Name Set this to be the name of your application as 27 | # you'd like it show up in New Relic. The service will then auto-map 28 | # instances of your application into an "application" on your 29 | # dashboard page. If you want to map this instance into multiple 30 | # apps, like "AJAX Requests" and "All UI" then specify a semicolon 31 | # separated list of up to three distinct names, or a yaml list. 32 | # Defaults to the capitalized RAILS_ENV or RACK_ENV (i.e., 33 | # Production, Staging, etc) 34 | # 35 | # Example: 36 | # 37 | # app_name: 38 | # - Ajax Service 39 | # - All Services 40 | # 41 | app_name: My Application 42 | 43 | # When "true", the agent collects performance data about your 44 | # application and reports this data to the New Relic service at 45 | # newrelic.com. This global switch is normally overridden for each 46 | # environment below. (formerly called 'enabled') 47 | monitor_mode: true 48 | 49 | # Developer mode should be off in every environment but 50 | # development as it has very high overhead in memory. 51 | developer_mode: false 52 | 53 | # The newrelic agent generates its own log file to keep its logging 54 | # information separate from that of your application. Specify its 55 | # log level here. 56 | log_level: info 57 | 58 | # Optionally set the path to the log file This is expanded from the 59 | # root directory (may be relative or absolute, e.g. 'log/' or 60 | # '/var/log/') The agent will attempt to create this directory if it 61 | # does not exist. 62 | # log_file_path: 'log' 63 | 64 | # Optionally set the name of the log file, defaults to 'newrelic_agent.log' 65 | # log_file_name: 'newrelic_agent.log' 66 | 67 | # The newrelic agent communicates with the service via https by default. This 68 | # prevents eavesdropping on the performance metrics transmitted by the agent. 69 | # The encryption required by SSL introduces a nominal amount of CPU overhead, 70 | # which is performed asynchronously in a background thread. If you'd prefer 71 | # to send your metrics over http uncomment the following line. 72 | # ssl: false 73 | 74 | #============================== Browser Monitoring =============================== 75 | # New Relic Real User Monitoring gives you insight into the performance real users are 76 | # experiencing with your website. This is accomplished by measuring the time it takes for 77 | # your users' browsers to download and render your web pages by injecting a small amount 78 | # of JavaScript code into the header and footer of each page. 79 | browser_monitoring: 80 | # By default the agent automatically injects the monitoring JavaScript 81 | # into web pages. Set this attribute to false to turn off this behavior. 82 | auto_instrument: true 83 | 84 | # Proxy settings for connecting to the New Relic server. 85 | # 86 | # If a proxy is used, the host setting is required. Other settings 87 | # are optional. Default port is 8080. 88 | # 89 | # proxy_host: hostname 90 | # proxy_port: 8080 91 | # proxy_user: 92 | # proxy_pass: 93 | 94 | # The agent can optionally log all data it sends to New Relic servers to a 95 | # separate log file for human inspection and auditing purposes. To enable this 96 | # feature, change 'enabled' below to true. 97 | # See: https://newrelic.com/docs/ruby/audit-log 98 | audit_log: 99 | enabled: false 100 | 101 | # Tells transaction tracer and error collector (when enabled) 102 | # whether or not to capture HTTP params. When true, frameworks can 103 | # exclude HTTP parameters from being captured. 104 | # Rails: the RoR filter_parameter_logging excludes parameters 105 | # Java: create a config setting called "ignored_params" and set it to 106 | # a comma separated list of HTTP parameter names. 107 | # ex: ignored_params: credit_card, ssn, password 108 | capture_params: false 109 | 110 | # Transaction tracer captures deep information about slow 111 | # transactions and sends this to the New Relic service once a 112 | # minute. Included in the transaction is the exact call sequence of 113 | # the transactions including any SQL statements issued. 114 | transaction_tracer: 115 | 116 | # Transaction tracer is enabled by default. Set this to false to 117 | # turn it off. This feature is only available at the Professional 118 | # and above product levels. 119 | enabled: true 120 | 121 | # Threshold in seconds for when to collect a transaction 122 | # trace. When the response time of a controller action exceeds 123 | # this threshold, a transaction trace will be recorded and sent to 124 | # New Relic. Valid values are any float value, or (default) "apdex_f", 125 | # which will use the threshold for an dissatisfying Apdex 126 | # controller action - four times the Apdex T value. 127 | transaction_threshold: apdex_f 128 | 129 | # When transaction tracer is on, SQL statements can optionally be 130 | # recorded. The recorder has three modes, "off" which sends no 131 | # SQL, "raw" which sends the SQL statement in its original form, 132 | # and "obfuscated", which strips out numeric and string literals. 133 | record_sql: obfuscated 134 | 135 | # Threshold in seconds for when to collect stack trace for a SQL 136 | # call. In other words, when SQL statements exceed this threshold, 137 | # then capture and send to New Relic the current stack trace. This is 138 | # helpful for pinpointing where long SQL calls originate from. 139 | stack_trace_threshold: 0.500 140 | 141 | # Determines whether the agent will capture query plans for slow 142 | # SQL queries. Only supported in mysql and postgres. Should be 143 | # set to false when using other adapters. 144 | # explain_enabled: true 145 | 146 | # Threshold for query execution time below which query plans will 147 | # not be captured. Relevant only when `explain_enabled` is true. 148 | # explain_threshold: 0.5 149 | 150 | # Error collector captures information about uncaught exceptions and 151 | # sends them to New Relic for viewing 152 | error_collector: 153 | 154 | # Error collector is enabled by default. Set this to false to turn 155 | # it off. This feature is only available at the Professional and above 156 | # product levels. 157 | enabled: true 158 | 159 | # Rails Only - tells error collector whether or not to capture a 160 | # source snippet around the place of the error when errors are View 161 | # related. 162 | capture_source: true 163 | 164 | # To stop specific errors from reporting to New Relic, set this property 165 | # to comma-separated values. Default is to ignore routing errors, 166 | # which are how 404's get triggered. 167 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 168 | 169 | # If you're interested in capturing memcache keys as though they 170 | # were SQL uncomment this flag. Note that this does increase 171 | # overhead slightly on every memcached call, and can have security 172 | # implications if your memcached keys are sensitive 173 | # capture_memcache_keys: true 174 | 175 | # Application Environments 176 | # ------------------------------------------ 177 | # Environment-specific settings are in this section. 178 | # For Rails applications, RAILS_ENV is used to determine the environment. 179 | # For Java applications, pass -Dnewrelic.environment to set 180 | # the environment. 181 | 182 | # NOTE if your application has other named environments, you should 183 | # provide newrelic configuration settings for these environments here. 184 | 185 | development: 186 | <<: *default_settings 187 | # Turn off communication to New Relic service in development mode (also 188 | # 'enabled'). 189 | # NOTE: for initial evaluation purposes, you may want to temporarily 190 | # turn the agent on in development mode. 191 | monitor_mode: false 192 | 193 | # Rails Only - when running in Developer Mode, the New Relic Agent will 194 | # present performance information on the last 100 transactions you have 195 | # executed since starting the mongrel. 196 | # NOTE: There is substantial overhead when running in developer mode. 197 | # Do not use for production or load testing. 198 | developer_mode: false 199 | 200 | # Enable textmate links 201 | # textmate: true 202 | 203 | test: 204 | <<: *default_settings 205 | # It almost never makes sense to turn on the agent when running 206 | # unit, functional or integration tests or the like. 207 | monitor_mode: false 208 | 209 | # Turn on the agent in production for 24x7 monitoring. NewRelic 210 | # testing shows an average performance impact of < 5 ms per 211 | # transaction, you can leave this on all the time without 212 | # incurring any user-visible performance degradation. 213 | production: 214 | <<: *default_settings 215 | monitor_mode: true 216 | app_name: 'SuperIMAP - Production' 217 | 218 | # Many applications have a staging environment which behaves 219 | # identically to production. Support for that environment is provided 220 | # here. By default, the staging environment has the agent turned on. 221 | staging: 222 | <<: *default_settings 223 | monitor_mode: true 224 | app_name: 'SuperIMAP - Staging' 225 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | devise_for :admin_users, ActiveAdmin::Devise.config 3 | ActiveAdmin.routes(self) 4 | 5 | namespace :api do 6 | namespace :v1 do 7 | resources :connections, { :param => :imap_provider_code } do 8 | resources :users, { :param => :tag } 9 | end 10 | end 11 | end 12 | 13 | namespace :users do 14 | resource :connect, :only => [:new] do 15 | get :callback 16 | end 17 | 18 | resource :disconnect, :only => [:new] do 19 | get :callback 20 | end 21 | end 22 | 23 | post 'webhook_test/new_mail' 24 | post 'webhook_test/user_connected' 25 | post 'webhook_test/user_disconnected' 26 | end 27 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 67e673bc90fb6b678536387f99d16c785fe68efb21fbc292e2e28230f1c3e1299702b50e6259c62ca1475f6a974ed10eea98b07f3039a15417f4252612ac0553 15 | 16 | test: 17 | secret_key_base: 7d7386508cd60715b05c1e8c62d755dd3b3cb4057f257c7759422c6877ea4d951e88951cd8685c50fea3996b6962bdaced05436cd8fa541eba5ab61b702d6b27 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | # config/unicorn.rb 2 | 3 | worker_processes Integer(ENV['WEB_CONCURRENCY'] || 2) 4 | timeout Integer(ENV['WEB_TIMEOUT'] || 30) 5 | preload_app true 6 | 7 | before_fork do |server, worker| 8 | Signal.trap 'TERM' do 9 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead' 10 | Process.kill 'QUIT', Process.pid 11 | end 12 | 13 | if defined?(ActiveRecord::Base) 14 | ActiveRecord::Base.connection.disconnect! 15 | end 16 | end 17 | 18 | after_fork do |server, worker| 19 | Signal.trap 'TERM' do 20 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to sent QUIT' 21 | end 22 | 23 | if defined?(ActiveRecord::Base) 24 | ActiveRecord::Base.establish_connection 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20141029214610_devise_create_admin_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateAdminUsers < ActiveRecord::Migration 2 | def migrate(direction) 3 | super 4 | # Create a default user 5 | AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if direction == :up 6 | end 7 | 8 | def change 9 | create_table(:admin_users) do |t| 10 | ## Database authenticatable 11 | t.string :email, null: false, default: "" 12 | t.string :encrypted_password, null: false, default: "" 13 | 14 | ## Recoverable 15 | t.string :reset_password_token 16 | t.datetime :reset_password_sent_at 17 | 18 | ## Rememberable 19 | t.datetime :remember_created_at 20 | 21 | ## Trackable 22 | t.integer :sign_in_count, default: 0, null: false 23 | t.datetime :current_sign_in_at 24 | t.datetime :last_sign_in_at 25 | t.string :current_sign_in_ip 26 | t.string :last_sign_in_ip 27 | 28 | ## Confirmable 29 | # t.string :confirmation_token 30 | # t.datetime :confirmed_at 31 | # t.datetime :confirmation_sent_at 32 | # t.string :unconfirmed_email # Only if using reconfirmable 33 | 34 | ## Lockable 35 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 36 | # t.string :unlock_token # Only if unlock strategy is :email or :both 37 | # t.datetime :locked_at 38 | 39 | 40 | t.timestamps 41 | end 42 | 43 | add_index :admin_users, :email, unique: true 44 | add_index :admin_users, :reset_password_token, unique: true 45 | # add_index :admin_users, :confirmation_token, unique: true 46 | # add_index :admin_users, :unlock_token, unique: true 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /db/migrate/20141029214612_create_active_admin_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAdminComments < ActiveRecord::Migration 2 | def self.up 3 | create_table :active_admin_comments do |t| 4 | t.string :namespace 5 | t.text :body 6 | t.string :resource_id, null: false 7 | t.string :resource_type, null: false 8 | t.references :author, polymorphic: true 9 | t.timestamps 10 | end 11 | add_index :active_admin_comments, [:namespace] 12 | add_index :active_admin_comments, [:author_type, :author_id] 13 | add_index :active_admin_comments, [:resource_type, :resource_id] 14 | end 15 | 16 | def self.down 17 | drop_table :active_admin_comments 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20141029215033_create_partners.rb: -------------------------------------------------------------------------------- 1 | class CreatePartners < ActiveRecord::Migration 2 | def change 3 | create_table :partners do |t| 4 | t.string :api_key, :index => true 5 | t.string :name 6 | t.string :success_webhook 7 | t.string :failure_webhook 8 | t.integer :partner_connections_count, :default => 0 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20141029215101_create_mail_logs.rb: -------------------------------------------------------------------------------- 1 | class CreateMailLogs < ActiveRecord::Migration 2 | def change 3 | create_table :mail_logs do |t| 4 | t.references :user, index: true 5 | t.string :md5, :limit => 32, index: true 6 | t.string :message_id 7 | t.integer :transmit_logs_count, :default => 0 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20141029215105_create_transmit_logs.rb: -------------------------------------------------------------------------------- 1 | class CreateTransmitLogs < ActiveRecord::Migration 2 | def change 3 | create_table :transmit_logs do |t| 4 | t.references :mail_log, index: true 5 | t.integer :response_code 6 | t.string :response_body, :limit => 1024 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20141031010321_create_imap_providers.rb: -------------------------------------------------------------------------------- 1 | class CreateImapProviders < ActiveRecord::Migration 2 | def change 3 | create_table :imap_providers do |t| 4 | t.string :code 5 | t.string :title 6 | t.integer :partner_connections_count 7 | t.string :host 8 | t.integer :port 9 | t.boolean :use_ssl 10 | t.string :oauth1_access_token_path 11 | t.string :oauth1_authorize_path 12 | t.string :oauth1_request_token_path 13 | t.string :oauth1_scope 14 | t.string :oauth1_site 15 | t.string :oauth2_grant_type 16 | t.string :oauth2_scope 17 | t.string :oauth2_site 18 | t.string :oauth2_token_method 19 | t.string :oauth2_token_url 20 | 21 | t.timestamps 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20141031010353_create_partner_connections.rb: -------------------------------------------------------------------------------- 1 | class CreatePartnerConnections < ActiveRecord::Migration 2 | def change 3 | create_table :partner_connections do |t| 4 | t.references :partner, index: true 5 | t.references :imap_provider, index: true 6 | t.integer :users_count, :default => 0 7 | t.string :oauth1_consumer_key 8 | t.string :oauth1_consumer_secret 9 | t.string :oauth2_client_id 10 | t.string :oauth2_client_secret 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20141031010433_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.references :partner_connection, index: true 5 | t.string :email 6 | t.string :tag 7 | t.integer :mail_logs_count, :default => 0 8 | t.datetime :last_connected_at 9 | t.datetime :last_email_at 10 | t.integer :last_uid 11 | t.string :last_uid_validity 12 | t.datetime :last_internal_date 13 | t.string :login_username 14 | t.string :login_password 15 | t.string :oauth1_token 16 | t.string :oauth1_token_secret 17 | t.string :oauth2_refresh_token 18 | t.timestamps 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20141104202256_create_imap_daemon_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class CreateImapDaemonHeartbeats < ActiveRecord::Migration 2 | def change 3 | create_table :imap_daemon_heartbeats do |t| 4 | t.string :tag 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20141111204248_add_archived_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddArchivedToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :archived, :boolean, :default => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141113163147_add_type_to_users_and_imap_providers.rb: -------------------------------------------------------------------------------- 1 | class AddTypeToUsersAndImapProviders < ActiveRecord::Migration 2 | def change 3 | add_column :imap_providers, :type, :string 4 | add_column :users, :type, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141114233206_add_locked_at_to_admin_user.rb: -------------------------------------------------------------------------------- 1 | class AddLockedAtToAdminUser < ActiveRecord::Migration 2 | def change 3 | add_column :admin_users, :locked_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141118170010_add_type_to_partner_connection.rb: -------------------------------------------------------------------------------- 1 | class AddTypeToPartnerConnection < ActiveRecord::Migration 2 | def change 3 | add_column :partner_connections, :type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141121152941_add_oauth2_fields_to_imap_provider.rb: -------------------------------------------------------------------------------- 1 | class AddOauth2FieldsToImapProvider < ActiveRecord::Migration 2 | def change 3 | add_column :imap_providers, :oauth2_authorize_url, :string 4 | add_column :imap_providers, :oauth2_response_type, :string 5 | add_column :imap_providers, :oauth2_access_type, :string 6 | add_column :imap_providers, :oauth2_approval_prompt, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20141121182537_add_redirect_urls_to_partner.rb: -------------------------------------------------------------------------------- 1 | class AddRedirectUrlsToPartner < ActiveRecord::Migration 2 | def change 3 | add_column :partners, :success_url, :string 4 | add_column :partners, :failure_url, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141121184010_rename_fields.rb: -------------------------------------------------------------------------------- 1 | class RenameFields < ActiveRecord::Migration 2 | def change 3 | remove_column :users, :last_connected_at, :datetime 4 | add_column :users, :connected_at, :datetime 5 | add_column :users, :last_login_at, :datetime 6 | remove_column :mail_logs, :md5, :string 7 | add_column :mail_logs, :sha1, :string, :limit => 40, :index => true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20141205024759_rename_partner_webhook_columns.rb: -------------------------------------------------------------------------------- 1 | class RenamePartnerWebhookColumns < ActiveRecord::Migration 2 | def change 3 | rename_column :partners, :success_webhook, :new_mail_webhook 4 | remove_column :partners, :failure_webhook 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141207191800_add_webhooks_to_partner.rb: -------------------------------------------------------------------------------- 1 | class AddWebhooksToPartner < ActiveRecord::Migration 2 | def change 3 | add_column :partners, :user_connected_webhook, :string 4 | add_column :partners, :user_disconnected_webhook, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141207200312_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs, :force => true do |table| 4 | table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. 6 | table.text :handler, :null => false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20141215194630_add_tracer_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTracerToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :enable_tracer, :boolean, :default => false 4 | # add_index :users, :enable_tracer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141215194652_create_tracer_logs.rb: -------------------------------------------------------------------------------- 1 | class CreateTracerLogs < ActiveRecord::Migration 2 | def change 3 | create_table :tracer_logs do |t| 4 | t.references :user, index: true 5 | t.string :uid, :limit => 20 6 | t.datetime :detected_at 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :tracer_logs, :uid 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20141215194754_add_smtp_settings_to_imap_provider.rb: -------------------------------------------------------------------------------- 1 | class AddSmtpSettingsToImapProvider < ActiveRecord::Migration 2 | def change 3 | rename_column :imap_providers, :host, :imap_host 4 | rename_column :imap_providers, :port, :imap_port 5 | rename_column :imap_providers, :use_ssl, :imap_use_ssl 6 | add_column :imap_providers, :smtp_host, :string 7 | add_column :imap_providers, :smtp_port, :integer 8 | add_column :imap_providers, :smtp_domain, :string 9 | add_column :imap_providers, :smtp_enable_starttls_auto, :boolean 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20141215212628_remove_oauth1_fields.rb: -------------------------------------------------------------------------------- 1 | class RemoveOauth1Fields < ActiveRecord::Migration 2 | def change 3 | remove_column :imap_providers, :oauth1_access_token_path 4 | remove_column :imap_providers, :oauth1_authorize_path 5 | remove_column :imap_providers, :oauth1_request_token_path 6 | remove_column :imap_providers, :oauth1_scope 7 | remove_column :imap_providers, :oauth1_site 8 | remove_column :partner_connections, :oauth1_consumer_key 9 | remove_column :partner_connections, :oauth1_consumer_secret 10 | remove_columns :users, :oauth1_token 11 | remove_columns :users, :oauth1_token_secret 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20150119185401_encrypt_existing_data.rb: -------------------------------------------------------------------------------- 1 | class EncryptExistingData < ActiveRecord::Migration 2 | def up 3 | # Update Oauth2::PartnerConnection entries. 4 | connections = PartnerConnection.where(:type => "Oauth2::PartnerConnection") 5 | connections.each do |connection| 6 | connection.oauth2_client_secret = connection.oauth2_client_secret_secure 7 | connection.save! 8 | end 9 | 10 | # Update Oauth2::User entries. 11 | users = User.where(:type => "Oauth2::User") 12 | users.each_index do |index| 13 | user = users[index] 14 | print "(#{index + 1}/#{users.length}) #{user.email}\n" 15 | user.oauth2_refresh_token = user.oauth2_refresh_token_secure 16 | user.save! 17 | end 18 | 19 | # Update Plain::User entries. 20 | users = User.where(:type => "Plain::User") 21 | users.each_index do |index| 22 | user = users[index] 23 | print "(#{index + 1}/#{users.length}) #{user.email}\n" 24 | user.login_password = user.login_password_secure 25 | user.save! 26 | end 27 | end 28 | 29 | def down 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/20150611134232_add_more_indexes_to_mail_logs.rb: -------------------------------------------------------------------------------- 1 | class AddMoreIndexesToMailLogs < ActiveRecord::Migration 2 | def up 3 | add_index :mail_logs, [:user_id, :message_id] 4 | add_index :mail_logs, [:user_id, :sha1] 5 | end 6 | 7 | def down 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150611134232) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | enable_extension "pg_stat_statements" 19 | 20 | create_table "active_admin_comments", force: true do |t| 21 | t.string "namespace" 22 | t.text "body" 23 | t.string "resource_id", null: false 24 | t.string "resource_type", null: false 25 | t.integer "author_id" 26 | t.string "author_type" 27 | t.datetime "created_at" 28 | t.datetime "updated_at" 29 | end 30 | 31 | add_index "active_admin_comments", ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id", using: :btree 32 | add_index "active_admin_comments", ["namespace"], name: "index_active_admin_comments_on_namespace", using: :btree 33 | add_index "active_admin_comments", ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id", using: :btree 34 | 35 | create_table "admin_users", force: true do |t| 36 | t.string "email", default: "", null: false 37 | t.string "encrypted_password", default: "", null: false 38 | t.string "reset_password_token" 39 | t.datetime "reset_password_sent_at" 40 | t.datetime "remember_created_at" 41 | t.integer "sign_in_count", default: 0, null: false 42 | t.datetime "current_sign_in_at" 43 | t.datetime "last_sign_in_at" 44 | t.string "current_sign_in_ip" 45 | t.string "last_sign_in_ip" 46 | t.datetime "created_at" 47 | t.datetime "updated_at" 48 | t.datetime "locked_at" 49 | end 50 | 51 | add_index "admin_users", ["email"], name: "index_admin_users_on_email", unique: true, using: :btree 52 | add_index "admin_users", ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true, using: :btree 53 | 54 | create_table "delayed_jobs", force: true do |t| 55 | t.integer "priority", default: 0, null: false 56 | t.integer "attempts", default: 0, null: false 57 | t.text "handler", null: false 58 | t.text "last_error" 59 | t.datetime "run_at" 60 | t.datetime "locked_at" 61 | t.datetime "failed_at" 62 | t.string "locked_by" 63 | t.string "queue" 64 | t.datetime "created_at" 65 | t.datetime "updated_at" 66 | end 67 | 68 | add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree 69 | 70 | create_table "imap_daemon_heartbeats", force: true do |t| 71 | t.string "tag" 72 | t.datetime "created_at" 73 | t.datetime "updated_at" 74 | end 75 | 76 | create_table "imap_providers", force: true do |t| 77 | t.string "code" 78 | t.string "title" 79 | t.integer "partner_connections_count" 80 | t.string "imap_host" 81 | t.integer "imap_port" 82 | t.boolean "imap_use_ssl" 83 | t.string "oauth2_grant_type" 84 | t.string "oauth2_scope" 85 | t.string "oauth2_site" 86 | t.string "oauth2_token_method" 87 | t.string "oauth2_token_url" 88 | t.datetime "created_at" 89 | t.datetime "updated_at" 90 | t.string "type" 91 | t.string "oauth2_authorize_url" 92 | t.string "oauth2_response_type" 93 | t.string "oauth2_access_type" 94 | t.string "oauth2_approval_prompt" 95 | t.string "smtp_host" 96 | t.integer "smtp_port" 97 | t.string "smtp_domain" 98 | t.boolean "smtp_enable_starttls_auto" 99 | end 100 | 101 | create_table "mail_logs", force: true do |t| 102 | t.integer "user_id" 103 | t.string "message_id" 104 | t.integer "transmit_logs_count", default: 0 105 | t.datetime "created_at" 106 | t.datetime "updated_at" 107 | t.string "sha1", limit: 40 108 | end 109 | 110 | add_index "mail_logs", ["user_id", "message_id"], name: "index_mail_logs_on_user_id_and_message_id", using: :btree 111 | add_index "mail_logs", ["user_id", "sha1"], name: "index_mail_logs_on_user_id_and_sha1", using: :btree 112 | add_index "mail_logs", ["user_id"], name: "index_mail_logs_on_user_id", using: :btree 113 | 114 | create_table "partner_connections", force: true do |t| 115 | t.integer "partner_id" 116 | t.integer "imap_provider_id" 117 | t.integer "users_count", default: 0 118 | t.string "oauth2_client_id" 119 | t.string "oauth2_client_secret" 120 | t.datetime "created_at" 121 | t.datetime "updated_at" 122 | t.string "type" 123 | end 124 | 125 | add_index "partner_connections", ["imap_provider_id"], name: "index_partner_connections_on_imap_provider_id", using: :btree 126 | add_index "partner_connections", ["partner_id"], name: "index_partner_connections_on_partner_id", using: :btree 127 | 128 | create_table "partners", force: true do |t| 129 | t.string "api_key" 130 | t.string "name" 131 | t.string "new_mail_webhook" 132 | t.integer "partner_connections_count", default: 0 133 | t.datetime "created_at" 134 | t.datetime "updated_at" 135 | t.string "success_url" 136 | t.string "failure_url" 137 | t.string "user_connected_webhook" 138 | t.string "user_disconnected_webhook" 139 | end 140 | 141 | create_table "tracer_logs", force: true do |t| 142 | t.integer "user_id" 143 | t.string "uid", limit: 20 144 | t.datetime "detected_at" 145 | t.datetime "created_at" 146 | t.datetime "updated_at" 147 | end 148 | 149 | add_index "tracer_logs", ["uid"], name: "index_tracer_logs_on_uid", using: :btree 150 | add_index "tracer_logs", ["user_id"], name: "index_tracer_logs_on_user_id", using: :btree 151 | 152 | create_table "transmit_logs", force: true do |t| 153 | t.integer "mail_log_id" 154 | t.integer "response_code" 155 | t.string "response_body", limit: 1024 156 | t.datetime "created_at" 157 | t.datetime "updated_at" 158 | end 159 | 160 | add_index "transmit_logs", ["mail_log_id"], name: "index_transmit_logs_on_mail_log_id", using: :btree 161 | 162 | create_table "users", force: true do |t| 163 | t.integer "partner_connection_id" 164 | t.string "email" 165 | t.string "tag" 166 | t.integer "mail_logs_count", default: 0 167 | t.datetime "last_email_at" 168 | t.integer "last_uid" 169 | t.string "last_uid_validity" 170 | t.datetime "last_internal_date" 171 | t.string "login_username" 172 | t.string "login_password" 173 | t.string "oauth2_refresh_token" 174 | t.datetime "created_at" 175 | t.datetime "updated_at" 176 | t.boolean "archived", default: false 177 | t.string "type" 178 | t.datetime "connected_at" 179 | t.datetime "last_login_at" 180 | t.boolean "enable_tracer", default: false 181 | end 182 | 183 | add_index "users", ["partner_connection_id"], name: "index_users_on_partner_connection_id", using: :btree 184 | 185 | end 186 | -------------------------------------------------------------------------------- /db/seeds-development.rb: -------------------------------------------------------------------------------- 1 | AdminUser.new(:email => "admin@example.com", :password => "password").save 2 | 3 | plain_provider = Plain::ImapProvider.create( 4 | :code => 'PLAIN', 5 | :title => "Fake IMAP", 6 | :imap_host => "localhost", 7 | :imap_port => 10143, 8 | :imap_use_ssl => false) 9 | 10 | Oauth2::ImapProvider.create!( 11 | :code => 'GMAIL_OAUTH2', 12 | :title => "Google Mail - OAuth 2.0", 13 | :imap_host => "imap.gmail.com", 14 | :imap_port => 993, 15 | :imap_use_ssl => true, 16 | :smtp_host => "smtp.gmail.com", 17 | :smtp_port => 587, 18 | :smtp_domain => "gmail.com", 19 | :smtp_enable_starttls_auto => true, 20 | :oauth2_site => "https://accounts.google.com", 21 | :oauth2_token_method => "post", 22 | :oauth2_grant_type => "refresh_token", 23 | :oauth2_scope => "https://www.googleapis.com/auth/userinfo.email https://mail.google.com/", 24 | :oauth2_token_url => "/o/oauth2/token", 25 | :oauth2_authorize_url => "/o/oauth2/auth", 26 | :oauth2_response_type => "code", 27 | :oauth2_access_type => "offline", 28 | :oauth2_approval_prompt => "force") 29 | 30 | def create_transmit_log(mail_log, n) 31 | mail_log.transmit_logs.create(:response_code => 200, :response_body => "Response #{n}") 32 | end 33 | 34 | def create_mail_log(user, n) 35 | user.mail_logs.create!(:message_id => "abc#{n}").tap do |mail_log| 36 | create_transmit_log(mail_log, 1) 37 | create_transmit_log(mail_log, 2) 38 | create_transmit_log(mail_log, 3) 39 | end 40 | end 41 | 42 | def create_user(connection, n) 43 | connection.users.create!( 44 | :tag => "User #{n}", 45 | :email => "user#{n}@localhost", 46 | :login_username => "user#{n}@localhost", 47 | :login_password => "password").tap do |user| 48 | create_mail_log(user, 1) 49 | create_mail_log(user, 2) 50 | create_mail_log(user, 3) 51 | end 52 | end 53 | 54 | def create_partner_connection(partner, imap_provider) 55 | partner.connections.create!(:imap_provider_id => imap_provider.id).tap do |connection| 56 | 5.times.each do |n| 57 | create_user(connection, n) 58 | end 59 | end 60 | end 61 | 62 | Partner.create!( 63 | :name => "Partner", 64 | :new_mail_webhook => "http://localhost:5000/webhook_test/new_mail", 65 | :user_connected_webhook => "http://localhost:5000/webhook_test/user_connected", 66 | :user_disconnected_webhook => "http://localhost:5000/webhook_test/user_disconnected", 67 | :success_url => "/success.html", 68 | :failure_url => "/failure.html").tap do |partner| 69 | create_partner_connection(partner, plain_provider) 70 | end 71 | -------------------------------------------------------------------------------- /db/seeds-production.rb: -------------------------------------------------------------------------------- 1 | Oauth2::ImapProvider.create!( 2 | :code => 'GMAIL_OAUTH2', 3 | :title => "Google Mail - OAuth 2.0", 4 | :imap_host => "imap.gmail.com", 5 | :imap_port => 993, 6 | :imap_use_ssl => true, 7 | :smtp_host => "smtp.gmail.com", 8 | :smtp_port => 587, 9 | :smtp_domain => "gmail.com", 10 | :smtp_enable_starttls_auto => true, 11 | :oauth2_site => "https://accounts.google.com", 12 | :oauth2_token_url => "/o/oauth2/token", 13 | :oauth2_token_method => "post", 14 | :oauth2_grant_type => "refresh_token", 15 | :oauth2_scope => "https://www.googleapis.com/auth/userinfo.email https://mail.google.com/", 16 | :oauth2_token_url => "/o/oauth2/token", 17 | :oauth2_authorize_url => "/o/oauth2/auth", 18 | :oauth2_response_type => "code", 19 | :oauth2_access_type => "offline", 20 | :oauth2_approval_prompt => "force") 21 | 22 | AdminUser.new(:email => "admin@example.com", :password => "password").save 23 | -------------------------------------------------------------------------------- /db/seeds-stress.rb: -------------------------------------------------------------------------------- 1 | AdminUser.new(:email => "admin@example.com", :password => "password").save 2 | 3 | imap_provider = Plain::ImapProvider.create!( 4 | :code => 'PLAIN', 5 | :title => "Fake IMAP", 6 | :imap_host => "127.0.0.1", 7 | :imap_port => 10143, 8 | :imap_use_ssl => false) 9 | 10 | def create_user(connection, n) 11 | connection.users.create!( 12 | :tag => "User #{n}", 13 | :email => "user#{n}@localhost", 14 | :login_username => "user#{n}@localhost", 15 | :login_password => "password", 16 | :connected_at => Time.now) 17 | end 18 | 19 | def create_partner_connection(partner, imap_provider) 20 | partner.connections.create!(:imap_provider_id => imap_provider.id).tap do |connection| 21 | 1000.times.each do |n| 22 | create_user(connection, n) 23 | end 24 | end 25 | end 26 | 27 | Partner.create!( 28 | :name => "Partner", 29 | :new_mail_webhook => "ignored", 30 | :success_url => "ignored", 31 | :failure_url => "ignored").tap do |partner| 32 | create_partner_connection(partner, imap_provider) 33 | end 34 | -------------------------------------------------------------------------------- /db/seeds-test.rb: -------------------------------------------------------------------------------- 1 | AdminUser.new(:email => "admin@example.com", :password => "password").save 2 | 3 | plain_provider = Plain::ImapProvider.create!( 4 | :code => 'PLAIN', 5 | :title => "Fake IMAP", 6 | :imap_host => "localhost", 7 | :imap_port => 10143, 8 | :imap_use_ssl => false) 9 | 10 | def create_transmit_log(mail_log, n) 11 | mail_log.transmit_logs.create!(:response_code => 200, :response_body => "Response #{n}") 12 | end 13 | 14 | def create_mail_log(user, n) 15 | user.mail_logs.create!(:message_id => "abc#{n}").tap do |mail_log| 16 | create_transmit_log(mail_log, 1) 17 | create_transmit_log(mail_log, 2) 18 | create_transmit_log(mail_log, 3) 19 | end 20 | end 21 | 22 | def create_user(connection, n) 23 | connection.users.create!( 24 | :tag => "User #{n}", 25 | :email => "user#{n}@localhost", 26 | :login_username => "user#{n}@localhost", 27 | :login_password => "password").tap do |user| 28 | create_mail_log(user, 1) 29 | create_mail_log(user, 2) 30 | create_mail_log(user, 3) 31 | end 32 | end 33 | 34 | def create_partner_connection(partner, imap_provider) 35 | connection = partner.connections.create!(:imap_provider_id => imap_provider.id).tap do |connection| 36 | 5.times.each do |n| 37 | create_user(connection, n) 38 | end 39 | end 40 | end 41 | 42 | Partner.create!( 43 | :name => "Partner", 44 | :new_mail_webhook => "ignored", 45 | :success_url => "ignored", 46 | :failure_url => "ignored").tap do |partner| 47 | create_partner_connection(partner, plain_provider) 48 | end 49 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | require_relative "./seeds-#{Rails.env}.rb" 2 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/imap.rake: -------------------------------------------------------------------------------- 1 | namespace :imap do 2 | task :client => :environment do 3 | Log.info("Starting an IMAP Client process...") 4 | 5 | # Read environment variables. 6 | config = {} 7 | config[:stress_test_mode] = (ENV['STRESS_TEST_MODE'] == "true") 8 | config[:num_worker_threads] = (ENV['NUM_WORKER_THREADS'] || 5).to_i 9 | config[:max_user_threads] = (ENV['MAX_USER_THREADS'] || 500).to_i 10 | config[:max_email_size] = (ENV['MAX_EMAIL_SIZE'] || (1024 * 1024)).to_i 11 | config[:tracer_interval] = (ENV['TRACER_INTERVAL'] || 10 * 60).to_i 12 | config[:num_tracers] = (ENV['NUM_TRACERS'] || 3).to_i 13 | config[:enable_chaos] = (ENV['ENABLE_CHAOS'] || "true") == "true" 14 | config[:enable_profiler] = (ENV['ENABLE_PROFILER'] || "false") == "true" 15 | 16 | require 'imap_client' 17 | ImapClient::Daemon.new(config).run 18 | end 19 | 20 | task :test_server => :environment do 21 | Log.info("Starting an IMAP Test Server process...") 22 | ImapDaemonHeartbeat.destroy_all 23 | 24 | # Read environment variables. 25 | config = {} 26 | config[:port] = ImapProvider.first.imap_port 27 | config[:length_of_test] = (ENV['LENGTH_OF_TEST'] || 1).to_i 28 | config[:emails_per_minute] = (ENV['EMAILS_PER_MINUTE'] || 500).to_i 29 | config[:enable_chaos] = (ENV['ENABLE_CHAOS'] || "true") == "true" 30 | 31 | require 'imap_test_server' 32 | ImapTestServer::Daemon.new(config).run 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/xoauth2_authenticator.rb: -------------------------------------------------------------------------------- 1 | require 'net/imap' 2 | 3 | class Net::IMAP 4 | class XOAuth2Authenticator 5 | def initialize(email_address, access_token) 6 | @email_address = email_address 7 | @access_token = access_token 8 | end 9 | 10 | def process(s) 11 | "user=#{@email_address}\x01auth=Bearer #{@access_token}\x01\x01" 12 | end 13 | end 14 | 15 | add_authenticator 'XOAUTH2', XOAuth2Authenticator 16 | end 17 | 18 | class Net::SMTP 19 | def auth_xoauth2(email_address, access_token) 20 | res = critical { 21 | auth_string = "user=#{email_address}\x01auth=Bearer #{access_token}\x01\x01" 22 | get_response('AUTH XOAUTH2 ' + base64_encode(auth_string)) 23 | } 24 | check_auth_response res 25 | res 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/log/.keep -------------------------------------------------------------------------------- /log/stress/.gitcreate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/log/stress/.gitcreate -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/failure.html: -------------------------------------------------------------------------------- 1 | FAILURE 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/success.html: -------------------------------------------------------------------------------- 1 | SUCCESS 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/screenshot.png -------------------------------------------------------------------------------- /script/analyze-stress-test.R: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env Rscript 2 | 3 | library(ggplot2) 4 | 5 | ### 6 | ### READ DATA 7 | ### 8 | 9 | ## Read CSV files. 10 | cat("Reading CSV files.") 11 | generated <- read.csv('./log/stress/generated_emails.csv') 12 | fetched <- read.csv('./log/stress/fetched_emails.csv') 13 | events <- read.csv('./log/stress/events.csv') 14 | 15 | ## There are multiple processed files, one for each server. 16 | paths <- list.files(path = "./log/stress/", 17 | pattern = "processed_emails_.*.csv", 18 | full.names = TRUE) 19 | processed <- do.call(rbind, lapply(paths, FUN=function(path) { 20 | df <- read.csv(path) 21 | names(df) <- c("time", "email", "message.id") 22 | df$time <- as.POSIXct(df$time) 23 | return(df) 24 | })) 25 | processed <- processed[order(processed$time), ] 26 | 27 | ## Rename columns. 28 | names(generated) <- c("time", "email", "message.id") 29 | names(fetched) <- c("time", "email", "message.id") 30 | names(processed) <- c("time", "email", "message.id") 31 | names(events) <- c("time", "email", "event") 32 | 33 | ## Normalize column values. 34 | generated$time <- as.POSIXct(generated$time) 35 | fetched$time <- as.POSIXct(fetched$time) 36 | processed$time <- as.POSIXct(processed$time) 37 | events$time <- as.POSIXct(events$time) 38 | 39 | ## Split out chaotic events. 40 | chaotic.events <- events[grepl("chaos", events$event),] 41 | 42 | ## Hack, make sure we have at least one chaotic event, otherwise we'll 43 | ## get errors. 44 | if (nrow(chaotic.events) == 0) { 45 | chaotic.events <- data.frame(time=min(generated$time), email=NA, event=NA, total=NA) 46 | } 47 | 48 | ## Add count columns. 49 | generated$count <- 1 50 | fetched$count <- 1 51 | processed$count <- 1 52 | chaotic.events$count <- 1 53 | 54 | ## Add total columns. 55 | generated$total <- cumsum(generated$count) 56 | fetched$total <- cumsum(fetched$count) 57 | processed$total <- cumsum(processed$count) 58 | chaotic.events$total <- cumsum(chaotic.events$count) 59 | 60 | 61 | ### 62 | ### SAVE A PLOT 63 | ### 64 | 65 | cat("Generating plots.\n") 66 | 67 | x.limits <- c(min(generated$time), max(processed$time)) 68 | 69 | title = "# of Emails Generated, Fetched, & Processed Over Time" 70 | p1 <- ggplot() + 71 | ggtitle(title) + 72 | xlab("Time") + 73 | ylab("Count") + 74 | xlim(x.limits) + 75 | guides(color = guide_legend(title = NULL)) + 76 | geom_line(aes(generated$time, generated$total, col="Generated")) + 77 | geom_line(aes(fetched$time, fetched$total, col="Fetched")) + 78 | geom_line(aes(processed$time, processed$total, col="Processed")) + 79 | geom_line(aes(chaotic.events$time, chaotic.events$total, col="Chaos")) + 80 | theme(legend.position = "left") 81 | 82 | title = "Chaotic Events" 83 | p2 <- ggplot() + 84 | ggtitle(title) + 85 | xlab("Time") + 86 | ylab("Chaotic Events") + 87 | xlim(x.limits) + 88 | geom_point(aes(chaotic.events$time, chaotic.events$event)) 89 | 90 | ## Save the plots. 91 | cat("Saving plots.\n") 92 | pdf("./tmp/stress-test-results.pdf") 93 | print(p1) 94 | print(p2) 95 | dev.off() 96 | -------------------------------------------------------------------------------- /script/stress-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ulimit -n 2048 4 | 5 | [ -z "$LENGTH_OF_TEST" ] && LENGTH_OF_TEST="1" 6 | [ -z "$EMAILS_PER_MINUTE" ] && EMAILS_PER_MINUTE="500" 7 | [ -z "$ENABLE_CHAOS" ] && ENABLE_CHAOS="true" 8 | [ -z "$ENABLE_PROFILER" ] && ENABLE_PROFILER="false" 9 | [ -z "$RUBY_PROF_MEASURE_MODE" ] && RUBY_PROF_MEASURE_MODE="process" 10 | 11 | clear 12 | 13 | echo 14 | echo "Did you set up database? Postgres works best, and you need to run this:" 15 | echo "$ RAILS_ENV=stress rake db:drop db:create db:setup db:seed" 16 | echo 17 | echo "The goal of this stress test is to ensure that:" 18 | echo 19 | echo "1. CPU load remains stable." 20 | echo "2. Memory remains stable." 21 | echo "3. IO handles are properly closed." 22 | echo "4. The system actually reads all of the email." 23 | echo 24 | echo "Use system tools to monitor #1, #2, and #3." 25 | echo "View 'stress-test-results.pdf' for #4." 26 | echo 27 | echo "By design, the stress test generates bad IMAP responses." 28 | echo "These will look like exceptions in the imap_client process." 29 | echo "Do not be alarmed; you can ignore the errors." 30 | echo 31 | echo "Some useful environment variables to set:" 32 | echo 33 | echo "+ LENGTH_OF_TEST - Total number of minutes to run test. [$LENGTH_OF_TEST]" 34 | echo "+ EMAILS_PER_MINUTE - Rate of fake email generation. [$EMAILS_PER_MINUTE]" 35 | echo "+ ENABLE_CHAOS - 'true' if we should generate bad IMAP responses. [$ENABLE_CHAOS]" 36 | echo "+ ENABLE_PROFILER - 'true' to start the ruby-prof profiler. [$ENABLE_PROFILER]" 37 | echo "+ RUBY_PROF_MEASURE_MODE - What should ruby-prof measure? [$RUBY_PROF_MEASURE_MODE]" 38 | echo " - Can be 'wall', 'process', 'cpu', 'allocations', 'memory', 'gc_time', or 'gc_runs'" 39 | echo " - See https://github.com/ruby-prof/ruby-prof" 40 | echo 41 | echo "Press enter to continue, Control-C to exit." 42 | read 43 | 44 | echo "Beginning stress test. Please wait." 45 | 46 | # Remove old log files. 47 | rm ./log/stress/* 48 | 49 | # Begin the stress test. 50 | RAILS_ENV=stress foreman s -f Procfile.stress-test 51 | 52 | # Generate some plots. 53 | Rscript script/analyze-stress-test.R 54 | 55 | echo 56 | echo "Stress test results are in 'tmp/stress-test-results.pdf'." 57 | echo "If enabled, profiler results are in 'tmp/profile.html'." 58 | echo 59 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/api/v1/connections_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::V1::ConnectionsControllerTest < ActionController::TestCase 4 | setup do 5 | @partner = Partner.first 6 | @connection = @partner.connections.first 7 | end 8 | 9 | test "index" do 10 | get(:index, :api_key => @partner.api_key) 11 | assert_response :success 12 | end 13 | 14 | test "create" do 15 | code = @connection.imap_provider_code 16 | @connection.delete 17 | post(:create, :api_key => @partner.api_key, :imap_provider_code => code) 18 | assert_response :success 19 | end 20 | 21 | test "create without code" do 22 | post(:create, :api_key => @partner.api_key) 23 | assert_response :not_found 24 | end 25 | 26 | test "update" do 27 | post(:update, :api_key => @partner.api_key, :imap_provider_code => @connection.imap_provider_code) 28 | assert_response :success 29 | end 30 | 31 | test "show" do 32 | get(:show, :api_key => @partner.api_key, :imap_provider_code => @connection.imap_provider_code) 33 | assert_response :success 34 | end 35 | 36 | test "destroy" do 37 | delete(:destroy, :api_key => @partner.api_key, :imap_provider_code => @connection.imap_provider_code) 38 | assert_response :success 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/controllers/api/v1/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::V1::UsersControllerTest < ActionController::TestCase 4 | setup do 5 | @partner = Partner.first 6 | @connection = @partner.connections.first 7 | @code = @connection.imap_provider_code 8 | @user = @connection.users.first 9 | @data = { 10 | :api_key => @partner.api_key, 11 | :connection_imap_provider_code => @code 12 | } 13 | end 14 | 15 | test "index" do 16 | get(:index, @data) 17 | assert_response :success 18 | end 19 | 20 | test "create" do 21 | post(:create, @data.merge(:tag => "TAG", :email => "EMAIL", 22 | :login_username => "LOGIN_USERNAME", 23 | :login_password => "LOGIN_PASSWORD")) 24 | assert_response :success 25 | 26 | user = User.find_by_tag("TAG") 27 | assert_equal "LOGIN_USERNAME", user.login_username 28 | assert_equal "LOGIN_PASSWORD", user.login_password_secure 29 | end 30 | 31 | test "create without tag and email" do 32 | post(:create, @data) 33 | assert_response :bad_request 34 | end 35 | 36 | test "update" do 37 | post(:update, @data.merge(:tag => @user.tag, :login_username => "NEW_USERNAME")) 38 | assert_response :success 39 | assert "NEW_USERNAME", @user.reload.login_username 40 | end 41 | 42 | test "show" do 43 | get(:show, @data.merge(:tag => @user.tag)) 44 | assert_response :success 45 | end 46 | 47 | test "destroy" do 48 | delete(:destroy, @data.merge(:tag => @user.tag)) 49 | assert_response :success 50 | assert @user.reload.archived 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/imap_daemon_heartbeats.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | tag: MyString 5 | 6 | two: 7 | tag: MyString 8 | -------------------------------------------------------------------------------- /test/fixtures/tracer_logs.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user_id: 5 | uid: MyString 6 | detected_at: 2014-12-15 14:46:52 7 | 8 | two: 9 | user_id: 10 | uid: MyString 11 | detected_at: 2014-12-15 14:46:52 12 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/helpers/.keep -------------------------------------------------------------------------------- /test/imap/rendezvous_hash_test.rb: -------------------------------------------------------------------------------- 1 | require 'imap_client' 2 | require 'test_helper' 3 | 4 | class RendezvousHashTest < ActiveSupport::TestCase 5 | test "Hashing" do 6 | site_tags = ["site 1", "site 2", "site 3"] 7 | r = ImapClient::RendezvousHash.new 8 | r.site_tags = site_tags 9 | assert site_tags.include?(r.hash("A")) 10 | assert_equal r.hash("A"), r.hash("A") 11 | assert_not_equal r.hash("A"), r.hash("B") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/mailers/.keep -------------------------------------------------------------------------------- /test/mailers/previews/tracer_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/tracer_mailer 2 | class TracerMailerPreview < ActionMailer::Preview 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/mailers/tracer_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TracerMailerTest < ActionMailer::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/test/models/.keep -------------------------------------------------------------------------------- /test/models/admin_user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdminUserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/imap_daemon_heartbeat_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ImapDaemonHeartbeatTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/imap_provider_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ImapProviderTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/mail_log_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MailLogTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/partner_connection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PartnerConnectionTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/partner_credential_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PartnerCredentialTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/partner_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PartnerTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/tracer_log_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TracerLogTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/transmit_log_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TransmitLogTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/super-imap/98b407b86b8499228a8920e0b5ed28a033e50ccf/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------