mail 1.1.0 → 1.2.1

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 (50) hide show
  1. data/CHANGELOG.rdoc +14 -1
  2. data/README.rdoc +37 -13
  3. data/lib/mail/configuration.rb +4 -0
  4. data/lib/mail/elements/address.rb +11 -0
  5. data/lib/mail/encodings/base64.rb +1 -0
  6. data/lib/mail/encodings/encodings.rb +133 -6
  7. data/lib/mail/encodings/quoted_printable.rb +1 -1
  8. data/lib/mail/field.rb +34 -34
  9. data/lib/mail/fields/bcc_field.rb +10 -1
  10. data/lib/mail/fields/cc_field.rb +10 -1
  11. data/lib/mail/fields/comments_field.rb +2 -1
  12. data/lib/mail/fields/common/common_address.rb +25 -1
  13. data/lib/mail/fields/common/common_date.rb +8 -5
  14. data/lib/mail/fields/common/common_field.rb +2 -74
  15. data/lib/mail/fields/common/common_message_id.rb +10 -0
  16. data/lib/mail/fields/common/parameter_hash.rb +10 -0
  17. data/lib/mail/fields/content_description_field.rb +3 -2
  18. data/lib/mail/fields/content_disposition_field.rb +12 -2
  19. data/lib/mail/fields/content_id_field.rb +12 -2
  20. data/lib/mail/fields/content_location_field.rb +12 -2
  21. data/lib/mail/fields/content_transfer_encoding_field.rb +12 -2
  22. data/lib/mail/fields/content_type_field.rb +26 -5
  23. data/lib/mail/fields/date_field.rb +11 -2
  24. data/lib/mail/fields/from_field.rb +10 -1
  25. data/lib/mail/fields/in_reply_to_field.rb +10 -1
  26. data/lib/mail/fields/keywords_field.rb +10 -1
  27. data/lib/mail/fields/message_id_field.rb +11 -2
  28. data/lib/mail/fields/mime_version_field.rb +11 -2
  29. data/lib/mail/fields/received_field.rb +10 -1
  30. data/lib/mail/fields/references_field.rb +10 -1
  31. data/lib/mail/fields/reply_to_field.rb +10 -1
  32. data/lib/mail/fields/resent_bcc_field.rb +10 -1
  33. data/lib/mail/fields/resent_cc_field.rb +10 -1
  34. data/lib/mail/fields/resent_date_field.rb +10 -1
  35. data/lib/mail/fields/resent_from_field.rb +10 -1
  36. data/lib/mail/fields/resent_message_id_field.rb +10 -1
  37. data/lib/mail/fields/resent_sender_field.rb +10 -1
  38. data/lib/mail/fields/resent_to_field.rb +10 -1
  39. data/lib/mail/fields/return_path_field.rb +10 -1
  40. data/lib/mail/fields/sender_field.rb +10 -1
  41. data/lib/mail/fields/subject_field.rb +2 -1
  42. data/lib/mail/fields/to_field.rb +10 -1
  43. data/lib/mail/fields/unstructured_field.rb +82 -0
  44. data/lib/mail/header.rb +2 -2
  45. data/lib/mail/message.rb +25 -9
  46. data/lib/mail/utilities.rb +7 -1
  47. data/lib/mail/version.rb +2 -2
  48. data/lib/mail/version_specific/ruby_1_8.rb +30 -10
  49. data/lib/mail/version_specific/ruby_1_9.rb +30 -8
  50. metadata +1 -1
data/CHANGELOG.rdoc CHANGED
@@ -1,6 +1,19 @@
1
+ == Wed Nov 4 23:24:32 UTC 2009 Mikel Lindsaar <raasdnil@gmail.com>
2
+
3
+ * Created commit 2b5d608: Closes Issue #2 - Empty header field values not parsing <mikel>
4
+ * Version bumb to 1.2.1
5
+
6
+ == Wed Nov 4 12:54:43 UTC 2009 Mikel Lindsaar <raasdnil@gmail.com>
7
+
8
+ * Renamed Mail::Message.encode! to Mail::Message.ready_to_send!, deprecated :encode! <mikel>
9
+ * Rewrote encoding and decoding methods on all classes. Adds a lot of boiler plate code, but allows us to
10
+ be really precise in each field type that needs custom encoding. Now all encoding is done by the field_type
11
+ itself. Need to follow through on the body. <mikel>
12
+ * Bump version to 1.2.0 due to changes of :encoded, :decoded behaviour <mikel>
13
+
1
14
  == Tue Nov 3 00:59:45 UTC 2009 Mikel Lindsaar <raasdnil@gmail.com>
2
15
 
3
- * Tested mail against entire Enron set (2.3gb) and the Trec 2005 set (0.5gb), ~ half a million emails without crashing
16
+ * Tested mail against entire Enron set (2.3gb) and the Trec 2005 set (0.5gb), ~ half a million emails without crashing <jlindley>
4
17
  * Some headers only can appear once, enforce during header parse assignment. <jlindley>
5
18
  * Convert empty bodies into empty arrays instead of nil. <jlindley>
6
19
  * Handle blank content dispositions. <jlindley>
data/README.rdoc CHANGED
@@ -49,7 +49,8 @@ and you can manually create any other type of MIME email.
49
49
 
50
50
  Next TODO:
51
51
 
52
- * Add multilingual MIME support for mixed ASCII and multibyte headers
52
+ * Improve MIME support for character sets in headers, currently works, mostly, needs
53
+ refinement.
53
54
  * Add IMAP wrapper
54
55
 
55
56
  == Testing Policy
@@ -64,13 +65,10 @@ It also means you can be sure Mail will behave correctly.
64
65
 
65
66
  == API Policy
66
67
 
67
- Right now Mail is still under development for a 1.0.0 release.
68
+ No API removals within a single point release. All removals to be depreciated with
69
+ warnings for at least one MINOR point release before removal.
68
70
 
69
- The API will change between now and 1.0.0. How much? Not sure. Basically though
70
- once 1.0.0 is released, no API removals within a single point release. All removals
71
- to be depreciated with warnings for at least one point release before removal.
72
-
73
- Also, all private or protected methods to be declared as such.
71
+ Also, all private or protected methods to be declared as such - though this is still I/P.
74
72
 
75
73
  == Installation
76
74
 
@@ -89,7 +87,34 @@ may or may not be a problem for you.
89
87
 
90
88
  If you want to install mail manually, you can download the gem from github and do:
91
89
 
92
- # gem install mail-1.0.0.gem
90
+ # gem install mail-1.2.1.gem
91
+
92
+ == Encodings
93
+
94
+ If you didn't know, handling encodings in Emails is not as straight forward as you
95
+ would hope.
96
+
97
+ I have tried to simplify it some:
98
+
99
+ 1. All objects that can render into an email, have an :encoded method. Encoded will
100
+ return the object as a complete string ready to send in the mail system, that is,
101
+ it will include the header field and value and CRLF at the end and wrapped as
102
+ needed.
103
+
104
+ 2. All objects that can render into an email, have a :decoded method. Decoded will
105
+ return the object's "value" only as a string. This means it will not include
106
+ the header fields (like 'To:' or 'Subject:').
107
+
108
+ 3. By default, calling :to_s on an object will call it's encoded method, that is, make
109
+ it ready to send in an email.
110
+
111
+ 4. Structured fields that have parameter values that can be encoded (e.g. Content-Type) will
112
+ provide decoded parameter values when you call the parameter names as methods against
113
+ the object.
114
+
115
+ 5. Structured fields that have parameter values that can be encoded (e.g. Content-Type) will
116
+ provide encoded parameter values when you call the parameter names through the
117
+ object.parameters['<parameter_name>'] method call.
93
118
 
94
119
  == Contributing
95
120
 
@@ -417,11 +442,10 @@ Of course... Mail will round trip an attachment as well
417
442
 
418
443
  == Excerpts from TREC Spam Corpus 2005
419
444
 
420
- The spec fixture files in spec/fixtures/emails/from_trec_2005
421
- are from the 2005 TREC Public Spam Corpus. They remain copyrighted
422
- under the terms of that project and license agreement. They are used
423
- in this project to verify and describe the development of this
424
- email parser implementation.
445
+ The spec fixture files in spec/fixtures/emails/from_trec_2005 are from the
446
+ 2005 TREC Public Spam Corpus. They remain copyrighted under the terms of
447
+ that project and license agreement. They are used in this project to verify
448
+ and describe the development of this email parser implementation.
425
449
 
426
450
  http://plg.uwaterloo.ca/~gvcormac/treccorpus/
427
451
 
@@ -85,6 +85,10 @@ module Mail
85
85
  @tls || false
86
86
  end
87
87
 
88
+ def param_encode_language(value = nil)
89
+ value ? @encode_language = value : @encode_language ||= 'en'
90
+ end
91
+
88
92
  end
89
93
 
90
94
  end
@@ -82,6 +82,7 @@ module Mail
82
82
  def display_name
83
83
  parse unless @parsed
84
84
  @display_name ||= get_display_name
85
+ Encodings.decode_encode(@display_name, @output_type) if @display_name
85
86
  end
86
87
 
87
88
  # Provides a way to assign a display name to an already made Mail::Address object.
@@ -154,6 +155,16 @@ module Mail
154
155
  parse unless @parsed
155
156
  "#<#{self.class}:#{self.object_id} Address: |#{to_s}| >"
156
157
  end
158
+
159
+ def encoded
160
+ @output_type = :encode
161
+ format
162
+ end
163
+
164
+ def decoded
165
+ @output_type = :decode
166
+ format
167
+ end
157
168
 
158
169
  private
159
170
 
@@ -12,6 +12,7 @@ module Mail
12
12
  def self.encode(str)
13
13
  RubyVer.encode_base64( str )
14
14
  end
15
+
15
16
  end
16
17
  end
17
18
  end
@@ -2,26 +2,153 @@
2
2
  module Mail
3
3
  module Encodings
4
4
 
5
+ include Mail::Patterns
6
+
7
+ # Is the encoding we want defined?
8
+ #
9
+ # Example:
10
+ #
11
+ # Encodings.defined?(:base64) #=> true
5
12
  def Encodings.defined?( str )
6
13
  string = str.to_s.split(/[_-]/).map { |v| v.capitalize }.join('')
7
14
  RubyVer.has_constant?(Mail::Encodings, string)
8
15
  end
9
16
 
17
+ # Gets a defined encoding type, QuotedPrintable or Base64 for now.
18
+ #
19
+ # Each encoding needs to be defined as a Mail::Encodings::ClassName for
20
+ # this to work, allows us to add other encodings in the future.
21
+ #
22
+ # Example:
23
+ #
24
+ # Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
10
25
  def Encodings.get_encoding( str )
11
26
  string = str.to_s.split(/[_-]/).map { |v| v.capitalize }.join('')
12
27
  RubyVer.get_constant(Mail::Encodings, string)
13
28
  end
29
+
30
+ # Encodes a parameter value using URI Escaping, note the language field 'en' can
31
+ # be set using Mail::Configuration, like so:
32
+ #
33
+ # Mail.defaults.do
34
+ # param_encode_language 'jp'
35
+ # end
36
+ #
37
+ # The character set used for encoding will either be the value of $KCODE for
38
+ # Ruby < 1.9 or the encoding on the string passed in.
39
+ #
40
+ # Example:
41
+ #
42
+ # Mail::Encodings.param_encode("This is fun") #=> "us-ascii'en'This%20is%20fun"
43
+ def Encodings.param_encode(str)
44
+ RubyVer.param_encode(str)
45
+ end
46
+
47
+ # Decodes a parameter value using URI Escaping.
48
+ #
49
+ # Example:
50
+ #
51
+ # Mail::Encodings.param_decode("This%20is%20fun", 'us-ascii') #=> "This is fun"
52
+ #
53
+ # str = Mail::Encodings.param_decode("This%20is%20fun", 'iso-8559-1')
54
+ # str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
55
+ # str #=> "This is fun"
56
+ def Encodings.param_decode(str, encoding)
57
+ RubyVer.param_decode(str, encoding)
58
+ end
14
59
 
15
- def Encodings.b_encode(str, encoding = nil)
16
- RubyVer.b_encode(str, encoding)
60
+ # Decodes or encodes a string as needed for either Base64 or QP encoding types in
61
+ # the =?<encoding>?[QB]?<string>?=" format.
62
+ #
63
+ # The output type needs to be :decode to decode the input string or :encode to
64
+ # encode the input string. The character set used for encoding will either be
65
+ # the value of $KCODE for Ruby < 1.9 or the encoding on the string passed in.
66
+ #
67
+ # On encoding, will only send out Base64 encoded strings.
68
+ def Encodings.decode_encode(str, output_type)
69
+ case
70
+ when output_type == :decode
71
+ Encodings.value_decode(str)
72
+ else
73
+ if str.ascii_only?
74
+ str
75
+ else
76
+ Encodings.b_value_encode(str, find_encoding(str))
77
+ end
78
+ end
17
79
  end
18
80
 
19
- def Encodings.q_encode(str, encoding = nil)
20
- RubyVer.q_encode(str, encoding)
81
+ # Decodes a given string as Base64 or Quoted Printable, depending on what
82
+ # type it is.
83
+ #
84
+ #
85
+ def Encodings.value_decode(str)
86
+ case
87
+ when str =~ /\=\?.+?\?B\?.+?\?\=/
88
+ Encodings.b_value_decode(str)
89
+ when str =~ /\=\?.+?\?Q\?.+?\?\=/
90
+ Encodings.q_value_decode(str)
91
+ else
92
+ str
93
+ end
94
+ end
95
+
96
+ # Encode a string with Base64 Encoding and returns it ready to be inserted
97
+ # as a value for a field, that is, in the =?<charset>?B?<string>?= format
98
+ #
99
+ # Example:
100
+ #
101
+ # Encodings.b_value_encode('This is あ string', 'UTF-8')
102
+ # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
103
+ def Encodings.b_value_encode(str, encoding = nil)
104
+ string, encoding = RubyVer.b_value_encode(str, encoding)
105
+ string.split("\n").map do |str|
106
+ "=?#{encoding}?B?#{str.chomp}?="
107
+ end.join(" ")
21
108
  end
22
109
 
23
- def Encodings.param_decode(str, encoding)
24
- RubyVer.param_decode(str, encoding)
110
+ # Decodes a Base64 string from the "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=" format
111
+ #
112
+ # Example:
113
+ #
114
+ # Encodings.b_value_encode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
115
+ # #=> 'This is あ string'
116
+ def Encodings.b_value_decode(str)
117
+ string = str.split(Mail::Patterns::WSP)
118
+ string.flatten.map do |s|
119
+ RubyVer.b_value_decode(s)
120
+ end.join('')
121
+ end
122
+
123
+ # Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
124
+ # as a value for a field, that is, in the =?<charset>?Q?<string>?= format
125
+ #
126
+ # Example:
127
+ #
128
+ # Encodings.q_value_encode('This is あ string', 'UTF-8')
129
+ # #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
130
+ def Encodings.q_value_encode(str, encoding = nil)
131
+ string, encoding = RubyVer.q_value_encode(str, encoding)
132
+ "=?#{encoding}?Q?#{string.chomp}?="
133
+ end
134
+
135
+ # Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
136
+ #
137
+ # Example:
138
+ #
139
+ # Encodings.b_value_encode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
140
+ # #=> 'This is あ string'
141
+ def Encodings.q_value_decode(str)
142
+ string = str.split(Mail::Patterns::WSP)
143
+ string.flatten.map do |s|
144
+ RubyVer.q_value_decode(s)
145
+ end.join('')
146
+ end
147
+
148
+ private
149
+
150
+ def Encodings.find_encoding(str)
151
+ RUBY_VERSION >= '1.9' ? str.encoding : $KCODE
25
152
  end
26
153
 
27
154
  end
@@ -5,7 +5,7 @@ module Mail
5
5
 
6
6
  # Decode the string from Quoted-Printable
7
7
  def self.decode(str)
8
- str.unpack("M*").first
8
+ str.unpack("M*").first.gsub('_', ' ')
9
9
  end
10
10
 
11
11
  def self.encode(str)
data/lib/mail/field.rb CHANGED
@@ -69,7 +69,7 @@ module Mail
69
69
  case
70
70
  when name =~ /:/ && value.blank? # Field.new("field-name: field data")
71
71
  name, value = split(name)
72
- create_field(name, value)
72
+ create_field(name, value.to_s)
73
73
  when name !~ /:/ && value.blank? # Field.new("field-name")
74
74
  create_field(name, nil)
75
75
  else # Field.new("field-name", "value")
@@ -111,8 +111,8 @@ module Mail
111
111
  end
112
112
 
113
113
  def <=>( other )
114
- self_order = FIELD_ORDER.rindex(self.name.downcase) || 100
115
- other_order = FIELD_ORDER.rindex(other.name.downcase) || 100
114
+ self_order = FIELD_ORDER.rindex(self.name.to_s.downcase) || 100
115
+ other_order = FIELD_ORDER.rindex(other.name.to_s.downcase) || 100
116
116
  self_order <=> other_order
117
117
  end
118
118
 
@@ -132,7 +132,7 @@ module Mail
132
132
  private
133
133
 
134
134
  def split(raw_field)
135
- match_data = raw_field.match(/^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})$/)
135
+ match_data = raw_field.match(/^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/)
136
136
  [match_data[1].to_s.strip, match_data[2].to_s.strip]
137
137
  rescue
138
138
  STDERR.puts "WARNING: Could not parse (and so ignorning) '#{raw_field}'"
@@ -149,64 +149,64 @@ module Mail
149
149
  def new_field(name, value)
150
150
  # Could do this with constantize and make it "as DRY as", but a simple case
151
151
  # statement is, well, simpler...
152
- case name.downcase
153
- when /^to$/
152
+ case name
153
+ when /^to$/i
154
154
  ToField.new(name, value)
155
- when /^cc$/
155
+ when /^cc$/i
156
156
  CcField.new(name, value)
157
- when /^bcc$/
157
+ when /^bcc$/i
158
158
  BccField.new(name, value)
159
- when /^message-id$/
159
+ when /^message-id$/i
160
160
  MessageIdField.new(name, value)
161
- when /^in-reply-to$/
161
+ when /^in-reply-to$/i
162
162
  InReplyToField.new(name, value)
163
- when /^references$/
163
+ when /^references$/i
164
164
  ReferencesField.new(name, value)
165
- when /^subject$/
165
+ when /^subject$/i
166
166
  SubjectField.new(name, value)
167
- when /^comments$/
167
+ when /^comments$/i
168
168
  CommentsField.new(name, value)
169
- when /^keywords$/
169
+ when /^keywords$/i
170
170
  KeywordsField.new(name, value)
171
- when /^date$/
171
+ when /^date$/i
172
172
  DateField.new(name, value)
173
- when /^from$/
173
+ when /^from$/i
174
174
  FromField.new(name, value)
175
- when /^sender$/
175
+ when /^sender$/i
176
176
  SenderField.new(name, value)
177
- when /^reply-to$/
177
+ when /^reply-to$/i
178
178
  ReplyToField.new(name, value)
179
- when /^resent-date$/
179
+ when /^resent-date$/i
180
180
  ResentDateField.new(name, value)
181
- when /^resent-from$/
181
+ when /^resent-from$/i
182
182
  ResentFromField.new(name, value)
183
- when /^resent-sender$/
183
+ when /^resent-sender$/i
184
184
  ResentSenderField.new(name, value)
185
- when /^resent-to$/
185
+ when /^resent-to$/i
186
186
  ResentToField.new(name, value)
187
- when /^resent-cc$/
187
+ when /^resent-cc$/i
188
188
  ResentCcField.new(name, value)
189
- when /^resent-bcc$/
189
+ when /^resent-bcc$/i
190
190
  ResentBccField.new(name, value)
191
- when /^resent-message-id$/
191
+ when /^resent-message-id$/i
192
192
  ResentMessageIdField.new(name, value)
193
- when /^return-path$/
193
+ when /^return-path$/i
194
194
  ReturnPathField.new(name, value)
195
- when /^received$/
195
+ when /^received$/i
196
196
  ReceivedField.new(name, value)
197
- when /^mime-version$/
197
+ when /^mime-version$/i
198
198
  MimeVersionField.new(name, value)
199
- when /^content-transfer-encoding$/
199
+ when /^content-transfer-encoding$/i
200
200
  ContentTransferEncodingField.new(name, value)
201
- when /^content-description$/
201
+ when /^content-description$/i
202
202
  ContentDescriptionField.new(name, value)
203
- when /^content-disposition$/
203
+ when /^content-disposition$/i
204
204
  ContentDispositionField.new(name, value)
205
- when /^content-type$/
205
+ when /^content-type$/i
206
206
  ContentTypeField.new(name, value)
207
- when /^content-id$/
207
+ when /^content-id$/i
208
208
  ContentIdField.new(name, value)
209
- when /^content-location$/
209
+ when /^content-location$/i
210
210
  ContentLocationField.new(name, value)
211
211
  else
212
212
  OptionalField.new(name, value)
@@ -31,9 +31,18 @@ module Mail
31
31
  include Mail::CommonAddress
32
32
 
33
33
  FIELD_NAME = 'bcc'
34
+ CAPITALIZED_FIELD = 'Bcc'
34
35
 
35
36
  def initialize(*args)
36
- super(FIELD_NAME, strip_field(FIELD_NAME, args.last))
37
+ super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, args.last))
38
+ end
39
+
40
+ def encoded
41
+ do_encode(CAPITALIZED_FIELD)
42
+ end
43
+
44
+ def decoded
45
+ do_decode
37
46
  end
38
47
 
39
48
  end