sisimai 4.25.16-java → 5.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
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 +412 -393
  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 -326
  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 -45
  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,366 +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
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
+ 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]+/, ' ') }
270
210
  end
211
+ headermaps['received'] = recvheader
271
212
 
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
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)
296
234
  end
235
+ return headermaps
297
236
  end
298
237
 
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
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
314
255
  end
315
256
 
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
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] = ''
324
279
  end
325
280
 
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
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
333
293
  end
334
294
 
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
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] = ''
340
302
  end
341
303
 
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
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
348
331
  end
349
-
350
- break # as of now, we have no sample email for coding this block
332
+ email << e + "\n"
351
333
  end
334
+
335
+ email << "\n" unless email.end_with?("\n\n")
336
+ return email
352
337
  end
353
- return nil unless parseddata
354
338
 
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 ""
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
360
470
  end
361
- return parseddata
362
- end
363
471
 
472
+ end
364
473
  end
365
474
  end
366
475