Skip to content

Commit

Permalink
Import Pull-Request sisimai/p5-sisimai#420 only lib/ directory
Browse files Browse the repository at this point in the history
  • Loading branch information
azumakuniyuki committed Dec 9, 2020
1 parent ba6a79a commit 08b5879
Show file tree
Hide file tree
Showing 50 changed files with 937 additions and 995 deletions.
39 changes: 18 additions & 21 deletions lib/sisimai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
# results. Sisimai is the system formerly known as bounceHammer 4, is the successor to bounceHammer.
require 'sisimai/version'
module Sisimai
# Imported from p5-Sisimail/lib/Sisimai.pm
class << self
def version(); return Sisimai::VERSION; end
def libname(); return 'Sisimai'; end

# Emulate "rise" method for the backward compatible
def make(argv0, **argv1)
warn ' ***warning: Sisimai.make will be removed at v5.1.0. Use Sisimai.rise instead';
return Sisimai.rise(argv0, **argv1)
end

# Wrapper method for parsing mailbox/maidir
# @param [String] argv0 Path to mbox or Maildir/
# @param [Hash] argv0 or Hash (decoded JSON)
Expand All @@ -16,46 +21,38 @@ def libname(); return 'Sisimai'; end
# @options argv1 [Array] c___ Proc object to a callback method for the message and each file
# @return [Array] Parsed objects
# @return [nil] nil if the argument was wrong or an empty array
def make(argv0, **argv1)
def rise(argv0, **argv1)
return nil unless argv0
require 'sisimai/data'
require 'sisimai/message'
require 'sisimai/mail'
require 'sisimai/fact'

list = []
return nil unless mail = Sisimai::Mail.new(argv0)
kind = mail.kind
c___ = argv1[:c___].is_a?(Array) ? argv1[:c___] : [nil, nil]
sisi = []

while r = mail.data.read do
# Read and parse each email file
args = { data: r, hook: c___[0] }
path = mail.data.path
sisi = []

mesg = Sisimai::Message.new(args)
unless mesg.void
# Sisimai::Message object was created successfully
args = { data: mesg, delivered: argv1[:delivered], origin: path }
sisi = Sisimai::Data.make(args)
end
args = { data: r, hook: c___[0], origin: path, deliverd: argv1[:delivered] }
fact = Sisimai::Fact->rise(args) || []

if c___[1]
# Run the callback function specified with "c___" parameter of Sisimai.make
# after reading each email file in Maildir/ every time
args = { 'kind' => kind, 'mail' => r, 'path' => path, 'sisi' => sisi }
# Run the callback function specified with "c___" parameter of Sisimai.make after reading
# each email file in Maildir/ every time
args = { 'kind' => kind, 'mail' => r, 'path' => path, 'fact' => fact }
begin
c___[1].call(args) if c___[1].is_a?(Proc)
rescue StandardError => ce
warn ' ***warning: Something is wrong in the second element of the ":c___":' << ce.to_s
end
end

list += sisi unless sisi.empty?
sisi += fact unless fact.empty?
end

return nil if list.empty?
return list
return nil if sisi.empty?
return sisi
end

# Wrapper method to parse mailbox/Maildir and dump as JSON
Expand All @@ -69,7 +66,7 @@ def make(argv0, **argv1)
def dump(argv0, **argv1)
return nil unless argv0

nyaan = Sisimai.make(argv0, argv1) || []
nyaan = Sisimai.rise(argv0, argv1) || []
if RUBY_PLATFORM.start_with?('java')
# java-based ruby environment like JRuby.
require 'jrjackson'
Expand Down
142 changes: 87 additions & 55 deletions lib/sisimai/address.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,74 @@
module Sisimai
# Sisimai::Address provide methods for dealing email address.
class Address
# Imported from p5-Sisimail/lib/Sisimai/Address.pm
require 'sisimai/rfc5322'
Indicators = {
build_regular_expressions = lambda do
# See http://www.ietf.org/rfc/rfc5322.txt
# or http://www.ex-parrot.com/pdw/Mail-RFC822-Address.html ...
# addr-spec = local-part "@" domain
# local-part = dot-atom / quoted-string / obs-local-part
# domain = dot-atom / domain-literal / obs-domain
# domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
# dcontent = dtext / quoted-pair
# dtext = NO-WS-CTL / ; Non white space controls
# %d33-90 / ; The rest of the US-ASCII
# %d94-126 ; characters not including "[", "]", or "\"
re = { rfc5322: nil, ignored: nil, domain: nil }
atom = %r([a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)o
quoted_string = %r/"(?:\\[^\r\n]|[^\\"])*"/o
domain_literal = %r/\[(?:\\[\x01-\x09\x0B-\x0c\x0e-\x7f]|[\x21-\x5a\x5e-\x7e])*\]/o
dot_atom = %r/#{atom}(?:[.]#{atom})*/o
local_part = %r/(?:#{dot_atom}|#{quoted_string})/o
domain = %r/(?:#{dot_atom}|#{domain_literal})/o

re[:rfc5322] = %r/\A#{local_part}[@]#{domain}\z/o
re[:ignored] = %r/\A#{local_part}[.]*[@]#{domain}\z/o
re[:domain] = %r/\A#{domain}\z/o

return re
end

Re = build_regular_expressions.call
HeaderIndex = build_flatten_rfc822header_list.call
Indicators = {
:'email-address' => (1 << 0), # <neko@example.org>
:'quoted-string' => (1 << 1), # "Neko, Nyaan"
:'comment-block' => (1 << 2), # (neko)
}.freeze
Delimiters = { '<' => 1, '>' => 1, '(' => 1, ')' => 1, '"' => 1, ',' => 1 }.freeze

# Check that the argument is an email address or not
# @param [String] email Email address string
# @return [True,False] true: is an email address
# false: is not an email address
def self.is_emailaddress(email)
return false unless email.is_a?(::String)
return false if email =~ %r/(?:[\x00-\x1f]|\x1f)/
return false if email.size > 254
return true if email =~ Re[:ignored]
return false
end

# Check that the argument is mailer-daemon or not
# @param [String] email Email address
# @return [True,False] true: mailer-daemon
# false: Not mailer-daemon
def self.is_mailerdaemon(email)
return false unless email.is_a?(::String)
regex = %r/(?:
(?:mailer-daemon|postmaster)[@]
|[<(](?:mailer-daemon|postmaster)[)>]
|\A(?:mailer-daemon|postmaster)\z
|[ ]?mailer-daemon[ ]
)
/x.freeze
return true if email.downcase =~ regex
return false
end

# Return pseudo recipient or sender address
# @param [Symbol] argv1 Address type: :r or :s
# @return [String, Nil] Pseudo recipient address or sender address or
# nil when the argv1 is neither :r nor :s
# @return [String, nil] Pseudo recipient address or sender address or nil when the argv1 is
# neither :r nor :s
def self.undisclosed(argv1)
return nil unless argv1
return nil unless %w[r s].index(argv1)
Expand All @@ -22,34 +77,12 @@ def self.undisclosed(argv1)
return sprintf('undisclosed-%s-in-headers@libsisimai.org.invalid', local)
end

# New constructor of Sisimai::Address
# @param [Hash] argvs Email address, name, and other elements
# @return [Sisimai::Address] Object or nil when the email address was
# not valid.
# @example make({address: 'neko@example.org', name: 'Neko', comment: '(nyaan)')}
# # => Sisimai::Address object
def self.make(argvs)
return nil unless argvs.is_a? Hash
return nil unless argvs[:address]
return nil if argvs[:address].empty?

thing = Sisimai::Address.new(argvs[:address])
return nil unless thing
return nil if thing.void

thing.name = argvs[:name] || ''
thing.comment = argvs[:comment] || ''

return thing
end

def self.find(argv1 = nil, addrs = false)
# Email address parser with a name and a comment
# @param [String] argv1 String including email address
# @param [Boolean] addrs true: Returns list including all the elements
# false: Returns list including email addresses only
# @return [Array, Nil] Email address list or nil when there is no
# email address in the argument
# @return [Array, Nil] Email address list or nil when there is no email address in the argument
# @example Parse email address
# find('Neko <neko(nyaan)@example.org>')
# #=> [{ address: 'neko@example.org', name: 'Neko', comment: '(nyaan)'}]
Expand Down Expand Up @@ -213,7 +246,7 @@ def self.find(argv1 = nil, addrs = false)
# String like an email address will be set to the value of "address"
v[:address] = cv[1] + '@' + cv[2]

elsif Sisimai::RFC5322.is_mailerdaemon(v[:name])
elsif Sisimai::Address.is_mailerdaemon(v[:name])
# Allow if the argument is MAILER-DAEMON
v[:address] = v[:name]
end
Expand All @@ -235,12 +268,12 @@ def self.find(argv1 = nil, addrs = false)
next if e[:address] =~ /[^\x20-\x7e]/
unless e[:address] =~ /\A.+[@].+\z/
# Allow if the argument is MAILER-DAEMON
next unless Sisimai::RFC5322.is_mailerdaemon(e[:address])
next unless Sisimai::Address.is_mailerdaemon(e[:address])
end

# Remove angle brackets, other brackets, and quotations: []<>{}'`
# except a domain part is an IP address like neko@[192.0.2.222]
e[:address] = e[:address].sub(/\A[\[<{('`]/, '').sub(/['`>})]\z/, '')
# Remove angle brackets, other brackets, and quotations: []<>{}'` except a domain part is
# an IP address like neko@[192.0.2.222]
e[:address] = e[:address].sub(/\A[\[<{('`]/, '').sub(/[.'`>});]\z/, '')
e[:address].chomp!(']') unless e[:address] =~ /[@]\[[0-9A-Za-z:\.]+\]\z/
e[:address] = e[:address].sub(/\A["]/, '').chomp('"') unless e[:address] =~ /\A["].+["][@]/

Expand Down Expand Up @@ -285,7 +318,7 @@ def self.expand_verp(email)
return nil unless email.is_a? Object::String
return nil unless cv = email.split('@', 2).first.match(/\A[-\w]+?[+](\w[-.\w]+\w)[=](\w[-.\w]+\w)\z/)
verp0 = cv[1] + '@' + cv[2]
return verp0 if Sisimai::RFC5322.is_emailaddress(verp0)
return verp0 if Sisimai::Address.is_emailaddress(verp0)
end

# Expand alias: remove from '+' to '@'
Expand All @@ -294,7 +327,7 @@ def self.expand_verp(email)
# @example Expand alias
# expand_alias('neko+straycat@example.org') #=> 'neko@example.org'
def self.expand_alias(email)
return nil unless Sisimai::RFC5322.is_emailaddress(email)
return nil unless Sisimai::Address.is_emailaddress(email)

local = email.split('@')
return nil unless cv = local[0].match(/\A([-\w]+?)[+].+\z/)
Expand All @@ -312,48 +345,47 @@ def self.expand_alias(email)
attr_accessor :name, :comment

# Constructor of Sisimai::Address
# @param <str> [String] argv1 Email address
# @return [Sisimai::Address, Nil] Object or nil when the email
# address was not valid
# @param [Hash] argv1 Email address, name, and other elements
# @return [Sisimai::Address] Object or nil when the email address was not valid.
# @example new({address: 'neko@example.org', name: 'Neko', comment: '(nyaan)')} # => Sisimai::Address object
def initialize(argv1)
return nil unless argv1

addrs = Sisimai::Address.find(argv1)
return nil unless addrs
return nil if addrs.empty?
thing = addrs.shift

if cv = thing[:address].match(/\A([^\s]+)[@]([^@]+)\z/) ||
thing[:address].match(/\A(["].+?["])[@]([^@]+)\z/)
return nil unless argv1.is_a? Hash
return nil unless argv1[:address]
return nil if argv1[:address].empty?

heads = ['<']
tails = ['>', ',', '.', ';']
if cv = argv1[:address].match(/\A([^\s]+)[@]([^@]+)\z/) ||
argv1[:address].match(/\A(["].+?["])[@]([^@]+)\z/)
# Get the local part and the domain part from the email address
lpart = cv[1]
dpart = cv[2]
email = Sisimai::Address.expand_verp(thing[:address])
lpart = cv[1]; heads.each do { |e| lpart.gsub!(/\A#{e}/, '') if lpart.start_with?(e) }
dpart = cv[2]; tails.each do { |e| dpart.gsub!(/#{e}\z/, '') if dpart.end_with?(e) }
email = Sisimai::Address.expand_verp(argv1[:address])
aname = nil

unless email
# Is not VERP address, try to expand the address as an alias
email = Sisimai::Address.expand_alias(thing[:address]) || ''
email = Sisimai::Address.expand_alias(argv1[:address]) || ''
aname = true unless email.empty?
end

if email =~ /\A.+[@].+?\z/
# The address is a VERP or an alias
if aname
# The address is an alias: neko+nyaan@example.jp
@alias = thing[:address]
@alias = argv1[:address]
else
# The address is a VERP: b+neko=example.jp@example.org
@verp = thing[:address]
@verp = argv1[:address]
end
end
@user = lpart
@host = dpart
@address = lpart + '@' + dpart
else
# The argument does not include "@"
return nil unless Sisimai::RFC5322.is_mailerdaemon(thing[:address])
return nil if thing[:address].include?(' ')
return nil unless Sisimai::Address.is_mailerdaemon(argv1[:address])
return nil if argv1[:address].include?(' ')

# The argument does not include " "
@user = thing[:address]
Expand Down
33 changes: 0 additions & 33 deletions lib/sisimai/data/yaml.rb

This file was deleted.

7 changes: 4 additions & 3 deletions lib/sisimai/datetime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,10 @@ def parse(argv1)

while p = timetokens.shift do
# Parse each piece of time
if p =~ /\A[A-Z][a-z]{2}[,]?\z/
if p =~ /\A[A-Z][a-z]{2,}[,]?\z/
# Day of week or Day of week; Thu, Apr, ...
p.chop if p.size == 4 # Thu, -> Thu
p.gsub!(/,\z/, '') if p.end_with?(',') # "Thu," => "Thu"
p = p[0,3] if p.size > 3

if DayOfWeek[:abbr].include?(p)
# Day of week; Mon, Thu, Sun,...
Expand All @@ -273,7 +274,7 @@ def parse(argv1)
# Year or Day; 2005, 31, 04, 1, ...
if p.to_i > 31
# The piece is the value of an year
v[:Y] = p
v[:Y] = p.to_i
else
# The piece is the value of a day
if v[:d]
Expand Down

0 comments on commit 08b5879

Please sign in to comment.