sisimai 4.25.17-java → 5.0.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +3 -3
- data/ANALYTICAL-PRECISION +2 -2
- data/Benchmarks.mk +3 -3
- data/CONTRIBUTING +1 -1
- data/ChangeLog.md +406 -407
- data/Developers.mk +5 -6
- data/Gemfile +1 -1
- data/Makefile +12 -12
- data/README-JA.md +142 -94
- data/README.md +282 -150
- data/Rakefile +9 -3
- data/Repository.mk +2 -3
- data/lib/sisimai/address.rb +118 -74
- data/lib/sisimai/arf.rb +84 -82
- data/lib/sisimai/datetime.rb +5 -52
- data/lib/sisimai/{data → fact}/json.rb +7 -9
- data/lib/sisimai/fact/yaml.rb +31 -0
- data/lib/sisimai/fact.rb +468 -0
- data/lib/sisimai/lhost/activehunter.rb +12 -14
- data/lib/sisimai/lhost/amavis.rb +11 -14
- data/lib/sisimai/lhost/amazonses.rb +37 -41
- data/lib/sisimai/lhost/amazonworkmail.rb +15 -18
- data/lib/sisimai/lhost/aol.rb +12 -14
- data/lib/sisimai/lhost/apachejames.rb +19 -21
- data/lib/sisimai/lhost/barracuda.rb +10 -12
- data/lib/sisimai/lhost/bigfoot.rb +21 -21
- data/lib/sisimai/lhost/biglobe.rb +15 -16
- data/lib/sisimai/lhost/courier.rb +20 -20
- data/lib/sisimai/lhost/domino.rb +23 -19
- data/lib/sisimai/lhost/einsundeins.rb +20 -16
- data/lib/sisimai/lhost/exchange2003.rb +30 -29
- data/lib/sisimai/lhost/exchange2007.rb +70 -58
- data/lib/sisimai/lhost/exim.rb +175 -161
- data/lib/sisimai/lhost/ezweb.rb +31 -56
- data/lib/sisimai/lhost/facebook.rb +21 -33
- data/lib/sisimai/lhost/fml.rb +43 -48
- data/lib/sisimai/lhost/gmail.rb +29 -29
- data/lib/sisimai/lhost/gmx.rb +18 -17
- data/lib/sisimai/lhost/googlegroups.rb +9 -10
- data/lib/sisimai/lhost/gsuite.rb +21 -27
- data/lib/sisimai/lhost/imailserver.rb +25 -39
- data/lib/sisimai/lhost/interscanmss.rb +28 -31
- data/lib/sisimai/lhost/kddi.rb +22 -28
- data/lib/sisimai/lhost/mailfoundry.rb +11 -12
- data/lib/sisimai/lhost/mailmarshalsmtp.rb +25 -29
- data/lib/sisimai/lhost/mailru.rb +33 -27
- data/lib/sisimai/lhost/mcafee.rb +21 -31
- data/lib/sisimai/lhost/messagelabs.rb +17 -20
- data/lib/sisimai/lhost/messagingserver.rb +40 -37
- data/lib/sisimai/lhost/mfilter.rb +15 -16
- data/lib/sisimai/lhost/mxlogic.rb +24 -23
- data/lib/sisimai/lhost/notes.rb +17 -17
- data/lib/sisimai/lhost/office365.rb +63 -27
- data/lib/sisimai/lhost/opensmtpd.rb +12 -13
- data/lib/sisimai/lhost/outlook.rb +12 -15
- data/lib/sisimai/lhost/postfix.rb +179 -129
- data/lib/sisimai/lhost/powermta.rb +12 -14
- data/lib/sisimai/lhost/qmail.rb +44 -47
- data/lib/sisimai/lhost/receivingses.rb +15 -20
- data/lib/sisimai/lhost/sendgrid.rb +34 -32
- data/lib/sisimai/lhost/sendmail.rb +66 -53
- data/lib/sisimai/lhost/surfcontrol.rb +19 -19
- data/lib/sisimai/lhost/v5sendmail.rb +45 -39
- data/lib/sisimai/lhost/verizon.rb +35 -39
- data/lib/sisimai/lhost/x1.rb +18 -17
- data/lib/sisimai/lhost/x2.rb +17 -14
- data/lib/sisimai/lhost/x3.rb +19 -19
- data/lib/sisimai/lhost/x4.rb +72 -57
- data/lib/sisimai/lhost/x5.rb +17 -19
- data/lib/sisimai/lhost/x6.rb +41 -17
- data/lib/sisimai/lhost/yahoo.rb +17 -16
- data/lib/sisimai/lhost/yandex.rb +16 -20
- data/lib/sisimai/lhost/zoho.rb +16 -15
- data/lib/sisimai/lhost.rb +8 -10
- data/lib/sisimai/mail/maildir.rb +1 -3
- data/lib/sisimai/mail/mbox.rb +3 -4
- data/lib/sisimai/mail/memory.rb +0 -1
- data/lib/sisimai/mail/stdin.rb +1 -3
- data/lib/sisimai/mail.rb +3 -7
- data/lib/sisimai/mda.rb +28 -42
- data/lib/sisimai/message.rb +435 -325
- data/lib/sisimai/order.rb +5 -5
- data/lib/sisimai/reason/authfailure.rb +64 -0
- data/lib/sisimai/reason/badreputation.rb +53 -0
- data/lib/sisimai/reason/blocked.rb +94 -160
- data/lib/sisimai/reason/contenterror.rb +8 -9
- data/lib/sisimai/reason/delivered.rb +4 -6
- data/lib/sisimai/reason/exceedlimit.rb +10 -12
- data/lib/sisimai/reason/expired.rb +6 -8
- data/lib/sisimai/reason/feedback.rb +2 -3
- data/lib/sisimai/reason/filtered.rb +17 -19
- data/lib/sisimai/reason/hasmoved.rb +9 -10
- data/lib/sisimai/reason/hostunknown.rb +15 -15
- data/lib/sisimai/reason/mailboxfull.rb +10 -12
- data/lib/sisimai/reason/mailererror.rb +18 -20
- data/lib/sisimai/reason/mesgtoobig.rb +9 -11
- data/lib/sisimai/reason/networkerror.rb +5 -8
- data/lib/sisimai/reason/norelaying.rb +8 -11
- data/lib/sisimai/reason/notaccept.rb +13 -14
- data/lib/sisimai/reason/notcompliantrfc.rb +43 -0
- data/lib/sisimai/reason/onhold.rb +6 -9
- data/lib/sisimai/reason/policyviolation.rb +14 -12
- data/lib/sisimai/reason/rejected.rb +26 -24
- data/lib/sisimai/reason/requireptr.rb +69 -0
- data/lib/sisimai/reason/securityerror.rb +33 -36
- data/lib/sisimai/reason/spamdetected.rb +114 -147
- data/lib/sisimai/reason/speeding.rb +49 -0
- data/lib/sisimai/reason/suspend.rb +11 -11
- data/lib/sisimai/reason/syntaxerror.rb +11 -10
- data/lib/sisimai/reason/systemerror.rb +7 -9
- data/lib/sisimai/reason/systemfull.rb +7 -8
- data/lib/sisimai/reason/toomanyconn.rb +9 -11
- data/lib/sisimai/reason/undefined.rb +2 -3
- data/lib/sisimai/reason/userunknown.rb +129 -146
- data/lib/sisimai/reason/vacation.rb +3 -4
- data/lib/sisimai/reason/virusdetected.rb +10 -11
- data/lib/sisimai/reason.rb +59 -64
- data/lib/sisimai/rfc1894.rb +55 -28
- data/lib/sisimai/rfc2045.rb +373 -0
- data/lib/sisimai/rfc3464.rb +250 -308
- data/lib/sisimai/rfc3834.rb +42 -45
- data/lib/sisimai/rfc5322.rb +75 -100
- data/lib/sisimai/rfc5965.rb +31 -0
- data/lib/sisimai/rhost/cox.rb +5 -6
- data/lib/sisimai/rhost/franceptt.rb +6 -8
- data/lib/sisimai/rhost/godaddy.rb +12 -12
- data/lib/sisimai/rhost/{googleapps.rb → google.rb} +80 -72
- data/lib/sisimai/rhost/iua.rb +9 -10
- data/lib/sisimai/rhost/kddi.rb +6 -8
- data/lib/sisimai/rhost/{exchangeonline.rb → microsoft.rb} +115 -114
- data/lib/sisimai/rhost/mimecast.rb +42 -40
- data/lib/sisimai/rhost/nttdocomo.rb +13 -18
- data/lib/sisimai/rhost/spectrum.rb +10 -12
- data/lib/sisimai/rhost/{tencentqq.rb → tencent.rb} +7 -8
- data/lib/sisimai/rhost.rb +23 -31
- data/lib/sisimai/smtp/command.rb +59 -0
- data/lib/sisimai/smtp/error.rb +4 -7
- data/lib/sisimai/smtp/reply.rb +161 -74
- data/lib/sisimai/smtp/status.rb +504 -393
- data/lib/sisimai/smtp/transcript.rb +124 -0
- data/lib/sisimai/smtp.rb +0 -1
- data/lib/sisimai/string.rb +74 -5
- data/lib/sisimai/time.rb +1 -2
- data/lib/sisimai/version.rb +1 -1
- data/lib/sisimai.rb +35 -21
- data/set-of-emails/maildir/bsd/lhost-domino-02.eml +6 -3
- data/set-of-emails/maildir/bsd/lhost-googlegroups-15.eml +174 -0
- data/set-of-emails/maildir/bsd/lhost-gsuite-15.eml +229 -0
- data/set-of-emails/maildir/bsd/lhost-postfix-75.eml +51 -0
- data/set-of-emails/maildir/bsd/lhost-postfix-76.eml +101 -0
- data/set-of-emails/maildir/bsd/lhost-postfix-77.eml +74 -0
- data/set-of-emails/maildir/bsd/lhost-postfix-78.eml +91 -0
- data/set-of-emails/maildir/bsd/lhost-receivingses-08.eml +88 -0
- data/set-of-emails/maildir/bsd/rfc3464-43.eml +88 -0
- data/set-of-emails/maildir/bsd/rhost-google-03.eml +101 -0
- data/set-of-emails/maildir/bsd/rhost-google-04.eml +102 -0
- data/set-of-emails/maildir/bsd/rhost-google-05.eml +82 -0
- data/set-of-emails/maildir/bsd/rhost-google-06.eml +102 -0
- data/set-of-emails/maildir/bsd/rhost-google-07.eml +69 -0
- data/set-of-emails/maildir/bsd/rhost-google-08.eml +99 -0
- data/sisimai-java.gemspec +1 -1
- data/sisimai.gemspec +1 -1
- metadata +42 -23
- data/.rspec +0 -2
- data/lib/sisimai/data/yaml.rb +0 -33
- data/lib/sisimai/data.rb +0 -411
- data/lib/sisimai/mime.rb +0 -456
- data/set-of-emails/maildir/mac/reported-from-nick4tech-san-01.eml +0 -6
- /data/set-of-emails/maildir/bsd/{rfc3464-41.eml → rfc3834-05.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-googleapps-01.eml → rhost-google-01.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-googleapps-02.eml → rhost-google-02.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-01.eml → rhost-microsoft-01.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-02.eml → rhost-microsoft-02.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-03.eml → rhost-microsoft-03.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-tencentqq-01.eml → rhost-tencent-01.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-tencentqq-02.eml → rhost-tencent-02.eml} +0 -0
- /data/set-of-emails/maildir/bsd/{rhost-tencentqq-03.eml → rhost-tencent-03.eml} +0 -0
data/lib/sisimai/message.rb
CHANGED
@@ -1,365 +1,475 @@
|
|
1
1
|
module Sisimai
|
2
|
-
# Sisimai::Message convert bounce email text to data structure. It resolve
|
3
|
-
#
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
2
|
+
# Sisimai::Message convert bounce email text to data structure. It resolve email text into an UNIX
|
3
|
+
# From line, the header part of the mail, delivery status, and RFC822 header part. When the email
|
4
|
+
# given as a argument of "rise" method is not a bounce email, the method returns nil.
|
5
|
+
module Message
|
6
|
+
class << self
|
7
|
+
require 'sisimai/rfc1894'
|
8
|
+
require 'sisimai/rfc2045'
|
9
|
+
require 'sisimai/rfc5322'
|
10
|
+
require 'sisimai/rfc5965'
|
11
|
+
require 'sisimai/address'
|
12
|
+
require 'sisimai/string'
|
13
|
+
require 'sisimai/order'
|
14
|
+
require 'sisimai/lhost'
|
15
|
+
|
16
|
+
DefaultSet = Sisimai::Order.another.freeze
|
17
|
+
LhostTable = Sisimai::Lhost.path.freeze
|
18
|
+
Fields1894 = Sisimai::RFC1894.FIELDINDEX.freeze
|
19
|
+
Fields5322 = Sisimai::RFC5322.FIELDINDEX.freeze
|
20
|
+
Fields5965 = Sisimai::RFC5965.FIELDINDEX.freeze
|
21
|
+
FieldIndex = [Fields1894.flatten, Fields5322.flatten, Fields5965.flatten].flatten.freeze
|
22
|
+
FieldTable = FieldIndex.map { |e| [e.downcase, e] }.to_h.freeze
|
23
|
+
ReplacesAs = { 'Content-Type' => [%w[message/xdelivery-status message/delivery-status]] }.freeze
|
24
|
+
Boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers'].freeze
|
25
|
+
|
26
|
+
# Read an email message and convert to structured format
|
27
|
+
# @param [Hash] argvs Module to be loaded
|
28
|
+
# @options argvs [String] :data Entire email message
|
29
|
+
# @options argvs [Array] :load User defined MTA module list
|
30
|
+
# @options argvs [Array] :order The order of MTA modules
|
31
|
+
# @options argvs [Code] :hook Reference to callback method
|
32
|
+
# @return [Sisimai::Message] Structured email data or nil if each
|
33
|
+
# value of the arguments are missing
|
34
|
+
def rise(**argvs)
|
35
|
+
return nil unless argvs
|
36
|
+
email = argvs[:data].scrub('?').gsub("\r\n", "\n")
|
37
|
+
thing = { 'from' => '','header' => {}, 'rfc822' => '', 'ds' => [], 'catch' => nil }
|
38
|
+
param = {}
|
39
|
+
|
40
|
+
# 0. Load specified MTA modules
|
41
|
+
[:load, :order].each do |e|
|
42
|
+
# Order of MTA modules
|
43
|
+
next unless argvs[e]
|
44
|
+
next unless argvs[e].is_a? Array
|
45
|
+
next if argvs[e].empty?
|
46
|
+
param[e.to_s] = argvs[e]
|
47
|
+
end
|
48
|
+
tobeloaded = Sisimai::Message.load(param)
|
49
|
+
|
50
|
+
aftersplit = nil
|
51
|
+
beforefact = nil
|
52
|
+
parseagain = 0
|
53
|
+
|
54
|
+
while parseagain < 2 do
|
55
|
+
# 1. Split email data to headers and a body part.
|
56
|
+
break unless aftersplit = Sisimai::Message.part(email)
|
57
|
+
|
58
|
+
# 2. Convert email headers from text to hash reference
|
59
|
+
thing['from'] = aftersplit[0]
|
60
|
+
thing['header'] = Sisimai::Message.makemap(aftersplit[1])
|
61
|
+
|
62
|
+
# 3. Decode and rewrite the "Subject:" header
|
63
|
+
unless thing['header']['subject'].empty?
|
64
|
+
# Decode MIME-Encoded "Subject:" header
|
65
|
+
cv = thing['header']['subject']
|
66
|
+
cq = Sisimai::RFC2045.is_encoded(cv) ? Sisimai::RFC2045.decodeH(cv.split(/[ ]/)) : cv
|
67
|
+
cl = cq.downcase
|
68
|
+
p1 = cl.index('fwd:'); p1 = cl.index('fw:') unless p1
|
69
|
+
|
70
|
+
# Remove "Fwd:" string from the Subject: header
|
71
|
+
if p1
|
72
|
+
# Delete quoted strings, quote symbols(>)
|
73
|
+
cq = Sisimai::String.sweep(cq[cq.index(':') + 1, cq.size])
|
74
|
+
aftersplit[2] = aftersplit[2].gsub(/^[>]+[ ]/, '').gsub(/^[>]$/, '')
|
75
|
+
end
|
76
|
+
thing['header']['subject'] = cq
|
77
|
+
end
|
78
|
+
|
79
|
+
# 4. Rewrite message body for detecting the bounce reason
|
80
|
+
param = {
|
81
|
+
'hook' => argvs[:hook] || nil,
|
82
|
+
'mail' => thing,
|
83
|
+
'body' => aftersplit[2],
|
84
|
+
'tobeloaded' => tobeloaded,
|
85
|
+
'tryonfirst' => Sisimai::Order.make(thing['header']['subject'])
|
86
|
+
}
|
87
|
+
break if beforefact = Sisimai::Message.sift(param)
|
88
|
+
break unless Boundaries.any? { |a| aftersplit[2].include?(a) }
|
89
|
+
|
90
|
+
# 5. Try to sift again
|
91
|
+
# There is a bounce message inside of mutipart/*, try to sift the first message/rfc822
|
92
|
+
# part as a entire message body again.
|
93
|
+
parseagain += 1
|
94
|
+
email = Sisimai::RFC5322.part(aftersplit[2], Boundaries, true).pop.sub(/\A[\r\n\s]+/, '')
|
95
|
+
break unless email.size > 128
|
67
96
|
end
|
68
|
-
|
97
|
+
return nil unless beforefact
|
98
|
+
return nil if beforefact.empty?
|
99
|
+
|
100
|
+
# 6. Rewrite headers of the original message in the body part
|
101
|
+
%w|ds catch rfc822|.each { |e| thing[e] = beforefact[e] }
|
102
|
+
p = beforefact['rfc822']
|
103
|
+
p = aftersplit[2] if p.empty?
|
104
|
+
thing['rfc822'] = p.is_a?(::String) ? Sisimai::Message.makemap(p, true) : p
|
105
|
+
|
106
|
+
return thing
|
69
107
|
end
|
70
108
|
|
71
|
-
#
|
72
|
-
param
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
@header = thing['header']
|
90
|
-
@ds = thing['ds']
|
91
|
-
@rfc822 = thing['rfc822']
|
92
|
-
@catch = thing['catch'] || nil
|
93
|
-
end
|
109
|
+
# Load MTA modules which specified at 'order' and 'load' in the argument
|
110
|
+
# @param [Hash] argvs Module information to be loaded
|
111
|
+
# @options argvs [Array] load User defined MTA module list
|
112
|
+
# @options argvs [Array] order The order of MTA modules
|
113
|
+
# @return [Array] Module list
|
114
|
+
# @since v4.20.0
|
115
|
+
def load(argvs)
|
116
|
+
modulelist = []
|
117
|
+
tobeloaded = []
|
118
|
+
|
119
|
+
%w[load order].each do |e|
|
120
|
+
# The order of MTA modules specified by user
|
121
|
+
next unless argvs[e]
|
122
|
+
next unless argvs[e].is_a? Array
|
123
|
+
next if argvs[e].empty?
|
124
|
+
|
125
|
+
modulelist += argvs['order'] if e == 'order'
|
126
|
+
next unless e == 'load'
|
94
127
|
|
95
|
-
# Check whether the object has valid content or not
|
96
|
-
# @return [True,False] returns true if the object is void
|
97
|
-
def void; return @ds ? false : true; end
|
98
|
-
|
99
|
-
# Load MTA modules which specified at 'order' and 'load' in the argument
|
100
|
-
# @param [Hash] argvs Module information to be loaded
|
101
|
-
# @options argvs [Array] load User defined MTA module list
|
102
|
-
# @options argvs [Array] order The order of MTA modules
|
103
|
-
# @return [Array] Module list
|
104
|
-
# @since v4.20.0
|
105
|
-
def self.load(argvs)
|
106
|
-
modulelist = []
|
107
|
-
tobeloaded = []
|
108
|
-
|
109
|
-
%w[load order].each do |e|
|
110
|
-
# The order of MTA modules specified by user
|
111
|
-
next unless argvs[e]
|
112
|
-
next unless argvs[e].is_a? Array
|
113
|
-
next if argvs[e].empty?
|
114
|
-
|
115
|
-
modulelist += argvs['order'] if e == 'order'
|
116
|
-
next unless e == 'load'
|
117
|
-
|
118
|
-
# Load user defined MTA module
|
119
|
-
argvs['load'].each do |v|
|
120
128
|
# Load user defined MTA module
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
129
|
+
argvs['load'].each do |v|
|
130
|
+
# Load user defined MTA module
|
131
|
+
begin
|
132
|
+
require v.to_s.gsub('::', '/').downcase
|
133
|
+
rescue LoadError
|
134
|
+
warn ' ***warning: Failed to load ' << v
|
135
|
+
next
|
136
|
+
end
|
137
|
+
tobeloaded << v
|
126
138
|
end
|
127
|
-
tobeloaded << v
|
128
139
|
end
|
129
|
-
end
|
130
140
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
141
|
+
while e = modulelist.shift do
|
142
|
+
# Append the custom order of MTA modules
|
143
|
+
next if tobeloaded.index(e)
|
144
|
+
tobeloaded << e
|
145
|
+
end
|
146
|
+
|
147
|
+
return tobeloaded
|
135
148
|
end
|
136
149
|
|
137
|
-
|
138
|
-
|
150
|
+
# Divide email data up headers and a body part.
|
151
|
+
# @param [String] email Email data
|
152
|
+
# @return [Array] Email data after split
|
153
|
+
def part(email)
|
154
|
+
return nil if email.empty?
|
139
155
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
def self.divideup(email)
|
144
|
-
return nil if email.empty?
|
145
|
-
|
146
|
-
block = ['', '', ''] # 0:From, 1:Header, 2:Body
|
147
|
-
email.gsub!(/\r\n/, "\n") if email.include?("\r\n")
|
148
|
-
email.gsub!(/[ \t]+$/, '') if email =~ /[ \t]+$/
|
149
|
-
|
150
|
-
(block[1], block[2]) = email.split(/\n\n/, 2)
|
151
|
-
return nil unless block[1]
|
152
|
-
return nil unless block[2]
|
153
|
-
|
154
|
-
if block[1].start_with?('From ')
|
155
|
-
# From MAILER-DAEMON Tue Feb 11 00:00:00 2014
|
156
|
-
block[0] = block[1].split(/\n/, 2)[0].delete("\r")
|
157
|
-
else
|
158
|
-
# Set pseudo UNIX From line
|
159
|
-
block[0] = 'MAILER-DAEMON Tue Feb 11 00:00:00 2014'
|
160
|
-
end
|
161
|
-
block[1] << "\n" unless block[1].end_with?("\n")
|
162
|
-
|
163
|
-
%w[image/ application/ text/html].each do |e|
|
164
|
-
# https://github.com/sisimai/p5-sisimai/issues/492, Reduce email size
|
165
|
-
p0 = 0
|
166
|
-
p1 = 0
|
167
|
-
ep = e == 'text/html' ? '</html>' : "--\n"
|
168
|
-
while true
|
169
|
-
# Remove each part from "Content-Type: image/..." to "--\n" (the end of each boundary)
|
170
|
-
p0 = block[2].index('Content-Type: ' + e, p0); break unless p0
|
171
|
-
p1 = block[2].index(ep, p0 + 32); break unless p1
|
172
|
-
block[2][p0, p1 - p0] = ''
|
173
|
-
end
|
174
|
-
end
|
175
|
-
block[2] << "\n"
|
156
|
+
parts = ['', '', ''] # 0:From, 1:Header, 2:Body
|
157
|
+
email.gsub!(/\A\s+/, '')
|
158
|
+
email.gsub!(/\r\n/, "\n") if email.include?("\r\n")
|
176
159
|
|
177
|
-
|
178
|
-
|
160
|
+
(parts[1], parts[2]) = email.split(/\n\n/, 2)
|
161
|
+
return nil unless parts[1]
|
162
|
+
return nil unless parts[2]
|
179
163
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
# @return [Hash] Structured email header data
|
184
|
-
# @since v4.25.6
|
185
|
-
def self.makemap(argv0 = '', argv1 = nil)
|
186
|
-
return {} if argv0.empty?
|
187
|
-
argv0.gsub!(/^[>]+[ ]/m, '') # Remove '>' indent symbol of forwarded message
|
188
|
-
|
189
|
-
# Select and convert all the headers in $argv0. The following regular expression
|
190
|
-
# is based on https://gist.github.com/xtetsuji/b080e1f5551d17242f6415aba8a00239
|
191
|
-
headermaps = { 'subject' => '' }
|
192
|
-
recvheader = []
|
193
|
-
argv0.scan(/^([\w-]+):[ ]*(.*?)\n(?![\s\t])/m) { |e| headermaps[e[0].downcase] = e[1] }
|
194
|
-
headermaps.delete('received')
|
195
|
-
headermaps.each_key { |e| headermaps[e].gsub!(/\n[\s\t]+/, ' ') }
|
196
|
-
|
197
|
-
if argv0.include?('Received:')
|
198
|
-
# Capture values of each Received: header
|
199
|
-
recvheader = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
|
200
|
-
recvheader.each { |e| e.gsub!(/\n[\s\t]+/, ' ') }
|
201
|
-
end
|
202
|
-
headermaps['received'] = recvheader
|
203
|
-
|
204
|
-
return headermaps unless argv1
|
205
|
-
return headermaps if headermaps['subject'].empty?
|
206
|
-
|
207
|
-
# Convert MIME-Encoded subject
|
208
|
-
if Sisimai::String.is_8bit(headermaps['subject'])
|
209
|
-
# The value of ``Subject'' header is including multibyte character,
|
210
|
-
# is not MIME-Encoded text.
|
211
|
-
headermaps['subject'].scrub!('?')
|
212
|
-
else
|
213
|
-
# MIME-Encoded subject field or ASCII characters only
|
214
|
-
r = []
|
215
|
-
if Sisimai::MIME.is_mimeencoded(headermaps['subject'])
|
216
|
-
# split the value of Subject by borderline
|
217
|
-
headermaps['subject'].split(/ /).each do |v|
|
218
|
-
# Insert value to the array if the string is MIME encoded text
|
219
|
-
r << v if Sisimai::MIME.is_mimeencoded(v)
|
220
|
-
end
|
164
|
+
if parts[1].start_with?('From ')
|
165
|
+
# From MAILER-DAEMON Tue Feb 11 00:00:00 2014
|
166
|
+
parts[0] = parts[1].split(/\n/, 2)[0].delete("\r")
|
221
167
|
else
|
222
|
-
#
|
223
|
-
|
168
|
+
# Set pseudo UNIX From line
|
169
|
+
parts[0] = 'MAILER-DAEMON Tue Feb 11 00:00:00 2014'
|
224
170
|
end
|
225
|
-
|
171
|
+
parts[1] << "\n" unless parts[1].end_with?("\n")
|
172
|
+
|
173
|
+
%w[image/ application/ text/html].each do |e|
|
174
|
+
# https://github.com/sisimai/p5-sisimai/issues/492, Reduce email size
|
175
|
+
p0 = 0
|
176
|
+
p1 = 0
|
177
|
+
ep = e == 'text/html' ? '</html>' : "--\n"
|
178
|
+
while true
|
179
|
+
# Remove each part from "Content-Type: image/..." to "--\n" (the end of each boundary)
|
180
|
+
p0 = parts[2].index('Content-Type: ' + e, p0); break unless p0
|
181
|
+
p1 = parts[2].index(ep, p0 + 32); break unless p1
|
182
|
+
parts[2][p0, p1 - p0] = ''
|
183
|
+
end
|
184
|
+
end
|
185
|
+
parts[2] << "\n"
|
186
|
+
return parts
|
226
187
|
end
|
227
|
-
return headermaps
|
228
|
-
end
|
229
188
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
# PRECHECK_EACH_HEADER:
|
252
|
-
# Set empty string if the value is nil
|
253
|
-
mailheader['from'] ||= ''
|
254
|
-
mailheader['subject'] ||= ''
|
255
|
-
mailheader['content-type'] ||= ''
|
256
|
-
|
257
|
-
# Decode BASE64 Encoded message body, rewrite.
|
258
|
-
mesgformat = (mailheader['content-type'] || '').downcase
|
259
|
-
ctencoding = (mailheader['content-transfer-encoding'] || '').downcase
|
260
|
-
if mesgformat.start_with?('text/')
|
261
|
-
# Content-Type: text/plain; charset=UTF-8
|
262
|
-
if ctencoding == 'base64'
|
263
|
-
# Content-Transfer-Encoding: base64
|
264
|
-
bodystring = Sisimai::MIME.base64d(bodystring)
|
265
|
-
|
266
|
-
elsif ctencoding == 'quoted-printable'
|
267
|
-
# Content-Transfer-Encoding: quoted-printable
|
268
|
-
bodystring = Sisimai::MIME.qprintd(bodystring)
|
189
|
+
# Convert a text including email headers to a hash reference
|
190
|
+
# @param [String] argv0 Email header data
|
191
|
+
# @param [Bool] argv1 Decode "Subject:" header
|
192
|
+
# @return [Hash] Structured email header data
|
193
|
+
# @since v4.25.6
|
194
|
+
def makemap(argv0 = '', argv1 = nil)
|
195
|
+
return {} if argv0.empty?
|
196
|
+
argv0.gsub!(/^[>]+[ ]/m, '') # Remove '>' indent symbol of forwarded message
|
197
|
+
|
198
|
+
# Select and convert all the headers in $argv0. The following regular expression is based on
|
199
|
+
# https://gist.github.com/xtetsuji/b080e1f5551d17242f6415aba8a00239
|
200
|
+
headermaps = { 'subject' => '' }
|
201
|
+
recvheader = []
|
202
|
+
argv0.scan(/^([\w-]+):[ ]*(.*?)\n(?![\s\t])/m) { |e| headermaps[e[0].downcase] = e[1] }
|
203
|
+
headermaps.delete('received')
|
204
|
+
headermaps.each_key { |e| headermaps[e].gsub!(/\n[\s\t]+/, ' ') }
|
205
|
+
|
206
|
+
if argv0.include?('Received:')
|
207
|
+
# Capture values of each Received: header
|
208
|
+
recvheader = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
|
209
|
+
recvheader.each { |e| e.gsub!(/\n[\s\t]+/, ' ') }
|
269
210
|
end
|
211
|
+
headermaps['received'] = recvheader
|
270
212
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
havecaught = hookmethod.call(p)
|
293
|
-
rescue StandardError => ce
|
294
|
-
warn ' ***warning: Something is wrong in hook method :' << ce.to_s
|
213
|
+
return headermaps unless argv1
|
214
|
+
return headermaps if headermaps['subject'].empty?
|
215
|
+
|
216
|
+
# Convert MIME-Encoded subject
|
217
|
+
if Sisimai::String.is_8bit(headermaps['subject'])
|
218
|
+
# The value of ``Subject'' header is including multibyte character, is not MIME-Encoded text.
|
219
|
+
headermaps['subject'].scrub!('?')
|
220
|
+
else
|
221
|
+
# MIME-Encoded subject field or ASCII characters only
|
222
|
+
r = []
|
223
|
+
if Sisimai::RFC2045.is_encoded(headermaps['subject'])
|
224
|
+
# split the value of Subject by borderline
|
225
|
+
headermaps['subject'].split(/ /).each do |v|
|
226
|
+
# Insert value to the array if the string is MIME encoded text
|
227
|
+
r << v if Sisimai::RFC2045.is_encoded(v)
|
228
|
+
end
|
229
|
+
else
|
230
|
+
# Subject line is not MIME encoded
|
231
|
+
r << headermaps['subject']
|
232
|
+
end
|
233
|
+
headermaps['subject'] = Sisimai::RFC2045.decodeH(r)
|
295
234
|
end
|
235
|
+
return headermaps
|
296
236
|
end
|
297
237
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
238
|
+
# @abstract Tidy up each field name and format
|
239
|
+
# @param [String] argv0 Strings including field and value used at an email
|
240
|
+
# @return [String] Strings tidied up
|
241
|
+
# @since v5.0.0
|
242
|
+
def tidy(argv0 = '')
|
243
|
+
return '' if argv0.empty?
|
244
|
+
|
245
|
+
email = ''
|
246
|
+
argv0.split("\n").each do |e|
|
247
|
+
# Find and tidy up fields defined in RFC5322, RFC1894, and RFC5965
|
248
|
+
# 1. Find a field label defined in RFC5322, RFC1894, or RFC5965 from this line
|
249
|
+
p0 = e.index(':') || 0
|
250
|
+
cf = e.downcase[0, p0]
|
251
|
+
|
252
|
+
unless FieldTable.has_key?(cf)
|
253
|
+
email << e + "\n"
|
254
|
+
next
|
313
255
|
end
|
314
256
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
257
|
+
# 2. There is a field label defined in RFC5322, RFC1894, or RFC5965 from this line.
|
258
|
+
# Code below replaces the field name with a valid name listed in @fieldindex when
|
259
|
+
# the field name does not match with a valid name.
|
260
|
+
# - Before: Message-id: <...>
|
261
|
+
# - After: Message-Id: <...>
|
262
|
+
fieldlabel = FieldTable[cf]
|
263
|
+
substring0 = e[0, p0]
|
264
|
+
e[0, p0] = fieldlabel unless substring0.empty?
|
265
|
+
|
266
|
+
# 3. There is no " " (space character) immediately after ":"
|
267
|
+
# - before: Content-Type:text/plain
|
268
|
+
# - After: Content-Type: text/plain
|
269
|
+
substring0 = e[p0 + 1, 1]
|
270
|
+
e[p0, 1] = ': ' if substring0 != ' '
|
271
|
+
|
272
|
+
# 4. Remove redundant space characters after ":"
|
273
|
+
while true
|
274
|
+
# - Before: Message-Id: <...>
|
275
|
+
# - After: Message-Id: <...>
|
276
|
+
break unless p0 + 2 < e.size
|
277
|
+
break unless e[p0 + 2, 1] == ' '
|
278
|
+
e[p0 + 2, 1] = ''
|
323
279
|
end
|
324
280
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
281
|
+
# 5. Tidy up a sub type of each field defined in RFC1894 such as Reporting-MTA: DNS;...
|
282
|
+
p1 = e.index(';') || -1
|
283
|
+
while true
|
284
|
+
# Such as Diagnostic-Code, Remote-MTA, and so on
|
285
|
+
# - Before: Diagnostic-Code: SMTP;550 User unknown
|
286
|
+
# - After: Diagnostic-Code: smtp; 550 User unknown
|
287
|
+
break unless p1 > p0
|
288
|
+
break unless ['Content-Type'].concat(Fields1894).any? { |a| a.start_with?(fieldlabel) }
|
289
|
+
|
290
|
+
substring0 = e[p0 + 2, p1 - p0 - 1]
|
291
|
+
e[p0 + 2, substring0.size] = substring0.downcase + ' '
|
292
|
+
break
|
332
293
|
end
|
333
294
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
295
|
+
# 6. Remove redundant space characters after ";"
|
296
|
+
while true
|
297
|
+
# - Before: Diagnostic-Code: SMTP; 550 User unknown
|
298
|
+
# - After: Diagnostic-Code: SMTP; 550 User unknown
|
299
|
+
break unless p1 + 2 < e.size
|
300
|
+
break unless e[p1 + 2, 1] == ' '
|
301
|
+
e[p1 + 2, 1] = ''
|
339
302
|
end
|
340
303
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
304
|
+
# 7. Tidy up a value, and a parameter of Content-Type: field
|
305
|
+
while true
|
306
|
+
# Replace the value of "Content-Type" field
|
307
|
+
break unless ReplacesAs.has_key?(fieldlabel)
|
308
|
+
p2 = 0
|
309
|
+
|
310
|
+
ReplacesAs[fieldlabel].each do |f|
|
311
|
+
# Content-Type: message/xdelivery-status
|
312
|
+
p2 = e.index(f[0]) || -1
|
313
|
+
next unless p2 > 1
|
314
|
+
|
315
|
+
e[p2, f[0].size] = f[1]
|
316
|
+
p1 = e.index(';')
|
317
|
+
break
|
318
|
+
end
|
319
|
+
|
320
|
+
# A parameter name of Content-Type field should be a lower-cased string
|
321
|
+
# - Before: Content-Type: text/plain; CharSet=ascii; Boundary=...
|
322
|
+
# - After: Content-Type: text/plain; charset=ascii; boundary=...
|
323
|
+
break unless fieldlabel == 'Content-Type'
|
324
|
+
p2 = e.index('=') || -1
|
325
|
+
break unless p2 > 0
|
326
|
+
break unless p2 > p1
|
327
|
+
|
328
|
+
substring0 = e[p1 + 2, p2 - p1 - 2]
|
329
|
+
e[p1 + 2, p2 - p1 - 2] = substring0.downcase
|
330
|
+
break
|
347
331
|
end
|
348
|
-
|
349
|
-
break # as of now, we have no sample email for coding this block
|
332
|
+
email << e + "\n"
|
350
333
|
end
|
334
|
+
|
335
|
+
email << "\n" unless email.end_with?("\n\n")
|
336
|
+
return email
|
351
337
|
end
|
352
|
-
return nil unless parseddata
|
353
338
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
339
|
+
# @abstract Sift bounce mail with each MTA module
|
340
|
+
# @param [Hash] argvs Processing message entity.
|
341
|
+
# @param options argvs [Hash] mail Email message entity
|
342
|
+
# @param options mail [String] from From line of mbox
|
343
|
+
# @param options mail [Hash] header Email header data
|
344
|
+
# @param options mail [String] rfc822 Original message part
|
345
|
+
# @param options mail [Array] ds Delivery status list(parsed data)
|
346
|
+
# @param options argvs [String] body Email message body
|
347
|
+
# @param options argvs [Array] tryonfirst MTA module list to load on first
|
348
|
+
# @param options argvs [Array] tobeloaded User defined MTA module list
|
349
|
+
# @return [Hash] Parsed and structured bounce mails
|
350
|
+
def sift(argvs)
|
351
|
+
return nil unless argvs['mail']
|
352
|
+
return nil unless argvs['body']
|
353
|
+
|
354
|
+
mailheader = argvs['mail']['header']
|
355
|
+
bodystring = argvs['body']
|
356
|
+
hookmethod = argvs['hook'] || nil
|
357
|
+
havecaught = nil
|
358
|
+
return nil unless mailheader
|
359
|
+
|
360
|
+
# PRECHECK_EACH_HEADER:
|
361
|
+
# Set empty string if the value is nil
|
362
|
+
mailheader['from'] ||= ''
|
363
|
+
mailheader['subject'] ||= ''
|
364
|
+
mailheader['content-type'] ||= ''
|
365
|
+
|
366
|
+
# Tidy up each field name and value in the entire message body
|
367
|
+
bodystring = Sisimai::Message.tidy(bodystring)
|
368
|
+
|
369
|
+
# Decode BASE64 Encoded message body, rewrite.
|
370
|
+
mesgformat = (mailheader['content-type'] || '').downcase
|
371
|
+
ctencoding = (mailheader['content-transfer-encoding'] || '').downcase
|
372
|
+
if mesgformat.start_with?('text/plain', 'text/html')
|
373
|
+
# Content-Type: text/plain; charset=UTF-8
|
374
|
+
if ctencoding == 'base64'
|
375
|
+
# Content-Transfer-Encoding: base64
|
376
|
+
bodystring = Sisimai::RFC2045.decodeB(bodystring)
|
377
|
+
|
378
|
+
elsif ctencoding == 'quoted-printable'
|
379
|
+
# Content-Transfer-Encoding: quoted-printable
|
380
|
+
bodystring = Sisimai::RFC2045.decodeQ(bodystring)
|
381
|
+
end
|
382
|
+
|
383
|
+
if mesgformat.start_with?('text/html;')
|
384
|
+
# Content-Type: text/html;...
|
385
|
+
bodystring = Sisimai::String.to_plain(bodystring, true)
|
386
|
+
end
|
387
|
+
elsif mesgformat.start_with?('multipart/')
|
388
|
+
# NOT text/plain
|
389
|
+
# In case of Content-Type: multipart/*
|
390
|
+
p = Sisimai::RFC2045.makeflat(mailheader['content-type'], bodystring)
|
391
|
+
bodystring = p unless p.empty?
|
392
|
+
end
|
393
|
+
bodystring = bodystring.scrub('?').delete("\r").gsub("\t", " ")
|
394
|
+
|
395
|
+
haveloaded = {}
|
396
|
+
havesifted = nil
|
397
|
+
modulename = ''
|
398
|
+
if hookmethod.is_a? Proc
|
399
|
+
# Call the hook method
|
400
|
+
begin
|
401
|
+
p = { 'headers' => mailheader, 'message' => bodystring }
|
402
|
+
havecaught = hookmethod.call(p)
|
403
|
+
rescue StandardError => ce
|
404
|
+
warn ' ***warning: Something is wrong in hook method ":hook":' << ce.to_s
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
catch :PARSER do
|
409
|
+
while true
|
410
|
+
# 1. User-Defined Module
|
411
|
+
# 2. MTA Module Candidates to be tried on first
|
412
|
+
# 3. Sisimai::Lhost::*
|
413
|
+
# 4. Sisimai::RFC3464
|
414
|
+
# 5. Sisimai::ARF
|
415
|
+
# 6. Sisimai::RFC3834
|
416
|
+
while r = argvs['tobeloaded'].shift do
|
417
|
+
# Call user defined MTA modules
|
418
|
+
next if haveloaded[r]
|
419
|
+
havesifted = Module.const_get(r).inquire(mailheader, bodystring)
|
420
|
+
haveloaded[r] = true
|
421
|
+
modulename = r
|
422
|
+
throw :PARSER if havesifted
|
423
|
+
end
|
424
|
+
|
425
|
+
[argvs['tryonfirst'], DefaultSet].flatten.each do |r|
|
426
|
+
# Try MTA module candidates
|
427
|
+
next if haveloaded[r]
|
428
|
+
require LhostTable[r]
|
429
|
+
havesifted = Module.const_get(r).inquire(mailheader, bodystring)
|
430
|
+
haveloaded[r] = true
|
431
|
+
modulename = r
|
432
|
+
throw :PARSER if havesifted
|
433
|
+
end
|
434
|
+
|
435
|
+
unless haveloaded['Sisimai::RFC3464']
|
436
|
+
# When the all of Sisimai::Lhost::* modules did not return bounce data, call Sisimai::RFC3464;
|
437
|
+
require 'sisimai/rfc3464'
|
438
|
+
havesifted = Sisimai::RFC3464.inquire(mailheader, bodystring)
|
439
|
+
modulename = 'RFC3464'
|
440
|
+
throw :PARSER if havesifted
|
441
|
+
end
|
442
|
+
|
443
|
+
unless haveloaded['Sisimai::ARF']
|
444
|
+
# Feedback Loop message
|
445
|
+
require 'sisimai/arf'
|
446
|
+
havesifted = Sisimai::ARF.inquire(mailheader, bodystring) if Sisimai::ARF.is_arf(mailheader)
|
447
|
+
throw :PARSER if havesifted
|
448
|
+
end
|
449
|
+
|
450
|
+
unless haveloaded['Sisimai::RFC3834']
|
451
|
+
# Try to sift the message as auto reply message defined in RFC3834
|
452
|
+
require 'sisimai/rfc3834'
|
453
|
+
havesifted = Sisimai::RFC3834.inquire(mailheader, bodystring)
|
454
|
+
modulename = 'RFC3834'
|
455
|
+
throw :PARSER if havesifted
|
456
|
+
end
|
457
|
+
|
458
|
+
break # as of now, we have no sample email for coding this block
|
459
|
+
end
|
460
|
+
end
|
461
|
+
return nil unless havesifted
|
462
|
+
|
463
|
+
havesifted['catch'] = havecaught
|
464
|
+
modulename = modulename.sub(/\A.+::/, '')
|
465
|
+
havesifted['ds'].each do |e|
|
466
|
+
e['agent'] = modulename unless e['agent']
|
467
|
+
e.each_key { |a| e[a] ||= '' } # Replace nil with ""
|
468
|
+
end
|
469
|
+
return havesifted
|
359
470
|
end
|
360
|
-
return parseddata
|
361
|
-
end
|
362
471
|
|
472
|
+
end
|
363
473
|
end
|
364
474
|
end
|
365
475
|
|