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.
Files changed (180) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rake-test.yml +55 -0
  3. data/.travis.yml +3 -3
  4. data/ANALYTICAL-PRECISION +2 -2
  5. data/Benchmarks.mk +3 -3
  6. data/CONTRIBUTING +1 -1
  7. data/ChangeLog.md +451 -393
  8. data/Developers.mk +5 -6
  9. data/Gemfile +1 -1
  10. data/Makefile +15 -15
  11. data/README-JA.md +323 -149
  12. data/README.md +319 -149
  13. data/Rakefile +9 -3
  14. data/Repository.mk +2 -3
  15. data/lib/sisimai/address.rb +118 -74
  16. data/lib/sisimai/arf.rb +84 -82
  17. data/lib/sisimai/datetime.rb +5 -52
  18. data/lib/sisimai/{data → fact}/json.rb +7 -9
  19. data/lib/sisimai/fact/yaml.rb +31 -0
  20. data/lib/sisimai/fact.rb +506 -0
  21. data/lib/sisimai/lhost/activehunter.rb +12 -14
  22. data/lib/sisimai/lhost/amavis.rb +11 -14
  23. data/lib/sisimai/lhost/amazonses.rb +37 -42
  24. data/lib/sisimai/lhost/amazonworkmail.rb +15 -19
  25. data/lib/sisimai/lhost/aol.rb +12 -15
  26. data/lib/sisimai/lhost/apachejames.rb +19 -21
  27. data/lib/sisimai/lhost/barracuda.rb +10 -12
  28. data/lib/sisimai/lhost/bigfoot.rb +21 -22
  29. data/lib/sisimai/lhost/biglobe.rb +15 -16
  30. data/lib/sisimai/lhost/courier.rb +20 -20
  31. data/lib/sisimai/lhost/domino.rb +23 -20
  32. data/lib/sisimai/lhost/einsundeins.rb +23 -18
  33. data/lib/sisimai/lhost/exchange2003.rb +30 -29
  34. data/lib/sisimai/lhost/exchange2007.rb +70 -58
  35. data/lib/sisimai/lhost/exim.rb +179 -174
  36. data/lib/sisimai/lhost/ezweb.rb +31 -56
  37. data/lib/sisimai/lhost/facebook.rb +21 -34
  38. data/lib/sisimai/lhost/fml.rb +43 -48
  39. data/lib/sisimai/lhost/gmail.rb +29 -29
  40. data/lib/sisimai/lhost/gmx.rb +18 -17
  41. data/lib/sisimai/lhost/googlegroups.rb +11 -11
  42. data/lib/sisimai/lhost/gsuite.rb +21 -28
  43. data/lib/sisimai/lhost/imailserver.rb +25 -39
  44. data/lib/sisimai/lhost/interscanmss.rb +28 -31
  45. data/lib/sisimai/lhost/kddi.rb +22 -28
  46. data/lib/sisimai/lhost/mailfoundry.rb +11 -12
  47. data/lib/sisimai/lhost/mailmarshalsmtp.rb +25 -29
  48. data/lib/sisimai/lhost/mailru.rb +37 -40
  49. data/lib/sisimai/lhost/mcafee.rb +21 -31
  50. data/lib/sisimai/lhost/messagelabs.rb +17 -21
  51. data/lib/sisimai/lhost/messagingserver.rb +40 -37
  52. data/lib/sisimai/lhost/mfilter.rb +16 -17
  53. data/lib/sisimai/lhost/mxlogic.rb +24 -33
  54. data/lib/sisimai/lhost/notes.rb +17 -17
  55. data/lib/sisimai/lhost/office365.rb +64 -28
  56. data/lib/sisimai/lhost/opensmtpd.rb +12 -13
  57. data/lib/sisimai/lhost/outlook.rb +12 -16
  58. data/lib/sisimai/lhost/postfix.rb +179 -130
  59. data/lib/sisimai/lhost/powermta.rb +12 -14
  60. data/lib/sisimai/lhost/qmail.rb +44 -47
  61. data/lib/sisimai/lhost/receivingses.rb +15 -21
  62. data/lib/sisimai/lhost/sendgrid.rb +34 -34
  63. data/lib/sisimai/lhost/sendmail.rb +65 -53
  64. data/lib/sisimai/lhost/surfcontrol.rb +19 -19
  65. data/lib/sisimai/lhost/v5sendmail.rb +45 -39
  66. data/lib/sisimai/lhost/verizon.rb +35 -39
  67. data/lib/sisimai/lhost/x1.rb +18 -17
  68. data/lib/sisimai/lhost/x2.rb +17 -14
  69. data/lib/sisimai/lhost/x3.rb +19 -19
  70. data/lib/sisimai/lhost/x4.rb +72 -57
  71. data/lib/sisimai/lhost/x5.rb +17 -19
  72. data/lib/sisimai/lhost/x6.rb +41 -17
  73. data/lib/sisimai/lhost/yahoo.rb +17 -16
  74. data/lib/sisimai/lhost/yandex.rb +16 -21
  75. data/lib/sisimai/lhost/zoho.rb +16 -15
  76. data/lib/sisimai/lhost.rb +8 -10
  77. data/lib/sisimai/mail/maildir.rb +1 -3
  78. data/lib/sisimai/mail/mbox.rb +3 -4
  79. data/lib/sisimai/mail/memory.rb +0 -1
  80. data/lib/sisimai/mail/stdin.rb +1 -3
  81. data/lib/sisimai/mail.rb +3 -7
  82. data/lib/sisimai/mda.rb +28 -42
  83. data/lib/sisimai/message.rb +444 -326
  84. data/lib/sisimai/order.rb +5 -5
  85. data/lib/sisimai/reason/authfailure.rb +65 -0
  86. data/lib/sisimai/reason/badreputation.rb +53 -0
  87. data/lib/sisimai/reason/blocked.rb +96 -160
  88. data/lib/sisimai/reason/contenterror.rb +8 -9
  89. data/lib/sisimai/reason/delivered.rb +4 -6
  90. data/lib/sisimai/reason/exceedlimit.rb +10 -12
  91. data/lib/sisimai/reason/expired.rb +7 -8
  92. data/lib/sisimai/reason/feedback.rb +2 -3
  93. data/lib/sisimai/reason/filtered.rb +17 -19
  94. data/lib/sisimai/reason/hasmoved.rb +9 -10
  95. data/lib/sisimai/reason/hostunknown.rb +15 -15
  96. data/lib/sisimai/reason/mailboxfull.rb +11 -12
  97. data/lib/sisimai/reason/mailererror.rb +18 -20
  98. data/lib/sisimai/reason/mesgtoobig.rb +9 -11
  99. data/lib/sisimai/reason/networkerror.rb +5 -8
  100. data/lib/sisimai/reason/norelaying.rb +8 -11
  101. data/lib/sisimai/reason/notaccept.rb +13 -14
  102. data/lib/sisimai/reason/notcompliantrfc.rb +43 -0
  103. data/lib/sisimai/reason/onhold.rb +6 -9
  104. data/lib/sisimai/reason/policyviolation.rb +14 -12
  105. data/lib/sisimai/reason/rejected.rb +26 -24
  106. data/lib/sisimai/reason/requireptr.rb +69 -0
  107. data/lib/sisimai/reason/securityerror.rb +34 -36
  108. data/lib/sisimai/reason/spamdetected.rb +115 -147
  109. data/lib/sisimai/reason/speeding.rb +49 -0
  110. data/lib/sisimai/reason/suspend.rb +12 -11
  111. data/lib/sisimai/reason/syntaxerror.rb +11 -10
  112. data/lib/sisimai/reason/systemerror.rb +7 -9
  113. data/lib/sisimai/reason/systemfull.rb +7 -8
  114. data/lib/sisimai/reason/toomanyconn.rb +9 -11
  115. data/lib/sisimai/reason/undefined.rb +2 -3
  116. data/lib/sisimai/reason/userunknown.rb +129 -146
  117. data/lib/sisimai/reason/vacation.rb +3 -4
  118. data/lib/sisimai/reason/virusdetected.rb +10 -11
  119. data/lib/sisimai/reason.rb +59 -64
  120. data/lib/sisimai/rfc1894.rb +55 -28
  121. data/lib/sisimai/rfc2045.rb +373 -0
  122. data/lib/sisimai/rfc3464.rb +250 -308
  123. data/lib/sisimai/rfc3834.rb +42 -45
  124. data/lib/sisimai/rfc5322.rb +177 -146
  125. data/lib/sisimai/rfc5965.rb +31 -0
  126. data/lib/sisimai/rhost/cox.rb +5 -6
  127. data/lib/sisimai/rhost/franceptt.rb +6 -8
  128. data/lib/sisimai/rhost/godaddy.rb +12 -12
  129. data/lib/sisimai/rhost/google.rb +530 -0
  130. data/lib/sisimai/rhost/iua.rb +9 -10
  131. data/lib/sisimai/rhost/kddi.rb +6 -8
  132. data/lib/sisimai/rhost/{exchangeonline.rb → microsoft.rb} +115 -114
  133. data/lib/sisimai/rhost/mimecast.rb +51 -42
  134. data/lib/sisimai/rhost/nttdocomo.rb +12 -12
  135. data/lib/sisimai/rhost/spectrum.rb +10 -12
  136. data/lib/sisimai/rhost/{tencentqq.rb → tencent.rb} +7 -8
  137. data/lib/sisimai/rhost.rb +23 -31
  138. data/lib/sisimai/smtp/command.rb +59 -0
  139. data/lib/sisimai/smtp/error.rb +4 -7
  140. data/lib/sisimai/smtp/reply.rb +161 -74
  141. data/lib/sisimai/smtp/status.rb +507 -393
  142. data/lib/sisimai/smtp/transcript.rb +124 -0
  143. data/lib/sisimai/smtp.rb +0 -1
  144. data/lib/sisimai/string.rb +74 -5
  145. data/lib/sisimai/time.rb +1 -2
  146. data/lib/sisimai/version.rb +1 -1
  147. data/lib/sisimai.rb +46 -31
  148. data/set-of-emails/maildir/bsd/lhost-domino-02.eml +6 -3
  149. data/set-of-emails/maildir/bsd/lhost-googlegroups-15.eml +174 -0
  150. data/set-of-emails/maildir/bsd/lhost-gsuite-15.eml +229 -0
  151. data/set-of-emails/maildir/bsd/lhost-postfix-75.eml +51 -0
  152. data/set-of-emails/maildir/bsd/lhost-postfix-76.eml +101 -0
  153. data/set-of-emails/maildir/bsd/lhost-postfix-77.eml +74 -0
  154. data/set-of-emails/maildir/bsd/lhost-postfix-78.eml +91 -0
  155. data/set-of-emails/maildir/bsd/lhost-receivingses-08.eml +88 -0
  156. data/set-of-emails/maildir/bsd/lhost-sendmail-60.eml +85 -0
  157. data/set-of-emails/maildir/bsd/rfc3464-43.eml +88 -0
  158. data/set-of-emails/maildir/bsd/rhost-google-03.eml +101 -0
  159. data/set-of-emails/maildir/bsd/rhost-google-04.eml +102 -0
  160. data/set-of-emails/maildir/bsd/rhost-google-05.eml +82 -0
  161. data/set-of-emails/maildir/bsd/rhost-google-06.eml +102 -0
  162. data/set-of-emails/maildir/bsd/rhost-google-07.eml +69 -0
  163. data/set-of-emails/maildir/bsd/rhost-google-08.eml +99 -0
  164. data/sisimai-java.gemspec +1 -1
  165. data/sisimai.gemspec +1 -1
  166. metadata +48 -26
  167. data/.rspec +0 -2
  168. data/lib/sisimai/data/yaml.rb +0 -33
  169. data/lib/sisimai/data.rb +0 -411
  170. data/lib/sisimai/mime.rb +0 -456
  171. data/lib/sisimai/rhost/googleapps.rb +0 -261
  172. /data/set-of-emails/maildir/bsd/{rfc3464-41.eml → rfc3834-05.eml} +0 -0
  173. /data/set-of-emails/maildir/bsd/{rhost-googleapps-01.eml → rhost-google-01.eml} +0 -0
  174. /data/set-of-emails/maildir/bsd/{rhost-googleapps-02.eml → rhost-google-02.eml} +0 -0
  175. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-01.eml → rhost-microsoft-01.eml} +0 -0
  176. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-02.eml → rhost-microsoft-02.eml} +0 -0
  177. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-03.eml → rhost-microsoft-03.eml} +0 -0
  178. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-01.eml → rhost-tencent-01.eml} +0 -0
  179. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-02.eml → rhost-tencent-02.eml} +0 -0
  180. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-03.eml → rhost-tencent-03.eml} +0 -0
@@ -1,366 +1,484 @@
1
1
  module Sisimai
2
- # Sisimai::Message convert bounce email text to data structure. It resolve
3
- # email text into an UNIX From line, the header part of the mail, delivery
4
- # status, and RFC822 header part. When the email given as a argument of "new"
5
- # method is not a bounce email, the method returns nil.
6
- class Message
7
- # Imported from p5-Sisimail/lib/Sisimai/Message.pm
8
- # :from [String] UNIX From line
9
- # :header [Hash] Header part of an email
10
- # :ds [Array] Parsed data by Sisimai::Lhost::* module
11
- # :rfc822 [Hash] Header part of the original message
12
- # :catch [Any] The results returned by hook method
13
- attr_accessor :from, :header, :ds, :rfc822, :catch
14
-
15
- require 'sisimai/mime'
16
- require 'sisimai/order'
17
- require 'sisimai/lhost'
18
- require 'sisimai/string'
19
- require 'sisimai/address'
20
- require 'sisimai/rfc5322'
21
- DefaultSet = Sisimai::Order.another
22
- LhostTable = Sisimai::Lhost.path
23
-
24
- # Constructor of Sisimai::Message
25
- # @param [String] data Email text data
26
- # @param [Hash] argvs Module to be loaded
27
- # @options argvs [String] :data Entire email message
28
- # @options argvs [Array] :load User defined MTA module list
29
- # @options argvs [Array] :order The order of MTA modules
30
- # @options argvs [Code] :hook Reference to callback method
31
- # @return [Sisimai::Message] Structured email data or nil if each
32
- # value of the arguments are missing
33
- def initialize(data: '', **argvs)
34
- return nil if data.empty?
35
- param = {}
36
- email = data.scrub('?').gsub("\r\n", "\n")
37
- thing = { 'from' => '','header' => {}, 'rfc822' => '', ds => [], 'catch' => nil }
38
-
39
- #methodargv = { 'data' => email, 'hook' => argvs[:hook] || nil }
40
- # 1. 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
- # 2. Split email data to headers and a body part.
51
- return nil unless aftersplit = Sisimai::Message.divideup(email)
52
-
53
- # 3. Convert email headers from text to hash reference
54
- thing['from'] = aftersplit[0]
55
- thing['header'] = Sisimai::Message.makemap(aftersplit[1])
56
-
57
- # 4. Decode and rewrite the "Subject:" header
58
- unless thing['header']['subject'].empty?
59
- # Decode MIME-Encoded "Subject:" header
60
- s = thing['header']['subject']
61
- q = Sisimai::MIME.is_mimeencoded(s) ? Sisimai::MIME.mimedecode(s.split(/[ ]/)) : s
62
-
63
- # Remove "Fwd:" string from the Subject: header
64
- if cv = q.downcase.match(/\A[ \t]*fwd?:[ ]*(.*)\z/)
65
- # Delete quoted strings, quote symbols(>)
66
- q = cv[1]
67
- aftersplit[2] = aftersplit[2].gsub(/^[>]+[ ]/, '').gsub(/^[>]$/, '')
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
- thing['header']['subject'] = q
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
- # 5. Rewrite message body for detecting the bounce reason
73
- param = {
74
- 'hook' => argvs[:hook] || nil,
75
- 'mail' => thing,
76
- 'body' => aftersplit[2],
77
- 'tobeloaded' => tobeloaded,
78
- 'tryonfirst' => Sisimai::Order.make(thing['header']['subject'])
79
- }
80
- return nil unless bouncedata = Sisimai::Message.parse(param)
81
- return nil if bouncedata.empty?
82
-
83
- # 6. Rewrite headers of the original message in the body part
84
- %w|ds catch rfc822|.each { |e| thing[e] = bouncedata[e] }
85
- p = bouncedata['rfc822']
86
- p = aftersplit[2] if p.empty?
87
- thing['rfc822'] = p.is_a?(::String) ? Sisimai::Message.makemap(p, true) : p
88
-
89
- @from = thing['from']
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
- begin
123
- require v.to_s.gsub('::', '/').downcase
124
- rescue LoadError
125
- warn ' ***warning: Failed to load ' << v
126
- next
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
- while e = modulelist.shift do
133
- # Append the custom order of MTA modules
134
- next if tobeloaded.index(e)
135
- tobeloaded << e
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
- return tobeloaded
139
- end
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
- # Divide email data up headers and a body part.
142
- # @param [String] email Email data
143
- # @return [Array] Email data after split
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
- return block
179
- end
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
- # Convert a text including email headers to a hash reference
182
- # @param [String] argv0 Email header data
183
- # @param [Bool] argv1 Decode "Subject:" header
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
- # Subject line is not MIME encoded
224
- r << headermaps['subject']
168
+ # Set pseudo UNIX From line
169
+ parts[0] = 'MAILER-DAEMON Tue Feb 11 00:00:00 2014'
225
170
  end
226
- headermaps['subject'] = Sisimai::MIME.mimedecode(r)
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
- # @abstract Parse bounce mail with each MTA module
232
- # @param [Hash] argvs Processing message entity.
233
- # @param options argvs [Hash] mail Email message entity
234
- # @param options mail [String] from From line of mbox
235
- # @param options mail [Hash] header Email header data
236
- # @param options mail [String] rfc822 Original message part
237
- # @param options mail [Array] ds Delivery status list(parsed data)
238
- # @param options argvs [String] body Email message body
239
- # @param options argvs [Array] tryonfirst MTA module list to load on first
240
- # @param options argvs [Array] tobeloaded User defined MTA module list
241
- # @return [Hash] Parsed and structured bounce mails
242
- def self.parse(argvs)
243
- return nil unless argvs['mail']
244
- return nil unless argvs['body']
245
-
246
- mailheader = argvs['mail']['header']
247
- bodystring = argvs['body']
248
- hookmethod = argvs['hook'] || nil
249
- havecaught = nil
250
- return nil unless mailheader
251
-
252
- # PRECHECK_EACH_HEADER:
253
- # Set empty string if the value is nil
254
- mailheader['from'] ||= ''
255
- mailheader['subject'] ||= ''
256
- mailheader['content-type'] ||= ''
257
-
258
- # Decode BASE64 Encoded message body, rewrite.
259
- mesgformat = (mailheader['content-type'] || '').downcase
260
- ctencoding = (mailheader['content-transfer-encoding'] || '').downcase
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
- if mesgformat.start_with?('text/html;')
273
- # Content-Type: text/html;...
274
- bodystring = Sisimai::String.to_plain(bodystring, true)
275
- end
276
- else
277
- # NOT text/plain
278
- if mesgformat.start_with?('multipart/')
279
- # In case of Content-Type: multipart/*
280
- p = Sisimai::MIME.makeflat(mailheader['content-type'], bodystring)
281
- bodystring = p unless p.empty?
282
- end
283
- end
284
- bodystring = bodystring.scrub('?').delete("\r")
285
-
286
- haveloaded = {}
287
- parseddata = nil
288
- modulename = ''
289
- if hookmethod.is_a? Proc
290
- # Call the hook method
291
- begin
292
- p = { 'headers' => mailheader, 'message' => bodystring }
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
- catch :PARSER do
300
- while true
301
- # 1. User-Defined Module
302
- # 2. MTA Module Candidates to be tried on first
303
- # 3. Sisimai::Lhost::*
304
- # 4. Sisimai::RFC3464
305
- # 5. Sisimai::ARF
306
- # 6. Sisimai::RFC3834
307
- while r = argvs['tobeloaded'].shift do
308
- # Call user defined MTA modules
309
- next if haveloaded[r]
310
- parseddata = Module.const_get(r).make(mailheader, bodystring)
311
- haveloaded[r] = true
312
- modulename = r
313
- throw :PARSER if parseddata
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
- [argvs['tryonfirst'], DefaultSet].flatten.each do |r|
317
- # Try MTA module candidates
318
- next if haveloaded[r]
319
- require LhostTable[r]
320
- parseddata = Module.const_get(r).make(mailheader, bodystring)
321
- haveloaded[r] = true
322
- modulename = r
323
- throw :PARSER if parseddata
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
- unless haveloaded['Sisimai::RFC3464']
327
- # When the all of Sisimai::Lhost::* modules did not return bounce
328
- # data, call Sisimai::RFC3464;
329
- require 'sisimai/rfc3464'
330
- parseddata = Sisimai::RFC3464.make(mailheader, bodystring)
331
- modulename = 'RFC3464'
332
- throw :PARSER if parseddata
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
- unless haveloaded['Sisimai::ARF']
336
- # Feedback Loop message
337
- require 'sisimai/arf'
338
- parseddata = Sisimai::ARF.make(mailheader, bodystring) if Sisimai::ARF.is_arf(mailheader)
339
- throw :PARSER if parseddata
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
- unless haveloaded['Sisimai::RFC3834']
343
- # Try to parse the message as auto reply message defined in RFC3834
344
- require 'sisimai/rfc3834'
345
- parseddata = Sisimai::RFC3834.make(mailheader, bodystring)
346
- modulename = 'RFC3834'
347
- throw :PARSER if parseddata
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
- parseddata['catch'] = havecaught
356
- modulename = modulename.sub(/\A.+::/, '')
357
- parseddata['ds'].each do |e|
358
- e['agent'] = modulename unless e['agent']
359
- e.each_key { |a| e[a] ||= '' } # Replace nil with ""
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