mail 2.7.0.rc1 → 2.7.0
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.
- checksums.yaml +4 -4
- data/README.md +71 -98
- data/lib/mail/attachments_list.rb +5 -2
- data/lib/mail/body.rb +39 -33
- data/lib/mail/check_delivery_params.rb +8 -6
- data/lib/mail/configuration.rb +2 -0
- data/lib/mail/elements/address.rb +19 -18
- data/lib/mail/encodings/7bit.rb +5 -15
- data/lib/mail/encodings/8bit.rb +2 -21
- data/lib/mail/encodings/base64.rb +11 -12
- data/lib/mail/encodings/binary.rb +3 -22
- data/lib/mail/encodings/identity.rb +24 -0
- data/lib/mail/encodings/quoted_printable.rb +6 -6
- data/lib/mail/encodings/transfer_encoding.rb +38 -29
- data/lib/mail/encodings/unix_to_unix.rb +2 -1
- data/lib/mail/encodings.rb +89 -31
- data/lib/mail/envelope.rb +1 -1
- data/lib/mail/field.rb +93 -61
- data/lib/mail/fields/bcc_field.rb +2 -2
- data/lib/mail/fields/cc_field.rb +1 -1
- data/lib/mail/fields/comments_field.rb +1 -1
- data/lib/mail/fields/common/common_address.rb +32 -7
- data/lib/mail/fields/common/common_field.rb +1 -10
- data/lib/mail/fields/content_description_field.rb +1 -1
- data/lib/mail/fields/content_disposition_field.rb +3 -3
- data/lib/mail/fields/content_id_field.rb +2 -2
- data/lib/mail/fields/content_location_field.rb +1 -1
- data/lib/mail/fields/content_transfer_encoding_field.rb +1 -1
- data/lib/mail/fields/content_type_field.rb +1 -1
- data/lib/mail/fields/date_field.rb +2 -3
- data/lib/mail/fields/from_field.rb +1 -1
- data/lib/mail/fields/in_reply_to_field.rb +1 -1
- data/lib/mail/fields/keywords_field.rb +1 -1
- data/lib/mail/fields/message_id_field.rb +1 -1
- data/lib/mail/fields/mime_version_field.rb +1 -1
- data/lib/mail/fields/optional_field.rb +4 -1
- data/lib/mail/fields/received_field.rb +1 -1
- data/lib/mail/fields/references_field.rb +1 -1
- data/lib/mail/fields/reply_to_field.rb +1 -1
- data/lib/mail/fields/resent_bcc_field.rb +1 -1
- data/lib/mail/fields/resent_cc_field.rb +1 -1
- data/lib/mail/fields/resent_date_field.rb +0 -1
- data/lib/mail/fields/resent_from_field.rb +1 -1
- data/lib/mail/fields/resent_message_id_field.rb +1 -1
- data/lib/mail/fields/resent_sender_field.rb +1 -1
- data/lib/mail/fields/resent_to_field.rb +1 -1
- data/lib/mail/fields/return_path_field.rb +1 -1
- data/lib/mail/fields/sender_field.rb +1 -1
- data/lib/mail/fields/subject_field.rb +1 -1
- data/lib/mail/fields/to_field.rb +1 -1
- data/lib/mail/fields/unstructured_field.rb +19 -2
- data/lib/mail/header.rb +9 -8
- data/lib/mail/mail.rb +2 -10
- data/lib/mail/matchers/has_sent_mail.rb +21 -1
- data/lib/mail/message.rb +63 -50
- data/lib/mail/multibyte/chars.rb +2 -1
- data/lib/mail/multibyte.rb +14 -16
- data/lib/mail/network/delivery_methods/exim.rb +6 -10
- data/lib/mail/network/delivery_methods/logger_delivery.rb +37 -0
- data/lib/mail/network/delivery_methods/sendmail.rb +14 -7
- data/lib/mail/network/delivery_methods/smtp.rb +56 -55
- data/lib/mail/network/delivery_methods/smtp_connection.rb +9 -1
- data/lib/mail/network/retriever_methods/imap.rb +18 -5
- data/lib/mail/network/retriever_methods/pop3.rb +3 -1
- data/lib/mail/network.rb +1 -0
- data/lib/mail/parser_tools.rb +15 -0
- data/lib/mail/parsers/address_lists_parser.rb +30462 -12597
- data/lib/mail/parsers/address_lists_parser.rl +18 -12
- data/lib/mail/parsers/content_disposition_parser.rb +405 -215
- data/lib/mail/parsers/content_disposition_parser.rl +11 -5
- data/lib/mail/parsers/content_location_parser.rb +443 -208
- data/lib/mail/parsers/content_location_parser.rl +9 -3
- data/lib/mail/parsers/content_transfer_encoding_parser.rb +180 -80
- data/lib/mail/parsers/content_transfer_encoding_parser.rl +8 -2
- data/lib/mail/parsers/content_type_parser.rb +436 -245
- data/lib/mail/parsers/content_type_parser.rl +12 -6
- data/lib/mail/parsers/date_time_parser.rb +172 -72
- data/lib/mail/parsers/date_time_parser.rl +10 -4
- data/lib/mail/parsers/envelope_from_parser.rb +2833 -1320
- data/lib/mail/parsers/envelope_from_parser.rl +9 -3
- data/lib/mail/parsers/message_ids_parser.rb +2325 -976
- data/lib/mail/parsers/message_ids_parser.rl +9 -3
- data/lib/mail/parsers/mime_version_parser.rb +164 -64
- data/lib/mail/parsers/mime_version_parser.rl +9 -3
- data/lib/mail/parsers/phrase_lists_parser.rb +582 -237
- data/lib/mail/parsers/phrase_lists_parser.rl +9 -3
- data/lib/mail/parsers/received_parser.rb +7036 -3004
- data/lib/mail/parsers/received_parser.rl +11 -5
- data/lib/mail/parsers/rfc2045_content_transfer_encoding.rl +1 -0
- data/lib/mail/parsers/rfc2045_content_type.rl +1 -0
- data/lib/mail/parsers/rfc2045_mime.rl +1 -0
- data/lib/mail/parsers/rfc2183_content_disposition.rl +1 -0
- data/lib/mail/parsers/rfc3629_utf8.rl +19 -0
- data/lib/mail/parsers/rfc5234_abnf_core_rules.rl +7 -1
- data/lib/mail/parsers/rfc5322.rl +3 -1
- data/lib/mail/parsers/rfc5322_address.rl +3 -1
- data/lib/mail/parsers/rfc5322_date_time.rl +1 -0
- data/lib/mail/parsers/rfc5322_lexical_tokens.rl +9 -5
- data/lib/mail/part.rb +1 -1
- data/lib/mail/utilities.rb +44 -15
- data/lib/mail/version.rb +1 -1
- data/lib/mail/version_specific/ruby_1_8.rb +12 -1
- data/lib/mail/version_specific/ruby_1_9.rb +13 -1
- data/lib/mail.rb +1 -6
- metadata +9 -15
- data/CHANGELOG.rdoc +0 -822
- data/CONTRIBUTING.md +0 -60
- data/Dependencies.txt +0 -1
- data/Gemfile +0 -11
- data/Rakefile +0 -23
- data/TODO.rdoc +0 -9
- data/lib/mail/multibyte/exceptions.rb +0 -9
data/lib/mail/encodings/8bit.rb
CHANGED
|
@@ -6,32 +6,13 @@ module Mail
|
|
|
6
6
|
module Encodings
|
|
7
7
|
class EightBit < Binary
|
|
8
8
|
NAME = '8bit'
|
|
9
|
-
|
|
10
9
|
PRIORITY = 4
|
|
11
|
-
|
|
12
|
-
# 8bit is an identiy encoding, meaning nothing to do
|
|
13
|
-
|
|
14
|
-
# Decode the string
|
|
15
|
-
def self.decode(str)
|
|
16
|
-
::Mail::Utilities.to_lf str
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Encode the string
|
|
20
|
-
def self.encode(str)
|
|
21
|
-
::Mail::Utilities.to_crlf str
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Idenity encodings have a fixed cost, 1 byte out per 1 byte in
|
|
25
|
-
def self.cost(str)
|
|
26
|
-
1.0
|
|
27
|
-
end
|
|
10
|
+
Encodings.register(NAME, self)
|
|
28
11
|
|
|
29
12
|
# Per RFC 2821 4.5.3.1, SMTP lines may not be longer than 1000 octets including the <CRLF>.
|
|
30
13
|
def self.compatible_input?(str)
|
|
31
|
-
!str.lines.find { |line| line.
|
|
14
|
+
!str.lines.find { |line| line.bytesize > 998 }
|
|
32
15
|
end
|
|
33
|
-
|
|
34
|
-
Encodings.register(NAME, self)
|
|
35
16
|
end
|
|
36
17
|
end
|
|
37
18
|
end
|
|
@@ -4,36 +4,35 @@ require 'mail/encodings/7bit'
|
|
|
4
4
|
|
|
5
5
|
module Mail
|
|
6
6
|
module Encodings
|
|
7
|
+
# Base64 encoding handles binary content at the cost of 4 output bytes
|
|
8
|
+
# per input byte.
|
|
7
9
|
class Base64 < SevenBit
|
|
8
10
|
NAME = 'base64'
|
|
9
|
-
|
|
10
11
|
PRIORITY = 3
|
|
11
|
-
|
|
12
|
+
Encodings.register(NAME, self)
|
|
13
|
+
|
|
12
14
|
def self.can_encode?(enc)
|
|
13
15
|
true
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
# Decode the string from Base64
|
|
17
18
|
def self.decode(str)
|
|
18
|
-
RubyVer.decode_base64(
|
|
19
|
+
RubyVer.decode_base64(str)
|
|
19
20
|
end
|
|
20
|
-
|
|
21
|
-
# Encode the string to Base64
|
|
21
|
+
|
|
22
22
|
def self.encode(str)
|
|
23
|
-
::Mail::Utilities.
|
|
23
|
+
::Mail::Utilities.binary_unsafe_to_crlf(RubyVer.encode_base64(str))
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# 3 bytes in -> 4 bytes out
|
|
27
27
|
def self.cost(str)
|
|
28
|
-
4.0/3
|
|
28
|
+
4.0 / 3
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
# Base64 inserts newlines automatically
|
|
31
|
+
# Ruby Base64 inserts newlines automatically, so it doesn't exceed
|
|
32
|
+
# SMTP line length limits.
|
|
32
33
|
def self.compatible_input?(str)
|
|
33
34
|
true
|
|
34
35
|
end
|
|
35
|
-
|
|
36
|
-
Encodings.register(NAME, self)
|
|
37
36
|
end
|
|
38
37
|
end
|
|
39
38
|
end
|
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
# encoding: utf-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
|
-
require 'mail/encodings/
|
|
3
|
+
require 'mail/encodings/identity'
|
|
4
4
|
|
|
5
5
|
module Mail
|
|
6
6
|
module Encodings
|
|
7
|
-
class Binary <
|
|
7
|
+
class Binary < Identity
|
|
8
8
|
NAME = 'binary'
|
|
9
|
-
|
|
10
9
|
PRIORITY = 5
|
|
11
|
-
|
|
12
|
-
# Binary is an identiy encoding, meaning nothing to do
|
|
13
|
-
|
|
14
|
-
# Decode the string
|
|
15
|
-
def self.decode(str)
|
|
16
|
-
str
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Encode the string
|
|
20
|
-
def self.encode(str)
|
|
21
|
-
str
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Idenity encodings have a fixed cost, 1 byte out per 1 byte in
|
|
25
|
-
def self.cost(str)
|
|
26
|
-
1.0
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
Encodings.register(NAME, self)
|
|
10
|
+
Encodings.register(NAME, self)
|
|
30
11
|
end
|
|
31
12
|
end
|
|
32
13
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
require 'mail/encodings/transfer_encoding'
|
|
4
|
+
|
|
5
|
+
module Mail
|
|
6
|
+
module Encodings
|
|
7
|
+
# Identity encodings do no encoding/decoding and have a fixed cost:
|
|
8
|
+
# 1 byte in -> 1 byte out.
|
|
9
|
+
class Identity < TransferEncoding #:nodoc:
|
|
10
|
+
def self.decode(str)
|
|
11
|
+
str
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.encode(str)
|
|
15
|
+
str
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 1 output byte per input byte.
|
|
19
|
+
def self.cost(str)
|
|
20
|
+
1.0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -6,27 +6,27 @@ module Mail
|
|
|
6
6
|
module Encodings
|
|
7
7
|
class QuotedPrintable < SevenBit
|
|
8
8
|
NAME='quoted-printable'
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
PRIORITY = 2
|
|
11
11
|
|
|
12
|
-
def self.can_encode?(
|
|
13
|
-
EightBit.can_encode?
|
|
12
|
+
def self.can_encode?(enc)
|
|
13
|
+
EightBit.can_encode? enc
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Decode the string from Quoted-Printable. Cope with hard line breaks
|
|
17
17
|
# that were incorrectly encoded as hex instead of literal CRLF.
|
|
18
18
|
def self.decode(str)
|
|
19
|
-
|
|
19
|
+
str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.encode(str)
|
|
23
|
-
|
|
23
|
+
[str].pack("M")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def self.cost(str)
|
|
27
27
|
# These bytes probably do not need encoding
|
|
28
28
|
c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
|
|
29
|
-
# Everything else turns into =XX where XX is a
|
|
29
|
+
# Everything else turns into =XX where XX is a
|
|
30
30
|
# two digit hex number (taking 3 bytes)
|
|
31
31
|
total = (str.bytesize - c)*3 + c
|
|
32
32
|
total.to_f/str.bytesize
|
|
@@ -7,17 +7,17 @@ module Mail
|
|
|
7
7
|
|
|
8
8
|
PRIORITY = -1
|
|
9
9
|
|
|
10
|
+
# And encoding's superclass can always transport it since the
|
|
11
|
+
# class hierarchy is arranged e.g. Base64 < 7bit < 8bit < Binary.
|
|
10
12
|
def self.can_transport?(enc)
|
|
11
|
-
enc
|
|
12
|
-
if Encodings.defined? enc
|
|
13
|
-
Encodings.get_encoding(enc).new.is_a? self
|
|
14
|
-
else
|
|
15
|
-
false
|
|
16
|
-
end
|
|
13
|
+
enc && enc <= self
|
|
17
14
|
end
|
|
18
15
|
|
|
16
|
+
# Override in subclasses to indicate that they can encode text
|
|
17
|
+
# that couldn't be directly transported, e.g. Base64 has 7bit output,
|
|
18
|
+
# but it can encode binary.
|
|
19
19
|
def self.can_encode?(enc)
|
|
20
|
-
can_transport? enc
|
|
20
|
+
can_transport? enc
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def self.cost(str)
|
|
@@ -32,36 +32,45 @@ module Mail
|
|
|
32
32
|
self::NAME
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def self.
|
|
36
|
-
|
|
35
|
+
def self.negotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
|
|
36
|
+
message_encoding = Encodings.get_encoding(message_encoding || '8bit')
|
|
37
|
+
source_encoding = Encodings.get_encoding(source_encoding)
|
|
38
|
+
|
|
39
|
+
if message_encoding && source_encoding && message_encoding.can_transport?(source_encoding) && source_encoding.compatible_input?(str)
|
|
37
40
|
source_encoding
|
|
38
41
|
else
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
renegotiate(message_encoding, source_encoding, str, allowed_encodings)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
def self.renegotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
|
|
47
|
+
encodings = Encodings.get_all.select do |enc|
|
|
48
|
+
(allowed_encodings.nil? || allowed_encodings.include?(enc)) &&
|
|
49
|
+
message_encoding.can_transport?(enc) &&
|
|
50
|
+
enc.can_encode?(source_encoding)
|
|
51
|
+
end
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# give priority to other choices but allow it to be used as a fallback.
|
|
49
|
-
this_cost = enc.cost(str) if enc.compatible_input?(str)
|
|
53
|
+
lowest_cost(str, encodings)
|
|
54
|
+
end
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
elsif this_cost == best_cost
|
|
55
|
-
best = enc if enc::PRIORITY < best::PRIORITY
|
|
56
|
-
end
|
|
57
|
-
end
|
|
56
|
+
def self.lowest_cost(str, encodings)
|
|
57
|
+
best = nil
|
|
58
|
+
best_cost = nil
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
encodings.each do |enc|
|
|
61
|
+
# If the current choice cannot be transported safely, give priority
|
|
62
|
+
# to other choices but allow it to be used as a fallback.
|
|
63
|
+
this_cost = enc.cost(str) if enc.compatible_input?(str)
|
|
64
|
+
|
|
65
|
+
if !best_cost || (this_cost && this_cost < best_cost)
|
|
66
|
+
best_cost = this_cost
|
|
67
|
+
best = enc
|
|
68
|
+
elsif this_cost == best_cost
|
|
69
|
+
best = enc if enc::PRIORITY < best::PRIORITY
|
|
70
|
+
end
|
|
60
71
|
end
|
|
61
|
-
end
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
self.class.to_s
|
|
73
|
+
best
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module Mail
|
|
3
3
|
module Encodings
|
|
4
|
-
|
|
4
|
+
class UnixToUnix < TransferEncoding
|
|
5
5
|
NAME = "x-uuencode"
|
|
6
6
|
|
|
7
7
|
def self.decode(str)
|
|
@@ -13,6 +13,7 @@ module Mail
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
Encodings.register(NAME, self)
|
|
16
|
+
Encodings.register("uuencode", self)
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
data/lib/mail/encodings.rb
CHANGED
|
@@ -7,7 +7,6 @@ module Mail
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
module Encodings
|
|
10
|
-
|
|
11
10
|
include Mail::Constants
|
|
12
11
|
extend Mail::Utilities
|
|
13
12
|
|
|
@@ -19,7 +18,7 @@ module Mail
|
|
|
19
18
|
#
|
|
20
19
|
# Encodings.register "base64", Mail::Encodings::Base64
|
|
21
20
|
def Encodings.register(name, cls)
|
|
22
|
-
|
|
21
|
+
@transfer_encodings[get_name(name)] = cls
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
# Is the encoding we want defined?
|
|
@@ -27,8 +26,8 @@ module Mail
|
|
|
27
26
|
# Example:
|
|
28
27
|
#
|
|
29
28
|
# Encodings.defined?(:base64) #=> true
|
|
30
|
-
def Encodings.defined?(
|
|
31
|
-
@transfer_encodings.include? get_name(
|
|
29
|
+
def Encodings.defined?(name)
|
|
30
|
+
@transfer_encodings.include? get_name(name)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
# Gets a defined encoding type, QuotedPrintable or Base64 for now.
|
|
@@ -39,16 +38,16 @@ module Mail
|
|
|
39
38
|
# Example:
|
|
40
39
|
#
|
|
41
40
|
# Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
|
|
42
|
-
def Encodings.get_encoding(
|
|
43
|
-
@transfer_encodings[get_name(
|
|
41
|
+
def Encodings.get_encoding(name)
|
|
42
|
+
@transfer_encodings[get_name(name)]
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
def Encodings.get_all
|
|
47
46
|
@transfer_encodings.values
|
|
48
47
|
end
|
|
49
48
|
|
|
50
|
-
def Encodings.get_name(
|
|
51
|
-
underscoreize(
|
|
49
|
+
def Encodings.get_name(name)
|
|
50
|
+
underscoreize(name).downcase
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
|
|
@@ -168,31 +167,51 @@ module Mail
|
|
|
168
167
|
|
|
169
168
|
def Encodings.address_encode(address, charset = 'utf-8')
|
|
170
169
|
if address.is_a?(Array)
|
|
171
|
-
# loop back through for each element
|
|
172
170
|
address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
encode_non_usascii(address, charset) if address
|
|
171
|
+
elsif address
|
|
172
|
+
encode_non_usascii(address, charset)
|
|
176
173
|
end
|
|
177
174
|
end
|
|
178
175
|
|
|
179
176
|
def Encodings.encode_non_usascii(address, charset)
|
|
180
177
|
return address if address.ascii_only? or charset.nil?
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
|
|
179
|
+
# With KCODE=u we can't use regexps on other encodings. Go ASCII.
|
|
180
|
+
with_ascii_kcode do
|
|
181
|
+
# Encode all strings embedded inside of quotes
|
|
182
|
+
address = address.gsub(/("[^"]*[^\/]")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
|
|
183
|
+
|
|
184
|
+
# Then loop through all remaining items and encode as needed
|
|
185
|
+
tokens = address.split(/\s/)
|
|
186
|
+
|
|
187
|
+
map_with_index(tokens) do |word, i|
|
|
188
|
+
if word.ascii_only?
|
|
189
|
+
word
|
|
190
|
+
else
|
|
191
|
+
previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
|
|
192
|
+
if previous_non_ascii #why are we adding an extra space here?
|
|
193
|
+
word = " #{word}"
|
|
194
|
+
end
|
|
195
|
+
Encodings.b_value_encode(word, charset)
|
|
192
196
|
end
|
|
193
|
-
|
|
197
|
+
end.join(' ')
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if RUBY_VERSION < '1.9'
|
|
202
|
+
# With KCODE=u we can't use regexps on other encodings. Go ASCII.
|
|
203
|
+
def Encodings.with_ascii_kcode #:nodoc:
|
|
204
|
+
if $KCODE
|
|
205
|
+
$KCODE, original_kcode = '', $KCODE
|
|
194
206
|
end
|
|
195
|
-
|
|
207
|
+
yield
|
|
208
|
+
ensure
|
|
209
|
+
$KCODE = original_kcode if original_kcode
|
|
210
|
+
end
|
|
211
|
+
else
|
|
212
|
+
def Encodings.with_ascii_kcode #:nodoc:
|
|
213
|
+
yield
|
|
214
|
+
end
|
|
196
215
|
end
|
|
197
216
|
|
|
198
217
|
# Encode a string with Base64 Encoding and returns it ready to be inserted
|
|
@@ -202,12 +221,15 @@ module Mail
|
|
|
202
221
|
#
|
|
203
222
|
# Encodings.b_value_encode('This is あ string', 'UTF-8')
|
|
204
223
|
# #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
|
|
205
|
-
def Encodings.b_value_encode(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
def Encodings.b_value_encode(string, encoding = nil)
|
|
225
|
+
if string.to_s.ascii_only?
|
|
226
|
+
string
|
|
227
|
+
else
|
|
228
|
+
Encodings.each_base64_chunk_byterange(string, 60).map do |chunk|
|
|
229
|
+
str, encoding = RubyVer.b_value_encode(chunk, encoding)
|
|
230
|
+
"=?#{encoding}?B?#{str.chomp}?="
|
|
231
|
+
end.join(" ")
|
|
232
|
+
end
|
|
211
233
|
end
|
|
212
234
|
|
|
213
235
|
# Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
|
|
@@ -281,5 +303,41 @@ module Mail
|
|
|
281
303
|
|
|
282
304
|
results
|
|
283
305
|
end
|
|
306
|
+
|
|
307
|
+
# Partition the string into bounded-size chunks without splitting
|
|
308
|
+
# multibyte characters.
|
|
309
|
+
def Encodings.each_base64_chunk_byterange(str, max_bytesize_per_base64_chunk, &block)
|
|
310
|
+
raise "size per chunk must be multiple of 4" if (max_bytesize_per_base64_chunk % 4).nonzero?
|
|
311
|
+
|
|
312
|
+
if block_given?
|
|
313
|
+
max_bytesize = ((3 * max_bytesize_per_base64_chunk) / 4.0).floor
|
|
314
|
+
each_chunk_byterange(str, max_bytesize, &block)
|
|
315
|
+
else
|
|
316
|
+
enum_for :each_base64_chunk_byterange, str, max_bytesize_per_base64_chunk
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Partition the string into bounded-size chunks without splitting
|
|
321
|
+
# multibyte characters.
|
|
322
|
+
def Encodings.each_chunk_byterange(str, max_bytesize_per_chunk)
|
|
323
|
+
return enum_for(:each_chunk_byterange, str, max_bytesize_per_chunk) unless block_given?
|
|
324
|
+
|
|
325
|
+
offset = 0
|
|
326
|
+
chunksize = 0
|
|
327
|
+
|
|
328
|
+
str.each_char do |chr|
|
|
329
|
+
charsize = chr.bytesize
|
|
330
|
+
|
|
331
|
+
if chunksize + charsize > max_bytesize_per_chunk
|
|
332
|
+
yield RubyVer.string_byteslice(str, offset, chunksize)
|
|
333
|
+
offset += chunksize
|
|
334
|
+
chunksize = charsize
|
|
335
|
+
else
|
|
336
|
+
chunksize += charsize
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
yield RubyVer.string_byteslice(str, offset, chunksize)
|
|
341
|
+
end
|
|
284
342
|
end
|
|
285
343
|
end
|
data/lib/mail/envelope.rb
CHANGED
data/lib/mail/field.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mail/fields'
|
|
3
|
+
require 'mail/constants'
|
|
3
4
|
|
|
4
5
|
# encoding: utf-8
|
|
5
6
|
module Mail
|
|
@@ -83,9 +84,31 @@ module Mail
|
|
|
83
84
|
|
|
84
85
|
def initialize(element, value, reason)
|
|
85
86
|
@element = element
|
|
86
|
-
@value = value
|
|
87
|
-
@reason = reason
|
|
88
|
-
super("#{element} can not parse |#{value}
|
|
87
|
+
@value = to_utf8(value)
|
|
88
|
+
@reason = to_utf8(reason)
|
|
89
|
+
super("#{@element} can not parse |#{@value}|: #{@reason}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
def to_utf8(text)
|
|
94
|
+
if text.respond_to?(:force_encoding)
|
|
95
|
+
text.dup.force_encoding(Encoding::UTF_8)
|
|
96
|
+
else
|
|
97
|
+
text
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class NilParseError < ParseError #:nodoc:
|
|
103
|
+
def initialize(element)
|
|
104
|
+
super element, nil, 'nil is invalid'
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class IncompleteParseError < ParseError #:nodoc:
|
|
109
|
+
def initialize(element, original_text, unparsed_index)
|
|
110
|
+
parsed_text = to_utf8(original_text[0...unparsed_index])
|
|
111
|
+
super element, original_text, "Only able to parse up to #{parsed_text.inspect}"
|
|
89
112
|
end
|
|
90
113
|
end
|
|
91
114
|
|
|
@@ -93,41 +116,62 @@ module Mail
|
|
|
93
116
|
class SyntaxError < FieldError #:nodoc:
|
|
94
117
|
end
|
|
95
118
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
class << self
|
|
120
|
+
# Parse a field from a raw header line:
|
|
121
|
+
#
|
|
122
|
+
# Mail::Field.parse("field-name: field data")
|
|
123
|
+
# # => #<Mail::Field …>
|
|
124
|
+
def parse(field, charset = nil)
|
|
125
|
+
name, value = split(field)
|
|
126
|
+
if name && value
|
|
127
|
+
new name, value, charset
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def split(raw_field) #:nodoc:
|
|
132
|
+
if raw_field.index(Constants::COLON)
|
|
133
|
+
name, value = raw_field.split(Constants::COLON, 2)
|
|
134
|
+
name.rstrip!
|
|
135
|
+
if name =~ /\A#{Constants::FIELD_NAME}\z/
|
|
136
|
+
[ name.rstrip, value.strip ]
|
|
137
|
+
else
|
|
138
|
+
Kernel.warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: invalid header name syntax: #{name.inspect}"
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
raw_field.strip
|
|
143
|
+
end
|
|
144
|
+
rescue => error
|
|
145
|
+
warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: #{error.class}: #{error.message}"
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Create a field by name and optional value:
|
|
103
151
|
#
|
|
104
|
-
#
|
|
152
|
+
# Mail::Field.new("field-name", "value")
|
|
153
|
+
# # => #<Mail::Field …>
|
|
105
154
|
#
|
|
106
|
-
#
|
|
155
|
+
# Values that aren't strings or arrays are coerced to Strings with `#to_s`.
|
|
107
156
|
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# it will be passed through as is, for example, content-type
|
|
111
|
-
# field can accept an array with the type and a hash of
|
|
112
|
-
# parameters:
|
|
157
|
+
# Mail::Field.new("field-name", 1234)
|
|
158
|
+
# # => #<Mail::Field …>
|
|
113
159
|
#
|
|
114
|
-
# Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
|
|
160
|
+
# Mail::Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
|
|
161
|
+
# # => #<Mail::Field …>
|
|
115
162
|
def initialize(name, value = nil, charset = 'utf-8')
|
|
116
163
|
case
|
|
117
|
-
when name.index(COLON)
|
|
164
|
+
when name.index(COLON)
|
|
165
|
+
Kernel.warn 'Passing an unparsed header field to Mail::Field.new is deprecated and will be removed in Mail 2.8.0. Use Mail::Field.parse instead.'
|
|
166
|
+
@name, @unparsed_value = self.class.split(name)
|
|
118
167
|
@charset = Utilities.blank?(value) ? charset : value
|
|
119
|
-
|
|
120
|
-
@raw_value = name
|
|
121
|
-
@value = nil
|
|
122
|
-
when Utilities.blank?(value) # Field.new("field-name")
|
|
168
|
+
when Utilities.blank?(value)
|
|
123
169
|
@name = name
|
|
124
|
-
@
|
|
125
|
-
@raw_value = nil
|
|
170
|
+
@unparsed_value = nil
|
|
126
171
|
@charset = charset
|
|
127
|
-
else
|
|
172
|
+
else
|
|
128
173
|
@name = name
|
|
129
|
-
@
|
|
130
|
-
@raw_value = nil
|
|
174
|
+
@unparsed_value = value
|
|
131
175
|
@charset = charset
|
|
132
176
|
end
|
|
133
177
|
@name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
|
|
@@ -138,8 +182,7 @@ module Mail
|
|
|
138
182
|
end
|
|
139
183
|
|
|
140
184
|
def field
|
|
141
|
-
|
|
142
|
-
@field ||= create_field(@name, @value, @charset)
|
|
185
|
+
@field ||= create_field(@name, @unparsed_value, @charset)
|
|
143
186
|
end
|
|
144
187
|
|
|
145
188
|
def name
|
|
@@ -217,14 +260,26 @@ module Mail
|
|
|
217
260
|
|
|
218
261
|
private
|
|
219
262
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
263
|
+
def create_field(name, value, charset)
|
|
264
|
+
new_field(name, value, charset)
|
|
265
|
+
rescue Mail::Field::ParseError => e
|
|
266
|
+
field = Mail::UnstructuredField.new(name, value)
|
|
267
|
+
field.errors << [name, value, e]
|
|
268
|
+
field
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def new_field(name, value, charset)
|
|
272
|
+
value = unfold(value) if value.is_a?(String)
|
|
273
|
+
|
|
274
|
+
if klass = field_class_for(name)
|
|
275
|
+
klass.new(value, charset)
|
|
276
|
+
else
|
|
277
|
+
OptionalField.new(name, value, charset)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def field_class_for(name)
|
|
282
|
+
FIELDS_MAP[name.to_s.downcase]
|
|
228
283
|
end
|
|
229
284
|
|
|
230
285
|
# 2.2.3. Long Header Fields
|
|
@@ -238,28 +293,5 @@ module Mail
|
|
|
238
293
|
def unfold(string)
|
|
239
294
|
string.gsub(/#{CRLF}(#{WSP})/m, '\1')
|
|
240
295
|
end
|
|
241
|
-
|
|
242
|
-
def create_field(name, value, charset)
|
|
243
|
-
value = unfold(value) if value.is_a?(String)
|
|
244
|
-
|
|
245
|
-
begin
|
|
246
|
-
new_field(name, value, charset)
|
|
247
|
-
rescue Mail::Field::ParseError => e
|
|
248
|
-
field = Mail::UnstructuredField.new(name, value)
|
|
249
|
-
field.errors << [name, value, e]
|
|
250
|
-
field
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def new_field(name, value, charset)
|
|
255
|
-
lower_case_name = name.to_s.downcase
|
|
256
|
-
if field_klass = FIELDS_MAP[lower_case_name]
|
|
257
|
-
field_klass.new(value, charset)
|
|
258
|
-
else
|
|
259
|
-
OptionalField.new(name, value, charset)
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
296
|
end
|
|
264
|
-
|
|
265
297
|
end
|