mail 2.3.3 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of mail might be problematic. Click here for more details.

Files changed (38) hide show
  1. data/CHANGELOG.rdoc +26 -0
  2. data/CONTRIBUTING.md +45 -0
  3. data/Gemfile +2 -8
  4. data/Gemfile.lock +31 -0
  5. data/README.md +649 -0
  6. data/Rakefile +4 -30
  7. data/lib/VERSION +2 -2
  8. data/lib/mail.rb +3 -1
  9. data/lib/mail/attachments_list.rb +2 -3
  10. data/lib/mail/body.rb +1 -2
  11. data/lib/mail/configuration.rb +3 -3
  12. data/lib/mail/core_extensions/nil.rb +4 -0
  13. data/lib/mail/core_extensions/shellwords.rb +57 -0
  14. data/lib/mail/core_extensions/string.rb +6 -4
  15. data/lib/mail/core_extensions/string/access.rb +41 -0
  16. data/lib/mail/encodings.rb +8 -8
  17. data/lib/mail/field.rb +2 -0
  18. data/lib/mail/fields/common/parameter_hash.rb +1 -1
  19. data/lib/mail/fields/unstructured_field.rb +1 -1
  20. data/lib/mail/matchers/has_sent_mail.rb +124 -0
  21. data/lib/mail/message.rb +72 -19
  22. data/lib/mail/multibyte/chars.rb +2 -2
  23. data/lib/mail/multibyte/utils.rb +1 -1
  24. data/lib/mail/network/delivery_methods/exim.rb +5 -38
  25. data/lib/mail/network/delivery_methods/file_delivery.rb +2 -2
  26. data/lib/mail/network/delivery_methods/sendmail.rb +3 -3
  27. data/lib/mail/network/delivery_methods/smtp.rb +20 -4
  28. data/lib/mail/network/retriever_methods/imap.rb +4 -2
  29. data/lib/mail/parsers/rfc2822.treetop +1 -1
  30. data/lib/mail/parsers/rfc2822_obsolete.rb +14 -3
  31. data/lib/mail/parsers/rfc2822_obsolete.treetop +2 -2
  32. data/lib/mail/parts_list.rb +4 -0
  33. data/lib/mail/utilities.rb +6 -2
  34. data/lib/mail/version_specific/ruby_1_8.rb +1 -1
  35. data/lib/mail/version_specific/ruby_1_9.rb +8 -4
  36. metadata +18 -12
  37. data/README.rdoc +0 -563
  38. data/lib/mail/core_extensions/shell_escape.rb +0 -56
data/Rakefile CHANGED
@@ -21,9 +21,8 @@ end
21
21
 
22
22
  require File.expand_path('../spec/environment', __FILE__)
23
23
 
24
- require 'rake/rdoctask'
25
24
  require 'rake/testtask'
26
- require 'spec/rake/spectask'
25
+ require 'rspec/core/rake_task'
27
26
 
28
27
  desc "Build a gem file"
29
28
  task :build do
@@ -32,34 +31,9 @@ end
32
31
 
33
32
  task :default => :spec
34
33
 
35
- Spec::Rake::SpecTask.new(:rcov) do |t|
36
- t.spec_files = FileList['test/**/tc_*.rb', 'spec/**/*_spec.rb']
37
- t.rcov = true
38
- t.rcov_opts = t.rcov_opts << ['--exclude', '/Library,/opt,/System,/usr']
39
- end
40
-
41
- Spec::Rake::SpecTask.new(:spec) do |t|
42
- t.warning = true
43
- t.spec_files = FileList["#{File.dirname(__FILE__)}/spec/**/*_spec.rb"]
44
- t.spec_opts = %w(--backtrace --diff --color)
45
- t.libs << "#{File.dirname(__FILE__)}/spec"
46
- t.libs << "#{File.dirname(__FILE__)}/spec/mail"
47
- end
48
-
49
- Rake::RDocTask.new(:rdoc) do |rdoc|
50
- rdoc.rdoc_dir = 'rdoc'
51
- rdoc.title = 'Mail - A Ruby Mail Library'
52
-
53
- rdoc.options << '-c' << 'utf-8'
54
- rdoc.options << '--line-numbers'
55
- rdoc.options << '--inline-source'
56
- rdoc.options << '-m' << 'README.rdoc'
57
-
58
- rdoc.rdoc_files.include('README.rdoc')
59
- rdoc.rdoc_files.include('lib/**/*.rb')
60
- rdoc.rdoc_files.include('lib/network/**/*.rb')
61
- rdoc.rdoc_files.exclude('lib/parsers/*')
62
-
34
+ RSpec::Core::RakeTask.new(:spec) do |t|
35
+ t.ruby_opts = '-w'
36
+ t.rspec_opts = %w(--backtrace --color)
63
37
  end
64
38
 
65
39
  # load custom rake tasks
data/lib/VERSION CHANGED
@@ -1,4 +1,4 @@
1
1
  major:2
2
- minor:3
3
- patch:3
2
+ minor:4
3
+ patch:0
4
4
  build:
data/lib/mail.rb CHANGED
@@ -29,7 +29,7 @@ module Mail # :doc:
29
29
  require 'mail/core_extensions/nil'
30
30
  require 'mail/core_extensions/object'
31
31
  require 'mail/core_extensions/string'
32
- require 'mail/core_extensions/shell_escape'
32
+ require 'mail/core_extensions/shellwords' unless String.new.respond_to?(:shellescape)
33
33
  require 'mail/core_extensions/smtp' if RUBY_VERSION < '1.9.3'
34
34
  require 'mail/indifferent_hash'
35
35
 
@@ -84,6 +84,8 @@ module Mail # :doc:
84
84
  require 'mail/encodings/base64'
85
85
  require 'mail/encodings/quoted_printable'
86
86
 
87
+ require 'mail/matchers/has_sent_mail'
88
+
87
89
  # Finally... require all the Mail.methods
88
90
  require 'mail/mail'
89
91
  end
@@ -94,10 +94,9 @@ module Mail
94
94
  def set_mime_type(filename)
95
95
  # Have to do this because MIME::Types is not Ruby 1.9 safe yet
96
96
  if RUBY_VERSION >= '1.9'
97
- new_file = String.new(filename).force_encoding(Encoding::BINARY)
98
- ext = new_file.split('.'.force_encoding(Encoding::BINARY)).last
99
- filename = "file.#{ext}".force_encoding('US-ASCII')
97
+ filename = filename.encode(Encoding::UTF_8) if filename.respond_to?(:encode)
100
98
  end
99
+
101
100
  @mime_type = MIME::Types.type_for(filename).first
102
101
  end
103
102
 
data/lib/mail/body.rb CHANGED
@@ -267,8 +267,7 @@ module Mail
267
267
  end
268
268
 
269
269
  def only_us_ascii?
270
- raw_source.each_byte {|b| return false if (b == 0 || b > 127)}
271
- true
270
+ !(raw_source =~ /[^\x01-\x7f]/)
272
271
  end
273
272
 
274
273
  def empty?
@@ -5,10 +5,10 @@
5
5
  require 'singleton'
6
6
 
7
7
  module Mail
8
-
8
+
9
9
  # The Configuration class is a Singleton used to hold the default
10
10
  # configuration for all Mail objects.
11
- #
11
+ #
12
12
  # Each new mail object gets a copy of these values at initialization
13
13
  # which can be overwritten on a per mail object basis.
14
14
  class Configuration
@@ -50,7 +50,7 @@ module Mail
50
50
  return @retriever_method if @retriever_method && method.nil?
51
51
  @retriever_method = lookup_retriever_method(method).new(settings)
52
52
  end
53
-
53
+
54
54
  def lookup_retriever_method(method)
55
55
  case method
56
56
  when nil
@@ -3,6 +3,10 @@
3
3
  # This is not loaded if ActiveSupport is already loaded
4
4
 
5
5
  class NilClass #:nodoc:
6
+ def blank?
7
+ true
8
+ end
9
+
6
10
  def to_crlf
7
11
  ''
8
12
  end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ # The following is imported from ruby 1.9.2 shellwords.rb
4
+ #
5
+ module Shellwords
6
+ # Escapes a string so that it can be safely used in a Bourne shell
7
+ # command line.
8
+ #
9
+ # Note that a resulted string should be used unquoted and is not
10
+ # intended for use in double quotes nor in single quotes.
11
+ #
12
+ # open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
13
+ # # ...
14
+ # }
15
+ #
16
+ # +String#shellescape+ is a shorthand for this function.
17
+ #
18
+ # open("| grep #{pattern.shellescape} file") { |pipe|
19
+ # # ...
20
+ # }
21
+ #
22
+ def shellescape(str)
23
+ # An empty argument will be skipped, so return empty quotes.
24
+ return "''" if str.empty?
25
+
26
+ str = str.dup
27
+
28
+ # Process as a single byte sequence because not all shell
29
+ # implementations are multibyte aware.
30
+ str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
31
+
32
+ # A LF cannot be escaped with a backslash because a backslash + LF
33
+ # combo is regarded as line continuation and simply ignored.
34
+ str.gsub!(/\n/, "'\n'")
35
+
36
+ return str
37
+ end
38
+
39
+ module_function :shellescape
40
+
41
+ class << self
42
+ alias escape shellescape
43
+ end
44
+
45
+ end
46
+
47
+ class String
48
+ # call-seq:
49
+ # str.shellescape => string
50
+ #
51
+ # Escapes +str+ so that it can be safely used in a Bourne shell
52
+ # command line. See +Shellwords::shellescape+ for details.
53
+ #
54
+ def shellescape
55
+ Shellwords.escape(self)
56
+ end
57
+ end
@@ -1,15 +1,17 @@
1
1
  # encoding: utf-8
2
2
  class String #:nodoc:
3
3
  def to_crlf
4
- gsub(/\n|\r\n|\r/) { "\r\n" }
4
+ to_str.gsub(/\n|\r\n|\r/) { "\r\n" }
5
5
  end
6
6
 
7
7
  def to_lf
8
- gsub(/\n|\r\n|\r/) { "\n" }
8
+ to_str.gsub(/\n|\r\n|\r/) { "\n" }
9
9
  end
10
10
 
11
- def blank?
12
- self !~ /\S/
11
+ unless String.instance_methods(false).map {|m| m.to_sym}.include?(:blank?)
12
+ def blank?
13
+ self !~ /\S/
14
+ end
13
15
  end
14
16
 
15
17
  unless method_defined?(:ascii_only?)
@@ -101,4 +101,45 @@ class String
101
101
  end
102
102
  end
103
103
  end
104
+
105
+ if Module.method(:const_get).arity == 1
106
+ # Tries to find a constant with the name specified in the argument string:
107
+ #
108
+ # "Module".constantize # => Module
109
+ # "Test::Unit".constantize # => Test::Unit
110
+ #
111
+ # The name is assumed to be the one of a top-level constant, no matter whether
112
+ # it starts with "::" or not. No lexical context is taken into account:
113
+ #
114
+ # C = 'outside'
115
+ # module M
116
+ # C = 'inside'
117
+ # C # => 'inside'
118
+ # "C".constantize # => 'outside', same as ::C
119
+ # end
120
+ #
121
+ # NameError is raised when the name is not in CamelCase or the constant is
122
+ # unknown.
123
+ def constantize
124
+ names = self.split('::')
125
+ names.shift if names.empty? || names.first.empty?
126
+
127
+ constant = Object
128
+ names.each do |name|
129
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
130
+ end
131
+ constant
132
+ end
133
+ else
134
+ def constantize #:nodoc:
135
+ names = self.split('::')
136
+ names.shift if names.empty? || names.first.empty?
137
+
138
+ constant = Object
139
+ names.each do |name|
140
+ constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
141
+ end
142
+ constant
143
+ end
144
+ end
104
145
  end
@@ -181,7 +181,7 @@ module Mail
181
181
  return address if address.ascii_only? or charset.nil?
182
182
  us_ascii = %Q{\x00-\x7f}
183
183
  # Encode any non usascii strings embedded inside of quotes
184
- address.gsub!(/(".*?[^#{us_ascii}].+?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
184
+ address.gsub!(/(".*?[^#{us_ascii}].*?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
185
185
  # Then loop through all remaining items and encode as needed
186
186
  tokens = address.split(/\s/)
187
187
  map_with_index(tokens) do |word, i|
@@ -204,9 +204,9 @@ module Mail
204
204
  #
205
205
  # Encodings.b_value_encode('This is あ string', 'UTF-8')
206
206
  # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
207
- def Encodings.b_value_encode(str, encoding = nil)
208
- return str if str.to_s.ascii_only?
209
- string, encoding = RubyVer.b_value_encode(str, encoding)
207
+ def Encodings.b_value_encode(encoded_str, encoding = nil)
208
+ return encoded_str if encoded_str.to_s.ascii_only?
209
+ string, encoding = RubyVer.b_value_encode(encoded_str, encoding)
210
210
  map_lines(string) do |str|
211
211
  "=?#{encoding}?B?#{str.chomp}?="
212
212
  end.join(" ")
@@ -219,9 +219,9 @@ module Mail
219
219
  #
220
220
  # Encodings.q_value_encode('This is あ string', 'UTF-8')
221
221
  # #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
222
- def Encodings.q_value_encode(str, encoding = nil)
223
- return str if str.to_s.ascii_only?
224
- string, encoding = RubyVer.q_value_encode(str, encoding)
222
+ def Encodings.q_value_encode(encoded_str, encoding = nil)
223
+ return encoded_str if encoded_str.to_s.ascii_only?
224
+ string, encoding = RubyVer.q_value_encode(encoded_str, encoding)
225
225
  string.gsub!("=\r\n", '') # We already have limited the string to the length we want
226
226
  map_lines(string) do |str|
227
227
  "=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
@@ -247,7 +247,7 @@ module Mail
247
247
  # Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
248
248
  # #=> 'This is あ string'
249
249
  def Encodings.q_value_decode(str)
250
- RubyVer.q_value_decode(str).gsub(/_/, ' ')
250
+ RubyVer.q_value_decode(str)
251
251
  end
252
252
 
253
253
  def Encodings.split_encoding_from_string( str )
data/lib/mail/field.rb CHANGED
@@ -113,6 +113,8 @@ module Mail
113
113
  match_to_s(other.name, field.name)
114
114
  end
115
115
 
116
+ alias_method :==, :same
117
+
116
118
  def <=>( other )
117
119
  self_order = FIELD_ORDER.rindex(self.name.to_s.downcase) || 100
118
120
  other_order = FIELD_ORDER.rindex(other.name.to_s.downcase) || 100
@@ -29,7 +29,7 @@ module Mail
29
29
  super(exact || key_name)
30
30
  else # Dealing with a multiple value pair or a single encoded value pair
31
31
  string = pairs.sort { |a,b| a.first.to_s <=> b.first.to_s }.map { |v| v.last }.join('')
32
- if mt = string.match(/([\w\d\-]+)'(\w\w)'(.*)/)
32
+ if mt = string.match(/([\w\-]+)'(\w\w)'(.*)/)
33
33
  string = mt[3]
34
34
  encoding = mt[1]
35
35
  else
@@ -166,7 +166,7 @@ module Mail
166
166
  end
167
167
 
168
168
  def encode(value)
169
- value.encode!(charset) if defined?(Encoding) && charset
169
+ value.encode!(charset) if charset && value.respond_to?(:encode!)
170
170
  (value.not_ascii_only? ? [value].pack("M").gsub("=\n", '') : value).gsub("\r", "=0D").gsub("\n", "=0A")
171
171
  end
172
172
 
@@ -0,0 +1,124 @@
1
+ module Mail
2
+ module Matchers
3
+ def have_sent_email
4
+ HasSentEmailMatcher.new(self)
5
+ end
6
+
7
+ class HasSentEmailMatcher
8
+ def initialize(_context)
9
+ end
10
+
11
+ def matches?(subject)
12
+ matching_deliveries = filter_matched_deliveries(Mail::TestMailer.deliveries)
13
+ !(matching_deliveries.empty?)
14
+ end
15
+
16
+ def from(sender)
17
+ @sender = sender
18
+ self
19
+ end
20
+
21
+ def to(recipient_or_list)
22
+ @recipients ||= []
23
+
24
+ if recipient_or_list.kind_of?(Array)
25
+ @recipients += recipient_or_list
26
+ else
27
+ @recipients << recipient_or_list
28
+ end
29
+ self
30
+ end
31
+
32
+ def with_subject(subject)
33
+ @subject = subject
34
+ self
35
+ end
36
+
37
+ def matching_subject(subject_matcher)
38
+ @subject_matcher = subject_matcher
39
+ self
40
+ end
41
+
42
+ def with_body(body)
43
+ @body = body
44
+ self
45
+ end
46
+
47
+ def matching_body(body_matcher)
48
+ @body_matcher = body_matcher
49
+ self
50
+ end
51
+
52
+ def description
53
+ result = "send a matching email"
54
+ result
55
+ end
56
+
57
+ def failure_message
58
+ result = "Expected email to be sent "
59
+ result += explain_expectations
60
+ result += dump_deliveries
61
+ result
62
+ end
63
+
64
+ def negative_failure_message
65
+ result = "Expected no email to be sent "
66
+ result += explain_expectations
67
+ result += dump_deliveries
68
+ result
69
+ end
70
+
71
+ protected
72
+
73
+ def filter_matched_deliveries(deliveries)
74
+ candidate_deliveries = deliveries
75
+
76
+ %w(sender recipients subject subject_matcher body body_matcher).each do |modifier_name|
77
+ next unless instance_variable_defined?("@#{modifier_name}")
78
+ candidate_deliveries = candidate_deliveries.select{|matching_delivery| self.send("matches_on_#{modifier_name}?", matching_delivery)}
79
+ end
80
+
81
+ candidate_deliveries
82
+ end
83
+
84
+ def matches_on_sender?(delivery)
85
+ delivery.from.include?(@sender)
86
+ end
87
+
88
+ def matches_on_recipients?(delivery)
89
+ @recipients.all? {|recipient| delivery.to.include?(recipient) }
90
+ end
91
+
92
+ def matches_on_subject?(delivery)
93
+ delivery.subject == @subject
94
+ end
95
+
96
+ def matches_on_subject_matcher?(delivery)
97
+ @subject_matcher.match delivery.subject
98
+ end
99
+
100
+ def matches_on_body?(delivery)
101
+ delivery.body == @body
102
+ end
103
+
104
+ def matches_on_body_matcher?(delivery)
105
+ @body_matcher.match delivery.body.raw_source
106
+ end
107
+
108
+ def explain_expectations
109
+ result = ''
110
+ result += "from #{@sender} " if instance_variable_defined?('@sender')
111
+ result += "to #{@recipients.inspect} " if instance_variable_defined?('@recipients')
112
+ result += "with subject \"#{@subject}\" " if instance_variable_defined?('@subject')
113
+ result += "with subject matching \"#{@subject_matcher}\" " if instance_variable_defined?('@subject_matcher')
114
+ result += "with body \"#{@body}\" " if instance_variable_defined?('@body')
115
+ result += "with body matching \"#{@body_matcher}\" " if instance_variable_defined?('@body_matcher')
116
+ result
117
+ end
118
+
119
+ def dump_deliveries
120
+ "(actual deliveries: " + Mail::TestMailer.deliveries.inspect + ")"
121
+ end
122
+ end
123
+ end
124
+ end