mail-trunk 2.3.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.
Files changed (137) hide show
  1. data/CHANGELOG.rdoc +555 -0
  2. data/Dependencies.txt +3 -0
  3. data/Gemfile +29 -0
  4. data/README.mkd +583 -0
  5. data/Rakefile +66 -0
  6. data/TODO.rdoc +9 -0
  7. data/lib/VERSION +4 -0
  8. data/lib/mail.rb +89 -0
  9. data/lib/mail/attachments_list.rb +105 -0
  10. data/lib/mail/body.rb +292 -0
  11. data/lib/mail/configuration.rb +73 -0
  12. data/lib/mail/core_extensions/nil.rb +17 -0
  13. data/lib/mail/core_extensions/object.rb +13 -0
  14. data/lib/mail/core_extensions/shellwords.rb +57 -0
  15. data/lib/mail/core_extensions/smtp.rb +25 -0
  16. data/lib/mail/core_extensions/string.rb +31 -0
  17. data/lib/mail/core_extensions/string/access.rb +104 -0
  18. data/lib/mail/core_extensions/string/multibyte.rb +78 -0
  19. data/lib/mail/elements.rb +14 -0
  20. data/lib/mail/elements/address.rb +306 -0
  21. data/lib/mail/elements/address_list.rb +74 -0
  22. data/lib/mail/elements/content_disposition_element.rb +30 -0
  23. data/lib/mail/elements/content_location_element.rb +25 -0
  24. data/lib/mail/elements/content_transfer_encoding_element.rb +24 -0
  25. data/lib/mail/elements/content_type_element.rb +35 -0
  26. data/lib/mail/elements/date_time_element.rb +26 -0
  27. data/lib/mail/elements/envelope_from_element.rb +34 -0
  28. data/lib/mail/elements/message_ids_element.rb +29 -0
  29. data/lib/mail/elements/mime_version_element.rb +26 -0
  30. data/lib/mail/elements/phrase_list.rb +21 -0
  31. data/lib/mail/elements/received_element.rb +30 -0
  32. data/lib/mail/encodings.rb +266 -0
  33. data/lib/mail/encodings/7bit.rb +31 -0
  34. data/lib/mail/encodings/8bit.rb +31 -0
  35. data/lib/mail/encodings/base64.rb +33 -0
  36. data/lib/mail/encodings/binary.rb +31 -0
  37. data/lib/mail/encodings/quoted_printable.rb +38 -0
  38. data/lib/mail/encodings/transfer_encoding.rb +58 -0
  39. data/lib/mail/envelope.rb +35 -0
  40. data/lib/mail/field.rb +224 -0
  41. data/lib/mail/field_list.rb +33 -0
  42. data/lib/mail/fields.rb +35 -0
  43. data/lib/mail/fields/bcc_field.rb +56 -0
  44. data/lib/mail/fields/cc_field.rb +55 -0
  45. data/lib/mail/fields/comments_field.rb +41 -0
  46. data/lib/mail/fields/common/address_container.rb +16 -0
  47. data/lib/mail/fields/common/common_address.rb +125 -0
  48. data/lib/mail/fields/common/common_date.rb +42 -0
  49. data/lib/mail/fields/common/common_field.rb +51 -0
  50. data/lib/mail/fields/common/common_message_id.rb +44 -0
  51. data/lib/mail/fields/common/parameter_hash.rb +58 -0
  52. data/lib/mail/fields/content_description_field.rb +19 -0
  53. data/lib/mail/fields/content_disposition_field.rb +69 -0
  54. data/lib/mail/fields/content_id_field.rb +63 -0
  55. data/lib/mail/fields/content_location_field.rb +42 -0
  56. data/lib/mail/fields/content_transfer_encoding_field.rb +50 -0
  57. data/lib/mail/fields/content_type_field.rb +198 -0
  58. data/lib/mail/fields/date_field.rb +57 -0
  59. data/lib/mail/fields/from_field.rb +55 -0
  60. data/lib/mail/fields/in_reply_to_field.rb +55 -0
  61. data/lib/mail/fields/keywords_field.rb +44 -0
  62. data/lib/mail/fields/message_id_field.rb +83 -0
  63. data/lib/mail/fields/mime_version_field.rb +53 -0
  64. data/lib/mail/fields/optional_field.rb +13 -0
  65. data/lib/mail/fields/received_field.rb +75 -0
  66. data/lib/mail/fields/references_field.rb +55 -0
  67. data/lib/mail/fields/reply_to_field.rb +55 -0
  68. data/lib/mail/fields/resent_bcc_field.rb +55 -0
  69. data/lib/mail/fields/resent_cc_field.rb +55 -0
  70. data/lib/mail/fields/resent_date_field.rb +35 -0
  71. data/lib/mail/fields/resent_from_field.rb +55 -0
  72. data/lib/mail/fields/resent_message_id_field.rb +34 -0
  73. data/lib/mail/fields/resent_sender_field.rb +62 -0
  74. data/lib/mail/fields/resent_to_field.rb +55 -0
  75. data/lib/mail/fields/return_path_field.rb +65 -0
  76. data/lib/mail/fields/sender_field.rb +67 -0
  77. data/lib/mail/fields/structured_field.rb +51 -0
  78. data/lib/mail/fields/subject_field.rb +16 -0
  79. data/lib/mail/fields/to_field.rb +55 -0
  80. data/lib/mail/fields/unstructured_field.rb +182 -0
  81. data/lib/mail/header.rb +265 -0
  82. data/lib/mail/indifferent_hash.rb +146 -0
  83. data/lib/mail/mail.rb +255 -0
  84. data/lib/mail/message.rb +2017 -0
  85. data/lib/mail/multibyte.rb +42 -0
  86. data/lib/mail/multibyte/chars.rb +474 -0
  87. data/lib/mail/multibyte/exceptions.rb +8 -0
  88. data/lib/mail/multibyte/unicode.rb +392 -0
  89. data/lib/mail/multibyte/utils.rb +60 -0
  90. data/lib/mail/network.rb +13 -0
  91. data/lib/mail/network/delivery_methods/file_delivery.rb +40 -0
  92. data/lib/mail/network/delivery_methods/sendmail.rb +62 -0
  93. data/lib/mail/network/delivery_methods/smtp.rb +137 -0
  94. data/lib/mail/network/delivery_methods/smtp_connection.rb +74 -0
  95. data/lib/mail/network/delivery_methods/test_mailer.rb +40 -0
  96. data/lib/mail/network/retriever_methods/base.rb +63 -0
  97. data/lib/mail/network/retriever_methods/imap.rb +158 -0
  98. data/lib/mail/network/retriever_methods/pop3.rb +140 -0
  99. data/lib/mail/network/retriever_methods/test_retriever.rb +47 -0
  100. data/lib/mail/parsers/address_lists.rb +64 -0
  101. data/lib/mail/parsers/address_lists.treetop +19 -0
  102. data/lib/mail/parsers/content_disposition.rb +535 -0
  103. data/lib/mail/parsers/content_disposition.treetop +46 -0
  104. data/lib/mail/parsers/content_location.rb +139 -0
  105. data/lib/mail/parsers/content_location.treetop +20 -0
  106. data/lib/mail/parsers/content_transfer_encoding.rb +162 -0
  107. data/lib/mail/parsers/content_transfer_encoding.treetop +20 -0
  108. data/lib/mail/parsers/content_type.rb +967 -0
  109. data/lib/mail/parsers/content_type.treetop +68 -0
  110. data/lib/mail/parsers/date_time.rb +114 -0
  111. data/lib/mail/parsers/date_time.treetop +11 -0
  112. data/lib/mail/parsers/envelope_from.rb +194 -0
  113. data/lib/mail/parsers/envelope_from.treetop +32 -0
  114. data/lib/mail/parsers/message_ids.rb +45 -0
  115. data/lib/mail/parsers/message_ids.treetop +15 -0
  116. data/lib/mail/parsers/mime_version.rb +144 -0
  117. data/lib/mail/parsers/mime_version.treetop +19 -0
  118. data/lib/mail/parsers/phrase_lists.rb +45 -0
  119. data/lib/mail/parsers/phrase_lists.treetop +15 -0
  120. data/lib/mail/parsers/received.rb +71 -0
  121. data/lib/mail/parsers/received.treetop +11 -0
  122. data/lib/mail/parsers/rfc2045.rb +464 -0
  123. data/lib/mail/parsers/rfc2045.treetop +36 -0
  124. data/lib/mail/parsers/rfc2822.rb +5341 -0
  125. data/lib/mail/parsers/rfc2822.treetop +410 -0
  126. data/lib/mail/parsers/rfc2822_obsolete.rb +3757 -0
  127. data/lib/mail/parsers/rfc2822_obsolete.treetop +241 -0
  128. data/lib/mail/part.rb +116 -0
  129. data/lib/mail/parts_list.rb +51 -0
  130. data/lib/mail/patterns.rb +35 -0
  131. data/lib/mail/utilities.rb +215 -0
  132. data/lib/mail/version.rb +24 -0
  133. data/lib/mail/version_specific/ruby_1_8.rb +98 -0
  134. data/lib/mail/version_specific/ruby_1_9.rb +113 -0
  135. data/lib/tasks/corpus.rake +125 -0
  136. data/lib/tasks/treetop.rake +10 -0
  137. metadata +221 -0
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class AddressList # :nodoc:
4
+
5
+ # Mail::AddressList is the class that parses To, From and other address fields from
6
+ # emails passed into Mail.
7
+ #
8
+ # AddressList provides a way to query the groups and mailbox lists of the passed in
9
+ # string.
10
+ #
11
+ # It can supply all addresses in an array, or return each address as an address object.
12
+ #
13
+ # Mail::AddressList requires a correctly formatted group or mailbox list per RFC2822 or
14
+ # RFC822. It also handles all obsolete versions in those RFCs.
15
+ #
16
+ # list = 'ada@test.lindsaar.net, My Group: mikel@test.lindsaar.net, Bob <bob@test.lindsaar.net>;'
17
+ # a = AddressList.new(list)
18
+ # a.addresses #=> [#<Mail::Address:14943130 Address: |ada@test.lindsaar.net...
19
+ # a.group_names #=> ["My Group"]
20
+ def initialize(string)
21
+ if string.blank?
22
+ @address_nodes = []
23
+ return self
24
+ end
25
+ parser = Mail::AddressListsParser.new
26
+ if tree = parser.parse(string)
27
+ @address_nodes = tree.addresses
28
+ else
29
+ raise Mail::Field::ParseError, "AddressListsParser can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
30
+ end
31
+ end
32
+
33
+ # Returns a list of address objects from the parsed line
34
+ def addresses
35
+ @addresses ||= get_addresses.map do |address_tree|
36
+ Mail::Address.new(address_tree)
37
+ end
38
+ end
39
+
40
+ # Returns a list of all recipient syntax trees that are not part of a group
41
+ def individual_recipients # :nodoc:
42
+ @individual_recipients ||= @address_nodes - group_recipients
43
+ end
44
+
45
+ # Returns a list of all recipient syntax trees that are part of a group
46
+ def group_recipients # :nodoc:
47
+ @group_recipients ||= @address_nodes.select { |an| an.respond_to?(:group_name) }
48
+ end
49
+
50
+ # Returns the names as an array of strings of all groups
51
+ def group_names # :nodoc:
52
+ group_recipients.map { |g| g.group_name.text_value }
53
+ end
54
+
55
+ # Returns a list of address syntax trees
56
+ def address_nodes # :nodoc:
57
+ @address_nodes
58
+ end
59
+
60
+ private
61
+
62
+ def get_addresses
63
+ (individual_recipients + group_recipients.map { |g| get_group_addresses(g) }).flatten
64
+ end
65
+
66
+ def get_group_addresses(g)
67
+ if g.group_list.respond_to?(:addresses)
68
+ g.group_list.addresses
69
+ else
70
+ []
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class ContentDispositionElement # :nodoc:
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::ContentDispositionParser.new
9
+ if tree = parser.parse(cleaned(string))
10
+ @disposition_type = tree.disposition_type.text_value.downcase
11
+ @parameters = tree.parameters
12
+ else
13
+ raise Mail::Field::ParseError, "ContentDispositionElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
14
+ end
15
+ end
16
+
17
+ def disposition_type
18
+ @disposition_type
19
+ end
20
+
21
+ def parameters
22
+ @parameters
23
+ end
24
+
25
+ def cleaned(string)
26
+ string =~ /(.+);\s*$/ ? $1 : string
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class ContentLocationElement # :nodoc:
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::ContentLocationParser.new
9
+ if tree = parser.parse(string)
10
+ @location = tree.location.text_value
11
+ else
12
+ raise Mail::Field::ParseError, "ContentLocationElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
13
+ end
14
+ end
15
+
16
+ def location
17
+ @location
18
+ end
19
+
20
+ def to_s(*args)
21
+ location.to_s
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class ContentTransferEncodingElement
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::ContentTransferEncodingParser.new
9
+ case
10
+ when string.blank?
11
+ @encoding = ''
12
+ when tree = parser.parse(string.to_s.downcase)
13
+ @encoding = tree.encoding.text_value
14
+ else
15
+ raise Mail::Field::ParseError, "ContentTransferEncodingElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
16
+ end
17
+ end
18
+
19
+ def encoding
20
+ @encoding
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class ContentTypeElement # :nodoc:
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::ContentTypeParser.new
9
+ if tree = parser.parse(cleaned(string))
10
+ @main_type = tree.main_type.text_value.downcase
11
+ @sub_type = tree.sub_type.text_value.downcase
12
+ @parameters = tree.parameters
13
+ else
14
+ raise Mail::Field::ParseError, "ContentTypeElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
15
+ end
16
+ end
17
+
18
+ def main_type
19
+ @main_type
20
+ end
21
+
22
+ def sub_type
23
+ @sub_type
24
+ end
25
+
26
+ def parameters
27
+ @parameters
28
+ end
29
+
30
+ def cleaned(string)
31
+ string =~ /(.+);\s*$/ ? $1 : string
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class DateTimeElement # :nodoc:
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::DateTimeParser.new
9
+ if tree = parser.parse(string)
10
+ @date_string = tree.date.text_value
11
+ @time_string = tree.time.text_value
12
+ else
13
+ raise Mail::Field::ParseError, "DateTimeElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
14
+ end
15
+ end
16
+
17
+ def date_string
18
+ @date_string
19
+ end
20
+
21
+ def time_string
22
+ @time_string
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class EnvelopeFromElement
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::EnvelopeFromParser.new
9
+ if @tree = parser.parse(string)
10
+ @address = tree.addr_spec.text_value.strip
11
+ @date_time = ::DateTime.parse("#{tree.ctime_date.text_value}")
12
+ else
13
+ raise Mail::Field::ParseError, "EnvelopeFromElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
14
+ end
15
+ end
16
+
17
+ def tree
18
+ @tree
19
+ end
20
+
21
+ def date_time
22
+ @date_time
23
+ end
24
+
25
+ def address
26
+ @address
27
+ end
28
+
29
+ def to_s(*args)
30
+ "#{@info}; #{@date_time.to_s(*args)}"
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class MessageIdsElement
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize(string)
8
+ parser = Mail::MessageIdsParser.new
9
+ if tree = parser.parse(string)
10
+ @message_ids = tree.message_ids.map { |msg_id| clean_msg_id(msg_id.text_value) }
11
+ else
12
+ raise Mail::Field::ParseError, "MessageIdsElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
13
+ end
14
+ end
15
+
16
+ def message_ids
17
+ @message_ids
18
+ end
19
+
20
+ def message_id
21
+ @message_ids.first
22
+ end
23
+
24
+ def clean_msg_id( val )
25
+ val =~ /.*<(.*)>.*/ ; $1
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class MimeVersionElement
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::MimeVersionParser.new
9
+ if tree = parser.parse(string)
10
+ @major = tree.major.text_value
11
+ @minor = tree.minor.text_value
12
+ else
13
+ raise Mail::Field::ParseError, "MimeVersionElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
14
+ end
15
+ end
16
+
17
+ def major
18
+ @major
19
+ end
20
+
21
+ def minor
22
+ @minor
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class PhraseList
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize(string)
8
+ parser = Mail::PhraseListsParser.new
9
+ if tree = parser.parse(string)
10
+ @phrases = tree.phrases
11
+ else
12
+ raise Mail::Field::ParseError, "PhraseList can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
13
+ end
14
+ end
15
+
16
+ def phrases
17
+ @phrases.map { |p| unquote(p.text_value) }
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ module Mail
3
+ class ReceivedElement
4
+
5
+ include Mail::Utilities
6
+
7
+ def initialize( string )
8
+ parser = Mail::ReceivedParser.new
9
+ if tree = parser.parse(string)
10
+ @date_time = ::DateTime.parse("#{tree.date_time.date.text_value} #{tree.date_time.time.text_value}")
11
+ @info = tree.name_val_list.text_value
12
+ else
13
+ raise Mail::Field::ParseError, "ReceivedElement can not parse |#{string}|\nReason was: #{parser.failure_reason}\n"
14
+ end
15
+ end
16
+
17
+ def date_time
18
+ @date_time
19
+ end
20
+
21
+ def info
22
+ @info
23
+ end
24
+
25
+ def to_s(*args)
26
+ "#{@info}; #{@date_time.to_s(*args)}"
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,266 @@
1
+ # encoding: utf-8
2
+
3
+ module Mail
4
+ # Raised when attempting to decode an unknown encoding type
5
+ class UnknownEncodingType < StandardError #:nodoc:
6
+ end
7
+
8
+ module Encodings
9
+
10
+ include Mail::Patterns
11
+ extend Mail::Utilities
12
+
13
+ @transfer_encodings = {}
14
+
15
+ # Register transfer encoding
16
+ #
17
+ # Example
18
+ #
19
+ # Encodings.register "base64", Mail::Encodings::Base64
20
+ def Encodings.register(name, cls)
21
+ @transfer_encodings[get_name(name)] = cls
22
+ end
23
+
24
+ # Is the encoding we want defined?
25
+ #
26
+ # Example:
27
+ #
28
+ # Encodings.defined?(:base64) #=> true
29
+ def Encodings.defined?( str )
30
+ @transfer_encodings.include? get_name(str)
31
+ end
32
+
33
+ # Gets a defined encoding type, QuotedPrintable or Base64 for now.
34
+ #
35
+ # Each encoding needs to be defined as a Mail::Encodings::ClassName for
36
+ # this to work, allows us to add other encodings in the future.
37
+ #
38
+ # Example:
39
+ #
40
+ # Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
41
+ def Encodings.get_encoding( str )
42
+ @transfer_encodings[get_name(str)]
43
+ end
44
+
45
+ def Encodings.get_all
46
+ @transfer_encodings.values
47
+ end
48
+
49
+ def Encodings.get_name(enc)
50
+ enc = enc.to_s.gsub("-", "_").downcase
51
+ end
52
+
53
+ # Encodes a parameter value using URI Escaping, note the language field 'en' can
54
+ # be set using Mail::Configuration, like so:
55
+ #
56
+ # Mail.defaults.do
57
+ # param_encode_language 'jp'
58
+ # end
59
+ #
60
+ # The character set used for encoding will either be the value of $KCODE for
61
+ # Ruby < 1.9 or the encoding on the string passed in.
62
+ #
63
+ # Example:
64
+ #
65
+ # Mail::Encodings.param_encode("This is fun") #=> "us-ascii'en'This%20is%20fun"
66
+ def Encodings.param_encode(str)
67
+ case
68
+ when str.ascii_only? && str =~ TOKEN_UNSAFE
69
+ %Q{"#{str}"}
70
+ when str.ascii_only?
71
+ str
72
+ else
73
+ RubyVer.param_encode(str)
74
+ end
75
+ end
76
+
77
+ # Decodes a parameter value using URI Escaping.
78
+ #
79
+ # Example:
80
+ #
81
+ # Mail::Encodings.param_decode("This%20is%20fun", 'us-ascii') #=> "This is fun"
82
+ #
83
+ # str = Mail::Encodings.param_decode("This%20is%20fun", 'iso-8559-1')
84
+ # str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
85
+ # str #=> "This is fun"
86
+ def Encodings.param_decode(str, encoding)
87
+ RubyVer.param_decode(str, encoding)
88
+ end
89
+
90
+ # Decodes or encodes a string as needed for either Base64 or QP encoding types in
91
+ # the =?<encoding>?[QB]?<string>?=" format.
92
+ #
93
+ # The output type needs to be :decode to decode the input string or :encode to
94
+ # encode the input string. The character set used for encoding will either be
95
+ # the value of $KCODE for Ruby < 1.9 or the encoding on the string passed in.
96
+ #
97
+ # On encoding, will only send out Base64 encoded strings.
98
+ def Encodings.decode_encode(str, output_type)
99
+ case
100
+ when output_type == :decode
101
+ Encodings.value_decode(str)
102
+ else
103
+ if str.ascii_only?
104
+ str
105
+ else
106
+ Encodings.b_value_encode(str, find_encoding(str))
107
+ end
108
+ end
109
+ end
110
+
111
+ # Decodes a given string as Base64 or Quoted Printable, depending on what
112
+ # type it is.
113
+ #
114
+ # String has to be of the format =?<encoding>?[QB]?<string>?=
115
+ def Encodings.value_decode(str)
116
+ # Optimization: If there's no encoded-words in the string, just return it
117
+ return str unless str.index("=?")
118
+
119
+ str = str.gsub(/\?=(\s*)=\?/, '?==?') # Remove whitespaces between 'encoded-word's
120
+
121
+ # Split on white-space boundaries with capture, so we capture the white-space as well
122
+ str.split(/([ \t])/).map do |text|
123
+ if text.index('=?') .nil?
124
+ text
125
+ else
126
+ # Join QP encoded-words that are adjacent to avoid decoding partial chars
127
+ text.gsub!(/\?\=\=\?.+?\?[Qq]\?/m, '') if text =~ /\?==\?/
128
+
129
+ # Separate encoded-words with a space, so we can treat them one by one
130
+ text.gsub!(/\?\=\=\?/, '?= =?')
131
+ text.split(/ /).map do |word|
132
+ word.to_str.
133
+ gsub( /=\?.+\?[Bb]\?.+\?=/m ) { |substr| b_value_decode(substr) }.
134
+ gsub( /=\?.+\?[Qq]\?.+\?=/m ) { |substr| q_value_decode(substr) }
135
+ end
136
+ end
137
+ end.join("")
138
+ end
139
+
140
+ # Takes an encoded string of the format =?<encoding>?[QB]?<string>?=
141
+ def Encodings.unquote_and_convert_to(str, to_encoding)
142
+ original_encoding, string = split_encoding_from_string( str )
143
+
144
+ output = value_decode( str ).to_s
145
+
146
+ if original_encoding.to_s.downcase.gsub("-", "") == to_encoding.to_s.downcase.gsub("-", "")
147
+ output
148
+ elsif original_encoding && to_encoding
149
+ begin
150
+ if RUBY_VERSION >= '1.9'
151
+ output.encode(to_encoding)
152
+ else
153
+ require 'iconv'
154
+ Iconv.iconv(to_encoding, original_encoding, output).first
155
+ end
156
+ rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
157
+ # the 'from' parameter specifies a charset other than what the text
158
+ # actually is...not much we can do in this case but just return the
159
+ # unconverted text.
160
+ #
161
+ # Ditto if either parameter represents an unknown charset, like
162
+ # X-UNKNOWN.
163
+ output
164
+ end
165
+ else
166
+ output
167
+ end
168
+ end
169
+
170
+ def Encodings.address_encode(address, charset = 'utf-8')
171
+ if address.is_a?(Array)
172
+ # loop back through for each element
173
+ address.map { |a| Encodings.address_encode(a, charset) }.join(", ")
174
+ else
175
+ # find any word boundary that is not ascii and encode it
176
+ encode_non_usascii(address, charset)
177
+ end
178
+ end
179
+
180
+ def Encodings.encode_non_usascii(address, charset)
181
+ return address if address.ascii_only? or charset.nil?
182
+ us_ascii = %Q{\x00-\x7f}
183
+ # Encode any non usascii strings embedded inside of quotes
184
+ address.gsub!(/(".*?[^#{us_ascii}].+?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
185
+ # Then loop through all remaining items and encode as needed
186
+ tokens = address.split(/\s/)
187
+ map_with_index(tokens) do |word, i|
188
+ if word.ascii_only?
189
+ word
190
+ else
191
+ previous_non_ascii = tokens[i-1] && !tokens[i-1].ascii_only?
192
+ if previous_non_ascii
193
+ word = " #{word}"
194
+ end
195
+ Encodings.b_value_encode(word, charset)
196
+ end
197
+ end.join(' ')
198
+ end
199
+
200
+ # Encode a string with Base64 Encoding and returns it ready to be inserted
201
+ # as a value for a field, that is, in the =?<charset>?B?<string>?= format
202
+ #
203
+ # Example:
204
+ #
205
+ # Encodings.b_value_encode('This is あ string', 'UTF-8')
206
+ # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
207
+ def Encodings.b_value_encode(encoded_str, encoding = nil)
208
+ return encoded_str if encoded_str.to_s.ascii_only?
209
+ string, encoding = RubyVer.b_value_encode(encoded_str, encoding)
210
+ map_lines(string) do |str|
211
+ "=?#{encoding}?B?#{str.chomp}?="
212
+ end.join(" ")
213
+ end
214
+
215
+ # Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
216
+ # as a value for a field, that is, in the =?<charset>?Q?<string>?= format
217
+ #
218
+ # Example:
219
+ #
220
+ # Encodings.q_value_encode('This is あ string', 'UTF-8')
221
+ # #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
222
+ def Encodings.q_value_encode(encoded_str, encoding = nil)
223
+ return encoded_str if encoded_str.to_s.ascii_only?
224
+ string, encoding = RubyVer.q_value_encode(encoded_str, encoding)
225
+ string.gsub!("=\r\n", '') # We already have limited the string to the length we want
226
+ map_lines(string) do |str|
227
+ "=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
228
+ end.join(" ")
229
+ end
230
+
231
+ private
232
+
233
+ # Decodes a Base64 string from the "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=" format
234
+ #
235
+ # Example:
236
+ #
237
+ # Encodings.b_value_decode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
238
+ # #=> 'This is あ string'
239
+ def Encodings.b_value_decode(str)
240
+ RubyVer.b_value_decode(str)
241
+ end
242
+
243
+ # Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
244
+ #
245
+ # Example:
246
+ #
247
+ # Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
248
+ # #=> 'This is あ string'
249
+ def Encodings.q_value_decode(str)
250
+ RubyVer.q_value_decode(str).gsub(/_/, ' ')
251
+ end
252
+
253
+ def Encodings.split_encoding_from_string( str )
254
+ match = str.match(/\=\?([^?]+)?\?[QB]\?(.+)?\?\=/mi)
255
+ if match
256
+ [match[1], match[2]]
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ def Encodings.find_encoding(str)
263
+ RUBY_VERSION >= '1.9' ? str.encoding : $KCODE
264
+ end
265
+ end
266
+ end