sisimai 4.25.16-java → 5.0.2-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/rake-test.yml +55 -0
- data/.travis.yml +3 -3
- data/ANALYTICAL-PRECISION +2 -2
- data/Benchmarks.mk +3 -3
- data/CONTRIBUTING +1 -1
- data/ChangeLog.md +451 -393
- data/Developers.mk +5 -6
- data/Gemfile +1 -1
- data/Makefile +15 -15
- data/README-JA.md +323 -149
- data/README.md +319 -149
- 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 +506 -0
- data/lib/sisimai/lhost/activehunter.rb +12 -14
- data/lib/sisimai/lhost/amavis.rb +11 -14
- data/lib/sisimai/lhost/amazonses.rb +37 -42
- data/lib/sisimai/lhost/amazonworkmail.rb +15 -19
- data/lib/sisimai/lhost/aol.rb +12 -15
- data/lib/sisimai/lhost/apachejames.rb +19 -21
- data/lib/sisimai/lhost/barracuda.rb +10 -12
- data/lib/sisimai/lhost/bigfoot.rb +21 -22
- data/lib/sisimai/lhost/biglobe.rb +15 -16
- data/lib/sisimai/lhost/courier.rb +20 -20
- data/lib/sisimai/lhost/domino.rb +23 -20
- data/lib/sisimai/lhost/einsundeins.rb +23 -18
- data/lib/sisimai/lhost/exchange2003.rb +30 -29
- data/lib/sisimai/lhost/exchange2007.rb +70 -58
- data/lib/sisimai/lhost/exim.rb +179 -174
- data/lib/sisimai/lhost/ezweb.rb +31 -56
- data/lib/sisimai/lhost/facebook.rb +21 -34
- 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 +11 -11
- data/lib/sisimai/lhost/gsuite.rb +21 -28
- 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 +37 -40
- data/lib/sisimai/lhost/mcafee.rb +21 -31
- data/lib/sisimai/lhost/messagelabs.rb +17 -21
- data/lib/sisimai/lhost/messagingserver.rb +40 -37
- data/lib/sisimai/lhost/mfilter.rb +16 -17
- data/lib/sisimai/lhost/mxlogic.rb +24 -33
- data/lib/sisimai/lhost/notes.rb +17 -17
- data/lib/sisimai/lhost/office365.rb +64 -28
- data/lib/sisimai/lhost/opensmtpd.rb +12 -13
- data/lib/sisimai/lhost/outlook.rb +12 -16
- data/lib/sisimai/lhost/postfix.rb +179 -130
- data/lib/sisimai/lhost/powermta.rb +12 -14
- data/lib/sisimai/lhost/qmail.rb +44 -47
- data/lib/sisimai/lhost/receivingses.rb +15 -21
- data/lib/sisimai/lhost/sendgrid.rb +34 -34
- data/lib/sisimai/lhost/sendmail.rb +65 -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 -21
- 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 +444 -326
- data/lib/sisimai/order.rb +5 -5
- data/lib/sisimai/reason/authfailure.rb +65 -0
- data/lib/sisimai/reason/badreputation.rb +53 -0
- data/lib/sisimai/reason/blocked.rb +96 -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 +7 -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 +11 -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 +34 -36
- data/lib/sisimai/reason/spamdetected.rb +115 -147
- data/lib/sisimai/reason/speeding.rb +49 -0
- data/lib/sisimai/reason/suspend.rb +12 -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 +177 -146
- 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/google.rb +530 -0
- 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 +51 -42
- data/lib/sisimai/rhost/nttdocomo.rb +12 -12
- 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 +507 -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 +46 -31
- 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/lhost-sendmail-60.eml +85 -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 +48 -26
- 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/lib/sisimai/rhost/googleapps.rb +0 -261
- /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,366 +1,484 @@
|
|
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
|
-
|
67
|
-
|
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
|
68
96
|
end
|
69
|
-
|
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
|
70
107
|
end
|
71
108
|
|
72
|
-
#
|
73
|
-
param
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
@header = thing['header']
|
91
|
-
@ds = thing['ds']
|
92
|
-
@rfc822 = thing['rfc822']
|
93
|
-
@catch = thing['catch'] || nil
|
94
|
-
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'
|
95
127
|
|
96
|
-
# Check whether the object has valid content or not
|
97
|
-
# @return [True,False] returns true if the object is void
|
98
|
-
def void; return @ds ? false : true; end
|
99
|
-
|
100
|
-
# Load MTA modules which specified at 'order' and 'load' in the argument
|
101
|
-
# @param [Hash] argvs Module information to be loaded
|
102
|
-
# @options argvs [Array] load User defined MTA module list
|
103
|
-
# @options argvs [Array] order The order of MTA modules
|
104
|
-
# @return [Array] Module list
|
105
|
-
# @since v4.20.0
|
106
|
-
def self.load(argvs)
|
107
|
-
modulelist = []
|
108
|
-
tobeloaded = []
|
109
|
-
|
110
|
-
%w[load order].each do |e|
|
111
|
-
# The order of MTA modules specified by user
|
112
|
-
next unless argvs[e]
|
113
|
-
next unless argvs[e].is_a? Array
|
114
|
-
next if argvs[e].empty?
|
115
|
-
|
116
|
-
modulelist += argvs['order'] if e == 'order'
|
117
|
-
next unless e == 'load'
|
118
|
-
|
119
|
-
# Load user defined MTA module
|
120
|
-
argvs['load'].each do |v|
|
121
128
|
# Load user defined MTA module
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
127
138
|
end
|
128
|
-
tobeloaded << v
|
129
139
|
end
|
130
|
-
end
|
131
140
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
136
148
|
end
|
137
149
|
|
138
|
-
|
139
|
-
|
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?
|
140
155
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
def self.divideup(email)
|
145
|
-
return nil if email.empty?
|
146
|
-
|
147
|
-
block = ['', '', ''] # 0:From, 1:Header, 2:Body
|
148
|
-
email.gsub!(/\r\n/, "\n") if email.include?("\r\n")
|
149
|
-
email.gsub!(/[ \t]+$/, '') if email =~ /[ \t]+$/
|
150
|
-
|
151
|
-
(block[1], block[2]) = email.split(/\n\n/, 2)
|
152
|
-
return nil unless block[1]
|
153
|
-
return nil unless block[2]
|
154
|
-
|
155
|
-
if block[1].start_with?('From ')
|
156
|
-
# From MAILER-DAEMON Tue Feb 11 00:00:00 2014
|
157
|
-
block[0] = block[1].split(/\n/, 2)[0].delete("\r")
|
158
|
-
else
|
159
|
-
# Set pseudo UNIX From line
|
160
|
-
block[0] = 'MAILER-DAEMON Tue Feb 11 00:00:00 2014'
|
161
|
-
end
|
162
|
-
block[1] << "\n" unless block[1].end_with?("\n")
|
163
|
-
|
164
|
-
%w[image/ application/ text/html].each do |e|
|
165
|
-
# https://github.com/sisimai/p5-sisimai/issues/492, Reduce email size
|
166
|
-
p0 = 0
|
167
|
-
p1 = 0
|
168
|
-
ep = e == 'text/html' ? '</html>' : "--\n"
|
169
|
-
while true
|
170
|
-
# Remove each part from "Content-Type: image/..." to "--\n" (the end of each boundary)
|
171
|
-
p0 = block[2].index('Content-Type: ' + e, p0); break unless p0
|
172
|
-
p1 = block[2].index(ep, p0 + 32); break unless p1
|
173
|
-
block[2][p0, p1 - p0] = ''
|
174
|
-
end
|
175
|
-
end
|
176
|
-
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")
|
177
159
|
|
178
|
-
|
179
|
-
|
160
|
+
(parts[1], parts[2]) = email.split(/\n\n/, 2)
|
161
|
+
return nil unless parts[1]
|
162
|
+
return nil unless parts[2]
|
180
163
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
# @return [Hash] Structured email header data
|
185
|
-
# @since v4.25.6
|
186
|
-
def self.makemap(argv0 = '', argv1 = nil)
|
187
|
-
return {} if argv0.empty?
|
188
|
-
argv0.gsub!(/^[>]+[ ]/m, '') # Remove '>' indent symbol of forwarded message
|
189
|
-
|
190
|
-
# Select and convert all the headers in $argv0. The following regular expression
|
191
|
-
# is based on https://gist.github.com/xtetsuji/b080e1f5551d17242f6415aba8a00239
|
192
|
-
headermaps = { 'subject' => '' }
|
193
|
-
recvheader = []
|
194
|
-
argv0.scan(/^([\w-]+):[ ]*(.*?)\n(?![\s\t])/m) { |e| headermaps[e[0].downcase] = e[1] }
|
195
|
-
headermaps.delete('received')
|
196
|
-
headermaps.each_key { |e| headermaps[e].gsub!(/\n[\s\t]+/, ' ') }
|
197
|
-
|
198
|
-
if argv0.include?('Received:')
|
199
|
-
# Capture values of each Received: header
|
200
|
-
recvheader = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
|
201
|
-
recvheader.each { |e| e.gsub!(/\n[\s\t]+/, ' ') }
|
202
|
-
end
|
203
|
-
headermaps['received'] = recvheader
|
204
|
-
|
205
|
-
return headermaps unless argv1
|
206
|
-
return headermaps if headermaps['subject'].empty?
|
207
|
-
|
208
|
-
# Convert MIME-Encoded subject
|
209
|
-
if Sisimai::String.is_8bit(headermaps['subject'])
|
210
|
-
# The value of ``Subject'' header is including multibyte character,
|
211
|
-
# is not MIME-Encoded text.
|
212
|
-
headermaps['subject'].scrub!('?')
|
213
|
-
else
|
214
|
-
# MIME-Encoded subject field or ASCII characters only
|
215
|
-
r = []
|
216
|
-
if Sisimai::MIME.is_mimeencoded(headermaps['subject'])
|
217
|
-
# split the value of Subject by borderline
|
218
|
-
headermaps['subject'].split(/ /).each do |v|
|
219
|
-
# Insert value to the array if the string is MIME encoded text
|
220
|
-
r << v if Sisimai::MIME.is_mimeencoded(v)
|
221
|
-
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")
|
222
167
|
else
|
223
|
-
#
|
224
|
-
|
168
|
+
# Set pseudo UNIX From line
|
169
|
+
parts[0] = 'MAILER-DAEMON Tue Feb 11 00:00:00 2014'
|
225
170
|
end
|
226
|
-
|
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
|
227
187
|
end
|
228
|
-
return headermaps
|
229
|
-
end
|
230
188
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
if mesgformat.start_with?('text/')
|
262
|
-
# Content-Type: text/plain; charset=UTF-8
|
263
|
-
if ctencoding == 'base64'
|
264
|
-
# Content-Transfer-Encoding: base64
|
265
|
-
bodystring = Sisimai::MIME.base64d(bodystring)
|
266
|
-
|
267
|
-
elsif ctencoding == 'quoted-printable'
|
268
|
-
# Content-Transfer-Encoding: quoted-printable
|
269
|
-
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
|
+
receivedby = []
|
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
|
+
re = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
|
209
|
+
re.each do |e|
|
210
|
+
# 1. Exclude the Received header including "(qmail ** invoked from network)".
|
211
|
+
# 2. Convert all consecutive spaces and line breaks into a single space character.
|
212
|
+
next if e.include?(' invoked by uid')
|
213
|
+
next if e.include?(' invoked from network')
|
214
|
+
|
215
|
+
e.gsub!(/\n[\s\t]+/, ' ')
|
216
|
+
e.squeeze!("\n\t ")
|
217
|
+
receivedby << e
|
218
|
+
end
|
270
219
|
end
|
220
|
+
headermaps['received'] = receivedby
|
271
221
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
havecaught = hookmethod.call(p)
|
294
|
-
rescue StandardError => ce
|
295
|
-
warn ' ***warning: Something is wrong in hook method :' << ce.to_s
|
222
|
+
return headermaps unless argv1
|
223
|
+
return headermaps if headermaps['subject'].empty?
|
224
|
+
|
225
|
+
# Convert MIME-Encoded subject
|
226
|
+
if Sisimai::String.is_8bit(headermaps['subject'])
|
227
|
+
# The value of ``Subject'' header is including multibyte character, is not MIME-Encoded text.
|
228
|
+
headermaps['subject'].scrub!('?')
|
229
|
+
else
|
230
|
+
# MIME-Encoded subject field or ASCII characters only
|
231
|
+
r = []
|
232
|
+
if Sisimai::RFC2045.is_encoded(headermaps['subject'])
|
233
|
+
# split the value of Subject by borderline
|
234
|
+
headermaps['subject'].split(/ /).each do |v|
|
235
|
+
# Insert value to the array if the string is MIME encoded text
|
236
|
+
r << v if Sisimai::RFC2045.is_encoded(v)
|
237
|
+
end
|
238
|
+
else
|
239
|
+
# Subject line is not MIME encoded
|
240
|
+
r << headermaps['subject']
|
241
|
+
end
|
242
|
+
headermaps['subject'] = Sisimai::RFC2045.decodeH(r)
|
296
243
|
end
|
244
|
+
return headermaps
|
297
245
|
end
|
298
246
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
247
|
+
# @abstract Tidy up each field name and format
|
248
|
+
# @param [String] argv0 Strings including field and value used at an email
|
249
|
+
# @return [String] Strings tidied up
|
250
|
+
# @since v5.0.0
|
251
|
+
def tidy(argv0 = '')
|
252
|
+
return '' if argv0.empty?
|
253
|
+
|
254
|
+
email = ''
|
255
|
+
argv0.split("\n").each do |e|
|
256
|
+
# Find and tidy up fields defined in RFC5322, RFC1894, and RFC5965
|
257
|
+
# 1. Find a field label defined in RFC5322, RFC1894, or RFC5965 from this line
|
258
|
+
p0 = e.index(':') || 0
|
259
|
+
cf = e.downcase[0, p0]
|
260
|
+
|
261
|
+
unless FieldTable.has_key?(cf)
|
262
|
+
email << e + "\n"
|
263
|
+
next
|
314
264
|
end
|
315
265
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
266
|
+
# 2. There is a field label defined in RFC5322, RFC1894, or RFC5965 from this line.
|
267
|
+
# Code below replaces the field name with a valid name listed in @fieldindex when
|
268
|
+
# the field name does not match with a valid name.
|
269
|
+
# - Before: Message-id: <...>
|
270
|
+
# - After: Message-Id: <...>
|
271
|
+
fieldlabel = FieldTable[cf]
|
272
|
+
substring0 = e[0, p0]
|
273
|
+
e[0, p0] = fieldlabel unless substring0.empty?
|
274
|
+
|
275
|
+
# 3. There is no " " (space character) immediately after ":"
|
276
|
+
# - before: Content-Type:text/plain
|
277
|
+
# - After: Content-Type: text/plain
|
278
|
+
substring0 = e[p0 + 1, 1]
|
279
|
+
e[p0, 1] = ': ' if substring0 != ' '
|
280
|
+
|
281
|
+
# 4. Remove redundant space characters after ":"
|
282
|
+
while true
|
283
|
+
# - Before: Message-Id: <...>
|
284
|
+
# - After: Message-Id: <...>
|
285
|
+
break unless p0 + 2 < e.size
|
286
|
+
break unless e[p0 + 2, 1] == ' '
|
287
|
+
e[p0 + 2, 1] = ''
|
324
288
|
end
|
325
289
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
290
|
+
# 5. Tidy up a sub type of each field defined in RFC1894 such as Reporting-MTA: DNS;...
|
291
|
+
p1 = e.index(';') || -1
|
292
|
+
while true
|
293
|
+
# Such as Diagnostic-Code, Remote-MTA, and so on
|
294
|
+
# - Before: Diagnostic-Code: SMTP;550 User unknown
|
295
|
+
# - After: Diagnostic-Code: smtp; 550 User unknown
|
296
|
+
break unless p1 > p0
|
297
|
+
break unless ['Content-Type'].concat(Fields1894).any? { |a| a.start_with?(fieldlabel) }
|
298
|
+
|
299
|
+
substring0 = e[p0 + 2, p1 - p0 - 1]
|
300
|
+
e[p0 + 2, substring0.size] = substring0.downcase + ' '
|
301
|
+
break
|
333
302
|
end
|
334
303
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
304
|
+
# 6. Remove redundant space characters after ";"
|
305
|
+
while true
|
306
|
+
# - Before: Diagnostic-Code: SMTP; 550 User unknown
|
307
|
+
# - After: Diagnostic-Code: SMTP; 550 User unknown
|
308
|
+
break unless p1 + 2 < e.size
|
309
|
+
break unless e[p1 + 2, 1] == ' '
|
310
|
+
e[p1 + 2, 1] = ''
|
340
311
|
end
|
341
312
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
313
|
+
# 7. Tidy up a value, and a parameter of Content-Type: field
|
314
|
+
while true
|
315
|
+
# Replace the value of "Content-Type" field
|
316
|
+
break unless ReplacesAs.has_key?(fieldlabel)
|
317
|
+
p2 = 0
|
318
|
+
|
319
|
+
ReplacesAs[fieldlabel].each do |f|
|
320
|
+
# Content-Type: message/xdelivery-status
|
321
|
+
p2 = e.index(f[0]) || -1
|
322
|
+
next unless p2 > 1
|
323
|
+
|
324
|
+
e[p2, f[0].size] = f[1]
|
325
|
+
p1 = e.index(';')
|
326
|
+
break
|
327
|
+
end
|
328
|
+
|
329
|
+
# A parameter name of Content-Type field should be a lower-cased string
|
330
|
+
# - Before: Content-Type: text/plain; CharSet=ascii; Boundary=...
|
331
|
+
# - After: Content-Type: text/plain; charset=ascii; boundary=...
|
332
|
+
break unless fieldlabel == 'Content-Type'
|
333
|
+
p2 = e.index('=') || -1
|
334
|
+
break unless p2 > 0
|
335
|
+
break unless p2 > p1
|
336
|
+
|
337
|
+
substring0 = e[p1 + 2, p2 - p1 - 2]
|
338
|
+
e[p1 + 2, p2 - p1 - 2] = substring0.downcase
|
339
|
+
break
|
348
340
|
end
|
349
|
-
|
350
|
-
break # as of now, we have no sample email for coding this block
|
341
|
+
email << e + "\n"
|
351
342
|
end
|
343
|
+
|
344
|
+
email << "\n" unless email.end_with?("\n\n")
|
345
|
+
return email
|
352
346
|
end
|
353
|
-
return nil unless parseddata
|
354
347
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
348
|
+
# @abstract Sift bounce mail with each MTA module
|
349
|
+
# @param [Hash] argvs Processing message entity.
|
350
|
+
# @param options argvs [Hash] mail Email message entity
|
351
|
+
# @param options mail [String] from From line of mbox
|
352
|
+
# @param options mail [Hash] header Email header data
|
353
|
+
# @param options mail [String] rfc822 Original message part
|
354
|
+
# @param options mail [Array] ds Delivery status list(parsed data)
|
355
|
+
# @param options argvs [String] body Email message body
|
356
|
+
# @param options argvs [Array] tryonfirst MTA module list to load on first
|
357
|
+
# @param options argvs [Array] tobeloaded User defined MTA module list
|
358
|
+
# @return [Hash] Parsed and structured bounce mails
|
359
|
+
def sift(argvs)
|
360
|
+
return nil unless argvs['mail']
|
361
|
+
return nil unless argvs['body']
|
362
|
+
|
363
|
+
mailheader = argvs['mail']['header']
|
364
|
+
bodystring = argvs['body']
|
365
|
+
hookmethod = argvs['hook'] || nil
|
366
|
+
havecaught = nil
|
367
|
+
return nil unless mailheader
|
368
|
+
|
369
|
+
# PRECHECK_EACH_HEADER:
|
370
|
+
# Set empty string if the value is nil
|
371
|
+
mailheader['from'] ||= ''
|
372
|
+
mailheader['subject'] ||= ''
|
373
|
+
mailheader['content-type'] ||= ''
|
374
|
+
|
375
|
+
# Tidy up each field name and value in the entire message body
|
376
|
+
bodystring = Sisimai::Message.tidy(bodystring)
|
377
|
+
|
378
|
+
# Decode BASE64 Encoded message body, rewrite.
|
379
|
+
mesgformat = (mailheader['content-type'] || '').downcase
|
380
|
+
ctencoding = (mailheader['content-transfer-encoding'] || '').downcase
|
381
|
+
if mesgformat.start_with?('text/plain', 'text/html')
|
382
|
+
# Content-Type: text/plain; charset=UTF-8
|
383
|
+
if ctencoding == 'base64'
|
384
|
+
# Content-Transfer-Encoding: base64
|
385
|
+
bodystring = Sisimai::RFC2045.decodeB(bodystring)
|
386
|
+
|
387
|
+
elsif ctencoding == 'quoted-printable'
|
388
|
+
# Content-Transfer-Encoding: quoted-printable
|
389
|
+
bodystring = Sisimai::RFC2045.decodeQ(bodystring)
|
390
|
+
end
|
391
|
+
|
392
|
+
if mesgformat.start_with?('text/html;')
|
393
|
+
# Content-Type: text/html;...
|
394
|
+
bodystring = Sisimai::String.to_plain(bodystring, true)
|
395
|
+
end
|
396
|
+
elsif mesgformat.start_with?('multipart/')
|
397
|
+
# NOT text/plain
|
398
|
+
# In case of Content-Type: multipart/*
|
399
|
+
p = Sisimai::RFC2045.makeflat(mailheader['content-type'], bodystring)
|
400
|
+
bodystring = p unless p.empty?
|
401
|
+
end
|
402
|
+
bodystring = bodystring.scrub('?').delete("\r").gsub("\t", " ")
|
403
|
+
|
404
|
+
haveloaded = {}
|
405
|
+
havesifted = nil
|
406
|
+
modulename = ''
|
407
|
+
if hookmethod.is_a? Proc
|
408
|
+
# Call the hook method
|
409
|
+
begin
|
410
|
+
p = { 'headers' => mailheader, 'message' => bodystring }
|
411
|
+
havecaught = hookmethod.call(p)
|
412
|
+
rescue StandardError => ce
|
413
|
+
warn ' ***warning: Something is wrong in hook method ":hook":' << ce.to_s
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
catch :PARSER do
|
418
|
+
while true
|
419
|
+
# 1. User-Defined Module
|
420
|
+
# 2. MTA Module Candidates to be tried on first
|
421
|
+
# 3. Sisimai::Lhost::*
|
422
|
+
# 4. Sisimai::RFC3464
|
423
|
+
# 5. Sisimai::ARF
|
424
|
+
# 6. Sisimai::RFC3834
|
425
|
+
while r = argvs['tobeloaded'].shift do
|
426
|
+
# Call user defined MTA modules
|
427
|
+
next if haveloaded[r]
|
428
|
+
havesifted = Module.const_get(r).inquire(mailheader, bodystring)
|
429
|
+
haveloaded[r] = true
|
430
|
+
modulename = r
|
431
|
+
throw :PARSER if havesifted
|
432
|
+
end
|
433
|
+
|
434
|
+
[argvs['tryonfirst'], DefaultSet].flatten.each do |r|
|
435
|
+
# Try MTA module candidates
|
436
|
+
next if haveloaded[r]
|
437
|
+
require LhostTable[r]
|
438
|
+
havesifted = Module.const_get(r).inquire(mailheader, bodystring)
|
439
|
+
haveloaded[r] = true
|
440
|
+
modulename = r
|
441
|
+
throw :PARSER if havesifted
|
442
|
+
end
|
443
|
+
|
444
|
+
unless haveloaded['Sisimai::RFC3464']
|
445
|
+
# When the all of Sisimai::Lhost::* modules did not return bounce data, call Sisimai::RFC3464;
|
446
|
+
require 'sisimai/rfc3464'
|
447
|
+
havesifted = Sisimai::RFC3464.inquire(mailheader, bodystring)
|
448
|
+
modulename = 'RFC3464'
|
449
|
+
throw :PARSER if havesifted
|
450
|
+
end
|
451
|
+
|
452
|
+
unless haveloaded['Sisimai::ARF']
|
453
|
+
# Feedback Loop message
|
454
|
+
require 'sisimai/arf'
|
455
|
+
havesifted = Sisimai::ARF.inquire(mailheader, bodystring) if Sisimai::ARF.is_arf(mailheader)
|
456
|
+
throw :PARSER if havesifted
|
457
|
+
end
|
458
|
+
|
459
|
+
unless haveloaded['Sisimai::RFC3834']
|
460
|
+
# Try to sift the message as auto reply message defined in RFC3834
|
461
|
+
require 'sisimai/rfc3834'
|
462
|
+
havesifted = Sisimai::RFC3834.inquire(mailheader, bodystring)
|
463
|
+
modulename = 'RFC3834'
|
464
|
+
throw :PARSER if havesifted
|
465
|
+
end
|
466
|
+
|
467
|
+
break # as of now, we have no sample email for coding this block
|
468
|
+
end
|
469
|
+
end
|
470
|
+
return nil unless havesifted
|
471
|
+
|
472
|
+
havesifted['catch'] = havecaught
|
473
|
+
modulename = modulename.sub(/\A.+::/, '')
|
474
|
+
havesifted['ds'].each do |e|
|
475
|
+
e['agent'] = modulename unless e['agent']
|
476
|
+
e.each_key { |a| e[a] ||= '' } # Replace nil with ""
|
477
|
+
end
|
478
|
+
return havesifted
|
360
479
|
end
|
361
|
-
return parseddata
|
362
|
-
end
|
363
480
|
|
481
|
+
end
|
364
482
|
end
|
365
483
|
end
|
366
484
|
|