sisimai 5.6.0 → 5.7.1

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake-test.yml +0 -4
  3. data/ChangeLog.md +47 -0
  4. data/README-JA.md +19 -16
  5. data/README.md +23 -21
  6. data/lib/sisimai/address.rb +8 -31
  7. data/lib/sisimai/arf.rb +5 -6
  8. data/lib/sisimai/eb.rb +116 -0
  9. data/lib/sisimai/fact.rb +33 -51
  10. data/lib/sisimai/lda.rb +20 -18
  11. data/lib/sisimai/lhost/activehunter.rb +0 -2
  12. data/lib/sisimai/lhost/amazonses.rb +17 -18
  13. data/lib/sisimai/lhost/apachejames.rb +0 -1
  14. data/lib/sisimai/lhost/biglobe.rb +0 -16
  15. data/lib/sisimai/lhost/courier.rb +9 -7
  16. data/lib/sisimai/lhost/deutschetelekom.rb +120 -0
  17. data/lib/sisimai/lhost/domino.rb +2 -4
  18. data/lib/sisimai/lhost/dragonfly.rb +0 -27
  19. data/lib/sisimai/lhost/einsundeins.rb +4 -12
  20. data/lib/sisimai/lhost/exchange2003.rb +12 -11
  21. data/lib/sisimai/lhost/exchange2007.rb +17 -17
  22. data/lib/sisimai/lhost/exim.rb +34 -81
  23. data/lib/sisimai/lhost/ezweb.rb +15 -51
  24. data/lib/sisimai/lhost/fml.rb +8 -32
  25. data/lib/sisimai/lhost/gmail.rb +10 -32
  26. data/lib/sisimai/lhost/gmx.rb +7 -24
  27. data/lib/sisimai/lhost/googlegroups.rb +6 -5
  28. data/lib/sisimai/lhost/googleworkspace.rb +2 -5
  29. data/lib/sisimai/lhost/imailserver.rb +4 -9
  30. data/lib/sisimai/lhost/kddi.rb +8 -21
  31. data/lib/sisimai/lhost/mailfoundry.rb +0 -2
  32. data/lib/sisimai/lhost/mailmarshal.rb +1 -3
  33. data/lib/sisimai/lhost/messagingserver.rb +4 -15
  34. data/lib/sisimai/lhost/mfilter.rb +0 -1
  35. data/lib/sisimai/lhost/mimecast.rb +0 -1
  36. data/lib/sisimai/lhost/notes.rb +3 -3
  37. data/lib/sisimai/lhost/opensmtpd.rb +0 -40
  38. data/lib/sisimai/lhost/postfix.rb +14 -14
  39. data/lib/sisimai/lhost/qmail.rb +23 -89
  40. data/lib/sisimai/lhost/sendmail.rb +8 -7
  41. data/lib/sisimai/lhost/trendmicro.rb +5 -4
  42. data/lib/sisimai/lhost/v5sendmail.rb +2 -2
  43. data/lib/sisimai/lhost/verizon.rb +4 -4
  44. data/lib/sisimai/lhost/x1.rb +1 -2
  45. data/lib/sisimai/lhost/x2.rb +0 -2
  46. data/lib/sisimai/lhost/x3.rb +4 -9
  47. data/lib/sisimai/lhost/x6.rb +0 -1
  48. data/lib/sisimai/lhost/zoho.rb +0 -12
  49. data/lib/sisimai/lhost.rb +38 -19
  50. data/lib/sisimai/message.rb +1 -1
  51. data/lib/sisimai/order.rb +4 -1
  52. data/lib/sisimai/reason/authfailure.rb +8 -7
  53. data/lib/sisimai/reason/badreputation.rb +5 -4
  54. data/lib/sisimai/reason/blocked.rb +6 -5
  55. data/lib/sisimai/reason/contenterror.rb +8 -5
  56. data/lib/sisimai/reason/delivered.rb +3 -2
  57. data/lib/sisimai/reason/emailtoolarge.rb +7 -6
  58. data/lib/sisimai/reason/expired.rb +16 -4
  59. data/lib/sisimai/reason/failedstarttls.rb +5 -3
  60. data/lib/sisimai/reason/feedback.rb +2 -1
  61. data/lib/sisimai/reason/filtered.rb +9 -8
  62. data/lib/sisimai/reason/hasmoved.rb +5 -4
  63. data/lib/sisimai/reason/hostunknown.rb +14 -4
  64. data/lib/sisimai/reason/mailboxfull.rb +10 -8
  65. data/lib/sisimai/reason/mailererror.rb +3 -2
  66. data/lib/sisimai/reason/networkerror.rb +16 -3
  67. data/lib/sisimai/reason/norelaying.rb +8 -6
  68. data/lib/sisimai/reason/notaccept.rb +15 -7
  69. data/lib/sisimai/reason/notcompliantrfc.rb +5 -3
  70. data/lib/sisimai/reason/onhold.rb +8 -7
  71. data/lib/sisimai/reason/policyviolation.rb +11 -5
  72. data/lib/sisimai/reason/ratelimited.rb +5 -4
  73. data/lib/sisimai/reason/rejected.rb +24 -14
  74. data/lib/sisimai/reason/requireptr.rb +5 -4
  75. data/lib/sisimai/reason/securityerror.rb +5 -3
  76. data/lib/sisimai/reason/spamdetected.rb +6 -5
  77. data/lib/sisimai/reason/suppressed.rb +4 -3
  78. data/lib/sisimai/reason/suspend.rb +8 -3
  79. data/lib/sisimai/reason/syntaxerror.rb +5 -11
  80. data/lib/sisimai/reason/systemerror.rb +21 -2
  81. data/lib/sisimai/reason/systemfull.rb +4 -3
  82. data/lib/sisimai/reason/undefined.rb +2 -1
  83. data/lib/sisimai/reason/userunknown.rb +15 -9
  84. data/lib/sisimai/reason/vacation.rb +2 -1
  85. data/lib/sisimai/reason/virusdetected.rb +6 -5
  86. data/lib/sisimai/reason.rb +47 -42
  87. data/lib/sisimai/rfc1123.rb +1 -1
  88. data/lib/sisimai/rfc1894.rb +15 -8
  89. data/lib/sisimai/rfc2045.rb +8 -5
  90. data/lib/sisimai/rfc3464/thirdparty.rb +11 -10
  91. data/lib/sisimai/rfc3464.rb +10 -14
  92. data/lib/sisimai/rfc3834.rb +6 -5
  93. data/lib/sisimai/rfc791.rb +3 -38
  94. data/lib/sisimai/rhost/aol.rb +3 -2
  95. data/lib/sisimai/rhost/apple.rb +12 -11
  96. data/lib/sisimai/rhost/cloudflare.rb +3 -2
  97. data/lib/sisimai/rhost/cox.rb +38 -37
  98. data/lib/sisimai/rhost/facebook.rb +17 -16
  99. data/lib/sisimai/rhost/franceptt.rb +39 -38
  100. data/lib/sisimai/rhost/godaddy.rb +35 -34
  101. data/lib/sisimai/rhost/google.rb +22 -21
  102. data/lib/sisimai/rhost/gsuite.rb +8 -7
  103. data/lib/sisimai/rhost/iua.rb +11 -10
  104. data/lib/sisimai/rhost/kddi.rb +3 -2
  105. data/lib/sisimai/rhost/messagelabs.rb +13 -12
  106. data/lib/sisimai/rhost/microsoft.rb +37 -32
  107. data/lib/sisimai/rhost/mimecast.rb +18 -17
  108. data/lib/sisimai/rhost/nttdocomo.rb +10 -9
  109. data/lib/sisimai/rhost/outlook.rb +3 -2
  110. data/lib/sisimai/rhost/spectrum.rb +19 -18
  111. data/lib/sisimai/rhost/tencent.rb +10 -9
  112. data/lib/sisimai/rhost/yahooinc.rb +11 -10
  113. data/lib/sisimai/rhost/zoho.rb +8 -7
  114. data/lib/sisimai/rhost.rb +1 -1
  115. data/lib/sisimai/smtp/command.rb +13 -8
  116. data/lib/sisimai/smtp/failure.rb +9 -8
  117. data/lib/sisimai/smtp/reply.rb +23 -22
  118. data/lib/sisimai/smtp/status.rb +164 -159
  119. data/lib/sisimai/smtp/transcript.rb +3 -2
  120. data/lib/sisimai/string.rb +0 -12
  121. data/lib/sisimai/version.rb +1 -1
  122. data/lib/sisimai.rb +2 -1
  123. data/set-of-emails/maildir/bsd/lhost-deutschetelekom-01.eml +66 -0
  124. data/set-of-emails/maildir/bsd/lhost-deutschetelekom-02.eml +68 -0
  125. data/set-of-emails/maildir/bsd/lhost-deutschetelekom-03.eml +50 -0
  126. data/set-of-emails/maildir/bsd/lhost-postfix-81.eml +130 -0
  127. data/set-of-emails/should-not-crash/go-423-deeply-nested-mime-parts.eml +20050 -0
  128. data/set-of-emails/should-not-crash/p5-664-iomart-mail-filter.eml +258 -0
  129. data/set-of-emails/to-be-debugged-because/sisimai-cannot-parse-yet/.gitkeep +0 -0
  130. metadata +11 -2
data/lib/sisimai/fact.rb CHANGED
@@ -15,11 +15,13 @@ module Sisimai
15
15
  require 'sisimai/string'
16
16
  require 'sisimai/rhost'
17
17
  require 'sisimai/lda'
18
+ require 'sisimai/eb'
18
19
 
19
20
  @@rwaccessors = [
20
21
  :action, # [String] The value of Action: header
21
22
  :addresser, # [Sisimai::Address] From address
22
23
  :alias, # [String] Alias of the recipient address
24
+ :bogus, # [Integer] EXPERIMENTAL
23
25
  :catch, # [?] Results generated by hook method
24
26
  :command, # [String] The last SMTP command
25
27
  :decodedby, # [String] MTA module name since v5.2.0
@@ -43,7 +45,7 @@ module Sisimai
43
45
  :timestamp, # [Sisimai::Time] Date: header in the original message
44
46
  :timezoneoffset, # [Integer] Time zone offset(seconds)
45
47
  :token, # [String] Message token/MD5 Hex digest value
46
- :toxic, # [Boolean] EXPERIMENTAL
48
+ :toxic, # [Integer] EXPERIMENTAL
47
49
  ]
48
50
  attr_accessor(*@@rwaccessors)
49
51
 
@@ -71,6 +73,7 @@ module Sisimai
71
73
  @alias = argvs['alias'] || ''
72
74
  @addresser = argvs['addresser']
73
75
  @action = argvs['action']
76
+ @bogus = argvs['bogus']
74
77
  @catch = argvs['catch']
75
78
  @command = argvs['command']
76
79
  @decodedby = argvs['decodedby']
@@ -120,7 +123,7 @@ module Sisimai
120
123
  while e = deliveries.shift do
121
124
  # Create parameters for each Sisimai::Fact object
122
125
  next if e['recipient'].size < 5
123
- next if ! argvs[:vacation] && e['reason'] == 'vacation'
126
+ next if ! argvs[:vacation] && e['reason'] == Sisimai::Eb::ReAWAY
124
127
  next if ! argvs[:delivered] && e['status'].start_with?('2.')
125
128
 
126
129
  thing = {} # To be passed to each accessor of Sisimai::Fact
@@ -141,7 +144,6 @@ module Sisimai
141
144
  "replycode" => e["replycode"],
142
145
  "rhost" => e["rhost"],
143
146
  "decodedby" => e["agent"],
144
- "toxic" => e["toxic"],
145
147
  }
146
148
 
147
149
  # EMAILADDRESS: Detect an email address from message/rfc822 part
@@ -344,7 +346,7 @@ module Sisimai
344
346
  p1 = dc.index('<html>')
345
347
  p2 = dc.index('</html>')
346
348
  piece['diagnosticcode'][p1, p2 + 7 - p1] = '' if p1 && p2
347
- piece['diagnosticcode'] = Sisimai::String.sweep(piece['diagnosticcode'])
349
+ piece['diagnosticcode'] = piece['diagnosticcode'].split.join(" ")
348
350
  end
349
351
 
350
352
  if Sisimai::String.is_8bit(piece['diagnosticcode'])
@@ -352,13 +354,17 @@ module Sisimai
352
354
  piece['diagnosticcode'] = piece['diagnosticcode'].force_encoding('UTF-8').scrub('?')
353
355
  end
354
356
 
355
- piece["diagnostictype"] = "X-UNIX" if piece["reason"] == "mailererror"
357
+ piece["diagnostictype"] = "X-UNIX" if piece["reason"] == Sisimai::Eb::ReUNIX
356
358
  if piece["diagnostictype"].empty?
357
359
  piece["diagnostictype"] = "SMTP" if %w[feedback vacation].include?(piece["reason"]) == false
358
360
  end
359
361
 
360
- # Check the value of SMTP command
362
+ # When "RCPT first" in the error message, set "RCPT" as the last command.
363
+ # - <<< 503 RCPT first (#5.5.1)
364
+ # - <<< 503-5.5.1 RCPT first. A mail transaction protocol command was issued ...
365
+ # - RCPT first (in reply to DATA command)
361
366
  piece['command'] = '' if Sisimai::SMTP::Command.test(piece['command']) == false
367
+ piece['command'] = Sisimai::Eb::CeRCPT if piece['diagnosticcode'].include?('RCPT first')
362
368
 
363
369
  # Create parameters for the constructor
364
370
  as = Sisimai::Address.new(piece['addresser']) || next; next if as.void
@@ -379,13 +385,14 @@ module Sisimai
379
385
  ea.each { |q| thing[q] = piece[q] if thing[q].nil? || thing[q].empty? }
380
386
 
381
387
  # Other accessors
388
+ thing['bogus'] = -1
382
389
  thing['catch'] = piece['catch'] || nil
383
390
  thing["feedbackid"] = ""
384
391
  thing['hardbounce'] = piece['hardbounce']
385
- thing['toxic'] = piece['toxic']
386
392
  thing['replycode'] = Sisimai::SMTP::Reply.find(piece['diagnosticcode']) if thing['replycode'].empty?
387
393
  thing['timestamp'] = TimeModule.parse(::Time.at(piece['timestamp']).to_s)
388
394
  thing['timezoneoffset'] = piece['timezoneoffset'] || '+0000'
395
+ thing['toxic'] = -1
389
396
  ea.each { |q| thing[q] = piece[q] if thing[q].empty? }
390
397
 
391
398
  # ALIAS
@@ -416,25 +423,30 @@ module Sisimai
416
423
  while true
417
424
  if thing["reason"].empty? || RetryIndex[thing["reason"]]
418
425
  # The value of "reason" is empty or is needed to check with other values again
419
- re = thing["reason"].empty? ? "undefined" : thing["reason"]
426
+ re = thing["reason"].empty? ? Sisimai::Eb::Re___0 : thing["reason"]
420
427
  cr = Sisimai::LDA.find(thing); if Sisimai::Reason.is_explicit(cr) then thing["reason"] = cr; break; end
421
428
  cr = Sisimai::Rhost.find(thing); if Sisimai::Reason.is_explicit(cr) then thing["reason"] = cr; break; end
422
429
  cr = Sisimai::Reason.find(thing); if Sisimai::Reason.is_explicit(cr) then thing["reason"] = cr; break; end
423
- thing["reason"] = thing["diagnosticcode"].size > 0 ? "onhold" : re
430
+ thing["reason"] = thing["diagnosticcode"].size > 0 ? Sisimai::Eb::Re___1 : re
424
431
  break
425
432
  end
426
433
  break
427
434
  end
428
435
 
429
- # HARDBOUNCE: Set the value of "hardbounce", default value of "bouncebounce" is false
430
- if thing['reason'] == 'delivered' || thing['reason'] == 'feedback' || thing['reason'] == 'vacation'
431
- # Delete the value of ReplyCode when the Reason is "feedback" or "vacation"
432
- thing['replycode'] = '' if thing['reason'] != 'delivered'
433
- else
434
- # The reason is not "delivered", or "feedback", or "vacation"
436
+ while true
437
+ # HARDBOUNCE: Set the value of "hardbounce", default value of "bouncebounce" is false
438
+ break if thing['reason'] == Sisimai::Eb::ReSENT
439
+ if thing['reason'] == Sisimai::Eb::ReFEED || thing['reason'] == Sisimai::Eb::ReAWAY
440
+ # Delete the value of ReplyCode when the Reason is "Feedback" or "Vacation"
441
+ thing['replycode'] = ""
442
+ break
443
+ end
444
+
445
+ # The reason is not "Delivered", or "Feedback", or "Vacation"
435
446
  smtperrors = "#{piece['deliverystatus']} #{piece['diagnosticcode']}"
436
447
  smtperrors = '' if smtperrors.size < 4
437
448
  thing['hardbounce'] = Sisimai::SMTP::Failure.is_hardbounce(thing['reason'], smtperrors)
449
+ break
438
450
  end
439
451
 
440
452
  # DELIVERYSTATUS: Set a pseudo status code if the value of "deliverystatus" is empty
@@ -466,10 +478,10 @@ module Sisimai
466
478
  thing['action'] = ox[2]
467
479
  end
468
480
  end
469
- thing["action"] = "" if thing["action"].nil?
470
- thing["action"] = "delivered" if thing["action"].empty? && thing["reason"] == "delivered"
471
- thing["action"] = "delayed" if thing["action"].empty? && thing["reason"] == "expired"
472
- thing["action"] = "failed" if thing["action"].empty? && cx[0] == "4" || cx[0] == "5"
481
+ thing["action"] = "" if thing["action"].nil?
482
+ thing["action"] = Sisimai::Eb::AeSENT if thing["action"].empty? && thing["reason"] == Sisimai::Eb::ReSENT
483
+ thing["action"] = Sisimai::Eb::AeSTAY if thing["action"].empty? && thing["reason"] == Sisimai::Eb::ReTIME
484
+ thing["action"] = Sisimai::Eb::AeFAIL if thing["action"].empty? && cx[0] == "4" || cx[0] == "5"
473
485
 
474
486
  if thing["replycode"].size > 0
475
487
  # Fill empty values: ["SMTP Command", "DSN", "Reason"]
@@ -483,44 +495,13 @@ module Sisimai
483
495
 
484
496
  # Feedback-ID: 1.us-west-2.QHuyeCQrGtIIMGKQfVdUhP9hCQR2LglVOrRamBc+Prk=:AmazonSES
485
497
  thing["feedbackid"] = rfc822data["feedback-id"] || ""
486
- thing["toxic"] ||= is_toxic(thing)
498
+ thing["reason"] = thing["reason"].downcase
487
499
 
488
500
  listoffact << Sisimai::Fact.new(thing)
489
501
  end
490
502
  return listoffact
491
503
  end
492
504
 
493
- def self.is_toxic(thing = nil)
494
- return false unless thing
495
- cr = thing['reason'] || 'undefined'
496
- cv = thing['replycode'] || ''
497
- cw = thing['deliverystatus'] || ''
498
-
499
- # 1. Hard bounces or some soft bounces with a permanent error.
500
- # 1-1. Hard bounce: UserUnknown, HostUnknown, HasMoved, NotAccept
501
- # 1-2. Almost hard bounce: Suspend, Suppressed
502
- return false if cv.start_with?('4') || cw.start_with?('4')
503
- return true if %w[userunknown hostunknown hasmoved notaccept suspend suppressed].any? { |a| cr == a }
504
-
505
- if %w[mailboxfull filtered norelaying].any? { |a| cr == a }
506
- # 2. Several softbounces: MailboxFull, Filtered, NoRelaying
507
- # 2-1. The SMTP command is "RCPT" except "MailboxFull".
508
- # 2-2. The SMTP reply code begins with "5" such as "550".
509
- # 2-3. The SMTP status code is explicit code (not empty, not 5.9.***).
510
- # 2-4. The SMTP status code begins with "5." such as "5.1.1".
511
- return true if cr != 'mailboxfull' && thing['command'] == "RCPT"
512
- return true if cv.start_with?('5')
513
- return false if Sisimai::SMTP::Status.is_explicit(cw) == false
514
- return true if cw.start_with?('5.')
515
-
516
- elsif cr == 'feedback'
517
- # 3. Feedback Loop
518
- # 3-1. The Feedback Type is any of "abuse", "fraud", "opt-out"
519
- return true if %w[abuse fraud opt-out].any? { |a| thing['feedbacktype'] == a }
520
- end
521
- return false
522
- end
523
-
524
505
  # Create message token from addresser and recipient
525
506
  # @param [String] addr1 Sender address
526
507
  # @param [String] addr2 Recipient address
@@ -551,6 +532,7 @@ module Sisimai
551
532
  stringdata.each { |e| v[e] = self.send(e.to_sym) || '' }
552
533
  v['hardbounce'] = self.hardbounce
553
534
  v['toxic'] = self.toxic
535
+ v['bogus'] = self.bogus
554
536
  v['addresser'] = self.addresser.address
555
537
  v['recipient'] = self.recipient.address
556
538
  v['timestamp'] = self.timestamp.to_time.to_i
data/lib/sisimai/lda.rb CHANGED
@@ -2,6 +2,8 @@ module Sisimai
2
2
  # Sisimai::LDA - Error message decoder for LDA
3
3
  module LDA
4
4
  class << self
5
+ require 'sisimai/eb'
6
+
5
7
  LocalAgent = {
6
8
  # Each error message should be a lower-cased string
7
9
  # dovecot/src/deliver/deliver.c
@@ -19,20 +21,20 @@ module Sisimai
19
21
  # Each error message should be a lower-cased string
20
22
  "dovecot" => {
21
23
  # dovecot/src/deliver/mail-send.c:94
22
- "mailboxfull" => [
24
+ Sisimai::Eb::ReFULL => [
23
25
  "not enough disk space",
24
26
  "quota exceeded", # Dovecot 1.2 dovecot/src/plugins/quota/quota.c
25
27
  "quota exceeded (mailbox for user is full)", # dovecot/src/plugins/quota/quota.c
26
28
  ],
27
- "userunknown" => ["mailbox doesn't exist: "],
29
+ Sisimai::Eb::ReUSER => ["mailbox doesn't exist: "],
28
30
  },
29
31
  "mail.local" => {
30
- "mailboxfull" => [
32
+ Sisimai::Eb::ReFULL => [
31
33
  "disc quota exceeded",
32
34
  "mailbox full or quota exceeded",
33
35
  ],
34
- "systemerror" => ["temporary file write error"],
35
- "userunknown" => [
36
+ Sisimai::Eb::RePROC => ["temporary file write error"],
37
+ Sisimai::Eb::ReUSER => [
36
38
  ": invalid mailbox path",
37
39
  ": unknown user:",
38
40
  ": user missing home directory",
@@ -40,23 +42,23 @@ module Sisimai
40
42
  ],
41
43
  },
42
44
  "procmail" => {
43
- "mailboxfull" => ["quota exceeded while writing", "user over quota"],
44
- "systemerror" => ["service unavailable"],
45
- "systemfull" => ["no space left to finish writing"],
45
+ Sisimai::Eb::ReFULL => ["quota exceeded while writing", "user over quota"],
46
+ Sisimai::Eb::RePROC => ["service unavailable"],
47
+ Sisimai::Eb::ReDISK => ["no space left to finish writing"],
46
48
  },
47
49
  "maildrop" => {
48
- "userunknown" => ["cannot find system user", "invalid user specified."],
49
- "mailboxfull" => ["maildir over quota."],
50
+ Sisimai::Eb::ReUSER => ["cannot find system user", "invalid user specified."],
51
+ Sisimai::Eb::ReFULL => ["maildir over quota."],
50
52
  },
51
53
  "vpopmail" => {
52
- "filtered" => ["user does not exist, but will deliver to "],
53
- "mailboxfull" => ["domain is over quota", "user is over quota"],
54
- "suspend" => ["account is locked email bounced"],
55
- "userunknown" => ["sorry, no mailbox here by that name."],
54
+ Sisimai::Eb::ReFILT => ["user does not exist, but will deliver to "],
55
+ Sisimai::Eb::ReFULL => ["domain is over quota", "user is over quota"],
56
+ Sisimai::Eb::ReQUIT => ["account is locked email bounced"],
57
+ Sisimai::Eb::ReUSER => ["sorry, no mailbox here by that name."],
56
58
  },
57
59
  "vmailmgr" => {
58
- "mailboxfull" => ["delivery failed due to system quota violation"],
59
- "userunknown" => [
60
+ Sisimai::Eb::ReFULL => ["delivery failed due to system quota violation"],
61
+ Sisimai::Eb::ReUSER => [
60
62
  "invalid or unknown base user or domain",
61
63
  "invalid or unknown virtual user",
62
64
  "user name does not refer to a virtual user",
@@ -70,7 +72,7 @@ module Sisimai
70
72
  def find(argvs)
71
73
  return "" if argvs.nil?
72
74
  return "" if argvs["diagnosticcode"].empty?
73
- return "" if argvs["command"] != "" && argvs["command"] != "DATA"
75
+ return "" if argvs["command"] != "" && argvs["command"] != Sisimai::Eb::CeDATA
74
76
 
75
77
  deliversby = "" # [String] Local Delivery Agent name
76
78
  reasontext = "" # [String] Error reason
@@ -89,7 +91,7 @@ module Sisimai
89
91
  reasontext = e; break
90
92
  end
91
93
 
92
- reasontext = "mailererror" if reasontext.empty?
94
+ reasontext = Sisimai::Eb::ReUNIX if reasontext.empty?
93
95
  return reasontext
94
96
  end
95
97
  end
@@ -63,8 +63,6 @@ module Sisimai::Lhost
63
63
  end
64
64
  end
65
65
  return nil if recipients == 0
66
-
67
- dscontents.each { |e| e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) }
68
66
  return {"ds" => dscontents, "rfc822" => emailparts[1]}
69
67
  end
70
68
  def description; return 'TransWARE Active!hunter'; end
@@ -42,16 +42,17 @@ module Sisimai::Lhost
42
42
  # Transient/ContentRejected -- message you sent contains content that the provider doesn't allow
43
43
  # Transient/AttachmentRejected the message contained an unacceptable attachment
44
44
  class << self
45
+ require 'sisimai/eb'
45
46
  require 'sisimai/lhost'
46
47
 
47
48
  ReasonPair = {
48
- "Suppressed" => "suppressed",
49
- "OnAccountSuppressionList" => "suppressed",
50
- "General" => "onhold",
51
- "MailboxFull" => "mailboxfull",
52
- "MessageTooLarge" => "emailtoolarge",
53
- "ContentRejected" => "contenterror",
54
- "AttachmentRejected" => "securityerror",
49
+ "Suppressed" => Sisimai::Eb::ReSTOP,
50
+ "OnAccountSuppressionList" => Sisimai::Eb::ReSTOP,
51
+ "General" => Sisimai::Eb::Re___1,
52
+ "MailboxFull" => Sisimai::Eb::ReFULL,
53
+ "MessageTooLarge" => Sisimai::Eb::ReSIZE,
54
+ "ContentRejected" => Sisimai::Eb::ReBODY,
55
+ "AttachmentRejected" => Sisimai::Eb::ReSAFE,
55
56
  }.freeze
56
57
 
57
58
  # @abstract Decodes the bounce message from Amazon SES
@@ -124,7 +125,8 @@ module Sisimai::Lhost
124
125
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
125
126
  whatnotify = jsonobject["notificationType"][0, 1] || ""
126
127
 
127
- if whatnotify == "B"
128
+ case whatnotify
129
+ when "B"
128
130
  # "notificationType":"Bounce"
129
131
  p = jsonobject["bounce"]
130
132
  r = p["bounceType"] == "Permanent" ? "5" : "4"
@@ -137,7 +139,7 @@ module Sisimai::Lhost
137
139
  v = dscontents[-1]
138
140
  end
139
141
  v["recipient"] = e["emailAddress"]
140
- v["diagnosis"] = Sisimai::String.sweep(e["diagnosticCode"])
142
+ v["diagnosis"] = e["diagnosticCode"]
141
143
  v["command"] = Sisimai::SMTP::Command.find(v["diagnosis"])
142
144
  v["action"] = e["action"]
143
145
  v["status"] = Sisimai::SMTP::Status.find(v["diagnosis"], r)
@@ -152,8 +154,7 @@ module Sisimai::Lhost
152
154
  next if ReasonPair[f] != p["bounceSubType"]
153
155
  v["reason"] = f; break
154
156
  end
155
-
156
- elsif whatnotify == "C"
157
+ when "C"
157
158
  # "notificationType":"Complaint"
158
159
  p = jsonobject["complaint"]
159
160
  p["complainedRecipients"].each do |e|
@@ -164,14 +165,13 @@ module Sisimai::Lhost
164
165
  v = dscontents[-1]
165
166
  end
166
167
  v["recipient"] = e["emailAddress"]
167
- v["reason"] = "feedback"
168
+ v["reason"] = Sisimai::Eb::ReFEED
168
169
  v["feedbacktype"] = p["complaintFeedbackType"]
169
170
  v["date"] = p["timestamp"]
170
171
  v["diagnosis"] = sprintf('"feedbackid":"%s", "useragent":"%s"}', p["feedbackId"], p["userAgent"])
171
172
  recipients += 1
172
173
  end
173
-
174
- elsif whatnotify == "D"
174
+ when "D"
175
175
  # "notificationType":"Delivery"
176
176
  p = jsonobject["delivery"]
177
177
  p["recipients"].each do |e|
@@ -182,17 +182,16 @@ module Sisimai::Lhost
182
182
  v = dscontents[-1]
183
183
  end
184
184
  v["recipient"] = e
185
- v["reason"] = "delivered"
186
- v["action"] = "delivered"
185
+ v["reason"] = Sisimai::Eb::ReSENT
186
+ v["action"] = Sisimai::Eb::AeSENT
187
187
  v["date"] = p["timestamp"]
188
188
  v["lhost"] = Sisimai::RFC1123.find(p["reportingMTA"])
189
- v["diagnosis"] = Sisimai::String.sweep(p["smtpResponse"])
189
+ v["diagnosis"] = p["smtpResponse"]
190
190
  v["command"] = Sisimai::SMTP::Command.find(v["diagnosis"])
191
191
  v["status"] = Sisimai::SMTP::Status.find(v["diagnosis"], "2")
192
192
  v["replycode"] = Sisimai::SMTP::Reply.find(v["diagnosis"], "2")
193
193
  recipients += 1
194
194
  end
195
-
196
195
  else
197
196
  # Unknown "notificationType" value
198
197
  warn sprintf(" ***warning: There is no notificationType field or unknown type of notificationType field")
@@ -103,7 +103,6 @@ module Sisimai::Lhost
103
103
  emailparts[1] += sprintf("Subject: %s\n", alternates[3]) if alternates[3] != ""
104
104
  end
105
105
 
106
- dscontents.each { |e| e["diagnosis"] = Sisimai::String.sweep(e["diagnosis"]) }
107
106
  return { "ds" => dscontents, "rfc822" => emailparts[1] }
108
107
  end
109
108
  def description; return 'Java Apache Mail Enterprise Server'; end
@@ -11,10 +11,6 @@ module Sisimai::Lhost
11
11
  message: [' ----- The following addresses had delivery problems -----'],
12
12
  error: [' ----- Non-delivered information -----'],
13
13
  }.freeze
14
- MessagesOf = {
15
- 'filtered' => ['Mail Delivery Failed... User unknown'],
16
- 'mailboxfull' => ["The number of messages in recipient's mailbox exceeded the local limit."],
17
- }.freeze
18
14
 
19
15
  # @asbtract Decodes the bounce message from Biglobe
20
16
  # @param [Hash] mhead Message headers of a bounce email
@@ -76,18 +72,6 @@ module Sisimai::Lhost
76
72
  end
77
73
  end
78
74
  return nil if recipients == 0
79
-
80
- dscontents.each do |e|
81
- e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
82
-
83
- MessagesOf.each_key do |r|
84
- # Verify each regular expression of session errors
85
- next if MessagesOf[r].none? { |a| e['diagnosis'].include?(a) }
86
- e['reason'] = r
87
- break
88
- end
89
- end
90
-
91
75
  return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
92
76
  end
93
77
  def description; return 'BIGLOBE: https://www.biglobe.ne.jp'; end
@@ -3,6 +3,7 @@ module Sisimai::Lhost
3
3
  # Methods in the module are called from only Sisimai::Message.
4
4
  module Courier
5
5
  class << self
6
+ require 'sisimai/eb'
6
7
  require 'sisimai/lhost'
7
8
 
8
9
  # https://www.courier-mta.org/courierdsn.html
@@ -14,12 +15,12 @@ module Sisimai::Lhost
14
15
  }.freeze
15
16
  MessagesOf = {
16
17
  # courier/module.esmtp/esmtpclient.c:526| hard_error(del, ctf, "No such domain.");
17
- 'hostunknown' => ['No such domain.'],
18
+ Sisimai::Eb::ReHOST => ['No such domain.'],
18
19
  # courier/module.esmtp/esmtpclient.c:531| hard_error(del, ctf,
19
20
  # courier/module.esmtp/esmtpclient.c:532| "This domain's DNS violates RFC 1035.");
20
- 'systemerror' => ["This domain's DNS violates RFC 1035."],
21
+ Sisimai::Eb::RePROC => ["This domain's DNS violates RFC 1035."],
21
22
  # courier/module.esmtp/esmtpclient.c:535| soft_error(del, ctf, "DNS lookup failed.");
22
- 'networkerror' => ['DNS lookup failed.'],
23
+ Sisimai::Eb::ReINET => ['DNS lookup failed.'],
23
24
  }.freeze
24
25
 
25
26
  # @abstract Decodes the bounce message from Courier MTA
@@ -72,7 +73,8 @@ module Sisimai::Lhost
72
73
  next unless o = Sisimai::RFC1894.field(e)
73
74
  v = dscontents[-1]
74
75
 
75
- if o[3] == 'addr'
76
+ case o[3]
77
+ when "addr"
76
78
  # Final-Recipient: rfc822; kijitora@example.jp
77
79
  # X-Actual-Recipient: rfc822; kijitora@example.co.jp
78
80
  if o[0] == 'final-recipient'
@@ -88,7 +90,7 @@ module Sisimai::Lhost
88
90
  # X-Actual-Recipient: rfc822; kijitora@example.co.jp
89
91
  v['alias'] = o[2]
90
92
  end
91
- elsif o[3] == 'code'
93
+ when "code"
92
94
  # Diagnostic-Code: SMTP; 550 5.1.1 <userunknown@example.jp>... User Unknown
93
95
  v['spec'] = o[1]
94
96
  v['diagnosis'] = o[2]
@@ -116,7 +118,7 @@ module Sisimai::Lhost
116
118
  # Continued line of the value of Diagnostic-Code field
117
119
  next if readslices[-2].start_with?('Diagnostic-Code:') == false
118
120
  next if e.start_with?(' ') == false
119
- v['diagnosis'] += " #{Sisimai::String.sweep(e)}"
121
+ v['diagnosis'] += " " + e
120
122
  readslices[-1] = "Diagnostic-Code: #{e}"
121
123
  end
122
124
  end
@@ -127,7 +129,7 @@ module Sisimai::Lhost
127
129
  # Set default values if each value is empty.
128
130
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
129
131
  e['command'] = thecommand if e["command"].empty?
130
- e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) || ''
132
+ e['diagnosis'] = e['diagnosis'] || ''
131
133
 
132
134
  MessagesOf.each_key do |r|
133
135
  # Verify each regular expression of session errors
@@ -0,0 +1,120 @@
1
+ module Sisimai::Lhost
2
+ # Sisimai::Lhost::DeutscheTelekom decodes a bounce email which created by Deutsche Telekom or
3
+ # T-Online. Methods in the module are called from only Sisimai::Message.
4
+ module DeutscheTelekom
5
+ class << self
6
+ require 'sisimai/lhost'
7
+
8
+ Indicators = Sisimai::Lhost.INDICATORS
9
+ BannerDTAG = Sisimai::Lhost.BannerDTAG
10
+ StartingOf = { message: [BannerDTAG[1]] }.freeze
11
+
12
+ # @abstract Decodes the bounce message from DeutscheTelekom
13
+ # @param [Hash] mhead Message headers of a bounce email
14
+ # @param [String] mbody Message body of a bounce email
15
+ # @return [Hash] Bounce data list and message/rfc822 part
16
+ # @return [Nil] it failed to decode or the arguments are missing
17
+ def inquire(mhead, mbody)
18
+ # - T-Online: https://www.t-online.de/, @t-online.de, @magenta.de
19
+ # - DeutscheTelekom: https://www.telekom.com/
20
+ # - Based on the bounce format of Smail 3, the original design model for Exim
21
+ # - Tailored for Deutsche Telekom's internal Smail 3 fork with custom banners
22
+ # - Module name follows the infrastructure owner for cross-language compatibility
23
+ # - Smail 3: http://www.weird.com/~woods/projects/smail.html
24
+ return nil unless BannerDTAG.any? { |a| mbody.include?(a) }
25
+
26
+ # smail-3.2.0.108/src/
27
+ # notify.c:1052|(void) fprintf(f, "Subject: mail failed, %s\nReference: <%s@%s>\n\n",
28
+ # notify.c:1053| subject_to, message_id, primary_name);
29
+ #
30
+ # T-Online specific headers
31
+ # Received: from mailin42.aul.t-online.de (mailin42.aul.t-online.de [192.51.100.1])
32
+ # by mailout11.t-online.de (Postfix) with SMTP id 05E5A1CAC0
33
+ # From: Mail Delivery System <Mailer-Daemon@t-online.de>
34
+ # X-TOI-MSGID: c9412855-531f-497b-b007-5ffc033877a0
35
+ dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = nil
36
+ emailparts = Sisimai::RFC5322.part(mbody, [BannerDTAG[3], BannerDTAG[2]])
37
+ bodyslices = emailparts[0].split("\n")
38
+ messagelog = ''
39
+ readcursor = 0 # (Integer) Points the current cursor position
40
+ recipients = 0 # (Integer) The number of 'Final-Recipient' header
41
+
42
+ while e = bodyslices.shift do
43
+ # Read error messages and delivery status lines from the head of the email to the previous
44
+ # line of the beginning of the original message.
45
+ if readcursor == 0
46
+ # Beginning of the bounce message or delivery status part
47
+ if e.start_with?(StartingOf[:message][0])
48
+ # |------------------------- Failed addresses follow: ---------------------|
49
+ readcursor |= Indicators[:deliverystatus]
50
+ else
51
+ # |------------------------- Message log follows: -------------------------|
52
+ # The line above may appears only in Smail 3.
53
+ #
54
+ # smail-3.2.0.108/src/
55
+ # models.c:787| if (deliver == NULL && defer == NULL) {
56
+ # models.c:788| write_log(WRITE_LOG_MLOG, "no valid recipients were found for this message");
57
+ # models.c:789| return_to_sender = TRUE;
58
+ # models.c:879| }
59
+ messagelog += ' ' + e if e != "" && e.include?(BannerDTAG[0]) == false
60
+ end
61
+ next
62
+ end
63
+ next if (readcursor & Indicators[:deliverystatus]) == 0 || e.empty?
64
+
65
+ # |------------------------- Failed addresses follow: ---------------------|
66
+ # <example@t-online.de>
67
+ # 552 5.2.2 <example@t-online.de> Quota exceeded (mailbox for user is full)
68
+ #
69
+ # |------------------------- Message header follows: ----------------------|
70
+ # Received: from mail.fragdenstaat.de ([94.130.55.89]) by mailin41.mgt.mul.t-online.de.example.com
71
+ # with (TLSv1.3:TLS_AES_256_GCM_SHA384 encrypted)
72
+ # ...
73
+ v = dscontents[-1]
74
+
75
+
76
+ if e.start_with?(' <') && e.end_with?('>') && e.count(' ') == 1
77
+ # Deutsche Telekom: The recipient address is enclosed in angle brackets.
78
+ # |------------------------- Failed addresses follow: ---------------------|
79
+ if v["recipient"] != ""
80
+ # There are multiple recipient addresses in the message body.
81
+ dscontents << Sisimai::Lhost.DELIVERYSTATUS
82
+ v = dscontents[-1]
83
+ end
84
+ v['recipient'] = e[2, e.size]
85
+ recipients += 1
86
+
87
+ elsif Sisimai::String.aligned(e, [' ', '@', '.', ' ... '])
88
+ # Smail 3:
89
+ # - The recipient address is not enclosed in angle brackets.
90
+ # - Error message begins with " ... failed:"
91
+ # smail-3.2.0.108/src/
92
+ # notify.c:845| if (cur->error) {
93
+ # notify.c:846| (void) fprintf(f, " %s ... failed: %s\n",
94
+ # notify.c:847| cur->in_addr ? cur->in_addr : "(unknown)",
95
+ # notify.c:848| cur->error->message);
96
+ # notify.c:849| }
97
+ # |------------------------- Failed addresses follow: ---------------------|
98
+ # kijitora@neko.nyaan.example.com ... unknown host
99
+ if v["recipient"] != ""
100
+ # There are multiple recipient addresses in the message body.
101
+ dscontents << Sisimai::Lhost.DELIVERYSTATUS
102
+ v = dscontents[-1]
103
+ end
104
+ v['recipient'] = e[1, e.index(' ... ')]
105
+ v['diagnosis'] = messagelog << ' ' + e
106
+ recipients += 1
107
+
108
+ else
109
+ # 552 5.2.2 <example@t-online.de> Quota exceeded (mailbox for user is full)
110
+ v['diagnosis'] = e
111
+ end
112
+ end
113
+ return nil if recipients == 0
114
+ return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
115
+ end
116
+ def description; return 'DeutscheTelekom'; end
117
+ end
118
+ end
119
+ end
120
+
@@ -3,15 +3,14 @@ module Sisimai::Lhost
3
3
  # Methods in the module are called from only Sisimai::Message.
4
4
  module Domino
5
5
  class << self
6
+ require 'sisimai/eb'
6
7
  require 'sisimai/lhost'
7
8
 
8
9
  Indicators = Sisimai::Lhost.INDICATORS
9
10
  Boundaries = ['Content-Type: message/rfc822'].freeze
10
11
  StartingOf = {message: ['Your message']}.freeze
11
12
  MessagesOf = {
12
- "filtered" => ["Cannot route mail to user"],
13
- "systemerror" => ["Several matches found in Domino Directory"],
14
- "userunknown" => [
13
+ Sisimai::Eb::ReUSER => [
15
14
  "not listed in Domino Directory",
16
15
  "not listed in public Name & Address Book",
17
16
  "non répertorié dans l'annuaire Domino",
@@ -120,7 +119,6 @@ module Sisimai::Lhost
120
119
  return nil if recipients == 0
121
120
 
122
121
  dscontents.each do |e|
123
- e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
124
122
  e['recipient'] = Sisimai::Address.s3s4(e['recipient'])
125
123
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
126
124