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.
- data/CHANGELOG.rdoc +26 -0
- data/CONTRIBUTING.md +45 -0
- data/Gemfile +2 -8
- data/Gemfile.lock +31 -0
- data/README.md +649 -0
- data/Rakefile +4 -30
- data/lib/VERSION +2 -2
- data/lib/mail.rb +3 -1
- data/lib/mail/attachments_list.rb +2 -3
- data/lib/mail/body.rb +1 -2
- data/lib/mail/configuration.rb +3 -3
- data/lib/mail/core_extensions/nil.rb +4 -0
- data/lib/mail/core_extensions/shellwords.rb +57 -0
- data/lib/mail/core_extensions/string.rb +6 -4
- data/lib/mail/core_extensions/string/access.rb +41 -0
- data/lib/mail/encodings.rb +8 -8
- data/lib/mail/field.rb +2 -0
- data/lib/mail/fields/common/parameter_hash.rb +1 -1
- data/lib/mail/fields/unstructured_field.rb +1 -1
- data/lib/mail/matchers/has_sent_mail.rb +124 -0
- data/lib/mail/message.rb +72 -19
- data/lib/mail/multibyte/chars.rb +2 -2
- data/lib/mail/multibyte/utils.rb +1 -1
- data/lib/mail/network/delivery_methods/exim.rb +5 -38
- data/lib/mail/network/delivery_methods/file_delivery.rb +2 -2
- data/lib/mail/network/delivery_methods/sendmail.rb +3 -3
- data/lib/mail/network/delivery_methods/smtp.rb +20 -4
- data/lib/mail/network/retriever_methods/imap.rb +4 -2
- data/lib/mail/parsers/rfc2822.treetop +1 -1
- data/lib/mail/parsers/rfc2822_obsolete.rb +14 -3
- data/lib/mail/parsers/rfc2822_obsolete.treetop +2 -2
- data/lib/mail/parts_list.rb +4 -0
- data/lib/mail/utilities.rb +6 -2
- data/lib/mail/version_specific/ruby_1_8.rb +1 -1
- data/lib/mail/version_specific/ruby_1_9.rb +8 -4
- metadata +18 -12
- data/README.rdoc +0 -563
- 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 '
|
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
|
-
|
36
|
-
t.
|
37
|
-
t.
|
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
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/
|
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
|
-
|
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
data/lib/mail/configuration.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
12
|
-
|
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
|
data/lib/mail/encodings.rb
CHANGED
@@ -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}]
|
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(
|
208
|
-
return
|
209
|
-
string, encoding = RubyVer.b_value_encode(
|
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(
|
223
|
-
return
|
224
|
-
string, encoding = RubyVer.q_value_encode(
|
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)
|
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
|
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
|
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
|