sisimai 4.25.15-java → 5.0.0-java

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