mailcatcher 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,6 +4,8 @@ Catches mail and serves it through a dream.
4
4
 
5
5
  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.
6
6
 
7
+ ![MailCatcher screenshot](http://puu.sh/2fZR)
8
+
7
9
  ## How
8
10
 
9
11
  1. `gem install mailcatcher`
@@ -15,21 +17,25 @@ MailCatcher runs a super simple SMTP server which catches any message sent to it
15
17
 
16
18
  * Catches all mail and stores it for display.
17
19
  * Shows HTML, Plain Text and Source version of messages, as applicable.
18
- * Rewrites HTML enabling display of embedded, inline images/etc. (currently very basic)
20
+ * Download original email to view in your native mail client(s).
21
+ * Rewrites HTML enabling display of embedded, inline images/etc and open links in a new window. (currently very basic)
19
22
  * Lists attachments and allows separate downloading of parts.
20
23
  * Written super-simply in EventMachine, easy to dig in and change.
21
24
  * Command line options to override the default SMTP/HTTP IP and port settings.
22
25
  * Mail appears instantly if your browser supports [WebSockets][websockets].
26
+ * Daemonizable to run in the background.
23
27
 
24
28
  ## Caveats
25
29
 
26
- * 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.
27
- * 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.
30
+ * Mail processing 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.
28
31
  * The interface is very basic and has not been tested on many browsers yet.
29
32
 
33
+ ## API
34
+
35
+ 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`.
36
+
30
37
  ## TODO
31
38
 
32
- * Download link to view original message in mail client.
33
39
  * Growl support.
34
40
  * Test suite.
35
41
  * Add mail delivery on request, optionally multiple times.
data/bin/mailcatcher CHANGED
@@ -1,44 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
4
-
5
3
  require 'mail_catcher'
6
- require 'optparse'
7
-
8
- options = {}
9
-
10
- OptionParser.new do |opts|
11
- opts.banner = 'Usage: mailcatcher [options]'
12
-
13
- options[:smtp_ip] = '127.0.0.1'
14
- opts.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
15
- options[:smtp_ip] = ip
16
- end
17
-
18
- options[:smtp_port] = 1025
19
- opts.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
20
- options[:smtp_port] = port
21
- end
22
-
23
- options[:http_ip] = '127.0.0.1'
24
- opts.on('--http-ip IP', 'Set the ip address of the http server') do |ip|
25
- options[:http_ip] = ip
26
- end
27
-
28
- options[:http_port] = 1080
29
- opts.on('--http-port PORT', Integer, 'Set the port address of the http server') do |port|
30
- options[:http_port] = port
31
- end
32
-
33
- options[:verbose] = false
34
- opts.on('-v', '--verbose', 'Be more verbose') do
35
- options[:verbose] = true
36
- end
37
-
38
- opts.on('-h', '--help', 'Display this help information') do
39
- puts opts
40
- exit!
41
- end
42
- end.parse!
43
-
44
- MailCatcher.run(options)
4
+ MailCatcher.run!
data/lib/mail_catcher.rb CHANGED
@@ -1,26 +1,89 @@
1
+ require 'active_support/all'
2
+ require 'daemons'
1
3
  require 'eventmachine'
2
4
  require 'thin'
3
5
 
4
6
  module MailCatcher
5
- autoload :Events, 'mail_catcher/events'
6
- autoload :Mail, 'mail_catcher/mail'
7
- autoload :Smtp, 'mail_catcher/smtp'
8
- autoload :Web, 'mail_catcher/web'
7
+ extend ActiveSupport::Autoload
9
8
 
10
- def self.run(options = {})
11
- options[:smtp_ip] ||= '127.0.0.1'
12
- options[:smtp_port] ||= 1025
13
- options[:http_ip] ||= '127.0.0.1'
14
- options[:http_port] ||= 1080
9
+ autoload :Events
10
+ autoload :Mail
11
+ autoload :Smtp
12
+ autoload :Web
13
+
14
+ @@defaults = {
15
+ :smtp_ip => '127.0.0.1',
16
+ :smtp_port => '1025',
17
+ :http_ip => '127.0.0.1',
18
+ :http_port => '1080',
19
+ :verbose => false,
20
+ :daemon => false,
21
+ }
22
+
23
+ def self.parse! arguments=ARGV, defaults=@@defaults
24
+ @@defaults.dup.tap do |options|
25
+ OptionParser.new do |parser|
26
+ parser.banner = 'Usage: mailcatcher [options]'
27
+
28
+ parser.on('--ip IP', 'Set the ip address of both servers') do |ip|
29
+ options[:smtp_ip] = options[:http_ip] = ip
30
+ end
31
+
32
+ parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
33
+ options[:smtp_ip] = ip
34
+ end
35
+
36
+ parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
37
+ options[:smtp_port] = port
38
+ end
39
+
40
+ parser.on('--http-ip IP', 'Set the ip address of the http server') do |ip|
41
+ options[:http_ip] = ip
42
+ end
43
+
44
+ parser.on('--http-port PORT', Integer, 'Set the port address of the http server') do |port|
45
+ options[:http_port] = port
46
+ end
47
+
48
+ parser.on('-d', '--daemon', 'Run as a daemon') do
49
+ options[:daemon] = true
50
+ end
51
+
52
+ parser.on('-v', '--verbose', 'Be more verbose') do
53
+ options[:verbose] = true
54
+ end
55
+
56
+ parser.on('-h', '--help', 'Display this help information') do
57
+ puts parser
58
+ exit!
59
+ end
60
+ end.parse!
61
+ end
62
+ end
63
+
64
+ def self.run! options=nil
65
+ # If we are passed options, fill in the blanks
66
+ options &&= @@defaults.merge options
67
+ # Otherwise, parse them from ARGV
68
+ options ||= parse!
15
69
 
16
70
  puts "Starting MailCatcher"
17
71
  puts "==> smtp://#{options[:smtp_ip]}:#{options[:smtp_port]}"
18
72
  puts "==> http://#{options[:http_ip]}:#{options[:http_port]}"
19
73
 
20
74
  Thin::Logging.silent = true
75
+
21
76
  EventMachine.run do
22
77
  EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
78
+
23
79
  Thin::Server.start options[:http_ip], options[:http_port], Web
80
+
81
+ if options[:daemon]
82
+ # Make sure the servers start before daemonizing.
83
+ EventMachine.next_tick do
84
+ Daemons.daemonize :app_name => "mailcatcher"
85
+ end
86
+ end
24
87
  end
25
88
  end
26
89
  end
@@ -40,7 +40,7 @@ module MailCatcher::Mail
40
40
  @@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, size, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))")
41
41
 
42
42
  mail = Mail.new(message[:source])
43
- result = @@add_message_query.execute(message[:sender], message[:recipients].inspect, mail.subject, message[:source], message[:source].length)
43
+ result = @@add_message_query.execute(message[:sender], message[:recipients].to_json, mail.subject, message[:source], message[:source].length)
44
44
  message_id = db.last_insert_row_id
45
45
  parts = mail.all_parts
46
46
  parts = [mail] if parts.empty?
@@ -69,12 +69,18 @@ module MailCatcher::Mail
69
69
 
70
70
  def messages
71
71
  @@messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at DESC"
72
- @@messages_query.execute.to_a
72
+ @@messages_query.execute.to_a.tap do |messages|
73
+ messages.each do |message|
74
+ message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
75
+ end
76
+ end
73
77
  end
74
78
 
75
79
  def message(id)
76
80
  @@message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
77
- @@message_query.execute(id).next
81
+ @@message_query.execute(id).next.to_hash.tap do |message|
82
+ message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
83
+ end
78
84
  end
79
85
 
80
86
  def message_has_html?(id)
@@ -54,7 +54,16 @@ module MailCatcher
54
54
  id = params[:id].to_i
55
55
  if part = MailCatcher::Mail.message_part_html(id)
56
56
  content_type part["type"], :charset => (part["charset"] || "utf8")
57
- part["body"].gsub(/cid:([^'"> ]+)/, "#{id}/\\1")
57
+
58
+ body = part["body"]
59
+
60
+ # Rewrite body to link to embedded attachments served by cid
61
+ body.gsub! /cid:([^'"> ]+)/, "#{id}/\\1"
62
+
63
+ # Rewrite body to open links in a new window
64
+ body.gsub! /<a\s+/, '<a target="_blank" '
65
+
66
+ body
58
67
  else
59
68
  not_found
60
69
  end
@@ -80,6 +89,16 @@ module MailCatcher
80
89
  end
81
90
  end
82
91
 
92
+ get "/messages/:id.eml" do
93
+ id = params[:id].to_i
94
+ if message = MailCatcher::Mail.message(id)
95
+ content_type "message/rfc822"
96
+ message["source"]
97
+ else
98
+ not_found
99
+ end
100
+ end
101
+
83
102
  get "/messages/:id/:cid" do
84
103
  id = params[:id].to_i
85
104
  if part = MailCatcher::Mail.message_part_cid(id, params[:cid])
@@ -4,24 +4,26 @@ var MailCatcher = {
4
4
  MailCatcher.load($(this).attr('data-message-id'));
5
5
  });
6
6
 
7
- $('#message .formats ul li').live('click', function() {
7
+ $('#message .actions ul li.tab').live('click', function() {
8
8
  MailCatcher.loadBody($('#mail tr.selected').attr('data-message-id'), $(this).attr('data-message-format'));
9
9
  });
10
10
 
11
11
  MailCatcher.refresh();
12
+
12
13
  MailCatcher.subscribe();
13
14
  },
14
15
 
15
16
  addMessage: function(message) {
16
- var row = $('<tr />').attr('data-message-id', message.id.toString());
17
- $.each(['sender', 'recipients', 'subject', 'created_at'], function (i, property) {
18
- row.append($('<td />').text(message[property]));
19
- });
20
- $('#mail tbody').append(row);
17
+ $('#mail tbody').append(
18
+ $('<tr />').attr('data-message-id', message.id.toString())
19
+ .append($('<td/>').text(message.sender))
20
+ .append($('<td/>').text((message.recipients || []).join(', ')))
21
+ .append($('<td/>').text(message.subject))
22
+ .append($('<td/>').text((new Date(message.created_at)).toString("dddd, d MMM yyyy h:mm:ss tt")))
23
+ );
21
24
  },
22
25
 
23
26
  refresh: function() {
24
- console.log('Refreshing messages');
25
27
  $.getJSON('/messages', function(mail) {
26
28
  $.each(mail, function(i, message) {
27
29
  MailCatcher.addMessage(message);
@@ -30,27 +32,31 @@ var MailCatcher = {
30
32
  },
31
33
 
32
34
  subscribe: function () {
33
- MailCatcher.websocket = new WebSocket("ws" + (window.location.scheme == 'https' ? 's' : '') + "://" + window.location.host + "/messages");
34
- MailCatcher.websocket.onmessage = function (event) {
35
- console.log('Message received:', event.data);
36
- MailCatcher.addMessage($.parseJSON(event.data));
37
- };
35
+ if (WebSocket !== undefined) {
36
+ MailCatcher.websocket = new WebSocket("ws" + (window.location.scheme == 'https' ? 's' : '') + "://" + window.location.host + "/messages");
37
+ MailCatcher.websocket.onmessage = function (event) {
38
+ MailCatcher.addMessage($.parseJSON(event.data));
39
+ };
40
+ } else {
41
+ if (!MailCatcher.refreshInterval) {
42
+ MailCatcher.refreshInterval = setInterval(MailCatcher.refresh, 30000);
43
+ }
44
+ }
38
45
  },
39
46
 
40
47
  load: function(id) {
41
48
  id = id || $('#mail tr.selected').attr('data-message-id');
42
49
 
43
50
  if (id !== null) {
44
- console.log('Loading message', id);
45
-
46
51
  $('#mail tbody tr:not([data-message-id="'+id+'"])').removeClass('selected');
47
52
  $('#mail tbody tr[data-message-id="'+id+'"]').addClass('selected');
48
- $.getJSON('/messages/' + id + '.json', function(message) {
49
- $('#message .received span').text(message.created_at);
53
+
54
+ $.getJSON('/messages/' + id + '.json', function(message) {
55
+ $('#message .received span').text((new Date(message.created_at)).toString("dddd, d MMM yyyy h:mm:ss tt"));
50
56
  $('#message .from span').text(message.sender);
51
- $('#message .to span').text(message.recipients);
57
+ $('#message .to span').text((message.recipients || []).join(', '));
52
58
  $('#message .subject span').text(message.subject);
53
- $('#message .formats ul li').each(function(i, el) {
59
+ $('#message .actions ul li.format').each(function(i, el) {
54
60
  var $el = $(el),
55
61
  format = $el.attr('data-message-format');
56
62
  if ($.inArray(format, message.formats) >= 0) {
@@ -59,20 +65,20 @@ var MailCatcher = {
59
65
  $el.hide();
60
66
  }
61
67
  });
62
- if ($("#message .formats ul li.selected:not(:visible)")) {
63
- $("#message .formats ul li.selected").removeClass("selected");
64
- $("#message .formats ul li:visible:first").addClass("selected");
68
+ if ($("#message .actions ul li.tab.selected:not(:visible)")) {
69
+ $("#message .actions ul li.tab.selected").removeClass("selected");
70
+ $("#message .actions ul li.tab:visible:first").addClass("selected");
65
71
  }
66
72
  if (message.attachments.length > 0) {
67
- console.log(message.attachments);
68
- $('#message .attachments ul').empty();
73
+ $('#message .metadata .attachments ul').empty();
69
74
  $.each(message.attachments, function (i, attachment) {
70
- $('#message .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
75
+ $('#message .metadata .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
71
76
  });
72
- $('#message .attachments').show();
77
+ $('#message .metadata .attachments').show();
73
78
  } else {
74
- $('#message .attachments').hide();
79
+ $('#message .metadata .attachments').hide();
75
80
  }
81
+ $('#message .actions ul li.download a').attr('href', '/messages/' + id + '.eml');
76
82
  MailCatcher.loadBody();
77
83
  });
78
84
  }
@@ -80,14 +86,12 @@ var MailCatcher = {
80
86
 
81
87
  loadBody: function(id, format) {
82
88
  id = id || $('#mail tr.selected').attr('data-message-id');
83
- format = format || $('#message .formats ul li.selected').first().attr('data-message-format') || 'html';
89
+ format = format || $('#message .actions ul li.selected').first().attr('data-message-format') || 'html';
84
90
 
85
- $('#message .formats ul li[data-message-format="'+format+'"]').addClass('selected');
86
- $('#message .formats ul li:not([data-message-format="'+format+'"])').removeClass('selected');
91
+ $('#message .actions ul li.tab[data-message-format="'+format+'"]').addClass('selected');
92
+ $('#message .actions ul li.tab:not([data-message-format="'+format+'"])').removeClass('selected');
87
93
 
88
94
  if (id !== undefined && id !== null) {
89
- console.log('Loading message', id, 'in format', format);
90
-
91
95
  $('#message iframe').attr('src', '/messages/' + id + '.' + format);
92
96
  }
93
97
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Version: 1.0 Alpha-1
3
+ * Build Date: 13-Nov-2007
4
+ * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
5
+ * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
6
+ * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
7
+ */
8
+ 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"}};
9
+ 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;}}
10
+ 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;}}
11
+ 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;}}
12
+ return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
13
+ 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;}
14
+ var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
15
+ if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
16
+ if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
17
+ if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
18
+ if(x.month||x.months){this.addMonths(x.month||x.months);}
19
+ if(x.year||x.years){this.addYears(x.year||x.years);}
20
+ if(x.day||x.days){this.addDays(x.day||x.days);}
21
+ 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+".");}
22
+ 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;}
23
+ if(!x.second&&x.second!==0){x.second=-1;}
24
+ if(!x.minute&&x.minute!==0){x.minute=-1;}
25
+ if(!x.hour&&x.hour!==0){x.hour=-1;}
26
+ if(!x.day&&x.day!==0){x.day=-1;}
27
+ if(!x.month&&x.month!==0){x.month=-1;}
28
+ if(!x.year&&x.year!==0){x.year=-1;}
29
+ if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
30
+ if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
31
+ if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
32
+ if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
33
+ if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
34
+ if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
35
+ if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
36
+ if(x.timezone){this.setTimezone(x.timezone);}
37
+ if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
38
+ 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;}
39
+ 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;}}
40
+ 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();};
41
+ 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;}
42
+ 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);}
43
+ var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
44
+ 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);}
45
+ var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
46
+ 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";}};
47
+ (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;}
48
+ break;}
49
+ 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];}
50
+ rx.push(r[0]);s=r[1];}
51
+ return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
52
+ return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
53
+ 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));}
54
+ 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;}
55
+ 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;}
56
+ try{r=(px[i].call(this,s));}catch(e){r=null;}
57
+ if(r){return r;}}
58
+ 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;}
59
+ try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
60
+ rx.push(r[0]);s=r[1];}
61
+ 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];}
62
+ 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;}
63
+ rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
64
+ s=q[1];}
65
+ if(!r){throw new $P.Exception(s);}
66
+ if(q){throw new $P.Exception(q[1]);}
67
+ if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
68
+ 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;}
69
+ 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;}
70
+ if(!last&&q[1].length===0){last=true;}
71
+ if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
72
+ p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
73
+ if(rx[1].length<best[1].length){best=rx;}
74
+ if(best[1].length===0){break;}}
75
+ if(best[0].length===0){return best;}
76
+ if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
77
+ best[1]=q[1];}
78
+ 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);}
79
+ 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];}
80
+ 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]]);}
81
+ 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]);}}}
82
+ 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);}}
83
+ 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.");}
84
+ 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});}
85
+ return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
86
+ for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
87
+ if(this.now){return new Date();}
88
+ 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);}
89
+ 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;}
90
+ if(!this.unit){this.unit="day";}
91
+ if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
92
+ if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
93
+ this[this.unit+"s"]=this.value*orient;}
94
+ return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
95
+ if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
96
+ if(this.month&&!this.day){this.day=1;}
97
+ 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]));}
98
+ fn=_C[keys]=_.any.apply(null,px);}
99
+ 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]));}
100
+ 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){}
101
+ return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
102
+ try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
103
+ 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;}
104
+ return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};
@@ -1,23 +1,46 @@
1
1
  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; }
2
- body { line-height: 1; color: black; background: Window; color: WindowText; font-size: 12px; font-family: Helvetica, Arial, sans-serif; }
2
+
3
+ body { line-height: 1; background: #eee; color: #000; font-size: 12px; font-family: Helvetica, Arial, sans-serif; }
3
4
  ol, ul { list-style: none; }
4
5
  table { border-collapse: separate; border-spacing: 0px; }
5
6
  caption, th, td { text-align: left; font-weight: normal; }
6
7
 
7
- #mail { height: 10em; overflow: auto; background: ThreeDHighlight; border-bottom: 1px solid ButtonFace; }
8
+ html, body { width: 100%; height: 100%; }
9
+ body {
10
+ display: -webkit-box; display: -moz-box; display: box;
11
+ -webkit-box-orient: vertical; -moz-box-orient: vertical; box-orient: vertical;
12
+ }
13
+ #message {
14
+ display: -webkit-box; display: -moz-box; display: box;
15
+ -webkit-box-orient: vertical; -moz-box-orient: vertical; box-orient: vertical;
16
+ -webkit-box-flex: 1; -moz-box-flex: 1; box-flex: 1;
17
+ }
18
+ #message iframe {
19
+ display: -webkit-box; display: -moz-box; display: box;
20
+ -webkit-box-flex: 1; -moz-box-flex: 1; box-flex: 1;
21
+ }
22
+
23
+ #mail { height: 10em; overflow: auto; background: #fff; border-bottom: 1px solid #ccc; }
8
24
  #mail table { width: 100%; }
9
- #mail table thead tr { background: ButtonHighlight; color: ButtonText; }
10
- #mail table thead tr th { padding: .25em; font-weight: bold; }
11
- #mail table tbody tr:nth-child(even) { background: ButtonHighlight; color: ButtonText; }
25
+ #mail table thead tr { background: #eee; color: #333; }
26
+ #mail table thead tr th { padding: .25em; font-weight: bold; color: #666; text-shadow: 0 1px 0 #fff; }
27
+ #mail table tbody tr:nth-child(even) { background: #ddd; color: #333; }
12
28
  #mail table tbody tr.selected { background: Highlight; color: HighlightText; }
13
29
  #mail table tbody tr td { padding: .25em; }
30
+
14
31
  #message .metadata { padding: 1em; }
15
32
  #message .metadata div { padding: .25em;; }
16
- #message .metadata div label { display: inline-block; width: 8em; text-align: right; font-weight: bold; }
33
+ #message .metadata div label { display: inline-block; width: 8em; margin-right: .5em; text-align: right; font-weight: bold; color: #666; text-shadow: 0 1px 0 #fff; }
17
34
  #message .metadata .attachments { display: none; }
18
35
  #message .metadata .attachments ul { display: inline; }
19
36
  #message .metadata .attachments ul li { display: inline-block; margin-right: .5em; }
20
- #message .formats ul { border-bottom: 1px solid WindowFrame; padding: 0 .5em; }
21
- #message .formats ul li { display: inline-block; padding: .5em; border: solid WindowFrame; background: ButtonFace; color: ButtonText; border-width: 1px 1px 0 1px; }
22
- #message .formats ul li.selected { background: ThreeDHighlight; color: WindowText; height: 13px; margin-bottom: -1px; }
23
- #message iframe { width: 100%; height: 42em; background: ThreeDHighlight; }
37
+ #message .actions ul { border-bottom: 1px solid #999; padding: 0 .5em; }
38
+ #message .actions ul li.tab { display: inline-block; padding: .5em; border: 1px solid #999; background: #ddd; color: #333; border-width: 1px 1px 0 1px; cursor: pointer; text-shadow: 0 1px 0 #eee; }
39
+ #message .actions ul li.tab:not(.selected):hover { background-color: #ddd; }
40
+ #message .actions ul li.tab.selected { background: #fff; color: #000; height: 13px; margin-bottom: -1px; }
41
+ #message .actions ul li.button { display: inline-block; float: right; margin: 0 .25em; }
42
+ #message .actions ul li.button a { display: inline-block; padding: .25em .5em; border: 1px solid #999; border-radius: .35em; background: #ddd; color: #333; text-decoration: none; text-shadow: 1px 1px 0 #eee; box-shadow: 1px 1px 0 #ccc; }
43
+ #message .actions ul li.button a:hover { background: #fff; text-shadow: none; }
44
+ #message .actions ul li.button a:active { margin: 1px -1px -1px 1px; box-shadow: none; }
45
+
46
+ #message .body { width: 100%; min-height: 20em; height: 100%; background: #fff; }
data/views/index.haml CHANGED
@@ -4,6 +4,7 @@
4
4
  %title MailCatcher
5
5
  %link{:rel => "stylesheet", :href => "/stylesheets/application.css"}
6
6
  %script{:src => "/javascripts/jquery.js"}
7
+ %script{:src => "/javascripts/date.js"}
7
8
  %script{:src => "/javascripts/application.js"}
8
9
  :javascript
9
10
  $(MailCatcher.init);
@@ -33,9 +34,12 @@
33
34
  .attachments
34
35
  %label Attachments
35
36
  %ul
36
- .formats
37
+ .actions
37
38
  %ul
38
- %li.selected{'data-message-format' => 'html'} HTML
39
- %li{'data-message-format' => 'plain'} Plain Text
40
- %li{'data-message-format' => 'source'} Source
41
- %iframe
39
+ %li.format.tab.html.selected{'data-message-format' => 'html'} HTML
40
+ %li.format.tab.plain{'data-message-format' => 'plain'} Plain Text
41
+ %li.format.tab.source{'data-message-format' => 'source'} Source
42
+ %li.button.download
43
+ %a{:href => '#'}
44
+ %span Download
45
+ %iframe.body
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: mailcatcher
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.4
5
+ version: 0.3.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Samuel Cochran
@@ -10,51 +10,50 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-10 00:00:00 +08:00
14
- default_executable: mailcatcher
13
+ date: 2011-05-27 00:00:00 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
- name: eventmachine
16
+ name: activesupport
18
17
  prerelease: false
19
18
  requirement: &id001 !ruby/object:Gem::Requirement
20
19
  none: false
21
20
  requirements:
22
- - - ">="
21
+ - - ~>
23
22
  - !ruby/object:Gem::Version
24
- version: "0"
23
+ version: "3.0"
25
24
  type: :runtime
26
25
  version_requirements: *id001
27
26
  - !ruby/object:Gem::Dependency
28
- name: mail
27
+ name: eventmachine
29
28
  prerelease: false
30
29
  requirement: &id002 !ruby/object:Gem::Requirement
31
30
  none: false
32
31
  requirements:
33
- - - ">="
32
+ - - ~>
34
33
  - !ruby/object:Gem::Version
35
- version: "0"
34
+ version: "0.12"
36
35
  type: :runtime
37
36
  version_requirements: *id002
38
37
  - !ruby/object:Gem::Dependency
39
- name: i18n
38
+ name: mail
40
39
  prerelease: false
41
40
  requirement: &id003 !ruby/object:Gem::Requirement
42
41
  none: false
43
42
  requirements:
44
- - - ">="
43
+ - - ~>
45
44
  - !ruby/object:Gem::Version
46
- version: "0"
45
+ version: "2.3"
47
46
  type: :runtime
48
47
  version_requirements: *id003
49
48
  - !ruby/object:Gem::Dependency
50
- name: sqlite3-ruby
49
+ name: sqlite3
51
50
  prerelease: false
52
51
  requirement: &id004 !ruby/object:Gem::Requirement
53
52
  none: false
54
53
  requirements:
55
- - - ">="
54
+ - - ~>
56
55
  - !ruby/object:Gem::Version
57
- version: "0"
56
+ version: "1.3"
58
57
  type: :runtime
59
58
  version_requirements: *id004
60
59
  - !ruby/object:Gem::Dependency
@@ -63,9 +62,9 @@ dependencies:
63
62
  requirement: &id005 !ruby/object:Gem::Requirement
64
63
  none: false
65
64
  requirements:
66
- - - ">="
65
+ - - ~>
67
66
  - !ruby/object:Gem::Version
68
- version: "0"
67
+ version: "1.2"
69
68
  type: :runtime
70
69
  version_requirements: *id005
71
70
  - !ruby/object:Gem::Dependency
@@ -74,9 +73,9 @@ dependencies:
74
73
  requirement: &id006 !ruby/object:Gem::Requirement
75
74
  none: false
76
75
  requirements:
77
- - - ">="
76
+ - - ~>
78
77
  - !ruby/object:Gem::Version
79
- version: 0.1.2
78
+ version: "0.1"
80
79
  type: :runtime
81
80
  version_requirements: *id006
82
81
  - !ruby/object:Gem::Dependency
@@ -85,9 +84,9 @@ dependencies:
85
84
  requirement: &id007 !ruby/object:Gem::Requirement
86
85
  none: false
87
86
  requirements:
88
- - - ">="
87
+ - - ~>
89
88
  - !ruby/object:Gem::Version
90
- version: "0"
89
+ version: "1.2"
91
90
  type: :runtime
92
91
  version_requirements: *id007
93
92
  - !ruby/object:Gem::Dependency
@@ -96,9 +95,9 @@ dependencies:
96
95
  requirement: &id008 !ruby/object:Gem::Requirement
97
96
  none: false
98
97
  requirements:
99
- - - ">="
98
+ - - ~>
100
99
  - !ruby/object:Gem::Version
101
- version: "0"
100
+ version: "3.1"
102
101
  type: :runtime
103
102
  version_requirements: *id008
104
103
  - !ruby/object:Gem::Dependency
@@ -107,37 +106,34 @@ dependencies:
107
106
  requirement: &id009 !ruby/object:Gem::Requirement
108
107
  none: false
109
108
  requirements:
110
- - - ">="
109
+ - - ~>
111
110
  - !ruby/object:Gem::Version
112
- version: "0"
111
+ version: "1.0"
113
112
  type: :runtime
114
113
  version_requirements: *id009
115
- description: " MailCatcher runs a super simple SMTP server which catches any\n message sent to it to display in a web interface. Run\n mailcatcher, set your favourite app to deliver to\n smtp://127.0.0.1:1025 instead of your default SMTP server,\n then check out http://127.0.0.1:1080 to see the mail.\n"
114
+ description: " MailCatcher runs a super simple SMTP server which catches any\n message sent to it to display in a web interface. Run\n mailcatcher, set your favourite app to deliver to\n smtp://127.0.0.1:1025 instead of your default SMTP server,\n then check out http://127.0.0.1:1080 to see the mail.\n"
116
115
  email: sj26@sj26.com
117
116
  executables:
118
117
  - mailcatcher
119
118
  extensions: []
120
119
 
121
120
  extra_rdoc_files:
122
- - LICENSE
123
121
  - README.md
124
- files:
125
122
  - LICENSE
123
+ files:
126
124
  - README.md
127
- - Rakefile
128
- - VERSION
125
+ - LICENSE
129
126
  - bin/mailcatcher
130
- - contrib/mailcatcherd.bash
131
- - lib/mail_catcher.rb
132
127
  - lib/mail_catcher/events.rb
133
128
  - lib/mail_catcher/mail.rb
134
129
  - lib/mail_catcher/smtp.rb
135
130
  - lib/mail_catcher/web.rb
131
+ - lib/mail_catcher.rb
136
132
  - public/javascripts/application.js
133
+ - public/javascripts/date.js
137
134
  - public/javascripts/jquery.js
138
135
  - public/stylesheets/application.css
139
136
  - views/index.haml
140
- has_rdoc: true
141
137
  homepage: http://github.com/sj26/mailcatcher
142
138
  licenses: []
143
139
 
@@ -161,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
157
  requirements: []
162
158
 
163
159
  rubyforge_project:
164
- rubygems_version: 1.6.2
160
+ rubygems_version: 1.7.2
165
161
  signing_key:
166
162
  specification_version: 3
167
163
  summary: Runs an SMTP server, catches and displays email in a web interface.
data/Rakefile DELETED
@@ -1,44 +0,0 @@
1
- require 'rubygems'
2
- require 'rake'
3
-
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "mailcatcher"
8
- gem.summary = %Q{Runs an SMTP server, catches and displays email in a web interface.}
9
- gem.description = <<-EOD
10
- MailCatcher runs a super simple SMTP server which catches any
11
- message sent to it to display in a web interface. Run
12
- mailcatcher, set your favourite app to deliver to
13
- smtp://127.0.0.1:1025 instead of your default SMTP server,
14
- then check out http://127.0.0.1:1080 to see the mail.
15
- EOD
16
- gem.email = "sj26@sj26.com"
17
- gem.homepage = "http://github.com/sj26/mailcatcher"
18
- gem.authors = ["Samuel Cochran"]
19
-
20
- gem.add_dependency 'eventmachine'
21
- gem.add_dependency 'mail'
22
- gem.add_dependency 'i18n'
23
- gem.add_dependency 'sqlite3-ruby'
24
- gem.add_dependency 'thin'
25
- gem.add_dependency 'skinny', '>=0.1.2'
26
- gem.add_dependency 'sinatra'
27
- gem.add_dependency 'haml'
28
- gem.add_dependency 'json'
29
- end
30
- Jeweler::GemcutterTasks.new
31
- rescue LoadError
32
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
33
- end
34
-
35
- require 'rake/rdoctask'
36
- Rake::RDocTask.new do |rdoc|
37
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
38
-
39
- rdoc.rdoc_dir = 'rdoc'
40
- rdoc.title = "MailCatcher #{version}"
41
- rdoc.rdoc_files.include('README*')
42
- rdoc.rdoc_files.include('lib/*.rb')
43
- rdoc.rdoc_files.include('lib/**/*.rb')
44
- end
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.2.4
@@ -1,35 +0,0 @@
1
- # mailcatchered: Start mailcatcher in a screen session
2
- mailcatcherd() {
3
- local check=`screen -list|grep mailcatcher|awk '{print $1}'`
4
- if [[ "$1" = "" ]]; then
5
-
6
- local hostname=""
7
-
8
- if [[ $hostname = "" ]]; then
9
- hostname='localhost'
10
- fi
11
-
12
-
13
- if [[ "${check}" = "" ]]; then
14
- screen -dmS mailcatcher mailcatcher
15
- echo "Started mailcatcher in background"
16
- open "http://${hostname}:1080"
17
- else
18
- echo "mailcatcher is running in background..."
19
-
20
- read -ep "re-attach(y/n) or run(r)?" choice
21
- if [[ $choice = [yY] ]]; then
22
- echo "Attaching mailcatcher session..."
23
- screen -r $check
24
- elif [[ $choice = [rR] ]]; then
25
- open "http://${hostname}:1080"
26
- else
27
- echo "mailcatcher currently running at ${check}"
28
- fi
29
- fi
30
- else
31
- if [[ $1 -eq "load" ]]; then
32
- screen -r mailcatcher
33
- fi
34
- fi
35
- }