Compare commits

...

374 Commits

Author SHA1 Message Date
Travis Burtrum 1c049fc071 Send deletes across websocket and handle in poll as well so deletes update in realtime 2016-08-11 12:52:04 -04:00
Travis Burtrum 017e704791 Add --show-from-server option to parse the server from the Recieved header and display in a column, off by default 2016-08-11 08:23:33 -04:00
Travis Burtrum 0aa927ab50 Add --keep-num-emails to only keep that many emails and delete oldest 2016-08-10 16:35:05 -04:00
Travis Burtrum 93dd5d1758 Add --delete-older-than to delete messages older than a time modifier every time a mail is recieved 2016-08-10 16:15:56 -04:00
Travis Burtrum d42f2bf11d Add --sqlite-db option to allow for persistant storage 2016-08-10 15:38:34 -04:00
Travis Burtrum fb91dd0902 Parse from/to from the mail headers if possible 2016-08-10 14:45:58 -04:00
Samuel Cochran 7fe655fdac
Recommend turning off raise delivery errors
Folks may not always want MailCatcher running, and don't want errors
raised during mail delivery if it's not.
2016-08-10 10:03:45 +10:00
Samuel Cochran fdbe5c4535
Bump v0.6.5 2016-08-10 09:37:33 +10:00
Samuel Cochran e8531da70d
Catch errors during websocket send
Tidy up the error handling code a little, and substitute out gem paths.
2016-08-10 09:37:33 +10:00
Samuel Cochran 132f1c0f42
Fix mime-types in Gemfile 2016-07-26 21:20:34 +10:00
Samuel Cochran 0021a2909c We don't need no support 2016-04-23 15:53:56 +08:00
Samuel Cochran 2e27c830b1 Not-nil is enough 2016-04-23 15:49:55 +08:00
Samuel Cochran c29336b78d Just use a method 2016-04-23 15:49:03 +08:00
Samuel Cochran d932eff261 Use plain old JSON 2016-04-23 15:48:37 +08:00
Samuel Cochran e2d89b65db Remove unused variable (threw warning) 2016-04-23 15:45:25 +08:00
Samuel Cochran ba4ca7f8d6 Remove unused fractal endpoint 2016-04-23 15:45:06 +08:00
Samuel Cochran 17054f80ad Remove use of `which` 2016-04-23 15:43:27 +08:00
Samuel Cochran f5cbdec8b3 Just keep mime-types < 3, and use strong version comparison 2016-04-23 15:22:46 +08:00
Sasha Gerrand 02abbcbeb4 Constrain version for Ruby v1.9.3
The `mime-types`  gem removed support for MRI Ruby versions prior to 2.0 in
version 3. Adding a conditional constraint around the `RUBY_VERSION` constant in
the Gemfile allows the required Ruby version constraint in the gemspec to be
maintained.
2016-04-23 15:07:58 +08:00
Samuel Cochran bc86e995ef Merge pull request #221 from csiszarattila/master
Fix encoding errors for other than UTF-8 emails
2016-04-12 12:48:22 +10:00
Samuel Cochran ed9174ab42 Simplify and fix docker builder 2016-04-07 11:10:33 +10:00
Samuel Cochran 4dcf7776aa Merge pull request #281 from twe4ked/port-taken-error
Improve port taken error message
2016-03-30 15:57:45 +11:00
Odin Dutton dc6ae47749 Improve port taken error message 2016-03-30 15:21:58 +11:00
Samuel Cochran e3c23333bf HTTPS screenshot 2016-03-29 10:26:16 +11:00
Samuel Cochran 2411713bba Bump 0.6.4
Folks are having trouble with activesupport 5 beta. This release should
prevent mailcatcher attempting to use the beta gem.
2016-02-04 11:00:43 +11:00
Samuel Cochran f2a7385097 Rubygems doesn't know how to gem 2016-02-04 11:00:42 +11:00
Samuel Cochran cff07b758c Make sure rack doesn't go 2.0 2016-02-04 10:46:47 +11:00
Samuel Cochran f80a80ea9c Exclude activesupport 5 pre-releases 2016-01-31 22:28:33 +11:00
Samuel Cochran 2b53ab2735 Remove defunct TODOs 2016-01-18 14:49:03 +11:00
Samuel Cochran 4d7429c127 Revert "Relax thin and skinny restriction"
This reverts commit e6283f590b.

Thin 1.6 breaks mailcatcher exiting correctly.
2016-01-18 14:47:49 +11:00
Samuel Cochran e6283f590b Relax thin and skinny restriction
MailCatcher uses only public Thin API so should be okay, while Skinny
can take care of more intricate version dependencies.
2016-01-18 14:42:30 +11:00
Samuel Cochran 844c0a7d72 Bump 0.6.3 2016-01-18 14:04:04 +11:00
Samuel Cochran 420d24e914 Merge pull request #258 from grant-mccarriagher/fix/delete-queries
Fix delete message queries conflicting
2016-01-18 13:56:19 +11:00
Samuel Cochran 0c1a1e4148 Use new travis infrastructure 2016-01-15 17:22:04 +11:00
Samuel Cochran 79e023ef58 Bump to eventmachine 1.0.9.1 2016-01-15 17:12:53 +11:00
Samuel Cochran 433fad4e4d Remove Gemfile lock 2016-01-15 17:10:25 +11:00
Grant McCarriagher a6ef2147e0 Make the delete all messages variables not conflict with the delete single message variables. 2016-01-11 12:23:41 -05:00
Samuel Cochran efd7b4ff0e Missed Gemfile.lock update on 0.6.2 bump 2015-12-16 09:29:26 +11:00
Samuel Cochran c9c015b930 Merge pull request #219 from lichtamberg/master
Added mail count to document title
2015-12-14 16:06:07 +11:00
Samuel Cochran ca0166ded4 Bump version
Let's see if bumping eventmachine solves the memory leak issues.
2015-12-14 13:59:42 +11:00
Samuel Cochran 134a99c9c1 Make sure rubygem requires ruby > 1.9.3 2015-12-14 13:59:27 +11:00
Samuel Cochran d32b8465ba Make eventmachine requirement explicit 2015-12-14 13:57:20 +11:00
Samuel Cochran 885f0d95d8 Merge pull request #252 from CloCkWeRX/bump_event_machine
Fix #210 by upgrading eventmachine
2015-12-14 13:53:03 +11:00
Daniel O'Connor 9eabbd331b Fix #210 by upgrading eventmachine to a version with https://github.com/eventmachine/eventmachine/pull/586 applied 2015-12-14 12:49:36 +10:30
Csiszár Attila 101c068ac2 Fix encoding errors for other than UTF-8 emails
Always sended "Content-Type: text/html;charset=utf-8" header therefore its not worked for other than utf-8 encoded emails, showed non-ascii characters as question marks.
2015-04-16 17:12:01 +02:00
s 681ef78caa Added mail count to document title 2015-03-25 11:42:25 +01:00
Samuel Cochran d7a4737532 Fix view broken in 264e912
Thanks @vlada79, original commit 8e0fc5e, closes #211, #212.
2015-03-06 09:16:04 +11:00
Samuel Cochran 0179a924a0 Bump 0.6.1 2015-02-04 09:43:51 +11:00
Samuel Cochran 8e77cde268 Dammit eventmachine 2015-02-04 09:43:51 +11:00
Samuel Cochran 5e2e3d378b Bump 0.6.0 2015-01-29 21:51:59 +11:00
Samuel Cochran 80cbb52448 We now work on ruby 2.2 🎉 2015-01-29 21:50:06 +11:00
Samuel Cochran 5b3781b44c Un-exclude eventmachine 1.0.4 2015-01-29 21:44:56 +11:00
Samuel Cochran 757eafe337 Add monkey patch for eventmachine 1.0.4 2015-01-29 21:44:55 +11:00
Samuel Cochran d3336535ae Add instructions about ruby 2.2 and eventmachine 1.0.4 2015-01-29 20:50:31 +11:00
Samuel Cochran 23398d7042 Bump versions for prerelease eventmachine fix 2015-01-28 08:44:37 +11:00
Samuel Cochran 14a86ef8d4 Allow failure on 2.2.0 for now 2015-01-28 08:44:37 +11:00
Samuel Cochran 8e760d8a50 Why am I even using compass
I mean all of this stuff should just go away really. Might be time for
an interface overhaul. Or an everything overhaul.
2015-01-28 08:44:37 +11:00
Samuel Cochran 442a3f5404 bundle update 2015-01-28 08:28:40 +11:00
Samuel Cochran 7533eb4317 eventmachine 1.0.4 doesn't play nice 2015-01-28 08:01:30 +11:00
Samuel Cochran 3e1f362250 Update README instructions for PHP
Looks like sendmail_path is now restricted to PHP_INI_SYSTEM.

Thanks @albancrommer. Fixes #177.
2015-01-20 08:59:03 +11:00
Pavel de0edff86b Load active_support's core extensions 2015-01-17 17:28:02 +11:00
Samuel Cochran 1fee20497c Merge pull request #158 from boffbowsh/master
Add Dockerfile
2015-01-17 17:22:36 +11:00
Samuel Cochran e703dbcf13 Merge pull request #184 from captbaritone/patch-1
Ensure messages are shown in the order they were recieved
2015-01-17 17:18:02 +11:00
Samuel Cochran a4d9735b9a Update travis ruby versions 2015-01-17 17:07:57 +11:00
Samuel Cochran aa9b8c925a Set content types
Fixes #161
2015-01-17 16:54:07 +11:00
Samuel Cochran d3f083f8df Fix secure websocket protocol negotiation
Fixes #160
2015-01-17 16:41:57 +11:00
Samuel Cochran e76d367755 Bring `catchmail` closer to `sendmail`
Sendmail's conventional interface is `sendmail [options] [recipient ...]`

Fixes #188
2015-01-17 15:21:13 +11:00
Samuel Cochran 927b8a1aae Placate ambiguous argument warning 2015-01-17 15:08:42 +11:00
Samuel Cochran 021022c20f No need for namespaces 2015-01-17 15:08:26 +11:00
Samuel Cochran 841c3a2ef3 Simplify selenium setup 2015-01-17 15:08:13 +11:00
Samuel Cochran 640d3710ae Bundle update 2015-01-17 15:03:12 +11:00
Samuel Cochran 6ec409bfcb Remove haml 2015-01-17 15:03:00 +11:00
Paul Bowsher 18655f41f2 Clear apt cache too
Contribution from @thaJeztah
2015-01-15 16:30:49 +00:00
Paul Bowsher 9f2457aa4d Allow IP to be changed on the command line 2015-01-15 16:24:35 +00:00
Paul Bowsher 09e8349b6f Combined apt-get RUN commands 2015-01-15 16:22:47 +00:00
Jordan Eldredge de8a4735a7 Ensure messages are shown in the order they were recieved
If two messages were received within the same second, they may be sorted by the order they were received. Using `id` as a tie breaker should give a more consistent result.

I noticed this issue when writing tests for my Codeception testing framework Mailcatcher module: https://github.com/captbaritone/codeception-mailcatcher-module/issues/13 

**Note:** I have not tested this change, but I think it should be correct.
2014-12-31 08:34:58 -08:00
Paul Bowsher 1ab18c821d Add Dockerfile 2014-08-28 16:08:57 +01:00
Samuel Cochran eff638f920 bundle update 2014-06-05 21:32:24 +10:00
Samuel Cochran e3e7dec757 Load all of ActiveSupport
ActiveSupport has weird load order problems when loading only the
core extensions now.

For instance, active_support/core_ext/numeric/conversions.rb now
loads active_support/number_helper which needs
ActiveSupport::Autoload but doesn't require it, so causes a missing
constant error in MailCatcher.

Also tightening up the version constraint because I can't guarantee
backward compatibility now.
2014-06-05 21:27:47 +10:00
Samuel Cochran ed72712daa Fix some load order issues 2014-06-05 21:19:31 +10:00
Samuel Cochran 18797b26ee Travis doesn't like running on 1.8.7 2014-06-05 21:08:52 +10:00
Samuel Cochran d1f3a75929 Testing needs compiled assets 2014-06-05 21:08:52 +10:00
Samuel Cochran f3b7befe94 Looks like we've upgraded activesupport 2014-06-05 21:08:52 +10:00
Samuel Cochran 21ae84c059 Merge pull request #140 from dirkkelly/patch-1
README fix link to CM css matrix
2014-06-03 10:20:54 +10:00
Dirk Kelly c42fad387a README fix link to CM css matrix 2014-06-02 13:32:40 -04:00
Samuel Cochran d4c853db63 Merge pull request #139 from lime/activesupport-4x
Extend activesupport dependency to include 4.x
2014-06-01 13:41:38 +10:00
Emil Sågfors d661bd35bf Include all 4.x versions of activesupport in dependency 2014-05-28 19:49:15 +03:00
Samuel Cochran 2ad4ea38a9 Test on all the rubies 2014-05-18 15:10:40 +10:00
Samuel Cochran 2a8d42849c Merge pull request #136 from davad/patch-1
Small typo in README
2014-05-04 11:21:15 +10:00
David Landry fdb5483560 Small typo in README 2014-05-03 17:26:41 -04:00
Samuel Cochran 44ca32c3af Remove Growl; nobody uses it any more 2014-03-31 15:15:41 +11:00
Samuel Cochran 213119cb5f Remove reference link to Fractal, too 2014-03-31 13:24:00 +11:00
Samuel Cochran bb128422c4 Mention command line --help in README [fixes #131] 2014-03-31 12:54:21 +11:00
Samuel Cochran aa5e5bfd31 Remove deminishing of rewriting 2014-03-31 12:39:27 +11:00
Samuel Cochran dfaf307071 Remove README mention of Fractal 2014-03-31 12:39:15 +11:00
Samuel Cochran 184773b664 Remove fractal analysis, it doesn't work at the moment anyway 2014-03-26 20:33:37 +11:00
Samuel Cochran 65743c22fd Fix messages count on initial page load 2014-03-26 20:30:59 +11:00
Samuel Cochran 27277a6cbe An example that used to break 2014-03-26 12:33:38 +11:00
Samuel Cochran 90b4cefb5a Trial travisci configuration 2014-03-26 12:33:38 +11:00
Samuel Cochran c7cf74b7ad gem-named alias namespace 2014-03-26 12:33:38 +11:00
Samuel Cochran c9c7ef840d Annotate favicon for message count 2014-03-26 12:33:38 +11:00
Samuel Cochran c577f06ba0 Make sure message count is updated everywhere 2014-03-26 12:33:38 +11:00
Jakub Pavlík jn e1ee9eefc9 show message count in title #126 2014-03-26 12:33:38 +11:00
Samuel Cochran 8c5c8a51bb Use sprockets-helpers during development 2014-03-26 12:33:38 +11:00
Samuel Cochran 5812eadddb CoffeeScript stylistic changes 2014-03-26 12:33:38 +11:00
Samuel Cochran 903e3ad2fb Break out 404 file 2014-03-26 11:12:55 +11:00
Samuel Cochran a4e62f2e92 Bump version 2014-03-21 14:45:07 +11:00
Samuel Cochran 0de09cdf55 Make test the default rake task 2014-03-21 14:43:46 +11:00
Samuel Cochran d496655cf9 Make sure HTTP is up too 2014-03-21 14:41:15 +11:00
Samuel Cochran c3f6979314 Basic but ugly acceptance specs with selenium 2014-03-21 14:36:54 +11:00
Samuel Cochran 77212c3e98 Beginnings of specs 2014-03-21 11:58:44 +11:00
Samuel Cochran 8f5a5b59ce Use compressors when precompiling assets 2014-03-21 10:30:01 +11:00
Samuel Cochran 20ffd4433e Rename asset bundles for no particular reason 2014-03-21 10:13:52 +11:00
Samuel Cochran 70be04c478 Use packaged assets 2014-03-17 19:35:58 +11:00
Samuel Cochran 0398d2d6a3 Switch to sprockets for assets 2014-03-17 19:06:32 +11:00
Samuel Cochran 272b4fa855 My style has evolved 2014-03-17 16:31:05 +11:00
Samuel Cochran 2056339bdb Add a gem-named requirable file 2014-03-17 15:58:02 +11:00
Samuel Cochran d2ba6d19f2 Ignore .bundle directory 2014-03-17 15:57:22 +11:00
Samuel Cochran 5b9424c650 Fix line endings
SMTP lines always end with CR LF "\r\n", so just make sure the
lines received do too.
2014-02-25 16:30:33 +11:00
Rick Cobb 64e1ef41d8 Oops. Write the newline in correctly. 2014-02-19 16:44:10 -08:00
Rick Cobb 295691d625 Example mail with = problem at the end of quoted-printable
And a workaround for it.
2014-02-19 16:13:42 -08:00
Samuel Cochran f575b849a4 Merge pull request #121 from jjoos/master
Syncing output when running in foreground.
2014-02-13 16:33:07 +11:00
Jan Deelstra a90e115c89 Syncing output when running in foreground. 2014-02-12 13:45:39 +01:00
Samuel Cochran 7c31360b3a Refine language a little 2013-12-12 10:31:23 +11:00
Sylvain Rayé f4061fa861 add information for PHP users to set sendmail_path using mailcatcher with a different SMTP ip (usefull on OSX because port 1025 was used in my case) 2013-12-04 11:53:51 +01:00
Samuel Cochran f93df88021 Merge branch 'no-exit' [closes #92] 2013-11-18 11:00:03 +11:00
Samuel Cochran 264e912a02 Refine the no-exit implementaton a little 2013-11-18 10:59:11 +11:00
Samuel Cochran a6c8f680c4 Tidy up exit behaviour 2013-09-16 12:03:45 +10:00
Samuel Cochran 8913812b92 Expose whole options hash instead of using define_method 2013-09-16 12:02:13 +10:00
Samuel Cochran 61f0fdede3 Merge pull request #97 from igneus/url-clickable-in-plain-text
New display format: plain text with clickable URLs
2013-09-15 17:24:18 -07:00
Samuel Cochran c1e4b5da86 Merge pull request #108 from toy/remember-height
Save separator sizing in localStorage if available
2013-09-15 17:20:34 -07:00
Samuel Cochran cc17f2d765 Merge pull request #110 from maxgalbu/ignorecommoncommands
Ignore common sendmail options
2013-09-15 17:18:54 -07:00
maxgalbu e9f59ba608 ignore common sendmail commands 2013-09-06 10:17:10 +02:00
Ivan Kuchin 3ffa6a3237 save separator sizing in localStorage if available 2013-08-22 00:49:34 +02:00
Jakub Pavlík jn 2f5c2ff01c decoration of the message body in iframe's load event handler 2013-07-28 22:57:36 +02:00
Samuel Cochran 7cc20ce471 Don't guarantee we'll work on HAML 4.1+ 2013-07-18 11:40:48 +10:00
Samuel Cochran f8df981d96 Allow activesupport ~> 4.0.0
Closes #98 (didn't want to update Gemfile.lock so extensively)
2013-07-18 11:39:34 +10:00
Jakub Pavlík jn ffb4ec4e4c plain text html-decorated on the client side 2013-07-14 21:17:37 +02:00
Jakub Pavlík jn ef09b0fde3 new display format: plain text with clickable URLs 2013-07-13 16:15:41 +02:00
Samuel Cochran fb13a62589 More correctly, add from option to catchmail invocation 2013-06-07 16:45:26 +08:00
Samuel Cochran dd180d96cb Make PHP code example consistent 2013-06-07 15:11:25 +08:00
Samuel Cochran 0b6d041b93 Merge pull request #95 from gondo/patch-1
notes for PHP
2013-06-07 14:51:04 +08:00
Samuel Cochran 44262f9862 They are prepended in JS-land anyway
Addresses @gondo's concern in #78.
2013-06-07 14:25:58 +08:00
gondo 9d71c72c42 notes for PHP
- "dont forget FROM" otherwise mail() will just fail
- added php code example
2013-06-06 11:50:37 +10:00
Samuel Cochran a06f51d4bb Merge pull request #93 from hzy-/patch-1
Added Django usage instructions
2013-05-30 21:40:57 -07:00
Jacob Haslehurst 8e5ef66f0a only use mailcatcher if debug mode is on 2013-05-31 13:41:28 +10:00
Jacob Haslehurst 091ae6d1fc Added Django usage instructions 2013-05-31 13:37:08 +10:00
Samuel Cochran 16264a89ba Bump 0.5.12 2013-05-30 19:43:49 +10:00
Julien Kirch 6ce4dda354 Add option to disable the exit button and action 2013-05-13 13:27:06 +02:00
Samuel Cochran e19a2096b0 Merge pull request #85 from hdabrows/master
Fix clearing all messages
2013-05-04 22:33:25 -07:00
Samuel Cochran cca077af28 Merge pull request #90 from sj26/leading-periods
HTML Links can break in quoted printable email output
2013-05-04 22:30:19 -07:00
Samuel Cochran 808ada7a3b It seems EventMachine already does this for us 2013-05-05 15:27:41 +10:00
Hubert Dabrowski f10ac38e90 Correct an error message 2013-04-08 22:49:04 +02:00
Hubert Dabrowski b6e0daf0ac Fix clearing all messages 2013-04-08 22:20:30 +02:00
Samuel Cochran 1241446fa0 Bump 0.5.11 2013-03-12 12:02:58 +11:00
Samuel Cochran 37f55300ff Add gem license 2013-03-12 10:21:23 +11:00
Samuel Cochran 6dfed04dd9 Fix gem packaging for rubygems 2.0 2013-03-12 10:21:23 +11:00
Samuel Cochran e8ca2a8fcf Rebuild assets with updated gems 2013-03-12 10:12:33 +11:00
Samuel Cochran 2a931fdc6b bundle update 2013-03-12 10:11:55 +11:00
Samuel Cochran 6605fc266f Upgrade Haml, bowing to the will of the masses 2013-03-12 10:11:11 +11:00
Samuel Cochran 2e7f9562df Remove guard, I wasn't using it anyway 2013-03-12 10:07:16 +11:00
Samuel Cochran 6dca1a8e6b Everyone loves secure rubygems 2013-03-12 10:06:53 +11:00
Samuel Cochran 327cd355b0 Update RVM instructions 2013-02-25 18:10:36 +11:00
Samuel Cochran f25eef73ef Add note about bundler to README 2013-02-25 18:10:36 +11:00
Samuel Cochran c971f543d4 Merge pull request #78 from asynchrony/master
Sort messages from newest to oldest [also closes #70].
2013-02-09 19:56:18 -08:00
Charlie Sanders 2ae395a0ad Sort messages from newest to oldest 2013-01-28 20:21:32 -06:00
Samuel Cochran fbd6b946f3 Merge remote branch 'remiprev/master'
Update Fractal terms page URL
2012-11-18 20:24:36 +11:00
Rémi Prévost 7d242e9fc0 Update Fractal terms page URL 2012-11-15 13:05:00 -05:00
Samuel Cochran 3aa2815cea Merge remote-tracking branch 'toy/delete_message' 2012-11-06 18:32:47 +08:00
Ivan Kuchin b3a4e86a48 extracted unselectMessage 2012-10-30 00:00:50 +01:00
Ivan Kuchin 2e35d0cc9d no need for «existential operator» for results of jquery selector methods 2012-10-29 23:50:05 +01:00
Ivan Kuchin 0a4775092a using coffeescript interpolations 2012-10-29 23:49:58 +01:00
Ivan Kuchin eb3e14a8a6 Merge remote-tracking branch 'sj26/master' into delete_message 2012-10-29 23:49:28 +01:00
Samuel Cochran a1b8f8b3d3 Merge pull request #63 from 907th/master
Declare favicon explicitly in view
2012-10-28 20:17:11 -07:00
Samuel Cochran c24e075d6f Merge pull request #65 from toy/stop_key_events
Stop key events
2012-10-28 19:48:18 -07:00
Samuel Cochran 74beed307c Merge branch 'expand-path-on-sinatra-root' [closes #67]
Thanks @toy!
2012-10-29 10:37:49 +08:00
Samuel Cochran 76cef09b4e Expand path on sinatra root 2012-10-29 10:36:36 +08:00
Ivan Kuchin 81cb465357 don't use id when switching messages, go to first message on up/down when nothing selected 2012-10-25 01:30:26 +02:00
Ivan Kuchin 7bc086ff49 switch to next message on delete 2012-10-25 01:27:00 +02:00
Ivan Kuchin 09312ffb6e delete individual messages 2012-10-25 01:27:00 +02:00
Ivan Kuchin cb3974f1b2 scroll to selected message row 2012-10-24 23:27:13 +02:00
Ivan Kuchin a161dedf0a stop caught key events 2012-10-24 23:27:09 +02:00
Alexey Chernenkov e7b39e9234 Favicon declared explicitly. 2012-10-20 19:24:14 +06:00
Samuel Cochran 3f4abe1d32 Version v0.5.10 2012-09-30 18:43:35 +08:00
Samuel Cochran 15ebba22a5 Bump dependencies 2012-09-30 18:43:25 +08:00
Samuel Cochran ae61a04033 It's been long enough. 2012-09-30 18:27:01 +08:00
Samuel Cochran 39aac3422b Don't on windows [finishes #56] 2012-09-19 15:41:25 +08:00
Samuel Cochran 85ce6f49d1 Be more strict about thin and skinny deps 2012-09-12 10:22:59 +08:00
Samuel Cochran a0620de9d5 Release v0.5.9 2012-09-10 22:03:18 +08:00
Samuel Cochran 502dad4493 Bump eventmachine, amonst other things 2012-09-10 22:02:07 +08:00
Samuel Cochran 76ca9ac918 Use `doc` as rdoc task and directory 2012-07-25 12:05:07 +08:00
Samuel Cochran aa5f056ae6 Update Rakefile for version file change 2012-07-25 12:04:43 +08:00
Samuel Cochran 9d91e3f82c Don't use RVM any more. 2012-07-25 12:01:36 +08:00
Samuel Cochran 19eb9ce540 Use `extend self` instead of `module_function` 2012-07-25 12:00:31 +08:00
Samuel Cochran a2f9808c75 Some syntax bits 2012-07-25 11:47:12 +08:00
Samuel Cochran d74b700216 Remove autoload 2012-07-25 11:47:01 +08:00
Samuel Cochran 5f05ea0d5d Re-add favicon to gem manifest 2012-07-25 11:13:41 +08:00
Samuel Cochran c920bad92f Use bundler-style version file 2012-07-25 11:13:02 +08:00
Samuel Cochran 20cf6e98f1 Discard periods prefixing SMTP data per RFC 821 4.5.2 2012-07-18 11:27:27 +08:00
Samuel Cochran cfdadc01fd Bump v0.5.8 2012-07-17 23:03:36 +08:00
Samuel Cochran 9d50d202a1 WebKit's fussy about WebSocket protocols now 2012-07-17 23:03:20 +08:00
Samuel Cochran 1c68ff5662 Version v0.5.7.2 2012-07-10 15:51:27 +08:00
Samuel Cochran 8a69d92fca Bump skinny requirement [fixes #41] 2012-07-10 15:51:19 +08:00
Samuel Cochran 5416c0b860 Version 0.5.7.1 2012-06-19 16:26:24 +08:00
Samuel Cochran 69b91a1fa8 1.8.7 doesn't support IO.popen with options 2012-06-19 16:25:19 +08:00
Samuel Cochran 744ee93c57 Version 0.5.7 2012-06-19 12:53:58 +08:00
Samuel Cochran faf18259ef Merge remote-tracking branch 'kulesa/master'
Adds keyboard navigation—thanks!
2012-06-19 12:47:29 +08:00
Alexey Kuleshov 3b446217ad Add keyboard navigation 2012-04-19 18:26:40 +03:00
Samuel Cochran 20b5635aff [#42] Option to open web browser, [#38] Don't use which -s 2012-04-06 14:16:15 +08:00
Samuel Cochran 73562c77be [#4] Allow sending multiple MAIL FROM: commands per RFC2821 2012-04-06 13:37:13 +08:00
Samuel Cochran 0e58ca6887 Bump MailCatcher 0.5.6 2012-03-22 11:44:08 +08:00
Samuel Cochran fb3e63e74c Update gems, remove deprecated guard-ego 2012-03-22 11:43:47 +08:00
Samuel Cochran b0cce663f4 Tweak message searching
Clean up styles, use case-insensitive, multi-token filtering.
2012-03-22 11:40:22 +08:00
Josh McArthur cee3ed232f Regenerate application.css without Compass line comments 2012-03-12 23:40:06 +13:00
Josh McArthur 5233db0bb5 Adjust compass configuration to not add pesky line comments to generated
CSS
2012-03-12 23:39:47 +13:00
Josh McArthur 089ddaa579 Add search input element in nav area 2012-03-12 23:31:28 +13:00
Josh McArthur 7b3825c8dd Generate stylesheet 2012-03-12 23:31:20 +13:00
Josh McArthur c9f6127b77 Add some basic positioning to search input 2012-03-12 23:30:48 +13:00
Josh McArthur aa4a51fb6d Add search and clearSearch methods to CoffeeScript 2012-03-12 23:29:42 +13:00
Samuel Cochran c8090609e5 Add flexie for flexible box model support in IE and Opera
Closes #33
2012-01-25 15:15:42 +08:00
Samuel Cochran e350050fa2 Don't require later version of mail 2012-01-25 15:15:19 +08:00
Samuel Cochran 0964de9fe4 Add catchmail support for `-f FROM` a la sendmail
Commonly used by PHP frameworks like Drupal.

Sets return path on email, should be visible in MailCatcher.

Closes #28.
2012-01-23 15:30:03 +08:00
Samuel Cochran 2ab9c6c0f0 Bump gemfile lock 2012-01-23 15:23:04 +08:00
Samuel Cochran 86bd77bcd1 Merge pull request #31 from britto/patch-1
Typo
2012-01-16 18:35:47 -08:00
João Britto 5ba5413a9a Typo 2012-01-16 18:02:19 -02:00
Samuel Cochran 8682ab266b Update README.md 2012-01-02 20:31:09 +08:00
Samuel Cochran fcb9a0e4bb Merge branch 'master' of github.com:sj26/mailcatcher 2012-01-02 11:02:17 +08:00
Samuel Cochran 1e717a7d76 Only do timezone offset if we have a date 2012-01-02 11:01:26 +08:00
Sachin Ranchod 3d6f2a4b6a [fixes #24] Javascript adjusts timestamp according to locale's timezone 2012-01-02 10:59:16 +08:00
Samuel Cochran 1b4ab9fdac Merge pull request #25 from kimptoc/patch-1
amended rails config, smtp_settings is :address, not :host (at least und...
2011-12-22 11:38:16 -08:00
Samuel Cochran 0a083c0300 Replace whole iframe body, looks like coffeescript got an upgrade too 2011-12-23 03:34:40 +08:00
Samuel Cochran 4978e8c5e7 Make it pretty 2011-12-23 03:34:15 +08:00
Samuel Cochran 1067c208cb Pretty up XSL, cater to empty case 2011-12-23 03:34:02 +08:00
Samuel Cochran 50ea84cebf Actually add XSLT to loaded scripts 2011-12-23 03:33:45 +08:00
Ryan Montgomery a9e46c1d55 Fractal Analysis Output
* Added analysis.xsl to transform the fractal validation xml into html which is injected into the analysis iframe. This allowed for easier styling of the returned xml data.
* Added xslt query plugin to fetch and transform the xml
* Styled the output in a similar manner to the Fractal website
2011-12-11 16:59:30 -05:00
Chris Kimpton e4eb0992f4 amended rails config, smtp_settings is :address, not :host (at least under Rails 3.1.3) 2011-12-11 20:31:33 +00:00
Samuel Cochran c1027deee6 Merge branch 'master' of github.com:sj26/mailcatcher 2011-12-04 22:30:37 +08:00
Samuel Cochran 59f0ec8532 v0.5.4 2011-12-04 22:29:43 +08:00
Samuel Cochran e5c8ef9b08 Fix links to attachments (Thanks Ben!) 2011-12-04 22:26:09 +08:00
Samuel Cochran 0a29130d0f Merge pull request #23 from fnando/master
Just removed Config's deprecation warning
2011-10-10 04:43:14 -07:00
Nando Vieira d815809607 Removed deprecation warning. 2011-10-10 08:28:49 -03:00
Samuel Cochran 428e7cdf1e Version bump 2011-10-10 14:33:09 +08:00
Gregor Schmidt 3bbaa6f2de Using old-style Hash literal to work with 1.8.7 2011-10-10 08:23:26 +02:00
Samuel Cochran d9986bad86 Don't include rdoc in build process 2011-10-09 16:50:36 +08:00
Samuel Cochran bbcd792a03 Make mailcatcher aware of its version. 2011-10-09 16:50:17 +08:00
Samuel Cochran 024186ebc3 Version 0.5.2
* Avoid '#' in location (thanks @schmidt).
 * Add rdoc and rake as development dependencies (thanks @schmidt).
 * Update to Fractal to go through API, presents ugly XML now instead of nothing.
 * Bump Skinny to work with latest WebSocket draft.
2011-10-09 15:46:55 +08:00
Samuel Cochran 54eb83d7a2 Little refactor to reduce nesting 2011-10-09 15:45:33 +08:00
Samuel Cochran 08ab0c5f69 Update Skinny dependency 2011-10-09 15:44:58 +08:00
Samuel Cochran 93a31fd62b Merge branch 'add-missing-dev-dependencies' of https://github.com/schmidt/mailcatcher 2011-10-08 00:26:36 +08:00
Samuel Cochran 623f2b4514 Merge branch 'avoid-hash-in-urls-by-preventing-default-link-action' of https://github.com/schmidt/mailcatcher 2011-10-08 00:25:25 +08:00
Samuel Cochran 60160e1642 Expose updated fractal api access in front-end 2011-10-08 00:25:04 +08:00
Gregor Schmidt 32ae22e4c7 Adding preventDefault for some onclick handlers
this should avoid ugly '#' in the location bar
2011-10-07 17:31:02 +02:00
Gregor Schmidt 2379603d16 Adding rake and rdoc to list of dev dependencies 2011-10-07 17:29:43 +02:00
Samuel Cochran 88711084e7 Analysis by Fractal 2011-10-07 22:16:17 +08:00
Samuel Cochran 63432c27c8 Refactor to refernce parts by sub-path 2011-10-07 22:15:59 +08:00
Samuel Cochran c3a3bb1880 Update homepage. 2011-09-13 05:43:40 +08:00
Samuel Cochran 736f3ee821 Merge pull request #18 from UnderpantsGnome/better_growl_image
better image for growl
2011-08-25 17:33:03 -07:00
Michael Moen 2290c0eb3e better image for growl 2011-08-25 15:39:49 -05:00
Samuel Cochran 56113d9c3f Bump to 0.5.1, bugfix 2011-07-08 14:48:11 +08:00
Samuel Cochran e9da024345 Fix attachments not appearing after clearing 2011-07-08 14:47:41 +08:00
Samuel Cochran 6b5f8cbfca Bump to 0.5.0, new feature 2011-07-07 16:16:15 +08:00
Samuel Cochran eb45a62413 Tweak SASS 2011-07-07 16:15:26 +08:00
Samuel Cochran eed61fe11e Load stylesheets earlier in Analysis tab. 2011-07-07 16:11:29 +08:00
Samuel Cochran 6ba508c635 Link to issues page in README. 2011-07-07 16:11:17 +08:00
Samuel Cochran a6e9fafbfc Add Fractal analysis as a feature in README. 2011-07-07 16:11:09 +08:00
Samuel Cochran 0de42cf049 Add Fractal analysis support. 2011-07-07 16:08:32 +08:00
Samuel Cochran 4ba18f7296 Make sure to prevent default link behaviour for tabs. 2011-07-07 15:57:28 +08:00
Samuel Cochran 51e74fdac2 Who knew JavaScript thinks months are 0-offset? Fixes #14 2011-07-07 13:41:22 +08:00
Samuel Cochran d2bb7e8c66 Adding RVM RC, just because. 2011-07-07 13:39:35 +08:00
Samuel Cochran cc517b051c Add actual links to format tabs so they can be opened in new tabs, per #13. 2011-07-07 11:40:53 +08:00
Samuel Cochran c760c01b81 Adjustable message list height, per #12. 2011-07-07 11:39:38 +08:00
Samuel Cochran ae83b075c2 Make sure there's *something* inside the table cells, per #11.
Thanks @excepttheweasel!
2011-06-26 11:34:25 +08:00
Samuel Cochran 515b5ce4bc Switch to compass guard to make guard actually generate SASS. 2011-06-26 11:31:51 +08:00
Samuel Cochran 26eb609085 Add guard Gemfile groups. 2011-06-26 11:20:42 +08:00
Samuel Cochran 628755ece8 Version bump to 0.4.7 2011-06-24 08:33:02 +08:00
Samuel Cochran 9a914ddfd0 Images need to go in the gemspec. -.- 2011-06-24 08:32:33 +08:00
Samuel Cochran ae9a324118 Rakefile should strip read VERSION 2011-06-23 23:48:19 +08:00
Samuel Cochran ad6b61edf3 Version bump to 0.4.6, did I miss one?! 2011-06-23 23:45:55 +08:00
Samuel Cochran f5efe23c6b Version bump to 0.4.5 2011-06-23 23:41:46 +08:00
Samuel Cochran 6571f84794 MailCatcher has a logo! 2011-06-23 23:41:07 +08:00
Samuel Cochran baba52adcb Slightly nicer, GitHub-inspired buttons. 2011-06-12 14:00:59 +08:00
Samuel Cochran 8424fedaaf Add modernizr with HTML5 shiv 2011-06-12 13:32:01 +08:00
Samuel Cochran 55790a91a4 Merge commit '59a8162b915de59dfcea9e3c0c2b768800a18a00' from @pnc
Thanks!
2011-06-10 20:57:07 +08:00
Samuel Cochran e22d6766e3 Fix download button 2011-06-10 20:56:18 +08:00
Samuel Cochran 559452ecc1 The donate link didn't work! Wooops. 2011-06-10 12:39:08 +08:00
Samuel Cochran cbc49f7ba6 Version bump! 2011-06-10 12:27:59 +08:00
Samuel Cochran 0e8ac1c5a2 Remove JSON as a dependency, we let ActiveSupport worry about that. 2011-06-10 12:26:18 +08:00
Samuel Cochran 5ba5624c13 A couple of ideas for the README. 2011-06-10 11:25:03 +08:00
Samuel Cochran d0d6b29606 Rawr! (Basic Growl support.) 2011-06-10 11:24:53 +08:00
Samuel Cochran 4b689159df Whitespace pedantry. 2011-06-10 11:22:59 +08:00
Samuel Cochran 49d42da4e5 Convert to SASS and tweak the Rake build process to work. 2011-06-10 11:22:56 +08:00
Samuel Cochran b59cffd784 Add browser polling in README features. 2011-06-10 11:22:45 +08:00
Samuel Cochran e85036a994 Added Rails, PHP, and better RVM instructions to README 2011-06-07 12:21:29 +08:00
Samuel Cochran 51042a28a8 Add examples. 2011-06-03 09:48:25 +08:00
Samuel Cochran 22d5fcaf33 Ignore SASS cache. 2011-06-03 09:47:39 +08:00
Samuel Cochran ad556f2fdc Version bump for optparse require fix 2011-06-02 08:57:57 +08:00
Samuel Cochran a4269a895f Require optparse now 2011-06-02 08:57:14 +08:00
Samuel Cochran 5b4d6ed26e Version bump, bug fix. 2011-06-02 08:17:20 +08:00
Samuel Cochran 811a3ac47f Fix syntax and quotes for 1.8.7. 2011-06-02 08:15:50 +08:00
Samuel Cochran 290d2db96a Merge branch 'master' of github.com:sj26/mailcatcher 2011-06-02 00:19:16 +08:00
Samuel Cochran f0b8c2c63c README edits. People tell me I should allow donations or smth. 2011-06-01 09:09:37 -07:00
Samuel Cochran 4e65f6aedc Version 0.4.1
Don't daemonize on Windows.
2011-06-01 22:04:52 +08:00
Samuel Cochran 6b1f3e343d Process.daemon doesn't work on Windows 2011-06-01 22:04:45 +08:00
Samuel Cochran f92a0684be A screenshot showing an actual message might be nice. 2011-06-01 02:07:29 +08:00
Samuel Cochran f9c32e6b9f README: New screenshot of v0.4.0 2011-06-01 01:46:23 +08:00
Samuel Cochran 4d0163ca0e Re-word daemonize feature 2011-06-01 01:38:19 +08:00
Samuel Cochran 30a34984bd Version 0.4.0 2011-06-01 01:36:14 +08:00
Samuel Cochran 406a7980ed Run as a daemon by default. 2011-06-01 01:36:05 +08:00
Samuel Cochran 0ef6d42349 Recover from used ports. 2011-06-01 01:35:57 +08:00
Samuel Cochran b8d72b0b53 Clear all mail. 2011-06-01 01:15:05 +08:00
Samuel Cochran 06f2238ef3 Quit via "DELETE /" - this seems more elegant, somehow. 2011-06-01 01:14:56 +08:00
Samuel Cochran 803a569ac8 Header button style tweaks. 2011-06-01 01:00:52 +08:00
Samuel Cochran 17d2cb3de9 Make "Quit" link work. 2011-06-01 00:49:36 +08:00
Samuel Cochran bd1bfc47cf Present messages in ascending order from DB to make message order in UI predictable. 2011-06-01 00:40:35 +08:00
Samuel Cochran 72a761c198 Refactoring with compass, hooray! 2011-06-01 00:40:13 +08:00
Samuel Cochran 56a82f5b46 Merge branch 'master' of github.com:sj26/mailcatcher 2011-05-31 22:04:28 +08:00
Samuel Cochran f63c85629d Update LICENSE year 2011-05-31 06:48:57 -07:00
Samuel Cochran bcdf16680a README changes.
Re-order, copyright, thanks to TFG, PHP feature,
2011-05-31 06:48:15 -07:00
Samuel Cochran 63f7f85437 Remove daemons and use Process.daemon.
Daemons is overkill and ruby has a freakin' method for it anyways. (since when?!)
2011-05-30 15:37:13 +08:00
Samuel Cochran ac6a439796 Add an RVM note about gemsets and wrappers. 2011-05-30 12:35:47 +08:00
Samuel Cochran df7cda92d2 Guard makes everything easier. 2011-05-29 21:32:38 +08:00
Samuel Cochran d07a6dff46 Version bump. 2011-05-29 21:18:15 +08:00
Samuel Cochran 895f7f4f59 Fix format tabs. 2011-05-29 21:16:40 +08:00
Samuel Cochran 0abd0037db Add whole-message types as well as multipart types. 2011-05-29 21:16:33 +08:00
Samuel Cochran e33448c965 Nicer styles, better formatted. 2011-05-29 21:16:13 +08:00
Samuel Cochran 858d4c4290 One console.log too many. 2011-05-29 20:28:04 +08:00
Samuel Cochran 8137c06c52 Turn SQLite3 result rows into hashes ourselves.
SQLite3's results_as_hash is a little broken. Pull request submitted: https://github.com/luislavena/sqlite3-ruby/pull/38
2011-05-29 20:26:15 +08:00
Samuel Cochran ac1e200111 This should have been compact from the get-go. 2011-05-29 19:57:04 +08:00
Samuel Cochran 08e3745a96 Safari sucks so bad; explicitly parse dates. 2011-05-29 15:42:07 +08:00
Samuel Cochran 42796bbc69 Compiled css never made it into 0.3.1. 2011-05-29 15:03:38 +08:00
Samuel Cochran 4f7c72e532 There should be a row in there... 2011-05-29 14:49:29 +08:00
Samuel Cochran 00a415f386 Re-factor into coffee script. 2011-05-29 14:46:38 +08:00
Samuel Cochran a9d884859b Add coffee-script. 2011-05-29 14:46:28 +08:00
Samuel Cochran da6fcfff29 Version bump. 2011-05-29 12:40:19 +08:00
Samuel Cochran 531a5c07f6 Add catchmail, a sendmail-analogue for deliverying to MailCatcher via SMTP. 2011-05-29 12:40:14 +08:00
Samuel Cochran 53ca9485e1 Better zebra row colour. 2011-05-29 12:38:27 +08:00
Samuel Cochran e41d40549d Split up SASS. 2011-05-29 12:37:56 +08:00
Samuel Cochran 46d1e2a6e8 Remove deprecated screen/daemonisation. 2011-05-29 12:37:31 +08:00
Samuel Cochran 63dce8410e Let's get SASSy. 2011-05-28 14:21:11 +08:00
Samuel Cochran ef7c6828ab Add ruby version requirement like a good gem. 2011-05-28 13:44:30 +08:00
Samuel Cochran fbcdf4a1c2 I guess we're locked to ourselves at 0.3.0 now. 2011-05-28 13:44:21 +08:00
Samuel Cochran da9f1d6b17 Update README with new screenshot and feature. 2011-05-27 22:34:20 +08:00
Samuel Cochran 1a0e2a1f64 Minor version bump! 2011-05-27 22:10:14 +08:00
Samuel Cochran 6703495637 Slightly nicer recipients listing. 2011-05-27 22:10:05 +08:00
Samuel Cochran 0cbaab1a56 Bundler for development pulling from Gemfile, no dist. 2011-05-27 21:47:43 +08:00
Samuel Cochran afee4a2c4a Refactor, style refine. 2011-05-27 21:47:04 +08:00
Samuel Cochran ca89f36ab6 Allow downloading emails. 2011-05-27 21:40:16 +08:00
Samuel Cochran 3e954599ff Ignore generated gems and rdoc 2011-05-27 20:38:39 +08:00
Samuel Cochran 1ff11d6920 Actually do both rewrites on HTML bodies. 2011-05-27 17:29:00 +08:00
Samuel Cochran b4ad9041c3 Merge remote-tracking branch 'refs/remotes/origin/master' 2011-05-27 12:48:06 +08:00
Samuel Cochran 90b0aadeaf Update readme with new bits. 2011-05-27 12:48:03 +08:00
Samuel Cochran 6003e631fc Rewrite HTML links to open in new window. 2011-05-27 12:45:16 +08:00
Samuel Cochran f8897680ce Allow running MailCatcher as a daemon. 2011-05-27 12:39:03 +08:00
Samuel Cochran 8d6c237d62 Set both IPs with one options.
Good if you want to set up a single MailCatcher instance for a development team.
2011-05-27 12:24:02 +08:00
Samuel Cochran 36faf40f5f Better autoloading. 2011-05-27 12:19:15 +08:00
Samuel Cochran 9c6b80cb63 Refactor option parsing and running out of bin file. 2011-05-27 12:19:04 +08:00
Samuel Cochran cbf6a77b5d Who needs Jeweler? 2011-05-27 12:16:48 +08:00
Samuel Cochran 3ad06e148f Mail requires i18n itself these days. 2011-05-27 12:15:55 +08:00
Samuel Cochran 66d258a859 Added some notes to the README about potential API usage as requested by some people. 2011-05-26 19:01:27 -07:00
Samuel Cochran 412bbfa00b Added a pretty picture. 2011-05-19 07:33:26 -07:00
Samuel Cochran 675c0b369d Update to respect sqlite3 rename. 2011-05-10 13:05:28 +08:00
Samuel Cochran bb2dcba797 Regenerate gemspec for version 0.2.4 2011-05-10 11:32:38 +08:00
Samuel Cochran f396958ba3 Version bump to 0.2.4 2011-05-10 11:27:09 +08:00
Samuel Cochran e482f33693 We don't use activesupport. -.- 2011-05-10 11:26:53 +08:00
Samuel Cochran d6c13f3d57 Added Greg's bash contrib script for running mailcatcher in screen 2011-04-05 10:33:26 +08:00
Samuel Cochran 2fdce6900a Handle non-multipart messages correctly 2010-11-04 12:48:06 +08:00
Samuel Cochran d62d0e6d78 Version bump to 0.2.3 2010-11-04 12:47:24 +08:00
Samuel Cochran aeaba34034 Woops, Skinny 0.1.2 changes RequestHelpers to Helpers 2010-11-04 11:15:12 +08:00
Samuel Cochran 13405c9686 Version bump to 0.2.2 2010-11-04 11:13:40 +08:00
Samuel Cochran 02367c1443 Version bump to 0.2.1 2010-10-29 13:52:49 +08:00
Samuel Cochran 6fa12ee55b Forgot to put Sinatra as a dependency. 2010-10-29 13:52:21 +08:00
Samuel Cochran a89b4c2860 Fixing markdown syntax 2010-10-28 03:30:37 +08:00
Samuel Cochran 76c22e3971 Added simple instructions in README 2010-10-28 03:27:07 +08:00
Samuel Cochran 7613141e9a Fiddling README 2010-10-28 03:23:29 +08:00
47 changed files with 4965 additions and 524 deletions

14
.gitignore vendored
View File

@ -1 +1,13 @@
pkg
# Caches
/.bundle
/.sass-cache
# Gemfile locks ignored for gems
/Gemfile.lock
# Generated documentation and assets
/doc
/public/assets
# Build gems
*.gem

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
sudo: false
language: ruby
rvm:
- 1.9.3
- 2.0.0
- 2.1.5
- 2.2.0

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM ruby:2.3
MAINTAINER Samuel Cochran <sj26@sj26.com>
RUN gem install mailcatcher
EXPOSE 1025
EXPOSE 1080
ENTRYPOINT ["mailcatcher", "-f"]
CMD ["--ip", "0.0.0.0"]

10
Gemfile Normal file
View File

@ -0,0 +1,10 @@
source "https://rubygems.org"
gemspec
# mime-types 3+, required by mail, requires ruby 2.0+
gem "mime-types", "< 3" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2")
#group :development do
# gem "pry"
#end

View File

@ -1,4 +1,4 @@
Copyright (c) 2010 Samuel Cochran
Copyright (c) 2010-2011 Samuel Cochran
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

106
README.md
View File

@ -4,42 +4,118 @@ Catches mail and serves it through a dream.
MailCatcher runs a super simple SMTP server which catches any message sent to it to display in a web interface. Run mailcatcher, set your favourite app to deliver to smtp://127.0.0.1:1025 instead of your default SMTP server, then check out http://127.0.0.1:1080 to see the mail that's arrived so far.
![MailCatcher screenshot](https://cloud.githubusercontent.com/assets/14028/14093249/4100f904-f598-11e5-936b-e6a396f18e39.png)
## Features
* Catches all mail and stores it for display.
* Shows HTML, Plain Text and Source version of messages, as applicable.
* Rewrites HTML enabling display of embedded, inline images/etc. (currently very basic)
* Rewrites HTML enabling display of embedded, inline images/etc and open links in a new window.
* Lists attachments and allows separate downloading of parts.
* Written super-simply in EventMachine, easy to dig in and change.
* Download original email to view in your native mail client(s).
* Command line options to override the default SMTP/HTTP IP and port settings.
* Mail appears instantly if your browser supports [WebSockets][websockets], otherwise updates every thirty seconds.
* Runs as a daemon run in the background.
* Sendmail-analogue command, `catchmail`, makes [using mailcatcher from PHP][withphp] a lot easier.
* Written super-simply in EventMachine, easy to dig in and change.
* Keyboard navigation between messages
## How
1. `gem install mailcatcher`
2. `mailcatcher`
3. Go to http://localhost:1080/
4. Send mail through smtp://localhost:1025
Use `mailcatcher --help` to see the command line options. The brave can get the source from [the GitHub repository][mailcatcher-github].
### Bundler
Please don't put mailcatcher into your Gemfile. It will conflict with your applications gems at some point.
Instead, pop a note in your README stating you use mailcatcher. Simply run `gem install mailcatcher` then `mailcatcher` to get started.
### RVM
Under RVM your mailcatcher command may only be available under the ruby you install mailcatcher into. To prevent this, and to prevent gem conflicts, install mailcatcher into a dedicated gemset and create wrapper scripts:
rvm default@mailcatcher --create do gem install mailcatcher
rvm wrapper default@mailcatcher --no-prefix mailcatcher catchmail
### Rails
To set up your rails app, I recommend adding this to your `environments/development.rb`:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
config.action_mailer.raise_delivery_errors = false
### PHP
For projects using PHP, or PHP frameworks and application platforms like Drupal, you can set [PHP's mail configuration](http://www.php.net/manual/en/mail.configuration.php) in your [php.ini](http://www.php.net/manual/en/configuration.file.php) to send via MailCatcher with:
sendmail_path = /usr/bin/env catchmail -f some@from.address
You can do this in your [Apache configuration](http://php.net/manual/en/configuration.changes.php) like so:
php_admin_value sendmail_path "/usr/bin/env catchmail -f some@from.address"
If you've installed via RVM this probably won't work unless you've manually added your RVM bin paths to your system environment's PATH. In that case, run `which catchmail` and put that path into the `sendmail_path` directive above instead of `/usr/bin/env catchmail`.
If starting `mailcatcher` on alternative SMTP IP and/or port with parameters like `--smtp-ip 192.168.0.1 --smtp-port 10025`, add the same parameters to your `catchmail` command:
sendmail_path = /usr/bin/env catchmail --smtp-ip 192.160.0.1 --smtp-port 10025 -f some@from.address
### Django
For use in Django, simply add the following configuration to your projects' settings.py
```python
if DEBUG:
EMAIL_HOST = '127.0.0.1'
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
```
### API
A fairly RESTful URL schema means you can download a list of messages in JSON from `/messages`, each message's metadata with `/messages/:id.json`, and then the pertinent parts with `/messages/:id.html` and `/messages/:id.plain` for the default HTML and plain text version, `/messages/:id/:cid` for individual attachments by CID, or the whole message with `/messages/:id.source`.
## Caveats
* Mail requires activesupport which requires i18n, but it doesn't list it as a dependency. For now I've added i18n as a requirement for MailCatcher.
* For now you need to refresh the page to see new mail. Websockets support is coming soon to show new mail immediately.
* Mail proccessing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or file an issue and let me know. Include the whole message you're having problems with.
* Mail processing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or [file an issue][mailcatcher-issues] and let me know. Include the whole message you're having problems with.
* The interface is very basic and has not been tested on many browsers yet.
## TODO
* Websockets for immediate mail viewing.
* Download link to view original message in mail client.
* Growl support.
* Test suite.
* Better organisation.
* Better interface. SproutCore?
* Add mail delivery on request, optionally multiple times.
* Compatibility testing against CampaignMonitor's [design guidelines](http://www.campaignmonitor.com/design-guidelines/) and [CSS support matrix](http://www.campaignmonitor.com/css/).
* Forward mail to rendering service, maybe CampaignMonitor?
* Package as an app? Native interfaces?
## Thanks
MailCatcher is just a mishmash of other people's hard work. Thank you so much to the people who have built the wonderful guts on which this project relies.
## Copyright
Thanks also to [The Frontier Group][tfg] for giving me the idea, being great guinea pigs and letting me steal pieces of time to keep the project alive.
Copyright (c) 2010 Samuel Cochran. See LICENSE for details.
## Donations
I work on MailCatcher mostly in my own spare time. If you've found Mailcatcher useful and would like to help feed me and fund continued development and new features, please [donate via PayPal][donate]. If you'd like a specific feature added to MailCatcher and are willing to pay for it, please [email me](mailto:sj26@sj26.com).
## License
Copyright © 2010-2011 Samuel Cochran (sj26@sj26.com). Released under the MIT License, see [LICENSE][license] for details.
## Dreams
For dream catching, try [this](http://goo.gl/kgbh).
For dream catching, try [this](http://goo.gl/kgbh). OR [THIS](http://www.nyanicorn.com), OMG.
[donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=522WUPLRWUSKE
[license]: https://github.com/sj26/mailcatcher/blob/master/LICENSE
[mailcatcher-github]: https://github.com/sj26/mailcatcher
[mailcatcher-issues]: https://github.com/sj26/mailcatcher/issues
[tfg]: http://www.thefrontiergroup.com.au
[websockets]: http://www.whatwg.org/specs/web-socket-protocol/
[withphp]: http://webschuur.com/publications/blogs/2011-05-29-catchmail_for_drupal_and_other_phpapplications_the_simple_version

View File

@ -1,43 +1,61 @@
require 'rubygems'
require 'rake'
require "fileutils"
require "rubygems"
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "mailcatcher"
gem.summary = %Q{Runs an SMTP server, catches and displays email in a web interface.}
gem.description = <<-EOD
MailCatcher runs a super simple SMTP server which catches any
message sent to it to display in a web interface. Run
mailcatcher, set your favourite app to deliver to
smtp://127.0.0.1:1025 instead of your default SMTP server,
then check out http://127.0.0.1:1080 to see the mail.
EOD
gem.email = "sj26@sj26.com"
gem.homepage = "http://github.com/sj26/mailcatcher"
gem.authors = ["Samuel Cochran"]
gem.add_dependency 'eventmachine'
gem.add_dependency 'mail'
gem.add_dependency 'i18n'
gem.add_dependency 'sqlite3-ruby'
gem.add_dependency 'thin'
gem.add_dependency 'skinny'
gem.add_dependency 'haml'
gem.add_dependency 'json'
require "mail_catcher/version"
# XXX: Would prefer to use Rake::SprocketsTask but can't populate
# non-digest assets, and we don't want sprockets at runtime so
# can't use manifest directly. Perhaps index.html should be
# precompiled with digest assets paths?
desc "Compile assets"
task "assets" do
compiled_path = File.expand_path("../public/assets", __FILE__)
FileUtils.mkdir_p(compiled_path)
require "mail_catcher/web/assets"
sprockets = MailCatcher::Web::Assets
sprockets.css_compressor = :sass
sprockets.js_compressor = :uglifier
sprockets.each_logical_path(/(\Amailcatcher\.(js|css)|\.(xsl|png)\Z)/) do |logical_path|
if asset = sprockets.find_asset(logical_path)
target = File.join(compiled_path, logical_path)
asset.write_to target
end
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
desc "Package as Gem"
task "package" => ["assets"] do
require "rubygems/package"
require "rubygems/specification"
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "MailCatcher #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/*.rb')
rdoc.rdoc_files.include('lib/**/*.rb')
spec_file = File.expand_path("../mailcatcher.gemspec", __FILE__)
spec = Gem::Specification.load(spec_file)
Gem::Package.build spec
end
desc "Release Gem to RubyGems"
task "release" => ["package"] do
%x[gem push mailcatcher-#{MailCatcher::VERSION}.gem]
end
require "rdoc/task"
RDoc::Task.new(:rdoc => "doc",:clobber_rdoc => "doc:clean", :rerdoc => "doc:force") do |rdoc|
rdoc.title = "MailCatcher #{MailCatcher::VERSION}"
rdoc.rdoc_dir = "doc"
rdoc.main = "README.md"
rdoc.rdoc_files.include "lib/**/*.rb"
end
require "rake/testtask"
Rake::TestTask.new do |task|
task.pattern = "spec/*_spec.rb"
end
task :test => :assets
task :default => :test

View File

@ -1 +0,0 @@
0.2.0

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,340 @@
#= require modernizr
#= require jquery
#= require date
#= require favcount
#= require flexie
#= require keymaster
# Add a new jQuery selector expression which does a case-insensitive :contains
jQuery.expr[":"].icontains = (a, i, m) ->
(a.textContent ? a.innerText ? "").toUpperCase().indexOf(m[3].toUpperCase()) >= 0
class MailCatcher
constructor: ->
$("#messages tr").live "click", (e) =>
e.preventDefault()
@loadMessage $(e.currentTarget).attr("data-message-id")
$("input[name=search]").keyup (e) =>
query = $.trim $(e.currentTarget).val()
if query
@searchMessages query
else
@clearSearch()
$("#message .views .format.tab a").live "click", (e) =>
e.preventDefault()
@loadMessageBody @selectedMessage(), $($(e.currentTarget).parent("li")).data("message-format")
$("#message iframe").load =>
@decorateMessageBody()
$("#resizer").live
mousedown: (e) =>
e.preventDefault()
$(window).bind events =
mouseup: (e) =>
e.preventDefault()
$(window).unbind events
mousemove: (e) =>
e.preventDefault()
@resizeTo e.clientY
@resizeToSaved()
$("nav.app .clear a").live "click", (e) =>
e.preventDefault()
if confirm "You will lose all your received messages.\n\nAre you sure you want to clear all messages?"
$.ajax
url: "/messages"
type: "DELETE"
success: =>
@unselectMessage()
@updateMessagesCount()
error: ->
alert "Error while clearing all messages."
$("nav.app .quit a").live "click", (e) =>
e.preventDefault()
if confirm "You will lose all your received messages.\n\nAre you sure you want to quit?"
$.ajax
type: "DELETE"
success: ->
location.replace $("body > header h1 a").attr("href")
error: ->
alert "Error while quitting."
@favcount = new Favcount($("""link[rel="icon"]""").attr("href"))
key "up", =>
if @selectedMessage()
@loadMessage $("#messages tr.selected").prev().data("message-id")
else
@loadMessage $("#messages tbody tr[data-message-id]:first").data("message-id")
false
key "down", =>
if @selectedMessage()
@loadMessage $("#messages tr.selected").next().data("message-id")
else
@loadMessage $("#messages tbody tr[data-message-id]:first").data("message-id")
false
key "⌘+up, ctrl+up", =>
@loadMessage $("#messages tbody tr[data-message-id]:first").data("message-id")
false
key "⌘+down, ctrl+down", =>
@loadMessage $("#messages tbody tr[data-message-id]:last").data("message-id")
false
key "left", =>
@openTab @previousTab()
false
key "right", =>
@openTab @nextTab()
false
key "backspace, delete", =>
id = @selectedMessage()
if id?
$.ajax
url: "/messages/" + id
type: "DELETE"
success: =>
# callback will delete, this gets us into race conditions...
#@deleteMessage [id]
error: ->
alert "Error while removing message."
false
@refresh()
@subscribe()
# Only here because Safari's Date parsing *sucks*
# We throw away the timezone, but you could use it for something...
parseDateRegexp: /^(\d{4})[-\/\\](\d{2})[-\/\\](\d{2})(?:\s+|T)(\d{2})[:-](\d{2})[:-](\d{2})(?:([ +-]\d{2}:\d{2}|\s*\S+|Z?))?$/
parseDate: (date) ->
if match = @parseDateRegexp.exec(date)
new Date match[1], match[2] - 1, match[3], match[4], match[5], match[6], 0
offsetTimeZone: (date) ->
offset = Date.now().getTimezoneOffset() * 60000 #convert timezone difference to milliseconds
date.setTime(date.getTime() - offset)
date
formatDate: (date) ->
date &&= @parseDate(date) if typeof(date) == "string"
date &&= @offsetTimeZone(date)
date &&= date.toString("dddd, d MMM yyyy h:mm:ss tt")
messagesCount: ->
$("#messages tr").length - 1
updateMessagesCount: ->
@favcount.set(@messagesCount())
document.title = 'MailCatcher (' + @messagesCount() + ')'
tabs: ->
$("#message ul").children(".tab")
getTab: (i) =>
$(@tabs()[i])
selectedTab: =>
@tabs().index($("#message li.tab.selected"))
openTab: (i) =>
@getTab(i).children("a").click()
previousTab: (tab)=>
i = if tab || tab is 0 then tab else @selectedTab() - 1
i = @tabs().length - 1 if i < 0
if @getTab(i).is(":visible")
i
else
@previousTab(i - 1)
nextTab: (tab) =>
i = if tab then tab else @selectedTab() + 1
i = 0 if i > @tabs().length - 1
if @getTab(i).is(":visible")
i
else
@nextTab(i + 1)
haveMessage: (id) ->
$("""#messages tbody tr[data-message-id="#{id}"]""").length > 0
selectedMessage: ->
$("#messages tr.selected").data "message-id"
searchMessages: (query) ->
selector = (":icontains('#{token}')" for token in query.split /\s+/).join("")
$rows = $("#messages tbody tr")
$rows.not(selector).hide()
$rows.filter(selector).show()
clearSearch: ->
$("#messages tbody tr").show()
addMessage: (message) ->
row = $("<tr />").attr("data-message-id", message.id.toString())
.append($("<td/>").text(message.sender or "No sender").toggleClass("blank", !message.sender))
.append($("<td/>").text((message.recipients || []).join(", ") or "No receipients").toggleClass("blank", !message.recipients.length))
.append($("<td/>").text(message.subject or "No subject").toggleClass("blank", !message.subject))
.append($("<td/>").text(@formatDate(message.created_at)))
if 'from_server' of message
row = row.append($("<td/>").text(message.from_server or "Unknown").toggleClass("blank", !message.from_server))
row.prependTo($("#messages tbody"))
@updateMessagesCount()
deleteMessage: (ids) ->
selectedId = @selectedMessage()
for id in ids
if @haveMessage id
messageRow = $("""#messages tbody tr[data-message-id="#{id}"]""")
if messageRow
if id == selectedId
switchTo = messageRow.next().data("message-id") || messageRow.prev().data("message-id")
messageRow.remove()
if switchTo && @haveMessage switchTo
@loadMessage switchTo
#else if selectedId
# @unselectMessage()
@updateMessagesCount()
scrollToRow: (row) ->
relativePosition = row.offset().top - $("#messages").offset().top
if relativePosition < 0
$("#messages").scrollTop($("#messages").scrollTop() + relativePosition - 20)
else
overflow = relativePosition + row.height() - $("#messages").height()
if overflow > 0
$("#messages").scrollTop($("#messages").scrollTop() + overflow + 20)
unselectMessage: ->
$("#messages tbody, #message .metadata dd").empty()
$("#message .metadata .attachments").hide()
$("#message iframe").attr("src", "about:blank")
true
loadMessage: (id) ->
id = id.id if id?.id?
id ||= $("#messages tr.selected").attr "data-message-id"
if id?
$("#messages tbody tr:not([data-message-id='#{id}'])").removeClass("selected")
messageRow = $("#messages tbody tr[data-message-id='#{id}']")
messageRow.addClass("selected")
@scrollToRow(messageRow)
$.getJSON "/messages/#{id}.json", (message) =>
$("#message .metadata dd.created_at").text(@formatDate message.created_at)
$("#message .metadata dd.from").text(message.sender)
$("#message .metadata dd.to").text((message.recipients || []).join(", "))
$("#message .metadata dd.subject").text(message.subject)
$("#message .views .tab.format").each (i, el) ->
$el = $(el)
format = $el.attr("data-message-format")
if $.inArray(format, message.formats) >= 0
$el.find("a").attr("href", "/messages/#{id}.#{format}")
$el.show()
else
$el.hide()
if $("#message .views .tab.selected:not(:visible)").length
$("#message .views .tab.selected").removeClass("selected")
$("#message .views .tab:visible:first").addClass("selected")
if message.attachments.length
$ul = $("<ul/>").appendTo($("#message .metadata dd.attachments").empty())
$.each message.attachments, (i, attachment) ->
$ul.append($("<li>").append($("<a>").attr("href", attachment["href"]).addClass(attachment["type"].split("/", 1)[0]).addClass(attachment["type"].replace("/", "-")).text(attachment["filename"])))
$("#message .metadata .attachments").show()
else
$("#message .metadata .attachments").hide()
$("#message .views .download a").attr("href", "/messages/#{id}.eml")
@loadMessageBody()
# XXX: These should probably cache their iframes for the current message now we're using a remote service:
loadMessageBody: (id, format) ->
id ||= @selectedMessage()
format ||= $("#message .views .tab.format.selected").attr("data-message-format")
format ||= "html"
$("""#message .views .tab[data-message-format="#{format}"]:not(.selected)""").addClass("selected")
$("""#message .views .tab:not([data-message-format="#{format}"]).selected""").removeClass("selected")
if id?
$("#message iframe").attr("src", "/messages/#{id}.#{format}")
decorateMessageBody: ->
format = $("#message .views .tab.format.selected").attr("data-message-format")
switch format
when "html"
body = $("#message iframe").contents().find("body")
$("a", body).attr("target", "_blank")
when "plain"
message_iframe = $("#message iframe").contents()
text = message_iframe.text()
text = text.replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:\/~\+#]*[\w\-\@?^=%&amp;\/~\+#])?)/g, """<a href="$1" target="_blank">$1</a>""")
text = text.replace(/\n/g, "<br/>")
message_iframe.find("html").html("<html><body>#{text}</html></body>")
refresh: ->
$.getJSON "/messages", (messages) =>
all_msg_ids = []
$.each messages, (i, message) =>
id = message.id if message.id?
all_msg_ids.push(id)
unless @haveMessage id
@addMessage message
ids_to_remove = []
$.each $("#messages tbody tr[data-message-id]"), (i, row) =>
existing_id = $(row).data("message-id")
unless existing_id in all_msg_ids
ids_to_remove.push(existing_id)
@deleteMessage ids_to_remove
subscribe: ->
if WebSocket?
@subscribeWebSocket()
else
@subscribePoll()
subscribeWebSocket: ->
secure = window.location.protocol is "https:"
protocol = if secure then "wss" else "ws"
@websocket = new WebSocket("#{protocol}://#{window.location.host}/messages")
@websocket.onmessage = (event) =>
message = $.parseJSON event.data
if 'id' of message
@addMessage message
else
@deleteMessage message
subscribePoll: ->
unless @refreshInterval?
@refreshInterval = setInterval (=> @refresh()), 1000
resizeToSavedKey: "mailcatcherSeparatorHeight"
resizeTo: (height) ->
$("#messages").css
height: height - $("#messages").offset().top
window.localStorage?.setItem(@resizeToSavedKey, height)
resizeToSaved: ->
height = parseInt(window.localStorage?.getItem(@resizeToSavedKey))
unless isNaN height
@resizeTo height
$ -> window.MailCatcher = new MailCatcher

View File

@ -0,0 +1,257 @@
@import "compass"
@import "compass/reset"
html, body
width: 100%
height: 100%
body
+establish-baseline(12px)
display: -moz-box
display: -webkit-box
display: box
-moz-box-orient: vertical
-webkit-box-orient: vertical
box-orient: vertical
background: #eee
color: #000
font-size: 12px
font-family: Helvetica, sans-serif
&.iframe
background: #fff
h1
font-size: 1.3em
margin: 12px
p, form
margin: 0 12px 12px 12px
line-height: 1.25
.loading
color: #666
margin-left: 0.5em
.button
padding: .5em 1em
border: 1px solid #ccc
+border-radius(2px)
+background(linear-gradient(color-stops(#f4f4f4, #ececec)), #ececec)
color: #666
+text-shadow(1px 1px 0 #fff)
text-decoration: none
&:hover, &:focus
border-color: #999
border-bottom-color: #666
+background(linear-gradient(color-stops(#eee, #ddd)), #ddd)
color: #333
text-decoration: none
&:active, &.active
border-color: #666
border-bottom-color: #999
+background(linear-gradient(color-stops(#ddd, #eee)), #eee)
color: #333
text-decoration: none
+text-shadow(-1px -1px 0 #eee)
body > header
+clearfix
border-bottom: 1px solid #ccc
h1
float: left
margin-left: 6px
padding: 6px
padding-left: 30px
background: url(/assets/logo.png) left no-repeat
font-size: 18px
font-weight: bold
a
color: black
text-decoration: none
+text-shadow(0 1px 0 white)
+transition(0.1s ease)
&:hover
color: #4183C4
nav
&.project
float: left
&.app
float: right
border-left: 1px solid #ccc
li
display: block
float: left
border-left: 1px solid #fff
border-right: 1px solid #ccc
input
margin: 6px
a
display: block
padding: 10px
text-decoration: none
+text-shadow(0 1px 0 white)
+background(linear-gradient(color-stops(#f4f4f4, #ececec)), #ececec)
color: #666
+text-shadow(1px 1px 0 #fff)
text-decoration: none
&:hover, &:focus
+background(linear-gradient(color-stops(#eee, #ddd)), #ddd)
color: #333
text-decoration: none
&:active, &.active
+background(linear-gradient(color-stops(#ddd, #eee)), #eee)
color: #333
text-decoration: none
+text-shadow(-1px -1px 0 #eee)
#messages
width: 100%
height: 10em
// Two rows with padding:
min-height: (2 * (1em + .5em))
overflow: auto
background: #fff
border-top: 1px solid #fff
table
+clearfix
width: 100%
thead tr
background: #eee
color: #333
th
padding: .25em
font-weight: bold
color: #666
+text-shadow(0 1px 0 white)
tbody tr
cursor: pointer
+transition(0.1s ease)
color: #333
&:hover
color: #000
&:nth-child(even)
background: #f0f0f0
&.selected
background: Highlight
color: HighlightText
td
padding: .25em
&.blank
color: #666
font-style: italic
#resizer
padding-bottom: 5px
cursor: ns-resize
.ruler
border-top: 1px solid #ccc
border-bottom: 1px solid #fff
#message
display: -moz-box
display: -webkit-box
display: box
-moz-box-orient: vertical
-webkit-box-orient: vertical
box-orient: vertical
-moz-box-flex: 1
-webkit-box-flex: 1
box-flex: 1
> header
+clearfix
.metadata
+clearfix
padding: .5em
// This is already padded by resizer
padding-top: 0
dt, dd
padding: .25em
dt
float: left
clear: left
width: 8em
margin-right: .5em
text-align: right
font-weight: bold
color: #666
+text-shadow(0 1px 0 white)
dd.subject
font-weight: bold
.attachments
display: none
ul
display: inline
li
+inline-block
margin-right: .5em
.views
ul
padding: 0 .5em
border-bottom: 1px solid #ccc
.tab
+inline-block
a
+inline-block
padding: .5em
border: 1px solid #ccc
background: #ddd
color: #333
border-width: 1px 1px 0 1px
cursor: pointer
+text-shadow(0 1px 0 #eeeeee)
text-decoration: none
&:not(.selected):hover a
background-color: #eee
&.selected a
background: #fff
color: #000
height: 13px
+box-shadow(1px 1px 0 #ccc)
margin-bottom: -2px
cursor: default
.action
+inline-block
float: right
margin: 0 .25em
.fractal-analysis
margin: 12px 0
.report-intro
font-weight: bold
&.valid
color: #090
&.invalid
color: #c33
code
font-family: Monaco, "Courier New", Courier, monospace
background-color: #f8f8ff
color: #444
padding: 0 .2em
border: 1px solid #dedede
ul
margin: 1em 0 1em 1em
list-style-type: square
ol
margin: 1em 0 1em 2em
list-style-type: decimal
ul li, ol li
display: list-item
margin: .5em 0 .5em 1em
.error-intro
strong
font-weight: bold
.unsupported-clients
dt
padding-left: 1em
dd
padding-left: 2em
ul
li
display: list-item
iframe
display: -moz-box
display: -webkit-box
display: box
-moz-box-flex: 1
-webkit-box-flex: 1
box-flex: 1
background: #fff

71
bin/catchmail Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env ruby
begin
require 'mail'
rescue LoadError
require 'rubygems'
require 'mail'
end
require 'optparse'
options = {:smtp_ip => '127.0.0.1', :smtp_port => 1025}
OptionParser.new do |parser|
parser.banner = <<-BANNER.gsub /^ +/, ""
Usage: catchmail [options] [recipient ...]
sendmail-like interface to forward mail to MailCatcher.
BANNER
parser.on('--ip IP') do |ip|
options[:smtp_ip] = ip
end
parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
options[:smtp_ip] = ip
end
parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
options[:smtp_port] = port
end
parser.on('-f FROM', 'Set the sending address') do |from|
options[:from] = from
end
parser.on('-oi', 'Ignored option -oi') do |ignored|
end
parser.on('-t', 'Ignored option -t') do |ignored|
end
parser.on('-q', 'Ignored option -q') do |ignored|
end
parser.on('-x', '--no-exit', 'Can\'t exit from the application') do
options[:no_exit] = true
end
parser.on('-h', '--help', 'Display this help information') do
puts parser
exit!
end
end.parse!
Mail.defaults do
delivery_method :smtp,
:address => options[:smtp_ip],
:port => options[:smtp_port]
end
message = Mail.new($stdin.read)
message.return_path = options[:from] if options[:from]
ARGV.each do |recipient|
if message.to.nil?
message.to = recipient
else
message.to << recipient
end
end
message.deliver

View File

@ -1,44 +1,5 @@
#!/usr/bin/env ruby
$: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
require 'mail_catcher'
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: mailcatcher [options]'
options[:smtp_ip] = '127.0.0.1'
opts.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
options[:smtp_ip] = ip
end
options[:smtp_port] = 1025
opts.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
options[:smtp_port] = port
end
options[:http_ip] = '127.0.0.1'
opts.on('--http-ip IP', 'Set the ip address of the http server') do |ip|
options[:http_ip] = ip
end
options[:http_port] = 1080
opts.on('--http-port PORT', Integer, 'Set the port address of the http server') do |port|
options[:http_port] = port
end
options[:verbose] = false
opts.on('-v', '--verbose', 'Be more verbose') do
options[:verbose] = true
end
opts.on('-h', '--help', 'Display this help information') do
puts opts
exit!
end
end.parse!
MailCatcher.run(options)
MailCatcher.run!

20
examples/attachmail Normal file
View File

@ -0,0 +1,20 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test Attachment Mail
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary=BOUNDARY--198849662
Header
--BOUNDARY--198849662
Content-Type: text/plain
Content-Disposition: attachment; filename=blah.txt
Plain text mail
--BOUNDARY--198849662
Content-Type: text/html
<html><body><em>HTML</em> mail</body></html>
--BOUNDARY--198849662--

39
examples/breaking Normal file
View File

@ -0,0 +1,39 @@
Date: Wed, 08 Jan 2014 06:52:20 +0000
From: survey@place.com
Reply-To: Support support@someplace.com
To: asdfasdf@asdfasdf.com
Message-ID:
Subject: Subject
Mime-Version: 1.0
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
First Name:Asdf
Last Name:Asdf
Full Name:Asdf Asdf
Formal Name:Mr. Asdf Asdf
Company Name:Some Company
URL: http://localhost:3000/surveys/er2014/en

14
examples/dotmail Normal file
View File

@ -0,0 +1,14 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Whatever
Content-Type: text/plain
Plain text mail
With some dot lines:
.
...
Done.

10
examples/htmlmail Normal file
View File

@ -0,0 +1,10 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test HTML Mail
Content-Type: text/html
<html>
<body>
Yo, you <em>slimey scoundrel</em>.
</body>
</html>

5
examples/mail Normal file
View File

@ -0,0 +1,5 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test mail
Test mail.

19
examples/multipartmail Normal file
View File

@ -0,0 +1,19 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test Multipart Mail
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary=BOUNDARY--198849662
Header
--BOUNDARY--198849662
Content-Type: text/plain
Plain text mail
--BOUNDARY--198849662
Content-Type: text/html
<html><body><em>HTML</em> mail</body></html>
--BOUNDARY--198849662--

6
examples/plainmail Normal file
View File

@ -0,0 +1,6 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Plain mail
Content-Type: text/plain
Here's some text

View File

@ -0,0 +1,18 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test quoted-printable HTML mail
Mime-Version: 1.0
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<html class=3D"slim"></html>
<p>
Thank you for allowing Grand Rounds to provide a test case that ma=
y demonstrate a limitation in MailCatcher. Open source makes dev good=
</p>
<p>
You can access an error at <a href=3D"http://localhost:9876/big/long/d50=
243b933ddd425">here</a>
</p>=

6
examples/unknownmail Normal file
View File

@ -0,0 +1,6 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test mail
Content-Type: application/x-weird
Weird stuff~

10
examples/xhtmlmail Normal file
View File

@ -0,0 +1,10 @@
To: Blah <blah@blah.com>
From: Me <me@sj26.com>
Subject: Test XHTML Mail
Content-Type: application/xhtml+xml
<html>
<body>
Yo, you <em>slimey scoundrel</em>.
</body>
</html>

View File

@ -1,26 +1,262 @@
require 'eventmachine'
require 'thin'
# Apparently rubygems won't activate these on its own, so here we go. Let's
# repeat the invention of Bundler all over again.
gem "eventmachine", "1.0.9.1"
gem "mail", "~> 2.3"
gem "rack", "~> 1.5"
gem "sinatra", "~> 1.2"
gem "sqlite3", "~> 1.3"
gem "thin", "~> 1.5.0"
gem "skinny", "~> 0.2.3"
require "open3"
require "optparse"
require "rbconfig"
require "eventmachine"
require "thin"
module EventMachine
# Monkey patch fix for 10deb4
# See https://github.com/eventmachine/eventmachine/issues/569
def self.reactor_running?
(@reactor_running || false)
end
end
require "mail_catcher/events"
require "mail_catcher/mail"
require "mail_catcher/smtp"
require "mail_catcher/web"
require "mail_catcher/version"
module MailCatcher extend self
def which?(command)
ENV["PATH"].split(File::PATH_SEPARATOR).any? do |directory|
File.executable?(File.join(directory, command.to_s))
end
end
def mac?
RbConfig::CONFIG['host_os'] =~ /darwin/
end
def windows?
RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
end
def macruby?
mac? and const_defined? :MACRUBY_VERSION
end
def browseable?
windows? or which? "open"
end
def browse url
if windows?
system "start", "/b", url
elsif which? "open"
system "open", url
end
end
def log_exception(message, context, exception)
gems_paths = (Gem.path | [Gem.default_dir]).map { |path| Regexp.escape(path) }
gems_regexp = %r{(?:#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
gems_replace = '\1 (\2) \3'
puts "*** #{message}: #{context.inspect}"
puts " Exception: #{exception}"
puts " Backtrace:", *exception.backtrace.map { |line| " #{line.sub(gems_regexp, gems_replace)}" }
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
end
@@defaults = {
:smtp_ip => '127.0.0.1',
:smtp_port => '1025',
:http_ip => '127.0.0.1',
:http_port => '1080',
:verbose => false,
:daemon => !windows?,
:browse => false,
:quit => true,
:sqlite_db => ':memory:',
:delete_older_than => nil,
:keep_num_emails => nil,
:show_from_server => false,
}
def options
@@options
end
def quittable?
options[:quit]
end
def show_from_server?
options[:show_from_server]
end
def parse! arguments=ARGV, defaults=@defaults
@@defaults.dup.tap do |options|
OptionParser.new do |parser|
parser.banner = "Usage: mailcatcher [options]"
parser.version = VERSION
parser.on("--ip IP", "Set the ip address of both servers") do |ip|
options[:smtp_ip] = options[:http_ip] = ip
end
parser.on("--smtp-ip IP", "Set the ip address of the smtp server") do |ip|
options[:smtp_ip] = ip
end
parser.on("--smtp-port PORT", Integer, "Set the port of the smtp server") do |port|
options[:smtp_port] = port
end
parser.on("--http-ip IP", "Set the ip address of the http server") do |ip|
options[:http_ip] = ip
end
parser.on("--http-port PORT", Integer, "Set the port address of the http server") do |port|
options[:http_port] = port
end
parser.on("--no-quit", "Don't allow quitting the process") do
options[:quit] = false
end
parser.on("--show-from-server", "Show From Server column") do
options[:show_from_server] = true
end
parser.on("--sqlite-db PATH", "Set the path to the sqlite database, default in-memory only") do |db|
options[:sqlite_db] = db
end
parser.on("--delete-older-than TIME_MODIFIER", "On mail receipt, delete all mail older than this, examples: '-5 minutes', '-2 days'") do |db|
options[:delete_older_than] = db
end
parser.on("--keep-num-emails MAX_EMAILS", "Only keep this many emails, deletes oldest") do |db|
options[:keep_num_emails] = db
end
if mac?
parser.on("--[no-]growl") do |growl|
puts "Growl is no longer supported"
exit -2
end
end
unless windows?
parser.on('-f', '--foreground', 'Run in the foreground') do
options[:daemon] = false
end
end
if browseable?
parser.on('-b', '--browse', 'Open web browser') do
options[:browse] = true
end
end
parser.on('-v', '--verbose', 'Be more verbose') do
options[:verbose] = true
end
parser.on('-h', '--help', 'Display this help information') do
puts parser
exit
end
end.parse!
end
end
def run! options=nil
# If we are passed options, fill in the blanks
options &&= options.reverse_merge @@defaults
# Otherwise, parse them from ARGV
options ||= parse!
# Stash them away for later
@@options = options
# If we're running in the foreground sync the output.
unless options[:daemon]
$stdout.sync = $stderr.sync = true
end
module MailCatcher
autoload :Events, 'mail_catcher/events'
autoload :Mail, 'mail_catcher/mail'
autoload :Smtp, 'mail_catcher/smtp'
autoload :Web, 'mail_catcher/web'
def self.run(options = {})
options[:smtp_ip] ||= '127.0.0.1'
options[:smtp_port] ||= 1025
options[:http_ip] ||= '127.0.0.1'
options[:http_port] ||= 1080
puts "Starting MailCatcher"
puts "==> smtp://#{options[:smtp_ip]}:#{options[:smtp_port]}"
puts "==> http://#{options[:http_ip]}:#{options[:http_port]}"
Thin::Logging.silent = true
Thin::Logging.silent = (ENV["MAILCATCHER_ENV"] != "development")
# One EventMachine loop...
EventMachine.run do
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
Thin::Server.start options[:http_ip], options[:http_port], Web
# Set up an SMTP server to run within EventMachine
rescue_port options[:smtp_port] do
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
puts "==> #{smtp_url}"
end
# Let Thin set itself up inside our EventMachine loop
# (Skinny/WebSockets just works on the inside)
rescue_port options[:http_port] do
Thin::Server.start(options[:http_ip], options[:http_port], Web)
puts "==> #{http_url}"
end
# Open the web browser before detatching console
if options[:browse]
EventMachine.next_tick do
browse http_url
end
end
# Daemonize, if we should, but only after the servers have started.
if options[:daemon]
EventMachine.next_tick do
if quittable?
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
else
puts "*** MailCatcher is now running as a daemon that cannot be quit."
end
Process.daemon
end
end
end
end
def quit!
EventMachine.next_tick { EventMachine.stop_event_loop }
end
protected
def smtp_url
"smtp://#{@@options[:smtp_ip]}:#{@@options[:smtp_port]}"
end
def http_url
"http://#{@@options[:http_ip]}:#{@@options[:http_port]}"
end
def rescue_port port
begin
yield
# XXX: EventMachine only spits out RuntimeError with a string description
rescue RuntimeError
if $!.to_s =~ /\bno acceptor\b/
puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?"
puts "==> #{smtp_url}"
puts "==> #{http_url}"
exit -1
else
raise
end
end
end
end

View File

@ -1,7 +1,7 @@
require 'eventmachine'
require "eventmachine"
module MailCatcher
module Events
MessageAdded = EventMachine::Channel.new
end
end
end

View File

@ -1,119 +1,200 @@
require 'mail'
require 'sqlite3'
require 'eventmachine'
require "eventmachine"
require "json"
require "mail"
require "sqlite3"
module MailCatcher::Mail
class << self
def db
@@__db ||= begin
SQLite3::Database.new(':memory:', :results_as_hash => true, :type_translation => true).tap do |db|
db.execute(<<-SQL)
CREATE TABLE message (
id INTEGER PRIMARY KEY ASC,
sender TEXT,
recipients TEXT,
subject TEXT,
source BLOB,
size TEXT,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
db.execute(<<-SQL)
CREATE TABLE message_part (
id INTEGER PRIMARY KEY ASC,
message_id INTEGER NOT NULL,
cid TEXT,
type TEXT,
is_attachment INTEGER,
filename TEXT,
charset TEXT,
body BLOB,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
end
module MailCatcher::Mail extend self
def db
@__db ||= begin
SQLite3::Database.new(MailCatcher.options[:sqlite_db], :type_translation => true).tap do |db|
db.execute(<<-SQL)
CREATE TABLE IF NOT EXISTS message (
id INTEGER PRIMARY KEY ASC,
sender TEXT,
recipients TEXT,
subject TEXT,
from_server TEXT,
source BLOB,
size TEXT,
type TEXT,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
db.execute(<<-SQL)
CREATE TABLE IF NOT EXISTS message_part (
id INTEGER PRIMARY KEY ASC,
message_id INTEGER NOT NULL,
cid TEXT,
type TEXT,
is_attachment INTEGER,
filename TEXT,
charset TEXT,
body BLOB,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
end
end
def add_message(message)
@@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, size, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))")
mail = Mail.new(message[:source])
result = @@add_message_query.execute(message[:sender], message[:recipients].inspect, mail.subject, message[:source], message[:source].length)
message_id = db.last_insert_row_id
(mail.all_parts || [mail]).each do |part|
body = part.body.to_s
add_message_part(message_id, part.cid, part.mime_type || 'text/plain', part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
end
EventMachine.next_tick do
message = MailCatcher::Mail.message message_id
MailCatcher::Events::MessageAdded.push message
end
end
def add_message_part(*args)
@@add_message_part_query ||= db.prepare "INSERT INTO message_part (message_id, cid, type, is_attachment, filename, charset, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))"
@@add_message_part_query.execute(*args)
end
def latest_created_at
@@latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
@@latest_created_at_query.execute.next
end
def messages
@@messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at DESC"
@@messages_query.execute.to_a
end
def message(id)
@@message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
@@message_query.execute(id).next
end
def message_has_html?(id)
@@message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/html' LIMIT 1"
!!@@message_has_html_query.execute(id).next
end
def message_has_plain?(id)
@@message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
!!@@message_has_html_query.execute(id).next
end
def message_parts(id)
@@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
@@message_parts_query.execute(id).to_a
end
def message_attachments(id)
@@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
@@message_parts_query.execute(id).to_a
end
def message_part(message_id, part_id)
@@message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
@@message_part_query.execute(message_id, part_id).next
end
def message_part_type(message_id, part_type)
@@message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
@@message_part_type_query.execute(message_id, part_type).next
end
def message_part_html(message_id)
message_part_type message_id, "text/html"
end
def message_part_plain(message_id)
message_part_type message_id, "text/plain"
end
def message_part_cid(message_id, cid)
@@message_part_cid_query ||= db.prepare 'SELECT * FROM message_part WHERE message_id = ?'
@@message_part_cid_query.execute(message_id).find { |part| part["cid"] == cid }
end
end
end
def add_message(message)
@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, from_server, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))")
mail = Mail.new(message[:source])
from_server = mail.received ? mail.received.value.sub(/^from\s+/, '').sub(/\s+.*$/, '') : nil
sender = (mail.from && !mail.from.empty?) ? mail.from : message[:sender]
recipients = (mail.to && !mail.to.empty?) ? mail.to : message[:recipients]
@add_message_query.execute(sender, JSON.generate(recipients), mail.subject, from_server, message[:source], mail.mime_type || "text/plain", message[:source].length)
message_id = db.last_insert_row_id
parts = mail.all_parts
parts = [mail] if parts.empty?
parts.each do |part|
body = part.body.to_s
# Only parts have CIDs, not mail
cid = part.cid if part.respond_to? :cid
add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
end
EventMachine.next_tick do
message = MailCatcher::Mail.message message_id
MailCatcher::Events::MessageAdded.push message
if MailCatcher.options[:delete_older_than]
MailCatcher::Mail.delete_messages_older_than!(MailCatcher.options[:delete_older_than])
end
if MailCatcher.options[:keep_num_emails]
MailCatcher::Mail.delete_messages_keep!(MailCatcher.options[:keep_num_emails])
end
end
end
def add_message_part(*args)
@add_message_part_query ||= db.prepare "INSERT INTO message_part (message_id, cid, type, is_attachment, filename, charset, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))"
@add_message_part_query.execute(*args)
end
def latest_created_at
@latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
@latest_created_at_query.execute.next
end
def messages
@messages_query ||= db.prepare MailCatcher.show_from_server? ?
"SELECT id, sender, recipients, subject, from_server, size, created_at FROM message ORDER BY created_at, id ASC" :
"SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at, id ASC"
@messages_query.execute.map do |row|
Hash[row.fields.zip(row)].tap do |message|
message["recipients"] &&= JSON.parse(message["recipients"])
end
end
end
def message(id)
@message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
row = @message_query.execute(id).next
row && Hash[row.fields.zip(row)].tap do |message|
unless MailCatcher.show_from_server?
message.delete('from_server')
end
message["recipients"] &&= JSON.parse(message["recipients"])
end
end
def message_has_html?(id)
@message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type IN ('application/xhtml+xml', 'text/html') LIMIT 1"
(!!@message_has_html_query.execute(id).next) || ["text/html", "application/xhtml+xml"].include?(message(id)["type"])
end
def message_has_plain?(id)
@message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
(!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
end
def message_parts(id)
@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
@message_parts_query.execute(id).map do |row|
Hash[row.fields.zip(row)]
end
end
def message_attachments(id)
@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
@message_parts_query.execute(id).map do |row|
Hash[row.fields.zip(row)]
end
end
def message_part(message_id, part_id)
@message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
row = @message_part_query.execute(message_id, part_id).next
row && Hash[row.fields.zip(row)]
end
def message_part_type(message_id, part_type)
@message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
row = @message_part_type_query.execute(message_id, part_type).next
row && Hash[row.fields.zip(row)]
end
def message_part_html(message_id)
part = message_part_type(message_id, "text/html")
part ||= message_part_type(message_id, "application/xhtml+xml")
part ||= begin
message = message(message_id)
message if message and ["text/html", "application/xhtml+xml"].include? message["type"]
end
end
def message_part_plain(message_id)
message_part_type message_id, "text/plain"
end
def message_part_cid(message_id, cid)
@message_part_cid_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ?"
@message_part_cid_query.execute(message_id).map do |row|
Hash[row.fields.zip(row)]
end.find do |part|
part["cid"] == cid
end
end
def delete_ids_query!(query, *bind_vars)
ids ||= []
query.execute(bind_vars).map do |row|
ids << row[0]
end
delete_ids!(ids)
end
def delete_ids!(ids)
unless ids.empty?
id_query = ids.join(',')
db.execute("DELETE FROM message WHERE id IN (#{id_query})")
db.execute("DELETE FROM message_part WHERE id IN (#{id_query})")
EventMachine.next_tick do
MailCatcher::Events::MessageAdded.push ids
end
end
end
def delete!
@delete_all_messages_query ||= db.prepare "SELECT id FROM message"
delete_ids_query!(@delete_all_messages_query)
end
def delete_message!(message_id)
delete_ids!([message_id.to_i])
end
def delete_messages_older_than!(modifier)
@delete_messages_older_than_query ||= db.prepare "SELECT id FROM message WHERE created_at < datetime('now', ?)"
delete_ids_query!(@delete_messages_older_than_query, modifier)
end
def delete_messages_keep!(keep_num_emails)
@delete_messages_older_than_query ||= db.prepare "SELECT id FROM message ORDER BY id DESC LIMIT -1 OFFSET ?"
delete_ids_query!(@delete_messages_older_than_query, keep_num_emails)
end
end

View File

@ -1,48 +1,55 @@
require 'eventmachine'
require "eventmachine"
module MailCatcher
class Smtp < EventMachine::Protocols::SmtpServer
def current_message
@current_message ||= {}
end
def receive_reset
@current_message = nil
true
end
def receive_sender(sender)
current_message[:sender] = sender
true
end
def receive_recipient(recipient)
current_message[:recipients] ||= []
current_message[:recipients] << recipient
true
end
def receive_data_chunk(lines)
current_message[:source] ||= ""
current_message[:source] += lines.join("\n")
true
require "mail_catcher/mail"
class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
# We override EM's mail from processing to allow multiple mail-from commands
# per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
def process_mail_from sender
if @state.include? :mail_from
@state -= [:mail_from, :rcpt, :data]
receive_reset
end
def receive_message
MailCatcher::Mail.add_message current_message
puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
true
rescue
puts "*** Error receiving message: #{current_message.inspect}"
puts " Exception: #{$!}"
puts " Backtrace:"
$!.backtrace.each do |line|
puts " #{line}"
end
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
false
ensure
@current_message = nil
end
super
end
end
def current_message
@current_message ||= {}
end
def receive_reset
@current_message = nil
true
end
def receive_sender(sender)
current_message[:sender] = sender
true
end
def receive_recipient(recipient)
current_message[:recipients] ||= []
current_message[:recipients] << recipient
true
end
def receive_data_chunk(lines)
current_message[:source] ||= ""
lines.each do |line|
current_message[:source] << line << "\r\n"
end
true
end
def receive_message
MailCatcher::Mail.add_message current_message
puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
true
rescue => exception
MailCatcher.log_exception("Error receiving message", @current_message, exception)
false
ensure
@current_message = nil
end
end

View File

@ -0,0 +1,3 @@
module MailCatcher
VERSION = "0.6.5"
end

View File

@ -1,98 +1,22 @@
require 'sinatra'
require 'json'
require 'pathname'
require "rack/builder"
require 'skinny'
class Sinatra::Request
include Skinny::RequestHelpers
end
require "mail_catcher/web/application"
module MailCatcher
class Web < Sinatra::Base
set :root, Pathname.new(__FILE__).dirname.parent.parent
set :haml, :format => :html5
get '/' do
haml :index
end
get '/messages' do
if request.websocket?
request.websocket!(
:protocol => "MailCatcher 0.2 Message Push",
:on_start => proc do |websocket|
subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
websocket.on_close do |websocket|
MailCatcher::Events::MessageAdded.unsubscribe subscription
end
end)
else
MailCatcher::Mail.messages.to_json
module Web extend self
def app
@@app ||= Rack::Builder.new do
if ENV["MAILCATCHER_ENV"] == "development"
require "mail_catcher/web/assets"
map("/assets") { run Assets }
end
map("/") { run Application }
end
end
get '/messages/:id.json' do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
message.merge({
"formats" => [
"source",
("html" if MailCatcher::Mail.message_has_html? id),
("plain" if MailCatcher::Mail.message_has_plain? id),
].flatten,
"attachments" => MailCatcher::Mail.message_attachments(id).map do |attachment|
attachment.merge({"href" => "/messages/#{escape(id)}/#{escape(attachment['cid'])}"})
end,
}).to_json
else
not_found
end
end
get '/messages/:id.html' do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_html(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
part["body"].gsub(/cid:([^'"> ]+)/, "#{id}/\\1")
else
not_found
end
end
get "/messages/:id.plain" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_plain(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
part["body"]
else
not_found
end
end
get "/messages/:id.source" do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
content_type "text/plain"
message["source"]
else
not_found
end
end
get "/messages/:id/:cid" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_cid(id, params[:cid])
content_type part["type"], :charset => (part["charset"] || "utf8")
attachment part["filename"] if part["is_attachment"] == 1
body part["body"].to_s
else
not_found
end
end
not_found do
"<html><body><h1>No Dice</h1><p>The message you were looking for does not exist, or doesn't have content of this type.</p></body></html>"
def call(env)
app.call(env)
end
end
end
end

View File

@ -0,0 +1,181 @@
require "pathname"
require "net/http"
require "uri"
require "sinatra"
require "skinny"
require "mail_catcher/events"
require "mail_catcher/mail"
class Sinatra::Request
include Skinny::Helpers
end
module MailCatcher
module Web
class Application < Sinatra::Base
set :development, ENV["MAILCATCHER_ENV"] == "development"
set :root, File.expand_path("#{__FILE__}/../../../..")
if development?
require "sprockets-helpers"
configure do
require "mail_catcher/web/assets"
Sprockets::Helpers.configure do |config|
config.environment = Assets
config.prefix = "/assets"
config.digest = false
config.public_path = public_folder
config.debug = true
end
end
helpers do
include Sprockets::Helpers
end
else
helpers do
def javascript_tag(name)
%{<script src="/assets/#{name}.js"></script>}
end
def stylesheet_tag(name)
%{<link rel="stylesheet" href="/assets/#{name}.css">}
end
end
end
get "/" do
erb :index
end
delete "/" do
if MailCatcher.quittable?
MailCatcher.quit!
status 204
else
status 403
end
end
get "/messages" do
if request.websocket?
request.websocket!(
:on_start => proc do |websocket|
subscription = Events::MessageAdded.subscribe do |message|
begin
websocket.send_message(JSON.generate(message))
rescue => exception
MailCatcher.log_exception("Error sending message through websocket", message, exception)
end
end
websocket.on_close do |*|
Events::MessageAdded.unsubscribe subscription
end
end)
else
content_type :json
JSON.generate(Mail.messages)
end
end
delete "/messages" do
Mail.delete!
status 204
end
get "/messages/:id.json" do
id = params[:id].to_i
if message = Mail.message(id)
content_type :json
JSON.generate(message.merge({
"formats" => [
"source",
("html" if Mail.message_has_html? id),
("plain" if Mail.message_has_plain? id)
].compact,
"attachments" => Mail.message_attachments(id).map do |attachment|
attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
end,
}))
else
not_found
end
end
get "/messages/:id.html" do
id = params[:id].to_i
if part = Mail.message_part_html(id)
content_type :html, :charset => (part["charset"] || "utf8")
body = part["body"]
# Rewrite body to link to embedded attachments served by cid
body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
body
else
not_found
end
end
get "/messages/:id.plain" do
id = params[:id].to_i
if part = Mail.message_part_plain(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
part["body"]
else
not_found
end
end
get "/messages/:id.source" do
id = params[:id].to_i
if message = Mail.message(id)
content_type "text/plain"
message["source"]
else
not_found
end
end
get "/messages/:id.eml" do
id = params[:id].to_i
if message = Mail.message(id)
content_type "message/rfc822"
message["source"]
else
not_found
end
end
get "/messages/:id/parts/:cid" do
id = params[:id].to_i
if part = Mail.message_part_cid(id, params[:cid])
content_type part["type"], :charset => (part["charset"] || "utf8")
attachment part["filename"] if part["is_attachment"] == 1
body part["body"].to_s
else
not_found
end
end
delete "/messages/:id" do
id = params[:id].to_i
if Mail.message(id)
Mail.delete_message!(id)
status 204
else
not_found
end
end
not_found do
erb :"404"
end
end
end
end

View File

@ -0,0 +1,13 @@
require "sprockets"
require "sprockets-sass"
require "compass"
module MailCatcher
module Web
Assets = Sprockets::Environment.new(File.expand_path("#{__FILE__}/../../../..")).tap do |sprockets|
Dir["#{sprockets.root}/{,vendor}/assets/*"].each do |path|
sprockets.append_path(path)
end
end
end
end

3
lib/mailcatcher.rb Normal file
View File

@ -0,0 +1,3 @@
require "mail_catcher"
Mailcatcher = MailCatcher

52
mailcatcher.gemspec Normal file
View File

@ -0,0 +1,52 @@
require File.expand_path("../lib/mail_catcher/version", __FILE__)
Gem::Specification.new do |s|
s.name = "mailcatcher"
s.version = MailCatcher::VERSION
s.license = "MIT"
s.summary = "Runs an SMTP server, catches and displays email in a web interface."
s.description = <<-END
MailCatcher runs a super simple SMTP server which catches any
message sent to it to display in a web interface. Run
mailcatcher, set your favourite app to deliver to
smtp://127.0.0.1:1025 instead of your default SMTP server,
then check out http://127.0.0.1:1080 to see the mail.
END
s.author = "Samuel Cochran"
s.email = "sj26@sj26.com"
s.homepage = "http://mailcatcher.me"
s.files = Dir[
"README.md", "LICENSE", "VERSION",
"bin/*",
"lib/**/*.rb",
"public/**/*",
"views/**/*",
] - Dir["lib/mail_catcher/web/assets.rb"]
s.require_paths = ["lib"]
s.executables = ["mailcatcher", "catchmail"]
s.extra_rdoc_files = ["README.md", "LICENSE"]
s.required_ruby_version = ">= 1.9.3"
s.add_dependency "eventmachine", "1.0.9.1"
s.add_dependency "mail", "~> 2.3"
s.add_dependency "rack", "~> 1.5"
s.add_dependency "sinatra", "~> 1.2"
s.add_dependency "sqlite3", "~> 1.3"
s.add_dependency "thin", "~> 1.5.0"
s.add_dependency "skinny", "~> 0.2.3"
s.add_development_dependency "coffee-script"
s.add_development_dependency "compass"
s.add_development_dependency "minitest", "~> 5.0"
s.add_development_dependency "rake"
s.add_development_dependency "rdoc"
s.add_development_dependency "sass"
s.add_development_dependency "selenium-webdriver"
s.add_development_dependency "sprockets"
s.add_development_dependency "sprockets-sass"
s.add_development_dependency "sprockets-helpers"
s.add_development_dependency "uglifier"
end

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,94 +0,0 @@
var MailCatcher = {
init: function() {
$('#mail tr').live('click', function() {
MailCatcher.load($(this).attr('data-message-id'));
});
$('#message .formats ul li').live('click', function() {
MailCatcher.loadBody($('#mail tr.selected').attr('data-message-id'), $(this).attr('data-message-format'));
});
MailCatcher.refresh();
MailCatcher.subscribe();
},
addMessage: function(message) {
var row = $('<tr />').attr('data-message-id', message.id.toString());
$.each(['sender', 'recipients', 'subject', 'created_at'], function (i, property) {
row.append($('<td />').text(message[property]));
});
$('#mail tbody').append(row);
},
refresh: function() {
console.log('Refreshing messages');
$.getJSON('/messages', function(mail) {
$.each(mail, function(i, message) {
MailCatcher.addMessage(message);
});
});
},
subscribe: function () {
MailCatcher.websocket = new WebSocket("ws" + (window.location.scheme == 'https' ? 's' : '') + "://" + window.location.host + "/messages");
MailCatcher.websocket.onmessage = function (event) {
console.log('Message received:', event.data);
MailCatcher.addMessage($.parseJSON(event.data));
};
},
load: function(id) {
id = id || $('#mail tr.selected').attr('data-message-id');
if (id !== null) {
console.log('Loading message', id);
$('#mail tbody tr:not([data-message-id="'+id+'"])').removeClass('selected');
$('#mail tbody tr[data-message-id="'+id+'"]').addClass('selected');
$.getJSON('/messages/' + id + '.json', function(message) {
$('#message .received span').text(message.created_at);
$('#message .from span').text(message.sender);
$('#message .to span').text(message.recipients);
$('#message .subject span').text(message.subject);
$('#message .formats ul li').each(function(i, el) {
var $el = $(el),
format = $el.attr('data-message-format');
if ($.inArray(format, message.formats) >= 0) {
$el.show();
} else {
$el.hide();
}
});
if ($("#message .formats ul li.selected:not(:visible)")) {
$("#message .formats ul li.selected").removeClass("selected");
$("#message .formats ul li:visible:first").addClass("selected");
}
if (message.attachments.length > 0) {
console.log(message.attachments);
$('#message .attachments ul').empty();
$.each(message.attachments, function (i, attachment) {
$('#message .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
});
$('#message .attachments').show();
} else {
$('#message .attachments').hide();
}
MailCatcher.loadBody();
});
}
},
loadBody: function(id, format) {
id = id || $('#mail tr.selected').attr('data-message-id');
format = format || $('#message .formats ul li.selected').first().attr('data-message-format') || 'html';
$('#message .formats ul li[data-message-format="'+format+'"]').addClass('selected');
$('#message .formats ul li:not([data-message-format="'+format+'"])').removeClass('selected');
if (id !== undefined && id !== null) {
console.log('Loading message', id, 'in format', format);
$('#message iframe').attr('src', '/messages/' + id + '.' + format);
}
}
};

View File

@ -1,23 +0,0 @@
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin:0px; padding:0px; border:0px; outline:0px; font-weight:inherit; font-style:inherit; font-size:100%; font-family:inherit; vertical-align:baseline; }
body { line-height: 1; color: black; background: Window; color: WindowText; font-size: 12px; font-family: Helvetica, Arial, sans-serif; }
ol, ul { list-style: none; }
table { border-collapse: separate; border-spacing: 0px; }
caption, th, td { text-align: left; font-weight: normal; }
#mail { height: 10em; overflow: auto; background: ThreeDHighlight; border-bottom: 1px solid ButtonFace; }
#mail table { width: 100%; }
#mail table thead tr { background: ButtonHighlight; color: ButtonText; }
#mail table thead tr th { padding: .25em; font-weight: bold; }
#mail table tbody tr:nth-child(even) { background: ButtonHighlight; color: ButtonText; }
#mail table tbody tr.selected { background: Highlight; color: HighlightText; }
#mail table tbody tr td { padding: .25em; }
#message .metadata { padding: 1em; }
#message .metadata div { padding: .25em;; }
#message .metadata div label { display: inline-block; width: 8em; text-align: right; font-weight: bold; }
#message .metadata .attachments { display: none; }
#message .metadata .attachments ul { display: inline; }
#message .metadata .attachments ul li { display: inline-block; margin-right: .5em; }
#message .formats ul { border-bottom: 1px solid WindowFrame; padding: 0 .5em; }
#message .formats ul li { display: inline-block; padding: .5em; border: solid WindowFrame; background: ButtonFace; color: ButtonText; border-width: 1px 1px 0 1px; }
#message .formats ul li.selected { background: ThreeDHighlight; color: WindowText; height: 13px; margin-bottom: -1px; }
#message iframe { width: 100%; height: 42em; background: ThreeDHighlight; }

227
spec/acceptance_spec.rb Normal file
View File

@ -0,0 +1,227 @@
ENV["MAILCATCHER_ENV"] = "test"
require "minitest/autorun"
require "mail_catcher"
require "socket"
require "net/smtp"
require "selenium-webdriver"
SMTP_PORT = 10025
HTTP_PORT = 10080
# Start MailCatcher
MAILCATCHER_PID = spawn "bundle", "exec", "mailcatcher", "--foreground", "--smtp-port", SMTP_PORT.to_s, "--http-port", HTTP_PORT.to_s
# Make sure it will be stopped
MiniTest.after_run do
Process.kill("TERM", MAILCATCHER_PID) and Process.wait
end
# Wait for it to boot
begin
TCPSocket.new("127.0.0.1", SMTP_PORT).close
TCPSocket.new("127.0.0.1", HTTP_PORT).close
rescue Errno::ECONNREFUSED
retry
end
describe MailCatcher do
DEFAULT_FROM = "from@example.com"
DEFAULT_TO = "to@example.com"
def deliver(message, options={})
options = {:from => DEFAULT_FROM, :to => DEFAULT_TO}.merge(options)
Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp|
smtp.send_message message, options[:from], options[:to]
end
end
def read_example(name)
File.read(File.expand_path("../../examples/#{name}", __FILE__))
end
def deliver_example(name, options={})
deliver(read_example(name), options)
end
def selenium
@selenium ||= Selenium::WebDriver.for(:phantomjs)
end
before { selenium.navigate.to("http://127.0.0.1:#{HTTP_PORT}") }
def messages_element
selenium.find_element(:id, "messages")
end
def message_row_element
messages_element.find_element(:xpath, ".//table/tbody/tr[1]")
end
def message_from_element
message_row_element.find_element(:xpath, ".//td[1]")
end
def message_to_element
message_row_element.find_element(:xpath, ".//td[2]")
end
def message_subject_element
message_row_element.find_element(:xpath, ".//td[3]")
end
def message_received_element
message_row_element.find_element(:xpath, ".//td[4]")
end
def html_tab_element
selenium.find_element(:css, "#message header .format.html a")
end
def plain_tab_element
selenium.find_element(:css, "#message header .format.plain a")
end
def source_tab_element
selenium.find_element(:css, "#message header .format.source a")
end
def iframe_element
selenium.find_element(:css, "#message iframe")
end
def body_element
selenium.find_element(:tag_name, "body")
end
it "catches and displays a plain text message as plain text and source" do
deliver_example("plainmail")
message_from_element.text.must_include DEFAULT_FROM
message_to_element.text.must_include DEFAULT_TO
message_subject_element.text.must_equal "Plain mail"
Time.parse(message_received_element.text).must_be_close_to Time.now, 5
message_row_element.click
source_tab_element.displayed?.must_equal true
plain_tab_element.displayed?.must_equal true
html_tab_element.displayed?.must_equal false
plain_tab_element.click
iframe_element.displayed?.must_equal true
iframe_element.attribute(:src).must_match(/\.plain\Z/)
selenium.switch_to.frame(iframe_element)
body_element.text.wont_include "Subject: Plain mail"
body_element.text.must_include "Here's some text"
selenium.switch_to.default_content
source_tab_element.click
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "Subject: Plain mail"
body_element.text.must_include "Here's some text"
end
it "catches and displays an html message as html and source" do
deliver_example("htmlmail")
message_from_element.text.must_include DEFAULT_FROM
message_to_element.text.must_include DEFAULT_TO
message_subject_element.text.must_equal "Test HTML Mail"
Time.parse(message_received_element.text).must_be_close_to Time.now, 5
message_row_element.click
source_tab_element.displayed?.must_equal true
plain_tab_element.displayed?.must_equal false
html_tab_element.displayed?.must_equal true
html_tab_element.click
iframe_element.displayed?.must_equal true
iframe_element.attribute(:src).must_match /\.html\Z/
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "Yo, you slimey scoundrel."
body_element.text.wont_include "Content-Type: text/html"
body_element.text.wont_include "Yo, you <em>slimey scoundrel</em>."
selenium.switch_to.default_content
source_tab_element.click
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "Content-Type: text/html"
body_element.text.must_include "Yo, you <em>slimey scoundrel</em>."
body_element.text.wont_include "Yo, you slimey scoundrel."
end
it "catches and displays a multipart message as text, html and source" do
deliver_example("multipartmail")
message_from_element.text.must_include DEFAULT_FROM
message_to_element.text.must_include DEFAULT_TO
message_subject_element.text.must_equal "Test Multipart Mail"
Time.parse(message_received_element.text).must_be_close_to Time.now, 5
message_row_element.click
source_tab_element.displayed?.must_equal true
plain_tab_element.displayed?.must_equal true
html_tab_element.displayed?.must_equal true
plain_tab_element.click
iframe_element.displayed?.must_equal true
iframe_element.attribute(:src).must_match /\.plain\Z/
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "Plain text mail"
body_element.text.wont_include "HTML mail"
body_element.text.wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662"
selenium.switch_to.default_content
html_tab_element.click
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "HTML mail"
body_element.text.wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662"
selenium.switch_to.default_content
source_tab_element.click
selenium.switch_to.frame(iframe_element)
body_element.text.must_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662"
body_element.text.must_include "Plain text mail"
body_element.text.must_include "<em>HTML</em> mail"
end
it "catches and displays an unknown message as source" do
deliver_example("unknownmail")
skip
end
it "catches and displays a message with multipart attachments" do
deliver_example("attachmail")
skip
end
it "doesn't choke on messages containing dots" do
deliver_example("dotmail")
skip
end
it "doesn't choke on messages containing quoted printables" do
deliver_example("quoted_printable_htmlmail")
skip
end
end

104
vendor/assets/javascripts/date.js vendored Normal file
View File

@ -0,0 +1,104 @@
/**
* Version: 1.0 Alpha-1
* Build Date: 13-Nov-2007
* Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
* License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
* Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
*/
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
if(x.month||x.months){this.addMonths(x.month||x.months);}
if(x.year||x.years){this.addYears(x.year||x.years);}
if(x.day||x.days){this.addDays(x.day||x.days);}
return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
if(!x.second&&x.second!==0){x.second=-1;}
if(!x.minute&&x.minute!==0){x.minute=-1;}
if(!x.hour&&x.hour!==0){x.hour=-1;}
if(!x.day&&x.day!==0){x.day=-1;}
if(!x.month&&x.month!==0){x.month=-1;}
if(!x.year&&x.year!==0){x.year=-1;}
if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
if(x.timezone){this.setTimezone(x.timezone);}
if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
break;}
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
rx.push(r[0]);s=r[1];}
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){r=null;}
if(r){return r;}}
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
rx.push(r[0]);s=r[1];}
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
s=q[1];}
if(!r){throw new $P.Exception(s);}
if(q){throw new $P.Exception(q[1]);}
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
if(!last&&q[1].length===0){last=true;}
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
if(rx[1].length<best[1].length){best=rx;}
if(best[1].length===0){break;}}
if(best[0].length===0){return best;}
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
best[1]=q[1];}
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
if(this.now){return new Date();}
var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
if(!this.unit){this.unit="day";}
if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
this[this.unit+"s"]=this.value*orient;}
return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
if(this.month&&!this.day){this.day=1;}
return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
fn=_C[keys]=_.any.apply(null,px);}
return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};

102
vendor/assets/javascripts/favcount.js vendored Normal file
View File

@ -0,0 +1,102 @@
/*
* favcount.js v1.5.0
* http://chrishunt.co/favcount
* Dynamically updates the favicon with a number.
*
* Copyright 2013, Chris Hunt
* Released under the MIT license
*/
(function(){
function Favcount(icon) {
this.icon = icon;
this.opacity = 0.4;
this.canvas = document.createElement('canvas');
this.font = "Helvetica, Arial, sans-serif";
}
Favcount.prototype.set = function(count) {
var self = this,
img = document.createElement('img');
if (self.canvas.getContext) {
img.crossOrigin = "anonymous";
img.onload = function() {
drawCanvas(self.canvas, self.opacity, self.font, img, normalize(count));
};
img.src = this.icon;
}
};
function normalize(count) {
count = Math.round(count);
if (isNaN(count) || count < 1) {
return '';
} else if (count < 10) {
return ' ' + count;
} else if (count > 99) {
return '99';
} else {
return count;
}
}
function drawCanvas(canvas, opacity, font, img, count) {
var head = document.getElementsByTagName('head')[0],
favicon = document.createElement('link'),
multiplier, fontSize, context, xOffset, yOffset, border, shadow;
favicon.rel = 'icon';
// Scale canvas elements based on favicon size
multiplier = img.width / 16;
fontSize = multiplier * 11;
xOffset = multiplier;
yOffset = multiplier * 11;
border = multiplier;
shadow = multiplier * 2;
canvas.height = canvas.width = img.width;
context = canvas.getContext('2d');
context.font = 'bold ' + fontSize + 'px ' + font;
// Draw faded favicon background
if (count) { context.globalAlpha = opacity; }
context.drawImage(img, 0, 0);
context.globalAlpha = 1.0;
// Draw white drop shadow
context.shadowColor = '#FFF';
context.shadowBlur = shadow;
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
// Draw white border
context.fillStyle = '#FFF';
context.fillText(count, xOffset, yOffset);
context.fillText(count, xOffset + border, yOffset);
context.fillText(count, xOffset, yOffset + border);
context.fillText(count, xOffset + border, yOffset + border);
// Draw black count
context.fillStyle = '#000';
context.fillText(count,
xOffset + (border / 2.0),
yOffset + (border / 2.0)
);
// Replace favicon with new favicon
favicon.href = canvas.toDataURL('image/png');
head.removeChild(document.querySelector('link[rel=icon]'));
head.appendChild(favicon);
}
this.Favcount = Favcount;
}).call(this);
(function(){
Favcount.VERSION = '1.5.0';
}).call(this);

2034
vendor/assets/javascripts/flexie.js vendored Normal file

File diff suppressed because it is too large Load Diff

296
vendor/assets/javascripts/keymaster.js vendored Normal file
View File

@ -0,0 +1,296 @@
// keymaster.js
// (c) 2011-2013 Thomas Fuchs
// keymaster.js may be freely distributed under the MIT license.
;(function(global){
var k,
_handlers = {},
_mods = { 16: false, 18: false, 17: false, 91: false },
_scope = 'all',
// modifier keys
_MODIFIERS = {
'⇧': 16, shift: 16,
'⌥': 18, alt: 18, option: 18,
'⌃': 17, ctrl: 17, control: 17,
'⌘': 91, command: 91
},
// special keys
_MAP = {
backspace: 8, tab: 9, clear: 12,
enter: 13, 'return': 13,
esc: 27, escape: 27, space: 32,
left: 37, up: 38,
right: 39, down: 40,
del: 46, 'delete': 46,
home: 36, end: 35,
pageup: 33, pagedown: 34,
',': 188, '.': 190, '/': 191,
'`': 192, '-': 189, '=': 187,
';': 186, '\'': 222,
'[': 219, ']': 221, '\\': 220
},
code = function(x){
return _MAP[x] || x.toUpperCase().charCodeAt(0);
},
_downKeys = [];
for(k=1;k<20;k++) _MAP['f'+k] = 111+k;
// IE doesn't support Array#indexOf, so have a simple replacement
function index(array, item){
var i = array.length;
while(i--) if(array[i]===item) return i;
return -1;
}
// for comparing mods before unassignment
function compareArray(a1, a2) {
if (a1.length != a2.length) return false;
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) return false;
}
return true;
}
var modifierMap = {
16:'shiftKey',
18:'altKey',
17:'ctrlKey',
91:'metaKey'
};
function updateModifierKey(event) {
for(k in _mods) _mods[k] = event[modifierMap[k]];
};
// handle keydown event
function dispatch(event) {
var key, handler, k, i, modifiersMatch, scope;
key = event.keyCode;
if (index(_downKeys, key) == -1) {
_downKeys.push(key);
}
// if a modifier key, set the key.<modifierkeyname> property to true and return
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
if(key in _mods) {
_mods[key] = true;
// 'assignKey' from inside this closure is exported to window.key
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
return;
}
updateModifierKey(event);
// see if we need to ignore the keypress (filter() can can be overridden)
// by default ignore key presses if a select, textarea, or input is focused
if(!assignKey.filter.call(this, event)) return;
// abort if no potentially matching shortcuts found
if (!(key in _handlers)) return;
scope = getScope();
// for each potential shortcut
for (i = 0; i < _handlers[key].length; i++) {
handler = _handlers[key][i];
// see if it's in the current scope
if(handler.scope == scope || handler.scope == 'all'){
// check if modifiers match if any
modifiersMatch = handler.mods.length > 0;
for(k in _mods)
if((!_mods[k] && index(handler.mods, +k) > -1) ||
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
// call the handler and stop the event if neccessary
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
if(handler.method(event, handler)===false){
if(event.preventDefault) event.preventDefault();
else event.returnValue = false;
if(event.stopPropagation) event.stopPropagation();
if(event.cancelBubble) event.cancelBubble = true;
}
}
}
}
};
// unset modifier keys on keyup
function clearModifier(event){
var key = event.keyCode, k,
i = index(_downKeys, key);
// remove key from _downKeys
if (i >= 0) {
_downKeys.splice(i, 1);
}
if(key == 93 || key == 224) key = 91;
if(key in _mods) {
_mods[key] = false;
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
}
};
function resetModifiers() {
for(k in _mods) _mods[k] = false;
for(k in _MODIFIERS) assignKey[k] = false;
};
// parse and assign shortcut
function assignKey(key, scope, method){
var keys, mods;
keys = getKeys(key);
if (method === undefined) {
method = scope;
scope = 'all';
}
// for each shortcut
for (var i = 0; i < keys.length; i++) {
// set modifier keys if any
mods = [];
key = keys[i].split('+');
if (key.length > 1){
mods = getMods(key);
key = [key[key.length-1]];
}
// convert to keycode and...
key = key[0]
key = code(key);
// ...store handler
if (!(key in _handlers)) _handlers[key] = [];
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
}
};
// unbind all handlers for given key in current scope
function unbindKey(key, scope) {
var multipleKeys, keys,
mods = [],
i, j, obj;
multipleKeys = getKeys(key);
for (j = 0; j < multipleKeys.length; j++) {
keys = multipleKeys[j].split('+');
if (keys.length > 1) {
mods = getMods(keys);
key = keys[keys.length - 1];
}
key = code(key);
if (scope === undefined) {
scope = getScope();
}
if (!_handlers[key]) {
return;
}
for (i = 0; i < _handlers[key].length; i++) {
obj = _handlers[key][i];
// only clear handlers if correct scope and mods match
if (obj.scope === scope && compareArray(obj.mods, mods)) {
_handlers[key][i] = {};
}
}
}
};
// Returns true if the key with code 'keyCode' is currently down
// Converts strings into key codes.
function isPressed(keyCode) {
if (typeof(keyCode)=='string') {
keyCode = code(keyCode);
}
return index(_downKeys, keyCode) != -1;
}
function getPressedKeyCodes() {
return _downKeys.slice(0);
}
function filter(event){
var tagName = (event.target || event.srcElement).tagName;
// ignore keypressed in any elements that support keyboard data input
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
}
// initialize key.<modifier> to false
for(k in _MODIFIERS) assignKey[k] = false;
// set current scope (default 'all')
function setScope(scope){ _scope = scope || 'all' };
function getScope(){ return _scope || 'all' };
// delete all handlers for a given scope
function deleteScope(scope){
var key, handlers, i;
for (key in _handlers) {
handlers = _handlers[key];
for (i = 0; i < handlers.length; ) {
if (handlers[i].scope === scope) handlers.splice(i, 1);
else i++;
}
}
};
// abstract key logic for assign and unassign
function getKeys(key) {
var keys;
key = key.replace(/\s/g, '');
keys = key.split(',');
if ((keys[keys.length - 1]) == '') {
keys[keys.length - 2] += ',';
}
return keys;
}
// abstract mods logic for assign and unassign
function getMods(key) {
var mods = key.slice(0, key.length - 1);
for (var mi = 0; mi < mods.length; mi++)
mods[mi] = _MODIFIERS[mods[mi]];
return mods;
}
// cross-browser events
function addEvent(object, event, method) {
if (object.addEventListener)
object.addEventListener(event, method, false);
else if(object.attachEvent)
object.attachEvent('on'+event, function(){ method(window.event) });
};
// set the handlers globally on document
addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
addEvent(document, 'keyup', clearModifier);
// reset modifiers to false whenever the window is (re)focused.
addEvent(window, 'focus', resetModifiers);
// store previously defined key
var previousKey = global.key;
// restore previously defined key and return reference to our key object
function noConflict() {
var k = global.key;
global.key = previousKey;
return k;
}
// set window.key and window.key.set/get/deleteScope, and the default filter
global.key = assignKey;
global.key.setScope = setScope;
global.key.getScope = getScope;
global.key.deleteScope = deleteScope;
global.key.filter = filter;
global.key.isPressed = isPressed;
global.key.getPressedKeyCodes = getPressedKeyCodes;
global.key.noConflict = noConflict;
global.key.unbind = unbindKey;
if(typeof module !== 'undefined') module.exports = key;
})(this);

357
vendor/assets/javascripts/modernizr.js vendored Normal file
View File

@ -0,0 +1,357 @@
/* Modernizr 2.7.1 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-shiv-cssclasses
*/
;
window.Modernizr = (function( window, document, undefined ) {
var version = '2.7.1',
Modernizr = {},
enableClasses = true,
docElement = document.documentElement,
mod = 'modernizr',
modElem = document.createElement(mod),
mStyle = modElem.style,
inputElem ,
toString = {}.toString, tests = {},
inputs = {},
attrs = {},
classes = [],
slice = classes.slice,
featureName,
_hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;
if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {
hasOwnProp = function (object, property) {
return _hasOwnProperty.call(object, property);
};
}
else {
hasOwnProp = function (object, property) {
return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
};
}
if (!Function.prototype.bind) {
Function.prototype.bind = function bind(that) {
var target = this;
if (typeof target != "function") {
throw new TypeError();
}
var args = slice.call(arguments, 1),
bound = function () {
if (this instanceof bound) {
var F = function(){};
F.prototype = target.prototype;
var self = new F();
var result = target.apply(
self,
args.concat(slice.call(arguments))
);
if (Object(result) === result) {
return result;
}
return self;
} else {
return target.apply(
that,
args.concat(slice.call(arguments))
);
}
};
return bound;
};
}
function setCss( str ) {
mStyle.cssText = str;
}
function setCssAll( str1, str2 ) {
return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));
}
function is( obj, type ) {
return typeof obj === type;
}
function contains( str, substr ) {
return !!~('' + str).indexOf(substr);
}
function testDOMProps( props, obj, elem ) {
for ( var i in props ) {
var item = obj[props[i]];
if ( item !== undefined) {
if (elem === false) return props[i];
if (is(item, 'function')){
return item.bind(elem || obj);
}
return item;
}
}
return false;
}
for ( var feature in tests ) {
if ( hasOwnProp(tests, feature) ) {
featureName = feature.toLowerCase();
Modernizr[featureName] = tests[feature]();
classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
}
}
Modernizr.addTest = function ( feature, test ) {
if ( typeof feature == 'object' ) {
for ( var key in feature ) {
if ( hasOwnProp( feature, key ) ) {
Modernizr.addTest( key, feature[ key ] );
}
}
} else {
feature = feature.toLowerCase();
if ( Modernizr[feature] !== undefined ) {
return Modernizr;
}
test = typeof test == 'function' ? test() : test;
if (typeof enableClasses !== "undefined" && enableClasses) {
docElement.className += ' ' + (test ? '' : 'no-') + feature;
}
Modernizr[feature] = test;
}
return Modernizr;
};
setCss('');
modElem = inputElem = null;
;(function(window, document) {
var version = '3.7.0';
var options = window.html5 || {};
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
var supportsHtml5Styles;
var expando = '_html5shiv';
var expanID = 0;
var expandoData = {};
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
getElements().join().replace(/[\w\-]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
'mark{background:#FF0;color:#000}' +
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
var html5 = {
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video',
'version': version,
'shivCSS': (options.shivCSS !== false),
'supportsUnknownElements': supportsUnknownElements,
'shivMethods': (options.shivMethods !== false),
'type': 'default',
'shivDocument': shivDocument,
createElement: createElement,
createDocumentFragment: createDocumentFragment
};
window.html5 = html5;
shivDocument(document);
}(this, document));
Modernizr._version = version;
docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') +
(enableClasses ? ' js ' + classes.join(' ') : '');
return Modernizr;
})(this, this.document);
;

6
views/404.erb Normal file
View File

@ -0,0 +1,6 @@
<html>
<body>
<h1>No Dice</h1>
<p>The message you were looking for does not exist, or doesn't have content of this type.</p>
</body>
</html>

65
views/index.erb Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html class="mailcatcher">
<head>
<title>MailCatcher</title>
<link href="/favicon.ico" rel="icon" />
<%= stylesheet_tag "mailcatcher" %>
<%= javascript_tag "mailcatcher" %>
</head>
<body>
<header>
<h1><a href="http://mailcatcher.me" target="_blank">MailCatcher</a></h1>
<nav class="app">
<ul>
<li class="search"><input type="search" name="search" placeholder="Search messages..." incremental="true" /></li>
<li class="clear"><a href="#" title="Clear all messages">Clear</a></li>
<% if MailCatcher.quittable? %>
<li class="quit"><a href="#" title="Quit MailCatcher">Quit</a></li>
<% end %>
</ul>
</nav>
</header>
<nav id="messages">
<table>
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Subject</th>
<th>Received</th>
<% if MailCatcher.show_from_server? %>
<th>From Server</th>
<% end %>
</tr>
</thead>
<tbody></tbody>
</table>
</nav>
<div id="resizer"><div class="ruler"></div></div>
<article id="message">
<header>
<dl class="metadata">
<dt class="created_at">Received</dt>
<dd class="created_at"></dd>
<dt class="from">From</dt>
<dd class="from"></dd>
<dt class="to">To</dt>
<dd class="to"></dd>
<dt class="subject">Subject</dt>
<dd class="subject"></dd>
<dt class="attachments">Attachments</dt>
<dd class="attachments"></dd>
</dl>
<nav class="views">
<ul>
<li class="format tab html selected" data-message-format="html"><a href="#">HTML</a></li>
<li class="format tab plain" data-message-format="plain"><a href="#">Plain Text</a></li>
<li class="format tab source" data-message-format="source"><a href="#">Source</a></li>
<li class="action download" data-message-format="html"><a href="#" class="button"><span>Download</span></a></li>
</ul>
</nav>
</header>
<iframe class="body"></iframe>
</article>
</body>
</html>

View File

@ -1,41 +0,0 @@
!!!
%html
%head
%title MailCatcher
%link{:rel => "stylesheet", :href => "/stylesheets/application.css"}
%script{:src => "/javascripts/jquery.js"}
%script{:src => "/javascripts/application.js"}
:javascript
$(MailCatcher.init);
%body
#mail
%table
%thead
%th From
%th To
%th Subject
%th Received
%tbody
#message
.metadata
.received
%label Received
%span
.from
%label From
%span
.to
%label To
%span
.subject
%label Subject
%span
.attachments
%label Attachments
%ul
.formats
%ul
%li.selected{'data-message-format' => 'html'} HTML
%li{'data-message-format' => 'plain'} Plain Text
%li{'data-message-format' => 'source'} Source
%iframe