mail 2.1.5.3 → 2.2.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 (60) hide show
  1. data/CHANGELOG.rdoc +16 -0
  2. data/Rakefile +1 -1
  3. data/lib/mail.rb +3 -1
  4. data/lib/mail/body.rb +3 -2
  5. data/lib/mail/core_extensions/string.rb +4 -0
  6. data/lib/mail/elements/address.rb +3 -3
  7. data/lib/mail/elements/content_transfer_encoding_element.rb +1 -1
  8. data/lib/mail/encodings.rb +48 -8
  9. data/lib/mail/encodings/quoted_printable.rb +1 -14
  10. data/lib/mail/field.rb +44 -44
  11. data/lib/mail/fields/bcc_field.rb +3 -2
  12. data/lib/mail/fields/cc_field.rb +3 -2
  13. data/lib/mail/fields/comments_field.rb +3 -3
  14. data/lib/mail/fields/common/common_address.rb +19 -10
  15. data/lib/mail/fields/common/common_field.rb +6 -6
  16. data/lib/mail/fields/common/common_message_id.rb +1 -1
  17. data/lib/mail/fields/content_description_field.rb +3 -3
  18. data/lib/mail/fields/content_disposition_field.rb +3 -3
  19. data/lib/mail/fields/content_id_field.rb +6 -6
  20. data/lib/mail/fields/content_location_field.rb +3 -3
  21. data/lib/mail/fields/content_transfer_encoding_field.rb +6 -3
  22. data/lib/mail/fields/content_type_field.rb +8 -7
  23. data/lib/mail/fields/date_field.rb +8 -8
  24. data/lib/mail/fields/from_field.rb +3 -3
  25. data/lib/mail/fields/in_reply_to_field.rb +3 -2
  26. data/lib/mail/fields/keywords_field.rb +3 -2
  27. data/lib/mail/fields/message_id_field.rb +4 -3
  28. data/lib/mail/fields/mime_version_field.rb +5 -6
  29. data/lib/mail/fields/received_field.rb +3 -2
  30. data/lib/mail/fields/references_field.rb +3 -3
  31. data/lib/mail/fields/reply_to_field.rb +3 -3
  32. data/lib/mail/fields/resent_bcc_field.rb +3 -3
  33. data/lib/mail/fields/resent_cc_field.rb +3 -3
  34. data/lib/mail/fields/resent_date_field.rb +7 -7
  35. data/lib/mail/fields/resent_from_field.rb +3 -3
  36. data/lib/mail/fields/resent_message_id_field.rb +3 -3
  37. data/lib/mail/fields/resent_sender_field.rb +3 -3
  38. data/lib/mail/fields/resent_to_field.rb +3 -3
  39. data/lib/mail/fields/return_path_field.rb +3 -3
  40. data/lib/mail/fields/sender_field.rb +3 -3
  41. data/lib/mail/fields/structured_field.rb +12 -3
  42. data/lib/mail/fields/subject_field.rb +3 -2
  43. data/lib/mail/fields/to_field.rb +3 -3
  44. data/lib/mail/fields/unstructured_field.rb +68 -26
  45. data/lib/mail/header.rb +20 -4
  46. data/lib/mail/message.rb +36 -26
  47. data/lib/mail/parsers/content_disposition.rb +1 -1
  48. data/lib/mail/parsers/content_disposition.treetop +1 -1
  49. data/lib/mail/parsers/content_transfer_encoding.rb +1 -8
  50. data/lib/mail/parsers/content_transfer_encoding.treetop +1 -1
  51. data/lib/mail/parsers/content_type.rb +1 -1
  52. data/lib/mail/parsers/content_type.treetop +1 -1
  53. data/lib/mail/parsers/rfc2045.rb +6 -6
  54. data/lib/mail/parsers/rfc2045.treetop +1 -1
  55. data/lib/mail/parsers/rfc2822.rb +236 -236
  56. data/lib/mail/parsers/rfc2822.treetop +12 -12
  57. data/lib/mail/patterns.rb +6 -4
  58. data/lib/mail/utilities.rb +8 -5
  59. data/lib/mail/version.rb +3 -4
  60. metadata +16 -17
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,19 @@
1
+ == Sun Apr 11 07:49:15 UTC 2010 Mikel Lindsaar <raasdnil@gmail.com>
2
+
3
+ * Lots of updates on encoding and decoding of headers and unstructured fields. This
4
+ is now a lot cleaner and nicer. Also more predictable.
5
+ * Merged encoding branch back into head
6
+ * Version bump to 2.2.0
7
+ * Tagged 2.2.0
8
+
9
+ == Sun Apr 4 06:41:46 UTC 2010 Mikel Lindsaar <raasdnil@gmail.com>
10
+
11
+ * Created non-ascii header auto encoding for address fields and unstructured fields
12
+ * Changed default behaviour of mail, if you specify a charset, it will use that charset
13
+ regardless of what is in the body. Previously, if the body was all US-ASCII, it would
14
+ set the charset to US-ASCII in preference.
15
+ * Many internal version jumps from 2.1.5.3 => 2.1.5.8 - unreleased development versions
16
+
1
17
  == Mon 29 Mar 2010 07:04:34 UTC Mikel Lindsaar <raasdnil@gmail.com>
2
18
 
3
19
  * Version bump to 2.1.5.3
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ require 'cucumber/rake/task'
8
8
 
9
9
  spec = Gem::Specification.new do |s|
10
10
  s.name = "mail"
11
- s.version = "2.1.5.3"
11
+ s.version = "2.2.0"
12
12
  s.author = "Mike Lindsaar"
13
13
  s.email = "raasdnil@gmail.com"
14
14
  s.homepage = "http://github.com/mikel/mail"
data/lib/mail.rb CHANGED
@@ -6,6 +6,7 @@ module Mail # :doc:
6
6
  require 'active_support'
7
7
  require 'active_support/core_ext/hash/indifferent_access'
8
8
  require 'active_support/core_ext/object/blank'
9
+ require 'active_support/core_ext/string'
9
10
 
10
11
  require 'uri'
11
12
  require 'net/smtp'
@@ -54,6 +55,7 @@ module Mail # :doc:
54
55
  date_time received message_ids envelope_from rfc2045
55
56
  mime_version content_type content_disposition
56
57
  content_transfer_encoding content_location ]
58
+
57
59
  parsers.each do |parser|
58
60
  begin
59
61
  # Try requiring the pre-compiled ruby version first
@@ -63,7 +65,7 @@ module Mail # :doc:
63
65
  # Otherwise, get treetop to compile and load it
64
66
  require 'treetop/runtime'
65
67
  require 'treetop/compiler'
66
- Treetop.load("mail/parsers/#{parser}")
68
+ Treetop.load(File.join(File.dirname(__FILE__)) + "/mail/parsers/#{parser}")
67
69
  end
68
70
  end
69
71
 
data/lib/mail/body.rb CHANGED
@@ -30,6 +30,7 @@ module Mail
30
30
  @boundary = nil
31
31
  @preamble = nil
32
32
  @epilogue = nil
33
+ @charset = nil
33
34
  @part_sort_order = [ "text/plain", "text/enriched", "text/html" ]
34
35
  @parts = Mail::PartsList.new
35
36
  if string.blank?
@@ -125,8 +126,8 @@ module Mail
125
126
  end
126
127
 
127
128
  def get_best_encoding(target)
128
- te = Mail::Encodings.get_encoding(target)
129
- te.get_best_compatible(encoding, raw_source)
129
+ target_encoding = Mail::Encodings.get_encoding(target)
130
+ target_encoding.get_best_compatible(encoding, raw_source)
130
131
  end
131
132
 
132
133
  # Returns a body encoded using transfer_encoding. Multipart always uses an
@@ -16,6 +16,10 @@ class String #:nodoc:
16
16
  !(self =~ /[^#{US_ASCII_REGEXP}]/)
17
17
  end
18
18
  end
19
+
20
+ def not_ascii_only?
21
+ !ascii_only?
22
+ end
19
23
 
20
24
  unless method_defined?(:bytesize)
21
25
  alias :bytesize :length
@@ -84,7 +84,7 @@ module Mail
84
84
  def display_name
85
85
  parse unless @parsed
86
86
  @display_name ||= get_display_name
87
- Encodings.decode_encode(@display_name, @output_type) if @display_name
87
+ Encodings.decode_encode(@display_name.to_s, @output_type) if @display_name
88
88
  end
89
89
 
90
90
  # Provides a way to assign a display name to an already made Mail::Address object.
@@ -217,7 +217,7 @@ module Mail
217
217
 
218
218
  def get_comments
219
219
  if tree.respond_to?(:comments)
220
- @comments ||= tree.comments.map { |c| unparen(c.text_value) }
220
+ @comments = tree.comments.map { |c| unparen(c.text_value.to_str) }
221
221
  else
222
222
  @comments = []
223
223
  end
@@ -226,7 +226,7 @@ module Mail
226
226
  def get_display_name
227
227
  if tree.respond_to?(:display_name)
228
228
  name = unquote(tree.display_name.text_value.strip)
229
- str = strip_all_comments(name)
229
+ str = strip_all_comments(name.to_s)
230
230
  elsif comments
231
231
  if domain
232
232
  str = strip_domain_comments(format_comments)
@@ -9,7 +9,7 @@ module Mail
9
9
  case
10
10
  when string.blank?
11
11
  @encoding = ''
12
- when tree = parser.parse(string.downcase)
12
+ when tree = parser.parse(string.to_s.downcase)
13
13
  @encoding = tree.encoding.text_value
14
14
  else
15
15
  raise Mail::Field::ParseError, "ContentTransferEncodingElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
@@ -8,6 +8,7 @@ module Mail
8
8
  module Encodings
9
9
 
10
10
  include Mail::Patterns
11
+ extend Mail::Utilities
11
12
 
12
13
  @transfer_encodings = {}
13
14
 
@@ -106,21 +107,25 @@ module Mail
106
107
  end
107
108
  end
108
109
  end
109
-
110
+
110
111
  # Decodes a given string as Base64 or Quoted Printable, depending on what
111
112
  # type it is.
112
113
  #
113
114
  # String has to be of the format =?<encoding>?[QB]?<string>?=
114
115
  def Encodings.value_decode(str)
115
- str.gsub!(/\?=(\s*)=\?/, '?==?') # Remove whitespaces between 'encoded-word's
116
- str.gsub(/(.*?)(=\?.*?\?.\?.*?\?=)|$/m) do
116
+ str = str.gsub(/\?=(\s*)=\?/, '?==?') # Remove whitespaces between 'encoded-word's
117
+
118
+ # Join QP encoded-words that are adjacent to avoid decoding partial chars
119
+ str.gsub!( /=\?==\?.+?\?[Qq]\?/m, '' ) if str =~ /\?==\?/
120
+
121
+ str.gsub(/(.*?)(=\?.*?\?.\?.*?\?=)|$/m) do # Grab the insides of each encoded-word
117
122
  before = $1.to_s
118
123
  text = $2.to_s
119
-
124
+
120
125
  case
121
- when text =~ /=\?.+\?[Bb]\?/m
126
+ when text.to_str =~ /=\?.+\?[Bb]\?/m
122
127
  before + b_value_decode(text)
123
- when text =~ /=\?.+\?[Qq]\?/m
128
+ when text.to_str =~ /=\?.+\?[Qq]\?/m
124
129
  before + q_value_decode(text)
125
130
  else
126
131
  before + text
@@ -154,6 +159,36 @@ module Mail
154
159
  end
155
160
  end
156
161
 
162
+ def Encodings.address_encode(address, charset = 'utf-8')
163
+ if address.is_a?(Array)
164
+ # loop back through for each element
165
+ address.map { |a| Encodings.address_encode(a, charset) }.join(", ")
166
+ else
167
+ # find any word boundary that is not ascii and encode it
168
+ encode_non_usascii(address, charset)
169
+ end
170
+ end
171
+
172
+ def Encodings.encode_non_usascii(address, charset)
173
+ return address if address.ascii_only?
174
+ us_ascii = %Q{\x00-\x7f}
175
+ # Encode any non usascii strings embedded inside of quotes
176
+ address.gsub!(/(".*?[^#{us_ascii}].+?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
177
+ # Then loop through all remaining items and encode as needed
178
+ tokens = address.split(/\s/)
179
+ tokens.each_with_index.map do |word, i|
180
+ if word.ascii_only?
181
+ word
182
+ else
183
+ previous_non_ascii = tokens[i-1] && !tokens[i-1].ascii_only?
184
+ if previous_non_ascii
185
+ word = " #{word}"
186
+ end
187
+ Encodings.b_value_encode(word, charset)
188
+ end
189
+ end.join(' ')
190
+ end
191
+
157
192
  # Encode a string with Base64 Encoding and returns it ready to be inserted
158
193
  # as a value for a field, that is, in the =?<charset>?B?<string>?= format
159
194
  #
@@ -162,8 +197,9 @@ module Mail
162
197
  # Encodings.b_value_encode('This is あ string', 'UTF-8')
163
198
  # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
164
199
  def Encodings.b_value_encode(str, encoding = nil)
200
+ return str if str.to_s.ascii_only?
165
201
  string, encoding = RubyVer.b_value_encode(str, encoding)
166
- string.split("\n").map do |str|
202
+ string.each_line.map do |str|
167
203
  "=?#{encoding}?B?#{str.chomp}?="
168
204
  end.join(" ")
169
205
  end
@@ -176,8 +212,12 @@ module Mail
176
212
  # Encodings.q_value_encode('This is あ string', 'UTF-8')
177
213
  # #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
178
214
  def Encodings.q_value_encode(str, encoding = nil)
215
+ return str if str.to_s.ascii_only?
179
216
  string, encoding = RubyVer.q_value_encode(str, encoding)
180
- "=?#{encoding}?Q?#{string.chomp.gsub(/ /, '_')}?="
217
+ string.gsub!("=\r\n=", '=') # We already have limited the string to the length we want
218
+ string.each_line.map do |str|
219
+ "=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
220
+ end.join(" ")
181
221
  end
182
222
 
183
223
  private
@@ -18,9 +18,7 @@ module Mail
18
18
  end
19
19
 
20
20
  def self.encode(str)
21
- l = []
22
- str.each_line{|line| l << qp_encode_line(line)}
23
- l.join("\r\n")
21
+ [str].pack("M").gsub(/\n/, "\r\n")
24
22
  end
25
23
 
26
24
  def self.cost(str)
@@ -33,17 +31,6 @@ module Mail
33
31
  end
34
32
 
35
33
  private
36
- def self.qp_encode_line(str)
37
- str.chomp.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }
38
- end
39
-
40
- # Convert the given character to quoted printable format, taking into
41
- # account multi-byte characters (if executing with $KCODE="u", for instance)
42
- def self.quoted_printable_encode(character)
43
- result = ""
44
- character.each_byte { |b| result << "=%02X" % b }
45
- result
46
- end
47
34
 
48
35
  Encodings.register(NAME, self)
49
36
  end
data/lib/mail/field.rb CHANGED
@@ -67,15 +67,15 @@ module Mail
67
67
  # parameters:
68
68
  #
69
69
  # Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
70
- def initialize(name, value = nil)
70
+ def initialize(name, value = nil, charset = 'utf-8')
71
71
  case
72
72
  when name =~ /:/ && value.blank? # Field.new("field-name: field data")
73
73
  name, value = split(name)
74
- create_field(name, value)
74
+ create_field(name, value, charset)
75
75
  when name !~ /:/ && value.blank? # Field.new("field-name")
76
- create_field(name, nil)
76
+ create_field(name, nil, charset)
77
77
  else # Field.new("field-name", "value")
78
- create_field(name, value)
78
+ create_field(name, value, charset)
79
79
  end
80
80
  return self
81
81
  end
@@ -96,8 +96,8 @@ module Mail
96
96
  field.value
97
97
  end
98
98
 
99
- def value=(str)
100
- create_field(name, str)
99
+ def value=(val)
100
+ create_field(name, val, charset)
101
101
  end
102
102
 
103
103
  def to_s
@@ -105,7 +105,7 @@ module Mail
105
105
  end
106
106
 
107
107
  def update(name, value)
108
- create_field(name, value)
108
+ create_field(name, value, charset)
109
109
  end
110
110
 
111
111
  def same( other )
@@ -134,86 +134,86 @@ module Mail
134
134
  private
135
135
 
136
136
  def split(raw_field)
137
- match_data = raw_field.match(/^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/)
138
- [match_data[1].to_s.strip, match_data[2].to_s.strip]
137
+ match_data = raw_field.mb_chars.match(/^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/)
138
+ [match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip]
139
139
  rescue
140
140
  STDERR.puts "WARNING: Could not parse (and so ignorning) '#{raw_field}'"
141
141
  end
142
-
143
- def create_field(name, value)
142
+
143
+ def create_field(name, value, charset)
144
144
  begin
145
- self.field = new_field(name, value)
146
- rescue => e
145
+ self.field = new_field(name, value, charset)
146
+ rescue Mail::Field::ParseError => e
147
147
  self.field = Mail::UnstructuredField.new(name, value)
148
148
  self.field.errors << [name, value, e]
149
149
  self.field
150
150
  end
151
151
  end
152
152
 
153
- def new_field(name, value)
153
+ def new_field(name, value, charset)
154
154
  # Could do this with constantize and make it "as DRY as", but a simple case
155
155
  # statement is, well, simpler...
156
156
  case name.to_s.downcase
157
157
  when /^to$/i
158
- ToField.new(name, value)
158
+ ToField.new(value, charset)
159
159
  when /^cc$/i
160
- CcField.new(name, value)
160
+ CcField.new(value, charset)
161
161
  when /^bcc$/i
162
- BccField.new(name, value)
162
+ BccField.new(value, charset)
163
163
  when /^message-id$/i
164
- MessageIdField.new(name, value)
164
+ MessageIdField.new(value, charset)
165
165
  when /^in-reply-to$/i
166
- InReplyToField.new(name, value)
166
+ InReplyToField.new(value, charset)
167
167
  when /^references$/i
168
- ReferencesField.new(name, value)
168
+ ReferencesField.new(value, charset)
169
169
  when /^subject$/i
170
- SubjectField.new(name, value)
170
+ SubjectField.new(value, charset)
171
171
  when /^comments$/i
172
- CommentsField.new(name, value)
172
+ CommentsField.new(value, charset)
173
173
  when /^keywords$/i
174
- KeywordsField.new(name, value)
174
+ KeywordsField.new(value, charset)
175
175
  when /^date$/i
176
- DateField.new(name, value)
176
+ DateField.new(value, charset)
177
177
  when /^from$/i
178
- FromField.new(name, value)
178
+ FromField.new(value, charset)
179
179
  when /^sender$/i
180
- SenderField.new(name, value)
180
+ SenderField.new(value, charset)
181
181
  when /^reply-to$/i
182
- ReplyToField.new(name, value)
182
+ ReplyToField.new(value, charset)
183
183
  when /^resent-date$/i
184
- ResentDateField.new(name, value)
184
+ ResentDateField.new(value, charset)
185
185
  when /^resent-from$/i
186
- ResentFromField.new(name, value)
186
+ ResentFromField.new(value, charset)
187
187
  when /^resent-sender$/i
188
- ResentSenderField.new(name, value)
188
+ ResentSenderField.new(value, charset)
189
189
  when /^resent-to$/i
190
- ResentToField.new(name, value)
190
+ ResentToField.new(value, charset)
191
191
  when /^resent-cc$/i
192
- ResentCcField.new(name, value)
192
+ ResentCcField.new(value, charset)
193
193
  when /^resent-bcc$/i
194
- ResentBccField.new(name, value)
194
+ ResentBccField.new(value, charset)
195
195
  when /^resent-message-id$/i
196
- ResentMessageIdField.new(name, value)
196
+ ResentMessageIdField.new(value, charset)
197
197
  when /^return-path$/i
198
- ReturnPathField.new(name, value)
198
+ ReturnPathField.new(value, charset)
199
199
  when /^received$/i
200
- ReceivedField.new(name, value)
200
+ ReceivedField.new(value, charset)
201
201
  when /^mime-version$/i
202
- MimeVersionField.new(name, value)
202
+ MimeVersionField.new(value, charset)
203
203
  when /^content-transfer-encoding$/i
204
- ContentTransferEncodingField.new(name, value)
204
+ ContentTransferEncodingField.new(value, charset)
205
205
  when /^content-description$/i
206
- ContentDescriptionField.new(name, value)
206
+ ContentDescriptionField.new(value, charset)
207
207
  when /^content-disposition$/i
208
- ContentDispositionField.new(name, value)
208
+ ContentDispositionField.new(value, charset)
209
209
  when /^content-type$/i
210
- ContentTypeField.new(name, value)
210
+ ContentTypeField.new(value, charset)
211
211
  when /^content-id$/i
212
- ContentIdField.new(name, value)
212
+ ContentIdField.new(value, charset)
213
213
  when /^content-location$/i
214
- ContentLocationField.new(name, value)
214
+ ContentLocationField.new(value, charset)
215
215
  else
216
- OptionalField.new(name, value)
216
+ OptionalField.new(name, value, charset)
217
217
  end
218
218
 
219
219
  end
@@ -36,8 +36,9 @@ module Mail
36
36
  FIELD_NAME = 'bcc'
37
37
  CAPITALIZED_FIELD = 'Bcc'
38
38
 
39
- def initialize(*args)
40
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, args.last))
39
+ def initialize(value = '', charset = 'utf-8')
40
+ @charset = charset
41
+ super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
41
42
  self.parse
42
43
  self
43
44
  end
@@ -36,8 +36,9 @@ module Mail
36
36
  FIELD_NAME = 'cc'
37
37
  CAPITALIZED_FIELD = 'Cc'
38
38
 
39
- def initialize(*args)
40
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, args.last))
39
+ def initialize(value = nil, charset = 'utf-8')
40
+ self.charset = charset
41
+ super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
41
42
  self.parse
42
43
  self
43
44
  end
@@ -30,11 +30,11 @@ module Mail
30
30
  FIELD_NAME = 'comments'
31
31
  CAPITALIZED_FIELD = 'Comments'
32
32
 
33
- def initialize(*args)
34
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, args.last))
33
+ def initialize(value = nil, charset = 'utf-8')
34
+ @charset = charset
35
+ super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value))
35
36
  self.parse
36
37
  self
37
-
38
38
  end
39
39
 
40
40
  end