sisimai 4.25.16 → 5.0.0

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 +4 -4
  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 +41 -20
  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,13 +1,12 @@
1
1
  module Sisimai::Lhost
2
- # Sisimai::Lhost::Qmail parses a bounce email which created by qmail.
3
- # Methods in the module are called from only Sisimai::Message.
2
+ # Sisimai::Lhost::Qmail parses a bounce email which created by qmail. Methods in the module are called
3
+ # from only Sisimai::Message.
4
4
  module Qmail
5
5
  class << self
6
- # Imported from p5-Sisimail/lib/Sisimai/Lhost/qmail.pm
7
6
  require 'sisimai/lhost'
8
7
 
9
8
  Indicators = Sisimai::Lhost.INDICATORS
10
- ReBackbone = %r|^--- Below this line is a copy of the message[.]|.freeze
9
+ Boundaries = ['--- Below this line is a copy of the message.'].freeze
11
10
  StartingOf = {
12
11
  # qmail-remote.c:248| if (code >= 500) {
13
12
  # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
@@ -16,45 +15,33 @@ module Sisimai::Lhost
16
15
  #
17
16
  # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c
18
17
  # K = success, Z = temporary error, D = permanent error
19
- message: ['Hi. This is the qmail'],
20
18
  error: ['Remote host said:'],
19
+ message: ['Hi. This is the qmail'],
20
+ rhost: ['Giving up on ', 'Connected to ', 'remote host '],
21
21
  }.freeze
22
-
23
- ReSMTP = {
22
+ CommandSet = {
24
23
  # Error text regular expressions which defined in qmail-remote.c
25
24
  # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed");
26
- 'conn' => %r/(?:Error:)?Connected to [^ ]+ but greeting failed[.]/,
25
+ 'conn' => [' but greeting failed.'],
27
26
  # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected");
28
- 'ehlo' => %r/(?:Error:)?Connected to [^ ]+ but my name was rejected[.]/,
27
+ 'ehlo' => [' but my name was rejected.'],
29
28
  # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected");
30
29
  # reason = rejected
31
- 'mail' => %r/(?:Error:)?Connected to [^ ]+ but sender was rejected[.]/,
30
+ 'mail' => [' but sender was rejected.'],
32
31
  # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
33
32
  # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n");
34
33
  # reason = userunknown
35
- 'rcpt' => %r/(?:Error:)?[^ ]+ does not like recipient[.]/,
34
+ 'rcpt' => [' does not like recipient.'],
36
35
  # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command");
37
36
  # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command");
38
37
  # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message");
39
38
  # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message");
40
- 'data' => %r{(?:
41
- (?:Error:)?[^ ]+[ ]failed[ ]on[ ]DATA[ ]command[.]
42
- |(?:Error:)?[^ ]+[ ]failed[ ]after[ ]I[ ]sent[ ]the[ ]message[.]
43
- )
44
- }x,
39
+ 'data' => [' failed on DATA command', ' failed after I sent the message'],
45
40
  }.freeze
46
- # qmail-remote.c:261| if (!flagbother) quit("DGiving up on ","");
47
- ReHost = %r{(?:
48
- Giving[ ]up[ ]on[ ]([^ ]+[0-9a-zA-Z])[.]?\z
49
- |Connected[ ]to[ ]([-0-9a-zA-Z.]+[0-9a-zA-Z])[ ]
50
- |remote[ ]host[ ]([-0-9a-zA-Z.]+[0-9a-zA-Z])[ ]said:
51
- )
52
- }x
53
41
 
54
42
  # qmail-send.c:922| ... (&dline[c],"I'm not going to try again; this message has been in the queue too long.\n")) nomem();
55
43
  HasExpired = 'this message has been in the queue too long.'
56
- ReCommands = %r/Sorry, no SMTP connection got far enough; most progress was ([A-Z]{4}) /
57
- ReIsOnHold = %r/\A[^ ]+ does not like recipient[.][ ]+.+this message has been in the queue too long[.]\z/
44
+ OnHoldPair = [' does not like recipient.', 'this message has been in the queue too long.'].freeze
58
45
  FailOnLDAP = {
59
46
  # qmail-ldap-1.03-20040101.patch:19817 - 19866
60
47
  'suspend' => ['Mailaddress is administrative?le?y disabled'], # 5.2.1
@@ -104,27 +91,31 @@ module Sisimai::Lhost
104
91
  # @param [String] mbody Message body of a bounce email
105
92
  # @return [Hash] Bounce data list and message/rfc822 part
106
93
  # @return [Nil] it failed to parse or the arguments are missing
107
- def make(mhead, mbody)
94
+ def inquire(mhead, mbody)
108
95
  # Pre process email headers and the body part of the message which generated
109
96
  # by qmail, see https://cr.yp.to/qmail.html
110
97
  # e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000
111
98
  # Subject: failure notice
112
- tryto = /\A[(]qmail[ ]+\d+[ ]+invoked[ ]+(?:for[ ]+bounce|from[ ]+network)[)]/
99
+ tryto = [['(qmail ', 'invoked for bounce)'], ['(qmail ', 'invoked from ', 'network)']]
113
100
  match = 0
114
101
  match += 1 if mhead['subject'] == 'failure notice'
115
- match += 1 if mhead['received'].any? { |a| a =~ tryto }
102
+ mhead['received'].each do |e|
103
+ # Received: (qmail 2222 invoked for bounce);29 Apr 2017 23:34:45 +0900
104
+ # Received: (qmail 2202 invoked from network); 29 Apr 2018 00:00:00 +0900
105
+ match += 1 if tryto.any? { |a| Sisimai::String.aligned(e, a) }
106
+ end
116
107
  return nil unless match > 0
117
108
 
118
109
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
119
- emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
120
- bodyslices = emailsteak[0].split("\n")
110
+ emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
111
+ bodyslices = emailparts[0].split("\n")
121
112
  readcursor = 0 # (Integer) Points the current cursor position
122
113
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
123
114
  v = nil
124
115
 
125
116
  while e = bodyslices.shift do
126
- # Read error messages and delivery status lines from the head of the email
127
- # to the previous line of the beginning of the original message.
117
+ # Read error messages and delivery status lines from the head of the email to the previous
118
+ # line of the beginning of the original message.
128
119
  if readcursor == 0
129
120
  # Beginning of the bounce message or delivery status part
130
121
  readcursor |= Indicators[:deliverystatus] if e.start_with?(StartingOf[:message][0])
@@ -139,14 +130,14 @@ module Sisimai::Lhost
139
130
  # Giving up on 192.0.2.153.
140
131
  v = dscontents[-1]
141
132
 
142
- if cv = e.match(/\A(?:To[ ]*:)?[<](.+[@].+)[>]:[ \t]*\z/)
133
+ if e.start_with?('<') && Sisimai::String.aligned(e, ['<', '@', '>', ':'])
143
134
  # <kijitora@example.jp>:
144
135
  if v['recipient']
145
136
  # There are multiple recipient addresses in the message body.
146
137
  dscontents << Sisimai::Lhost.DELIVERYSTATUS
147
138
  v = dscontents[-1]
148
139
  end
149
- v['recipient'] = cv[1]
140
+ v['recipient'] = Sisimai::Address.s3s4(e[e.index('<'), e.size])
150
141
  recipients += 1
151
142
 
152
143
  elsif dscontents.size == recipients
@@ -157,8 +148,15 @@ module Sisimai::Lhost
157
148
  v['alterrors'] = e if e.start_with?(StartingOf[:error][0])
158
149
 
159
150
  next if v['rhost']
160
- next unless cv = e.match(ReHost)
161
- v['rhost'] = cv[1]
151
+ StartingOf[:rhost].each do |r|
152
+ # Find a remote host name
153
+ p1 = e.index(r); next unless p1
154
+ cm = r.size
155
+ p2 = e.index(' ', p1 + cm + 1) || p2 = e.rindex('.')
156
+
157
+ v['rhost'] = e[p1 + cm, p2 - p1 - cm]
158
+ break
159
+ end
162
160
  end
163
161
  end
164
162
  return nil unless recipients > 0
@@ -169,17 +167,16 @@ module Sisimai::Lhost
169
167
 
170
168
  unless e['command']
171
169
  # Get the SMTP command name for the session
172
- ReSMTP.each_key do |r|
170
+ CommandSet.each_key do |r|
173
171
  # Verify each regular expression of SMTP commands
174
- next unless e['diagnosis'] =~ ReSMTP[r]
172
+ next unless CommandSet[r].any? { |a| e['diagnosis'].include?(a) }
175
173
  e['command'] = r.upcase
176
174
  break
177
175
  end
178
176
 
179
- unless e['command']
180
- # Verify each regular expression of patches
181
- if cv = e['diagnosis'].match(ReCommands) then e['command'] = cv[1].upcase end
182
- e['command'] ||= ''
177
+ if e['diagnosis'].include?('Sorry, no SMTP connection got far enough; most progress was ')
178
+ # Get the last SMTP command:from the error message
179
+ e['command'] ||= Sisimai::SMTP::Command.find(e['diagnosis']) || ''
183
180
  end
184
181
  end
185
182
 
@@ -193,9 +190,8 @@ module Sisimai::Lhost
193
190
  e['reason'] = 'blocked'
194
191
  else
195
192
  # Try to match with each error message in the table
196
- if e['diagnosis'] =~ ReIsOnHold
197
- # To decide the reason require pattern match with
198
- # Sisimai::Reason::* modules
193
+ if Sisimai::String.aligned(e['diagnosis'], OnHoldPair)
194
+ # To decide the reason require pattern match with Sisimai::Reason::* modules
199
195
  e['reason'] = 'onhold'
200
196
  else
201
197
  MessagesOf.each_key do |r|
@@ -227,10 +223,11 @@ module Sisimai::Lhost
227
223
  end
228
224
  end
229
225
 
230
- e['status'] = Sisimai::SMTP::Status.find(e['diagnosis']) || ''
226
+ e['command'] ||= Sisimai::SMTP::Command.find(e['diagnosis'])
227
+ e['status'] = Sisimai::SMTP::Status.find(e['diagnosis']) || ''
231
228
  end
232
229
 
233
- return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
230
+ return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
234
231
  end
235
232
  def description; return 'qmail'; end
236
233
  end
@@ -1,15 +1,13 @@
1
1
  module Sisimai::Lhost
2
- # Sisimai::Lhost::ReceivingSES parses a bounce email which created
3
- # by Amazon Simple Email Service. Methods in the module are called from
4
- # only Sisimai::Message.
2
+ # Sisimai::Lhost::ReceivingSES parses a bounce email which created by Amazon Simple Email Service.
3
+ # Methods in the module are called from only Sisimai::Message.
5
4
  module ReceivingSES
6
5
  class << self
7
- # Imported from p5-Sisimail/lib/Sisimai/Lhost/ReceivingSES.pm
8
6
  require 'sisimai/lhost'
9
7
 
10
8
  # https://aws.amazon.com/ses/
11
9
  Indicators = Sisimai::Lhost.INDICATORS
12
- ReBackbone = %r|^content-type:[ ]text/rfc822-headers|.freeze
10
+ Boundaries = ['Content-Type: text/rfc822-headers'].freeze
13
11
  StartingOf = { message: ['This message could not be delivered.'] }.freeze
14
12
  MessagesOf = {
15
13
  # The followings are error messages in Rule sets/*/Actions/Template
@@ -24,26 +22,25 @@ module Sisimai::Lhost
24
22
  # @param [String] mbody Message body of a bounce email
25
23
  # @return [Hash] Bounce data list and message/rfc822 part
26
24
  # @return [Nil] it failed to parse or the arguments are missing
27
- def make(mhead, mbody)
25
+ def inquire(mhead, mbody)
28
26
  # X-SES-Outgoing: 2015.10.01-54.240.27.7
29
27
  # Feedback-ID: 1.us-west-2.HX6/J9OVlHTadQhEu1+wdF9DBj6n6Pa9sW5Y/0pSOi8=:AmazonSES
30
28
  return nil unless mhead['x-ses-outgoing']
31
29
 
32
- require 'sisimai/rfc1894'
33
30
  fieldtable = Sisimai::RFC1894.FIELDTABLE
34
31
  permessage = {} # (Hash) Store values of each Per-Message field
35
32
 
36
33
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
37
- emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
38
- bodyslices = emailsteak[0].split("\n")
34
+ emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
35
+ bodyslices = emailparts[0].split("\n")
39
36
  readslices = ['']
40
37
  readcursor = 0 # (Integer) Points the current cursor position
41
38
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
42
39
  v = nil
43
40
 
44
41
  while e = bodyslices.shift do
45
- # Read error messages and delivery status lines from the head of the email
46
- # to the previous line of the beginning of the original message.
42
+ # Read error messages and delivery status lines from the head of the email to the previous
43
+ # line of the beginning of the original message.
47
44
  readslices << e # Save the current line for the next loop
48
45
 
49
46
  if readcursor == 0
@@ -84,14 +81,14 @@ module Sisimai::Lhost
84
81
  next unless fieldtable[o[0]]
85
82
  v[fieldtable[o[0]]] = o[2]
86
83
 
87
- next unless f == 1
84
+ next unless f
88
85
  permessage[fieldtable[o[0]]] = o[2]
89
86
  end
90
87
  else
91
88
  # Continued line of the value of Diagnostic-Code field
92
89
  next unless readslices[-2].start_with?('Diagnostic-Code:')
93
- next unless cv = e.match(/\A[ \t]+(.+)\z/)
94
- v['diagnosis'] << ' ' << cv[1]
90
+ next unless e.start_with?(' ')
91
+ v['diagnosis'] << ' ' << Sisimai::String.sweep(e)
95
92
  readslices[-1] = 'Diagnostic-Code: ' << e
96
93
  end
97
94
  end
@@ -106,11 +103,9 @@ module Sisimai::Lhost
106
103
  if e['status'].to_s.start_with?('5.0.0', '5.1.0', '4.0.0', '4.1.0')
107
104
  # Get other D.S.N. value from the error message
108
105
  errormessage = e['diagnosis']
109
-
110
- if cv = e['diagnosis'].match(/["'](\d[.]\d[.]\d.+)['"]/)
111
- # 5.1.0 - Unknown address error 550-'5.7.1 ...
112
- errormessage = cv[1]
113
- end
106
+ p1 = e['diagnosis'].index("-'"); p1 = e['diagnosis'].index('-"') unless p1
107
+ p2 = e['diagnosis'].rindex("' "); p2 = e['diagnosis'].rindex('" ') unless p2
108
+ errormessage = e['diagnosis'][p1 + 2, p2 - p1 - 2] if p1 && p2
114
109
  e['status'] = Sisimai::SMTP::Status.find(errormessage) || e['status']
115
110
  end
116
111
 
@@ -123,7 +118,7 @@ module Sisimai::Lhost
123
118
  e['reason'] ||= Sisimai::SMTP::Status.name(e['status']) || ''
124
119
  end
125
120
 
126
- return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
121
+ return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
127
122
  end
128
123
  def description; return 'Amazon SES(Receiving): https://aws.amazon.com/ses/'; end
129
124
  end
@@ -1,13 +1,12 @@
1
1
  module Sisimai::Lhost
2
- # Sisimai::Lhost::SendGrid parses a bounce email which created by
3
- # SendGrid. Methods in the module are called from only Sisimai::Message.
2
+ # Sisimai::Lhost::SendGrid parses a bounce email which created by SendGrid. Methods in the module
3
+ # are called from only Sisimai::Message.
4
4
  module SendGrid
5
5
  class << self
6
- # Imported from p5-Sisimail/lib/Sisimai/Lhost/SendGrid.pm
7
6
  require 'sisimai/lhost'
8
7
 
9
8
  Indicators = Sisimai::Lhost.INDICATORS
10
- ReBackbone = %r|^Content-Type:[ ]message/rfc822|.freeze
9
+ Boundaries = ['Content-Type: message/rfc822'].freeze
11
10
  StartingOf = { message: ['This is an automatically generated message from SendGrid.'] }.freeze
12
11
 
13
12
  # Parse bounce messages from SendGrid
@@ -15,29 +14,29 @@ module Sisimai::Lhost
15
14
  # @param [String] mbody Message body of a bounce email
16
15
  # @return [Hash] Bounce data list and message/rfc822 part
17
16
  # @return [Nil] it failed to parse or the arguments are missing
18
- def make(mhead, mbody)
17
+ def inquire(mhead, mbody)
19
18
  # Return-Path: <apps@sendgrid.net>
20
19
  # X-Mailer: MIME-tools 5.502 (Entity 5.502)
21
20
  return nil unless mhead['return-path']
22
21
  return nil unless mhead['return-path'] == '<apps@sendgrid.net>'
23
22
  return nil unless mhead['subject'] == 'Undelivered Mail Returned to Sender'
24
23
 
25
- require 'sisimai/rfc1894'
24
+ require 'sisimai/smtp/command'
26
25
  fieldtable = Sisimai::RFC1894.FIELDTABLE
27
26
  permessage = {} # (Hash) Store values of each Per-Message field
28
27
 
29
28
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
30
- emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
31
- bodyslices = emailsteak[0].split("\n")
29
+ emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
30
+ bodyslices = emailparts[0].split("\n")
32
31
  readslices = ['']
33
32
  readcursor = 0 # (Integer) Points the current cursor position
34
33
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
35
- commandtxt = '' # (String) SMTP Command name begin with the string '>>>'
34
+ thecommand = '' # (String) SMTP Command name begin with the string '>>>'
36
35
  v = nil
37
36
 
38
37
  while e = bodyslices.shift do
39
- # Read error messages and delivery status lines from the head of the email
40
- # to the previous line of the beginning of the original message.
38
+ # Read error messages and delivery status lines from the head of the email to the previous
39
+ # line of the beginning of the original message.
41
40
  readslices << e # Save the current line for the next loop
42
41
 
43
42
  if readcursor == 0
@@ -57,8 +56,7 @@ module Sisimai::Lhost
57
56
  # Fallback code for empty value or invalid formatted value
58
57
  # - Status: (empty)
59
58
  # - Diagnostic-Code: 550 5.1.1 ... (No "diagnostic-type" sub field)
60
- next unless cv = e.match(/\ADiagnostic-Code:[ ]*(.+)/)
61
- v['diagnosis'] = cv[1]
59
+ v['diagnosis'] = e[e.index(':') + 2, e.size] if e.start_with?('Diagnostic-Code: ')
62
60
  next
63
61
  end
64
62
 
@@ -84,29 +82,37 @@ module Sisimai::Lhost
84
82
  v['diagnosis'] = o[2]
85
83
  elsif o[-1] == 'date'
86
84
  # Arrival-Date: 2012-12-31 23-59-59
87
- next unless cv = e.match(/\AArrival-Date: (\d{4})[-](\d{2})[-](\d{2}) (\d{2})[-](\d{2})[-](\d{2})\z/)
88
- o[1] << 'Thu, ' << cv[3] + ' '
89
- o[1] << Sisimai::DateTime.monthname(false)[cv[2].to_i - 1]
90
- o[1] << ' ' << cv[1] + ' ' << [cv[4], cv[5], cv[6]].join(':')
85
+ next unless e.start_with?('Arrival-Date: ')
86
+ cf = e[e.index(': ') + 2, e.size].split(' '); next unless cf.size == 2
87
+ cw = cf[0].split('-'); next unless cw.size == 3
88
+ ce = cf[1].split('-'); next unless ce.size == 3
89
+
90
+ o[1] << 'Thu, ' << cw[2] + ' '
91
+ o[1] << Sisimai::DateTime.monthname(false)[cw[1].to_i - 1]
92
+ o[1] << ' ' << cw[0] + ' ' << ce.join(':')
91
93
  o[1] << ' ' << Sisimai::DateTime.abbr2tz('CDT')
92
94
  else
93
95
  # Other DSN fields defined in RFC3464
94
96
  next unless fieldtable[o[0]]
95
97
  v[fieldtable[o[0]]] = o[2]
96
98
 
97
- next unless f == 1
99
+ next unless f
98
100
  permessage[fieldtable[o[0]]] = o[2]
99
101
  end
100
102
  else
101
103
  # The line does not begin with a DSN field defined in RFC3464
102
- if cv = e.match(/.+ in (?:End of )?([A-Z]{4}).*\z/)
104
+ if cv = Sisimai::SMTP::Command.find(e)
103
105
  # in RCPT TO, in MAIL FROM, end of DATA
104
- commandtxt = cv[1]
106
+ thecommand = cv
107
+ elsif e.start_with?('Diagnostic-Code: ')
108
+ # Diagnostic-Code: 550 5.1.1 <kijitora@example.jp>... User Unknown
109
+ v['diagnosis'] = e[e.index(':') + 2, e.size]
105
110
  else
106
111
  # Continued line of the value of Diagnostic-Code field
107
112
  next unless readslices[-2].start_with?('Diagnostic-Code:')
108
- next unless cv = e.match(/\A[ \t]+(.+)\z/)
109
- v['diagnosis'] << ' ' << cv[1]
113
+ next unless e.start_with?(' ')
114
+ v['diagnosis'] ||= ''
115
+ v['diagnosis'] << ' ' << Sisimai::String.sweep(e)
110
116
  readslices[-1] = 'Diagnostic-Code: ' << e
111
117
  end
112
118
  end
@@ -116,14 +122,11 @@ module Sisimai::Lhost
116
122
  dscontents.each do |e|
117
123
  # Get the value of SMTP status code as a pseudo D.S.N.
118
124
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
119
- if cv = e['diagnosis'].match(/\b([45])\d\d[ \t]*/)
120
- # 4xx or 5xx
121
- e['status'] = cv[1] + '.0.0'
122
- end
125
+ e['replycode'] = Sisimai::SMTP::Reply.find(e['diagnosis']) || ''
126
+ e['status'] = e['replycode'][0, 1] + '.0.0' if e['replycode'].size == 3
123
127
 
124
128
  if e['status'] == '5.0.0' || e['status'] == '4.0.0'
125
- # Get the value of D.S.N. from the error message or the value of
126
- # Diagnostic-Code header.
129
+ # Get the value of D.S.N. from the error message or the value of Diagnostic-Code header.
127
130
  e['status'] = Sisimai::SMTP::Status.find(e['diagnosis']) || e['status']
128
131
  end
129
132
 
@@ -131,17 +134,16 @@ module Sisimai::Lhost
131
134
  # Action: expired
132
135
  e['reason'] = 'expired'
133
136
  if !e['status'] || e['status'].end_with?('.0.0')
134
- # Set pseudo Status code value if the value of Status is not
135
- # defined or 4.0.0 or 5.0.0.
137
+ # Set pseudo Status code value if the value of Status is not defined or 4.0.0 or 5.0.0.
136
138
  e['status'] = Sisimai::SMTP::Status.code('expired') || e['status']
137
139
  end
138
140
  end
139
141
 
140
142
  e['lhost'] ||= permessage['rhost']
141
- e['command'] = commandtxt
143
+ e['command'] = thecommand
142
144
  end
143
145
 
144
- return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
146
+ return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
145
147
  end
146
148
  def description; return 'SendGrid: https://sendgrid.com/'; end
147
149
  end
@@ -1,13 +1,15 @@
1
1
  module Sisimai::Lhost
2
- # Sisimai::Lhost::Sendmail parses a bounce email which created by
3
- # v8 Sendmail. Methods in the module are called from only Sisimai::Message.
2
+ # Sisimai::Lhost::Sendmail parses a bounce email which created by v8 Sendmail. Methods in the module
3
+ # are called from only Sisimai::Message.
4
4
  module Sendmail
5
5
  class << self
6
- # Imported from p5-Sisimail/lib/Sisimai/Lhost/Sendmail.pm
7
6
  require 'sisimai/lhost'
7
+ require 'sisimai/smtp/reply'
8
+ require 'sisimai/smtp/status'
9
+ require 'sisimai/smtp/command'
8
10
 
9
11
  Indicators = Sisimai::Lhost.INDICATORS
10
- ReBackbone = %r<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>.freeze
12
+ Boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers'].freeze
11
13
  StartingOf = {
12
14
  # Error text regular expressions which defined in sendmail/savemail.c
13
15
  # savemail.c:1040|if (printheader && !putline(" ----- Transcript of session follows -----\n",
@@ -26,29 +28,30 @@ module Sisimai::Lhost
26
28
  # @param [String] mbody Message body of a bounce email
27
29
  # @return [Hash] Bounce data list and message/rfc822 part
28
30
  # @return [Nil] it failed to parse or the arguments are missing
29
- def make(mhead, mbody)
30
- return nil unless mhead['subject'] =~ /(?:see transcript for details\z|\AWarning: )/
31
+ def inquire(mhead, mbody)
31
32
  return nil if mhead['x-aol-ip']
33
+ match = nil
34
+ match ||= true if mhead['subject'].end_with?('see transcript for details')
35
+ match ||= true if mhead['subject'].start_with?('Warning: ')
36
+ return nil unless match
32
37
 
33
- require 'sisimai/rfc1894'
34
38
  fieldtable = Sisimai::RFC1894.FIELDTABLE
35
39
  permessage = {} # (Hash) Store values of each Per-Message field
36
-
37
40
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
38
- emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
39
- bodyslices = emailsteak[0].split("\n")
41
+ emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
42
+ bodyslices = emailparts[0].split("\n")
40
43
  readslices = ['']
41
44
  readcursor = 0 # (Integer) Points the current cursor position
42
45
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
43
- commandtxt = '' # (String) SMTP Command name begin with the string '>>>'
46
+ thecommand = '' # (String) SMTP Command name begin with the string '>>>'
44
47
  esmtpreply = [] # (Array) Reply from remote server on SMTP session
45
48
  sessionerr = false # (Boolean) Flag, "true" if it is SMTP session error
46
49
  anotherset = {} # Another error information
47
50
  v = nil
48
51
 
49
52
  while e = bodyslices.shift do
50
- # Read error messages and delivery status lines from the head of the email
51
- # to the previous line of the beginning of the original message.
53
+ # Read error messages and delivery status lines from the head of the email to the previous
54
+ # line of the beginning of the original message.
52
55
  readslices << e # Save the current line for the next loop
53
56
 
54
57
  if readcursor == 0
@@ -89,7 +92,7 @@ module Sisimai::Lhost
89
92
  next unless fieldtable[o[0]]
90
93
  v[fieldtable[o[0]]] = o[2]
91
94
 
92
- next unless f == 1
95
+ next unless f
93
96
  permessage[fieldtable[o[0]]] = o[2]
94
97
  end
95
98
  else
@@ -105,13 +108,14 @@ module Sisimai::Lhost
105
108
  # Received-From-MTA: DNS; x1x2x3x4.dhcp.example.ne.jp
106
109
  # Arrival-Date: Wed, 29 Apr 2009 16:03:18 +0900
107
110
  unless e.start_with?(' ')
108
- if cv = e.match(/\A[>]{3}[ ]+([A-Z]{4})[ ]?/)
111
+ if e.start_with?('>>> ')
109
112
  # >>> DATA
110
- commandtxt = cv[1]
113
+ thecommand = Sisimai::SMTP::Command.find(e)
111
114
 
112
- elsif cv = e.match(/\A[<]{3}[ ]+(.+)\z/)
115
+ elsif e.start_with?('<<< ')
113
116
  # <<< Response
114
- esmtpreply << cv[1] unless esmtpreply.index(cv[1])
117
+ cv = e[4, e.size - 4]
118
+ esmtpreply << cv unless esmtpreply.index(cv)
115
119
  else
116
120
  # Detect SMTP session error or connection error
117
121
  next if sessionerr
@@ -122,21 +126,23 @@ module Sisimai::Lhost
122
126
  next
123
127
  end
124
128
 
125
- if cv = e.match(/\A[<](.+)[>][.]+ (.+)\z/)
129
+ if e.start_with?('<') && Sisimai::String.aligned(e, ['@', '>.', ' '])
126
130
  # <kijitora@example.co.jp>... Deferred: Name server: example.co.jp.: host name lookup failure
127
- anotherset['recipient'] = cv[1]
128
- anotherset['diagnosis'] = cv[2]
131
+ anotherset['recipient'] = Sisimai::Address.s3s4(e[0, e.index('>')])
132
+ anotherset['diagnosis'] = e[e.index(' ') + 1, e.size]
129
133
  else
130
134
  # ----- Transcript of session follows -----
131
135
  # Message could not be delivered for too long
132
136
  # Message will be deleted from queue
133
- next if e =~ /\A[ \t]*[-]+/
134
- if cv = e.match(/\A[45]\d\d[ \t]([45][.]\d[.]\d)[ \t].+/)
137
+ cr = Sisimai::SMTP::Reply.find(e) || ''
138
+ cs = Sisimai::SMTP::Status.find(e) || ''
139
+
140
+ if cr.size + cs.size > 7
135
141
  # 550 5.1.2 <kijitora@example.org>... Message
136
142
  #
137
143
  # DBI connect('dbname=...')
138
144
  # 554 5.3.0 unknown mailer error 255
139
- anotherset['status'] = cv[1]
145
+ anotherset['status'] = cs
140
146
  anotherset['diagnosis'] ||= ''
141
147
  anotherset['diagnosis'] << ' ' << e
142
148
 
@@ -151,8 +157,8 @@ module Sisimai::Lhost
151
157
  else
152
158
  # Continued line of the value of Diagnostic-Code field
153
159
  next unless readslices[-2].start_with?('Diagnostic-Code:')
154
- next unless cv = e.match(/\A[ \t]+(.+)\z/)
155
- v['diagnosis'] << ' ' << cv[1]
160
+ next unless e.start_with?(' ')
161
+ v['diagnosis'] << ' ' << Sisimai::String.sweep(e)
156
162
  readslices[-1] = 'Diagnostic-Code: ' << e
157
163
  end
158
164
  end
@@ -161,47 +167,54 @@ module Sisimai::Lhost
161
167
 
162
168
  dscontents.each do |e|
163
169
  # Set default values if each value is empty.
164
- e['lhost'] ||= permessage['rhost']
170
+ e['diagnosis'] ||= ''
171
+ e['lhost'] ||= permessage['rhost']
165
172
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
166
173
 
167
- e['command'] ||= commandtxt
168
- if e['command'].empty?
169
- e['command'] = 'EHLO' unless esmtpreply.empty?
170
- end
171
-
172
174
  if anotherset['diagnosis']
173
175
  # Copy alternative error message
174
- e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'] =~ /\A[ \t]+\z/
175
- e['diagnosis'] = anotherset['diagnosis'] unless e['diagnosis']
176
- e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'] =~ /\A\d+\z/
176
+ e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'].start_with?(' ')
177
+ e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'].=~ /\A\d+\z/
178
+ e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'].empty?
177
179
  end
178
- unless esmtpreply.empty?
179
- # Replace the error message in "diagnosis" with the ESMTP Reply
180
- r = esmtpreply.join(' ')
181
- e['diagnosis'] = r if r.size > e['diagnosis'].to_s.size
180
+
181
+ while true
182
+ # Replace or append the error message in "diagnosis" with the ESMTP Reply Code when the
183
+ # following conditions have matched
184
+ break if esmtpreply.empty?
185
+ break if recipients != 1
186
+
187
+ e['diagnosis'] = sprintf("%s %s", esmtpreply.join(' '), e['diagnosis'])
188
+ break
182
189
  end
183
- e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
184
190
 
185
- if anotherset['status']
186
- # Check alternative status code
187
- if e['status'].empty? || e['status'] !~ /\A[45][.]\d[.]\d{1,3}\z/
188
- # Override alternative status code
189
- e['status'] = anotherset['status']
190
- end
191
+ e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
192
+ e['command'] ||= thecommand || Sisimai::SMTP::Command.find(e['diagnosis']) || ''
193
+ if e['command'].empty?
194
+ e['command'] = 'EHLO' unless esmtpreply.empty?
191
195
  end
192
196
 
193
- unless e['recipient'] =~ /\A[^ ]+[@][^ ]+\z/
194
- # @example.jp, no local part
195
- if cv = e['diagnosis'].match(/[<]([^ ]+[@][^ ]+)[>]/)
196
- # Get email address from the value of Diagnostic-Code header
197
- e['recipient'] = cv[1]
198
- end
197
+ while true
198
+ # Check alternative status code and override it
199
+ break unless anotherset.has_key?('status')
200
+ break unless anotherset['status'].size > 0
201
+ break if Sisimai::SMTP::Status.test(e['status'])
202
+
203
+ e['status'] = anotherset['status']
204
+ break
199
205
  end
206
+
207
+ # @example.jp, no local part
208
+ # # Get email address from the value of Diagnostic-Code field
209
+ next unless e['recipient'].start_with?('@')
210
+ cv = Sisimai::Address.find(e['diagnosis'], true) || []
211
+ e['recipient'] = cv[0][:address] if cv.size > 0
200
212
  end
201
213
 
202
- return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
214
+ return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
203
215
  end
204
216
  def description; return 'V8Sendmail: /usr/sbin/sendmail'; end
205
217
  end
206
218
  end
207
219
  end
220
+