mail 2.5.5 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +170 -108
  4. data/lib/mail/attachments_list.rb +13 -10
  5. data/lib/mail/body.rb +105 -91
  6. data/lib/mail/check_delivery_params.rb +30 -22
  7. data/lib/mail/configuration.rb +3 -0
  8. data/lib/mail/constants.rb +79 -0
  9. data/lib/mail/elements/address.rb +118 -174
  10. data/lib/mail/elements/address_list.rb +16 -56
  11. data/lib/mail/elements/content_disposition_element.rb +12 -22
  12. data/lib/mail/elements/content_location_element.rb +9 -17
  13. data/lib/mail/elements/content_transfer_encoding_element.rb +8 -19
  14. data/lib/mail/elements/content_type_element.rb +20 -30
  15. data/lib/mail/elements/date_time_element.rb +10 -21
  16. data/lib/mail/elements/envelope_from_element.rb +23 -31
  17. data/lib/mail/elements/message_ids_element.rb +22 -20
  18. data/lib/mail/elements/mime_version_element.rb +10 -21
  19. data/lib/mail/elements/phrase_list.rb +13 -15
  20. data/lib/mail/elements/received_element.rb +26 -21
  21. data/lib/mail/elements.rb +1 -0
  22. data/lib/mail/encodings/7bit.rb +10 -14
  23. data/lib/mail/encodings/8bit.rb +5 -18
  24. data/lib/mail/encodings/base64.rb +15 -10
  25. data/lib/mail/encodings/binary.rb +4 -22
  26. data/lib/mail/encodings/identity.rb +24 -0
  27. data/lib/mail/encodings/quoted_printable.rb +13 -7
  28. data/lib/mail/encodings/transfer_encoding.rb +47 -28
  29. data/lib/mail/encodings/unix_to_unix.rb +20 -0
  30. data/lib/mail/encodings.rb +102 -93
  31. data/lib/mail/envelope.rb +12 -19
  32. data/lib/mail/field.rb +143 -71
  33. data/lib/mail/field_list.rb +73 -19
  34. data/lib/mail/fields/bcc_field.rb +42 -48
  35. data/lib/mail/fields/cc_field.rb +29 -50
  36. data/lib/mail/fields/comments_field.rb +28 -37
  37. data/lib/mail/fields/common_address_field.rb +170 -0
  38. data/lib/mail/fields/common_date_field.rb +58 -0
  39. data/lib/mail/fields/common_field.rb +77 -0
  40. data/lib/mail/fields/common_message_id_field.rb +42 -0
  41. data/lib/mail/fields/content_description_field.rb +8 -14
  42. data/lib/mail/fields/content_disposition_field.rb +20 -44
  43. data/lib/mail/fields/content_id_field.rb +25 -51
  44. data/lib/mail/fields/content_location_field.rb +12 -25
  45. data/lib/mail/fields/content_transfer_encoding_field.rb +31 -36
  46. data/lib/mail/fields/content_type_field.rb +51 -80
  47. data/lib/mail/fields/date_field.rb +24 -52
  48. data/lib/mail/fields/from_field.rb +29 -50
  49. data/lib/mail/fields/in_reply_to_field.rb +39 -49
  50. data/lib/mail/fields/keywords_field.rb +19 -32
  51. data/lib/mail/fields/message_id_field.rb +26 -71
  52. data/lib/mail/fields/mime_version_field.rb +20 -30
  53. data/lib/mail/fields/named_structured_field.rb +11 -0
  54. data/lib/mail/fields/named_unstructured_field.rb +11 -0
  55. data/lib/mail/fields/optional_field.rb +10 -7
  56. data/lib/mail/fields/{common/parameter_hash.rb → parameter_hash.rb} +16 -13
  57. data/lib/mail/fields/received_field.rb +44 -57
  58. data/lib/mail/fields/references_field.rb +36 -49
  59. data/lib/mail/fields/reply_to_field.rb +29 -50
  60. data/lib/mail/fields/resent_bcc_field.rb +29 -50
  61. data/lib/mail/fields/resent_cc_field.rb +29 -50
  62. data/lib/mail/fields/resent_date_field.rb +6 -30
  63. data/lib/mail/fields/resent_from_field.rb +29 -50
  64. data/lib/mail/fields/resent_message_id_field.rb +6 -29
  65. data/lib/mail/fields/resent_sender_field.rb +28 -57
  66. data/lib/mail/fields/resent_to_field.rb +29 -50
  67. data/lib/mail/fields/return_path_field.rb +51 -55
  68. data/lib/mail/fields/sender_field.rb +35 -56
  69. data/lib/mail/fields/structured_field.rb +4 -30
  70. data/lib/mail/fields/subject_field.rb +10 -11
  71. data/lib/mail/fields/to_field.rb +29 -50
  72. data/lib/mail/fields/unstructured_field.rb +43 -51
  73. data/lib/mail/fields.rb +1 -0
  74. data/lib/mail/header.rb +78 -129
  75. data/lib/mail/indifferent_hash.rb +1 -0
  76. data/lib/mail/mail.rb +18 -11
  77. data/lib/mail/matchers/attachment_matchers.rb +44 -0
  78. data/lib/mail/matchers/has_sent_mail.rb +81 -4
  79. data/lib/mail/message.rb +142 -139
  80. data/lib/mail/multibyte/chars.rb +24 -180
  81. data/lib/mail/multibyte/unicode.rb +32 -27
  82. data/lib/mail/multibyte/utils.rb +27 -43
  83. data/lib/mail/multibyte.rb +56 -16
  84. data/lib/mail/network/delivery_methods/exim.rb +6 -4
  85. data/lib/mail/network/delivery_methods/file_delivery.rb +12 -10
  86. data/lib/mail/network/delivery_methods/logger_delivery.rb +34 -0
  87. data/lib/mail/network/delivery_methods/sendmail.rb +63 -21
  88. data/lib/mail/network/delivery_methods/smtp.rb +76 -50
  89. data/lib/mail/network/delivery_methods/smtp_connection.rb +4 -4
  90. data/lib/mail/network/delivery_methods/test_mailer.rb +5 -2
  91. data/lib/mail/network/retriever_methods/base.rb +9 -8
  92. data/lib/mail/network/retriever_methods/imap.rb +37 -18
  93. data/lib/mail/network/retriever_methods/pop3.rb +6 -3
  94. data/lib/mail/network/retriever_methods/test_retriever.rb +4 -2
  95. data/lib/mail/network.rb +2 -0
  96. data/lib/mail/parser_tools.rb +15 -0
  97. data/lib/mail/parsers/address_lists_parser.rb +33242 -0
  98. data/lib/mail/parsers/address_lists_parser.rl +179 -0
  99. data/lib/mail/parsers/content_disposition_parser.rb +901 -0
  100. data/lib/mail/parsers/content_disposition_parser.rl +89 -0
  101. data/lib/mail/parsers/content_location_parser.rb +822 -0
  102. data/lib/mail/parsers/content_location_parser.rl +78 -0
  103. data/lib/mail/parsers/content_transfer_encoding_parser.rb +522 -0
  104. data/lib/mail/parsers/content_transfer_encoding_parser.rl +71 -0
  105. data/lib/mail/parsers/content_type_parser.rb +1048 -0
  106. data/lib/mail/parsers/content_type_parser.rl +90 -0
  107. data/lib/mail/parsers/date_time_parser.rb +891 -0
  108. data/lib/mail/parsers/date_time_parser.rl +69 -0
  109. data/lib/mail/parsers/envelope_from_parser.rb +3675 -0
  110. data/lib/mail/parsers/envelope_from_parser.rl +89 -0
  111. data/lib/mail/parsers/message_ids_parser.rb +5161 -0
  112. data/lib/mail/parsers/message_ids_parser.rl +93 -0
  113. data/lib/mail/parsers/mime_version_parser.rb +513 -0
  114. data/lib/mail/parsers/mime_version_parser.rl +68 -0
  115. data/lib/mail/parsers/phrase_lists_parser.rb +884 -0
  116. data/lib/mail/parsers/phrase_lists_parser.rl +90 -0
  117. data/lib/mail/parsers/received_parser.rb +8782 -0
  118. data/lib/mail/parsers/received_parser.rl +91 -0
  119. data/lib/mail/parsers/rfc2045_content_transfer_encoding.rl +13 -0
  120. data/lib/mail/parsers/rfc2045_content_type.rl +25 -0
  121. data/lib/mail/parsers/rfc2045_mime.rl +16 -0
  122. data/lib/mail/parsers/rfc2183_content_disposition.rl +15 -0
  123. data/lib/mail/parsers/rfc3629_utf8.rl +19 -0
  124. data/lib/mail/parsers/rfc5234_abnf_core_rules.rl +22 -0
  125. data/lib/mail/parsers/rfc5322.rl +74 -0
  126. data/lib/mail/parsers/rfc5322_address.rl +72 -0
  127. data/lib/mail/parsers/rfc5322_date_time.rl +37 -0
  128. data/lib/mail/parsers/rfc5322_lexical_tokens.rl +60 -0
  129. data/lib/mail/parsers.rb +13 -0
  130. data/lib/mail/part.rb +11 -12
  131. data/lib/mail/parts_list.rb +90 -14
  132. data/lib/mail/smtp_envelope.rb +57 -0
  133. data/lib/mail/utilities.rb +415 -76
  134. data/lib/mail/values/unicode_tables.dat +0 -0
  135. data/lib/mail/version.rb +8 -15
  136. data/lib/mail/yaml.rb +30 -0
  137. data/lib/mail.rb +9 -32
  138. metadata +127 -79
  139. data/CHANGELOG.rdoc +0 -742
  140. data/CONTRIBUTING.md +0 -45
  141. data/Dependencies.txt +0 -3
  142. data/Gemfile +0 -32
  143. data/Rakefile +0 -21
  144. data/TODO.rdoc +0 -9
  145. data/lib/VERSION +0 -4
  146. data/lib/load_parsers.rb +0 -35
  147. data/lib/mail/core_extensions/nil.rb +0 -19
  148. data/lib/mail/core_extensions/object.rb +0 -13
  149. data/lib/mail/core_extensions/smtp.rb +0 -24
  150. data/lib/mail/core_extensions/string/access.rb +0 -145
  151. data/lib/mail/core_extensions/string/multibyte.rb +0 -78
  152. data/lib/mail/core_extensions/string.rb +0 -33
  153. data/lib/mail/fields/common/address_container.rb +0 -16
  154. data/lib/mail/fields/common/common_address.rb +0 -140
  155. data/lib/mail/fields/common/common_date.rb +0 -42
  156. data/lib/mail/fields/common/common_field.rb +0 -57
  157. data/lib/mail/fields/common/common_message_id.rb +0 -48
  158. data/lib/mail/multibyte/exceptions.rb +0 -8
  159. data/lib/mail/parsers/address_lists.rb +0 -64
  160. data/lib/mail/parsers/address_lists.treetop +0 -19
  161. data/lib/mail/parsers/content_disposition.rb +0 -535
  162. data/lib/mail/parsers/content_disposition.treetop +0 -46
  163. data/lib/mail/parsers/content_location.rb +0 -139
  164. data/lib/mail/parsers/content_location.treetop +0 -20
  165. data/lib/mail/parsers/content_transfer_encoding.rb +0 -201
  166. data/lib/mail/parsers/content_transfer_encoding.treetop +0 -18
  167. data/lib/mail/parsers/content_type.rb +0 -971
  168. data/lib/mail/parsers/content_type.treetop +0 -68
  169. data/lib/mail/parsers/date_time.rb +0 -114
  170. data/lib/mail/parsers/date_time.treetop +0 -11
  171. data/lib/mail/parsers/envelope_from.rb +0 -194
  172. data/lib/mail/parsers/envelope_from.treetop +0 -32
  173. data/lib/mail/parsers/message_ids.rb +0 -45
  174. data/lib/mail/parsers/message_ids.treetop +0 -15
  175. data/lib/mail/parsers/mime_version.rb +0 -144
  176. data/lib/mail/parsers/mime_version.treetop +0 -19
  177. data/lib/mail/parsers/phrase_lists.rb +0 -45
  178. data/lib/mail/parsers/phrase_lists.treetop +0 -15
  179. data/lib/mail/parsers/received.rb +0 -71
  180. data/lib/mail/parsers/received.treetop +0 -11
  181. data/lib/mail/parsers/rfc2045.rb +0 -421
  182. data/lib/mail/parsers/rfc2045.treetop +0 -35
  183. data/lib/mail/parsers/rfc2822.rb +0 -5397
  184. data/lib/mail/parsers/rfc2822.treetop +0 -408
  185. data/lib/mail/parsers/rfc2822_obsolete.rb +0 -3768
  186. data/lib/mail/parsers/rfc2822_obsolete.treetop +0 -241
  187. data/lib/mail/patterns.rb +0 -35
  188. data/lib/mail/version_specific/ruby_1_8.rb +0 -119
  189. data/lib/mail/version_specific/ruby_1_9.rb +0 -147
  190. data/lib/tasks/corpus.rake +0 -125
  191. data/lib/tasks/treetop.rake +0 -10
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ class SmtpEnvelope #:nodoc:
5
+ # Reasonable cap on address length to avoid SMTP line length
6
+ # overflow on old SMTP servers.
7
+ MAX_ADDRESS_BYTESIZE = 2000
8
+
9
+ attr_reader :from, :to, :message
10
+
11
+ def initialize(mail)
12
+ self.from = mail.smtp_envelope_from
13
+ self.to = mail.smtp_envelope_to
14
+ self.message = mail.encoded
15
+ end
16
+
17
+ def from=(addr)
18
+ if Utilities.blank? addr
19
+ raise ArgumentError, "SMTP From address may not be blank: #{addr.inspect}"
20
+ end
21
+
22
+ @from = validate_addr 'From', addr
23
+ end
24
+
25
+ def to=(addr)
26
+ if Utilities.blank?(addr)
27
+ raise ArgumentError, "SMTP To address may not be blank: #{addr.inspect}"
28
+ end
29
+
30
+ @to = Array(addr).map do |addr|
31
+ validate_addr 'To', addr
32
+ end
33
+ end
34
+
35
+ def message=(message)
36
+ if Utilities.blank?(message)
37
+ raise ArgumentError, 'SMTP message may not be blank'
38
+ end
39
+
40
+ @message = message
41
+ end
42
+
43
+
44
+ private
45
+ def validate_addr(addr_name, addr)
46
+ if addr.bytesize > MAX_ADDRESS_BYTESIZE
47
+ raise ArgumentError, "SMTP #{addr_name} address may not exceed #{MAX_ADDRESS_BYTESIZE} bytes: #{addr.inspect}"
48
+ end
49
+
50
+ if /[\r\n]/ =~ addr
51
+ raise ArgumentError, "SMTP #{addr_name} address may not contain CR or LF line breaks: #{addr.inspect}"
52
+ end
53
+
54
+ addr
55
+ end
56
+ end
57
+ end
@@ -1,44 +1,58 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ require 'mail/constants'
4
+ require 'socket'
5
+
2
6
  module Mail
3
7
  module Utilities
4
- include Patterns
5
-
8
+ extend self
9
+
6
10
  # Returns true if the string supplied is free from characters not allowed as an ATOM
7
11
  def atom_safe?( str )
8
- not ATOM_UNSAFE === str
12
+ not Constants::ATOM_UNSAFE === str
9
13
  end
10
14
 
11
- # If the string supplied has ATOM unsafe characters in it, will return the string quoted
15
+ # If the string supplied has ATOM unsafe characters in it, will return the string quoted
12
16
  # in double quotes, otherwise returns the string unmodified
13
17
  def quote_atom( str )
14
18
  atom_safe?( str ) ? str : dquote(str)
15
19
  end
16
20
 
17
- # If the string supplied has PHRASE unsafe characters in it, will return the string quoted
21
+ # If the string supplied has PHRASE unsafe characters in it, will return the string quoted
18
22
  # in double quotes, otherwise returns the string unmodified
19
23
  def quote_phrase( str )
20
- if RUBY_VERSION >= '1.9'
24
+ if str.respond_to?(:force_encoding)
21
25
  original_encoding = str.encoding
22
- str.force_encoding('ASCII-8BIT')
23
- if (PHRASE_UNSAFE === str)
24
- dquote(str).force_encoding(original_encoding)
26
+ ascii_str = str.to_s.dup.force_encoding('ASCII-8BIT')
27
+ if Constants::PHRASE_UNSAFE === ascii_str
28
+ dquote(ascii_str).force_encoding(original_encoding)
25
29
  else
26
- str.force_encoding(original_encoding)
30
+ str
27
31
  end
28
32
  else
29
- (PHRASE_UNSAFE === str) ? dquote(str) : str
33
+ Constants::PHRASE_UNSAFE === str ? dquote(str) : str
30
34
  end
31
35
  end
32
36
 
33
37
  # Returns true if the string supplied is free from characters not allowed as a TOKEN
34
38
  def token_safe?( str )
35
- not TOKEN_UNSAFE === str
39
+ not Constants::TOKEN_UNSAFE === str
36
40
  end
37
41
 
38
- # If the string supplied has TOKEN unsafe characters in it, will return the string quoted
42
+ # If the string supplied has TOKEN unsafe characters in it, will return the string quoted
39
43
  # in double quotes, otherwise returns the string unmodified
40
44
  def quote_token( str )
41
- token_safe?( str ) ? str : dquote(str)
45
+ if str.respond_to?(:force_encoding)
46
+ original_encoding = str.encoding
47
+ ascii_str = str.to_s.dup.force_encoding('ASCII-8BIT')
48
+ if token_safe?( ascii_str )
49
+ str
50
+ else
51
+ dquote(ascii_str).force_encoding(original_encoding)
52
+ end
53
+ else
54
+ token_safe?( str ) ? str : dquote(str)
55
+ end
42
56
  end
43
57
 
44
58
  # Wraps supplied string in double quotes and applies \-escaping as necessary,
@@ -67,157 +81,482 @@ module Mail
67
81
  # unqoute(string) #=> 'This is "a string"'
68
82
  def unquote( str )
69
83
  if str =~ /^"(.*?)"$/
70
- $1.gsub(/\\(.)/, '\1')
84
+ unescape($1)
71
85
  else
72
86
  str
73
87
  end
74
88
  end
75
-
89
+
90
+ # Removes any \-escaping.
91
+ #
92
+ # Example:
93
+ #
94
+ # string = 'This is \"a string\"'
95
+ # unescape(string) #=> 'This is "a string"'
96
+ #
97
+ # string = '"This is \"a string\""'
98
+ # unescape(string) #=> '"This is "a string""'
99
+ def unescape( str )
100
+ str.gsub(/\\(.)/, '\1')
101
+ end
102
+
76
103
  # Wraps a string in parenthesis and escapes any that are in the string itself.
77
- #
104
+ #
78
105
  # Example:
79
- #
106
+ #
80
107
  # paren( 'This is a string' ) #=> '(This is a string)'
81
108
  def paren( str )
82
- RubyVer.paren( str )
109
+ Utilities.paren( str )
83
110
  end
84
-
111
+
85
112
  # Unwraps a string from being wrapped in parenthesis
86
- #
113
+ #
87
114
  # Example:
88
- #
115
+ #
89
116
  # str = '(This is a string)'
90
117
  # unparen( str ) #=> 'This is a string'
91
118
  def unparen( str )
92
- match = str.match(/^\((.*?)\)$/)
93
- match ? match[1] : str
119
+ if str.start_with?('(') && str.end_with?(')')
120
+ str.slice(1..-2)
121
+ else
122
+ str
123
+ end
94
124
  end
95
-
125
+
96
126
  # Wraps a string in angle brackets and escapes any that are in the string itself
97
- #
127
+ #
98
128
  # Example:
99
- #
129
+ #
100
130
  # bracket( 'This is a string' ) #=> '<This is a string>'
101
131
  def bracket( str )
102
- RubyVer.bracket( str )
132
+ Utilities.bracket( str )
103
133
  end
104
-
134
+
105
135
  # Unwraps a string from being wrapped in parenthesis
106
- #
136
+ #
107
137
  # Example:
108
- #
138
+ #
109
139
  # str = '<This is a string>'
110
140
  # unbracket( str ) #=> 'This is a string'
111
141
  def unbracket( str )
112
- match = str.match(/^\<(.*?)\>$/)
113
- match ? match[1] : str
142
+ if str.start_with?('<') && str.end_with?('>')
143
+ str.slice(1..-2)
144
+ else
145
+ str
146
+ end
114
147
  end
115
-
148
+
116
149
  # Escape parenthesies in a string
117
- #
150
+ #
118
151
  # Example:
119
- #
152
+ #
120
153
  # str = 'This is (a) string'
121
154
  # escape_paren( str ) #=> 'This is \(a\) string'
122
155
  def escape_paren( str )
123
- RubyVer.escape_paren( str )
156
+ Utilities.escape_paren( str )
124
157
  end
125
-
158
+
126
159
  def uri_escape( str )
127
160
  uri_parser.escape(str)
128
161
  end
129
-
162
+
130
163
  def uri_unescape( str )
131
164
  uri_parser.unescape(str)
132
165
  end
133
-
166
+
134
167
  def uri_parser
135
- @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
168
+ @uri_parser ||= URI.const_defined?(:DEFAULT_PARSER) ? URI::DEFAULT_PARSER : URI
136
169
  end
137
-
170
+
138
171
  # Matches two objects with their to_s values case insensitively
139
- #
172
+ #
140
173
  # Example:
141
- #
174
+ #
142
175
  # obj2 = "This_is_An_object"
143
176
  # obj1 = :this_IS_an_object
144
177
  # match_to_s( obj1, obj2 ) #=> true
145
178
  def match_to_s( obj1, obj2 )
146
179
  obj1.to_s.casecmp(obj2.to_s) == 0
147
180
  end
148
-
181
+
149
182
  # Capitalizes a string that is joined by hyphens correctly.
150
- #
183
+ #
151
184
  # Example:
152
- #
185
+ #
153
186
  # string = 'resent-from-field'
154
187
  # capitalize_field( string ) #=> 'Resent-From-Field'
155
188
  def capitalize_field( str )
156
189
  str.to_s.split("-").map { |v| v.capitalize }.join("-")
157
190
  end
158
-
191
+
159
192
  # Takes an underscored word and turns it into a class name
160
- #
193
+ #
161
194
  # Example:
162
- #
195
+ #
163
196
  # constantize("hello") #=> "Hello"
164
197
  # constantize("hello-there") #=> "HelloThere"
165
198
  # constantize("hello-there-mate") #=> "HelloThereMate"
166
199
  def constantize( str )
167
200
  str.to_s.split(/[-_]/).map { |v| v.capitalize }.to_s
168
201
  end
169
-
202
+
170
203
  # Swaps out all underscores (_) for hyphens (-) good for stringing from symbols
171
204
  # a field name.
172
- #
205
+ #
173
206
  # Example:
174
- #
207
+ #
175
208
  # string = :resent_from_field
176
- # dasherize ( string ) #=> 'resent_from_field'
209
+ # dasherize( string ) #=> 'resent-from-field'
177
210
  def dasherize( str )
178
- str.to_s.gsub('_', '-')
211
+ str.to_s.tr(Constants::UNDERSCORE, Constants::HYPHEN)
179
212
  end
180
213
 
181
214
  # Swaps out all hyphens (-) for underscores (_) good for stringing to symbols
182
215
  # a field name.
183
- #
216
+ #
184
217
  # Example:
185
- #
218
+ #
186
219
  # string = :resent_from_field
187
220
  # underscoreize ( string ) #=> 'resent_from_field'
188
221
  def underscoreize( str )
189
- str.to_s.downcase.gsub('-', '_')
222
+ str.to_s.downcase.tr(Constants::HYPHEN, Constants::UNDERSCORE)
223
+ end
224
+
225
+ def map_lines( str, &block )
226
+ str.each_line.map(&block)
227
+ end
228
+
229
+ def map_with_index( enum, &block )
230
+ enum.each_with_index.map(&block)
231
+ end
232
+
233
+ def self.binary_unsafe_to_lf(string) #:nodoc:
234
+ string.gsub(/\r\n|\r/, Constants::LF)
235
+ end
236
+
237
+ TO_CRLF_REGEX =
238
+ # This 1.9 only regex can save a reasonable amount of time (~20%)
239
+ # by not matching "\r\n" so the string is returned unchanged in
240
+ # the common case.
241
+ Regexp.new("(?<!\r)\n|\r(?!\n)")
242
+
243
+ def self.binary_unsafe_to_crlf(string) #:nodoc:
244
+ string.gsub(TO_CRLF_REGEX, Constants::CRLF)
245
+ end
246
+
247
+ def self.safe_for_line_ending_conversion?(string) #:nodoc:
248
+ if string.encoding == Encoding::BINARY
249
+ string.ascii_only?
250
+ else
251
+ string.valid_encoding?
252
+ end
253
+ end
254
+
255
+ # Convert line endings to \n unless the string is binary. Used for
256
+ # sendmail delivery and for decoding 8bit Content-Transfer-Encoding.
257
+ def self.to_lf(string)
258
+ string = string.to_s
259
+ if safe_for_line_ending_conversion? string
260
+ binary_unsafe_to_lf string
261
+ else
262
+ string
263
+ end
264
+ end
265
+
266
+ # Convert line endings to \r\n unless the string is binary. Used for
267
+ # encoding 8bit and base64 Content-Transfer-Encoding and for convenience
268
+ # when parsing emails with \n line endings instead of the required \r\n.
269
+ def self.to_crlf(string)
270
+ string = string.to_s
271
+ if safe_for_line_ending_conversion? string
272
+ binary_unsafe_to_crlf string
273
+ else
274
+ string
275
+ end
276
+ end
277
+
278
+ # Returns true if the object is considered blank.
279
+ # A blank includes things like '', ' ', nil,
280
+ # and arrays and hashes that have nothing in them.
281
+ #
282
+ # This logic is mostly shared with ActiveSupport's blank?
283
+ def blank?(value)
284
+ if value.kind_of?(NilClass)
285
+ true
286
+ elsif value.kind_of?(String)
287
+ value !~ /\S/
288
+ else
289
+ value.respond_to?(:empty?) ? value.empty? : !value
290
+ end
291
+ end
292
+
293
+ def generate_message_id
294
+ "<#{Mail.random_tag}@#{::Socket.gethostname}.mail>"
190
295
  end
191
296
 
192
- if RUBY_VERSION <= '1.8.6'
297
+ class StrictCharsetEncoder
298
+ def encode(string, charset)
299
+ case charset
300
+ when /utf-?7/i
301
+ Mail::Utilities.decode_utf7(string)
302
+ else
303
+ string.force_encoding(Mail::Utilities.pick_encoding(charset))
304
+ end
305
+ end
306
+ end
193
307
 
194
- def map_lines( str, &block )
195
- results = []
196
- str.each_line do |line|
197
- results << yield(line)
308
+ class BestEffortCharsetEncoder
309
+ def encode(string, charset)
310
+ case charset
311
+ when /utf-?7/i
312
+ Mail::Utilities.decode_utf7(string)
313
+ else
314
+ string.force_encoding(pick_encoding(charset))
198
315
  end
199
- results
200
316
  end
201
-
202
- def map_with_index( enum, &block )
203
- results = []
204
- enum.each_with_index do |token, i|
205
- results[i] = yield(token, i)
317
+
318
+ private
319
+
320
+ def pick_encoding(charset)
321
+ charset = case charset
322
+ when /ansi_x3.110-1983/
323
+ 'ISO-8859-1'
324
+ when /Windows-?1258/i # Windows-1258 is similar to 1252
325
+ "Windows-1252"
326
+ else
327
+ charset
206
328
  end
207
- results
329
+ Mail::Utilities.pick_encoding(charset)
208
330
  end
209
-
210
- else
331
+ end
332
+
333
+ class << self
334
+ attr_accessor :charset_encoder
335
+ end
336
+ self.charset_encoder = BestEffortCharsetEncoder.new
337
+
338
+ # Escapes any parenthesis in a string that are unescaped this uses
339
+ # a Ruby 1.9.1 regexp feature of negative look behind
340
+ def Utilities.escape_paren( str )
341
+ re = /(?<!\\)([\(\)])/ # Only match unescaped parens
342
+ str.gsub(re) { |s| '\\' + s }
343
+ end
344
+
345
+ def Utilities.paren( str )
346
+ str = ::Mail::Utilities.unparen( str )
347
+ str = escape_paren( str )
348
+ '(' + str + ')'
349
+ end
350
+
351
+ def Utilities.escape_bracket( str )
352
+ re = /(?<!\\)([\<\>])/ # Only match unescaped brackets
353
+ str.gsub(re) { |s| '\\' + s }
354
+ end
355
+
356
+ def Utilities.bracket( str )
357
+ str = ::Mail::Utilities.unbracket( str )
358
+ str = escape_bracket( str )
359
+ '<' + str + '>'
360
+ end
211
361
 
212
- def map_lines( str, &block )
213
- str.each_line.map(&block)
362
+ def Utilities.decode_base64(str)
363
+ if !str.end_with?("=") && str.length % 4 != 0
364
+ str = str.ljust((str.length + 3) & ~3, "=")
214
365
  end
366
+ str.unpack( 'm' ).first
367
+ end
368
+
369
+ def Utilities.encode_base64(str)
370
+ [str].pack( 'm' )
371
+ end
372
+
373
+ def Utilities.has_constant?(klass, string)
374
+ klass.const_defined?( string, false )
375
+ end
376
+
377
+ def Utilities.get_constant(klass, string)
378
+ klass.const_get( string )
379
+ end
380
+
381
+ def Utilities.transcode_charset(str, from_encoding, to_encoding = Encoding::UTF_8)
382
+ to_encoding = Encoding.find(to_encoding)
383
+ replacement_char = to_encoding == Encoding::UTF_8 ? '�' : '?'
384
+ charset_encoder.encode(str.dup, from_encoding).encode(to_encoding, :undef => :replace, :invalid => :replace, :replace => replacement_char)
385
+ end
386
+
387
+ # From Ruby stdlib Net::IMAP
388
+ def Utilities.encode_utf7(string)
389
+ string.gsub(/(&)|[^\x20-\x7e]+/) do
390
+ if $1
391
+ "&-"
392
+ else
393
+ base64 = [$&.encode(Encoding::UTF_16BE)].pack("m0")
394
+ "&" + base64.delete("=").tr("/", ",") + "-"
395
+ end
396
+ end.force_encoding(Encoding::ASCII_8BIT)
397
+ end
215
398
 
216
- def map_with_index( enum, &block )
217
- enum.each_with_index.map(&block)
399
+ def Utilities.decode_utf7(utf7)
400
+ utf7.gsub(/&([^-]+)?-/n) do
401
+ if $1
402
+ ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
403
+ else
404
+ "&"
405
+ end
218
406
  end
407
+ end
219
408
 
409
+ def Utilities.b_value_encode(str, encoding = nil)
410
+ encoding = str.encoding.to_s
411
+ [Utilities.encode_base64(str), encoding]
220
412
  end
221
413
 
414
+ def Utilities.b_value_decode(str)
415
+ match = str.match(/\=\?(.+)?\?[Bb]\?(.*)\?\=/m)
416
+ if match
417
+ charset = match[1]
418
+ str = Utilities.decode_base64(match[2])
419
+ str = charset_encoder.encode(str, charset)
420
+ end
421
+ transcode_to_scrubbed_utf8(str)
422
+ rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError, Encoding::InvalidByteSequenceError
423
+ warn "Encoding conversion failed #{$!}"
424
+ str.dup.force_encoding(Encoding::UTF_8)
425
+ end
426
+
427
+ def Utilities.q_value_encode(str, encoding = nil)
428
+ encoding = str.encoding.to_s
429
+ [Encodings::QuotedPrintable.encode(str), encoding]
430
+ end
431
+
432
+ def Utilities.q_value_decode(str)
433
+ match = str.match(/\=\?(.+)?\?[Qq]\?(.*)\?\=/m)
434
+ if match
435
+ charset = match[1]
436
+ string = match[2].gsub(/_/, '=20')
437
+ # Remove trailing = if it exists in a Q encoding
438
+ string = string.sub(/\=$/, '')
439
+ str = Encodings::QuotedPrintable.decode(string)
440
+ str = charset_encoder.encode(str, charset)
441
+ # We assume that binary strings hold utf-8 directly to work around
442
+ # jruby/jruby#829 which subtly changes String#encode semantics.
443
+ str.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::ASCII_8BIT
444
+ end
445
+ transcode_to_scrubbed_utf8(str)
446
+ rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
447
+ warn "Encoding conversion failed #{$!}"
448
+ str.dup.force_encoding(Encoding::UTF_8)
449
+ end
450
+
451
+ def Utilities.param_decode(str, encoding)
452
+ str = uri_parser.unescape(str)
453
+ str = charset_encoder.encode(str, encoding) if encoding
454
+ transcode_to_scrubbed_utf8(str)
455
+ rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
456
+ warn "Encoding conversion failed #{$!}"
457
+ str.dup.force_encoding(Encoding::UTF_8)
458
+ end
459
+
460
+ def Utilities.param_encode(str)
461
+ encoding = str.encoding.to_s.downcase
462
+ language = Configuration.instance.param_encode_language
463
+ "#{encoding}'#{language}'#{uri_parser.escape(str)}"
464
+ end
465
+
466
+ def Utilities.uri_parser
467
+ URI::DEFAULT_PARSER
468
+ end
469
+
470
+ # Pick a Ruby encoding corresponding to the message charset. Most
471
+ # charsets have a Ruby encoding, but some need manual aliasing here.
472
+ #
473
+ # TODO: add this as a test somewhere:
474
+ # Encoding.list.map { |e| [e.to_s.upcase == pick_encoding(e.to_s.downcase.gsub("-", "")), e.to_s] }.select {|a,b| !b}
475
+ # Encoding.list.map { |e| [e.to_s == pick_encoding(e.to_s), e.to_s] }.select {|a,b| !b}
476
+ def Utilities.pick_encoding(charset)
477
+ charset = charset.to_s
478
+ encoding = case charset.downcase
479
+
480
+ # ISO-8859-8-I etc. http://en.wikipedia.org/wiki/ISO-8859-8-I
481
+ when /^iso[-_]?8859-(\d+)(-i)?$/
482
+ "ISO-8859-#{$1}"
483
+
484
+ # ISO-8859-15, ISO-2022-JP and alike
485
+ when /^iso[-_]?(\d{4})-?(\w{1,2})$/
486
+ "ISO-#{$1}-#{$2}"
487
+
488
+ # "ISO-2022-JP-KDDI" and alike
489
+ when /^iso[-_]?(\d{4})-?(\w{1,2})-?(\w*)$/
490
+ "ISO-#{$1}-#{$2}-#{$3}"
491
+
492
+ # UTF-8, UTF-32BE and alike
493
+ when /^utf[\-_]?(\d{1,2})?(\w{1,2})$/
494
+ "UTF-#{$1}#{$2}".gsub(/\A(UTF-(?:16|32))\z/, '\\1BE')
495
+
496
+ # Windows-1252 and alike
497
+ when /^windows-?(.*)$/
498
+ "Windows-#{$1}"
499
+
500
+ when '8bit'
501
+ Encoding::ASCII_8BIT
502
+
503
+ # alternatives/misspellings of us-ascii seen in the wild
504
+ when /^iso[-_]?646(-us)?$/, 'us=ascii'
505
+ Encoding::ASCII
506
+
507
+ # Microsoft-specific alias for MACROMAN
508
+ when 'macintosh'
509
+ Encoding::MACROMAN
510
+
511
+ # Microsoft-specific alias for CP949 (Korean)
512
+ when 'ks_c_5601-1987'
513
+ Encoding::CP949
514
+
515
+ # Wrongly written Shift_JIS (Japanese)
516
+ when 'shift-jis'
517
+ Encoding::Shift_JIS
518
+
519
+ # GB2312 (Chinese charset) is a subset of GB18030 (its replacement)
520
+ when 'gb2312'
521
+ Encoding::GB18030
522
+
523
+ when 'cp-850'
524
+ Encoding::CP850
525
+
526
+ when 'latin2'
527
+ Encoding::ISO_8859_2
528
+
529
+ else
530
+ charset
531
+ end
532
+
533
+ convert_to_encoding(encoding)
534
+ end
535
+
536
+ def Utilities.string_byteslice(str, *args)
537
+ str.byteslice(*args)
538
+ end
539
+
540
+ class << self
541
+ private
542
+
543
+ def convert_to_encoding(encoding)
544
+ if encoding.is_a?(Encoding)
545
+ encoding
546
+ else
547
+ # Fall back to ASCII for charsets that Ruby doesn't recognize
548
+ begin
549
+ Encoding.find(encoding)
550
+ rescue ArgumentError
551
+ Encoding::BINARY
552
+ end
553
+ end
554
+ end
555
+
556
+ def transcode_to_scrubbed_utf8(str)
557
+ decoded = str.encode(Encoding::UTF_8, :undef => :replace, :invalid => :replace, :replace => "�")
558
+ decoded.valid_encoding? ? decoded : decoded.encode(Encoding::UTF_16LE, :invalid => :replace, :replace => "�").encode(Encoding::UTF_8)
559
+ end
560
+ end
222
561
  end
223
562
  end
Binary file