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/lib/mail/message.rb CHANGED
@@ -10,7 +10,7 @@ module Mail
10
10
  #
11
11
  # A Message object by default has the following objects inside it:
12
12
  #
13
- # * A Header object which contians all information and settings of the header of the email
13
+ # * A Header object which contains all information and settings of the header of the email
14
14
  # * Body object which contains all parts of the email that are not part of the header, this
15
15
  # includes any attachments, body text, MIME parts etc.
16
16
  #
@@ -100,7 +100,6 @@ module Mail
100
100
  def initialize(*args, &block)
101
101
  @body = nil
102
102
  @body_raw = nil
103
- @body_raw_index = nil
104
103
  @separate_parts = false
105
104
  @text_part = nil
106
105
  @html_part = nil
@@ -1134,7 +1133,7 @@ module Mail
1134
1133
  # mail.parts.length #=> 2
1135
1134
  # mail.parts.last.content_type.content_type #=> 'This is a body'
1136
1135
  def body=(value)
1137
- body_lazy(value, 0)
1136
+ body_lazy(value)
1138
1137
  end
1139
1138
 
1140
1139
  # Returns the body of the message object. Or, if passed
@@ -1711,19 +1710,54 @@ module Mail
1711
1710
  buffer
1712
1711
  end
1713
1712
 
1714
- def to_yaml
1715
- ready_to_send!
1713
+ def without_attachments!
1714
+ return self unless has_attachments?
1715
+
1716
+ parts.delete_if { |p| p.attachment? }
1717
+ body_raw = if parts.empty?
1718
+ ''
1719
+ else
1720
+ body.encoded
1721
+ end
1722
+
1723
+ @body = Mail::Body.new(body_raw)
1724
+
1725
+ self
1726
+ end
1727
+
1728
+ def to_yaml(opts = {})
1716
1729
  hash = {}
1730
+ hash['headers'] = {}
1717
1731
  header.fields.each do |field|
1718
- hash[field.name] = field.value
1732
+ hash['headers'][field.name] = field.value
1733
+ end
1734
+ hash['delivery_handler'] = delivery_handler.to_s if delivery_handler
1735
+ hash['transport_encoding'] = transport_encoding.to_s
1736
+ special_variables = [:@header, :@delivery_handler, :@transport_encoding]
1737
+ (instance_variables.map(&:to_sym) - special_variables).each do |var|
1738
+ hash[var.to_s] = instance_variable_get(var)
1719
1739
  end
1720
- hash['subject'] = subject
1721
- hash['body'] = body.encoded(content_transfer_encoding)
1722
- hash.to_yaml
1740
+ hash.to_yaml(opts)
1723
1741
  end
1724
1742
 
1725
1743
  def self.from_yaml(str)
1726
- from_hash(YAML::load(str))
1744
+ hash = YAML.load(str)
1745
+ m = Mail::Message.new(:headers => hash['headers'])
1746
+ hash.delete('headers')
1747
+ hash.each do |k,v|
1748
+ case
1749
+ when k == 'delivery_handler'
1750
+ begin
1751
+ m.delivery_handler = Object.const_get(v) unless v.blank?
1752
+ rescue NameError
1753
+ end
1754
+ when k == 'transport_encoding'
1755
+ m.transport_encoding(v)
1756
+ when k =~ /^@/
1757
+ m.instance_variable_set(k.to_sym, v)
1758
+ end
1759
+ end
1760
+ m
1727
1761
  end
1728
1762
 
1729
1763
  def self.from_hash(hash)
@@ -1740,6 +1774,8 @@ module Mail
1740
1774
 
1741
1775
  def decoded
1742
1776
  case
1777
+ when self.text?
1778
+ decode_body_as_text
1743
1779
  when self.attachment?
1744
1780
  decode_body
1745
1781
  when !self.multipart?
@@ -1814,6 +1850,10 @@ module Mail
1814
1850
  return @mark_for_delete
1815
1851
  end
1816
1852
 
1853
+ def text?
1854
+ has_content_type? ? !!(main_type =~ /^text$/i) : false
1855
+ end
1856
+
1817
1857
  private
1818
1858
 
1819
1859
  # 2.1. General Description
@@ -1839,31 +1879,28 @@ module Mail
1839
1879
  @raw_source = value.to_crlf
1840
1880
  end
1841
1881
 
1842
- # see comments to body=. We take data starting from index and process it lazily
1843
- def body_lazy(value, index)
1882
+ # see comments to body=. We take data and process it lazily
1883
+ def body_lazy(value)
1844
1884
  process_body_raw if @body_raw && value
1845
1885
  case
1846
- when value == nil || value.length<=index
1886
+ when value == nil || value.length<=0
1847
1887
  @body = Mail::Body.new('')
1848
1888
  @body_raw = nil
1849
- @body_raw_index = nil
1850
1889
  add_encoding_to_body
1851
1890
  when @body && @body.multipart?
1852
- @body << Mail::Part.new(value[index, value.length-index])
1891
+ @body << Mail::Part.new(value)
1853
1892
  add_encoding_to_body
1854
1893
  else
1855
1894
  @body_raw = value
1856
- @body_raw_index = index
1857
1895
  # process_body_raw
1858
1896
  end
1859
1897
  end
1860
1898
 
1861
1899
 
1862
1900
  def process_body_raw
1863
- @body = Mail::Body.new(@body_raw[@body_raw_index, @body_raw.length-@body_raw_index])
1901
+ @body = Mail::Body.new(@body_raw)
1864
1902
  @body_raw = nil
1865
- @body_raw_index = nil
1866
- separate_parts if @separate_parts
1903
+ separate_parts if @separate_parts
1867
1904
 
1868
1905
  add_encoding_to_body
1869
1906
  end
@@ -1993,5 +2030,21 @@ module Mail
1993
2030
  end
1994
2031
  end
1995
2032
 
2033
+ def decode_body_as_text
2034
+ body_text = decode_body
2035
+ if charset
2036
+ if RUBY_VERSION < '1.9'
2037
+ require 'iconv'
2038
+ return Iconv.conv("UTF-8//TRANSLIT//IGNORE", charset, body_text)
2039
+ else
2040
+ if encoding = Encoding.find(charset) rescue nil
2041
+ body_text.force_encoding(encoding)
2042
+ return body_text.encode(Encoding::UTF_8)
2043
+ end
2044
+ end
2045
+ end
2046
+ body_text
2047
+ end
2048
+
1996
2049
  end
1997
2050
  end
@@ -339,7 +339,7 @@ module Mail #:nodoc:
339
339
  # Example:
340
340
  # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
341
341
  def upcase
342
- chars(Unicode.apply_mapping @wrapped_string, :uppercase_mapping)
342
+ chars(Unicode.apply_mapping(@wrapped_string), :uppercase_mapping)
343
343
  end
344
344
 
345
345
  # Convert characters in the string to lowercase.
@@ -347,7 +347,7 @@ module Mail #:nodoc:
347
347
  # Example:
348
348
  # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
349
349
  def downcase
350
- chars(Unicode.apply_mapping @wrapped_string, :lowercase_mapping)
350
+ chars(Unicode.apply_mapping(@wrapped_string), :lowercase_mapping)
351
351
  end
352
352
 
353
353
  # Converts the first character to uppercase and the remainder to lowercase.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Mail #:nodoc:
4
4
  module Multibyte #:nodoc:
5
- if Kernel.const_defined?(:Encoding)
5
+ if RUBY_VERSION >= "1.9"
6
6
  # Returns a regular expression that matches valid characters in the current encoding
7
7
  def self.valid_character
8
8
  VALID_CHARACTER[Encoding.default_external.to_s]
@@ -1,45 +1,12 @@
1
1
  module Mail
2
2
 
3
- # A delivery method implementation which sends via exim.
4
- #
5
- # To use this, first find out where the exim binary is on your computer,
6
- # if you are on a mac or unix box, it is usually in /usr/sbin/exim, this will
7
- # be your exim location.
8
- #
9
- # Mail.defaults do
10
- # delivery_method :exim
11
- # end
12
- #
13
- # Or if your exim binary is not at '/usr/sbin/exim'
14
- #
15
- # Mail.defaults do
16
- # delivery_method :exim, :location => '/absolute/path/to/your/exim'
17
- # end
18
- #
19
- # Then just deliver the email as normal:
20
- #
21
- # Mail.deliver do
22
- # to 'mikel@test.lindsaar.net'
23
- # from 'ada@test.lindsaar.net'
24
- # subject 'testing exim'
25
- # body 'testing exim'
26
- # end
27
- #
28
- # Or by calling deliver on a Mail message
29
- #
30
- # mail = Mail.new do
31
- # to 'mikel@test.lindsaar.net'
32
- # from 'ada@test.lindsaar.net'
33
- # subject 'testing exim'
34
- # body 'testing exim'
35
- # end
36
- #
37
- # mail.deliver!
38
3
  class Exim < Sendmail
39
4
 
40
- def initialize(values)
41
- self.settings = { :location => '/usr/sbin/exim',
42
- :arguments => '-i -t' }.merge(values)
5
+ def deliver!(mail)
6
+ envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
7
+ return_path = "-f \"#{envelope_from.to_s.shellescape}\"" if envelope_from
8
+ arguments = [settings[:arguments], return_path].compact.join(" ")
9
+ self.class.call(settings[:location], arguments, mail)
43
10
  end
44
11
 
45
12
  def self.call(path, arguments, mail)
@@ -6,7 +6,7 @@ module Mail
6
6
  # So if you have an email going to fred@test, bob@test, joe@anothertest, and you
7
7
  # set your location path to /path/to/mails then FileDelivery will create the directory
8
8
  # if it does not exist, and put one copy of the email in three files, called
9
- # by their message id
9
+ # "fred@test", "bob@test" and "joe@anothertest"
10
10
  #
11
11
  # Make sure the path you specify with :location is writable by the Ruby process
12
12
  # running Mail.
@@ -32,7 +32,7 @@ module Mail
32
32
  end
33
33
 
34
34
  mail.destinations.uniq.each do |to|
35
- ::File.open(::File.join(settings[:location], File.basename(to.to_s)), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
35
+ ::File.open(::File.join(settings[:location], to), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
36
36
  end
37
37
  end
38
38
 
@@ -45,14 +45,14 @@ module Mail
45
45
 
46
46
  def deliver!(mail)
47
47
  envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
48
- return_path = "-f " + '"' + envelope_from.escape_for_shell + '"' if envelope_from
48
+ return_path = "-f \"#{envelope_from.to_s.gsub('"', '\"')}\"" if envelope_from
49
49
 
50
50
  arguments = [settings[:arguments], return_path].compact.join(" ")
51
51
 
52
- self.class.call(settings[:location], arguments, mail.destinations.collect(&:escape_for_shell).join(" "), mail)
52
+ Sendmail.call(settings[:location], arguments, mail.destinations.collect(&:shellescape).join(" "), mail)
53
53
  end
54
54
 
55
- def self.call(path, arguments, destinations, mail)
55
+ def Sendmail.call(path, arguments, destinations, mail)
56
56
  IO.popen("#{path} #{arguments} #{destinations}", "w+") do |io|
57
57
  io.puts mail.encoded.to_lf
58
58
  io.flush
@@ -81,7 +81,9 @@ module Mail
81
81
  :password => nil,
82
82
  :authentication => nil,
83
83
  :enable_starttls_auto => true,
84
- :openssl_verify_mode => nil
84
+ :openssl_verify_mode => nil,
85
+ :ssl => nil,
86
+ :tls => nil
85
87
  }.merge!(values)
86
88
  end
87
89
 
@@ -108,7 +110,21 @@ module Mail
108
110
  end
109
111
 
110
112
  smtp = Net::SMTP.new(settings[:address], settings[:port])
111
- if settings[:enable_starttls_auto]
113
+ if settings[:tls] || settings[:ssl]
114
+ if smtp.respond_to?(:enable_tls)
115
+ unless settings[:openssl_verify_mode]
116
+ smtp.enable_tls
117
+ else
118
+ openssl_verify_mode = settings[:openssl_verify_mode]
119
+ if openssl_verify_mode.kind_of?(String)
120
+ openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{openssl_verify_mode.upcase}".constantize
121
+ end
122
+ context = Net::SMTP.default_ssl_context
123
+ context.verify_mode = openssl_verify_mode
124
+ smtp.enable_tls(context)
125
+ end
126
+ end
127
+ elsif settings[:enable_starttls_auto]
112
128
  if smtp.respond_to?(:enable_starttls_auto)
113
129
  unless settings[:openssl_verify_mode]
114
130
  smtp.enable_starttls_auto
@@ -125,8 +141,8 @@ module Mail
125
141
  end
126
142
 
127
143
  response = nil
128
- smtp.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do |smtp|
129
- response = smtp.sendmail(message, envelope_from, destinations)
144
+ smtp.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do |smtp_obj|
145
+ response = smtp_obj.sendmail(message, envelope_from, destinations)
130
146
  end
131
147
 
132
148
  return settings[:return_response] ? response : self
@@ -54,6 +54,8 @@ module Mail
54
54
  # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
55
55
  # count: number of emails to retrieve. The default value is 10. A value of 1 returns an
56
56
  # instance of Message, not an array of Message instances.
57
+ # ready_only: will ensure that no writes are made to the inbox during the session.
58
+ # This is helpful when you don't want your messages to be set to read automatically. Default is false.
57
59
  # delete_after_find: flag for whether to delete each retreived email after find. Default
58
60
  # is false. Use #find_and_delete if you would like this to default to true.
59
61
  #
@@ -61,7 +63,7 @@ module Mail
61
63
  options = validate_options(options)
62
64
 
63
65
  start do |imap|
64
- imap.select(options[:mailbox])
66
+ options[:read_only] ? imap.select(options[:mailbox]) : imap.examine(options[:mailbox])
65
67
 
66
68
  message_ids = imap.uid_search(options[:keys])
67
69
  message_ids.reverse! if options[:what].to_sym == :last
@@ -101,7 +103,6 @@ module Mail
101
103
  mailbox = Net::IMAP.encode_utf7(mailbox)
102
104
 
103
105
  start do |imap|
104
- imap.select(mailbox)
105
106
  imap.uid_search(['ALL']).each do |message_id|
106
107
  imap.uid_store(message_id, "+FLAGS", [Net::IMAP::DELETED])
107
108
  end
@@ -130,6 +131,7 @@ module Mail
130
131
  options[:keys] ||= 'ALL'
131
132
  options[:delete_after_find] ||= false
132
133
  options[:mailbox] = Net::IMAP.encode_utf7(options[:mailbox])
134
+ options[:read_only] ||= false
133
135
 
134
136
  options
135
137
  end
@@ -110,7 +110,7 @@ module Mail
110
110
  end
111
111
 
112
112
  rule local_dot_atom_text
113
- ("."* domain_text)+
113
+ ("."? domain_text)+
114
114
  end
115
115
 
116
116
  rule domain_text
@@ -257,8 +257,19 @@ module Mail
257
257
  if r3
258
258
  r1 = r3
259
259
  else
260
- @index = i1
261
- r1 = nil
260
+ if has_terminal?("@", false, index)
261
+ r4 = instantiate_node(SyntaxNode,input, index...(index + 1))
262
+ @index += 1
263
+ else
264
+ terminal_parse_failure("@")
265
+ r4 = nil
266
+ end
267
+ if r4
268
+ r1 = r4
269
+ else
270
+ @index = i1
271
+ r1 = nil
272
+ end
262
273
  end
263
274
  end
264
275
  if r1
@@ -3754,4 +3765,4 @@ module Mail
3754
3765
  include RFC2822Obsolete
3755
3766
  end
3756
3767
 
3757
- end
3768
+ end
@@ -20,7 +20,7 @@ module Mail
20
20
  end
21
21
 
22
22
  rule obs_phrase
23
- (word / ".")+
23
+ (word / "." / "@")+
24
24
  end
25
25
 
26
26
  rule obs_phrase_list
@@ -238,4 +238,4 @@ module Mail
238
238
  field_name WSP* ":" unstructured CRLF
239
239
  end
240
240
  end
241
- end
241
+ end
@@ -26,6 +26,10 @@ module Mail
26
26
  raise NoMethodError, "#collect! is not defined, please call #collect and create a new PartsList"
27
27
  end
28
28
 
29
+ def sort
30
+ self.class.new(super)
31
+ end
32
+
29
33
  def sort!(order)
30
34
  sorted = self.sort do |a, b|
31
35
  # OK, 10000 is arbitrary... if anyone actually wants to explicitly sort 10000 parts of a
@@ -116,11 +116,15 @@ module Mail
116
116
  end
117
117
 
118
118
  def uri_escape( str )
119
- URI.escape(str)
119
+ uri_parser.escape(str)
120
120
  end
121
121
 
122
122
  def uri_unescape( str )
123
- URI.unescape(str)
123
+ uri_parser.unescape(str)
124
+ end
125
+
126
+ def uri_parser
127
+ @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
124
128
  end
125
129
 
126
130
  # Matches two objects with their to_s values case insensitively
@@ -80,7 +80,7 @@ module Mail
80
80
  match = str.match(/\=\?(.+)?\?[Qq]\?(.+)?\?\=/m)
81
81
  if match
82
82
  encoding = match[1]
83
- str = Encodings::QuotedPrintable.decode(match[2])
83
+ str = Encodings::QuotedPrintable.decode(match[2].gsub(/_/, '=20'))
84
84
  end
85
85
  str
86
86
  end