'
subject 'First multipart email sent with Mail'
text_part do
body 'Here is the attachment you wanted'
end
html_part do
content_type 'text/html; charset=UTF-8'
body 'Funky Title
Here is the attachment you wanted
'
end
add_file '/path/to/myfile.pdf'
end
@round_tripped_mail = Mail.new(@mail.encoded)
@round_tripped_mail.attachments.length #=> 1
@round_tripped_mail.attachments.first.filename #=> 'myfile.pdf'
```
See "Testing and extracting attachments" above for more details.
## Using Mail with Testing or Spec'ing Libraries
If mail is part of your system, you'll need a way to test it without actually
sending emails, the TestMailer can do this for you.
```ruby
require 'mail'
=> true
Mail.defaults do
delivery_method :test
end
=> #
Mail::TestMailer.deliveries
=> []
Mail.deliver do
to 'mikel@me.com'
from 'you@you.com'
subject 'testing'
body 'hello'
end
=> # 1
Mail::TestMailer.deliveries.first
=> # []
```
There is also a set of RSpec matchers stolen/inspired by Shoulda's ActionMailer matchers (you'll want to set delivery_method
as above too):
```ruby
Mail.defaults do
delivery_method :test # in practice you'd do this in spec_helper.rb
end
describe "sending an email" do
include Mail::Matchers
before(:each) do
Mail::TestMailer.deliveries.clear
Mail.deliver do
to ['mikel@me.com', 'mike2@me.com']
from 'you@you.com'
subject 'testing'
body 'hello'
end
end
it { is_expected.to have_sent_email } # passes if any email at all was sent
it { is_expected.to have_sent_email.from('you@you.com') }
it { is_expected.to have_sent_email.to('mike1@me.com') }
# can specify a list of recipients...
it { is_expected.to have_sent_email.to(['mike1@me.com', 'mike2@me.com']) }
# ...or chain recipients together
it { is_expected.to have_sent_email.to('mike1@me.com').to('mike2@me.com') }
it { is_expected.to have_sent_email.with_subject('testing') }
it { is_expected.to have_sent_email.with_body('hello') }
# Can match subject or body with a regex
# (or anything that responds_to? :match)
it { is_expected.to have_sent_email.matching_subject(/test(ing)?/) }
it { is_expected.to have_sent_email.matching_body(/h(a|e)llo/) }
# Can chain together modifiers
# Note that apart from recipients, repeating a modifier overwrites old value.
it { is_expected.to have_sent_email.from('you@you.com').to('mike1@me.com').matching_body(/hell/)
# test for attachments
# ... by specific attachment
it { is_expected.to have_sent_email.with_attachments(my_attachment) }
# ... or any attachment
it { is_expected.to have_sent_email.with_attachments(any_attachment) }
# ... or attachment with filename
it { is_expected.to have_sent_email.with_attachments(an_attachment_with_filename('file.txt')) }
# ... or attachment with mime_type
it { is_expected.to have_sent_email.with_attachments(an_attachment_with_mime_type('application/pdf')) }
# ... by array of attachments
it { is_expected.to have_sent_email.with_attachments([my_attachment1, my_attachment2]) } #note that order is important
#... by presence
it { is_expected.to have_sent_email.with_any_attachments }
#... or by absence
it { is_expected.to have_sent_email.with_no_attachments }
end
```
## Excerpts from TREC Spam Corpus 2005
The spec fixture files in spec/fixtures/emails/from_trec_2005 are from the
2005 TREC Public Spam Corpus. They remain copyrighted under the terms of
that project and license agreement. They are used in this project to verify
and describe the development of this email parser implementation.
http://plg.uwaterloo.ca/~gvcormac/treccorpus/
They are used as allowed by 'Permitted Uses, Clause 3':
"Small excerpts of the information may be displayed to others
or published in a scientific or technical context, solely for
the purpose of describing the research and development and
related issues."
-- http://plg.uwaterloo.ca/~gvcormac/treccorpus/
## License
(The MIT License)
Copyright (c) 2009-2016 Mikel Lindsaar
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
mail-2.8.1/Rakefile 0000664 0000000 0000000 00000001024 14363721317 0014115 0 ustar 00root root 0000000 0000000 if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"]
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __FILE__)
end
require 'rubygems'
require 'bundler/setup'
require 'rake/testtask'
require 'rspec/core/rake_task'
desc "Build a gem file"
task :build do
system "gem build mail.gemspec"
end
task :default => :spec
RSpec::Core::RakeTask.new(:spec) do |t|
t.ruby_opts = '-w'
t.rspec_opts = %w(--backtrace --color)
end
# load custom rake tasks
Dir["#{File.dirname(__FILE__)}/tasks/**/*.rake"].sort.each { |ext| load ext }
mail-2.8.1/lib/ 0000775 0000000 0000000 00000000000 14363721317 0013221 5 ustar 00root root 0000000 0000000 mail-2.8.1/lib/mail.rb 0000664 0000000 0000000 00000002765 14363721317 0014502 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
module Mail # :doc:
require 'date'
require 'shellwords'
require 'uri'
require 'net/smtp'
require 'mini_mime'
require 'mail/version'
require 'mail/indifferent_hash'
require 'mail/multibyte'
require 'mail/constants'
require 'mail/utilities'
require 'mail/configuration'
@@autoloads = {}
def self.register_autoload(name, path)
@@autoloads[name] = path
autoload(name, path)
end
# This runs through the autoload list and explictly requires them for you.
# Useful when running mail in a threaded process.
#
# Usage:
#
# require 'mail'
# Mail.eager_autoload!
def self.eager_autoload!
@@autoloads.each { |_,path| require(path) }
end
# Autoload mail send and receive classes.
require 'mail/network'
require 'mail/message'
require 'mail/part'
require 'mail/header'
require 'mail/parts_list'
require 'mail/attachments_list'
require 'mail/body'
require 'mail/field'
require 'mail/field_list'
require 'mail/envelope'
# Autoload header field elements and transfer encodings.
require 'mail/elements'
require 'mail/encodings'
require 'mail/encodings/base64'
require 'mail/encodings/quoted_printable'
require 'mail/encodings/unix_to_unix'
require 'mail/matchers/has_sent_mail'
require 'mail/matchers/attachment_matchers.rb'
# Deprecated will be removed in 3.0 release
require 'mail/check_delivery_params'
# Finally... require all the Mail.methods
require 'mail/mail'
end
mail-2.8.1/lib/mail/ 0000775 0000000 0000000 00000000000 14363721317 0014143 5 ustar 00root root 0000000 0000000 mail-2.8.1/lib/mail/attachments_list.rb 0000664 0000000 0000000 00000006631 14363721317 0020044 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Mail
class AttachmentsList < Array
def initialize(parts_list)
@parts_list = parts_list
@content_disposition_type = 'attachment'
parts_list.map { |p|
if p.mime_type == 'message/rfc822'
Mail.new(p.body.encoded).attachments
elsif p.parts.empty?
p if p.attachment?
else
p.attachments
end
}.flatten.compact.each { |a| self << a }
self
end
def inline
@content_disposition_type = 'inline'
self
end
# Returns the attachment by filename or at index.
#
# mail.attachments['test.png'] = File.read('test.png')
# mail.attachments['test.jpg'] = File.read('test.jpg')
#
# mail.attachments['test.png'].filename #=> 'test.png'
# mail.attachments[1].filename #=> 'test.jpg'
def [](index_value)
if index_value.is_a?(Integer)
self.fetch(index_value)
else
self.select { |a| a.filename == index_value }.first
end
end
def []=(name, value)
encoded_name = Mail::Encodings.decode_encode name, :encode
default_values = { :content_type => "#{set_mime_type(name)}; filename=\"#{encoded_name}\"",
:content_transfer_encoding => "#{guess_encoding}",
:content_disposition => "#{@content_disposition_type}; filename=\"#{encoded_name}\"" }
if value.is_a?(Hash)
if path = value.delete(:filename)
value[:content] ||= File.open(path, 'rb') { |f| f.read }
end
default_values[:body] = value.delete(:content) if value[:content]
default_values[:body] = value.delete(:data) if value[:data]
encoding = value.delete(:transfer_encoding) || value.delete(:encoding)
if encoding
if Mail::Encodings.defined? encoding
default_values[:content_transfer_encoding] = encoding
else
raise "Do not know how to handle Content Transfer Encoding #{encoding}, please choose either quoted-printable or base64"
end
end
if value[:mime_type]
default_values[:content_type] = value.delete(:mime_type)
@mime_type = MiniMime.lookup_by_content_type(default_values[:content_type])
default_values[:content_transfer_encoding] ||= guess_encoding
end
hash = default_values.merge(value)
else
default_values[:body] = value
hash = default_values
end
if hash[:body].respond_to? :force_encoding and hash[:body].respond_to? :valid_encoding?
if not hash[:body].valid_encoding? and default_values[:content_transfer_encoding].casecmp('binary').zero?
hash[:body] = hash[:body].dup if hash[:body].frozen?
hash[:body].force_encoding("BINARY")
end
end
attachment = Part.new(hash)
attachment.add_content_id(hash[:content_id])
@parts_list << attachment
end
# Uses the mime type to try and guess the encoding, if it is a binary type, or unknown, then we
# set it to binary, otherwise as set to plain text
def guess_encoding
if @mime_type && !@mime_type.binary?
"7bit"
else
"binary"
end
end
def set_mime_type(filename)
filename = filename.encode(Encoding::UTF_8) if filename.respond_to?(:encode)
@mime_type = MiniMime.lookup_by_filename(filename)
@mime_type && @mime_type.content_type
end
end
end
mail-2.8.1/lib/mail/body.rb 0000664 0000000 0000000 00000022511 14363721317 0015426 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
module Mail
# = Body
#
# The body is where the text of the email is stored. Mail treats the body
# as a single object. The body itself has no information about boundaries
# used in the MIME standard, it just looks at its content as either a single
# block of text, or (if it is a multipart message) as an array of blocks of text.
#
# A body has to be told to split itself up into a multipart message by calling
# #split with the correct boundary. This is because the body object has no way
# of knowing what the correct boundary is for itself (there could be many
# boundaries in a body in the case of a nested MIME text).
#
# Once split is called, Mail::Body will slice itself up on this boundary,
# assigning anything that appears before the first part to the preamble, and
# anything that appears after the closing boundary to the epilogue, then
# each part gets initialized into a Mail::Part object.
#
# The boundary that is used to split up the Body is also stored in the Body
# object for use on encoding itself back out to a string. You can
# overwrite this if it needs to be changed.
#
# On encoding, the body will return the preamble, then each part joined by
# the boundary, followed by a closing boundary string and then the epilogue.
class Body
def initialize(string = '')
@boundary = nil
@preamble = nil
@epilogue = nil
@charset = nil
@part_sort_order = [ "text/plain", "text/enriched", "text/html", "multipart/alternative" ]
@parts = Mail::PartsList.new
if Utilities.blank?(string)
@raw_source = ''
else
# Do join first incase we have been given an Array in Ruby 1.9
if string.respond_to?(:join)
@raw_source = ::Mail::Utilities.to_crlf(string.join(''))
elsif string.respond_to?(:to_s)
@raw_source = ::Mail::Utilities.to_crlf(string.to_s)
else
raise "You can only assign a string or an object that responds_to? :join or :to_s to a body."
end
end
@encoding = default_encoding
set_charset
end
def init_with(coder)
coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
@parts = Mail::PartsList.new(coder['parts'])
end
# Matches this body with another body. Also matches the decoded value of this
# body with a string.
#
# Examples:
#
# body = Mail::Body.new('The body')
# body == body #=> true
#
# body = Mail::Body.new('The body')
# body == 'The body' #=> true
#
# body = Mail::Body.new("VGhlIGJvZHk=\n")
# body.encoding = 'base64'
# body == "The body" #=> true
def ==(other)
if other.class == String
self.decoded == other
else
super
end
end
# Accepts a string and performs a regular expression against the decoded text
#
# Examples:
#
# body = Mail::Body.new('The body')
# body =~ /The/ #=> 0
#
# body = Mail::Body.new("VGhlIGJvZHk=\n")
# body.encoding = 'base64'
# body =~ /The/ #=> 0
def =~(regexp)
self.decoded =~ regexp
end
# Accepts a string and performs a regular expression against the decoded text
#
# Examples:
#
# body = Mail::Body.new('The body')
# body.match(/The/) #=> #
#
# body = Mail::Body.new("VGhlIGJvZHk=\n")
# body.encoding = 'base64'
# body.match(/The/) #=> #
def match(regexp)
self.decoded.match(regexp)
end
# Accepts anything that responds to #to_s and checks if it's a substring of the decoded text
#
# Examples:
#
# body = Mail::Body.new('The body')
# body.include?('The') #=> true
#
# body = Mail::Body.new("VGhlIGJvZHk=\n")
# body.encoding = 'base64'
# body.include?('The') #=> true
def include?(other)
self.decoded.include?(other.to_s)
end
# Allows you to set the sort order of the parts, overriding the default sort order.
# Defaults to 'text/plain', then 'text/enriched', then 'text/html', then 'multipart/alternative'
# with any other content type coming after.
def set_sort_order(order)
@part_sort_order = order
end
# Allows you to sort the parts according to the default sort order, or the sort order you
# set with :set_sort_order.
#
# sort_parts! is also called from :encode, so there is no need for you to call this explicitly
def sort_parts!
@parts.each do |p|
p.body.set_sort_order(@part_sort_order)
p.body.sort_parts!
end
@parts.sort!(@part_sort_order)
end
def negotiate_best_encoding(message_encoding, allowed_encodings = nil)
Mail::Encodings::TransferEncoding.negotiate(message_encoding, encoding, raw_source, allowed_encodings)
end
# Returns a body encoded using transfer_encoding. Multipart always uses an
# identiy encoding (i.e. no encoding).
# Calling this directly is not a good idea, but supported for compatibility
# TODO: Validate that preamble and epilogue are valid for requested encoding
def encoded(transfer_encoding = nil)
if multipart?
self.sort_parts!
encoded_parts = parts.map { |p| p.encoded }
([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
else
dec = Mail::Encodings.get_encoding(encoding)
enc =
if Utilities.blank?(transfer_encoding)
dec
else
negotiate_best_encoding(transfer_encoding)
end
if dec.nil?
# Cannot decode, so skip normalization
raw_source
else
# Decode then encode to normalize and allow transforming
# from base64 to Q-P and vice versa
decoded = dec.decode(raw_source)
if defined?(Encoding) && charset && charset != "US-ASCII"
decoded = decoded.encode(charset)
decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
end
enc.encode(decoded)
end
end
end
def decoded
if !Encodings.defined?(encoding)
raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
else
Encodings.get_encoding(encoding).decode(raw_source)
end
end
def to_s
decoded
end
def encoding(val = nil)
if val
self.encoding = val
else
@encoding
end
end
def encoding=( val )
@encoding =
if val == "text" || Utilities.blank?(val)
default_encoding
else
val
end
end
# Returns the raw source that the body was initialized with, without
# any tampering
attr_reader :raw_source
# Returns parts of the body
attr_reader :parts
# Returns and sets the original character encoding
attr_accessor :charset
# Returns and sets the preamble as a string (any text that is before the first MIME boundary)
attr_accessor :preamble
# Returns and sets the epilogue as a string (any text that is after the last MIME boundary)
attr_accessor :epilogue
# Returns and sets the boundary used by the body
# Allows you to change the boundary of this Body object
attr_accessor :boundary
# Returns true if there are parts defined in the body
def multipart?
true unless parts.empty?
end
def <<( val )
if @parts
@parts << val
else
@parts = Mail::PartsList.new[val]
end
end
def split!(boundary)
self.boundary = boundary
parts = extract_parts
# Make the preamble equal to the preamble (if any)
self.preamble = parts[0].to_s.strip
# Make the epilogue equal to the epilogue (if any)
self.epilogue = parts[-1].to_s.strip
parts[1...-1].to_a.each { |part| @parts << Mail::Part.new(part) }
self
end
def ascii_only?
unless defined? @ascii_only
@ascii_only = raw_source.ascii_only?
end
@ascii_only
end
def empty?
!!raw_source.to_s.empty?
end
def default_encoding
ascii_only? ? '7bit' : '8bit'
end
private
# split parts by boundary, ignore first part if empty, append final part when closing boundary was missing
def extract_parts
parts_regex = /
(?: # non-capturing group
\A | # start of string OR
\r\n # line break
)
(
--#{Regexp.escape(boundary || "")} # boundary delimiter
(?:--)? # with non-capturing optional closing
)
(?=\s*$) # lookahead matching zero or more spaces followed by line-ending
/x
parts = raw_source.split(parts_regex).each_slice(2).to_a
parts.each_with_index { |(part, _), index| parts.delete_at(index) if index > 0 && Utilities.blank?(part) }
if parts.size > 1
final_separator = parts[-2][1]
parts << [""] if final_separator != "--#{boundary}--"
end
parts.map(&:first)
end
def crlf_boundary
"\r\n--#{boundary}\r\n"
end
def end_boundary
"\r\n--#{boundary}--\r\n"
end
def set_charset
@charset = ascii_only? ? 'US-ASCII' : nil
end
end
end
mail-2.8.1/lib/mail/check_delivery_params.rb 0000664 0000000 0000000 00000003624 14363721317 0021020 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
#
# This whole class and associated specs is deprecated and will go away in the version 3 release of mail.
module Mail
module CheckDeliveryParams #:nodoc:
class << self
extend Gem::Deprecate
def check(mail)
envelope = Mail::SmtpEnvelope.new(mail)
[ envelope.from,
envelope.to,
envelope.message ]
end
deprecate :check, 'Mail::SmtpEnvelope.new created in commit c106bebea066782a72e4f24dd37b532d95773df7', 2023, 6
def check_from(addr)
mail = Mail.new(from: 'deprecated@example.com', to: 'deprecated@example.com')
Mail::SmtpEnvelope.new(mail).send(:validate_addr, 'From', addr)
end
deprecate :check_from, :none, 2023, 6
def check_to(addrs)
mail = Mail.new(from: 'deprecated@example.com', to: 'deprecated@example.com')
Array(addrs).map do |addr|
Mail::SmtpEnvelope.new(mail).send(:validate_addr, 'To', addr)
end
end
deprecate :check_to, :none, 2023, 6
def check_addr(addr_name, addr)
mail = Mail.new(from: 'deprecated@example.com', to: 'deprecated@example.com')
Mail::SmtpEnvelope.new(mail).send(:validate_addr, addr_name, addr)
end
deprecate :check_addr, :none, 2023, 6
def validate_smtp_addr(addr)
if addr
if addr.bytesize > 2048
yield 'may not exceed 2kB'
end
if /[\r\n]/ =~ addr
yield 'may not contain CR or LF line breaks'
end
end
addr
end
deprecate :validate_smtp_addr, :none, 2023, 6
def check_message(message)
message = message.encoded if message.respond_to?(:encoded)
if Utilities.blank?(message)
raise ArgumentError, 'An encoded message is required to send an email'
end
message
end
deprecate :check_message, :none, 2023, 6
end
end
end
mail-2.8.1/lib/mail/configuration.rb 0000664 0000000 0000000 00000003365 14363721317 0017346 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
#
# Thanks to Nicolas Fouché for this wrapper
#
require 'singleton'
module Mail
# The Configuration class is a Singleton used to hold the default
# configuration for all Mail objects.
#
# Each new mail object gets a copy of these values at initialization
# which can be overwritten on a per mail object basis.
class Configuration
include Singleton
def initialize
@delivery_method = nil
@retriever_method = nil
super
end
def delivery_method(method = nil, settings = {})
return @delivery_method if @delivery_method && method.nil?
@delivery_method = lookup_delivery_method(method).new(settings)
end
def lookup_delivery_method(method)
case method.is_a?(String) ? method.to_sym : method
when nil
Mail::SMTP
when :smtp
Mail::SMTP
when :sendmail
Mail::Sendmail
when :exim
Mail::Exim
when :file
Mail::FileDelivery
when :smtp_connection
Mail::SMTPConnection
when :test
Mail::TestMailer
when :logger
Mail::LoggerDelivery
else
method
end
end
def retriever_method(method = nil, settings = {})
return @retriever_method if @retriever_method && method.nil?
@retriever_method = lookup_retriever_method(method).new(settings)
end
def lookup_retriever_method(method)
case method
when nil
Mail::POP3
when :pop3
Mail::POP3
when :imap
Mail::IMAP
when :test
Mail::TestRetriever
else
method
end
end
def param_encode_language(value = nil)
value ? @encode_language = value : @encode_language ||= 'en'
end
end
end
mail-2.8.1/lib/mail/constants.rb 0000664 0000000 0000000 00000004650 14363721317 0016511 0 ustar 00root root 0000000 0000000 # encoding: us-ascii
# frozen_string_literal: true
module Mail
module Constants
white_space = %Q|\x9\x20|
text = %Q|\x1-\x8\xB\xC\xE-\x7f|
field_name = %Q|\x21-\x39\x3b-\x7e|
qp_safe = %Q|\x20-\x3c\x3e-\x7e|
aspecial = %Q|()<>[]:;@\\,."| # RFC5322
tspecial = %Q|()<>@,;:\\"/[]?=| # RFC2045
sp = %Q| |
control = %Q|\x00-\x1f\x7f-\xff|
if control.respond_to?(:force_encoding)
control = control.dup.force_encoding(Encoding::BINARY)
end
LAX_CRLF = /\r?\n/
WSP = /[#{white_space}]/
FWS = /#{LAX_CRLF}#{WSP}*/
UNFOLD_WS = /#{LAX_CRLF}(#{WSP})/m
TEXT = /[#{text}]/ # + obs-text
FIELD_NAME = /[#{field_name}]+/
FIELD_PREFIX = /\A(#{FIELD_NAME})/
FIELD_BODY = /.+/m
FIELD_LINE = /^[#{field_name}]+:\s*.+$/
FIELD_SPLIT = /^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/
HEADER_LINE = /^([#{field_name}]+:\s*.+)$/
HEADER_SPLIT = /#{LAX_CRLF}(?!#{WSP})/
QP_UNSAFE = /[^#{qp_safe}]/
QP_SAFE = /[#{qp_safe}]/
CONTROL_CHAR = /[#{control}]/n
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{sp}]/n
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{sp}]/n
ENCODED_VALUE = %r{
\=\? # literal =?
([^?]+) #
\? # literal ?
([QB]) # either a "Q" or a "B"
\? # literal ?
.*? # lazily match all characters
\?\= # literal ?=
}mix # m is multi-line, i is case-insensitive, x is free-spacing
FULL_ENCODED_VALUE = %r{ # Identical to ENCODED_VALUE but captures the whole rather than components of
(
\=\? # literal =?
[^?]+ #
\? # literal ?
[QB] # either a "Q" or a "B"
\? # literal ?
.*? # lazily match all characters
\?\= # literal ?=
)
}mix # m is multi-line, i is case-insensitive, x is free-spacing
EMPTY = ''
SPACE = ' '
UNDERSCORE = '_'
HYPHEN = '-'
COLON = ':'
ASTERISK = '*'
CRLF = "\r\n"
CR = "\r"
LF = "\n"
CR_ENCODED = "=0D"
LF_ENCODED = "=0A"
CAPITAL_M = 'M'
EQUAL_LF = "=\n"
NULL_SENDER = '<>'
Q_VALUES = ['Q','q']
B_VALUES = ['B','b']
end
end
mail-2.8.1/lib/mail/elements.rb 0000664 0000000 0000000 00000001700 14363721317 0016302 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Mail
register_autoload :Address, 'mail/elements/address'
register_autoload :AddressList, 'mail/elements/address_list'
register_autoload :ContentDispositionElement, 'mail/elements/content_disposition_element'
register_autoload :ContentLocationElement, 'mail/elements/content_location_element'
register_autoload :ContentTransferEncodingElement, 'mail/elements/content_transfer_encoding_element'
register_autoload :ContentTypeElement, 'mail/elements/content_type_element'
register_autoload :DateTimeElement, 'mail/elements/date_time_element'
register_autoload :EnvelopeFromElement, 'mail/elements/envelope_from_element'
register_autoload :MessageIdsElement, 'mail/elements/message_ids_element'
register_autoload :MimeVersionElement, 'mail/elements/mime_version_element'
register_autoload :PhraseList, 'mail/elements/phrase_list'
register_autoload :ReceivedElement, 'mail/elements/received_element'
end
mail-2.8.1/lib/mail/elements/ 0000775 0000000 0000000 00000000000 14363721317 0015757 5 ustar 00root root 0000000 0000000 mail-2.8.1/lib/mail/elements/address.rb 0000664 0000000 0000000 00000017722 14363721317 0017742 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/address_lists_parser'
require 'mail/constants'
require 'mail/utilities'
module Mail
# Mail::Address handles all email addresses in Mail. It takes an email address string
# and parses it, breaking it down into its component parts and allowing you to get the
# address, comments, display name, name, local part, domain part and fully formatted
# address.
#
# Mail::Address requires a correctly formatted email address per RFC2822 or RFC822. It
# handles all obsolete versions including obsolete domain routing on the local part.
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.format #=> 'Mikel Lindsaar (My email address)'
# a.address #=> 'mikel@test.lindsaar.net'
# a.display_name #=> 'Mikel Lindsaar'
# a.local #=> 'mikel'
# a.domain #=> 'test.lindsaar.net'
# a.comments #=> ['My email address']
# a.to_s #=> 'Mikel Lindsaar (My email address)'
class Address
def initialize(value = nil)
if value.nil?
@parsed = false
@data = nil
else
parse(value)
end
end
# Returns the raw input of the passed in string, this is before it is passed
# by the parser.
def raw
@data.raw
end
# Returns a correctly formatted address for the email going out. If given
# an incorrectly formatted address as input, Mail::Address will do its best
# to format it correctly. This includes quoting display names as needed and
# putting the address in angle brackets etc.
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.format #=> 'Mikel Lindsaar (My email address)'
def format(output_type = :decode)
parse unless @parsed
if @data.nil?
Constants::EMPTY
elsif name = display_name(output_type)
[Utilities.quote_phrase(name), "<#{address(output_type)}>", format_comments].compact.join(Constants::SPACE)
elsif a = address(output_type)
[a, format_comments].compact.join(Constants::SPACE)
else
raw
end
end
# Returns the address that is in the address itself. That is, the
# local@domain string, without any angle brackets or the like.
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.address #=> 'mikel@test.lindsaar.net'
def address(output_type = :decode)
parse unless @parsed
if d = domain(output_type)
"#{local(output_type)}@#{d}"
else
local(output_type)
end
end
# Provides a way to assign an address to an already made Mail::Address object.
#
# a = Address.new
# a.address = 'Mikel Lindsaar (My email address) '
# a.address #=> 'mikel@test.lindsaar.net'
def address=(value)
parse(value)
end
# Returns the display name of the email address passed in.
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.display_name #=> 'Mikel Lindsaar'
def display_name(output_type = :decode)
parse unless @parsed
@display_name ||= get_display_name
Encodings.decode_encode(@display_name.to_s, output_type) if @display_name
end
# Provides a way to assign a display name to an already made Mail::Address object.
#
# a = Address.new
# a.address = 'mikel@test.lindsaar.net'
# a.display_name = 'Mikel Lindsaar'
# a.format #=> 'Mikel Lindsaar '
def display_name=( str )
@display_name = str.nil? ? nil : str.dup # in case frozen
end
# Returns the local part (the left hand side of the @ sign in the email address) of
# the address
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.local #=> 'mikel'
def local(output_type = :decode)
parse unless @parsed
Encodings.decode_encode("#{@data.obs_domain_list}#{get_local.strip}", output_type) if get_local
end
# Returns the domain part (the right hand side of the @ sign in the email address) of
# the address
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.domain #=> 'test.lindsaar.net'
def domain(output_type = :decode)
parse unless @parsed
Encodings.decode_encode(strip_all_comments(get_domain), output_type) if get_domain
end
# Returns an array of comments that are in the email, or nil if there
# are no comments
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.comments #=> ['My email address']
#
# b = Address.new('Mikel Lindsaar ')
# b.comments #=> nil
def comments
parse unless @parsed
comments = get_comments
if comments.nil? || comments.none?
nil
else
comments.map { |c| c.squeeze(Constants::SPACE) }
end
end
# Sometimes an address will not have a display name, but might have the name
# as a comment field after the address. This returns that name if it exists.
#
# a = Address.new('mikel@test.lindsaar.net (Mikel Lindsaar)')
# a.name #=> 'Mikel Lindsaar'
def name
parse unless @parsed
get_name
end
# Returns the format of the address, or returns nothing
#
# a = Address.new('Mikel Lindsaar (My email address) ')
# a.format #=> 'Mikel Lindsaar (My email address)'
def to_s
parse unless @parsed
format
end
# Shows the Address object basic details, including the Address
# a = Address.new('Mikel (My email) ')
# a.inspect #=> "# (My email)| >"
def inspect
parse unless @parsed
"#<#{self.class}:#{self.object_id} Address: |#{to_s}| >"
end
def encoded
format :encode
end
def decoded
format :decode
end
def group
@data && @data.group
end
private
def parse(value = nil)
@parsed = true
@data = nil
case value
when Mail::Parsers::AddressListsParser::AddressStruct
@data = value
when String
unless Utilities.blank?(value)
address_list = Mail::Parsers::AddressListsParser.parse(value)
@data = address_list.addresses.first
end
end
end
def strip_all_comments(string)
unless Utilities.blank?(comments)
comments.each do |comment|
string = string.gsub("(#{comment})", Constants::EMPTY)
end
end
string.strip
end
def strip_domain_comments(value)
unless Utilities.blank?(comments)
comments.each do |comment|
if @data.domain && @data.domain.include?("(#{comment})")
value = value.gsub("(#{comment})", Constants::EMPTY)
end
end
end
value.to_s.strip
end
def get_display_name
if @data && @data.display_name
str = strip_all_comments(@data.display_name.to_s)
elsif @data && @data.comments && @data.domain
str = strip_domain_comments(format_comments)
end
str unless Utilities.blank?(str)
end
def get_name
if display_name
str = display_name
elsif comments
str = "(#{comments.join(Constants::SPACE).squeeze(Constants::SPACE)})"
end
Utilities.unparen(str) unless Utilities.blank?(str)
end
def format_comments
if comments
comment_text = comments.map {|c| Utilities.escape_paren(c) }.join(Constants::SPACE).squeeze(Constants::SPACE)
@format_comments ||= "(#{comment_text})"
else
nil
end
end
def get_local
@data && @data.local
end
def get_domain
@data && @data.domain
end
def get_comments
@data && @data.comments
end
end
end
mail-2.8.1/lib/mail/elements/address_list.rb 0000664 0000000 0000000 00000002344 14363721317 0020767 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/address_lists_parser'
module Mail
class AddressList #:nodoc:
attr_reader :addresses, :group_names
# Mail::AddressList is the class that parses To, From and other address fields from
# emails passed into Mail.
#
# AddressList provides a way to query the groups and mailbox lists of the passed in
# string.
#
# It can supply all addresses in an array, or return each address as an address object.
#
# Mail::AddressList requires a correctly formatted group or mailbox list per RFC2822 or
# RFC822. It also handles all obsolete versions in those RFCs.
#
# list = 'ada@test.lindsaar.net, My Group: mikel@test.lindsaar.net, Bob ;'
# a = AddressList.new(list)
# a.addresses #=> [# ["My Group"]
def initialize(string)
address_list = Parsers::AddressListsParser.parse(string)
@addresses = address_list.addresses.map { |a| Address.new(a) }
@group_names = address_list.group_names
end
def addresses_grouped_by_group
addresses.select(&:group).group_by(&:group)
end
end
end
mail-2.8.1/lib/mail/elements/content_disposition_element.rb 0000664 0000000 0000000 00000001033 14363721317 0024110 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/content_disposition_parser'
module Mail
class ContentDispositionElement #:nodoc:
attr_reader :disposition_type, :parameters
def initialize(string)
content_disposition = Mail::Parsers::ContentDispositionParser.parse(cleaned(string))
@disposition_type = content_disposition.disposition_type
@parameters = content_disposition.parameters
end
private
def cleaned(string)
string =~ /(.+);\s*$/ ? $1 : string
end
end
end
mail-2.8.1/lib/mail/elements/content_location_element.rb 0000664 0000000 0000000 00000000533 14363721317 0023360 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/content_location_parser'
module Mail
class ContentLocationElement #:nodoc:
attr_reader :location
def initialize(string)
@location = Mail::Parsers::ContentLocationParser.parse(string).location
end
def to_s(*args)
location.to_s
end
end
end
mail-2.8.1/lib/mail/elements/content_transfer_encoding_element.rb 0000664 0000000 0000000 00000000503 14363721317 0025237 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/content_transfer_encoding_parser'
module Mail
class ContentTransferEncodingElement #:nodoc:
attr_reader :encoding
def initialize(string)
@encoding = Mail::Parsers::ContentTransferEncodingParser.parse(string).encoding
end
end
end
mail-2.8.1/lib/mail/elements/content_type_element.rb 0000664 0000000 0000000 00000001075 14363721317 0022533 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/content_type_parser'
module Mail
class ContentTypeElement #:nodoc:
attr_reader :main_type, :sub_type, :parameters
def initialize(string)
content_type = Mail::Parsers::ContentTypeParser.parse(cleaned(string))
@main_type = content_type.main_type
@sub_type = content_type.sub_type
@parameters = content_type.parameters
end
private
def cleaned(string)
if string =~ /;\s*$/
$`
else
string
end
end
end
end
mail-2.8.1/lib/mail/elements/date_time_element.rb 0000664 0000000 0000000 00000000563 14363721317 0021754 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/date_time_parser'
module Mail
class DateTimeElement #:nodoc:
attr_reader :date_string, :time_string
def initialize(string)
date_time = Mail::Parsers::DateTimeParser.parse(string)
@date_string = date_time.date_string
@time_string = date_time.time_string
end
end
end
mail-2.8.1/lib/mail/elements/envelope_from_element.rb 0000664 0000000 0000000 00000002034 14363721317 0022654 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/envelope_from_parser'
require 'date'
module Mail
class EnvelopeFromElement #:nodoc:
attr_reader :date_time, :address
def initialize(string)
envelope_from = Mail::Parsers::EnvelopeFromParser.parse(string)
@address = envelope_from.address
@date_time = ::DateTime.parse(envelope_from.ctime_date) if envelope_from.ctime_date
end
# RFC 4155:
# a timestamp indicating the UTC date and time when the message
# was originally received, conformant with the syntax of the
# traditional UNIX 'ctime' output sans timezone (note that the
# use of UTC precludes the need for a timezone indicator);
def formatted_date_time
if date_time
if date_time.respond_to?(:ctime)
date_time.ctime
else
date_time.strftime '%a %b %e %T %Y'
end
end
end
def to_s
if date_time
"#{address} #{formatted_date_time}"
else
address
end
end
end
end
mail-2.8.1/lib/mail/elements/message_ids_element.rb 0000664 0000000 0000000 00000001112 14363721317 0022273 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/message_ids_parser'
require 'mail/utilities'
module Mail
class MessageIdsElement #:nodoc:
def self.parse(string)
new(string).tap(&:message_ids)
end
attr_reader :message_ids
def initialize(string)
@message_ids = parse(string)
end
def message_id
message_ids.first
end
private
def parse(string)
if Utilities.blank? string
[]
else
Mail::Parsers::MessageIdsParser.parse(string).message_ids
end
end
end
end
mail-2.8.1/lib/mail/elements/mime_version_element.rb 0000664 0000000 0000000 00000000541 14363721317 0022511 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/mime_version_parser'
module Mail
class MimeVersionElement #:nodoc:
attr_reader :major, :minor
def initialize(string)
mime_version = Mail::Parsers::MimeVersionParser.parse(string)
@major = mime_version.major
@minor = mime_version.minor
end
end
end
mail-2.8.1/lib/mail/elements/phrase_list.rb 0000664 0000000 0000000 00000000646 14363721317 0020627 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/phrase_lists_parser'
require 'mail/utilities'
module Mail
class PhraseList #:nodoc:
attr_reader :phrases
def initialize(string)
@phrases =
if Utilities.blank? string
[]
else
Mail::Parsers::PhraseListsParser.parse(string).phrases.map { |p| Mail::Utilities.unquote(p) }
end
end
end
end
mail-2.8.1/lib/mail/elements/received_element.rb 0000664 0000000 0000000 00000001603 14363721317 0021603 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/parsers/received_parser'
require 'mail/utilities'
require 'date'
module Mail
class ReceivedElement #:nodoc:
attr_reader :info, :date_time
def initialize(string)
if Utilities.blank? string
@date_time = nil
@info = nil
else
received = Mail::Parsers::ReceivedParser.parse(string)
@date_time = datetime_for(received)
@info = received.info
end
end
def to_s(*args)
"#{info}; #{date_time.to_s(*args)}"
end
private
def datetime_for(received)
::DateTime.parse("#{received.date} #{received.time}")
rescue ArgumentError => e
raise e unless e.message == 'invalid date'
warn "WARNING: Invalid date field for received element (#{received.date} #{received.time}): #{e.class}: #{e.message}"
nil
end
end
end
mail-2.8.1/lib/mail/encodings.rb 0000664 0000000 0000000 00000023100 14363721317 0016435 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
module Mail
# Raised when attempting to decode an unknown encoding type
class UnknownEncodingType < StandardError #:nodoc:
end
module Encodings
include Mail::Constants
extend Mail::Utilities
@transfer_encodings = {}
# Register transfer encoding
#
# Example
#
# Encodings.register "base64", Mail::Encodings::Base64
def Encodings.register(name, cls)
@transfer_encodings[get_name(name)] = cls
end
# Is the encoding we want defined?
#
# Example:
#
# Encodings.defined?(:base64) #=> true
def Encodings.defined?(name)
@transfer_encodings.include? get_name(name)
end
# Gets a defined encoding type, QuotedPrintable or Base64 for now.
#
# Each encoding needs to be defined as a Mail::Encodings::ClassName for
# this to work, allows us to add other encodings in the future.
#
# Example:
#
# Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
def Encodings.get_encoding(name)
@transfer_encodings[get_name(name)]
end
def Encodings.get_all
@transfer_encodings.values
end
def Encodings.get_name(name)
underscoreize(name).downcase
end
def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
if from_charset
Utilities.transcode_charset str, from_charset, to_charset
else
str
end
end
# Encodes a parameter value using URI Escaping, note the language field 'en' can
# be set using Mail::Configuration, like so:
#
# Mail.defaults do
# param_encode_language 'jp'
# end
#
# The character set used for encoding will be the encoding on the string passed in.
#
# Example:
#
# Mail::Encodings.param_encode("This is fun") #=> "us-ascii'en'This%20is%20fun"
def Encodings.param_encode(str)
case
when str.ascii_only? && str =~ TOKEN_UNSAFE
%Q{"#{str}"}
when str.ascii_only?
str
else
Utilities.param_encode(str)
end
end
# Decodes a parameter value using URI Escaping.
#
# Example:
#
# Mail::Encodings.param_decode("This%20is%20fun", 'us-ascii') #=> "This is fun"
#
# str = Mail::Encodings.param_decode("This%20is%20fun", 'iso-8559-1')
# str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
# str #=> "This is fun"
def Encodings.param_decode(str, encoding)
Utilities.param_decode(str, encoding)
end
# Decodes or encodes a string as needed for either Base64 or QP encoding types in
# the =??[QB]??=" format.
#
# The output type needs to be :decode to decode the input string or :encode to
# encode the input string. The character set used for encoding will be the
# encoding on the string passed in.
#
# On encoding, will only send out Base64 encoded strings.
def Encodings.decode_encode(str, output_type)
case
when output_type == :decode
Encodings.value_decode(str)
else
if str.ascii_only?
str
else
Encodings.b_value_encode(str, str.encoding)
end
end
end
# Decodes a given string as Base64 or Quoted Printable, depending on what
# type it is.
#
# String has to be of the format =??[QB]??=
def Encodings.value_decode(str)
# Optimization: If there's no encoded-words in the string, just return it
return str unless str =~ ENCODED_VALUE
lines = collapse_adjacent_encodings(str)
# Split on white-space boundaries with capture, so we capture the white-space as well
lines.each do |line|
line.gsub!(ENCODED_VALUE) do |string|
case $2
when *B_VALUES then b_value_decode(string)
when *Q_VALUES then q_value_decode(string)
end
end
end.join("")
end
# Takes an encoded string of the format =??[QB]??=
def Encodings.unquote_and_convert_to(str, to_encoding)
output = value_decode( str ).to_s # output is already converted to UTF-8
if 'utf8' == to_encoding.to_s.downcase.gsub("-", "")
output
elsif to_encoding
begin
output.encode(to_encoding)
rescue Errno::EINVAL
# the 'from' parameter specifies a charset other than what the text
# actually is...not much we can do in this case but just return the
# unconverted text.
#
# Ditto if either parameter represents an unknown charset, like
# X-UNKNOWN.
output
end
else
output
end
end
def Encodings.address_encode(address, charset = 'utf-8')
if address.is_a?(Array)
address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
elsif address
encode_non_usascii(address, charset)
end
end
def Encodings.encode_non_usascii(address, charset)
return address if address.ascii_only? or charset.nil?
# Encode all strings embedded inside of quotes
address = address.gsub(/("[^"]*[^\/]")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
# Then loop through all remaining items and encode as needed
tokens = address.split(/\s/)
map_with_index(tokens) do |word, i|
if word.ascii_only?
word
else
previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
if previous_non_ascii #why are we adding an extra space here?
word = " #{word}"
end
Encodings.b_value_encode(word, charset)
end
end.join(' ')
end
# Encode a string with Base64 Encoding and returns it ready to be inserted
# as a value for a field, that is, in the =??B??= format
#
# Example:
#
# Encodings.b_value_encode('This is あ string', 'UTF-8')
# #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
def Encodings.b_value_encode(string, encoding = nil)
if string.to_s.ascii_only?
string
else
Encodings.each_base64_chunk_byterange(string, 60).map do |chunk|
str, encoding = Utilities.b_value_encode(chunk, encoding)
"=?#{encoding}?B?#{str.chomp}?="
end.join(" ")
end
end
# Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
# as a value for a field, that is, in the =??Q??= format
#
# Example:
#
# Encodings.q_value_encode('This is あ string', 'UTF-8')
# #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
def Encodings.q_value_encode(encoded_str, encoding = nil)
return encoded_str if encoded_str.to_s.ascii_only?
string, encoding = Utilities.q_value_encode(encoded_str, encoding)
string.gsub!("=\r\n", '') # We already have limited the string to the length we want
map_lines(string) do |str|
"=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
end.join(" ")
end
private
# Decodes a Base64 string from the "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=" format
#
# Example:
#
# Encodings.b_value_decode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
# #=> 'This is あ string'
def Encodings.b_value_decode(str)
Utilities.b_value_decode(str)
end
# Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
#
# Example:
#
# Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
# #=> 'This is あ string'
def Encodings.q_value_decode(str)
Utilities.q_value_decode(str)
end
# Gets the encoding type (Q or B) from the string.
def Encodings.value_encoding_from_string(str)
str[ENCODED_VALUE, 1]
end
# Split header line into proper encoded and unencoded parts.
#
# String has to be of the format =??[QB]??=
#
# Omit unencoded space after an encoded-word.
def Encodings.collapse_adjacent_encodings(str)
results = []
last_encoded = nil # Track whether to preserve or drop whitespace
lines = str.split(FULL_ENCODED_VALUE)
lines.each_slice(2) do |unencoded, encoded|
if last_encoded = encoded
if !Utilities.blank?(unencoded) || (!last_encoded && unencoded != EMPTY)
results << unencoded
end
results << encoded
else
results << unencoded
end
end
results
end
# Partition the string into bounded-size chunks without splitting
# multibyte characters.
def Encodings.each_base64_chunk_byterange(str, max_bytesize_per_base64_chunk, &block)
raise "size per chunk must be multiple of 4" if (max_bytesize_per_base64_chunk % 4).nonzero?
if block_given?
max_bytesize = ((3 * max_bytesize_per_base64_chunk) / 4.0).floor
each_chunk_byterange(str, max_bytesize, &block)
else
enum_for :each_base64_chunk_byterange, str, max_bytesize_per_base64_chunk
end
end
# Partition the string into bounded-size chunks without splitting
# multibyte characters.
def Encodings.each_chunk_byterange(str, max_bytesize_per_chunk)
return enum_for(:each_chunk_byterange, str, max_bytesize_per_chunk) unless block_given?
offset = 0
chunksize = 0
str.each_char do |chr|
charsize = chr.bytesize
if chunksize + charsize > max_bytesize_per_chunk
yield Utilities.string_byteslice(str, offset, chunksize)
offset += chunksize
chunksize = charsize
else
chunksize += charsize
end
end
yield Utilities.string_byteslice(str, offset, chunksize)
end
end
end
mail-2.8.1/lib/mail/encodings/ 0000775 0000000 0000000 00000000000 14363721317 0016114 5 ustar 00root root 0000000 0000000 mail-2.8.1/lib/mail/encodings/7bit.rb 0000664 0000000 0000000 00000001223 14363721317 0017304 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/8bit'
module Mail
module Encodings
# 7bit and 8bit are equivalent. 7bit encoding is for text only.
class SevenBit < EightBit
NAME = '7bit'
PRIORITY = 1
Encodings.register(NAME, self)
def self.decode(str)
::Mail::Utilities.binary_unsafe_to_lf str
end
def self.encode(str)
::Mail::Utilities.binary_unsafe_to_crlf str
end
# Per RFC 2045 2.7. 7bit Data, No octets with decimal values greater than 127 are allowed.
def self.compatible_input?(str)
str.ascii_only? && super
end
end
end
end
mail-2.8.1/lib/mail/encodings/8bit.rb 0000664 0000000 0000000 00000000664 14363721317 0017315 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/binary'
module Mail
module Encodings
class EightBit < Binary
NAME = '8bit'
PRIORITY = 4
Encodings.register(NAME, self)
# Per RFC 2821 4.5.3.1, SMTP lines may not be longer than 1000 octets including the .
def self.compatible_input?(str)
!str.lines.find { |line| line.bytesize > 998 }
end
end
end
end
mail-2.8.1/lib/mail/encodings/base64.rb 0000664 0000000 0000000 00000001500 14363721317 0017521 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/7bit'
module Mail
module Encodings
# Base64 encoding handles binary content at the cost of 4 output bytes
# per input byte.
class Base64 < SevenBit
NAME = 'base64'
PRIORITY = 3
Encodings.register(NAME, self)
def self.can_encode?(enc)
true
end
def self.decode(str)
Utilities.decode_base64(str)
end
def self.encode(str)
::Mail::Utilities.binary_unsafe_to_crlf(Utilities.encode_base64(str))
end
# 3 bytes in -> 4 bytes out
def self.cost(str)
4.0 / 3
end
# Ruby Base64 inserts newlines automatically, so it doesn't exceed
# SMTP line length limits.
def self.compatible_input?(str)
true
end
end
end
end
mail-2.8.1/lib/mail/encodings/binary.rb 0000664 0000000 0000000 00000000356 14363721317 0017731 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/identity'
module Mail
module Encodings
class Binary < Identity
NAME = 'binary'
PRIORITY = 5
Encodings.register(NAME, self)
end
end
end
mail-2.8.1/lib/mail/encodings/identity.rb 0000664 0000000 0000000 00000000734 14363721317 0020276 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/transfer_encoding'
module Mail
module Encodings
# Identity encodings do no encoding/decoding and have a fixed cost:
# 1 byte in -> 1 byte out.
class Identity < TransferEncoding #:nodoc:
def self.decode(str)
str
end
def self.encode(str)
str
end
# 1 output byte per input byte.
def self.cost(str)
1.0
end
end
end
end
mail-2.8.1/lib/mail/encodings/quoted_printable.rb 0000664 0000000 0000000 00000002301 14363721317 0021776 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/encodings/7bit'
module Mail
module Encodings
class QuotedPrintable < SevenBit
NAME='quoted-printable'
PRIORITY = 2
def self.can_encode?(enc)
EightBit.can_encode? enc
end
# Decode the string from Quoted-Printable. Cope with hard line breaks
# that were incorrectly encoded as hex instead of literal CRLF.
def self.decode(str)
::Mail::Utilities.to_lf ::Mail::Utilities.to_crlf(str).gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
end
def self.encode(str)
::Mail::Utilities.to_crlf [::Mail::Utilities.to_lf(str)].pack("M")
end
def self.cost(str)
# These bytes probably do not need encoding
c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
# Everything else turns into =XX where XX is a
# two digit hex number (taking 3 bytes)
total = (str.bytesize - c)*3 + c
total.to_f/str.bytesize
end
# QP inserts newlines automatically and cannot violate the SMTP spec.
def self.compatible_input?(str)
true
end
private
Encodings.register(NAME, self)
end
end
end
mail-2.8.1/lib/mail/encodings/transfer_encoding.rb 0000664 0000000 0000000 00000004421 14363721317 0022134 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
module Mail
module Encodings
class TransferEncoding
NAME = ''
PRIORITY = -1
# And encoding's superclass can always transport it since the
# class hierarchy is arranged e.g. Base64 < 7bit < 8bit < Binary.
def self.can_transport?(enc)
enc && enc <= self
end
# Override in subclasses to indicate that they can encode text
# that couldn't be directly transported, e.g. Base64 has 7bit output,
# but it can encode binary.
def self.can_encode?(enc)
can_transport? enc
end
def self.cost(str)
raise "Unimplemented"
end
def self.compatible_input?(str)
true
end
def self.to_s
self::NAME
end
def self.negotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
message_encoding = Encodings.get_encoding(message_encoding) || Encodings.get_encoding('8bit')
source_encoding = Encodings.get_encoding(source_encoding)
if message_encoding && source_encoding && message_encoding.can_transport?(source_encoding) && source_encoding.compatible_input?(str)
source_encoding
else
renegotiate(message_encoding, source_encoding, str, allowed_encodings)
end
end
def self.renegotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
encodings = Encodings.get_all.select do |enc|
(allowed_encodings.nil? || allowed_encodings.include?(enc)) &&
message_encoding.can_transport?(enc) &&
enc.can_encode?(source_encoding)
end
lowest_cost(str, encodings)
end
def self.lowest_cost(str, encodings)
best = nil
best_cost = nil
encodings.each do |enc|
# If the current choice cannot be transported safely, give priority
# to other choices but allow it to be used as a fallback.
this_cost = enc.cost(str) if enc.compatible_input?(str)
if !best_cost || (this_cost && this_cost < best_cost)
best_cost = this_cost
best = enc
elsif this_cost == best_cost
best = enc if enc::PRIORITY < best::PRIORITY
end
end
best
end
end
end
end
mail-2.8.1/lib/mail/encodings/unix_to_unix.rb 0000664 0000000 0000000 00000000654 14363721317 0021176 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Mail
module Encodings
class UnixToUnix < TransferEncoding
NAME = "x-uuencode"
def self.decode(str)
str.sub(/\Abegin \d+ [^\n]*\n/, '').unpack('u').first
end
def self.encode(str)
[str].pack("u")
end
Encodings.register(NAME, self)
Encodings.register("uuencode", self)
Encodings.register("x-uue", self)
end
end
end
mail-2.8.1/lib/mail/envelope.rb 0000664 0000000 0000000 00000001075 14363721317 0016310 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
#
# = Mail Envelope
#
# The Envelope class provides a field for the first line in an
# mbox file, that looks like "From mikel@test.lindsaar.net DATETIME"
#
# This envelope class reads that line, and turns it into an
# Envelope.from and Envelope.date for your use.
module Mail
class Envelope < NamedStructuredField
NAME = 'Envelope-From'
def element
@element ||= Mail::EnvelopeFromElement.new(value)
end
def from
element.address
end
def date
element.date_time
end
end
end
mail-2.8.1/lib/mail/field.rb 0000664 0000000 0000000 00000021265 14363721317 0015561 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'mail/fields'
require 'mail/constants'
# encoding: utf-8
module Mail
# Provides a single class to call to create a new structured or unstructured
# field. Works out per RFC what field of field it is being given and returns
# the correct field of class back on new.
#
# ===Per RFC 2822
#
# 2.2. Header Fields
#
# Header fields are lines composed of a field name, followed by a colon
# (":"), followed by a field body, and terminated by CRLF. A field
# name MUST be composed of printable US-ASCII characters (i.e.,
# characters that have values between 33 and 126, inclusive), except
# colon. A field body may be composed of any US-ASCII characters,
# except for CR and LF. However, a field body may contain CRLF when
# used in header "folding" and "unfolding" as described in section
# 2.2.3. All field bodies MUST conform to the syntax described in
# sections 3 and 4 of this standard.
#
class Field
include Comparable
STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
content-id content-location content-transfer-encoding
content-type date from in-reply-to keywords message-id
mime-version received references reply-to
resent-bcc resent-cc resent-date resent-from
resent-message-id resent-sender resent-to
return-path sender to ]
KNOWN_FIELDS = STRUCTURED_FIELDS + ['comments', 'subject']
FIELDS_MAP = {
"to" => ToField,
"cc" => CcField,
"bcc" => BccField,
"message-id" => MessageIdField,
"in-reply-to" => InReplyToField,
"references" => ReferencesField,
"subject" => SubjectField,
"comments" => CommentsField,
"keywords" => KeywordsField,
"date" => DateField,
"from" => FromField,
"sender" => SenderField,
"reply-to" => ReplyToField,
"resent-date" => ResentDateField,
"resent-from" => ResentFromField,
"resent-sender" => ResentSenderField,
"resent-to" => ResentToField,
"resent-cc" => ResentCcField,
"resent-bcc" => ResentBccField,
"resent-message-id" => ResentMessageIdField,
"return-path" => ReturnPathField,
"received" => ReceivedField,
"mime-version" => MimeVersionField,
"content-transfer-encoding" => ContentTransferEncodingField,
"content-description" => ContentDescriptionField,
"content-disposition" => ContentDispositionField,
"content-type" => ContentTypeField,
"content-id" => ContentIdField,
"content-location" => ContentLocationField,
}
FIELD_NAME_MAP = FIELDS_MAP.inject({}) do |map, (field, field_klass)|
map.update(field => field_klass::NAME)
end
# Generic Field Exception
class FieldError < StandardError
end
# Raised when a parsing error has occurred (ie, a StructuredField has tried
# to parse a field that is invalid or improperly written)
class ParseError < FieldError #:nodoc:
attr_accessor :element, :value, :reason
def initialize(element, value, reason)
@element = element
@value = to_utf8(value)
@reason = to_utf8(reason)
super("#{@element} can not parse |#{@value}|: #{@reason}")
end
private
def to_utf8(text)
if text.respond_to?(:force_encoding)
text.dup.force_encoding(Encoding::UTF_8)
else
text
end
end
end
class NilParseError < ParseError #:nodoc:
def initialize(element)
super element, nil, 'nil is invalid'
end
end
class IncompleteParseError < ParseError #:nodoc:
def initialize(element, original_text, unparsed_index)
parsed_text = to_utf8(original_text[0...unparsed_index])
super element, original_text, "Only able to parse up to #{parsed_text.inspect}"
end
end
# Raised when attempting to set a structured field's contents to an invalid syntax
class SyntaxError < FieldError #:nodoc:
end
class << self
# Parse a field from a raw header line:
#
# Mail::Field.parse("field-name: field data")
# # => #
def parse(field, charset = 'utf-8')
name, value = split(field)
if name && value
new name, value, charset
end
end
def split(raw_field) #:nodoc:
if raw_field.index(Constants::COLON)
name, value = raw_field.split(Constants::COLON, 2)
name.rstrip!
if name =~ /\A#{Constants::FIELD_NAME}\z/
[ name.rstrip, value.strip ]
else
Kernel.warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: invalid header name syntax: #{name.inspect}"
nil
end
else
raw_field.strip
end
rescue => error
warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: #{error.class}: #{error.message}"
nil
end
def field_class_for(name) #:nodoc:
FIELDS_MAP[name.to_s.downcase]
end
end
attr_reader :unparsed_value
# Create a field by name and optional value:
#
# Mail::Field.new("field-name", "value")
# # => #
#
# Values that aren't strings or arrays are coerced to Strings with `#to_s`.
#
# Mail::Field.new("field-name", 1234)
# # => #
#
# Mail::Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
# # => #
def initialize(name, value = nil, charset = 'utf-8')
case
when name.index(Constants::COLON)
raise ArgumentError, 'Passing an unparsed header field to Mail::Field.new is not supported in Mail 2.8.0+. Use Mail::Field.parse instead.'
when Utilities.blank?(value)
@name = name
@unparsed_value = nil
@charset = charset
else
@name = name
@unparsed_value = value
@charset = charset
end
@name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
end
def field=(field)
@field = field
end
def field
@field ||= create_field(@name, @unparsed_value, @charset)
end
def name
@name
end
def value
field.value
end
def value=(val)
@field = create_field(name, val, @charset)
end
def to_s
field.to_s
end
def inspect
"#<#{self.class.name} 0x#{(object_id * 2).to_s(16)} #{instance_variables.map do |ivar|
"#{ivar}=#{instance_variable_get(ivar).inspect}"
end.join(" ")}>"
end
def same(other)
other.kind_of?(self.class) && Utilities.match_to_s(other.name, name)
end
def ==(other)
same(other) && Utilities.match_to_s(other.value, value)
end
def responsible_for?(field_name)
name.to_s.casecmp(field_name.to_s) == 0
end
def <=>(other)
field_order_id <=> other.field_order_id
end
def field_order_id
@field_order_id ||= FIELD_ORDER_LOOKUP.fetch(self.name.to_s.downcase, 100)
end
def method_missing(name, *args, &block)
field.send(name, *args, &block)
end
def respond_to_missing?(method_name, include_private)
field.respond_to?(method_name, include_private) || super
end
FIELD_ORDER_LOOKUP = Hash[%w[
return-path received
resent-date resent-from resent-sender resent-to
resent-cc resent-bcc resent-message-id
date from sender reply-to to cc bcc
message-id in-reply-to references
subject comments keywords
mime-version content-type content-transfer-encoding
content-location content-disposition content-description
].each_with_index.to_a]
private
def create_field(name, value, charset)
parse_field(name, value, charset)
rescue Mail::Field::ParseError => e
field = Mail::UnstructuredField.new(name, value)
field.errors << [name, value, e]
field
end
def parse_field(name, value, charset)
value = unfold(value) if value.is_a?(String)
if klass = self.class.field_class_for(name)
klass.parse(value, charset)
else
OptionalField.parse(name, value, charset)
end
end
# 2.2.3. Long Header Fields
#
# The process of moving from this folded multiple-line representation
# of a header field to its single line representation is called
# "unfolding". Unfolding is accomplished by simply removing any CRLF
# that is immediately followed by WSP. Each header field should be
# treated in its unfolded form for further syntactic and semantic
# evaluation.
def unfold(string)
string.gsub(Constants::UNFOLD_WS, '\1')
end
end
end
mail-2.8.1/lib/mail/field_list.rb 0000664 0000000 0000000 00000004035 14363721317 0016610 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
module Mail
# Field List class provides an enhanced array that keeps a list of
# email fields in order. And allows you to insert new fields without
# having to worry about the order they will appear in.
class FieldList < Array
def has_field?(field_name)
any? { |f| f.responsible_for? field_name }
end
def get_field(field_name)
fields = select_fields(field_name)
case fields.size
when 0; nil
when 1; fields.first
else fields
end
end
def add_field(field)
if field.singular?
replace_field field
else
insert_field field
end
end
alias_method :<<, :add_field
def replace_field(field)
if first_offset = index { |f| f.responsible_for? field.name }
delete_field field.name
insert first_offset, field
else
insert_field field
end
end
# Insert the field in sorted order.
#
# Heavily based on bisect.insort from Python, which is:
# Copyright (C) 2001-2013 Python Software Foundation.
# Licensed under
# From
def insert_field(field)
lo, hi = 0, size
while lo < hi
mid = (lo + hi).div(2)
if field < self[mid]
hi = mid
else
lo = mid + 1
end
end
insert lo, field
end
def delete_field(name)
delete_if { |f| f.responsible_for? name }
end
def summary
map { |f| "<#{f.name}: #{f.value}>" }.join(", ")
end
private
def select_fields(field_name)
fields = select { |f| f.responsible_for? field_name }
if fields.size > 1 && singular?(field_name)
Array(fields.detect { |f| f.errors.size == 0 } || fields.first)
else
fields
end
end
def singular?(field_name)
if klass = Mail::Field.field_class_for(field_name)
klass.singular?
else
false
end
end
end
end
mail-2.8.1/lib/mail/fields.rb 0000664 0000000 0000000 00000004304 14363721317 0015737 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Mail
register_autoload :UnstructuredField, 'mail/fields/unstructured_field'
register_autoload :StructuredField, 'mail/fields/structured_field'
register_autoload :OptionalField, 'mail/fields/optional_field'
register_autoload :BccField, 'mail/fields/bcc_field'
register_autoload :CcField, 'mail/fields/cc_field'
register_autoload :CommentsField, 'mail/fields/comments_field'
register_autoload :ContentDescriptionField, 'mail/fields/content_description_field'
register_autoload :ContentDispositionField, 'mail/fields/content_disposition_field'
register_autoload :ContentIdField, 'mail/fields/content_id_field'
register_autoload :ContentLocationField, 'mail/fields/content_location_field'
register_autoload :ContentTransferEncodingField, 'mail/fields/content_transfer_encoding_field'
register_autoload :ContentTypeField, 'mail/fields/content_type_field'
register_autoload :DateField, 'mail/fields/date_field'
register_autoload :FromField, 'mail/fields/from_field'
register_autoload :InReplyToField, 'mail/fields/in_reply_to_field'
register_autoload :KeywordsField, 'mail/fields/keywords_field'
register_autoload :MessageIdField, 'mail/fields/message_id_field'
register_autoload :MimeVersionField, 'mail/fields/mime_version_field'
register_autoload :ReceivedField, 'mail/fields/received_field'
register_autoload :ReferencesField, 'mail/fields/references_field'
register_autoload :ReplyToField, 'mail/fields/reply_to_field'
register_autoload :ResentBccField, 'mail/fields/resent_bcc_field'
register_autoload :ResentCcField, 'mail/fields/resent_cc_field'
register_autoload :ResentDateField, 'mail/fields/resent_date_field'
register_autoload :ResentFromField, 'mail/fields/resent_from_field'
register_autoload :ResentMessageIdField, 'mail/fields/resent_message_id_field'
register_autoload :ResentSenderField, 'mail/fields/resent_sender_field'
register_autoload :ResentToField, 'mail/fields/resent_to_field'
register_autoload :ReturnPathField, 'mail/fields/return_path_field'
register_autoload :SenderField, 'mail/fields/sender_field'
register_autoload :SubjectField, 'mail/fields/subject_field'
register_autoload :ToField, 'mail/fields/to_field'
end
mail-2.8.1/lib/mail/fields/ 0000775 0000000 0000000 00000000000 14363721317 0015411 5 ustar 00root root 0000000 0000000 mail-2.8.1/lib/mail/fields/bcc_field.rb 0000664 0000000 0000000 00000003267 14363721317 0017640 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/common_address_field'
module Mail
# = Blind Carbon Copy Field
#
# The Bcc field inherits from StructuredField and handles the Bcc: header
# field in the email.
#
# Sending bcc to a mail message will instantiate a Mail::Field object that
# has a BccField as its field type. This includes all Mail::CommonAddress
# module instance metods.
#
# Only one Bcc field can appear in a header, though it can have multiple
# addresses and groups of addresses.
#
# == Examples:
#
# mail = Mail.new
# mail.bcc = 'Mikel Lindsaar , ada@test.lindsaar.net'
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
# mail[:bcc] #=> '# '# '# '' # Bcc field does not get output into an email
# mail[:bcc].decoded #=> 'Mikel Lindsaar , ada@test.lindsaar.net'
# mail[:bcc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
# mail[:bcc].formatted #=> ['Mikel Lindsaar ', 'ada@test.lindsaar.net']
class BccField < CommonAddressField #:nodoc:
NAME = 'Bcc'
attr_accessor :include_in_headers
def initialize(value = nil, charset = nil)
super
self.include_in_headers = false
end
# Bcc field should not be :encoded by default
def encoded
if include_in_headers
super
else
''
end
end
end
end
mail-2.8.1/lib/mail/fields/cc_field.rb 0000664 0000000 0000000 00000002614 14363721317 0017471 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/common_address_field'
module Mail
# = Carbon Copy Field
#
# The Cc field inherits from StructuredField and handles the Cc: header
# field in the email.
#
# Sending cc to a mail message will instantiate a Mail::Field object that
# has a CcField as its field type. This includes all Mail::CommonAddress
# module instance metods.
#
# Only one Cc field can appear in a header, though it can have multiple
# addresses and groups of addresses.
#
# == Examples:
#
# mail = Mail.new
# mail.cc = 'Mikel Lindsaar , ada@test.lindsaar.net'
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
# mail[:cc] #=> '# '# '# 'Cc: Mikel Lindsaar , ada@test.lindsaar.net\r\n'
# mail[:cc].decoded #=> 'Mikel Lindsaar , ada@test.lindsaar.net'
# mail[:cc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
# mail[:cc].formatted #=> ['Mikel Lindsaar ', 'ada@test.lindsaar.net']
class CcField < CommonAddressField #:nodoc:
NAME = 'Cc'
end
end
mail-2.8.1/lib/mail/fields/comments_field.rb 0000664 0000000 0000000 00000002236 14363721317 0020731 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_unstructured_field'
module Mail
# = Comments Field
#
# The Comments field inherits from UnstructuredField and handles the Comments:
# header field in the email.
#
# Sending comments to a mail message will instantiate a Mail::Field object that
# has a CommentsField as its field type.
#
# An email header can have as many comments fields as it wants. There is no upper
# limit, the comments field is also optional (that is, no comment is needed)
#
# == Examples:
#
# mail = Mail.new
# mail.comments = 'This is a comment'
# mail.comments #=> 'This is a comment'
# mail[:comments] #=> '# '# '# ['This is a comment', "This is another comment"]
class CommentsField < NamedUnstructuredField #:nodoc:
NAME = 'Comments'
end
end
mail-2.8.1/lib/mail/fields/common_address_field.rb 0000664 0000000 0000000 00000011333 14363721317 0022077 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
module Mail
class AddressContainer < Array #:nodoc:
def initialize(field, list = nil)
@field = field
super list if list
end
def <<(address)
@field << address
end
end
class CommonAddressField < NamedStructuredField #:nodoc:
def self.singular?
true
end
def initialize(value = nil, charset = nil)
super encode_if_needed(value, charset), charset
end
def element # :nodoc:
@element ||= AddressList.new(value)
end
# Allows you to iterate through each address object in the address_list
def each
element.addresses.each do |address|
yield(address)
end
end
def default
addresses
end
def address
addresses.first
end
# Returns the address string of all the addresses in the address list
def addresses
list = element.addresses.map { |a| a.address }
Mail::AddressContainer.new(self, list)
end
# Returns the formatted string of all the addresses in the address list
def formatted
list = element.addresses.map { |a| a.format }
Mail::AddressContainer.new(self, list)
end
# Returns the display name of all the addresses in the address list
def display_names
list = element.addresses.map { |a| a.display_name }
Mail::AddressContainer.new(self, list)
end
# Returns the actual address objects in the address list
def addrs
list = element.addresses
Mail::AddressContainer.new(self, list)
end
# Returns a hash of group name => address strings for the address list
def groups
element.addresses_grouped_by_group
end
# Returns the addresses that are part of groups
def group_addresses
decoded_group_addresses
end
# Returns a list of decoded group addresses
def decoded_group_addresses
groups.map { |k,v| v.map { |a| a.decoded } }.flatten
end
# Returns a list of encoded group addresses
def encoded_group_addresses
groups.map { |k,v| v.map { |a| a.encoded } }.flatten
end
# Returns the name of all the groups in a string
def group_names # :nodoc:
element.group_names
end
def <<(val)
case
when val.nil?
raise ArgumentError, "Need to pass an address to <<"
when Utilities.blank?(val)
self
else
self.value = [self.value, encode_if_needed(val)].reject { |a| Utilities.blank?(a) }.join(", ")
end
end
def encode_if_needed(val, val_charset = charset) #:nodoc:
case val
when nil
val
# Need to join arrays of addresses into a single value
when Array
val.compact.map { |a| encode_if_needed a, val_charset }.join(', ')
# Pass through UTF-8; encode non-UTF-8.
else
utf8_if_needed(val, val_charset) || Encodings.encode_non_usascii(val, val_charset)
end
end
private
if 'string'.respond_to?(:encoding)
# Pass through UTF-8 addresses
def utf8_if_needed(val, val_charset)
if val_charset =~ /\AUTF-?8\z/i
val
elsif val.encoding == Encoding::UTF_8
val
elsif (utf8 = val.dup.force_encoding(Encoding::UTF_8)).valid_encoding?
utf8
end
end
else
def utf8_if_needed(val, val_charset)
if val_charset =~ /\AUTF-?8\z/i
val
end
end
end
def do_encode
return '' if Utilities.blank?(value)
address_array = element.addresses.reject { |a| encoded_group_addresses.include?(a.encoded) }.compact.map { |a| a.encoded }
address_text = address_array.join(", \r\n\s")
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.encoded }.join(", \r\n\s")};" }
group_text = group_array.join(" \r\n\s")
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
"#{name}: #{return_array.join(", \r\n\s")}\r\n"
end
def do_decode
return nil if Utilities.blank?(value)
address_array = element.addresses.reject { |a| decoded_group_addresses.include?(a.decoded) }.map { |a| a.decoded }
address_text = address_array.join(", ")
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.decoded }.join(", ")};" }
group_text = group_array.join(" ")
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
return_array.join(", ")
end
def get_group_addresses(group_list)
if group_list.respond_to?(:addresses)
group_list.addresses.map do |address|
Mail::Address.new(address)
end
else
[]
end
end
end
end
mail-2.8.1/lib/mail/fields/common_date_field.rb 0000664 0000000 0000000 00000002412 14363721317 0021365 0 ustar 00root root 0000000 0000000 require 'mail/fields/named_structured_field'
require 'mail/elements/date_time_element'
require 'mail/utilities'
module Mail
class CommonDateField < NamedStructuredField #:nodoc:
def self.singular?
true
end
def self.normalize_datetime(string)
if Utilities.blank?(string)
datetime = ::DateTime.now
else
stripped = string.to_s.gsub(/\(.*?\)/, '').squeeze(' ')
begin
datetime = ::DateTime.parse(stripped)
rescue ArgumentError => e
raise unless 'invalid date' == e.message
end
end
if datetime
datetime.strftime('%a, %d %b %Y %H:%M:%S %z')
else
string
end
end
def initialize(value = nil, charset = nil)
super self.class.normalize_datetime(value), charset
end
# Returns a date time object of the parsed date
def date_time
::DateTime.parse("#{element.date_string} #{element.time_string}")
rescue ArgumentError => e
raise e unless e.message == 'invalid date'
end
def default
date_time
end
def element
@element ||= Mail::DateTimeElement.new(value)
end
private
def do_encode
"#{name}: #{value}\r\n"
end
def do_decode
value.to_s
end
end
end
mail-2.8.1/lib/mail/fields/common_field.rb 0000664 0000000 0000000 00000002332 14363721317 0020371 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/constants'
module Mail
class CommonField #:nodoc:
def self.singular?
false
end
def self.parse(*args)
new(*args).tap(&:parse)
end
attr_accessor :name
attr_reader :value
attr_accessor :charset
attr_reader :errors
def initialize(name = nil, value = nil, charset = nil)
@errors = []
self.name = name
self.value = value
self.charset = charset || 'utf-8'
end
def singular?
self.class.singular?
end
def value=(value)
@element = nil
@value = value.is_a?(Array) ? value : value.to_s
parse
end
def parse
tap(&:element)
end
def element
nil
end
def to_s
decoded.to_s
end
def default
decoded
end
def decoded
do_decode
end
def encoded
do_encode
end
def responsible_for?(field_name)
name.to_s.casecmp(field_name.to_s) == 0
end
private
FILENAME_RE = /\b(filename|name)=([^;"\r\n]+\s[^;"\r\n]+)/
def ensure_filename_quoted(value)
if value.is_a?(String)
value.sub FILENAME_RE, '\1="\2"'
else
value
end
end
end
end
mail-2.8.1/lib/mail/fields/common_message_id_field.rb 0000664 0000000 0000000 00000001441 14363721317 0022551 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
require 'mail/utilities'
module Mail
class CommonMessageIdField < NamedStructuredField #:nodoc:
def element
@element ||= Mail::MessageIdsElement.new(value)
end
def message_id
element.message_id
end
def message_ids
element.message_ids
end
def default
ids = message_ids
ids.one? ? ids.first : ids
end
def to_s
decoded.to_s
end
private
def do_encode
%Q{#{name}: #{formatted_message_ids("\r\n ")}\r\n}
end
def do_decode
formatted_message_ids
end
def formatted_message_ids(join = ' ')
message_ids.map { |m| "<#{m}>" }.join(join) if message_ids.any?
end
end
end
mail-2.8.1/lib/mail/fields/content_description_field.rb 0000664 0000000 0000000 00000000404 14363721317 0023154 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_unstructured_field'
module Mail
class ContentDescriptionField < NamedUnstructuredField #:nodoc:
NAME = 'Content-Description'
def self.singular?
true
end
end
end
mail-2.8.1/lib/mail/fields/content_disposition_field.rb 0000664 0000000 0000000 00000002060 14363721317 0023175 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
require 'mail/fields/parameter_hash'
module Mail
class ContentDispositionField < NamedStructuredField #:nodoc:
NAME = 'Content-Disposition'
def self.singular?
true
end
def initialize(value = nil, charset = nil)
super ensure_filename_quoted(value), charset
end
def element
@element ||= Mail::ContentDispositionElement.new(value)
end
def disposition_type
element.disposition_type
end
def parameters
@parameters = ParameterHash.new
element.parameters.each { |p| @parameters.merge!(p) } unless element.parameters.nil?
@parameters
end
def filename
@filename ||= parameters['filename'] || parameters['name']
end
def encoded
p = ";\r\n\s#{parameters.encoded}" if parameters.length > 0
"#{name}: #{disposition_type}#{p}\r\n"
end
def decoded
p = "; #{parameters.decoded}" if parameters.length > 0
"#{disposition_type}#{p}"
end
end
end
mail-2.8.1/lib/mail/fields/content_id_field.rb 0000664 0000000 0000000 00000001263 14363721317 0021231 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
require 'mail/utilities'
module Mail
class ContentIdField < NamedStructuredField #:nodoc:
NAME = 'Content-ID'
def self.singular?
true
end
def initialize(value = nil, charset = nil)
value = Mail::Utilities.generate_message_id if Utilities.blank?(value)
super value, charset
end
def element
@element ||= Mail::MessageIdsElement.new(value)
end
def content_id
element.message_id
end
private
def do_decode
"<#{content_id}>"
end
def do_encode
"#{name}: #{do_decode}\r\n"
end
end
end
mail-2.8.1/lib/mail/fields/content_location_field.rb 0000664 0000000 0000000 00000000741 14363721317 0022445 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
module Mail
class ContentLocationField < NamedStructuredField #:nodoc:
NAME = 'Content-Location'
def self.singular?
true
end
def element
@element ||= Mail::ContentLocationElement.new(value)
end
def location
element.location
end
def encoded
"#{name}: #{location}\r\n"
end
def decoded
location
end
end
end
mail-2.8.1/lib/mail/fields/content_transfer_encoding_field.rb 0000664 0000000 0000000 00000001532 14363721317 0024326 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
module Mail
class ContentTransferEncodingField < NamedStructuredField #:nodoc:
NAME = 'Content-Transfer-Encoding'
def self.singular?
true
end
def self.normalize_content_transfer_encoding(value)
case value
when /7-?bits?/i
'7bit'
when /8-?bits?/i
'8bit'
else
value
end
end
def initialize(value = nil, charset = nil)
super self.class.normalize_content_transfer_encoding(value), charset
end
def element
@element ||= Mail::ContentTransferEncodingElement.new(value)
end
def encoding
element.encoding
end
private
def do_encode
"#{name}: #{encoding}\r\n"
end
def do_decode
encoding
end
end
end
mail-2.8.1/lib/mail/fields/content_type_field.rb 0000664 0000000 0000000 00000010651 14363721317 0021617 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/named_structured_field'
require 'mail/fields/parameter_hash'
module Mail
class ContentTypeField < NamedStructuredField #:nodoc:
NAME = 'Content-Type'
class << self
def singular?
true
end
def with_boundary(type)
new "#{type}; boundary=#{generate_boundary}"
end
def generate_boundary
"--==_mimepart_#{Mail.random_tag}"
end
end
def initialize(value = nil, charset = nil)
if value.is_a? Array
@main_type = value[0]
@sub_type = value[1]
@parameters = ParameterHash.new.merge!(value.last)
else
@main_type = nil
@sub_type = nil
value = value.to_s
end
super ensure_filename_quoted(value), charset
end
def element
@element ||=
begin
Mail::ContentTypeElement.new(value)
rescue Mail::Field::ParseError
attempt_to_clean
end
end
def attempt_to_clean
# Sanitize the value, handle special cases
Mail::ContentTypeElement.new(sanitize(value))
rescue Mail::Field::ParseError
# All else fails, just get the MIME media type
Mail::ContentTypeElement.new(get_mime_type(value))
end
def main_type
@main_type ||= element.main_type
end
def sub_type
@sub_type ||= element.sub_type
end
def string
"#{main_type}/#{sub_type}"
end
alias_method :content_type, :string
def default
decoded
end
def parameters
unless defined? @parameters
@parameters = ParameterHash.new
element.parameters.each { |p| @parameters.merge!(p) }
end
@parameters
end
def value
if @value.is_a? Array
"#{@main_type}/#{@sub_type}; #{stringify(parameters)}"
else
@value
end
end
def stringify(params)
params.map { |k,v| "#{k}=#{Encodings.param_encode(v)}" }.join("; ")
end
def filename
@filename ||= parameters['filename'] || parameters['name']
end
def encoded
p = ";\r\n\s#{parameters.encoded}" if parameters && parameters.length > 0
"#{name}: #{content_type}#{p}\r\n"
end
def decoded
p = "; #{parameters.decoded}" if parameters && parameters.length > 0
"#{content_type}#{p}"
end
private
def method_missing(name, *args, &block)
if name.to_s =~ /(\w+)=/
self.parameters[$1] = args.first
@value = "#{content_type}; #{stringify(parameters)}"
else
super
end
end
# Various special cases from random emails found that I am not going to change
# the parser for
def sanitize(val)
# TODO: check if there are cases where whitespace is not a separator
val = val.
gsub(/\s*=\s*/,'='). # remove whitespaces around equal sign
gsub(/[; ]+/, '; '). #use '; ' as a separator (or EOL)
gsub(/;\s*$/,'') #remove trailing to keep examples below
if val =~ /((boundary|name|filename)=(\S*))/i
val = "#{$`.downcase}#{$2}=#{$3}#{$'.downcase}"
else
val.downcase!
end
case
when val.chomp =~ /^\s*([\w\-]+)\/([\w\-]+)\s*;\s?(ISO[\w\-]+)$/i
# Microsoft helper:
# Handles 'type/subtype;ISO-8559-1'
"#{$1}/#{$2}; charset=#{Utilities.quote_atom($3)}"
when val.chomp =~ /^text;?$/i
# Handles 'text;' and 'text'
"text/plain;"
when val.chomp =~ /^(\w+);\s(.*)$/i
# Handles 'text; '
"text/plain; #{$2}"
when val =~ /([\w\-]+\/[\w\-]+);\scharset="charset="(\w+)""/i
# Handles text/html; charset="charset="GB2312""
"#{$1}; charset=#{Utilities.quote_atom($2)}"
when val =~ /([\w\-]+\/[\w\-]+);\s+(.*)/i
type = $1
# Handles misquoted param values
# e.g: application/octet-stream; name=archiveshelp1[1].htm
# and: audio/x-midi;\r\n\sname=Part .exe
params = $2.to_s.split(/\s+/)
params = params.map { |i| i.to_s.chomp.strip }
params = params.map { |i| i.split(/\s*\=\s*/, 2) }
params = params.map { |i| "#{i[0]}=#{Utilities.dquote(i[1].to_s.gsub(/;$/,""))}" }.join('; ')
"#{type}; #{params}"
when val =~ /^\s*$/
'text/plain'
else
val
end
end
def get_mime_type(val)
case val
when /^([\w\-]+)\/([\w\-]+);.+$/i
"#{$1}/#{$2}"
else
'text/plain'
end
end
end
end
mail-2.8.1/lib/mail/fields/date_field.rb 0000664 0000000 0000000 00000002011 14363721317 0020010 0 ustar 00root root 0000000 0000000 # encoding: utf-8
# frozen_string_literal: true
require 'mail/fields/common_date_field'
module Mail
# = Date Field
#
# The Date field inherits from StructuredField and handles the Date: header
# field in the email.
#
# Sending date to a mail message will instantiate a Mail::Field object that
# has a DateField as its field type. This includes all Mail::CommonAddress
# module instance methods.
#
# There must be excatly one Date field in an RFC2822 email.
#
# == Examples:
#
# mail = Mail.new
# mail.date = 'Mon, 24 Nov 1997 14:22:01 -0800'
# mail.date #=> #
# mail.date.to_s #=> 'Mon, 24 Nov 1997 14:22:01 -0800'
# mail[:date] #=> '# '# '#