sisimai 4.25.16-java → 5.0.2-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.
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